From 117239a709410a307d63eb0a7916a24722976b98 Mon Sep 17 00:00:00 2001 From: Daniele Bonaldo Date: Tue, 24 Apr 2018 17:14:31 +0100 Subject: [PATCH 0001/1335] Apply ColorSpan if subtitle cue startTag contains color class --- .../exoplayer2/text/webvtt/WebvttCueParser.java | 13 +++++++++++++ .../google/android/exoplayer2/util/ColorParser.java | 4 ++++ 2 files changed, 17 insertions(+) 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 80ebecdc0e..4ef78ffb87 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 @@ -33,6 +33,7 @@ import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; import android.util.Log; import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.util.ColorParser; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; import java.util.Arrays; @@ -380,6 +381,8 @@ public final class WebvttCueParser { text.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case TAG_CLASS: + applySupportedClasses(text, startTag.classes, start, end); + break; case TAG_LANG: case TAG_VOICE: case "": // Case of the "whole cue" virtual tag. @@ -395,6 +398,16 @@ public final class WebvttCueParser { } } + private static void applySupportedClasses(SpannableStringBuilder text, String[] classes, + int start, int end) { + for (String className : classes) { + if (ColorParser.isNamedColor(className)) { + int color = ColorParser.parseCssColor(className); + text.setSpan(new ForegroundColorSpan(color), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + } + private static void applyStyleToText(SpannableStringBuilder spannedText, WebvttCssStyle style, int start, int end) { if (style == null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ColorParser.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ColorParser.java index a9df80e9fe..87202663b8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ColorParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ColorParser.java @@ -43,6 +43,10 @@ public final class ColorParser { private static final Map COLOR_MAP; + public static boolean isNamedColor(String expression) { + return COLOR_MAP.containsKey(expression); + } + /** * Parses a TTML color expression. * From dcb8417a3c6a18e636be07c44d21eddf98208639 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 17 Apr 2019 09:30:25 +0100 Subject: [PATCH 0002/1335] 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 0003/1335] 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 0004/1335] 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 0005/1335] 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 0006/1335] 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 0007/1335] 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 0008/1335] 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 0009/1335] 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 0010/1335] 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 0011/1335] [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 0012/1335] 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 0013/1335] 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 0014/1335] 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 0015/1335] 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 0016/1335] 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 0017/1335] 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 0018/1335] 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 0019/1335] 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 0020/1335] 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 0021/1335] 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 0022/1335] 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 0023/1335] 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 0024/1335] 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 0025/1335] 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 0026/1335] 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 0027/1335] 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 0028/1335] 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 0029/1335] 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 0030/1335] 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 0031/1335] 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 0032/1335] 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 0033/1335] 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 0034/1335] 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 0035/1335] 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 0036/1335] 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 0037/1335] 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 0038/1335] 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 0039/1335] 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 0040/1335] 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 0041/1335] 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 0042/1335] 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 0043/1335] 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 0044/1335] 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 0045/1335] 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 0046/1335] 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 0047/1335] 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 0048/1335] 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 0049/1335] 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 0050/1335] 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 0051/1335] 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 0052/1335] 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 0053/1335] 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 0054/1335] 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 0055/1335] 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 0056/1335] 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 0057/1335] 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 0058/1335] 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 0059/1335] 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 0060/1335] 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 0061/1335] 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 0062/1335] 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 0063/1335] 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 0064/1335] 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 0065/1335] 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 0066/1335] 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 0067/1335] 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 0068/1335] 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 0069/1335] 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 0070/1335] 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 0071/1335] 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 0072/1335] 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 0073/1335] 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 0074/1335] 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 0075/1335] 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 0076/1335] 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 0077/1335] 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 0078/1335] 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 0079/1335] 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 0080/1335] 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 0081/1335] 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 0082/1335] 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 0083/1335] 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 0084/1335] 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 0085/1335] 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 0086/1335] 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 0087/1335] 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 0088/1335] 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 0089/1335] 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 0090/1335] 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 0091/1335] 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 0092/1335] 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 0093/1335] 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 0094/1335] 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 0095/1335] 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 0096/1335] 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 0097/1335] 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 0098/1335] 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 0099/1335] 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 0100/1335] 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 0101/1335] 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 0102/1335] 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 0103/1335] 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 0104/1335] 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 0105/1335] 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 0106/1335] 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 0107/1335] 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 0108/1335] 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 0109/1335] 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 0110/1335] 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 0111/1335] 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 0112/1335] 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 0113/1335] 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 0114/1335] 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 0115/1335] 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 0116/1335] 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 0117/1335] 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 0118/1335] 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 0119/1335] 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 0120/1335] 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 0165/1335] 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 0166/1335] 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 0167/1335] 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 0168/1335] 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 0169/1335] 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 0170/1335] 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 0171/1335] 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 0172/1335] 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 0173/1335] 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 0174/1335] 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 0175/1335] 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 0176/1335] 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 0177/1335] 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 0178/1335] 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 0179/1335] 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 0180/1335] 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 0181/1335] 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 0182/1335] 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 0183/1335] 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 0184/1335] 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 0185/1335] 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 0186/1335] 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 0187/1335] 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 0188/1335] 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 0189/1335] 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 0190/1335] 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 0191/1335] 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 0192/1335] 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 0193/1335] 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 0194/1335] 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 0195/1335] 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 0196/1335] 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 0197/1335] 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 0198/1335] 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 0199/1335] 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 0200/1335] 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 0201/1335] 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 0202/1335] 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 0203/1335] 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 0204/1335] 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 0205/1335] 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 0206/1335] 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 0207/1335] 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 0208/1335] 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 0209/1335] 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 0210/1335] 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 0211/1335] 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 0212/1335] 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 0213/1335] 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 0214/1335] 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 0215/1335] 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 0216/1335] 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 0217/1335] 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 0218/1335] 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 0219/1335] 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 0220/1335] 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 0221/1335] 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 0222/1335] 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 0223/1335] 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 0224/1335] 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 0225/1335] 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 0226/1335] 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 0227/1335] 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 0228/1335] 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 0229/1335] 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 0230/1335] 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 0231/1335] 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 0232/1335] 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 0233/1335] 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 0234/1335] 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 0235/1335] 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 0236/1335] 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 0237/1335] 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 0238/1335] 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 0239/1335] 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 0240/1335] 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 0241/1335] 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 0242/1335] 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 0243/1335] 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 0244/1335] 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 0245/1335] 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 0246/1335] 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 0247/1335] 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 0248/1335] 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 0249/1335] 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 0250/1335] 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 0251/1335] 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 0252/1335] 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 0253/1335] 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 0254/1335] 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 0255/1335] 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 0256/1335] 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 0257/1335] 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 0258/1335] 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 0259/1335] 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 0260/1335] 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 0261/1335] 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 0262/1335] 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 0263/1335] 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 0264/1335] 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 0265/1335] 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 0266/1335] 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 0267/1335] 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 0432/1335] 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 0433/1335] 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 0434/1335] 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 0435/1335] 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 0436/1335] 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 0437/1335] 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 0438/1335] 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 0439/1335] 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 0440/1335] 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 0441/1335] 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 0442/1335] 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 0443/1335] 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 0444/1335] 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 0445/1335] 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 0446/1335] 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 0447/1335] 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 0448/1335] 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 0449/1335] 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 0450/1335] 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 0451/1335] 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 0452/1335] 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 0453/1335] 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 0454/1335] 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 0455/1335] 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 0456/1335] 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 0457/1335] 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 0458/1335] 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 d1d43d62e665cf905c3816275d49126eb1dfa4f9 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 6 Dec 2019 22:26:59 +0000 Subject: [PATCH 0459/1335] Add demo app support for attaching DrmSessions to clear content Issue:#4867 PiperOrigin-RevId: 284262626 --- RELEASENOTES.md | 1 + demos/main/src/main/assets/media.exolist.json | 7 +++ .../exoplayer2/demo/PlayerActivity.java | 2 + .../android/exoplayer2/demo/Sample.java | 43 ++++++++++++++++++- .../demo/SampleChooserActivity.java | 11 +++++ 5 files changed, 63 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 07bb6914ab..13676f2735 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -29,6 +29,7 @@ * Add `play` and `pause` methods to `Player`. * Upgrade Truth dependency from 0.44 to 1.0. * Upgrade to JUnit 4.13-rc-2. +* Add support for attaching DRM sessions to clear content in the demo app. ### 2.11.0 (not yet released) ### diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 4375bdf3a7..06f063b1c1 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -208,6 +208,13 @@ "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure and Clear SD & HD (cenc,MP4,H264)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/widevine/tears_enc_clear_enc.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test", + "drm_session_for_clear_types": ["audio", "video"] } ] }, 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 4636c252f5..82fd987089 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 @@ -115,6 +115,7 @@ public class PlayerActivity extends AppCompatActivity public static final String DRM_SCHEME_EXTRA = "drm_scheme"; public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url"; public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties"; + public static final String DRM_SESSION_FOR_CLEAR_TYPES_EXTRA = "drm_session_for_clear_types"; public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session"; public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders"; public static final String TUNNELING_EXTRA = "tunneling"; @@ -488,6 +489,7 @@ public class PlayerActivity extends AppCompatActivity new DefaultDrmSessionManager.Builder() .setUuidAndExoMediaDrmProvider(drmInfo.drmScheme, FrameworkMediaDrm.DEFAULT_PROVIDER) .setMultiSession(drmInfo.drmMultiSession) + .setUseDrmSessionsForClearContent(drmInfo.drmSessionForClearTypes) .build(mediaDrmCallback); } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java index 85530b993b..0bf0d2a80c 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java @@ -22,6 +22,7 @@ import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_LICENSE_URL_ import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_MULTI_SESSION_EXTRA; import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_EXTRA; import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_UUID_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SESSION_FOR_CLEAR_TYPES_EXTRA; import static com.google.android.exoplayer2.demo.PlayerActivity.EXTENSION_EXTRA; import static com.google.android.exoplayer2.demo.PlayerActivity.IS_LIVE_EXTRA; import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_LANGUAGE_EXTRA; @@ -32,9 +33,11 @@ import static com.google.android.exoplayer2.demo.PlayerActivity.URI_EXTRA; import android.content.Intent; import android.net.Uri; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; +import java.util.HashSet; import java.util.UUID; /* package */ abstract class Sample { @@ -147,24 +150,35 @@ import java.util.UUID; String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix); String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix); + String[] drmSessionForClearTypesExtra = + intent.getStringArrayExtra(DRM_SESSION_FOR_CLEAR_TYPES_EXTRA + extrasKeySuffix); + int[] drmSessionForClearTypes = toTrackTypeArray(drmSessionForClearTypesExtra); boolean drmMultiSession = intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false); - return new DrmInfo(drmScheme, drmLicenseUrl, keyRequestPropertiesArray, drmMultiSession); + return new DrmInfo( + drmScheme, + drmLicenseUrl, + keyRequestPropertiesArray, + drmSessionForClearTypes, + drmMultiSession); } public final UUID drmScheme; public final String drmLicenseUrl; public final String[] drmKeyRequestProperties; + public final int[] drmSessionForClearTypes; public final boolean drmMultiSession; public DrmInfo( UUID drmScheme, String drmLicenseUrl, String[] drmKeyRequestProperties, + int[] drmSessionForClearTypes, boolean drmMultiSession) { this.drmScheme = drmScheme; this.drmLicenseUrl = drmLicenseUrl; this.drmKeyRequestProperties = drmKeyRequestProperties; + this.drmSessionForClearTypes = drmSessionForClearTypes; this.drmMultiSession = drmMultiSession; } @@ -173,6 +187,13 @@ import java.util.UUID; intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmScheme.toString()); intent.putExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix, drmLicenseUrl); intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties); + ArrayList typeStrings = new ArrayList<>(); + for (int type : drmSessionForClearTypes) { + // Only audio and video are supported. + typeStrings.add(type == C.TRACK_TYPE_AUDIO ? "audio" : "video"); + } + intent.putExtra( + DRM_SESSION_FOR_CLEAR_TYPES_EXTRA + extrasKeySuffix, typeStrings.toArray(new String[0])); intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmMultiSession); } } @@ -207,6 +228,26 @@ import java.util.UUID; } } + public static int[] toTrackTypeArray(@Nullable String[] trackTypeStringsArray) { + if (trackTypeStringsArray == null) { + return new int[0]; + } + HashSet trackTypes = new HashSet<>(); + for (String trackTypeString : trackTypeStringsArray) { + switch (Util.toLowerInvariant(trackTypeString)) { + case "audio": + trackTypes.add(C.TRACK_TYPE_AUDIO); + break; + case "video": + trackTypes.add(C.TRACK_TYPE_VIDEO); + break; + default: + throw new IllegalArgumentException("Invalid track type: " + trackTypeString); + } + } + return Util.toArray(new ArrayList<>(trackTypes)); + } + public static Sample createFromIntent(Intent intent) { if (ACTION_VIEW_LIST.equals(intent.getAction())) { ArrayList intentUris = new ArrayList<>(); diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index cdce29aa5e..66bf4bad5a 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -307,6 +307,7 @@ public class SampleChooserActivity extends AppCompatActivity String drmScheme = null; String drmLicenseUrl = null; String[] drmKeyRequestProperties = null; + String[] drmSessionForClearTypes = null; boolean drmMultiSession = false; ArrayList playlistSamples = null; String adTagUri = null; @@ -348,6 +349,15 @@ public class SampleChooserActivity extends AppCompatActivity reader.endObject(); drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]); break; + case "drm_session_for_clear_types": + ArrayList drmSessionForClearTypesList = new ArrayList<>(); + reader.beginArray(); + while (reader.hasNext()) { + drmSessionForClearTypesList.add(reader.nextString()); + } + reader.endArray(); + drmSessionForClearTypes = drmSessionForClearTypesList.toArray(new String[0]); + break; case "drm_multi_session": drmMultiSession = reader.nextBoolean(); break; @@ -389,6 +399,7 @@ public class SampleChooserActivity extends AppCompatActivity Util.getDrmUuid(drmScheme), drmLicenseUrl, drmKeyRequestProperties, + Sample.toTrackTypeArray(drmSessionForClearTypes), drmMultiSession); Sample.SubtitleInfo subtitleInfo = subtitleUri == null From 96ba4ecd7951ff70c0c4539982171807c9dd8adb Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 9 Dec 2019 08:43:38 +0000 Subject: [PATCH 0460/1335] Finalize 2.11.0 release notes PiperOrigin-RevId: 284500197 --- RELEASENOTES.md | 41 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 13676f2735..79990a716d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -7,11 +7,6 @@ This extractor does not support seeking and live streams. If `DefaultExtractorsFactory` is used, this extractor is only used if the FLAC extension is not loaded. -* 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)). * Make `MediaSourceEventListener.LoadEventInfo` and `MediaSourceEventListener.MediaLoadData` top-level classes. * Rename `MediaCodecRenderer.onOutputFormatChanged` to @@ -19,19 +14,14 @@ clarifying the distinction between `Format` and `MediaFormat`. * Downloads: Merge downloads in `SegmentDownloader` to improve overall download speed ([#5978](https://github.com/google/ExoPlayer/issues/5978)). -* Allow `AdtsExtractor` to encounter EoF when calculating average frame size - ([#6700](https://github.com/google/ExoPlayer/issues/6700)). * In MP4 streams, store the Android capture frame rate only in `Format.metadata`. `Format.frameRate` now stores the calculated frame rate. -* Make media session connector dispatch ACTION_SET_CAPTIONING_ENABLED. -* Add support for position and overlapping start/end times in SSA/ASS subtitles - ([#6320](https://github.com/google/ExoPlayer/issues/6320)). * Add `play` and `pause` methods to `Player`. * Upgrade Truth dependency from 0.44 to 1.0. * Upgrade to JUnit 4.13-rc-2. * Add support for attaching DRM sessions to clear content in the demo app. -### 2.11.0 (not yet released) ### +### 2.11.0 (2019-12-11) ### * Core library: * Replace `ExoPlayerFactory` by `SimpleExoPlayer.Builder` and @@ -113,6 +103,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 + ([#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). * UI: * Make showing and hiding player controls accessible to TalkBack in `PlayerView`. @@ -124,7 +122,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. @@ -153,28 +151,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: 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 8b3c3ffb91a0449644b9415814ca1f6271255707 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 9 Dec 2019 09:56:50 +0000 Subject: [PATCH 0461/1335] 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 74faa3aa9f045c2478ef62f7b7741552ac365a4f Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 9 Dec 2019 10:33:23 +0000 Subject: [PATCH 0462/1335] rename setMediaItem to setMediaSource PiperOrigin-RevId: 284514142 --- .../exoplayer2/demo/PlayerActivity.java | 2 +- .../google/android/exoplayer2/ExoPlayer.java | 15 ++++++------ .../android/exoplayer2/ExoPlayerImpl.java | 12 +++++----- .../android/exoplayer2/SimpleExoPlayer.java | 24 +++++++++---------- .../android/exoplayer2/ExoPlayerTest.java | 2 +- .../exoplayer2/testutil/ExoHostedTest.java | 2 +- .../testutil/ExoPlayerTestRunner.java | 2 +- .../exoplayer2/testutil/StubExoPlayer.java | 4 ++-- 8 files changed, 32 insertions(+), 31 deletions(-) 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 82fd987089..b291d5afe8 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 @@ -395,7 +395,7 @@ public class PlayerActivity extends AppCompatActivity if (haveStartPosition) { player.seekTo(startWindow, startPosition); } - player.setMediaItem(mediaSource); + player.setMediaSource(mediaSource); player.prepare(); updateButtonVisibility(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 4d947e27cf..f02ec3dd43 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -358,12 +358,13 @@ public interface ExoPlayer extends Player { void prepare(); /** - * @deprecated Use {@code setMediaItem(mediaSource, C.TIME_UNSET)} and {@link #prepare()} instead. + * @deprecated Use {@code setMediaSource(mediaSource, C.TIME_UNSET)} and {@link #prepare()} + * instead. */ @Deprecated void prepare(MediaSource mediaSource); - /** @deprecated Use {@link #setMediaItem(MediaSource, long)} and {@link #prepare()} instead. */ + /** @deprecated Use {@link #setMediaSource(MediaSource, long)} and {@link #prepare()} instead. */ @Deprecated void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); @@ -373,9 +374,9 @@ public interface ExoPlayer extends Player { *

    Note: This is an intermediate implementation towards a larger change. Until then {@link * #prepare()} has to be called immediately after calling this method. * - * @param mediaItem The new {@link MediaSource}. + * @param mediaSource The new {@link MediaSource}. */ - void setMediaItem(MediaSource mediaItem); + void setMediaSource(MediaSource mediaSource); /** * Sets the specified {@link MediaSource}. @@ -391,13 +392,13 @@ public interface ExoPlayer extends Player { * player.stop(true); * } * player.seekTo(0, startPositionMs); - * player.setMediaItem(mediaItem); + * player.setMediaSource(mediaSource); * * - * @param mediaItem The new {@link MediaSource}. + * @param mediaSource The new {@link MediaSource}. * @param startPositionMs The position in milliseconds to start playback from. */ - void setMediaItem(MediaSource mediaItem, long startPositionMs); + void setMediaSource(MediaSource mediaSource, long startPositionMs); /** * Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message 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 e79a61da9a..98eaaa0c2c 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 @@ -236,14 +236,14 @@ import java.util.concurrent.TimeoutException; @Override @Deprecated public void prepare(MediaSource mediaSource) { - setMediaItem(mediaSource); + setMediaSource(mediaSource); prepareInternal(/* resetPosition= */ true, /* resetState= */ true); } @Override @Deprecated public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - setMediaItem(mediaSource); + setMediaSource(mediaSource); prepareInternal(resetPosition, resetState); } @@ -254,17 +254,17 @@ import java.util.concurrent.TimeoutException; } @Override - public void setMediaItem(MediaSource mediaItem, long startPositionMs) { + public void setMediaSource(MediaSource mediaSource, long startPositionMs) { if (!getCurrentTimeline().isEmpty()) { stop(/* reset= */ true); } seekTo(/* windowIndex= */ 0, startPositionMs); - setMediaItem(mediaItem); + setMediaSource(mediaSource); } @Override - public void setMediaItem(MediaSource mediaItem) { - mediaSource = mediaItem; + public void setMediaSource(MediaSource mediaSource) { + this.mediaSource = mediaSource; } @Override 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 3ba9fd4564..3e73a0eb04 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 @@ -1185,7 +1185,7 @@ public class SimpleExoPlayer extends BasePlayer @Deprecated public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { verifyApplicationThread(); - setMediaItem(mediaSource); + setMediaSource(mediaSource); prepareInternal(resetPosition, resetState); } @@ -1196,17 +1196,17 @@ public class SimpleExoPlayer extends BasePlayer } @Override - public void setMediaItem(MediaSource mediaItem, long startPositionMs) { + public void setMediaSource(MediaSource mediaSource, long startPositionMs) { verifyApplicationThread(); - setMediaItemInternal(mediaItem); - player.setMediaItem(mediaItem, startPositionMs); + setMediaSourceInternal(mediaSource); + player.setMediaSource(mediaSource, startPositionMs); } @Override - public void setMediaItem(MediaSource mediaItem) { + public void setMediaSource(MediaSource mediaSource) { verifyApplicationThread(); - setMediaItemInternal(mediaItem); - player.setMediaItem(mediaItem); + setMediaSourceInternal(mediaSource); + player.setMediaSource(mediaSource); } @Override @@ -1463,13 +1463,13 @@ public class SimpleExoPlayer extends BasePlayer player.prepareInternal(resetPosition, resetState); } - private void setMediaItemInternal(MediaSource mediaItem) { - if (mediaSource != null) { - mediaSource.removeEventListener(analyticsCollector); + private void setMediaSourceInternal(MediaSource mediaSource) { + if (this.mediaSource != null) { + this.mediaSource.removeEventListener(analyticsCollector); analyticsCollector.resetForNewMediaSource(); } - mediaSource = mediaItem; - mediaSource.addEventListener(eventHandler, analyticsCollector); + this.mediaSource = mediaSource; + this.mediaSource.addEventListener(eventHandler, analyticsCollector); } private void removeSurfaceCallbacks() { 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 c4f7ed32ad..f48c33ebda 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 @@ -3062,7 +3062,7 @@ public final class ExoPlayerTest { new PlayerRunnable() { @Override public void run(SimpleExoPlayer player) { - player.setMediaItem(mediaSource, /* startPositionMs= */ 5000); + player.setMediaSource(mediaSource, /* startPositionMs= */ 5000); player.prepare(); } }) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index a353efdf5b..cb5ef0fa0f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -141,7 +141,7 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest { pendingSchedule = null; } DrmSessionManager drmSessionManager = buildDrmSessionManager(userAgent); - player.setMediaItem(buildSource(host, Util.getUserAgent(host, userAgent), drmSessionManager)); + player.setMediaSource(buildSource(host, Util.getUserAgent(host, userAgent), drmSessionManager)); player.prepare(); } 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 23f9a0d5a4..4416ab0ef3 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 @@ -447,7 +447,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc if (actionSchedule != null) { actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this); } - player.setMediaItem(mediaSource); + player.setMediaSource(mediaSource); player.prepare(); } catch (Exception e) { handleException(e); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 47f34712b9..81efb3ba78 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -112,12 +112,12 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { } @Override - public void setMediaItem(MediaSource mediaItem) { + public void setMediaSource(MediaSource mediaSource) { throw new UnsupportedOperationException(); } @Override - public void setMediaItem(MediaSource mediaItem, long startPositionMs) { + public void setMediaSource(MediaSource mediaSource, long startPositionMs) { throw new UnsupportedOperationException(); } From 87ca7961c27fffdb437b671a0938bc568965de98 Mon Sep 17 00:00:00 2001 From: kimvde Date: Mon, 9 Dec 2019 10:34:41 +0000 Subject: [PATCH 0463/1335] Remove TODO for supporting streams in Java FLAC extractor Flac streams exist but are not commonly used. Also, they are not supported by the FLAC extension extractor. PiperOrigin-RevId: 284514327 --- .../google/android/exoplayer2/extractor/flac/FlacExtractor.java | 1 - 1 file changed, 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java index 0f67153e61..9c8136e9a9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java @@ -43,7 +43,6 @@ import java.lang.annotation.RetentionPolicy; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // TODO: implement seeking. -// TODO: support live streams. /** * Extracts data from FLAC container format. * From 2462aeb44358b156e7838e25a3e32926a20861ab Mon Sep 17 00:00:00 2001 From: kimvde Date: Mon, 9 Dec 2019 13:57:21 +0000 Subject: [PATCH 0464/1335] Add peek() method to ExtractorInput PiperOrigin-RevId: 284537150 --- .../extractor/DefaultExtractorInput.java | 31 ++- .../exoplayer2/extractor/ExtractorInput.java | 44 ++-- .../extractor/DefaultExtractorInputTest.java | 226 +++++++++++++++--- .../testutil/FakeExtractorInput.java | 46 ++-- 4 files changed, 281 insertions(+), 66 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java index 450cca42b0..c6f1129da8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java @@ -58,7 +58,9 @@ public final class DefaultExtractorInput implements ExtractorInput { public int read(byte[] target, int offset, int length) throws IOException, InterruptedException { int bytesRead = readFromPeekBuffer(target, offset, length); if (bytesRead == 0) { - bytesRead = readFromDataSource(target, offset, length, 0, true); + bytesRead = + readFromDataSource( + target, offset, length, /* bytesAlreadyRead= */ 0, /* allowEndOfInput= */ true); } commitBytesRead(bytesRead); return bytesRead; @@ -110,6 +112,31 @@ public final class DefaultExtractorInput implements ExtractorInput { skipFully(length, false); } + @Override + public int peek(byte[] target, int offset, int length) throws IOException, InterruptedException { + ensureSpaceForPeek(length); + int peekBufferRemainingBytes = peekBufferLength - peekBufferPosition; + int bytesPeeked; + if (peekBufferRemainingBytes == 0) { + bytesPeeked = + readFromDataSource( + peekBuffer, + peekBufferPosition, + length, + /* bytesAlreadyRead= */ 0, + /* allowEndOfInput= */ true); + if (bytesPeeked == C.RESULT_END_OF_INPUT) { + return C.RESULT_END_OF_INPUT; + } + peekBufferLength += bytesPeeked; + } else { + bytesPeeked = Math.min(length, peekBufferRemainingBytes); + } + System.arraycopy(peekBuffer, peekBufferPosition, target, offset, bytesPeeked); + peekBufferPosition += bytesPeeked; + return bytesPeeked; + } + @Override public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput) throws IOException, InterruptedException { @@ -201,7 +228,7 @@ public final class DefaultExtractorInput implements ExtractorInput { } /** - * Reads from the peek buffer + * Reads from the peek buffer. * * @param target A target array into which data should be written. * @param offset The offset into the target array at which to write. 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 461b059bad..8e5d6f0448 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 @@ -27,19 +27,19 @@ import java.io.InputStream; * for more info about each mode. * *

      - *
    • The {@code read()} and {@code skip()} methods provide {@link InputStream}-like byte-level - * access operations. + *
    • The {@code read()/peek()} 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. + *

    The {@code read()/peek()} 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

    * @@ -102,7 +102,8 @@ public interface ExtractorInput { throws IOException, InterruptedException; /** - * Equivalent to {@code readFully(target, offset, length, false)}. + * Equivalent to {@link #readFully(byte[], int, int, boolean) readFully(target, offset, length, + * false)}. * * @param target A target array into which data should be written. * @param offset The offset into the target array at which to write. @@ -155,8 +156,11 @@ public interface ExtractorInput { void skipFully(int length) throws IOException, InterruptedException; /** - * Peeks {@code length} bytes from the peek position, writing them into {@code target} at index - * {@code offset}. The current read position is left unchanged. + * Peeks up to {@code length} bytes from the peek position. The current read position is left + * unchanged. + * + *

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

    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 @@ -164,6 +168,18 @@ public interface ExtractorInput { * * @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 maximum number of bytes to peek from the input. + * @return The number of bytes peeked, or {@link C#RESULT_END_OF_INPUT} if the input has ended. + * @throws IOException If an error occurs peeking from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + int peek(byte[] target, int offset, int length) throws IOException, InterruptedException; + + /** + * Like {@link #peek(byte[], int, int)}, but peeks the requested {@code length} in full. + * + * @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 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 @@ -181,12 +197,8 @@ public interface ExtractorInput { throws IOException, InterruptedException; /** - * Peeks {@code length} bytes from the peek position, writing them into {@code target} at index - * {@code offset}. The current read position is left unchanged. - *

    - * Calling {@link #resetPeekPosition()} resets the peek position to equal the current read - * position, so the caller can peek the same data again. Reading and skipping also reset the peek - * position. + * Equivalent to {@link #peekFully(byte[], int, int, boolean) peekFully(target, offset, length, + * false)}. * * @param target A target array into which data should be written. * @param offset The offset into the target array at which to write. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java index 6dbec3ecf4..ccc806fe61 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java @@ -49,7 +49,7 @@ public class DefaultExtractorInputTest { } @Test - public void testRead() throws Exception { + public void testReadMultipleTimes() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; // We expect to perform three reads of three bytes, as setup in buildTestDataSource. @@ -60,39 +60,70 @@ public class DefaultExtractorInputTest { assertThat(bytesRead).isEqualTo(6); bytesRead += input.read(target, 6, TEST_DATA.length); assertThat(bytesRead).isEqualTo(9); - // Check the read data is correct. - assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); - // Check we're now indicated that the end of input is reached. - int expectedEndOfInput = input.read(target, 0, TEST_DATA.length); - assertThat(expectedEndOfInput).isEqualTo(RESULT_END_OF_INPUT); + assertThat(input.getPosition()).isEqualTo(9); + assertThat(TEST_DATA).isEqualTo(target); } @Test - public void testReadPeeked() throws Exception { + public void testReadAlreadyPeeked() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; input.advancePeekPosition(TEST_DATA.length); + int bytesRead = input.read(target, 0, TEST_DATA.length - 1); + assertThat(bytesRead).isEqualTo(TEST_DATA.length - 1); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length - 1); + assertThat(Arrays.copyOf(TEST_DATA, TEST_DATA.length - 1)) + .isEqualTo(Arrays.copyOf(target, TEST_DATA.length - 1)); + } + + @Test + public void testReadPartiallyPeeked() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + input.advancePeekPosition(TEST_DATA.length - 1); int bytesRead = input.read(target, 0, TEST_DATA.length); - assertThat(bytesRead).isEqualTo(TEST_DATA.length); - // Check the read data is correct. - assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(bytesRead).isEqualTo(TEST_DATA.length - 1); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length - 1); + assertThat(Arrays.copyOf(TEST_DATA, TEST_DATA.length - 1)) + .isEqualTo(Arrays.copyOf(target, TEST_DATA.length - 1)); } @Test - public void testReadMoreDataPeeked() throws Exception { + public void testReadEndOfInputBeforeFirstByteRead() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; - input.advancePeekPosition(TEST_DATA.length); + input.skipFully(TEST_DATA.length); + int bytesRead = input.read(target, 0, TEST_DATA.length); - int bytesRead = input.read(target, 0, TEST_DATA.length + 1); - assertThat(bytesRead).isEqualTo(TEST_DATA.length); + assertThat(bytesRead).isEqualTo(RESULT_END_OF_INPUT); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); + } - // Check the read data is correct. - assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + @Test + public void testReadEndOfInputAfterFirstByteRead() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + input.skipFully(TEST_DATA.length - 1); + int bytesRead = input.read(target, 0, TEST_DATA.length); + + assertThat(bytesRead).isEqualTo(1); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); + } + + @Test + public void testReadZeroLength() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + int bytesRead = input.read(target, /* offset= */ 0, /* length= */ 0); + + assertThat(bytesRead).isEqualTo(0); } @Test @@ -101,7 +132,7 @@ public class DefaultExtractorInputTest { byte[] target = new byte[TEST_DATA.length]; input.readFully(target, 0, TEST_DATA.length); // Check that we read the whole of TEST_DATA. - assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(TEST_DATA).isEqualTo(target); assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); // Check that we see end of input if we read again with allowEndOfInput set. boolean result = input.readFully(target, 0, 1, true); @@ -121,11 +152,11 @@ public class DefaultExtractorInputTest { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[5]; input.readFully(target, 0, 5); - assertThat(Arrays.equals(copyOf(TEST_DATA, 5), target)).isTrue(); + assertThat(copyOf(TEST_DATA, 5)).isEqualTo(target); assertThat(input.getPosition()).isEqualTo(5); target = new byte[4]; input.readFully(target, 0, 4); - assertThat(Arrays.equals(copyOfRange(TEST_DATA, 5, 9), target)).isTrue(); + assertThat(copyOfRange(TEST_DATA, 5, 9)).isEqualTo(target); assertThat(input.getPosition()).isEqualTo(5 + 4); } @@ -180,27 +211,23 @@ public class DefaultExtractorInputTest { input.readFully(target, 0, TEST_DATA.length); // Check the read data is correct. - assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(TEST_DATA).isEqualTo(target); assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); } @Test - public void testSkip() throws Exception { - FakeDataSource testDataSource = buildDataSource(); - DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); + public void testSkipMultipleTimes() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); // We expect to perform three skips of three bytes, as setup in buildTestDataSource. for (int i = 0; i < 3; i++) { assertThat(input.skip(TEST_DATA.length)).isEqualTo(3); } - // Check we're now indicated that the end of input is reached. - int expectedEndOfInput = input.skip(TEST_DATA.length); - assertThat(expectedEndOfInput).isEqualTo(RESULT_END_OF_INPUT); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); } @Test public void testLargeSkip() throws Exception { - FakeDataSource testDataSource = buildLargeDataSource(); - DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); + DefaultExtractorInput input = createDefaultExtractorInput(); // Check that skipping the entire data source succeeds. int bytesToSkip = LARGE_TEST_DATA_LENGTH; while (bytesToSkip > 0) { @@ -208,6 +235,59 @@ public class DefaultExtractorInputTest { } } + @Test + public void testSkipAlreadyPeeked() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + + input.advancePeekPosition(TEST_DATA.length); + int bytesSkipped = input.skip(TEST_DATA.length - 1); + + assertThat(bytesSkipped).isEqualTo(TEST_DATA.length - 1); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length - 1); + } + + @Test + public void testSkipPartiallyPeeked() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + + input.advancePeekPosition(TEST_DATA.length - 1); + int bytesSkipped = input.skip(TEST_DATA.length); + + assertThat(bytesSkipped).isEqualTo(TEST_DATA.length - 1); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length - 1); + } + + @Test + public void testSkipEndOfInputBeforeFirstByteSkipped() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + + input.skipFully(TEST_DATA.length); + int bytesSkipped = input.skip(TEST_DATA.length); + + assertThat(bytesSkipped).isEqualTo(RESULT_END_OF_INPUT); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); + } + + @Test + public void testSkipEndOfInputAfterFirstByteSkipped() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + + input.skipFully(TEST_DATA.length - 1); + int bytesSkipped = input.skip(TEST_DATA.length); + + assertThat(bytesSkipped).isEqualTo(1); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); + } + + @Test + public void testSkipZeroLength() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + + int bytesRead = input.skip(0); + + assertThat(bytesRead).isEqualTo(0); + } + @Test public void testSkipFullyOnce() throws Exception { // Skip TEST_DATA. @@ -309,6 +389,86 @@ public class DefaultExtractorInputTest { } } + @Test + public void testPeekMultipleTimes() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + // We expect to perform three peeks of three bytes, as setup in buildTestDataSource. + int bytesPeeked = 0; + bytesPeeked += input.peek(target, 0, TEST_DATA.length); + assertThat(bytesPeeked).isEqualTo(3); + bytesPeeked += input.peek(target, 3, TEST_DATA.length); + assertThat(bytesPeeked).isEqualTo(6); + bytesPeeked += input.peek(target, 6, TEST_DATA.length); + assertThat(bytesPeeked).isEqualTo(9); + assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); + assertThat(TEST_DATA).isEqualTo(target); + } + + @Test + public void testPeekAlreadyPeeked() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + input.advancePeekPosition(TEST_DATA.length); + input.resetPeekPosition(); + int bytesPeeked = input.peek(target, 0, TEST_DATA.length - 1); + + assertThat(bytesPeeked).isEqualTo(TEST_DATA.length - 1); + assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length - 1); + assertThat(Arrays.copyOf(TEST_DATA, TEST_DATA.length - 1)) + .isEqualTo(Arrays.copyOf(target, TEST_DATA.length - 1)); + } + + @Test + public void testPeekPartiallyPeeked() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + input.advancePeekPosition(TEST_DATA.length - 1); + input.resetPeekPosition(); + int bytesPeeked = input.peek(target, 0, TEST_DATA.length); + + assertThat(bytesPeeked).isEqualTo(TEST_DATA.length - 1); + assertThat(Arrays.copyOf(TEST_DATA, TEST_DATA.length - 1)) + .isEqualTo(Arrays.copyOf(target, TEST_DATA.length - 1)); + } + + @Test + public void testPeekEndOfInputBeforeFirstBytePeeked() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + input.advancePeekPosition(TEST_DATA.length); + int bytesPeeked = input.peek(target, 0, TEST_DATA.length); + + assertThat(bytesPeeked).isEqualTo(RESULT_END_OF_INPUT); + assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); + } + + @Test + public void testPeekEndOfInputAfterFirstBytePeeked() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + input.advancePeekPosition(TEST_DATA.length - 1); + int bytesPeeked = input.peek(target, 0, TEST_DATA.length); + + assertThat(bytesPeeked).isEqualTo(1); + assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); + } + + @Test + public void testPeekZeroLength() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + int bytesPeeked = input.peek(target, /* offset= */ 0, /* length= */ 0); + + assertThat(bytesPeeked).isEqualTo(0); + } + @Test public void testPeekFully() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); @@ -316,14 +476,14 @@ public class DefaultExtractorInputTest { input.peekFully(target, 0, TEST_DATA.length); // Check that we read the whole of TEST_DATA. - assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(TEST_DATA).isEqualTo(target); assertThat(input.getPosition()).isEqualTo(0); assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); // Check that we can read again from the buffer byte[] target2 = new byte[TEST_DATA.length]; input.readFully(target2, 0, TEST_DATA.length); - assertThat(Arrays.equals(TEST_DATA, target2)).isTrue(); + assertThat(TEST_DATA).isEqualTo(target2); assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); @@ -350,7 +510,7 @@ public class DefaultExtractorInputTest { input.peekFully(target, /* offset= */ 0, /* length= */ TEST_DATA.length); assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); - assertThat(Arrays.equals(TEST_DATA, Arrays.copyOf(target, TEST_DATA.length))).isTrue(); + assertThat(TEST_DATA).isEqualTo(Arrays.copyOf(target, TEST_DATA.length)); } @Test @@ -360,14 +520,14 @@ public class DefaultExtractorInputTest { input.peekFully(target, 0, TEST_DATA.length); // Check that we read the whole of TEST_DATA. - assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(TEST_DATA).isEqualTo(target); assertThat(input.getPosition()).isEqualTo(0); // Check that we can peek again after resetting. input.resetPeekPosition(); byte[] target2 = new byte[TEST_DATA.length]; input.peekFully(target2, 0, TEST_DATA.length); - assertThat(Arrays.equals(TEST_DATA, target2)).isTrue(); + assertThat(TEST_DATA).isEqualTo(target2); // Check that we fail with EOFException if we peek past the end of the input. try { 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 443ffdb12c..7323cfd0fe 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 @@ -65,7 +65,8 @@ public final class FakeExtractorInput implements ExtractorInput { private int readPosition; private int peekPosition; - private final SparseBooleanArray partiallySatisfiedTargetPositions; + private final SparseBooleanArray partiallySatisfiedTargetReadPositions; + private final SparseBooleanArray partiallySatisfiedTargetPeekPositions; private final SparseBooleanArray failedReadPositions; private final SparseBooleanArray failedPeekPositions; @@ -75,7 +76,8 @@ public final class FakeExtractorInput implements ExtractorInput { this.simulateUnknownLength = simulateUnknownLength; this.simulatePartialReads = simulatePartialReads; this.simulateIOErrors = simulateIOErrors; - partiallySatisfiedTargetPositions = new SparseBooleanArray(); + partiallySatisfiedTargetReadPositions = new SparseBooleanArray(); + partiallySatisfiedTargetPeekPositions = new SparseBooleanArray(); failedReadPositions = new SparseBooleanArray(); failedPeekPositions = new SparseBooleanArray(); } @@ -84,7 +86,8 @@ public final class FakeExtractorInput implements ExtractorInput { public void reset() { readPosition = 0; peekPosition = 0; - partiallySatisfiedTargetPositions.clear(); + partiallySatisfiedTargetReadPositions.clear(); + partiallySatisfiedTargetPeekPositions.clear(); failedReadPositions.clear(); failedPeekPositions.clear(); } @@ -104,7 +107,7 @@ public final class FakeExtractorInput implements ExtractorInput { @Override public int read(byte[] target, int offset, int length) throws IOException { checkIOException(readPosition, failedReadPositions); - length = getReadLength(length); + length = getLengthToRead(readPosition, length, partiallySatisfiedTargetReadPositions); return readFullyInternal(target, offset, length, true) ? length : C.RESULT_END_OF_INPUT; } @@ -123,7 +126,7 @@ public final class FakeExtractorInput implements ExtractorInput { @Override public int skip(int length) throws IOException { checkIOException(readPosition, failedReadPositions); - length = getReadLength(length); + length = getLengthToRead(readPosition, length, partiallySatisfiedTargetReadPositions); return skipFullyInternal(length, true) ? length : C.RESULT_END_OF_INPUT; } @@ -138,16 +141,18 @@ public final class FakeExtractorInput implements ExtractorInput { skipFully(length, false); } + @Override + public int peek(byte[] target, int offset, int length) throws IOException { + checkIOException(peekPosition, failedPeekPositions); + length = getLengthToRead(peekPosition, length, partiallySatisfiedTargetPeekPositions); + return peekFullyInternal(target, offset, length, true) ? length : C.RESULT_END_OF_INPUT; + } + @Override public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput) throws IOException { checkIOException(peekPosition, failedPeekPositions); - if (!checkXFully(allowEndOfInput, peekPosition, length)) { - return false; - } - System.arraycopy(data, peekPosition, target, offset, length); - peekPosition += length; - return true; + return peekFullyInternal(target, offset, length, allowEndOfInput); } @Override @@ -221,18 +226,19 @@ public final class FakeExtractorInput implements ExtractorInput { return true; } - private int getReadLength(int requestedLength) { - if (readPosition == data.length) { + private int getLengthToRead( + int position, int requestedLength, SparseBooleanArray partiallySatisfiedTargetPositions) { + if (position == data.length) { // If the requested length is non-zero, the end of the input will be read. return requestedLength == 0 ? 0 : Integer.MAX_VALUE; } - int targetPosition = readPosition + requestedLength; + int targetPosition = position + requestedLength; if (simulatePartialReads && requestedLength > 1 && !partiallySatisfiedTargetPositions.get(targetPosition)) { partiallySatisfiedTargetPositions.put(targetPosition, true); return 1; } - return Math.min(requestedLength, data.length - readPosition); + return Math.min(requestedLength, data.length - position); } private boolean readFullyInternal(byte[] target, int offset, int length, boolean allowEndOfInput) @@ -255,6 +261,16 @@ public final class FakeExtractorInput implements ExtractorInput { return true; } + private boolean peekFullyInternal(byte[] target, int offset, int length, boolean allowEndOfInput) + throws EOFException { + if (!checkXFully(allowEndOfInput, peekPosition, length)) { + return false; + } + System.arraycopy(data, peekPosition, target, offset, length); + peekPosition += length; + return true; + } + /** * Builder of {@link FakeExtractorInput} instances. */ From 3156fbfc6e4fcc07d05f6e66f58c24253cb70406 Mon Sep 17 00:00:00 2001 From: christosts Date: Mon, 9 Dec 2019 13:57:36 +0000 Subject: [PATCH 0465/1335] Move MediaCodecAdapter out of MediaCodecRenderer Move MediaCodeAdapter and implementations to separate files and add unit tests for AsynchronousMediaCodecAdapter. PiperOrigin-RevId: 284537185 --- .../AsynchronousMediaCodecAdapter.java | 140 +++++++++++ .../mediacodec/MediaCodecAdapter.java | 79 ++++++ .../mediacodec/MediaCodecRenderer.java | 218 +++------------- .../SynchronousMediaCodecAdapter.java | 56 +++++ .../AsynchronousMediaCodecAdapterTest.java | 235 ++++++++++++++++++ .../MediaCodecAsyncCallbackTest.java | 14 +- .../mediacodec/MediaCodecTestUtils.java | 59 +++++ 7 files changed, 610 insertions(+), 191 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecTestUtils.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java new file mode 100644 index 0000000000..c0596c0550 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java @@ -0,0 +1,140 @@ +/* + * 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.mediacodec; + +import android.media.MediaCodec; +import android.media.MediaFormat; +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; +import com.google.android.exoplayer2.util.Assertions; + +/** + * A {@link MediaCodecAdapter} that operates the {@link MediaCodec} in asynchronous mode. + * + *

    The AsynchronousMediaCodecAdapter routes callbacks to the current Thread's {@link Looper} + * obtained via {@link Looper#myLooper()} + */ +@RequiresApi(21) +/* package */ final class AsynchronousMediaCodecAdapter implements MediaCodecAdapter { + private MediaCodecAsyncCallback mediaCodecAsyncCallback; + private final Handler handler; + private final MediaCodec codec; + @Nullable private IllegalStateException internalException; + private boolean flushing; + private Runnable onCodecStart; + + /** + * Create a new {@code AsynchronousMediaCodecAdapter}. + * + * @param codec the {@link MediaCodec} to wrap. + */ + public AsynchronousMediaCodecAdapter(MediaCodec codec) { + this(codec, Assertions.checkNotNull(Looper.myLooper())); + } + + @VisibleForTesting + /* package */ AsynchronousMediaCodecAdapter(MediaCodec codec, Looper looper) { + this.mediaCodecAsyncCallback = new MediaCodecAsyncCallback(); + handler = new Handler(looper); + this.codec = codec; + this.codec.setCallback(mediaCodecAsyncCallback); + onCodecStart = () -> codec.start(); + } + + @Override + public int dequeueInputBufferIndex() { + if (flushing) { + return MediaCodec.INFO_TRY_AGAIN_LATER; + } else { + maybeThrowException(); + return mediaCodecAsyncCallback.dequeueInputBufferIndex(); + } + } + + @Override + public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { + if (flushing) { + return MediaCodec.INFO_TRY_AGAIN_LATER; + } else { + maybeThrowException(); + return mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo); + } + } + + @Override + public MediaFormat getOutputFormat() { + return mediaCodecAsyncCallback.getOutputFormat(); + } + + @Override + public void flush() { + clearPendingFlushState(); + flushing = true; + codec.flush(); + handler.post(this::onCompleteFlush); + } + + @Override + public void shutdown() { + clearPendingFlushState(); + } + + @VisibleForTesting + /* package */ MediaCodec.Callback getMediaCodecCallback() { + return mediaCodecAsyncCallback; + } + + private void onCompleteFlush() { + flushing = false; + mediaCodecAsyncCallback.flush(); + try { + onCodecStart.run(); + } catch (IllegalStateException e) { + // Catch IllegalStateException directly so that we don't have to wrap it. + internalException = e; + } catch (Exception e) { + internalException = new IllegalStateException(e); + } + } + + @VisibleForTesting + /* package */ void setOnCodecStart(Runnable onCodecStart) { + this.onCodecStart = onCodecStart; + } + + private void maybeThrowException() throws IllegalStateException { + maybeThrowInternalException(); + mediaCodecAsyncCallback.maybeThrowMediaCodecException(); + } + + private void maybeThrowInternalException() { + if (internalException != null) { + IllegalStateException e = internalException; + internalException = null; + throw e; + } + } + + /** Clear state related to pending flush events. */ + private void clearPendingFlushState() { + handler.removeCallbacksAndMessages(null); + internalException = null; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java new file mode 100644 index 0000000000..9d86f37736 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java @@ -0,0 +1,79 @@ +/* + * 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.mediacodec; + +import android.media.MediaCodec; +import android.media.MediaFormat; + +/** + * Abstracts {@link MediaCodec} operations. + * + *

    {@code MediaCodecAdapter} offers a common interface to interact with a {@link MediaCodec} + * regardless of the {@link + * com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.MediaCodecOperationMode} the {@link + * MediaCodec} is operating in. + * + * @see com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.MediaCodecOperationMode + */ +/* package */ interface MediaCodecAdapter { + + /** + * Returns the next available input buffer index from the underlying {@link MediaCodec} or {@link + * MediaCodec#INFO_TRY_AGAIN_LATER} if no such buffer exists. + * + * @throws {@link IllegalStateException} if the underlying {@link MediaCodec} raised an error. + */ + int dequeueInputBufferIndex(); + + /** + * Returns the next available output buffer index from the underlying {@link MediaCodec}. If the + * next available output is a MediaFormat change, it will return {@link + * MediaCodec#INFO_OUTPUT_FORMAT_CHANGED} and you should call {@link #getOutputFormat()} to get + * the format. If there is no available output, this method will return {@link + * MediaCodec#INFO_TRY_AGAIN_LATER}. + * + * @throws {@link IllegalStateException} if the underlying {@link MediaCodec} raised an error. + */ + int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo); + + /** + * Gets the {@link MediaFormat} that was output from the {@link MediaCodec}. + * + *

    Call this method if a previous call to {@link #dequeueOutputBufferIndex} returned {@link + * MediaCodec#INFO_OUTPUT_FORMAT_CHANGED}. + */ + MediaFormat getOutputFormat(); + + /** + * Flushes the {@code MediaCodecAdapter}. + * + *

    Note: {@link #flush()} should also call any {@link MediaCodec} methods needed to flush the + * {@link MediaCodec}, i.e., {@link MediaCodec#flush()} and optionally {@link + * MediaCodec#start()}, if the {@link MediaCodec} operates in asynchronous mode. + */ + void flush(); + + /** + * Shutdown the {@code MediaCodecAdapter}. + * + *

    Note: This method does not release the underlying {@link MediaCodec}. Make sure to call + * {@link #shutdown()} before stopping or releasing the underlying {@link MediaCodec} to ensure + * the adapter is fully shutdown before the {@link MediaCodec} stops executing. Otherwise, there + * is a risk the adapter might interact with a stopped or released {@link MediaCodec}. + */ + void shutdown(); +} 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 10276abd5f..e5b62a97cc 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 @@ -23,13 +23,10 @@ import android.media.MediaCrypto; import android.media.MediaCryptoException; import android.media.MediaFormat; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.os.SystemClock; import androidx.annotation.CheckResult; import androidx.annotation.IntDef; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -193,6 +190,24 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } } + /** The modes to operate the {@link MediaCodec}. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + MediaCodecOperationMode.SYNCHRONOUS, + MediaCodecOperationMode.ASYNCHRONOUS_PLAYBACK_THREAD + }) + public @interface MediaCodecOperationMode { + + /** Operates the {@link MediaCodec} in synchronous mode. */ + int SYNCHRONOUS = 0; + /** + * Operates the {@link MediaCodec} in asynchronous mode and routes {@link MediaCodec.Callback} + * callbacks to the playback Thread. + */ + int ASYNCHRONOUS_PLAYBACK_THREAD = 1; + } + /** Indicates no codec operating rate should be set. */ protected static final float CODEC_OPERATING_RATE_UNSET = -1; @@ -293,50 +308,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { }) private @interface AdaptationWorkaroundMode {} - /** - * Abstracts {@link MediaCodec} operations that differ whether a {@link MediaCodec} is used in - * synchronous or asynchronous mode. - */ - private interface MediaCodecAdapter { - - /** - * Returns the next available input buffer index from the underlying {@link MediaCodec} or - * {@link MediaCodec#INFO_TRY_AGAIN_LATER} if no such buffer exists. - * - * @throws {@link IllegalStateException} if the underling {@link MediaCodec} raised an error. - */ - int dequeueInputBufferIndex(); - - /** - * Returns the next available output buffer index from the underlying {@link MediaCodec}. If the - * next available output is a MediaFormat change, it will return {@link - * MediaCodec#INFO_OUTPUT_FORMAT_CHANGED} and you should call {@link #getOutputFormat()} to get - * the format. If there is no available output, this method will return {@link - * MediaCodec#INFO_TRY_AGAIN_LATER}. - * - * @throws {@link IllegalStateException} if the underling {@link MediaCodec} raised an error. - */ - int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo); - - /** - * Gets the {@link MediaFormat} that was output from the {@link MediaCodec}. - * - *

    Call this method if a previous call to {@link #dequeueOutputBufferIndex} returned {@link - * MediaCodec#INFO_OUTPUT_FORMAT_CHANGED}. - */ - MediaFormat getOutputFormat(); - - /** Flushes the {@code MediaCodecAdapter}. */ - void flush(); - - /** - * Shutdown the {@code MediaCodecAdapter}. - * - *

    Note: it does not release the underlying codec. - */ - void shutdown(); - } - /** * The adaptation workaround is never used. */ @@ -424,7 +395,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private boolean skipMediaCodecStopOnRelease; private boolean pendingOutputEndOfStream; - private boolean useMediaCodecInAsyncMode; + private @MediaCodecOperationMode int mediaCodecOperationMode; protected DecoderCounters decoderCounters; @@ -470,6 +441,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecOperatingRate = CODEC_OPERATING_RATE_UNSET; rendererOperatingRate = 1f; renderTimeLimitMs = C.TIME_UNSET; + mediaCodecOperationMode = MediaCodecOperationMode.SYNCHRONOUS; } /** @@ -503,16 +475,25 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } /** - * Use the underlying {@link MediaCodec} in asynchronous mode to obtain available input and output - * buffers. + * Set the mode of operation of the underlying {@link MediaCodec}. * *

    This method is experimental, and will be renamed or removed in a future release. It should * only be called before the renderer is used. * - * @param enabled enable of disable the feature. + * @param mode the mode of the MediaCodec. The supported modes are: + *

      + *
    • {@link MediaCodecOperationMode#SYNCHRONOUS}: The {@link MediaCodec} will operate in + * synchronous mode. + *
    • {@link MediaCodecOperationMode#ASYNCHRONOUS_PLAYBACK_THREAD}: The {@link MediaCodec} + * will operate in asynchronous mode and {@link MediaCodec.Callback} callbacks will be + * routed to the Playback Thread. This mode requires API level ≥ 21; if the API level + * is ≤ 20, the operation mode will be set to {@link + * MediaCodecOperationMode#SYNCHRONOUS}. + *
    + * By default, the operation mode is set to {@link MediaCodecOperationMode#SYNCHRONOUS}. */ - public void experimental_setUseMediaCodecInAsyncMode(boolean enabled) { - useMediaCodecInAsyncMode = enabled; + public void experimental_setMediaCodecOperationMode(@MediaCodecOperationMode int mode) { + mediaCodecOperationMode = mode; } @Override @@ -709,6 +690,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { availableCodecInfos = null; codecInfo = null; codecFormat = null; + if (codecAdapter != null) { + codecAdapter.shutdown(); + codecAdapter = null; + } resetInputBuffer(); resetOutputBuffer(); resetCodecBuffers(); @@ -730,10 +715,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } } finally { codec = null; - if (codecAdapter != null) { - codecAdapter.shutdown(); - codecAdapter = null; - } try { if (mediaCrypto != null) { mediaCrypto.release(); @@ -982,7 +963,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecInitializingTimestamp = SystemClock.elapsedRealtime(); TraceUtil.beginSection("createCodec:" + codecName); codec = MediaCodec.createByCodecName(codecName); - if (useMediaCodecInAsyncMode && Util.SDK_INT >= 21) { + if (mediaCodecOperationMode == MediaCodecOperationMode.ASYNCHRONOUS_PLAYBACK_THREAD + && Util.SDK_INT >= 21) { codecAdapter = new AsynchronousMediaCodecAdapter(codec); } else { codecAdapter = new SynchronousMediaCodecAdapter(codec, getDequeueOutputBufferTimeoutUs()); @@ -2021,124 +2003,4 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return Util.SDK_INT <= 18 && format.channelCount == 1 && "OMX.MTK.AUDIO.DECODER.MP3".equals(name); } - - @RequiresApi(21) - private static class AsynchronousMediaCodecAdapter implements MediaCodecAdapter { - - private MediaCodecAsyncCallback mediaCodecAsyncCallback; - private final Handler handler; - private final MediaCodec codec; - @Nullable private IllegalStateException internalException; - private boolean flushing; - - public AsynchronousMediaCodecAdapter(MediaCodec codec) { - mediaCodecAsyncCallback = new MediaCodecAsyncCallback(); - handler = new Handler(Looper.myLooper()); - this.codec = codec; - this.codec.setCallback(mediaCodecAsyncCallback); - } - - @Override - public int dequeueInputBufferIndex() { - if (flushing) { - return MediaCodec.INFO_TRY_AGAIN_LATER; - } else { - maybeThrowException(); - return mediaCodecAsyncCallback.dequeueInputBufferIndex(); - } - } - - @Override - public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { - if (flushing) { - return MediaCodec.INFO_TRY_AGAIN_LATER; - } else { - maybeThrowException(); - return mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo); - } - } - - @Override - public MediaFormat getOutputFormat() { - return mediaCodecAsyncCallback.getOutputFormat(); - } - - @Override - public void flush() { - clearPendingFlushState(); - flushing = true; - codec.flush(); - handler.post(this::onCompleteFlush); - } - - @Override - public void shutdown() { - clearPendingFlushState(); - } - - private void onCompleteFlush() { - flushing = false; - mediaCodecAsyncCallback.flush(); - try { - codec.start(); - } catch (IllegalStateException e) { - // Catch IllegalStateException directly so that we don't have to wrap it - internalException = e; - } catch (Exception e) { - internalException = new IllegalStateException(e); - } - } - - private void maybeThrowException() throws IllegalStateException { - maybeThrowInternalException(); - mediaCodecAsyncCallback.maybeThrowMediaCodecException(); - } - - private void maybeThrowInternalException() { - if (internalException != null) { - IllegalStateException e = internalException; - internalException = null; - throw e; - } - } - - /** Clear state related to pending flush events. */ - private void clearPendingFlushState() { - handler.removeCallbacksAndMessages(null); - internalException = null; - } - } - - private static class SynchronousMediaCodecAdapter implements MediaCodecAdapter { - private final MediaCodec codec; - private final long dequeueOutputBufferTimeoutMs; - - public SynchronousMediaCodecAdapter(MediaCodec mediaCodec, long dequeueOutputBufferTimeoutMs) { - this.codec = mediaCodec; - this.dequeueOutputBufferTimeoutMs = dequeueOutputBufferTimeoutMs; - } - - @Override - public int dequeueInputBufferIndex() { - return codec.dequeueInputBuffer(0); - } - - @Override - public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { - return codec.dequeueOutputBuffer(bufferInfo, dequeueOutputBufferTimeoutMs); - } - - @Override - public MediaFormat getOutputFormat() { - return codec.getOutputFormat(); - } - - @Override - public void flush() { - codec.flush(); - } - - @Override - public void shutdown() {} - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java new file mode 100644 index 0000000000..8caf72ecf4 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java @@ -0,0 +1,56 @@ +/* + * 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.mediacodec; + +import android.media.MediaCodec; +import android.media.MediaFormat; + +/** + * A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in synchronous mode. + */ +/* package */ final class SynchronousMediaCodecAdapter implements MediaCodecAdapter { + private final MediaCodec codec; + private final long dequeueOutputBufferTimeoutMs; + + public SynchronousMediaCodecAdapter(MediaCodec mediaCodec, long dequeueOutputBufferTimeoutMs) { + this.codec = mediaCodec; + this.dequeueOutputBufferTimeoutMs = dequeueOutputBufferTimeoutMs; + } + + @Override + public int dequeueInputBufferIndex() { + return codec.dequeueInputBuffer(0); + } + + @Override + public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { + return codec.dequeueOutputBuffer(bufferInfo, dequeueOutputBufferTimeoutMs); + } + + @Override + public MediaFormat getOutputFormat() { + return codec.getOutputFormat(); + } + + @Override + public void flush() { + codec.flush(); + } + + @Override + public void shutdown() {} +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java new file mode 100644 index 0000000000..d2bb0fcc5b --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java @@ -0,0 +1,235 @@ +/* + * 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.mediacodec; + +import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.areEqual; +import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.waitUntilAllEventsAreExecuted; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import android.media.MediaCodec; +import android.media.MediaFormat; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link AsynchronousMediaCodecAdapter}. */ +@RunWith(AndroidJUnit4.class) +public class AsynchronousMediaCodecAdapterTest { + private AsynchronousMediaCodecAdapter adapter; + private MediaCodec codec; + private HandlerThread handlerThread; + private Looper looper; + private MediaCodec.BufferInfo bufferInfo; + + @Before + public void setup() throws IOException { + handlerThread = new HandlerThread("TestHandlerThread"); + handlerThread.start(); + looper = handlerThread.getLooper(); + codec = MediaCodec.createByCodecName("h264"); + adapter = new AsynchronousMediaCodecAdapter(codec, looper); + bufferInfo = new MediaCodec.BufferInfo(); + } + + @After + public void tearDown() { + handlerThread.quit(); + } + + @Test + public void dequeueInputBufferIndex_withoutInputBuffer_returnsTryAgainLater() { + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeueInputBufferIndex_withInputBuffer_returnsInputBuffer() { + adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0); + + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0); + } + + @Test + public void dequeueInputBufferIndex_whileFlushing_returnsTryAgainLater() { + adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0); + adapter.flush(); + adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 1); + + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeueInputBufferIndex_afterFlushCompletes_returnsNextInputBuffer() + throws InterruptedException { + // Disable calling codec.start() after flush() completes to avoid receiving buffers from the + // shadow codec impl + adapter.setOnCodecStart(() -> {}); + Handler handler = new Handler(looper); + handler.post( + () -> adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0)); + adapter.flush(); // enqueues a flush event on the looper + handler.post( + () -> adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 1)); + + assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(1); + } + + @Test + public void dequeueInputBufferIndex_afterFlushCompletesWithError_throwsException() + throws InterruptedException { + adapter.setOnCodecStart( + () -> { + throw new IllegalStateException("codec#start() exception"); + }); + adapter.flush(); + + assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); + try { + adapter.dequeueInputBufferIndex(); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test + public void dequeueOutputBufferIndex_withoutOutputBuffer_returnsTryAgainLater() { + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeueOutputBufferIndex_withOutputBuffer_returnsOutputBuffer() { + MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); + outBufferInfo.presentationTimeUs = 10; + adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, outBufferInfo); + + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(0); + assertThat(areEqual(bufferInfo, outBufferInfo)).isTrue(); + } + + @Test + public void dequeueOutputBufferIndex_whileFlushing_returnsTryAgainLater() { + adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, bufferInfo); + adapter.flush(); + adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 1, bufferInfo); + + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeueOutputBufferIndex_afterFlushCompletes_returnsNextOutputBuffer() + throws InterruptedException { + // Disable calling codec.start() after flush() completes to avoid receiving buffers from the + // shadow codec impl + adapter.setOnCodecStart(() -> {}); + Handler handler = new Handler(looper); + MediaCodec.BufferInfo info0 = new MediaCodec.BufferInfo(); + handler.post( + () -> adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, info0)); + adapter.flush(); // enqueues a flush event on the looper + MediaCodec.BufferInfo info1 = new MediaCodec.BufferInfo(); + info1.presentationTimeUs = 1; + handler.post( + () -> adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 1, info1)); + + assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(1); + assertThat(areEqual(bufferInfo, info1)).isTrue(); + } + + @Test + public void dequeueOutputBufferIndex_afterFlushCompletesWithError_throwsException() + throws InterruptedException { + adapter.setOnCodecStart( + () -> { + throw new RuntimeException("codec#start() exception"); + }); + adapter.flush(); + + assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); + try { + adapter.dequeueOutputBufferIndex(bufferInfo); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test + public void getOutputFormat_withoutFormat_throwsException() { + try { + adapter.getOutputFormat(); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test + public void getOutputFormat_withMultipleFormats_returnsFormatsInCorrectOrder() { + MediaFormat[] formats = new MediaFormat[10]; + MediaCodec.Callback mediaCodecCallback = adapter.getMediaCodecCallback(); + for (int i = 0; i < formats.length; i++) { + formats[i] = new MediaFormat(); + mediaCodecCallback.onOutputFormatChanged(codec, formats[i]); + } + + for (MediaFormat format : formats) { + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); + assertThat(adapter.getOutputFormat()).isEqualTo(format); + // Call it again to ensure same format is returned + assertThat(adapter.getOutputFormat()).isEqualTo(format); + } + // Obtain next output buffer + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + // Format should remain as is + assertThat(adapter.getOutputFormat()).isEqualTo(formats[formats.length - 1]); + } + + @Test + public void getOutputFormat_afterFlush_returnsPreviousFormat() throws InterruptedException { + MediaFormat format = new MediaFormat(); + adapter.getMediaCodecCallback().onOutputFormatChanged(codec, format); + adapter.dequeueOutputBufferIndex(bufferInfo); + adapter.flush(); + + assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); + assertThat(adapter.getOutputFormat()).isEqualTo(format); + } + + @Test + public void shutdown_withPendingFlush_cancelsFlush() throws InterruptedException { + AtomicBoolean onCodecStartCalled = new AtomicBoolean(false); + Runnable onCodecStart = () -> onCodecStartCalled.set(true); + adapter.setOnCodecStart(onCodecStart); + adapter.flush(); + adapter.shutdown(); + + assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); + assertThat(onCodecStartCalled.get()).isFalse(); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallbackTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallbackTest.java index 1ada9f8583..5b6af91110 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallbackTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallbackTest.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.mediacodec; +import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.areEqual; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; @@ -204,17 +205,4 @@ public class MediaCodecAsyncCallbackTest { mediaCodecAsyncCallback.maybeThrowMediaCodecException(); } - - /** - * Compares if two {@link android.media.MediaCodec.BufferInfo} are equal by inspecting {@link - * android.media.MediaCodec.BufferInfo#flags}, {@link android.media.MediaCodec.BufferInfo#size}, - * {@link android.media.MediaCodec.BufferInfo#presentationTimeUs} and {@link - * android.media.MediaCodec.BufferInfo#offset}. - */ - private static boolean areEqual(MediaCodec.BufferInfo lhs, MediaCodec.BufferInfo rhs) { - return lhs.flags == rhs.flags - && lhs.offset == rhs.offset - && lhs.presentationTimeUs == rhs.presentationTimeUs - && lhs.size == rhs.size; - } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecTestUtils.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecTestUtils.java new file mode 100644 index 0000000000..ea816be4aa --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecTestUtils.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.mediacodec; + +import static org.robolectric.Shadows.shadowOf; + +import android.media.MediaCodec; +import android.os.Handler; +import android.os.Looper; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** Testing utilities for MediaCodec related test classes */ +public class MediaCodecTestUtils { + /** + * Compares if two {@link android.media.MediaCodec.BufferInfo} are equal by inspecting {@link + * android.media.MediaCodec.BufferInfo#flags}, {@link android.media.MediaCodec.BufferInfo#size}, + * {@link android.media.MediaCodec.BufferInfo#presentationTimeUs} and {@link + * android.media.MediaCodec.BufferInfo#offset}. + */ + public static boolean areEqual(MediaCodec.BufferInfo lhs, MediaCodec.BufferInfo rhs) { + return lhs.flags == rhs.flags + && lhs.offset == rhs.offset + && lhs.presentationTimeUs == rhs.presentationTimeUs + && lhs.size == rhs.size; + } + + /** + * Blocks until all events of a shadow looper are executed or the specified time elapses. + * + * @param looper the shadow looper + * @param time the time to wait + * @param unit the time units + * @return true if all events are executed, false if the time elapsed. + * @throws InterruptedException if the Thread was interrupted while waiting. + */ + public static boolean waitUntilAllEventsAreExecuted(Looper looper, long time, TimeUnit unit) + throws InterruptedException { + Handler handler = new Handler(looper); + CountDownLatch latch = new CountDownLatch(1); + handler.post(() -> latch.countDown()); + shadowOf(looper).idle(); + return latch.await(time, unit); + } +} From a9b327d932f421db3cddbeecef615bd89f56626d Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 9 Dec 2019 14:13:30 +0000 Subject: [PATCH 0466/1335] Rollback of https://github.com/google/ExoPlayer/commit/2462aeb44358b156e7838e25a3e32926a20861ab *** Original commit *** Add peek() method to ExtractorInput *** PiperOrigin-RevId: 284539719 --- .../extractor/DefaultExtractorInput.java | 31 +-- .../exoplayer2/extractor/ExtractorInput.java | 44 ++-- .../extractor/DefaultExtractorInputTest.java | 226 +++--------------- .../testutil/FakeExtractorInput.java | 46 ++-- 4 files changed, 66 insertions(+), 281 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java index c6f1129da8..450cca42b0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java @@ -58,9 +58,7 @@ public final class DefaultExtractorInput implements ExtractorInput { public int read(byte[] target, int offset, int length) throws IOException, InterruptedException { int bytesRead = readFromPeekBuffer(target, offset, length); if (bytesRead == 0) { - bytesRead = - readFromDataSource( - target, offset, length, /* bytesAlreadyRead= */ 0, /* allowEndOfInput= */ true); + bytesRead = readFromDataSource(target, offset, length, 0, true); } commitBytesRead(bytesRead); return bytesRead; @@ -112,31 +110,6 @@ public final class DefaultExtractorInput implements ExtractorInput { skipFully(length, false); } - @Override - public int peek(byte[] target, int offset, int length) throws IOException, InterruptedException { - ensureSpaceForPeek(length); - int peekBufferRemainingBytes = peekBufferLength - peekBufferPosition; - int bytesPeeked; - if (peekBufferRemainingBytes == 0) { - bytesPeeked = - readFromDataSource( - peekBuffer, - peekBufferPosition, - length, - /* bytesAlreadyRead= */ 0, - /* allowEndOfInput= */ true); - if (bytesPeeked == C.RESULT_END_OF_INPUT) { - return C.RESULT_END_OF_INPUT; - } - peekBufferLength += bytesPeeked; - } else { - bytesPeeked = Math.min(length, peekBufferRemainingBytes); - } - System.arraycopy(peekBuffer, peekBufferPosition, target, offset, bytesPeeked); - peekBufferPosition += bytesPeeked; - return bytesPeeked; - } - @Override public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput) throws IOException, InterruptedException { @@ -228,7 +201,7 @@ public final class DefaultExtractorInput implements ExtractorInput { } /** - * Reads from the peek buffer. + * Reads from the peek buffer * * @param target A target array into which data should be written. * @param offset The offset into the target array at which to write. 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 8e5d6f0448..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 @@ -27,19 +27,19 @@ import java.io.InputStream; * for more info about each mode. * *
      - *
    • The {@code read()/peek()} and {@code skip()} methods provide {@link InputStream}-like - * byte-level access operations. + *
    • 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()/peek()} 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. + *

    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

    * @@ -102,8 +102,7 @@ public interface ExtractorInput { throws IOException, InterruptedException; /** - * Equivalent to {@link #readFully(byte[], int, int, boolean) readFully(target, offset, length, - * false)}. + * Equivalent to {@code readFully(target, offset, length, false)}. * * @param target A target array into which data should be written. * @param offset The offset into the target array at which to write. @@ -156,11 +155,8 @@ public interface ExtractorInput { void skipFully(int length) throws IOException, InterruptedException; /** - * Peeks up to {@code length} bytes from the peek position. The current read position is left - * unchanged. - * - *

    This method blocks until at least one byte of data can be peeked, the end of the input is - * detected, or an exception is thrown. + * Peeks {@code length} bytes from the peek position, writing them into {@code target} at index + * {@code offset}. The current read position is left unchanged. * *

    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 @@ -168,18 +164,6 @@ public interface ExtractorInput { * * @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 maximum number of bytes to peek from the input. - * @return The number of bytes peeked, or {@link C#RESULT_END_OF_INPUT} if the input has ended. - * @throws IOException If an error occurs peeking from the input. - * @throws InterruptedException If the thread has been interrupted. - */ - int peek(byte[] target, int offset, int length) throws IOException, InterruptedException; - - /** - * Like {@link #peek(byte[], int, int)}, but peeks the requested {@code length} in full. - * - * @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 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 @@ -197,8 +181,12 @@ public interface ExtractorInput { throws IOException, InterruptedException; /** - * Equivalent to {@link #peekFully(byte[], int, int, boolean) peekFully(target, offset, length, - * false)}. + * Peeks {@code length} bytes from the peek position, writing them into {@code target} at index + * {@code offset}. The current read position is left unchanged. + *

    + * Calling {@link #resetPeekPosition()} resets the peek position to equal the current read + * position, so the caller can peek the same data again. Reading and skipping also reset the peek + * position. * * @param target A target array into which data should be written. * @param offset The offset into the target array at which to write. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java index ccc806fe61..6dbec3ecf4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java @@ -49,7 +49,7 @@ public class DefaultExtractorInputTest { } @Test - public void testReadMultipleTimes() throws Exception { + public void testRead() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; // We expect to perform three reads of three bytes, as setup in buildTestDataSource. @@ -60,70 +60,39 @@ public class DefaultExtractorInputTest { assertThat(bytesRead).isEqualTo(6); bytesRead += input.read(target, 6, TEST_DATA.length); assertThat(bytesRead).isEqualTo(9); - assertThat(input.getPosition()).isEqualTo(9); - assertThat(TEST_DATA).isEqualTo(target); + // Check the read data is correct. + assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + // Check we're now indicated that the end of input is reached. + int expectedEndOfInput = input.read(target, 0, TEST_DATA.length); + assertThat(expectedEndOfInput).isEqualTo(RESULT_END_OF_INPUT); } @Test - public void testReadAlreadyPeeked() throws Exception { + public void testReadPeeked() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; input.advancePeekPosition(TEST_DATA.length); - int bytesRead = input.read(target, 0, TEST_DATA.length - 1); - assertThat(bytesRead).isEqualTo(TEST_DATA.length - 1); - assertThat(input.getPosition()).isEqualTo(TEST_DATA.length - 1); - assertThat(Arrays.copyOf(TEST_DATA, TEST_DATA.length - 1)) - .isEqualTo(Arrays.copyOf(target, TEST_DATA.length - 1)); - } - - @Test - public void testReadPartiallyPeeked() throws Exception { - DefaultExtractorInput input = createDefaultExtractorInput(); - byte[] target = new byte[TEST_DATA.length]; - - input.advancePeekPosition(TEST_DATA.length - 1); int bytesRead = input.read(target, 0, TEST_DATA.length); + assertThat(bytesRead).isEqualTo(TEST_DATA.length); - assertThat(bytesRead).isEqualTo(TEST_DATA.length - 1); - assertThat(input.getPosition()).isEqualTo(TEST_DATA.length - 1); - assertThat(Arrays.copyOf(TEST_DATA, TEST_DATA.length - 1)) - .isEqualTo(Arrays.copyOf(target, TEST_DATA.length - 1)); + // Check the read data is correct. + assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); } @Test - public void testReadEndOfInputBeforeFirstByteRead() throws Exception { + public void testReadMoreDataPeeked() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; - input.skipFully(TEST_DATA.length); - int bytesRead = input.read(target, 0, TEST_DATA.length); + input.advancePeekPosition(TEST_DATA.length); - assertThat(bytesRead).isEqualTo(RESULT_END_OF_INPUT); - assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); - } + int bytesRead = input.read(target, 0, TEST_DATA.length + 1); + assertThat(bytesRead).isEqualTo(TEST_DATA.length); - @Test - public void testReadEndOfInputAfterFirstByteRead() throws Exception { - DefaultExtractorInput input = createDefaultExtractorInput(); - byte[] target = new byte[TEST_DATA.length]; - - input.skipFully(TEST_DATA.length - 1); - int bytesRead = input.read(target, 0, TEST_DATA.length); - - assertThat(bytesRead).isEqualTo(1); - assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); - } - - @Test - public void testReadZeroLength() throws Exception { - DefaultExtractorInput input = createDefaultExtractorInput(); - byte[] target = new byte[TEST_DATA.length]; - - int bytesRead = input.read(target, /* offset= */ 0, /* length= */ 0); - - assertThat(bytesRead).isEqualTo(0); + // Check the read data is correct. + assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); } @Test @@ -132,7 +101,7 @@ public class DefaultExtractorInputTest { byte[] target = new byte[TEST_DATA.length]; input.readFully(target, 0, TEST_DATA.length); // Check that we read the whole of TEST_DATA. - assertThat(TEST_DATA).isEqualTo(target); + assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); // Check that we see end of input if we read again with allowEndOfInput set. boolean result = input.readFully(target, 0, 1, true); @@ -152,11 +121,11 @@ public class DefaultExtractorInputTest { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[5]; input.readFully(target, 0, 5); - assertThat(copyOf(TEST_DATA, 5)).isEqualTo(target); + assertThat(Arrays.equals(copyOf(TEST_DATA, 5), target)).isTrue(); assertThat(input.getPosition()).isEqualTo(5); target = new byte[4]; input.readFully(target, 0, 4); - assertThat(copyOfRange(TEST_DATA, 5, 9)).isEqualTo(target); + assertThat(Arrays.equals(copyOfRange(TEST_DATA, 5, 9), target)).isTrue(); assertThat(input.getPosition()).isEqualTo(5 + 4); } @@ -211,23 +180,27 @@ public class DefaultExtractorInputTest { input.readFully(target, 0, TEST_DATA.length); // Check the read data is correct. - assertThat(TEST_DATA).isEqualTo(target); + assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); } @Test - public void testSkipMultipleTimes() throws Exception { - DefaultExtractorInput input = createDefaultExtractorInput(); + public void testSkip() throws Exception { + FakeDataSource testDataSource = buildDataSource(); + DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); // We expect to perform three skips of three bytes, as setup in buildTestDataSource. for (int i = 0; i < 3; i++) { assertThat(input.skip(TEST_DATA.length)).isEqualTo(3); } - assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); + // Check we're now indicated that the end of input is reached. + int expectedEndOfInput = input.skip(TEST_DATA.length); + assertThat(expectedEndOfInput).isEqualTo(RESULT_END_OF_INPUT); } @Test public void testLargeSkip() throws Exception { - DefaultExtractorInput input = createDefaultExtractorInput(); + FakeDataSource testDataSource = buildLargeDataSource(); + DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); // Check that skipping the entire data source succeeds. int bytesToSkip = LARGE_TEST_DATA_LENGTH; while (bytesToSkip > 0) { @@ -235,59 +208,6 @@ public class DefaultExtractorInputTest { } } - @Test - public void testSkipAlreadyPeeked() throws Exception { - DefaultExtractorInput input = createDefaultExtractorInput(); - - input.advancePeekPosition(TEST_DATA.length); - int bytesSkipped = input.skip(TEST_DATA.length - 1); - - assertThat(bytesSkipped).isEqualTo(TEST_DATA.length - 1); - assertThat(input.getPosition()).isEqualTo(TEST_DATA.length - 1); - } - - @Test - public void testSkipPartiallyPeeked() throws Exception { - DefaultExtractorInput input = createDefaultExtractorInput(); - - input.advancePeekPosition(TEST_DATA.length - 1); - int bytesSkipped = input.skip(TEST_DATA.length); - - assertThat(bytesSkipped).isEqualTo(TEST_DATA.length - 1); - assertThat(input.getPosition()).isEqualTo(TEST_DATA.length - 1); - } - - @Test - public void testSkipEndOfInputBeforeFirstByteSkipped() throws Exception { - DefaultExtractorInput input = createDefaultExtractorInput(); - - input.skipFully(TEST_DATA.length); - int bytesSkipped = input.skip(TEST_DATA.length); - - assertThat(bytesSkipped).isEqualTo(RESULT_END_OF_INPUT); - assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); - } - - @Test - public void testSkipEndOfInputAfterFirstByteSkipped() throws Exception { - DefaultExtractorInput input = createDefaultExtractorInput(); - - input.skipFully(TEST_DATA.length - 1); - int bytesSkipped = input.skip(TEST_DATA.length); - - assertThat(bytesSkipped).isEqualTo(1); - assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); - } - - @Test - public void testSkipZeroLength() throws Exception { - DefaultExtractorInput input = createDefaultExtractorInput(); - - int bytesRead = input.skip(0); - - assertThat(bytesRead).isEqualTo(0); - } - @Test public void testSkipFullyOnce() throws Exception { // Skip TEST_DATA. @@ -389,86 +309,6 @@ public class DefaultExtractorInputTest { } } - @Test - public void testPeekMultipleTimes() throws Exception { - DefaultExtractorInput input = createDefaultExtractorInput(); - byte[] target = new byte[TEST_DATA.length]; - - // We expect to perform three peeks of three bytes, as setup in buildTestDataSource. - int bytesPeeked = 0; - bytesPeeked += input.peek(target, 0, TEST_DATA.length); - assertThat(bytesPeeked).isEqualTo(3); - bytesPeeked += input.peek(target, 3, TEST_DATA.length); - assertThat(bytesPeeked).isEqualTo(6); - bytesPeeked += input.peek(target, 6, TEST_DATA.length); - assertThat(bytesPeeked).isEqualTo(9); - assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); - assertThat(TEST_DATA).isEqualTo(target); - } - - @Test - public void testPeekAlreadyPeeked() throws Exception { - DefaultExtractorInput input = createDefaultExtractorInput(); - byte[] target = new byte[TEST_DATA.length]; - - input.advancePeekPosition(TEST_DATA.length); - input.resetPeekPosition(); - int bytesPeeked = input.peek(target, 0, TEST_DATA.length - 1); - - assertThat(bytesPeeked).isEqualTo(TEST_DATA.length - 1); - assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length - 1); - assertThat(Arrays.copyOf(TEST_DATA, TEST_DATA.length - 1)) - .isEqualTo(Arrays.copyOf(target, TEST_DATA.length - 1)); - } - - @Test - public void testPeekPartiallyPeeked() throws Exception { - DefaultExtractorInput input = createDefaultExtractorInput(); - byte[] target = new byte[TEST_DATA.length]; - - input.advancePeekPosition(TEST_DATA.length - 1); - input.resetPeekPosition(); - int bytesPeeked = input.peek(target, 0, TEST_DATA.length); - - assertThat(bytesPeeked).isEqualTo(TEST_DATA.length - 1); - assertThat(Arrays.copyOf(TEST_DATA, TEST_DATA.length - 1)) - .isEqualTo(Arrays.copyOf(target, TEST_DATA.length - 1)); - } - - @Test - public void testPeekEndOfInputBeforeFirstBytePeeked() throws Exception { - DefaultExtractorInput input = createDefaultExtractorInput(); - byte[] target = new byte[TEST_DATA.length]; - - input.advancePeekPosition(TEST_DATA.length); - int bytesPeeked = input.peek(target, 0, TEST_DATA.length); - - assertThat(bytesPeeked).isEqualTo(RESULT_END_OF_INPUT); - assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); - } - - @Test - public void testPeekEndOfInputAfterFirstBytePeeked() throws Exception { - DefaultExtractorInput input = createDefaultExtractorInput(); - byte[] target = new byte[TEST_DATA.length]; - - input.advancePeekPosition(TEST_DATA.length - 1); - int bytesPeeked = input.peek(target, 0, TEST_DATA.length); - - assertThat(bytesPeeked).isEqualTo(1); - assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); - } - - @Test - public void testPeekZeroLength() throws Exception { - DefaultExtractorInput input = createDefaultExtractorInput(); - byte[] target = new byte[TEST_DATA.length]; - - int bytesPeeked = input.peek(target, /* offset= */ 0, /* length= */ 0); - - assertThat(bytesPeeked).isEqualTo(0); - } - @Test public void testPeekFully() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); @@ -476,14 +316,14 @@ public class DefaultExtractorInputTest { input.peekFully(target, 0, TEST_DATA.length); // Check that we read the whole of TEST_DATA. - assertThat(TEST_DATA).isEqualTo(target); + assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); assertThat(input.getPosition()).isEqualTo(0); assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); // Check that we can read again from the buffer byte[] target2 = new byte[TEST_DATA.length]; input.readFully(target2, 0, TEST_DATA.length); - assertThat(TEST_DATA).isEqualTo(target2); + assertThat(Arrays.equals(TEST_DATA, target2)).isTrue(); assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); @@ -510,7 +350,7 @@ public class DefaultExtractorInputTest { input.peekFully(target, /* offset= */ 0, /* length= */ TEST_DATA.length); assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); - assertThat(TEST_DATA).isEqualTo(Arrays.copyOf(target, TEST_DATA.length)); + assertThat(Arrays.equals(TEST_DATA, Arrays.copyOf(target, TEST_DATA.length))).isTrue(); } @Test @@ -520,14 +360,14 @@ public class DefaultExtractorInputTest { input.peekFully(target, 0, TEST_DATA.length); // Check that we read the whole of TEST_DATA. - assertThat(TEST_DATA).isEqualTo(target); + assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); assertThat(input.getPosition()).isEqualTo(0); // Check that we can peek again after resetting. input.resetPeekPosition(); byte[] target2 = new byte[TEST_DATA.length]; input.peekFully(target2, 0, TEST_DATA.length); - assertThat(TEST_DATA).isEqualTo(target2); + assertThat(Arrays.equals(TEST_DATA, target2)).isTrue(); // Check that we fail with EOFException if we peek past the end of the input. try { 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 7323cfd0fe..443ffdb12c 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 @@ -65,8 +65,7 @@ public final class FakeExtractorInput implements ExtractorInput { private int readPosition; private int peekPosition; - private final SparseBooleanArray partiallySatisfiedTargetReadPositions; - private final SparseBooleanArray partiallySatisfiedTargetPeekPositions; + private final SparseBooleanArray partiallySatisfiedTargetPositions; private final SparseBooleanArray failedReadPositions; private final SparseBooleanArray failedPeekPositions; @@ -76,8 +75,7 @@ public final class FakeExtractorInput implements ExtractorInput { this.simulateUnknownLength = simulateUnknownLength; this.simulatePartialReads = simulatePartialReads; this.simulateIOErrors = simulateIOErrors; - partiallySatisfiedTargetReadPositions = new SparseBooleanArray(); - partiallySatisfiedTargetPeekPositions = new SparseBooleanArray(); + partiallySatisfiedTargetPositions = new SparseBooleanArray(); failedReadPositions = new SparseBooleanArray(); failedPeekPositions = new SparseBooleanArray(); } @@ -86,8 +84,7 @@ public final class FakeExtractorInput implements ExtractorInput { public void reset() { readPosition = 0; peekPosition = 0; - partiallySatisfiedTargetReadPositions.clear(); - partiallySatisfiedTargetPeekPositions.clear(); + partiallySatisfiedTargetPositions.clear(); failedReadPositions.clear(); failedPeekPositions.clear(); } @@ -107,7 +104,7 @@ public final class FakeExtractorInput implements ExtractorInput { @Override public int read(byte[] target, int offset, int length) throws IOException { checkIOException(readPosition, failedReadPositions); - length = getLengthToRead(readPosition, length, partiallySatisfiedTargetReadPositions); + length = getReadLength(length); return readFullyInternal(target, offset, length, true) ? length : C.RESULT_END_OF_INPUT; } @@ -126,7 +123,7 @@ public final class FakeExtractorInput implements ExtractorInput { @Override public int skip(int length) throws IOException { checkIOException(readPosition, failedReadPositions); - length = getLengthToRead(readPosition, length, partiallySatisfiedTargetReadPositions); + length = getReadLength(length); return skipFullyInternal(length, true) ? length : C.RESULT_END_OF_INPUT; } @@ -141,18 +138,16 @@ public final class FakeExtractorInput implements ExtractorInput { skipFully(length, false); } - @Override - public int peek(byte[] target, int offset, int length) throws IOException { - checkIOException(peekPosition, failedPeekPositions); - length = getLengthToRead(peekPosition, length, partiallySatisfiedTargetPeekPositions); - return peekFullyInternal(target, offset, length, true) ? length : C.RESULT_END_OF_INPUT; - } - @Override public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput) throws IOException { checkIOException(peekPosition, failedPeekPositions); - return peekFullyInternal(target, offset, length, allowEndOfInput); + if (!checkXFully(allowEndOfInput, peekPosition, length)) { + return false; + } + System.arraycopy(data, peekPosition, target, offset, length); + peekPosition += length; + return true; } @Override @@ -226,19 +221,18 @@ public final class FakeExtractorInput implements ExtractorInput { return true; } - private int getLengthToRead( - int position, int requestedLength, SparseBooleanArray partiallySatisfiedTargetPositions) { - if (position == data.length) { + private int getReadLength(int requestedLength) { + if (readPosition == data.length) { // If the requested length is non-zero, the end of the input will be read. return requestedLength == 0 ? 0 : Integer.MAX_VALUE; } - int targetPosition = position + requestedLength; + int targetPosition = readPosition + requestedLength; if (simulatePartialReads && requestedLength > 1 && !partiallySatisfiedTargetPositions.get(targetPosition)) { partiallySatisfiedTargetPositions.put(targetPosition, true); return 1; } - return Math.min(requestedLength, data.length - position); + return Math.min(requestedLength, data.length - readPosition); } private boolean readFullyInternal(byte[] target, int offset, int length, boolean allowEndOfInput) @@ -261,16 +255,6 @@ public final class FakeExtractorInput implements ExtractorInput { return true; } - private boolean peekFullyInternal(byte[] target, int offset, int length, boolean allowEndOfInput) - throws EOFException { - if (!checkXFully(allowEndOfInput, peekPosition, length)) { - return false; - } - System.arraycopy(data, peekPosition, target, offset, length); - peekPosition += length; - return true; - } - /** * Builder of {@link FakeExtractorInput} instances. */ From 002acc680b03411c46063ab42615493dac93a816 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 9 Dec 2019 14:34:12 +0000 Subject: [PATCH 0467/1335] 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 0b7f93a5d4f31a23dba88e14e3df842e3e41f260 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 9 Dec 2019 15:02:11 +0000 Subject: [PATCH 0468/1335] 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 79990a716d..adc42e5fd9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### dev-v2 (not yet released) ### +* Matroska: Support lacing in Blocks + ([#3026](https://github.com/google/ExoPlayer/issues/3026)). * Add Java FLAC extractor ([#6406](https://github.com/google/ExoPlayer/issues/6406)). This extractor does not support seeking and live streams. If 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 c90c10c981d8e553a76583654c26fff2e6acc84c Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 9 Dec 2019 15:22:11 +0000 Subject: [PATCH 0469/1335] Add NonNull annotations to upstream.cache and upstream.crypto PiperOrigin-RevId: 284548019 --- .../upstream/cache/CacheDataSink.java | 10 +++++---- .../upstream/cache/CacheDataSource.java | 4 ++-- .../cache/CacheFileMetadataIndex.java | 2 +- .../upstream/cache/CacheKeyFactory.java | 1 + .../exoplayer2/upstream/cache/CacheUtil.java | 4 ++-- .../upstream/cache/CachedContentIndex.java | 10 +++++---- .../upstream/cache/CachedRegionTracker.java | 11 +++++----- .../upstream/cache/ContentMetadata.java | 2 +- .../cache/ContentMetadataMutations.java | 8 +++---- .../cache/DefaultContentMetadata.java | 18 ++++++++-------- .../upstream/cache/SimpleCache.java | 21 ++++++++++--------- .../upstream/cache/SimpleCacheSpan.java | 20 ++++++++++-------- .../upstream/cache/package-info.java | 19 +++++++++++++++++ .../upstream/crypto/package-info.java | 19 +++++++++++++++++ 14 files changed, 97 insertions(+), 52 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/package-info.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index 22ed3892ec..89de5fb343 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream.cache; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSpec; @@ -27,6 +28,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Writes data into a cache. @@ -49,13 +51,13 @@ public final class CacheDataSink implements DataSink { private final long fragmentSize; private final int bufferSize; - private DataSpec dataSpec; + @Nullable private DataSpec dataSpec; private long dataSpecFragmentSize; - private File file; - private OutputStream outputStream; + @Nullable private File file; + @Nullable private OutputStream outputStream; private long outputStreamBytesWritten; private long dataSpecBytesWritten; - private ReusableBufferedOutputStream bufferedOutputStream; + @MonotonicNonNull private ReusableBufferedOutputStream bufferedOutputStream; /** * Thrown when IOException is encountered when writing data into sink. 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 541c3b2d9d..c51f75116f 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 @@ -377,7 +377,7 @@ public final class CacheDataSource implements DataSource { * reading from {@link #upstreamDataSource}, which is the currently open source. */ private void openNextSource(boolean checkCache) throws IOException { - CacheSpan nextSpan; + @Nullable CacheSpan nextSpan; if (currentRequestIgnoresCache) { nextSpan = null; } else if (blockOnCache) { @@ -487,7 +487,7 @@ public final class CacheDataSource implements DataSource { } private static Uri getRedirectedUriOrDefault(Cache cache, String key, Uri defaultUri) { - Uri redirectedUri = ContentMetadata.getRedirectedUri(cache.getContentMetadata(key)); + @Nullable Uri redirectedUri = ContentMetadata.getRedirectedUri(cache.getContentMetadata(key)); return redirectedUri != null ? redirectedUri : defaultUri; } 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..b2de05ca77 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 @@ -60,7 +60,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final DatabaseProvider databaseProvider; - private @MonotonicNonNull String tableName; + @MonotonicNonNull private String tableName; /** * Deletes index data for the specified cache. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java index bfa404c074..3401d6f575 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java @@ -24,6 +24,7 @@ public interface CacheKeyFactory { * Returns a cache key for the given {@link DataSpec}. * * @param dataSpec The data being cached. + * @return The cache key. */ String buildCacheKey(DataSpec dataSpec); } 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 ce16ea2439..fadffd9b95 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 @@ -183,7 +183,7 @@ public final class CacheUtil { String key = buildCacheKey(dataSpec, cacheKeyFactory); long bytesLeft; - ProgressNotifier progressNotifier = null; + @Nullable ProgressNotifier progressNotifier = null; if (progressListener != null) { progressNotifier = new ProgressNotifier(progressListener); Pair lengthAndBytesAlreadyCached = getCached(dataSpec, cache, cacheKeyFactory); @@ -373,7 +373,7 @@ public final class CacheUtil { } /* package */ static boolean isCausedByPositionOutOfRange(IOException e) { - Throwable cause = e; + @Nullable Throwable cause = e; while (cause != null) { if (cause instanceof DataSourceException) { int reason = ((DataSourceException) cause).reason; 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 7e09025ddd..1b9b4a3629 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 @@ -229,11 +229,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; * @return A new or existing CachedContent instance with the given key. */ public CachedContent getOrAdd(String key) { - CachedContent cachedContent = keyToContent.get(key); + @Nullable CachedContent cachedContent = keyToContent.get(key); return cachedContent == null ? addNew(key) : cachedContent; } /** Returns a CachedContent instance with the given key or null if there isn't one. */ + @Nullable public CachedContent get(String key) { return keyToContent.get(key); } @@ -254,14 +255,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return getOrAdd(key).id; } - /** Returns the key which has the given id assigned. */ + /** Returns the key which has the given id assigned, or {@code null} if no such key exists. */ + @Nullable public String getKeyForId(int id) { return idToKey.get(id); } /** Removes {@link CachedContent} with the given key from index if it's empty and not locked. */ public void maybeRemove(String key) { - CachedContent cachedContent = keyToContent.get(key); + @Nullable CachedContent cachedContent = keyToContent.get(key); if (cachedContent != null && cachedContent.isEmpty() && !cachedContent.isLocked()) { keyToContent.remove(key); int id = cachedContent.id; @@ -626,7 +628,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } private void writeFile(HashMap content) throws IOException { - DataOutputStream output = null; + @Nullable DataOutputStream output = null; try { OutputStream outputStream = atomicFile.startWrite(); if (bufferedOutputStream == null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java index fb2d4f694f..15a827ba74 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream.cache; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; @@ -77,7 +78,7 @@ public final class CachedRegionTracker implements Cache.Listener { */ public synchronized int getRegionEndTimeMs(long byteOffset) { lookupRegion.startOffset = byteOffset; - Region floorRegion = regions.floor(lookupRegion); + @Nullable Region floorRegion = regions.floor(lookupRegion); if (floorRegion == null || byteOffset > floorRegion.endOffset || floorRegion.endOffsetIndex == -1) { return NOT_CACHED; @@ -102,7 +103,7 @@ public final class CachedRegionTracker implements Cache.Listener { Region removedRegion = new Region(span.position, span.position + span.length); // Look up a region this span falls into. - Region floorRegion = regions.floor(removedRegion); + @Nullable Region floorRegion = regions.floor(removedRegion); if (floorRegion == null) { Log.e(TAG, "Removed a span we were not aware of"); return; @@ -134,8 +135,8 @@ public final class CachedRegionTracker implements Cache.Listener { private void mergeSpan(CacheSpan span) { Region newRegion = new Region(span.position, span.position + span.length); - Region floorRegion = regions.floor(newRegion); - Region ceilingRegion = regions.ceiling(newRegion); + @Nullable Region floorRegion = regions.floor(newRegion); + @Nullable Region ceilingRegion = regions.ceiling(newRegion); boolean floorConnects = regionsConnect(floorRegion, newRegion); boolean ceilingConnects = regionsConnect(newRegion, ceilingRegion); @@ -168,7 +169,7 @@ public final class CachedRegionTracker implements Cache.Listener { } } - private boolean regionsConnect(Region lower, Region upper) { + private boolean regionsConnect(@Nullable Region lower, @Nullable Region upper) { return lower != null && upper != null && lower.endOffset == upper.startOffset; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java index 4cc6e6b860..26b6d83a43 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java @@ -81,7 +81,7 @@ public interface ContentMetadata { */ @Nullable static Uri getRedirectedUri(ContentMetadata contentMetadata) { - String redirectedUri = contentMetadata.get(KEY_REDIRECTED_URI, (String) null); + @Nullable String redirectedUri = contentMetadata.get(KEY_REDIRECTED_URI, (String) null); return redirectedUri == null ? null : Uri.parse(redirectedUri); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java index 5715b8fbd4..f6cac58997 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java @@ -73,8 +73,7 @@ public class ContentMetadataMutations { } /** - * Adds a mutation to set a metadata value. Passing {@code null} as {@code name} or {@code value} - * isn't allowed. + * Adds a mutation to set a metadata value. * * @param name The name of the metadata value. * @param value The value to be set. @@ -85,7 +84,7 @@ public class ContentMetadataMutations { } /** - * Adds a mutation to set a metadata value. Passing {@code null} as {@code name} isn't allowed. + * Adds a mutation to set a metadata value. * * @param name The name of the metadata value. * @param value The value to be set. @@ -96,8 +95,7 @@ public class ContentMetadataMutations { } /** - * Adds a mutation to set a metadata value. Passing {@code null} as {@code name} or {@code value} - * isn't allowed. + * Adds a mutation to set a metadata value. * * @param name The name of the metadata value. * @param value The value to be set. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java index 1f07af938a..c3f06252e4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java @@ -67,8 +67,8 @@ public final class DefaultContentMetadata implements ContentMetadata { @Override @Nullable public final byte[] get(String name, @Nullable byte[] defaultValue) { - if (metadata.containsKey(name)) { - byte[] bytes = metadata.get(name); + @Nullable byte[] bytes = metadata.get(name); + if (bytes != null) { return Arrays.copyOf(bytes, bytes.length); } else { return defaultValue; @@ -78,8 +78,8 @@ public final class DefaultContentMetadata implements ContentMetadata { @Override @Nullable public final String get(String name, @Nullable String defaultValue) { - if (metadata.containsKey(name)) { - byte[] bytes = metadata.get(name); + @Nullable byte[] bytes = metadata.get(name); + if (bytes != null) { return new String(bytes, Charset.forName(C.UTF8_NAME)); } else { return defaultValue; @@ -88,8 +88,8 @@ public final class DefaultContentMetadata implements ContentMetadata { @Override public final long get(String name, long defaultValue) { - if (metadata.containsKey(name)) { - byte[] bytes = metadata.get(name); + @Nullable byte[] bytes = metadata.get(name); + if (bytes != null) { return ByteBuffer.wrap(bytes).getLong(); } else { return defaultValue; @@ -130,7 +130,7 @@ public final class DefaultContentMetadata implements ContentMetadata { } for (Entry entry : first.entrySet()) { byte[] value = entry.getValue(); - byte[] otherValue = second.get(entry.getKey()); + @Nullable byte[] otherValue = second.get(entry.getKey()); if (!Arrays.equals(value, otherValue)) { return false; } @@ -153,8 +153,8 @@ public final class DefaultContentMetadata implements ContentMetadata { } private static void addValues(HashMap metadata, Map values) { - for (String name : values.keySet()) { - metadata.put(name, getBytes(values.get(name))); + for (Entry entry : values.entrySet()) { + metadata.put(entry.getKey(), getBytes(entry.getValue())); } } 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 a4fade25e0..5f420c3197 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 @@ -465,8 +465,7 @@ public final class SimpleCache implements Cache { @Override public synchronized void releaseHoleSpan(CacheSpan holeSpan) { Assertions.checkState(!released); - CachedContent cachedContent = contentIndex.get(holeSpan.key); - Assertions.checkNotNull(cachedContent); + CachedContent cachedContent = Assertions.checkNotNull(contentIndex.get(holeSpan.key)); Assertions.checkState(cachedContent.isLocked()); cachedContent.setLocked(false); contentIndex.maybeRemove(cachedContent.key); @@ -482,14 +481,14 @@ public final class SimpleCache implements Cache { @Override public synchronized boolean isCached(String key, long position, long length) { Assertions.checkState(!released); - CachedContent cachedContent = contentIndex.get(key); + @Nullable CachedContent cachedContent = contentIndex.get(key); return cachedContent != null && cachedContent.getCachedBytesLength(position, length) >= length; } @Override public synchronized long getCachedLength(String key, long position, long length) { Assertions.checkState(!released); - CachedContent cachedContent = contentIndex.get(key); + @Nullable CachedContent cachedContent = contentIndex.get(key); return cachedContent != null ? cachedContent.getCachedBytesLength(position, length) : -length; } @@ -524,7 +523,7 @@ public final class SimpleCache implements Cache { } } - File[] files = cacheDir.listFiles(); + @Nullable File[] files = cacheDir.listFiles(); if (files == null) { String message = "Failed to list cache directory files: " + cacheDir; Log.e(TAG, message); @@ -605,11 +604,13 @@ public final class SimpleCache implements Cache { } long length = C.LENGTH_UNSET; long lastTouchTimestamp = C.TIME_UNSET; + @Nullable CacheFileMetadata metadata = fileMetadata != null ? fileMetadata.remove(fileName) : null; if (metadata != null) { length = metadata.length; lastTouchTimestamp = metadata.lastTouchTimestamp; } + @Nullable SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(file, length, lastTouchTimestamp, contentIndex); if (span != null) { @@ -666,7 +667,7 @@ public final class SimpleCache implements Cache { * @return The corresponding cache {@link SimpleCacheSpan}. */ private SimpleCacheSpan getSpan(String key, long position) { - CachedContent cachedContent = contentIndex.get(key); + @Nullable CachedContent cachedContent = contentIndex.get(key); if (cachedContent == null) { return SimpleCacheSpan.createOpenHole(key, position); } @@ -694,7 +695,7 @@ public final class SimpleCache implements Cache { } private void removeSpanInternal(CacheSpan span) { - CachedContent cachedContent = contentIndex.get(span.key); + @Nullable CachedContent cachedContent = contentIndex.get(span.key); if (cachedContent == null || !cachedContent.removeSpan(span)) { return; } @@ -732,7 +733,7 @@ public final class SimpleCache implements Cache { } private void notifySpanRemoved(CacheSpan span) { - ArrayList keyListeners = listeners.get(span.key); + @Nullable ArrayList keyListeners = listeners.get(span.key); if (keyListeners != null) { for (int i = keyListeners.size() - 1; i >= 0; i--) { keyListeners.get(i).onSpanRemoved(this, span); @@ -742,7 +743,7 @@ public final class SimpleCache implements Cache { } private void notifySpanAdded(SimpleCacheSpan span) { - ArrayList keyListeners = listeners.get(span.key); + @Nullable ArrayList keyListeners = listeners.get(span.key); if (keyListeners != null) { for (int i = keyListeners.size() - 1; i >= 0; i--) { keyListeners.get(i).onSpanAdded(this, span); @@ -752,7 +753,7 @@ public final class SimpleCache implements Cache { } private void notifySpanTouched(SimpleCacheSpan oldSpan, CacheSpan newSpan) { - ArrayList keyListeners = listeners.get(oldSpan.key); + @Nullable ArrayList keyListeners = listeners.get(oldSpan.key); if (keyListeners != null) { for (int i = keyListeners.size() - 1; i >= 0; i--) { keyListeners.get(i).onSpanTouched(this, oldSpan, newSpan); 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 5f6ea338e6..ce195baf4e 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 @@ -91,6 +91,7 @@ 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 index The cached content index. * @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. */ @@ -108,6 +109,7 @@ import java.util.regex.Pattern; * that already know the length of the file should pass it explicitly. * @param lastTouchTimestamp The last touch timestamp, or {@link C#TIME_UNSET} to use the file * timestamp. + * @param index The cached content index. * @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. */ @@ -130,7 +132,7 @@ import java.util.regex.Pattern; } int id = Integer.parseInt(matcher.group(1)); - String key = index.getKeyForId(id); + @Nullable String key = index.getKeyForId(id); if (key == null) { return null; } @@ -153,26 +155,26 @@ import java.util.regex.Pattern; * Upgrades the cache file if it is created by an earlier version of {@link SimpleCache}. * * @param file The cache file. - * @param index Cached content index. + * @param index The cached content index. * @return Upgraded cache file or {@code null} if the file name is not correctly formatted or the * file can not be renamed. */ @Nullable private static File upgradeFile(File file, CachedContentIndex index) { - String key; + @Nullable String key = null; String filename = file.getName(); Matcher matcher = CACHE_FILE_PATTERN_V2.matcher(filename); if (matcher.matches()) { key = Util.unescapeFileName(matcher.group(1)); - if (key == null) { - return null; - } } else { matcher = CACHE_FILE_PATTERN_V1.matcher(filename); - if (!matcher.matches()) { - return null; + if (matcher.matches()) { + key = matcher.group(1); // Keys were not escaped in version 1. } - key = matcher.group(1); // Keys were not escaped in version 1. + } + + if (key == null) { + return null; } File newCacheFile = diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/package-info.java new file mode 100644 index 0000000000..bb6cf77458 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/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.upstream.cache; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/package-info.java new file mode 100644 index 0000000000..9c4005e815 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/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.upstream.crypto; + +import com.google.android.exoplayer2.util.NonNullApi; From 4b281cec3b4d359cad718dd1e24266acfb345f22 Mon Sep 17 00:00:00 2001 From: samrobinson Date: Mon, 9 Dec 2019 15:55:05 +0000 Subject: [PATCH 0470/1335] Clean `WakeLockManager.updateWakeLock` logic. PiperOrigin-RevId: 284552723 --- .../java/com/google/android/exoplayer2/WakeLockManager.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/WakeLockManager.java b/library/core/src/main/java/com/google/android/exoplayer2/WakeLockManager.java index f498eea6f4..1e718136e8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/WakeLockManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/WakeLockManager.java @@ -88,11 +88,9 @@ import com.google.android.exoplayer2.util.Log; private void updateWakeLock() { // Needed for the library nullness check. If enabled is true, the wakelock will not be null. if (wakeLock != null) { - if (enabled) { - if (stayAwake && !wakeLock.isHeld()) { + if (enabled && stayAwake) { + if (!wakeLock.isHeld()) { wakeLock.acquire(); - } else if (!stayAwake && wakeLock.isHeld()) { - wakeLock.release(); } } else if (wakeLock.isHeld()) { wakeLock.release(); From 567f2a6575a9c4e92762be9d411fbe49b902ac80 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 9 Dec 2019 09:56:50 +0000 Subject: [PATCH 0471/1335] 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 0472/1335] 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 0473/1335] 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 539a1ac2e23986aa36c98a7d17cea14bf1b230c6 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 9 Dec 2019 16:32:22 +0000 Subject: [PATCH 0474/1335] Partial nullness annotations for AtomParsers PiperOrigin-RevId: 284558886 --- .../exoplayer2/extractor/mp4/AtomParsers.java | 149 +++++++++++------- 1 file changed, 94 insertions(+), 55 deletions(-) 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 bf05424b7f..919fd80b06 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 @@ -40,6 +40,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */ @SuppressWarnings({"ConstantField"}) @@ -85,15 +86,21 @@ import java.util.List; * * @param trak Atom to decode. * @param mvhd Movie header atom, used to get the timescale. - * @param duration The duration in units of the timescale declared in the mvhd atom, or - * {@link C#TIME_UNSET} if the duration should be parsed from the tkhd atom. - * @param drmInitData {@link DrmInitData} to be included in the format. + * @param duration The duration in units of the timescale declared in the mvhd atom, or {@link + * C#TIME_UNSET} if the duration should be parsed from the tkhd atom. + * @param drmInitData {@link DrmInitData} to be included in the format, or {@code null}. * @param ignoreEditLists Whether to ignore any edit lists in the trak box. * @param isQuickTime True for QuickTime media. False otherwise. * @return A {@link Track} instance, or {@code null} if the track's type isn't supported. */ - public static Track parseTrak(Atom.ContainerAtom trak, Atom.LeafAtom mvhd, long duration, - DrmInitData drmInitData, boolean ignoreEditLists, boolean isQuickTime) + @Nullable + public static Track parseTrak( + Atom.ContainerAtom trak, + Atom.LeafAtom mvhd, + long duration, + @Nullable DrmInitData drmInitData, + boolean ignoreEditLists, + boolean isQuickTime) throws ParserException { Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia); int trackType = getTrackTypeForHdlr(parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data)); @@ -121,9 +128,12 @@ import java.util.List; long[] editListDurations = null; long[] editListMediaTimes = null; if (!ignoreEditLists) { + @Nullable Pair edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts)); - editListDurations = edtsData.first; - editListMediaTimes = edtsData.second; + if (edtsData != null) { + editListDurations = edtsData.first; + editListMediaTimes = edtsData.second; + } } return stsdData.format == null ? null : new Track(tkhdData.id, trackType, mdhdData.first, movieTimescale, durationUs, @@ -736,19 +746,25 @@ import java.util.List; * @param trackId The track's identifier in its container. * @param rotationDegrees The rotation of the track in degrees. * @param language The language of the track. - * @param drmInitData {@link DrmInitData} to be included in the format. + * @param drmInitData {@link DrmInitData} to be included in the format, or {@code null}. * @param isQuickTime True for QuickTime media. False otherwise. * @return An object containing the parsed data. */ - private static StsdData parseStsd(ParsableByteArray stsd, int trackId, int rotationDegrees, - String language, DrmInitData drmInitData, boolean isQuickTime) throws ParserException { + private static StsdData parseStsd( + ParsableByteArray stsd, + int trackId, + int rotationDegrees, + String language, + @Nullable DrmInitData drmInitData, + boolean isQuickTime) + throws ParserException { stsd.setPosition(Atom.FULL_HEADER_SIZE); int numberOfEntries = stsd.readInt(); StsdData out = new StsdData(numberOfEntries); for (int i = 0; i < numberOfEntries; i++) { int childStartPosition = stsd.getPosition(); int childAtomSize = stsd.readInt(); - Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + Assertions.checkState(childAtomSize > 0, "childAtomSize should be positive"); int childAtomType = stsd.readInt(); if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_avc3 @@ -846,9 +862,17 @@ import java.util.List; initializationData); } - private static void parseVideoSampleEntry(ParsableByteArray parent, int atomType, int position, - int size, int trackId, int rotationDegrees, DrmInitData drmInitData, StsdData out, - int entryIndex) throws ParserException { + private static void parseVideoSampleEntry( + ParsableByteArray parent, + int atomType, + int position, + int size, + int trackId, + int rotationDegrees, + @Nullable DrmInitData drmInitData, + StsdData out, + int entryIndex) + throws ParserException { parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); parent.skipBytes(16); @@ -860,8 +884,9 @@ import java.util.List; int childPosition = parent.getPosition(); if (atomType == Atom.TYPE_encv) { - Pair sampleEntryEncryptionData = parseSampleEntryEncryptionData( - parent, position, size); + @Nullable + Pair sampleEntryEncryptionData = + parseSampleEntryEncryptionData(parent, position, size); if (sampleEntryEncryptionData != null) { atomType = sampleEntryEncryptionData.first; drmInitData = drmInitData == null ? null @@ -875,10 +900,10 @@ import java.util.List; // drmInitData = null; // } - List initializationData = null; - String mimeType = null; - String codecs = null; - byte[] projectionData = null; + @Nullable List initializationData = null; + @Nullable String mimeType = null; + @Nullable String codecs = null; + @Nullable byte[] projectionData = null; @C.StereoMode int stereoMode = Format.NO_VALUE; while (childPosition - position < size) { @@ -889,7 +914,7 @@ import java.util.List; // Handle optional terminating four zero bytes in MOV files. break; } - Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + Assertions.checkState(childAtomSize > 0, "childAtomSize should be positive"); int childAtomType = parent.readInt(); if (childAtomType == Atom.TYPE_avcC) { Assertions.checkState(mimeType == null); @@ -925,10 +950,13 @@ import java.util.List; mimeType = MimeTypes.VIDEO_H263; } else if (childAtomType == Atom.TYPE_esds) { Assertions.checkState(mimeType == null); - Pair mimeTypeAndInitializationData = + Pair<@NullableType String, byte @NullableType []> mimeTypeAndInitializationDataBytes = parseEsdsFromParent(parent, childStartPosition); - mimeType = mimeTypeAndInitializationData.first; - initializationData = Collections.singletonList(mimeTypeAndInitializationData.second); + mimeType = mimeTypeAndInitializationDataBytes.first; + @Nullable byte[] initializationDataBytes = mimeTypeAndInitializationDataBytes.second; + if (initializationDataBytes != null) { + initializationData = Collections.singletonList(initializationDataBytes); + } } else if (childAtomType == Atom.TYPE_pasp) { pixelWidthHeightRatio = parsePaspFromParent(parent, childStartPosition); pixelWidthHeightRatioFromPasp = true; @@ -988,13 +1016,14 @@ import java.util.List; * Parses the edts atom (defined in 14496-12 subsection 8.6.5). * * @param edtsAtom edts (edit box) atom to decode. - * @return Pair of edit list durations and edit list media times, or a pair of nulls if they are - * not present. + * @return Pair of edit list durations and edit list media times, or {@code null} if they are not + * present. */ + @Nullable private static Pair parseEdts(Atom.ContainerAtom edtsAtom) { Atom.LeafAtom elst; if (edtsAtom == null || (elst = edtsAtom.getLeafAtomOfType(Atom.TYPE_elst)) == null) { - return Pair.create(null, null); + return null; } ParsableByteArray elstData = elst.data; elstData.setPosition(Atom.HEADER_SIZE); @@ -1024,9 +1053,18 @@ import java.util.List; return (float) hSpacing / vSpacing; } - private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType, int position, - int size, int trackId, String language, boolean isQuickTime, DrmInitData drmInitData, - StsdData out, int entryIndex) throws ParserException { + private static void parseAudioSampleEntry( + ParsableByteArray parent, + int atomType, + int position, + int size, + int trackId, + String language, + boolean isQuickTime, + @Nullable DrmInitData drmInitData, + StsdData out, + int entryIndex) + throws ParserException { parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); int quickTimeSoundDescriptionVersion = 0; @@ -1064,8 +1102,9 @@ import java.util.List; int childPosition = parent.getPosition(); if (atomType == Atom.TYPE_enca) { - Pair sampleEntryEncryptionData = parseSampleEntryEncryptionData( - parent, position, size); + @Nullable + Pair sampleEntryEncryptionData = + parseSampleEntryEncryptionData(parent, position, size); if (sampleEntryEncryptionData != null) { atomType = sampleEntryEncryptionData.first; drmInitData = drmInitData == null ? null @@ -1080,7 +1119,7 @@ import java.util.List; // } // If the atom type determines a MIME type, set it immediately. - String mimeType = null; + @Nullable String mimeType = null; if (atomType == Atom.TYPE_ac_3) { mimeType = MimeTypes.AUDIO_AC3; } else if (atomType == Atom.TYPE_ec_3) { @@ -1113,21 +1152,21 @@ import java.util.List; mimeType = MimeTypes.AUDIO_FLAC; } - byte[] initializationData = null; + @Nullable byte[] initializationData = null; while (childPosition - position < size) { parent.setPosition(childPosition); int childAtomSize = parent.readInt(); - Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + Assertions.checkState(childAtomSize > 0, "childAtomSize should be positive"); int childAtomType = parent.readInt(); if (childAtomType == Atom.TYPE_esds || (isQuickTime && childAtomType == Atom.TYPE_wave)) { int esdsAtomPosition = childAtomType == Atom.TYPE_esds ? childPosition : findEsdsPosition(parent, childPosition, childAtomSize); if (esdsAtomPosition != C.POSITION_UNSET) { - Pair mimeTypeAndInitializationData = + Pair<@NullableType String, byte @NullableType []> mimeTypeAndInitializationData = parseEsdsFromParent(parent, esdsAtomPosition); mimeType = mimeTypeAndInitializationData.first; initializationData = mimeTypeAndInitializationData.second; - if (MimeTypes.AUDIO_AAC.equals(mimeType)) { + if (MimeTypes.AUDIO_AAC.equals(mimeType) && initializationData != null) { // Update sampleRate and channelCount from the AudioSpecificConfig initialization data, // which is more reliable. See [Internal: b/10903778]. Pair audioSpecificConfig = @@ -1204,7 +1243,7 @@ import java.util.List; while (childAtomPosition - position < size) { parent.setPosition(childAtomPosition); int childAtomSize = parent.readInt(); - Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + Assertions.checkState(childAtomSize > 0, "childAtomSize should be positive"); int childType = parent.readInt(); if (childType == Atom.TYPE_esds) { return childAtomPosition; @@ -1214,10 +1253,9 @@ import java.util.List; return C.POSITION_UNSET; } - /** - * Returns codec-specific initialization data contained in an esds box. - */ - private static Pair parseEsdsFromParent(ParsableByteArray parent, int position) { + /** Returns codec-specific initialization data contained in an esds box. */ + private static Pair<@NullableType String, byte @NullableType []> parseEsdsFromParent( + ParsableByteArray parent, int position) { parent.setPosition(position + Atom.HEADER_SIZE + 4); // Start of the ES_Descriptor (defined in 14496-1) parent.skipBytes(1); // ES_Descriptor tag @@ -1263,13 +1301,14 @@ import java.util.List; * unencrypted atom type and a {@link TrackEncryptionBox}. Null is returned if no common * encryption sinf atom was present. */ + @Nullable private static Pair parseSampleEntryEncryptionData( ParsableByteArray parent, int position, int size) { int childPosition = parent.getPosition(); while (childPosition - position < size) { parent.setPosition(childPosition); int childAtomSize = parent.readInt(); - Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + Assertions.checkState(childAtomSize > 0, "childAtomSize should be positive"); int childAtomType = parent.readInt(); if (childAtomType == Atom.TYPE_sinf) { Pair result = parseCommonEncryptionSinfFromParent(parent, @@ -1283,6 +1322,7 @@ import java.util.List; return null; } + @Nullable /* package */ static Pair parseCommonEncryptionSinfFromParent( ParsableByteArray parent, int position, int size) { int childPosition = position + Atom.HEADER_SIZE; @@ -1309,20 +1349,21 @@ import java.util.List; if (C.CENC_TYPE_cenc.equals(schemeType) || C.CENC_TYPE_cbc1.equals(schemeType) || C.CENC_TYPE_cens.equals(schemeType) || C.CENC_TYPE_cbcs.equals(schemeType)) { - Assertions.checkArgument(dataFormat != null, "frma atom is mandatory"); - Assertions.checkArgument(schemeInformationBoxPosition != C.POSITION_UNSET, - "schi atom is mandatory"); + Assertions.checkStateNotNull(dataFormat, "frma atom is mandatory"); + Assertions.checkState( + schemeInformationBoxPosition != C.POSITION_UNSET, "schi atom is mandatory"); TrackEncryptionBox encryptionBox = parseSchiFromParent(parent, schemeInformationBoxPosition, schemeInformationBoxSize, schemeType); - Assertions.checkArgument(encryptionBox != null, "tenc atom is mandatory"); + Assertions.checkStateNotNull(encryptionBox, "tenc atom is mandatory"); return Pair.create(dataFormat, encryptionBox); } else { return null; } } - private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position, - int size, String schemeType) { + @Nullable + private static TrackEncryptionBox parseSchiFromParent( + ParsableByteArray parent, int position, int size, String schemeType) { int childPosition = position + Atom.HEADER_SIZE; while (childPosition - position < size) { parent.setPosition(childPosition); @@ -1359,9 +1400,8 @@ import java.util.List; return null; } - /** - * Parses the proj box from sv3d box, as specified by https://github.com/google/spatial-media. - */ + /** Parses the proj box from sv3d box, as specified by https://github.com/google/spatial-media. */ + @Nullable private static byte[] parseProjFromParent(ParsableByteArray parent, int position, int size) { int childPosition = position + Atom.HEADER_SIZE; while (childPosition - position < size) { @@ -1477,10 +1517,9 @@ import java.util.List; public final TrackEncryptionBox[] trackEncryptionBoxes; - public Format format; + @Nullable public Format format; public int nalUnitLengthFieldLength; - @Track.Transformation - public int requiredSampleTransformation; + @Track.Transformation public int requiredSampleTransformation; public StsdData(int numberOfEntries) { trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries]; From bcb51ec155be23aead455d6b09044f40b155db20 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 9 Dec 2019 18:01:01 +0000 Subject: [PATCH 0475/1335] Move matroska lacing into 2.11 PiperOrigin-RevId: 284576903 --- RELEASENOTES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index adc42e5fd9..714644878d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,8 +2,6 @@ ### dev-v2 (not yet released) ### -* Matroska: Support lacing in Blocks - ([#3026](https://github.com/google/ExoPlayer/issues/3026)). * Add Java FLAC extractor ([#6406](https://github.com/google/ExoPlayer/issues/6406)). This extractor does not support seeking and live streams. If @@ -140,6 +138,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/) From 70ba4b197c7a54d89c49b6959dc65ca616a92c2f Mon Sep 17 00:00:00 2001 From: kimvde Date: Mon, 9 Dec 2019 18:42:55 +0000 Subject: [PATCH 0476/1335] Add peek() method to ExtractorInput PiperOrigin-RevId: 284586799 --- .../extractor/DefaultExtractorInput.java | 31 ++- .../exoplayer2/extractor/ExtractorInput.java | 44 ++-- .../extractor/DefaultExtractorInputTest.java | 226 +++++++++++++++--- .../testutil/FakeExtractorInput.java | 46 ++-- 4 files changed, 281 insertions(+), 66 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java index 450cca42b0..c6f1129da8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java @@ -58,7 +58,9 @@ public final class DefaultExtractorInput implements ExtractorInput { public int read(byte[] target, int offset, int length) throws IOException, InterruptedException { int bytesRead = readFromPeekBuffer(target, offset, length); if (bytesRead == 0) { - bytesRead = readFromDataSource(target, offset, length, 0, true); + bytesRead = + readFromDataSource( + target, offset, length, /* bytesAlreadyRead= */ 0, /* allowEndOfInput= */ true); } commitBytesRead(bytesRead); return bytesRead; @@ -110,6 +112,31 @@ public final class DefaultExtractorInput implements ExtractorInput { skipFully(length, false); } + @Override + public int peek(byte[] target, int offset, int length) throws IOException, InterruptedException { + ensureSpaceForPeek(length); + int peekBufferRemainingBytes = peekBufferLength - peekBufferPosition; + int bytesPeeked; + if (peekBufferRemainingBytes == 0) { + bytesPeeked = + readFromDataSource( + peekBuffer, + peekBufferPosition, + length, + /* bytesAlreadyRead= */ 0, + /* allowEndOfInput= */ true); + if (bytesPeeked == C.RESULT_END_OF_INPUT) { + return C.RESULT_END_OF_INPUT; + } + peekBufferLength += bytesPeeked; + } else { + bytesPeeked = Math.min(length, peekBufferRemainingBytes); + } + System.arraycopy(peekBuffer, peekBufferPosition, target, offset, bytesPeeked); + peekBufferPosition += bytesPeeked; + return bytesPeeked; + } + @Override public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput) throws IOException, InterruptedException { @@ -201,7 +228,7 @@ public final class DefaultExtractorInput implements ExtractorInput { } /** - * Reads from the peek buffer + * Reads from the peek buffer. * * @param target A target array into which data should be written. * @param offset The offset into the target array at which to write. 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 461b059bad..8e5d6f0448 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 @@ -27,19 +27,19 @@ import java.io.InputStream; * for more info about each mode. * *

      - *
    • The {@code read()} and {@code skip()} methods provide {@link InputStream}-like byte-level - * access operations. + *
    • The {@code read()/peek()} 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. + *

    The {@code read()/peek()} 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

    * @@ -102,7 +102,8 @@ public interface ExtractorInput { throws IOException, InterruptedException; /** - * Equivalent to {@code readFully(target, offset, length, false)}. + * Equivalent to {@link #readFully(byte[], int, int, boolean) readFully(target, offset, length, + * false)}. * * @param target A target array into which data should be written. * @param offset The offset into the target array at which to write. @@ -155,8 +156,11 @@ public interface ExtractorInput { void skipFully(int length) throws IOException, InterruptedException; /** - * Peeks {@code length} bytes from the peek position, writing them into {@code target} at index - * {@code offset}. The current read position is left unchanged. + * Peeks up to {@code length} bytes from the peek position. The current read position is left + * unchanged. + * + *

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

    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 @@ -164,6 +168,18 @@ public interface ExtractorInput { * * @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 maximum number of bytes to peek from the input. + * @return The number of bytes peeked, or {@link C#RESULT_END_OF_INPUT} if the input has ended. + * @throws IOException If an error occurs peeking from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + int peek(byte[] target, int offset, int length) throws IOException, InterruptedException; + + /** + * Like {@link #peek(byte[], int, int)}, but peeks the requested {@code length} in full. + * + * @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 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 @@ -181,12 +197,8 @@ public interface ExtractorInput { throws IOException, InterruptedException; /** - * Peeks {@code length} bytes from the peek position, writing them into {@code target} at index - * {@code offset}. The current read position is left unchanged. - *

    - * Calling {@link #resetPeekPosition()} resets the peek position to equal the current read - * position, so the caller can peek the same data again. Reading and skipping also reset the peek - * position. + * Equivalent to {@link #peekFully(byte[], int, int, boolean) peekFully(target, offset, length, + * false)}. * * @param target A target array into which data should be written. * @param offset The offset into the target array at which to write. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java index 6dbec3ecf4..ccc806fe61 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java @@ -49,7 +49,7 @@ public class DefaultExtractorInputTest { } @Test - public void testRead() throws Exception { + public void testReadMultipleTimes() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; // We expect to perform three reads of three bytes, as setup in buildTestDataSource. @@ -60,39 +60,70 @@ public class DefaultExtractorInputTest { assertThat(bytesRead).isEqualTo(6); bytesRead += input.read(target, 6, TEST_DATA.length); assertThat(bytesRead).isEqualTo(9); - // Check the read data is correct. - assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); - // Check we're now indicated that the end of input is reached. - int expectedEndOfInput = input.read(target, 0, TEST_DATA.length); - assertThat(expectedEndOfInput).isEqualTo(RESULT_END_OF_INPUT); + assertThat(input.getPosition()).isEqualTo(9); + assertThat(TEST_DATA).isEqualTo(target); } @Test - public void testReadPeeked() throws Exception { + public void testReadAlreadyPeeked() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; input.advancePeekPosition(TEST_DATA.length); + int bytesRead = input.read(target, 0, TEST_DATA.length - 1); + assertThat(bytesRead).isEqualTo(TEST_DATA.length - 1); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length - 1); + assertThat(Arrays.copyOf(TEST_DATA, TEST_DATA.length - 1)) + .isEqualTo(Arrays.copyOf(target, TEST_DATA.length - 1)); + } + + @Test + public void testReadPartiallyPeeked() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + input.advancePeekPosition(TEST_DATA.length - 1); int bytesRead = input.read(target, 0, TEST_DATA.length); - assertThat(bytesRead).isEqualTo(TEST_DATA.length); - // Check the read data is correct. - assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(bytesRead).isEqualTo(TEST_DATA.length - 1); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length - 1); + assertThat(Arrays.copyOf(TEST_DATA, TEST_DATA.length - 1)) + .isEqualTo(Arrays.copyOf(target, TEST_DATA.length - 1)); } @Test - public void testReadMoreDataPeeked() throws Exception { + public void testReadEndOfInputBeforeFirstByteRead() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; - input.advancePeekPosition(TEST_DATA.length); + input.skipFully(TEST_DATA.length); + int bytesRead = input.read(target, 0, TEST_DATA.length); - int bytesRead = input.read(target, 0, TEST_DATA.length + 1); - assertThat(bytesRead).isEqualTo(TEST_DATA.length); + assertThat(bytesRead).isEqualTo(RESULT_END_OF_INPUT); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); + } - // Check the read data is correct. - assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + @Test + public void testReadEndOfInputAfterFirstByteRead() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + input.skipFully(TEST_DATA.length - 1); + int bytesRead = input.read(target, 0, TEST_DATA.length); + + assertThat(bytesRead).isEqualTo(1); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); + } + + @Test + public void testReadZeroLength() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + int bytesRead = input.read(target, /* offset= */ 0, /* length= */ 0); + + assertThat(bytesRead).isEqualTo(0); } @Test @@ -101,7 +132,7 @@ public class DefaultExtractorInputTest { byte[] target = new byte[TEST_DATA.length]; input.readFully(target, 0, TEST_DATA.length); // Check that we read the whole of TEST_DATA. - assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(TEST_DATA).isEqualTo(target); assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); // Check that we see end of input if we read again with allowEndOfInput set. boolean result = input.readFully(target, 0, 1, true); @@ -121,11 +152,11 @@ public class DefaultExtractorInputTest { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[5]; input.readFully(target, 0, 5); - assertThat(Arrays.equals(copyOf(TEST_DATA, 5), target)).isTrue(); + assertThat(copyOf(TEST_DATA, 5)).isEqualTo(target); assertThat(input.getPosition()).isEqualTo(5); target = new byte[4]; input.readFully(target, 0, 4); - assertThat(Arrays.equals(copyOfRange(TEST_DATA, 5, 9), target)).isTrue(); + assertThat(copyOfRange(TEST_DATA, 5, 9)).isEqualTo(target); assertThat(input.getPosition()).isEqualTo(5 + 4); } @@ -180,27 +211,23 @@ public class DefaultExtractorInputTest { input.readFully(target, 0, TEST_DATA.length); // Check the read data is correct. - assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(TEST_DATA).isEqualTo(target); assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); } @Test - public void testSkip() throws Exception { - FakeDataSource testDataSource = buildDataSource(); - DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); + public void testSkipMultipleTimes() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); // We expect to perform three skips of three bytes, as setup in buildTestDataSource. for (int i = 0; i < 3; i++) { assertThat(input.skip(TEST_DATA.length)).isEqualTo(3); } - // Check we're now indicated that the end of input is reached. - int expectedEndOfInput = input.skip(TEST_DATA.length); - assertThat(expectedEndOfInput).isEqualTo(RESULT_END_OF_INPUT); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); } @Test public void testLargeSkip() throws Exception { - FakeDataSource testDataSource = buildLargeDataSource(); - DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); + DefaultExtractorInput input = createDefaultExtractorInput(); // Check that skipping the entire data source succeeds. int bytesToSkip = LARGE_TEST_DATA_LENGTH; while (bytesToSkip > 0) { @@ -208,6 +235,59 @@ public class DefaultExtractorInputTest { } } + @Test + public void testSkipAlreadyPeeked() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + + input.advancePeekPosition(TEST_DATA.length); + int bytesSkipped = input.skip(TEST_DATA.length - 1); + + assertThat(bytesSkipped).isEqualTo(TEST_DATA.length - 1); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length - 1); + } + + @Test + public void testSkipPartiallyPeeked() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + + input.advancePeekPosition(TEST_DATA.length - 1); + int bytesSkipped = input.skip(TEST_DATA.length); + + assertThat(bytesSkipped).isEqualTo(TEST_DATA.length - 1); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length - 1); + } + + @Test + public void testSkipEndOfInputBeforeFirstByteSkipped() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + + input.skipFully(TEST_DATA.length); + int bytesSkipped = input.skip(TEST_DATA.length); + + assertThat(bytesSkipped).isEqualTo(RESULT_END_OF_INPUT); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); + } + + @Test + public void testSkipEndOfInputAfterFirstByteSkipped() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + + input.skipFully(TEST_DATA.length - 1); + int bytesSkipped = input.skip(TEST_DATA.length); + + assertThat(bytesSkipped).isEqualTo(1); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); + } + + @Test + public void testSkipZeroLength() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + + int bytesRead = input.skip(0); + + assertThat(bytesRead).isEqualTo(0); + } + @Test public void testSkipFullyOnce() throws Exception { // Skip TEST_DATA. @@ -309,6 +389,86 @@ public class DefaultExtractorInputTest { } } + @Test + public void testPeekMultipleTimes() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + // We expect to perform three peeks of three bytes, as setup in buildTestDataSource. + int bytesPeeked = 0; + bytesPeeked += input.peek(target, 0, TEST_DATA.length); + assertThat(bytesPeeked).isEqualTo(3); + bytesPeeked += input.peek(target, 3, TEST_DATA.length); + assertThat(bytesPeeked).isEqualTo(6); + bytesPeeked += input.peek(target, 6, TEST_DATA.length); + assertThat(bytesPeeked).isEqualTo(9); + assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); + assertThat(TEST_DATA).isEqualTo(target); + } + + @Test + public void testPeekAlreadyPeeked() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + input.advancePeekPosition(TEST_DATA.length); + input.resetPeekPosition(); + int bytesPeeked = input.peek(target, 0, TEST_DATA.length - 1); + + assertThat(bytesPeeked).isEqualTo(TEST_DATA.length - 1); + assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length - 1); + assertThat(Arrays.copyOf(TEST_DATA, TEST_DATA.length - 1)) + .isEqualTo(Arrays.copyOf(target, TEST_DATA.length - 1)); + } + + @Test + public void testPeekPartiallyPeeked() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + input.advancePeekPosition(TEST_DATA.length - 1); + input.resetPeekPosition(); + int bytesPeeked = input.peek(target, 0, TEST_DATA.length); + + assertThat(bytesPeeked).isEqualTo(TEST_DATA.length - 1); + assertThat(Arrays.copyOf(TEST_DATA, TEST_DATA.length - 1)) + .isEqualTo(Arrays.copyOf(target, TEST_DATA.length - 1)); + } + + @Test + public void testPeekEndOfInputBeforeFirstBytePeeked() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + input.advancePeekPosition(TEST_DATA.length); + int bytesPeeked = input.peek(target, 0, TEST_DATA.length); + + assertThat(bytesPeeked).isEqualTo(RESULT_END_OF_INPUT); + assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); + } + + @Test + public void testPeekEndOfInputAfterFirstBytePeeked() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + input.advancePeekPosition(TEST_DATA.length - 1); + int bytesPeeked = input.peek(target, 0, TEST_DATA.length); + + assertThat(bytesPeeked).isEqualTo(1); + assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); + } + + @Test + public void testPeekZeroLength() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + int bytesPeeked = input.peek(target, /* offset= */ 0, /* length= */ 0); + + assertThat(bytesPeeked).isEqualTo(0); + } + @Test public void testPeekFully() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); @@ -316,14 +476,14 @@ public class DefaultExtractorInputTest { input.peekFully(target, 0, TEST_DATA.length); // Check that we read the whole of TEST_DATA. - assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(TEST_DATA).isEqualTo(target); assertThat(input.getPosition()).isEqualTo(0); assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); // Check that we can read again from the buffer byte[] target2 = new byte[TEST_DATA.length]; input.readFully(target2, 0, TEST_DATA.length); - assertThat(Arrays.equals(TEST_DATA, target2)).isTrue(); + assertThat(TEST_DATA).isEqualTo(target2); assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); @@ -350,7 +510,7 @@ public class DefaultExtractorInputTest { input.peekFully(target, /* offset= */ 0, /* length= */ TEST_DATA.length); assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); - assertThat(Arrays.equals(TEST_DATA, Arrays.copyOf(target, TEST_DATA.length))).isTrue(); + assertThat(TEST_DATA).isEqualTo(Arrays.copyOf(target, TEST_DATA.length)); } @Test @@ -360,14 +520,14 @@ public class DefaultExtractorInputTest { input.peekFully(target, 0, TEST_DATA.length); // Check that we read the whole of TEST_DATA. - assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(TEST_DATA).isEqualTo(target); assertThat(input.getPosition()).isEqualTo(0); // Check that we can peek again after resetting. input.resetPeekPosition(); byte[] target2 = new byte[TEST_DATA.length]; input.peekFully(target2, 0, TEST_DATA.length); - assertThat(Arrays.equals(TEST_DATA, target2)).isTrue(); + assertThat(TEST_DATA).isEqualTo(target2); // Check that we fail with EOFException if we peek past the end of the input. try { 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 443ffdb12c..7323cfd0fe 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 @@ -65,7 +65,8 @@ public final class FakeExtractorInput implements ExtractorInput { private int readPosition; private int peekPosition; - private final SparseBooleanArray partiallySatisfiedTargetPositions; + private final SparseBooleanArray partiallySatisfiedTargetReadPositions; + private final SparseBooleanArray partiallySatisfiedTargetPeekPositions; private final SparseBooleanArray failedReadPositions; private final SparseBooleanArray failedPeekPositions; @@ -75,7 +76,8 @@ public final class FakeExtractorInput implements ExtractorInput { this.simulateUnknownLength = simulateUnknownLength; this.simulatePartialReads = simulatePartialReads; this.simulateIOErrors = simulateIOErrors; - partiallySatisfiedTargetPositions = new SparseBooleanArray(); + partiallySatisfiedTargetReadPositions = new SparseBooleanArray(); + partiallySatisfiedTargetPeekPositions = new SparseBooleanArray(); failedReadPositions = new SparseBooleanArray(); failedPeekPositions = new SparseBooleanArray(); } @@ -84,7 +86,8 @@ public final class FakeExtractorInput implements ExtractorInput { public void reset() { readPosition = 0; peekPosition = 0; - partiallySatisfiedTargetPositions.clear(); + partiallySatisfiedTargetReadPositions.clear(); + partiallySatisfiedTargetPeekPositions.clear(); failedReadPositions.clear(); failedPeekPositions.clear(); } @@ -104,7 +107,7 @@ public final class FakeExtractorInput implements ExtractorInput { @Override public int read(byte[] target, int offset, int length) throws IOException { checkIOException(readPosition, failedReadPositions); - length = getReadLength(length); + length = getLengthToRead(readPosition, length, partiallySatisfiedTargetReadPositions); return readFullyInternal(target, offset, length, true) ? length : C.RESULT_END_OF_INPUT; } @@ -123,7 +126,7 @@ public final class FakeExtractorInput implements ExtractorInput { @Override public int skip(int length) throws IOException { checkIOException(readPosition, failedReadPositions); - length = getReadLength(length); + length = getLengthToRead(readPosition, length, partiallySatisfiedTargetReadPositions); return skipFullyInternal(length, true) ? length : C.RESULT_END_OF_INPUT; } @@ -138,16 +141,18 @@ public final class FakeExtractorInput implements ExtractorInput { skipFully(length, false); } + @Override + public int peek(byte[] target, int offset, int length) throws IOException { + checkIOException(peekPosition, failedPeekPositions); + length = getLengthToRead(peekPosition, length, partiallySatisfiedTargetPeekPositions); + return peekFullyInternal(target, offset, length, true) ? length : C.RESULT_END_OF_INPUT; + } + @Override public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput) throws IOException { checkIOException(peekPosition, failedPeekPositions); - if (!checkXFully(allowEndOfInput, peekPosition, length)) { - return false; - } - System.arraycopy(data, peekPosition, target, offset, length); - peekPosition += length; - return true; + return peekFullyInternal(target, offset, length, allowEndOfInput); } @Override @@ -221,18 +226,19 @@ public final class FakeExtractorInput implements ExtractorInput { return true; } - private int getReadLength(int requestedLength) { - if (readPosition == data.length) { + private int getLengthToRead( + int position, int requestedLength, SparseBooleanArray partiallySatisfiedTargetPositions) { + if (position == data.length) { // If the requested length is non-zero, the end of the input will be read. return requestedLength == 0 ? 0 : Integer.MAX_VALUE; } - int targetPosition = readPosition + requestedLength; + int targetPosition = position + requestedLength; if (simulatePartialReads && requestedLength > 1 && !partiallySatisfiedTargetPositions.get(targetPosition)) { partiallySatisfiedTargetPositions.put(targetPosition, true); return 1; } - return Math.min(requestedLength, data.length - readPosition); + return Math.min(requestedLength, data.length - position); } private boolean readFullyInternal(byte[] target, int offset, int length, boolean allowEndOfInput) @@ -255,6 +261,16 @@ public final class FakeExtractorInput implements ExtractorInput { return true; } + private boolean peekFullyInternal(byte[] target, int offset, int length, boolean allowEndOfInput) + throws EOFException { + if (!checkXFully(allowEndOfInput, peekPosition, length)) { + return false; + } + System.arraycopy(data, peekPosition, target, offset, length); + peekPosition += length; + return true; + } + /** * Builder of {@link FakeExtractorInput} instances. */ From c027b4e71a65ad4ac82219cc2632301a7a9ed6c2 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 10 Dec 2019 10:48:14 +0000 Subject: [PATCH 0477/1335] Turn on nullness checker for playback stats The nullness checker complains about Integers with @IntDef annotations so replace pairs with custom pair types for the timed event records in PlaybackStats. PiperOrigin-RevId: 284731834 --- .../exoplayer2/analytics/PlaybackStats.java | 177 +++++++++++++++--- .../analytics/PlaybackStatsListener.java | 29 +-- 2 files changed, 166 insertions(+), 40 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java index b370c893de..893ecb07c2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.analytics; import android.os.SystemClock; -import android.util.Pair; 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.analytics.AnalyticsListener.EventTime; @@ -28,11 +28,136 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.Collections; import java.util.List; -import org.checkerframework.checker.nullness.compatqual.NullableType; /** Statistics about playbacks. */ public final class PlaybackStats { + /** Stores a playback state with the event time at which it became active. */ + public static final class EventTimeAndPlaybackState { + /** The event time at which the playback state became active. */ + public final EventTime eventTime; + /** The playback state that became active. */ + public final @PlaybackState int playbackState; + + /** + * Creates a new timed playback state event. + * + * @param eventTime The event time at which the playback state became active. + * @param playbackState The playback state that became active. + */ + public EventTimeAndPlaybackState(EventTime eventTime, @PlaybackState int playbackState) { + this.eventTime = eventTime; + this.playbackState = playbackState; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EventTimeAndPlaybackState that = (EventTimeAndPlaybackState) o; + if (playbackState != that.playbackState) { + return false; + } + return eventTime.equals(that.eventTime); + } + + @Override + public int hashCode() { + int result = eventTime.hashCode(); + result = 31 * result + playbackState; + return result; + } + } + + /** + * Stores a format with the event time at which it started being used, or {@code null} to indicate + * that no format was used. + */ + public static final class EventTimeAndFormat { + /** The event time associated with {@link #format}. */ + public final EventTime eventTime; + /** The format that started being used, or {@code null} if no format was used. */ + @Nullable public final Format format; + + /** + * Creates a new timed format event. + * + * @param eventTime The event time associated with {@code format}. + * @param format The format that started being used, or {@code null} if no format was used. + */ + public EventTimeAndFormat(EventTime eventTime, @Nullable Format format) { + this.eventTime = eventTime; + this.format = format; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EventTimeAndFormat that = (EventTimeAndFormat) o; + if (!eventTime.equals(that.eventTime)) { + return false; + } + return format != null ? format.equals(that.format) : that.format == null; + } + + @Override + public int hashCode() { + int result = eventTime.hashCode(); + result = 31 * result + (format != null ? format.hashCode() : 0); + return result; + } + } + + /** Stores an exception with the event time at which it occurred. */ + public static final class EventTimeAndException { + /** The event time at which the exception occurred. */ + public final EventTime eventTime; + /** The exception that was thrown. */ + public final Exception exception; + + /** + * Creates a new timed exception event. + * + * @param eventTime The event time at which the exception occurred. + * @param exception The exception that was thrown. + */ + public EventTimeAndException(EventTime eventTime, Exception exception) { + this.eventTime = eventTime; + this.exception = exception; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EventTimeAndException that = (EventTimeAndException) o; + if (!eventTime.equals(that.eventTime)) { + return false; + } + return exception.equals(that.exception); + } + + @Override + public int hashCode() { + int result = eventTime.hashCode(); + result = 31 * result + exception.hashCode(); + return result; + } + } + /** * State of a playback. One of {@link #PLAYBACK_STATE_NOT_STARTED}, {@link * #PLAYBACK_STATE_JOINING_FOREGROUND}, {@link #PLAYBACK_STATE_JOINING_BACKGROUND}, {@link @@ -258,10 +383,10 @@ public final class PlaybackStats { // Playback state stats. /** - * The playback state history as ordered pairs of the {@link EventTime} at which a state became - * active and the {@link PlaybackState}. + * The playback state history as {@link EventTimeAndPlaybackState EventTimeAndPlaybackStates} + * ordered by {@code EventTime.realTimeMs}. */ - public final List> playbackStateHistory; + public final List playbackStateHistory; /** * The media time history as an ordered list of long[2] arrays with [0] being the realtime as * returned by {@code SystemClock.elapsedRealtime()} and [1] being the media time at this @@ -319,15 +444,15 @@ public final class PlaybackStats { // Format stats. /** - * The video format history as ordered pairs of the {@link EventTime} at which a format started - * being used and the {@link Format}. The {@link Format} may be null if no video format was used. + * The video format history as {@link EventTimeAndFormat EventTimeAndFormats} ordered by {@code + * EventTime.realTimeMs}. The {@link Format} may be null if no video format was used. */ - public final List> videoFormatHistory; + public final List videoFormatHistory; /** - * The audio format history as ordered pairs of the {@link EventTime} at which a format started - * being used and the {@link Format}. The {@link Format} may be null if no audio format was used. + * The audio format history as {@link EventTimeAndFormat EventTimeAndFormats} ordered by {@code + * EventTime.realTimeMs}. The {@link Format} may be null if no audio format was used. */ - public final List> audioFormatHistory; + public final List audioFormatHistory; /** The total media time for which video format height data is available, in milliseconds. */ public final long totalVideoFormatHeightTimeMs; /** @@ -400,23 +525,23 @@ public final class PlaybackStats { */ public final int nonFatalErrorCount; /** - * The history of fatal errors as ordered pairs of the {@link EventTime} at which an error - * occurred and the error. Errors are fatal if playback stopped due to this error. + * The history of fatal errors as {@link EventTimeAndException EventTimeAndExceptions} ordered by + * {@code EventTime.realTimeMs}. Errors are fatal if playback stopped due to this error. */ - public final List> fatalErrorHistory; + public final List fatalErrorHistory; /** - * The history of non-fatal errors as ordered pairs of the {@link EventTime} at which an error - * occurred and the error. Error are non-fatal if playback can recover from the error without - * stopping. + * The history of non-fatal errors as {@link EventTimeAndException EventTimeAndExceptions} ordered + * by {@code EventTime.realTimeMs}. Errors are non-fatal if playback can recover from the error + * without stopping. */ - public final List> nonFatalErrorHistory; + public final List nonFatalErrorHistory; private final long[] playbackStateDurationsMs; /* package */ PlaybackStats( int playbackCount, long[] playbackStateDurationsMs, - List> playbackStateHistory, + List playbackStateHistory, List mediaTimeHistory, long firstReportedTimeMs, int foregroundPlaybackCount, @@ -431,8 +556,8 @@ public final class PlaybackStats { int totalRebufferCount, long maxRebufferTimeMs, int adPlaybackCount, - List> videoFormatHistory, - List> audioFormatHistory, + List videoFormatHistory, + List audioFormatHistory, long totalVideoFormatHeightTimeMs, long totalVideoFormatHeightTimeProduct, long totalVideoFormatBitrateTimeMs, @@ -452,8 +577,8 @@ public final class PlaybackStats { int fatalErrorPlaybackCount, int fatalErrorCount, int nonFatalErrorCount, - List> fatalErrorHistory, - List> nonFatalErrorHistory) { + List fatalErrorHistory, + List nonFatalErrorHistory) { this.playbackCount = playbackCount; this.playbackStateDurationsMs = playbackStateDurationsMs; this.playbackStateHistory = Collections.unmodifiableList(playbackStateHistory); @@ -515,11 +640,11 @@ public final class PlaybackStats { */ public @PlaybackState int getPlaybackStateAtTime(long realtimeMs) { @PlaybackState int state = PLAYBACK_STATE_NOT_STARTED; - for (Pair timeAndState : playbackStateHistory) { - if (timeAndState.first.realtimeMs > realtimeMs) { + for (EventTimeAndPlaybackState timeAndState : playbackStateHistory) { + if (timeAndState.eventTime.realtimeMs > realtimeMs) { break; } - state = timeAndState.second; + state = timeAndState.playbackState; } return state; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java index 2f8ca3a8cc..5927b9dd6e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.analytics; import android.os.SystemClock; -import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -25,6 +24,9 @@ import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.analytics.PlaybackStats.EventTimeAndException; +import com.google.android.exoplayer2.analytics.PlaybackStats.EventTimeAndFormat; +import com.google.android.exoplayer2.analytics.PlaybackStats.EventTimeAndPlaybackState; import com.google.android.exoplayer2.analytics.PlaybackStats.PlaybackState; import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.MediaLoadData; @@ -42,7 +44,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import org.checkerframework.checker.nullness.compatqual.NullableType; /** * {@link AnalyticsListener} to gather {@link PlaybackStats} from the player. @@ -433,12 +434,12 @@ public final class PlaybackStatsListener // Final stats. private final boolean keepHistory; private final long[] playbackStateDurationsMs; - private final List> playbackStateHistory; + private final List playbackStateHistory; private final List mediaTimeHistory; - private final List> videoFormatHistory; - private final List> audioFormatHistory; - private final List> fatalErrorHistory; - private final List> nonFatalErrorHistory; + private final List videoFormatHistory; + private final List audioFormatHistory; + private final List fatalErrorHistory; + private final List nonFatalErrorHistory; private final boolean isAd; private long firstReportedTimeMs; @@ -589,7 +590,7 @@ public final class PlaybackStatsListener public void onFatalError(EventTime eventTime, Exception error) { fatalErrorCount++; if (keepHistory) { - fatalErrorHistory.add(Pair.create(eventTime, error)); + fatalErrorHistory.add(new EventTimeAndException(eventTime, error)); } hasFatalError = true; isInterruptedByAd = false; @@ -743,7 +744,7 @@ public final class PlaybackStatsListener public void onNonFatalError(EventTime eventTime, Exception error) { nonFatalErrorCount++; if (keepHistory) { - nonFatalErrorHistory.add(Pair.create(eventTime, error)); + nonFatalErrorHistory.add(new EventTimeAndException(eventTime, error)); } } @@ -776,9 +777,9 @@ public final class PlaybackStatsListener : playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND]; boolean hasBackgroundJoin = playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND] > 0; - List> videoHistory = + List videoHistory = isFinal ? videoFormatHistory : new ArrayList<>(videoFormatHistory); - List> audioHistory = + List audioHistory = isFinal ? audioFormatHistory : new ArrayList<>(audioFormatHistory); return new PlaybackStats( /* playbackCount= */ 1, @@ -864,7 +865,7 @@ public final class PlaybackStatsListener currentPlaybackState = newPlaybackState; currentPlaybackStateStartTimeMs = eventTime.realtimeMs; if (keepHistory) { - playbackStateHistory.add(Pair.create(eventTime, currentPlaybackState)); + playbackStateHistory.add(new EventTimeAndPlaybackState(eventTime, currentPlaybackState)); } } @@ -973,7 +974,7 @@ public final class PlaybackStatsListener } currentVideoFormat = newFormat; if (keepHistory) { - videoFormatHistory.add(Pair.create(eventTime, currentVideoFormat)); + videoFormatHistory.add(new EventTimeAndFormat(eventTime, currentVideoFormat)); } } @@ -989,7 +990,7 @@ public final class PlaybackStatsListener } currentAudioFormat = newFormat; if (keepHistory) { - audioFormatHistory.add(Pair.create(eventTime, currentAudioFormat)); + audioFormatHistory.add(new EventTimeAndFormat(eventTime, currentAudioFormat)); } } From 90329a14c39275c2052612476a0f2ec74edd7618 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 10 Dec 2019 11:26:41 +0000 Subject: [PATCH 0478/1335] Make DefaultTimeBar exclude itself for gestures Issue: #6685 PiperOrigin-RevId: 284736041 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/ui/DefaultTimeBar.java | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 714644878d..57c96a4442 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -20,6 +20,8 @@ * Upgrade Truth dependency from 0.44 to 1.0. * Upgrade to JUnit 4.13-rc-2. * Add support for attaching DRM sessions to clear content in the demo app. +* UI: Exclude `DefaultTimeBar` region from system gesture detection + ([#6685](https://github.com/google/ExoPlayer/issues/6685)). ### 2.11.0 (2019-12-11) ### 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 8b737bc006..89bcaf84bc 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 @@ -36,12 +36,15 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import androidx.annotation.ColorInt; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import java.util.Collections; import java.util.Formatter; import java.util.Locale; import java.util.concurrent.CopyOnWriteArraySet; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A time bar that shows a current position, buffered position, duration and ad markers. @@ -199,6 +202,7 @@ public class DefaultTimeBar extends View implements TimeBar { private int keyCountIncrement; private long keyTimeIncrement; private int lastCoarseScrubXPosition; + @MonotonicNonNull private Rect lastExclusionRectangle; private boolean scrubbing; private long scrubPosition; @@ -604,6 +608,9 @@ public class DefaultTimeBar extends View implements TimeBar { seekBounds.set(seekLeft, barY, seekRight, barY + touchTargetHeight); progressBar.set(seekBounds.left + scrubberPadding, progressY, seekBounds.right - scrubberPadding, progressY + barHeight); + if (Util.SDK_INT >= 29) { + setSystemGestureExclusionRectsV29(width, height); + } update(); } @@ -834,6 +841,18 @@ public class DefaultTimeBar extends View implements TimeBar { } } + @RequiresApi(29) + private void setSystemGestureExclusionRectsV29(int width, int height) { + if (lastExclusionRectangle != null + && lastExclusionRectangle.width() == width + && lastExclusionRectangle.height() == height) { + // Allocating inside onLayout is considered a DrawAllocation lint error, so avoid if possible. + return; + } + lastExclusionRectangle = new Rect(/* left= */ 0, /* top= */ 0, width, height); + setSystemGestureExclusionRects(Collections.singletonList(lastExclusionRectangle)); + } + private String getProgressText() { return Util.getStringForTime(formatBuilder, formatter, position); } From 4c4cabdfacd7622459edcb01b85d1e965103aa4d Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 10 Dec 2019 11:50:20 +0000 Subject: [PATCH 0479/1335] (partial) Rollback of https://github.com/google/ExoPlayer/commit/880b879e8c55b1e709fd3ed6a48005737d26e75a *** Original commit *** Suppress warnings emitted by Checker Framework version 2.11.1 More information: https://docs.google.com/document/d/16tpK6aXqN68PvTyvt4siM-m7f0NXi_8xEeitLDzr8xY/edit?usp=sharing Tested: TAP train for global presubmit queue http://test/OCL:278152710:BASE:278144052:1572760370662:22459c12 *** PiperOrigin-RevId: 284738438 --- .../exoplayer2/ext/mediasession/MediaSessionConnector.java | 2 -- .../exoplayer2/audio/ChannelMappingAudioProcessor.java | 1 - .../android/exoplayer2/drm/DefaultDrmSessionManager.java | 2 -- .../com/google/android/exoplayer2/extractor/mp4/Track.java | 2 -- .../android/exoplayer2/mediacodec/MediaCodecUtil.java | 4 ---- .../android/exoplayer2/offline/DefaultDownloadIndex.java | 2 -- .../android/exoplayer2/source/SingleSampleMediaPeriod.java | 2 -- .../exoplayer2/trackselection/AdaptiveTrackSelection.java | 7 +------ .../android/exoplayer2/upstream/DataSchemeDataSource.java | 2 -- .../android/exoplayer2/ui/spherical/SceneRenderer.java | 2 -- 10 files changed, 1 insertion(+), 25 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 5382e286a1..6ae35d8c57 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 @@ -688,8 +688,6 @@ public final class MediaSessionConnector { * @param customActionProviders The custom action providers, or null to remove all existing custom * action providers. */ - // incompatible types in assignment. - @SuppressWarnings("nullness:assignment.type.incompatible") public void setCustomActionProviders(@Nullable CustomActionProvider... customActionProviders) { this.customActionProviders = customActionProviders == null ? new CustomActionProvider[0] : customActionProviders; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java index 4fb6af1af4..b94d972dc5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java @@ -24,7 +24,6 @@ import java.nio.ByteBuffer; * An {@link AudioProcessor} that applies a mapping from input channels onto specified output * channels. This can be used to reorder, duplicate or discard channels. */ -@SuppressWarnings("nullness:initialization.fields.uninitialized") /* package */ final class ChannelMappingAudioProcessor extends BaseAudioProcessor { @Nullable private int[] pendingOutputChannels; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 1c27d745de..8b404660b1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -339,8 +339,6 @@ public class DefaultDrmSessionManager implements DrmSe new DefaultLoadErrorHandlingPolicy(initialDrmRequestRetryCount)); } - // the constructor does not initialize fields: offlineLicenseKeySetId - @SuppressWarnings("nullness:initialization.fields.uninitialized") private DefaultDrmSessionManager( UUID uuid, ExoMediaDrm.Provider exoMediaDrmProvider, 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 0a21ddd3a3..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 @@ -129,8 +129,6 @@ public final class Track { : sampleDescriptionEncryptionBoxes[sampleDescriptionIndex]; } - // incompatible types in argument. - @SuppressWarnings("nullness:argument.type.incompatible") public Track copyWithFormat(Format format) { return new Track( id, 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 9adb6bc7bc..07836672e5 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 @@ -943,8 +943,6 @@ public final class MediaCodecUtil { @Nullable private android.media.MediaCodecInfo[] mediaCodecInfos; - // the constructor does not initialize fields: mediaCodecInfos - @SuppressWarnings("nullness:initialization.fields.uninitialized") public MediaCodecListCompatV21(boolean includeSecure, boolean includeTunneling) { codecKind = includeSecure || includeTunneling @@ -958,8 +956,6 @@ public final class MediaCodecUtil { return mediaCodecInfos.length; } - // incompatible types in return. - @SuppressWarnings("nullness:return.type.incompatible") @Override public android.media.MediaCodecInfo getCodecInfoAt(int index) { ensureMediaCodecInfosInitialized(); 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 7ed1eb095f..a1c73f74c5 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 @@ -302,8 +302,6 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { } } - // incompatible types in argument. - @SuppressWarnings("nullness:argument.type.incompatible") private Cursor getCursor(String selection, @Nullable String[] selectionArgs) throws DatabaseIOException { try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index ca50c342b5..a5d8266ef6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -383,8 +383,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Nullable private byte[] sampleData; - // the constructor does not initialize fields: sampleData - @SuppressWarnings("nullness:initialization.fields.uninitialized") public SourceLoadable(DataSpec dataSpec, DataSource dataSource) { this.dataSpec = dataSpec; this.dataSource = new StatsDataSource(dataSource); 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 3e8cdd1ca4..9a599279ec 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 @@ -610,18 +610,13 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { @Nullable private long[][] allocationCheckpoints; - /* package */ - // the constructor does not initialize fields: allocationCheckpoints - @SuppressWarnings("nullness:initialization.fields.uninitialized") - DefaultBandwidthProvider( + /* package */ DefaultBandwidthProvider( BandwidthMeter bandwidthMeter, float bandwidthFraction, long reservedBandwidth) { this.bandwidthMeter = bandwidthMeter; this.bandwidthFraction = bandwidthFraction; this.reservedBandwidth = reservedBandwidth; } - // unboxing a possibly-null reference allocationCheckpoints[nextIndex][0] - @SuppressWarnings("nullness:unboxing.of.nullable") @Override public long getAllocatedBandwidth() { long totalBandwidth = (long) (bandwidthMeter.getBitrateEstimate() * bandwidthFraction); 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 e592c3bec3..55c580ead2 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 @@ -36,8 +36,6 @@ public final class DataSchemeDataSource extends BaseDataSource { private int endPosition; private int readPosition; - // the constructor does not initialize fields: data - @SuppressWarnings("nullness:initialization.fields.uninitialized") public DataSchemeDataSource() { super(/* isNetwork= */ false); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java index 01fa6837ea..5080e86345 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java @@ -60,8 +60,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // Methods called on any thread. - // the constructor does not initialize fields: lastProjectionData - @SuppressWarnings("nullness:initialization.fields.uninitialized") public SceneRenderer() { frameAvailable = new AtomicBoolean(); resetRotationAtNextFrame = new AtomicBoolean(true); From 323399544103481b14c9aa52cd1e373131b7ebd0 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 10 Dec 2019 12:08:14 +0000 Subject: [PATCH 0480/1335] Rollback of https://github.com/google/ExoPlayer/commit/4fd881a5516a04409cc0f54d4e6be6c059519f79 *** Original commit *** Suppress warnings emitted by Checker Framework version 3.0.0 More information: https://docs.google.com/document/d/16tpK6aXqN68PvTyvt4siM-m7f0NXi_8xEeitLDzr8xY/edit?usp=sharing Tested: TAP --sample ran all affected tests and none failed http://test/OCL:279845168:BASE:279870402:1573537714395:80ca701c *** PiperOrigin-RevId: 284740695 --- .../android/exoplayer2/ui/PlayerNotificationManager.java | 6 ++---- 1 file changed, 2 insertions(+), 4 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 6c77284e46..aeb5292187 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 @@ -358,7 +358,7 @@ public class PlayerNotificationManager { private final Timeline.Window window; @Nullable private NotificationCompat.Builder builder; - @Nullable private ArrayList builderActions; + @Nullable private List builderActions; @Nullable private Player player; @Nullable private PlaybackPreparer playbackPreparer; private ControlDispatcher controlDispatcher; @@ -998,8 +998,6 @@ public class PlayerNotificationManager { * NotificationCompat.Builder#build()} to obtain the notification, or {@code null} if no * notification should be displayed. */ - // incompatible types in argument. - @SuppressWarnings("nullness:argument.type.incompatible") @Nullable protected NotificationCompat.Builder createNotification( Player player, @@ -1013,7 +1011,7 @@ public class PlayerNotificationManager { } List actionNames = getActions(player); - ArrayList actions = new ArrayList<>(actionNames.size()); + List actions = new ArrayList<>(actionNames.size()); for (int i = 0; i < actionNames.size(); i++) { String actionName = actionNames.get(i); NotificationCompat.Action action = From 9ec524a7e2a8bf929d8f4e28499b572904d176fd Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 10 Dec 2019 12:18:30 +0000 Subject: [PATCH 0481/1335] Rollback of https://github.com/google/ExoPlayer/commit/355ed11a3cd6d90eb02d9f773a546561884324b3 *** Original commit *** Suppress warnings emitted by Checker Framework version 2.11.1 More information: https://docs.google.com/document/d/16tpK6aXqN68PvTyvt4siM-m7f0NXi_8xEeitLDzr8xY/edit?usp=sharing Tested: TAP --sample ran all affected tests and none failed http://test/OCL:278915274:BASE:278884711:1573074344615:a6701677 *** PiperOrigin-RevId: 284741721 --- .../google/android/exoplayer2/ext/flac/FlacDecoderJni.java | 6 ------ .../com/google/android/exoplayer2/ui/DefaultTimeBar.java | 6 +----- 2 files changed, 1 insertion(+), 11 deletions(-) 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 60f1d32a79..5e020175e7 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 @@ -51,12 +51,6 @@ import java.nio.ByteBuffer; @Nullable private byte[] tempBuffer; private boolean endOfExtractorInput; - // the constructor does not initialize fields: tempBuffer - // call to flacInit() not allowed on the given receiver. - @SuppressWarnings({ - "nullness:initialization.fields.uninitialized", - "nullness:method.invocation.invalid" - }) public FlacDecoderJni() throws FlacDecoderException { if (!FlacLibrary.isAvailable()) { throw new FlacDecoderException("Failed to load decoder native libraries."); 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 89bcaf84bc..79e50c19de 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 @@ -226,11 +226,7 @@ public class DefaultTimeBar extends View implements TimeBar { } // Suppress warnings due to usage of View methods in the constructor. - // the constructor does not initialize fields: adGroupTimesMs, playedAdGroups - @SuppressWarnings({ - "nullness:method.invocation.invalid", - "nullness:initialization.fields.uninitialized" - }) + @SuppressWarnings("nullness:method.invocation.invalid") public DefaultTimeBar( Context context, @Nullable AttributeSet attrs, From 006418ab38ceb12b1279b818463944b28dc56eb2 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 10 Dec 2019 12:33:47 +0000 Subject: [PATCH 0482/1335] 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 b2de05ca77..eab4e6b8a1 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 1de7ec2c703de7b1d657507b497f1a9c488e61da Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 10 Dec 2019 12:33:47 +0000 Subject: [PATCH 0483/1335] 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 5da510cf00e5609cf8c886a4ae0605abae1e2d6a Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 10 Dec 2019 15:05:13 +0000 Subject: [PATCH 0484/1335] 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 5cee5cba30fb71bb65a641576fd774159abfc580 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 10 Dec 2019 16:10:47 +0000 Subject: [PATCH 0485/1335] Add NonNull annotations to upstream PiperOrigin-RevId: 284771928 --- .../upstream/DataSourceInputStream.java | 5 ++- .../exoplayer2/upstream/DefaultAllocator.java | 3 +- .../upstream/DefaultBandwidthMeter.java | 3 +- .../upstream/DefaultHttpDataSource.java | 13 +++---- .../exoplayer2/upstream/FileDataSource.java | 34 +++++++++---------- .../exoplayer2/upstream/HttpDataSource.java | 2 +- .../android/exoplayer2/upstream/Loader.java | 2 +- .../upstream/LoaderErrorThrower.java | 5 ++- .../exoplayer2/upstream/ParsingLoadable.java | 3 +- .../upstream/ResolvingDataSource.java | 2 +- .../exoplayer2/upstream/package-info.java | 19 +++++++++++ 11 files changed, 54 insertions(+), 37 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/upstream/package-info.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSourceInputStream.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSourceInputStream.java index 6c4e77a90a..c4296bd6f6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSourceInputStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSourceInputStream.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.upstream; -import androidx.annotation.NonNull; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -72,12 +71,12 @@ public final class DataSourceInputStream extends InputStream { } @Override - public int read(@NonNull byte[] buffer) throws IOException { + public int read(byte[] buffer) throws IOException { return read(buffer, 0, buffer.length); } @Override - public int read(@NonNull byte[] buffer, int offset, int length) throws IOException { + public int read(byte[] buffer, int offset, int length) throws IOException { Assertions.checkState(!closed); checkOpened(); int bytesRead = dataSource.read(buffer, offset, length); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultAllocator.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultAllocator.java index 71e2d8d19f..ca9cca255d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultAllocator.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultAllocator.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -28,7 +29,7 @@ public final class DefaultAllocator implements Allocator { private final boolean trimOnReset; private final int individualAllocationSize; - private final byte[] initialAllocationBlock; + @Nullable private final byte[] initialAllocationBlock; private final Allocation[] singleAllocationReleaseHolder; private int targetBufferSize; 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 f688bb9447..0309292164 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 @@ -210,7 +210,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList } private static int[] getCountryGroupIndices(String countryCode) { - int[] groupIndices = DEFAULT_INITIAL_BITRATE_COUNTRY_GROUPS.get(countryCode); + @Nullable int[] groupIndices = DEFAULT_INITIAL_BITRATE_COUNTRY_GROUPS.get(countryCode); // Assume median group if not found. return groupIndices == null ? new int[] {2, 2, 2, 2} : groupIndices; } @@ -304,7 +304,6 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList } @Override - @Nullable public TransferListener getTransferListener() { return this; } 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 ae115ab58c..f2cf344b9f 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 @@ -386,7 +386,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * * @return The current open connection, or null. */ - protected final @Nullable HttpURLConnection getConnection() { + @Nullable + protected final HttpURLConnection getConnection() { return connection; } @@ -428,7 +429,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException { URL url = new URL(dataSpec.uri.toString()); @HttpMethod int httpMethod = dataSpec.httpMethod; - byte[] httpBody = dataSpec.httpBody; + @Nullable byte[] httpBody = dataSpec.httpBody; long position = dataSpec.position; long length = dataSpec.length; boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP); @@ -495,7 +496,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * * @param url The url to connect to. * @param httpMethod The http method. - * @param httpBody The body data. + * @param httpBody The body data, or {@code null} if not required. * @param position The byte offset of the requested data. * @param length The length of the requested data, or {@link C#LENGTH_UNSET}. * @param allowGzip Whether to allow the use of gzip. @@ -505,7 +506,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou private HttpURLConnection makeConnection( URL url, @HttpMethod int httpMethod, - byte[] httpBody, + @Nullable byte[] httpBody, long position, long length, boolean allowGzip, @@ -562,11 +563,11 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * Handles a redirect. * * @param originalUrl The original URL. - * @param location The Location header in the response. + * @param location The Location header in the response. May be {@code null}. * @return The next URL. * @throws IOException If redirection isn't possible. */ - private static URL handleRedirect(URL originalUrl, String location) throws IOException { + private static URL handleRedirect(URL originalUrl, @Nullable String location) throws IOException { if (location == null) { throw new ProtocolException("Null location redirect"); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java index 2661469efd..93c1ce9adf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java @@ -86,7 +86,6 @@ public final class FileDataSource extends BaseDataSource { transferInitializing(dataSpec); this.file = openLocalFile(uri); - file.seek(dataSpec.position); bytesRemaining = dataSpec.length == C.LENGTH_UNSET ? file.length() - dataSpec.position : dataSpec.length; @@ -103,23 +102,6 @@ public final class FileDataSource extends BaseDataSource { return bytesRemaining; } - private static RandomAccessFile openLocalFile(Uri uri) throws FileDataSourceException { - try { - return new RandomAccessFile(Assertions.checkNotNull(uri.getPath()), "r"); - } catch (FileNotFoundException e) { - if (!TextUtils.isEmpty(uri.getQuery()) || !TextUtils.isEmpty(uri.getFragment())) { - throw new FileDataSourceException( - String.format( - "uri has query and/or fragment, which are not supported. Did you call Uri.parse()" - + " on a string containing '?' or '#'? Use Uri.fromFile(new File(path)) to" - + " avoid this. path=%s,query=%s,fragment=%s", - uri.getPath(), uri.getQuery(), uri.getFragment()), - e); - } - throw new FileDataSourceException(e); - } - } - @Override public int read(byte[] buffer, int offset, int readLength) throws FileDataSourceException { if (readLength == 0) { @@ -168,4 +150,20 @@ public final class FileDataSource extends BaseDataSource { } } + private static RandomAccessFile openLocalFile(Uri uri) throws FileDataSourceException { + try { + return new RandomAccessFile(Assertions.checkNotNull(uri.getPath()), "r"); + } catch (FileNotFoundException e) { + if (!TextUtils.isEmpty(uri.getQuery()) || !TextUtils.isEmpty(uri.getFragment())) { + throw new FileDataSourceException( + String.format( + "uri has query and/or fragment, which are not supported. Did you call Uri.parse()" + + " on a string containing '?' or '#'? Use Uri.fromFile(new File(path)) to" + + " avoid this. path=%s,query=%s,fragment=%s", + uri.getPath(), uri.getQuery(), uri.getFragment()), + e); + } + throw new FileDataSourceException(e); + } + } } 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 63cad8786b..9d4f9b6811 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 @@ -89,7 +89,7 @@ public interface HttpDataSource extends DataSource { final class RequestProperties { private final Map requestProperties; - private Map requestPropertiesSnapshot; + @Nullable private Map requestPropertiesSnapshot; public RequestProperties() { requestProperties = new HashMap<>(); 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 a498f510dd..d9d84bdda1 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 @@ -363,7 +363,7 @@ public final class Loader implements LoaderErrorThrower { } else { canceled = true; loadable.cancelLoad(); - Thread executorThread = this.executorThread; + @Nullable Thread executorThread = this.executorThread; if (executorThread != null) { executorThread.interrupt(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java index 4f9e9fa5e6..54c3d4cbe5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java @@ -49,15 +49,14 @@ public interface LoaderErrorThrower { final class Dummy implements LoaderErrorThrower { @Override - public void maybeThrowError() throws IOException { + public void maybeThrowError() { // Do nothing. } @Override - public void maybeThrowError(int minRetryCount) throws IOException { + public void maybeThrowError(int minRetryCount) { // Do nothing. } - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java index edec849b88..7fceda1700 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java @@ -127,7 +127,8 @@ public final class ParsingLoadable implements Loadable { } /** Returns the loaded object, or null if an object has not been loaded. */ - public final @Nullable T getResult() { + @Nullable + public final T getResult() { return result; } 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 index 412f866e99..7e5a274c81 100644 --- 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 @@ -113,7 +113,7 @@ public final class ResolvingDataSource implements DataSource { @Nullable @Override public Uri getUri() { - Uri reportedUri = upstreamDataSource.getUri(); + @Nullable Uri reportedUri = upstreamDataSource.getUri(); return reportedUri == null ? null : resolver.resolveReportedUri(reportedUri); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/package-info.java new file mode 100644 index 0000000000..1fb49d4b96 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/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.upstream; + +import com.google.android.exoplayer2.util.NonNullApi; From 614a92b607996d65b5a9691de300a3f81d415499 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 10 Dec 2019 16:41:41 +0000 Subject: [PATCH 0486/1335] Fix javadoc errors exposed by -Xdoclint PiperOrigin-RevId: 284776790 --- .../java/com/google/android/exoplayer2/util/IntArrayQueue.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/IntArrayQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/util/IntArrayQueue.java index d6eb1ca35a..3277d042ed 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/IntArrayQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/IntArrayQueue.java @@ -56,7 +56,7 @@ public final class IntArrayQueue { /** * Remove an item from the queue. * - * @throws {@link NoSuchElementException} if the queue is empty. + * @throws NoSuchElementException if the queue is empty. */ public int remove() { if (size == 0) { From c1573106fabc40aa294dc040997b687aac239a2c Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 10 Dec 2019 16:42:33 +0000 Subject: [PATCH 0487/1335] Fix javadoc warnings exposed by -Xdoclint PiperOrigin-RevId: 284776943 --- .../google/android/exoplayer2/ExoPlayer.java | 4 +- .../google/android/exoplayer2/Renderer.java | 3 +- .../google/android/exoplayer2/Timeline.java | 50 +++++++++---------- .../trackselection/TrackSelector.java | 2 - 4 files changed, 29 insertions(+), 30 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index f02ec3dd43..0374c04cb3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -93,8 +93,8 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; * *

    The figure below shows ExoPlayer's threading model. * - *

    ExoPlayer's threading
- * model + *

    ExoPlayer's
+ * threading model * *

    * By default, the operation mode is set to {@link MediaCodecOperationMode#SYNCHRONOUS}. */ @@ -984,6 +993,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer { && Util.SDK_INT >= 23) { codecAdapter = new DedicatedThreadAsyncMediaCodecAdapter(codec, getTrackType()); ((DedicatedThreadAsyncMediaCodecAdapter) codecAdapter).start(); + } else if (mediaCodecOperationMode + == MediaCodecOperationMode.ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK + && Util.SDK_INT >= 23) { + codecAdapter = new MultiLockAsynchMediaCodecAdapter(codec, getTrackType()); + ((MultiLockAsynchMediaCodecAdapter) codecAdapter).start(); } else { codecAdapter = new SynchronousMediaCodecAdapter(codec, getDequeueOutputBufferTimeoutUs()); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsynchMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsynchMediaCodecAdapter.java new file mode 100644 index 0000000000..2fa40545d5 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsynchMediaCodecAdapter.java @@ -0,0 +1,344 @@ +/* + * 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.mediacodec; + +import android.media.MediaCodec; +import android.media.MediaFormat; +import android.os.Handler; +import android.os.HandlerThread; +import androidx.annotation.GuardedBy; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.IntArrayQueue; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayDeque; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in asynchronous mode + * and routes {@link MediaCodec.Callback} callbacks on a dedicated Thread that is managed + * internally. + * + *

    The main difference of this class compared to the {@link + * DedicatedThreadAsyncMediaCodecAdapter} is that its internal implementation applies finer-grained + * locking. The {@link DedicatedThreadAsyncMediaCodecAdapter} uses a single lock to synchronize + * access, whereas this class uses a different lock to access the available input and available + * output buffer indexes returned from the {@link MediaCodec}. This class assumes that the {@link + * MediaCodecAdapter} methods will be accessed by the Playback Thread and the {@link + * MediaCodec.Callback} methods will be accessed by the internal Thread. This class is + * NOT generally thread-safe in the sense that its public methods cannot be called + * by any thread. + * + *

    After creating an instance, you need to call {@link #start()} to start the internal Thread. + */ +@RequiresApi(23) +/* package */ final class MultiLockAsynchMediaCodecAdapter extends MediaCodec.Callback + implements MediaCodecAdapter { + + @IntDef({State.CREATED, State.STARTED, State.SHUT_DOWN}) + private @interface State { + int CREATED = 0; + int STARTED = 1; + int SHUT_DOWN = 2; + } + + private final MediaCodec codec; + private final Object inputBufferLock; + private final Object outputBufferLock; + private final Object objectStateLock; + + @GuardedBy("inputBufferLock") + private final IntArrayQueue availableInputBuffers; + + @GuardedBy("outputBufferLock") + private final IntArrayQueue availableOutputBuffers; + + @GuardedBy("outputBufferLock") + private final ArrayDeque bufferInfos; + + @GuardedBy("outputBufferLock") + private final ArrayDeque formats; + + @GuardedBy("objectStateLock") + @MonotonicNonNull + private MediaFormat currentFormat; + + @GuardedBy("objectStateLock") + private long pendingFlush; + + @GuardedBy("objectStateLock") + @Nullable + private IllegalStateException codecException; + + @GuardedBy("objectStateLock") + private @State int state; + + private final HandlerThread handlerThread; + @MonotonicNonNull private Handler handler; + private Runnable onCodecStart; + + /** Creates a new instance that wraps the specified {@link MediaCodec}. */ + /* package */ MultiLockAsynchMediaCodecAdapter(MediaCodec codec, int trackType) { + this(codec, new HandlerThread(createThreadLabel(trackType))); + } + + @VisibleForTesting + /* package */ MultiLockAsynchMediaCodecAdapter(MediaCodec codec, HandlerThread handlerThread) { + this.codec = codec; + inputBufferLock = new Object(); + outputBufferLock = new Object(); + objectStateLock = new Object(); + availableInputBuffers = new IntArrayQueue(); + availableOutputBuffers = new IntArrayQueue(); + bufferInfos = new ArrayDeque<>(); + formats = new ArrayDeque<>(); + codecException = null; + state = State.CREATED; + this.handlerThread = handlerThread; + onCodecStart = codec::start; + } + + /** + * Starts the operation of this instance. + * + *

    After a call to this method, make sure to call {@link #shutdown()} to terminate the internal + * Thread. You can only call this method once during the lifetime of an instance; calling this + * method again will throw an {@link IllegalStateException}. + * + * @throws IllegalStateException If this method has been called already. + */ + public void start() { + synchronized (objectStateLock) { + Assertions.checkState(state == State.CREATED); + + handlerThread.start(); + handler = new Handler(handlerThread.getLooper()); + codec.setCallback(this, handler); + state = State.STARTED; + } + } + + @Override + public int dequeueInputBufferIndex() { + synchronized (objectStateLock) { + Assertions.checkState(state == State.STARTED); + + if (isFlushing()) { + return MediaCodec.INFO_TRY_AGAIN_LATER; + } else { + maybeThrowException(); + return dequeueAvailableInputBufferIndex(); + } + } + } + + @Override + public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { + synchronized (objectStateLock) { + Assertions.checkState(state == State.STARTED); + + if (isFlushing()) { + return MediaCodec.INFO_TRY_AGAIN_LATER; + } else { + maybeThrowException(); + return dequeueAvailableOutputBufferIndex(bufferInfo); + } + } + } + + @Override + public MediaFormat getOutputFormat() { + synchronized (objectStateLock) { + Assertions.checkState(state == State.STARTED); + + if (currentFormat == null) { + throw new IllegalStateException(); + } + + return currentFormat; + } + } + + @Override + public void flush() { + synchronized (objectStateLock) { + Assertions.checkState(state == State.STARTED); + + codec.flush(); + pendingFlush++; + Util.castNonNull(handler).post(this::onFlushComplete); + } + } + + @Override + public void shutdown() { + synchronized (objectStateLock) { + if (state == State.STARTED) { + handlerThread.quit(); + } + state = State.SHUT_DOWN; + } + } + + @VisibleForTesting + /* package */ void setOnCodecStart(Runnable onCodecStart) { + this.onCodecStart = onCodecStart; + } + + private int dequeueAvailableInputBufferIndex() { + synchronized (inputBufferLock) { + return availableInputBuffers.isEmpty() + ? MediaCodec.INFO_TRY_AGAIN_LATER + : availableInputBuffers.remove(); + } + } + + @GuardedBy("objectStateLock") + private int dequeueAvailableOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { + int bufferIndex; + synchronized (outputBufferLock) { + if (availableOutputBuffers.isEmpty()) { + bufferIndex = MediaCodec.INFO_TRY_AGAIN_LATER; + } else { + bufferIndex = availableOutputBuffers.remove(); + if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + currentFormat = formats.remove(); + } else if (bufferIndex >= 0) { + MediaCodec.BufferInfo outBufferInfo = bufferInfos.remove(); + bufferInfo.set( + outBufferInfo.offset, + outBufferInfo.size, + outBufferInfo.presentationTimeUs, + outBufferInfo.flags); + } + } + } + return bufferIndex; + } + + @GuardedBy("objectStateLock") + private boolean isFlushing() { + return pendingFlush > 0; + } + + @GuardedBy("objectStateLock") + private void maybeThrowException() { + @Nullable IllegalStateException exception = codecException; + if (exception != null) { + codecException = null; + throw exception; + } + } + + // Called by the internal Thread. + + @Override + public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) { + synchronized (inputBufferLock) { + availableInputBuffers.add(index); + } + } + + @Override + public void onOutputBufferAvailable( + @NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) { + synchronized (outputBufferLock) { + availableOutputBuffers.add(index); + bufferInfos.add(info); + } + } + + @Override + public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) { + onMediaCodecError(e); + } + + @Override + public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) { + synchronized (outputBufferLock) { + availableOutputBuffers.add(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); + formats.add(format); + } + } + + @VisibleForTesting + /* package */ void onMediaCodecError(IllegalStateException e) { + synchronized (objectStateLock) { + codecException = e; + } + } + + private void onFlushComplete() { + synchronized (objectStateLock) { + if (state == State.SHUT_DOWN) { + return; + } + + --pendingFlush; + if (pendingFlush > 0) { + // Another flush() has been called. + return; + } else if (pendingFlush < 0) { + // This should never happen. + codecException = new IllegalStateException(); + return; + } + + clearAvailableInput(); + clearAvailableOutput(); + codecException = null; + try { + onCodecStart.run(); + } catch (IllegalStateException e) { + codecException = e; + } catch (Exception e) { + codecException = new IllegalStateException(e); + } + } + } + + private void clearAvailableInput() { + synchronized (inputBufferLock) { + availableInputBuffers.clear(); + } + } + + private void clearAvailableOutput() { + synchronized (outputBufferLock) { + availableOutputBuffers.clear(); + bufferInfos.clear(); + formats.clear(); + } + } + + private static String createThreadLabel(int trackType) { + StringBuilder labelBuilder = new StringBuilder("MediaCodecAsyncAdapter:"); + if (trackType == C.TRACK_TYPE_AUDIO) { + labelBuilder.append("Audio"); + } else if (trackType == C.TRACK_TYPE_VIDEO) { + labelBuilder.append("Video"); + } else { + labelBuilder.append("Unknown(").append(trackType).append(")"); + } + return labelBuilder.toString(); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java new file mode 100644 index 0000000000..815d6ab3da --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java @@ -0,0 +1,437 @@ +/* + * 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.mediacodec; + +import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.areEqual; +import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.waitUntilAllEventsAreExecuted; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.robolectric.Shadows.shadowOf; + +import android.media.MediaCodec; +import android.media.MediaFormat; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.shadows.ShadowLooper; + +/** Unit tests for {@link MultiLockAsynchMediaCodecAdapter}. */ +@RunWith(AndroidJUnit4.class) +public class MultiLockAsyncMediaCodecAdapterTest { + private MultiLockAsynchMediaCodecAdapter adapter; + private MediaCodec codec; + private MediaCodec.BufferInfo bufferInfo = null; + private MediaCodecAsyncCallback mediaCodecAsyncCallbackSpy; + private TestHandlerThread handlerThread; + + @Before + public void setup() throws IOException { + codec = MediaCodec.createByCodecName("h264"); + handlerThread = new TestHandlerThread("TestHandlerThread"); + adapter = new MultiLockAsynchMediaCodecAdapter(codec, handlerThread); + bufferInfo = new MediaCodec.BufferInfo(); + } + + @After + public void tearDown() { + adapter.shutdown(); + assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0); + } + + @Test + public void startAndShutdown_works() { + adapter.start(); + adapter.shutdown(); + } + + @Test + public void start_calledTwice_throwsException() { + adapter.start(); + try { + adapter.start(); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test + public void dequeueInputBufferIndex_withoutStart_throwsException() { + try { + adapter.dequeueInputBufferIndex(); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test + public void dequeueInputBufferIndex_afterShutdown_throwsException() { + adapter.start(); + adapter.shutdown(); + try { + adapter.dequeueInputBufferIndex(); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test + public void dequeueInputBufferIndex_withAfterFlushFailed_throwsException() + throws InterruptedException { + adapter.setOnCodecStart( + () -> { + throw new IllegalStateException("codec#start() exception"); + }); + adapter.start(); + adapter.flush(); + + assertThat( + waitUntilAllEventsAreExecuted( + handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS)) + .isTrue(); + try { + adapter.dequeueInputBufferIndex(); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test + public void dequeueInputBufferIndex_withoutInputBuffer_returnsTryAgainLater() { + adapter.start(); + + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeueInputBufferIndex_withInputBuffer_returnsInputBuffer() { + adapter.start(); + adapter.onInputBufferAvailable(codec, 0); + + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0); + } + + @Test + public void dequeueInputBufferIndex_withPendingFlush_returnsTryAgainLater() { + adapter.start(); + adapter.onInputBufferAvailable(codec, 0); + adapter.flush(); + + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer() + throws InterruptedException { + // Disable calling codec.start() after flush to avoid receiving buffers from the + // shadow codec impl + adapter.setOnCodecStart(() -> {}); + adapter.start(); + Looper looper = handlerThread.getLooper(); + Handler handler = new Handler(looper); + // Enqueue 10 callbacks from codec + for (int i = 0; i < 10; i++) { + int bufferIndex = i; + handler.post(() -> adapter.onInputBufferAvailable(codec, bufferIndex)); + } + adapter.flush(); // Enqueues a flush event after the onInputBufferAvailable callbacks + // Enqueue another onInputBufferAvailable after the flush event + handler.post(() -> adapter.onInputBufferAvailable(codec, 10)); + + // Wait until all tasks have been handled + assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(10); + } + + @Test + public void dequeueInputBufferIndex_withMediaCodecError_throwsException() { + adapter.start(); + adapter.onMediaCodecError(new IllegalStateException("error from codec")); + + try { + adapter.dequeueInputBufferIndex(); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test + public void dequeueOutputBufferIndex_withoutStart_throwsException() { + try { + adapter.dequeueOutputBufferIndex(bufferInfo); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test + public void dequeueOutputBufferIndex_afterShutdown_throwsException() { + adapter.start(); + adapter.shutdown(); + try { + adapter.dequeueOutputBufferIndex(bufferInfo); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test + public void dequeueOutputBufferIndex_withInternalException_throwsException() + throws InterruptedException { + adapter.setOnCodecStart( + () -> { + throw new RuntimeException("codec#start() exception"); + }); + adapter.start(); + adapter.flush(); + + assertThat( + waitUntilAllEventsAreExecuted( + handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS)) + .isTrue(); + try { + adapter.dequeueOutputBufferIndex(bufferInfo); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test + public void dequeueOutputBufferIndex_withoutInputBuffer_returnsTryAgainLater() { + adapter.start(); + + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeueOutputBufferIndex_withOutputBuffer_returnsOutputBuffer() { + adapter.start(); + MediaCodec.BufferInfo enqueuedBufferInfo = new MediaCodec.BufferInfo(); + adapter.onOutputBufferAvailable(codec, 0, enqueuedBufferInfo); + + assertThat(adapter.dequeueOutputBufferIndex((bufferInfo))).isEqualTo(0); + assertThat(areEqual(bufferInfo, enqueuedBufferInfo)).isTrue(); + } + + @Test + public void dequeueOutputBufferIndex_withPendingFlush_returnsTryAgainLater() { + adapter.start(); + adapter.dequeueOutputBufferIndex(bufferInfo); + adapter.flush(); + + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeueOutputBufferIndex_withFlushCompletedAndOutputBuffer_returnsOutputBuffer() + throws InterruptedException { + adapter.start(); + Looper looper = handlerThread.getLooper(); + Handler handler = new Handler(looper); + // Enqueue 10 callbacks from codec + for (int i = 0; i < 10; i++) { + int bufferIndex = i; + MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); + outBufferInfo.presentationTimeUs = i; + handler.post(() -> adapter.onOutputBufferAvailable(codec, bufferIndex, outBufferInfo)); + } + adapter.flush(); // Enqueues a flush event after the onOutputBufferAvailable callbacks + // Enqueue another onOutputBufferAvailable after the flush event + MediaCodec.BufferInfo lastBufferInfo = new MediaCodec.BufferInfo(); + lastBufferInfo.presentationTimeUs = 10; + handler.post(() -> adapter.onOutputBufferAvailable(codec, 10, lastBufferInfo)); + + // Wait until all tasks have been handled + assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(10); + assertThat(areEqual(bufferInfo, lastBufferInfo)).isTrue(); + } + + @Test + public void dequeueOutputBufferIndex_withMediaCodecError_throwsException() { + adapter.start(); + adapter.onMediaCodecError(new IllegalStateException("error from codec")); + + try { + adapter.dequeueOutputBufferIndex(bufferInfo); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test + public void getOutputFormat_withoutStart_throwsException() { + try { + adapter.getOutputFormat(); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test + public void getOutputFormat_afterShutdown_throwsException() { + adapter.start(); + adapter.shutdown(); + try { + adapter.getOutputFormat(); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test + public void getOutputFormat_withoutFormatReceived_throwsException() { + adapter.start(); + + try { + adapter.getOutputFormat(); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test + public void getOutputFormat_withMultipleFormats_returnsCorrectFormat() { + adapter.start(); + MediaFormat[] formats = new MediaFormat[10]; + for (int i = 0; i < formats.length; i++) { + formats[i] = new MediaFormat(); + adapter.onOutputFormatChanged(codec, formats[i]); + } + + for (int i = 0; i < 10; i++) { + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); + assertThat(adapter.getOutputFormat()).isEqualTo(formats[i]); + // A subsequent call to getOutputFormat() should return the previously fetched format + assertThat(adapter.getOutputFormat()).isEqualTo(formats[i]); + } + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void getOutputFormat_afterFlush_returnsPreviousFormat() throws InterruptedException { + MediaFormat format = new MediaFormat(); + adapter.start(); + adapter.onOutputFormatChanged(codec, format); + + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); + assertThat(adapter.getOutputFormat()).isEqualTo(format); + + adapter.flush(); + assertThat( + waitUntilAllEventsAreExecuted( + handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS)) + .isTrue(); + assertThat(adapter.getOutputFormat()).isEqualTo(format); + } + + @Test + public void flush_withoutStarted_throwsException() { + try { + adapter.flush(); + } catch (IllegalStateException expected) { + } + } + + @Test + public void flush_afterShutdown_throwsException() { + adapter.start(); + adapter.shutdown(); + try { + adapter.flush(); + } catch (IllegalStateException expected) { + } + } + + @Test + public void flush_multipleTimes_onlyLastFlushExecutes() throws InterruptedException { + AtomicInteger onCodecStartCount = new AtomicInteger(0); + adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet()); + adapter.start(); + Looper looper = handlerThread.getLooper(); + Handler handler = new Handler(looper); + handler.post(() -> adapter.onInputBufferAvailable(codec, 0)); + adapter.flush(); // Enqueues a flush event + handler.post(() -> adapter.onInputBufferAvailable(codec, 2)); + AtomicInteger milestoneCount = new AtomicInteger(0); + handler.post(() -> milestoneCount.incrementAndGet()); + adapter.flush(); // Enqueues a second flush event + handler.post(() -> adapter.onInputBufferAvailable(codec, 3)); + + // Progress the looper until the milestoneCount is increased - first flush event + // should have been a no-op + ShadowLooper shadowLooper = shadowOf(looper); + while (milestoneCount.get() < 1) { + shadowLooper.runOneTask(); + } + assertThat(onCodecStartCount.get()).isEqualTo(0); + + assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(3); + assertThat(onCodecStartCount.get()).isEqualTo(1); + } + + @Test + public void flush_andImmediatelyShutdown_flushIsNoOp() throws InterruptedException { + AtomicInteger onCodecStartCount = new AtomicInteger(0); + adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet()); + adapter.start(); + // Obtain looper when adapter is started. + Looper looper = handlerThread.getLooper(); + adapter.flush(); + adapter.shutdown(); + + assertThat(waitUntilAllEventsAreExecuted(looper, 5, TimeUnit.SECONDS)).isTrue(); + // Only shutdown flushes the MediaCodecAsync handler. + assertThat(onCodecStartCount.get()).isEqualTo(0); + } + + private static class TestHandlerThread extends HandlerThread { + + private static final AtomicLong INSTANCES_STARTED = new AtomicLong(0); + + public TestHandlerThread(String name) { + super(name); + } + + @Override + public synchronized void start() { + super.start(); + INSTANCES_STARTED.incrementAndGet(); + } + + @Override + public boolean quit() { + boolean quit = super.quit(); + INSTANCES_STARTED.decrementAndGet(); + return quit; + } + } +} From fdfbfc0b8ea1cc834e61f18042406a9a5dfb8b86 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 17 Dec 2019 12:30:49 +0000 Subject: [PATCH 0535/1335] Cleanup codec state reset methods in MediaCodecRenderer - Remove duplicated null assignments - Move mediaCryptoRequiresSecureDecoder reset to be with all the other mediaCrypto stuff. PiperOrigin-RevId: 285955134 --- .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 4 ---- 1 file changed, 4 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 de975b1536..9f6ff1212c 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 @@ -840,10 +840,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { resetCodecStateForFlush(); availableCodecInfos = null; - codec = null; codecInfo = null; codecFormat = null; - codecAdapter = null; codecOperatingRate = CODEC_OPERATING_RATE_UNSET; codecAdaptationWorkaroundMode = ADAPTATION_WORKAROUND_MODE_NEVER; codecNeedsReconfigureWorkaround = false; @@ -856,8 +854,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecReconfigured = false; codecReconfigurationState = RECONFIGURATION_STATE_NONE; resetCodecBuffers(); - - mediaCrypto = null; mediaCryptoRequiresSecureDecoder = false; } From 04b1782a53526a7ee8519cac41e15264ef70aee2 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 17 Dec 2019 12:39:31 +0000 Subject: [PATCH 0536/1335] Add a Cue.Builder I want to add fields related to vertical text support, and neither adding another constructor nor updating all call-sites of existing constructors seemed like attractive propositions. PiperOrigin-RevId: 285956024 --- .../google/android/exoplayer2/text/Cue.java | 240 ++++++++++++++++++ .../android/exoplayer2/text/CueTest.java | 76 ++++++ 2 files changed, 316 insertions(+) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.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 8880a76e1e..25b71d9410 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 @@ -17,9 +17,12 @@ package com.google.android.exoplayer2.text; import android.graphics.Bitmap; import android.graphics.Color; +import android.text.Layout; import android.text.Layout.Alignment; +import androidx.annotation.ColorInt; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -240,7 +243,9 @@ public final class Cue { * @param height The height of the cue as a fraction of the viewport height, or {@link * #DIMEN_UNSET} if the bitmap should be displayed at its natural height for the specified * {@code width}. + * @deprecated Use {@link Builder}. */ + @Deprecated public Cue( Bitmap bitmap, float horizontalPosition, @@ -271,7 +276,9 @@ public final class Cue { * {@link #TYPE_UNSET} and whose dimension parameters are set to {@link #DIMEN_UNSET}. * * @param text See {@link #text}. + * @deprecated Use {@link Builder}. */ + @Deprecated public Cue(CharSequence text) { this( text, @@ -295,7 +302,9 @@ public final class Cue { * @param position See {@link #position}. * @param positionAnchor See {@link #positionAnchor}. * @param size See {@link #size}. + * @deprecated Use {@link Builder}. */ + @Deprecated public Cue( CharSequence text, @Nullable Alignment textAlignment, @@ -331,7 +340,9 @@ public final class Cue { * @param size See {@link #size}. * @param textSizeType See {@link #textSizeType}. * @param textSize See {@link #textSize}. + * @deprecated Use {@link Builder}. */ + @Deprecated public Cue( CharSequence text, @Nullable Alignment textAlignment, @@ -373,7 +384,9 @@ public final class Cue { * @param size See {@link #size}. * @param windowColorSet See {@link #windowColorSet}. * @param windowColor See {@link #windowColor}. + * @deprecated Use {@link Builder}. */ + @Deprecated public Cue( CharSequence text, @Nullable Alignment textAlignment, @@ -417,6 +430,12 @@ public final class Cue { float bitmapHeight, boolean windowColorSet, int windowColor) { + // Exactly one of text or bitmap should be set. + if (text == null) { + Assertions.checkNotNull(bitmap); + } else { + Assertions.checkArgument(bitmap == null); + } this.text = text; this.textAlignment = textAlignment; this.bitmap = bitmap; @@ -433,4 +452,225 @@ public final class Cue { this.textSize = textSize; } + /** A builder for {@link Cue} objects. */ + public static final class Builder { + @Nullable private CharSequence text; + @Nullable private Bitmap bitmap; + @Nullable private Alignment textAlignment; + private float line; + @LineType private int lineType; + @AnchorType private int lineAnchor; + private float position; + @AnchorType private int positionAnchor; + @TextSizeType private int textSizeType; + private float textSize; + private float size; + private float bitmapHeight; + private boolean windowColorSet; + @ColorInt private int windowColor; + + public Builder() { + text = null; + bitmap = null; + textAlignment = null; + line = DIMEN_UNSET; + lineType = TYPE_UNSET; + lineAnchor = TYPE_UNSET; + position = DIMEN_UNSET; + positionAnchor = TYPE_UNSET; + textSizeType = TYPE_UNSET; + textSize = DIMEN_UNSET; + size = DIMEN_UNSET; + bitmapHeight = DIMEN_UNSET; + windowColorSet = false; + windowColor = Color.BLACK; + } + + /** + * Sets the cue text. + * + *

    Note that {@code text} may be decorated with styling spans. + * + * @see Cue#text + */ + public Builder setText(CharSequence text) { + this.text = text; + return this; + } + + /** Sets the cue image. */ + public Builder setBitmap(Bitmap bitmap) { + this.bitmap = bitmap; + return this; + } + + /** + * Sets the alignment of the cue text within the cue box. + * + *

    Passing null means the alignment is undefined. + * + * @see Cue#textAlignment + */ + public Builder setTextAlignment(@Nullable Layout.Alignment textAlignment) { + this.textAlignment = textAlignment; + return this; + } + + /** + * Sets the position of the {@code lineAnchor} of the cue box within the viewport in the + * direction orthogonal to the writing direction. + * + *

    The interpretation of the {@code line} depends on the value of {@code lineType}. + * + *

      + *
    • {@link #LINE_TYPE_FRACTION} indicates that {@code line} is a fractional position within + * the viewport. + *
    • {@link #LINE_TYPE_NUMBER} indicates that {@code line} is a line number, where the size + * of each line is taken to be the size of the first line of the cue. + *
        + *
      • When {@code line} is greater than or equal to 0 lines count from the start of the + * viewport, with 0 indicating zero offset from the start edge. + *
      • When {@code line} is negative lines count from the end of the viewport, with -1 + * indicating zero offset from the end edge. + *
      • For horizontal text the line spacing is the height of the first line of the cue, + * and the start and end of the viewport are the top and bottom respectively. + *
      + *
    + * + *

    Note that it's particularly important to consider the effect of {@link #setLineAnchor(int) + * lineAnchor} when using {@link #LINE_TYPE_NUMBER}. + * + *

      + *
    • {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)} positions a (potentially + * multi-line) cue at the very start of the viewport. + *
    • {@code (line == -1 && lineAnchor == ANCHOR_TYPE_END)} positions a (potentially + * multi-line) cue at the very end of the viewport. + *
    • {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)} and {@code (line == -1 && + * lineAnchor == ANCHOR_TYPE_START)} position cues entirely outside of the viewport. + *
    • {@code (line == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only the + * last line is visible at the start of the viewport. + *
    • {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a cue so that only its + * first line is visible at the end of the viewport. + *
    + * + * @see Cue#line + * @see Cue#lineType + */ + public Builder setLine(float line, @LineType int lineType) { + this.line = line; + this.lineType = lineType; + return this; + } + + /** + * Sets the cue box anchor positioned by {@link #setLine(float, int) line}. + * + *

    For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link + * #ANCHOR_TYPE_MIDDLE} and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of + * the cue box respectively. + * + * @see Cue#lineAnchor + */ + public Builder setLineAnchor(@AnchorType int lineAnchor) { + this.lineAnchor = lineAnchor; + return this; + } + + /** + * Sets the fractional position of the {@link #setPositionAnchor(int) positionAnchor} of the cue + * box within the viewport in the direction orthogonal to {@link #setLine(float, int) line}. + * + *

    For horizontal text, this is the horizontal position relative to the left of the viewport. + * Note that positioning is relative to the left of the viewport even in the case of + * right-to-left text. + * + * @see Cue#position + */ + public Builder setPosition(float position) { + this.position = position; + return this; + } + + /** + * Sets the cue box anchor positioned by {@link #setPosition(float) position}. + * + *

    For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link + * #ANCHOR_TYPE_MIDDLE} and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of + * the cue box respectively. + * + * @see Cue#positionAnchor + */ + public Builder setPositionAnchor(@AnchorType int positionAnchor) { + this.positionAnchor = positionAnchor; + return this; + } + + /** + * Sets the default text size type for this cue's text. + * + * @see Cue#textSize + * @see Cue#textSizeType + */ + public Builder setTextSize(float textSize, @TextSizeType int textSizeType) { + this.textSize = textSize; + this.textSizeType = textSizeType; + return this; + } + + /** + * Sets the size of the cue box in the writing direction specified as a fraction of the viewport + * size in that direction. + * + * @see Cue#textSize + * @see Cue#textSizeType + * @see Cue#size + */ + public Builder setSize(float size) { + this.size = size; + return this; + } + + /** + * Sets the bitmap height as a fraction of the of the viewport size. + * + * @see Cue#bitmapHeight + */ + public Builder setBitmapHeight(float bitmapHeight) { + this.bitmapHeight = bitmapHeight; + return this; + } + + /** + * Sets the fill color of the window. + * + *

    Also sets {@link Cue#windowColorSet} to true. + * + * @see Cue#windowColor + * @see Cue#windowColorSet + */ + public Builder setWindowColor(@ColorInt int windowColor) { + this.windowColor = windowColor; + this.windowColorSet = true; + return this; + } + + /** Build the cue. */ + public Cue build() { + return new Cue( + text, + textAlignment, + bitmap, + line, + lineType, + lineAnchor, + position, + positionAnchor, + textSizeType, + textSize, + size, + bitmapHeight, + windowColorSet, + windowColor); + } + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.java new file mode 100644 index 0000000000..bd1acdf02b --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.java @@ -0,0 +1,76 @@ +/* + * 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; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.text.Layout; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link Cue}. */ +@RunWith(AndroidJUnit4.class) +public class CueTest { + + @Test + public void buildSucceeds() { + Cue cue = + new Cue.Builder() + .setText("text") + .setTextAlignment(Layout.Alignment.ALIGN_CENTER) + .setLine(5, Cue.LINE_TYPE_NUMBER) + .setLineAnchor(Cue.ANCHOR_TYPE_END) + .setPosition(0.4f) + .setPositionAnchor(Cue.ANCHOR_TYPE_MIDDLE) + .setTextSize(0.2f, Cue.TEXT_SIZE_TYPE_FRACTIONAL) + .setSize(0.8f) + .setWindowColor(Color.CYAN) + .build(); + + assertThat(cue.text).isEqualTo("text"); + assertThat(cue.textAlignment).isEqualTo(Layout.Alignment.ALIGN_CENTER); + assertThat(cue.line).isEqualTo(5); + assertThat(cue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER); + assertThat(cue.position).isEqualTo(0.4f); + assertThat(cue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + assertThat(cue.textSize).isEqualTo(0.2f); + assertThat(cue.textSizeType).isEqualTo(Cue.TEXT_SIZE_TYPE_FRACTIONAL); + assertThat(cue.size).isEqualTo(0.8f); + assertThat(cue.windowColor).isEqualTo(Color.CYAN); + assertThat(cue.windowColorSet).isTrue(); + } + + @Test + public void buildWithNoTextOrBitmapFails() { + assertThrows(RuntimeException.class, () -> new Cue.Builder().build()); + } + + @Test + public void buildWithBothTextAndBitmapFails() { + assertThrows( + RuntimeException.class, + () -> + new Cue.Builder() + .setText("foo") + .setBitmap(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)) + .build()); + } +} From f10bc37831ca72dafc7ccf451ea82f7f83da3e24 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 17 Dec 2019 12:44:15 +0000 Subject: [PATCH 0537/1335] Migrate usages of Cue's bitmap constructor to Cue.Builder PiperOrigin-RevId: 285956436 --- .../google/android/exoplayer2/text/Cue.java | 44 ------------------- .../exoplayer2/text/dvb/DvbParser.java | 23 +++++++--- .../exoplayer2/text/pgs/PgsDecoder.java | 17 +++---- .../exoplayer2/text/ttml/TtmlNode.java | 17 +++---- 4 files changed, 35 insertions(+), 66 deletions(-) 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 25b71d9410..1aecc09385 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 @@ -227,50 +227,6 @@ public final class Cue { */ public final float textSize; - /** - * Creates an image cue. - * - * @param bitmap See {@link #bitmap}. - * @param horizontalPosition The position of the horizontal anchor within the viewport, expressed - * as a fraction of the viewport width. - * @param horizontalPositionAnchor The horizontal anchor. One of {@link #ANCHOR_TYPE_START}, - * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. - * @param verticalPosition The position of the vertical anchor within the viewport, expressed as a - * fraction of the viewport height. - * @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START}, {@link - * #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. - * @param width The width of the cue as a fraction of the viewport width. - * @param height The height of the cue as a fraction of the viewport height, or {@link - * #DIMEN_UNSET} if the bitmap should be displayed at its natural height for the specified - * {@code width}. - * @deprecated Use {@link Builder}. - */ - @Deprecated - public Cue( - Bitmap bitmap, - float horizontalPosition, - @AnchorType int horizontalPositionAnchor, - float verticalPosition, - @AnchorType int verticalPositionAnchor, - float width, - float height) { - this( - /* text= */ null, - /* textAlignment= */ null, - bitmap, - verticalPosition, - /* lineType= */ LINE_TYPE_FRACTION, - verticalPositionAnchor, - horizontalPosition, - horizontalPositionAnchor, - /* textSizeType= */ TYPE_UNSET, - /* textSize= */ DIMEN_UNSET, - width, - height, - /* windowColorSet= */ false, - /* windowColor= */ Color.BLACK); - } - /** * Creates a text cue whose {@link #textAlignment} is null, whose type parameters are set to * {@link #TYPE_UNSET} and whose dimension parameters are set to {@link #DIMEN_UNSET}. 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 e1cde75532..228973ce0c 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 @@ -208,12 +208,23 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; fillRegionPaint); } - Bitmap cueBitmap = Bitmap.createBitmap(bitmap, baseHorizontalAddress, baseVerticalAddress, - regionComposition.width, regionComposition.height); - cues.add(new Cue(cueBitmap, (float) baseHorizontalAddress / displayDefinition.width, - Cue.ANCHOR_TYPE_START, (float) baseVerticalAddress / displayDefinition.height, - Cue.ANCHOR_TYPE_START, (float) regionComposition.width / displayDefinition.width, - (float) regionComposition.height / displayDefinition.height)); + cues.add( + new Cue.Builder() + .setBitmap( + Bitmap.createBitmap( + bitmap, + baseHorizontalAddress, + baseVerticalAddress, + regionComposition.width, + regionComposition.height)) + .setPosition((float) baseHorizontalAddress / displayDefinition.width) + .setPositionAnchor(Cue.ANCHOR_TYPE_START) + .setLine( + (float) baseVerticalAddress / displayDefinition.height, Cue.LINE_TYPE_FRACTION) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .setSize((float) regionComposition.width / displayDefinition.width) + .setBitmapHeight((float) regionComposition.height / displayDefinition.height) + .build()); canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); // Restore clean clipping state. 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 9ef3556c8f..fe8bf12d47 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 @@ -235,14 +235,15 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { Bitmap bitmap = Bitmap.createBitmap(argbBitmapData, bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); // Build the cue. - return new Cue( - bitmap, - (float) bitmapX / planeWidth, - Cue.ANCHOR_TYPE_START, - (float) bitmapY / planeHeight, - Cue.ANCHOR_TYPE_START, - (float) bitmapWidth / planeWidth, - (float) bitmapHeight / planeHeight); + return new Cue.Builder() + .setBitmap(bitmap) + .setPosition((float) bitmapX / planeWidth) + .setPositionAnchor(Cue.ANCHOR_TYPE_START) + .setLine((float) bitmapY / planeHeight, Cue.LINE_TYPE_FRACTION) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .setSize((float) bitmapWidth / planeWidth) + .setBitmapHeight((float) bitmapHeight / planeHeight) + .build(); } public void reset() { 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 3365749e1a..b025a4e139 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 @@ -228,14 +228,15 @@ import java.util.TreeSet; TtmlRegion region = regionMap.get(regionImagePair.first); cues.add( - new Cue( - bitmap, - region.position, - Cue.ANCHOR_TYPE_START, - region.line, - region.lineAnchor, - region.width, - region.height)); + new Cue.Builder() + .setBitmap(bitmap) + .setPosition(region.position) + .setPositionAnchor(Cue.ANCHOR_TYPE_START) + .setLine(region.line, Cue.LINE_TYPE_FRACTION) + .setLineAnchor(region.lineAnchor) + .setSize(region.width) + .setBitmapHeight(region.height) + .build()); } // Create text based cues. From dfc15733d2bba16f10bcce6bf8b9395357d90fd2 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 17 Dec 2019 13:34:33 +0000 Subject: [PATCH 0538/1335] Add NonNull annotations to the most extractor packages PiperOrigin-RevId: 285961788 --- .../extractor/amr/AmrExtractor.java | 21 +++++++++++--- .../extractor/amr/package-info.java | 19 ++++++++++++ .../extractor/flac/package-info.java | 19 ++++++++++++ .../extractor/flv/FlvExtractor.java | 13 +++++++-- .../extractor/flv/package-info.java | 19 ++++++++++++ .../extractor/mkv/DefaultEbmlReader.java | 14 +++++---- .../extractor/mkv/MatroskaExtractor.java | 23 ++++++++------- .../extractor/mkv/package-info.java | 19 ++++++++++++ .../extractor/mp3/Mp3Extractor.java | 29 ++++++++++++++----- .../exoplayer2/extractor/mp3/VbriSeeker.java | 3 +- .../exoplayer2/extractor/mp3/XingSeeker.java | 3 +- .../extractor/mp3/package-info.java | 19 ++++++++++++ .../extractor/ogg/DefaultOggSeeker.java | 5 +++- .../exoplayer2/extractor/ogg/FlacReader.java | 5 ++-- .../extractor/ogg/OggExtractor.java | 9 ++++-- .../exoplayer2/extractor/ogg/OggSeeker.java | 6 ++-- .../extractor/ogg/StreamReader.java | 7 +++-- .../extractor/ogg/VorbisReader.java | 2 +- .../extractor/ogg/package-info.java | 19 ++++++++++++ .../extractor/rawcc/RawCcExtractor.java | 10 ++++--- .../extractor/rawcc/package-info.java | 19 ++++++++++++ .../extractor/wav/package-info.java | 19 ++++++++++++ 22 files changed, 255 insertions(+), 47 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/package-info.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java index f6b64245fc..6a31c941d2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.extractor.amr; 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.ParserException; @@ -28,6 +27,7 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.EOFException; @@ -36,6 +36,9 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; 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; /** * Extracts data from the AMR containers format (either AMR or AMR-WB). This follows RFC-4867, @@ -138,9 +141,9 @@ public final class AmrExtractor implements Extractor { private int numSamplesWithSameSize; private long timeOffsetUs; - private ExtractorOutput extractorOutput; - private TrackOutput trackOutput; - @Nullable private SeekMap seekMap; + @MonotonicNonNull private ExtractorOutput extractorOutput; + @MonotonicNonNull private TrackOutput trackOutput; + @MonotonicNonNull private SeekMap seekMap; private boolean hasOutputFormat; public AmrExtractor() { @@ -171,6 +174,7 @@ public final class AmrExtractor implements Extractor { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + assertInitialized(); if (input.getPosition() == 0) { if (!readAmrHeader(input)) { throw new ParserException("Could not find AMR header."); @@ -245,6 +249,7 @@ public final class AmrExtractor implements Extractor { return Arrays.equals(header, amrSignature); } + @RequiresNonNull("trackOutput") private void maybeOutputFormat() { if (!hasOutputFormat) { hasOutputFormat = true; @@ -267,6 +272,7 @@ public final class AmrExtractor implements Extractor { } } + @RequiresNonNull("trackOutput") private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException { if (currentSampleBytesRemaining == 0) { try { @@ -346,6 +352,7 @@ public final class AmrExtractor implements Extractor { return !isWideBand && (frameType < 12 || frameType > 14); } + @RequiresNonNull("extractorOutput") private void maybeOutputSeekMap(long inputLength, int sampleReadResult) { if (hasOutputSeekMap) { return; @@ -370,6 +377,12 @@ public final class AmrExtractor implements Extractor { return new ConstantBitrateSeekMap(inputLength, firstSamplePosition, bitrate, firstSampleSize); } + @EnsuresNonNull({"extractorOutput", "trackOutput"}) + private void assertInitialized() { + Assertions.checkStateNotNull(trackOutput); + Util.castNonNull(extractorOutput); + } + /** * Returns the stream bitrate, given a frame size and the duration of that frame in microseconds. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/package-info.java new file mode 100644 index 0000000000..31d58fadc9 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/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.extractor.amr; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/package-info.java new file mode 100644 index 0000000000..44d3427910 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/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.extractor.flac; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java index f6835558f2..0ec98f9653 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java @@ -23,11 +23,14 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Extracts data from the FLV container format. @@ -71,7 +74,7 @@ public final class FlvExtractor implements Extractor { private final ParsableByteArray tagData; private final ScriptTagPayloadReader metadataReader; - private ExtractorOutput extractorOutput; + @MonotonicNonNull private ExtractorOutput extractorOutput; private @States int state; private boolean outputFirstSample; private long mediaTagTimestampOffsetUs; @@ -80,8 +83,8 @@ public final class FlvExtractor implements Extractor { private int tagDataSize; private long tagTimestampUs; private boolean outputSeekMap; - private AudioTagPayloadReader audioReader; - private VideoTagPayloadReader videoReader; + @MonotonicNonNull private AudioTagPayloadReader audioReader; + @MonotonicNonNull private VideoTagPayloadReader videoReader; public FlvExtractor() { scratch = new ParsableByteArray(4); @@ -143,6 +146,7 @@ public final class FlvExtractor implements Extractor { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + Assertions.checkStateNotNull(extractorOutput); // Asserts that init has been called. while (true) { switch (state) { case STATE_READING_FLV_HEADER: @@ -178,6 +182,7 @@ public final class FlvExtractor implements Extractor { * @throws IOException If an error occurred reading or parsing data from the source. * @throws InterruptedException If the thread was interrupted. */ + @RequiresNonNull("extractorOutput") private boolean readFlvHeader(ExtractorInput input) throws IOException, InterruptedException { if (!input.readFully(headerBuffer.data, 0, FLV_HEADER_SIZE, true)) { // We've reached the end of the stream. @@ -250,6 +255,7 @@ public final class FlvExtractor implements Extractor { * @throws IOException If an error occurred reading or parsing data from the source. * @throws InterruptedException If the thread was interrupted. */ + @RequiresNonNull("extractorOutput") private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException { boolean wasConsumed = true; boolean wasSampleOutput = false; @@ -293,6 +299,7 @@ public final class FlvExtractor implements Extractor { return tagData; } + @RequiresNonNull("extractorOutput") private void ensureReadyForMediaOutput() { if (!outputSeekMap) { extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/package-info.java new file mode 100644 index 0000000000..e726bb50e2 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/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.extractor.flv; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java index b5da6dbf2f..5e6164249b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.mkv; + import androidx.annotation.IntDef; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; @@ -26,6 +27,8 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayDeque; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Default implementation of {@link EbmlReader}. @@ -52,7 +55,7 @@ import java.util.ArrayDeque; private final ArrayDeque masterElementsStack; private final VarintReader varintReader; - private EbmlProcessor processor; + @MonotonicNonNull private EbmlProcessor processor; private @ElementState int elementState; private int elementId; private long elementContentSize; @@ -79,8 +82,8 @@ import java.util.ArrayDeque; public boolean read(ExtractorInput input) throws IOException, InterruptedException { Assertions.checkNotNull(processor); while (true) { - if (!masterElementsStack.isEmpty() - && input.getPosition() >= masterElementsStack.peek().elementEndPosition) { + MasterElement head = masterElementsStack.peek(); + if (head != null && input.getPosition() >= head.elementEndPosition) { processor.endMasterElement(masterElementsStack.pop().elementId); return true; } @@ -159,8 +162,9 @@ import java.util.ArrayDeque; * @throws IOException If an error occurs reading from the input. * @throws InterruptedException If the thread is interrupted. */ - private long maybeResyncToNextLevel1Element(ExtractorInput input) throws IOException, - InterruptedException { + @RequiresNonNull("processor") + private long maybeResyncToNextLevel1Element(ExtractorInput input) + throws IOException, InterruptedException { input.resetPeekPosition(); while (true) { input.peekFully(scratch, 0, MAX_ID_BYTES); 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 b30fbf105e..ed2acc5898 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 @@ -57,6 +57,8 @@ import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.UUID; +import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** Extracts data from the Matroska and WebM container formats. */ public class MatroskaExtractor implements Extractor { @@ -342,7 +344,7 @@ public class MatroskaExtractor implements Extractor { private long durationUs = C.TIME_UNSET; // The track corresponding to the current TrackEntry element, or null. - private Track currentTrack; + @Nullable private Track currentTrack; // Whether a seek map has been sent to the output. private boolean sentSeekMap; @@ -356,8 +358,8 @@ public class MatroskaExtractor implements Extractor { private long cuesContentPosition = C.POSITION_UNSET; private long seekPositionAfterBuildingCues = C.POSITION_UNSET; private long clusterTimecodeUs = C.TIME_UNSET; - private LongArray cueTimesUs; - private LongArray cueClusterPositions; + @Nullable private LongArray cueTimesUs; + @Nullable private LongArray cueClusterPositions; private boolean seenClusterPositionForCurrentCuePoint; // Reading state. @@ -372,8 +374,7 @@ public class MatroskaExtractor implements Extractor { private int[] blockSampleSizes; private int blockTrackNumber; private int blockTrackNumberLength; - @C.BufferFlags - private int blockFlags; + @C.BufferFlags private int blockFlags; private int blockAdditionalId; private boolean blockHasReferenceBlock; @@ -389,7 +390,7 @@ public class MatroskaExtractor implements Extractor { private boolean sampleInitializationVectorRead; // Extractor outputs. - private ExtractorOutput extractorOutput; + @MonotonicNonNull private ExtractorOutput extractorOutput; public MatroskaExtractor() { this(0); @@ -415,6 +416,7 @@ public class MatroskaExtractor implements Extractor { encryptionInitializationVector = new ParsableByteArray(ENCRYPTION_IV_SIZE); encryptionSubsampleData = new ParsableByteArray(); blockAdditionalData = new ParsableByteArray(); + blockSampleSizes = new int[1]; } @Override @@ -1924,7 +1926,7 @@ public class MatroskaExtractor implements Extractor { String mimeType; int maxInputSize = Format.NO_VALUE; @C.PcmEncoding int pcmEncoding = Format.NO_VALUE; - List initializationData = null; + @Nullable List initializationData = null; switch (codecId) { case CODEC_ID_VP8: mimeType = MimeTypes.VIDEO_VP8; @@ -1958,7 +1960,8 @@ public class MatroskaExtractor implements Extractor { nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength; break; case CODEC_ID_FOURCC: - Pair> pair = parseFourCcPrivate(new ParsableByteArray(codecPrivate)); + Pair> pair = + parseFourCcPrivate(new ParsableByteArray(codecPrivate)); mimeType = pair.first; initializationData = pair.second; break; @@ -2220,8 +2223,8 @@ public class MatroskaExtractor implements Extractor { * is {@code null}. * @throws ParserException If the initialization data could not be built. */ - private static Pair> parseFourCcPrivate(ParsableByteArray buffer) - throws ParserException { + private static Pair> parseFourCcPrivate( + ParsableByteArray buffer) throws ParserException { try { buffer.skipBytes(16); // size(4), width(4), height(4), planes(2), bitcount(2). long compression = buffer.readLittleEndianUnsignedInt(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/package-info.java new file mode 100644 index 0000000000..15629ba584 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/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.extractor.mkv; + +import com.google.android.exoplayer2.util.NonNullApi; 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 7a25677c55..c69cdac1e9 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 @@ -34,12 +34,17 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; import com.google.android.exoplayer2.metadata.id3.MlltFrame; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Extracts data from the MP3 container format. @@ -107,13 +112,13 @@ public final class Mp3Extractor implements Extractor { private final Id3Peeker id3Peeker; // Extractor outputs. - private ExtractorOutput extractorOutput; - private TrackOutput trackOutput; + @MonotonicNonNull private ExtractorOutput extractorOutput; + @MonotonicNonNull private TrackOutput trackOutput; private int synchronizedHeaderData; - private Metadata metadata; - @Nullable private Seeker seeker; + @Nullable private Metadata metadata; + @MonotonicNonNull private Seeker seeker; private boolean disableSeeking; private long basisTimeUs; private long samplesRead; @@ -176,6 +181,7 @@ public final class Mp3Extractor implements Extractor { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + assertInitialized(); if (synchronizedHeaderData == 0) { try { synchronize(input, false); @@ -242,6 +248,7 @@ public final class Mp3Extractor implements Extractor { // Internal methods. + @RequiresNonNull({"trackOutput", "seeker"}) private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException { if (sampleBytesRemaining == 0) { extractorInput.resetPeekPosition(); @@ -390,6 +397,7 @@ public final class Mp3Extractor implements Extractor { * @throws InterruptedException Thrown if reading from the stream was interrupted. Not expected if * the next two frames were already peeked during synchronization. */ + @Nullable private Seeker maybeReadSeekFrame(ExtractorInput input) throws IOException, InterruptedException { ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize); input.peekFully(frame.data, 0, synchronizedHeader.frameSize); @@ -397,7 +405,7 @@ public final class Mp3Extractor implements Extractor { ? (synchronizedHeader.channels != 1 ? 36 : 21) // MPEG 1 : (synchronizedHeader.channels != 1 ? 21 : 13); // MPEG 2 or 2.5 int seekHeader = getSeekFrameHeader(frame, xingBase); - Seeker seeker; + @Nullable Seeker seeker; if (seekHeader == SEEK_HEADER_XING || seekHeader == SEEK_HEADER_INFO) { seeker = XingSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame); if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) { @@ -435,6 +443,12 @@ public final class Mp3Extractor implements Extractor { return new ConstantBitrateSeeker(input.getLength(), input.getPosition(), synchronizedHeader); } + @EnsuresNonNull({"extractorOutput", "trackOutput"}) + private void assertInitialized() { + Assertions.checkStateNotNull(trackOutput); + Util.castNonNull(extractorOutput); + } + /** * Returns whether the headers match in those bits masked by {@link #MPEG_AUDIO_HEADER_MASK}. */ @@ -465,7 +479,8 @@ public final class Mp3Extractor implements Extractor { } @Nullable - private static MlltSeeker maybeHandleSeekMetadata(Metadata metadata, long firstFramePosition) { + private static MlltSeeker maybeHandleSeekMetadata( + @Nullable Metadata metadata, long firstFramePosition) { if (metadata != null) { int length = metadata.length(); for (int i = 0; i < length; i++) { @@ -477,6 +492,4 @@ public final class Mp3Extractor implements Extractor { } return null; } - - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java index 86551319e1..dcccc675ed 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java @@ -41,7 +41,8 @@ import com.google.android.exoplayer2.util.Util; * @return A {@link VbriSeeker} for seeking in the stream, or {@code null} if the required * information is not present. */ - public static @Nullable VbriSeeker create( + @Nullable + public static VbriSeeker create( long inputLength, long position, MpegAudioHeader mpegAudioHeader, ParsableByteArray frame) { frame.skipBytes(10); int numFrames = frame.readInt(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java index db1a0199ac..aa1d8be316 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java @@ -42,7 +42,8 @@ import com.google.android.exoplayer2.util.Util; * @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required * information is not present. */ - public static @Nullable XingSeeker create( + @Nullable + public static XingSeeker create( long inputLength, long position, MpegAudioHeader mpegAudioHeader, ParsableByteArray frame) { int samplesPerFrame = mpegAudioHeader.samplesPerFrame; int sampleRate = mpegAudioHeader.sampleRate; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/package-info.java new file mode 100644 index 0000000000..3483b26b47 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/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.extractor.mp3; + +import com.google.android.exoplayer2.util.NonNullApi; 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 51ab94ba0e..ecbaaeb143 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ogg; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; @@ -39,7 +40,7 @@ import java.io.IOException; private static final int STATE_SKIP = 3; private static final int STATE_IDLE = 4; - private final OggPageHeader pageHeader = new OggPageHeader(); + private final OggPageHeader pageHeader; private final long payloadStartPosition; private final long payloadEndPosition; private final StreamReader streamReader; @@ -83,6 +84,7 @@ import java.io.IOException; } else { state = STATE_SEEK_TO_END; } + pageHeader = new OggPageHeader(); } @Override @@ -121,6 +123,7 @@ import java.io.IOException; } @Override + @Nullable public OggSeekMap createSeekMap() { return totalGranules != 0 ? new OggSeekMap() : null; } 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 f99b2420cc..258390d21d 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ogg; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.FlacFrameReader; import com.google.android.exoplayer2.extractor.FlacMetadataReader; @@ -37,8 +38,8 @@ import java.util.Arrays; private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4; - private FlacStreamMetadata streamMetadata; - private FlacOggSeeker flacOggSeeker; + @Nullable private FlacStreamMetadata streamMetadata; + @Nullable private FlacOggSeeker flacOggSeeker; public static boolean verifyBitstreamType(ParsableByteArray data) { return data.bytesLeft() >= 5 && data.readUnsignedByte() == 0x7F && // packet type diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java index 5e74eab8d4..4c13cc946c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java @@ -23,8 +23,11 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Extracts data from the Ogg container format. @@ -36,8 +39,8 @@ public class OggExtractor implements Extractor { private static final int MAX_VERIFICATION_BYTES = 8; - private ExtractorOutput output; - private StreamReader streamReader; + @MonotonicNonNull private ExtractorOutput output; + @MonotonicNonNull private StreamReader streamReader; private boolean streamReaderInitialized; @Override @@ -69,6 +72,7 @@ public class OggExtractor implements Extractor { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + Assertions.checkStateNotNull(output); // Asserts that init has been called. if (streamReader == null) { if (!sniffInternal(input)) { throw new ParserException("Failed to determine bitstream type"); @@ -84,6 +88,7 @@ public class OggExtractor implements Extractor { return streamReader.read(input, seekPosition); } + @EnsuresNonNullIf(expression = "streamReader", result = true) private boolean sniffInternal(ExtractorInput input) throws IOException, InterruptedException { OggPageHeader header = new OggPageHeader(); if (!header.populate(input, true) || (header.type & 0x02) != 0x02) { 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 e4c3a163e6..1fa7478488 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ogg; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.SeekMap; import java.io.IOException; @@ -27,9 +28,10 @@ import java.io.IOException; /* package */ interface OggSeeker { /** - * Returns a {@link SeekMap} that returns an initial estimated position for progressive seeking - * or the final position for direct seeking. Returns null if {@link #read} has yet to return -1. + * Returns a {@link SeekMap} that returns an initial estimated position for progressive seeking or + * the final position for direct seeking. Returns null if {@link #read} has yet to return -1. */ + @Nullable SeekMap createSeekMap(); /** 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 d2671125e4..2aec9cdd59 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 @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** StreamReader abstract class. */ @SuppressWarnings("UngroupedOverloads") @@ -42,9 +43,9 @@ import java.io.IOException; private final OggPacket oggPacket; - private TrackOutput trackOutput; - private ExtractorOutput extractorOutput; - private OggSeeker oggSeeker; + @MonotonicNonNull private TrackOutput trackOutput; + @MonotonicNonNull private ExtractorOutput extractorOutput; + @MonotonicNonNull private OggSeeker oggSeeker; private long targetGranule; private long payloadStartPosition; private long currentGranule; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java index b57678266a..71c6c3b73e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java @@ -88,7 +88,7 @@ import java.util.ArrayList; @Override protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) - throws IOException, InterruptedException { + throws IOException { if (vorbisSetup != null) { return false; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/package-info.java new file mode 100644 index 0000000000..ef8ed054a4 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/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.extractor.ogg; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java index 3d76276240..accdd89795 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java @@ -24,8 +24,11 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Extracts data from the RawCC container format. @@ -44,11 +47,9 @@ public final class RawCcExtractor implements Extractor { private static final int STATE_READING_SAMPLES = 2; private final Format format; - private final ParsableByteArray dataScratch; - private TrackOutput trackOutput; - + @MonotonicNonNull private TrackOutput trackOutput; private int parserState; private int version; private long timestampUs; @@ -79,6 +80,7 @@ public final class RawCcExtractor implements Extractor { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + Assertions.checkStateNotNull(trackOutput); // Asserts that init has been called. while (true) { switch (parserState) { case STATE_READING_HEADER: @@ -153,6 +155,7 @@ public final class RawCcExtractor implements Extractor { return true; } + @RequiresNonNull("trackOutput") private void parseSamples(ExtractorInput input) throws IOException, InterruptedException { for (; remainingSampleCount > 0; remainingSampleCount--) { dataScratch.reset(); @@ -166,5 +169,4 @@ public final class RawCcExtractor implements Extractor { trackOutput.sampleMetadata(timestampUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytesWritten, 0, null); } } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/package-info.java new file mode 100644 index 0000000000..b01e56b8dd --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/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.extractor.rawcc; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/package-info.java new file mode 100644 index 0000000000..4769ea693a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/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.extractor.wav; + +import com.google.android.exoplayer2.util.NonNullApi; From a035c2e20a4ce25bd86774a870b5960bcfa6f0df Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 17 Dec 2019 13:56:33 +0000 Subject: [PATCH 0539/1335] Reformat some javadoc on Cue PiperOrigin-RevId: 285964228 --- .../google/android/exoplayer2/text/Cue.java | 44 ++++++++++++------- 1 file changed, 29 insertions(+), 15 deletions(-) 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 1aecc09385..d35c444c18 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 @@ -142,23 +142,37 @@ public final class Cue { *

    {@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within the * viewport. * - *

    {@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of - * each line is taken to be the size of the first line of the cue. When {@link #line} is greater - * than or equal to 0 lines count from the start of the viewport, with 0 indicating zero offset - * from the start edge. When {@link #line} is negative lines count from the end of the viewport, - * with -1 indicating zero offset from the end edge. For horizontal text the line spacing is the - * height of the first line of the cue, and the start and end of the viewport are the top and - * bottom respectively. + *

      + *
    • {@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within + * the viewport. + *
    • + *
        + *
      • {@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the + * size of each line is taken to be the size of the first line of the cue. + *
      • When {@link #line} is greater than or equal to 0 lines count from the start of the + * viewport, with 0 indicating zero offset from the start edge. When {@link #line} is + * negative lines count from the end of the viewport, with -1 indicating zero offset + * from the end edge. + *
      • For horizontal text the line spacing is the height of the first line of the cue, + * and the start and end of the viewport are the top and bottom respectively. + *
      + *
    * *

    Note that it's particularly important to consider the effect of {@link #lineAnchor} when - * using {@link #LINE_TYPE_NUMBER}. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)} - * positions a (potentially multi-line) cue at the very top of the viewport. {@code (line == -1 && - * lineAnchor == ANCHOR_TYPE_END)} positions a (potentially multi-line) cue at the very bottom of - * the viewport. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)} and {@code (line == -1 && - * lineAnchor == ANCHOR_TYPE_START)} position cues entirely outside of the viewport. {@code (line - * == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only the last line is visible - * at the top of the viewport. {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a - * cue so that only its first line is visible at the bottom of the viewport. + * using {@link #LINE_TYPE_NUMBER}. + * + *

      + *
    • {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)} positions a (potentially + * multi-line) cue at the very start of the viewport. + *
    • {@code (line == -1 && lineAnchor == ANCHOR_TYPE_END)} positions a (potentially + * multi-line) cue at the very end of the viewport. + *
    • {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)} and {@code (line == -1 && lineAnchor + * == ANCHOR_TYPE_START)} position cues entirely outside of the viewport. + *
    • {@code (line == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only the + * last line is visible at the start of the viewport. + *
    • {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a cue so that only its + * first line is visible at the end of the viewport. + *
    */ public final @LineType int lineType; From 021291b38ff81d0e5fdee13e82819d633a9f9209 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 17 Dec 2019 14:51:06 +0000 Subject: [PATCH 0540/1335] Add server-client time offset to Window. This offset allows to improve the calculated live offset because it can take known client-server time offsets into account. PiperOrigin-RevId: 285970738 --- .../exoplayer2/ext/cast/CastTimeline.java | 1 + .../google/android/exoplayer2/BasePlayer.java | 2 +- .../google/android/exoplayer2/Timeline.java | 34 ++++++++++++++++--- .../exoplayer2/source/MaskingMediaSource.java | 1 + .../source/SinglePeriodTimeline.java | 17 +++++++--- .../google/android/exoplayer2/util/Util.java | 14 ++++++++ .../android/exoplayer2/TimelineTest.java | 1 + .../source/dash/DashChunkSource.java | 3 +- .../source/dash/DashMediaSource.java | 19 +++++------ .../source/dash/DefaultDashChunkSource.java | 12 ++----- .../exoplayer2/source/hls/HlsMediaSource.java | 2 ++ .../exoplayer2/testutil/FakeTimeline.java | 1 + 12 files changed, 77 insertions(+), 30 deletions(-) diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java index a3bdc5e415..38a7a692b2 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java @@ -130,6 +130,7 @@ import java.util.Arrays; /* manifest= */ null, /* presentationStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, /* isSeekable= */ !isDynamic, isDynamic, isLive[windowIndex], diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java index 42cf11cdae..0f00621676 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java @@ -153,7 +153,7 @@ public abstract class BasePlayer implements Player { if (windowStartTimeMs == C.TIME_UNSET) { return C.TIME_UNSET; } - return System.currentTimeMillis() - window.windowStartTimeMs - getContentPosition(); + return window.getCurrentUnixTimeMs() - window.windowStartTimeMs - getContentPosition(); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 93a87da0dc..b07a9792a1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2; +import android.os.SystemClock; import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.ads.AdPlaybackState; @@ -136,19 +137,28 @@ public abstract class Timeline { /** * The start time of the presentation to which this window belongs in milliseconds since the - * epoch, or {@link C#TIME_UNSET} if unknown or not applicable. For informational purposes only. + * Unix epoch, or {@link C#TIME_UNSET} if unknown or not applicable. For informational purposes + * only. */ public long presentationStartTimeMs; /** - * The window's start time in milliseconds since the epoch, or {@link C#TIME_UNSET} if unknown - * or not applicable. For informational purposes only. + * The window's start time in milliseconds since the Unix epoch, or {@link C#TIME_UNSET} if + * unknown or not applicable. For informational purposes only. */ public long windowStartTimeMs; /** - * Whether it's possible to seek within this window. + * The offset between {@link SystemClock#elapsedRealtime()} and the time since the Unix epoch + * according to the clock of the media origin server, or {@link C#TIME_UNSET} if unknown or not + * applicable. + * + *

    Note that the current Unix time can be retrieved using {@link #getCurrentUnixTimeMs()} and + * is calculated as {@code SystemClock.elapsedRealtime() + elapsedRealtimeEpochOffsetMs}. */ + public long elapsedRealtimeEpochOffsetMs; + + /** Whether it's possible to seek within this window. */ public boolean isSeekable; // TODO: Split this to better describe which parts of the window might change. For example it @@ -205,6 +215,7 @@ public abstract class Timeline { @Nullable Object manifest, long presentationStartTimeMs, long windowStartTimeMs, + long elapsedRealtimeEpochOffsetMs, boolean isSeekable, boolean isDynamic, boolean isLive, @@ -218,6 +229,7 @@ public abstract class Timeline { this.manifest = manifest; this.presentationStartTimeMs = presentationStartTimeMs; this.windowStartTimeMs = windowStartTimeMs; + this.elapsedRealtimeEpochOffsetMs = elapsedRealtimeEpochOffsetMs; this.isSeekable = isSeekable; this.isDynamic = isDynamic; this.isLive = isLive; @@ -279,6 +291,16 @@ public abstract class Timeline { return positionInFirstPeriodUs; } + /** + * Returns the current time in milliseconds since the Unix epoch. + * + *

    This method applies {@link #elapsedRealtimeEpochOffsetMs known corrections} made available + * by the media such that this time corresponds to the clock of the media origin server. + */ + public long getCurrentUnixTimeMs() { + return Util.getNowUnixTimeMs(elapsedRealtimeEpochOffsetMs); + } + @Override public boolean equals(@Nullable Object obj) { if (this == obj) { @@ -293,6 +315,7 @@ public abstract class Timeline { && Util.areEqual(manifest, that.manifest) && presentationStartTimeMs == that.presentationStartTimeMs && windowStartTimeMs == that.windowStartTimeMs + && elapsedRealtimeEpochOffsetMs == that.elapsedRealtimeEpochOffsetMs && isSeekable == that.isSeekable && isDynamic == that.isDynamic && isLive == that.isLive @@ -311,6 +334,9 @@ public abstract class Timeline { result = 31 * result + (manifest == null ? 0 : manifest.hashCode()); result = 31 * result + (int) (presentationStartTimeMs ^ (presentationStartTimeMs >>> 32)); result = 31 * result + (int) (windowStartTimeMs ^ (windowStartTimeMs >>> 32)); + result = + 31 * result + + (int) (elapsedRealtimeEpochOffsetMs ^ (elapsedRealtimeEpochOffsetMs >>> 32)); result = 31 * result + (isSeekable ? 1 : 0); result = 31 * result + (isDynamic ? 1 : 0); result = 31 * result + (isLive ? 1 : 0); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java index 213a8b0272..aff40d891d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java @@ -337,6 +337,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { /* manifest= */ null, /* presentationStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, /* isSeekable= */ false, // Dynamic window to indicate pending timeline updates. /* isDynamic= */ true, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index 45f64cacf2..5b47398dd5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -29,6 +29,7 @@ public final class SinglePeriodTimeline extends Timeline { private final long presentationStartTimeMs; private final long windowStartTimeMs; + private final long elapsedRealtimeEpochOffsetMs; private final long periodDurationUs; private final long windowDurationUs; private final long windowPositionInPeriodUs; @@ -110,6 +111,7 @@ public final class SinglePeriodTimeline extends Timeline { this( /* presentationStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, periodDurationUs, windowDurationUs, windowPositionInPeriodUs, @@ -126,8 +128,12 @@ public final class SinglePeriodTimeline extends Timeline { * position in the period. * * @param presentationStartTimeMs The start time of the presentation in milliseconds since the - * epoch. - * @param windowStartTimeMs The window's start time in milliseconds since the epoch. + * epoch, or {@link C#TIME_UNSET} if unknown or not applicable. + * @param windowStartTimeMs The window's start time in milliseconds since the epoch, or {@link + * C#TIME_UNSET} if unknown or not applicable. + * @param elapsedRealtimeEpochOffsetMs The offset between {@link + * android.os.SystemClock#elapsedRealtime()} and the time since the Unix epoch according to + * the clock of the media origin server, or {@link C#TIME_UNSET} if unknown or not applicable. * @param periodDurationUs The duration of the period in microseconds. * @param windowDurationUs The duration of the window in microseconds. * @param windowPositionInPeriodUs The position of the start of the window in the period, in @@ -143,6 +149,7 @@ public final class SinglePeriodTimeline extends Timeline { public SinglePeriodTimeline( long presentationStartTimeMs, long windowStartTimeMs, + long elapsedRealtimeEpochOffsetMs, long periodDurationUs, long windowDurationUs, long windowPositionInPeriodUs, @@ -154,6 +161,7 @@ public final class SinglePeriodTimeline extends Timeline { @Nullable Object tag) { this.presentationStartTimeMs = presentationStartTimeMs; this.windowStartTimeMs = windowStartTimeMs; + this.elapsedRealtimeEpochOffsetMs = elapsedRealtimeEpochOffsetMs; this.periodDurationUs = periodDurationUs; this.windowDurationUs = windowDurationUs; this.windowPositionInPeriodUs = windowPositionInPeriodUs; @@ -192,13 +200,14 @@ public final class SinglePeriodTimeline extends Timeline { manifest, presentationStartTimeMs, windowStartTimeMs, + elapsedRealtimeEpochOffsetMs, isSeekable, isDynamic, isLive, windowDefaultStartPositionUs, windowDurationUs, - 0, - 0, + /* firstPeriodIndex= */ 0, + /* lastPeriodIndex= */ 0, windowPositionInPeriodUs); } 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 5d20de1bcf..54e65797f0 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 @@ -39,6 +39,7 @@ import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Parcel; +import android.os.SystemClock; import android.security.NetworkSecurityPolicy; import android.telephony.TelephonyManager; import android.text.TextUtils; @@ -2045,6 +2046,19 @@ public final class Util { } } + /** + * Returns the current time in milliseconds since the epoch. + * + * @param elapsedRealtimeEpochOffsetMs The offset between {@link SystemClock#elapsedRealtime()} + * and the time since the Unix epoch, or {@link C#TIME_UNSET} if unknown. + * @return The Unix time in milliseconds since the epoch. + */ + public static long getNowUnixTimeMs(long elapsedRealtimeEpochOffsetMs) { + return elapsedRealtimeEpochOffsetMs == C.TIME_UNSET + ? System.currentTimeMillis() + : SystemClock.elapsedRealtime() + elapsedRealtimeEpochOffsetMs; + } + @Nullable private static String getSystemProperty(String name) { try { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java index 5110ad411c..6bc70e3188 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java @@ -134,6 +134,7 @@ public class TimelineTest { window.manifest, window.presentationStartTimeMs, window.windowStartTimeMs, + window.elapsedRealtimeEpochOffsetMs, window.isSeekable, window.isDynamic, window.isLive, 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 f7edf62182..e12a67a754 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 @@ -42,7 +42,8 @@ public interface DashChunkSource extends ChunkSource { * @param trackSelection The track selection. * @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between * server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, - * specified as the server's unix time minus the local elapsed time. If unknown, set to 0. + * specified as the server's unix time minus the local elapsed time. Or {@link + * com.google.android.exoplayer2.C#TIME_UNSET} if unknown. * @param enableEventMessageTrack Whether to output an event message track. * @param closedCaptionFormats The {@link Format Formats} of closed caption tracks to be output. * @param transferListener The transfer listener which should be informed of any data transfers. diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index f919e8eade..bced8b3126 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -621,6 +621,7 @@ public final class DashMediaSource extends BaseMediaSource { periodsById = new SparseArray<>(); playerEmsgCallback = new DefaultPlayerEmsgCallback(); expiredManifestPublishTimeUs = C.TIME_UNSET; + elapsedRealtimeOffsetMs = C.TIME_UNSET; if (sideloadedManifest) { Assertions.checkState(!manifest.dynamic); manifestCallback = null; @@ -723,7 +724,7 @@ public final class DashMediaSource extends BaseMediaSource { handler.removeCallbacksAndMessages(null); handler = null; } - elapsedRealtimeOffsetMs = 0; + elapsedRealtimeOffsetMs = C.TIME_UNSET; staleManifestReloadAttempt = 0; expiredManifestPublishTimeUs = C.TIME_UNSET; firstPeriodId = 0; @@ -969,7 +970,8 @@ public final class DashMediaSource extends BaseMediaSource { if (manifest.dynamic && !lastPeriodSeekInfo.isIndexExplicit) { // The manifest describes an incomplete live stream. Update the start/end times to reflect the // live stream duration and the manifest's time shift buffer depth. - long liveStreamDurationUs = getNowUnixTimeUs() - C.msToUs(manifest.availabilityStartTimeMs); + long nowUnixTimeUs = C.msToUs(Util.getNowUnixTimeMs(elapsedRealtimeOffsetMs)); + long liveStreamDurationUs = nowUnixTimeUs - C.msToUs(manifest.availabilityStartTimeMs); long liveStreamEndPositionInLastPeriodUs = liveStreamDurationUs - C.msToUs(manifest.getPeriod(lastPeriodIndex).startMs); currentEndTimeUs = Math.min(liveStreamEndPositionInLastPeriodUs, currentEndTimeUs); @@ -1022,6 +1024,7 @@ public final class DashMediaSource extends BaseMediaSource { new DashTimeline( manifest.availabilityStartTimeMs, windowStartTimeMs, + elapsedRealtimeOffsetMs, firstPeriodId, currentStartTimeUs, windowDurationUs, @@ -1093,14 +1096,6 @@ public final class DashMediaSource extends BaseMediaSource { manifestEventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs); } - private long getNowUnixTimeUs() { - if (elapsedRealtimeOffsetMs != 0) { - return C.msToUs(SystemClock.elapsedRealtime() + elapsedRealtimeOffsetMs); - } else { - return C.msToUs(System.currentTimeMillis()); - } - } - private static final class PeriodSeekInfo { public static PeriodSeekInfo createPeriodSeekInfo( @@ -1170,6 +1165,7 @@ public final class DashMediaSource extends BaseMediaSource { private final long presentationStartTimeMs; private final long windowStartTimeMs; + private final long elapsedRealtimeEpochOffsetMs; private final int firstPeriodId; private final long offsetInFirstPeriodUs; @@ -1181,6 +1177,7 @@ public final class DashMediaSource extends BaseMediaSource { public DashTimeline( long presentationStartTimeMs, long windowStartTimeMs, + long elapsedRealtimeEpochOffsetMs, int firstPeriodId, long offsetInFirstPeriodUs, long windowDurationUs, @@ -1189,6 +1186,7 @@ public final class DashMediaSource extends BaseMediaSource { @Nullable Object windowTag) { this.presentationStartTimeMs = presentationStartTimeMs; this.windowStartTimeMs = windowStartTimeMs; + this.elapsedRealtimeEpochOffsetMs = elapsedRealtimeEpochOffsetMs; this.firstPeriodId = firstPeriodId; this.offsetInFirstPeriodUs = offsetInFirstPeriodUs; this.windowDurationUs = windowDurationUs; @@ -1228,6 +1226,7 @@ public final class DashMediaSource extends BaseMediaSource { manifest, presentationStartTimeMs, windowStartTimeMs, + elapsedRealtimeEpochOffsetMs, /* isSeekable= */ true, /* isDynamic= */ isMovingLiveWindow(manifest), /* isLive= */ manifest.dynamic, 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 2904944493..340c5b9b64 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 @@ -136,7 +136,7 @@ public class DefaultDashChunkSource implements DashChunkSource { * @param dataSource A {@link DataSource} suitable for loading the media data. * @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between * server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified - * as the server's unix time minus the local elapsed time. If unknown, set to 0. + * as the server's unix time minus the local elapsed time. Or {@link C#TIME_UNSET} if unknown. * @param maxSegmentsPerLoad The maximum number of segments to combine into a single request. Note * that segments will only be combined if their {@link Uri}s are the same and if their data * ranges are adjacent. @@ -267,7 +267,7 @@ public class DefaultDashChunkSource implements DashChunkSource { return; } - long nowUnixTimeUs = getNowUnixTimeUs(); + long nowUnixTimeUs = C.msToUs(Util.getNowUnixTimeMs(elapsedRealtimeOffsetMs)); MediaChunk previous = queue.isEmpty() ? null : queue.get(queue.size() - 1); MediaChunkIterator[] chunkIterators = new MediaChunkIterator[trackSelection.length()]; for (int i = 0; i < chunkIterators.length; i++) { @@ -474,14 +474,6 @@ public class DefaultDashChunkSource implements DashChunkSource { ? representationHolder.getSegmentEndTimeUs(lastAvailableSegmentNum) : C.TIME_UNSET; } - private long getNowUnixTimeUs() { - if (elapsedRealtimeOffsetMs != 0) { - return (SystemClock.elapsedRealtime() + elapsedRealtimeOffsetMs) * 1000; - } else { - return System.currentTimeMillis() * 1000; - } - } - private long resolveTimeToLiveEdgeUs(long playbackPositionUs) { boolean resolveTimeToLiveEdgePossible = manifest.dynamic && liveEdgeTimeUs != C.TIME_UNSET; return resolveTimeToLiveEdgePossible ? liveEdgeTimeUs - playbackPositionUs : C.TIME_UNSET; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 8798d08039..411eb448be 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -468,6 +468,7 @@ public final class HlsMediaSource extends BaseMediaSource new SinglePeriodTimeline( presentationStartTimeMs, windowStartTimeMs, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, periodDurationUs, /* windowDurationUs= */ playlist.durationUs, /* windowPositionInPeriodUs= */ offsetFromInitialStartTimeUs, @@ -485,6 +486,7 @@ public final class HlsMediaSource extends BaseMediaSource new SinglePeriodTimeline( presentationStartTimeMs, windowStartTimeMs, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, /* periodDurationUs= */ playlist.durationUs, /* windowDurationUs= */ playlist.durationUs, /* windowPositionInPeriodUs= */ 0, diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index c8c7190007..8160dc3147 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -219,6 +219,7 @@ public final class FakeTimeline extends Timeline { manifests[windowIndex], /* presentationStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, windowDefinition.isSeekable, windowDefinition.isDynamic, windowDefinition.isLive, From d48dc4c15933dd8354cbcc6260c48565bb850e15 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 17 Dec 2019 15:59:43 +0000 Subject: [PATCH 0541/1335] Move getting-stuck-prevention into DefaultLoadControl. We recently added code that prevents getting stuck if the buffer is low and the LoadControl refuses to continue loading (https://github.com/google/ExoPlayer/commit/b84bde025258e7307c52eaf6bbe58157d788aa06). Move this logic into DefaultLoadControl to keep the workaround, and also apply the maximum buffer size check in bytes if enabled. ExoPlayerImplInternal will now throw if a LoadControl lets playback get stuck. This includes the case where DefaultLoadControl reaches its maximum buffer size and not even a mimimal buffer duration. PiperOrigin-RevId: 285979989 --- .../exoplayer2/DefaultLoadControl.java | 25 +++++++---- .../exoplayer2/ExoPlayerImplInternal.java | 15 ++++--- .../exoplayer2/DefaultLoadControlTest.java | 19 +++++++- .../android/exoplayer2/ExoPlayerTest.java | 44 ++++++++++++++++++- 4 files changed, 85 insertions(+), 18 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 1244b96d94..7bfd4c7cbe 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 @@ -246,7 +246,7 @@ public class DefaultLoadControl implements LoadControl { private final long backBufferDurationUs; private final boolean retainBackBufferFromKeyframe; - private int targetBufferSize; + private int targetBufferBytes; private boolean isBuffering; private boolean hasVideo; @@ -334,6 +334,10 @@ public class DefaultLoadControl implements LoadControl { this.bufferForPlaybackUs = C.msToUs(bufferForPlaybackMs); this.bufferForPlaybackAfterRebufferUs = C.msToUs(bufferForPlaybackAfterRebufferMs); this.targetBufferBytesOverwrite = targetBufferBytes; + this.targetBufferBytes = + targetBufferBytesOverwrite != C.LENGTH_UNSET + ? targetBufferBytesOverwrite + : DEFAULT_MUXED_BUFFER_SIZE; this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds; this.backBufferDurationUs = C.msToUs(backBufferDurationMs); this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe; @@ -348,11 +352,11 @@ public class DefaultLoadControl implements LoadControl { public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { hasVideo = hasVideo(renderers, trackSelections); - targetBufferSize = + targetBufferBytes = targetBufferBytesOverwrite == C.LENGTH_UNSET - ? calculateTargetBufferSize(renderers, trackSelections) + ? calculateTargetBufferBytes(renderers, trackSelections) : targetBufferBytesOverwrite; - allocator.setTargetBufferSize(targetBufferSize); + allocator.setTargetBufferSize(targetBufferBytes); } @Override @@ -382,7 +386,7 @@ public class DefaultLoadControl implements LoadControl { @Override public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { - boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; + boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferBytes; long minBufferUs = hasVideo ? minBufferVideoUs : minBufferAudioUs; if (playbackSpeed > 1) { // The playback speed is faster than real time, so scale up the minimum required media @@ -391,6 +395,8 @@ public class DefaultLoadControl implements LoadControl { Util.getMediaDurationForPlayoutDuration(minBufferUs, playbackSpeed); minBufferUs = Math.min(mediaDurationMinBufferUs, maxBufferUs); } + // Prevent playback from getting stuck if minBufferUs is too small. + minBufferUs = Math.max(minBufferUs, 500_000); if (bufferedDurationUs < minBufferUs) { isBuffering = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached; } else if (bufferedDurationUs >= maxBufferUs || targetBufferSizeReached) { @@ -407,7 +413,7 @@ public class DefaultLoadControl implements LoadControl { return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs || (!prioritizeTimeOverSizeThresholds - && allocator.getTotalBytesAllocated() >= targetBufferSize); + && allocator.getTotalBytesAllocated() >= targetBufferBytes); } /** @@ -418,7 +424,7 @@ public class DefaultLoadControl implements LoadControl { * @param trackSelectionArray The selected tracks. * @return The target buffer size in bytes. */ - protected int calculateTargetBufferSize( + protected int calculateTargetBufferBytes( Renderer[] renderers, TrackSelectionArray trackSelectionArray) { int targetBufferSize = 0; for (int i = 0; i < renderers.length; i++) { @@ -430,7 +436,10 @@ public class DefaultLoadControl implements LoadControl { } private void reset(boolean resetAllocator) { - targetBufferSize = 0; + targetBufferBytes = + targetBufferBytesOverwrite == C.LENGTH_UNSET + ? DEFAULT_MUXED_BUFFER_SIZE + : targetBufferBytesOverwrite; isBuffering = false; if (resetAllocator) { allocator.reset(); 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 547c6c3f25..2c6f7631cf 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 @@ -707,6 +707,14 @@ import java.util.concurrent.atomic.AtomicBoolean; for (Renderer renderer : enabledRenderers) { renderer.maybeThrowStreamError(); } + if (!shouldContinueLoading + && playbackInfo.totalBufferedDurationUs < 500_000 + && isLoadingPossible()) { + // Throw if the LoadControl prevents loading even if the buffer is empty or almost empty. We + // can't compare against 0 to account for small differences between the renderer position + // and buffered position in the media at the point where playback gets stuck. + throw new IllegalStateException("Playback stuck buffering and not loading"); + } } if ((playWhenReady && playbackInfo.playbackState == Player.STATE_READY) @@ -1831,13 +1839,6 @@ import java.util.concurrent.atomic.AtomicBoolean; } long bufferedDurationUs = getTotalBufferedDurationUs(queue.getLoadingPeriod().getNextLoadPositionUs()); - if (bufferedDurationUs < 500_000) { - // Prevent loading from getting stuck even if LoadControl.shouldContinueLoading returns false - // when the buffer is empty or almost empty. We can't compare against 0 to account for small - // differences between the renderer position and buffered position in the media at the point - // where playback gets stuck. - return true; - } float playbackSpeed = mediaClock.getPlaybackParameters().speed; return loadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java index 31f432db15..2222d1a8d0 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java @@ -46,6 +46,7 @@ public class DefaultLoadControlTest { @Test public void testShouldContinueLoading_untilMaxBufferExceeded() { createDefaultLoadControl(); + assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isTrue(); assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US - 1, SPEED)).isTrue(); @@ -56,11 +57,27 @@ public class DefaultLoadControlTest { public void testShouldNotContinueLoadingOnceBufferingStopped_untilBelowMinBuffer() { createDefaultLoadControl(); assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse(); + assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US - 1, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isTrue(); } + @Test + public void + testContinueLoadingOnceBufferingStopped_andBufferAlmostEmpty_evenIfMinBufferNotReached() { + builder.setBufferDurationsMs( + /* minBufferMs= */ 0, + /* maxBufferMs= */ (int) C.usToMs(MAX_BUFFER_US), + /* bufferForPlaybackMs= */ 0, + /* bufferForPlaybackAfterRebufferMs= */ 0); + createDefaultLoadControl(); + assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse(); + + assertThat(loadControl.shouldContinueLoading(5 * C.MICROS_PER_SECOND, SPEED)).isFalse(); + assertThat(loadControl.shouldContinueLoading(500L, SPEED)).isTrue(); + } + @Test public void testShouldContinueLoadingWithTargetBufferBytesReached_untilMinBufferReached() { createDefaultLoadControl(); @@ -81,6 +98,7 @@ public class DefaultLoadControlTest { makeSureTargetBufferBytesReached(); assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isFalse(); + assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse(); } @@ -91,7 +109,6 @@ public class DefaultLoadControlTest { // At normal playback speed, we stop buffering when the buffer reaches the minimum. assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); - // At double playback speed, we continue loading. assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, /* playbackSpeed= */ 2f)).isTrue(); } 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 f48c33ebda..320854565d 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 @@ -3150,14 +3150,54 @@ public final class ExoPlayerTest { } @Test - public void loadControlNeverWantsToLoadOrPlay_playbackDoesNotGetStuck() throws Exception { - LoadControl neverLoadingOrPlayingLoadControl = + public void loadControlNeverWantsToLoad_throwsIllegalStateException() throws Exception { + LoadControl neverLoadingLoadControl = new DefaultLoadControl() { @Override public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { return false; } + @Override + public boolean shouldStartPlayback( + long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { + return true; + } + }; + + // Use chunked data to ensure the player actually needs to continue loading and playing. + FakeAdaptiveDataSet.Factory dataSetFactory = + new FakeAdaptiveDataSet.Factory( + /* chunkDurationUs= */ 500_000, /* bitratePercentStdDev= */ 10.0); + MediaSource chunkedMediaSource = + new FakeAdaptiveMediaSource( + new FakeTimeline(/* windowCount= */ 1), + new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT)), + new FakeChunkSource.Factory(dataSetFactory, new FakeDataSource.Factory())); + + try { + new ExoPlayerTestRunner.Builder() + .setLoadControl(neverLoadingLoadControl) + .setMediaSource(chunkedMediaSource) + .build(context) + .start() + .blockUntilEnded(TIMEOUT_MS); + fail(); + } catch (ExoPlaybackException e) { + assertThat(e.type).isEqualTo(ExoPlaybackException.TYPE_UNEXPECTED); + assertThat(e.getUnexpectedException()).isInstanceOf(IllegalStateException.class); + } + } + + @Test + public void loadControlNeverWantsToPlay_playbackDoesNotGetStuck() throws Exception { + LoadControl neverLoadingOrPlayingLoadControl = + new DefaultLoadControl() { + @Override + public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { + return true; + } + @Override public boolean shouldStartPlayback( long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { From 7a4b35b59f4520348a083b81c523c9192bff357c Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 17 Dec 2019 17:00:54 +0000 Subject: [PATCH 0542/1335] Retain AV1 constructor for DefaultRenderersFactory Issue: #6773 PiperOrigin-RevId: 285990377 --- RELEASENOTES.md | 2 ++ library/core/proguard-rules.txt | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 26cbb99137..dfbf06f4d2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -30,6 +30,8 @@ Also change `IcyInfo.rawMetadata` from `String` to `byte[]` to allow developers to handle data that's neither UTF-8 nor ISO-8859-1 ([#6753](https://github.com/google/ExoPlayer/issues/6753)). +* AV1 extension: Fix ProGuard rules + ([6773](https://github.com/google/ExoPlayer/issues/6773)). ### 2.11.0 (2019-12-11) ### diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index 67646be956..bfd691259b 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -5,6 +5,10 @@ -keepclassmembers class com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer { (long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int); } +-dontnote com.google.android.exoplayer2.ext.av1.Libgav1VideoRenderer +-keepclassmembers class com.google.android.exoplayer2.ext.av1.Libgav1VideoRenderer { + (long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int); +} -dontnote com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer -keepclassmembers class com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer { (android.os.Handler, com.google.android.exoplayer2.audio.AudioRendererEventListener, com.google.android.exoplayer2.audio.AudioProcessor[]); From 863bf453418f869efeeaa500e01abba60da248e9 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 17 Dec 2019 17:13:53 +0000 Subject: [PATCH 0543/1335] Suppress ProGuard warnings about javax.annotation These annotations are compile-only - so we don't mind they're not accessible at runtime. PiperOrigin-RevId: 285993063 --- RELEASENOTES.md | 2 ++ library/core/proguard-rules.txt | 1 + 2 files changed, 3 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index dfbf06f4d2..a1becd899e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -32,6 +32,8 @@ ([#6753](https://github.com/google/ExoPlayer/issues/6753)). * AV1 extension: Fix ProGuard rules ([6773](https://github.com/google/ExoPlayer/issues/6773)). +* Suppress ProGuard warnings for compile-time `javax.annotation` package + ([#6771](https://github.com/google/ExoPlayer/issues/6771)). ### 2.11.0 (2019-12-11) ### diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index bfd691259b..ab4af32da4 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -65,6 +65,7 @@ # Don't warn about checkerframework and Kotlin annotations -dontwarn org.checkerframework.** -dontwarn kotlin.annotations.jvm.** +-dontwarn javax.annotation.** # Some members of this class are being accessed from native methods. Keep them unobfuscated. -keep class com.google.android.exoplayer2.ext.video.VideoDecoderOutputBuffer { From 43bbc172a4ef52e7cb6a60edc9ec6404af72297e Mon Sep 17 00:00:00 2001 From: christosts Date: Tue, 17 Dec 2019 18:12:00 +0000 Subject: [PATCH 0544/1335] DefaultRenderersFactory can set MediaCodecOperation Add experimental method on DefaultRenderersFactory to set the MediaCodecOperationMode on MediaCodecRenderer instances. PiperOrigin-RevId: 286004667 --- .../exoplayer2/DefaultRenderersFactory.java | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) 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 f53d72f598..97ad6613e3 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 @@ -28,6 +28,7 @@ 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; +import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.metadata.MetadataRenderer; @@ -93,6 +94,7 @@ public class DefaultRenderersFactory implements RenderersFactory { private boolean playClearSamplesWithoutKeys; private boolean enableDecoderFallback; private MediaCodecSelector mediaCodecSelector; + @MediaCodecRenderer.MediaCodecOperationMode private int mediaCodecOperationMode; /** @param context A {@link Context}. */ public DefaultRenderersFactory(Context context) { @@ -100,6 +102,7 @@ public class DefaultRenderersFactory implements RenderersFactory { extensionRendererMode = EXTENSION_RENDERER_MODE_OFF; allowedVideoJoiningTimeMs = DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS; mediaCodecSelector = MediaCodecSelector.DEFAULT; + mediaCodecOperationMode = MediaCodecRenderer.MediaCodecOperationMode.SYNCHRONOUS; } /** @@ -185,6 +188,21 @@ public class DefaultRenderersFactory implements RenderersFactory { return this; } + /** + * Set the {@link MediaCodecRenderer.MediaCodecOperationMode} of {@link MediaCodecRenderer} + * instances. + * + *

    This method is experimental, and will be renamed or removed in a future release. + * + * @param mode The {@link MediaCodecRenderer.MediaCodecOperationMode} to set. + * @return This factory, for convenience. + */ + public DefaultRenderersFactory experimental_setMediaCodecOperationMode( + @MediaCodecRenderer.MediaCodecOperationMode int mode) { + mediaCodecOperationMode = mode; + return this; + } + /** * Sets 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. For encrypted media that @@ -319,7 +337,7 @@ public class DefaultRenderersFactory implements RenderersFactory { VideoRendererEventListener eventListener, long allowedVideoJoiningTimeMs, ArrayList out) { - out.add( + MediaCodecVideoRenderer videoRenderer = new MediaCodecVideoRenderer( context, mediaCodecSelector, @@ -329,7 +347,9 @@ public class DefaultRenderersFactory implements RenderersFactory { enableDecoderFallback, eventHandler, eventListener, - MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); + MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY); + videoRenderer.experimental_setMediaCodecOperationMode(mediaCodecOperationMode); + out.add(videoRenderer); if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { return; @@ -425,7 +445,7 @@ public class DefaultRenderersFactory implements RenderersFactory { Handler eventHandler, AudioRendererEventListener eventListener, ArrayList out) { - out.add( + MediaCodecAudioRenderer audioRenderer = new MediaCodecAudioRenderer( context, mediaCodecSelector, @@ -434,7 +454,9 @@ public class DefaultRenderersFactory implements RenderersFactory { enableDecoderFallback, eventHandler, eventListener, - new DefaultAudioSink(AudioCapabilities.getCapabilities(context), audioProcessors))); + new DefaultAudioSink(AudioCapabilities.getCapabilities(context), audioProcessors)); + audioRenderer.experimental_setMediaCodecOperationMode(mediaCodecOperationMode); + out.add(audioRenderer); if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { return; From 36fa9d5a434812ef637ad27a0a9f1c4dd9651b1b Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 17 Dec 2019 19:21:27 +0000 Subject: [PATCH 0545/1335] add top-level playlist API Design doc: https://docs.google.com/document/d/11h0S91KI5TB3NNZUtsCzg0S7r6nyTnF_tDZZAtmY93g/edit Issue: #6161, #5155 PiperOrigin-RevId: 286020313 --- RELEASENOTES.md | 1 + .../exoplayer2/demo/PlayerActivity.java | 61 +- .../exoplayer2/ext/cast/CastPlayer.java | 14 +- .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 2 +- .../google/android/exoplayer2/ExoPlayer.java | 171 +- .../android/exoplayer2/ExoPlayerFactory.java | 10 +- .../android/exoplayer2/ExoPlayerImpl.java | 555 +++- .../exoplayer2/ExoPlayerImplInternal.java | 580 +++- .../android/exoplayer2/MediaPeriodHolder.java | 24 +- .../android/exoplayer2/MediaPeriodQueue.java | 7 +- .../android/exoplayer2/PlaybackInfo.java | 7 +- .../com/google/android/exoplayer2/Player.java | 25 +- .../android/exoplayer2/SimpleExoPlayer.java | 217 +- .../analytics/AnalyticsCollector.java | 17 +- .../android/exoplayer2/util/EventLogger.java | 10 +- .../android/exoplayer2/ExoPlayerTest.java | 2944 +++++++++++++++-- .../exoplayer2/MediaPeriodQueueTest.java | 123 +- .../analytics/AnalyticsCollectorTest.java | 101 +- .../android/exoplayer2/testutil/Action.java | 324 +- .../exoplayer2/testutil/ActionSchedule.java | 168 +- .../testutil/ExoPlayerTestRunner.java | 158 +- .../exoplayer2/testutil/FakeMediaSource.java | 31 +- .../exoplayer2/testutil/StubExoPlayer.java | 88 + 23 files changed, 4691 insertions(+), 947 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a1becd899e..ac5ba1b045 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -34,6 +34,7 @@ ([6773](https://github.com/google/ExoPlayer/issues/6773)). * Suppress ProGuard warnings for compile-time `javax.annotation` package ([#6771](https://github.com/google/ExoPlayer/issues/6771)). +* Add playlist API ([#6161](https://github.com/google/ExoPlayer/issues/6161)). ### 2.11.0 (2019-12-11) ### 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 b291d5afe8..b759c97da5 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 @@ -51,7 +51,6 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryExcep 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; import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.MergingMediaSource; @@ -82,6 +81,8 @@ import java.lang.reflect.Constructor; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; +import java.util.ArrayList; +import java.util.List; /** An activity that plays media using {@link SimpleExoPlayer}. */ public class PlayerActivity extends AppCompatActivity @@ -147,7 +148,7 @@ public class PlayerActivity extends AppCompatActivity private DataSource.Factory dataSourceFactory; private SimpleExoPlayer player; - private MediaSource mediaSource; + private List mediaSources; private DefaultTrackSelector trackSelector; private DefaultTrackSelector.Parameters trackSelectorParameters; private DebugTextViewHelper debugViewHelper; @@ -349,12 +350,10 @@ public class PlayerActivity extends AppCompatActivity private void initializePlayer() { if (player == null) { Intent intent = getIntent(); - - mediaSource = createTopLevelMediaSource(intent); - if (mediaSource == null) { + mediaSources = createTopLevelMediaSources(intent); + if (mediaSources.isEmpty()) { return; } - TrackSelection.Factory trackSelectionFactory; String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA); if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) { @@ -395,13 +394,12 @@ public class PlayerActivity extends AppCompatActivity if (haveStartPosition) { player.seekTo(startWindow, startPosition); } - player.setMediaSource(mediaSource); + player.setMediaSources(mediaSources, /* resetPosition= */ !haveStartPosition); player.prepare(); updateButtonVisibility(); } - @Nullable - private MediaSource createTopLevelMediaSource(Intent intent) { + private List createTopLevelMediaSources(Intent intent) { String action = intent.getAction(); boolean actionIsListView = ACTION_VIEW_LIST.equals(action); if (!actionIsListView && !ACTION_VIEW.equals(action)) { @@ -429,10 +427,10 @@ public class PlayerActivity extends AppCompatActivity } } - MediaSource[] mediaSources = new MediaSource[samples.length]; - for (int i = 0; i < samples.length; i++) { - mediaSources[i] = createLeafMediaSource(samples[i]); - Sample.SubtitleInfo subtitleInfo = samples[i].subtitleInfo; + List mediaSources = new ArrayList<>(); + for (UriSample sample : samples) { + MediaSource mediaSource = createLeafMediaSource(sample); + Sample.SubtitleInfo subtitleInfo = sample.subtitleInfo; if (subtitleInfo != null) { Format subtitleFormat = Format.createTextSampleFormat( @@ -443,33 +441,30 @@ public class PlayerActivity extends AppCompatActivity MediaSource subtitleMediaSource = new SingleSampleMediaSource.Factory(dataSourceFactory) .createMediaSource(subtitleInfo.uri, subtitleFormat, C.TIME_UNSET); - mediaSources[i] = new MergingMediaSource(mediaSources[i], subtitleMediaSource); + mediaSource = new MergingMediaSource(mediaSource, subtitleMediaSource); } + mediaSources.add(mediaSource); } - MediaSource mediaSource = - mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); - - if (seenAdsTagUri) { + if (seenAdsTagUri && mediaSources.size() == 1) { Uri adTagUri = samples[0].adTagUri; - if (actionIsListView) { - showToast(R.string.unsupported_ads_in_concatenation); - } else { - if (!adTagUri.equals(loadedAdTagUri)) { - releaseAdsLoader(); - loadedAdTagUri = adTagUri; - } - MediaSource adsMediaSource = createAdsMediaSource(mediaSource, adTagUri); - if (adsMediaSource != null) { - mediaSource = adsMediaSource; - } else { - showToast(R.string.ima_not_loaded); - } + if (!adTagUri.equals(loadedAdTagUri)) { + releaseAdsLoader(); + loadedAdTagUri = adTagUri; } + MediaSource adsMediaSource = createAdsMediaSource(mediaSources.get(0), adTagUri); + if (adsMediaSource != null) { + mediaSources.set(0, adsMediaSource); + } else { + showToast(R.string.ima_not_loaded); + } + } else if (seenAdsTagUri && mediaSources.size() > 1) { + showToast(R.string.unsupported_ads_in_concatenation); + releaseAdsLoader(); } else { releaseAdsLoader(); } - return mediaSource; + return mediaSources; } private MediaSource createLeafMediaSource(UriSample parameters) { @@ -557,7 +552,7 @@ public class PlayerActivity extends AppCompatActivity debugViewHelper = null; player.release(); player = null; - mediaSource = null; + mediaSources = null; trackSelector = null; } if (adsLoader != null) { 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 c198b49777..5b91410ff9 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 @@ -110,7 +110,6 @@ public final class CastPlayer extends BasePlayer { private int pendingSeekCount; private int pendingSeekWindowIndex; private long pendingSeekPositionMs; - private boolean waitingForInitialTimeline; /** * @param castContext The context from which the cast session is obtained. @@ -173,7 +172,6 @@ public final class CastPlayer extends BasePlayer { MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) { if (remoteMediaClient != null) { positionMs = positionMs != C.TIME_UNSET ? positionMs : 0; - waitingForInitialTimeline = true; return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode), positionMs, null); } @@ -641,15 +639,13 @@ public final class CastPlayer extends BasePlayer { private void updateTimelineAndNotifyIfChanged() { if (updateTimeline()) { - @Player.TimelineChangeReason - int reason = - waitingForInitialTimeline - ? Player.TIMELINE_CHANGE_REASON_PREPARED - : Player.TIMELINE_CHANGE_REASON_DYNAMIC; - waitingForInitialTimeline = false; + // TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and + // TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553]. notificationsBatch.add( new ListenerNotificationTask( - listener -> listener.onTimelineChanged(currentTimeline, reason))); + listener -> + listener.onTimelineChanged( + currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE))); } } 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 9e1f8848c3..2452da474d 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 @@ -288,7 +288,7 @@ public class ImaAdsLoaderTest { this.adPlaybackState = adPlaybackState; fakeExoPlayer.updateTimeline( new SinglePeriodAdTimeline(contentTimeline, adPlaybackState), - Player.TIMELINE_CHANGE_REASON_DYNAMIC); + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 0374c04cb3..b35f617048 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.source.LoopingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.SingleSampleMediaSource; import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; @@ -39,6 +40,7 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; +import java.util.List; /** * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from {@link @@ -139,7 +141,7 @@ public interface ExoPlayer extends Player { private LoadControl loadControl; private BandwidthMeter bandwidthMeter; private Looper looper; - private AnalyticsCollector analyticsCollector; + @Nullable private AnalyticsCollector analyticsCollector; private boolean useLazyPreparation; private boolean buildCalled; @@ -172,7 +174,7 @@ public interface ExoPlayer extends Player { new DefaultLoadControl(), DefaultBandwidthMeter.getSingletonInstance(context), Util.getLooper(), - new AnalyticsCollector(Clock.DEFAULT), + /* analyticsCollector= */ null, /* useLazyPreparation= */ true, Clock.DEFAULT); } @@ -199,7 +201,7 @@ public interface ExoPlayer extends Player { LoadControl loadControl, BandwidthMeter bandwidthMeter, Looper looper, - AnalyticsCollector analyticsCollector, + @Nullable AnalyticsCollector analyticsCollector, boolean useLazyPreparation, Clock clock) { Assertions.checkArgument(renderers.length > 0); @@ -335,7 +337,15 @@ public interface ExoPlayer extends Player { Assertions.checkState(!buildCalled); buildCalled = true; ExoPlayerImpl player = - new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); + new ExoPlayerImpl( + renderers, + trackSelector, + loadControl, + bandwidthMeter, + analyticsCollector, + useLazyPreparation, + clock, + looper); if (releaseTimeoutMs > 0) { player.experimental_setReleaseTimeoutMs(releaseTimeoutMs); @@ -348,58 +358,157 @@ public interface ExoPlayer extends Player { /** Returns the {@link Looper} associated with the playback thread. */ Looper getPlaybackLooper(); - /** - * Retries a failed or stopped playback. Does nothing if the player has been reset, or if playback - * has not failed or been stopped. - */ + /** @deprecated Use {@link #prepare()} instead. */ + @Deprecated void retry(); /** Prepares the player. */ void prepare(); - /** - * @deprecated Use {@code setMediaSource(mediaSource, C.TIME_UNSET)} and {@link #prepare()} - * instead. - */ + /** @deprecated Use {@link #setMediaSource(MediaSource)} and {@link #prepare()} instead. */ @Deprecated void prepare(MediaSource mediaSource); - /** @deprecated Use {@link #setMediaSource(MediaSource, long)} and {@link #prepare()} instead. */ + /** + * @deprecated Use {@link #setMediaSource(MediaSource, boolean)} and {@link #prepare()} instead. + */ @Deprecated void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); /** - * Sets the specified {@link MediaSource}. + * Clears the playlist, adds the specified {@link MediaSource MediaSources} and resets the + * position to the default position. * - *

    Note: This is an intermediate implementation towards a larger change. Until then {@link - * #prepare()} has to be called immediately after calling this method. + * @param mediaSources The new {@link MediaSource MediaSources}. + */ + void setMediaSources(List mediaSources); + + /** + * Clears the playlist and adds the specified {@link MediaSource MediaSources}. + * + * @param mediaSources The new {@link MediaSource MediaSources}. + * @param resetPosition Whether the playback position should be reset to the default position in + * the first {@link Timeline.Window}. If false, playback will start from the position defined + * by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}. + */ + void setMediaSources(List mediaSources, boolean resetPosition); + + /** + * Clears the playlist and adds the specified {@link MediaSource MediaSources}. + * + * @param mediaSources The new {@link MediaSource MediaSources}. + * @param startWindowIndex The window index to start playback from. If {@link C#INDEX_UNSET} is + * passed, the current position is not reset. + * @param startPositionMs The position in milliseconds to start playback from. If {@link + * C#TIME_UNSET} is passed, the default position of the given window is used. In any case, if + * {@code startWindowIndex} is set to {@link C#INDEX_UNSET}, this parameter is ignored and the + * position is not reset at all. + */ + void setMediaSources(List mediaSources, int startWindowIndex, long startPositionMs); + + /** + * Clears the playlist, adds the specified {@link MediaSource} and resets the position to the + * default position. * * @param mediaSource The new {@link MediaSource}. */ void setMediaSource(MediaSource mediaSource); /** - * Sets the specified {@link MediaSource}. - * - *

    Note: This is an intermediate implementation towards a larger change. Until then {@link - * #prepare()} has to be called immediately after calling this method. - * - *

    This intermediate implementation calls {@code stop(true)} before seeking to avoid seeking in - * a media item that has been set previously. It is equivalent with calling - * - *

    
    -   *   if (!getCurrentTimeline().isEmpty()) {
    -   *     player.stop(true);
    -   *   }
    -   *   player.seekTo(0, startPositionMs);
    -   *   player.setMediaSource(mediaSource);
    -   * 
    + * Clears the playlist and adds the specified {@link MediaSource}. * * @param mediaSource The new {@link MediaSource}. * @param startPositionMs The position in milliseconds to start playback from. */ void setMediaSource(MediaSource mediaSource, long startPositionMs); + /** + * Clears the playlist and adds the specified {@link MediaSource}. + * + * @param mediaSource The new {@link MediaSource}. + * @param resetPosition Whether the playback position should be reset to the default position. If + * false, playback will start from the position defined by {@link #getCurrentWindowIndex()} + * and {@link #getCurrentPosition()}. + */ + void setMediaSource(MediaSource mediaSource, boolean resetPosition); + + /** + * Adds a media source to the end of the playlist. + * + * @param mediaSource The {@link MediaSource} to add. + */ + void addMediaSource(MediaSource mediaSource); + + /** + * Adds a media source at the given index of the playlist. + * + * @param index The index at which to add the source. + * @param mediaSource The {@link MediaSource} to add. + */ + void addMediaSource(int index, MediaSource mediaSource); + + /** + * Adds a list of media sources to the end of the playlist. + * + * @param mediaSources The {@link MediaSource MediaSources} to add. + */ + void addMediaSources(List mediaSources); + + /** + * Adds a list of media sources at the given index of the playlist. + * + * @param index The index at which to add the media sources. + * @param mediaSources The {@link MediaSource MediaSources} to add. + */ + void addMediaSources(int index, List mediaSources); + + /** + * Moves the media item at the current index to the new index. + * + * @param currentIndex The current index of the media item to move. + * @param newIndex The new index of the media item. If the new index is larger than the size of + * the playlist the item is moved to the end of the playlist. + */ + void moveMediaItem(int currentIndex, int newIndex); + + /** + * Moves the media item range to the new index. + * + * @param fromIndex The start of the range to move. + * @param toIndex The first item not to be included in the range (exclusive). + * @param newIndex The new index of the first media item of the range. If the new index is larger + * than the size of the remaining playlist after removing the range, the range is moved to the + * end of the playlist. + */ + void moveMediaItems(int fromIndex, int toIndex, int newIndex); + + /** + * Removes the media item at the given index of the playlist. + * + * @param index The index at which to remove the media item. + * @return The removed {@link MediaSource} or null if no item exists at the given index. + */ + @Nullable + MediaSource removeMediaItem(int index); + + /** + * Removes a range of media items from the playlist. + * + * @param fromIndex The index at which to start removing media items. + * @param toIndex The index of the first item to be kept (exclusive). + */ + void removeMediaItems(int fromIndex, int toIndex); + + /** Clears the playlist. */ + void clearMediaItems(); + + /** + * Sets the shuffle order. + * + * @param shuffleOrder The shuffle order. + */ + void setShuffleOrder(ShuffleOrder shuffleOrder); + /** * Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message * will be delivered immediately without blocking on the playback thread. The default {@link diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java index e4f239df77..add82f2f7e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java @@ -297,6 +297,7 @@ public final class ExoPlayerFactory { drmSessionManager, bandwidthMeter, analyticsCollector, + /* useLazyPreparation= */ true, Clock.DEFAULT, looper); } @@ -345,6 +346,13 @@ public final class ExoPlayerFactory { BandwidthMeter bandwidthMeter, Looper looper) { return new ExoPlayerImpl( - renderers, trackSelector, loadControl, bandwidthMeter, Clock.DEFAULT, looper); + renderers, + trackSelector, + loadControl, + bandwidthMeter, + /* analyticsCollector= */ null, + /* useLazyPreparation= */ true, + Clock.DEFAULT, + looper); } } 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 98eaaa0c2c..97ba989c34 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 @@ -22,8 +22,10 @@ import android.os.Message; import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.analytics.AnalyticsCollector; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -35,6 +37,9 @@ import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeoutException; @@ -62,19 +67,20 @@ import java.util.concurrent.TimeoutException; private final CopyOnWriteArrayList listeners; private final Timeline.Period period; private final ArrayDeque pendingListenerNotifications; + private final List mediaSourceHolders; + private final boolean useLazyPreparation; - @Nullable private MediaSource mediaSource; private boolean playWhenReady; @PlaybackSuppressionReason private int playbackSuppressionReason; @RepeatMode private int repeatMode; private boolean shuffleModeEnabled; private int pendingOperationAcks; - private boolean hasPendingPrepare; private boolean hasPendingSeek; private boolean foregroundMode; private int pendingSetPlaybackParametersAcks; private PlaybackParameters playbackParameters; private SeekParameters seekParameters; + private ShuffleOrder shuffleOrder; // Playback information when there is no pending seek/set source operation. private PlaybackInfo playbackInfo; @@ -91,6 +97,10 @@ import java.util.concurrent.TimeoutException; * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance. * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. + * @param analyticsCollector The {@link AnalyticsCollector} that will be used by the instance. + * @param useLazyPreparation Whether playlist items are prepared lazily. If false, all manifest + * loads and other initial preparation steps happen immediately. If true, these initial + * preparations are triggered only when the player starts buffering the media. * @param clock The {@link Clock} that will be used by the instance. * @param looper The {@link Looper} which must be used for all calls to the player and which is * used to call listeners on. @@ -101,6 +111,8 @@ import java.util.concurrent.TimeoutException; TrackSelector trackSelector, LoadControl loadControl, BandwidthMeter bandwidthMeter, + @Nullable AnalyticsCollector analyticsCollector, + boolean useLazyPreparation, Clock clock, Looper looper) { Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " [" @@ -108,10 +120,13 @@ import java.util.concurrent.TimeoutException; Assertions.checkState(renderers.length > 0); this.renderers = Assertions.checkNotNull(renderers); this.trackSelector = Assertions.checkNotNull(trackSelector); - this.playWhenReady = false; - this.repeatMode = Player.REPEAT_MODE_OFF; - this.shuffleModeEnabled = false; - this.listeners = new CopyOnWriteArrayList<>(); + this.useLazyPreparation = useLazyPreparation; + playWhenReady = false; + repeatMode = Player.REPEAT_MODE_OFF; + shuffleModeEnabled = false; + listeners = new CopyOnWriteArrayList<>(); + mediaSourceHolders = new ArrayList<>(); + shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0); emptyTrackSelectorResult = new TrackSelectorResult( new RendererConfiguration[renderers.length], @@ -121,6 +136,7 @@ import java.util.concurrent.TimeoutException; playbackParameters = PlaybackParameters.DEFAULT; seekParameters = SeekParameters.DEFAULT; playbackSuppressionReason = PLAYBACK_SUPPRESSION_REASON_NONE; + maskingWindowIndex = C.INDEX_UNSET; eventHandler = new Handler(looper) { @Override @@ -130,6 +146,9 @@ import java.util.concurrent.TimeoutException; }; playbackInfo = PlaybackInfo.createDummy(/* startPositionUs= */ 0, emptyTrackSelectorResult); pendingListenerNotifications = new ArrayDeque<>(); + if (analyticsCollector != null) { + analyticsCollector.setPlayer(this); + } internalPlayer = new ExoPlayerImplInternal( renderers, @@ -140,6 +159,7 @@ import java.util.concurrent.TimeoutException; playWhenReady, repeatMode, shuffleModeEnabled, + analyticsCollector, eventHandler, clock); internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); @@ -226,45 +246,182 @@ import java.util.concurrent.TimeoutException; return playbackInfo.playbackError; } + /** @deprecated Use {@link #prepare()} instead. */ + @Deprecated @Override public void retry() { - if (mediaSource != null && playbackInfo.playbackState == Player.STATE_IDLE) { - prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); - } - } - - @Override - @Deprecated - public void prepare(MediaSource mediaSource) { - setMediaSource(mediaSource); - prepareInternal(/* resetPosition= */ true, /* resetState= */ true); - } - - @Override - @Deprecated - public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - setMediaSource(mediaSource); - prepareInternal(resetPosition, resetState); + prepare(); } @Override public void prepare() { - Assertions.checkNotNull(mediaSource); - prepareInternal(/* resetPosition= */ false, /* resetState= */ true); + if (playbackInfo.playbackState != Player.STATE_IDLE) { + return; + } + PlaybackInfo playbackInfo = + getResetPlaybackInfo( + /* clearPlaylist= */ false, + /* resetError= */ true, + /* playbackState= */ this.playbackInfo.timeline.isEmpty() + ? Player.STATE_ENDED + : Player.STATE_BUFFERING); + // Trigger internal prepare first before updating the playback info and notifying external + // listeners to ensure that new operations issued in the listener notifications reach the + // player after this prepare. The internal player can't change the playback info immediately + // because it uses a callback. + pendingOperationAcks++; + internalPlayer.prepare(); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + /* ignored */ TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + /* seekProcessed= */ false); } + /** + * @deprecated Use {@link #setMediaSource(MediaSource)} and {@link ExoPlayer#prepare()} instead. + */ + @Deprecated @Override - public void setMediaSource(MediaSource mediaSource, long startPositionMs) { - if (!getCurrentTimeline().isEmpty()) { - stop(/* reset= */ true); - } - seekTo(/* windowIndex= */ 0, startPositionMs); + public void prepare(MediaSource mediaSource) { setMediaSource(mediaSource); + prepare(); + } + + /** + * @deprecated Use {@link #setMediaSource(MediaSource, boolean)} and {@link ExoPlayer#prepare()} + * instead. + */ + @Deprecated + @Override + public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + setMediaSource(mediaSource, resetPosition); + prepare(); } @Override public void setMediaSource(MediaSource mediaSource) { - this.mediaSource = mediaSource; + setMediaSources(Collections.singletonList(mediaSource)); + } + + @Override + public void setMediaSource(MediaSource mediaSource, long startPositionMs) { + setMediaSources( + Collections.singletonList(mediaSource), /* startWindowIndex= */ 0, startPositionMs); + } + + @Override + public void setMediaSource(MediaSource mediaSource, boolean resetPosition) { + setMediaSources(Collections.singletonList(mediaSource), resetPosition); + } + + @Override + public void setMediaSources(List mediaSources) { + setMediaSources(mediaSources, /* resetPosition= */ true); + } + + @Override + public void setMediaSources(List mediaSources, boolean resetPosition) { + setMediaItemsInternal( + mediaSources, + /* startWindowIndex= */ C.INDEX_UNSET, + /* startPositionMs= */ C.TIME_UNSET, + /* resetToDefaultPosition= */ resetPosition); + } + + @Override + public void setMediaSources( + List mediaSources, int startWindowIndex, long startPositionMs) { + setMediaItemsInternal( + mediaSources, startWindowIndex, startPositionMs, /* resetToDefaultPosition= */ false); + } + + @Override + public void addMediaSource(MediaSource mediaSource) { + addMediaSources(Collections.singletonList(mediaSource)); + } + + @Override + public void addMediaSource(int index, MediaSource mediaSource) { + addMediaSources(index, Collections.singletonList(mediaSource)); + } + + @Override + public void addMediaSources(List mediaSources) { + addMediaSources(/* index= */ mediaSourceHolders.size(), mediaSources); + } + + @Override + public void addMediaSources(int index, List mediaSources) { + Assertions.checkArgument(index >= 0); + int currentWindowIndex = getCurrentWindowIndex(); + long currentPositionMs = getCurrentPosition(); + Timeline oldTimeline = getCurrentTimeline(); + pendingOperationAcks++; + List holders = addMediaSourceHolders(index, mediaSources); + Timeline timeline = + maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline); + internalPlayer.addMediaSources(index, holders, shuffleOrder); + notifyListeners( + listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)); + } + + @Override + public MediaSource removeMediaItem(int index) { + List mediaSourceHolders = + removeMediaItemsInternal(/* fromIndex= */ index, /* toIndex= */ index + 1); + return mediaSourceHolders.isEmpty() ? null : mediaSourceHolders.get(0).mediaSource; + } + + @Override + public void removeMediaItems(int fromIndex, int toIndex) { + Assertions.checkArgument(toIndex > fromIndex); + removeMediaItemsInternal(fromIndex, toIndex); + } + + @Override + public void moveMediaItem(int currentIndex, int newIndex) { + Assertions.checkArgument(currentIndex != newIndex); + moveMediaItems(/* fromIndex= */ currentIndex, /* toIndex= */ currentIndex + 1, newIndex); + } + + @Override + public void moveMediaItems(int fromIndex, int toIndex, int newFromIndex) { + Assertions.checkArgument( + fromIndex >= 0 + && fromIndex <= toIndex + && toIndex <= mediaSourceHolders.size() + && newFromIndex >= 0); + int currentWindowIndex = getCurrentWindowIndex(); + long currentPositionMs = getCurrentPosition(); + Timeline oldTimeline = getCurrentTimeline(); + pendingOperationAcks++; + newFromIndex = Math.min(newFromIndex, mediaSourceHolders.size() - (toIndex - fromIndex)); + Playlist.moveMediaSourceHolders(mediaSourceHolders, fromIndex, toIndex, newFromIndex); + Timeline timeline = + maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline); + internalPlayer.moveMediaSources(fromIndex, toIndex, newFromIndex, shuffleOrder); + notifyListeners( + listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)); + } + + @Override + public void clearMediaItems() { + if (mediaSourceHolders.isEmpty()) { + return; + } + removeMediaItemsInternal(/* fromIndex= */ 0, /* toIndex= */ mediaSourceHolders.size()); + } + + @Override + public void setShuffleOrder(ShuffleOrder shuffleOrder) { + pendingOperationAcks++; + this.shuffleOrder = shuffleOrder; + Timeline timeline = maskTimeline(); + internalPlayer.setShuffleOrder(shuffleOrder); + notifyListeners( + listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)); } @Override @@ -365,18 +522,7 @@ import java.util.concurrent.TimeoutException; .sendToTarget(); return; } - maskingWindowIndex = windowIndex; - if (timeline.isEmpty()) { - maskingWindowPositionMs = positionMs == C.TIME_UNSET ? 0 : positionMs; - maskingPeriodIndex = 0; - } else { - long windowPositionUs = positionMs == C.TIME_UNSET - ? timeline.getWindow(windowIndex, window).getDefaultPositionUs() : C.msToUs(positionMs); - Pair periodUidAndPosition = - timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs); - maskingWindowPositionMs = C.usToMs(windowPositionUs); - maskingPeriodIndex = timeline.getIndexOfPeriod(periodUidAndPosition.first); - } + maskWindowIndexAndPositionForSeek(timeline, windowIndex, positionMs); internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs)); notifyListeners(listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK)); } @@ -427,13 +573,9 @@ import java.util.concurrent.TimeoutException; @Override public void stop(boolean reset) { - if (reset) { - mediaSource = null; - } PlaybackInfo playbackInfo = getResetPlaybackInfo( - /* resetPosition= */ reset, - /* resetState= */ reset, + /* clearPlaylist= */ reset, /* resetError= */ reset, /* playbackState= */ Player.STATE_IDLE); // Trigger internal stop first before updating the playback info and notifying external @@ -446,7 +588,7 @@ import java.util.concurrent.TimeoutException; playbackInfo, /* positionDiscontinuity= */ false, /* ignored */ DISCONTINUITY_REASON_INTERNAL, - TIMELINE_CHANGE_REASON_RESET, + TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, /* seekProcessed= */ false); } @@ -455,7 +597,6 @@ import java.util.concurrent.TimeoutException; Log.i(TAG, "Release " + Integer.toHexString(System.identityHashCode(this)) + " [" + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "] [" + ExoPlayerLibraryInfo.registeredModules() + "]"); - mediaSource = null; if (!internalPlayer.release()) { notifyListeners( listener -> @@ -466,8 +607,7 @@ import java.util.concurrent.TimeoutException; eventHandler.removeCallbacksAndMessages(null); playbackInfo = getResetPlaybackInfo( - /* resetPosition= */ false, - /* resetState= */ false, + /* clearPlaylist= */ false, /* resetError= */ false, /* playbackState= */ Player.STATE_IDLE); } @@ -493,12 +633,8 @@ import java.util.concurrent.TimeoutException; @Override public int getCurrentWindowIndex() { - if (shouldMaskPosition()) { - return maskingWindowIndex; - } else { - return playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period) - .windowIndex; - } + int currentWindowIndex = getCurrentWindowIndexInternal(); + return currentWindowIndex == C.INDEX_UNSET ? 0 : currentWindowIndex; } @Override @@ -615,10 +751,11 @@ import java.util.concurrent.TimeoutException; // Not private so it can be called from an inner class without going through a thunk method. /* package */ void handleEvent(Message msg) { + switch (msg.what) { case ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED: handlePlaybackInfo( - (PlaybackInfo) msg.obj, + /* playbackInfo= */ (PlaybackInfo) msg.obj, /* operationAcks= */ msg.arg1, /* positionDiscontinuity= */ msg.arg2 != C.INDEX_UNSET, /* positionDiscontinuityReason= */ msg.arg2); @@ -631,27 +768,13 @@ import java.util.concurrent.TimeoutException; } } - /* package */ void prepareInternal(boolean resetPosition, boolean resetState) { - Assertions.checkNotNull(mediaSource); - PlaybackInfo playbackInfo = - getResetPlaybackInfo( - resetPosition, - resetState, - /* resetError= */ true, - /* playbackState= */ Player.STATE_BUFFERING); - // Trigger internal prepare first before updating the playback info and notifying external - // listeners to ensure that new operations issued in the listener notifications reach the - // player after this prepare. The internal player can't change the playback info immediately - // because it uses a callback. - hasPendingPrepare = true; - pendingOperationAcks++; - internalPlayer.prepare(mediaSource, resetPosition, resetState); - updatePlaybackInfo( - playbackInfo, - /* positionDiscontinuity= */ false, - /* ignored */ DISCONTINUITY_REASON_INTERNAL, - TIMELINE_CHANGE_REASON_RESET, - /* seekProcessed= */ false); + private int getCurrentWindowIndexInternal() { + if (shouldMaskPosition()) { + return maskingWindowIndex; + } else { + return playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period) + .windowIndex; + } } private void handlePlaybackParameters( @@ -685,59 +808,51 @@ import java.util.concurrent.TimeoutException; } if (!this.playbackInfo.timeline.isEmpty() && playbackInfo.timeline.isEmpty()) { // Update the masking variables, which are used when the timeline becomes empty. - maskingPeriodIndex = 0; - maskingWindowIndex = 0; - maskingWindowPositionMs = 0; + resetMaskingPosition(); } - @Player.TimelineChangeReason - int timelineChangeReason = - hasPendingPrepare - ? Player.TIMELINE_CHANGE_REASON_PREPARED - : Player.TIMELINE_CHANGE_REASON_DYNAMIC; boolean seekProcessed = hasPendingSeek; - hasPendingPrepare = false; hasPendingSeek = false; updatePlaybackInfo( playbackInfo, positionDiscontinuity, positionDiscontinuityReason, - timelineChangeReason, + TIMELINE_CHANGE_REASON_SOURCE_UPDATE, seekProcessed); } } private PlaybackInfo getResetPlaybackInfo( - boolean resetPosition, - boolean resetState, - boolean resetError, - @Player.State int playbackState) { - if (resetPosition) { - maskingWindowIndex = 0; - maskingPeriodIndex = 0; - maskingWindowPositionMs = 0; + boolean clearPlaylist, boolean resetError, @Player.State int playbackState) { + if (clearPlaylist) { + // Reset list of media source holders which are used for creating the masking timeline. + removeMediaSourceHolders( + /* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolders.size()); + resetMaskingPosition(); } else { maskingWindowIndex = getCurrentWindowIndex(); maskingPeriodIndex = getCurrentPeriodIndex(); maskingWindowPositionMs = getCurrentPosition(); } - // Also reset period-based PlaybackInfo positions if resetting the state. - resetPosition = resetPosition || resetState; - MediaPeriodId mediaPeriodId = - resetPosition - ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period) - : playbackInfo.periodId; - long startPositionUs = resetPosition ? 0 : playbackInfo.positionUs; - long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs; + Timeline timeline = playbackInfo.timeline; + MediaPeriodId mediaPeriodId = playbackInfo.periodId; + long contentPositionUs = playbackInfo.contentPositionUs; + long startPositionUs = playbackInfo.positionUs; + if (clearPlaylist) { + timeline = Timeline.EMPTY; + mediaPeriodId = playbackInfo.getDummyPeriodForEmptyTimeline(); + contentPositionUs = C.TIME_UNSET; + startPositionUs = C.TIME_UNSET; + } return new PlaybackInfo( - resetState ? Timeline.EMPTY : playbackInfo.timeline, + timeline, mediaPeriodId, startPositionUs, contentPositionUs, playbackState, resetError ? null : playbackInfo.playbackError, /* isLoading= */ false, - resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, - resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, + clearPlaylist ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, + clearPlaylist ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, mediaPeriodId, startPositionUs, /* totalBufferedDurationUs= */ 0, @@ -747,8 +862,8 @@ import java.util.concurrent.TimeoutException; private void updatePlaybackInfo( PlaybackInfo playbackInfo, boolean positionDiscontinuity, - @Player.DiscontinuityReason int positionDiscontinuityReason, - @Player.TimelineChangeReason int timelineChangeReason, + @DiscontinuityReason int positionDiscontinuityReason, + @TimelineChangeReason int timelineChangeReason, boolean seekProcessed) { boolean previousIsPlaying = isPlaying(); // Assign playback info immediately such that all getters return the right values. @@ -769,6 +884,218 @@ import java.util.concurrent.TimeoutException; /* isPlayingChanged= */ previousIsPlaying != isPlaying)); } + private void setMediaItemsInternal( + List mediaItems, + int startWindowIndex, + long startPositionMs, + boolean resetToDefaultPosition) { + int currentWindowIndex = getCurrentWindowIndexInternal(); + long currentPositionMs = getCurrentPosition(); + boolean currentPlayWhenReady = getPlayWhenReady(); + pendingOperationAcks++; + if (!mediaSourceHolders.isEmpty()) { + removeMediaSourceHolders( + /* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolders.size()); + } + List holders = addMediaSourceHolders(/* index= */ 0, mediaItems); + Timeline timeline = maskTimeline(); + if (!timeline.isEmpty() && startWindowIndex >= timeline.getWindowCount()) { + throw new IllegalSeekPositionException(timeline, startWindowIndex, startPositionMs); + } + // Evaluate the actual start position. + if (resetToDefaultPosition) { + startWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); + startPositionMs = C.TIME_UNSET; + } else if (startWindowIndex == C.INDEX_UNSET) { + startWindowIndex = currentWindowIndex; + startPositionMs = currentPositionMs; + } + maskWindowIndexAndPositionForSeek( + timeline, startWindowIndex == C.INDEX_UNSET ? 0 : startWindowIndex, startPositionMs); + // mask the playback state + int maskingPlaybackState = playbackInfo.playbackState; + if (startWindowIndex != C.INDEX_UNSET) { + // Position reset to startWindowIndex (results in pending initial seek). + if (timeline.isEmpty() || startWindowIndex >= timeline.getWindowCount()) { + // Setting an empty timeline or invalid seek transitions to ended. + maskingPlaybackState = STATE_ENDED; + } else { + maskingPlaybackState = STATE_BUFFERING; + } + } + boolean playbackStateChanged = + playbackInfo.playbackState != STATE_IDLE + && playbackInfo.playbackState != maskingPlaybackState; + int finalMaskingPlaybackState = maskingPlaybackState; + if (playbackStateChanged) { + playbackInfo = playbackInfo.copyWithPlaybackState(finalMaskingPlaybackState); + } + internalPlayer.setMediaSources( + holders, startWindowIndex, C.msToUs(startPositionMs), shuffleOrder); + notifyListeners( + listener -> { + listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + if (playbackStateChanged) { + listener.onPlayerStateChanged(currentPlayWhenReady, finalMaskingPlaybackState); + } + }); + } + + private List addMediaSourceHolders( + int index, List mediaSources) { + List holders = new ArrayList<>(); + for (int i = 0; i < mediaSources.size(); i++) { + Playlist.MediaSourceHolder holder = + new Playlist.MediaSourceHolder(mediaSources.get(i), useLazyPreparation); + holders.add(holder); + mediaSourceHolders.add(i + index, holder); + } + shuffleOrder = + shuffleOrder.cloneAndInsert( + /* insertionIndex= */ index, /* insertionCount= */ holders.size()); + return holders; + } + + private List removeMediaItemsInternal(int fromIndex, int toIndex) { + Assertions.checkArgument( + fromIndex >= 0 && toIndex >= fromIndex && toIndex <= mediaSourceHolders.size()); + int currentWindowIndex = getCurrentWindowIndex(); + long currentPositionMs = getCurrentPosition(); + boolean currentPlayWhenReady = getPlayWhenReady(); + Timeline oldTimeline = getCurrentTimeline(); + int currentMediaSourceCount = mediaSourceHolders.size(); + pendingOperationAcks++; + List removedHolders = + removeMediaSourceHolders(fromIndex, /* toIndexExclusive= */ toIndex); + Timeline timeline = + maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline); + // Player transitions to STATE_ENDED if the current index is part of the removed tail. + final boolean transitionsToEnded = + playbackInfo.playbackState != STATE_IDLE + && playbackInfo.playbackState != STATE_ENDED + && fromIndex < toIndex + && toIndex == currentMediaSourceCount + && currentWindowIndex >= timeline.getWindowCount(); + if (transitionsToEnded) { + playbackInfo = playbackInfo.copyWithPlaybackState(STATE_ENDED); + } + internalPlayer.removeMediaSources(fromIndex, toIndex, shuffleOrder); + notifyListeners( + listener -> { + listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + if (transitionsToEnded) { + listener.onPlayerStateChanged(currentPlayWhenReady, STATE_ENDED); + } + }); + return removedHolders; + } + + private List removeMediaSourceHolders( + int fromIndex, int toIndexExclusive) { + List removed = new ArrayList<>(); + for (int i = toIndexExclusive - 1; i >= fromIndex; i--) { + removed.add(mediaSourceHolders.remove(i)); + } + shuffleOrder = shuffleOrder.cloneAndRemove(fromIndex, toIndexExclusive); + return removed; + } + + private Timeline maskTimeline() { + playbackInfo = + playbackInfo.copyWithTimeline( + mediaSourceHolders.isEmpty() + ? Timeline.EMPTY + : new Playlist.PlaylistTimeline(mediaSourceHolders, shuffleOrder)); + return playbackInfo.timeline; + } + + private Timeline maskTimelineAndWindowIndex( + int currentWindowIndex, long currentPositionMs, Timeline oldTimeline) { + Timeline maskingTimeline = maskTimeline(); + if (oldTimeline.isEmpty()) { + // The index is the default index or was set by a seek in the empty old timeline. + maskingWindowIndex = currentWindowIndex; + if (!maskingTimeline.isEmpty() && currentWindowIndex >= maskingTimeline.getWindowCount()) { + // The seek is not valid in the new timeline. + maskWithDefaultPosition(maskingTimeline); + } + return maskingTimeline; + } + @Nullable + Pair periodPosition = + oldTimeline.getPeriodPosition( + window, + period, + currentWindowIndex, + C.msToUs(currentPositionMs), + /* defaultPositionProjectionUs= */ 0); + Object periodUid = Util.castNonNull(periodPosition).first; + if (maskingTimeline.getIndexOfPeriod(periodUid) != C.INDEX_UNSET) { + // Get the window index of the current period that exists in the new timeline also. + maskingWindowIndex = maskingTimeline.getPeriodByUid(periodUid, period).windowIndex; + } else { + // Period uid not found in new timeline. Try to get subsequent period. + @Nullable + Object nextPeriodUid = + ExoPlayerImplInternal.resolveSubsequentPeriod( + window, + period, + repeatMode, + shuffleModeEnabled, + periodUid, + oldTimeline, + maskingTimeline); + if (nextPeriodUid != null) { + // Set masking to the default position of the window of the subsequent period. + maskingWindowIndex = maskingTimeline.getPeriodByUid(nextPeriodUid, period).windowIndex; + maskingPeriodIndex = maskingTimeline.getWindow(maskingWindowIndex, window).firstPeriodIndex; + maskingWindowPositionMs = window.getDefaultPositionMs(); + } else { + // Reset if no subsequent period is found. + maskWithDefaultPosition(maskingTimeline); + } + } + return maskingTimeline; + } + + private void maskWindowIndexAndPositionForSeek( + Timeline timeline, int windowIndex, long positionMs) { + maskingWindowIndex = windowIndex; + if (timeline.isEmpty()) { + maskingWindowPositionMs = positionMs == C.TIME_UNSET ? 0 : positionMs; + maskingPeriodIndex = 0; + } else if (windowIndex >= timeline.getWindowCount()) { + // An initial seek now proves to be invalid in the actual timeline. + maskWithDefaultPosition(timeline); + } else { + long windowPositionUs = + positionMs == C.TIME_UNSET + ? timeline.getWindow(windowIndex, window).getDefaultPositionUs() + : C.msToUs(positionMs); + Pair periodUidAndPosition = + timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs); + maskingWindowPositionMs = C.usToMs(windowPositionUs); + maskingPeriodIndex = timeline.getIndexOfPeriod(periodUidAndPosition.first); + } + } + + private void maskWithDefaultPosition(Timeline timeline) { + if (timeline.isEmpty()) { + resetMaskingPosition(); + return; + } + maskingWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); + timeline.getWindow(maskingWindowIndex, window); + maskingWindowPositionMs = window.getDefaultPositionMs(); + maskingPeriodIndex = window.firstPeriodIndex; + } + + private void resetMaskingPosition() { + maskingWindowIndex = C.INDEX_UNSET; + maskingWindowPositionMs = 0; + maskingPeriodIndex = 0; + } + private void notifyListeners(ListenerInvocation listenerInvocation) { CopyOnWriteArrayList listenerSnapshot = new CopyOnWriteArrayList<>(listeners); notifyListeners(() -> invokeAll(listenerSnapshot, listenerInvocation)); @@ -804,7 +1131,7 @@ import java.util.concurrent.TimeoutException; private final TrackSelector trackSelector; private final boolean positionDiscontinuity; private final @Player.DiscontinuityReason int positionDiscontinuityReason; - private final @Player.TimelineChangeReason int timelineChangeReason; + private final int timelineChangeReason; private final boolean seekProcessed; private final boolean playbackStateChanged; private final boolean playbackErrorChanged; @@ -838,15 +1165,15 @@ import java.util.concurrent.TimeoutException; playbackErrorChanged = previousPlaybackInfo.playbackError != playbackInfo.playbackError && playbackInfo.playbackError != null; - timelineChanged = previousPlaybackInfo.timeline != playbackInfo.timeline; isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading; + timelineChanged = !previousPlaybackInfo.timeline.equals(playbackInfo.timeline); trackSelectorResultChanged = previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult; } @Override public void run() { - if (timelineChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) { + if (timelineChanged) { invokeAll( listenerSnapshot, listener -> listener.onTimelineChanged(playbackInfo.timeline, timelineChangeReason)); 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 2c6f7631cf..47f85b603a 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 @@ -25,11 +25,11 @@ import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.analytics.AnalyticsCollector; 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.MediaSource.MediaSourceCaller; import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelector; @@ -44,6 +44,7 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; /** Implements the internal behavior of {@link ExoPlayerImpl}. */ @@ -51,7 +52,7 @@ import java.util.concurrent.atomic.AtomicBoolean; implements Handler.Callback, MediaPeriod.Callback, TrackSelector.InvalidationListener, - MediaSourceCaller, + Playlist.PlaylistInfoRefreshListener, PlaybackParameterListener, PlayerMessage.Sender { @@ -70,16 +71,21 @@ import java.util.concurrent.atomic.AtomicBoolean; private static final int MSG_SET_SEEK_PARAMETERS = 5; private static final int MSG_STOP = 6; private static final int MSG_RELEASE = 7; - private static final int MSG_REFRESH_SOURCE_INFO = 8; - private static final int MSG_PERIOD_PREPARED = 9; - private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 10; - private static final int MSG_TRACK_SELECTION_INVALIDATED = 11; - private static final int MSG_SET_REPEAT_MODE = 12; - private static final int MSG_SET_SHUFFLE_ENABLED = 13; - private static final int MSG_SET_FOREGROUND_MODE = 14; - private static final int MSG_SEND_MESSAGE = 15; - private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 16; - private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 17; + private static final int MSG_PERIOD_PREPARED = 8; + private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 9; + private static final int MSG_TRACK_SELECTION_INVALIDATED = 10; + private static final int MSG_SET_REPEAT_MODE = 11; + private static final int MSG_SET_SHUFFLE_ENABLED = 12; + private static final int MSG_SET_FOREGROUND_MODE = 13; + private static final int MSG_SEND_MESSAGE = 14; + private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 15; + private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 16; + private static final int MSG_SET_MEDIA_SOURCES = 17; + private static final int MSG_ADD_MEDIA_SOURCES = 18; + private static final int MSG_MOVE_MEDIA_SOURCES = 19; + private static final int MSG_REMOVE_MEDIA_SOURCES = 20; + private static final int MSG_SET_SHUFFLE_ORDER = 21; + private static final int MSG_PLAYLIST_UPDATE_REQUESTED = 22; private static final int ACTIVE_INTERVAL_MS = 10; private static final int IDLE_INTERVAL_MS = 1000; @@ -102,12 +108,12 @@ import java.util.concurrent.atomic.AtomicBoolean; private final ArrayList pendingMessages; private final Clock clock; private final MediaPeriodQueue queue; + private final Playlist playlist; @SuppressWarnings("unused") private SeekParameters seekParameters; private PlaybackInfo playbackInfo; - private MediaSource mediaSource; private Renderer[] enabledRenderers; private boolean released; private boolean playWhenReady; @@ -117,8 +123,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private boolean shuffleModeEnabled; private boolean foregroundMode; - private int pendingPrepareCount; - private SeekPosition pendingInitialSeekPosition; + @Nullable private SeekPosition pendingInitialSeekPosition; private long rendererPositionUs; private int nextPendingMessageIndex; private boolean deliverPendingMessageAtStartPositionRequired; @@ -134,6 +139,7 @@ import java.util.concurrent.atomic.AtomicBoolean; boolean playWhenReady, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled, + @Nullable AnalyticsCollector analyticsCollector, Handler eventHandler, Clock clock) { this.renderers = renderers; @@ -174,16 +180,18 @@ import java.util.concurrent.atomic.AtomicBoolean; internalPlaybackThread.start(); handler = clock.createHandler(internalPlaybackThread.getLooper(), this); deliverPendingMessageAtStartPositionRequired = true; + playlist = new Playlist(this); + if (analyticsCollector != null) { + playlist.setAnalyticsCollector(eventHandler, analyticsCollector); + } } public void experimental_setReleaseTimeoutMs(long releaseTimeoutMs) { this.releaseTimeoutMs = releaseTimeoutMs; } - public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - handler - .obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, resetState ? 1 : 0, mediaSource) - .sendToTarget(); + public void prepare() { + handler.obtainMessage(MSG_PREPARE).sendToTarget(); } public void setPlayWhenReady(boolean playWhenReady) { @@ -216,6 +224,50 @@ import java.util.concurrent.atomic.AtomicBoolean; handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget(); } + public void setMediaSources( + List mediaSources, + int windowIndex, + long positionUs, + ShuffleOrder shuffleOrder) { + handler + .obtainMessage( + MSG_SET_MEDIA_SOURCES, + new PlaylistUpdateMessage(mediaSources, shuffleOrder, windowIndex, positionUs)) + .sendToTarget(); + } + + public void addMediaSources( + int index, List mediaSources, ShuffleOrder shuffleOrder) { + handler + .obtainMessage( + MSG_ADD_MEDIA_SOURCES, + index, + /* ignored */ 0, + new PlaylistUpdateMessage( + mediaSources, + shuffleOrder, + /* windowIndex= */ C.INDEX_UNSET, + /* positionUs= */ C.TIME_UNSET)) + .sendToTarget(); + } + + public void removeMediaSources(int fromIndex, int toIndex, ShuffleOrder shuffleOrder) { + handler + .obtainMessage(MSG_REMOVE_MEDIA_SOURCES, fromIndex, toIndex, shuffleOrder) + .sendToTarget(); + } + + public void moveMediaSources( + int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) { + MoveMediaItemsMessage moveMediaItemsMessage = + new MoveMediaItemsMessage(fromIndex, toIndex, newFromIndex, shuffleOrder); + handler.obtainMessage(MSG_MOVE_MEDIA_SOURCES, moveMediaItemsMessage).sendToTarget(); + } + + public void setShuffleOrder(ShuffleOrder shuffleOrder) { + handler.obtainMessage(MSG_SET_SHUFFLE_ORDER, shuffleOrder).sendToTarget(); + } + @Override public synchronized void sendMessage(PlayerMessage message) { if (released || !internalPlaybackThread.isAlive()) { @@ -275,13 +327,11 @@ import java.util.concurrent.atomic.AtomicBoolean; return internalPlaybackThread.getLooper(); } - // MediaSource.MediaSourceCaller implementation. + // Playlist.PlaylistInfoRefreshListener implementation. @Override - public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { - handler - .obtainMessage(MSG_REFRESH_SOURCE_INFO, new MediaSourceRefreshInfo(source, timeline)) - .sendToTarget(); + public void onPlaylistUpdateRequested() { + handler.sendEmptyMessage(MSG_PLAYLIST_UPDATE_REQUESTED); } // MediaPeriod.Callback implementation. @@ -313,14 +363,12 @@ import java.util.concurrent.atomic.AtomicBoolean; // Handler.Callback implementation. @Override + @SuppressWarnings("unchecked") public boolean handleMessage(Message msg) { try { switch (msg.what) { case MSG_PREPARE: - prepareInternal( - (MediaSource) msg.obj, - /* resetPosition= */ msg.arg1 != 0, - /* resetState= */ msg.arg2 != 0); + prepareInternal(); break; case MSG_SET_PLAY_WHEN_READY: setPlayWhenReadyInternal(msg.arg1 != 0); @@ -356,9 +404,6 @@ import java.util.concurrent.atomic.AtomicBoolean; case MSG_PERIOD_PREPARED: handlePeriodPrepared((MediaPeriod) msg.obj); break; - case MSG_REFRESH_SOURCE_INFO: - handleSourceInfoRefreshed((MediaSourceRefreshInfo) msg.obj); - break; case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: handleContinueLoadingRequested((MediaPeriod) msg.obj); break; @@ -375,6 +420,24 @@ import java.util.concurrent.atomic.AtomicBoolean; case MSG_SEND_MESSAGE_TO_TARGET_THREAD: sendMessageToTargetThread((PlayerMessage) msg.obj); break; + case MSG_SET_MEDIA_SOURCES: + setMediaItemsInternal((PlaylistUpdateMessage) msg.obj); + break; + case MSG_ADD_MEDIA_SOURCES: + addMediaItemsInternal((PlaylistUpdateMessage) msg.obj, msg.arg1); + break; + case MSG_MOVE_MEDIA_SOURCES: + moveMediaItemsInternal((MoveMediaItemsMessage) msg.obj); + break; + case MSG_REMOVE_MEDIA_SOURCES: + removeMediaItemsInternal(msg.arg1, msg.arg2, (ShuffleOrder) msg.obj); + break; + case MSG_SET_SHUFFLE_ORDER: + setShuffleOrderInternal((ShuffleOrder) msg.obj); + break; + case MSG_PLAYLIST_UPDATE_REQUESTED: + playlistUpdateRequestedInternal(); + break; case MSG_RELEASE: releaseInternal(); // Return immediately to not send playback info updates after release. @@ -509,21 +572,77 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - private void prepareInternal(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - pendingPrepareCount++; + private void prepareInternal() { + playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); resetInternal( /* resetRenderers= */ false, - /* releaseMediaSource= */ true, - resetPosition, - resetState, + /* resetPosition= */ false, + /* releasePlaylist= */ false, + /* clearPlaylist= */ false, /* resetError= */ true); loadControl.onPrepared(); - this.mediaSource = mediaSource; - setState(Player.STATE_BUFFERING); - mediaSource.prepareSource(/* caller= */ this, bandwidthMeter.getTransferListener()); + setState(playbackInfo.timeline.isEmpty() ? Player.STATE_ENDED : Player.STATE_BUFFERING); + playlist.prepare(bandwidthMeter.getTransferListener()); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } + private void setMediaItemsInternal(PlaylistUpdateMessage playlistUpdateMessage) + throws ExoPlaybackException { + playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); + if (playlistUpdateMessage.windowIndex != C.INDEX_UNSET) { + pendingInitialSeekPosition = + new SeekPosition( + new Playlist.PlaylistTimeline( + playlistUpdateMessage.mediaSourceHolders, playlistUpdateMessage.shuffleOrder), + playlistUpdateMessage.windowIndex, + playlistUpdateMessage.positionUs); + } + Timeline timeline = + playlist.setMediaSources( + playlistUpdateMessage.mediaSourceHolders, playlistUpdateMessage.shuffleOrder); + handlePlaylistInfoRefreshed(timeline); + } + + private void addMediaItemsInternal(PlaylistUpdateMessage addMessage, int insertionIndex) + throws ExoPlaybackException { + playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); + Timeline timeline = + playlist.addMediaSources( + insertionIndex == C.INDEX_UNSET ? playlist.getSize() : insertionIndex, + addMessage.mediaSourceHolders, + addMessage.shuffleOrder); + handlePlaylistInfoRefreshed(timeline); + } + + private void moveMediaItemsInternal(MoveMediaItemsMessage moveMediaItemsMessage) + throws ExoPlaybackException { + playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); + Timeline timeline = + playlist.moveMediaSourceRange( + moveMediaItemsMessage.fromIndex, + moveMediaItemsMessage.toIndex, + moveMediaItemsMessage.newFromIndex, + moveMediaItemsMessage.shuffleOrder); + handlePlaylistInfoRefreshed(timeline); + } + + private void removeMediaItemsInternal(int fromIndex, int toIndex, ShuffleOrder shuffleOrder) + throws ExoPlaybackException { + playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); + Timeline timeline = playlist.removeMediaSourceRange(fromIndex, toIndex, shuffleOrder); + handlePlaylistInfoRefreshed(timeline); + } + + private void playlistUpdateRequestedInternal() throws ExoPlaybackException { + handlePlaylistInfoRefreshed(playlist.createTimeline()); + } + + private void setShuffleOrderInternal(ShuffleOrder shuffleOrder) throws ExoPlaybackException { + playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); + Timeline timeline = playlist.setShuffleOrder(shuffleOrder); + handlePlaylistInfoRefreshed(timeline); + } + private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException { rebuffering = false; this.playWhenReady = playWhenReady; @@ -563,7 +682,11 @@ import java.util.concurrent.atomic.AtomicBoolean; // position of the playing period to make sure none of the removed period is played. MediaPeriodId periodId = queue.getPlayingPeriod().info.id; long newPositionUs = - seekToPeriodPosition(periodId, playbackInfo.positionUs, /* forceDisableRenderers= */ true); + seekToPeriodPosition( + periodId, + playbackInfo.positionUs, + /* forceDisableRenderers= */ true, + /* forceBufferingState= */ false); if (newPositionUs != playbackInfo.positionUs) { playbackInfo = copyWithNewPosition(periodId, newPositionUs, playbackInfo.contentPositionUs); if (sendDiscontinuity) { @@ -741,14 +864,15 @@ import java.util.concurrent.atomic.AtomicBoolean; long periodPositionUs; long contentPositionUs; boolean seekPositionAdjusted; + @Nullable Pair resolvedSeekPosition = resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true); if (resolvedSeekPosition == null) { // The seek position was valid for the timeline that it was performed into, but the // timeline has changed or is not ready and a suitable seek position could not be resolved. - periodId = playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period); - periodPositionUs = C.TIME_UNSET; + periodId = getDummyFirstMediaPeriodForAds(); contentPositionUs = C.TIME_UNSET; + periodPositionUs = C.TIME_UNSET; seekPositionAdjusted = true; } else { // Update the resolved seek position to take ads into account. @@ -765,7 +889,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } try { - if (mediaSource == null || pendingPrepareCount > 0) { + if (playbackInfo.timeline.isEmpty() || !playlist.isPrepared()) { // Save seek position for later, as we are still waiting for a prepared source. pendingInitialSeekPosition = seekPosition; } else if (periodPositionUs == C.TIME_UNSET) { @@ -773,9 +897,9 @@ import java.util.concurrent.atomic.AtomicBoolean; setState(Player.STATE_ENDED); resetInternal( /* resetRenderers= */ false, - /* releaseMediaSource= */ false, /* resetPosition= */ true, - /* resetState= */ false, + /* releasePlaylist= */ false, + /* clearPlaylist= */ false, /* resetError= */ true); } else { // Execute the seek in the current media periods. @@ -795,7 +919,11 @@ import java.util.concurrent.atomic.AtomicBoolean; return; } } - newPeriodPositionUs = seekToPeriodPosition(periodId, newPeriodPositionUs); + newPeriodPositionUs = + seekToPeriodPosition( + periodId, + newPeriodPositionUs, + /* forceBufferingState= */ playbackInfo.playbackState == Player.STATE_ENDED); seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs; periodPositionUs = newPeriodPositionUs; } @@ -807,19 +935,26 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - private long seekToPeriodPosition(MediaPeriodId periodId, long periodPositionUs) + private long seekToPeriodPosition( + MediaPeriodId periodId, long periodPositionUs, boolean forceBufferingState) throws ExoPlaybackException { // Force disable renderers if they are reading from a period other than the one being played. return seekToPeriodPosition( - periodId, periodPositionUs, queue.getPlayingPeriod() != queue.getReadingPeriod()); + periodId, + periodPositionUs, + queue.getPlayingPeriod() != queue.getReadingPeriod(), + forceBufferingState); } private long seekToPeriodPosition( - MediaPeriodId periodId, long periodPositionUs, boolean forceDisableRenderers) + MediaPeriodId periodId, + long periodPositionUs, + boolean forceDisableRenderers, + boolean forceBufferingState) throws ExoPlaybackException { stopRenderers(); rebuffering = false; - if (playbackInfo.playbackState != Player.STATE_IDLE && !playbackInfo.timeline.isEmpty()) { + if (forceBufferingState || playbackInfo.playbackState == Player.STATE_READY) { setState(Player.STATE_BUFFERING); } @@ -920,13 +1055,11 @@ import java.util.concurrent.atomic.AtomicBoolean; boolean forceResetRenderers, boolean resetPositionAndState, boolean acknowledgeStop) { resetInternal( /* resetRenderers= */ forceResetRenderers || !foregroundMode, - /* releaseMediaSource= */ true, /* resetPosition= */ resetPositionAndState, - /* resetState= */ resetPositionAndState, + /* releasePlaylist= */ true, + /* clearPlaylist= */ resetPositionAndState, /* resetError= */ resetPositionAndState); - playbackInfoUpdate.incrementPendingOperationAcks( - pendingPrepareCount + (acknowledgeStop ? 1 : 0)); - pendingPrepareCount = 0; + playbackInfoUpdate.incrementPendingOperationAcks(acknowledgeStop ? 1 : 0); loadControl.onStopped(); setState(Player.STATE_IDLE); } @@ -934,9 +1067,9 @@ import java.util.concurrent.atomic.AtomicBoolean; private void releaseInternal() { resetInternal( /* resetRenderers= */ true, - /* releaseMediaSource= */ true, /* resetPosition= */ true, - /* resetState= */ true, + /* releasePlaylist= */ true, + /* clearPlaylist= */ true, /* resetError= */ false); loadControl.onReleased(); setState(Player.STATE_IDLE); @@ -949,9 +1082,9 @@ import java.util.concurrent.atomic.AtomicBoolean; private void resetInternal( boolean resetRenderers, - boolean releaseMediaSource, boolean resetPosition, - boolean resetState, + boolean releasePlaylist, + boolean clearPlaylist, boolean resetError) { handler.removeMessages(MSG_DO_SOME_WORK); rebuffering = false; @@ -979,8 +1112,8 @@ import java.util.concurrent.atomic.AtomicBoolean; if (resetPosition) { pendingInitialSeekPosition = null; - } else if (resetState) { - // When resetting the state, also reset the period-based PlaybackInfo position and convert + } else if (clearPlaylist) { + // When clearing the playlist, also reset the period-based PlaybackInfo position and convert // existing position to initial seek instead. resetPosition = true; if (pendingInitialSeekPosition == null && !playbackInfo.timeline.isEmpty()) { @@ -991,51 +1124,65 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - queue.clear(/* keepFrontPeriodUid= */ !resetState); + queue.clear(/* keepFrontPeriodUid= */ !clearPlaylist); shouldContinueLoading = false; - if (resetState) { - queue.setTimeline(Timeline.EMPTY); + Timeline timeline = playbackInfo.timeline; + if (clearPlaylist) { + timeline = playlist.clear(/* shuffleOrder= */ null); + queue.setTimeline(timeline); for (PendingMessageInfo pendingMessageInfo : pendingMessages) { pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false); } pendingMessages.clear(); nextPendingMessageIndex = 0; } - MediaPeriodId mediaPeriodId = - resetPosition - ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period) - : playbackInfo.periodId; + MediaPeriodId mediaPeriodId = playbackInfo.periodId; + long contentPositionUs = playbackInfo.contentPositionUs; + if (resetPosition) { + mediaPeriodId = + timeline.isEmpty() + ? playbackInfo.getDummyPeriodForEmptyTimeline() + : getDummyFirstMediaPeriodForAds(); + contentPositionUs = C.TIME_UNSET; + } // Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored. long startPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.positionUs; - long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs; playbackInfo = new PlaybackInfo( - resetState ? Timeline.EMPTY : playbackInfo.timeline, + timeline, mediaPeriodId, startPositionUs, contentPositionUs, playbackInfo.playbackState, resetError ? null : playbackInfo.playbackError, /* isLoading= */ false, - resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, - resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, + clearPlaylist ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, + clearPlaylist ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, mediaPeriodId, startPositionUs, /* totalBufferedDurationUs= */ 0, startPositionUs); - if (releaseMediaSource) { - if (mediaSource != null) { - mediaSource.releaseSource(/* caller= */ this); - mediaSource = null; - } + if (releasePlaylist) { + playlist.release(); } } + private MediaPeriodId getDummyFirstMediaPeriodForAds() { + MediaPeriodId dummyFirstMediaPeriodId = + playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period); + if (!playbackInfo.timeline.isEmpty()) { + // add ad metadata if any and propagate the window sequence number to new period id. + dummyFirstMediaPeriodId = + queue.resolveMediaPeriodIdForAds(dummyFirstMediaPeriodId.periodUid, /* positionUs= */ 0); + } + return dummyFirstMediaPeriodId; + } + private void sendMessageInternal(PlayerMessage message) throws ExoPlaybackException { if (message.getPositionMs() == C.TIME_UNSET) { // If no delivery time is specified, trigger immediate message delivery. sendMessageToTarget(message); - } else if (mediaSource == null || pendingPrepareCount > 0) { + } else if (playbackInfo.timeline.isEmpty()) { // Still waiting for initial timeline to resolve position. pendingMessages.add(new PendingMessageInfo(message)); } else { @@ -1355,86 +1502,109 @@ import java.util.concurrent.atomic.AtomicBoolean; } } } - mediaSource.maybeThrowSourceInfoRefreshError(); + playlist.maybeThrowSourceInfoRefreshError(); } - private void handleSourceInfoRefreshed(MediaSourceRefreshInfo sourceRefreshInfo) - throws ExoPlaybackException { - if (sourceRefreshInfo.source != mediaSource) { - // Stale event. - return; - } - playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount); - pendingPrepareCount = 0; - + private void handlePlaylistInfoRefreshed(Timeline timeline) throws ExoPlaybackException { Timeline oldTimeline = playbackInfo.timeline; - Timeline timeline = sourceRefreshInfo.timeline; queue.setTimeline(timeline); playbackInfo = playbackInfo.copyWithTimeline(timeline); resolvePendingMessagePositions(); - - MediaPeriodId newPeriodId = playbackInfo.periodId; + if (timeline.isEmpty()) { + @Nullable SeekPosition pendingInitialSeekPosition = this.pendingInitialSeekPosition; + handleEndOfPlaylist(); + // Retain seek position if any. + this.pendingInitialSeekPosition = pendingInitialSeekPosition; + return; + } + MediaPeriodId oldPeriodId = playbackInfo.periodId; + Object newPeriodUid = oldPeriodId.periodUid; long oldContentPositionUs = - playbackInfo.periodId.isAd() ? playbackInfo.contentPositionUs : playbackInfo.positionUs; + oldPeriodId.isAd() ? playbackInfo.contentPositionUs : playbackInfo.positionUs; long newContentPositionUs = oldContentPositionUs; + boolean forceBufferingState = false; if (pendingInitialSeekPosition != null) { // Resolve initial seek position. + @Nullable Pair periodPosition = resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true); - pendingInitialSeekPosition = null; if (periodPosition == null) { - // The seek position was valid for the timeline that it was performed into, but the - // timeline has changed and a suitable seek position could not be resolved in the new one. - handleSourceInfoRefreshEndedPlayback(); - return; + // The initial seek in the empty old timeline is invalid in the new timeline. + handleEndOfPlaylist(); + // Use the period resulting from the reset. + newPeriodUid = playbackInfo.periodId.periodUid; + newContentPositionUs = C.TIME_UNSET; + } else { + // The pending seek has been resolved successfully in the new timeline. + newPeriodUid = periodPosition.first; + newContentPositionUs = + pendingInitialSeekPosition.windowPositionUs == C.TIME_UNSET + ? C.TIME_UNSET + : periodPosition.second; + forceBufferingState = playbackInfo.playbackState == Player.STATE_ENDED; } - newContentPositionUs = periodPosition.second; - newPeriodId = queue.resolveMediaPeriodIdForAds(periodPosition.first, newContentPositionUs); - } else if (oldContentPositionUs == C.TIME_UNSET && !timeline.isEmpty()) { - // Resolve unset start position to default position. + pendingInitialSeekPosition = null; + } else if (oldTimeline.isEmpty()) { + // Resolve to default position if the old timeline is empty and no seek is requested above. Pair defaultPosition = getPeriodPosition( - timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET); - 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) { + timeline, + timeline.getFirstWindowIndex(shuffleModeEnabled), + /* windowPositionUs= */ C.TIME_UNSET); + newPeriodUid = defaultPosition.first; + newContentPositionUs = C.TIME_UNSET; + } else if (timeline.getIndexOfPeriod(newPeriodUid) == C.INDEX_UNSET) { // The current period isn't in the new timeline. Attempt to resolve a subsequent period whose // window we can restart from. - Object newPeriodUid = resolveSubsequentPeriod(newPeriodId.periodUid, oldTimeline, timeline); - if (newPeriodUid == null) { - // We failed to resolve a suitable restart position. - handleSourceInfoRefreshEndedPlayback(); - return; - } - // We resolved a subsequent period. Start at the default position in the corresponding window. - Pair defaultPosition = - getPeriodPosition( - timeline, timeline.getPeriodByUid(newPeriodUid, period).windowIndex, C.TIME_UNSET); - newContentPositionUs = defaultPosition.second; - newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, 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; + @Nullable + Object subsequentPeriodUid = + resolveSubsequentPeriod( + window, period, repeatMode, shuffleModeEnabled, newPeriodUid, oldTimeline, timeline); + if (subsequentPeriodUid == null) { + // We failed to resolve a suitable restart position but the timeline is not empty. + handleEndOfPlaylist(); + // Use period and position resulting from the reset. + newPeriodUid = playbackInfo.periodId.periodUid; + newContentPositionUs = C.TIME_UNSET; + } else { + // We resolved a subsequent period. Start at the default position in the corresponding + // window. + Pair defaultPosition = + getPeriodPosition( + timeline, + timeline.getPeriodByUid(subsequentPeriodUid, period).windowIndex, + C.TIME_UNSET); + newPeriodUid = defaultPosition.first; + newContentPositionUs = C.TIME_UNSET; } } - if (playbackInfo.periodId.equals(newPeriodId) && oldContentPositionUs == newContentPositionUs) { + // Ensure ad insertion metadata is up to date. + long contentPositionForAdResolution = newContentPositionUs; + if (contentPositionForAdResolution == C.TIME_UNSET) { + contentPositionForAdResolution = + timeline.getWindow(timeline.getPeriodByUid(newPeriodUid, period).windowIndex, window) + .defaultPositionUs; + } + MediaPeriodId periodIdWithAds = + queue.resolveMediaPeriodIdForAds(newPeriodUid, contentPositionForAdResolution); + boolean oldAndNewPeriodIdAreSame = + oldPeriodId.periodUid.equals(newPeriodUid) + && !oldPeriodId.isAd() + && !periodIdWithAds.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. + MediaPeriodId newPeriodId = oldAndNewPeriodIdAreSame ? oldPeriodId : periodIdWithAds; + + if (oldPeriodId.equals(newPeriodId) && oldContentPositionUs == newContentPositionUs) { // We can keep the current playing period. Update the rest of the queued periods. if (!queue.updateQueuedPeriods(rendererPositionUs, getMaxRendererReadPositionUs())) { seekToCurrentPosition(/* sendDiscontinuity= */ false); } } else { // Something changed. Seek to new start position. - MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); + @Nullable MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); if (periodHolder != null) { // Update the new playing media period info if it already exists. while (periodHolder.getNext() != null) { @@ -1444,9 +1614,16 @@ import java.util.concurrent.atomic.AtomicBoolean; } } } - // Actually do the seek. long newPositionUs = newPeriodId.isAd() ? 0 : newContentPositionUs; - long seekedToPositionUs = seekToPeriodPosition(newPeriodId, newPositionUs); + if (!newPeriodId.isAd() && newContentPositionUs == C.TIME_UNSET) { + // Get the default position for the first new period that is not an ad. + int windowIndex = timeline.getPeriodByUid(newPeriodId.periodUid, period).windowIndex; + newContentPositionUs = timeline.getWindow(windowIndex, window).getDefaultPositionUs(); + newPositionUs = newContentPositionUs; + } + // Actually do the seek. + long seekedToPositionUs = + seekToPeriodPosition(newPeriodId, newPositionUs, forceBufferingState); playbackInfo = copyWithNewPosition(newPeriodId, seekedToPositionUs, newContentPositionUs); } handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); @@ -1477,47 +1654,19 @@ import java.util.concurrent.atomic.AtomicBoolean; return maxReadPositionUs; } - private void handleSourceInfoRefreshEndedPlayback() { + private void handleEndOfPlaylist() { if (playbackInfo.playbackState != Player.STATE_IDLE) { setState(Player.STATE_ENDED); } - // Reset, but retain the source so that it can still be used should a seek occur. + // Reset, but retain the playlist so that it can still resume after a seek or be modified. resetInternal( /* resetRenderers= */ false, - /* releaseMediaSource= */ false, /* resetPosition= */ true, - /* resetState= */ false, + /* releasePlaylist= */ false, + /* clearPlaylist= */ false, /* resetError= */ true); } - /** - * Given a period index into an old timeline, finds the first subsequent period that also exists - * in a new timeline. The uid of this period in the new timeline is returned. - * - * @param oldPeriodUid The index of the period in the old timeline. - * @param oldTimeline The old timeline. - * @param newTimeline The new timeline. - * @return The uid in the new timeline of the first subsequent period, or null if no such period - * was found. - */ - private @Nullable Object resolveSubsequentPeriod( - Object oldPeriodUid, Timeline oldTimeline, Timeline newTimeline) { - int oldPeriodIndex = oldTimeline.getIndexOfPeriod(oldPeriodUid); - int newPeriodIndex = C.INDEX_UNSET; - int maxIterations = oldTimeline.getPeriodCount(); - for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) { - oldPeriodIndex = - oldTimeline.getNextPeriodIndex( - oldPeriodIndex, period, window, repeatMode, shuffleModeEnabled); - if (oldPeriodIndex == C.INDEX_UNSET) { - // We've reached the end of the old timeline. - break; - } - newPeriodIndex = newTimeline.getIndexOfPeriod(oldTimeline.getUidOfPeriod(oldPeriodIndex)); - } - return newPeriodIndex == C.INDEX_UNSET ? null : newTimeline.getUidOfPeriod(newPeriodIndex); - } - /** * Converts a {@link SeekPosition} into the corresponding (periodUid, periodPositionUs) for the * internal timeline. @@ -1566,7 +1715,15 @@ import java.util.concurrent.atomic.AtomicBoolean; if (trySubsequentPeriods) { // Try and find a subsequent period from the seek timeline in the internal timeline. @Nullable - Object periodUid = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); + Object periodUid = + resolveSubsequentPeriod( + window, + period, + repeatMode, + shuffleModeEnabled, + periodPosition.first, + seekTimeline, + timeline); if (periodUid != null) { // We found one. Use the default position of the corresponding window. return getPeriodPosition( @@ -1587,13 +1744,9 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void updatePeriods() throws ExoPlaybackException, IOException { - if (mediaSource == null) { - // The player has no media source yet. - return; - } - if (pendingPrepareCount > 0) { + if (playbackInfo.timeline.isEmpty() || !playlist.isPrepared()) { // We're waiting to get information about periods. - mediaSource.maybeThrowSourceInfoRefreshError(); + playlist.maybeThrowSourceInfoRefreshError(); return; } maybeUpdateLoadingPeriod(); @@ -1613,7 +1766,7 @@ import java.util.concurrent.atomic.AtomicBoolean; rendererCapabilities, trackSelector, loadControl.getAllocator(), - mediaSource, + playlist, info, emptyTrackSelectorResult); mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs); @@ -1632,7 +1785,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void maybeUpdateReadingPeriod() throws ExoPlaybackException { - MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + @Nullable MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); if (readingPeriodHolder == null) { return; } @@ -2008,6 +2161,44 @@ import java.util.concurrent.atomic.AtomicBoolean; .sendToTarget(); } + /** + * Given a period index into an old timeline, finds the first subsequent period that also exists + * in a new timeline. The uid of this period in the new timeline is returned. + * + * @param window A {@link Timeline.Window} to be used internally. + * @param period A {@link Timeline.Period} to be used internally. + * @param repeatMode The repeat mode to use. + * @param shuffleModeEnabled Whether the shuffle mode is enabled. + * @param oldPeriodUid The index of the period in the old timeline. + * @param oldTimeline The old timeline. + * @param newTimeline The new timeline. + * @return The uid in the new timeline of the first subsequent period, or null if no such period + * was found. + */ + /* package */ static @Nullable Object resolveSubsequentPeriod( + Timeline.Window window, + Timeline.Period period, + @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled, + Object oldPeriodUid, + Timeline oldTimeline, + Timeline newTimeline) { + int oldPeriodIndex = oldTimeline.getIndexOfPeriod(oldPeriodUid); + int newPeriodIndex = C.INDEX_UNSET; + int maxIterations = oldTimeline.getPeriodCount(); + for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) { + oldPeriodIndex = + oldTimeline.getNextPeriodIndex( + oldPeriodIndex, period, window, repeatMode, shuffleModeEnabled); + if (oldPeriodIndex == C.INDEX_UNSET) { + // We've reached the end of the old timeline. + break; + } + newPeriodIndex = newTimeline.getIndexOfPeriod(oldTimeline.getUidOfPeriod(oldPeriodIndex)); + } + return newPeriodIndex == C.INDEX_UNSET ? null : newTimeline.getUidOfPeriod(newPeriodIndex); + } + private static Format[] getFormats(TrackSelection newSelection) { // Build an array of formats contained by the selection. int length = newSelection != null ? newSelection.length() : 0; @@ -2068,14 +2259,38 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - private static final class MediaSourceRefreshInfo { + private static final class PlaylistUpdateMessage { - public final MediaSource source; - public final Timeline timeline; + private final List mediaSourceHolders; + private final ShuffleOrder shuffleOrder; + private final int windowIndex; + private final long positionUs; - public MediaSourceRefreshInfo(MediaSource source, Timeline timeline) { - this.source = source; - this.timeline = timeline; + private PlaylistUpdateMessage( + List mediaSourceHolders, + ShuffleOrder shuffleOrder, + int windowIndex, + long positionUs) { + this.mediaSourceHolders = mediaSourceHolders; + this.shuffleOrder = shuffleOrder; + this.windowIndex = windowIndex; + this.positionUs = positionUs; + } + } + + private static class MoveMediaItemsMessage { + + public final int fromIndex; + public final int toIndex; + public final int newFromIndex; + public final ShuffleOrder shuffleOrder; + + public MoveMediaItemsMessage( + int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) { + this.fromIndex = fromIndex; + this.toIndex = toIndex; + this.newFromIndex = newFromIndex; + this.shuffleOrder = shuffleOrder; } } @@ -2084,7 +2299,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private PlaybackInfo lastPlaybackInfo; private int operationAcks; private boolean positionDiscontinuity; - private @DiscontinuityReason int discontinuityReason; + @DiscontinuityReason private int discontinuityReason; public boolean hasPendingUpdate(PlaybackInfo playbackInfo) { return playbackInfo != lastPlaybackInfo || operationAcks > 0 || positionDiscontinuity; @@ -2112,5 +2327,4 @@ import java.util.concurrent.atomic.AtomicBoolean; this.discontinuityReason = discontinuityReason; } } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java index 850d2b7d10..5bbbcbea2a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java @@ -19,7 +19,6 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.ClippingMediaPeriod; import com.google.android.exoplayer2.source.EmptySampleStream; 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.SampleStream; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -56,7 +55,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private final boolean[] mayRetainStreamFlags; private final RendererCapabilities[] rendererCapabilities; private final TrackSelector trackSelector; - private final MediaSource mediaSource; + private final Playlist playlist; @Nullable private MediaPeriodHolder next; private TrackGroupArray trackGroups; @@ -70,7 +69,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; * @param rendererPositionOffsetUs The renderer time of the start of the period, in microseconds. * @param trackSelector The track selector. * @param allocator The allocator. - * @param mediaSource The media source that produced the media period. + * @param playlist The playlist. * @param info Information used to identify this media period in its timeline period. * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each * renderer. @@ -80,13 +79,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; long rendererPositionOffsetUs, TrackSelector trackSelector, Allocator allocator, - MediaSource mediaSource, + Playlist playlist, MediaPeriodInfo info, TrackSelectorResult emptyTrackSelectorResult) { this.rendererCapabilities = rendererCapabilities; this.rendererPositionOffsetUs = rendererPositionOffsetUs; this.trackSelector = trackSelector; - this.mediaSource = mediaSource; + this.playlist = playlist; this.uid = info.id.periodUid; this.info = info; this.trackGroups = TrackGroupArray.EMPTY; @@ -94,8 +93,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; sampleStreams = new SampleStream[rendererCapabilities.length]; mayRetainStreamFlags = new boolean[rendererCapabilities.length]; mediaPeriod = - createMediaPeriod( - info.id, mediaSource, allocator, info.startPositionUs, info.endPositionUs); + createMediaPeriod(info.id, playlist, allocator, info.startPositionUs, info.endPositionUs); } /** @@ -305,7 +303,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Releases the media period. No other method should be called after the release. */ public void release() { disableTrackSelectionsInResult(); - releaseMediaPeriod(info.endPositionUs, mediaSource, mediaPeriod); + releaseMediaPeriod(info.endPositionUs, playlist, mediaPeriod); } /** @@ -402,11 +400,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Returns a media period corresponding to the given {@code id}. */ private static MediaPeriod createMediaPeriod( MediaPeriodId id, - MediaSource mediaSource, + Playlist playlist, Allocator allocator, long startPositionUs, long endPositionUs) { - MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator, startPositionUs); + MediaPeriod mediaPeriod = playlist.createPeriod(id, allocator, startPositionUs); if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) { mediaPeriod = new ClippingMediaPeriod( @@ -417,12 +415,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Releases the given {@code mediaPeriod}, logging and suppressing any errors. */ private static void releaseMediaPeriod( - long endPositionUs, MediaSource mediaSource, MediaPeriod mediaPeriod) { + long endPositionUs, Playlist playlist, MediaPeriod mediaPeriod) { try { if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) { - mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); + playlist.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); } else { - mediaSource.releasePeriod(mediaPeriod); + playlist.releasePeriod(mediaPeriod); } } catch (RuntimeException e) { // There's nothing we can do. 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 901b7b4d94..5b39db54aa 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 @@ -19,7 +19,6 @@ import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Player.RepeatMode; 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.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; @@ -134,7 +133,7 @@ import com.google.android.exoplayer2.util.Assertions; * @param rendererCapabilities The renderer capabilities. * @param trackSelector The track selector. * @param allocator The allocator. - * @param mediaSource The media source that produced the media period. + * @param playlist The playlist. * @param info Information used to identify this media period in its timeline period. * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each * renderer. @@ -143,7 +142,7 @@ import com.google.android.exoplayer2.util.Assertions; RendererCapabilities[] rendererCapabilities, TrackSelector trackSelector, Allocator allocator, - MediaSource mediaSource, + Playlist playlist, MediaPeriodInfo info, TrackSelectorResult emptyTrackSelectorResult) { long rendererPositionOffsetUs = @@ -158,7 +157,7 @@ import com.google.android.exoplayer2.util.Assertions; rendererPositionOffsetUs, trackSelector, allocator, - mediaSource, + playlist, info, emptyTrackSelectorResult); if (loading != null) { 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 9d2a3b5459..1f678f9e2a 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 @@ -162,7 +162,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; public MediaPeriodId getDummyFirstMediaPeriodId( boolean shuffleModeEnabled, Timeline.Window window, Timeline.Period period) { if (timeline.isEmpty()) { - return DUMMY_MEDIA_PERIOD_ID; + return getDummyPeriodForEmptyTimeline(); } int firstWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); int firstPeriodIndex = timeline.getWindow(firstWindowIndex, window).firstPeriodIndex; @@ -178,6 +178,11 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; return new MediaPeriodId(timeline.getUidOfPeriod(firstPeriodIndex), windowSequenceNumber); } + /** Returns dummy period id for an empty timeline. */ + public MediaPeriodId getDummyPeriodForEmptyTimeline() { + return DUMMY_MEDIA_PERIOD_ID; + } + /** * Copies playback info with new playing position. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index d89cce6025..1f1c23a980 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -381,7 +381,8 @@ public interface Player { * {@link #onPositionDiscontinuity(int)}. * * @param timeline The latest timeline. Never null, but may be empty. - * @param manifest The latest manifest. May be null. + * @param manifest The latest manifest in case the timeline has a single window only. Always + * null if the timeline has more than a single window. * @param reason The {@link TimelineChangeReason} responsible for this timeline change. * @deprecated Use {@link #onTimelineChanged(Timeline, int)} instead. The manifest can be * accessed by using {@link #getCurrentManifest()} or {@code timeline.getWindow(windowIndex, @@ -619,25 +620,17 @@ public interface Player { int DISCONTINUITY_REASON_INTERNAL = 4; /** - * Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PREPARED}, {@link - * #TIMELINE_CHANGE_REASON_RESET} or {@link #TIMELINE_CHANGE_REASON_DYNAMIC}. + * Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED} or {@link + * #TIMELINE_CHANGE_REASON_SOURCE_UPDATE}. */ @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({ - TIMELINE_CHANGE_REASON_PREPARED, - TIMELINE_CHANGE_REASON_RESET, - TIMELINE_CHANGE_REASON_DYNAMIC - }) + @IntDef({TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, TIMELINE_CHANGE_REASON_SOURCE_UPDATE}) @interface TimelineChangeReason {} - /** Timeline and manifest changed as a result of a player initialization with new media. */ - int TIMELINE_CHANGE_REASON_PREPARED = 0; - /** Timeline and manifest changed as a result of a player reset. */ - int TIMELINE_CHANGE_REASON_RESET = 1; - /** - * Timeline or manifest changed as a result of an dynamic update introduced by the played media. - */ - int TIMELINE_CHANGE_REASON_DYNAMIC = 2; + /** Timeline changed as a result of a change of the playlist items or the order of the items. */ + int TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED = 0; + /** Timeline changed as a result of a dynamic update introduced by the played media. */ + int TIMELINE_CHANGE_REASON_SOURCE_UPDATE = 1; /** Returns the component of this player for audio output, or null if audio is not supported. */ @Nullable 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 3e73a0eb04..a3dd3018ed 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 @@ -42,6 +42,7 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextOutput; @@ -163,7 +164,9 @@ public class SimpleExoPlayer extends BasePlayer * @param bandwidthMeter A {@link BandwidthMeter}. * @param looper A {@link Looper} that must be used for all calls to the player. * @param analyticsCollector An {@link AnalyticsCollector}. - * @param useLazyPreparation Whether media sources should be initialized lazily. + * @param useLazyPreparation Whether playlist items should be prepared lazily. If false, all + * initial preparation steps (e.g., manifest loads) happen immediately. If true, these + * initial preparations are triggered only when the player starts buffering the media. * @param clock A {@link Clock}. Should always be {@link Clock#DEFAULT}. */ public Builder( @@ -300,6 +303,7 @@ public class SimpleExoPlayer extends BasePlayer loadControl, bandwidthMeter, analyticsCollector, + useLazyPreparation, clock, looper); } @@ -342,7 +346,6 @@ public class SimpleExoPlayer extends BasePlayer private int audioSessionId; private AudioAttributes audioAttributes; private float audioVolume; - @Nullable private MediaSource mediaSource; private List currentCues; @Nullable private VideoFrameMetadataListener videoFrameMetadataListener; @Nullable private CameraMotionListener cameraMotionListener; @@ -359,6 +362,9 @@ public class SimpleExoPlayer extends BasePlayer * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. * @param analyticsCollector A factory for creating the {@link AnalyticsCollector} that will * collect and forward all player events. + * @param useLazyPreparation Whether playlist items are prepared lazily. If false, all manifest + * loads and other initial preparation steps happen immediately. If true, these initial + * preparations are triggered only when the player starts buffering the media. * @param clock The {@link Clock} that will be used by the instance. Should always be {@link * Clock#DEFAULT}, unless the player is being used from a test. * @param looper The {@link Looper} which must be used for all calls to the player and which is @@ -372,6 +378,7 @@ public class SimpleExoPlayer extends BasePlayer LoadControl loadControl, BandwidthMeter bandwidthMeter, AnalyticsCollector analyticsCollector, + boolean useLazyPreparation, Clock clock, Looper looper) { this( @@ -382,26 +389,14 @@ public class SimpleExoPlayer extends BasePlayer DrmSessionManager.getDummyDrmSessionManager(), bandwidthMeter, analyticsCollector, + useLazyPreparation, clock, looper); } /** - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance - * will not be used for DRM protected playbacks. - * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. - * @param analyticsCollector The {@link AnalyticsCollector} that will collect and forward all - * player events. - * @param clock The {@link Clock} that will be used by the instance. Should always be {@link - * Clock#DEFAULT}, unless the player is being used from a test. - * @param looper The {@link Looper} which must be used for all calls to the player and which is - * used to call listeners on. * @deprecated Use {@link #SimpleExoPlayer(Context, RenderersFactory, TrackSelector, LoadControl, - * BandwidthMeter, AnalyticsCollector, Clock, Looper)} instead, and pass the {@link + * BandwidthMeter, AnalyticsCollector, boolean, Clock, Looper)} instead, and pass the {@link * DrmSessionManager} to the {@link MediaSource} factories. */ @Deprecated @@ -413,6 +408,7 @@ public class SimpleExoPlayer extends BasePlayer @Nullable DrmSessionManager drmSessionManager, BandwidthMeter bandwidthMeter, AnalyticsCollector analyticsCollector, + boolean useLazyPreparation, Clock clock, Looper looper) { this.bandwidthMeter = bandwidthMeter; @@ -443,7 +439,15 @@ public class SimpleExoPlayer extends BasePlayer // Build the player and associated objects. player = - new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); + new ExoPlayerImpl( + renderers, + trackSelector, + loadControl, + bandwidthMeter, + analyticsCollector, + useLazyPreparation, + clock, + looper); analyticsCollector.setPlayer(player); addListener(analyticsCollector); addListener(componentListener); @@ -1164,51 +1168,151 @@ public class SimpleExoPlayer extends BasePlayer return player.getPlaybackError(); } + /** @deprecated Use {@link #prepare()} instead. */ + @Deprecated @Override - @SuppressWarnings("deprecation") public void retry() { verifyApplicationThread(); - if (mediaSource != null - && (getPlaybackError() != null || getPlaybackState() == Player.STATE_IDLE)) { - prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); - } - } - - @Override - @Deprecated - @SuppressWarnings("deprecation") - public void prepare(MediaSource mediaSource) { - prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true); - } - - @Override - @Deprecated - public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - verifyApplicationThread(); - setMediaSource(mediaSource); - prepareInternal(resetPosition, resetState); + prepare(); } @Override public void prepare() { verifyApplicationThread(); - prepareInternal(/* resetPosition= */ false, /* resetState= */ true); + @AudioFocusManager.PlayerCommand + int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady()); + updatePlayWhenReady(getPlayWhenReady(), playerCommand); + player.prepare(); + } + + /** + * @deprecated Use {@link #setMediaSource(MediaSource)} and {@link ExoPlayer#prepare()} instead. + */ + @Deprecated + @Override + @SuppressWarnings("deprecation") + public void prepare(MediaSource mediaSource) { + prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true); + } + + /** + * @deprecated Use {@link #setMediaSource(MediaSource, boolean)} and {@link ExoPlayer#prepare()} + * instead. + */ + @Deprecated + @Override + public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + verifyApplicationThread(); + setMediaSources( + Collections.singletonList(mediaSource), + /* startWindowIndex= */ resetPosition ? 0 : C.INDEX_UNSET, + /* startPositionMs= */ C.TIME_UNSET); + prepare(); } @Override - public void setMediaSource(MediaSource mediaSource, long startPositionMs) { + public void setMediaSources(List mediaSources) { verifyApplicationThread(); - setMediaSourceInternal(mediaSource); - player.setMediaSource(mediaSource, startPositionMs); + analyticsCollector.resetForNewPlaylist(); + player.setMediaSources(mediaSources); + } + + @Override + public void setMediaSources(List mediaSources, boolean resetPosition) { + verifyApplicationThread(); + analyticsCollector.resetForNewPlaylist(); + player.setMediaSources(mediaSources, resetPosition); + } + + @Override + public void setMediaSources( + List mediaSources, int startWindowIndex, long startPositionMs) { + verifyApplicationThread(); + analyticsCollector.resetForNewPlaylist(); + player.setMediaSources(mediaSources, startWindowIndex, startPositionMs); } @Override public void setMediaSource(MediaSource mediaSource) { verifyApplicationThread(); - setMediaSourceInternal(mediaSource); + analyticsCollector.resetForNewPlaylist(); player.setMediaSource(mediaSource); } + @Override + public void setMediaSource(MediaSource mediaSource, boolean resetPosition) { + verifyApplicationThread(); + analyticsCollector.resetForNewPlaylist(); + player.setMediaSource(mediaSource, resetPosition); + } + + @Override + public void setMediaSource(MediaSource mediaSource, long startPositionMs) { + verifyApplicationThread(); + analyticsCollector.resetForNewPlaylist(); + player.setMediaSource(mediaSource, startPositionMs); + } + + @Override + public void addMediaSource(MediaSource mediaSource) { + verifyApplicationThread(); + player.addMediaSource(mediaSource); + } + + @Override + public void addMediaSource(int index, MediaSource mediaSource) { + verifyApplicationThread(); + player.addMediaSource(index, mediaSource); + } + + @Override + public void addMediaSources(List mediaSources) { + verifyApplicationThread(); + player.addMediaSources(mediaSources); + } + + @Override + public void addMediaSources(int index, List mediaSources) { + verifyApplicationThread(); + player.addMediaSources(index, mediaSources); + } + + @Override + public void moveMediaItem(int currentIndex, int newIndex) { + verifyApplicationThread(); + player.moveMediaItem(currentIndex, newIndex); + } + + @Override + public void moveMediaItems(int fromIndex, int toIndex, int newIndex) { + verifyApplicationThread(); + player.moveMediaItems(fromIndex, toIndex, newIndex); + } + + @Override + public MediaSource removeMediaItem(int index) { + verifyApplicationThread(); + return player.removeMediaItem(index); + } + + @Override + public void removeMediaItems(int fromIndex, int toIndex) { + verifyApplicationThread(); + player.removeMediaItems(fromIndex, toIndex); + } + + @Override + public void clearMediaItems() { + verifyApplicationThread(); + player.clearMediaItems(); + } + + @Override + public void setShuffleOrder(ShuffleOrder shuffleOrder) { + verifyApplicationThread(); + player.setShuffleOrder(shuffleOrder); + } + @Override public void setPlayWhenReady(boolean playWhenReady) { verifyApplicationThread(); @@ -1286,6 +1390,7 @@ public class SimpleExoPlayer extends BasePlayer @Override public void setForegroundMode(boolean foregroundMode) { + verifyApplicationThread(); player.setForegroundMode(foregroundMode); } @@ -1293,13 +1398,6 @@ public class SimpleExoPlayer extends BasePlayer public void stop(boolean reset) { verifyApplicationThread(); player.stop(reset); - if (mediaSource != null) { - mediaSource.removeEventListener(analyticsCollector); - analyticsCollector.resetForNewMediaSource(); - if (reset) { - mediaSource = null; - } - } audioFocusManager.handleStop(); currentCues = Collections.emptyList(); } @@ -1318,10 +1416,6 @@ public class SimpleExoPlayer extends BasePlayer } surface = null; } - if (mediaSource != null) { - mediaSource.removeEventListener(analyticsCollector); - mediaSource = null; - } if (isPriorityTaskManagerRegistered) { Assertions.checkNotNull(priorityTaskManager).remove(C.PRIORITY_PLAYBACK); isPriorityTaskManagerRegistered = false; @@ -1455,23 +1549,6 @@ public class SimpleExoPlayer extends BasePlayer // Internal methods. - private void prepareInternal(boolean resetPosition, boolean resetState) { - Assertions.checkNotNull(mediaSource); - @AudioFocusManager.PlayerCommand - int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady()); - updatePlayWhenReady(getPlayWhenReady(), playerCommand); - player.prepareInternal(resetPosition, resetState); - } - - private void setMediaSourceInternal(MediaSource mediaSource) { - if (this.mediaSource != null) { - this.mediaSource.removeEventListener(analyticsCollector); - analyticsCollector.resetForNewMediaSource(); - } - this.mediaSource = mediaSource; - this.mediaSource.addEventListener(eventHandler, analyticsCollector); - } - private void removeSurfaceCallbacks() { if (textureView != null) { if (textureView.getSurfaceTextureListener() != componentListener) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java index efc1650192..2cb46e099b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -135,11 +135,8 @@ public class AnalyticsCollector } } - /** - * Resets the analytics collector for a new media source. Should be called before the player is - * prepared with a new media source. - */ - public final void resetForNewMediaSource() { + /** Resets the analytics collector for a new playlist. */ + public final void resetForNewPlaylist() { // Copying the list is needed because onMediaPeriodReleased will modify the list. List mediaPeriodInfos = new ArrayList<>(mediaPeriodQueueTracker.mediaPeriodInfoQueue); @@ -806,9 +803,13 @@ public class AnalyticsCollector /** Updates the queue with a newly created media period. */ public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { - boolean isInTimeline = timeline.getIndexOfPeriod(mediaPeriodId.periodUid) != C.INDEX_UNSET; + int periodIndex = timeline.getIndexOfPeriod(mediaPeriodId.periodUid); + boolean isInTimeline = periodIndex != C.INDEX_UNSET; MediaPeriodInfo mediaPeriodInfo = - new MediaPeriodInfo(mediaPeriodId, isInTimeline ? timeline : Timeline.EMPTY, windowIndex); + new MediaPeriodInfo( + mediaPeriodId, + isInTimeline ? timeline : Timeline.EMPTY, + isInTimeline ? timeline.getPeriod(periodIndex, period).windowIndex : windowIndex); mediaPeriodInfoQueue.add(mediaPeriodInfo); mediaPeriodIdToInfo.put(mediaPeriodId, mediaPeriodInfo); lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0); @@ -824,7 +825,7 @@ public class AnalyticsCollector public boolean onMediaPeriodReleased(MediaPeriodId mediaPeriodId) { MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.remove(mediaPeriodId); if (mediaPeriodInfo == null) { - // The media period has already been removed from the queue in resetForNewMediaSource(). + // The media period has already been removed from the queue in resetForNewPlaylist(). return false; } mediaPeriodInfoQueue.remove(mediaPeriodInfo); 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 3fa9e8804e..6d5820d1f3 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 @@ -617,12 +617,10 @@ public class EventLogger implements AnalyticsListener { private static String getTimelineChangeReasonString(@Player.TimelineChangeReason int reason) { switch (reason) { - case Player.TIMELINE_CHANGE_REASON_PREPARED: - return "PREPARED"; - case Player.TIMELINE_CHANGE_REASON_RESET: - return "RESET"; - case Player.TIMELINE_CHANGE_REASON_DYNAMIC: - return "DYNAMIC"; + case Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE: + return "SOURCE_UPDATE"; + case Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED: + return "PLAYLIST_CHANGED"; default: return "?"; } 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 320854565d..0893e01ec0 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 @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.source.ClippingMediaSource; import com.google.android.exoplayer2.source.CompositeMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.LoopingMediaSource; +import com.google.android.exoplayer2.source.MaskingMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; @@ -68,6 +69,7 @@ import com.google.android.exoplayer2.testutil.FakeTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; 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.Clock; import java.io.IOException; import java.util.ArrayList; @@ -99,10 +101,12 @@ public final class ExoPlayerTest { private static final int TIMEOUT_MS = 10000; private Context context; + private Timeline dummyTimeline; @Before public void setUp() { context = ApplicationProvider.getApplicationContext(); + dummyTimeline = new MaskingMediaSource.DummyTimeline(/* tag= */ 0); } /** @@ -112,6 +116,7 @@ public final class ExoPlayerTest { @Test public void testPlayEmptyTimeline() throws Exception { Timeline timeline = Timeline.EMPTY; + Timeline expectedMaskingTimeline = new MaskingMediaSource.DummyTimeline(/* tag= */ null); FakeRenderer renderer = new FakeRenderer(); ExoPlayerTestRunner testRunner = new Builder() @@ -121,7 +126,10 @@ public final class ExoPlayerTest { .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); - testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelinesSame(expectedMaskingTimeline, Timeline.EMPTY); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); assertThat(renderer.formatReadCount).isEqualTo(0); assertThat(renderer.sampleBufferReadCount).isEqualTo(0); assertThat(renderer.isEnded).isFalse(); @@ -142,8 +150,10 @@ public final class ExoPlayerTest { .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); - testRunner.assertTimelinesEqual(timeline); - testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertTimelinesSame(dummyTimeline, timeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); assertThat(renderer.formatReadCount).isEqualTo(1); assertThat(renderer.sampleBufferReadCount).isEqualTo(1); @@ -165,8 +175,10 @@ public final class ExoPlayerTest { testRunner.assertPositionDiscontinuityReasonsEqual( Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); - testRunner.assertTimelinesEqual(timeline); - testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertTimelinesSame(new FakeMediaSource.InitialTimeline(timeline), timeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); assertThat(renderer.formatReadCount).isEqualTo(3); assertThat(renderer.sampleBufferReadCount).isEqualTo(3); assertThat(renderer.isEnded).isTrue(); @@ -189,8 +201,10 @@ public final class ExoPlayerTest { Integer[] expectedReasons = new Integer[99]; Arrays.fill(expectedReasons, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); testRunner.assertPositionDiscontinuityReasonsEqual(expectedReasons); - testRunner.assertTimelinesEqual(timeline); - testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertTimelinesSame(dummyTimeline, timeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); assertThat(renderer.formatReadCount).isEqualTo(100); assertThat(renderer.sampleBufferReadCount).isEqualTo(100); assertThat(renderer.isEnded).isTrue(); @@ -262,17 +276,22 @@ public final class ExoPlayerTest { testRunner.assertPositionDiscontinuityReasonsEqual( Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); - testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelinesSame(new FakeMediaSource.InitialTimeline(timeline), timeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); assertThat(audioRenderer.positionResetCount).isEqualTo(1); assertThat(videoRenderer.isEnded).isTrue(); assertThat(audioRenderer.isEnded).isTrue(); } @Test - public void testRepreparationGivesFreshSourceInfo() throws Exception { + public void testResettingMediaSourcesGivesFreshSourceInfo() throws Exception { FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); - Object firstSourceManifest = new Object(); - Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1, firstSourceManifest); + Timeline firstTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* isSeekable= */ true, /* isDynamic= */ false, 1000_000_000)); MediaSource firstSource = new FakeMediaSource(firstTimeline, Builder.VIDEO_FORMAT); final CountDownLatch queuedSourceInfoCountDownLatch = new CountDownLatch(1); final CountDownLatch completePreparationCountDownLatch = new CountDownLatch(1); @@ -285,8 +304,8 @@ public final class ExoPlayerTest { @Nullable TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener); // We've queued a source info refresh on the playback thread's event queue. Allow the - // test thread to prepare the player with the third source, and block this thread (the - // playback thread) until the test thread's call to prepare() has returned. + // test thread to set the third source to the playlist, and block this thread (the + // playback thread) until the test thread's call to setMediaSources() has returned. queuedSourceInfoCountDownLatch.countDown(); try { completePreparationCountDownLatch.await(); @@ -301,12 +320,13 @@ public final class ExoPlayerTest { // Prepare the player with a source with the first manifest and a non-empty timeline. Prepare // the player again with a source and a new manifest, which will never be exposed. Allow the - // test thread to prepare the player with a third source, and block the playback thread until - // the test thread's call to prepare() has returned. + // test thread to set a third source, and block the playback thread until the test thread's call + // to setMediaSources() has returned. ActionSchedule actionSchedule = - new ActionSchedule.Builder("testRepreparation") - .waitForTimelineChanged(firstTimeline) - .prepareSource(secondSource) + new ActionSchedule.Builder("testResettingMediaSourcesGivesFreshSourceInfo") + .waitForTimelineChanged( + firstTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .setMediaSources(secondSource) .executeRunnable( () -> { try { @@ -315,26 +335,31 @@ public final class ExoPlayerTest { // Ignore. } }) - .prepareSource(thirdSource) + .setMediaSources(thirdSource) .executeRunnable(completePreparationCountDownLatch::countDown) .build(); ExoPlayerTestRunner testRunner = new Builder() - .setMediaSource(firstSource) + .setMediaSources(firstSource) .setRenderers(renderer) .setActionSchedule(actionSchedule) .build(context) .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); - // The first source's preparation completed with a non-empty timeline. When the player was - // re-prepared with the second source, it immediately exposed an empty timeline, but the source - // info refresh from the second source was suppressed as we re-prepared with the third source. - testRunner.assertTimelinesEqual(firstTimeline, Timeline.EMPTY, thirdTimeline); + // The first source's preparation completed with a real timeline. When the second source was + // prepared, it immediately exposed a dummy timeline, but the source info refresh from the + // second source was suppressed as we replace it with the third source before the update + // arrives. + testRunner.assertTimelinesSame( + dummyTimeline, firstTimeline, dummyTimeline, dummyTimeline, thirdTimeline); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PREPARED, - Player.TIMELINE_CHANGE_REASON_RESET, - Player.TIMELINE_CHANGE_REASON_PREPARED); + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); assertThat(renderer.isEnded).isTrue(); } @@ -346,7 +371,8 @@ public final class ExoPlayerTest { ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepeatMode") .pause() - .waitForTimelineChanged(timeline) + .waitForTimelineChanged( + timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .playUntilStartOfWindow(/* windowIndex= */ 1) .setRepeatMode(Player.REPEAT_MODE_ONE) .playUntilStartOfWindow(/* windowIndex= */ 1) @@ -381,8 +407,10 @@ public final class ExoPlayerTest { Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); - testRunner.assertTimelinesEqual(timeline); - testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertTimelinesSame(new FakeMediaSource.InitialTimeline(timeline), timeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); assertThat(renderer.isEnded).isTrue(); } @@ -411,7 +439,7 @@ public final class ExoPlayerTest { .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) + .setMediaSources(mediaSource) .setRenderers(renderer) .setActionSchedule(actionSchedule) .build(context) @@ -457,12 +485,13 @@ public final class ExoPlayerTest { .pause() .waitForPlaybackState(Player.STATE_READY) .executeRunnable(() -> fakeMediaSource.setNewSourceInfo(adErrorTimeline, null)) - .waitForTimelineChanged(adErrorTimeline) + .waitForTimelineChanged( + adErrorTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .play() .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSource(fakeMediaSource) + .setMediaSources(fakeMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -557,26 +586,31 @@ public final class ExoPlayerTest { } @Test - public void testSeekProcessedCalledWithIllegalSeekPosition() throws Exception { + public void testIllegalSeekPositionDoesThrow() throws Exception { + final IllegalSeekPositionException[] exception = new IllegalSeekPositionException[1]; ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSeekProcessedCalledWithIllegalSeekPosition") + new ActionSchedule.Builder("testIllegalSeekPositionDoesThrow") .waitForPlaybackState(Player.STATE_BUFFERING) - // The illegal seek position will end playback. - .seek(/* windowIndex= */ 100, /* positionMs= */ 0) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + try { + player.seekTo(/* windowIndex= */ 100, /* positionMs= */ 0); + } catch (IllegalSeekPositionException e) { + exception[0] = e; + } + } + }) .waitForPlaybackState(Player.STATE_ENDED) .build(); - final boolean[] onSeekProcessedCalled = new boolean[1]; - EventListener listener = - new EventListener() { - @Override - public void onSeekProcessed() { - onSeekProcessedCalled[0] = true; - } - }; - ExoPlayerTestRunner testRunner = - new Builder().setActionSchedule(actionSchedule).setEventListener(listener).build(context); - testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); - assertThat(onSeekProcessedCalled[0]).isTrue(); + new Builder() + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertThat(exception[0]).isNotNull(); } @Test @@ -620,7 +654,7 @@ public final class ExoPlayerTest { .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) + .setMediaSources(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -648,7 +682,7 @@ public final class ExoPlayerTest { }; ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) + .setMediaSources(mediaSource) .build(context) .start() .blockUntilEnded(TIMEOUT_MS); @@ -674,7 +708,7 @@ public final class ExoPlayerTest { }; ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) + .setMediaSources(mediaSource) .build(context) .start() .blockUntilEnded(TIMEOUT_MS); @@ -692,7 +726,7 @@ public final class ExoPlayerTest { FakeTrackSelector trackSelector = new FakeTrackSelector(); new Builder() - .setMediaSource(mediaSource) + .setMediaSources(mediaSource) .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) .build(context) @@ -721,7 +755,7 @@ public final class ExoPlayerTest { FakeTrackSelector trackSelector = new FakeTrackSelector(); new Builder() - .setMediaSource(mediaSource) + .setMediaSources(mediaSource) .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) .build(context) @@ -758,7 +792,7 @@ public final class ExoPlayerTest { .build(); new Builder() - .setMediaSource(mediaSource) + .setMediaSources(mediaSource) .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) .setActionSchedule(disableTrackAction) @@ -797,7 +831,7 @@ public final class ExoPlayerTest { .build(); new Builder() - .setMediaSource(mediaSource) + .setMediaSources(mediaSource) .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) .setActionSchedule(disableTrackAction) @@ -821,31 +855,35 @@ public final class ExoPlayerTest { @Test public void testDynamicTimelineChangeReason() throws Exception { - Timeline timeline1 = new FakeTimeline(new TimelineWindowDefinition(false, false, 100000)); + Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 100000)); final Timeline timeline2 = new FakeTimeline(new TimelineWindowDefinition(false, false, 20000)); - final FakeMediaSource mediaSource = new FakeMediaSource(timeline1, Builder.VIDEO_FORMAT); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testDynamicTimelineChangeReason") .pause() - .waitForTimelineChanged(timeline1) + .waitForTimelineChanged( + timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline2, null)) - .waitForTimelineChanged(timeline2) + .waitForTimelineChanged( + timeline2, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .play() .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) + .setMediaSources(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesEqual(timeline1, timeline2); + testRunner.assertTimelinesSame(dummyTimeline, timeline, timeline2); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_DYNAMIC); + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); } @Test - public void testRepreparationWithPositionResetAndShufflingUsesFirstPeriod() throws Exception { + public void testResetMediaSourcesWithPositionResetAndShufflingUsesFirstPeriod() throws Exception { Timeline fakeTimeline = new FakeTimeline( new TimelineWindowDefinition( @@ -863,22 +901,25 @@ public final class ExoPlayerTest { new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testRepreparationWithShuffle") + new ActionSchedule.Builder( + "testResetMediaSourcesWithPositionResetAndShufflingUsesFirstPeriod") // Wait for first preparation and enable shuffling. Plays period 0. .pause() .waitForPlaybackState(Player.STATE_READY) .setShuffleModeEnabled(true) - // Reprepare with second media source (keeping state, but with position reset). + // Set the second media source (with position reset). // Plays period 1 and 0 because of the reversed fake shuffle order. - .prepareSource(secondMediaSource, /* resetPosition= */ true, /* resetState= */ false) + .setMediaSources(/* resetPosition= */ true, secondMediaSource) .play() + .waitForPositionDiscontinuity() .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSource(firstMediaSource) + .setMediaSources(firstMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); testRunner.assertPlayedPeriodIndices(0, 1, 0); } @@ -923,7 +964,7 @@ public final class ExoPlayerTest { .executeRunnable(() -> fakeMediaPeriodHolder[0].setPreparationComplete()) .build(); new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) + .setMediaSources(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -956,8 +997,10 @@ public final class ExoPlayerTest { .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesEqual(timeline); - testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertTimelinesSame(dummyTimeline, timeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); testRunner.assertNoPositionDiscontinuities(); assertThat(positionHolder[0]).isAtLeast(50L); } @@ -988,8 +1031,10 @@ public final class ExoPlayerTest { .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesEqual(timeline); - testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertTimelinesSame(dummyTimeline, timeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); testRunner.assertNoPositionDiscontinuities(); assertThat(positionHolder[0]).isAtLeast(50L); } @@ -1020,9 +1065,11 @@ public final class ExoPlayerTest { .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY); + testRunner.assertTimelinesSame(dummyTimeline, timeline, Timeline.EMPTY); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_RESET); + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); testRunner.assertNoPositionDiscontinuities(); assertThat(positionHolder[0]).isEqualTo(0); } @@ -1068,45 +1115,29 @@ public final class ExoPlayerTest { } @Test - public void testRepreparationDoesNotResetAfterStopWithReset() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - MediaSource secondSource = new FakeMediaSource(timeline, Builder.VIDEO_FORMAT); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testRepreparationAfterStop") - .waitForPlaybackState(Player.STATE_READY) - .stop(/* reset= */ true) - .waitForPlaybackState(Player.STATE_IDLE) - .prepareSource(secondSource) - .build(); - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .setExpectedPlayerEndedCount(2) - .build(context) - .start() - .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PREPARED, - Player.TIMELINE_CHANGE_REASON_RESET, - Player.TIMELINE_CHANGE_REASON_PREPARED); - testRunner.assertNoPositionDiscontinuities(); - } - - @Test - public void testSeekBeforeRepreparationPossibleAfterStopWithReset() throws Exception { + public void testSettingNewStartPositionPossibleAfterStopWithReset() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 2); MediaSource secondSource = new FakeMediaSource(secondTimeline, Builder.VIDEO_FORMAT); + AtomicInteger windowIndexAfterStop = new AtomicInteger(); + AtomicLong positionAfterStop = new AtomicLong(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSeekAfterStopWithReset") + new ActionSchedule.Builder("testSettingNewStartPositionPossibleAfterStopWithReset") .waitForPlaybackState(Player.STATE_READY) .stop(/* reset= */ true) .waitForPlaybackState(Player.STATE_IDLE) - // If we were still using the first timeline, this would throw. - .seek(/* windowIndex= */ 1, /* positionMs= */ 0) - .prepareSource(secondSource, /* resetPosition= */ false, /* resetState= */ true) + .seek(/* windowIndex= */ 1, /* positionMs= */ 1000) + .setMediaSources(secondSource) + .prepare() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + windowIndexAfterStop.set(player.getCurrentWindowIndex()); + positionAfterStop.set(player.getCurrentPosition()); + } + }) + .waitForPlaybackState(Player.STATE_READY) .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() @@ -1116,32 +1147,55 @@ public final class ExoPlayerTest { .build(context) .start() .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, secondTimeline); + testRunner.assertPlaybackStatesEqual( + Player.STATE_IDLE, + Player.STATE_BUFFERING, + Player.STATE_READY, + Player.STATE_IDLE, + Player.STATE_BUFFERING, + Player.STATE_READY, + Player.STATE_ENDED); + testRunner.assertTimelinesSame( + dummyTimeline, + timeline, + Timeline.EMPTY, + new FakeMediaSource.InitialTimeline(secondTimeline), + secondTimeline); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PREPARED, - Player.TIMELINE_CHANGE_REASON_RESET, - Player.TIMELINE_CHANGE_REASON_PREPARED); - testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, // stop(true) + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + assertThat(windowIndexAfterStop.get()).isEqualTo(1); + assertThat(positionAfterStop.get()).isAtLeast(1000L); testRunner.assertPlayedPeriodIndices(0, 1); } @Test - public void testReprepareAndKeepPositionWithNewMediaSource() throws Exception { + public void testResetPlaylistWithPreviousPosition() throws Exception { + Object firstWindowId = new Object(); Timeline timeline = new FakeTimeline( - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ new Object())); + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ firstWindowId)); + Timeline firstExpectedMaskingTimeline = + new MaskingMediaSource.DummyTimeline(/* tag= */ firstWindowId); + Object secondWindowId = new Object(); Timeline secondTimeline = new FakeTimeline( - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ new Object())); + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ secondWindowId)); + Timeline secondExpectedMaskingTimeline = + new MaskingMediaSource.DummyTimeline(/* tag= */ secondWindowId); MediaSource secondSource = new FakeMediaSource(secondTimeline); AtomicLong positionAfterReprepare = new AtomicLong(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testReprepareAndKeepPositionWithNewMediaSource") + new ActionSchedule.Builder("testResetPlaylistWithPreviousPosition") .pause() .waitForPlaybackState(Player.STATE_READY) .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 2000) - .prepareSource(secondSource, /* resetPosition= */ false, /* resetState= */ true) - .waitForTimelineChanged(secondTimeline) + .setMediaSources(/* windowIndex= */ 0, /* positionMs= */ 2000, secondSource) + .waitForTimelineChanged( + secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .executeRunnable( new PlayerRunnable() { @Override @@ -1160,7 +1214,117 @@ public final class ExoPlayerTest { .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, secondTimeline); + testRunner.assertTimelinesSame( + firstExpectedMaskingTimeline, timeline, secondExpectedMaskingTimeline, secondTimeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + assertThat(positionAfterReprepare.get()).isAtLeast(2000L); + } + + @Test + public void testResetPlaylistStartsFromDefaultPosition() throws Exception { + Object firstWindowId = new Object(); + Timeline timeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ firstWindowId)); + Timeline firstExpectedDummyTimeline = + new MaskingMediaSource.DummyTimeline(/* tag= */ firstWindowId); + Object secondWindowId = new Object(); + Timeline secondTimeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ secondWindowId)); + Timeline secondExpectedDummyTimeline = + new MaskingMediaSource.DummyTimeline(/* tag= */ secondWindowId); + MediaSource secondSource = new FakeMediaSource(secondTimeline); + AtomicLong positionAfterReprepare = new AtomicLong(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testResetPlaylistStartsFromDefaultPosition") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 2000) + .setMediaSources(/* resetPosition= */ true, secondSource) + .waitForTimelineChanged( + secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + positionAfterReprepare.set(player.getCurrentPosition()); + } + }) + .play() + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + testRunner.assertTimelinesSame( + firstExpectedDummyTimeline, timeline, secondExpectedDummyTimeline, secondTimeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + assertThat(positionAfterReprepare.get()).isEqualTo(0L); + } + + @Test + public void testResetPlaylistWithoutResettingPositionStartsFromOldPosition() throws Exception { + Object firstWindowId = new Object(); + Timeline timeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ firstWindowId)); + Timeline firstExpectedDummyTimeline = + new MaskingMediaSource.DummyTimeline(/* tag= */ firstWindowId); + Object secondWindowId = new Object(); + Timeline secondTimeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ secondWindowId)); + Timeline secondExpectedDummyTimeline = + new MaskingMediaSource.DummyTimeline(/* tag= */ secondWindowId); + MediaSource secondSource = new FakeMediaSource(secondTimeline); + AtomicLong positionAfterReprepare = new AtomicLong(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testResetPlaylistWithoutResettingPositionStartsFromOldPosition") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 2000) + .setMediaSources(secondSource) + .waitForTimelineChanged( + secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + positionAfterReprepare.set(player.getCurrentPosition()); + } + }) + .play() + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + testRunner.assertTimelinesSame( + firstExpectedDummyTimeline, timeline, secondExpectedDummyTimeline, secondTimeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); assertThat(positionAfterReprepare.get()).isAtLeast(2000L); } @@ -1182,8 +1346,10 @@ public final class ExoPlayerTest { .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesEqual(Timeline.EMPTY); - testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertTimelinesSame(dummyTimeline, Timeline.EMPTY); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); } @@ -1208,8 +1374,10 @@ public final class ExoPlayerTest { .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesEqual(timeline); - testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertTimelinesSame(dummyTimeline, timeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); } @@ -1221,9 +1389,8 @@ public final class ExoPlayerTest { .waitForPlaybackState(Player.STATE_READY) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) - .prepareSource( - new FakeMediaSource(timeline), /* resetPosition= */ true, /* resetState= */ false) - .waitForPlaybackState(Player.STATE_READY) + .prepare() + .waitForPlaybackState(Player.STATE_BUFFERING) .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() @@ -1236,9 +1403,10 @@ public final class ExoPlayerTest { } catch (ExoPlaybackException e) { // Expected exception. } - testRunner.assertTimelinesEqual(timeline, timeline); + testRunner.assertTimelinesSame(dummyTimeline, timeline); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED); + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); } @Test @@ -1260,8 +1428,7 @@ public final class ExoPlayerTest { positionHolder[0] = player.getCurrentPosition(); } }) - .prepareSource( - new FakeMediaSource(timeline), /* resetPosition= */ false, /* resetState= */ false) + .prepare() .waitForPlaybackState(Player.STATE_READY) .executeRunnable( new PlayerRunnable() { @@ -1283,52 +1450,29 @@ public final class ExoPlayerTest { } catch (ExoPlaybackException e) { // Expected exception. } - testRunner.assertTimelinesEqual(timeline, timeline); + testRunner.assertTimelinesSame(dummyTimeline, timeline); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED); + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); assertThat(positionHolder[0]).isEqualTo(50); assertThat(positionHolder[1]).isEqualTo(50); } - @Test - public void testInvalidSeekPositionAfterSourceInfoRefreshStillUpdatesTimeline() throws Exception { - final Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - final FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testInvalidSeekPositionSourceInfoRefreshStillUpdatesTimeline") - .waitForPlaybackState(Player.STATE_BUFFERING) - // Seeking to an invalid position will end playback. - .seek(/* windowIndex= */ 100, /* positionMs= */ 0) - .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null)) - .waitForPlaybackState(Player.STATE_ENDED) - .build(); - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) - .setActionSchedule(actionSchedule) - .build(context); - testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); - - testRunner.assertTimelinesEqual(timeline); - testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); - } - @Test public void testInvalidSeekPositionAfterSourceInfoRefreshWithShuffleModeEnabledUsesCorrectFirstPeriod() throws Exception { - FakeMediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)); - ConcatenatingMediaSource concatenatingMediaSource = - new ConcatenatingMediaSource( - /* isAtomic= */ false, new FakeShuffleOrder(0), mediaSource, mediaSource); + FakeMediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2)); AtomicInteger windowIndexAfterUpdate = new AtomicInteger(); ActionSchedule actionSchedule = new ActionSchedule.Builder("testInvalidSeekPositionSourceInfoRefreshUsesCorrectFirstPeriod") + .setShuffleOrder(new FakeShuffleOrder(/* length= */ 0)) .setShuffleModeEnabled(true) .waitForPlaybackState(Player.STATE_BUFFERING) // Seeking to an invalid position will end playback. - .seek(/* windowIndex= */ 100, /* positionMs= */ 0) + .seek( + /* windowIndex= */ 100, /* positionMs= */ 0, /* catchIllegalSeekException= */ true) .waitForPlaybackState(Player.STATE_ENDED) .executeRunnable( new PlayerRunnable() { @@ -1338,12 +1482,13 @@ public final class ExoPlayerTest { } }) .build(); - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() - .setMediaSource(concatenatingMediaSource) - .setActionSchedule(actionSchedule) - .build(context); - testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); + new ExoPlayerTestRunner.Builder() + .setMediaSources(mediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); assertThat(windowIndexAfterUpdate.get()).isEqualTo(1); } @@ -1357,7 +1502,8 @@ public final class ExoPlayerTest { new ConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0)); AtomicInteger windowIndexAfterAddingSources = new AtomicInteger(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testRestartAfterEmptyTimelineUsesCorrectFirstPeriod") + new ActionSchedule.Builder( + "testRestartAfterEmptyTimelineWithShuffleModeEnabledUsesCorrectFirstPeriod") .setShuffleModeEnabled(true) // Preparing with an empty media source will transition to ended state. .waitForPlaybackState(Player.STATE_ENDED) @@ -1377,7 +1523,7 @@ public final class ExoPlayerTest { }) .build(); new ExoPlayerTestRunner.Builder() - .setMediaSource(concatenatingMediaSource) + .setMediaSources(concatenatingMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -1391,7 +1537,7 @@ public final class ExoPlayerTest { final Timeline timeline = new FakeTimeline(/* windowCount= */ 2); final long[] positionHolder = new long[3]; final int[] windowIndexHolder = new int[3]; - final FakeMediaSource secondMediaSource = new FakeMediaSource(/* timeline= */ null); + final FakeMediaSource firstMediaSource = new FakeMediaSource(timeline); ActionSchedule actionSchedule = new ActionSchedule.Builder("testPlaybackErrorDoesNotResetPosition") .pause() @@ -1408,8 +1554,7 @@ public final class ExoPlayerTest { windowIndexHolder[0] = player.getCurrentWindowIndex(); } }) - .prepareSource(secondMediaSource, /* resetPosition= */ false, /* resetState= */ false) - .waitForPlaybackState(Player.STATE_BUFFERING) + .prepare() .executeRunnable( new PlayerRunnable() { @Override @@ -1417,7 +1562,6 @@ public final class ExoPlayerTest { // Position while repreparing. positionHolder[1] = player.getCurrentPosition(); windowIndexHolder[1] = player.getCurrentWindowIndex(); - secondMediaSource.setNewSourceInfo(timeline, /* newManifest= */ null); } }) .waitForPlaybackState(Player.STATE_READY) @@ -1434,7 +1578,7 @@ public final class ExoPlayerTest { .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) + .setMediaSources(firstMediaSource) .setActionSchedule(actionSchedule) .build(context); try { @@ -1451,6 +1595,70 @@ public final class ExoPlayerTest { assertThat(windowIndexHolder[2]).isEqualTo(1); } + @Test + public void testSeekAfterPlaybackError() throws Exception { + final Timeline timeline = new FakeTimeline(/* windowCount= */ 2); + final long[] positionHolder = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; + final int[] windowIndexHolder = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + final FakeMediaSource firstMediaSource = new FakeMediaSource(timeline); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSeekAfterPlaybackError") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .playUntilPosition(/* windowIndex= */ 1, /* positionMs= */ 500) + .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) + .waitForPlaybackState(Player.STATE_IDLE) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Position while in error state + positionHolder[0] = player.getCurrentPosition(); + windowIndexHolder[0] = player.getCurrentWindowIndex(); + } + }) + .seek(/* windowIndex= */ 0, /* positionMs= */ C.TIME_UNSET) + .waitForSeekProcessed() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Position while in error state + positionHolder[1] = player.getCurrentPosition(); + windowIndexHolder[1] = player.getCurrentWindowIndex(); + } + }) + .prepare() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Position after prepare. + positionHolder[2] = player.getCurrentPosition(); + windowIndexHolder[2] = player.getCurrentWindowIndex(); + } + }) + .play() + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setMediaSources(firstMediaSource) + .setActionSchedule(actionSchedule) + .build(context); + try { + testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); + fail(); + } catch (ExoPlaybackException e) { + // Expected exception. + } + assertThat(positionHolder[0]).isAtLeast(500L); + assertThat(positionHolder[1]).isEqualTo(0L); + assertThat(positionHolder[2]).isEqualTo(0L); + assertThat(windowIndexHolder[0]).isEqualTo(1); + assertThat(windowIndexHolder[1]).isEqualTo(0); + assertThat(windowIndexHolder[2]).isEqualTo(0); + } + @Test public void playbackErrorAndReprepareWithPositionResetKeepsWindowSequenceNumber() throws Exception { @@ -1461,7 +1669,8 @@ public final class ExoPlayerTest { .waitForPlaybackState(Player.STATE_READY) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) - .prepareSource(mediaSource, /* resetPosition= */ true, /* resetState= */ false) + .seek(0, C.TIME_UNSET) + .prepare() .waitForPlaybackState(Player.STATE_READY) .play() .build(); @@ -1478,7 +1687,7 @@ public final class ExoPlayerTest { }; ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) + .setMediaSources(mediaSource) .setActionSchedule(actionSchedule) .setAnalyticsListener(listener) .build(context); @@ -1494,16 +1703,18 @@ public final class ExoPlayerTest { @Test public void testPlaybackErrorTwiceStillKeepsTimeline() throws Exception { final Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - final FakeMediaSource mediaSource2 = new FakeMediaSource(/* timeline= */ null); + final FakeMediaSource mediaSource2 = new FakeMediaSource(timeline); ActionSchedule actionSchedule = new ActionSchedule.Builder("testPlaybackErrorDoesNotResetPosition") .pause() .waitForPlaybackState(Player.STATE_READY) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) - .prepareSource(mediaSource2, /* resetPosition= */ false, /* resetState= */ false) + .setMediaSources(/* resetPosition= */ false, mediaSource2) + .prepare() .waitForPlaybackState(Player.STATE_BUFFERING) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) + .waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .waitForPlaybackState(Player.STATE_IDLE) .build(); ExoPlayerTestRunner testRunner = @@ -1517,9 +1728,12 @@ public final class ExoPlayerTest { } catch (ExoPlaybackException e) { // Expected exception. } - testRunner.assertTimelinesEqual(timeline, timeline); + testRunner.assertTimelinesSame(dummyTimeline, timeline, dummyTimeline, timeline); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED); + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); } @Test @@ -1549,7 +1763,8 @@ public final class ExoPlayerTest { ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessages") .pause() - .waitForTimelineChanged(timeline) + .waitForTimelineChanged( + timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .sendMessage(target, /* positionMs= */ 50) .play() .build(); @@ -1669,21 +1884,12 @@ public final class ExoPlayerTest { long duration1Ms = timeline.getWindow(0, new Window()).getDurationMs(); long duration2Ms = timeline.getWindow(1, new Window()).getDurationMs(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .pause() - .waitForPlaybackState(Player.STATE_BUFFERING) + new ActionSchedule.Builder("testSendMessagesAtStartAndEndOfPeriod") .sendMessage(targetStartFirstPeriod, /* windowIndex= */ 0, /* positionMs= */ 0) .sendMessage(targetEndMiddlePeriod, /* windowIndex= */ 0, /* positionMs= */ duration1Ms) .sendMessage(targetStartMiddlePeriod, /* windowIndex= */ 1, /* positionMs= */ 0) .sendMessage(targetEndLastPeriod, /* windowIndex= */ 1, /* positionMs= */ duration2Ms) - .play() - // Add additional prepare at end and wait until it's processed to ensure that - // messages sent at end of playback are received before test ends. - .waitForPlaybackState(Player.STATE_ENDED) - .prepareSource( - new FakeMediaSource(timeline), /* resetPosition= */ false, /* resetState= */ true) - .waitForPlaybackState(Player.STATE_BUFFERING) - .waitForPlaybackState(Player.STATE_ENDED) + .waitForMessage(targetEndLastPeriod) .build(); new Builder() .setTimeline(timeline) @@ -1729,7 +1935,8 @@ public final class ExoPlayerTest { new ActionSchedule.Builder("testSendMessages") .waitForPlaybackState(Player.STATE_BUFFERING) .sendMessage(target, /* positionMs= */ 50) - .waitForTimelineChanged(timeline) + .waitForTimelineChanged( + timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .seek(/* positionMs= */ 50) .build(); new Builder() @@ -1770,7 +1977,8 @@ public final class ExoPlayerTest { new ActionSchedule.Builder("testSendMessages") .pause() .sendMessage(target, /* positionMs= */ 50) - .waitForTimelineChanged(timeline) + .waitForTimelineChanged( + timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .seek(/* positionMs= */ 51) .play() .build(); @@ -1849,14 +2057,16 @@ public final class ExoPlayerTest { ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessages") .pause() - .waitForTimelineChanged(timeline) + .waitForTimelineChanged( + timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .sendMessage(target, /* positionMs= */ 50) .executeRunnable(() -> mediaSource.setNewSourceInfo(secondTimeline, null)) - .waitForTimelineChanged(secondTimeline) + .waitForTimelineChanged( + secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .play() .build(); new Builder() - .setMediaSource(mediaSource) + .setMediaSources(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -1872,7 +2082,7 @@ public final class ExoPlayerTest { ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessages") .pause() - .waitForPlaybackState(Player.STATE_BUFFERING) + .waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) .play() .build(); @@ -1893,7 +2103,8 @@ public final class ExoPlayerTest { ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessages") .pause() - .waitForTimelineChanged(timeline) + .waitForTimelineChanged( + timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) .play() .build(); @@ -1922,15 +2133,17 @@ public final class ExoPlayerTest { ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessages") .pause() - .waitForTimelineChanged(timeline) + .waitForTimelineChanged( + timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .sendMessage(target, /* windowIndex = */ 1, /* positionMs= */ 50) .executeRunnable(() -> mediaSource.setNewSourceInfo(secondTimeline, null)) - .waitForTimelineChanged(secondTimeline) + .waitForTimelineChanged( + secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .seek(/* windowIndex= */ 0, /* positionMs= */ 0) .play() .build(); new Builder() - .setMediaSource(mediaSource) + .setMediaSources(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -1964,7 +2177,7 @@ public final class ExoPlayerTest { .play() .build(); new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) + .setMediaSources(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2102,16 +2315,21 @@ public final class ExoPlayerTest { /* windowIndex= */ 0, /* positionMs= */ C.usToMs(TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US)) .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline2, /* newManifest= */ null)) - .waitForTimelineChanged(timeline2) + .waitForTimelineChanged( + timeline2, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .play() .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) + .setMediaSources(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); testRunner.assertPlayedPeriodIndices(0, 1); // Assert that the second period was re-created from the new timeline. assertThat(mediaSource.getCreatedMediaPeriods()).hasSize(3); @@ -2153,7 +2371,7 @@ public final class ExoPlayerTest { .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) + .setMediaSources(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2191,7 +2409,7 @@ public final class ExoPlayerTest { ActionSchedule actionSchedule = new ActionSchedule.Builder("testInvalidSeekFallsBackToSubsequentPeriodOfTheRemovedPeriod") .pause() - .waitForTimelineChanged() + .waitForPlaybackState(Player.STATE_READY) .executeRunnable( new PlayerRunnable() { @Override @@ -2223,7 +2441,7 @@ public final class ExoPlayerTest { .play() .build(); new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) + .setMediaSources(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2335,6 +2553,60 @@ public final class ExoPlayerTest { assertThat(eventListenerPlayWhenReady).containsExactly(true, true, true, false).inOrder(); } + @Test + public void testRecursiveTimelineChangeInStopAreReportedInCorrectOrder() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 2); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 3); + final AtomicReference playerReference = new AtomicReference<>(); + FakeMediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final EventListener eventListener = + new EventListener() { + @Override + public void onPlayerStateChanged(boolean playWhenReady, int state) { + if (state == Player.STATE_IDLE) { + playerReference.get().setMediaSource(secondMediaSource); + } + } + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testRecursiveTimelineChangeInStopAreReportedInCorrectOrder") + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + playerReference.set(player); + player.addListener(eventListener); + } + }) + // Ensure there are no further pending callbacks. + .delay(1) + .stop(/* reset= */ true) + .waitForPlaybackState(Player.STATE_IDLE) + .prepare() + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .setActionSchedule(actionSchedule) + .setTimeline(firstTimeline) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + exoPlayerTestRunner.assertTimelinesSame( + new FakeMediaSource.InitialTimeline(firstTimeline), + firstTimeline, + Timeline.EMPTY, + new FakeMediaSource.InitialTimeline(secondTimeline), + secondTimeline); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + } + @Test public void testClippedLoopedPeriodsArePlayedFully() throws Exception { long startPositionUs = 300_000; @@ -2387,7 +2659,7 @@ public final class ExoPlayerTest { .build(); new ExoPlayerTestRunner.Builder() .setClock(clock) - .setMediaSource(mediaSource) + .setMediaSources(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2421,7 +2693,7 @@ public final class ExoPlayerTest { List trackGroupsList = new ArrayList<>(); List trackSelectionsList = new ArrayList<>(); new Builder() - .setMediaSource(mediaSource) + .setMediaSources(mediaSource) .setSupportedFormats(Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT) .setActionSchedule(actionSchedule) .setEventListener( @@ -2469,7 +2741,7 @@ public final class ExoPlayerTest { FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); ExoPlayerTestRunner testRunner = new Builder() - .setMediaSource(concatenatingMediaSource) + .setMediaSources(concatenatingMediaSource) .setRenderers(renderer) .build(context); try { @@ -2512,49 +2784,7 @@ public final class ExoPlayerTest { FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); ExoPlayerTestRunner testRunner = new Builder() - .setMediaSource(concatenatingMediaSource) - .setActionSchedule(actionSchedule) - .setRenderers(renderer) - .build(context); - try { - testRunner.start().blockUntilEnded(TIMEOUT_MS); - fail(); - } catch (ExoPlaybackException e) { - // Expected exception. - } - assertThat(renderer.sampleBufferReadCount).isAtLeast(1); - assertThat(renderer.hasReadStreamToEnd()).isTrue(); - } - - @Test - public void failingDynamicUpdateOnlyThrowsWhenAvailablePeriodHasBeenFullyRead() throws Exception { - Timeline fakeTimeline = - new FakeTimeline( - new TimelineWindowDefinition( - /* isSeekable= */ true, - /* isDynamic= */ true, - /* durationUs= */ 10 * C.MICROS_PER_SECOND)); - AtomicReference wasReadyOnce = new AtomicReference<>(false); - MediaSource mediaSource = - new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT) { - @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { - if (wasReadyOnce.get()) { - throw new IOException(); - } - } - }; - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testFailingDynamicMediaSourceInTimelineOnlyThrowsLater") - .pause() - .waitForPlaybackState(Player.STATE_READY) - .executeRunnable(() -> wasReadyOnce.set(true)) - .play() - .build(); - FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); - ExoPlayerTestRunner testRunner = - new Builder() - .setMediaSource(mediaSource) + .setMediaSources(concatenatingMediaSource) .setActionSchedule(actionSchedule) .setRenderers(renderer) .build(context); @@ -2588,7 +2818,7 @@ public final class ExoPlayerTest { .executeRunnable(concatenatingMediaSource::clear) .build(); new Builder() - .setMediaSource(concatenatingMediaSource) + .setMediaSources(concatenatingMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2612,7 +2842,7 @@ public final class ExoPlayerTest { .pause() .waitForPlaybackState(Player.STATE_BUFFERING) .seek(/* positionMs= */ 10) - .waitForTimelineChanged() + .waitForSeekProcessed() .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null)) .waitForTimelineChanged() .waitForPlaybackState(Player.STATE_READY) @@ -2626,7 +2856,7 @@ public final class ExoPlayerTest { .play() .build(); new Builder() - .setMediaSource(concatenatedMediaSource) + .setMediaSources(concatenatedMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2657,7 +2887,7 @@ public final class ExoPlayerTest { .waitForPlaybackState(Player.STATE_BUFFERING) // Seek 10ms into the second period. .seek(/* positionMs= */ periodDurationMs + 10) - .waitForTimelineChanged() + .waitForSeekProcessed() .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null)) .waitForTimelineChanged() .waitForPlaybackState(Player.STATE_READY) @@ -2672,7 +2902,7 @@ public final class ExoPlayerTest { .play() .build(); new Builder() - .setMediaSource(concatenatedMediaSource) + .setMediaSources(concatenatedMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2773,10 +3003,10 @@ public final class ExoPlayerTest { player.addListener(eventListener); } }) - .seek(5_000) + .seek(/* positionMs= */ 5_000) .build(); new ExoPlayerTestRunner.Builder() - .setMediaSource(fakeMediaSource) + .setMediaSources(fakeMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2970,6 +3200,17 @@ public final class ExoPlayerTest { Void id, MediaSource mediaSource, Timeline timeline) { refreshSourceInfo(timeline); } + + @Override + public boolean isSingleWindow() { + return false; + } + + @Nullable + @Override + public Timeline getInitialTimeline() { + return Timeline.EMPTY; + } }; int[] currentWindowIndices = new int[1]; long[] currentPlaybackPositions = new long[1]; @@ -2979,6 +3220,8 @@ public final class ExoPlayerTest { new ActionSchedule.Builder("testDelegatingMediaSourceApproach") .seek(/* windowIndex= */ 1, /* positionMs= */ 5000) .waitForSeekProcessed() + .waitForTimelineChanged( + /* expectedTimeline= */ null, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .executeRunnable( new PlayerRunnable() { @Override @@ -2991,13 +3234,15 @@ public final class ExoPlayerTest { .build(); ExoPlayerTestRunner exoPlayerTestRunner = new Builder() - .setMediaSource(delegatingMediaSource) + .setMediaSources(delegatingMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); assertArrayEquals(new long[] {2}, windowCounts); assertArrayEquals(new int[] {seekToWindowIndex}, currentWindowIndices); assertArrayEquals(new long[] {5_000}, currentPlaybackPositions); @@ -3014,7 +3259,6 @@ public final class ExoPlayerTest { new ActionSchedule.Builder("testSeekTo_windowIndexIsReset_deprecated") .pause() .seek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) - .waitForSeekProcessed() .playUntilPosition(/* windowIndex= */ 1, /* positionMs= */ 5000) .executeRunnable( new PlayerRunnable() { @@ -3035,7 +3279,7 @@ public final class ExoPlayerTest { }) .build(); new ExoPlayerTestRunner.Builder() - .setMediaSource(loopingMediaSource) + .setMediaSources(loopingMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -3056,7 +3300,6 @@ public final class ExoPlayerTest { new ActionSchedule.Builder("testSeekTo_windowIndexIsReset") .pause() .seek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) - .waitForSeekProcessed() .playUntilPosition(/* windowIndex= */ 1, /* positionMs= */ 5000) .executeRunnable( new PlayerRunnable() { @@ -3075,8 +3318,8 @@ public final class ExoPlayerTest { } }) .build(); - new ExoPlayerTestRunner.Builder() - .setMediaSource(loopingMediaSource) + new Builder() + .setMediaSources(loopingMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -3178,7 +3421,7 @@ public final class ExoPlayerTest { try { new ExoPlayerTestRunner.Builder() .setLoadControl(neverLoadingLoadControl) - .setMediaSource(chunkedMediaSource) + .setMediaSources(chunkedMediaSource) .build(context) .start() .blockUntilEnded(TIMEOUT_MS); @@ -3217,13 +3460,2142 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setLoadControl(neverLoadingOrPlayingLoadControl) - .setMediaSource(chunkedMediaSource) + .setMediaSources(chunkedMediaSource) .build(context) .start() // This throws if playback doesn't finish within timeout. .blockUntilEnded(TIMEOUT_MS); } + @Test + public void testMoveMediaItem() throws Exception { + TimelineWindowDefinition firstWindowDefinition = + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 1, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ C.msToUs(10000)); + TimelineWindowDefinition secondWindowDefinition = + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 2, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ C.msToUs(10000)); + Timeline timeline1 = new FakeTimeline(firstWindowDefinition); + Timeline timeline2 = new FakeTimeline(secondWindowDefinition); + MediaSource mediaSource1 = new FakeMediaSource(timeline1); + MediaSource mediaSource2 = new FakeMediaSource(timeline2); + Timeline expectedDummyTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 1, + /* isSeekable= */ false, + /* isDynamic= */ true, + /* isLive= */ false, + /* durationUs= */ C.TIME_UNSET, + AdPlaybackState.NONE), + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 2, + /* isSeekable= */ false, + /* isDynamic= */ true, + /* isLive= */ false, + /* durationUs= */ C.TIME_UNSET, + AdPlaybackState.NONE)); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testMoveMediaItem") + .waitForTimelineChanged( + /* expectedTimeline= */ null, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .moveMediaItem(/* currentIndex= */ 0, /* newIndex= */ 1) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .setMediaSources(mediaSource1, mediaSource2) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + Timeline expectedRealTimeline = new FakeTimeline(firstWindowDefinition, secondWindowDefinition); + Timeline expectedRealTimelineAfterMove = + new FakeTimeline(secondWindowDefinition, firstWindowDefinition); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + exoPlayerTestRunner.assertTimelinesSame( + expectedDummyTimeline, expectedRealTimeline, expectedRealTimelineAfterMove); + } + + @Test + public void testRemoveMediaItem() throws Exception { + TimelineWindowDefinition firstWindowDefinition = + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 1, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ C.msToUs(10000)); + TimelineWindowDefinition secondWindowDefinition = + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 2, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ C.msToUs(10000)); + TimelineWindowDefinition thirdWindowDefinition = + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 3, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ C.msToUs(10000)); + Timeline timeline1 = new FakeTimeline(firstWindowDefinition); + Timeline timeline2 = new FakeTimeline(secondWindowDefinition); + Timeline timeline3 = new FakeTimeline(thirdWindowDefinition); + MediaSource mediaSource1 = new FakeMediaSource(timeline1); + MediaSource mediaSource2 = new FakeMediaSource(timeline2); + MediaSource mediaSource3 = new FakeMediaSource(timeline3); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testRemoveMediaItems") + .waitForPlaybackState(Player.STATE_READY) + .removeMediaItem(/* index= */ 0) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .setMediaSources(mediaSource1, mediaSource2, mediaSource3) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + Timeline expectedDummyTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 1, + /* isSeekable= */ false, + /* isDynamic= */ true, + /* isLive= */ false, + /* durationUs= */ C.TIME_UNSET, + AdPlaybackState.NONE), + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 2, + /* isSeekable= */ false, + /* isDynamic= */ true, + /* isLive= */ false, + /* durationUs= */ C.TIME_UNSET, + AdPlaybackState.NONE), + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 3, + /* isSeekable= */ false, + /* isDynamic= */ true, + /* isLive= */ false, + /* durationUs= */ C.TIME_UNSET, + AdPlaybackState.NONE)); + Timeline expectedRealTimeline = + new FakeTimeline(firstWindowDefinition, secondWindowDefinition, thirdWindowDefinition); + Timeline expectedRealTimelineAfterRemove = + new FakeTimeline(secondWindowDefinition, thirdWindowDefinition); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + exoPlayerTestRunner.assertTimelinesSame( + expectedDummyTimeline, expectedRealTimeline, expectedRealTimelineAfterRemove); + } + + @Test + public void testRemoveMediaItems() throws Exception { + TimelineWindowDefinition firstWindowDefinition = + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 1, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ C.msToUs(10000)); + TimelineWindowDefinition secondWindowDefinition = + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 2, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ C.msToUs(10000)); + TimelineWindowDefinition thirdWindowDefinition = + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 3, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ C.msToUs(10000)); + Timeline timeline1 = new FakeTimeline(firstWindowDefinition); + Timeline timeline2 = new FakeTimeline(secondWindowDefinition); + Timeline timeline3 = new FakeTimeline(thirdWindowDefinition); + MediaSource mediaSource1 = new FakeMediaSource(timeline1); + MediaSource mediaSource2 = new FakeMediaSource(timeline2); + MediaSource mediaSource3 = new FakeMediaSource(timeline3); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testRemoveMediaItems") + .waitForPlaybackState(Player.STATE_READY) + .removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .setMediaSources(mediaSource1, mediaSource2, mediaSource3) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + Timeline expectedDummyTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 1, + /* isSeekable= */ false, + /* isDynamic= */ true, + /* isLive= */ false, + /* durationUs= */ C.TIME_UNSET, + AdPlaybackState.NONE), + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 2, + /* isSeekable= */ false, + /* isDynamic= */ true, + /* isLive= */ false, + /* durationUs= */ C.TIME_UNSET, + AdPlaybackState.NONE), + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 3, + /* isSeekable= */ false, + /* isDynamic= */ true, + /* isLive= */ false, + /* durationUs= */ C.TIME_UNSET, + AdPlaybackState.NONE)); + Timeline expectedRealTimeline = + new FakeTimeline(firstWindowDefinition, secondWindowDefinition, thirdWindowDefinition); + Timeline expectedRealTimelineAfterRemove = new FakeTimeline(firstWindowDefinition); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + exoPlayerTestRunner.assertTimelinesSame( + expectedDummyTimeline, expectedRealTimeline, expectedRealTimelineAfterRemove); + } + + @Test + public void testClearMediaItems() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testClearMediaItems") + .waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForPlaybackState(Player.STATE_READY) + .clearMediaItems() + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_IDLE, Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED); + exoPlayerTestRunner.assertTimelinesSame(dummyTimeline, timeline, Timeline.EMPTY); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item set (masked timeline) */, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* playlist cleared */); + } + + @Test + public void testMultipleModificationWithRecursiveListenerInvocations() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource mediaSource = new FakeMediaSource(timeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 2); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testMultipleModificationWithRecursiveListenerInvocations") + .waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .clearMediaItems() + .setMediaSources(secondMediaSource) + .waitForTimelineChanged() + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .setMediaSources(mediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + exoPlayerTestRunner.assertTimelinesSame( + dummyTimeline, + timeline, + Timeline.EMPTY, + new FakeMediaSource.InitialTimeline(secondTimeline), + secondTimeline); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + } + + @Test + public void testModifyPlaylistUnprepared_remainsInIdle_needsPrepareForBuffering() + throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + int[] playbackStates = new int[4]; + int[] timelineWindowCounts = new int[4]; + int[] maskingPlaybackState = {C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder( + "testModifyPlaylistUnprepared_remainsInIdle_needsPrepareForBuffering") + .waitForTimelineChanged(dummyTimeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) + .executeRunnable( + new PlaybackStateCollector(/* index= */ 0, playbackStates, timelineWindowCounts)) + .clearMediaItems() + .executeRunnable( + new PlaybackStateCollector(/* index= */ 1, playbackStates, timelineWindowCounts)) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.setMediaSource(firstMediaSource, /* startPositionMs= */ 1000); + maskingPlaybackState[0] = player.getPlaybackState(); + } + }) + .executeRunnable( + new PlaybackStateCollector(/* index= */ 2, playbackStates, timelineWindowCounts)) + .addMediaSources(secondMediaSource) + .executeRunnable( + new PlaybackStateCollector(/* index= */ 3, playbackStates, timelineWindowCounts)) + .seek(/* windowIndex= */ 1, /* positionMs= */ 2000) + .waitForSeekProcessed() + .prepare() + // The first expected buffering state arrives after prepare but not before. + .waitForPlaybackState(Player.STATE_BUFFERING) + .waitForPlaybackState(Player.STATE_READY) + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .setMediaSources(firstMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + assertArrayEquals( + new int[] {Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE}, + playbackStates); + assertArrayEquals(new int[] {1, 0, 1, 2}, timelineWindowCounts); + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_IDLE, + Player.STATE_BUFFERING /* first buffering state after prepare */, + Player.STATE_READY, + Player.STATE_ENDED); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* initial setMediaSources */, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* clear */, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* set media items */, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* add media items */, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source update after prepare */); + Timeline expectedSecondDummyTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ false, + /* isDynamic= */ true, + /* isLive= */ false, + /* durationUs= */ C.TIME_UNSET, + AdPlaybackState.NONE), + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ false, + /* isDynamic= */ true, + /* isLive= */ false, + /* durationUs= */ C.TIME_UNSET, + AdPlaybackState.NONE)); + Timeline expectedSecondRealTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ 10_000_000), + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ 10_000_000)); + exoPlayerTestRunner.assertTimelinesSame( + dummyTimeline, + Timeline.EMPTY, + dummyTimeline, + expectedSecondDummyTimeline, + expectedSecondRealTimeline); + assertArrayEquals(new int[] {Player.STATE_IDLE}, maskingPlaybackState); + } + + @Test + public void testModifyPlaylistPrepared_remainsInEnded_needsSeekForBuffering() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + FakeMediaSource secondMediaSource = new FakeMediaSource(timeline); + ActionSchedule actionSchedule = + new ActionSchedule.Builder( + "testModifyPlaylistPrepared_remainsInEnded_needsSeekForBuffering") + .waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForPlaybackState(Player.STATE_BUFFERING) + .waitForPlaybackState(Player.STATE_READY) + .clearMediaItems() + .waitForPlaybackState(Player.STATE_ENDED) + .addMediaSources(secondMediaSource) // add must not transition to buffering + .waitForTimelineChanged() + .clearMediaItems() // clear must remain in ended + .addMediaSources(secondMediaSource) // add again to be able to test the seek + .waitForTimelineChanged() + .seek(/* positionMs= */ 2_000) // seek must transition to buffering + .waitForSeekProcessed() + .waitForPlaybackState(Player.STATE_BUFFERING) + .waitForPlaybackState(Player.STATE_READY) + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .setExpectedPlayerEndedCount(/* expectedPlayerEndedCount= */ 2) + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_IDLE, + Player.STATE_BUFFERING, // first buffering + Player.STATE_READY, + Player.STATE_ENDED, // clear playlist + Player.STATE_BUFFERING, // second buffering after seek + Player.STATE_READY, + Player.STATE_ENDED); + exoPlayerTestRunner.assertTimelinesSame( + dummyTimeline, + timeline, + Timeline.EMPTY, + dummyTimeline, + timeline, + Timeline.EMPTY, + dummyTimeline, + timeline); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item set (masked timeline) */, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* playlist cleared */, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media items added */, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* playlist cleared */, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media items added */, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */); + } + + @Test + public void testStopWithNoReset_modifyingPlaylistRemainsInIdleState_needsPrepareForBuffering() + throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + FakeMediaSource secondMediaSource = new FakeMediaSource(timeline); + int[] playbackStateHolder = new int[3]; + int[] windowCountHolder = new int[3]; + ActionSchedule actionSchedule = + new ActionSchedule.Builder( + "testStopWithNoReset_modifyingPlaylistRemainsInIdleState_needsPrepareForBuffering") + .waitForPlaybackState(Player.STATE_READY) + .stop(/* reset= */ false) + .executeRunnable( + new PlaybackStateCollector(/* index= */ 0, playbackStateHolder, windowCountHolder)) + .clearMediaItems() + .executeRunnable( + new PlaybackStateCollector(/* index= */ 1, playbackStateHolder, windowCountHolder)) + .addMediaSources(secondMediaSource) + .executeRunnable( + new PlaybackStateCollector(/* index= */ 2, playbackStateHolder, windowCountHolder)) + .prepare() + .waitForPlaybackState(Player.STATE_BUFFERING) + .waitForPlaybackState(Player.STATE_READY) + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + assertArrayEquals( + new int[] {Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE}, playbackStateHolder); + assertArrayEquals(new int[] {1, 0, 1}, windowCountHolder); + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_IDLE, + Player.STATE_BUFFERING, // first buffering + Player.STATE_READY, + Player.STATE_IDLE, // stop + Player.STATE_BUFFERING, + Player.STATE_READY, + Player.STATE_ENDED); + exoPlayerTestRunner.assertTimelinesSame( + dummyTimeline, timeline, Timeline.EMPTY, dummyTimeline, timeline); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item set (masked timeline) */, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, /* source prepared */ + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* clear media items */, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item add (masked timeline) */, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */); + } + + @Test + public void testPrepareWithInvalidInitialSeek_expectEndedImmediately() throws Exception { + final int[] currentWindowIndices = {C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testPrepareWithInvalidInitialSeek_expectEnded") + .waitForPlaybackState(Player.STATE_ENDED) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + } + }) + .prepare() + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .skipSettingMediaSources() + .initialSeek(/* windowIndex= */ 1, C.TIME_UNSET) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + exoPlayerTestRunner.assertPlaybackStatesEqual(Player.STATE_IDLE, Player.STATE_ENDED); + exoPlayerTestRunner.assertTimelinesSame(); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual(); + assertArrayEquals(new int[] {1}, currentWindowIndices); + } + + @Test + public void testPrepareWhenAlreadyPreparedIsANoop() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testPrepareWhenAlreadyPreparedIsANoop") + .waitForPlaybackState(Player.STATE_READY) + .prepare() + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_IDLE, Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED); + exoPlayerTestRunner.assertTimelinesSame(dummyTimeline, timeline); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item set (masked timeline) */, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */); + } + + @Test + public void testSeekToIndexLargerThanNumberOfPlaylistItems() throws Exception { + Timeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 10_000)); + ConcatenatingMediaSource concatenatingMediaSource = + new ConcatenatingMediaSource( + /* isAtomic= */ false, + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); + int[] currentWindowIndices = new int[1]; + long[] currentPlaybackPositions = new long[1]; + int seekToWindowIndex = 1; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSeekToIndexLargerThanNumberOfPlaylistItems") + .waitForSeekProcessed() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + currentPlaybackPositions[0] = player.getCurrentPosition(); + } + }) + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setMediaSources(concatenatingMediaSource) + .initialSeek(seekToWindowIndex, 5000) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new long[] {5_000}, currentPlaybackPositions); + assertArrayEquals(new int[] {seekToWindowIndex}, currentWindowIndices); + } + + @Test + public void testSeekToIndexWithEmptyMultiWindowMediaSource() throws Exception { + Timeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 10_000)); + ConcatenatingMediaSource concatenatingMediaSource = + new ConcatenatingMediaSource(/* isAtomic= */ false); + int[] currentWindowIndices = new int[2]; + long[] currentPlaybackPositions = new long[2]; + long[] windowCounts = new long[2]; + int seekToWindowIndex = 1; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSeekToIndexWithEmptyMultiWindowMediaSource") + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + currentPlaybackPositions[0] = player.getCurrentPosition(); + windowCounts[0] = player.getCurrentTimeline().getWindowCount(); + } + }) + .executeRunnable( + () -> { + concatenatingMediaSource.addMediaSource( + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); + concatenatingMediaSource.addMediaSource( + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); + }) + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[1] = player.getCurrentWindowIndex(); + currentPlaybackPositions[1] = player.getCurrentPosition(); + windowCounts[1] = player.getCurrentTimeline().getWindowCount(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSources(concatenatingMediaSource) + .initialSeek(seekToWindowIndex, 5000) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new long[] {0, 2}, windowCounts); + assertArrayEquals(new int[] {seekToWindowIndex, seekToWindowIndex}, currentWindowIndices); + assertArrayEquals(new long[] {5_000, 5_000}, currentPlaybackPositions); + } + + @Test + public void testEmptyMultiWindowMediaSource_doesNotEnterBufferState() throws Exception { + ConcatenatingMediaSource concatenatingMediaSource = + new ConcatenatingMediaSource(/* isAtomic= */ false); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testEmptyMultiWindowMediaSource_doesNotEnterBufferState") + .waitForTimelineChanged() + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .setMediaSources(concatenatingMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + exoPlayerTestRunner.assertPlaybackStatesEqual(1, 4); + } + + @Test + public void testSeekToIndexWithEmptyMultiWindowMediaSource_usesLazyPreparation() + throws Exception { + Timeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 10_000)); + ConcatenatingMediaSource concatenatingMediaSource = + new ConcatenatingMediaSource(/* isAtomic= */ false); + int[] currentWindowIndices = new int[2]; + long[] currentPlaybackPositions = new long[2]; + long[] windowCounts = new long[2]; + int seekToWindowIndex = 1; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSeekToIndexWithEmptyMultiWindowMediaSource") + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + currentPlaybackPositions[0] = player.getCurrentPosition(); + windowCounts[0] = player.getCurrentTimeline().getWindowCount(); + } + }) + .executeRunnable( + () -> { + concatenatingMediaSource.addMediaSource( + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); + concatenatingMediaSource.addMediaSource( + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); + }) + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[1] = player.getCurrentWindowIndex(); + currentPlaybackPositions[1] = player.getCurrentPosition(); + windowCounts[1] = player.getCurrentTimeline().getWindowCount(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSources(concatenatingMediaSource) + .setUseLazyPreparation(/* useLazyPreparation= */ true) + .initialSeek(seekToWindowIndex, 5000) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new long[] {0, 2}, windowCounts); + assertArrayEquals(new int[] {seekToWindowIndex, seekToWindowIndex}, currentWindowIndices); + assertArrayEquals(new long[] {5_000, 5_000}, currentPlaybackPositions); + } + + @Test + public void testSetMediaSources_empty_whenEmpty_correctMaskingWindowIndex() throws Exception { + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSetMediaSources_empty_whenEmpty_correctMaskingWindowIndex") + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + List listOfTwo = new ArrayList<>(); + listOfTwo.add(secondMediaSource); + listOfTwo.add(secondMediaSource); + player.addMediaSources(/* index= */ 0, listOfTwo); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + } + }) + .prepare() + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[2] = player.getCurrentWindowIndex(); + } + }) + .build(); + new Builder() + .setMediaSources(new ConcatenatingMediaSource()) + .setActionSchedule(actionSchedule) + .build(context) + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {0, 0, 0}, currentWindowIndices); + } + + @Test + public void testSetMediaSources_empty_whenEmpty_validInitialSeek_correctMaskingWindowIndex() + throws Exception { + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder( + "testSetMediaSources_empty_whenEmpty_validInitialSeek_correctMaskingWindowIndex") + .waitForSeekProcessed() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + List listOfTwo = new ArrayList<>(); + listOfTwo.add(secondMediaSource); + listOfTwo.add(secondMediaSource); + player.addMediaSources(/* index= */ 0, listOfTwo); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + } + }) + .prepare() + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[2] = player.getCurrentWindowIndex(); + } + }) + .build(); + new Builder() + .initialSeek(/* windowIndex= */ 1, C.TIME_UNSET) + .setMediaSources(new ConcatenatingMediaSource()) + .setActionSchedule(actionSchedule) + .build(context) + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {1, 1, 1}, currentWindowIndices); + } + + @Test + public void testSetMediaSources_empty_whenEmpty_invalidInitialSeek_correctMaskingWindowIndex() + throws Exception { + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder( + "testSetMediaSources_empty_whenEmpty_invalidInitialSeek_correctMaskingWindowIndex") + .waitForSeekProcessed() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + List listOfTwo = new ArrayList<>(); + listOfTwo.add(secondMediaSource); + listOfTwo.add(secondMediaSource); + player.addMediaSources(/* index= */ 0, listOfTwo); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + } + }) + .prepare() + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[2] = player.getCurrentWindowIndex(); + } + }) + .build(); + new Builder() + .initialSeek(/* windowIndex= */ 4, C.TIME_UNSET) + .setMediaSources(new ConcatenatingMediaSource()) + .setActionSchedule(actionSchedule) + .build(context) + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {4, 0, 0}, currentWindowIndices); + } + + @Test + public void testSetMediaSources_whenEmpty_correctMaskingWindowIndex() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSetMediaSources_whenEmpty_correctMaskingWindowIndex") + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Increase current window index. + player.addMediaSource(/* index= */ 0, secondMediaSource); + currentWindowIndices[0] = player.getCurrentWindowIndex(); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Current window index is unchanged. + player.addMediaSource(/* index= */ 2, secondMediaSource); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + MediaSource mediaSource = + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)); + ConcatenatingMediaSource concatenatingMediaSource = + new ConcatenatingMediaSource(mediaSource, mediaSource, mediaSource); + // Increase current window with multi window source. + player.addMediaSource(/* index= */ 0, concatenatingMediaSource); + currentWindowIndices[2] = player.getCurrentWindowIndex(); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + ConcatenatingMediaSource concatenatingMediaSource = + new ConcatenatingMediaSource(); + // Current window index is unchanged when adding empty source. + player.addMediaSource(/* index= */ 0, concatenatingMediaSource); + currentWindowIndices[3] = player.getCurrentWindowIndex(); + } + }) + .build(); + new Builder() + .setMediaSources(firstMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {1, 1, 4, 4}, currentWindowIndices); + } + + @Test + public void testSetMediaSources_whenEmpty_validInitialSeek_correctMaskingWindowIndex() + throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 2); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1, new Object()); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder( + "testSetMediaSources_whenEmpty_validInitialSeek_correctMaskingWindowIndex") + .waitForSeekProcessed() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + // Increase current window index. + player.addMediaSource(/* index= */ 0, secondMediaSource); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + } + }) + .prepare() + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[2] = player.getCurrentWindowIndex(); + } + }) + .build(); + new Builder() + .initialSeek(/* windowIndex= */ 1, C.TIME_UNSET) + .setMediaSources(firstMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {1, 2, 2}, currentWindowIndices); + } + + @Test + public void testSetMediaSources_whenEmpty_invalidInitialSeek_correctMaskingWindowIndex() + throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1, new Object()); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder( + "testSetMediaSources_whenEmpty_invalidInitialSeek_correctMaskingWindowIndex") + .waitForSeekProcessed() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + // Increase current window index. + player.addMediaSource(/* index= */ 0, secondMediaSource); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + } + }) + .prepare() + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[2] = player.getCurrentWindowIndex(); + } + }) + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + new Builder() + .initialSeek(/* windowIndex= */ 1, C.TIME_UNSET) + .setMediaSources(firstMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {0, 1, 1}, currentWindowIndices); + } + + @Test + public void testSetMediaSources_correctMaskingWindowIndex() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1, new Object()); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSetMediaSources_correctMaskingWindowIndex") + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + // Increase current window index. + player.addMediaSource(/* index= */ 0, secondMediaSource); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + } + }) + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[2] = player.getCurrentWindowIndex(); + } + }) + .build(); + new Builder() + .setMediaSources(firstMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {0, 1, 1}, currentWindowIndices); + } + + @Test + public void testSetMediaSources_whenIdle_correctMaskingPlaybackState() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1, 1L); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1, 2L); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] maskingPlaybackStates = new int[4]; + Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSetMediaSources_whenIdle_correctMaskingPlaybackState") + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set empty media item with no seek. + player.setMediaSource(new ConcatenatingMediaSource()); + maskingPlaybackStates[0] = player.getPlaybackState(); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set media item with an implicit seek to the current position. + player.setMediaSource(firstMediaSource); + maskingPlaybackStates[1] = player.getPlaybackState(); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set media item with an explicit seek. + player.setMediaSource(secondMediaSource, /* startPositionMs= */ C.TIME_UNSET); + maskingPlaybackStates[2] = player.getPlaybackState(); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set empty media item with an explicit seek. + player.setMediaSource( + new ConcatenatingMediaSource(), /* startPositionMs= */ C.TIME_UNSET); + maskingPlaybackStates[3] = player.getPlaybackState(); + } + }) + .prepare() + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .skipSettingMediaSources() + .setActionSchedule(actionSchedule) + .build(context) + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + // Expect reset of masking to first window. + exoPlayerTestRunner.assertPlaybackStatesEqual(Player.STATE_IDLE, Player.STATE_ENDED); + assertArrayEquals( + new int[] {Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE}, + maskingPlaybackStates); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + } + + @Test + public void testSetMediaSources_whenIdle_invalidSeek_correctMaskingPlaybackState() + throws Exception { + final int[] maskingPlaybackStates = new int[1]; + Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder( + "testSetMediaSources_whenIdle_invalidSeek_correctMaskingPlaybackState") + .waitForSeekProcessed() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set a media item with an implicit seek to the current position which is + // invalid in the new timeline. + player.setMediaSource( + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1, 1L))); + maskingPlaybackStates[0] = player.getPlaybackState(); + } + }) + .prepare() + .waitForPlaybackState(Player.STATE_READY) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .initialSeek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) + .setMediaSources(new ConcatenatingMediaSource()) + .setActionSchedule(actionSchedule) + .build(context) + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + // Expect reset of masking to first window. + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_IDLE, Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED); + assertArrayEquals(new int[] {Player.STATE_IDLE}, maskingPlaybackStates); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + } + + @Test + public void testSetMediaSources_whenIdle_noSeek_correctMaskingPlaybackState() throws Exception { + final int[] maskingPlaybackStates = new int[1]; + Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder( + "testSetMediaSources_whenIdle_noSeek_correctMaskingPlaybackState") + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set media item with no seek. + player.setMediaSource( + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1))); + maskingPlaybackStates[0] = player.getPlaybackState(); + } + }) + .prepare() + .waitForPlaybackState(Player.STATE_READY) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .skipSettingMediaSources() + .setActionSchedule(actionSchedule) + .build(context) + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + // Expect reset of masking to first window. + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_IDLE, Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED); + assertArrayEquals(new int[] {Player.STATE_IDLE}, maskingPlaybackStates); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + } + + @Test + public void testSetMediaSources_whenIdle_noSeekEmpty_correctMaskingPlaybackState() + throws Exception { + final int[] maskingPlaybackStates = new int[1]; + Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder( + "testSetMediaSources_whenIdle_noSeekEmpty_correctMaskingPlaybackState") + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set an empty media item with no seek. + player.setMediaSource(new ConcatenatingMediaSource()); + maskingPlaybackStates[0] = player.getPlaybackState(); + } + }) + .setMediaSources(new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1))) + .prepare() + .waitForPlaybackState(Player.STATE_READY) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .skipSettingMediaSources() + .setActionSchedule(actionSchedule) + .build(context) + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + // Expect reset of masking to first window. + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_IDLE, Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED); + assertArrayEquals(new int[] {Player.STATE_IDLE}, maskingPlaybackStates); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + } + + @Test + public void testSetMediaSources_whenEnded_correctMaskingPlaybackState() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1, 1L); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1, 2L); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] maskingPlaybackStates = new int[4]; + Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSetMediaSources_whenEnded_correctMaskingPlaybackState") + .waitForPlaybackState(Player.STATE_ENDED) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set empty media item with an implicit seek to the current position. + player.setMediaSource(new ConcatenatingMediaSource()); + maskingPlaybackStates[0] = player.getPlaybackState(); + } + }) + .waitForPlaybackState(Player.STATE_ENDED) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set empty media item with an explicit seek. + player.setMediaSource( + new ConcatenatingMediaSource(), /* startPositionMs= */ C.TIME_UNSET); + maskingPlaybackStates[1] = player.getPlaybackState(); + } + }) + .waitForPlaybackState(Player.STATE_ENDED) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set media item with an implicit seek to the current position. + player.setMediaSource(firstMediaSource); + maskingPlaybackStates[2] = player.getPlaybackState(); + } + }) + .waitForPlaybackState(Player.STATE_READY) + .clearMediaItems() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set media item with an explicit seek. + player.setMediaSource(secondMediaSource, /* startPositionMs= */ C.TIME_UNSET); + maskingPlaybackStates[3] = player.getPlaybackState(); + } + }) + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .setExpectedPlayerEndedCount(/* expectedPlayerEndedCount= */ 3) + .setMediaSources(new ConcatenatingMediaSource()) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + // Expect reset of masking to first window. + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_IDLE, + Player.STATE_ENDED, + Player.STATE_BUFFERING, + Player.STATE_READY, + Player.STATE_ENDED, + Player.STATE_BUFFERING, + Player.STATE_READY, + Player.STATE_ENDED); + assertArrayEquals( + new int[] { + Player.STATE_ENDED, Player.STATE_ENDED, Player.STATE_BUFFERING, Player.STATE_BUFFERING + }, + maskingPlaybackStates); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + } + + @Test + public void testSetMediaSources_whenEnded_invalidSeek_correctMaskingPlaybackState() + throws Exception { + final int[] maskingPlaybackStates = new int[1]; + Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder( + "testSetMediaSources_whenEnded_invalidSeek_correctMaskingPlaybackState") + .waitForSeekProcessed() + .waitForPlaybackState(Player.STATE_ENDED) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set media item with an invalid implicit seek to the current position. + player.setMediaSource( + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1, 1L)), + /* resetPosition= */ false); + maskingPlaybackStates[0] = player.getPlaybackState(); + } + }) + .waitForTimelineChanged() + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .initialSeek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) + .setMediaSources(new ConcatenatingMediaSource()) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + // Expect reset of masking to first window. + exoPlayerTestRunner.assertPlaybackStatesEqual(Player.STATE_IDLE, Player.STATE_ENDED); + assertArrayEquals(new int[] {Player.STATE_ENDED}, maskingPlaybackStates); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + } + + @Test + public void testSetMediaSources_whenEnded_noSeek_correctMaskingPlaybackState() throws Exception { + final int[] maskingPlaybackStates = new int[1]; + Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder( + "testSetMediaSources_whenEnded_noSeek_correctMaskingPlaybackState") + .waitForPlaybackState(Player.STATE_READY) + .clearMediaItems() + .waitForPlaybackState(Player.STATE_ENDED) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set media item with no seek (keep current position). + player.setMediaSource( + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + /* resetPosition= */ false); + maskingPlaybackStates[0] = player.getPlaybackState(); + } + }) + .waitForTimelineChanged() + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + // Expect reset of masking to first window. + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_IDLE, Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED); + assertArrayEquals(new int[] {Player.STATE_ENDED}, maskingPlaybackStates); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + } + + @Test + public void testSetMediaSources_whenEnded_noSeekEmpty_correctMaskingPlaybackState() + throws Exception { + final int[] maskingPlaybackStates = new int[1]; + Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder( + "testSetMediaSources_whenEnded_noSeekEmpty_correctMaskingPlaybackState") + .waitForPlaybackState(Player.STATE_READY) + .clearMediaItems() + .waitForPlaybackState(Player.STATE_ENDED) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set an empty media item with no seek. + player.setMediaSource(new ConcatenatingMediaSource()); + maskingPlaybackStates[0] = player.getPlaybackState(); + } + }) + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + // Expect reset of masking to first window. + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_IDLE, Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED); + assertArrayEquals(new int[] {Player.STATE_ENDED}, maskingPlaybackStates); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + } + + @Test + public void testSetMediaSources_whenPrepared_correctMaskingPlaybackState() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1, 1L); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1, 2L); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] maskingPlaybackStates = new int[4]; + Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSetMediaSources_whenPrepared_correctMaskingPlaybackState") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set empty media item with an implicit seek to current position. + player.setMediaSource( + new ConcatenatingMediaSource(), /* resetPosition= */ false); + // Expect masking state is ended, + maskingPlaybackStates[0] = player.getPlaybackState(); + } + }) + .waitForPlaybackState(Player.STATE_ENDED) + .setMediaSources(/* windowIndex= */ 0, /* positionMs= */ C.TIME_UNSET, firstMediaSource) + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set empty media item with an explicit seek. + player.setMediaSource( + new ConcatenatingMediaSource(), /* startPositionMs= */ C.TIME_UNSET); + // Expect masking state is ended, + maskingPlaybackStates[1] = player.getPlaybackState(); + } + }) + .waitForPlaybackState(Player.STATE_ENDED) + .setMediaSources(/* windowIndex= */ 0, /* positionMs= */ C.TIME_UNSET, firstMediaSource) + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set media item with an explicit seek. + player.setMediaSource(secondMediaSource, /* startPositionMs= */ C.TIME_UNSET); + // Expect masking state is buffering, + maskingPlaybackStates[2] = player.getPlaybackState(); + } + }) + .waitForPlaybackState(Player.STATE_READY) + .setMediaSources(/* windowIndex= */ 0, /* positionMs= */ C.TIME_UNSET, firstMediaSource) + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set media item with an implicit seek to the current position. + player.setMediaSource(secondMediaSource, /* resetPosition= */ false); + // Expect masking state is buffering, + maskingPlaybackStates[3] = player.getPlaybackState(); + } + }) + .play() + .waitForPlaybackState(Player.STATE_READY) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .setExpectedPlayerEndedCount(/* expectedPlayerEndedCount= */ 3) + .setMediaSources(firstMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + // Expect reset of masking to first window. + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_IDLE, + Player.STATE_IDLE, // Pause. + Player.STATE_BUFFERING, + Player.STATE_READY, // Ready after initial prepare. + Player.STATE_ENDED, // Ended after setting empty source without seek. + Player.STATE_BUFFERING, + Player.STATE_READY, // Ready again after re-setting source. + Player.STATE_ENDED, // Ended after setting empty source with seek. + Player.STATE_BUFFERING, + Player.STATE_READY, // Ready again after re-setting source. + Player.STATE_BUFFERING, + Player.STATE_READY, // Ready after setting media item with seek. + Player.STATE_BUFFERING, + Player.STATE_READY, // Ready again after re-setting source. + Player.STATE_BUFFERING, + Player.STATE_BUFFERING, // Play. + Player.STATE_READY, // Ready after setting media item without seek. + Player.STATE_ENDED); + assertArrayEquals( + new int[] { + Player.STATE_ENDED, Player.STATE_ENDED, Player.STATE_BUFFERING, Player.STATE_BUFFERING + }, + maskingPlaybackStates); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, // Initial source. + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, // Empty source. + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, // Reset source. + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, // Empty source. + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, // Reset source. + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, // Set source with seek. + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, // Reset source. + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); // Set source without seek. + } + + @Test + public void testSetMediaSources_whenPrepared_invalidSeek_correctMaskingPlaybackState() + throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1, 1L); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + final int[] maskingPlaybackStates = new int[1]; + Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder( + "testSetMediaSources_whenPrepared_invalidSeek_correctMaskingPlaybackState") + .pause() + .waitForPlaybackState(Player.STATE_ENDED) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // An implicit, invalid seek picking up the position set by the initial seek. + player.setMediaSource(firstMediaSource, /* resetPosition= */ false); + // Expect masking state is ended, + maskingPlaybackStates[0] = player.getPlaybackState(); + } + }) + .waitForTimelineChanged() + .setMediaSources(/* windowIndex= */ 0, /* positionMs= */ C.TIME_UNSET, firstMediaSource) + .waitForPlaybackState(Player.STATE_READY) + .play() + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .setExpectedPlayerEndedCount(/* expectedPlayerEndedCount= */ 2) + .initialSeek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) + .setMediaSources(new ConcatenatingMediaSource()) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + // Expect reset of masking to first window. + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_IDLE, + Player.STATE_IDLE, // Pause. + Player.STATE_ENDED, // Empty source has been prepared. + Player.STATE_BUFFERING, // After setting another source. + Player.STATE_READY, + Player.STATE_READY, // Play. + Player.STATE_ENDED); + assertArrayEquals(new int[] {Player.STATE_ENDED}, maskingPlaybackStates); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + } + + @Test + public void testAddMediaSources_skipSettingMediaItems_validInitialSeek_correctMaskingWindowIndex() + throws Exception { + final int[] currentWindowIndices = new int[5]; + Arrays.fill(currentWindowIndices, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder( + "testAddMediaSources_skipSettingMediaItems_validInitialSeek_correctMaskingWindowIndex") + .waitForSeekProcessed() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + player.addMediaSource(/* index= */ 0, new ConcatenatingMediaSource()); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + player.addMediaSource( + /* index= */ 0, + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2))); + currentWindowIndices[2] = player.getCurrentWindowIndex(); + player.addMediaSource( + /* index= */ 0, + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1))); + currentWindowIndices[3] = player.getCurrentWindowIndex(); + } + }) + .prepare() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[4] = player.getCurrentWindowIndex(); + } + }) + .build(); + new Builder() + .skipSettingMediaSources() + .initialSeek(/* windowIndex= */ 1, C.TIME_UNSET) + .setActionSchedule(actionSchedule) + .build(context) + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {1, 1, 1, 2, 2}, currentWindowIndices); + } + + @Test + public void + testAddMediaSources_skipSettingMediaItems_invalidInitialSeek_correctMaskingWindowIndex() + throws Exception { + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder( + "testAddMediaSources_skipSettingMediaItems_invalidInitialSeek_correctMaskingWindowIndex") + .waitForSeekProcessed() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + player.addMediaSource(secondMediaSource); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + } + }) + .prepare() + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[2] = player.getCurrentWindowIndex(); + } + }) + .build(); + new Builder() + .skipSettingMediaSources() + .initialSeek(/* windowIndex= */ 1, C.TIME_UNSET) + .setActionSchedule(actionSchedule) + .build(context) + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {1, 0, 0}, currentWindowIndices); + } + + @Test + public void testMoveMediaItems_correctMaskingWindowIndex() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource firstMediaSource = new FakeMediaSource(timeline); + MediaSource secondMediaSource = new FakeMediaSource(timeline); + MediaSource thirdMediaSource = new FakeMediaSource(timeline); + final int[] currentWindowIndices = { + C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testMoveMediaItems_correctMaskingWindowIndex") + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Move the current item down in the playlist. + player.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 2, /* newIndex= */ 1); + currentWindowIndices[0] = player.getCurrentWindowIndex(); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Move the current item up in the playlist. + player.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3, /* newIndex= */ 0); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + } + }) + .seek(/* windowIndex= */ 2, C.TIME_UNSET) + .waitForSeekProcessed() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Move items from before to behind the current item. + player.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 2, /* newIndex= */ 1); + currentWindowIndices[2] = player.getCurrentWindowIndex(); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Move items from behind to before the current item. + player.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3, /* newIndex= */ 0); + currentWindowIndices[3] = player.getCurrentWindowIndex(); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Move items from before to before the current item. + // No change in currentWindowIndex. + player.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 1, /* newIndex= */ 1); + currentWindowIndices[4] = player.getCurrentWindowIndex(); + } + }) + .seek(/* windowIndex= */ 0, C.TIME_UNSET) + .waitForSeekProcessed() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Move items from behind to behind the current item. + // No change in currentWindowIndex. + player.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 2, /* newIndex= */ 2); + currentWindowIndices[5] = player.getCurrentWindowIndex(); + } + }) + .build(); + new Builder() + .setMediaSources(firstMediaSource, secondMediaSource, thirdMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {1, 0, 0, 2, 2, 0}, currentWindowIndices); + } + + @Test + public void testMoveMediaItems_unprepared_correctMaskingWindowIndex() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testMoveMediaItems_unprepared_correctMaskingWindowIndex") + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Increase current window index. + currentWindowIndices[0] = player.getCurrentWindowIndex(); + player.moveMediaItem(/* currentIndex= */ 0, /* newIndex= */ 1); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + } + }) + .prepare() + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[2] = player.getCurrentWindowIndex(); + } + }) + .build(); + new Builder() + .setMediaSources(firstMediaSource, secondMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {0, 1, 1}, currentWindowIndices); + } + + @Test + public void testRemoveMediaItems_correctMaskingWindowIndex() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testRemoveMediaItems_correctMaskingWindowIndex") + .waitForSeekProcessed() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Decrease current window index. + currentWindowIndices[0] = player.getCurrentWindowIndex(); + player.removeMediaItem(/* index= */ 0); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + } + }) + .build(); + new Builder() + .initialSeek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) + .setMediaSources(firstMediaSource, secondMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {1, 0}, currentWindowIndices); + } + + @Test + public void testRemoveMediaItems_currentItemRemoved_correctMaskingWindowIndex() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] currentPositions = {C.TIME_UNSET, C.TIME_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder( + "testRemoveMediaItems_CurrentItemRemoved_correctMaskingWindowIndex") + .waitForSeekProcessed() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Remove the current item. + currentWindowIndices[0] = player.getCurrentWindowIndex(); + currentPositions[0] = player.getCurrentPosition(); + player.removeMediaItem(/* index= */ 1); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + currentPositions[1] = player.getCurrentPosition(); + } + }) + .build(); + new Builder() + .initialSeek(/* windowIndex= */ 1, /* positionMs= */ 5000) + .setMediaSources(firstMediaSource, secondMediaSource, firstMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {1, 1}, currentWindowIndices); + assertThat(currentPositions[0]).isAtLeast(5000L); + assertThat(currentPositions[1]).isEqualTo(0); + } + + @Test + public void testRemoveMediaItems_currentItemRemovedThatIsTheLast_correctMasking() + throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1, 1L); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1, 2L); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + Timeline thirdTimeline = new FakeTimeline(/* windowCount= */ 1, 3L); + MediaSource thirdMediaSource = new FakeMediaSource(thirdTimeline); + Timeline fourthTimeline = new FakeTimeline(/* windowCount= */ 1, 3L); + MediaSource fourthMediaSource = new FakeMediaSource(fourthTimeline); + final int[] currentWindowIndices = new int[9]; + Arrays.fill(currentWindowIndices, C.INDEX_UNSET); + final int[] maskingPlaybackStates = new int[4]; + Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder( + "testRemoveMediaItems_CurrentItemRemovedThatIsTheLast_correctMaskingWindowIndex") + .waitForSeekProcessed() + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Expect the current window index to be 2 after seek. + currentWindowIndices[0] = player.getCurrentWindowIndex(); + player.removeMediaItem(/* index= */ 2); + // Expect the current window index to be 0 + // (default position of timeline after not finding subsequent period). + currentWindowIndices[1] = player.getCurrentWindowIndex(); + // Transition to ENDED. + maskingPlaybackStates[0] = player.getPlaybackState(); + } + }) + .waitForPlaybackState(Player.STATE_ENDED) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Expects the current window index still on 0. + currentWindowIndices[2] = player.getCurrentWindowIndex(); + // Insert an item at begin when the playlist is not empty. + player.addMediaSource(/* index= */ 0, thirdMediaSource); + // Expects the current window index to be (0 + 1) after insertion at begin. + currentWindowIndices[3] = player.getCurrentWindowIndex(); + // Remains in ENDED. + maskingPlaybackStates[1] = player.getPlaybackState(); + } + }) + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[4] = player.getCurrentWindowIndex(); + // Implicit seek to the current window index, which is out of bounds in new + // timeline. + player.setMediaSource(fourthMediaSource, /* resetPosition= */ false); + // 0 after reset. + currentWindowIndices[5] = player.getCurrentWindowIndex(); + // Invalid seek, so we remain in ENDED. + maskingPlaybackStates[2] = player.getPlaybackState(); + } + }) + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[6] = player.getCurrentWindowIndex(); + // Explicit seek to (0, C.TIME_UNSET). Player transitions to BUFFERING. + player.setMediaSource(fourthMediaSource, /* startPositionMs= */ 5000); + // 0 after explicit seek. + currentWindowIndices[7] = player.getCurrentWindowIndex(); + // Transitions from ENDED to BUFFERING after explicit seek. + maskingPlaybackStates[3] = player.getPlaybackState(); + } + }) + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Check whether actual window index is equal masking index from above. + currentWindowIndices[8] = player.getCurrentWindowIndex(); + } + }) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .initialSeek(/* windowIndex= */ 2, /* positionMs= */ C.TIME_UNSET) + .setExpectedPlayerEndedCount(2) + .setMediaSources(firstMediaSource, secondMediaSource, thirdMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + // Expect reset of masking to first window. + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_IDLE, + Player.STATE_BUFFERING, + Player.STATE_READY, // Ready after initial prepare. + Player.STATE_ENDED, // ended after removing current window index + Player.STATE_BUFFERING, // buffers after set items with seek + Player.STATE_READY, + Player.STATE_ENDED); + assertArrayEquals( + new int[] { + Player.STATE_ENDED, // ended after removing current window index + Player.STATE_ENDED, // adding items does not change state + Player.STATE_ENDED, // set items with seek to current position. + Player.STATE_BUFFERING + }, // buffers after set items with seek + maskingPlaybackStates); + assertArrayEquals(new int[] {2, 0, 0, 1, 1, 0, 0, 0, 0}, currentWindowIndices); + } + + @Test + public void testRemoveMediaItems_removeTailWithCurrentWindow_whenIdle_finishesPlayback() + throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1, new Object()); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + ActionSchedule actionSchedule = + new ActionSchedule.Builder( + "testRemoveMediaItems_removeTailWithCurrentWindow_whenIdle_finishesPlayback") + .seek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) + .waitForSeekProcessed() + .removeMediaItem(/* index= */ 1) + .prepare() + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .setMediaSources(firstMediaSource, secondMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_IDLE, Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED); + } + + @Test + public void testClearMediaItems_correctMasking() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET}; + final int[] maskingPlaybackState = {C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testClearMediaItems_correctMaskingWindowIndex") + .waitForSeekProcessed() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + player.clearMediaItems(); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + maskingPlaybackState[0] = player.getPlaybackState(); + } + }) + .build(); + new Builder() + .initialSeek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) + .setMediaSources(firstMediaSource, secondMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {1, 0}, currentWindowIndices); + assertArrayEquals(new int[] {Player.STATE_ENDED}, maskingPlaybackState); + } + + @Test + public void testClearMediaItems_unprepared_correctMaskingWindowIndex_notEnded() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET}; + final int[] currentStates = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testClearMediaItems_correctMaskingWindowIndex") + .waitForSeekProcessed() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + currentStates[0] = player.getPlaybackState(); + player.clearMediaItems(); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + currentStates[1] = player.getPlaybackState(); + } + }) + .prepare() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Transitions to ended when prepared with zero media items. + currentStates[2] = player.getPlaybackState(); + } + }) + .build(); + new Builder() + .initialSeek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) + .setMediaSources(firstMediaSource, secondMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals( + new int[] {Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_ENDED}, currentStates); + assertArrayEquals(new int[] {1, 0}, currentWindowIndices); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { @@ -3287,4 +5659,38 @@ public final class ExoPlayerTest { timeline = player.getCurrentTimeline(); } } + /** + * Provides a wrapper for a {@link Runnable} which does collect playback states and window counts. + * Can be used with {@link ActionSchedule.Builder#executeRunnable(Runnable)} to verify that a + * playback state did not change and hence no observable callback is called. + * + *

    This is specifically useful in cases when the test may end before a given state arrives or + * when an action of the action schedule might execute before a callback is called. + */ + public static class PlaybackStateCollector extends PlayerRunnable { + + private final int[] playbackStates; + private final int[] timelineWindowCount; + private final int index; + + /** + * Creates the collector. + * + * @param index The index to populate. + * @param playbackStates An array of playback states to populate. + * @param timelineWindowCount An array of window counts to populate. + */ + public PlaybackStateCollector(int index, int[] playbackStates, int[] timelineWindowCount) { + Assertions.checkArgument(playbackStates.length > index && timelineWindowCount.length > index); + this.playbackStates = playbackStates; + this.timelineWindowCount = timelineWindowCount; + this.index = index; + } + + @Override + public void run(SimpleExoPlayer player) { + playbackStates[index] = player.getPlaybackState(); + timelineWindowCount[index] = player.getCurrentTimeline().getWindowCount(); + } + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java index 56726e3914..904702a9d5 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java @@ -21,15 +21,17 @@ import static org.mockito.Mockito.mock; import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline; +import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.upstream.Allocator; +import java.util.Collections; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -50,19 +52,20 @@ public final class MediaPeriodQueueTest { private MediaPeriodQueue mediaPeriodQueue; private AdPlaybackState adPlaybackState; - private Timeline timeline; private Object periodUid; private PlaybackInfo playbackInfo; private RendererCapabilities[] rendererCapabilities; private TrackSelector trackSelector; private Allocator allocator; - private MediaSource mediaSource; + private Playlist playlist; + private FakeMediaSource fakeMediaSource; + private Playlist.MediaSourceHolder mediaSourceHolder; @Before public void setUp() { mediaPeriodQueue = new MediaPeriodQueue(); - mediaSource = mock(MediaSource.class); + playlist = mock(Playlist.class); rendererCapabilities = new RendererCapabilities[0]; trackSelector = mock(TrackSelector.class); allocator = mock(Allocator.class); @@ -70,7 +73,7 @@ public final class MediaPeriodQueueTest { @Test public void getNextMediaPeriodInfo_withoutAds_returnsLastMediaPeriodInfo() { - setupTimeline(/* initialPositionUs= */ 0); + setupTimeline(); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* startPositionUs= */ 0, /* endPositionUs= */ C.TIME_UNSET, @@ -81,7 +84,7 @@ public final class MediaPeriodQueueTest { @Test public void getNextMediaPeriodInfo_withPrerollAd_returnsCorrectMediaPeriodInfos() { - setupTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs...= */ 0); + setupTimeline(/* adGroupTimesUs...= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0); assertNextMediaPeriodInfoIsAd(/* adGroupIndex= */ 0, /* contentPositionUs= */ 0); advance(); @@ -95,10 +98,7 @@ public final class MediaPeriodQueueTest { @Test public void getNextMediaPeriodInfo_withMidrollAds_returnsCorrectMediaPeriodInfos() { - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, - SECOND_AD_START_TIME_US); + setupTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* startPositionUs= */ 0, /* endPositionUs= */ FIRST_AD_START_TIME_US, @@ -133,10 +133,7 @@ public final class MediaPeriodQueueTest { @Test public void getNextMediaPeriodInfo_withMidrollAndPostroll_returnsCorrectMediaPeriodInfos() { - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, - C.TIME_END_OF_SOURCE); + setupTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, C.TIME_END_OF_SOURCE); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* startPositionUs= */ 0, /* endPositionUs= */ FIRST_AD_START_TIME_US, @@ -169,7 +166,7 @@ public final class MediaPeriodQueueTest { @Test public void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaPeriodInfo() { - setupTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE); + setupTimeline(/* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* startPositionUs= */ 0, /* endPositionUs= */ C.TIME_END_OF_SOURCE, @@ -189,10 +186,7 @@ public final class MediaPeriodQueueTest { @Test public void updateQueuedPeriods_withDurationChangeAfterReadingPeriod_handlesChangeAndRemovesPeriodsAfterChangedPeriod() { - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, - SECOND_AD_START_TIME_US); + setupTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); enqueueNext(); // Content before first ad. @@ -202,10 +196,8 @@ public final class MediaPeriodQueueTest { enqueueNext(); // Second ad. // Change position of second ad (= change duration of content between ads). - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, - SECOND_AD_START_TIME_US + 1); + updateAdPlaybackStateAndTimeline( + /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US + 1); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); boolean changeHandled = @@ -219,10 +211,7 @@ public final class MediaPeriodQueueTest { @Test public void updateQueuedPeriods_withDurationChangeBeforeReadingPeriod_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, - SECOND_AD_START_TIME_US); + setupTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); enqueueNext(); // Content before first ad. @@ -233,10 +222,8 @@ public final class MediaPeriodQueueTest { advanceReading(); // Reading first ad. // Change position of first ad (= change duration of content before first ad). - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US + 1, - SECOND_AD_START_TIME_US); + updateAdPlaybackStateAndTimeline( + /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US + 1, SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); boolean changeHandled = @@ -250,10 +237,7 @@ public final class MediaPeriodQueueTest { @Test public void updateQueuedPeriods_withDurationChangeInReadingPeriodAfterReadingPosition_handlesChangeAndRemovesPeriodsAfterChangedPeriod() { - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, - SECOND_AD_START_TIME_US); + setupTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); enqueueNext(); // Content before first ad. @@ -265,10 +249,8 @@ public final class MediaPeriodQueueTest { advanceReading(); // Reading content between ads. // Change position of second ad (= change duration of content between ads). - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, - SECOND_AD_START_TIME_US - 1000); + updateAdPlaybackStateAndTimeline( + /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); long readingPositionAtStartOfContentBetweenAds = FIRST_AD_START_TIME_US + AD_DURATION_US; @@ -284,10 +266,7 @@ public final class MediaPeriodQueueTest { @Test public void updateQueuedPeriods_withDurationChangeInReadingPeriodBeforeReadingPosition_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, - SECOND_AD_START_TIME_US); + setupTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); enqueueNext(); // Content before first ad. @@ -299,10 +278,8 @@ public final class MediaPeriodQueueTest { advanceReading(); // Reading content between ads. // Change position of second ad (= change duration of content between ads). - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, - SECOND_AD_START_TIME_US - 1000); + updateAdPlaybackStateAndTimeline( + /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); long readingPositionAtEndOfContentBetweenAds = SECOND_AD_START_TIME_US + AD_DURATION_US; @@ -318,10 +295,7 @@ public final class MediaPeriodQueueTest { @Test public void updateQueuedPeriods_withDurationChangeInReadingPeriodReadToEnd_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, - SECOND_AD_START_TIME_US); + setupTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); enqueueNext(); // Content before first ad. @@ -333,10 +307,8 @@ public final class MediaPeriodQueueTest { advanceReading(); // Reading content between ads. // Change position of second ad (= change duration of content between ads). - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, - SECOND_AD_START_TIME_US - 1000); + updateAdPlaybackStateAndTimeline( + /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); boolean changeHandled = @@ -347,16 +319,25 @@ public final class MediaPeriodQueueTest { assertThat(getQueueLength()).isEqualTo(3); } - private void setupTimeline(long initialPositionUs, long... adGroupTimesUs) { + private void setupTimeline(long... adGroupTimesUs) { adPlaybackState = new AdPlaybackState(adGroupTimesUs).withContentDurationUs(CONTENT_DURATION_US); - timeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); + + // Create a media source holder. + SinglePeriodAdTimeline adTimeline = + new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); + fakeMediaSource = new FakeMediaSource(adTimeline); + mediaSourceHolder = new Playlist.MediaSourceHolder(fakeMediaSource, false); + mediaSourceHolder.mediaSource.prepareSourceInternal(/* mediaTransferListener */ null); + + Timeline timeline = createPlaylistTimeline(); periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0); mediaPeriodQueue.setTimeline(timeline); + playbackInfo = new PlaybackInfo( timeline, - mediaPeriodQueue.resolveMediaPeriodIdForAds(periodUid, initialPositionUs), + mediaPeriodQueue.resolveMediaPeriodIdForAds(periodUid, /* positionUs= */ 0), /* startPositionUs= */ 0, /* contentPositionUs= */ 0, Player.STATE_READY, @@ -370,6 +351,25 @@ public final class MediaPeriodQueueTest { /* positionUs= */ 0); } + private void updateAdPlaybackStateAndTimeline(long... adGroupTimesUs) { + adPlaybackState = + new AdPlaybackState(adGroupTimesUs).withContentDurationUs(CONTENT_DURATION_US); + updateTimeline(); + } + + private void updateTimeline() { + SinglePeriodAdTimeline adTimeline = + new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); + fakeMediaSource.setNewSourceInfo(adTimeline, /* manifest */ null); + mediaPeriodQueue.setTimeline(createPlaylistTimeline()); + } + + private Playlist.PlaylistTimeline createPlaylistTimeline() { + return new Playlist.PlaylistTimeline( + Collections.singleton(mediaSourceHolder), + new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1)); + } + private void advance() { enqueueNext(); if (mediaPeriodQueue.getLoadingPeriod() != mediaPeriodQueue.getPlayingPeriod()) { @@ -390,7 +390,7 @@ public final class MediaPeriodQueueTest { rendererCapabilities, trackSelector, allocator, - mediaSource, + playlist, getNextMediaPeriodInfo(), new TrackSelectorResult( new RendererConfiguration[0], new TrackSelection[0], /* info= */ null)); @@ -422,11 +422,6 @@ public final class MediaPeriodQueueTest { updateTimeline(); } - private void updateTimeline() { - timeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); - mediaPeriodQueue.setTimeline(timeline); - } - private void assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( long startPositionUs, long endPositionUs, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index 2e26529a81..b34a7fb899 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -44,7 +44,6 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.ActionSchedule; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; -import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeTimeline; @@ -133,24 +132,29 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady */, WINDOW_0 /* BUFFERING */, WINDOW_0 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* DYNAMIC */); listener.assertNoMoreEvents(); } @Test public void testSinglePeriod() throws Exception { FakeMediaSource mediaSource = - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + new FakeMediaSource( + SINGLE_PERIOD_TIMELINE, + ExoPlayerTestRunner.Builder.VIDEO_FORMAT, + ExoPlayerTestRunner.Builder.AUDIO_FORMAT); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); - populateEventIds(SINGLE_PERIOD_TIMELINE); + populateEventIds(listener.lastReportedTimeline); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady */, WINDOW_0 /* BUFFERING */, period0 /* READY */, period0 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* DYNAMIC */); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0 /* started */, period0 /* stopped */); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0); @@ -179,9 +183,14 @@ public final class AnalyticsCollectorTest { public void testAutomaticPeriodTransition() throws Exception { MediaSource mediaSource = new ConcatenatingMediaSource( - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT), new FakeMediaSource( - SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT)); + SINGLE_PERIOD_TIMELINE, + ExoPlayerTestRunner.Builder.VIDEO_FORMAT, + ExoPlayerTestRunner.Builder.AUDIO_FORMAT), + new FakeMediaSource( + SINGLE_PERIOD_TIMELINE, + ExoPlayerTestRunner.Builder.VIDEO_FORMAT, + ExoPlayerTestRunner.Builder.AUDIO_FORMAT)); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); populateEventIds(listener.lastReportedTimeline); @@ -191,7 +200,8 @@ public final class AnalyticsCollectorTest { WINDOW_0 /* BUFFERING */, period0 /* READY */, period1 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0, period0, period0, period0); @@ -233,8 +243,8 @@ public final class AnalyticsCollectorTest { public void testPeriodTransitionWithRendererChange() throws Exception { MediaSource mediaSource = new ConcatenatingMediaSource( - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT), - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.AUDIO_FORMAT)); + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT), + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.AUDIO_FORMAT)); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); populateEventIds(listener.lastReportedTimeline); @@ -246,7 +256,8 @@ public final class AnalyticsCollectorTest { period1 /* BUFFERING */, period1 /* READY */, period1 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0, period0, period0, period0); @@ -286,8 +297,8 @@ public final class AnalyticsCollectorTest { public void testSeekToOtherPeriod() throws Exception { MediaSource mediaSource = new ConcatenatingMediaSource( - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT), - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.AUDIO_FORMAT)); + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT), + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.AUDIO_FORMAT)); ActionSchedule actionSchedule = new ActionSchedule.Builder("AnalyticsCollectorTest") .pause() @@ -308,7 +319,8 @@ public final class AnalyticsCollectorTest { period1 /* READY */, period1 /* setPlayWhenReady=true */, period1 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period1); @@ -350,9 +362,11 @@ public final class AnalyticsCollectorTest { public void testSeekBackAfterReadingAhead() throws Exception { MediaSource mediaSource = new ConcatenatingMediaSource( - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT), + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT), new FakeMediaSource( - SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT)); + SINGLE_PERIOD_TIMELINE, + ExoPlayerTestRunner.Builder.VIDEO_FORMAT, + ExoPlayerTestRunner.Builder.AUDIO_FORMAT)); long periodDurationMs = SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs(); ActionSchedule actionSchedule = @@ -380,7 +394,8 @@ public final class AnalyticsCollectorTest { period1Seq2 /* BUFFERING */, period1Seq2 /* READY */, period1Seq2 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)) .containsExactly(period0, period1Seq2); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0); @@ -428,18 +443,28 @@ public final class AnalyticsCollectorTest { @Test public void testPrepareNewSource() throws Exception { - MediaSource mediaSource1 = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); - MediaSource mediaSource2 = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); + MediaSource mediaSource1 = + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT); + MediaSource mediaSource2 = + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("AnalyticsCollectorTest") .pause() .waitForPlaybackState(Player.STATE_READY) - .prepareSource(mediaSource2) + .setMediaSources(/* resetPosition= */ false, mediaSource2) .play() .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource1, actionSchedule); - populateEventIds(SINGLE_PERIOD_TIMELINE); + // Populate all event ids with last timeline (after second prepare). + populateEventIds(listener.lastReportedTimeline); + // Populate event id of period 0, sequence 0 with timeline of initial preparation. + period0Seq0 = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId( + listener.reportedTimelines.get(1).getUidOfPeriod(/* periodIndex= */ 0), + /* windowSequenceNumber= */ 0)); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady=true */, @@ -451,12 +476,16 @@ public final class AnalyticsCollectorTest { period0Seq1 /* READY */, period0Seq1 /* ENDED */); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* prepared */, WINDOW_0 /* reset */, WINDOW_0 /* prepared */); + .containsExactly( + WINDOW_0 /* PLAYLIST_CHANGE */, + WINDOW_0 /* DYNAMIC */, + WINDOW_0 /* PLAYLIST_CHANGE */, + WINDOW_0 /* DYNAMIC */); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0Seq0, period0Seq0, period0Seq1, period0Seq1); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) .containsExactly( - period0Seq0 /* prepared */, WINDOW_0 /* reset */, period0Seq1 /* prepared */); + period0Seq0 /* prepared */, WINDOW_0 /* setMediaSources */, period0Seq1 /* prepared */); assertThat(listener.getEvents(EVENT_LOAD_STARTED)) .containsExactly( WINDOW_0 /* manifest */, @@ -490,19 +519,20 @@ public final class AnalyticsCollectorTest { @Test public void testReprepareAfterError() throws Exception { - MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); + MediaSource mediaSource = + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("AnalyticsCollectorTest") .waitForPlaybackState(Player.STATE_READY) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) .seek(/* positionMs= */ 0) - .prepareSource(mediaSource, /* resetPosition= */ false, /* resetState= */ false) + .prepare() .waitForPlaybackState(Player.STATE_ENDED) .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); - populateEventIds(SINGLE_PERIOD_TIMELINE); + populateEventIds(listener.lastReportedTimeline); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady=true */, @@ -556,7 +586,7 @@ public final class AnalyticsCollectorTest { @Test public void testDynamicTimelineChange() throws Exception { MediaSource childMediaSource = - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT); final ConcatenatingMediaSource concatenatedMediaSource = new ConcatenatingMediaSource(childMediaSource, childMediaSource); long periodDurationMs = @@ -588,7 +618,11 @@ public final class AnalyticsCollectorTest { period1Seq0 /* setPlayWhenReady=true */, period1Seq0 /* BUFFERING */, period1Seq0 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0, period1Seq0); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly( + WINDOW_0 /* PLAYLIST_CHANGED */, + window0Period1Seq0 /* DYNAMIC (concatenated timeline replaces dummy) */, + period1Seq0 /* DYNAMIC (child sources in concatenating source moved) */); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly( window0Period1Seq0, window0Period1Seq0, window0Period1Seq0, window0Period1Seq0); @@ -642,7 +676,7 @@ public final class AnalyticsCollectorTest { .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); - populateEventIds(SINGLE_PERIOD_TIMELINE); + populateEventIds(listener.lastReportedTimeline); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period0); } @@ -709,7 +743,7 @@ public final class AnalyticsCollectorTest { TestAnalyticsListener listener = new TestAnalyticsListener(); try { new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) + .setMediaSources(mediaSource) .setRenderersFactory(renderersFactory) .setAnalyticsListener(listener) .setActionSchedule(actionSchedule) @@ -731,7 +765,7 @@ public final class AnalyticsCollectorTest { private boolean renderedFirstFrame; public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) { - super(Builder.VIDEO_FORMAT); + super(ExoPlayerTestRunner.Builder.VIDEO_FORMAT); eventDispatcher = new VideoRendererEventListener.EventDispatcher(handler, eventListener); decoderCounters = new DecoderCounters(); } @@ -789,7 +823,7 @@ public final class AnalyticsCollectorTest { private boolean notifiedAudioSessionId; public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) { - super(Builder.AUDIO_FORMAT); + super(ExoPlayerTestRunner.Builder.AUDIO_FORMAT); eventDispatcher = new AudioRendererEventListener.EventDispatcher(handler, eventListener); decoderCounters = new DecoderCounters(); } @@ -873,10 +907,12 @@ public final class AnalyticsCollectorTest { public Timeline lastReportedTimeline; + private final List reportedTimelines; private final ArrayList reportedEvents; public TestAnalyticsListener() { reportedEvents = new ArrayList<>(); + reportedTimelines = new ArrayList<>(); lastReportedTimeline = Timeline.EMPTY; } @@ -906,6 +942,7 @@ public final class AnalyticsCollectorTest { @Override public void onTimelineChanged(EventTime eventTime, int reason) { lastReportedTimeline = eventTime.timeline; + reportedTimelines.add(eventTime.timeline); reportedEvents.add(new ReportedEvent(EVENT_TIMELINE_CHANGED, eventTime)); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index 20b80ace52..08148493bb 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -21,6 +21,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.IllegalSeekPositionException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.PlayerMessage; @@ -29,6 +30,7 @@ import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.testutil.ActionSchedule.ActionNode; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; @@ -38,6 +40,8 @@ import com.google.android.exoplayer2.util.ConditionVariable; import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; +import java.util.Arrays; +import java.util.List; /** Base class for actions to perform during playback tests. */ public abstract class Action { @@ -114,6 +118,7 @@ public abstract class Action { private final Integer windowIndex; private final long positionMs; + private final boolean catchIllegalSeekException; /** * Action calls {@link Player#seekTo(long)}. @@ -125,6 +130,7 @@ public abstract class Action { super(tag, "Seek:" + positionMs); this.windowIndex = null; this.positionMs = positionMs; + catchIllegalSeekException = false; } /** @@ -133,24 +139,191 @@ public abstract class Action { * @param tag A tag to use for logging. * @param windowIndex The window to seek to. * @param positionMs The seek position. + * @param catchIllegalSeekException Whether {@link IllegalSeekPositionException} should be + * silently caught or not. */ - public Seek(String tag, int windowIndex, long positionMs) { + public Seek(String tag, int windowIndex, long positionMs, boolean catchIllegalSeekException) { super(tag, "Seek:" + positionMs); this.windowIndex = windowIndex; this.positionMs = positionMs; + this.catchIllegalSeekException = catchIllegalSeekException; } @Override protected void doActionImpl( SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - if (windowIndex == null) { - player.seekTo(positionMs); - } else { - player.seekTo(windowIndex, positionMs); + try { + if (windowIndex == null) { + player.seekTo(positionMs); + } else { + player.seekTo(windowIndex, positionMs); + } + } catch (IllegalSeekPositionException e) { + if (!catchIllegalSeekException) { + throw e; + } } } } + /** Calls {@link SimpleExoPlayer#setMediaSources(List, int, long)}. */ + public static final class SetMediaItems extends Action { + + private final int windowIndex; + private final long positionMs; + private final MediaSource[] mediaSources; + + /** + * @param tag A tag to use for logging. + * @param windowIndex The window index to start playback from. + * @param positionMs The position in milliseconds to start playback from. + * @param mediaSources The media sources to populate the playlist with. + */ + public SetMediaItems( + String tag, int windowIndex, long positionMs, MediaSource... mediaSources) { + super(tag, "SetMediaItems"); + this.windowIndex = windowIndex; + this.positionMs = positionMs; + this.mediaSources = mediaSources; + } + + @Override + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { + player.setMediaSources(Arrays.asList(mediaSources), windowIndex, positionMs); + } + } + + /** Calls {@link SimpleExoPlayer#addMediaSources(List)}. */ + public static final class AddMediaItems extends Action { + + private final MediaSource[] mediaSources; + + /** + * @param tag A tag to use for logging. + * @param mediaSources The media sources to be added to the playlist. + */ + public AddMediaItems(String tag, MediaSource... mediaSources) { + super(tag, /* description= */ "AddMediaItems"); + this.mediaSources = mediaSources; + } + + @Override + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { + player.addMediaSources(Arrays.asList(mediaSources)); + } + } + + /** Calls {@link SimpleExoPlayer#setMediaSources(List, boolean)}. */ + public static final class SetMediaItemsResetPosition extends Action { + + private final boolean resetPosition; + private final MediaSource[] mediaSources; + + /** + * @param tag A tag to use for logging. + * @param resetPosition Whether the position should be reset. + * @param mediaSources The media sources to populate the playlist with. + */ + public SetMediaItemsResetPosition( + String tag, boolean resetPosition, MediaSource... mediaSources) { + super(tag, "SetMediaItems"); + this.resetPosition = resetPosition; + this.mediaSources = mediaSources; + } + + @Override + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { + player.setMediaSources(Arrays.asList(mediaSources), resetPosition); + } + } + + /** Calls {@link SimpleExoPlayer#moveMediaItem(int, int)}. */ + public static class MoveMediaItem extends Action { + + private final int currentIndex; + private final int newIndex; + + /** + * @param tag A tag to use for logging. + * @param currentIndex The current index of the media item. + * @param newIndex The new index of the media item. + */ + public MoveMediaItem(String tag, int currentIndex, int newIndex) { + super(tag, "MoveMediaItem"); + this.currentIndex = currentIndex; + this.newIndex = newIndex; + } + + @Override + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { + player.moveMediaItem(currentIndex, newIndex); + } + } + + /** Calls {@link SimpleExoPlayer#removeMediaItem(int)}. */ + public static class RemoveMediaItem extends Action { + + private final int index; + + /** + * @param tag A tag to use for logging. + * @param index The index of the item to remove. + */ + public RemoveMediaItem(String tag, int index) { + super(tag, "RemoveMediaItem"); + this.index = index; + } + + @Override + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { + player.removeMediaItem(index); + } + } + + /** Calls {@link SimpleExoPlayer#removeMediaItems(int, int)}. */ + public static class RemoveMediaItems extends Action { + + private final int fromIndex; + private final int toIndex; + + /** + * @param tag A tag to use for logging. + * @param fromIndex The start if the range of media items to remove. + * @param toIndex The end of the range of media items to remove (exclusive). + */ + public RemoveMediaItems(String tag, int fromIndex, int toIndex) { + super(tag, "RemoveMediaItem"); + this.fromIndex = fromIndex; + this.toIndex = toIndex; + } + + @Override + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { + player.removeMediaItems(fromIndex, toIndex); + } + } + + /** Calls {@link SimpleExoPlayer#clearMediaItems()}}. */ + public static class ClearMediaItems extends Action { + + /** @param tag A tag to use for logging. */ + public ClearMediaItems(String tag) { + super(tag, "ClearMediaItems"); + } + + @Override + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { + player.clearMediaItems(); + } + } + /** Calls {@link Player#stop()} or {@link Player#stop(boolean)}. */ public static final class Stop extends Action { @@ -209,7 +382,6 @@ public abstract class Action { SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { player.setPlayWhenReady(playWhenReady); } - } /** @@ -295,52 +467,31 @@ public abstract class Action { } } - /** Calls {@link ExoPlayer#prepare(MediaSource)}. */ - public static final class PrepareSource extends Action { - - private final MediaSource mediaSource; - private final boolean resetPosition; - private final boolean resetState; - - /** - * @param tag A tag to use for logging. - * @param mediaSource The {@link MediaSource} to prepare the player with. - */ - public PrepareSource(String tag, MediaSource mediaSource) { - this(tag, mediaSource, true, true); - } - - /** - * @param tag A tag to use for logging. - * @param mediaSource The {@link MediaSource} to prepare the player with. - * @param resetPosition Whether the player's position should be reset. - */ - public PrepareSource( - String tag, MediaSource mediaSource, boolean resetPosition, boolean resetState) { - super(tag, "PrepareSource"); - this.mediaSource = mediaSource; - this.resetPosition = resetPosition; - this.resetState = resetState; + /** Calls {@link ExoPlayer#prepare()}. */ + public static final class Prepare extends Action { + /** @param tag A tag to use for logging. */ + public Prepare(String tag) { + super(tag, "Prepare"); } @Override protected void doActionImpl( SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - player.prepare(mediaSource, resetPosition, resetState); + player.prepare(); } } /** Calls {@link Player#setRepeatMode(int)}. */ public static final class SetRepeatMode extends Action { - private final @Player.RepeatMode int repeatMode; + @Player.RepeatMode private final int repeatMode; /** * @param tag A tag to use for logging. * @param repeatMode The repeat mode. */ public SetRepeatMode(String tag, @Player.RepeatMode int repeatMode) { - super(tag, "SetRepeatMode:" + repeatMode); + super(tag, "SetRepeatMode: " + repeatMode); this.repeatMode = repeatMode; } @@ -351,6 +502,27 @@ public abstract class Action { } } + /** Calls {@link ExoPlayer#setShuffleOrder(ShuffleOrder)} . */ + public static final class SetShuffleOrder extends Action { + + private final ShuffleOrder shuffleOrder; + + /** + * @param tag A tag to use for logging. + * @param shuffleOrder The shuffle order. + */ + public SetShuffleOrder(String tag, ShuffleOrder shuffleOrder) { + super(tag, "SetShufflerOrder"); + this.shuffleOrder = shuffleOrder; + } + + @Override + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { + player.setShuffleOrder(shuffleOrder); + } + } + /** Calls {@link Player#setShuffleModeEnabled(boolean)}. */ public static final class SetShuffleModeEnabled extends Action { @@ -361,7 +533,7 @@ public abstract class Action { * @param shuffleModeEnabled Whether shuffling is enabled. */ public SetShuffleModeEnabled(String tag, boolean shuffleModeEnabled) { - super(tag, "SetShuffleModeEnabled:" + shuffleModeEnabled); + super(tag, "SetShuffleModeEnabled: " + shuffleModeEnabled); this.shuffleModeEnabled = shuffleModeEnabled; } @@ -448,7 +620,6 @@ public abstract class Action { SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { player.setPlaybackParameters(playbackParameters); } - } /** Throws a playback exception on the playback thread. */ @@ -546,17 +717,34 @@ public abstract class Action { public static final class WaitForTimelineChanged extends Action { @Nullable private final Timeline expectedTimeline; + private final boolean ignoreExpectedReason; + @Player.TimelineChangeReason private final int expectedReason; /** - * Creates action waiting for a timeline change. + * Creates action waiting for a timeline change for a given reason. * * @param tag A tag to use for logging. - * @param expectedTimeline The expected timeline to wait for. If null, wait for any timeline - * change. + * @param expectedTimeline The expected timeline or null if any timeline change is relevant. + * @param expectedReason The expected timeline change reason. */ - public WaitForTimelineChanged(String tag, @Nullable Timeline expectedTimeline) { + public WaitForTimelineChanged( + String tag, Timeline expectedTimeline, @Player.TimelineChangeReason int expectedReason) { super(tag, "WaitForTimelineChanged"); - this.expectedTimeline = expectedTimeline; + this.expectedTimeline = expectedTimeline != null ? new NoUidTimeline(expectedTimeline) : null; + this.ignoreExpectedReason = false; + this.expectedReason = expectedReason; + } + + /** + * Creates action waiting for any timeline change for any reason. + * + * @param tag A tag to use for logging. + */ + public WaitForTimelineChanged(String tag) { + super(tag, "WaitForTimelineChanged"); + this.expectedTimeline = null; + this.ignoreExpectedReason = true; + this.expectedReason = Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED; } @Override @@ -574,14 +762,16 @@ public abstract class Action { @Override public void onTimelineChanged( Timeline timeline, @Player.TimelineChangeReason int reason) { - if (expectedTimeline == null || timeline.equals(expectedTimeline)) { + if ((expectedTimeline == null || new NoUidTimeline(timeline).equals(expectedTimeline)) + && (ignoreExpectedReason || expectedReason == reason)) { player.removeListener(this); nextAction.schedule(player, trackSelector, surface, handler); } } }; player.addListener(listener); - if (expectedTimeline != null && player.getCurrentTimeline().equals(expectedTimeline)) { + Timeline currentTimeline = new NoUidTimeline(player.getCurrentTimeline()); + if (currentTimeline.equals(expectedTimeline)) { player.removeListener(listener); nextAction.schedule(player, trackSelector, surface, handler); } @@ -731,6 +921,50 @@ public abstract class Action { } } + /** + * Waits for a player message to arrive. If the target already received a message, the action + * returns immediately. + */ + public static final class WaitForMessage extends Action { + + private final PlayerTarget playerTarget; + + /** + * @param tag A tag to use for logging. + * @param playerTarget The target to observe. + */ + public WaitForMessage(String tag, PlayerTarget playerTarget) { + super(tag, "WaitForMessage"); + this.playerTarget = playerTarget; + } + + @Override + protected void doActionAndScheduleNextImpl( + final SimpleExoPlayer player, + final DefaultTrackSelector trackSelector, + final Surface surface, + final HandlerWrapper handler, + final ActionNode nextAction) { + if (nextAction == null) { + return; + } + PlayerTarget.Callback callback = + new PlayerTarget.Callback() { + @Override + public void onMessageArrived() { + nextAction.schedule(player, trackSelector, surface, handler); + } + }; + playerTarget.setCallback(callback); + } + + @Override + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { + // Not triggered. + } + } + /** * Waits for a specified loading state, returning either immediately or after a call to {@link * Player.EventListener#onLoadingChanged(boolean)}. @@ -816,7 +1050,7 @@ public abstract class Action { } } - /** Calls {@link Runnable#run()}. */ + /** Calls {@code Runnable.run()}. */ public static final class ExecuteRunnable extends Action { private final Runnable runnable; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index f6ab4b9924..9a9cfd50a4 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -28,10 +28,10 @@ import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.testutil.Action.ClearVideoSurface; import com.google.android.exoplayer2.testutil.Action.ExecuteRunnable; import com.google.android.exoplayer2.testutil.Action.PlayUntilPosition; -import com.google.android.exoplayer2.testutil.Action.PrepareSource; import com.google.android.exoplayer2.testutil.Action.Seek; import com.google.android.exoplayer2.testutil.Action.SendMessages; import com.google.android.exoplayer2.testutil.Action.SetAudioAttributes; @@ -40,10 +40,12 @@ import com.google.android.exoplayer2.testutil.Action.SetPlaybackParameters; import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled; import com.google.android.exoplayer2.testutil.Action.SetRepeatMode; import com.google.android.exoplayer2.testutil.Action.SetShuffleModeEnabled; +import com.google.android.exoplayer2.testutil.Action.SetShuffleOrder; import com.google.android.exoplayer2.testutil.Action.SetVideoSurface; import com.google.android.exoplayer2.testutil.Action.Stop; import com.google.android.exoplayer2.testutil.Action.ThrowPlaybackException; import com.google.android.exoplayer2.testutil.Action.WaitForIsLoading; +import com.google.android.exoplayer2.testutil.Action.WaitForMessage; import com.google.android.exoplayer2.testutil.Action.WaitForPlayWhenReady; import com.google.android.exoplayer2.testutil.Action.WaitForPlaybackState; import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuity; @@ -172,7 +174,19 @@ public final class ActionSchedule { * @return The builder, for convenience. */ public Builder seek(int windowIndex, long positionMs) { - return apply(new Seek(tag, windowIndex, positionMs)); + return apply(new Seek(tag, windowIndex, positionMs, /* catchIllegalSeekException= */ false)); + } + + /** + * Schedules a seek action to be executed. + * + * @param windowIndex The window to seek to. + * @param positionMs The seek position. + * @param catchIllegalSeekException Whether an illegal seek position should be caught or not. + * @return The builder, for convenience. + */ + public Builder seek(int windowIndex, long positionMs, boolean catchIllegalSeekException) { + return apply(new Seek(tag, windowIndex, positionMs, catchIllegalSeekException)); } /** @@ -313,23 +327,99 @@ public final class ActionSchedule { } /** - * Schedules a new source preparation action. + * Schedules a set media items action to be executed. * + * @param windowIndex The window index to start playback from or {@link C#INDEX_UNSET} if the + * playback position should not be reset. + * @param positionMs The position in milliseconds from where playback should start. If {@link + * C#TIME_UNSET} is passed the default position is used. In any case, if {@code windowIndex} + * is set to {@link C#INDEX_UNSET} the position is not reset at all and this parameter is + * ignored. * @return The builder, for convenience. */ - public Builder prepareSource(MediaSource mediaSource) { - return apply(new PrepareSource(tag, mediaSource)); + public Builder setMediaSources(int windowIndex, long positionMs, MediaSource... sources) { + return apply(new Action.SetMediaItems(tag, windowIndex, positionMs, sources)); } /** - * Schedules a new source preparation action. + * Schedules a set media items action to be executed. * - * @see com.google.android.exoplayer2.ExoPlayer#prepare(MediaSource, boolean, boolean) + * @param resetPosition Whether the playback position should be reset. * @return The builder, for convenience. */ - public Builder prepareSource( - MediaSource mediaSource, boolean resetPosition, boolean resetState) { - return apply(new PrepareSource(tag, mediaSource, resetPosition, resetState)); + public Builder setMediaSources(boolean resetPosition, MediaSource... sources) { + return apply(new Action.SetMediaItemsResetPosition(tag, resetPosition, sources)); + } + + /** + * Schedules a set media items action to be executed. + * + * @param mediaSources The media sources to add. + * @return The builder, for convenience. + */ + public Builder setMediaSources(MediaSource... mediaSources) { + return apply( + new Action.SetMediaItems( + tag, /* windowIndex= */ C.INDEX_UNSET, /* positionMs= */ C.TIME_UNSET, mediaSources)); + } + /** + * Schedules a add media items action to be executed. + * + * @param mediaSources The media sources to add. + * @return The builder, for convenience. + */ + public Builder addMediaSources(MediaSource... mediaSources) { + return apply(new Action.AddMediaItems(tag, mediaSources)); + } + + /** + * Schedules a move media item action to be executed. + * + * @param currentIndex The current index of the item to move. + * @param newIndex The index after the item has been moved. + * @return The builder, for convenience. + */ + public Builder moveMediaItem(int currentIndex, int newIndex) { + return apply(new Action.MoveMediaItem(tag, currentIndex, newIndex)); + } + + /** + * Schedules a remove media item action to be executed. + * + * @param index The index of the media item to be removed. + * @return The builder, for convenience. + */ + public Builder removeMediaItem(int index) { + return apply(new Action.RemoveMediaItem(tag, index)); + } + + /** + * Schedules a remove media items action to be executed. + * + * @param fromIndex The start of the range of media items to be removed. + * @param toIndex The end of the range of media items to be removed (exclusive). + * @return The builder, for convenience. + */ + public Builder removeMediaItems(int fromIndex, int toIndex) { + return apply(new Action.RemoveMediaItems(tag, fromIndex, toIndex)); + } + + /** + * Schedules a prepare action to be executed. + * + * @return The builder, for convenience. + */ + public Builder prepare() { + return apply(new Action.Prepare(tag)); + } + + /** + * Schedules a clear media items action to be created. + * + * @return The builder. for convenience, + */ + public Builder clearMediaItems() { + return apply(new Action.ClearMediaItems(tag)); } /** @@ -342,7 +432,17 @@ public final class ActionSchedule { } /** - * Schedules a shuffle setting action. + * Schedules a set shuffle order action to be executed. + * + * @param shuffleOrder The shuffle order. + * @return The builder, for convenience. + */ + public Builder setShuffleOrder(ShuffleOrder shuffleOrder) { + return apply(new SetShuffleOrder(tag, shuffleOrder)); + } + + /** + * Schedules a shuffle setting action to be executed. * * @return The builder, for convenience. */ @@ -394,18 +494,19 @@ public final class ActionSchedule { * @return The builder, for convenience. */ public Builder waitForTimelineChanged() { - return apply(new WaitForTimelineChanged(tag, /* expectedTimeline= */ null)); + return apply(new WaitForTimelineChanged(tag)); } /** * Schedules a delay until the timeline changed to a specified expected timeline. * - * @param expectedTimeline The expected timeline to wait for. If null, wait for any timeline - * change. + * @param expectedTimeline The expected timeline. + * @param expectedReason The expected reason of the timeline change. * @return The builder, for convenience. */ - public Builder waitForTimelineChanged(Timeline expectedTimeline) { - return apply(new WaitForTimelineChanged(tag, expectedTimeline)); + public Builder waitForTimelineChanged( + Timeline expectedTimeline, @Player.TimelineChangeReason int expectedReason) { + return apply(new WaitForTimelineChanged(tag, expectedTimeline, expectedReason)); } /** @@ -447,6 +548,16 @@ public final class ActionSchedule { return apply(new WaitForIsLoading(tag, targetIsLoading)); } + /** + * Schedules a delay until a message arrives at the {@link PlayerMessage.Target}. + * + * @param playerTarget The target to observe. + * @return The builder, for convenience. + */ + public Builder waitForMessage(PlayerTarget playerTarget) { + return apply(new WaitForMessage(tag, playerTarget)); + } + /** * Schedules a {@link Runnable}. * @@ -484,10 +595,28 @@ public final class ActionSchedule { /** * Provides a wrapper for a {@link Target} which has access to the player when handling messages. * Can be used with {@link Builder#sendMessage(Target, long)}. + * + *

    The target can be passed to {@link ActionSchedule.Builder#waitForMessage(PlayerTarget)} to + * wait for a message to arrive at the target. */ public abstract static class PlayerTarget implements Target { + /** Callback to be called when message arrives. */ + public interface Callback { + /** Notifies about the arrival of the message. */ + void onMessageArrived(); + } + private SimpleExoPlayer player; + private boolean hasArrived; + private Callback callback; + + public void setCallback(Callback callback) { + this.callback = callback; + if (hasArrived) { + callback.onMessageArrived(); + } + } /** Handles the message send to the component and additionally provides access to the player. */ public abstract void handleMessage( @@ -499,9 +628,12 @@ public final class ActionSchedule { } @Override - public final void handleMessage(int messageType, @Nullable Object message) - throws ExoPlaybackException { + public final void handleMessage(int messageType, @Nullable Object message) { handleMessage(player, messageType, message); + if (callback != null) { + hasArrived = true; + callback.onMessageArrived(); + } } } 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 4416ab0ef3..28cf8bab66 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 @@ -16,11 +16,14 @@ package com.google.android.exoplayer2.testutil; import static com.google.common.truth.Truth.assertThat; +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertTrue; import android.content.Context; import android.os.HandlerThread; import android.os.Looper; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; @@ -43,6 +46,7 @@ import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.MimeTypes; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -87,8 +91,8 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc private Clock clock; private Timeline timeline; + private List mediaSources; private Object manifest; - private MediaSource mediaSource; private DefaultTrackSelector trackSelector; private LoadControl loadControl; private BandwidthMeter bandwidthMeter; @@ -99,19 +103,31 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc private Player.EventListener eventListener; private AnalyticsListener analyticsListener; private Integer expectedPlayerEndedCount; + private boolean useLazyPreparation; + private int initialWindowIndex; + private long initialPositionMs; + private boolean skipSettingMediaSources; + + public Builder() { + mediaSources = new ArrayList<>(); + initialWindowIndex = C.INDEX_UNSET; + initialPositionMs = C.TIME_UNSET; + } /** * Sets a {@link Timeline} to be used by a {@link FakeMediaSource} in the test runner. The * default value is a seekable, non-dynamic {@link FakeTimeline} with a duration of {@link * FakeTimeline.TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US}. Setting the timeline is - * not allowed after a call to {@link #setMediaSource(MediaSource)}. + * not allowed after a call to {@link #setMediaSources(MediaSource...)} or {@link + * #skipSettingMediaSources()}. * * @param timeline A {@link Timeline} to be used by a {@link FakeMediaSource} in the test * runner. * @return This builder. */ public Builder setTimeline(Timeline timeline) { - assertThat(mediaSource).isNull(); + assertThat(mediaSources).isEmpty(); + assertFalse(skipSettingMediaSources); this.timeline = timeline; return this; } @@ -119,30 +135,73 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc /** * Sets a manifest to be used by a {@link FakeMediaSource} in the test runner. The default value * is null. Setting the manifest is not allowed after a call to {@link - * #setMediaSource(MediaSource)}. + * #setMediaSources(MediaSource...)} or {@link #skipSettingMediaSources()}. * * @param manifest A manifest to be used by a {@link FakeMediaSource} in the test runner. * @return This builder. */ public Builder setManifest(Object manifest) { - assertThat(mediaSource).isNull(); + assertThat(mediaSources).isEmpty(); + assertFalse(skipSettingMediaSources); this.manifest = manifest; return this; } /** - * Sets a {@link MediaSource} to be used by the test runner. The default value is a {@link - * FakeMediaSource} with the timeline and manifest provided by {@link #setTimeline(Timeline)} - * and {@link #setManifest(Object)}. Setting the media source is not allowed after calls to - * {@link #setTimeline(Timeline)} and/or {@link #setManifest(Object)}. + * Seeks before setting the media sources and preparing the player. * - * @param mediaSource A {@link MediaSource} to be used by the test runner. + * @param windowIndex The window index to seek to. + * @param positionMs The position in milliseconds to seek to. * @return This builder. */ - public Builder setMediaSource(MediaSource mediaSource) { + public Builder initialSeek(int windowIndex, long positionMs) { + this.initialWindowIndex = windowIndex; + this.initialPositionMs = positionMs; + return this; + } + + /** + * Sets the {@link MediaSource}s to be used by the test runner. The default value is a {@link + * FakeMediaSource} with the timeline and manifest provided by {@link #setTimeline(Timeline)} + * and {@link #setManifest(Object)}. Setting media sources is not allowed after calls to {@link + * #skipSettingMediaSources()}, {@link #setTimeline(Timeline)} and/or {@link + * #setManifest(Object)}. + * + * @param mediaSources The {@link MediaSource}s to be used by the test runner. + * @return This builder. + */ + public Builder setMediaSources(MediaSource... mediaSources) { assertThat(timeline).isNull(); assertThat(manifest).isNull(); - this.mediaSource = mediaSource; + assertFalse(skipSettingMediaSources); + this.mediaSources = Arrays.asList(mediaSources); + return this; + } + + /** + * Skips calling {@link com.google.android.exoplayer2.ExoPlayer#setMediaSources(List)} before + * preparing. Calling this method is not allowed after calls to {@link + * #setMediaSources(MediaSource...)}, {@link #setTimeline(Timeline)} and/or {@link + * #setManifest(Object)}. + * + * @return This builder. + */ + public Builder skipSettingMediaSources() { + assertThat(timeline).isNull(); + assertThat(manifest).isNull(); + assertTrue(mediaSources.isEmpty()); + skipSettingMediaSources = true; + return this; + } + + /** + * Sets whether to use lazy preparation. + * + * @param useLazyPreparation Whether to use lazy preparation. + * @return This builder. + */ + public Builder setUseLazyPreparation(boolean useLazyPreparation) { + this.useLazyPreparation = useLazyPreparation; return this; } @@ -186,7 +245,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc * Sets a list of {@link Format}s to be used by a {@link FakeMediaSource} to create media * periods and for setting up a {@link FakeRenderer}. The default value is a single {@link * #VIDEO_FORMAT}. Note that this parameter doesn't have any influence if both a media source - * with {@link #setMediaSource(MediaSource)} and renderers with {@link + * with {@link #setMediaSources(MediaSource...)} and renderers with {@link * #setRenderers(Renderer...)} or {@link #setRenderersFactory(RenderersFactory)} are set. * * @param supportedFormats A list of supported {@link Format}s. @@ -240,7 +299,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc /** * Sets an {@link ActionSchedule} to be run by the test runner. The first action will be - * executed immediately before {@link SimpleExoPlayer#prepare(MediaSource)}. + * executed immediately before {@link SimpleExoPlayer#prepare()}. * * @param actionSchedule An {@link ActionSchedule} to be used by the test runner. * @return This builder. @@ -321,11 +380,11 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc if (clock == null) { clock = new AutoAdvancingFakeClock(); } - if (mediaSource == null) { + if (mediaSources.isEmpty() && !skipSettingMediaSources) { if (timeline == null) { timeline = new FakeTimeline(/* windowCount= */ 1, manifest); } - mediaSource = new FakeMediaSource(timeline, supportedFormats); + mediaSources.add(new FakeMediaSource(timeline, supportedFormats)); } if (expectedPlayerEndedCount == null) { expectedPlayerEndedCount = 1; @@ -333,7 +392,11 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc return new ExoPlayerTestRunner( context, clock, - mediaSource, + initialWindowIndex, + initialPositionMs, + mediaSources, + skipSettingMediaSources, + useLazyPreparation, renderersFactory, trackSelector, loadControl, @@ -347,7 +410,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc private final Context context; private final Clock clock; - private final MediaSource mediaSource; + private final int initialWindowIndex; + private final long initialPositionMs; + private final List mediaSources; private final RenderersFactory renderersFactory; private final DefaultTrackSelector trackSelector; private final LoadControl loadControl; @@ -364,6 +429,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc private final ArrayList timelineChangeReasons; private final ArrayList periodIndices; private final ArrayList discontinuityReasons; + private final ArrayList playbackStates; + private final boolean skipSettingMediaSources; + private final boolean useLazyPreparation; private SimpleExoPlayer player; private Exception exception; @@ -373,7 +441,11 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc private ExoPlayerTestRunner( Context context, Clock clock, - MediaSource mediaSource, + int initialWindowIndex, + long initialPositionMs, + List mediaSources, + boolean skipSettingMediaSources, + boolean useLazyPreparation, RenderersFactory renderersFactory, DefaultTrackSelector trackSelector, LoadControl loadControl, @@ -384,7 +456,11 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc int expectedPlayerEndedCount) { this.context = context; this.clock = clock; - this.mediaSource = mediaSource; + this.initialWindowIndex = initialWindowIndex; + this.initialPositionMs = initialPositionMs; + this.mediaSources = mediaSources; + this.skipSettingMediaSources = skipSettingMediaSources; + this.useLazyPreparation = useLazyPreparation; this.renderersFactory = renderersFactory; this.trackSelector = trackSelector; this.loadControl = loadControl; @@ -396,6 +472,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc this.timelineChangeReasons = new ArrayList<>(); this.periodIndices = new ArrayList<>(); this.discontinuityReasons = new ArrayList<>(); + this.playbackStates = new ArrayList<>(); this.endedCountDownLatch = new CountDownLatch(expectedPlayerEndedCount); this.actionScheduleFinishedCountDownLatch = new CountDownLatch(actionSchedule != null ? 1 : 0); this.playerThread = new HandlerThread("ExoPlayerTest thread"); @@ -434,6 +511,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc .setBandwidthMeter(bandwidthMeter) .setAnalyticsCollector(new AnalyticsCollector(clock)) .setClock(clock) + .setUseLazyPreparation(useLazyPreparation) .setLooper(Looper.myLooper()) .build(); player.addListener(ExoPlayerTestRunner.this); @@ -447,8 +525,15 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc if (actionSchedule != null) { actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this); } - player.setMediaSource(mediaSource); - player.prepare(); + if (initialWindowIndex != C.INDEX_UNSET) { + player.seekTo(initialWindowIndex, initialPositionMs); + } + if (!skipSettingMediaSources) { + player.setMediaSources(mediaSources, /* resetPosition= */ false); + } + if (doPrepare) { + player.prepare(); + } } catch (Exception e) { handleException(e); } @@ -500,12 +585,17 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc /** * Asserts that the timelines reported by {@link Player.EventListener#onTimelineChanged(Timeline, - * int)} are equal to the provided timelines. + * int)} are the same to the provided timelines. This assert differs from testing equality by not + * comparing period ids which may be different due to id mapping of child source period ids. * * @param timelines A list of expected {@link Timeline}s. */ - public void assertTimelinesEqual(Timeline... timelines) { - assertThat(this.timelines).containsExactlyElementsIn(Arrays.asList(timelines)).inOrder(); + public void assertTimelinesSame(Timeline... timelines) { + assertThat(this.timelines).hasSize(timelines.length); + for (int i = 0; i < timelines.length; i++) { + assertThat(new NoUidTimeline(timelines[i])) + .isEqualTo(new NoUidTimeline(this.timelines.get(i))); + } } /** @@ -517,6 +607,15 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc assertThat(timelineChangeReasons).containsExactlyElementsIn(Arrays.asList(reasons)).inOrder(); } + /** + * Asserts that the playback states reported by {@link + * Player.EventListener#onPlayerStateChanged(boolean, int)} are equal to the provided playback + * states. + */ + public void assertPlaybackStatesEqual(Integer... states) { + assertThat(playbackStates).containsExactlyElementsIn(Arrays.asList(states)).inOrder(); + } + /** * Asserts that the last track group array reported by {@link * Player.EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)} is equal to the @@ -592,10 +691,12 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc @Override public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { - timelines.add(timeline); timelineChangeReasons.add(reason); - if (reason == Player.TIMELINE_CHANGE_REASON_PREPARED) { - periodIndices.add(player.getCurrentPeriodIndex()); + timelines.add(timeline); + int currentIndex = player.getCurrentPeriodIndex(); + if (periodIndices.isEmpty() || periodIndices.get(periodIndices.size() - 1) != currentIndex) { + // Ignore timeline changes that do not change the period index. + periodIndices.add(currentIndex); } } @@ -606,6 +707,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc @Override public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + playbackStates.add(playbackState); playerWasPrepared |= playbackState != Player.STATE_IDLE; if (playbackState == Player.STATE_ENDED || (playbackState == Player.STATE_IDLE && playerWasPrepared)) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index b347ecc0b7..e4bc539c47 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.source.BaseMediaSource; +import com.google.android.exoplayer2.source.ForwardingTimeline; import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaPeriod; @@ -49,6 +50,22 @@ import java.util.List; */ public class FakeMediaSource extends BaseMediaSource { + /** A forwarding timeline to provide an initial timeline for fake multi window sources. */ + public static class InitialTimeline extends ForwardingTimeline { + + public InitialTimeline(Timeline timeline) { + super(timeline); + } + + @Override + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { + Window childWindow = timeline.getWindow(windowIndex, window, defaultPositionProjectionUs); + childWindow.isDynamic = true; + childWindow.isSeekable = false; + return childWindow; + } + } + private static final DataSpec FAKE_DATA_SPEC = new DataSpec(Uri.parse("http://manifest.uri")); private static final int MANIFEST_LOAD_BYTES = 100; @@ -92,6 +109,19 @@ public class FakeMediaSource extends BaseMediaSource { return hasTimeline ? timeline.getWindow(0, new Timeline.Window()).tag : null; } + @Nullable + @Override + public Timeline getInitialTimeline() { + return timeline == null || timeline == Timeline.EMPTY || timeline.getWindowCount() == 1 + ? null + : new InitialTimeline(timeline); + } + + @Override + public boolean isSingleWindow() { + return timeline == null || timeline == Timeline.EMPTY || timeline.getWindowCount() == 1; + } + @Override public synchronized void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { assertThat(preparedSource).isFalse(); @@ -249,5 +279,4 @@ public class FakeMediaSource extends BaseMediaSource { } return new TrackGroupArray(trackGroups); } - } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 81efb3ba78..b1851106dc 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -25,8 +25,10 @@ import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import java.util.List; /** * An abstract {@link ExoPlayer} implementation that throws {@link UnsupportedOperationException} @@ -91,21 +93,36 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + /** @deprecated Use {@link #prepare()} instead. */ + @Deprecated @Override public void retry() { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #setMediaSource(MediaSource)} and {@link ExoPlayer#prepare()} instead. + */ + @Deprecated @Override public void prepare() { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #setMediaSource(MediaSource)} and {@link ExoPlayer#prepare()} instead. + */ + @Deprecated @Override public void prepare(MediaSource mediaSource) { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #setMediaSource(MediaSource, boolean)} and {@link ExoPlayer#prepare()} + * instead. + */ + @Deprecated @Override public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { throw new UnsupportedOperationException(); @@ -121,6 +138,72 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public void setMediaSource(MediaSource mediaSource, boolean resetPosition) { + throw new UnsupportedOperationException(); + } + + @Override + public void setMediaSources(List mediaSources) { + throw new UnsupportedOperationException(); + } + + @Override + public void setMediaSources(List mediaSources, boolean resetPosition) { + throw new UnsupportedOperationException(); + } + + @Override + public void setMediaSources( + List mediaSources, int startWindowIndex, long startPositionMs) { + throw new UnsupportedOperationException(); + } + + @Override + public void addMediaSource(MediaSource mediaSource) { + throw new UnsupportedOperationException(); + } + + @Override + public void addMediaSource(int index, MediaSource mediaSource) { + throw new UnsupportedOperationException(); + } + + @Override + public void addMediaSources(List mediaSources) { + throw new UnsupportedOperationException(); + } + + @Override + public void addMediaSources(int index, List mediaSources) { + throw new UnsupportedOperationException(); + } + + @Override + public void moveMediaItem(int currentIndex, int newIndex) { + throw new UnsupportedOperationException(); + } + + @Override + public void moveMediaItems(int fromIndex, int toIndex, int newIndex) { + throw new UnsupportedOperationException(); + } + + @Override + public MediaSource removeMediaItem(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeMediaItems(int fromIndex, int toIndex) { + throw new UnsupportedOperationException(); + } + + @Override + public void clearMediaItems() { + throw new UnsupportedOperationException(); + } + @Override public void setPlayWhenReady(boolean playWhenReady) { throw new UnsupportedOperationException(); @@ -141,6 +224,11 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public void setShuffleOrder(ShuffleOrder shuffleOrder) { + throw new UnsupportedOperationException(); + } + @Override public void setShuffleModeEnabled(boolean shuffleModeEnabled) { throw new UnsupportedOperationException(); From 3c56b113e43812f188bdd9750f48897e812697ce Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 17 Dec 2019 23:18:33 +0000 Subject: [PATCH 0546/1335] Rollback of https://github.com/google/ExoPlayer/commit/d48dc4c15933dd8354cbcc6260c48565bb850e15 *** Original commit *** Move getting-stuck-prevention into DefaultLoadControl. We recently added code that prevents getting stuck if the buffer is low and the LoadControl refuses to continue loading (https://github.com/google/ExoPlayer/commit/b84bde025258e7307c52eaf6bbe58157d788aa06). Move this logic into DefaultLoadControl to keep the workaround, and also apply the maximum buffer size check in bytes if enabled. ExoPlayerImplInternal will now throw if a LoadControl lets playback get stuck. This includes the case where DefaultLoadControl reaches its maximum buffer size and not even a mim... *** PiperOrigin-RevId: 286071115 --- .../exoplayer2/DefaultLoadControl.java | 25 ++++++------------- .../exoplayer2/ExoPlayerImplInternal.java | 15 ++++++----- .../exoplayer2/DefaultLoadControlTest.java | 19 +------------- .../android/exoplayer2/ExoPlayerTest.java | 25 ++++++++----------- 4 files changed, 26 insertions(+), 58 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 7bfd4c7cbe..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 @@ -246,7 +246,7 @@ public class DefaultLoadControl implements LoadControl { private final long backBufferDurationUs; private final boolean retainBackBufferFromKeyframe; - private int targetBufferBytes; + private int targetBufferSize; private boolean isBuffering; private boolean hasVideo; @@ -334,10 +334,6 @@ public class DefaultLoadControl implements LoadControl { this.bufferForPlaybackUs = C.msToUs(bufferForPlaybackMs); this.bufferForPlaybackAfterRebufferUs = C.msToUs(bufferForPlaybackAfterRebufferMs); this.targetBufferBytesOverwrite = targetBufferBytes; - this.targetBufferBytes = - targetBufferBytesOverwrite != C.LENGTH_UNSET - ? targetBufferBytesOverwrite - : DEFAULT_MUXED_BUFFER_SIZE; this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds; this.backBufferDurationUs = C.msToUs(backBufferDurationMs); this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe; @@ -352,11 +348,11 @@ public class DefaultLoadControl implements LoadControl { public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { hasVideo = hasVideo(renderers, trackSelections); - targetBufferBytes = + targetBufferSize = targetBufferBytesOverwrite == C.LENGTH_UNSET - ? calculateTargetBufferBytes(renderers, trackSelections) + ? calculateTargetBufferSize(renderers, trackSelections) : targetBufferBytesOverwrite; - allocator.setTargetBufferSize(targetBufferBytes); + allocator.setTargetBufferSize(targetBufferSize); } @Override @@ -386,7 +382,7 @@ public class DefaultLoadControl implements LoadControl { @Override public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { - boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferBytes; + boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; long minBufferUs = hasVideo ? minBufferVideoUs : minBufferAudioUs; if (playbackSpeed > 1) { // The playback speed is faster than real time, so scale up the minimum required media @@ -395,8 +391,6 @@ public class DefaultLoadControl implements LoadControl { Util.getMediaDurationForPlayoutDuration(minBufferUs, playbackSpeed); minBufferUs = Math.min(mediaDurationMinBufferUs, maxBufferUs); } - // Prevent playback from getting stuck if minBufferUs is too small. - minBufferUs = Math.max(minBufferUs, 500_000); if (bufferedDurationUs < minBufferUs) { isBuffering = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached; } else if (bufferedDurationUs >= maxBufferUs || targetBufferSizeReached) { @@ -413,7 +407,7 @@ public class DefaultLoadControl implements LoadControl { return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs || (!prioritizeTimeOverSizeThresholds - && allocator.getTotalBytesAllocated() >= targetBufferBytes); + && allocator.getTotalBytesAllocated() >= targetBufferSize); } /** @@ -424,7 +418,7 @@ public class DefaultLoadControl implements LoadControl { * @param trackSelectionArray The selected tracks. * @return The target buffer size in bytes. */ - protected int calculateTargetBufferBytes( + protected int calculateTargetBufferSize( Renderer[] renderers, TrackSelectionArray trackSelectionArray) { int targetBufferSize = 0; for (int i = 0; i < renderers.length; i++) { @@ -436,10 +430,7 @@ public class DefaultLoadControl implements LoadControl { } private void reset(boolean resetAllocator) { - targetBufferBytes = - targetBufferBytesOverwrite == C.LENGTH_UNSET - ? DEFAULT_MUXED_BUFFER_SIZE - : targetBufferBytesOverwrite; + targetBufferSize = 0; isBuffering = false; if (resetAllocator) { allocator.reset(); 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 47f85b603a..f2e78383e0 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 @@ -830,14 +830,6 @@ import java.util.concurrent.atomic.AtomicBoolean; for (Renderer renderer : enabledRenderers) { renderer.maybeThrowStreamError(); } - if (!shouldContinueLoading - && playbackInfo.totalBufferedDurationUs < 500_000 - && isLoadingPossible()) { - // Throw if the LoadControl prevents loading even if the buffer is empty or almost empty. We - // can't compare against 0 to account for small differences between the renderer position - // and buffered position in the media at the point where playback gets stuck. - throw new IllegalStateException("Playback stuck buffering and not loading"); - } } if ((playWhenReady && playbackInfo.playbackState == Player.STATE_READY) @@ -1992,6 +1984,13 @@ import java.util.concurrent.atomic.AtomicBoolean; } long bufferedDurationUs = getTotalBufferedDurationUs(queue.getLoadingPeriod().getNextLoadPositionUs()); + if (bufferedDurationUs < 500_000) { + // Prevent loading from getting stuck even if LoadControl.shouldContinueLoading returns false + // when the buffer is empty or almost empty. We can't compare against 0 to account for small + // differences between the renderer position and buffered position in the media at the point + // where playback gets stuck. + return true; + } float playbackSpeed = mediaClock.getPlaybackParameters().speed; return loadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java index 2222d1a8d0..31f432db15 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java @@ -46,7 +46,6 @@ public class DefaultLoadControlTest { @Test public void testShouldContinueLoading_untilMaxBufferExceeded() { createDefaultLoadControl(); - assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isTrue(); assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US - 1, SPEED)).isTrue(); @@ -57,27 +56,11 @@ public class DefaultLoadControlTest { public void testShouldNotContinueLoadingOnceBufferingStopped_untilBelowMinBuffer() { createDefaultLoadControl(); assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse(); - assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US - 1, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isTrue(); } - @Test - public void - testContinueLoadingOnceBufferingStopped_andBufferAlmostEmpty_evenIfMinBufferNotReached() { - builder.setBufferDurationsMs( - /* minBufferMs= */ 0, - /* maxBufferMs= */ (int) C.usToMs(MAX_BUFFER_US), - /* bufferForPlaybackMs= */ 0, - /* bufferForPlaybackAfterRebufferMs= */ 0); - createDefaultLoadControl(); - assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse(); - - assertThat(loadControl.shouldContinueLoading(5 * C.MICROS_PER_SECOND, SPEED)).isFalse(); - assertThat(loadControl.shouldContinueLoading(500L, SPEED)).isTrue(); - } - @Test public void testShouldContinueLoadingWithTargetBufferBytesReached_untilMinBufferReached() { createDefaultLoadControl(); @@ -98,7 +81,6 @@ public class DefaultLoadControlTest { makeSureTargetBufferBytesReached(); assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isFalse(); - assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse(); } @@ -109,6 +91,7 @@ public class DefaultLoadControlTest { // At normal playback speed, we stop buffering when the buffer reaches the minimum. assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); + // At double playback speed, we continue loading. assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, /* playbackSpeed= */ 2f)).isTrue(); } 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 0893e01ec0..ff03f09ff6 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 @@ -3393,8 +3393,8 @@ public final class ExoPlayerTest { } @Test - public void loadControlNeverWantsToLoad_throwsIllegalStateException() throws Exception { - LoadControl neverLoadingLoadControl = + public void loadControlNeverWantsToLoadOrPlay_playbackDoesNotGetStuck() throws Exception { + LoadControl neverLoadingOrPlayingLoadControl = new DefaultLoadControl() { @Override public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { @@ -3404,7 +3404,7 @@ public final class ExoPlayerTest { @Override public boolean shouldStartPlayback( long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { - return true; + return false; } }; @@ -3418,18 +3418,13 @@ public final class ExoPlayerTest { new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT)), new FakeChunkSource.Factory(dataSetFactory, new FakeDataSource.Factory())); - try { - new ExoPlayerTestRunner.Builder() - .setLoadControl(neverLoadingLoadControl) - .setMediaSources(chunkedMediaSource) - .build(context) - .start() - .blockUntilEnded(TIMEOUT_MS); - fail(); - } catch (ExoPlaybackException e) { - assertThat(e.type).isEqualTo(ExoPlaybackException.TYPE_UNEXPECTED); - assertThat(e.getUnexpectedException()).isInstanceOf(IllegalStateException.class); - } + new ExoPlayerTestRunner.Builder() + .setLoadControl(neverLoadingOrPlayingLoadControl) + .setMediaSources(chunkedMediaSource) + .build(context) + .start() + // This throws if playback doesn't finish within timeout. + .blockUntilEnded(TIMEOUT_MS); } @Test From c111138ac2656a1bda1cc739df7e386145b847b0 Mon Sep 17 00:00:00 2001 From: krocard Date: Wed, 18 Dec 2019 09:20:28 +0000 Subject: [PATCH 0547/1335] Parse MP3 header to retrieve the nb of sample per frames Add support for MP3 as an encoding format for passthrough. This change does not change the observable behavior of Exoplayer. Also name the magics. #exo-offload PiperOrigin-RevId: 286146539 --- .../java/com/google/android/exoplayer2/C.java | 9 ++- .../exoplayer2/audio/DefaultAudioSink.java | 38 +++++++----- .../exoplayer2/extractor/MpegAudioHeader.java | 60 +++++++++++++++++-- .../android/exoplayer2/util/MimeTypes.java | 2 + 4 files changed, 84 insertions(+), 25 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 9661b7d072..e431b2d899 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 @@ -151,9 +151,9 @@ public final class C { * Represents an audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE}, * {@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_E_AC3_JOC}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS}, - * {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}. + * #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link #ENCODING_MP3}, {@link + * #ENCODING_AC3}, {@link #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) @@ -167,6 +167,7 @@ public final class C { ENCODING_PCM_FLOAT, ENCODING_PCM_MU_LAW, ENCODING_PCM_A_LAW, + ENCODING_MP3, ENCODING_AC3, ENCODING_E_AC3, ENCODING_E_AC3_JOC, @@ -213,6 +214,8 @@ public final class C { public static final int ENCODING_PCM_MU_LAW = 0x10000000; /** Audio encoding for A-law. */ public static final int ENCODING_PCM_A_LAW = 0x20000000; + /** @see AudioFormat#ENCODING_MP3 */ + public static final int ENCODING_MP3 = AudioFormat.ENCODING_MP3; /** @see AudioFormat#ENCODING_AC3 */ public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3; /** @see AudioFormat#ENCODING_E_AC3 */ 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 27823e3006..240a8554b7 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 @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.audio.AudioProcessor.UnhandledAudioFormatException; +import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; @@ -1158,22 +1159,27 @@ public final class DefaultAudioSink implements AudioSink { } private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) { - if (encoding == C.ENCODING_DTS || encoding == C.ENCODING_DTS_HD) { - return DtsUtil.parseDtsAudioSampleCount(buffer); - } else if (encoding == C.ENCODING_AC3) { - return Ac3Util.getAc3SyncframeAudioSampleCount(); - } 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); - } else if (encoding == C.ENCODING_DOLBY_TRUEHD) { - int syncframeOffset = Ac3Util.findTrueHdSyncframeOffset(buffer); - return syncframeOffset == C.INDEX_UNSET - ? 0 - : (Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer, syncframeOffset) - * Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT); - } else { - throw new IllegalStateException("Unexpected audio encoding: " + encoding); + switch (encoding) { + case C.ENCODING_MP3: + return MpegAudioHeader.getFrameSampleCount(buffer.get(buffer.position())); + case C.ENCODING_DTS: + case C.ENCODING_DTS_HD: + return DtsUtil.parseDtsAudioSampleCount(buffer); + case C.ENCODING_AC3: + return Ac3Util.getAc3SyncframeAudioSampleCount(); + case C.ENCODING_E_AC3: + case C.ENCODING_E_AC3_JOC: + return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer); + case C.ENCODING_AC4: + return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer); + case C.ENCODING_DOLBY_TRUEHD: + int syncframeOffset = Ac3Util.findTrueHdSyncframeOffset(buffer); + return syncframeOffset == C.INDEX_UNSET + ? 0 + : (Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer, syncframeOffset) + * Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT); + default: + throw new IllegalStateException("Unexpected audio encoding: " + encoding); } } 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 04d85b8bc5..b3155233d0 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 @@ -56,12 +56,17 @@ public final class MpegAudioHeader { 160000 }; + private static final int SAMPLES_PER_FRAME_L1 = 384; + private static final int SAMPLES_PER_FRAME_L2 = 1152; + private static final int SAMPLES_PER_FRAME_L3_V1 = 1152; + private static final int SAMPLES_PER_FRAME_L3_V2 = 576; + /** * Returns the size of the frame associated with {@code header}, or {@link C#LENGTH_UNSET} if it * is invalid. */ public static int getFrameSize(int header) { - if ((header & 0xFFE00000) != 0xFFE00000) { + if (!isMagicPresent(header)) { return C.LENGTH_UNSET; } @@ -120,6 +125,36 @@ public final class MpegAudioHeader { } } + /** + * Returns the number of samples per frame associated with {@code header}, or {@link + * C#LENGTH_UNSET} if it is invalid. + */ + public static int getFrameSampleCount(int header) { + + if (!isMagicPresent(header)) { + return C.LENGTH_UNSET; + } + + int version = (header >>> 19) & 3; + if (version == 1) { + return C.LENGTH_UNSET; + } + + int layer = (header >>> 17) & 3; + if (layer == 0) { + return C.LENGTH_UNSET; + } + + // Those header values are not used but are checked for consistency with the other methods + int bitrateIndex = (header >>> 12) & 15; + int samplingRateIndex = (header >>> 10) & 3; + if (bitrateIndex == 0 || bitrateIndex == 0xF || samplingRateIndex == 3) { + return C.LENGTH_UNSET; + } + + return getFrameSizeInSamples(version, layer); + } + /** * Parses {@code headerData}, populating {@code header} with the parsed data. * @@ -129,7 +164,7 @@ public final class MpegAudioHeader { * is not a valid MPEG audio header. */ public static boolean populateHeader(int headerData, MpegAudioHeader header) { - if ((headerData & 0xFFE00000) != 0xFFE00000) { + if (!isMagicPresent(headerData)) { return false; } @@ -166,23 +201,20 @@ public final class MpegAudioHeader { int padding = (headerData >>> 9) & 1; int bitrate; int frameSize; - int samplesPerFrame; + int samplesPerFrame = getFrameSizeInSamples(version, layer); if (layer == 3) { // Layer I (layer == 3) bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1]; frameSize = (12 * bitrate / sampleRate + padding) * 4; - samplesPerFrame = 384; } else { // Layer II (layer == 2) or III (layer == 1) if (version == 3) { // Version 1 bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1]; - samplesPerFrame = 1152; frameSize = 144 * bitrate / sampleRate + padding; } else { // Version 2 or 2.5. bitrate = BITRATE_V2[bitrateIndex - 1]; - samplesPerFrame = layer == 1 ? 576 : 1152; frameSize = (layer == 1 ? 72 : 144) * bitrate / sampleRate + padding; } } @@ -193,6 +225,22 @@ public final class MpegAudioHeader { return true; } + private static boolean isMagicPresent(int header) { + return (header & 0xFFE00000) == 0xFFE00000; + } + + private static int getFrameSizeInSamples(int version, int layer) { + switch (layer) { + case 1: + return version == 3 ? SAMPLES_PER_FRAME_L3_V1 : SAMPLES_PER_FRAME_L3_V2; // Layer III + case 2: + return SAMPLES_PER_FRAME_L2; // Layer II + case 3: + return SAMPLES_PER_FRAME_L1; // Layer I + } + throw new IllegalArgumentException(); + } + /** MPEG audio header version. */ public int version; /** The mime type. */ 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 de30cfd214..803ef6f41d 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 @@ -345,6 +345,8 @@ public final class MimeTypes { */ public static @C.Encoding int getEncoding(String mimeType) { switch (mimeType) { + case MimeTypes.AUDIO_MPEG: + return C.ENCODING_MP3; case MimeTypes.AUDIO_AC3: return C.ENCODING_AC3; case MimeTypes.AUDIO_E_AC3: From 9d8a1635c24c3f917f5454b3ef29e49a1f4ae168 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 18 Dec 2019 10:24:10 +0000 Subject: [PATCH 0548/1335] Ensure raw resources are kept R8 does constant folding, so we need to keep buildRawResourceUri to ensure that resources passed to it are kept. PiperOrigin-RevId: 286153875 --- RELEASENOTES.md | 2 ++ library/core/proguard-rules.txt | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ac5ba1b045..bcd816b041 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -35,6 +35,8 @@ * Suppress ProGuard warnings for compile-time `javax.annotation` package ([#6771](https://github.com/google/ExoPlayer/issues/6771)). * Add playlist API ([#6161](https://github.com/google/ExoPlayer/issues/6161)). +* Fix proguard rules for R8 to ensure raw resources used with + `RawResourceDataSource` are kept. ### 2.11.0 (2019-12-11) ### diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index ab4af32da4..494837c3e9 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -1,5 +1,10 @@ # Proguard rules specific to the core module. +# Constant folding for resource integers may mean that a resource passed to this method appears to be unused. Keep the method to prevent this from happening. +-keep class com.google.android.exoplayer2.upstream.RawResourceDataSource { + public static android.net.Uri buildRawResourceUri(int); +} + # Constructors accessed via reflection in DefaultRenderersFactory -dontnote com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer -keepclassmembers class com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer { @@ -69,5 +74,5 @@ # Some members of this class are being accessed from native methods. Keep them unobfuscated. -keep class com.google.android.exoplayer2.ext.video.VideoDecoderOutputBuffer { - *; + *; } From 7219e5a314b549de5f93ac9720a6d86fc7301c29 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 18 Dec 2019 10:34:22 +0000 Subject: [PATCH 0549/1335] Fix nullness annotation on equals methods PiperOrigin-RevId: 286154938 --- .../google/android/exoplayer2/offline/DownloadManagerTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 452f20e957..cdb113918e 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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.offline; import static com.google.common.truth.Truth.assertThat; import android.net.Uri; +import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; @@ -714,7 +715,7 @@ public class DownloadManagerTest { } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } From 6c9357ba2f89dfdd4e6435338722ac17b252312d Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 18 Dec 2019 10:47:42 +0000 Subject: [PATCH 0550/1335] Fix keep rule for VideoDecoderOutputBuffer PiperOrigin-RevId: 286156361 --- RELEASENOTES.md | 2 ++ library/core/proguard-rules.txt | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index bcd816b041..9552be4e6f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -37,6 +37,8 @@ * Add playlist API ([#6161](https://github.com/google/ExoPlayer/issues/6161)). * Fix proguard rules for R8 to ensure raw resources used with `RawResourceDataSource` are kept. +* Fix proguard rules to keep `VideoDecoderOutputBuffer` for video decoder + extensions. ### 2.11.0 (2019-12-11) ### diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index 494837c3e9..fd4e196945 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -5,6 +5,11 @@ public static android.net.Uri buildRawResourceUri(int); } +# Some members of this class are being accessed from native methods. Keep them unobfuscated. +-keep class com.google.android.exoplayer2.video.VideoDecoderOutputBuffer { + *; +} + # Constructors accessed via reflection in DefaultRenderersFactory -dontnote com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer -keepclassmembers class com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer { @@ -71,8 +76,3 @@ -dontwarn org.checkerframework.** -dontwarn kotlin.annotations.jvm.** -dontwarn javax.annotation.** - -# Some members of this class are being accessed from native methods. Keep them unobfuscated. --keep class com.google.android.exoplayer2.ext.video.VideoDecoderOutputBuffer { - *; -} From fde59ccd1a6bbe2a6f6f091af303c97b9bf62f78 Mon Sep 17 00:00:00 2001 From: christosts Date: Wed, 18 Dec 2019 10:55:49 +0000 Subject: [PATCH 0551/1335] Formatting fixes on MediaCodecAdapter Fixes JavaDoc on MediaCodecAdapter and AsynchronousMediaCodecAdapter and a field declaration on MediaCodecRenderer. PiperOrigin-RevId: 286157106 --- .../mediacodec/AsynchronousMediaCodecAdapter.java | 2 +- .../android/exoplayer2/mediacodec/MediaCodecAdapter.java | 4 ++-- .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 6 ++---- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java index c0596c0550..0d126ff27f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java @@ -43,7 +43,7 @@ import com.google.android.exoplayer2.util.Assertions; /** * Create a new {@code AsynchronousMediaCodecAdapter}. * - * @param codec the {@link MediaCodec} to wrap. + * @param codec The {@link MediaCodec} to wrap. */ public AsynchronousMediaCodecAdapter(MediaCodec codec) { this(codec, Assertions.checkNotNull(Looper.myLooper())); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java index 9d86f37736..c984443041 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java @@ -35,7 +35,7 @@ import android.media.MediaFormat; * Returns the next available input buffer index from the underlying {@link MediaCodec} or {@link * MediaCodec#INFO_TRY_AGAIN_LATER} if no such buffer exists. * - * @throws {@link IllegalStateException} if the underlying {@link MediaCodec} raised an error. + * @throws IllegalStateException If the underlying {@link MediaCodec} raised an error. */ int dequeueInputBufferIndex(); @@ -46,7 +46,7 @@ import android.media.MediaFormat; * the format. If there is no available output, this method will return {@link * MediaCodec#INFO_TRY_AGAIN_LATER}. * - * @throws {@link IllegalStateException} if the underlying {@link MediaCodec} raised an error. + * @throws IllegalStateException If the underlying {@link MediaCodec} raised an error. */ int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo); 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 9f6ff1212c..953ffbe546 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 @@ -406,9 +406,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private boolean waitingForFirstSyncSample; private boolean waitingForFirstSampleInFormat; private boolean pendingOutputEndOfStream; - - private @MediaCodecOperationMode int mediaCodecOperationMode; - + @MediaCodecOperationMode private int mediaCodecOperationMode; protected DecoderCounters decoderCounters; /** @@ -473,7 +471,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { *

    This method is experimental, and will be renamed or removed in a future release. It should * only be called before the renderer is used. * - * @param mode the mode of the MediaCodec. The supported modes are: + * @param mode The mode of the MediaCodec. The supported modes are: *

      *
    • {@link MediaCodecOperationMode#SYNCHRONOUS}: The {@link MediaCodec} will operate in * synchronous mode. From 7a03e8edc0940de1d7b88c936bbe33baf096332f Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 18 Dec 2019 15:13:25 +0000 Subject: [PATCH 0552/1335] Add NonNull to testutil.truth PiperOrigin-RevId: 286185549 --- .../testutil/truth/package-info.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/package-info.java diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/package-info.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/package-info.java new file mode 100644 index 0000000000..003abfc059 --- /dev/null +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/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.testutil.truth; + +import com.google.android.exoplayer2.util.NonNullApi; From 821d4fb13ab0ff940f9c0dcebb822aa1357f78ae Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 18 Dec 2019 15:53:31 +0000 Subject: [PATCH 0553/1335] Add NonNull at package level for extractor.mp4 PiperOrigin-RevId: 286191078 --- .../exoplayer2/extractor/mp4/AtomParsers.java | 71 +++++++++++-------- .../extractor/mp4/FragmentedMp4Extractor.java | 52 ++++++++------ .../extractor/mp4/MdtaMetadataEntry.java | 4 +- .../extractor/mp4/MetadataUtil.java | 13 ++-- .../extractor/mp4/Mp4Extractor.java | 22 +++--- .../extractor/mp4/PsshAtomUtil.java | 17 ++--- .../extractor/mp4/TrackFragment.java | 52 +++++++------- .../extractor/mp4/package-info.java | 19 +++++ 8 files changed, 150 insertions(+), 100 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/package-info.java 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 919fd80b06..f6b4f4d463 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 @@ -125,14 +125,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; Pair mdhdData = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data); StsdData stsdData = parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data, tkhdData.id, tkhdData.rotationDegrees, mdhdData.second, drmInitData, isQuickTime); - long[] editListDurations = null; - long[] editListMediaTimes = null; + @Nullable long[] editListDurations = null; + @Nullable long[] editListMediaTimes = null; if (!ignoreEditLists) { - @Nullable - Pair edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts)); - if (edtsData != null) { - editListDurations = edtsData.first; - editListMediaTimes = edtsData.second; + @Nullable Atom.ContainerAtom edtsAtom = trak.getContainerAtomOfType(Atom.TYPE_edts); + if (edtsAtom != null) { + @Nullable Pair edtsData = parseEdts(edtsAtom); + if (edtsData != null) { + editListDurations = edtsData.first; + editListMediaTimes = edtsData.second; + } } } return stsdData.format == null ? null @@ -154,11 +156,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; Track track, Atom.ContainerAtom stblAtom, GaplessInfoHolder gaplessInfoHolder) throws ParserException { SampleSizeBox sampleSizeBox; - Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz); + @Nullable Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz); if (stszAtom != null) { sampleSizeBox = new StszSampleSizeBox(stszAtom); } else { - Atom.LeafAtom stz2Atom = stblAtom.getLeafAtomOfType(Atom.TYPE_stz2); + @Nullable Atom.LeafAtom stz2Atom = stblAtom.getLeafAtomOfType(Atom.TYPE_stz2); if (stz2Atom == null) { throw new ParserException("Track has no sample table size information"); } @@ -179,7 +181,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; // Entries are byte offsets of chunks. boolean chunkOffsetsAreLongs = false; - Atom.LeafAtom chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stco); + @Nullable Atom.LeafAtom chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stco); if (chunkOffsetsAtom == null) { chunkOffsetsAreLongs = true; chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_co64); @@ -190,11 +192,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; // Entries are (number of samples, timestamp delta between those samples). ParsableByteArray stts = stblAtom.getLeafAtomOfType(Atom.TYPE_stts).data; // Entries are the indices of samples that are synchronization samples. - Atom.LeafAtom stssAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stss); - ParsableByteArray stss = stssAtom != null ? stssAtom.data : null; + @Nullable Atom.LeafAtom stssAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stss); + @Nullable ParsableByteArray stss = stssAtom != null ? stssAtom.data : null; // Entries are (number of samples, timestamp offset). - Atom.LeafAtom cttsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_ctts); - ParsableByteArray ctts = cttsAtom != null ? cttsAtom.data : null; + @Nullable Atom.LeafAtom cttsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_ctts); + @Nullable ParsableByteArray ctts = cttsAtom != null ? cttsAtom.data : null; // Prepare to read chunk information. ChunkIterator chunkIterator = new ChunkIterator(stsc, chunkOffsets, chunkOffsetsAreLongs); @@ -542,9 +544,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; */ @Nullable public static Metadata parseMdtaFromMeta(Atom.ContainerAtom meta) { - Atom.LeafAtom hdlrAtom = meta.getLeafAtomOfType(Atom.TYPE_hdlr); - Atom.LeafAtom keysAtom = meta.getLeafAtomOfType(Atom.TYPE_keys); - Atom.LeafAtom ilstAtom = meta.getLeafAtomOfType(Atom.TYPE_ilst); + @Nullable Atom.LeafAtom hdlrAtom = meta.getLeafAtomOfType(Atom.TYPE_hdlr); + @Nullable Atom.LeafAtom keysAtom = meta.getLeafAtomOfType(Atom.TYPE_keys); + @Nullable Atom.LeafAtom ilstAtom = meta.getLeafAtomOfType(Atom.TYPE_ilst); if (hdlrAtom == null || keysAtom == null || ilstAtom == null @@ -575,6 +577,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; int keyIndex = ilst.readInt() - 1; if (keyIndex >= 0 && keyIndex < keyNames.length) { String key = keyNames[keyIndex]; + @Nullable Metadata.Entry entry = MetadataUtil.parseMdtaMetadataEntryFromIlst(ilst, atomPosition + atomSize, key); if (entry != null) { @@ -609,7 +612,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ilst.skipBytes(Atom.HEADER_SIZE); ArrayList entries = new ArrayList<>(); while (ilst.getPosition() < limit) { - Metadata.Entry entry = MetadataUtil.parseIlstElement(ilst); + @Nullable Metadata.Entry entry = MetadataUtil.parseIlstElement(ilst); if (entry != null) { entries.add(entry); } @@ -817,12 +820,18 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return out; } - private static void parseTextSampleEntry(ParsableByteArray parent, int atomType, int position, - int atomSize, int trackId, String language, StsdData out) throws ParserException { + private static void parseTextSampleEntry( + ParsableByteArray parent, + int atomType, + int position, + int atomSize, + int trackId, + String language, + StsdData out) { parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); // Default values. - List initializationData = null; + @Nullable List initializationData = null; long subsampleOffsetUs = Format.OFFSET_SAMPLE_RELATIVE; String mimeType; @@ -934,7 +943,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; initializationData = hevcConfig.initializationData; out.nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength; } else if (childAtomType == Atom.TYPE_dvcC || childAtomType == Atom.TYPE_dvvC) { - DolbyVisionConfig dolbyVisionConfig = DolbyVisionConfig.parse(parent); + @Nullable DolbyVisionConfig dolbyVisionConfig = DolbyVisionConfig.parse(parent); if (dolbyVisionConfig != null) { codecs = dolbyVisionConfig.codecs; mimeType = MimeTypes.VIDEO_DOLBY_VISION; @@ -1021,11 +1030,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; */ @Nullable private static Pair parseEdts(Atom.ContainerAtom edtsAtom) { - Atom.LeafAtom elst; - if (edtsAtom == null || (elst = edtsAtom.getLeafAtomOfType(Atom.TYPE_elst)) == null) { + @Nullable Atom.LeafAtom elstAtom = edtsAtom.getLeafAtomOfType(Atom.TYPE_elst); + if (elstAtom == null) { return null; } - ParsableByteArray elstData = elst.data; + ParsableByteArray elstData = elstAtom.data; elstData.setPosition(Atom.HEADER_SIZE); int fullAtom = elstData.readInt(); int version = Atom.parseFullAtomVersion(fullAtom); @@ -1328,8 +1337,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; int childPosition = position + Atom.HEADER_SIZE; int schemeInformationBoxPosition = C.POSITION_UNSET; int schemeInformationBoxSize = 0; - String schemeType = null; - Integer dataFormat = null; + @Nullable String schemeType = null; + @Nullable Integer dataFormat = null; while (childPosition - position < size) { parent.setPosition(childPosition); int childAtomSize = parent.readInt(); @@ -1352,9 +1361,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; Assertions.checkStateNotNull(dataFormat, "frma atom is mandatory"); Assertions.checkState( schemeInformationBoxPosition != C.POSITION_UNSET, "schi atom is mandatory"); - TrackEncryptionBox encryptionBox = parseSchiFromParent(parent, schemeInformationBoxPosition, - schemeInformationBoxSize, schemeType); - Assertions.checkStateNotNull(encryptionBox, "tenc atom is mandatory"); + TrackEncryptionBox encryptionBox = + Assertions.checkStateNotNull( + parseSchiFromParent( + parent, schemeInformationBoxPosition, schemeInformationBoxSize, schemeType), + "tenc atom is mandatory"); return Pair.create(dataFormat, encryptionBox); } else { return null; 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 f6e0fb8bc9..1172f8665a 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 @@ -55,6 +55,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.UUID; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** Extracts data from the FMP4 container format. */ @SuppressWarnings("ConstantField") @@ -155,14 +156,14 @@ public class FragmentedMp4Extractor implements Extractor { private int atomType; private long atomSize; private int atomHeaderBytesRead; - private ParsableByteArray atomData; + @Nullable private ParsableByteArray atomData; private long endOfMdatPosition; private int pendingMetadataSampleBytes; private long pendingSeekTimeUs; private long durationUs; private long segmentIndexEarliestPresentationTimeUs; - private TrackBundle currentTrackBundle; + @Nullable private TrackBundle currentTrackBundle; private int sampleSize; private int sampleBytesWritten; private int sampleCurrentNalBytesRemaining; @@ -170,7 +171,7 @@ public class FragmentedMp4Extractor implements Extractor { private boolean isAc4HeaderRequired; // Extractor output. - private ExtractorOutput extractorOutput; + @MonotonicNonNull private ExtractorOutput extractorOutput; private TrackOutput[] emsgTrackOutputs; private TrackOutput[] cea608TrackOutputs; @@ -495,6 +496,7 @@ public class FragmentedMp4Extractor implements Extractor { for (int i = 0; i < moovContainerChildrenSize; i++) { Atom.ContainerAtom atom = moov.containerChildren.get(i); if (atom.type == Atom.TYPE_trak) { + @Nullable Track track = modifyTrack( AtomParsers.parseTrak( @@ -712,7 +714,7 @@ public class FragmentedMp4Extractor implements Extractor { private static void parseTraf(ContainerAtom traf, SparseArray trackBundleArray, @Flags int flags, byte[] extendedTypeScratch) throws ParserException { LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd); - TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray); + @Nullable TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray); if (trackBundle == null) { return; } @@ -721,33 +723,34 @@ public class FragmentedMp4Extractor implements Extractor { long decodeTime = fragment.nextFragmentDecodeTime; trackBundle.reset(); - LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt); + @Nullable LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt); if (tfdtAtom != null && (flags & FLAG_WORKAROUND_IGNORE_TFDT_BOX) == 0) { decodeTime = parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data); } parseTruns(traf, trackBundle, decodeTime, flags); - TrackEncryptionBox encryptionBox = trackBundle.track - .getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex); + @Nullable + TrackEncryptionBox encryptionBox = + trackBundle.track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex); - LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz); + @Nullable LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz); if (saiz != null) { parseSaiz(encryptionBox, saiz.data, fragment); } - LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio); + @Nullable LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio); if (saio != null) { parseSaio(saio.data, fragment); } - LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc); + @Nullable LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc); if (senc != null) { parseSenc(senc.data, fragment); } - LeafAtom sbgp = traf.getLeafAtomOfType(Atom.TYPE_sbgp); - LeafAtom sgpd = traf.getLeafAtomOfType(Atom.TYPE_sgpd); + @Nullable LeafAtom sbgp = traf.getLeafAtomOfType(Atom.TYPE_sbgp); + @Nullable LeafAtom sgpd = traf.getLeafAtomOfType(Atom.TYPE_sgpd); if (sbgp != null && sgpd != null) { parseSgpd(sbgp.data, sgpd.data, encryptionBox != null ? encryptionBox.schemeType : null, fragment); @@ -863,13 +866,14 @@ public class FragmentedMp4Extractor implements Extractor { * @return The {@link TrackBundle} to which the {@link TrackFragment} belongs, or null if the tfhd * does not refer to any {@link TrackBundle}. */ + @Nullable private static TrackBundle parseTfhd( ParsableByteArray tfhd, SparseArray trackBundles) { tfhd.setPosition(Atom.HEADER_SIZE); int fullAtom = tfhd.readInt(); int atomFlags = Atom.parseFullAtomFlags(fullAtom); int trackId = tfhd.readInt(); - TrackBundle trackBundle = getTrackBundle(trackBundles, trackId); + @Nullable TrackBundle trackBundle = getTrackBundle(trackBundles, trackId); if (trackBundle == null) { return null; } @@ -1053,8 +1057,12 @@ public class FragmentedMp4Extractor implements Extractor { out.fillEncryptionData(senc); } - private static void parseSgpd(ParsableByteArray sbgp, ParsableByteArray sgpd, String schemeType, - TrackFragment out) throws ParserException { + private static void parseSgpd( + ParsableByteArray sbgp, + ParsableByteArray sgpd, + @Nullable String schemeType, + TrackFragment out) + throws ParserException { sbgp.setPosition(Atom.HEADER_SIZE); int sbgpFullAtom = sbgp.readInt(); if (sbgp.readInt() != SAMPLE_GROUP_TYPE_seig) { @@ -1216,7 +1224,7 @@ public class FragmentedMp4Extractor implements Extractor { private boolean readSample(ExtractorInput input) throws IOException, InterruptedException { if (parserState == STATE_READING_SAMPLE_START) { if (currentTrackBundle == null) { - TrackBundle currentTrackBundle = getNextFragmentRun(trackBundles); + @Nullable TrackBundle currentTrackBundle = getNextFragmentRun(trackBundles); if (currentTrackBundle == null) { // We've run out of samples in the current mdat. Discard any trailing data and prepare to // read the header of the next atom. @@ -1388,6 +1396,7 @@ public class FragmentedMp4Extractor implements Extractor { * Returns the {@link TrackBundle} whose fragment run has the earliest file position out of those * yet to be consumed, or null if all have been consumed. */ + @Nullable private static TrackBundle getNextFragmentRun(SparseArray trackBundles) { TrackBundle nextTrackBundle = null; long nextTrackRunOffset = Long.MAX_VALUE; @@ -1410,7 +1419,7 @@ public class FragmentedMp4Extractor implements Extractor { /** Returns DrmInitData from leaf atoms. */ private static DrmInitData getDrmInitDataFromAtoms(List leafChildren) { - ArrayList schemeDatas = null; + @Nullable ArrayList schemeDatas = null; int leafChildrenSize = leafChildren.size(); for (int i = 0; i < leafChildrenSize; i++) { LeafAtom child = leafChildren.get(i); @@ -1419,7 +1428,7 @@ public class FragmentedMp4Extractor implements Extractor { schemeDatas = new ArrayList<>(); } byte[] psshData = child.data.data; - UUID uuid = PsshAtomUtil.parseUuid(psshData); + @Nullable UUID uuid = PsshAtomUtil.parseUuid(psshData); if (uuid == null) { Log.w(TAG, "Skipped pssh atom (failed to extract uuid)"); } else { @@ -1496,9 +1505,10 @@ public class FragmentedMp4Extractor implements Extractor { } public void updateDrmInitData(DrmInitData drmInitData) { + @Nullable TrackEncryptionBox encryptionBox = track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex); - String schemeType = encryptionBox != null ? encryptionBox.schemeType : null; + @Nullable String schemeType = encryptionBox != null ? encryptionBox.schemeType : null; output.format(track.format.copyWithDrmInitData(drmInitData.copyWithSchemeType(schemeType))); } @@ -1595,7 +1605,7 @@ public class FragmentedMp4Extractor implements Extractor { /** Skips the encryption data for the current sample. */ private void skipSampleEncryptionData() { - TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted(); + @Nullable TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted(); if (encryptionBox == null) { return; } @@ -1609,8 +1619,10 @@ public class FragmentedMp4Extractor implements Extractor { } } + @Nullable private TrackEncryptionBox getEncryptionBoxIfEncrypted() { int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex; + @Nullable TrackEncryptionBox encryptionBox = fragment.trackEncryptionBox != null ? fragment.trackEncryptionBox diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java index e50fbd54f7..5ad2b63b4c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java @@ -47,8 +47,7 @@ public final class MdtaMetadataEntry implements Metadata.Entry { private MdtaMetadataEntry(Parcel in) { key = Util.castNonNull(in.readString()); - value = new byte[in.readInt()]; - in.readByteArray(value); + value = Util.castNonNull(in.createByteArray()); localeIndicator = in.readInt(); typeIndicator = in.readInt(); } @@ -88,7 +87,6 @@ public final class MdtaMetadataEntry implements Metadata.Entry { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(key); - dest.writeInt(value.length); dest.writeByteArray(value); dest.writeInt(localeIndicator); dest.writeInt(typeIndicator); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java index 174ae821ac..cf2d2c1f6e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java @@ -325,8 +325,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray; @Nullable private static TextInformationFrame parseStandardGenreAttribute(ParsableByteArray data) { int genreCode = parseUint8AttributeValue(data); - String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length) - ? STANDARD_GENRES[genreCode - 1] : null; + @Nullable + String genreString = + (0 < genreCode && genreCode <= STANDARD_GENRES.length) + ? STANDARD_GENRES[genreCode - 1] + : null; if (genreString != null) { return new TextInformationFrame("TCON", /* description= */ null, genreString); } @@ -341,7 +344,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; if (atomType == Atom.TYPE_data) { int fullVersionInt = data.readInt(); int flags = Atom.parseFullAtomFlags(fullVersionInt); - String mimeType = flags == 13 ? "image/jpeg" : flags == 14 ? "image/png" : null; + @Nullable String mimeType = flags == 13 ? "image/jpeg" : flags == 14 ? "image/png" : null; if (mimeType == null) { Log.w(TAG, "Unrecognized cover art flags: " + flags); return null; @@ -361,8 +364,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; @Nullable private static Id3Frame parseInternalAttribute(ParsableByteArray data, int endPosition) { - String domain = null; - String name = null; + @Nullable String domain = null; + @Nullable String name = null; int dataAtomPosition = -1; int dataAtomSize = -1; while (data.getPosition() < endPosition) { 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 ad58e832aa..971cc27d13 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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.mp4; 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.ParserException; @@ -42,6 +43,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Extracts data from the MP4 container format. @@ -105,7 +107,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { private int atomType; private long atomSize; private int atomHeaderBytesRead; - private ParsableByteArray atomData; + @Nullable private ParsableByteArray atomData; private int sampleTrackIndex; private int sampleBytesWritten; @@ -113,7 +115,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { private boolean isAc4HeaderRequired; // Extractor outputs. - private ExtractorOutput extractorOutput; + @MonotonicNonNull private ExtractorOutput extractorOutput; private Mp4Track[] tracks; private long[][] accumulatedSampleSizes; private int firstVideoTrackIndex; @@ -290,8 +292,11 @@ public final class Mp4Extractor implements Extractor, SeekMap { // The atom extends to the end of the file. Note that if the atom is within a container we can // work out its size even if the input length is unknown. long endPosition = input.getLength(); - if (endPosition == C.LENGTH_UNSET && !containerAtoms.isEmpty()) { - endPosition = containerAtoms.peek().endPosition; + if (endPosition == C.LENGTH_UNSET) { + @Nullable ContainerAtom containerAtom = containerAtoms.peek(); + if (containerAtom != null) { + endPosition = containerAtom.endPosition; + } } if (endPosition != C.LENGTH_UNSET) { atomSize = endPosition - input.getPosition() + atomHeaderBytesRead; @@ -386,17 +391,17 @@ public final class Mp4Extractor implements Extractor, SeekMap { List tracks = new ArrayList<>(); // Process metadata. - Metadata udtaMetadata = null; + @Nullable Metadata udtaMetadata = null; GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); - Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta); + @Nullable Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta); if (udta != null) { udtaMetadata = AtomParsers.parseUdta(udta, isQuickTime); if (udtaMetadata != null) { gaplessInfoHolder.setFromMetadata(udtaMetadata); } } - Metadata mdtaMetadata = null; - Atom.ContainerAtom meta = moov.getContainerAtomOfType(Atom.TYPE_meta); + @Nullable Metadata mdtaMetadata = null; + @Nullable Atom.ContainerAtom meta = moov.getContainerAtomOfType(Atom.TYPE_meta); if (meta != null) { mdtaMetadata = AtomParsers.parseMdtaFromMeta(meta); } @@ -453,6 +458,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { if (atom.type != Atom.TYPE_trak) { continue; } + @Nullable Track track = AtomParsers.parseTrak( atom, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java index b9ecaf174c..b4f537f0ce 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java @@ -49,8 +49,6 @@ public final class PsshAtomUtil { * @param data The scheme specific data. * @return The PSSH atom. */ - // dereference of possibly-null reference keyId - @SuppressWarnings({"ParameterNotNullable", "nullness:dereference.of.nullable"}) public static byte[] buildPsshAtom( UUID systemId, @Nullable UUID[] keyIds, @Nullable byte[] data) { int dataLength = data != null ? data.length : 0; @@ -97,8 +95,9 @@ public final class PsshAtomUtil { * @return The parsed UUID. Null if the input is not a valid PSSH atom, or if the PSSH atom has an * unsupported version. */ - public static @Nullable UUID parseUuid(byte[] atom) { - PsshAtom parsedAtom = parsePsshAtom(atom); + @Nullable + public static UUID parseUuid(byte[] atom) { + @Nullable PsshAtom parsedAtom = parsePsshAtom(atom); if (parsedAtom == null) { return null; } @@ -115,7 +114,7 @@ public final class PsshAtomUtil { * an unsupported version. */ public static int parseVersion(byte[] atom) { - PsshAtom parsedAtom = parsePsshAtom(atom); + @Nullable PsshAtom parsedAtom = parsePsshAtom(atom); if (parsedAtom == null) { return -1; } @@ -133,8 +132,9 @@ public final class PsshAtomUtil { * @return The parsed scheme specific data. Null if the input is not a valid PSSH atom, or if the * PSSH atom has an unsupported version, or if the PSSH atom does not match the passed UUID. */ - public static @Nullable byte[] parseSchemeSpecificData(byte[] atom, UUID uuid) { - PsshAtom parsedAtom = parsePsshAtom(atom); + @Nullable + public static byte[] parseSchemeSpecificData(byte[] atom, UUID uuid) { + @Nullable PsshAtom parsedAtom = parsePsshAtom(atom); if (parsedAtom == null) { return null; } @@ -153,7 +153,8 @@ public final class PsshAtomUtil { * has an unsupported version. */ // TODO: Support parsing of the key ids for version 1 PSSH atoms. - private static @Nullable PsshAtom parsePsshAtom(byte[] atom) { + @Nullable + private static PsshAtom parsePsshAtom(byte[] atom) { ParsableByteArray atomData = new ParsableByteArray(atom); if (atomData.limit() < Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */) { // Data too short. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java index 51ec2bf282..b4ad59127b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java @@ -15,19 +15,19 @@ */ package com.google.android.exoplayer2.extractor.mp4; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A holder for information corresponding to a single fragment of an mp4 file. */ /* package */ final class TrackFragment { - /** - * The default values for samples from the track fragment header. - */ - public DefaultSampleValues header; + /** The default values for samples from the track fragment header. */ + @MonotonicNonNull public DefaultSampleValues header; /** * The position (byte offset) of the start of fragment. */ @@ -81,20 +81,13 @@ import java.io.IOException; * Undefined otherwise. */ public boolean[] sampleHasSubsampleEncryptionTable; - /** - * Fragment specific track encryption. May be null. - */ - public TrackEncryptionBox trackEncryptionBox; - /** - * If {@link #definesEncryptionData} is true, indicates the length of the sample encryption data. - * Undefined otherwise. - */ - public int sampleEncryptionDataLength; + /** Fragment specific track encryption. May be null. */ + @Nullable public TrackEncryptionBox trackEncryptionBox; /** * If {@link #definesEncryptionData} is true, contains binary sample encryption data. Undefined * otherwise. */ - public ParsableByteArray sampleEncryptionData; + public final ParsableByteArray sampleEncryptionData; /** * Whether {@link #sampleEncryptionData} needs populating with the actual encryption data. */ @@ -104,6 +97,17 @@ import java.io.IOException; */ public long nextFragmentDecodeTime; + public TrackFragment() { + trunDataPosition = new long[0]; + trunLength = new int[0]; + sampleSizeTable = new int[0]; + sampleCompositionTimeOffsetTable = new int[0]; + sampleDecodingTimeTable = new long[0]; + sampleIsSyncFrameTable = new boolean[0]; + sampleHasSubsampleEncryptionTable = new boolean[0]; + sampleEncryptionData = new ParsableByteArray(); + } + /** * Resets the fragment. *

      @@ -130,11 +134,11 @@ import java.io.IOException; public void initTables(int trunCount, int sampleCount) { this.trunCount = trunCount; this.sampleCount = sampleCount; - if (trunLength == null || trunLength.length < trunCount) { + if (trunLength.length < trunCount) { trunDataPosition = new long[trunCount]; trunLength = new int[trunCount]; } - if (sampleSizeTable == null || sampleSizeTable.length < sampleCount) { + if (sampleSizeTable.length < sampleCount) { // Size the tables 25% larger than needed, so as to make future resize operations less // likely. The choice of 25% is relatively arbitrary. int tableSize = (sampleCount * 125) / 100; @@ -148,18 +152,14 @@ import java.io.IOException; /** * Configures the fragment to be one that defines encryption data of the specified length. - *

      - * {@link #definesEncryptionData} is set to true, {@link #sampleEncryptionDataLength} is set to - * the specified length, and {@link #sampleEncryptionData} is resized if necessary such that it - * is at least this length. + * + *

      {@link #definesEncryptionData} is set to true, and the {@link ParsableByteArray#limit() + * limit} of {@link #sampleEncryptionData} is set to the specified length. * * @param length The length in bytes of the encryption data. */ public void initEncryptionData(int length) { - if (sampleEncryptionData == null || sampleEncryptionData.limit() < length) { - sampleEncryptionData = new ParsableByteArray(length); - } - sampleEncryptionDataLength = length; + sampleEncryptionData.reset(length); definesEncryptionData = true; sampleEncryptionDataNeedsFill = true; } @@ -170,7 +170,7 @@ import java.io.IOException; * @param input An {@link ExtractorInput} from which to read the encryption data. */ public void fillEncryptionData(ExtractorInput input) throws IOException, InterruptedException { - input.readFully(sampleEncryptionData.data, 0, sampleEncryptionDataLength); + input.readFully(sampleEncryptionData.data, 0, sampleEncryptionData.limit()); sampleEncryptionData.setPosition(0); sampleEncryptionDataNeedsFill = false; } @@ -181,7 +181,7 @@ import java.io.IOException; * @param source A source from which to read the encryption data. */ public void fillEncryptionData(ParsableByteArray source) { - source.readBytes(sampleEncryptionData.data, 0, sampleEncryptionDataLength); + source.readBytes(sampleEncryptionData.data, 0, sampleEncryptionData.limit()); sampleEncryptionData.setPosition(0); sampleEncryptionDataNeedsFill = false; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/package-info.java new file mode 100644 index 0000000000..6d0ad27361 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/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.extractor.mp4; + +import com.google.android.exoplayer2.util.NonNullApi; From a8d39c1180f5f74e1bafa4bd91bb4948bfda8d98 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 18 Dec 2019 16:35:43 +0000 Subject: [PATCH 0554/1335] Read arrays directly from Parcel PiperOrigin-RevId: 286197990 --- .../metadata/scte35/PrivateCommand.java | 5 +-- .../exoplayer2/offline/DownloadRequest.java | 4 +- .../metadata/MetadataRendererTest.java | 1 - .../exoplayer2/metadata/MetadataTest.java | 45 +++++++++++++++++++ 4 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java index 4334fa99cb..44850b720f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.metadata.scte35; import android.os.Parcel; import android.os.Parcelable; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; /** * Represents a private command as defined in SCTE35, Section 9.3.6. @@ -46,8 +47,7 @@ public final class PrivateCommand extends SpliceCommand { private PrivateCommand(Parcel in) { ptsAdjustment = in.readLong(); identifier = in.readLong(); - commandBytes = new byte[in.readInt()]; - in.readByteArray(commandBytes); + commandBytes = Util.castNonNull(in.createByteArray()); } /* package */ static PrivateCommand parseFromSection(ParsableByteArray sectionData, @@ -64,7 +64,6 @@ public final class PrivateCommand extends SpliceCommand { public void writeToParcel(Parcel dest, int flags) { dest.writeLong(ptsAdjustment); dest.writeLong(identifier); - dest.writeInt(commandBytes.length); dest.writeByteArray(commandBytes); } 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 7ff43ceacd..988b908140 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 @@ -100,8 +100,7 @@ public final class DownloadRequest implements Parcelable { } streamKeys = Collections.unmodifiableList(mutableStreamKeys); customCacheKey = in.readString(); - data = new byte[in.readInt()]; - in.readByteArray(data); + data = castNonNull(in.createByteArray()); } /** @@ -194,7 +193,6 @@ public final class DownloadRequest implements Parcelable { dest.writeParcelable(streamKeys.get(i), /* parcelableFlags= */ 0); } dest.writeString(customCacheKey); - dest.writeInt(data.length); dest.writeByteArray(data); } 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 1ad0ce6b79..97e9a75686 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 @@ -12,7 +12,6 @@ * 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; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataTest.java new file mode 100644 index 0000000000..8331bce947 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataTest.java @@ -0,0 +1,45 @@ +/* + * 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 android.os.Parcel; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.metadata.id3.BinaryFrame; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link Metadata}. */ +@RunWith(AndroidJUnit4.class) +public class MetadataTest { + + @Test + public void testParcelable() { + Metadata metadataToParcel = + new Metadata( + new BinaryFrame("id1", new byte[] {1}), new BinaryFrame("id2", new byte[] {2})); + + Parcel parcel = Parcel.obtain(); + metadataToParcel.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + Metadata metadataFromParcel = Metadata.CREATOR.createFromParcel(parcel); + assertThat(metadataFromParcel).isEqualTo(metadataToParcel); + + parcel.recycle(); + } +} From 8cf4042ddd8f83d37c025023e533b55a7f68a061 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 18 Dec 2019 16:48:10 +0000 Subject: [PATCH 0555/1335] Move WebvttCueInfo.Builder inside WebvttCueParser This class is only used to hold temporary data while we parse the settings and text, so we don't need it outside the Parser class. Also remove all state from WebvttCueParser - this increases the number of allocations, but there are already many and subtitles generally aren't very frequent (compared to e.g. video frames). PiperOrigin-RevId: 286200002 --- .../text/webvtt/Mp4WebvttDecoder.java | 25 +- .../exoplayer2/text/webvtt/WebvttCueInfo.java | 285 +------------- .../text/webvtt/WebvttCueParser.java | 356 ++++++++++++++---- .../exoplayer2/text/webvtt/WebvttDecoder.java | 18 +- .../text/webvtt/WebvttSubtitle.java | 12 +- .../text/webvtt/Mp4WebvttDecoderTest.java | 6 +- .../text/webvtt/WebvttCueParserTest.java | 10 +- .../text/webvtt/WebvttSubtitleTest.java | 214 +++++------ 8 files changed, 409 insertions(+), 517 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java index 81ff0fdd65..82023e6c58 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text.webvtt; +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,12 +42,10 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { private static final int TYPE_vttc = 0x76747463; private final ParsableByteArray sampleData; - private final WebvttCueInfo.Builder builder; public Mp4WebvttDecoder() { super("Mp4WebvttDecoder"); sampleData = new ParsableByteArray(); - builder = new WebvttCueInfo.Builder(); } @Override @@ -63,7 +62,7 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { int boxSize = sampleData.readInt(); int boxType = sampleData.readInt(); if (boxType == TYPE_vttc) { - resultingCueList.add(parseVttCueBox(sampleData, builder, boxSize - BOX_HEADER_SIZE)); + resultingCueList.add(parseVttCueBox(sampleData, boxSize - BOX_HEADER_SIZE)); } else { // Peers of the VTTCueBox are still not supported and are skipped. sampleData.skipBytes(boxSize - BOX_HEADER_SIZE); @@ -72,10 +71,10 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { return new Mp4WebvttSubtitle(resultingCueList); } - private static Cue parseVttCueBox( - ParsableByteArray sampleData, WebvttCueInfo.Builder builder, int remainingCueBoxBytes) + private static Cue parseVttCueBox(ParsableByteArray sampleData, int remainingCueBoxBytes) throws SubtitleDecoderException { - builder.reset(); + @Nullable Cue.Builder cueBuilder = null; + @Nullable CharSequence cueText = null; while (remainingCueBoxBytes > 0) { if (remainingCueBoxBytes < BOX_HEADER_SIZE) { throw new SubtitleDecoderException("Incomplete vtt cue box header found."); @@ -89,14 +88,20 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { sampleData.skipBytes(payloadLength); remainingCueBoxBytes -= payloadLength; if (boxType == TYPE_sttg) { - WebvttCueParser.parseCueSettingsList(boxPayload, builder); + cueBuilder = WebvttCueParser.parseCueSettingsList(boxPayload); } else if (boxType == TYPE_payl) { - WebvttCueParser.parseCueText(null, boxPayload.trim(), builder, Collections.emptyList()); + cueText = + WebvttCueParser.parseCueText( + /* id= */ null, boxPayload.trim(), /* styles= */ Collections.emptyList()); } else { // Other VTTCueBox children are still not supported and are ignored. } } - return builder.build().cue; + if (cueText == null) { + cueText = ""; + } + return cueBuilder != null + ? cueBuilder.setText(cueText).build() + : WebvttCueParser.newCueForText(cueText); } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueInfo.java index c57d14ac5c..b04e06d744 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueInfo.java @@ -15,296 +15,19 @@ */ package com.google.android.exoplayer2.text.webvtt; -import static java.lang.annotation.RetentionPolicy.SOURCE; -import android.text.Layout.Alignment; -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Log; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; /** A representation of a WebVTT cue. */ public final class WebvttCueInfo { - /* package */ static final float DEFAULT_POSITION = 0.5f; - public final Cue cue; public final long startTime; public final long endTime; - private WebvttCueInfo( - long startTime, - long endTime, - CharSequence text, - @Nullable Alignment textAlignment, - float line, - @Cue.LineType int lineType, - @Cue.AnchorType int lineAnchor, - float position, - @Cue.AnchorType int positionAnchor, - float width) { - this.cue = - new Cue(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, width); - this.startTime = startTime; - this.endTime = endTime; - } - - /** Builder for WebVTT cues. */ - @SuppressWarnings("hiding") - public static class Builder { - - /** - * Valid values for {@link #setTextAlignment(int)}. - * - *

      We use a custom list (and not {@link Alignment} directly) in order to include both {@code - * START}/{@code LEFT} and {@code END}/{@code RIGHT}. The distinction is important for {@link - * #derivePosition(int)}. - * - *

      These correspond to the valid values for the 'align' cue setting in the WebVTT spec. - */ - @Documented - @Retention(SOURCE) - @IntDef({ - TextAlignment.START, - TextAlignment.CENTER, - TextAlignment.END, - TextAlignment.LEFT, - TextAlignment.RIGHT - }) - public @interface TextAlignment { - /** - * See WebVTT's align:start. - */ - int START = 1; - /** - * See WebVTT's align:center. - */ - int CENTER = 2; - /** - * See WebVTT's align:end. - */ - int END = 3; - /** - * See WebVTT's align:left. - */ - int LEFT = 4; - /** - * See WebVTT's align:right. - */ - int RIGHT = 5; - } - - private static final String TAG = "WebvttCueBuilder"; - - private long startTime; - private long endTime; - @Nullable private CharSequence text; - @TextAlignment private int textAlignment; - private float line; - // Equivalent to WebVTT's snap-to-lines flag: - // https://www.w3.org/TR/webvtt1/#webvtt-cue-snap-to-lines-flag - @Cue.LineType private int lineType; - @Cue.AnchorType private int lineAnchor; - private float position; - @Cue.AnchorType private int positionAnchor; - private float width; - - // Initialization methods - - // Calling reset() is forbidden because `this` isn't initialized. This can be safely - // suppressed because reset() only assigns fields, it doesn't read any. - @SuppressWarnings("nullness:method.invocation.invalid") - public Builder() { - reset(); - } - - public void reset() { - startTime = 0; - endTime = 0; - text = null; - // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment - textAlignment = TextAlignment.CENTER; - line = Cue.DIMEN_UNSET; - // Defaults to NUMBER (true): https://www.w3.org/TR/webvtt1/#webvtt-cue-snap-to-lines-flag - lineType = Cue.LINE_TYPE_NUMBER; - // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-line-alignment - lineAnchor = Cue.ANCHOR_TYPE_START; - position = Cue.DIMEN_UNSET; - positionAnchor = Cue.TYPE_UNSET; - // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-size - width = 1.0f; - } - - // Construction methods. - - public WebvttCueInfo build() { - line = computeLine(line, lineType); - - if (position == Cue.DIMEN_UNSET) { - position = derivePosition(textAlignment); - } - - if (positionAnchor == Cue.TYPE_UNSET) { - positionAnchor = derivePositionAnchor(textAlignment); - } - - width = Math.min(width, deriveMaxSize(positionAnchor, position)); - - return new WebvttCueInfo( - startTime, - endTime, - Assertions.checkNotNull(text), - convertTextAlignment(textAlignment), - line, - lineType, - lineAnchor, - position, - positionAnchor, - width); - } - - public Builder setStartTime(long time) { - startTime = time; - return this; - } - - public Builder setEndTime(long time) { - endTime = time; - return this; - } - - public Builder setText(CharSequence text) { - this.text = text; - return this; - } - - public Builder setTextAlignment(@TextAlignment int textAlignment) { - this.textAlignment = textAlignment; - return this; - } - - public Builder setLine(float line) { - this.line = line; - return this; - } - - public Builder setLineType(@Cue.LineType int lineType) { - this.lineType = lineType; - return this; - } - - public Builder setLineAnchor(@Cue.AnchorType int lineAnchor) { - this.lineAnchor = lineAnchor; - return this; - } - - public Builder setPosition(float position) { - this.position = position; - return this; - } - - public Builder setPositionAnchor(@Cue.AnchorType int positionAnchor) { - this.positionAnchor = positionAnchor; - return this; - } - - public Builder setWidth(float width) { - this.width = width; - return this; - } - - // https://www.w3.org/TR/webvtt1/#webvtt-cue-line - private static float computeLine(float line, @Cue.LineType int lineType) { - if (line != Cue.DIMEN_UNSET - && lineType == Cue.LINE_TYPE_FRACTION - && (line < 0.0f || line > 1.0f)) { - return 1.0f; // Step 1 - } else if (line != Cue.DIMEN_UNSET) { - // Step 2: Do nothing, line is already correct. - return line; - } else if (lineType == Cue.LINE_TYPE_FRACTION) { - return 1.0f; // Step 3 - } else { - // Steps 4 - 10 (stacking multiple simultaneous cues) are handled by - // WebvttSubtitle.getCues(long) and WebvttSubtitle.isNormal(Cue). - return Cue.DIMEN_UNSET; - } - } - - // https://www.w3.org/TR/webvtt1/#webvtt-cue-position - private static float derivePosition(@TextAlignment int textAlignment) { - switch (textAlignment) { - case TextAlignment.LEFT: - return 0.0f; - case TextAlignment.RIGHT: - return 1.0f; - case TextAlignment.START: - case TextAlignment.CENTER: - case TextAlignment.END: - default: - return DEFAULT_POSITION; - } - } - - // https://www.w3.org/TR/webvtt1/#webvtt-cue-position-alignment - @Cue.AnchorType - private static int derivePositionAnchor(@TextAlignment int textAlignment) { - switch (textAlignment) { - case TextAlignment.LEFT: - case TextAlignment.START: - return Cue.ANCHOR_TYPE_START; - case TextAlignment.RIGHT: - case TextAlignment.END: - return Cue.ANCHOR_TYPE_END; - case TextAlignment.CENTER: - default: - return Cue.ANCHOR_TYPE_MIDDLE; - } - } - - @Nullable - private static Alignment convertTextAlignment(@TextAlignment int textAlignment) { - switch (textAlignment) { - case TextAlignment.START: - case TextAlignment.LEFT: - return Alignment.ALIGN_NORMAL; - case TextAlignment.CENTER: - return Alignment.ALIGN_CENTER; - case TextAlignment.END: - case TextAlignment.RIGHT: - return Alignment.ALIGN_OPPOSITE; - default: - Log.w(TAG, "Unknown textAlignment: " + textAlignment); - return null; - } - } - - // Step 2 here: https://www.w3.org/TR/webvtt1/#processing-cue-settings - private static float deriveMaxSize(@Cue.AnchorType int positionAnchor, float position) { - switch (positionAnchor) { - case Cue.ANCHOR_TYPE_START: - return 1.0f - position; - case Cue.ANCHOR_TYPE_END: - return position; - case Cue.ANCHOR_TYPE_MIDDLE: - if (position <= 0.5f) { - return position * 2; - } else { - return (1.0f - position) * 2; - } - case Cue.TYPE_UNSET: - default: - throw new IllegalStateException(String.valueOf(positionAnchor)); - } - } + public WebvttCueInfo(Cue cue, long startTimeUs, long endTimeUs) { + this.cue = cue; + this.startTime = startTimeUs; + this.endTime = endTimeUs; } } 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 e6fa78ca65..565d324828 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 @@ -15,11 +15,14 @@ */ package com.google.android.exoplayer2.text.webvtt; +import static java.lang.annotation.RetentionPolicy.SOURCE; + import android.graphics.Typeface; import android.text.Layout; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; +import android.text.SpannedString; import android.text.TextUtils; import android.text.style.AbsoluteSizeSpan; import android.text.style.AlignmentSpan; @@ -30,6 +33,7 @@ import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; +import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; @@ -37,19 +41,70 @@ 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.lang.annotation.Documented; +import java.lang.annotation.Retention; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues) */ public final class WebvttCueParser { + /** + * Valid values for {@link WebvttCueInfoBuilder#textAlignment}. + * + *

      We use a custom list (and not {@link Layout.Alignment} directly) in order to include both + * {@code START}/{@code LEFT} and {@code END}/{@code RIGHT}. The distinction is important for + * {@link WebvttCueInfoBuilder#derivePosition(int)}. + * + *

      These correspond to the valid values for the 'align' cue setting in the WebVTT spec. + */ + @Documented + @Retention(SOURCE) + @IntDef({ + TEXT_ALIGNMENT_START, + TEXT_ALIGNMENT_CENTER, + TEXT_ALIGNMENT_END, + TEXT_ALIGNMENT_LEFT, + TEXT_ALIGNMENT_RIGHT + }) + private @interface TextAlignment {} + + /** + * See WebVTT's align:start. + */ + private static final int TEXT_ALIGNMENT_START = 1; + + /** + * See WebVTT's align:center. + */ + private static final int TEXT_ALIGNMENT_CENTER = 2; + + /** + * See WebVTT's align:end. + */ + private static final int TEXT_ALIGNMENT_END = 3; + + /** + * See WebVTT's align:left. + */ + private static final int TEXT_ALIGNMENT_LEFT = 4; + + /** + * See WebVTT's align:right. + */ + private static final int TEXT_ALIGNMENT_RIGHT = 5; + public static final Pattern CUE_HEADER_PATTERN = Pattern .compile("^(\\S+)\\s+-->\\s+(\\S+)(.*)?$"); - private static final Pattern CUE_SETTING_PATTERN = Pattern.compile("(\\S+?):(\\S+)"); private static final char CHAR_LESS_THAN = '<'; @@ -74,92 +129,70 @@ public final class WebvttCueParser { private static final int STYLE_BOLD = Typeface.BOLD; private static final int STYLE_ITALIC = Typeface.ITALIC; + /* package */ static final float DEFAULT_POSITION = 0.5f; + private static final String TAG = "WebvttCueParser"; - private final StringBuilder textBuilder; - - public WebvttCueParser() { - textBuilder = new StringBuilder(); - } - /** * Parses the next valid WebVTT cue in a parsable array, including timestamps, settings and text. * * @param webvttData Parsable WebVTT file data. - * @param builder Builder for WebVTT Cues (output parameter). * @param styles List of styles defined by the CSS style blocks preceding the cues. - * @return Whether a valid Cue was found. + * @return The parsed cue info, or null if no valid cue was found. */ - public boolean parseCue( - ParsableByteArray webvttData, WebvttCueInfo.Builder builder, List styles) { + @Nullable + public static WebvttCueInfo parseCue(ParsableByteArray webvttData, List styles) { @Nullable String firstLine = webvttData.readLine(); if (firstLine == null) { - return false; + return null; } Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(firstLine); if (cueHeaderMatcher.matches()) { // We have found the timestamps in the first line. No id present. - return parseCue(null, cueHeaderMatcher, webvttData, builder, textBuilder, styles); + return parseCue(null, cueHeaderMatcher, webvttData, styles); } // The first line is not the timestamps, but could be the cue id. @Nullable String secondLine = webvttData.readLine(); if (secondLine == null) { - return false; + return null; } cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(secondLine); if (cueHeaderMatcher.matches()) { // We can do the rest of the parsing, including the id. - return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, builder, textBuilder, - styles); + return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, styles); } - return false; + return null; } /** * Parses a string containing a list of cue settings. * * @param cueSettingsList String containing the settings for a given cue. - * @param builder The {@link WebvttCueInfo.Builder} where incremental construction takes place. + * @return The cue settings parsed into a {@link Cue.Builder}. */ - /* package */ static void parseCueSettingsList( - String cueSettingsList, WebvttCueInfo.Builder builder) { - // Parse the cue settings list. - Matcher cueSettingMatcher = CUE_SETTING_PATTERN.matcher(cueSettingsList); - while (cueSettingMatcher.find()) { - String name = cueSettingMatcher.group(1); - String value = cueSettingMatcher.group(2); - try { - if ("line".equals(name)) { - parseLineAttribute(value, builder); - } else if ("align".equals(name)) { - builder.setTextAlignment(parseTextAlignment(value)); - } else if ("position".equals(name)) { - parsePositionAttribute(value, builder); - } else if ("size".equals(name)) { - builder.setWidth(WebvttParserUtil.parsePercentage(value)); - } else { - Log.w(TAG, "Unknown cue setting " + name + ":" + value); - } - } catch (NumberFormatException e) { - Log.w(TAG, "Skipping bad cue setting: " + cueSettingMatcher.group()); - } - } + /* package */ static Cue.Builder parseCueSettingsList(String cueSettingsList) { + WebvttCueInfoBuilder builder = new WebvttCueInfoBuilder(); + parseCueSettingsList(cueSettingsList, builder); + return builder.toCueBuilder(); + } + + /** Create a new {@link Cue} containing {@code text} and with WebVTT default values. */ + /* package */ static Cue newCueForText(CharSequence text) { + WebvttCueInfoBuilder infoBuilder = new WebvttCueInfoBuilder(); + infoBuilder.text = text; + return infoBuilder.toCueBuilder().build(); } /** - * Parses the text payload of a WebVTT Cue and applies modifications on {@link - * WebvttCueInfo.Builder}. + * Parses the text payload of a WebVTT Cue and returns it as a styled {@link SpannedString}. * - * @param id Id of the cue, {@code null} if it is not present. + * @param id ID of the cue, {@code null} if it is not present. * @param markup The markup text to be parsed. * @param styles List of styles defined by the CSS style blocks preceding the cues. - * @param builder Output builder. + * @return The styled cue text. */ - /* package */ static void parseCueText( - @Nullable String id, - String markup, - WebvttCueInfo.Builder builder, - List styles) { + /* package */ static SpannedString parseCueText( + @Nullable String id, String markup, List styles) { SpannableStringBuilder spannedText = new SpannableStringBuilder(); ArrayDeque startTagStack = new ArrayDeque<>(); List scratchStyleMatches = new ArrayList<>(); @@ -227,29 +260,31 @@ public final class WebvttCueParser { } applySpansForTag(id, StartTag.buildWholeCueVirtualTag(), spannedText, styles, scratchStyleMatches); - builder.setText(spannedText); + return SpannedString.valueOf(spannedText); } - private static boolean parseCue( + // Internal methods + + @Nullable + private static WebvttCueInfo parseCue( @Nullable String id, Matcher cueHeaderMatcher, ParsableByteArray webvttData, - WebvttCueInfo.Builder builder, - StringBuilder textBuilder, List styles) { + WebvttCueInfoBuilder builder = new WebvttCueInfoBuilder(); try { // Parse the cue start and end times. - builder.setStartTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1))) - .setEndTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(2))); + builder.startTimeUs = WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1)); + builder.endTimeUs = WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(2)); } catch (NumberFormatException e) { Log.w(TAG, "Skipping cue with bad header: " + cueHeaderMatcher.group()); - return false; + return null; } parseCueSettingsList(cueHeaderMatcher.group(3), builder); // Parse the cue text. - textBuilder.setLength(0); + StringBuilder textBuilder = new StringBuilder(); for (String line = webvttData.readLine(); !TextUtils.isEmpty(line); line = webvttData.readLine()) { @@ -258,20 +293,44 @@ public final class WebvttCueParser { } textBuilder.append(line.trim()); } - parseCueText(id, textBuilder.toString(), builder, styles); - return true; + builder.text = parseCueText(id, textBuilder.toString(), styles); + return builder.build(); } - // Internal methods + private static void parseCueSettingsList(String cueSettingsList, WebvttCueInfoBuilder builder) { + // Parse the cue settings list. + Matcher cueSettingMatcher = CUE_SETTING_PATTERN.matcher(cueSettingsList); - private static void parseLineAttribute(String s, WebvttCueInfo.Builder builder) { + while (cueSettingMatcher.find()) { + String name = cueSettingMatcher.group(1); + String value = cueSettingMatcher.group(2); + try { + if ("line".equals(name)) { + parseLineAttribute(value, builder); + } else if ("align".equals(name)) { + builder.textAlignment = parseTextAlignment(value); + } else if ("position".equals(name)) { + parsePositionAttribute(value, builder); + } else if ("size".equals(name)) { + builder.size = WebvttParserUtil.parsePercentage(value); + } else { + Log.w(TAG, "Unknown cue setting " + name + ":" + value); + } + } catch (NumberFormatException e) { + Log.w(TAG, "Skipping bad cue setting: " + cueSettingMatcher.group()); + } + } + } + + private static void parseLineAttribute(String s, WebvttCueInfoBuilder builder) { int commaIndex = s.indexOf(','); if (commaIndex != -1) { - builder.setLineAnchor(parsePositionAnchor(s.substring(commaIndex + 1))); + builder.lineAnchor = parsePositionAnchor(s.substring(commaIndex + 1)); s = s.substring(0, commaIndex); } if (s.endsWith("%")) { - builder.setLine(WebvttParserUtil.parsePercentage(s)).setLineType(Cue.LINE_TYPE_FRACTION); + builder.line = WebvttParserUtil.parsePercentage(s); + builder.lineType = Cue.LINE_TYPE_FRACTION; } else { int lineNumber = Integer.parseInt(s); if (lineNumber < 0) { @@ -279,17 +338,18 @@ public final class WebvttCueParser { // Cue defines it to be the first row that's not visible. lineNumber--; } - builder.setLine(lineNumber).setLineType(Cue.LINE_TYPE_NUMBER); + builder.line = lineNumber; + builder.lineType = Cue.LINE_TYPE_NUMBER; } } - private static void parsePositionAttribute(String s, WebvttCueInfo.Builder builder) { + private static void parsePositionAttribute(String s, WebvttCueInfoBuilder builder) { int commaIndex = s.indexOf(','); if (commaIndex != -1) { - builder.setPositionAnchor(parsePositionAnchor(s.substring(commaIndex + 1))); + builder.positionAnchor = parsePositionAnchor(s.substring(commaIndex + 1)); s = s.substring(0, commaIndex); } - builder.setPosition(WebvttParserUtil.parsePercentage(s)); + builder.position = WebvttParserUtil.parsePercentage(s); } @Cue.AnchorType @@ -308,24 +368,24 @@ public final class WebvttCueParser { } } - @WebvttCueInfo.Builder.TextAlignment + @TextAlignment private static int parseTextAlignment(String s) { switch (s) { case "start": - return WebvttCueInfo.Builder.TextAlignment.START; + return TEXT_ALIGNMENT_START; case "left": - return WebvttCueInfo.Builder.TextAlignment.LEFT; + return TEXT_ALIGNMENT_LEFT; case "center": case "middle": - return WebvttCueInfo.Builder.TextAlignment.CENTER; + return TEXT_ALIGNMENT_CENTER; case "end": - return WebvttCueInfo.Builder.TextAlignment.END; + return TEXT_ALIGNMENT_END; case "right": - return WebvttCueInfo.Builder.TextAlignment.RIGHT; + return TEXT_ALIGNMENT_RIGHT; default: Log.w(TAG, "Invalid alignment value: " + s); // Default value: https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment - return WebvttCueInfo.Builder.TextAlignment.CENTER; + return TEXT_ALIGNMENT_CENTER; } } @@ -490,6 +550,151 @@ public final class WebvttCueParser { Collections.sort(output); } + private static final class WebvttCueInfoBuilder { + + public long startTimeUs; + public long endTimeUs; + public @MonotonicNonNull CharSequence text; + @TextAlignment public int textAlignment; + public float line; + // Equivalent to WebVTT's snap-to-lines flag: + // https://www.w3.org/TR/webvtt1/#webvtt-cue-snap-to-lines-flag + @Cue.LineType public int lineType; + @Cue.AnchorType public int lineAnchor; + public float position; + @Cue.AnchorType public int positionAnchor; + public float size; + + public WebvttCueInfoBuilder() { + startTimeUs = 0; + endTimeUs = 0; + // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment + textAlignment = TEXT_ALIGNMENT_CENTER; + line = Cue.DIMEN_UNSET; + // Defaults to NUMBER (true): https://www.w3.org/TR/webvtt1/#webvtt-cue-snap-to-lines-flag + lineType = Cue.LINE_TYPE_NUMBER; + // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-line-alignment + lineAnchor = Cue.ANCHOR_TYPE_START; + position = Cue.DIMEN_UNSET; + positionAnchor = Cue.TYPE_UNSET; + // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-size + size = 1.0f; + } + + public WebvttCueInfo build() { + return new WebvttCueInfo(toCueBuilder().build(), startTimeUs, endTimeUs); + } + + public Cue.Builder toCueBuilder() { + float position = + this.position != Cue.DIMEN_UNSET ? this.position : derivePosition(textAlignment); + @Cue.AnchorType + int positionAnchor = + this.positionAnchor != Cue.TYPE_UNSET + ? this.positionAnchor + : derivePositionAnchor(textAlignment); + Cue.Builder cueBuilder = + new Cue.Builder() + .setTextAlignment(convertTextAlignment(textAlignment)) + .setLine(computeLine(line, lineType), lineType) + .setLineAnchor(lineAnchor) + .setPosition(position) + .setPositionAnchor(positionAnchor) + .setSize(Math.min(size, deriveMaxSize(positionAnchor, position))); + + if (text != null) { + cueBuilder.setText(text); + } + + return cueBuilder; + } + + // https://www.w3.org/TR/webvtt1/#webvtt-cue-line + private static float computeLine(float line, @Cue.LineType int lineType) { + if (line != Cue.DIMEN_UNSET + && lineType == Cue.LINE_TYPE_FRACTION + && (line < 0.0f || line > 1.0f)) { + return 1.0f; // Step 1 + } else if (line != Cue.DIMEN_UNSET) { + // Step 2: Do nothing, line is already correct. + return line; + } else if (lineType == Cue.LINE_TYPE_FRACTION) { + return 1.0f; // Step 3 + } else { + // Steps 4 - 10 (stacking multiple simultaneous cues) are handled by + // WebvttSubtitle.getCues(long) and WebvttSubtitle.isNormal(Cue). + return Cue.DIMEN_UNSET; + } + } + + // https://www.w3.org/TR/webvtt1/#webvtt-cue-position + private static float derivePosition(@TextAlignment int textAlignment) { + switch (textAlignment) { + case TEXT_ALIGNMENT_LEFT: + return 0.0f; + case TEXT_ALIGNMENT_RIGHT: + return 1.0f; + case TEXT_ALIGNMENT_START: + case TEXT_ALIGNMENT_CENTER: + case TEXT_ALIGNMENT_END: + default: + return DEFAULT_POSITION; + } + } + + // https://www.w3.org/TR/webvtt1/#webvtt-cue-position-alignment + @Cue.AnchorType + private static int derivePositionAnchor(@TextAlignment int textAlignment) { + switch (textAlignment) { + case TEXT_ALIGNMENT_LEFT: + case TEXT_ALIGNMENT_START: + return Cue.ANCHOR_TYPE_START; + case TEXT_ALIGNMENT_RIGHT: + case TEXT_ALIGNMENT_END: + return Cue.ANCHOR_TYPE_END; + case TEXT_ALIGNMENT_CENTER: + default: + return Cue.ANCHOR_TYPE_MIDDLE; + } + } + + @Nullable + private static Layout.Alignment convertTextAlignment(@TextAlignment int textAlignment) { + switch (textAlignment) { + case TEXT_ALIGNMENT_START: + case TEXT_ALIGNMENT_LEFT: + return Layout.Alignment.ALIGN_NORMAL; + case TEXT_ALIGNMENT_CENTER: + return Layout.Alignment.ALIGN_CENTER; + case TEXT_ALIGNMENT_END: + case TEXT_ALIGNMENT_RIGHT: + return Layout.Alignment.ALIGN_OPPOSITE; + default: + Log.w(TAG, "Unknown textAlignment: " + textAlignment); + return null; + } + } + + // Step 2 here: https://www.w3.org/TR/webvtt1/#processing-cue-settings + private static float deriveMaxSize(@Cue.AnchorType int positionAnchor, float position) { + switch (positionAnchor) { + case Cue.ANCHOR_TYPE_START: + return 1.0f - position; + case Cue.ANCHOR_TYPE_END: + return position; + case Cue.ANCHOR_TYPE_MIDDLE: + if (position <= 0.5f) { + return position * 2; + } else { + return (1.0f - position) * 2; + } + case Cue.TYPE_UNSET: + default: + throw new IllegalStateException(String.valueOf(positionAnchor)); + } + } + } + private static final class StyleMatch implements Comparable { public final int score; @@ -550,5 +755,4 @@ public final class WebvttCueParser { } } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java index 6c1d61d126..fe36770aee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.text.webvtt; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.text.Subtitle; @@ -40,28 +41,20 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder { private static final String COMMENT_START = "NOTE"; private static final String STYLE_START = "STYLE"; - private final WebvttCueParser cueParser; private final ParsableByteArray parsableWebvttData; - private final WebvttCueInfo.Builder webvttCueBuilder; private final CssParser cssParser; - private final List definedStyles; public WebvttDecoder() { super("WebvttDecoder"); - cueParser = new WebvttCueParser(); parsableWebvttData = new ParsableByteArray(); - webvttCueBuilder = new WebvttCueInfo.Builder(); cssParser = new CssParser(); - definedStyles = new ArrayList<>(); } @Override protected Subtitle decode(byte[] bytes, int length, boolean reset) throws SubtitleDecoderException { parsableWebvttData.reset(bytes, length); - // Initialization for consistent starting state. - webvttCueBuilder.reset(); - definedStyles.clear(); + List definedStyles = new ArrayList<>(); // Validate the first line of the header, and skip the remainder. try { @@ -83,9 +76,10 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder { parsableWebvttData.readLine(); // Consume the "STYLE" header. definedStyles.addAll(cssParser.parseBlock(parsableWebvttData)); } else if (event == EVENT_CUE) { - if (cueParser.parseCue(parsableWebvttData, webvttCueBuilder, definedStyles)) { - cueInfos.add(webvttCueBuilder.build()); - webvttCueBuilder.reset(); + @Nullable + WebvttCueInfo cueInfo = WebvttCueParser.parseCue(parsableWebvttData, definedStyles); + if (cueInfo != null) { + cueInfos.add(cueInfo); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java index 83c588fb77..49ee73ea5a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java @@ -80,9 +80,9 @@ import java.util.List; // individual cues, but tweaking their `line` value): // https://www.w3.org/TR/webvtt1/#cue-computed-line if (isNormal(cue)) { - // we want to merge all of the normal cues into a single cue to ensure they are drawn + // We want to merge all of the normal cues into a single cue to ensure they are drawn // correctly (i.e. don't overlap) and to emulate roll-up, but only if there are multiple - // normal cues, otherwise we can just append the single normal cue + // normal cues, otherwise we can just append the single normal cue. if (firstNormalCue == null) { firstNormalCue = cue; } else if (normalCueTextBuilder == null) { @@ -100,10 +100,10 @@ import java.util.List; } } if (normalCueTextBuilder != null) { - // there were multiple normal cues, so create a new cue with all of the text - list.add(new WebvttCueInfo.Builder().setText(normalCueTextBuilder).build().cue); + // There were multiple normal cues, so create a new cue with all of the text. + list.add(WebvttCueParser.newCueForText(normalCueTextBuilder)); } else if (firstNormalCue != null) { - // there was only a single normal cue, so just add it to the list + // There was only a single normal cue, so just add it to the list. list.add(firstNormalCue); } return list; @@ -116,6 +116,6 @@ import java.util.List; * @return Whether this cue should be placed in the default position. */ private static boolean isNormal(Cue cue) { - return (cue.line == Cue.DIMEN_UNSET && cue.position == WebvttCueInfo.DEFAULT_POSITION); + return (cue.line == Cue.DIMEN_UNSET && cue.position == WebvttCueParser.DEFAULT_POSITION); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java index 69d7caa832..5f91193699 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java @@ -92,7 +92,7 @@ public final class Mp4WebvttDecoderTest { Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); Subtitle result = decoder.decode(SINGLE_CUE_SAMPLE, SINGLE_CUE_SAMPLE.length, false); // Line feed must be trimmed by the decoder - Cue expectedCue = new WebvttCueInfo.Builder().setText("Hello World").build().cue; + Cue expectedCue = WebvttCueParser.newCueForText("Hello World"); assertMp4WebvttSubtitleEquals(result, expectedCue); } @@ -100,8 +100,8 @@ public final class Mp4WebvttDecoderTest { public void testTwoCuesSample() throws SubtitleDecoderException { Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); Subtitle result = decoder.decode(DOUBLE_CUE_SAMPLE, DOUBLE_CUE_SAMPLE.length, false); - Cue firstExpectedCue = new WebvttCueInfo.Builder().setText("Hello World").build().cue; - Cue secondExpectedCue = new WebvttCueInfo.Builder().setText("Bye Bye").build().cue; + Cue firstExpectedCue = WebvttCueParser.newCueForText("Hello World"); + Cue secondExpectedCue = WebvttCueParser.newCueForText("Bye Bye"); assertMp4WebvttSubtitleEquals(result, firstExpectedCue, secondExpectedCue); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java index ebaec594f1..d23ed00e95 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java @@ -217,13 +217,7 @@ public final class WebvttCueParserTest { } private static Spanned parseCueText(String string) { - WebvttCueInfo.Builder builder = new WebvttCueInfo.Builder(); - WebvttCueParser.parseCueText(null, string, builder, Collections.emptyList()); - return (Spanned) builder.build().cue.text; + return WebvttCueParser.parseCueText( + /* id= */ null, string, /* styles= */ Collections.emptyList()); } - - private static T[] getSpans(Spanned text, Class spanType) { - return text.getSpans(0, text.length(), spanType); - } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java index 621751db94..61c6394db4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java @@ -21,7 +21,7 @@ import static java.lang.Long.MAX_VALUE; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.text.Cue; -import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import org.junit.Test; @@ -38,68 +38,41 @@ public class WebvttSubtitleTest { private static final WebvttSubtitle emptySubtitle = new WebvttSubtitle(Collections.emptyList()); - private static final WebvttSubtitle simpleSubtitle; + private static final WebvttSubtitle simpleSubtitle = + new WebvttSubtitle( + Arrays.asList( + new WebvttCueInfo( + WebvttCueParser.newCueForText(FIRST_SUBTITLE_STRING), + /* startTimeUs= */ 1_000_000, + /* endTimeUs= */ 2_000_000), + new WebvttCueInfo( + WebvttCueParser.newCueForText(SECOND_SUBTITLE_STRING), + /* startTimeUs= */ 3_000_000, + /* endTimeUs= */ 4_000_000))); - static { - ArrayList simpleSubtitleCues = new ArrayList<>(); - WebvttCueInfo firstCue = - new WebvttCueInfo.Builder() - .setStartTime(1000000) - .setEndTime(2000000) - .setText(FIRST_SUBTITLE_STRING) - .build(); - simpleSubtitleCues.add(firstCue); - WebvttCueInfo secondCue = - new WebvttCueInfo.Builder() - .setStartTime(3000000) - .setEndTime(4000000) - .setText(SECOND_SUBTITLE_STRING) - .build(); - simpleSubtitleCues.add(secondCue); - simpleSubtitle = new WebvttSubtitle(simpleSubtitleCues); - } + private static final WebvttSubtitle overlappingSubtitle = + new WebvttSubtitle( + Arrays.asList( + new WebvttCueInfo( + WebvttCueParser.newCueForText(FIRST_SUBTITLE_STRING), + /* startTimeUs= */ 1_000_000, + /* endTimeUs= */ 3_000_000), + new WebvttCueInfo( + WebvttCueParser.newCueForText(SECOND_SUBTITLE_STRING), + /* startTimeUs= */ 2_000_000, + /* endTimeUs= */ 4_000_000))); - private static final WebvttSubtitle overlappingSubtitle; - - static { - ArrayList overlappingSubtitleCues = new ArrayList<>(); - WebvttCueInfo firstCue = - new WebvttCueInfo.Builder() - .setStartTime(1000000) - .setEndTime(3000000) - .setText(FIRST_SUBTITLE_STRING) - .build(); - overlappingSubtitleCues.add(firstCue); - WebvttCueInfo secondCue = - new WebvttCueInfo.Builder() - .setStartTime(2000000) - .setEndTime(4000000) - .setText(SECOND_SUBTITLE_STRING) - .build(); - overlappingSubtitleCues.add(secondCue); - overlappingSubtitle = new WebvttSubtitle(overlappingSubtitleCues); - } - - private static final WebvttSubtitle nestedSubtitle; - - static { - ArrayList nestedSubtitleCues = new ArrayList<>(); - WebvttCueInfo firstCue = - new WebvttCueInfo.Builder() - .setStartTime(1000000) - .setEndTime(4000000) - .setText(FIRST_SUBTITLE_STRING) - .build(); - nestedSubtitleCues.add(firstCue); - WebvttCueInfo secondCue = - new WebvttCueInfo.Builder() - .setStartTime(2000000) - .setEndTime(3000000) - .setText(SECOND_SUBTITLE_STRING) - .build(); - nestedSubtitleCues.add(secondCue); - nestedSubtitle = new WebvttSubtitle(nestedSubtitleCues); - } + private static final WebvttSubtitle nestedSubtitle = + new WebvttSubtitle( + Arrays.asList( + new WebvttCueInfo( + WebvttCueParser.newCueForText(FIRST_SUBTITLE_STRING), + /* startTimeUs= */ 1_000_000, + /* endTimeUs= */ 4_000_000), + new WebvttCueInfo( + WebvttCueParser.newCueForText(SECOND_SUBTITLE_STRING), + /* startTimeUs= */ 2_000_000, + /* endTimeUs= */ 3_000_000))); @Test public void testEventCount() { @@ -123,27 +96,27 @@ public class WebvttSubtitleTest { public void testSimpleSubtitleText() { // Test before first subtitle assertSingleCueEmpty(simpleSubtitle.getCues(0)); - assertSingleCueEmpty(simpleSubtitle.getCues(500000)); - assertSingleCueEmpty(simpleSubtitle.getCues(999999)); + assertSingleCueEmpty(simpleSubtitle.getCues(500_000)); + assertSingleCueEmpty(simpleSubtitle.getCues(999_999)); // Test first subtitle - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1000000)); - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1500000)); - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1999999)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1_000_000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1_500_000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1_999_999)); // Test after first subtitle, before second subtitle - assertSingleCueEmpty(simpleSubtitle.getCues(2000000)); - assertSingleCueEmpty(simpleSubtitle.getCues(2500000)); - assertSingleCueEmpty(simpleSubtitle.getCues(2999999)); + assertSingleCueEmpty(simpleSubtitle.getCues(2_000_000)); + assertSingleCueEmpty(simpleSubtitle.getCues(2_500_000)); + assertSingleCueEmpty(simpleSubtitle.getCues(2_999_999)); // Test second subtitle - assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3000000)); - assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3500000)); - assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3999999)); + assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3_000_000)); + assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3_500_000)); + assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3_999_999)); // Test after second subtitle - assertSingleCueEmpty(simpleSubtitle.getCues(4000000)); - assertSingleCueEmpty(simpleSubtitle.getCues(4500000)); + assertSingleCueEmpty(simpleSubtitle.getCues(4_000_000)); + assertSingleCueEmpty(simpleSubtitle.getCues(4_500_000)); assertSingleCueEmpty(simpleSubtitle.getCues(Long.MAX_VALUE)); } @@ -161,30 +134,30 @@ public class WebvttSubtitleTest { public void testOverlappingSubtitleText() { // Test before first subtitle assertSingleCueEmpty(overlappingSubtitle.getCues(0)); - assertSingleCueEmpty(overlappingSubtitle.getCues(500000)); - assertSingleCueEmpty(overlappingSubtitle.getCues(999999)); + assertSingleCueEmpty(overlappingSubtitle.getCues(500_000)); + assertSingleCueEmpty(overlappingSubtitle.getCues(999_999)); // Test first subtitle - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1000000)); - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1500000)); - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1999999)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1_000_000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1_500_000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1_999_999)); // Test after first and second subtitle - assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, - overlappingSubtitle.getCues(2000000)); - assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, - overlappingSubtitle.getCues(2500000)); - assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, - overlappingSubtitle.getCues(2999999)); + assertSingleCueTextEquals( + FIRST_AND_SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(2_000_000)); + assertSingleCueTextEquals( + FIRST_AND_SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(2_500_000)); + assertSingleCueTextEquals( + FIRST_AND_SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(2_999_999)); // Test second subtitle - assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3000000)); - assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3500000)); - assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3999999)); + assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3_000_000)); + assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3_500_000)); + assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3_999_999)); // Test after second subtitle - assertSingleCueEmpty(overlappingSubtitle.getCues(4000000)); - assertSingleCueEmpty(overlappingSubtitle.getCues(4500000)); + assertSingleCueEmpty(overlappingSubtitle.getCues(4_000_000)); + assertSingleCueEmpty(overlappingSubtitle.getCues(4_500_000)); assertSingleCueEmpty(overlappingSubtitle.getCues(Long.MAX_VALUE)); } @@ -202,61 +175,61 @@ public class WebvttSubtitleTest { public void testNestedSubtitleText() { // Test before first subtitle assertSingleCueEmpty(nestedSubtitle.getCues(0)); - assertSingleCueEmpty(nestedSubtitle.getCues(500000)); - assertSingleCueEmpty(nestedSubtitle.getCues(999999)); + assertSingleCueEmpty(nestedSubtitle.getCues(500_000)); + assertSingleCueEmpty(nestedSubtitle.getCues(999_999)); // Test first subtitle - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1000000)); - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1500000)); - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1999999)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1_000_000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1_500_000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1_999_999)); // Test after first and second subtitle - assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2000000)); - assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2500000)); - assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2999999)); + assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2_000_000)); + assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2_500_000)); + assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2_999_999)); // Test first subtitle - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3000000)); - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3500000)); - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3999999)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3_000_000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3_500_000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3_999_999)); // Test after second subtitle - assertSingleCueEmpty(nestedSubtitle.getCues(4000000)); - assertSingleCueEmpty(nestedSubtitle.getCues(4500000)); + assertSingleCueEmpty(nestedSubtitle.getCues(4_000_000)); + assertSingleCueEmpty(nestedSubtitle.getCues(4_500_000)); assertSingleCueEmpty(nestedSubtitle.getCues(Long.MAX_VALUE)); } private void testSubtitleEventTimesHelper(WebvttSubtitle subtitle) { - assertThat(subtitle.getEventTime(0)).isEqualTo(1000000); - assertThat(subtitle.getEventTime(1)).isEqualTo(2000000); - assertThat(subtitle.getEventTime(2)).isEqualTo(3000000); - assertThat(subtitle.getEventTime(3)).isEqualTo(4000000); + assertThat(subtitle.getEventTime(0)).isEqualTo(1_000_000); + assertThat(subtitle.getEventTime(1)).isEqualTo(2_000_000); + assertThat(subtitle.getEventTime(2)).isEqualTo(3_000_000); + assertThat(subtitle.getEventTime(3)).isEqualTo(4_000_000); } private void testSubtitleEventIndicesHelper(WebvttSubtitle subtitle) { // Test first event assertThat(subtitle.getNextEventTimeIndex(0)).isEqualTo(0); - assertThat(subtitle.getNextEventTimeIndex(500000)).isEqualTo(0); - assertThat(subtitle.getNextEventTimeIndex(999999)).isEqualTo(0); + assertThat(subtitle.getNextEventTimeIndex(500_000)).isEqualTo(0); + assertThat(subtitle.getNextEventTimeIndex(999_999)).isEqualTo(0); // Test second event - assertThat(subtitle.getNextEventTimeIndex(1000000)).isEqualTo(1); - assertThat(subtitle.getNextEventTimeIndex(1500000)).isEqualTo(1); - assertThat(subtitle.getNextEventTimeIndex(1999999)).isEqualTo(1); + assertThat(subtitle.getNextEventTimeIndex(1_000_000)).isEqualTo(1); + assertThat(subtitle.getNextEventTimeIndex(1_500_000)).isEqualTo(1); + assertThat(subtitle.getNextEventTimeIndex(1_999_999)).isEqualTo(1); // Test third event - assertThat(subtitle.getNextEventTimeIndex(2000000)).isEqualTo(2); - assertThat(subtitle.getNextEventTimeIndex(2500000)).isEqualTo(2); - assertThat(subtitle.getNextEventTimeIndex(2999999)).isEqualTo(2); + assertThat(subtitle.getNextEventTimeIndex(2_000_000)).isEqualTo(2); + assertThat(subtitle.getNextEventTimeIndex(2_500_000)).isEqualTo(2); + assertThat(subtitle.getNextEventTimeIndex(2_999_999)).isEqualTo(2); // Test fourth event - assertThat(subtitle.getNextEventTimeIndex(3000000)).isEqualTo(3); - assertThat(subtitle.getNextEventTimeIndex(3500000)).isEqualTo(3); - assertThat(subtitle.getNextEventTimeIndex(3999999)).isEqualTo(3); + assertThat(subtitle.getNextEventTimeIndex(3_000_000)).isEqualTo(3); + assertThat(subtitle.getNextEventTimeIndex(3_500_000)).isEqualTo(3); + assertThat(subtitle.getNextEventTimeIndex(3_999_999)).isEqualTo(3); // Test null event (i.e. look for events after the last event) - assertThat(subtitle.getNextEventTimeIndex(4000000)).isEqualTo(INDEX_UNSET); - assertThat(subtitle.getNextEventTimeIndex(4500000)).isEqualTo(INDEX_UNSET); + assertThat(subtitle.getNextEventTimeIndex(4_000_000)).isEqualTo(INDEX_UNSET); + assertThat(subtitle.getNextEventTimeIndex(4_500_000)).isEqualTo(INDEX_UNSET); assertThat(subtitle.getNextEventTimeIndex(MAX_VALUE)).isEqualTo(INDEX_UNSET); } @@ -268,5 +241,4 @@ public class WebvttSubtitleTest { assertThat(cues).hasSize(1); assertThat(cues.get(0).text.toString()).isEqualTo(expected); } - } From 65e84811ff987aed9a48180c6a28c0032dcf7c86 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 18 Dec 2019 16:51:40 +0000 Subject: [PATCH 0556/1335] Add 'Us' suffix to WebvttCueInfo.{start,end}Time Clarify that the units used here are microseconds PiperOrigin-RevId: 286200583 --- .../android/exoplayer2/text/webvtt/WebvttCueInfo.java | 8 ++++---- .../android/exoplayer2/text/webvtt/WebvttSubtitle.java | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueInfo.java index b04e06d744..2119bd1c04 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueInfo.java @@ -22,12 +22,12 @@ import com.google.android.exoplayer2.text.Cue; public final class WebvttCueInfo { public final Cue cue; - public final long startTime; - public final long endTime; + public final long startTimeUs; + public final long endTimeUs; public WebvttCueInfo(Cue cue, long startTimeUs, long endTimeUs) { this.cue = cue; - this.startTime = startTimeUs; - this.endTime = endTimeUs; + this.startTimeUs = startTimeUs; + this.endTimeUs = endTimeUs; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java index 49ee73ea5a..620e1ef491 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java @@ -42,8 +42,8 @@ import java.util.List; WebvttCueInfo cueInfo = cueInfos.get(cueIndex); this.cues.add(cueInfo.cue); int arrayIndex = cueIndex * 2; - cueTimesUs[arrayIndex] = cueInfo.startTime; - cueTimesUs[arrayIndex + 1] = cueInfo.endTime; + cueTimesUs[arrayIndex] = cueInfo.startTimeUs; + cueTimesUs[arrayIndex + 1] = cueInfo.endTimeUs; } sortedCueTimesUs = Arrays.copyOf(cueTimesUs, cueTimesUs.length); Arrays.sort(sortedCueTimesUs); From e8068f0fcb45e894b66d39aa727cbdefcb7652fb Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 18 Dec 2019 16:56:31 +0000 Subject: [PATCH 0557/1335] Add omitted release note for 2.11 PiperOrigin-RevId: 286201458 --- RELEASENOTES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9552be4e6f..ce54fd8405 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -70,6 +70,10 @@ * 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. + * Where there are multiple platform decoders for a given MIME type, prefer to + use one that advertises support for the profile and level of the media being + played over one that does not, even if it does not come first in the + `MediaCodecList`. * DRM: * Inject `DrmSessionManager` into the `MediaSources` instead of `Renderers`. This allows each `MediaSource` in a `ConcatenatingMediaSource` to use a From 8b0f5b0a8629a5ef19debdf6ec47bd00e9a69bb4 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 18 Dec 2019 17:08:04 +0000 Subject: [PATCH 0558/1335] Some nullness cleanup for extractor.ogg PiperOrigin-RevId: 286203692 --- .../exoplayer2/extractor/ogg/FlacReader.java | 27 ++++++++++++------- .../exoplayer2/extractor/ogg/OpusReader.java | 16 ++++++++--- .../extractor/ogg/StreamReader.java | 9 ++++--- .../extractor/ogg/VorbisReader.java | 27 +++++++++++++------ 4 files changed, 55 insertions(+), 24 deletions(-) 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 258390d21d..e6436de2bb 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 @@ -24,9 +24,9 @@ import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.FlacConstants; import com.google.android.exoplayer2.util.FlacStreamMetadata; +import com.google.android.exoplayer2.util.FlacStreamMetadata.SeekTable; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; -import java.io.IOException; import java.util.Arrays; /** @@ -70,15 +70,17 @@ import java.util.Arrays; @Override protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) { byte[] data = packet.data; + @Nullable FlacStreamMetadata streamMetadata = this.streamMetadata; if (streamMetadata == null) { streamMetadata = new FlacStreamMetadata(data, 17); + this.streamMetadata = streamMetadata; byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit()); setupData.format = streamMetadata.getFormat(metadata, /* id3Metadata= */ null); } else if ((data[0] & 0x7F) == FlacConstants.METADATA_TYPE_SEEK_TABLE) { - flacOggSeeker = new FlacOggSeeker(); - FlacStreamMetadata.SeekTable seekTable = - FlacMetadataReader.readSeekTableMetadataBlock(packet); + SeekTable seekTable = FlacMetadataReader.readSeekTableMetadataBlock(packet); streamMetadata = streamMetadata.copyWithSeekTable(seekTable); + this.streamMetadata = streamMetadata; + flacOggSeeker = new FlacOggSeeker(streamMetadata, seekTable); } else if (isAudioPacket(data)) { if (flacOggSeeker != null) { flacOggSeeker.setFirstFrameOffset(position); @@ -101,12 +103,16 @@ import java.util.Arrays; return result; } - private class FlacOggSeeker implements OggSeeker { + private static final class FlacOggSeeker implements OggSeeker { + private FlacStreamMetadata streamMetadata; + private SeekTable seekTable; private long firstFrameOffset; private long pendingSeekGranule; - public FlacOggSeeker() { + public FlacOggSeeker(FlacStreamMetadata streamMetadata, SeekTable seekTable) { + this.streamMetadata = streamMetadata; + this.seekTable = seekTable; firstFrameOffset = -1; pendingSeekGranule = -1; } @@ -116,7 +122,7 @@ import java.util.Arrays; } @Override - public long read(ExtractorInput input) throws IOException, InterruptedException { + public long read(ExtractorInput input) { if (pendingSeekGranule >= 0) { long result = -(pendingSeekGranule + 2); pendingSeekGranule = -1; @@ -127,9 +133,10 @@ import java.util.Arrays; @Override public void startSeek(long targetGranule) { - Assertions.checkNotNull(streamMetadata.seekTable); - long[] seekPointGranules = streamMetadata.seekTable.pointSampleNumbers; - int index = Util.binarySearchFloor(seekPointGranules, targetGranule, true, true); + long[] seekPointGranules = seekTable.pointSampleNumbers; + int index = + Util.binarySearchFloor( + seekPointGranules, targetGranule, /* inclusive= */ true, /* stayInBounds= */ true); pendingSeekGranule = seekPointGranules[index]; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java index 90ae3f0f47..84a7ecc9f9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java @@ -76,9 +76,19 @@ import java.util.List; putNativeOrderLong(initializationData, preskip); putNativeOrderLong(initializationData, DEFAULT_SEEK_PRE_ROLL_SAMPLES); - setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_OPUS, null, - Format.NO_VALUE, Format.NO_VALUE, channelCount, SAMPLE_RATE, initializationData, null, 0, - null); + setupData.format = + Format.createAudioSampleFormat( + null, + MimeTypes.AUDIO_OPUS, + /* codecs= */ null, + Format.NO_VALUE, + Format.NO_VALUE, + channelCount, + SAMPLE_RATE, + initializationData, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); headerRead = true; } else { boolean headerPacket = packet.readInt() == OPUS_CODE; 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 2aec9cdd59..41ae394de2 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ogg; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.Extractor; @@ -23,6 +24,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -51,7 +53,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private long currentGranule; private int state; private int sampleRate; - private SetupData setupData; + @Nullable private SetupData setupData; private long lengthOfReadPacket; private boolean seekMapSet; private boolean formatSet; @@ -149,7 +151,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; boolean isLastPage = (firstPayloadPageHeader.type & 0x04) != 0; // Type 4 is end of stream. oggSeeker = new DefaultOggSeeker( - this, + /* streamReader= */ this, payloadStartPosition, input.getLength(), firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize, @@ -173,8 +175,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } else if (position < -1) { onSeekEnd(-(position + 2)); } + if (!seekMapSet) { - SeekMap seekMap = oggSeeker.createSeekMap(); + SeekMap seekMap = Assertions.checkNotNull(oggSeeker.createSeekMap()); extractorOutput.seekMap(seekMap); seekMapSet = true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java index 71c6c3b73e..5c573e2ec0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ogg; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; @@ -30,16 +31,16 @@ import java.util.ArrayList; */ /* package */ final class VorbisReader extends StreamReader { - private VorbisSetup vorbisSetup; + @Nullable private VorbisSetup vorbisSetup; private int previousPacketBlockSize; private boolean seenFirstAudioPacket; - private VorbisUtil.VorbisIdHeader vorbisIdHeader; - private VorbisUtil.CommentHeader commentHeader; + @Nullable private VorbisUtil.VorbisIdHeader vorbisIdHeader; + @Nullable private VorbisUtil.CommentHeader commentHeader; public static boolean verifyBitstreamType(ParsableByteArray data) { try { - return VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, data, true); + return VorbisUtil.verifyVorbisHeaderCapturePattern(/* headerType= */ 0x01, data, true); } catch (ParserException e) { return false; } @@ -102,14 +103,24 @@ import java.util.ArrayList; codecInitialisationData.add(vorbisSetup.idHeader.data); codecInitialisationData.add(vorbisSetup.setupHeaderData); - setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS, null, - this.vorbisSetup.idHeader.bitrateNominal, Format.NO_VALUE, - this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate, - codecInitialisationData, null, 0, null); + setupData.format = + Format.createAudioSampleFormat( + null, + MimeTypes.AUDIO_VORBIS, + /* codecs= */ null, + this.vorbisSetup.idHeader.bitrateNominal, + Format.NO_VALUE, + this.vorbisSetup.idHeader.channels, + (int) this.vorbisSetup.idHeader.sampleRate, + codecInitialisationData, + null, + /* selectionFlags= */ 0, + /* language= */ null); return true; } @VisibleForTesting + @Nullable /* package */ VorbisSetup readSetupHeaders(ParsableByteArray scratch) throws IOException { if (vorbisIdHeader == null) { From ed1de000e5f0fa7fbd98c824732869c6b3e70371 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 18 Dec 2019 18:09:42 +0000 Subject: [PATCH 0559/1335] Unwrap all nested IntDef values This seems to work with R8 but interact badly with ProGuard. issue:#6771 PiperOrigin-RevId: 286215262 --- .../exoplayer2/DefaultRenderersFactory.java | 2 +- ...DedicatedThreadAsyncMediaCodecAdapter.java | 32 ++--- .../mediacodec/MediaCodecRenderer.java | 112 ++++++++++-------- .../MultiLockAsynchMediaCodecAdapter.java | 32 ++--- .../exoplayer2/text/ssa/SsaDecoder.java | 64 +++++----- .../android/exoplayer2/text/ssa/SsaStyle.java | 103 +++++++++------- .../exoplayer2/source/hls/HlsMediaPeriod.java | 4 +- .../exoplayer2/source/hls/HlsMediaSource.java | 43 +++++-- .../source/hls/HlsMetadataType.java | 38 ------ .../source/hls/HlsSampleStreamWrapper.java | 11 +- .../source/hls/HlsMediaPeriodTest.java | 2 +- 11 files changed, 229 insertions(+), 214 deletions(-) delete mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMetadataType.java 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 97ad6613e3..f54794216b 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 @@ -102,7 +102,7 @@ public class DefaultRenderersFactory implements RenderersFactory { extensionRendererMode = EXTENSION_RENDERER_MODE_OFF; allowedVideoJoiningTimeMs = DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS; mediaCodecSelector = MediaCodecSelector.DEFAULT; - mediaCodecOperationMode = MediaCodecRenderer.MediaCodecOperationMode.SYNCHRONOUS; + mediaCodecOperationMode = MediaCodecRenderer.OPERATION_MODE_SYNCHRONOUS; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapter.java index d6e4e84525..bad21f91f8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapter.java @@ -41,12 +41,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /* package */ final class DedicatedThreadAsyncMediaCodecAdapter extends MediaCodec.Callback implements MediaCodecAdapter { - @IntDef({State.CREATED, State.STARTED, State.SHUT_DOWN}) - private @interface State { - int CREATED = 0; - int STARTED = 1; - int SHUT_DOWN = 2; - } + @IntDef({STATE_CREATED, STATE_STARTED, STATE_SHUT_DOWN}) + private @interface State {} + + private static final int STATE_CREATED = 0; + private static final int STATE_STARTED = 1; + private static final int STATE_SHUT_DOWN = 2; private final MediaCodecAsyncCallback mediaCodecAsyncCallback; private final MediaCodec codec; @@ -76,7 +76,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; mediaCodecAsyncCallback = new MediaCodecAsyncCallback(); this.codec = codec; this.handlerThread = handlerThread; - state = State.CREATED; + state = STATE_CREATED; onCodecStart = codec::start; } @@ -90,17 +90,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * @throws IllegalStateException If this method has been called already. */ public synchronized void start() { - Assertions.checkState(state == State.CREATED); + Assertions.checkState(state == STATE_CREATED); handlerThread.start(); handler = new Handler(handlerThread.getLooper()); codec.setCallback(this, handler); - state = State.STARTED; + state = STATE_STARTED; } @Override public synchronized int dequeueInputBufferIndex() { - Assertions.checkState(state == State.STARTED); + Assertions.checkState(state == STATE_STARTED); if (isFlushing()) { return MediaCodec.INFO_TRY_AGAIN_LATER; @@ -112,7 +112,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public synchronized int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { - Assertions.checkState(state == State.STARTED); + Assertions.checkState(state == STATE_STARTED); if (isFlushing()) { return MediaCodec.INFO_TRY_AGAIN_LATER; @@ -124,14 +124,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public synchronized MediaFormat getOutputFormat() { - Assertions.checkState(state == State.STARTED); + Assertions.checkState(state == STATE_STARTED); return mediaCodecAsyncCallback.getOutputFormat(); } @Override public synchronized void flush() { - Assertions.checkState(state == State.STARTED); + Assertions.checkState(state == STATE_STARTED); codec.flush(); ++pendingFlushCount; @@ -140,12 +140,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public synchronized void shutdown() { - if (state == State.STARTED) { + if (state == STATE_STARTED) { handlerThread.quit(); mediaCodecAsyncCallback.flush(); } - state = State.SHUT_DOWN; + state = STATE_SHUT_DOWN; } @Override @@ -182,7 +182,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } private synchronized void onFlushCompleted() { - if (state != State.STARTED) { + if (state != STATE_STARTED) { // The adapter has been shutdown. return; } 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 953ffbe546..48a415ec7f 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 @@ -61,8 +61,46 @@ import java.util.List; public abstract class MediaCodecRenderer extends BaseRenderer { /** - * Thrown when a failure occurs instantiating a decoder. + * The modes to operate the {@link MediaCodec}. + * + *

      Allowed values: + * + *

        + *
      • {@link #OPERATION_MODE_SYNCHRONOUS} + *
      • {@link #OPERATION_MODE_ASYNCHRONOUS_PLAYBACK_THREAD} + *
      • {@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD} + *
      • {@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK} + *
      */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + OPERATION_MODE_SYNCHRONOUS, + OPERATION_MODE_ASYNCHRONOUS_PLAYBACK_THREAD, + OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD, + OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK + }) + public @interface MediaCodecOperationMode {} + + /** Operates the {@link MediaCodec} in synchronous mode. */ + public static final int OPERATION_MODE_SYNCHRONOUS = 0; + /** + * Operates the {@link MediaCodec} in asynchronous mode and routes {@link MediaCodec.Callback} + * callbacks to the playback Thread. + */ + public static final int OPERATION_MODE_ASYNCHRONOUS_PLAYBACK_THREAD = 1; + /** + * Operates the {@link MediaCodec} in asynchronous mode and routes {@link MediaCodec.Callback} + * callbacks to a dedicated Thread. + */ + public static final int OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD = 2; + /** + * Operates the {@link MediaCodec} in asynchronous mode and routes {@link MediaCodec.Callback} + * callbacks to a dedicated Thread. Uses granular locking for input and output buffers. + */ + public static final int OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK = 3; + + /** Thrown when a failure occurs instantiating a decoder. */ public static class DecoderInitializationException extends Exception { private static final int CUSTOM_ERROR_CODE_BASE = -50000; @@ -191,36 +229,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } } - /** The modes to operate the {@link MediaCodec}. */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @IntDef({ - MediaCodecOperationMode.SYNCHRONOUS, - MediaCodecOperationMode.ASYNCHRONOUS_PLAYBACK_THREAD, - MediaCodecOperationMode.ASYNCHRONOUS_DEDICATED_THREAD, - MediaCodecOperationMode.ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK - }) - public @interface MediaCodecOperationMode { - - /** Operates the {@link MediaCodec} in synchronous mode. */ - int SYNCHRONOUS = 0; - /** - * Operates the {@link MediaCodec} in asynchronous mode and routes {@link MediaCodec.Callback} - * callbacks to the playback Thread. - */ - int ASYNCHRONOUS_PLAYBACK_THREAD = 1; - /** - * Operates the {@link MediaCodec} in asynchronous mode and routes {@link MediaCodec.Callback} - * callbacks to a dedicated Thread. - */ - int ASYNCHRONOUS_DEDICATED_THREAD = 2; - /** - * Operates the {@link MediaCodec} in asynchronous mode and routes {@link MediaCodec.Callback} - * callbacks to a dedicated Thread. Uses granular locking for input and output buffers. - */ - int ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK = 3; - } - /** Indicates no codec operating rate should be set. */ protected static final float CODEC_OPERATING_RATE_UNSET = -1; @@ -447,7 +455,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { outputBufferInfo = new MediaCodec.BufferInfo(); rendererOperatingRate = 1f; renderTimeLimitMs = C.TIME_UNSET; - mediaCodecOperationMode = MediaCodecOperationMode.SYNCHRONOUS; + mediaCodecOperationMode = OPERATION_MODE_SYNCHRONOUS; resetCodecStateForRelease(); } @@ -473,23 +481,24 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * * @param mode The mode of the MediaCodec. The supported modes are: *
        - *
      • {@link MediaCodecOperationMode#SYNCHRONOUS}: The {@link MediaCodec} will operate in - * synchronous mode. - *
      • {@link MediaCodecOperationMode#ASYNCHRONOUS_PLAYBACK_THREAD}: The {@link MediaCodec} - * will operate in asynchronous mode and {@link MediaCodec.Callback} callbacks will be - * routed to the Playback Thread. This mode requires API level ≥ 21; if the API level - * is ≤ 20, the operation mode will be set to {@link - * MediaCodecOperationMode#SYNCHRONOUS}. - *
      • {@link MediaCodecOperationMode#ASYNCHRONOUS_DEDICATED_THREAD}: The {@link MediaCodec} - * will operate in asynchronous mode and {@link MediaCodec.Callback} callbacks will be - * routed to a dedicated Thread. This mode requires API level ≥ 23; if the API level - * is ≤ 22, the operation mode will be set to {@link - * MediaCodecOperationMode#SYNCHRONOUS}. - *
      • {@link MediaCodecOperationMode#ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK}: Same as - * {@link MediaCodecOperationMode#ASYNCHRONOUS_DEDICATED_THREAD} but it will internally - * use a finer grained locking mechanism for increased performance. + *
      • {@link MediaCodecRenderer#OPERATION_MODE_SYNCHRONOUS}: The {@link MediaCodec} will + * operate in synchronous mode. + *
      • {@link MediaCodecRenderer#OPERATION_MODE_ASYNCHRONOUS_PLAYBACK_THREAD}: The {@link + * MediaCodec} will operate in asynchronous mode and {@link MediaCodec.Callback} + * callbacks will be routed to the Playback Thread. This mode requires API level ≥ + * 21; if the API level is ≤ 20, the operation mode will be set to {@link + * MediaCodecRenderer#OPERATION_MODE_SYNCHRONOUS}. + *
      • {@link MediaCodecRenderer#OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD}: The {@link + * MediaCodec} will operate in asynchronous mode and {@link MediaCodec.Callback} + * callbacks will be routed to a dedicated Thread. This mode requires API level ≥ 23; + * if the API level is ≤ 22, the operation mode will be set to {@link + * MediaCodecRenderer#OPERATION_MODE_SYNCHRONOUS}. + *
      • {@link MediaCodecRenderer#OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK}: + * Same as {@link MediaCodecRenderer#OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD} but + * it will internally use a finer grained locking mechanism for increased performance. *
      - * By default, the operation mode is set to {@link MediaCodecOperationMode#SYNCHRONOUS}. + * By default, the operation mode is set to {@link + * MediaCodecRenderer#OPERATION_MODE_SYNCHRONOUS}. */ public void experimental_setMediaCodecOperationMode(@MediaCodecOperationMode int mode) { mediaCodecOperationMode = mode; @@ -980,15 +989,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecInitializingTimestamp = SystemClock.elapsedRealtime(); TraceUtil.beginSection("createCodec:" + codecName); codec = MediaCodec.createByCodecName(codecName); - if (mediaCodecOperationMode == MediaCodecOperationMode.ASYNCHRONOUS_PLAYBACK_THREAD + if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_PLAYBACK_THREAD && Util.SDK_INT >= 21) { codecAdapter = new AsynchronousMediaCodecAdapter(codec); - } else if (mediaCodecOperationMode == MediaCodecOperationMode.ASYNCHRONOUS_DEDICATED_THREAD + } else if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD && Util.SDK_INT >= 23) { codecAdapter = new DedicatedThreadAsyncMediaCodecAdapter(codec, getTrackType()); ((DedicatedThreadAsyncMediaCodecAdapter) codecAdapter).start(); - } else if (mediaCodecOperationMode - == MediaCodecOperationMode.ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK + } else if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK && Util.SDK_INT >= 23) { codecAdapter = new MultiLockAsynchMediaCodecAdapter(codec, getTrackType()); ((MultiLockAsynchMediaCodecAdapter) codecAdapter).start(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsynchMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsynchMediaCodecAdapter.java index 2fa40545d5..7d00d15947 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsynchMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsynchMediaCodecAdapter.java @@ -54,12 +54,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /* package */ final class MultiLockAsynchMediaCodecAdapter extends MediaCodec.Callback implements MediaCodecAdapter { - @IntDef({State.CREATED, State.STARTED, State.SHUT_DOWN}) - private @interface State { - int CREATED = 0; - int STARTED = 1; - int SHUT_DOWN = 2; - } + @IntDef({STATE_CREATED, STATE_STARTED, STATE_SHUT_DOWN}) + private @interface State {} + + private static final int STATE_CREATED = 0; + private static final int STATE_STARTED = 1; + private static final int STATE_SHUT_DOWN = 2; private final MediaCodec codec; private final Object inputBufferLock; @@ -112,7 +112,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; bufferInfos = new ArrayDeque<>(); formats = new ArrayDeque<>(); codecException = null; - state = State.CREATED; + state = STATE_CREATED; this.handlerThread = handlerThread; onCodecStart = codec::start; } @@ -128,19 +128,19 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; */ public void start() { synchronized (objectStateLock) { - Assertions.checkState(state == State.CREATED); + Assertions.checkState(state == STATE_CREATED); handlerThread.start(); handler = new Handler(handlerThread.getLooper()); codec.setCallback(this, handler); - state = State.STARTED; + state = STATE_STARTED; } } @Override public int dequeueInputBufferIndex() { synchronized (objectStateLock) { - Assertions.checkState(state == State.STARTED); + Assertions.checkState(state == STATE_STARTED); if (isFlushing()) { return MediaCodec.INFO_TRY_AGAIN_LATER; @@ -154,7 +154,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { synchronized (objectStateLock) { - Assertions.checkState(state == State.STARTED); + Assertions.checkState(state == STATE_STARTED); if (isFlushing()) { return MediaCodec.INFO_TRY_AGAIN_LATER; @@ -168,7 +168,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public MediaFormat getOutputFormat() { synchronized (objectStateLock) { - Assertions.checkState(state == State.STARTED); + Assertions.checkState(state == STATE_STARTED); if (currentFormat == null) { throw new IllegalStateException(); @@ -181,7 +181,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public void flush() { synchronized (objectStateLock) { - Assertions.checkState(state == State.STARTED); + Assertions.checkState(state == STATE_STARTED); codec.flush(); pendingFlush++; @@ -192,10 +192,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public void shutdown() { synchronized (objectStateLock) { - if (state == State.STARTED) { + if (state == STATE_STARTED) { handlerThread.quit(); } - state = State.SHUT_DOWN; + state = STATE_SHUT_DOWN; } } @@ -289,7 +289,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private void onFlushComplete() { synchronized (objectStateLock) { - if (state == State.SHUT_DOWN) { + if (state == STATE_SHUT_DOWN) { return; } 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 917ac8e36e..eef9d2eec1 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 @@ -300,12 +300,12 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { float screenWidth, float screenHeight) { @SsaStyle.SsaAlignment int alignment; - if (styleOverrides.alignment != SsaStyle.SsaAlignment.UNKNOWN) { + if (styleOverrides.alignment != SsaStyle.SSA_ALIGNMENT_UNKNOWN) { alignment = styleOverrides.alignment; } else if (style != null) { alignment = style.alignment; } else { - alignment = SsaStyle.SsaAlignment.UNKNOWN; + alignment = SsaStyle.SSA_ALIGNMENT_UNKNOWN; } @Cue.AnchorType int positionAnchor = toPositionAnchor(alignment); @Cue.AnchorType int lineAnchor = toLineAnchor(alignment); @@ -337,19 +337,19 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { @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: + case SsaStyle.SSA_ALIGNMENT_BOTTOM_LEFT: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_LEFT: + case SsaStyle.SSA_ALIGNMENT_TOP_LEFT: return Layout.Alignment.ALIGN_NORMAL; - case SsaStyle.SsaAlignment.BOTTOM_CENTER: - case SsaStyle.SsaAlignment.MIDDLE_CENTER: - case SsaStyle.SsaAlignment.TOP_CENTER: + case SsaStyle.SSA_ALIGNMENT_BOTTOM_CENTER: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_CENTER: + case SsaStyle.SSA_ALIGNMENT_TOP_CENTER: return Layout.Alignment.ALIGN_CENTER; - case SsaStyle.SsaAlignment.BOTTOM_RIGHT: - case SsaStyle.SsaAlignment.MIDDLE_RIGHT: - case SsaStyle.SsaAlignment.TOP_RIGHT: + case SsaStyle.SSA_ALIGNMENT_BOTTOM_RIGHT: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_RIGHT: + case SsaStyle.SSA_ALIGNMENT_TOP_RIGHT: return Layout.Alignment.ALIGN_OPPOSITE; - case SsaStyle.SsaAlignment.UNKNOWN: + case SsaStyle.SSA_ALIGNMENT_UNKNOWN: return null; default: Log.w(TAG, "Unknown alignment: " + alignment); @@ -360,19 +360,19 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { @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: + case SsaStyle.SSA_ALIGNMENT_BOTTOM_LEFT: + case SsaStyle.SSA_ALIGNMENT_BOTTOM_CENTER: + case SsaStyle.SSA_ALIGNMENT_BOTTOM_RIGHT: return Cue.ANCHOR_TYPE_END; - case SsaStyle.SsaAlignment.MIDDLE_LEFT: - case SsaStyle.SsaAlignment.MIDDLE_CENTER: - case SsaStyle.SsaAlignment.MIDDLE_RIGHT: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_LEFT: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_CENTER: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_RIGHT: return Cue.ANCHOR_TYPE_MIDDLE; - case SsaStyle.SsaAlignment.TOP_LEFT: - case SsaStyle.SsaAlignment.TOP_CENTER: - case SsaStyle.SsaAlignment.TOP_RIGHT: + case SsaStyle.SSA_ALIGNMENT_TOP_LEFT: + case SsaStyle.SSA_ALIGNMENT_TOP_CENTER: + case SsaStyle.SSA_ALIGNMENT_TOP_RIGHT: return Cue.ANCHOR_TYPE_START; - case SsaStyle.SsaAlignment.UNKNOWN: + case SsaStyle.SSA_ALIGNMENT_UNKNOWN: return Cue.TYPE_UNSET; default: Log.w(TAG, "Unknown alignment: " + alignment); @@ -383,19 +383,19 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { @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: + case SsaStyle.SSA_ALIGNMENT_BOTTOM_LEFT: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_LEFT: + case SsaStyle.SSA_ALIGNMENT_TOP_LEFT: return Cue.ANCHOR_TYPE_START; - case SsaStyle.SsaAlignment.BOTTOM_CENTER: - case SsaStyle.SsaAlignment.MIDDLE_CENTER: - case SsaStyle.SsaAlignment.TOP_CENTER: + case SsaStyle.SSA_ALIGNMENT_BOTTOM_CENTER: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_CENTER: + case SsaStyle.SSA_ALIGNMENT_TOP_CENTER: return Cue.ANCHOR_TYPE_MIDDLE; - case SsaStyle.SsaAlignment.BOTTOM_RIGHT: - case SsaStyle.SsaAlignment.MIDDLE_RIGHT: - case SsaStyle.SsaAlignment.TOP_RIGHT: + case SsaStyle.SSA_ALIGNMENT_BOTTOM_RIGHT: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_RIGHT: + case SsaStyle.SSA_ALIGNMENT_TOP_RIGHT: return Cue.ANCHOR_TYPE_END; - case SsaStyle.SsaAlignment.UNKNOWN: + case SsaStyle.SSA_ALIGNMENT_UNKNOWN: return Cue.TYPE_UNSET; default: Log.w(TAG, "Unknown alignment: " + alignment); 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 index e8070976e7..fd2cb036b7 100644 --- 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 @@ -37,6 +37,52 @@ import java.util.regex.Pattern; private static final String TAG = "SsaStyle"; + /** + * The SSA/ASS alignments. + * + *

      Allowed values: + * + *

        + *
      • {@link #SSA_ALIGNMENT_UNKNOWN} + *
      • {@link #SSA_ALIGNMENT_BOTTOM_LEFT} + *
      • {@link #SSA_ALIGNMENT_BOTTOM_CENTER} + *
      • {@link #SSA_ALIGNMENT_BOTTOM_RIGHT} + *
      • {@link #SSA_ALIGNMENT_MIDDLE_LEFT} + *
      • {@link #SSA_ALIGNMENT_MIDDLE_CENTER} + *
      • {@link #SSA_ALIGNMENT_MIDDLE_RIGHT} + *
      • {@link #SSA_ALIGNMENT_TOP_LEFT} + *
      • {@link #SSA_ALIGNMENT_TOP_CENTER} + *
      • {@link #SSA_ALIGNMENT_TOP_RIGHT} + *
      + */ + @IntDef({ + SSA_ALIGNMENT_UNKNOWN, + SSA_ALIGNMENT_BOTTOM_LEFT, + SSA_ALIGNMENT_BOTTOM_CENTER, + SSA_ALIGNMENT_BOTTOM_RIGHT, + SSA_ALIGNMENT_MIDDLE_LEFT, + SSA_ALIGNMENT_MIDDLE_CENTER, + SSA_ALIGNMENT_MIDDLE_RIGHT, + SSA_ALIGNMENT_TOP_LEFT, + SSA_ALIGNMENT_TOP_CENTER, + SSA_ALIGNMENT_TOP_RIGHT, + }) + @Documented + @Retention(SOURCE) + public @interface SsaAlignment {} + + // The numbering follows the ASS (v4+) spec (i.e. the points on the number pad). + public static final int SSA_ALIGNMENT_UNKNOWN = -1; + public static final int SSA_ALIGNMENT_BOTTOM_LEFT = 1; + public static final int SSA_ALIGNMENT_BOTTOM_CENTER = 2; + public static final int SSA_ALIGNMENT_BOTTOM_RIGHT = 3; + public static final int SSA_ALIGNMENT_MIDDLE_LEFT = 4; + public static final int SSA_ALIGNMENT_MIDDLE_CENTER = 5; + public static final int SSA_ALIGNMENT_MIDDLE_RIGHT = 6; + public static final int SSA_ALIGNMENT_TOP_LEFT = 7; + public static final int SSA_ALIGNMENT_TOP_CENTER = 8; + public static final int SSA_ALIGNMENT_TOP_RIGHT = 9; + public final String name; @SsaAlignment public final int alignment; @@ -77,22 +123,22 @@ import java.util.regex.Pattern; // Swallow the exception and return UNKNOWN below. } Log.w(TAG, "Ignoring unknown alignment: " + alignmentStr); - return SsaAlignment.UNKNOWN; + return SSA_ALIGNMENT_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: + case SSA_ALIGNMENT_BOTTOM_CENTER: + case SSA_ALIGNMENT_BOTTOM_LEFT: + case SSA_ALIGNMENT_BOTTOM_RIGHT: + case SSA_ALIGNMENT_MIDDLE_CENTER: + case SSA_ALIGNMENT_MIDDLE_LEFT: + case SSA_ALIGNMENT_MIDDLE_RIGHT: + case SSA_ALIGNMENT_TOP_CENTER: + case SSA_ALIGNMENT_TOP_LEFT: + case SSA_ALIGNMENT_TOP_RIGHT: return true; - case SsaAlignment.UNKNOWN: + case SSA_ALIGNMENT_UNKNOWN: default: return false; } @@ -177,7 +223,7 @@ import java.util.regex.Pattern; } public static Overrides parseFromDialogue(String text) { - @SsaAlignment int alignment = SsaAlignment.UNKNOWN; + @SsaAlignment int alignment = SSA_ALIGNMENT_UNKNOWN; PointF position = null; Matcher matcher = BRACES_PATTERN.matcher(text); while (matcher.find()) { @@ -192,7 +238,7 @@ import java.util.regex.Pattern; } try { @SsaAlignment int parsedAlignment = parseAlignmentOverride(braceContents); - if (parsedAlignment != SsaAlignment.UNKNOWN) { + if (parsedAlignment != SSA_ALIGNMENT_UNKNOWN) { alignment = parsedAlignment; } } catch (RuntimeException e) { @@ -249,36 +295,7 @@ import java.util.regex.Pattern; @SsaAlignment private static int parseAlignmentOverride(String braceContents) { Matcher matcher = ALIGNMENT_OVERRIDE_PATTERN.matcher(braceContents); - return matcher.find() ? parseAlignment(matcher.group(1)) : SsaAlignment.UNKNOWN; + return matcher.find() ? parseAlignment(matcher.group(1)) : SSA_ALIGNMENT_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/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 f74d9b0b0c..3b723af435 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 @@ -75,7 +75,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final TimestampAdjusterProvider timestampAdjusterProvider; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final boolean allowChunklessPreparation; - private final @HlsMetadataType int metadataType; + private final @HlsMediaSource.MetadataType int metadataType; private final boolean useSessionKeys; @Nullable private Callback callback; @@ -118,7 +118,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper Allocator allocator, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, boolean allowChunklessPreparation, - @HlsMetadataType int metadataType, + @HlsMediaSource.MetadataType int metadataType, boolean useSessionKeys) { this.extractorFactory = extractorFactory; this.playlistTracker = playlistTracker; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 411eb448be..8e08fb8fb2 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -15,8 +15,11 @@ */ package com.google.android.exoplayer2.source.hls; +import static java.lang.annotation.RetentionPolicy.SOURCE; + import android.net.Uri; import android.os.Handler; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; @@ -47,6 +50,8 @@ import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; import java.util.List; /** An HLS {@link MediaSource}. */ @@ -57,6 +62,28 @@ public final class HlsMediaSource extends BaseMediaSource ExoPlayerLibraryInfo.registerModule("goog.exo.hls"); } + /** + * The types of metadata that can be extracted from HLS streams. + * + *

      Allowed values: + * + *

        + *
      • {@link #METADATA_TYPE_ID3} + *
      • {@link #METADATA_TYPE_EMSG} + *
      + * + *

      See {@link Factory#setMetadataType(int)}. + */ + @Documented + @Retention(SOURCE) + @IntDef({METADATA_TYPE_ID3, METADATA_TYPE_EMSG}) + public @interface MetadataType {} + + /** Type for ID3 metadata in HLS streams. */ + public static final int METADATA_TYPE_ID3 = 1; + /** Type for ESMG metadata in HLS streams. */ + public static final int METADATA_TYPE_EMSG = 3; + /** Factory for {@link HlsMediaSource}s. */ public static final class Factory implements MediaSourceFactory { @@ -70,7 +97,7 @@ public final class HlsMediaSource extends BaseMediaSource private DrmSessionManager drmSessionManager; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private boolean allowChunklessPreparation; - @HlsMetadataType private int metadataType; + @MetadataType private int metadataType; private boolean useSessionKeys; private boolean isCreateCalled; @Nullable private Object tag; @@ -100,7 +127,7 @@ public final class HlsMediaSource extends BaseMediaSource drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); - metadataType = HlsMetadataType.ID3; + metadataType = METADATA_TYPE_ID3; } /** @@ -232,24 +259,24 @@ public final class HlsMediaSource extends BaseMediaSource /** * Sets the type of metadata to extract from the HLS source (defaults to {@link - * HlsMetadataType#ID3}). + * #METADATA_TYPE_ID3}). * *

      HLS supports in-band ID3 in both TS and fMP4 streams, but in the fMP4 case the data is * wrapped in an EMSG box [spec]. * - *

      If this is set to {@link HlsMetadataType#ID3} then raw ID3 metadata of will be extracted + *

      If this is set to {@link #METADATA_TYPE_ID3} then raw ID3 metadata of will be extracted * from TS sources. From fMP4 streams EMSGs containing metadata of this type (in the variant * stream only) will be unwrapped to expose the inner data. All other in-band metadata will be * dropped. * - *

      If this is set to {@link HlsMetadataType#EMSG} then all EMSG data from the fMP4 variant + *

      If this is set to {@link #METADATA_TYPE_EMSG} then all EMSG data from the fMP4 variant * stream will be extracted. No metadata will be extracted from TS streams, since they don't * support EMSG. * * @param metadataType The type of metadata to extract. * @return This factory, for convenience. */ - public Factory setMetadataType(@HlsMetadataType int metadataType) { + public Factory setMetadataType(@MetadataType int metadataType) { Assertions.checkState(!isCreateCalled); this.metadataType = metadataType; return this; @@ -348,7 +375,7 @@ public final class HlsMediaSource extends BaseMediaSource private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final boolean allowChunklessPreparation; - private final @HlsMetadataType int metadataType; + private final @MetadataType int metadataType; private final boolean useSessionKeys; private final HlsPlaylistTracker playlistTracker; @Nullable private final Object tag; @@ -364,7 +391,7 @@ public final class HlsMediaSource extends BaseMediaSource LoadErrorHandlingPolicy loadErrorHandlingPolicy, HlsPlaylistTracker playlistTracker, boolean allowChunklessPreparation, - @HlsMetadataType int metadataType, + @MetadataType int metadataType, boolean useSessionKeys, @Nullable Object tag) { this.manifestUri = manifestUri; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMetadataType.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMetadataType.java deleted file mode 100644 index 8fb6c220cf..0000000000 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMetadataType.java +++ /dev/null @@ -1,38 +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.source.hls; - -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import androidx.annotation.IntDef; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; - -/** - * The types of metadata that can be extracted from HLS streams. - * - *

      See {@link HlsMediaSource.Factory#setMetadataType(int)}. - */ -@Documented -@Retention(SOURCE) -@IntDef({HlsMetadataType.ID3, HlsMetadataType.EMSG}) -public @interface HlsMetadataType { - /** Type for ID3 metadata in HLS streams. */ - int ID3 = 1; - /** Type for ESMG metadata in HLS streams. */ - int EMSG = 3; -} 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 d99bb817c1..8aedb6a545 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 @@ -116,7 +116,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final Loader loader; private final EventDispatcher eventDispatcher; - private final @HlsMetadataType int metadataType; + private final @HlsMediaSource.MetadataType int metadataType; private final HlsChunkSource.HlsChunkHolder nextChunkHolder; private final ArrayList mediaChunks; private final List readOnlyMediaChunks; @@ -190,7 +190,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, - @HlsMetadataType int metadataType) { + @HlsMediaSource.MetadataType int metadataType) { this.trackType = trackType; this.callback = callback; this.chunkSource = chunkSource; @@ -1362,14 +1362,15 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private byte[] buffer; private int bufferPosition; - public EmsgUnwrappingTrackOutput(TrackOutput delegate, @HlsMetadataType int metadataType) { + public EmsgUnwrappingTrackOutput( + TrackOutput delegate, @HlsMediaSource.MetadataType int metadataType) { this.emsgDecoder = new EventMessageDecoder(); this.delegate = delegate; switch (metadataType) { - case HlsMetadataType.ID3: + case HlsMediaSource.METADATA_TYPE_ID3: delegateFormat = ID3_FORMAT; break; - case HlsMetadataType.EMSG: + case HlsMediaSource.METADATA_TYPE_EMSG: delegateFormat = EMSG_FORMAT; break; default: diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java index 73ef11bda9..820c39c197 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java @@ -92,7 +92,7 @@ public final class HlsMediaPeriodTest { mock(Allocator.class), mock(CompositeSequenceableLoaderFactory.class), /* allowChunklessPreparation =*/ true, - HlsMetadataType.ID3, + HlsMediaSource.METADATA_TYPE_ID3, /* useSessionKeys= */ false); }; From 453bd392741a9b0136b56f8a6010aac51b51faf4 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 18 Dec 2019 18:57:47 +0000 Subject: [PATCH 0560/1335] Fix typo in class name PiperOrigin-RevId: 286225012 --- .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 4 ++-- ...decAdapter.java => MultiLockAsyncMediaCodecAdapter.java} | 6 +++--- .../mediacodec/MultiLockAsyncMediaCodecAdapterTest.java | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) rename library/core/src/main/java/com/google/android/exoplayer2/mediacodec/{MultiLockAsynchMediaCodecAdapter.java => MultiLockAsyncMediaCodecAdapter.java} (97%) 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 48a415ec7f..e973b70204 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 @@ -998,8 +998,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ((DedicatedThreadAsyncMediaCodecAdapter) codecAdapter).start(); } else if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK && Util.SDK_INT >= 23) { - codecAdapter = new MultiLockAsynchMediaCodecAdapter(codec, getTrackType()); - ((MultiLockAsynchMediaCodecAdapter) codecAdapter).start(); + codecAdapter = new MultiLockAsyncMediaCodecAdapter(codec, getTrackType()); + ((MultiLockAsyncMediaCodecAdapter) codecAdapter).start(); } else { codecAdapter = new SynchronousMediaCodecAdapter(codec, getDequeueOutputBufferTimeoutUs()); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsynchMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapter.java similarity index 97% rename from library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsynchMediaCodecAdapter.java rename to library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapter.java index 7d00d15947..56f503c71a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsynchMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapter.java @@ -51,7 +51,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; *

      After creating an instance, you need to call {@link #start()} to start the internal Thread. */ @RequiresApi(23) -/* package */ final class MultiLockAsynchMediaCodecAdapter extends MediaCodec.Callback +/* package */ final class MultiLockAsyncMediaCodecAdapter extends MediaCodec.Callback implements MediaCodecAdapter { @IntDef({STATE_CREATED, STATE_STARTED, STATE_SHUT_DOWN}) @@ -97,12 +97,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private Runnable onCodecStart; /** Creates a new instance that wraps the specified {@link MediaCodec}. */ - /* package */ MultiLockAsynchMediaCodecAdapter(MediaCodec codec, int trackType) { + /* package */ MultiLockAsyncMediaCodecAdapter(MediaCodec codec, int trackType) { this(codec, new HandlerThread(createThreadLabel(trackType))); } @VisibleForTesting - /* package */ MultiLockAsynchMediaCodecAdapter(MediaCodec codec, HandlerThread handlerThread) { + /* package */ MultiLockAsyncMediaCodecAdapter(MediaCodec codec, HandlerThread handlerThread) { this.codec = codec; inputBufferLock = new Object(); outputBufferLock = new Object(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java index 815d6ab3da..b984d28914 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java @@ -38,10 +38,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.shadows.ShadowLooper; -/** Unit tests for {@link MultiLockAsynchMediaCodecAdapter}. */ +/** Unit tests for {@link MultiLockAsyncMediaCodecAdapter}. */ @RunWith(AndroidJUnit4.class) public class MultiLockAsyncMediaCodecAdapterTest { - private MultiLockAsynchMediaCodecAdapter adapter; + private MultiLockAsyncMediaCodecAdapter adapter; private MediaCodec codec; private MediaCodec.BufferInfo bufferInfo = null; private MediaCodecAsyncCallback mediaCodecAsyncCallbackSpy; @@ -51,7 +51,7 @@ public class MultiLockAsyncMediaCodecAdapterTest { public void setup() throws IOException { codec = MediaCodec.createByCodecName("h264"); handlerThread = new TestHandlerThread("TestHandlerThread"); - adapter = new MultiLockAsynchMediaCodecAdapter(codec, handlerThread); + adapter = new MultiLockAsyncMediaCodecAdapter(codec, handlerThread); bufferInfo = new MediaCodec.BufferInfo(); } From fc4b258c109b170b7c46ad275d0d63e96b519809 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 19 Dec 2019 12:22:24 +0000 Subject: [PATCH 0561/1335] Bump to 2.11.1 PiperOrigin-RevId: 286368964 --- RELEASENOTES.md | 28 +++++++++++-------- constants.gradle | 4 +-- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 ++-- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ce54fd8405..6edf1e1f7f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -19,26 +19,32 @@ * Upgrade Truth dependency from 0.44 to 1.0. * Upgrade to JUnit 4.13-rc-2. * Add support for attaching DRM sessions to clear content in the demo app. -* UI: Exclude `DefaultTimeBar` region from system gesture detection - ([#6685](https://github.com/google/ExoPlayer/issues/6685)). * Add `SpannedSubject` to testutils, for assertions on [Span-styled text]( https://developer.android.com/guide/topics/text/spans) (e.g. subtitles). * Add `Player.getCurrentLiveOffset` to conveniently return the live offset. -* Propagate HTTP request headers through `CacheDataSource`. * Update `IcyDecoder` to try ISO-8859-1 decoding if UTF-8 decoding fails. Also change `IcyInfo.rawMetadata` from `String` to `byte[]` to allow developers to handle data that's neither UTF-8 nor ISO-8859-1 ([#6753](https://github.com/google/ExoPlayer/issues/6753)). -* AV1 extension: Fix ProGuard rules - ([6773](https://github.com/google/ExoPlayer/issues/6773)). -* Suppress ProGuard warnings for compile-time `javax.annotation` package - ([#6771](https://github.com/google/ExoPlayer/issues/6771)). * Add playlist API ([#6161](https://github.com/google/ExoPlayer/issues/6161)). -* Fix proguard rules for R8 to ensure raw resources used with - `RawResourceDataSource` are kept. -* Fix proguard rules to keep `VideoDecoderOutputBuffer` for video decoder - extensions. + +### 2.11.1 (2019-12-20) ### + +* UI: Exclude `DefaultTimeBar` region from system gesture detection + ([#6685](https://github.com/google/ExoPlayer/issues/6685)). +* ProGuard fixes: + * Ensure `Libgav1VideoRenderer` constructor is kept for use by + `DefaultRenderersFactory` + ([#6773](https://github.com/google/ExoPlayer/issues/6773)). + * Ensure `VideoDecoderOutputBuffer` and its members are kept for use by video + decoder extensions. + * Ensure raw resources used with `RawResourceDataSource` are kept. + * Suppress spurious warnings about the `javax.annotation` package, and + restructure use of `IntDef` annotations to remove spurious warnings about + `SsaStyle$SsaAlignment` + ([#6771](https://github.com/google/ExoPlayer/issues/6771)). +* Fix `CacheDataSource` to correctly propagate `DataSpec.httpRequestHeaders`. ### 2.11.0 (2019-12-11) ### diff --git a/constants.gradle b/constants.gradle index 67207b4156..88bfe41d5a 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.11.0' - releaseVersionCode = 2011000 + releaseVersion = '2.11.1' + releaseVersionCode = 2011001 minSdkVersion = 16 appTargetSdkVersion = 29 targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved 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 249ef7e44e..217f580e7b 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.11.0"; + public static final String VERSION = "2.11.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.11.0"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.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 = 2011000; + public static final int VERSION_INT = 2011001; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 0f94ebfb7da6ae02c11df0b0f890f60a8c628572 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 19 Dec 2019 14:26:44 +0000 Subject: [PATCH 0562/1335] Add NonNull at package level for extractor PiperOrigin-RevId: 286381499 --- .../extractor/BinarySearchSeeker.java | 3 +-- .../exoplayer2/extractor/ChunkIndex.java | 2 +- .../extractor/DefaultExtractorsFactory.java | 4 +++- .../extractor/FlacMetadataReader.java | 2 +- .../extractor/GaplessInfoHolder.java | 6 ++++-- .../exoplayer2/extractor/Id3Peeker.java | 2 +- .../exoplayer2/extractor/package-info.java | 19 +++++++++++++++++++ 7 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/extractor/package-info.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java index 0d823fa31d..9d5f7f1788 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java @@ -91,7 +91,7 @@ public abstract class BinarySearchSeeker { protected final BinarySearchSeekMap seekMap; protected final TimestampSeeker timestampSeeker; - protected @Nullable SeekOperationParams seekOperationParams; + @Nullable protected SeekOperationParams seekOperationParams; private final int minimumSearchRange; @@ -173,7 +173,6 @@ public abstract class BinarySearchSeeker { */ public int handlePendingSeek(ExtractorInput input, PositionHolder seekPositionHolder) throws InterruptedException, IOException { - TimestampSeeker timestampSeeker = Assertions.checkNotNull(this.timestampSeeker); while (true) { SeekOperationParams seekOperationParams = Assertions.checkNotNull(this.seekOperationParams); long floorPosition = seekOperationParams.getFloorBytePosition(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java index 7ddd03bbd5..45c567235a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java @@ -76,7 +76,7 @@ public final class ChunkIndex implements SeekMap { * @return The index of the corresponding chunk. */ public int getChunkIndex(long timeUs) { - return Util.binarySearchFloor(timesUs, timeUs, true, true); + return Util.binarySearchFloor(timesUs, timeUs, /* inclusive= */ true, /* stayInBounds= */ true); } // SeekMap implementation. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java index 588669e12f..1f7b6f7098 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.extractor.amr.AmrExtractor; import com.google.android.exoplayer2.extractor.flac.FlacExtractor; import com.google.android.exoplayer2.extractor.flv.FlvExtractor; @@ -56,10 +57,11 @@ import java.lang.reflect.Constructor; */ public final class DefaultExtractorsFactory implements ExtractorsFactory { + @Nullable private static final Constructor FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR; static { - Constructor flacExtensionExtractorConstructor = null; + @Nullable Constructor flacExtensionExtractorConstructor = null; try { // LINT.IfChange flacExtensionExtractorConstructor = diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacMetadataReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacMetadataReader.java index 49d4558ddc..dddff9f166 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacMetadataReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacMetadataReader.java @@ -171,7 +171,7 @@ public final class FlacMetadataReader { if (type == FlacConstants.METADATA_TYPE_STREAM_INFO) { metadataHolder.flacStreamMetadata = readStreamInfoBlock(input); } else { - FlacStreamMetadata flacStreamMetadata = metadataHolder.flacStreamMetadata; + @Nullable FlacStreamMetadata flacStreamMetadata = metadataHolder.flacStreamMetadata; if (flacStreamMetadata == null) { throw new IllegalArgumentException(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java index a0effc0df8..1ba316c64b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.extractor; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.CommentFrame; @@ -107,8 +109,8 @@ public final class GaplessInfoHolder { Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data); if (matcher.find()) { try { - int encoderDelay = Integer.parseInt(matcher.group(1), 16); - int encoderPadding = Integer.parseInt(matcher.group(2), 16); + int encoderDelay = Integer.parseInt(castNonNull(matcher.group(1)), 16); + int encoderPadding = Integer.parseInt(castNonNull(matcher.group(2)), 16); if (encoderDelay > 0 || encoderPadding > 0) { this.encoderDelay = encoderDelay; this.encoderPadding = encoderPadding; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java index 60386dcc3c..f2eb644870 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java @@ -50,7 +50,7 @@ public final class Id3Peeker { ExtractorInput input, @Nullable Id3Decoder.FramePredicate id3FramePredicate) throws IOException, InterruptedException { int peekedId3Bytes = 0; - Metadata metadata = null; + @Nullable Metadata metadata = null; while (true) { try { input.peekFully(scratch.data, /* offset= */ 0, Id3Decoder.ID3_HEADER_LENGTH); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/package-info.java new file mode 100644 index 0000000000..9920b247e6 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/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.extractor; + +import com.google.android.exoplayer2.util.NonNullApi; From 8c0f22c99ca4e25796d9e0614ecd561e41708e39 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 20 Dec 2019 10:23:27 +0000 Subject: [PATCH 0563/1335] Add missing @Nullable to MediaCodecAudioRenderer.getMediaClock Without this @Nullable, potential subclasses can't override the method to return null if they don't want to use the renderer as a media clock. Issue:#6792 PiperOrigin-RevId: 286545736 --- RELEASENOTES.md | 3 +++ .../android/exoplayer2/audio/MediaCodecAudioRenderer.java | 1 + .../android/exoplayer2/audio/SimpleDecoderAudioRenderer.java | 1 + 3 files changed, 5 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6edf1e1f7f..76d12bce4e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -45,6 +45,9 @@ `SsaStyle$SsaAlignment` ([#6771](https://github.com/google/ExoPlayer/issues/6771)). * Fix `CacheDataSource` to correctly propagate `DataSpec.httpRequestHeaders`. +* Add missing @Nullable to `MediaCodecAudioRenderer.getMediaClock` and + `SimpleDecoderAudioRenderer.getMediaClock` + ([#6792](https://github.com/google/ExoPlayer/issues/6792)). ### 2.11.0 (2019-12-11) ### 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 463f806c01..dfa13134ce 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 @@ -520,6 +520,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override + @Nullable public MediaClock getMediaClock() { return this; } 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 5ccbf04c5c..60870204cc 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 @@ -218,6 +218,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } @Override + @Nullable public MediaClock getMediaClock() { return this; } From 06ffd23cdc14fb13c6a474bb8e2a54f80505ebf4 Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 20 Dec 2019 11:24:26 +0000 Subject: [PATCH 0564/1335] make removeMediaItem return void PiperOrigin-RevId: 286551438 --- .../com/google/android/exoplayer2/ExoPlayer.java | 4 +--- .../com/google/android/exoplayer2/ExoPlayerImpl.java | 12 ++++-------- .../google/android/exoplayer2/SimpleExoPlayer.java | 4 ++-- .../android/exoplayer2/testutil/StubExoPlayer.java | 2 +- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index b35f617048..f845ece3c7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -486,10 +486,8 @@ public interface ExoPlayer extends Player { * Removes the media item at the given index of the playlist. * * @param index The index at which to remove the media item. - * @return The removed {@link MediaSource} or null if no item exists at the given index. */ - @Nullable - MediaSource removeMediaItem(int index); + void removeMediaItem(int index); /** * Removes a range of media items from the playlist. 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 97ba989c34..0f3e1d98ab 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 @@ -368,10 +368,8 @@ import java.util.concurrent.TimeoutException; } @Override - public MediaSource removeMediaItem(int index) { - List mediaSourceHolders = - removeMediaItemsInternal(/* fromIndex= */ index, /* toIndex= */ index + 1); - return mediaSourceHolders.isEmpty() ? null : mediaSourceHolders.get(0).mediaSource; + public void removeMediaItem(int index) { + removeMediaItemsInternal(/* fromIndex= */ index, /* toIndex= */ index + 1); } @Override @@ -956,7 +954,7 @@ import java.util.concurrent.TimeoutException; return holders; } - private List removeMediaItemsInternal(int fromIndex, int toIndex) { + private void removeMediaItemsInternal(int fromIndex, int toIndex) { Assertions.checkArgument( fromIndex >= 0 && toIndex >= fromIndex && toIndex <= mediaSourceHolders.size()); int currentWindowIndex = getCurrentWindowIndex(); @@ -965,8 +963,7 @@ import java.util.concurrent.TimeoutException; Timeline oldTimeline = getCurrentTimeline(); int currentMediaSourceCount = mediaSourceHolders.size(); pendingOperationAcks++; - List removedHolders = - removeMediaSourceHolders(fromIndex, /* toIndexExclusive= */ toIndex); + removeMediaSourceHolders(fromIndex, /* toIndexExclusive= */ toIndex); Timeline timeline = maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline); // Player transitions to STATE_ENDED if the current index is part of the removed tail. @@ -987,7 +984,6 @@ import java.util.concurrent.TimeoutException; listener.onPlayerStateChanged(currentPlayWhenReady, STATE_ENDED); } }); - return removedHolders; } private List removeMediaSourceHolders( 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 a3dd3018ed..709d8d9aab 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 @@ -1290,9 +1290,9 @@ public class SimpleExoPlayer extends BasePlayer } @Override - public MediaSource removeMediaItem(int index) { + public void removeMediaItem(int index) { verifyApplicationThread(); - return player.removeMediaItem(index); + player.removeMediaItem(index); } @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index b1851106dc..62666eac09 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -190,7 +190,7 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { } @Override - public MediaSource removeMediaItem(int index) { + public void removeMediaItem(int index) { throw new UnsupportedOperationException(); } From 5920305b84933a80aaffd9957a3e3f7d5368f6c2 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 20 Dec 2019 12:14:29 +0000 Subject: [PATCH 0565/1335] Fix typo Merge of https://github.com/google/ExoPlayer/pull/6793 PiperOrigin-RevId: 286556008 --- .../google/android/exoplayer2/trackselection/TrackSelector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java index bc6cf14371..d48c140ac8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java @@ -73,7 +73,7 @@ import com.google.android.exoplayer2.util.Assertions; * the two are tightly bound together. It may only be possible to play a certain combination tracks * if the renderers are configured in a particular way. Equally, it may only be possible to * configure renderers in a particular way if certain tracks are selected. Hence it makes sense to - * determined the track selection and corresponding renderer configurations in a single step. + * determine the track selection and corresponding renderer configurations in a single step. * *

      Threading model

      * From 8e96188909cae8b6734e948c6019b4edda4e2512 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 20 Dec 2019 15:37:08 +0000 Subject: [PATCH 0566/1335] Relax MP4 sniffing to allow an atom to extend beyond the file length Issue: #6774 PiperOrigin-RevId: 286575797 --- .../com/google/android/exoplayer2/extractor/mp4/Sniffer.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java index 95193785c0..dac74bfe2b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java @@ -118,10 +118,6 @@ import java.io.IOException; } } - if (inputLength != C.LENGTH_UNSET && bytesSearched + atomSize > inputLength) { - // The file is invalid because the atom extends past the end of the file. - return false; - } if (atomSize < headerSize) { // The file is invalid because the atom size is too small for its header. return false; From 19fb25101bd47b0e42e19ab1830d25bea13b2358 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 20 Dec 2019 15:41:01 +0000 Subject: [PATCH 0567/1335] DefaultDownloadIndex: Clear failure reason when removing download The Download constructor considers it invalid to have a failure reason if the download isn't in the failed state. Unfortunately, calling DefaultDownloadIndex.removeAllDownloads when there's a failed download will change the state without clearing the reason. If the downloads are then read back from the DefaultDownloadIndex we end up violating the Download constructor assertion. This change clears the failed reason for any existing rows in the invalid state, and also fixes the root cause that allows invalid rows to enter the table in the first place. Issue: #6785 PiperOrigin-RevId: 286576242 --- RELEASENOTES.md | 4 ++++ .../offline/DefaultDownloadIndex.java | 17 +++++++++++++++-- .../android/exoplayer2/offline/Download.java | 4 ++-- .../offline/DefaultDownloadIndexTest.java | 17 +++++++++++++++++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 76d12bce4e..917f62ddf0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -45,6 +45,10 @@ `SsaStyle$SsaAlignment` ([#6771](https://github.com/google/ExoPlayer/issues/6771)). * Fix `CacheDataSource` to correctly propagate `DataSpec.httpRequestHeaders`. +* Fix issue with `DefaultDownloadIndex` that could result in an + `IllegalStateException` being thrown from + `DefaultDownloadIndex.getDownloadForCurrentRow` + ([#6785](https://github.com/google/ExoPlayer/issues/6785)). * Add missing @Nullable to `MediaCodecAudioRenderer.getMediaClock` and `SimpleDecoderAudioRenderer.getMediaClock` ([#6792](https://github.com/google/ExoPlayer/issues/6792)). 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 a1c73f74c5..4437fccd16 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 @@ -26,6 +26,8 @@ import androidx.annotation.VisibleForTesting; 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.offline.Download.FailureReason; +import com.google.android.exoplayer2.offline.Download.State; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; @@ -239,6 +241,9 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { try { ContentValues values = new ContentValues(); values.put(COLUMN_STATE, Download.STATE_REMOVING); + // Only downloads in STATE_FAILED are allowed a failure reason, so we need to clear it here in + // case we're moving downloads from STATE_FAILED to STATE_REMOVING. + values.put(COLUMN_FAILURE_REASON, Download.FAILURE_REASON_NONE); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); writableDatabase.update(tableName, values, /* whereClause= */ null, /* whereArgs= */ null); } catch (SQLException e) { @@ -349,14 +354,22 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { DownloadProgress downloadProgress = new DownloadProgress(); downloadProgress.bytesDownloaded = cursor.getLong(COLUMN_INDEX_BYTES_DOWNLOADED); downloadProgress.percentDownloaded = cursor.getFloat(COLUMN_INDEX_PERCENT_DOWNLOADED); + @State int state = cursor.getInt(COLUMN_INDEX_STATE); + // It's possible the database contains failure reasons for non-failed downloads, which is + // invalid. Clear them here. See https://github.com/google/ExoPlayer/issues/6785. + @FailureReason + int failureReason = + state == Download.STATE_FAILED + ? cursor.getInt(COLUMN_INDEX_FAILURE_REASON) + : Download.FAILURE_REASON_NONE; return new Download( request, - /* state= */ cursor.getInt(COLUMN_INDEX_STATE), + 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), + failureReason, downloadProgress); } 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 97dff8394e..da46120b29 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 @@ -130,9 +130,9 @@ public final class Download { @FailureReason int failureReason, DownloadProgress progress) { Assertions.checkNotNull(progress); - Assertions.checkState((failureReason == FAILURE_REASON_NONE) == (state != STATE_FAILED)); + Assertions.checkArgument((failureReason == FAILURE_REASON_NONE) == (state != STATE_FAILED)); if (stopReason != 0) { - Assertions.checkState(state != STATE_DOWNLOADING && state != STATE_QUEUED); + Assertions.checkArgument(state != STATE_DOWNLOADING && state != STATE_QUEUED); } this.request = request; this.state = state; 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 f42a1c6086..d7664e21ca 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 @@ -248,6 +248,23 @@ public class DefaultDownloadIndexTest { assertEqual(readDownload, download); } + @Test + public void setStatesToRemoving_setsStateAndClearsFailureReason() throws Exception { + String id = "id"; + DownloadBuilder downloadBuilder = + new DownloadBuilder(id) + .setState(Download.STATE_FAILED) + .setFailureReason(Download.FAILURE_REASON_UNKNOWN); + Download download = downloadBuilder.build(); + downloadIndex.putDownload(download); + + downloadIndex.setStatesToRemoving(); + + download = downloadIndex.getDownload(id); + assertThat(download.state).isEqualTo(Download.STATE_REMOVING); + assertThat(download.failureReason).isEqualTo(Download.FAILURE_REASON_NONE); + } + @Test public void setSingleDownloadStopReason_setReasonToNone() throws Exception { String id = "id"; From 7bea558b31df52847713dc7f9baab2d4821d9112 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 20 Dec 2019 15:42:10 +0000 Subject: [PATCH 0568/1335] Add test case for mdat atom extending beyond the file length Issue: #6774 PiperOrigin-RevId: 286576383 --- .../test/assets/mp4/sample_mdat_too_long.mp4 | Bin 0 -> 101597 bytes .../mp4/sample_mdat_too_long.mp4.0.dump | 359 ++++++++++++++++++ .../mp4/sample_mdat_too_long.mp4.1.dump | 311 +++++++++++++++ .../mp4/sample_mdat_too_long.mp4.2.dump | 251 ++++++++++++ .../mp4/sample_mdat_too_long.mp4.3.dump | 191 ++++++++++ .../extractor/mp4/Mp4ExtractorTest.java | 9 + 6 files changed, 1121 insertions(+) create mode 100644 library/core/src/test/assets/mp4/sample_mdat_too_long.mp4 create mode 100644 library/core/src/test/assets/mp4/sample_mdat_too_long.mp4.0.dump create mode 100644 library/core/src/test/assets/mp4/sample_mdat_too_long.mp4.1.dump create mode 100644 library/core/src/test/assets/mp4/sample_mdat_too_long.mp4.2.dump create mode 100644 library/core/src/test/assets/mp4/sample_mdat_too_long.mp4.3.dump diff --git a/library/core/src/test/assets/mp4/sample_mdat_too_long.mp4 b/library/core/src/test/assets/mp4/sample_mdat_too_long.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..f50d4f49de53c19d49856c047580278c9a5e80ec GIT binary patch literal 101597 zcmbrl1ytP4(kDE)ySr;}cXxMpcN^Rj+zFQ88r+@W8r&U%2X_b__=Y_9?%wy^{r2oR z+h=}L)m7Ei)z#hg2W9{O0Hjv#J}$OEXGZ`49Pmy-U^DeJXLWSpU77mv0HV^`iwoXtM^%m|Fv0|pm_go_P^vH9T>p;f7*iV-t9rj|4R|t|Dk_38YIVv`>%RX z@2}netsbT5e?R{J)$jk)ctTK4%>Spf@hhz03<|D}5;p!_6gfR0TM=m=|r zy4;|=u7UV`5D)+WoGMU%3&cBvhJOR)ivq;IA9v7M!IA?VeFBi)4g@3s0Nn#bUqQeI z>CFHD_-+sd9ei*{(AY>0_vlK0P+Ed1iG7p{QhmAIA9Y2fG>LhKqV-r)z1LH zge(9s(**!55CQq_Amf2r$qo55FG&KaRUJJTLpl9BmsbhVu4~Z_&*c| z@PERC{8yNN3D|og|7P>QNdL0~0Eqn8F3Z0P1MPnlhIii#Z=m*%uYb+)*E#s`|9_FF zlmGx~CH{9s0_g1F^qqL7{>gyCv|SiVOj&$mF0W1tf@Kihyzt%080FJAnd8z|mo$K?LkQ zFQor*-~Z+bbbj9L-luqX`hPm(!~gA!GlJ}KK!64z1%yrzCO~+v3GbK!VH<=c5avKw z2H_V7pa&G-1cU<+c0l;I9CSYbS0L7|) zApQ}Qo97+?;01IqP~!n$7!?38Y|#0_IR@Pk0st60E(n(ZFdkt5m;epvu7LU~t^hDi zLjagA2zn9#FvB1Km@Oj!%#IWQ<_NNN6$gO1v4ZXm0szeS000&w2LKCZ1Av8qWZ{Yc zum~OiSfmI5EJ_pr7EK8NiviuWm}>x7?0Y_qJuKW!K~&q(@-GFk0J8sluz}9Go2iS- z`?KeNG)@8I`D0TfIv?xbuiEbNSIEUcU$p^dw{3m-GHmzNjQ z`%~H3!PJS#+0B~yeHJDgcSi@1jkAlpt+Nx5kJQ}M%+y?vl@#>k6=Wy1urzaUHn$gK z}DYdS}Pza_LzRpgTf^4h|tgNI~ra*UN7ofeZ%e%!t4qRP~ovo~ZmhOU#Y^3fsZlDn$ zM{J}H&d&CxHlUvIzlCh1KnGiMP%!@%u#h^r{hh?z*3s1cJrG+bcS|=1Q;-ozZsy?O zX6j>X?(FDd>JI9ggK`8azP3&v7a&77({~#yH&aJTAShdA#x6b}-qu2p4aA#Tn7X_# z!_3&s))e?YiLIOE--UTu+FIMVn}KxBE|yNl*3K> z2cxKpjCg zb|w~5*Z0K;vM_OhSeJLlf2parAQvx40Ccx>5#%7Xbph=X=tO`v5#-C%6?6dJw-XQy zdLU<8Muq~U*R10V(ogF2{n6((Nu>9Co1!~6-`?7!>6F5nlcU;7gT&Nk@xXK1Ow3O5 z7;X~Ob$gLTrO|$|4_f??Ieq$66GZy0_wd$MV}EaEnJ-@Vly%8)vo>IuKIs%SYlpZ} zi1-5OrbYwM<*GlPQI%9)E4v{4O*_^s$4Fvuaz#$Sy=THCD2CdToZF+aCOuYZ4!G-; zrV;emt$bi!R@GQQ6|au(UR8cy3V7sh`V)NIg;0|NWH6s8@9^g-IUst9;ccf&`L-EVck+?N^Jf|}FiAw#XF7+X@eTt{$dev1^z0Tu zF&BXFs*RQ)em!%X+!stVdv!!e#UL&JVG(@gE9y*QLRDixAx^-|D3g-YeFQ` z7K-Buu|p0k(<%SE03CdFXGd`igUGYzmc?yZ-HGJN3UY+@So*~jmJ0Xx+V)+y(RV={ zooa}L(b5$TD&H<*wxuIydoyMeJv-yn0d-lR%fZ}SOk(W-RyVWa5W@&xJ9R^e&*6_B z(X(RGuVSXi`c{YKhyGb_%I*nITQm}zOQnoTl-mW~PdmGUwlX16Y{y_`PWN!xHWu@n z%$gGCy)EgH-_pt45oh-d7%EUj&O8e*+r7#pNU>*!Ol8zcR30uWewOA7Kt_nU6mbfD zs%++%e(ax^T=yNCRF7=w*=%%Gdi-5%fC%a{$PNjahu+ zYLwqpu>G=;#FG)|P)J<`=Fr|ueBZswY&xpNFL<+2 z4s5)B^cMCdKg1KN*IaXucCVw(Nlm#QKgg>))p+X2u^(%V6krABpE(LTE1}xZtIs{L z2NWdja~4A>X;!ZojF$>!49@gNcJH!w3-P^J$;3ry(bMn-e02QgH#zobhny5T{MH&6 zC4YQm>)ThhSCERO8j+UGj9ImrBOk`*l-r4yeuNlq{)O+0gLvzGH$tR-NYul;P!SY_ z@l0Jyvu?9^S})GmPCR!5B={^Hruq|HOEh^XY`;;9fwbXz+}0Bljf?rzpgn^W=s$8O zgh&b;*6#T|~xLslFQE_NE}+Mw@9w;M!TPqb^(742$ybrV5aKlNXut=h1KOPQq@Uzs#?Y zUxE+7vv~l^C2wk-6IE|~^pw5x-!TsFAHvuKf7W%1aWgA!4lXv(j+rI)5ntxh`xtAa zW%+hkm_YVp8uy5sMx8GhWpc~$BXpP|eft6ue;N#HG?z;=aa*2s5yA+S$rQOWno9N{ zgVZ%e@kzA}dm0ocBYA>cIwQvoNwOy~;*Hj+j@ph@)*Abn_Y($D`7viZ#*O0yU&KN2 zCnqsDmrSkSs1-JI<7C<)xoAZ}!>f14DmFXTD2V*U>LZ%Ljpzy`Z^tKj++n~bI>jnk zWvHAyqoYdw#_N}!Pq&$hLGx6GC+pfNYF@H+Ub`mKh%S6bco#@<=PL*JV{uqtl7dlT>6tFMdur%F zkGBxVG6kjgSG8;nVs2HQP8SX*=TNO=i+en*HgZV?mc)9m#s6gT+9T3OY}pnvxYLWQ zvdSLZk0|K86=^TM%@gEh?D1^KC3X`CEpC~iIh_=X9Idtd<}VBE$0fgvkRw0S=fA*Q z$s7Jv5y<`E&2117_Mlp?!bg6Bucu3FCV3aP4hP^$d=62HJaaF-O>#*mAZS=@xE!H9 z++eVveMWgETyY*H{+Px|5#7ixSox!JY;>FIRp#W(o6Vj&LsGBUsp#u{qK;G%8V!#mRP^xnKfw%lH_auC;Mg7|oyKb} za8%QtaG#VGe4xi8OH60IZ$?z_p#Wk&?t-zqVo5u8!^Vx*>+jw z7X|O3lqUKMW8BeIEQzt03NJusM6cwRGdGfI@-t*I+AEaf_6`MT$z02cIf6J-FBwth ziyZC|s1bJQK#KynrtngUR7HKUKuKTS*iaYl_Qr%cvLpWWgecSbEH^jVFStq_vrG31A@g0fN zuWgkLI$PLgHqUj2=tDmdpjBdCyzI-=3&q-*V|E6PCqy^vNE%4u8g2UGeA` zpQ5vSS8rG-3*i|Xu09suaYMb~dDbrse_F0zuB!`Co*%eNscOaxVr@&v(0av@q$utF zaxajgG!f969MK`6wz&{ElYVRXxqpLi4~gddE4X&q{%=BFKkg1Q`_BHa#S>laq?H3u zD(f?7a0TmXvHD@-Eh)f?h6{tQ&!ppRzSFy-OepS~V$`k8F*UEGdIW6Jb;>f-{z<)D z`SFmwR0H-Z(}bF@lT1w(j&H3!8FY_fa~|hFhi+G~igCNEo(^T5HL zyXEukE%|OI6>de1*nL>$JzEDMFI&zpSv5De8ho2kwEbNqEYW4#M_leZ0p=o!8u45V zjjPT@P2qY`G|B`|7Y+`vG;+BC^QCX6PQ=9wxYpdBCE4^x%cv2*v^eKF7WMtyF-}ivF-tAHBeG6 z%rFx)-7a~QM)Og@K0iRvShhPrZ}@1WP58Ej+aP|Hy!nw1fok~6KHSpFgzy9I2a76{ z@|&H@up@(2tzRTmd2zoQ$}%o_@${}v1YLXnP+Mo0l)p-V8%|5}_h`WP?e=wimVDWP z<^CD7W%YZ^679?JTFC-?HMdW(vMA@;JlLv%7BI+_ z6u+(p3$xupaw6r2Ejbsb24-xMhz-#&4YP!v*xVf2&D_{SnL9J+c=0d`m;>k!`o~!2 z3Exi%B5)H4-T$;40R_G-aHnYo+;{U-{VMh~3yBqlf=6@n_p-lvxu`_PErG$o)F)4= zY3+t<88(a2iQ)L{NqgU$apV4(9^(S1=uxTHB<8n>s(_$fa8mA`8Y$?HHt#QWh|_6a z+|RtMtZi**3?`y@>aHL29%~Ybii8;=2Gy{h5RBwnE5GQW@HnX#V)S;mqmvUxhNyJB z9vDqQx}8u+zqoHpWFQ_s9Mg#x1pc(k;vbW8Gu`62d7YVL%)Wy|xTwWZq;HwKOz>Q) z(DnL;SsB8+kD?wIS*>{ohODZgs2_~TVBRK&%Z8P6i6Q9=H!7Zzi}7ldx00DbK7Y+s z)R~XA2*W*KD(Y4FoB?sKgtLJVA(M+a1}xdykxzeo-dE~VddsU81lttNie0+%1HTN- zW~LTOpT7oo>6pLmP&7QcG7B;gI77LlzbX*!f+LA#^_{ich@Y4kUsAIjHaXTeTbC-) zmta+Cawz8{LOFNrvkwwe{>^r=x&f02f71H_dyhDB>+IzYoD2U|_f`%7G2g3D20KEl=1f##{tX7);y17|2%tgp1lSIG+TlR8C80IdQV8nkllxiIX-X76 zLyBnS3*8RDuA2ZleKsRX@EYE4o1U4v$&je^tDQEwpN+Uz7BMvo0%0w)i_N3qGEts$ zj%LZEn9Z9f5%psL!yWuSeKG=cII`)>-29~q$+!DhTT*+HeMMBV43nma6S)>%D%Vxv z24sxm1Lg?0@3zDjz|_5rLRM#g{oYPZFOoxS`Cb+D@X{wtTsDk;fm!nqlMtBx6gIhY z`kiGmKb6JEYFp~xoE!O7nNz`oBZ16|m$BW7HY8%NGr@XKp9)5UgrTdx8R%w@F8WCC#~_?{DBw{>v9+AJkCs?@#)bEH7MeNAE%KVnJ{rh@OA_;OrC<@A` z6EcjV3`lw&y~Bi5jjI~q{RfN=g&G`XKgPORd0brZvphyRC1I%jfAqtKY8_!hyok-YT$}+N3N@uqufw8w0qT z>)mqy{t>iupIGY80R^?wD0S0UniT7~f#NdXL>(=@VZ<(v&^Gk8G-&OGW7zD@_UnSF z9sodbI8l&#Hu-hGTmNT;6DEs0E2oW(1L|&sI%Nm~_0?+<0j*m;6oJVF+QvX3*~mu-`LM)!b#$kkQ6`iF5Fp zF`wXQxe`=VBPi_-vIh)-nuvz#Y9TnWgiGQCWmd=YkcoJi6yFF^nsyBt%S4XC)Yv8) zSNVxW%g zAq&HnIs5n(!{EYWlJ1Z*m>s7!8cf_@g12JrPv1+@eIUx13#v~p{zlVJQg!3~mhxP# zS^NMVS@WJ(`jyBr{Jx`D(bd1Y{A_DcW2kDRlauZ_g6?>C zTi*J-5wsV=kRsO%gyY9%lESypElc5b%YC$t$MKFU+*1K&2{;ifYfEG zjH$p+X4PkxxB+1KIG4Wq)yAv05poTz@Rm^DIClv4i8zk^EiHs5xh44CeeEGaW9n?53qE0b*6%|?p4!~d z=UE(01~TCZ&L8`ddwh~)xxTF&ImreA&%=J@NM>jwA0AyOO^_+ai9+2qTZ`GhkL#E# zCxsy;)c;_EKL($ihYBp2%HYH0AOUn9*DFBMC$v3C?l1+D!(hbF9+PD4$c{DEB2Ate zUdpf0FUx-@Q*jJ?$>P6Tt4;NkcNi;>y86~r*_Sk?R&;;jzcZj8M{5UJk{-L{DUl99 zyKOr}zt3{o5&LYsOLiCxKy02qeTz@G{9|v)MSxa`(1tSh_1<)rn?!&~F_S=Ah18KC zCKeviw13mPv?9>G={|}E%3##^FCCvDAiS9f&snRcdZapp;9mlQMM zr56-%m>f4q1hoOO(6b)~K38?hD6kQ*!@ODgZT!hASys#UpEHYmnPDN>tDGwCW@6~; zq8ewlS9Lu*G8Nn#@WVVp48F5O-cyC7!aDiwtbSw37c2%OJ`JL7sGEgV;?TPswcd%h0;EbXm!1(PLjawmrEB3CVnldg$7WEX*SS6WG zGQF+$mV3KhIlX!4yl3#Lv-nm~vh1Y-?+s4}pU=kC!lYn-Ny^wH^*M*-X@>3E*@j`g z-YS1UWwSi7%99WLYF~Dc5}UiG3$ewf~b3cSj`edwK2ET#Nd?(v#7=K0t^;; zo%mz@_5?9pPg5qevqgdPs#PD#r`+gFElfHESr+bU)xh4}nX|2EZyjQ{I4(nh#8;z; zWxf8~XzHKorhjHPCi?Ma+Q^=SB7daS7c$7Tn>6w0Y>jTbl4-Qlg+aGNL|uSmaTJW$ z;2Ms$63;7HhjTKpPDaSjoRgOL@a|BXdlHS-LGZ>M;m!zI@_~K=vw2}!x4tG$-TI^F z+f0)2Pyo*TpyfhzM&@DIxD0pK7s*8`$IAIq9Rf@pkfRLLG?~&fySwtq z@S!%Axw@qK5f*{LvN=0bdZ(*@cxyFR7sRzS_GgIeI;t{;Krx?<8HcLiDMd&JhPuP`YUz~be-F*~3oPU;9GMoUm-T9bXHh__etyX%pd8Vj6zqq#C z2Fa)Olgi>Ju!_O%VdbA_Ue4mh+iUl?V2Iz(k%><^8P|v3`ot}s%Ip<817St&nhvp# zI+=hh9}|IAxk@{git8z^!<4PHfaWScv8qpG?6yYL0pH*_g2?4NenZ-4`2CvY5SiAZ zGyIk(lOj`7x|7?>fw6GX*c+?nq_I%iX(eEdS;xCv#ha1D-cg{FUuk+udL`A* z+Hd}J%t5=8yg%MOgha1_T(CK2aB?JS(cHy5wpW&}7z-=5x1?%>+mO}(5b6;7g>CV=cvzOq8}>Bh?N@>7ZJUJST!LfhP#|EuB=~bFDkLyD9>i; zzPja;6&NL29N#b6vmJ;{a-krUAVu8f_fh@4I@;FYRX3vdvfeMOuR=> z(K#3e2R6xrd&DPYe$B5(El}vT89!3mZGX<@_L}{OZ`d7vL;fvQ0xA4^U+c9^`wtY) z{8F~Z&UMiPXsE7AzcLiAJAK_6EfbqbIpw}mj}K9adD>MAL{|aq)^KuWFt|YVrt%t< zY*TIwhLFkoig7xG2x%pP4@milx#lD0n4k~V+Bnk98Uaq7&d0xI zCTu8q`m52AP~;z!Ui$o*XUYc(5hpkd&M}t?F0SAa!NI<_23RG__NcWRJgfU^4-&H+ z?H@+ItST)w`km#|t8azzTgUpUr!k_Ri_Xv32lkyI`xyRAdkM!A$^^3)%KY>}j*wE$ z*Mkkc^2npr;Lm6YsM|MD`Rs3i56#B2>w0hT<0rz0Ik2m`G?%bUm0!rEc?^)8K2E=b zq*59iM1C0=ZXNM-DP(S40J8e1bt*_4wxUD@FgiiN98 z$Rq^&PV&Oh(K{Gu37sE1BjzGOZiVP7ZV{NH%(|u{IPFem9t_W>F2N_G&G~%a6W;V7M>rHqp!rTb+#L{k;XmWsjvF^LWfjU=y!Z9 zt6kBDlhfQ_!Pxs_JXb^h?%3qP#$Txje`b)mGq+U}t;1pTT>a^0_m%$)bQe%d_OO8` zU%crZAQI!e+cjB;Fuf2e52ebMSqk1*FR=AY`_6hpoaRiC>%6X5(6F|-NXt>MD1|Ip<A2l)ZM zwK$@UtRb@94+Mj7jQOE_B22V;|#+-Xa>{Bzi@gW1EXPu{tS zFOM{qTN^AzfombX^}PZdrVUFu-lf#OfU*|FeM0VRy( z_%H$;X|*%_-b6fBed$!$1u2I7=Y_MGGw0Y=PJ5pij5>befwm@Dnu=YoXF1>CgBIc0 z!8l4V+MSE~4^#R_!I6{9+#CenMC6!6z2F`$Ri*2^G9Uqq;?6eZHecd4^$g zPiEho%02o#D7?SVK>S43G)NrEDLyTYVLg=>*Ay&cz~{ook)|QD{vy)U`}-^XBh&FV zvEt0lo8j%6yX%b+l6L4rPi_9E56Bb@-;ELzO7&Hx5BcT_RvEG@VVOe*hHPzF#(AD*gNc2#ohM%ddxFJ;uaDJONrR4H0 zf^!S}cr=w3bW%;1n2EmX2%+<>6}T`}0i5pn!YxvXzrDIlpc;NHQFzWUiDld@8N^Op zSG|6Z#9>7R-zK9CNv^fG1;XM>vgpf+{Z!|20%9wIK1qiFLr&Oy2)`iY7m{q2E$dqF zC@@~S+_*SM_?TzY_^o5XKT<#I3Om087=&G{9JU=+FS!P8tq_ z3jBl7-u$7QbkQeGM9M0tAcMVq^-#tm7UXksxy2>SF^u^T=VJtAk|joPxoC&T-fVE@ zFCkVXxV1_pBFVBEZ;f`^cL^yrdR&sb61{0nb{VnssC0A#`YOJ0Pkf3kr4Qs1r}85a zx`k~3@3`Y|X6%K^z0ct_H;Pw+0oq!Y#`Vs8#+rheHzPh`bk(}S|s zm2Yj(y-v`p7q!7788i64jByVuVk~e=K5g#}pK3 zFvByxZ((|rG^*mkozY`#PVz0)mf+}2^tVs((EX>6GJD8Z-am$lJ|`NYs?eo~M^ePg z=HJESoiSr2>0Sx;+##p&Q3Gkc_?db@-yw^d39ghb_5U1fdk@1OW^XK&NsYEdt{xB` zHXW}wb{eg%W~5*Fp(3-$p7-{yCVfXM{JLP1m09tR_0RKEK?!^=s@eRgUCh@V54oTz zzg~4@FN4$4Pk(rVzNaXvD9%t+|NLd=*$Fmh3t8k6q;c@fzQ9BAyDx!SFTB@b!zq^d zz?evhV~NIBs!`MnI^z&L?o48IyW=YiQR!DiUIFlDtbj!HAB2PGs{M@ziY=}BP&Tme)At zM?g1&HHpZSC(X>2Vr*z%e( zH27g`(I;W-*hgr!3$IWxOqWy_0w|9Z$KYcp1AeW_s{o=*?8fV8xjYw(#UrQu!y=6q zb_jiXQZ7|=4=}P(X3A=GblN^cgN)rSWy9QuqDly?R+cKja|33_o8Y7CjFOvGVBT&O zg~%$b4Lf}cX>C*@@F*Yp)@4-O0HHI6No}X6Ia>aR0LS`wDMz)G?r@(NQZ#Ih3{|Wo z!DDjYUI!6pH=g#@rqygsL-7K;r|s+@r1kUWSZI;m4-#w!tTn%aqMDyFPs~pfjwhjp zhI1?|+A@e3{CTPIOt9iwvGVV)iy@JBd>&mImton&guNxXVc zRZ_!6%+G{2d!!M57mIl--EV7_+=bInm(`BN zql#C8Y*UKBlR}I>pa2Oo7Ggi$*Sh4A?Zb0(Dh~!eksr~XYuk4z&QT#`WN?+lYO6eK zU#pLf`F|FS1Q%CCjhNDip-wvPtcd%e!UTu&j6T|$KLsu4-^Rc(hjZG(N&uN7;opG1 zsPUkWP1(yL|2!O|(aXsL7EsqBYPXOrVG?i9J&i~weBH}8Gq-6*NDLcwOjbpO!RiPz zEW`xuj?5T}k`f%XREwp8E?cWR3glI*OemLuT^cA?mVH7dO>vdrusnDm-;jVxYWwhG z!Q`v)EfiBPS+OYf4g`(eNGYBLFMMmGzX2&sZoI=Uo-il1DxTm)#YVM1g9+156Rv*0 zpekwqxL7 zI0WZ!R`JBk9>prKPy5`kH#A+rTta}OY;3FAfc0~CZ2I6nev$!p-%@Q;>@F0px@49? zO?b%2W2PpDalN^qETdXm2uVoq_m3D3?J6IEL-0hc8(tGRmLUF*xR@S7t`*H8X7YZ6 z9Ib>>RGipCP4z6nMoJUitEJfIQ#pq(onLkcv%c!{Rg8{brax&NyNV(UkhE0UTI z%Jqnl6#c9dWFgAIkNZ-=sr%b!89kGmY)uS{W8++>m9Ve}GA;Y#Ce~CsF1q6x2crtd zm)ka!cJ`7!M;g8zGX$5R(%@$jjx&c`v=9rs@6cXuRejgBBL;lVg?9?hmiuKX*x#l4 zH}@;~Mszs{EtkUv>jERdE))>1XAY*l)g^&7Gi-f{cDn}t)t@_PbT_e@>WDc9mBRM? zk};7u_)mQ%+VCTTCbXwQz6aS{_bpV3DMYdFzmnxw*L{#MxxWebYff=NY9?@SJRe@5 z!*NEw4n6sRN30mbz-Y$A&!k@&+@GlcgoaT*^dg3uU#BaI_sAoXH_ z48it7y*5T!S^6uiW?IzXc1?u3WU7aZ&8Q0T>2Si9i|z{wi-u*t4;0YcHt~4O`z|uD z=a>8;nv)9gnF~2x!cpN&8_jj9VpWbJ7uC-#6U7l*y@b>$IbQLKNM^wH>3z?i8}R;X zY}{gfI5=PJf*JvcWYyDJ^2@HYf*rzUr?o3Vh8xTz_cX@x`u=ZY6uLKtJ1!^`=foBw zoTmAHDR(#aF^7zz;5wrnfCb_hNt4Afq>2;0X@i*Ek-lqmO)_&Uk`fZLrEK{vRAqjj zy=kZBr6-@{Y;a8X1wEb5g>k#E;RkVAL79Ok5SE!N0+^USPXx0Os2-wRr1gK~%cg!| z1s8}-a~tGlWPTWIDxhyz`!$%{yNnt$b_lp=Sj1~|OGCw7Nd6f2+gmACir4N3ia79o zqXuIK&z^bJk;E^u8Gc-fjd@Eg#Q&;xF`HKU?qdCqx<4k1BhpTnsOv8y6ROuaCd52G zfm90-X?y7X*z=D5PC{DhsBJq0$?e473yk|SmYe0Z1wTud#cjzpcQJiswEXb=*<*Y#BPtjBQ=>SC2yw?8gSj6`wA0RQ5cED1 z*2W>U1bMySb%b_h?VDkOCOYy_yQh%*Jqk|ig!uB~{3&Tu9$~)Avi46R`E)^+R=2}c z;Z?-c>UntWwCenm$BUd#MYB@L`&w2=tLWxA^J{B_n8Cuy;ze#Hh-;^-d-FXpppYE} zWgyZC18j>%Jw{C|9Z>TvW-JG^9>l+MU|8=T?FUvUvdo=UCq4~ewp-+@2KybG7U94i zg(t9n-4dR63h~EcDXa*UjR?XTWJSqoaKjp+B$a;MN!EmKypr?Ut6iPpL7^UKhEc0_ z72qtrl#!+P-;~Z&a!6ZsEM|}N*S&XNUemLOijXt)RbM3(^4$N?I`Y88@njYu>I8iV z1ZE>b;h__hcrl^f9p+Hj4ALY0LP&9=>}Ae-d!9X8@eYEF6V%0n%TE+}jF>Ms`RJEyNo!OyI-Zc_AzE&t?AXA{s&8LM> z@|b4A7~zG{n0uN`A0e|SlF$3+?5_XCrDN;r_^CXSdI#wbU(q+pdqxpU9<=4>Vl{t! zT&QmDJvG@M_MTY6-8-49{$E`Ozwn*~eQiGXIIBOXXK5~jQ&A zw6j`F4{FmH=i)yQ+Y1)tw7Y&rPC*#m^<_MZqs_laJDAj~8MLkAP`uZ}We8!-kM;gd zbMlFQy6sXBRxV7t}=BPo#DxR=Ps5NE@P=-5p#~_>Pnx zi-Y%P888Fx`1KyR0N)ZQ@RP#6@90;^YkNK+zK5=)Kv&&l1O%egXIj|p`H4M*U|0+Z zfM?E(?CH;Bf)T?jj{EtMdY#oWq4-ud?#qw%f{RXx&$khImONpQQxdnJZX#mC}d%2oXmh+)i+18AeF)$bES3_VjZPWQ_be1GFY`a z%kWu(o1!iX(?ScUn^7xIY_^N4&*zA#g4;;SLAs><9xbqWs%{E1P$r2r!OM8wKo3pW ztRPaMS-686Py)}xynz99EUfs+nVfpt!g<2FM!(axdqv=J-F6>)INs~4$d%Ao1DFY=J#BuT1sidVTxB>4iTT(gChv;gCTb4*Y(b z$3uZAG}k1=A4M~3iAj*!N%bD2EIMDDJKfAbh*x?chw`tIa>lqV%SD@Z+CelJjc{iH zxFUjLHKY#BI$zwY>LjbE=tw2q1UGmzXg;>3Jkzo}(Fm&npX@b|5oRVb5iMvUVX}!Y zCjlu;>2B%QN&ZL@#!|6h-Sa#09cZrUsQAM{4D7oP=%rEH4O98(G=;trJ`xbS6a8rQ z8eeq{oo1i@+(=vy@u00I5VZx|CgwUOsK(u~=^#{04s=%g65*v_?cqqqI@tP5<+~0! z+8a$Hr;d598+>+@UC+EEZ6t1^Lj7?2l{;-~q`MJD;^7nfkjG5vL9Ns+I`mb@%C@Yz>j9i?ni_VmHJppY3MD<#&mDgYZUqf z&BxC@ql-)$Bs}EFx2`p&Q;xxTiu;BH3D!_nER+GhNhxm^o%d9I2JGH{m{$r2Ho+27 zCH(^vfif)DE(HTS_@udWSBi*Do41G)O9F1W8U{o+ZHKpdqH};xo^J8r#1{B^B3wEj z8YFpOn_L&4_!iJNh9*)C1f8H`-e_<2+q<>u%E^#CpmT*0~3g@P9XLZdmeGt-nv*o-*;xXbbPAm z_Ch15gW1N-iD0#E5W8`%BZN@5dHihuWzUMl;HKt;<4;R6tLT`}pxkWoY9>2RTR%ov zN(Vf-ExMZ)wZC-=HkBS-6^{;4U2kKy_dSe)Ay0-{yrfFIke`OfQ%gAKc|-SU|mRu#l(hIo92F;gBrd}UY(gTC{=qG9QVM_3UK`)~IQ z%C0V4UTkFu`QLV@%F;ql$UldK{1Qt0-gpvi-Dpi*h049*-t?Nmy0tb0U(#h87j)Gg z%62Qja_wHO6E{kEm9UdLYv+=6f}6b!(MPkF2(Xt!^!)LH>W+ z3N^H|4*NmQbGVS$X9K60(*Z5Eo-4>EkMU=EfmR)~=3sDs0tYX~{!~{2aHmEkIB60Pq#Zy|j{ziQI z2KqEOft{gN1HGQ!Ys)@%JwzcYk-G)IeHpMrbEZQDv{p$;5~=b|nyF6NmhJdg(*iH? z^}(lh6yP8*GfWC8-Q!Gu9Ofzor3tE+d?!X0wL}!VO*p)Wb#8cB^iI+iRiX74@$AHIi zdS3mB9rF>&rRe zwOQM6SNyfeHd{Qsmv<0uu6~9`QlwkZ^Jc@>K^s9c!Cz0xXGCj1oyFH__y~O}vwYSG zfUwumdV_`(Jo0_oN;r3Omqg^AT`ZIQb(@h!Km=r==$$K#=;js`6VWF8fEXGm7GJ&J&al3I4@>k0NCirXZs# zZMcSTWESc($@q2u_wk`GC89E2g+}>ZH7IHxbM-FtJda7e7`HCuO~od4xvh zS$@moRnqSxYg@nNTXm*;L-3z=bQ@{Qcv~CN8ys?BD(1=5P|pk<^Pv*Xf%Zzuk;r3U z%AfCIE55vOPm%)5^V7}Tu%06Bhx@>Fgx`*Irp{I!^LH}_q~A-tcW~)TESbYr2&gGF zCirY*FT;lFtzPK5vZ3eviJNS7JPY9EWkSUxN@z;@j7M|L^Y1-JBM>s<>o`V}|CY)Tw!G^!M-Iv6#DLzKcg5vjQ)P|vs% zPmjb3pXm-WAL%m&jexUI=1~Ru6I2yBoNF@$e?vVz_=Xhvq!q)S9vKy6hro@q`IF{g zU^7?vZSoWH)1o%H%UCo=o*eVJ`cR$66dM(A|0!gbInv+HhS%Kg3)y^AuB5H$am=gd zOp}g)0GtK-N1HmukNH2HogxcSHTlRD4`OjXw+SrSXn94a=%vOK;IqEswu(Um5Mv^I zznIx0el^YB!ZACx>wdad93{xHBd2oCV|ina*Y{H^{j&M+gp&Nr0h*F@F$}7thp+gX zNemO4>6Iu<*IXI`B#n%&R=nE;QRz=9*sZTPrUQ3>Dn}^y*&02=W z{3adL)GJ9Je6}Mg12 z0*1PyXBm8e*xc4Dqs6MJn#ksU{@g}C`5K|4AcVJ>b=}I-AG^Bs4Pj%tO@6LD;d7@S zG1J12tiFerv)yu?UPTGAJ3 z?_^Wc=?dPCcx8#Bz6zpTAhNQj9(?j`g)D;ln4r z4-nON$ef9>k+g+&P$^f+c~rHsxa2nhvblYqmavG9$z7g3Xbai&7h%xYYooTE05-fb zJoqil_qz@9K+7i{li>rM7}*`zxmr)$EFLd~rgb;HYXUDRUAob%vE+z~+G5}Dn3;ud zgPGuV9SWKI#7U4a8Q(jPf9;GIZ(n4Vyc*!mSsZ5u8_i_49n8FFbi_=(xU{OOkeQd) z;rx;|6AM31)&p!XH=BD?Rt(vhOXh zX3K7S%b$CDf^VPvUJqWFw+nyr9Ag|pp{n2uYavQfu3*uSpa+=v(HUJF1Zt2qgtL8? zXb@7D<~0|acxoZ-_+`i%Il#cP@8EdmuA11c@5{z466K<>|9O(X^C5l>_16%7!}N$i z&%pS7oC9OprANw(g=H8nU22kK;ZP#R$`*rAC$S}*o2mi8Qz1j4N;B1i>DuFU6KB`= z?<;*UX?&&1`c9}!(Xe`%;h}hfNJlBhd)e4=qB&<4g$e^9_)DPwCrl|rO0bm|Wxf+7 zZG1AQk86#jvN4Y(DnOm)#g7Nl}_nS&vZKT zZw}V}F2nqRnrpw`6Za!VGF3pAHF2ONqJmw1@6;BNunYto{G-4iw6Qq2g5PN>(q2bb zcnqUm6Zwloef+Abpc8%fEPAc=UFSE^*9-`gxOlPcH~H$=DeJ-CY_q;3rj@%@QnN2I z>=)Z-5+`FwpJt|#90!PVa1GJ7l~P zU0@{oW_e&fL#z4;39^U~pN8DfbdDVubP{nRk)ULh5To1zp~7N4n6OAnkI<(6{{cNf z!oL88oDgV=ceHYRcb7tou$5?<7VT%$X7f}|c#uj+bXXtpy9x(i0MyZmb1u;0L;-&8 z=`DnzT8FsVH88XuP7N$PrB^N1OkKiu8r9Q3IVWdy1^1sDM* zKQD1+8mxJ|V8cWCkiMwhb{4Yvwg3M6>C{({RFhzeoKe^0=cI}wcI=uX0MLtJ9NHm~j* z(^N$@ZtQ;lmSk8=3%@@cShAX%>lqnLVp}jkCS#F}18Fx`IEh=!F$Eq?2U#pwIonmd4I8Cbu0%>H%l)JgU^hS*|SzaUhGmXPiYn6jZ`H(PlglINBR1Q5}*?HIWjxM%E#kK_6&$O;hSxPY?RrfL_x0crIE z84F4Sir#5T)63G7MTx?pVDVdvxBhj6Tpn6{_iocvnTgGUI=sZKao}{klfFOq@xvEc z-?hy&6gK_Dr5qd?c#`w9EUM-S>ePqM#(Ex=l2uI4x2|`!f)a;uA$$Y<#qfg^r3bi* zJx%IyJ4@BQ;}d|ltN3L7wB~4EPLD2dNvwGL=#g%m12r|Yx)q)mbN7MlUlFyms%VV$ z?qZZ{9x(P#fZSPZcU1Bfgx&TXW`e1aKddwK7 ze+Q#ky`3rU#V>9Xg_ang1`P>Em8x4*2GHA7tg$8drROfd8s-JC+P^~+AnTsz=3|c&8zekQPLFZ2t%Q*3g(@;;{9n6s)RsYHP)U5TH5}v+JP2(4IYNDf=U|hr?ka19|Ila40GiuES74d zU74}bLLb&S%c^q16f`;bQY{4#5WSPwmV0Vz1PV7HV$~OCwO!_j!>0$bUFy3fH2Z2) z)Fv2aSnofU05nZK5y4~yfHX`%M!_N!g~a!XSW!C2*!y|hL9R8rQ^fP>1>Sh)sUNhV z;x3=TUX`X`@4;h87LxbxDZ;|JVamw~{Hfm|ECFhD+P_^<5p3-XvhNW$(0EVu(n7DV zo_6oSX7lQJ$KvGD!zm=FG35=I(;$mbH{y{uUl6_aRWMGQpkWyz(2iXl+3WM(p7tHY&!A{BWrRvCwf}6>4}W#7V25IUVFq67=Mlc zquS2}D}5EgMYf)~x14^Lx5VpaWkv`4&FfRGAwEXGYzJ0keTr|Hc#~1xM=W{7g5o8x zpk~K$n4s*ZprqoMRfVG2xilja?q{{k;4ZX{+v&{gryY~z=_VOSF-ABRc|V?RbYiLH zCNG#P!;If{U6ukI_2a+-etBwi3ueEHg5)C%-q47D)3D#+=gI7q7T2CpEh4dXu2cJ1 z9xb_$Jq%XcJ*t+U&UDMi6FjYu~O@N9=hAR3X@T9!}*^7<HVbgO?;-UbK5#!oNPqxfDtNb{t3q&DvA!H2dzi|#8o#9D-o((nko0_7oUGnH`zB6 z*Nf8LpczEY@9ZL1u1DQ(e?$S4{9EbU(eY(a4d)pl1FhkBl}}l)J9Gy(%}7w;hIyx0 zbsnIf$_z}Ksrq-cY(8L?i{>xJUKA(-h~S04unV(;X54S;$j4i-SVq-F$y!JCmgs~J zCFS^{{*U8f=!XVGu!WxrHa>LXs8Ts2?R2=Y4vt6EyMGK-PRHUjI83qKU7)&lVu}l)M{ZrALK^>J zM-yegl*I2|tuo1V9D*A58LE2+ zd^nLLVIECZBV!j>o-fs=A_ZSKf@^ZQ4-|jRQVD#7HRommmj6P6RW3E-*MIF)4mQLm z!dK1^2pj}#@PziH`wUO@wg(3zKamK~v>7_V1tUPUVMh~1X!@NWCg2>uI7KyAY5~(H zeJ)1xu3Lco~s4=)*JX&^nOmyFsDT9r$`*U$DePa==RgVB^>VJ$C)!pt|jyW z*)AJ#TB%H+fuknETGEuhmRH;tc~6=G(4;tmHv)Rmah_II`l$F)d06D*d;jIoJm6%G zObs`ZaXZG47~yqmZ20{$$bfTngad)}9!sob$D&n7&hc%<;z>F_>y4L)pg zMo~bv{3J*I+n-p&{ZbDtG?xtu$&>X&A#DIZut?maZ|GX93F!vQNEj<-bE3b1*wJpx zw@)|ZwX~40{y=krc}RQl0l^G(!&-a4S>1 zboRqP#j^92MD*!cb3fqB33KZ!X_;M&UY{jKjN4Jw8eU-MHmv~sr=Pt*d?Za-DJ z>&%S(^Ot+51lUp{8XfoRb-yua;xnA2vZH-Y?)YN-!#2G#H7vM4Zm~=^1PAXx26z_b{d~(cC3}o6N zfur)i12tYdz>y(6xV+U^Z`rsY7BvqRPqzzN6duf??<90P%L@ExQ5?PDvk5`{K`$8Q zAv{FNkC*q)T?!ST`2z})Hi|y7ie_bv9Qo3Bk6PF{a3e?Qe^ZO>JPN*^73nRoe_P>w z1jG;&dY6c9=)tV}qP;}7N;h&7PUpPx=^v~EGuu!904aBJ;@9;ly7y~HPN$t+pw&PdHpV=|m3Xn#raReftR zA|Eqe)Hz$cvG=cJ73BL&UOl{<1?3rnioG*0fpEf=F;l_Q(`(gx#2mpfeT#`xABcZ` zX$jc<)Qw7}?n+GOHkJGQjkN#FcnE=X#NM2KzV^vf%?9OICrNG0K>KeoA_M)zfozcR ziXGW0#YInw;t!x*{llkRj+5Y_I1qhH6J`IdY;j{ly^hnneP!H@Vr zxvc$-o2ByFRYLu7DGMB@I4v37FZ#luk?(;oGC9blPIPyFYJcJO5)UwfjMZ8)%QU-N zHB5S?rJB92#(ii0`ktI7zd@%{V|2;Kja;-4O;KOnV0JDv#5)H!p)S>F=qO90;cnrN6LcWS{h~hmap7=^`?Gv$ zDQvn5=iys)md>4k5Xaa;KS?(OZH8bXY1wcajFr*&aX^gNQqW?j)PH-r(lF@+ZnNyL zKyGd(&l0(l3N^%5>g8l{tNVpYp}JWi5c7^E*{Q>IEN+>EZLUhIO~Lj1rS`jg$}y zuYzVKy}w-mH7IS3GD4rBz!rg~Ab|P=9j1-^AO^E^cMnXIk<9!!u!V9BFf=rq%~X{J z;IFTWf}`1E^hu1BEkp$4o%_4f;hG!(hHBlXi`lN2skaVpi$9B;z9x|XGA_6cQ4IWP zm6r7!fWWh}>f(Qel3*5RqQm)k2pVMx#ml~L^j!rrI8WZkg~LE+B!M{l!_cjk285BauArS~2T^x`fzThI{bd>}f*6qN zu)*K9_ogUDL%pT8Vm&xQ$!qA$1{2B)We9WNbtc|t6;7`uf%N}Dpfd%Kj(v+{fkFk1KvWt51W~ z^-Z@1`CWmi4~wrf#xL^GG-rwwg-E$ulX@b4{i;p-wQ6-SC_$7OI<;-!w>H7BLU^|GcW)h8$( zg%_lX2ZK$Y{dAA3;WJEc-@QVxIg>hAkLLhS+s%Y^Y>ykgXa8_+aX9ZJZ)!i%j+U)SAd6igm}dWsPH{P+Z&OKaUgF+dJs6WSk%lo+@Fa|Xu9U%sCD`%tC!HZA z^i~?g@rwi}elZ{-+d?_vxPjrLRWVwr(4J(&Ir5Bj(j)w_fr3w|P~cs%sSYvlTkc3J6rQ7T3(fleWO^NVT9!I>r2kteAY_$aQcr8G2lx}Patyo#lU%O zoeu3(^mPuh;H89%xxxq=@9VlV54%NeyI&!3*$9i>xI5ovX0&t{r ziv-zPKJ92@X=TB#S*?lkIQWO^nMi-M-{vGDe}D4#c$zras{&YQvFaqYFGHPm9uBv;qEM}Cy-Y4mWcpHplD0Fse&nE&YeLVBgnb9GEJy0$Q|7CR( z=YzZM^upE8<+m6HLJ9jY>6+l@k(f=_ymq`^l4sEmPAK=g)Gpu(G8=_zDr7t|!LBY>V%odJ zf6I*Du$+@PtLdGm84WI@t^6u7#0B9uxr{5Y`ERgA}}<|;k|04 zS@g~i-dz^Z5AWbHi5=E}dJZheWP`sMJhE>TRO5!rU4k;anRegnLsQ|1t5WN6QK|CH zTNs~>tgrmeD}N~n@DZvGZj1Fz4Xfmd>+io6dXv1+VkB<~YVHHYQYM(FJ&-FStxnNK zfL+!EooJGO@b$+m9sFMgFggDe#n9wL)|RVQcwoJ+tlQ!xx~>#LEdf4bvIfifOQSsy zH+<(3OG(gpmJ{;zHL)=aIpz6&FLKXzirq^Hw*h6UdFGZh|jmdvFh01i)rzT_JanoRp@MY<5qYb-*OrS`>hv=lKZk~J?XFw6 z0)mk*2XneW-SdJFWGC*{JBioEy(zhz{fGK09Q489p->B<t-PwG0L)7X{n(gQ z5+o38`B&?)R%Z!npw|p5F`92_j08ncL<2maWUf|Y?JQ=vTXn-!=tWJIxBZ)n%La{GOc}3#%JWrKz$M{*tvW*|HG9Iu^*g z#Wtyg(9tf?@NBQ)fIMa#L3V^$iCP}Eu5q`(+HU;M49o9m{H*yt2@w#0K)CwG$jp2m z?36htSUaR3z%T66g_>x6ByQq78NwJ_$w*CarquN_Hw-hTqG11g^e=VF36BtpiMDXP zXdWMRrsxO^I&#c(y|Z75UOdOWle<(tG1nuVGO=2W4)OIHIY8bkWCG)B0$SJ`Puh zqC`n6a?)&5B_k11<&*>ND$0^${B)!Z73G#Oo}wex`DWuD(2tzW>Y5>YAU9Zm_Dx=( z#`HeF91lE%D*>?pO`+OI11dXWZ}oeX+dvXTbT(|Jc#ucLj*FCqayaN5NPCC@d;UXL z>Nwa_OwglBpnCc_1WuYp5$ka<_m3R2ezIZvt=+Jd3>cSm6AE~Ok(oWvov|JCU(I!b zL|A_7lQ)r=&Ktwybx3MzCA{nx%Ke5#nNUc!10;v!Rq8^yH-QA#ahyggA}@d^P{z9gZ&m|ZSv0Q#la|CE`}ks#pvM1CAYlN(ptT10C!(bM)Ah9h#7i$Ql;7?hZbU(&YG z$xhS`M1Lh_a_tdWD!;r;J`TPYD}6g9afOazCm}0o5EA2?i^8`Nfxuo}xyi<8#7;{5 zy-EPJZJP*u3oh)IJS7$b!BKyIBgLOe^sqZQ`yZr+sSpSzTKP=p!8{K4Ga*djKQ3PX zzRfMZM-~ffv)Sp%fEAyRuH-{tUpovaG%aGMmbYm-c+Hc;N3Jq0=Sc#5!IuS9BJ#EX zW3vT80}2z3VSqsH8B2TWJulLjK^_kQ1`Sq5Lb4Sh=Yj!DKF+?gL;@4FJ{0i z)dWPJ%=~MU)8dWCyQ{L8a2ImO6*U zq_y#8b^{U~*J5p`LF0^4g=!Y5zqfe~4GIz$QSm?mZ&FkE{mI@aX!%+!kDCY~0?!4o zouUZMOW@S)iTfeh9~{y@vh}&0-ELhR0qY&5Az2T70IxycFqwKsH{~-ExU`ZY;L%bV zi$}_JA}Yz!n*s#&!ugTdNsQZcPZ1L})1r;39X3$Hlvnm1K?=^DU}F#?yiZ%cNnO$E z5%3rqa1LtN>qw_RsO9!|e&NerGL_)+I2fMPcj*7=|4!UR=#QWi-9Pm@znTquvLW$n zacAs=CJGu(;Mc(R3PzsB+I;S2NUyjgU-)NaYqsDIfd;K8+*Dl4z7_k44j&jod(7m*IM%YeeJS--1kYJ7v{qYUE>MY85^{LnY5rG}f3f6B^pf(L(RlEJkbl-uxvC=(* z_cDIqAgXXrzm`VKVACX?o*B@x@x(0|X2dU+$)UaI%L5N=I1sTS#!{$r=5mqAA+;S- z&J-m7K0L@&LOOC>wR;PRv=mW-O|$n_wv8Ghbk$B-lL?v9=J1Gg{r=tp>o37U>>Wz< zlvL+3it!s1^O$6iXXyE}v3`8A6YiG+OKxOiLR3*FaXxN3scRD6FigxT1J(aSWe_97^-DO^ZMa7g#n6lgKfIO15v@U%PoBBo9RUeES4dDXMjQBmIA^ z)IEtdO6>oFfF^;)82r7@PW0sO+uR-7FEA<`A33y|F^r=~rN#t>mSAk(1j^{vV)}C& z&RQKT1k2eW`W^reQbFA)7&G!1L){KL4@jvE#4x&}4HJM3-&-IRH0;F=$LIM5v61A~ zeFyM63cCO(K0~m!bW8ORAtY8|(Z#`euUE^JT;n>N_Bt7K)D%}1`*`#$s(tBvgiVhh zO{;}f73ND*R%VQY2r3Ph6q0yP;~GW{AxNUbZ4g)B=KRyZ6v9vzr4nQ|K-|qlq=bo5 zQ+g#p2JamIJVyhADe=p^TUS33g*MuPg5E;c^O&PzC!X2SP>$J_!BZ)XdlO?5=NSC= zdw-q1;q5$edECKy$5_jYl5PqNkA8I}aG;TPy84fzpm}kFLM#iFfVH8=1^^YTv#5Z7 z;0(bLZu}Mn79Vw1(MMnHxmfNgBF#z7UUL+fm!y={p7K_Flw5VvKH;C%S*_stgq0tseYL+p zV$vpCN20LSp#~=~;bG$7Zz(otNEwU%W>{#f9aQyPvHs1Lr7jg^7<_+YqjV5PgYpy~ zZpPp`Oi|JNuOdb$KAay)w*moiw9rVAJ_Z5y-hfR({5vQHIMw3eo#;-?P;+tza&NI7)Cpyr9R;H%2dMS-x2Qde^i>>fsi# zt97OPc>T3biO+*MavM;vQ=6}cSvqIhUnIqy>rkgfN392hmpnt+V4&{=@|86!>a^>$F-rV$0z3=2>V&8ON5X z;(eZz%`iOf(Q^)SrcGTmnIG!WXYs;=SjV1sqqGFXPiMdngf*iZj z^225S1hAs38O+ZV`KN7;2QhxA=l&r>WpW)KC__35HqskUEn&;r_K9r5EqR0+dNN~X z@GFp=vI*Wem~ir;c347J=39 z18%_|sYURkp3gMnm?<=bUOQN@aWCi|KLOR5kdZ-9pUd6_d8i&K1h;od&LcBI6G9v_ zs2#<=bf}KsBv3(kGHJr0rCXQ>F?AR0lj|zhOVCF|x)6MWH`cAYhq|IvaIloOl-e`V zXen!S2-u|zoF^a#taaREzHgqtM&@|E{U4uRgZ|o09=X)P^%%Mx88=}>fC#SwJGE6# z(wyC_!IV}%nMbj?q5f?Ee>GAN|eSbd8|Nc}Icls9{KAwvYUV2XZX*N`Zy!g9d-1{v^lyI-kixa@j-^U+OiNKWlLH?)EyO{rICLUhyVk-) zGE%&paLybrIT0(85r#!@#f$rBSTAfNeq7+0*{xdT)rBAYRP=U@ToVaEgeaNrXrdD! zxWwe#?%y*VuUIJd#iP7aS8rJRVCZ%O0ot=0){(Q9r_!NqgaWzwurLLqb_WZ>3-9s*`>vZ`uZRz5C~v;z zNK?|(dQ^hD)Gh2;CJ)o+V&}%>Z)Oi6FPu7@n6P5!kbI0%4)P{_=MF=Dns>?J3pF0> zFhi-Tjy=eTtp#Ybieg&rT<&DKUV~ajF4#BGrKH9i3Vb+nDM8W9fA%MRuqDy!_kjas z-}*k&*~`Ba$+WGP2mL2 zsLsD3C8^t>Hvmxu9abXkdC+#{oSDa-s;HlB`M_h_8q+(4?=>x++oUxHj|ER631?8N zS}r*bvpF=FSrl0cCl142_lFPJV?QMVWNhS25qJrrJ;kM$v7snJP?|Tuj@rd>=iVfp zE7JcB0-m+xhN`#0j_R|@J~n#V=o#Jik|ZhcY|{hFWQe8emfqf-G}|6wLDA8>y;Apk zka8@0dNVOpKNaq#su&6TyswE$OllqiopS`5LORg2_OEt$HZ~1j%-+xPV1>-*v5)VIoVg$VnSjhB%9XCSwpif0K5e+C@*`g0wTG-mfyD-y z-gruO8rGp*uv&#-i$ylF{gGf-1g*ZoVp~q(?8z~ldZ(fPDjz3!${#w2&D+b_Rxwba zh?tBkVqV&L4Ufr($z48-w-95AUn^S)76eJ6xLJ$2?Ltr1D=@yW|Fo=@QTI)av%)C} zo}Bf*4|gXsj(WHO(_xpcdbahRSS^lV61g2LiU$z}f!0S|-Xog{+jeWs%! zF~1~t`u$zA67j3qY-&viB{S{yy#@!*Dh#97j-PB9p;)(}9QR|JR?#l&AtqVh=o^}* ztU-#Am~(Q1MNU0Yw52o3;^~Xa_eZtA+zu6qUb^Z9Gf)Tx5kEIe{x0E*zV`h_HbW`H zA3kGb1^46Ubrlg#kP8rCuXWL;&*pRaQ~z_$rJVZUjmK8b^&LV-0k@`kt+0?wZD~r; ze~{ThH+I{QMm{Tf%L|a(a&t~1rX~75UX9il)QOUc)@PlCWNgcj5T3s8fTH_|om^g6AtlDTNKBmKO;*=}+h zh+eIb9oKyJp*)o!@0+KX;{p!ugJ4jahWv1~^sRJXwc`-}J=$iT9eAB1;-GUd6=}FL zm?oE(ka%(#!)gV}*1Vv;H6Zq*4X!;+Qejr$zzH-%dT@ZO3REH1+8~Uxm#!O~qT0M0 z&iTap0=z8wDJsAIiT2s4d@?yizT?*_K}<$g%KK`J5+NDVzy4qd7h&r)wI1_s@}J6p z#DrOjKGp0B5cGn(qizs<{;&v?d+;z$q~0rmtQM`PynlS*hQ2Z70D zD6@Iw!J?>8Y%YCRixOyQ{hGbFV#Q_0F@%)dxtG$>V3E$vPJq_%zqsQ({YeMbVvj{- zP`D?)KLS4ZwOlh>y3jv^p2KpCQwUGn`et~fsTe22Ug{R{fN16Uyh5b;w}9a%nZn%? zcYA$P?!lyTnIWL{sS(a0==g%|Wu(&HgeU_Fm{dAYnoRVD{S-Q^1k8>75*VUPoJnKA z4NDnLg$^jSxMsLZ7+VV;eoR@4Aybum5GqskSzaSGMi`cMEDTaqVHuJf&C}*l*YDKG zYhuDma%k8}7!vj^DXi9p+Z#%kj0b@)OUO^vBykBl@!3xcCScYIiu+Y^o5v*~(yX6# zIbTYLA;-`MKPA^vq$vD+ackQceFLR{Ndb~Cu=^jDwmsrPhj}#VQ)y5e&r37u576+I zI~dr}@3$aYel094CZ4ggpR~l;65$>-%z0_@a{Jz)z35Wt3O>zR!@+b^*p4}Wo4$-U zIxs8y1}q2pNW>Ji+fAlYYw$kR99Vc<%sqz8>tap~4ravi>@|H%oI z@jHL|x)<%O;(mP)F*le}^gydl2aeQzl(O~~2EV+lD z;tKaT2BXZI0M6JB<1HWXUxW-*s>EE;5A|N{EjpatOuWkc)VMCU+lejE&Uj*%&*mUx zlmgVp#OUlDn;w`>}s*37AXX{$2$z&s=4MKFeu2;M~bE3}>g) zht^uB0iAW(U{^w>Y)vvO_POGti^ncCO@M*&Nx6QdIS8gy%-1KuFt{e?Q(U;EN2Ye0 z4k47Xvn%=4CNYn0k~EEUn~1NhEx{Xk=|7XUVK6utE1JMpe&J%>mtLTO%EIvm1OY8f z&4p6FplP=}rd~6L9yyijz55F$F!$La9yWXt?I23Q5z2Vdw7hY#);`yGynh&g%ELRG zns>bbxC@-@HCman#A4x`{mJ$Mk*h zMq49lJ1}(TW|GzETUs?b8%AaZ(5%E~M{Eqy$?{oli4XYqpvz@*G#m?&8+Hi-m`Eny z8X*!}7pPArIHS(PhvEN^yu9lnF5ZP3K?1*nj4ZdI_+MF>ci7_bd9y{I1k7~S%J(p` z4%uc*-h~;VQCqO3WVP#>jqjhVn=|k(P=w``E2L_C`!6h(I{q-@Es;9(Nai;yW*GNU zYBL}Z6J+pjW&{ly_!%Mzil@Rua|{8BaffSvqS1wCN%qXJPC0~?VVDwRR7g<%iV;-| zF{1edvQbsE`x1r(ONeYy1LZ!jzR#f-uWJLlZez|MPv+rslKQagkgV*0#^?` zjLrQG8~3$u*ERsbVL$yAHP4l)`g_`x{;I~Mq0@K+y~@ju|QoX)%y%oav7J-}CV%e08-sphWT|AqP((8B-zcYDTVxHC)cZ|vv8&RZ@gs^sm) z=Ap(<^N982tG=W_=y!JWH@C z361s{)UUwia@+R=6b<7jaakxFU)rs`EG$d%zVC!AS`DOZ~NaJ@42d{){(_k zXsn=(YM{x<+BW4)S()UqcRtN54QYh8$I*;X9x?kDtJC2>K~Yv4x!tYKBH)ZV(I*C4 zgZ&=_aL0{XbIC>fK_-b&>t?tIWacmG?_b)KhB{JQpCon^p0Y>(%xSTCzcRFzH6B4o z{22mZjdiGNagaT^orW}me7!Z$tXl;xWb zfp!JQ8pG4~Z9mS@1$Wc1&sKmqZRx*Yxt*D;bt#Wwbao!k$;uWJ9x=Uo6SC0`CVeU1 zesDLCZ$7%tVLZjyv`@=5OmkjIj%SuT;N_tg(o+k}C@_$- zRXnv9sVVmxXzOLi%6ypfCGS84jOuopY0fYEvDJy`0jJ+*T?=Snp*UJ~Bv6xMM7BE5 zPB^3H7@pS&C$zMzj}A~k`N>c`zr|XgUC=cz`Eu>|j>@+%9i!m8y2sx~G?;jlEq{uO z>S3X=s5)vq_B8=Oo&ZM(N+sr#AWhr#)dvXOWDz`64wbI2L z4f%yaAOldpX85kmEKFhZYV?9+{6u zo$hl4$B`eFAXN_sntpW4P5x2HCA}p3y%`Fv;JV;P@Wa{l}Naf4Xd8Fkf z63Cw&0l#c-W<-C`DKSKU?>|&@OwCk2wS@JYPl?&@27K`@Pv(O+e*5vzK2T^@k=!%^ zN*7Q#@Qq5HU?0tf4>p>ogH2OXeABvnJM*Oan9s@i&cw$_EH1Ku#0_OeE_Kmnx0KUa z4`>|IC4od%z3OS1E8JoELiyoHKYLeLhpKS*vibZ}F_-ev=SuIP|7ZbLxsVLjJB%@P zynuaAO0wTvrStkLv1BsaI2-p(M}FLK1;{5kLv+(yHfy%~1jw=J{z<{`83JmV(Y-V6 z08L1Ak)r#@fVX{j&8D0K^u*U6n}`R01t_;lF=Sk|D4;DJpP#y47LH})t%@#nYo@QS zBV2pEuOq!=6ugn#>-<6dOwEw1s#V^S3TzLnae@H--?S^JPpTg-O6V<4hzC@tn9Q{~ zs{RAeixlNg=5pX9PBQ~OjTsfK0m`&d^#=>k+B&SaZh4@T>psK=3O0T8fs2a7_d=Q; zdB2PqLS%^lb1iv6f;;!ww3uGxmY8j*S;VOf_mEiyzX0K#EO0`6N~J3M^guu9?j75{APyI8q%e%$C|!vo9-}j;&wNI-=Ovz<1b!{$IyK~ zP`Bw|cCExIMx`s(n1tU0J8`6#;Sl-b?wOelxog}&$dWrOO|{TDESDO$@1J(IQS@xu9bv_%1@|*Se}>Y-Bt2GSeY)9YyGVb7{Id2Sr;8lv$}@;d2lP+%$u-w|u2!489gE%3H;xDMqy!5Q6x1Rz`YI)^`73c9mjiL{qT9`X751*Yc;%*{;(;qmY!{*o-Y zvqb64$k~F3R!?SSQ4n?AT~9z-mP>tXUv$bWLo`_-#FOzqzY`Dyu%oLHV1vKy|JQ>ttVI?Ru|@@D=k+13)MYlPkX*y7C8ss zp|KpPtdtxRMQ9%1l*>~o>v#3XN;Q{yDtui$939X(-%g}l#;rj(p4LVvF5|!8aW@&; z6gbjg;Byl^Q&+qUD1;c!=-ao?g84`OlO59{^LY$L#mDrriW?N7ZB8UN4ToVA|JT!6 z@9CK`H%OZkaanjy#y|U-58onp%BLFtR&jd9_F}S2IsGtOAmz~VI{AI11t#a>15cxx zkGb1vz06;Pp*g6s3&Ghb0pf+1{Jd?~J<#lf&2)dfKQL3_53^}FVLS$q8CR?06#$%d z=u(4`Rqkd4nXX&Wh6r&z04XB9C-cair+xWj#@kZVos$L;?w3}%uc-6u)5Sc%jYw04}^Mkx*Wk))=+FKey|J@ zHkt5qX#4w85?>PPLCk1zM!x)r$$ynRB3@PHZMGg^kvX>aSSceM&Y}>!#rvPUCrUAM z_O$8ReEyUZ0TtEVelQzDkbj6=^pfX2PCS8fC;ioUN+I@}IHJ-c?l<%#pQimW4^XyQ zJFdBGcmTr($1)xG^Ew!UW&ql&PKBxb+t5)O=e8lr%2e=6%?A5I@ya-8qyCdWP*gEA zam7^BS8le)a6Dt%q#)l7N8;ZQ&HRPl+vL7TSv@Cz&X=dnE*}uUM!SM5(X>rGU@F%G z7nw_zr`wzfxTBdNyFn3k&Sga?uazPdi$O;2Wl{nirfTIVOt+Oi5kf!j1rbP$NL-T_ zPLpcFOMh2S4v7aOIXf75u`wBH_t_VaYt~<8f~*ROLz_2&ux#LIGVB5q zYol0&$XiXs6ucDLIwURl{jjyxfPbDg-+`!WcY_~7Jaj{5uKF!?pS`S!&}H;JvMk{B z4E5_j26W$DA(vT8LTm}u3WU3J_BkvMTUod*JIH+9ZO&R(P`C2ZvyODc>kTNR*I5Im z0!NfiC4BMWu8pGdJ_VA?55Aa<;Y!2tCNUCb1t~SzeRuFK&T`r=`l*6FBWrub#p5=* z&puo22Fgn(&2>s<{OkGKGg7^2Fp;%3mdGPozx{4AskY7q01AENvy$YJ0*tdiVpZGbgU60JSxt#%F%W|h;0WGrTE zm1f-LWLrXTKtXb4al)XPpwQ5e2_|Tr=d<&c0k>=m;~kL?6A`gsSJl`M@PJq@sOQu| zzi`#Ro(8=fLI^`zkumqJsWz@YQ;H+WbYV?Fhu4Yb_vjkP+1#X?fGDFH4miflxejBK zLIYwDMZuO2wQ)MgVSiU6W$E~?nkg8xyR@O-fs4A$vml`lKnVELMJoNl)uiX94@N{+OP0X@$a(!li*O5q2>C3Eu^rf@LP zyLz)pb!lUC_fax8O&EE=1}T`#Hge&)&#C?>6uN|jc!-u(FRQ=yIu(w>48geo7ggJ8 zP9CzKTi`v6x2sZvWP|O58K|2M>W|WFVkbNmgm09L;!Oaa`EL)Vi5q}ZSDR0_XIbE22ddkX1&Z%!ED_cJAcW_m3vgn<*@Qcrpr+R%TP+OX|b{o#~M;NtLYZ#^joj7?B z-mKk*RdlN4Ea#19GYLZ?r%!QJuPC6&PxeAO{un1^notI%odTyd0NpFW! zNY*NbL?BC}DSv!GRW@bbF9Jw7B34(NXn6XkWdjJ5RFD(dndZp{miTHyJf{BVE_@6E z%E}TH`2#Mqtl7kTRX^h*4bP_VdD4C56HwGA{1+922fA~Wl!(Ms*aA}@<<}M|GySDp zUNf3i8Z9)!WgU*xePO@q$P}=1d%^t1|wpTHy0;H zsrd}+ff_}aZM8V%EIWS~Rq}e@6DPtEMvqHpsRUm$ZsI5nw*oQK!DzK3vkV^M^9=o$ z4Cglp1*c(eGeAo1vEANo3BnowQ6V6X_ga%H<)Z%--@{9?^O^U*%FT~UZY-!f$fo^Z z>!NVqK=pn}DB|SS^CJZJ|0+fbJalC`w2=dc&QDifjKA$~V%C-X{CQnWmBL3=(c>Qv zN@q6-DjNxu5t)*A=eSVcL^WFlDfkd034=`GfeQj4Lgh@OP8Gk(OPHuW7SDm3A&%4? zc&!8e;iQtGwWFifB%XqrQ&<`wP6&Q4%**6qF1>PwQvU;(jtuC?zAm;aTF1Ib1p=Jn znlC8;VnXIyK`=vYv?Q~5$Xegf$L#&o%)Nc*N1?GM9ovIpD-4_JbQ(QKE(zrgF+eL=C~dp!z!vS!ZiHec{?%INN25*_Mw}L!Nw<_(dv|4RUsixqS!f zkgkNx5C53zo(w}2&d0C!0Sia(olr_7+QOICn$`5h9P~CgDF3N8~XIn`%Wp?I`Cq zYb*%ygGBd*;c=5Yhs5YY;HUr&gh2{e(~Z8g!*J!(VCMNvnLW@>U19BlM!vSB-i1jT z^{t(b38B&>^5GW{F-qf6HMlSKJUv#WrhD83Fy<+rX>%(^fQng)>i?gxCm^IY_eCVB z?YZfHF?3oTBXR%`n}@t$v8bT|GCenhoW{)?#g%sk7tA|VUKkiA1lNFUD<;2k;4NR_ z9m8O@>)E6A@4ljkrc_WvliV?rFqZ+rjJ(SSE4HEK-mdN3h!2T;Uq4++3iZW+-($)N zgZ%JvQ8mMVxGT-!v%pzBN4$f^KL2M z+OB-;qrw@6t>$ooD#L}o9ZY=v2+Z$A0Or*vs8ihwP$Ai7KErQ&*?}-yW>0fkcpXntS!(kvd=R5ps`7}VoT0Z+`Q!Q%F-$XcU{^Fp3NXB&7)-GwI0i>0xe668;Yh)jI+))gF{<R zbwmWEt+y4IQ1TITjJA?evf-zq2k0xzV7ItrujF)g7!f@ z{&BzkFizkUsB+>!4Xcx4&yg^^r#d>ugsHK2&FeCFV%jd-&HmwiPH2KUFjWNJgP-z4 z`i}rnq1Y~dT@{lT1G@_nvAdEi;7>9v7YlQk4%=i@fMXdgHrz@wGjG!oo(2f}0re5> zTj>V_*uqn$9P`Jkm4Y8#iNWd9ms#BL2H!}~6T($Pq zc3p4V)3JP@^4rB~9G4r5-b-mXNoFCl$@BiA;n~Zk0iJTDCDJ$aGG!P*ie+>x;5xk%wn9)8V$&-Q_#cjFHS51I?_bVeXC!me5@?yjW)VB=tP%94u zL0ZgaljP8FWDnvA{LN36j`?db(Wbf4!G(3uFAfyZfT93U@?<_Oxd2MKh3{pvqkryX zCD~H<@ouqXe)NSaBKBli^6=j{s4y>$``6sPXH`5AvI(4z;u6x+AoE=>_m`j~&ux^gA^g6!?(ceP_x1 zCJ=QIjr$Mp)lX|2bj{fY>oLJD4QoyMK&l#z#nx=!#!pmdo%h$qAP@)GH@f>b-;FAVr6qv;@C+GvhjQ9t5)AkuwnL>5~Ku@W!6 zr56}U{(YkPx$VHg4uIMAQgw6lU?$s#;c$Eh_z|S+p4}A;FqWu*^wv~kQ(~_AzH$LJ zgme5ysD?j=22D4401u{wWJ%6dV-K8j6}MCMkfGd$qWH#dyZbnZT9F;V3d5*p#Pje9 zTPtZT!Iw!^71s6%+dB+I4;-lkPO=twi3o`$_ycs*qdJE}Qir^({RO;={e6Fzm^I`R zJb{4v^mad3J3U;n+nzLg!GvVhO}R`fx$8M>IQy0uC?D}Hv$9xw`ia>Q{cvmuHaYIv z^XB~0)Mq%A5cl5(qn9{|SKby3am99HeEP5|y`6#Hj`>rWL~SY3d&*>#`Ze~#!j{vm z*$MEUNofzwPMTZl0v%WUDSq@&|G)@$`mSHVG%b#zMSnYuAL592YcANsCwhfi-Cw0@ ztiXHl*n*N)aeBepX^y}z`am_l;r7YXGDJICN{Yb>yyD}5EK&=+HKHAf=yi17Lw zDp|6GE9`Bu_!CcEf-1M~SGbl@jkI1E`_2J{(&dK@!4dm+&(ik3Ouea;2Qy)pT=|VgO|b}rroYq0%Dc9-jvhS9SL_J;Xwmp75H1#b`WH24H=IB&*W&rQktRZaXbpvOi2{WfefTguMOxjbyRRUY*ILH%iv4W^M^KkD0jq?SE zv5kJu*g|{Qcso40+Dsh5JlDtC#`UvQVWW#A{*8c!AVY7D_hTsled=GwmsQ(s$M;Ddzi2Nrh{Fu<_(ynD3je(F-z>E$ z0%y`(rlMUd@QXHSOVj|h-Is-$0eJ46x9H;~Q!iN!H!)t%2ZXdOmE^A4@bgCW6d8i5 zpPl%_w~v4=x~%4@<8?zD2628J7m`n1h+Ok@D%R_%+Z=eJw5*cAl${{RXpK^Sb5Zm( z#GSUjQ8>X17pat}(V{c;I`*0Uf8cSPXe@ZB&x9+D#+YnwhLK3Zn4fbfts! z&G|38sjaBqgR(p1xecC$QyD!Z?$p^<}x+h;K5LiI4+|K|eyHXpVK zJsqBrXc0n_vmk!rMH;}ZV!-3fycNv;M^TeqLM_RYs zaskRIG&_Vvx74;G60)04l^(a>Vof1;lQ>#BlOKC0*wi8Ko8UzHb%a4-{1U{|<#-+e zKycsX_x6(xWC^WT@;a81wQy{XKl<|>|N9WtoYbF5t>zFoLmQ(ljOR`+wJ_7p6-tRv zR@&R0sW^0P?WB^EeP0g=)v>atk*}q$5ZQgA^$n@s^^3My)-P1}S#@G>)!oBspHAAj z!PHRc;nRr}M8()TDLCXkO_51U5fveO*3{YBQ(W^;hxNBrGlaD1kJDg~vB=Nn+9c9U zOVxe%LM+IKQ7`n@9WQiIq!)sZ+kJa`-FBh(IO2cPpy#n@$UXt?$)v9~87;?Hz-;?% z6yLzJ2Z^;N6i!}#1?rZ(OAl0dSR&0YQZbk*_qS}Op$903W50&XyQe&)tdNDy@yK=rL`W7^|FrYG7zIFde*WO zY{m(A-~_KrWqu@KSqbAVv3^5?z1yFP#l)xhgqr~!x&++^^@&a$sI!!4rMbRah3 zhfHkLen)o9831@z;|E5;4aq6zjwE?R3G1@RD7|(6WVZF%8Dg~Fl#AHA^)U9QeO|zn z5I3-ZjxJDhXb>N^k7|E5Q$$Aii|Z4p3RMg69*Y1;={6uGGGuq`{fxQTKb&HrzxYoz zwVS!i!ftIUQsim9?{?#ttyvJqn}28S_Z01vC|jfc6qXOwYn3IUlleWVVZ&4pej(+@ zHN*NEt{d-~iB%!a-s&XT(;o3EShHa&@uf3n*tK7_QG{Ax@FO!7Zk~@*nc-VQq|4Xd zKQD2={X;BAoI8}PSxQf;#G>8z!cSirZwFR94g)Rsa!gWc*QbdTii2OYXXO7|TR0!B zHQBu@G3hY4Vw)in8Idp1ZjXUyp0n~VQ)@n6>UN1}fj@WYb61Y78((}gHR&6JDw(qJ50mL@azL%zuO)9yhWSh|3u zuUVF`evdTqHm=n)=@Sl#=L2;s@ zV>}C5THdYcZ#{k&PsoW>@R*{sYHAE%y-`>wxDkjXr%16FR(81Yi=)SrYHLa6`+BV+ z?{e&ga!X?ypEy{h1J_6B!iE=YyDflIm_PQ)Jdk2XMZ z|L^Ew6Ntiu|LxqO2T_M{uM%7L8kro-6>}wH%(r1A%9Z(qCwONmmIdKZ4_rTN5TKE( z!^NQDoOfNQ*|#-qqiLr1Ytc|!nqB8hA!u{_Qg{cup;MYqHYZ1=E?fJ zl%v_DLM&UaQfR%v)a7PHI098`Yu#%>4($rogQTi!aa69IzCwaw90qBgQ1zwo%1Z|b zP!dY!fXqlD%A%?|l>1L|8Sd0s!CYVRGQ59=%_sp-xl~(T#?a_zr6^%@SSG0s#^Dgx z;2mr>reXT!6zjh4#?|=B5|^f}vHSm!J7XL@ON{=C%s02offNKKXYt#RL^-R1!Hf~N zRn)ZNZ5dK-h`7rFLGM04lKO%*&Sw+zLImAD&vQK=)dFvr{sl|Jg(URP8sj%|PJhkw zw22(vkpCp0w$PpXVx5&XRHy5BjdbZyJo&jm`e~JA=9jSv0&S^I!A1Y2r`osqoPb{l zhMZt|%=U2ZWo9v{Wh&Eg+DHJR0|!>Z|93pjcE!k&uuwkKOE+&*Hz?0Jm;vT3DfuyC z^}Xm!KS~r+l)=QCoYB+W{&@WH-G{exZYVwm{o#>@=j^z%r=>7&Ou!I|Ha@8uV>dQm z%M`sghVmvSWHY$T?5X0eSmlNmRwc2%jE*}|Ih7zKMF`D{dtxB})lx$BQl83qbkchO z6WGAn{wb35>+Sq`x&E^z^+?kmC*otdamCY?hY=nGxgU|FNJrTmXu`-2Q4s_wv!Pw` z8tlHrpxFhPiANkae(M&ujp1-mZ~lMHWmHcSO-DdFM9B&cd3}LUgOnX0e?sfFuXNiI z#$u9d3&XAmjK>^eZ6X@gUi_|%k9}wH&b8=da9y%qBZ`3GTHk zm6q>r*i;Jb?J9w|PeqIW@SD&UMH=1Lf8i)0p4{TMJ2W5+>$Q!g97+tBEbRP%Yg1Il zsb}FrIjjUIaZMs1L>&^<5}~?pY`Ls}MuWjmdFNNjZ19(TrDHqkhpT?`+6i=5uG+@a zSB8E0FZG$sx0at8Sg;k}A$X>btw~vY1E}Cf1~n6|suX=J-g%sdh*nsqdW&$H&8>;^ z=IHi*SykjAWrka5nB!{YyZHeT7s5TrDj<8nQPxN@ab?&sQS*Q%aBJTZ=bc<1#_&L` zpqC~x;7+G}Yqpg@gQ8!T0-{VJ-BQA&IMOK0HrzXYFzI=BoZSVwD5~nS zvCt^t#!sR+`8eXCHcWq@&l=}Snp5EV@MbP44wZih`Wbg|GhI7!xxLHol@fx0@Hotx zuYi5YVA7(r%{~WnH;+;>yr7c~dVAB0X`c&>G*U5Pi%s_LKd`o*Ddz`{uin(P-@wVh zxv34rC+V^5SDKB{OV_&H4B#om?M~+ARkxT7s0Cnz;Yh?U>&*+$~E&#Y!rfx-(;J15FSK3$z$uo0K6Rm2*nT*r}r8P5YpvmMu zo8vDZ1$qs*@`RVjmMo@=+`)*~6x717WN{xu6A%lm<(i4f&_(%nL-hKGkXxbdHz7P; zfVuvs!f&K<41WHJSq4FdihEx{ETp%E!x$D~Qd5Fh?O9?>b+ zhsX=`iF11#Lko9?MN6}(*pSzfCQ84o1>*123b_~h-Gc0w762T?fj(#z%=7PN8w>xDD>g}p z|M3r;9~{G<^6em_R@J}ci?)wR>G!>K$!Je5As-XtXGxOve@445u?=)|;~H473TK0@ zkS7~$ljdB~nh&%~tBmqX8(b!L&mJrJ9{wg+uM)-|Ne1zcq!P;wGQn)?PiS7u2>hE| z?5e3`P-8W-(oG7HA|u${f$74AQCy2ZW5dQM{4cn-ApQ_yE5&?)`wD;%>+l!JseCid zj~kei*0wYx3nC%P|JRK;k(J70vsuctxv}xb$}$JF_VJY!dQcBm?=xBm)Le9KsJ%=1 zMCh;7@@egd3-370hEnu~PHrOK$i_(1h~1zY=mO*;7e#EN=#neKTslP0eDmI8FC!-R z-`ib#&}z!~Y&i{Zy}V;YE~d~ie#-oLot}{X3>7+=YCM1APDC;IV*#taAk9>f+J9kB zkHB(JOZiSc_P(`bNlE&(1^jU@fLw}hkLt>On~VJcW9_cY8G!I|!Rpm+9vVee_O};n zS#oE#&$*;ieA5cB7{u%_DKdc-g>%3b<*nH;|E0e*P{jevMarF|{eo@$e-hZgYT)f7vw>573lDCl9g33x04+#5)8$C})2 z&``aIH;k`j8~i4jft<^MNDSBm*^-CGA@eCsBQAwOI{a{Q*%4?cTv8u@GMWpiJ;lD~ z)cU|N5#@H+rS;eOZWCFX>n!0`bdYjpvpQ)TDQTIk7}a+^L>qa<^biki51Sg`!BOnV zcO{)`yOe`7MUer&s&Zya1VsxDswfSrIl=u;!&~3y^Jy}g2yIBoJba7!XOJf(3nXq} zd?foVaG{XHhREHX-F#1_0q5Nf_Bo}J981kThW%r9C)@WonO%(L2}@jhs{p%7Vh$+{ zT66E$a$*i0Ki}0a%R1_l?MG@?*!KcM)Qi~m0^f`4+h)C41TyW_NEV>no;)1?SIyfW z)ic=E7UjrpYEF3WEaQnKq{*IUCMC+PsJq}Z0E3T+?~#H(PCuduHZ)kyqfdzCm(&lX z*~{nVrC=|L3Rvt$1fA8i$enmbm{@Tc$5P_VZ2B>hIdWn@Woyz>Pp~tmOk{l7rcw$&cUXN)P(R}kq9H>wM;E5R-?0;%b4*Pc9clid7wvch3|-(7 z4suJBZWSQ4J06|v#!~-g7x^%hBujfwzq19gVAl5E^5g=RBwHPV+f4h^a!>@nkUX#U zuns5(jT+4$_f8%Q(Zp4YXKeAI#%T|J3ffl>s1{X4Q_=M`J zoUpZPF(21-x`hpMe zpAE-#&4H24qEJVNmq|!Snu&99LvTtNy2)Ctbt#%+H0k-~C*Z}y=fsvKi}9s_Sk82p z$|$#^mla`e0#Pp78VpPnM=63|sl(5SrUh+X{~Q-aFoop++T=#yW#V5V>LOdx;X) zO_G~Gz?Zgun-w#n-k@94(l=b1d@iMELMGLFr5lO@<^ad4%)P3#jOvE(0d3E7w7IAr z${IQ~SmO1zu3={m^^sJnI`*D9*<${pURj!u>)e`v5wQb}`RbRVzWY!1ivN6f(694d zV3V80?r4oA+3y;R7Ye8G@^s$Utoj2j_H=yL&u27G11^;^*zi2MBU?+Icoq)c_kT z1k6;+(WEiBes&tO;t?}?SNwMj1(%}r1W~3{A-^NcmY-~z>)4imRq~l&DSn+FYx!w8wI7w1l=^3QIn*T9g!cX}e&{?X z&%G0W&ViOqurop|8x%Z%j0}?%rH%9X>b)_Jg-3y1Z67uOZ1UZ7BB;bTKMHQ1t;~Xn z9xG3~N6#>?b{exc53&YNHC3A;iMyAWX;sNYA zO;JpMYZp|1rkTDH6>fP#g&SCGebcW^@8beUC%KRmO*G|oA=U_f=5(h_^jm>S*J<;?{VR98ZEk;XqF5eZZV#!l1jlW-MAW|hUymY3=T zM{P+v@}T}*aYONIuR!BAc-FAm1m`9!=35Ve&U|S`yEZ~AU!h;4RnVb2m;vpIBKyV% zwl&6(Cg!@@eo?m&>V^on|OX=bmE zQgmm&4Q(=ekI?tW7>rp32c`L5DHwsdKl>erd>IEAp5QD{(HM-W`ysy|Eb_iNl3(}0 z6)}wsWf@Z+8^A&?g)2raFxc!Mk8O!)9i-a3 zh#**%aTbKzxWLe?zzH_RJosg_x?GkJ=Z&*(D1^a*aAl?&Q>+|Ttr`rCI*f^_zwsb~ zv-ZHjs7~r>a2H$sDBapqsG06SbVxM@%Z{YVABIRqv;RV?1G6vy00L71ogk_!6wOfe9pK7KHLuP>!=}jf5{>FEK`{JC8e(S>kt`g7&qT-v!>LO0} zh!QB*-CKbV4;w-$qvwA7;Y1X#ptI@k_`Z!7#jzYd--;pz$KYr(imyY{zt(z16-+M% zb3Y>tR+5s%%LHgb0289B=YF@u!R@>4WD>Z+L3 zDwuhB)Yi;3JmtY3L36R9`cN8J6zgOQYqBS)Ax^E@I1LwA!~ErWv9GT{HvzTrK0p16 zapOQ=>swiY0F{zTC5j?m*4RiWD3DS;A-VQ}3b%Z!AfgEF>Q*8S3CW1!w&>aCx(JeIvX`PLUE)dyg9JOryb zx*$^l+}vOj4ZFY&@6hpW4ykzRL^;J=3XoyaHLL1D9cm*Jy*$m454$F=EFBrg2t*X`RI#+s^H!sMtH z#%~fyDY`;8dm$*P*@rJu-3~9DoNmo$XxXp5g8g|q^JF=91DYR?`xfwjDrk2?Ox$B32xa{4 zs6RZcO&@*)iQ@A|&M2x6)&WG!fUL$h6X`{OZ3>rJ{8fBCD3NnI`!sH>Ncu`m(yUBu z;&yTRgdGI;jPj1?Gmx1Q`BXAk^PO8R@Ve>*8LaHm0bUn_)7ZcqNS~ zNHeWaq>C(`bl>Eh6K&o)lEP71M42dXUOYWNnl{q!OPzc&&%_WHI7K`IlmiRcb^Va@ zaJ;6w;dO#;WJ2QvhiV&aXMJR-ubmjjacYL#Ioos2xPXtT`d{5DH)N!Uz;v4>Y~(sI zN_v5{pq=mbF=zp@4YKXnd|xu$$x{1;5-Art(vKDqTg?wzaaDeHEZrs9ukVrWQgpwX z0p`MdpTdWHQr3Z(y=PY#Y=mE-h0T+h0pjU%>(&+WY&0Gdr90w=J7{Io+hRvCJ!Vft@IP(| zydK?2K(AAE?a&8!^x5D^6kO~bmy?YET~@r{H$Y^S`g#rOzd;RnfejzvmQ0|Xk#w%o zXAtQ_0FI8`m$tFnzuIXw4J8>lV~fZ6O1YTO-|b#tuye6?4d{>a;o^)Ujod0)MOvgn z9NMnD?gHLsIfUp6oa6e@Oq4(_{&j`Ocw899-Gkn5Bh$gYC~=I={^^Hz28ipVpll~l z;dh`_wAo3l$Uqp01O8}z>XTx_O%a#DZOxBFHh3&WvNW~T<&9J%pwTr#4jv7UuH@FG zY%ua;<#Q)LUbzKh{_f$M4U%su@n4NGXFwpN0_Cu;6jb}^$w8v6xx9z7$_6|&@4roG z#+n2K+QAe1@OjhKp3c@<__XPQ7E{gOjr^J(CW1mC?L}q_(}2J(Ts0AD=)>%gqJ=V7 zSI%jZ-|XOqTX~lBc@d#nI2KrG@;Cz-0(_~QFusYY;T%0>Go9_iGUOt+0vfhj^H9pd zcK4{`pMDHejA&&Cf$}_8wzE^T(!?!Jnbms`A(N|QpaQ5c=KIlEk@=K-5K`R5QL4=q zk4A0hl4!@(#8o*uweV2UvXdE!^#ETRIOn?Afqg`Ke;bMXHz!6VY_`dNhG9$?qT6U_ z!`LlM-X8Pb>et|Tcx9JM6nOW8`HYnV(`ZclCKuhy2(&tM0&>hlz8hx;&*!C4c(2wZYsGt2HF8K)nqp zI0Zf%*4vZDj3C!oXC~eUAZ=0jCZmMoo9-)!fHUr$I;_PCG1I=hp^hbfx|1-2pq?8Q z34(0szu1b~XrxA%$u3k+=|WU^j5LS5`M5xI(oBuQrnZbJR(Ia>_{@CFHGu;%BWZ!r z2_sDMPFlEG1v4I_dGuD;av4R$iTx~uG@I&FFRH*<(C2Rs0A(tKZ@eNyleLx}Un2@Y z8u$gG^;iPl%GFd6ex5Z6ZcxLek@zOAT-6vq4pZ&R<*(T3Q%lyF&j0Q)pOa=$U3AT) z&Zy*18K?sZX{If@XE{eXsUMkV1OCS7Fkdv>NXfe#_={vd00cnlI$R%$QtCh*vt|@F zWTVvZ3U_O~B1yxVvE80JCYwROr(oE?7jFF^4ckuoujnJP0kXrKeVd~&wLUX~!;~Qo zfDFw@e+@V)X~uqF!D(O zG~NskG3)>WstxqpEhBp!${d@6wb|Wc4LIE;FY)F$=69`Ai@-vokH7sv`bJ%$CE`zuBW3BNUL0pQfs> zB=-xwAnYy4eSKg|x6jBwiF`l}$AW9b0Q?yxeh+9k|JqI1q1|WYpK&V! zq5^=Z5E$mE37y|yu6DFdzVXqG3s<0WYU@sMV#ufC!ZtluSur7Tvkss*vOhP3hywZ# zno_s`Q68OIYC^gQ@=&2E?zARgL$G<9UQ=qYjOT2rjf|4nZQ!oYc0QR?Ecp7Kf zR!+U`$Z)1@qCRVBN{QJr4A$am%ORXDwP`*s)48WUFV)-d`J#+>E}654Ch zRjUhV=MT0QK4bkA^s&8T#Vs-`x7$Q2J_3PFHFGYNOf~C_1@Nc4OS&BXaV%@RdZ)nP z(7Mf1gG3m&3y(fHAb->#%u(>@p|m8CSR`10B-)p|x-J9!yERg=e-9B( z6fjcD1!I5J4~NV4rb+4~XUyH{`w>q+xq+Y}fNsW=)%)9BQ-^3y-RlJW6?(-^*v8Gz zFjMZ;RG}c1c6YhA-Xix}y#3dSZC8)Q-dch@&4oC-O@vZJ%(=w(tR5hDi}}bUtR85E zZqr>4;=Fd4r3-;azy|LcnlkZjzwql4t9n>kLCy5{tsAa*q}?AB6wvy8!f|0}nCC(` z&d^>${_#w|g3=8whQ4FC7IrL1=Gm2au5$lD?I#Z~XR-_||F?@FF7_4ii>Jn;`S0Sm zC!F;@$}nqN(b(c+mX2>7uWHf&p8hzn?OvUum8v=#e`adfKgS`qX7}S~jT(eH+Gx*R zn9j<5Lwz1(?Pi*OZ`!;4=z{b%3+=+K_4eAqCTzPVCvpq2lWz-6rQ zA}w;tPR7Y54P}-Y0r@d~kMYn*untgG{v!PkhITVGqkS_K-T8HKkVkuA15 zV7ET3Z_vt*e;P`p7B`kMb2n-@WNc2bcJk)q^D^q@Yy)LilQMDk=z=cF%;b&_n7pl3 zlqg%n@;h*iN?;VYslk)ck^0zINFT^p0vOE$eQ5Y|2#+^JT9GE-7Di68vTG6QOBBeA zJ)jJ>X{EdwiqVKT7`J+QM*-^VICukQdp3Uw-`YdTP+ha_@r(xE+*tuOXRSVRgXmrR zmCjuRB3(bYaUOW>dx+`{F2*tN#vY%bS+aQIS^gw(mZ2ej{J9auiXy%3m$~S_hU~m< z3k70=-#@}UBPvZcxb>UvcT~dE4N1w%*mH&YmZ6y9-ZAlngsR%?0ZPK(><=@K#rr9* zg89~edn(RJ3)^{ge>yd^1p(k~DNHbgWZxp1pLphk`ISoOB#SALYIhCiR3kaB8ERG> zl}ft1@~NS9h&D4CB_z07;5ubTj}w0PP#?xOi%oi^ zIq(58do5GL84O`5t2X*^iEVi&P`|(Z9#i{aYor1!zy3kydNjt0XgyvTZ+NjS9zDcq zTmVdki3rs3uGDJY8-NH!S2ZG$*I|Yrf3Ij<4@{!XjJ-y;>z1!?O?=&>P`PPx8W4?- zQ#ht@PBMXfb~x_|YF)e4nxBrLU?bY1{Gcr}*L1OV`0|hwL}VZp#Jw5g_LEX~@oha6 z*@5E}D+i(+cO+{1+$n0dDR$jAS?cMRMIK^7x5m&OIep*xFy~)kFBk)pby8;N0(U%D zecC+KicP0<{T_xHHNw_vFgVIsooweh2_21Jb%!#&pc@!M_d@TfmIVO5MtJ~Zm{2$C0^uU7Y`ahBk!g@9SI|+P z(5+^A(ceg%=U1~fwF5*_ zgf%6%`sych3uBW51xEvngXO@o&k^QBqXeOKxFW4wETOglXl$nOw@{BUJp0;XSu$F67oop#I`R@PB3+N)4zUYbXq{m9tI!x^xXs@0^6kfd zJojFe?@k)Y(4Zwi4F$;8E$ZXm6C^0jWfBH_J^=VJ6=MU@(_~_XbwmG(36QVyBVd^0 z=YY#!WD9ZFu_2N47Ll8(+#Qko<+%R|;u{cpQU>i~lEmILjLV&{jKRH!@2MQ-cK}B% za-JGRj|@wjb(4Zaa@@TRUDll0AE#v6Jbhl@;Hj19hKN{Giu(~XSXnh_Jw-aT)o-IZK z`41aWFA4hyn~JyqB~YuN(x)LIm#tQjWAZi&rr|?Qxp7BD;)KL>JpYmOM0pt;*9vM| zwSa0)MpGi)-+i&LAssXiXMB#Oy^YEKB#U4Z<;eLv{72*bJLJgx2UqDkwd^2@B9~!2 zwIA=N4m3y=;4biTS5Kv?l-+`jkd>=&6#eEz9?c(m0F@?h{*|NyMSzFEC8s-go^R1vBRVv*A3WqnvKC{8}2P-PQ3E76rF>QBqFGk7< zN=G}epL6jlnEE}hsKudm22}B+SUqf)ebo%m5s??gz~lX?muM-nSO{>c)(s=GPG6X9 zuDgZiE33rTcW!6+qS?C_pBxXt<8u?zEeQNAC^B_@kWHRJ%PZhfL!2h)($@cwf)Xt` z*A>>93j^_kQ=O#q4yKBzRSq{bZw!igLHZwzrRHK3+5+nhK*i&1IFRTAoc(NwFm_di zdhQ7}(Tvhf7vnH@+@+Tq-E4f#XC3T;@qgyFytl_+bdC&VAv4J{GHF**lgaVVVc9w`P9ZKJOR+SvCXy9P zU|RM^CorRhWHY}&Lpx6KtnYyg;nsja1Xmq4KI5WGLo;w6kqjO3DP*Mgn|*20J|#jo zY_CZ$-ecqweEC?)a|Y`+j<%w`?f-!&_4;P!n?4n%xrAOFD!OSY4m_qMfgLUaVxLxs zGX#Y`!JD(jN%htNkBPz+A_^2tTu@paFERYQx%L5|53ytua-CtlvmD?NZ# z7z7{R1W9ZlOqvpT)Cv|F_gaw^Ra0?sYQwKGtf@)BgneDuo2=()jtwC@5V{< z*B0RlY{bz@PyOcRv+MIR1Dyh35)BALE>3y4EYKdPBn= zmW=PpF$)Jpewb53CKuDwg(!spfnkz!4%i2R$YdDjku3F|e`$&Ju9>F^s)^4Wm2wR2 zF|a~bcG|M{t8oMM85elSh%$NWPJ+lIr#?L)5jFDd`d4G}a?h+eLsFjJ<}4C0dNy4ZBk_>6J1WR7PK3sw>f16u1Iaku z;@tr`nB_aVdl@|ml{=7Rq78ngu#CbskVHS3X7SRll-<#TBwhFE&x<7VUelU_{KjT7 z2SH__$4%>LJdx^M`kq*0&qH zCZOQad!Wgx>G;2>Tda`0(o`xXDl2QX;DtB`3n15Defit40YM%f{@3`rCQg~jP&akwBj z@AAmTw*WP~6Qka--dd*AfC-_#ko(L4QfCDXxzx(ZG;6kBIRK!RLRKtdySI%;Sa+_8 zSUd5hMcGd_r40`x8b5rFzl`h?ENU-(8;Er90X8iWgkM&;+Z%~?L8uf=Z%**pD;0<= zZs5XcCBTf?E>6JclPrK>_UhCgp>8j%oL2?=BB0B$8vBPj)_~=;W&mA4qQB%8J^6fL zxW3F`LPAd$;Zuz?)1)F-4efAY8kVMv93-gXE#-aM=3Ecuu^CpQzHxUtI!2B>n3&kh zf@uWwk!B(5{*BF3P$+R;F9qxcy_x$Ovu9&oHpy*KDDNP@;xz=4A%DF|8vZgwM)o6X z3RP$iTDsBlY3!J>o6=RP7!}AcdAW8kM~|O5eig*`x-WqPprN0;)$XvAH zv1n>7)!#+cxKI_fQneqi;li?ZfhICf%7auv#Bk!@E{5vWuE04--%x@kx*)SD4MjGl zm7}E`gbjp&p7eWVCF8ryhK-6*H~<-wR%-v^D`y7O04(oTp|TUaC{|lL%=V9mokdMh z$XR3>nyE`r$$a~hNub!2}1)!eol9Hd!vE$N$?JpX9+2T~_u6d8i zVqsqpJ1=%w79zK_cNVZ+A0Bs$# zM~Q#K28>@3e34h)TsH^49@|h?NAdO0PHUY%F(6IO50xc8;Y5q(kdk#!L9wqa2VDIm z_FsBmVjsyN4}=5S6wGI{7a=4&&A&@k8B2eJL7)H?9tqo|yKP__wlhEIJm5;9fEM+s zc{FetqkA%B#U?KbtCN#hnHB+o0DDji>6f7rS-X?{@6#qh*z7yj{+E zby@($t=-avLpr=x{8BwddOsC)!m;i2SJN8A?4xTa2gH4b(w-yGmRVr-oAdwyLmRtS;30LN zu`hfAO%8j0l9)41-J_!2pnE^4Auo4)6>_-^2AwI&^?q?DPT}@a0O2sd-=No|2tUd$ zEO(&AW>xEsH-~he|8+~kP8dR^5+N$M0-d?ZK>l*eUdZQ%2VD99_ip1hBPp-*KFcF~ z0%>GvZu^fcl(j!9^`-Ju z^M>1IMYut2NWGkl12*j&3@gARJZWE|`XUkS0gS^O;^(*lNUU92&I>R8xO!^t29L@b zGwe1Xa6p9_w}61)3~^NJYVvH!2?T zYv!{%pZjKUAJ8`Q+4Y>^z(}FbJksPIBG9Wxm)yGQU#SDu$> zuuqIOr!RJ|`LlVaVPRKndGJ!ljZ$66f_<2>Uosr2vEaD`?P{pP+NFu17T{v2)vs?Z zZ1UHLna%hbVxm#The$ zV>jGrzlK^>`*x!~$MaeA(pe_a)R?K) zt4LQDt1^ADWYi1O-N=IEYMXplf*^(<1iXnd!cW!|7|x^#L`mVEy(mHg+Pu3XTr6DS?_q*d6yqZJ|-Ha{Y}@Xdpu#^MRNb( zi60+LuYleJ003-hVrF4sV<0guGB^ML0gwRzM*si=*Fm0SYE!~ryt&%Hrvl_U{FQSY zZk2?ETLCa=UOYtn0G40?YCG;9&A*Zxbb(w_O^6k^-6I_e7pGaYV zAV`b-pcSp_>JodRiyZK^il@|iuQAQcbn?M59vWzS0hoc-|Ax2!sBP}+J%q?CUc8iN7gmf|`Y2gI+CP!-<6__UjT&1I~y2ZbGSQ@W|cY}3pDLGX$ z40Z`nQ;y|2i_W)4Dq#Vp+u>iCOu7Wm*HlmbDOvrefzE$z*(j~H^Dk08A!C=>>g@N7 z4|DN}r33>H;i08HqD4o2{{2z;it?H)bkZDcW(MQR2nIMN*5i2wh|M7fK-+R;)#E5l zJ!AC4dW=BaBZv?z>m2yeVH0fJ6VuGd)S_T!9(_y4vJL>p0_R4de6JO4?BD7mIm*p) z-fM>w{X#`q<1?n#E=>v_`XT1@s)k%Y^kns)`ca)Eg4rvT}P4kepUZ} zDXs`XYZ$<5>0Kx*X@b8w`Wyzb$F^xhQmzbGg1Wv7uu&GFs~TQlO)i%T@n14aDIXmw zP`DQjsn6|6P84GAS3(pNmUsZ+9}w%_4nXzQj?n&cK?6c#m={&1)}cae7Y-1mG<@vp zTN5}cegceP&fL655} zdKXfHHB)~H&v=dcENY3bg_HfHuz%A!Q&_)>7^01o8zati)X`4E*Bjb@kCdqWqh0fY z@+vH1Pthr$FZq*ynI2yd`E5M?+tFN+0PA0qVONDs2QTNK?$6Ujl|vp!Wk5Sl1wd7^ z;ZUGoN7eG_;+pYG?2PZFRpGK#xId*`jcPQeqJ#>f3Z0#2 z$jr8$UrvCy{qU7cY-ujflkOq&$>u?v!lC*lD(D82yCGD;=?a*Rt6P^v*fwFFo)_S|qu<7ElJq^1T7p5?`>Ld_dIo{HT~ zT$ax^u+v56AcWxgBR^>qekUiay?#%bp{KWa00yh$KCb)i#r(1C z?x+9?)>!m2^sGKx_+JY!07hzFRNJ!Nyqo|40-XV#gLFdQx@>{%&A=NrhXz2`e8kMa z4>d0kT&1DMvG8;ky3LuFAz}a^pZ$|IB$Fo-_pG}_ zdwJVgYJNA&NN12PmnCUQea_% zzxr2_35Rv$7%l6{K-bv<9HU%<;;fiv%1uq$c`zs5Od%fsx0|!qUd<1drpL(L~dY0)Re0b=}qv|1G@deg16MeSPRE zCwNOc`w#p}?edvzEg+z-0d2P9AXU1~=WU`-Kj&UXgM{)+foiA3VM#5GUPYrW|kO zUz<#McS1SRm=RMMLhSi)NQQ$$fn%$h18Y~Cn*{y&NjBc8JxAY~#L-44>6K4qw4n>V zZ_>d{nrbbLr-fB2#Ly}eQY{kk}Rtp)yg%lDC`vVW3v&8>ePaQOTY`urFAPMD%K8w`GGs6GK(ciWHZ=U~#x_eH*fxgw6G?svMJv(!m2Juj0r4lR`?V2VC zfO4Jo)-ElATdzVoMc@zWJ2Gs?g3(e zoB*Q7000690iK0wLf@O~Yyt8h_zVtb4M8aWwxY^7PCZW5?rE?S!=kXEJ(waLLfR)- zX6$PC8V=tna$JV(VYWfoov7L(>3%dl=_F=Ti|FzXuJHQjgcrM%dn$A}(r%^!lMD_u zlop(@GQ8p?>gU@bVi`K|uEE4&nAiG0yQYcqc8sHxed$-8tPqCs*5?66DKLQ{*0?E` z?ggS=#p4_LKdfW~N!@9klx%4nH%OQRBJ1{uD-?E7HA%{V!35*)AzNK{LwbeIu~kt% zub&1Rp`88Wni#|LlM~rsTFx&o4w(p#d3hX_DAcA#WC@X}iMceDbJfDT=xxYfbN&$X zwLYVD+aUnik%iKRJR{8R)UQ1qFRtswRrV!=%Wm=2+L@LYVokGeZ&95cvzaEjnt;sa zw)_qo+h}R+l9X zU+?pxPNMvNHY4k&HT6#|CCjUK7;i4ltb1bYw`cxn^KrKQ9p{s#>&$*$-&cR<{!1P~ zRw1xCwxglySk4s_2?D`EC`=>~2?R``5;rql%+l{EP?fFgbA%xZ_JxPm z{@z;u!|WP*Z}+}Ff7{>6e09n1sHHppudmhgtN&CSIPvAk9F4xWAmioa z`#JFE%i!nc4t#ic`MB*XliZL0Bm2ME{9pEWti>F|hY}N113&g*(BHJ~25nf zlyM&t)9>Wn8fC26Ww((i#esc?OF1Yrwq-fp(cT>?9~wS@R`J-4f;Qup#O4Qx0GhxA zW(XAE1AqVk7pXy-ib<$JXqik1kBxo@D&Q`BmZG-0sCA6cL{)|b*lCl3rSU)z#19#( z9%h`pFbPp|Neb2P;{L7OPBm5~QHi4Uoc@f<^;4@Kx!;v)R@8kyVjP>dV0es2pgi

      aK|JAc3}y%BXQu-V^PwGPrKDCOYs_#ksR|V(A8bP{xF=8CKcaOR(TgGK z#wW%0aRMBsDgOgNDd&sJPBc(I>pA!)9-=eDJHG+4?C8$T+=kpM97ik|uvt^t9d&@%o|qr1`mBr>MP8UhA)g`{gj6^V$2+kwN~uHmnfXK^MjsxfCO(m z=fBa`8)9ofXv31}`-~1=;;k_{97=)FpZs+WolW=>^zI1eDUZjwf7u&B8((sV^h3Hh zyPzQt=nTLL<9=13ru{WW{vTSWfXk3I|LoIHw4!SIGzTxAL^5qdG!K%Lg#5P%nkVJc zLTbf1>yY>)eBm+dG94{oH&%*Obo70%&y1IUC^2W4NOJofZy=Sxj*&_o*Q=Jl>gawB zy4{H6`*dtsCtoNeWA1Kpb(0iie1Crq8EicU*yGqv1#IPwevUgrq50BQ60206(xfZY zj-;@ODe+MMn8LBcotX{`D7xUA<*Z;!Pt%SHp^S(*{|B@eV~()S9Gk`pd}d&S_nM%@ z&V|%Y=%KtDq>_)J96h&m= zbB~FNUBtfc<(2|w@a7U*p{WjQNq;reMA*e}4}UrlyGy>zT^5Bm!|T*z^gvG(%(|d; zJIbkk)SDz)y0;6&UI2Asw9(2c%G{N-lQNgFsdUy7;onPLByw`4bha3_47J zpfq<2Vts|#g|LN?&+&-fmJv}NjZgv{pdIn%y2r!UZi#B#vs;^ul*+Y{R3L#Ya{x+w zT4mm!>xO^6E-jN#X2nPG`?M@?pxKuY@^{s~ObtU9GKY_lpuBU^+k(mI9O{q~T5CHvgw|*3@(#dj zaXmn0tOWy$-stC=MTjr9yr0ioeG2-Cbbvr-`7r-L-?1)#$BzZPe@>Qjk^PE|HLZi~ z1%Q%^Nd;7LX7V6a7Nf@~JaG9m3L6r3(*r}LiqNK>wxH)X$BmGXo$oKV_d+?@GP0Hp zkxxHcmTNU0v#g#U=%=|cxg3fzwu)|^5^PbBh50xpdW84oh3#V&J6WUD1FXVjJKz52 z^9QK68XK^Ak}N|UFGG5ISZ<30V8g~lo$hSEcO(a9npfkTR>R6(FFf_=d^v8d6}e%c zGMmJ5aeohlQ(sfOR@iU;ECAffeUbsL2d#>tyAar!6wtdkpkvX_Hwrf;*|N|zO++*= zE@KfhTEhdbIx*K8hL(xT#!L}Q@D$2{6o7vRQ_!~t#*t9Byam-h&Vs%R&)=!YF94hv zTzL;ZvB0lK>>k$c0N!F4;Ik+M$XLwJAnrm?u0V~b~U#APml73O(96NqTr${F)1^AwbC#svgLuS z*(g>@bohvMXeWCkFly$38opVLo#8r&(!M(=Z~hUFS(-(@uvPl-*v<7E%$fVN!w z!2N*Lf6r+7;rvY&X>*s>-N59f4~k3or;ynhP({BB&kSa$qGqp6*cKIgOG88EOIeL+ z#8fb#_G}qsjQRDV?}q3a+TK07aS}BM7hzeW6Q5Yrjb!XYCGK7vJzrLm1iDO|&`GtU z;5Ff?rXV3Nw0v=RRb)~^doz_86oz3*XA3eV|7C&O8tuJ0~Nm~5lL_&q;UVxCkO zm>D<25MM1_CekVWrbT1(Xv==S%r;P4M%%!Er>oKhI6x2<0u5m94R?$7TKZF@Y-VM@_RJR<$>moK|AI%-*-Eb26Jx>4^mv zV*?3RgFSRonx5Hxsg~fszEp^)taG^D%ap-+)!gAdaMj{GQsDrF3fiy1+Q9TUe_HEO zW7R&|gepl!6Z zg@=k(^(j*tN`?6*Qg5kkBa-EJ^yX;>s`ipbRZ|&@GKrk2eGB-sM4q5!999E4HHbO( z&Q)xO>Y%r@eR!Dy;6l#8AT$B(%-=fg8sfOhSu^^i27I*kWbl!A53Av>no z`3idyEf;QQTj|qEo?zojGpQXwT@95;rBN?76E+|KOnrmKI8;* zA}%I7>|?MgIhc#e?4Cc2zG%N!s~u8Sp9qR=Ia3X`VdkO{0XdFJ0z?UVJm{p6+q(0Y z1>2O(!ffSeBs~5B+GI>6*7ir=xS>?m>5KDbWz-)8X&1y~0^3YKdu9Iahz^Q&0iQP& zK*RP8tFg89sBojA;wBQfOzIkv&;%_9%d4IMcjf;QJ_&Ng3km*A#wIP&-3x6Iju|Tx zzB?yz26c*Mks#Xq|58WJUV4H`O#b#fv zePKL)s)F&i;8!bBDRuPfZ>$?3>G5my|#D15cP6SuybbfTg6bT7@j z36AsFPnVxY*ndQb`sFEfD1>v9mJwVk1PfI^jWpuQi$$CRwt7#Ik5#|Ki-;_fk~>U2 zjoLwP5>YtPAs?Q0T%<;_?Rp@c5NWa8ncQ+39_#}+hvY*B(bdAcUAaJxF zGNvXKTHKc;vp@=aXgMV*wF!{n;A6!73;66GfGe@J)+aMm|I>~4288TnieCy$FrBPp@IFG)x^i)^k3*`>8p!s|c zTEZ*PtG)ys`wrT#E^m+wCDLda=SmMGlnEZ2zDif^87yR5TQqJgc&4vw6hP`)q~TZT zYOJeduySef)k;RWR_5;z!t^$hT(Hp#4tIMkmvi_FJB-E}Xms)0^M=S9ky`_n_2!#P zaz9W>8_I8}3uAj~6N#8#^Q%-ft4yR~`u_+mnr=1y7v|3Dhq2_Mgr&M{00bZf<67+C zuO7x|xv3%TMbu8$kV^?SpC{35z|u$78cTqKzEZXgbh$Ushu=!I2CFKD1sdNR0Ksz^ zS`ML;a)NH_n{wJmy&6rj9o-d*0dX*=K;VC89o7#_+E3m|PTTzSN`btIL&X`rA^0HQ zBlBY4sUvax-oh)9CTfDp7?h$dyUWAGXrGicw88Jx^$9@@tKFL^Yw5O)SIj-?!F#3HknXf(_ERA6=N|;}QHRoJD|G0X_n&Z@Mt4GJ zk17m~kmLLd(z?9^hvUcu!l~oaQM9!*xokvkB`g`EF_$Gm?uGx^M_yT$n+cx=L+<6- zjFo8T>G8?>;)<|2L_O_Pk@0t>%u6zXG(ac{2bO{=_XrvMu-OjdVO2j?oo>51rWeg} z;;u(a?fO6O2*XtRx*92j1XC)RIXw>U89U61siXIPf~F}Ltc(87sRx3y70GXd6eXeO zqbosaq$p&mg~&A@WYPf6bjGJ+!cMTnJrfVioWorEma^2d-?T5kXL10L^8`Po+pf7tIxWwOD!*#v8TcxlrpSFq4)thOE^ zi_Q>fI>!tm=V?OW?B)vyd2M0(yF_nB+tB^g@o^UgSRZ$oPKJ1@l8@MGgj#D;kOvyv zPaSat!R%Y1fJdUD_|-CKO8E9~*q)PqeflN76i!DW{U!8La!@PCg*ooKQ5Vc>|8U23 zvLO{b9*OWpBfNt=C=)k@A^K%7+`ss}Bax_P(3kheIJqk#ttm0b@pXLzxR!;kg#TLI zaBLcrHgMqh#z8qSx+PIgpNPmdiEk+r41Bi^H%-nVAE;M2Y|ohFC=3H zl@V$pqH#xDyH&AVeJw(jD#1`;`X$ZR=^_W4V^JtZKCW4AHBksZsBb> zXTe-%1%JG5)&B(?R@))^I^ZO|#C)LRcWqS&Qsb{@)^@>4%?dt6u4NZ+D%PR=PlA){ zAeyR`k0-;|_bLH>5R0qUn=a_MF(H@KGtHhQYkI~n1e4+z0 zSNw@LEaNKSDI*%HB`|2qy1Hj5!eppBz&iHLc%P5fL@=phZ9Ir*T#vm1*?*vSze)h!_>nVgsNsx3Bx4 zJH)(4G5xcoGh0=tGa&yKgfQ-|{?+j=tZ8fv^G}@TcOvf;q4e z!9}11K?7Bzsg`mqUAbuBtb(rCP_=Y8$v$dEjxccX;RCO($&!0kvWBOBEAB}2Ok8aQ zNM(lAR?Hdp%nmBL{KP@DHLSMEU=KY`R{4Jg}*T`scGGapp`7U`n|@~+;=!Vk&1oz z2lEoFLC32ScsEuhRz2fO6f#5zG|7!TeI67y8ZGEcgH!(SnAJ%kj|~5v^*hbxIz}m{ z?tk96Fj!-~Tmw>=hQSBEp*L`tvNr}K^wX>ptY|ia&jm+gccH(j{nrYYeqq^Zk`)90 zAQJ5Rs0gOXZr{LzPmtiXbmQGm)(W3mS@zM)WJ9y^J{ArwnBAhVk*}DE?_mN}x(SWy z_d5L$W-j;^F9QEZsDr~Pi*|W(4cjw2V)dF1x5`d#ht=Yg7r`oV-A)*n@sR;7HVQ3c z_~2Ygw<+xar=7D%>o`7Er=O8+k_;Es``^p3Pd@7&D&DIN+fHUXf&COxW*W`SY3pe5 zzssPa;zUji><3>V{`e+XUOyY6#cz`=Q#}$Yh!BkZ>U2WkH4J{ETLt{qDqwQZJ0*>u zw{N^w;a2qHAoJR5Qy04L+siI0Q>^3i+M=l(x%g@hUEF#JX(d*pjTG1T5g8kK9CD!!j98c&#OOcV ziAyl?f$P+A;@^1=L%f8C-)LqkHO29@;jV9jh_&dh*dLJgL6LFq5Vq6x;clT1+KUClkVOmzQk1eN6hHm$zc2RqY)uD9LaxzTo{!S2 zg5|Ks27T1);TUS|!4rSnNi#{5!j3D>C4%PsP?BO45lvmheXk_CC4K7i3%u$ z?T|1cy<;*T)qEMdlWQZ&gG>xUwO3b8(If)b2)d-M{SafCbg%6peKd&NByg!2V(i?V zon1P+_54R5hXkgyjRL!O7#D=jGr}LFzFEku4COzi4Cgy6nK&;6nRny85HYLoCURa7 z=gHE=SSaTH-kX$q!ynR{f38U51=Q6QdZr#jr_%QugI^O#D<&gofCT zYn38k8%l1DgI#+Ct=xcXpZLwCMZDwKEt`uQQL_&ibPYje#kkJ`xpZa&bzlQY0@+2P zt90U`{L_j~Nw6pOedn|}*(-q?K*CRs(UFenIJ(V`ae`|zE5L%Z4F%=llaW*f8NmQy%o{wzK%=C za}GB8e;&CAdFtWx;=aVDoBOKk#)`AguL{1u5W(zDujN&Q z2dsJcgd!O!apC67EvO07faA@@@al4WyZ3X;nVPOc=Hh}PhbPI%L*`|^$wq=)Hnp-f z10G6<{^X%~wy^V1%(`=1z8OVTYcU8pnJ4aoK@dPyu3(kG-~a#v+(DkGMG-6||8WnD z1ON?_YL3pw=j+`~TQZav85Q~r z&LX0km~0X9za`eHqe(3qfFUXm-aG4MHbm+pi7wxYh1XZBtJ+J)Xf{Ac%%H%yHd5vb zDAu_b%~_uRTCS!ol%)Q^Kqk#zoDI^Z(QlYVUEGDMqIZ_FWFf(bKhY6HHiOAHfa5E- z;E)Cl6In2?ZUB^#CdT@iZ zm?cCvV-b;h(O>6kw?{$$fRI6m^q;4WODZi^DTBhQSgNBmjkbWq{UwHqAovk{}1sivi+-6?^ZziYq_hFxrpwPtEq*jsUH!U5{= zS6}!py1pnHFUqX9F8+dCfVjC``HB%j(8qK zd=m5Qy*xuSn1_=!fQ{DYxNzu)R;r-3FipGnF$@Hgz}u!|w~-E{4=X&SZQsH55hP}Z zt#f~)u*(UDeQIuaT&PI|(ZCIcZYNE6S(HeSY6_tJ0HE738vu&G0=8Yz%l{`=MffiuZreRdBXheLX9v z*Bdb-jX3ZpJmdr+crPf2)KKu89@7}^kd#GBZ#-}#6VpUGnLlg`=9XNW>Bvjl%v;BB zp}MR8`hcP!*1$Sg_RbE2jZ`)xjK9J>Qj$i;IxDr4t()3^v}=?PUO!}J!6!Rd`291- zX%@-a1P(iEi<0_(A+KA1HV?yVyYuUWO3Z5X39PzmmaU^&goCA4wa&+;c<$9Q-%bnuDY3-lz*ken~Bd?L`K(b8#^=tpJn$sAQC+D6vT+45>R& z0~)uAJtU5`LvX`dNKl>CU%YvsK3wRlI{cjDVSd#4!8*$rbRtC6ArBzEY=XmuL0Scc z1Is|6r_KdO+l$Cn)Ma8T!hy)=V>+w1cvMDE zy6P@q7gd7{+dG(U!f3j@dT83|+X(;Z@!j?aV3hP70$F>FR8#ExJOK(7nouSh1%iT5 zkd!785RAelcuh{~?x8YNCRr|2wN#h6<-jhxf1JOL|1ZMNF@G`p-{;-*mfwZ-x2b*H zJu9w{C0X_I&oBPA=i}qJ{%&%7-X^vv)bAK&RIEFMw1ZpI3-O}c^t# z(i&$&gr#7*k`o@XIVzXf? zoGdsC1p>i) z<9}PP!KbpxPz-J>&POws%i;6?yZ#5zJ&AjjJD8b# zu|Lz^F3O<_HJxg7p0pt5wMk#Q>#Vw#tf_S@)TN=83r^N8p9H8ls8>-O^hAf5_CQ22 zg%&Bq4gdfGYXP3ebVA=|r(QV2v5on4As0bvg=Ez75P365dI3`D5P9*xu^vas_?oue zptE?3FiXXhFi{(KpHu)BSk@KG0Y9=q%_d#$)IJKPC@{)gE!(4wMN zivuU42wl~zE9Jcoc5mk~q7amX^K3|NAQ1TJje+^506vpF#%<0dgK+#8nx8PfSfFbp zbEY0-+535n2D!rF%MhS{Rf|h)+|k0xQ-M2TG+Trgd>Ij^i!T3VgZ}iDkyU-%xB1YB`?iKS9>AL5#(EcgaAofez1oX7CxRLd}Oq;Kr%XI^4Ah9 zQ;TN;hYSTii^Qqa&KgqiH7UPrVj0lWw?{U52d~;{y`f&67Bd>b>5EzLXs zDY4F4rLpKO-3ckGnz#d;VLpj~u`$@{N3|bhWkW#5^YiYfl}lh}@JG=|qMf^p#Q@xZ z-iXXl~sz$fY3OSmH7U^0n;luiq`z{%qQ##|se;t~s-0Cg^-ZEzqrd-BBv+#AZK_;97U&`DsNajZj zL#jWe0RR=8Fcu^Ul>uO=SWp%Ui2_4Vh*TyKL-Q4`eDc;sQ5xM_ORAdRTe?@6(0CLx zy?%esvp)`!LEz?5|Ej%@|9*)tZYlQ01AetW$h||$|JQrQ;?t$P-=I8%xy(G%??C0?vi!N-Z<^b0pN~&x1Len$Ha>N2>KgSnj^o~c zhx)MRW+PI5cp;yqlXbp88+tmHpKRQMqN8a|d&7~!P5>{`b(E6Mw^*H@Ip2Uz_Adz5 zV_#sZ0dnCIsKgEc00E%^p2=!L-?t#93i5!=cM7p=kAORxEQ`t(>H1YNd$Kr+{}rnO zNG#xO_|gBUL~%}br}?{0sFOd}UeX(>W|)|7!X?g<$BMNoXXop1l%ef!?UBS=(d?_C znSquap755K;uMr`#35kZ9slj}>_xG#vfe5q1ugLp`!v5C=m8sQnlYjvz@sWH{dFN;c|}KF4K&ZR8|-sS2Yz zW133m3n9G2uR;KK`xJ!n`c56c^w=%Zn&3e%@G zdL#nuL^Ta7OZ?Tr4CCj6y;cKWUeFO-;p1#Fd(>yQx(R zQ|guj;-+q1dJ--K^ozKtGS^Il6_S`qFRRDNZRTKsFav+@M zx2XC3E>J}Bu4gV|sS8=vTM~{*Qpo=Gk1I3{Oq5ipjEj_PC}NJ6DAp0odQ-(As!^?3 z*@R}KIy%Ct6ID*ZPM{=Iw-7i0;1v)s77K;~#(=P3EQbsQg5gmhNTNdsjsMeCyI(zJ z%FMW4Oqxr{DpZT}R6d?O-2Q)l($wks`1sB0)#drXXS@6Z-Z8wAf8CNL1B`q0Nz+%7 zb)VI5O#1taZJ|e(KMcF=46O@o{ZUt%f$HFgvy}F>?%eS;3k<&L&_9;Dr?|UNG4{H> zhe+uo4utY>=f%&DERo^iMFQe*OlV!s7mm76(;8@nS1p*GA z^lgCurxoIN%|{P;VJ$eZU_AY|7g-5S%phz9feCImrRK{lSGn#fs7>>q;6Pe}Y{f2z zgmtU3#+;r?gBL$Uv5;43a0rx5(xPt>N-N>lSb_RsZmAM&ch;fh@7iqGi)2{)uvVy_d_NArpAU(y0`duRrf)Uy_IE=hV35Fn>HAu0||Ns1GZWp=Rfu#M)PTqOmT zW2UMR5!i4!q%ZKxDG|!E^;ONB7Dr``t`Wg52Fe*ovYf|@iC`3$;sO0!Z`?yz4x)z0 z@B~o(ryJ>~(;lr`qUi4!=on=Zf479yZnMJEZ$8+&1R7Jvr=M4` z0|A)C0=xd&9BA{_xbkx{#u~CRPG@hrXgh*~>ktIo%JP)x;Zr!1QOKKj(qA}(-Z8Vn zLRb}zrYmJ5i4Yr%$Fy!eEbT5lAcQE(HWK0QPlb1e;24F)S0Z^9P@EY|m#r!4jaR1t zQAS@-kFy|@Juiqt#v=?Kq}1#YV0NqU2$CirRv(N3)uW5qO-!2;_A504_Bb=Jd=eF-lj5^gcPEw;waJn8!ZT>?&G{uKYkCL+yIa6J4MTwi zY&k-{L5WR4lO-&Cz1@u2D!@oTr1M;j-fh-9W6<{p!fF*==HUlcos;<(axWltqKWG? z1~sLZH}47F2XRGZ>(`s)UDUxOEvB6q$yw7eQN8GG>L(9i5KHMZ(8ps)e_fqbfFo|< zwUheeI#@MDFB{ zz>*~M7_Em+qNITf>i1YX#fPWa?#ESJO?BZ0$9Uer%9^>3+bebw5^f1ZT!hC?D>Q`A z7v!+y&2f8|h@p$9X^W1SH0rUlI%zRQjbkEpRZCjCCH%DmV1#83*qBaaC`wkC{BUF% zJC0&Jodj|u4~>1B0;MrvC?eD8#5z1xc0Ajx*Ps{xRkTr;7+vIITA z#7(H^28Xkn-^(tUAsm{(O}n1yBAJj-)0_KR8=cq4Qs7_6E$W>5vc@wo!js{T>r_+? zCWx3_Sz6iIof%rJslnGy!g;H}#`2{+fF_4cPU+^7d~#*Ggo5Y+H^ai#lw|w-kEGWq zo-ow-wgpM6+4q`@CI3k~>NOnZcDmEgzyLlnz?C%n0k+klaFsb#N@UhKGdY0PN|^<& zHI;icB2~Ty3YHWB63~k&u^IH2uRzpJYvHTeWZgDXHb7q z_sxAbY=IR#xPYDDMX@ zAc?~{tW1CA-A>FIvW460_?c+CSkEF#() z-idlRu*T};Lnr2N94;gsO+@38!~2Oo?{RNKXFuy1(59$OKE`e1i-OX}6~V&KZB@+F zCULPPUAGQT-hjxztp&7!@RK-$pO=Z45sXZ!(4ce+3WWuSrr4h;Un!;4ZTqLqN<-GB z=&`sZPk9kXd#+}0tPoMPOoMUGUd6YuQ){(zzAE{C_x0&? z7F+r0oZOu=S$@+dW-(q;G3e3rhgLSiCHUmC!(n{Gc3Cj~) zDv-@L*;-;p5D5EOX}X3`qz%e`z`#Z&Hwg zDhCkFa#F}sqxBUrJ-uaY-uP~M;TI7dX?c+fiPMQzj3xw@k*dv78h5n0x~zFM)$KIT zr8q$VF5MotQ%KRTC&^3T#HoT&Cu@9Sj@O|&K&0wT7ej`r!Dqx4I$qEp+H{0o##I=U||^0 zC#;K+024R96whs%Q_5dyrPR9NqZrqd31Q6mWPXXGIdkln|05cu0OZC+1J|2x&0u%~ z_YByX<6t7clsL z+UT-ID{x}et(gNgMWN8;O2CBg-_fus@4A1DiI~~tO3#FIu+G@XUu%TRU0_tpL+!D0 zHB2K1>1W{HqL8vCJWVE{%2mazyAP->&`P#9N`+ z$gmoS3HhUQR{qZl0paxwNy+ZV0|v;b16Ald%&{IU>CyKIgiFnCkzKP{X<;h{Uvv`Z zH1eS^FS4XA*;U%Mw%6O`7azN+B0iER4Wf#B!(OI}F`ru^|D9T{+tdrE1BZ#)xqxI$ zG4)$`_7dfh;72oWO;iB~oWc5(k|=%#1^h`%z+3SY0$AzEQo%)w)4t49517%EGkEu7 z2yh^0P436TdmThv=8A{eGQn!A!IV%cw1MCMkegl{`=pmBu9uLCnkUe~yu7w{Rv?K> zKvmc44}G2|GGm~j;WeY$6%6!}>Fq{L|Cp>It`3V3$Y>zxeu__+Kr-V2L}=k0&};Xm zwu8j8wM$NK49;TnNMgsP@GWA7PhcqiGMG5zi!E<4@GqR%7RXqB!WHxZrbswC9PoVQ zeyOftIKsnxq%O$(;RZ!}=*9>#@CB`8Rsb1?HD6HUccV`5EK((BUpejIkn(!qB72JF zSs_f9t>nvXk5GtyXf{G3!63Bq>a{uaZ(f>Te5`f-v z1HNg538djkW$ZgzV&`IHJk7!Yj+{nhFs;ph_Ht_Ks8HHi`Nqrz+w^VNfVqdcX(KJ> zR6!wF=1Aa`Ecti#l+sq80~>P8BLhmU%*&G_YwnX^qmX(X(9SZirHsa_C+nF(XG@@< zY4)Wiu}lZcr-JCMF3s^;)!TZ{xDa)?2%gU4#@jx0rxvO|r%tdA?oH)V|AwnsIDJsF zbVLDh&vPx$V8fxhr?Kkfb^EV;JoXSSwp5Sf%G826wwP#2+Pn%WvWmuI45b2*j6=7k zc*xP#X$y_Ppo87g-bsX&?bo8qX>H>QulFcYVb+=%-zz&aq7z;w58?3jSp31qZ0k;y zh>>9pJ8y1?+{#?$Bp~2^AOaP0Qcf1Ew>j*M+&IS9)s;5A?rpUFR9+6odKgb`KN zkOj;9w!`4!kUMN>fB95EVVSdo8(leyd^hCx<|H5P-RfMMA}J9cir>Mr-MAoFcE%ckF}hGfE=iSq6M z93*e0?g+ygPe>laOHIuVqD{~~tz5pep}^cK>2%35>Zuj@b^12O!(F2Pt;3=ezg$ql z(9W(s-KR=#BFvA|bCpaDU^ZVZx&slV5BIhLAa-^X z>GHJol{|yb%~BMlC}>O;r+zga*SPu{`3a=xD&(o7~yAE(mWL+lI z{(27x`5vSJ@e>c&IfA+r4N>axd6gd8u%J>?cyrv90|dKYi>ToF3UCM)x%z8ix~VuN zj%GAPh{De#4tMDUDAUtjw=eN9klmrca{w{muqI^s(4QrlQGRESBvbtMUVz(ZS8LIw zIN1%)O^L+f?3w%&C4CH?+lG~~0(hOJrTxbwC_k`}+%D?%n_}tJ1x2EVg20gRpkcpW zn3vsf?WyG;)dnjjGQ{$dpw-d;Gb;MJN=E|u2{7u!PLU+jT7fLMAc?%^v2+~GQ1@H*Lh*JE_5O62lO%u)Z5Ycoz*^k4X{%0x3}9~&py zpW&P(gt>kGNPt^Qgm~jq>Qv3A%}zgyMMIEF!9KD4u9XS4mvD2?8wTjZz8mjESaE93ZY{r z2F&I>juC2*t@dUbO({yfl0}gTz%zTJijJBIlIksRSwZRivuo~Bn_2*HwYwtd5-vi% zj;T;2t~LrBf??@JX?uu?z65f+Ibrs5!ez^Bn3)6alDuGKSzoBNaKSvM|Aqul+$3C? z)i3Y>-fZo={Zh|E0=G#4QTcl_xQ%a`$&9<1O}BSqAGJS=ec>Nr+*9Km_v4BDz(lIO zzbV4i6yG@WDt%T4AbB0RbZds6M27bKFzI#|+l!*Eocj4qpfgD}ynWy1A8XkiL}_a? zV_e=nH-KuX7JW`Kt}69~KDy$I^mEt_2RKeDSjD$J4yoURe4mLR*Lf)nBP*jq#sq$L zENBE$w5#-^jqG!`lPY#?5o7^TJrL_$huT8Ca<&bR5Ut@cf%3LW*VV3Ooc%9-g5oR~ zelw8@t--7x7#j`( z!GN&fOc)ad0>VJBR46qO2*M&TiGTlB6U0eN#i#?Si7|C|x_nUmYt_CVkArRH^8Z`C z-dxl$@J$wXf0>Wmy}Eb(y@fjU5MLebSzPw}^dG%d6>Cs!^ZobK zAE3sIYWe@qmKX>9|MP$I|F`{?{wVrkefiWLCz18|d3pG{Zuk9v>i;eZ*Pofk5B@G) z+1zIK8NNJ?VCBa4aOX5QtKk-xe+Tf8@T^7_F@xH^>}~bn(+V{!Wjh>88hzD?o}Q`B zJTVxGB}jBHdxF7!0006sL7wbF7XN7Hy8Jc|w*dqd!zt?Ep3W~y?PpN&yQ8bmG4QC? z+ZZE7t{9K?cjd0FLGXZyO4e=Kz}B_gG3)1}C5I?Z=>So^5rDE1y~}#M!<+LBU`D46 zpO@NX@rP&G94o=FY4%goVmQk8L?CpiSz8aYyh-$b_LX=-ikd8B-I~$esNi{PqJpva z9xx3AOZ%?XL7|G0Ph{cI^WprUBQ!~${W3>iDA%^CSF+N0<1iQHWKHd+uAD!6)mH;Y z{p}+%2^WK^an@u(xw%VWBqnZ(fXi(2y&BFAf)G;GD{{rEQ|eP4KdH=Ze{zqJ>Dj!tsAVQ*Pu<35kv_N=92m9Xbw2S6jaqN=SHI*{) zgoUFCD~^o}u#jX**IlgI92V>s!V+w~Lc+MbYg490hLuti_5G?OtN_N46AQnwUXcbE zRP?A05*c-M&HV&Vq!~+b)BdR$>Ls=ysp;#~`ljWAoRtPN80Y+SleQEm&t024#fO#p z`R`uSv9*$6E3coh5w;HcJT5xbgCw&gJxc4e!Bb0-5d{JvAt*%;wW%d0OUk@}yQE!3 z(xrP#50~rKhM(Q$+w1?$e%Hs=-cME@@6?pHtIM}yL{NFU1z0c#qd%UHOPXsjm(%C* z)BpaoV~!bNl~(?L_xsPeofg&f&;S0v-15is?+gFK|2zNZ{od~`LC23SLC>EzgxuQc z>2r=QZhZK1e7Wyc)VBZT$1|S43A8@3wa##^XK17L(Zx!(zRW$S0916dMqC1n?=kUT z0=W(#Wsemn8K=D%t9hk28LGa&PtvgtaQ%_ph*ezb>o z^&C6fPxN!NnIhw4X@Rz@QoMY-AAZZK53jVp0Y(nrL#cp9_F`CC~d87 z8$m8SS6Cg>SQ(OWcn0aypuN&w75ai~fRPz(hYIBb%+gjxpg%d^{z=}Q^TO(_3N33# zu%WzGZduXZd~UPOza6klhu{Nwt$!UU4#%^AGZf)I?VW-f7Jw3PRsr{UE!yaHZwOnm zm6&YdWdu6vi7V(#gM%AY89RmEKjtoXpO;hW=S(_6Og;mda3dyepisUCz6m=GO})GI zPd7)G%zisWSu5NH!;6fJIfo0XYScrGFMZ8b;q`qW##*#XYQz8Yv((G1pgsO2N2K0>VJBU@SKb350=Spj1c_ z34}sHFo=vISM9B(-!~LhYJk&BYO2z1dpHlZ`_0=wN+ARV{2sqlZZk1+XheD%6Ql8422E zn*qQ8{uP`s77PiC0b#(HFcuSugyCSAR3sD$1j0cOkW3;Y3WP@cPGplcj<;DaBKf^l z>kF!!b1#d|1H)rO?f&)5@(1fKfMit+OOi*Ftrc_+s6Nzp$`PK+VJzI^3j z6COB23c7fBTSwpDXnDt=-ZjfCfikVRCzGjIR0qB?zrz)cB-CeHNTX^C^OO^CxoDw-~Jzm zkT{trjTvs2FNZiK7f-cUt@c#O$Ff;i4O(i<)kSBsh3|^Vs%(n)O>8592ZMV7IZ{-N z-W1JW%RZohn{-<*(T{dL^#ouExQ!@qbt9dSXGYa-c11@C($}TP@n1mgSq`AfJc}gU z+9%xiCVrIhAIZKpV_7(a#uw!;Iy^n*vR0LDew`5KjYF5(HKuG>)&{^L4$d7jr-!i~ z@un8e+aUh_i@3dO>2ONbA$EwtBeCdKOHbkF;+4`-c^M14P@^)KdL<&=o1FvI07iNy zsG}^`Ky8symgUz>%-o)-o~iMyC4gw0!?hjxJuALRu;Tz{K0HUU2aAdMU?!HRSQhUaO(kbn47H38pxwr%aqBu~O ztCV-wl8>si?-m3zV(spU7G|PP++!^!^$v0Z83f)C)eUo6vW#^_ym%IlKn`WPfNC5I zk*&SYR=e8SU$|kQOJv%}S%3Fi$CnQ~|DU?~20vag;I-b$3 zViPfsK!kMUQs?-)55@BqD>k$Uz+vhLJxORt-|c%9bpou@&U++gqHjI#giDXq@M`cg;JI3auHKnT?tk|~wN$A)EPe$@ z1~41i&$0)1)e;M;V`?%XY0`WA&rk-gDDkA@{TF~xZC|in&lKnD8Xdb#+YrW$#GQp3 z?TflSQw_}$z5nF^8neG(+qkPr+*v+zed{}bshl0r5;U3g(iNkZcd*W`TyKMw^7qMh zf__}6B=Zz_LbUkYMn^o`0R8tbCuMPeTK)-0HF^dLUO*f@z6Wg!eh;E$v5|jEm8!r- z2Z#_6q?-bQ#H-Ml#iq7UL(to;J@Kr$8|Y`_^DkOFC1XY8(k1r~8=qNp+JfB2EuD?_ z8H8>Y0AvydIAIyadCh8H}_oiML?(>7gU!z`Ock8Rpe2bHTro}b*Q;F;E&(n01 z4p(#jz$#Zyxj)4tfYj$kgSvE9w|}3K+pm%}s6De{9NL^YC7e@?_6>_e8~KDAnkPkk z((%M5+nhZJfE)$qqyYhHXn<23Fl%uCudTE`WTvM`^zUWIpmna23XKciLm7Ez_)~Me zc5FN*i8{MbtwzRnqiU?C(ne$G#&lCRXrp9prCH^;@6t-QrT&>0DxP2PsywFLHrWN} znjznz6vb7@j&OWiZ8o%J1m-|~(brc&N@$u^FC2x>7rI8VLF(J_@n2lIy^6llHZNzR zE&`XZ9<})Y1{2rP^%lLZS^m-ksYZ-d;)0fdVC+1=MUqf&0A^PUO54vkvkW?2Y}Ms? zg=yZ0lRFx7%=&2AYjLc#SvLGAp_ocwVuW+{p$UFQc33KvC8U#IN-NDMsX z%b__c^O*P##)7u2fih%eHsL>9&~I3H_IHx=a+buKJ#g_mE2h6r7>lR35k#tEPUBG$ zcv?J0-rjfjVO%Kp#q+{bhq1`GlvRH^kPU0B-1cf-K-FL^oe=k7^}u12ZyU+=c7xX) zm2T@^6cD&!hAm)wEwLF&_Qx|%x!|HfUZ5<57)*5NksaQ*3(4KaB24c5_`XlF1BB5w zO`KzoLrMPG9AOt=NSd?FrOK=h1XKhM5 zQ}N@LpubHW-kdpfzdieN1-k3R-_J}B50fM+?pMFU?Q%8_P-5_K-x$SZQyrNjco9#+ z@Jl7x6vzKsDGAaW`^bAfu(ym6W>c+Y+ z8NdOV96OzMr)Z?Oefujf4Q8F;yB{&3joBYk{HY zT7u7%GZY`PydecI{(m?OdIvg?kEg0XJ)mcfE}!h?!Sw!(zPo?MgA@?dxyNp^=`@Ij zoyTdeCnhia6H8InQY zE$w(ohuQ9H!#-I-)d@9nc*yz91keUhJ4ZONvpv_wyn-N~6EbnaB5%Z}=N&^Y673fr z^_LO4XMUTjW*eSK`7P$sr%_uby~R0)!Lw>w&mft!7PFXADz=pv7Ep;inrP_^H5V9= zyUYz}Sl+ALGzDX33UmM+JUJg7_h`$g;#^cg^mCF3xg6xb<+S#6|AYcVWQb8-7;D&T z6$l|9R@TfDHQq9}_#HrEAxFOO^B(qyTjJk_)lM*P1nTWG-p8O(G0Jka1~Z261}>UJ zc~)$Gq@dQgeMAA@b|fPEkIVi$1AS%;L#Og`+#xtx0P?D)kAhkt=329Jz0feHqAYmZ zX6`KlvtgkN{|*|`p-}+R(#`mlW^klfoWA)?i+|PejtzQIZ%$bfu*^Xhf#^DPb3igl zJFYKsu8@@ zGAB;cJD`?G<9N5gnpnZD{JbM2Rx6}1Tk>{87n=u-^3F%NE=-3E-oRkNJ-%ZBG@)c2 zRFH|A`O>4k`|0Xgv9e?A4#+9^cIw{NWm?p7tz}&04^-EV1LIIK)t`pfHxxWW5D2_ zI)|;MBh7t!F`l(K#x6QqDmEYR1t~%U@>ILDZZI1w1RKbD5#!&))db3 zDdKGK&-lkAg!=77gu~;Qe9{N5rbAM{?i>|A;Uf$)X!0X@7499 zg~VsEq%I~YuZr;0j}KZ{{P9)<{R?D~5SClZKLv9f%{NK)(M-7mKiYBFA~ICd$P_#y zl8B|ZA=rDx%20%suoHHY@JXzvWn@Hr5Gav$oL3eeE{_T^VlDH;m5eFma}-Q4>;S%_ zU%tSt#!RkBv0}zbODgX_$-zX_1Z+OqVw=S%igPezk)t6rJh6x@3^~wPSQdI7y)FJQ0**}lwseQC~k`#!RTcmwZre+G0$C^hx zCHAxs6kQN*FRFF{Ld`)Q`dRV8;4m?D!+@$xNN(Hp&nuzHqOJALKBEC1rm-F9P7N;Z z<7BHjP;Z{5xWqME50Gni`?uouO*U*d{E5G9-f0oyGyYdm=1O8PdNQz9t|@04ZECLSfeec*?9CC%KL%ws$39E zQKHHD>%=({rK(aEVNt1|3##Qp$`!Mn1yar6Tmn;*Jl(4P{Zq5AUUY&~!7b!niaf(F z=GVz$T>}>y&7p^aiD}i=+LOWoOW>e2!bwZ1M6XDmlJ12x<~s{c1}i|r!hFLsIi`lQ z!2vICnZQ!1va+uMat~KcvN@KlnEVFJ_I?2}=K;L`Lo0U{*2YL>d;LmWYUEh`+^v14 z0vWrwnhz8esqgz>uk#XgYeKRhv*N>yl935%-yw95-W`p4pgBgafU#>GI-H}Ub zvp>B2CPvnu=PjXBq9R7DMt{JkYep(BMkKAG)cQB{bi3JF@;Aha{y2ao))w%$-kd`K zRaQIFVcgW`lng)JJJLvhXZO8fcOz4t#A^yU+m}r>WX)36U!X2&g!MK$h!mVF_Ac^7bx?J8oq`oz}_`HbkX_PF@oFM!9l;W zoR}g3KQ>PO76Jlr$$>IDp1*Wfy870WeszyczZCoCDlkW{d}nWu$g=M5r}tOj`zRLw zlZ}iAExXHU9iz)}JZy(|(&O;vB->~5SO2-+^lV&b)B~Y&j^{XDuY(H^9z$HRKWedb>FjIEcG=T_3$M#?HAyJ2c zkMlyRG+(+Ru2f+uoM?zg!h^>BfsTIdV0Sr=j^ig+K%^N~G1(_y^qLg}?$!_q&+>t+ zsv?@bJ<`#-k^n427i9=p7)$w{jiG40k^lJ50`V?PG|fGRc9fLgqnZ{@o=D`EL z&S^MPV!)?F-9GOKNx-@i08dn=VVn`M8m^ErdO_U*7&do30}@L&y_{$xGC8)qJL(CY zFCYN!t1$;u8H1a6!iwwIcxxP{s)!qvq{Z9rp973fyFF9e;0ZlAj%wKG6j{1YTEJB^ zTDGV$#1*Y$Q(yH`ib_J67tXuflm?wSV^`g>+Ce`ZhMg>5dBP9yFxKDKmKMZqM;0?uG??o{ z7}>$H5@Dq#gr9aS@9P?~>E4iax9PIdG|-q@1xcb3&wW0{Joh2>kN?VPkr=!i8x%L& znkB6QX|~yje5POM!2h=@PmODmwjp{AQyv@hNC)`#_CDBN4@#6KI70GbnBjc@027V0tEy2OLHQhEho(_e#(b zWbwLs!r>h&ipAR$ja2J#0R$D8E*1<4f}vr+Sa22$1%}~(uwX1D3I#+$5fDU16$p{O zYkg}sB`*rJspoIK3Zy| zt?5@Ho9$aazzeXkew7zDq6xsCr;zcZARz#{--?th`GX|M$NI!!RR2p$0kM0^JpXcJML z`?EQ^!6gARKj7R`{KFWCg8VrPxQyJ_@HZ$zj3xd7WILwEHNjMReZo}%o>ebMx2rC? zKXm*}`WiKOaXVu+3WZfza_tI%Zd|G)DJS+XTzhxlRz){F15R+~<^E&(`!{IMdCI@T z9@VDer%_8syi|UuW^TKAG_3q3;Huc7^_8-<_9;X;zv`v-Zm)No{;|+3N8BlDmU(~&F3{mnbB3OID zqbqs5KVU}Bd7BviHnTm0H)O#`5GKqk>d0bDxM0BF@=*o>0N4hz5gX1{9 z+Mv>7$~U3_R7gF|?{(wM6T6mYCxEVo;cG2-XvYe#?+TNritUJ@P6rX7umA%JDuCU@|Rh-_ljcMIA1vS-|=#H zYJ)t#rV;;A4_(n8J3=}HXcS?N zOsU6CpzF~iEHd5ZoOz=A>aB&~n|-=}q>6o)AwAq$@q;aarTE$*FjPR)mu}0w*&&Wu z;Q6y3(OK6sFnth>!gG?k;O6pS1yODvD5EIuMqp@2mr*V2=tt`=((4^~H_<<0940l# zgC*jZ>GOM9o@SJs+D?P}V2GnxH7E%tE?UX^ul}~w)<#>o=ROmATTNJ~^{HMxe1Y#; zLw*ipbAL%mKOi3>2sh8QWQ+niA+Ul}gBouov)?*~-8^s2cx(;aP&NMgVB`WgL!(v4 zc6{v-ULSgg4XtOW*Lcnm__Cv=!Sppa)*qr_ShkS4)C7voM8B&-rcqY^a4`mqRC~O^ zQAhaJCn`|yNLpwE<%a_#qKe#FL_YYch)@{s%U%RhefR5OVi3TBnmE#=g62d)JMjWg ztWH5!+M6Cmj3WXDmMA>9v#*%|2X$@CAz=`;>9sfp5n`sr<~KDsqdyq#2Bzm|aLpTL z{Lc2B2(W7X=YGwi3 z6`|O7*__Q@8cXYOVK*N$x98bs_J8y4TY;EZy;Fw%wcZy;F}K3z=ybjRt3%^%U#qxf z%sE`y{V$^0Grp=X!?KP6;Z?Z!4xWz>oOGRP?K}B$aQ5^n-zUS8I3F?1KyspPBe=f9 zi5{h1_r8aq-+$}U0Ah}^m@pO;1&aYe!@Rr z^ZWRIe^!3_7i#&p@A4xpQt`*{zi@a_;cfo^#UHHy8(z1IeWXhj~Y4jqMEUJMyk zm7(zQ!^Ah9Pti$N&0@nx$%m0 zLiQ_?F?L`;cj4IVP-17-oAPIN-w(8h#UV9Y_Tfw4EQ6sydBBsz1! zYueSTs3ST!o>5(DO4JAkfr07{g)~7sk_y&Iq^jHS940JjWYiU<5H8v>WGyv@(}LVI ztQ(q|GG@~O^e$p_+8U+6(+1Z3)*S*T350nP9b37epX>8va#4hsH$~h?iTW;35pJU7 zDU=KKDk?X~$@e^+_vlhUsRWkBQNT4+Z?>C0kMTY4NZzXW)eAwjgD@;Du@naUQpFN- zN8eCL|ISKTgFFm*(N^~RmrxVB_Nw5{DI>jJV~L2liQd@V1OJAn#DORdmd&kD=F(-X zDxm=99#|xQ4j$OvI^r=76aaukX6JYGf`JHJ0^5m+6W@Dvd@iQX2&WnxaSV^AQ>&1p zl{}c}2G86z@`gaz6lDebKOOBmou62PQ!1$=2KjW}Ug{WBZ$+E(v!cTeKXkv3x3BlX z$en;Ormf7%Rk%5kHg`05=j;*{6zNP$d{XR8vhq`fT(S5Ee52q3pK8kh>Pv~;H54@> z_dwL{eP7(g#l=n*88teOPoZb(2#Nyzo-s`5vPpS8&ivMHt?y->=I--$) z)~GiZQ|w$T6gnBYY$q6|HW@3)ES&M=x!Z6_Q806EOHQY@HRa+&pjZ*xU2nEsEtrM6 z)}i`Di-_@|%IP;?H(I2=lwV+Bmx$$syTHef+F$Y83&(g3n-;vXLzY?F5qeb5+E3e4 zP^b(N8+<;$2ZooMN6??MQweiC9r2xSX0i@Fd*@)JpUXsrk0BDAy$c&m_5#ThDJ@E&m5s&N)7ng__OB1zNvx2bK-Z9VQXN7H{R}@eLTb!h0%4{Y|}JJqbh~3=H(Zh||Ir9xSTUv0Rs6 z-Yv2Pg*WI-tntqv0RR;gFg8>Lg8^W`SdbP948lP$s7N9s2$}Pmo6RorZekKk(-L*@FYJ^IRwE~%@Xe*GH0^;8*AeW3sIG5mk_ zeMz7Hj5+Y9=nu5={hi(`QTdk+^Wt~4uV43-tnbBz6np@=(xik-q8~_+$<7?P(p3e$ zctI~zy$}>Ag(DbiDPxC?AVmbxodLiA00RjDpHON--!n4t8wD``lW-8Ab>jhC-N%Fg zs2e+032oxW?Z(bw-0WhLNUyROR~OJ7VIVjVsFHj>*0_2zt9wBIg=LsQU+1?LVGAfB z8ilV|jFDmZA)D3T^s%rt=8Pa)ZF(tzz9+}$%T3ucc`(!o%(^WV*xwZ_Mbq9AU`Hgg z2vCtf7_NE7@Z_wW;yMkKH$@-J_l|=~o;Xpu!>6ab9WZN$;U|AlIrS*R4|QKNIc72N zl$R5F=8@DtB>ExT(jr8K5p$Gu6rFLJEv1GuU7N-BlUUfqOrgewX0poaCBd z8bJ0)(1v$qPRvDEvQXiaqrK{Jnx}plEup;LBMoZFm3P2DX+76qz-4!sv(jlt9JT)s zs94mrg)_~IsFCWOOvfCv=~-TBpfEY4j>!}x!b0P=O~@f;7*<4Kslgz>ngvBhhS;%o zfW{rw6=2aY#PjAA{gAES8u*mB!(-|{ADL~IL_+WEJ&HMz?Zv@JYap>@yR7k8jo+^5 z#;pH%I~+fQJ2u(!eC#5f!v~-aNhXZfE7)5+i0@NBUke-)f=b+?c4Wu1?<(m5m!2V} zRW+UYyiU(Or8&W`t9r;eT}?1*%OOj6YsB^>>{YO+GS+>>m@jL9`1LH4lSoxg#Co~_ zowCDAw3U`+y@QZ9aP96NH(9aF*`7lhhYQ1!Cv;y;E$F9o1$7RwHZ;6HFe2B;#65Tm zvoOg-)M%570rk4zV?PFBjvC8DgdiXG^tmlh!a+nrT7OrpGJU5yi6oHyJpG^>M0>SX zn(5~f#!>n-8jPZaSWkYly=?=CWIDG9hft2oZ;9V%f+mzSz3h6)l+<}<#iuq-!ELnL zx2uX_a33Wemo@~V&e?f(C)B#js(c&MD5(@QFA1;zE+@sUV_)J|5((XHI)*%_oG>O6g@XZbm{cqj3W&m? zF$jzz69|d>bzAuHFP&9h^Nd2Jtz0u%3(hq{9M50x{7dEHUYqUp(7wN4jUAi3{=~BS z?)%TtjaIxV9=mO=H- z*MjrVw&o#c!FW9`p9$e~`v33U-}nFT3}x)j@#C@RfreBr5CjyNLqsL||ChnZ$;GEI z;H@|uxFYm?9rU-Cif(rItrYVsi_pa9Zy7ijPr{NI^5?pv|7q)x zH~~tc-~j{`6f7qU1&ZNfKwK~;3<-#VV5m?eBMAh;CwShojiqaLW$w|FM71w9GQNrr zw||?>;QW00j$L0>qqnoKcXOx7@%=t-sFChx9u6^w+q>< zw|45kSK)3w=iq^c;em%|KMWV+M;_~Q=|>#1wyZJfkDG^=mk&1};m<61)xg5~r3+#9 zc*lSP1!YLx#f=0(`F8R6c=87V=HpSmt2}JqQ4VS;c|7T-Fo{$j}@_g)cO3p;^AX7k(_-WOWoxoS6l03twZsG`gyIY zA_5qQka&M4)HYhVKx+%$I?}DU60I*jW?&H<>|@}AZLQJB`Y`)(O7sm82YeL$x-_Yf z@n|EeA+wPVmjs8w6leGVfuTLoof z9o(^R6gjgVshPy)81;k1c%0Vp#PRv9^O5NZhr2K$4hXIE)dTF**vf^0Hm{8lMyH0n zGakKCJobkiVBkmv`d}*>Ijm}+_&q2O7ISjU~2x*rQ4-fyd+IM1?0Vz7jDYN=N z#pi}#d*|+`UxLpS47-ntI-}+>@_G;T1*q7!i*vzxM6;dSw;pNc2YgY^m{C?`Lcym- zB|B`p@PvG880PyBM%@VMXA)>ZiNdtmaJgLgPe{@npEjafNE!xYCA+y>v7{@Fle?cl zveG-p#ofkx={<&h0!f=PaYB9(Hg%;T&-VF=*k-iTDEfTV9QJ!kD;3jfe_lp)C^MiT z7E13b*DCDe-^Fh?nxd*Y$*r~S^T$k*(U=XCE>s!8acbV>dMHv4WZiFF;Yg13{$Vw4 zWV6*zRt)Ob9#-FD#;_wSBtV(3tC2hmDER|_i*WVnNL7r7E8uG&Wl;oXkFbGU4hN~? zB$SganhXG;4EP{s4H5wH!=V~;+}ZogCO^x#Vga|{iS#DF8ft^8jZXfV%EVH5gzT9M6wIT}80a;!qv4N8z$t?d zMMd3+-zZ6kK^Il%WxOd}8_1L(P3xb8RWFhwB`iDDnN>tt`YX{Ipj2M9~m6akr)kI->x?sc~6K!i1L6< zXUou_?C^{VMdd)Q@#Bv7g{V2hPs- zGn}UmRxuEJOPzZs3Js6c6EE4(0m5FU=V`QlfbsfG$z4&;hMy(oki7b=ni@7v>4iG~ zni>NWM$@tZiB(CEWG`X5yTY- zrR?u2iWg4^K3M$U+dUAGT_>5~UzvA?x*kjyzmebN)S|%6jQG-{v%$`k@QtF#DJ;%; zU|nR!rT;etEQCx*eE6w+pvyh|e%i`X7=+bT^EOfuNhWM}C8f-dC8wYraeeT3CRg6p z&IDcNFk~V@iwwnNh$0$PBFTAIf-F$#HGHg@xJuU2NRkhhvK#3{2v^APhKx%bkakyI z>DC$95@oiVY^F^A(jjhv_ZFzEFepXPON3vD;HeeuL7cH74QvD+KG!hnmGcRqZ#f|c z@~|w(`NeP`bnP(4AL>zzXVg@yTNFbdr^7iDIX$a$UzQKAc*=aoc>;CCU1VR9oA<>6 z<7auuYAXmaSzIQCLA|QZ1=e*qxoiD^Mx9u*Myj!!SwP>vk}Wq=0cj)#F)sqJ^k$zE zqx2tHI+O+m2;(t~77anaTEc5%vf=n?D9rmW~cH+N6$MJ~;Nz{>W zqUVXn)^NfLpxu(Bec$K!*EiShElo5d(yDKx%X`!yVHGI@lPOc^B{Q{{4I-S7U;UW4VB!9 zf9cjxH|LwnUnh1Y7>T-BBfo8?_YO%`P?_|44vU&6arzx~GMRXc5W%zf}!)vG8f=`PfWOTPXy68xS z&!&2PhYRx@YMrGaS@3-##gjk~{ryXI$~{iWPR!8(p?G8=k?nISO)nMh(X_p^RNZI^ zgNS-0)}hsk_f(@sc|Qeq8ZwsLR2=RZUxlD;7!DL&5(-JxmG|d4*lBOMtD^zJ35Fqz z*sN`FtEwW$L;bNQ!a6$!&1j{P}V~4z`9rjXa9t_i#+Ap>oOox@wLE+*>=&A$8>6w?54U6olcl( zB}Yy%It!*w01HN`8wL8;qdN#;#-2c!bwmr6b3sqt9!O8=NC+G~LmJh*&*o=DF)v^s z2hNKcIIVg#THjSrk?Y+6%jR0y?0&lg`geX7Cx` zQ%VEin#>Wfu0Z8LPsSZ@+V0t)Nbm($O=}CSmB9mI85%=F5y7X}CDH5aCk>?bb*XST zqJus!8AlW)(b?!jzsopOjR$R;yRkxeIDMUXLJzV|nZJAQ9;*eoZWirfFsJI&O4_d# z>G6=M`yJhO89hxeG5b6!2GrSbmLfLm1YC-6$Z6MO2Cy2#GM|Un%!Bk~a~1Tuy!YJA z_LC?xqu?mnG--!D)c4 zrnlx!cLA5*L%&u~RKdE6(<76*g6W+}!6pAvvI6{IVv*nSvru1_7N=l};{FU%^$*q4 zjw0$T|6BktocD6hKJFcK7?a3XG7{atg|V3*1I+^91bZjcLCocjn6n1VTdNUl~N|f z&NEXaX`O6L*ReGNK@W`(h%eAC5?rTf;ubUVGEww-q+h)Y#W$7%zPqbR)||5NT=L&ERNP9qd%s#0+U5k z5FlJK`x$IGt&;kV#_<-~>Z3pE{22~BF=D^L@GPoM#ql#NWe+C)+lsE^ERo zcVoVyC6+`1_>vCfvtLzOd}@bz8q%C}LNcgNk`(HiCJ@YsVgNtA_btF?zKWkj&11`w zD_R0IA5`iNqnJMzA()53?Bh5t)03PXkT2j(1S$a~z}gU853VU!VJJ=}2&_3rIWN_H z1d3W|!Y<6WeIX&P+3P9NI}L=~A3-quqHhtBBj8HRUi97{^h0QP`sP41TXNIleH+ou z`8J+=nZf0(?P#R#ij)uL!>^=rBqLvCCrW^q!odQE3#UHH=;H6-IsTPy`en72<%oFn zuaSr3qehtVSfoI^+AEz0|9kN6dUZwd=+1KDLr!Fou?<5~i+z$|g?`~$|C^5F$}Y&C zW85(vZ&sAT>{cFd1No~p;n%6+Y8gFRiX~sIW>3c)u;yt)KKT#iFT}8c!7h52s7zdP za44kWUrd||2)97qMFW@s739A>`w-dJ1khFT^nHO1?ZIro43n#6`Y?lm*s-md*#u@u%Sfx<`=vnOmLNXzhl7`D`hFsx44k*vR}Oe&b?65nuVAd z2Qr6-HA?#Cx!8RBm9KN@&k+^DiO73^bRm=@fu}}(JZgRGN4i_V#>n9V#e*q<9EP0>NhF5=Wjqlh zfqQaikeoy$k~5}!APeyQAh}T_&yavwZsJ@|EZFt~vLd{=F;Xb~_GS`T`%C?>bBqt9 z_G5(9qWE}7=)uo(-EOugA_@WhS+gwM@sQ97m_jq$_~ym1I=v4IrE}MI%jy3J;0Ra` zK)rP#3uy2V`!R^5I_&~4TdO@PfW?)&x)y9O9*~YK@s$U^B_Cv1RIAUxO5T|t?J0XT zw4_RjK(3&gGt9Jy-6H(jMQ%gdiE%Q;kL}unYWKnCVP>&PM%4YGFZ=jDu624BIBJ|; zNg&uo{g^%Dw^4XFz>nmBv6+!R`mC$olnEz0n0%&9jZKykw^#n7-J!an_rOZSzjD;c zIzKvl1zKjfLh^$-#H6q}C;{EDK{bH^2o)SK7Aysa0b;a2-YcBjy#edv8I$*7Ee1v~xITCSG{9WK=YyGeaNO^=VxVqL< z-~a#v`az#_MHMV5|IpN|HaE+gFmP=EQ{0)oz93>14pcjOHkIRVZkr&73s^xt#z7JZ z9h?OJDwUqmHf?{gA`4T>+9EVkbr=U@{RV17mL{J}1kH%vYhgJK>QA40NQO4dri32$ z@&20eK!og0gz*>`<{T&oXw@^``MZ~VGhty6E3@A)5?u224EfA<$W<|$x`IbZg0&6_ zGFL4jKUC~|Cp977kgPxACrxq4L1;@~u2~B73ID}I3Zwk=;gJjTLYe$T&w0q!kKZps z>~f|7((A}MTtHqr>)ccS%eCkmgUg&|msO)fOg@-sP=9s!)-i}DQ8>%+vm;^8z34NR zW_$f6sBF0TI%#PHc}bM3L^(t$z`iF|%p+HEC!<~lCvTXL7tO^zUDlA3&inQTx*x3O z7c{8@6>%|YRr5C5xjsxq{XJODjNy5pHB~vR)d}SFGT65Qf0#$YJ16ie?fPV+u2ar? zQvBY{&*_u?z2~a>MAp~gb>A2(UpTQn#MEca-{ij89s%GbmiocOTsSg!D(+DS^ozo@ zGT9mnVhHS=8D+AgrvLK1MP%0>iCONa`iGqV%xVcg+&>{*T&MgO!ETVN(D+QdLs)8) z)ra&EFu$pU%BgsdgDjBB5GCiwmBEv0C9d?pI^rCh)Wn@MdprA;lGf$G{_O5d<%Gx< za2#86pEez)ADQq4j2lTi0ESHY(Oy67IUQAHSPibiJmS;*NU~Cj&tFor(P`*T&X%jN zf_(JrO^%4<&Kv@i+HK9S^bWVc0{~B}!;Sr(T5=T^7&ILO&@n#VnXw`WLYP>%iP0MQ z(@{zZs?e(UzCSbO%4maUrTTEbe)HP2_gV5l$` zeDIz9(Aov4^xnq|J`c$|?qI$=lud4~w0Fd-zqiGW#JPKfIwwi1Zu^?yS!XYSON#8x zZrvZEtNEillJ-i10J6To+#R>UHE%GktOMPWM|J7DC^NcnI}S&&!J^-;pV3 zJ+zv|9Ml?7ZK@T6)jD|q2TzRX)rf+VH2gWvuMcYajy}XV36_t;CL4o}mA$dLPcG`Bu5PS>5K z;F|K^bC==DFH5FNFb5(25iA5+e3PZ6s5Kkb{e-%^fvwm92o<1gC<_I`#elG2EEp3V zNP!@jL?#gl1i~eFy1mJF$$3dvDx^4*7rrsaKGDDqpBpo!BafR8#BTmO-uL60Z!aGv zzTYl2bm@)lIt?zJXW|d4%>H28x%zESovxeFVTzb|@#D^qHW*$V^i{^*y?uRSUyo9`lb5CotlCFN2`x z&*U{6H^bsHAz^sW;*5P@(j{4Y-{|f-cyz|+x!`q+zyJUP5&@r*bVA?I^OdMLfkh|Iscuvktb7^;N+(*9k#E@xm_G30S-@S= z!&I|+5-W|B@NZBmY22zFgO^v61z_9a$9+%;8r!?zOEAuYu=3DMS_ z5WA%t97=qxcGYs@^g-f6V2M+coe)vAdn=`FPVjE6aA>2+s%1*gvC&h5({UuCK*c8T z#22in0M&rS*m+GN?I5+E?RJva!02p;1Z>U@r?`l{AIzV5x9Xd4R2HVW&ouqhbTTL7 zNYiE6$#m)tHd)l1pn`vrht+H1C|esn!$+F4AF-C-cM*xy(Mz@1`dEf+`7K}t(U|~& z0WzVt1X9=*O!pC6Z1?A-|K0x8ms2+wqp98utfq{mXP8{6`)O3oW5$&+?xsd!*(~aH zZsHZhXf$}!wS?4>rfxD$FB?Sh)z;VTr8>6%ZA*D4-!&crSB=tkq(K z6)po7^{C{3e^;WLVJ{j*glcnQC>1dNkBO8w8bdDwj8g7?C4e8U>?IYD=@l9aCE~Du z*{-M5F7dx4m{mLHWUeu0zkl+iX#aG!a*>IOd=Bqk>e(H&UMci z$X2LpdXanMFMMx}`~sfu#Mpj*eUIs3()5AP_V)OPJcGZh(da$9IC|HwGu?mN^YZd} zc=dff|Bu(si)i}%@9VJMb{JjGd8YsN|5y21eipfI<9ZfhV>y@}=f{9~gO?vK3MixF z^Tyj~zn0$V=^?_?sIWt*EwbFC$9Ena&yT~GGn=0RmAU5Q!Dsk;-nxCn9_ZF&$L42r zdxGwQ>cE@y@Nv@Jak`O@C@ln2r=v>RQb{Ti_Xq1`bIDT}6jgbWsNewx6`W8;TnU8% zaKKn977B=nDtNu>mBuPjPBt#JCG8o19xA>I4+_4ge@9DQ(4f7C^UK$V(%-XuuBeoM zzvf?Q;hq0lzTt&jgZ_dk#YZTm1YryBH zd7A%?hueYp8@JvT{Jt60ejZ*P9$@qG0#pf&0ZDPkhetkDVa3Tk+_>_)p(7xg!vB(L zc`CN)Y}s3rZ6$$njoDG$N7+3Tli%0J8lq5{c%k}DIW4nq0p3J>#8yxQCjw*~86XD$ z00LeCpOk7s-`D*zncJWa)vzwYQU7py=YJ(L$spr9Wppk^Q|&(UAhu65BcL3xM(s92 zc-D~R2N9Iq$F!yTpEhvw@8o6x6h>BpebjCQ_ax!r=Khp9Vrpe5dxOx*9_twPe@z^A z1wNZYCsO56QY>9jQGZ&o9K+~ zkwz+P0@rMOn3r*g0-E&jB6DRf8rYqS*c@lS=VyLbfaXW`eXWOX2NoJdeV}^#ywb`Q zQ|UMX7hS9Ui9_a{S9_@`5xFt-3-QyagGSpE)yVP4G#yRfNZ0Vt_PLC&yqWpEJ*&UeYt3WsB;%#o~&=A#{twV6rLDg#no8qPZs$ zBo0l%n5L-~hdBGPdPd`7cMTM15^lW}XY}XFq@+G=dEJFR335owZP`DY0r`xaT7`c* zh*yM-%CX~nN}#+xvz7`Jf|ny8F;}CGK4%*wX#F+ z`zdB_Ld~-3VG2-=x&3HozuTe#N+j>oG>OF35Nk;I9N~? z3I+thfVhx`5)hceBY$mDRJB!^NQ~8KS9pmO*Aqeaw|l(JcbksSzi)(YR+c}mzeSw* z{+h4E6WfJfufM+9JgvKT^7Q#WtL)3aGIKQP<@mq#g}?ki`G4~7zXQxP%g{XA*{Fy)FKrjdS!qd000WRL7SOLs6lL*ObGADkLZzbF|e-~lOtPcecgwddF|e| zBX=r_prfvREm>UZV}(lN^*54^Tr{}7N=@ug@@sQ=cF^t1q&{hK_SYO*f@0H2+Em6t!5BvGAs3Bc z9;joVflSmMukO2li?I3TW|0T;r<>9HC*U_@`@~ht0SWW6D(U5{)@-oCLbkpu;w6+$ zE9;;Nhe=WeuqBj1q=9!lo>+ioQb7+-C&F*RpfNsVmKqxfRm2R%ECA>rdZI#|F*O@s z(+zW|ooRS2Qm3?3Vu#ILz{`1djy7ZcnA5Ys|d|@ZXYCBra|K4v|3` zQVsf^hq&}vLx-W=!^AfQd>5GI#OaQ{XL&8A-AF$9w`-@}#8!-hd5$#G8J2@cD9ZpN z+E?*yxBTHUX+9S*-6GT$VS;w?7X^yBb1J<<#r za4YD`!vt@;J@m5wH%_Mwg_PDS9CZAo$9O(y>R?d?!}DS6!)(b4tt(&J0XI)+_N)@h zVEDgrk7vw-B8nTNznhv6=6_Kk3;J4j7;HydzP%UI~rclMHHXcva5=g9-ny7!UZ0Px4s$YtyNZetjyzUenZJ zL{>=|<;X6gm_B9=X{o~vQG}H4b>%m)D#3QZap6g!4x@dVjWXCd9v5+PCM9w=e(=4G zsVS1%+{Uj!fgWRWnaoe3rKH8oHfcrhgVLkVkl(qwF>d;yg?R>(#Gtvk#`NaRere4 ziF9>X1X}W~@>ny`AaKN^W3&UeM3m0;sPxnrExpjeQO@1acOrUws$At z7|HmI=SKmr%W)-V)NCCF8d5DpJSl*GqYIcT5L?5r*68JjBFIVYB`MM58jG}`m6`AT z1o%B>gNgrqbVy$0p8w+gucm7g2K)T7NB@rmP2{FZqj8P4%4(Edirb8yB*4@Brg$}2 zTZ-`rh_;<7SP9|BMbkfLg(zn804cw=OO(G!wr1MlLc?dBje{C2+Jpt{&*R>`KFGAE zk+k#6LoZ39Ay}_Sp(7a%a6f0}RmAK`@h!k}0k}6RYFKn|PtdG;v)klEix+U30oavk z>zqG-0~@QiYMS)=g;F7`m>`dEJ16TmY1pCkv0kU#pp(npj zv*g4>EQO>S>Mn%=iiXr)iu7-#oS(1t;#~YnEVIeMoaPRkuG#>r@w#0op=A%qacf&G z%k?mOvb2G?Cec5%R(<96%AjS)1h|UzXtgjxvwnqmEo@yhX-o@M)q~+RyNM0XxQaGl z=^oBG)Ti1gjZrC&0pv&`ajQH$w{EGDnYzsMQMEhc_&ze8P(&@pv`g5SOiTIvtP%V4 z{NQ(ia>RgI36|k4R8LJ}=p=Cn+K78QLzb>)&AFZs>1&WG7NZ^vvN{L04b>t zqjV#(M$W;J6dnL<)fhCx4A0?5hXa^7=s=@qDctM`;JaU_Olw))B&14aJ&qz#@-;!2 zG=xjoFgAZ zcKZ8Wb#9S}OJ#9`(!^>;9Lb|Row#Sch<2m(njR@#(Y8ZkH;-M z_q}GE$b1G)VTqj4Jq8tdnTUzE-_eO8g^zAiEx_ZV&N+qhUs;t}VVtlkXtezl#g&(B zd#=NhpqxU6iVOBU`6j{P%uqbn_|`_h^5W)m)c;XG7+b<#&|XPPT>5B=-a}aiexR?V zc~nV`m_Cz!PNKEUh4~W*_c(Kld6O`N$-mJpE+IxEk>lR-TFh(5s-bGdxT_fVU;VQZ zqM~GEYhR1|DSnyR7omlhvK<@f9ZTK`hk{eqa(lKg-^2t1k4W{Jw#ycG(fwr-sT0fl?gJ1{v3ZDCe$-A!D@{#Rtzw}cR z8-zq-z9jl$MDo~{+$=fWn;b(*k8dc&`mUDKfB6#}3lLu5JpMU@B-m33nO{h&@fHC& zSYNdwHBba3J>0p;NbzaH;#1-VXLUG38vLfxigo1chkkMz#jOff<@1$3`8Qa#4gBNC zg~{wDj06UqXOhf((;<2VNI*{@mk!^yx&*w7BvD6+{2NVEV z6X+%=Z9!LSK_`LBzALe;F+ zEgJ&!I_!t`JCOuMd1P(f{&g(l7oD5o=T>iuHrsSBoU@fUU^qNTCptqr#9bfta4xl2X$E?N#1&k( zegZk~%a0q^&n%cEP1xImBXfN85S?9Mx@X}%x=c`5F73P=W9C6fJC!j;h(~qd3<;J~ z+O@ot|AgFUZGSDIsCq^U4JA46)FJI`t+QN2F`2 z4-mm$T&k?an%wZzO@o3d>_u3Mt5MVU?ij3wW{be21|4sa_uKq|6aeRUo#75|Q19^u zdT8)`YFmK!WHMW#4O7b%xhie@M@NSXQpU)%ccJ0$#C|?`xo$NiJPn69jGjXz`408E z4(s)W@T`G-fTK>=pZ<@my(XQbHT=TmWEjsEUqgkSmW$3L=-_MDnUE1(dz{CTj-3+I zFO4soaP@V3WgF)J{uLZ77z+*p!GN(KEEppN!oq-{R7e#P1j0cukW3^Y3WP-aYj>3z zq@txoC27S%qUm199w%mEOm~}q%~f7%pm@-_ z(r>xuH}3c>?B05NE$5B_*_R~txf}iV-*49+)<0ja82#wOCK}I$m>lLB|FiyI{h!?b z`sea|`SSSkaO3ki$39$UI6tC&FW3HG!vA;N>~%RP)N<34o0EafB|TK<%O0aOPf4}k zIc=CT3+FUhSfTHt>o~B+j3pVZvGA3-OFE579EiqZqZDn=^8ipD7m*NA2^;|k6&R2f zJOzUVV7OS06cUJnfgqSjCJ_k)NZP5b>ARb}NR8>NyO}dt*5!LpAL?)a`?fuW_Wq9! zo4*)0nyn}0-SvU}{IOw&O_ce6$RGcZGr!B<^}>$RF!w6=Lb9nR<3cT^%| zN=yr%m0O8LHK;^YJFK9CfB*mj2!~7@~IDL#ls`fBNPUDzTs&T-#{| z0&>jtU{@r*ijiHonVEl?r{L~@W^vz+c@2md+2w1I54o5tBRhA_G)1RQ?+pZ?$Due|9sI0U}4B#A~@Ye;ru28`5+|71fWCWQi<2KXua*Q>ntDeBnLsw7$_S-f!OcWr1jPTK0(~A zzkGw{_%jq42GP_@wYm`@;}J&HNPW^-;D5+A{cxX%)1G6{6Zo;Plxm|hr!;Z|DId9p z!AN#OyZ4fWdh&AYFe;%@uKBn7OxR=NZ~K@?BuWjDlYimJ^%hFVnF z|FMX0nu=&YyxB@~9jktK4mfa2cPeUPKJ3pXqvM2|ghpi38>K(y`Lnz@Jt@|QY)nG> z#+fVO&a~F@sBgI3$uL~xgmrOd7Nv6bgj| zL=?U1FP^H2MeZXlTrHKN=$%L}`}}b9VDjEtMiQj%nW$= zd3bvGe!2f2_Z+eAyVsqDuQ$78xLxC6r#98R!==31h3G!(pLaZa_Tz26Je>IXKs-*egjm;0YSJovt64tzMfI{lAAiwPrUeXnJ` zD@m~WFMSL+T(U-2DNG6&*gySFX^%E3y`0@If5?+%MM0T6Dey( zK+0B+^-}-_t7~7_g>pe1^jr(EYm||u+r4RSVLbmBEhUtg_!J+lXsi;6d1#QT%h2q} z$Fy^k7D&u|pc8Z*s|>l_P5rEAZ{E{aug!!eqdJasRZeDf;};wYDk#j#$SM#Z^-sH3 z!w=caY6V%T#PxeKvQN`f{Po7d38>jwA^ir8Pq-X~*q$deMnnS2)=%I6ES)_F0hzHs zVkrn24zj%O(^4*3s$15{BlwY+$YoOc6q<}LJvWxNQ?b;DDFG^Ae!MwNBSjsa&LLqT`g_NRM-}?(>S$m+T*Bm)2 z*?uL!Wv(|XjVC3zF**h2q*z!n**O$u(Zs5ZclWt!tH{Ei+Xp=!_ zuA)CE;Y&~9^cIuphvYR@R|GKpSc*2}AN-1EVhIPH>wOUq2ts(j&@gQjbCQ(tjyIs{ z5dioV94r_U4g|u0u;45w8wLWxL8wqlAqj{=K`{uQvSzvC6{B3yrd7!#lP`MK&Lj^j z|CN3%j9-su>&@o#(9?2I?%6+!Z3dnT^AA}?7Snw z@ZW#r^1R+3JDuTm-(Yc$y!04l$}j^y+B3e20@}YccXFcqxmkU`|9$`Ve`DSMaH?JE zzg6nJAHna7_gnknCuO)C@1Gq0JF7omGw?0+=zng%bh`O++T!G*$aPIhl(g{9hsud# z??&_Wz_qX3+n&cts}d_xrG-7jb1n~Y){8=-p-VUb`W2io78DJM0bxMcXcr3x0^wk& zP()D?ghXKxzh}3+uRF`jqcv7#*(F(4KlSx^UJ3A9Ixz~f@VBOK zF`E1iW*8xKbPo@i%m=2dd=iSp0rB8Ca*6G#Cz<}=uhX?SKHnwl%kO^5td2+}2HMP6WV0005? z0iW1vLf_Z7W*$NkxguO{KWn4Z84&K0Bos(gpc9Dbq6Z0a^0Pe(6&&hn-$tQnv8USkdNx$%nuUhEUw{j}JJb9t)RXEX}WlfpXL}d}N%B-}%_| zM+jt*jRV`+1*JD2$?5dF7^aaRz5_K)I*PEiI8T1}NOWJoMD7Vl+@td%7_F!Ur~8nR z@~qL<815eODlqJzqjxuRAO_P@HlB1*eH8C&x1f`<(ZvG&cum8_XZ5H5p9tNnAM~bo zRQg@yLK~>~Qra2uq2J0FYS@~%1TsQU)mRzCG-(ihCsWv3auIxYW8>Z*{aruRcrk}=-t}}GjIqFje07hT^N@(Y zU$u*vXq2h87fOM;>`(_T`d`Ozmor4CRc|yQEAUc&AJ(8%aP=4)Xi{mb1G5JeIB0Uf zAkv=D)P!2EMFeW>i0!I@L9C_x64vMA`9{4!rZ_cd#PwFypqZ;0!W%{}te*K4gx$l# zUgQ0qeU%|KDU(|MdayR>7#Aemt#f6CM{m1W7;`+W^}S%X3;5K~{C&Z6IQX&xK!Sr- zjEwjG3gt2c5I1eKWqfqdeRl{b-xOB{Pc0gFOL8O`U;z9T7<3pL3Bv(nz*s02A`M1j zAqdQ37uks!##<)Vl&&|z)n!-Lq}FsT?%C?A%vR5*&X2J3*X7;s{ZDnMY*jKR{Ll8~ z39D&wqvQCi`epS$t9PRQ%X~B9_It2WgAUwk@6Y~!{=VA30Y~c`@1m@+=Y?H3@#p2@ z97D&+_s=}B`UO>0-2RiR{>SV;+mji1|D5>G^sl_P@5%1};D;wk#m|jNT5*y!&aR(6 zals&QPanV1kHOQ2zyJUQ@Ijm1NvJ_=nM?><|4A0mAP)UYvW54rt}WHl)CQs|BK!c`HOf`BMS5u# z)auQQ6RcHpW%#NVV~X&&xi6z`%d4Mimi`4(vywzfY~=O8i4_0d(AHW$I{z&m!T_OM zXE)OW{}-=Hi&;H*^aCnjogT$}fy9`icVKC-sxf0cGj=VVAT7cPqRD)rDGSN_3i>Gt zcSYq3Sz0o^Iw@edKL;#yZw?R@RX$azU_Tn2-hYkLhAM3dy)BKo-7O9BuRqPUP$SQM z!Ir#25~Sr@N%H_3jb<3{TuD~o zfh*omU?#bcpH?F;(axM5p2QZMa(j9grKlND{bhXiCJ?7u7EDS+zCgj>$(mrNw zgd%q|$m_99{sJe%D*dhF^S#I{7TvD3_??Xl;WZf4iE`Hlc|Dc$9z6~}=EHD%i?P!) z9o{N7uZq4S_$`uWcQcWrPO?Q~a!sd@fxc5bSBm9Z4q}ZO;^fnQj>e9c{==*QcBFz* zL2$ykbNWwLYE+igU!T}xC@~d3PTG8%s+L!X(73;RE7{xYDuOY#nKALU;32A|diwyL zmn_Ob&39emE!s9caO*&k<91=SA?y6PGI+dPTu|x1pYO608fo%5dXlOHmxJR&_xYCg ze@ufSv;%}3aDB5y9~ zed!z$CHI-K1Mv5RVq8TBI(b@R`k+Xa{02HBv}561@9RcBrdBZ(9mJ%PU~nH%*k+0t zFli|tyB^@Y=}}9EpKugEnKTp>iyda;-3cBvPX(q3q=JnygPNZbS~Hn9cno0GX|31{tKhAJzyqura_XNA)sY)% z5Lwh!k$49A4mu-rY<@~!$m%|+7_w5zklI<=p(=umVSYw2`V5L<6T)Nl>gmpK=1>qy zyycGovpoa`fyy2>r7XJ|;I~-tEZOX7nnxS|S0i9J4cgHJLeiVrk~MsNwdp?ja&^j= z>|HJ)$*L<@3R_!fn`pbpd>o^X@4R}HeoeX)ZG7~}$BIZJ%=e;F~p z2Cf*xrpi)8XX#m_Bleh?Izmff1u^4nbd441hbNXCW!S6#jd6;#(a4P%F#@|z0vmZT zq-p7x`T0^Iig5e!*Uje$*dWt5>ksZ;tCmggg%Q=-4Plo|)1|EHMBII{0B|4Y?G5&w z@37(%-lM4L?&`J3?ciB$3uQj=1f`85#dHseUlp*I536-Af|ECLi5EbWX-bz~l~#n@ z&JDJ7raiwO8+snZ4bm!7t?5F%E4a$7wdY&82e3g=(t|s6Fe@szae4-s%7tis`RR>^*Jp@oH$?&o+Oygho7 zOh%<4aiSn_000AQL7)3Y6)Y+L-=4>2oCpQ-EEUeSba?o{8IW#=V6;^yKtA6C;3 z!tX8vyr_(2jCG5buKwpR;fcI=sOowXPlUx0iT6awbV=zCI>qdt2{w(&qLPhd99vKq z`i;*+-q3Xl)u z-L!tl73eYV!WuAbE*nwYU_HW~gh)RX&msz+UEmQ`3v(Tl9@KyNf2XmsSV&e1YOKj> z(U{~dOgdL>%iXpmFjltt=upKtc0dbx2mdw7M}y&4Q*k0oq@&eWrYeU$yF+|7$~Z2H;se;ib)k@*mtZ^a zld$Sb*@Wi|#9jJ ze>qBEk!qge(ky@Z@%p8XXp(e&`)y?q&!EN}qLlO18&uft3!I;5C5V8vW>h0aRznpM zNM6^Q3L0Q>T-L(%F|r4QI*vWUImtPYGhp}#2Ch)jS$8|6I`TsIIY=#dInEsU z^tirUqu{4GT6pns?E4*y|Kq=v-T&TuM|3-|p6m7?<~T4^A&$HsT8zk^RiWpvtFB@R zP^Ja_{0m+Wa_~d66_pZNc@Iaf2lCVoBm@!Q0JIeVFg7#=i2-21m?#(%g@l7ppiC+f z5`=lI%fq(<(dqW5~Gl9!!x@SzVnyHTx`_}|CJ%jWj^xOug2)S8sp{c-V+ zo>Y5fZ^~Qt^gjQp-;WP3i~2_})_%rGO1y^W;k(bL`(OUQ;%S1_G#)GjDhIs<-|84)Wjo`>ggF9$q-h*3kC2Iwzk3+#`V^ zXSKNK?weGj-)5~U;=_{HJUUX7hh}B|%edkaQfEj3(;LAnu_p!W(kbDJRGXTtjW6a3aWPGVrJ(76kmNqdU#Z9`Xf2#Xsi>Lb`XD&RGN{ZvKI=YP$4yc5cjK*WCBy@^1@%rNk zAoD-f-K9$Z_FY(vt{r}8LI{t(rUI9ek{C7zlMhigzCQD5?P44a=#H}|hSq$F>H8Zv z7(cXF((pv#K1A1{IhvMhppw0%1WIP&NsLM1e4v00J}to*ZgI-{)G|u3Zm1 zP9PS=I5}5w-F7n)*)@wq5ESyT1)TtZIRjVuFj}LnGxc_>twZEe2ycw-P-FW@&iF(r zA4RL?vN5QGb>VnKf2EKB!L1ZFe3v@;0oh$Zrb#kpS-`y-P&>F2NucG!-hF{s&#FpQJ(P1*M`OYs`Zm5Yel(iHQsLjjc zZ)s!~BJSP&)7YXj89R|UsR#Sn#VfPJG)y|d$Y%qqTFyvS%h9;MA_KKQe3O8W-s~Lr zO`+`&4k9juxb8k5j)%4(l(HHzF}T^9&RB`c+jF)!@5Mgm7=f#IfLvLsW>gU%*}+XagMJ5#^} zl>f{$>VS?cb4xSDB&G0d6E|O|u@Z&xZ7nT~OOK}m`Ha2 z{um7ck4a2kVs_3M?=&Gg8%aj1tL52`PnMyzdXP%|Odxfb9AmSW`#meGOZV%x_>#Dj ztlP#)?pD)hq!-u$!xCMQWM#aGy((?M|K2Cnl=VRY0#z7L78C`9;bA~nP!<#ki2-3D z6ec7Hq0e`k=1w-5c~VukYmzRnE6*+Y=jdCz`}6S1{#F0xeZQa6`Jdi57q==gjE03O z`@{Y1{q1Hz4SrwFmGy4otZ(bE-hg_ai724MKA2mUtGduQ_TSVWF}*6kKAlfT=3a=> z;lq3vuhrw#;#Ad8cvpcYldjpTw%?)kRrrsy+xqYMKND|Pi(cPf79St|KmP$AsBOpG zQ7lk6g;-Q=dJNBf&CA@ zm)&v-DsTY+nf{zcZd9d8mm;h}cg$T=7}JgEn%Jik)v#b<0eq=UeAKoy*ar$wt_~5G z%MvHMG@n3WCxg<5#nWp&pS!)y=%G-X577nK^kdb;w68}cnv9A7WmNZ8^C>ixo+60r zGdy%YX*1R!m}AsEvZKDAY|Jr+;GE)MGpNr~VH5A2^!4-^6L6&FOMxf6Oa_O%E<<3h zg}VPYb?K;QTj@w4WN@rRg6zUoK|nGK3@eBQjg=!SOF)L;_=~kfAPA6zXbgzx0D{hL z1XebqGhv~woo30C9D;1Z$c!XRnPaVU3ha7$7jeVB1fB0I?kf}qbH)LUY%yBUPs@hF zK0oFG5doiF^oIcenf?_RY>_E4nGHx-qoUPa#KZG*wgGE#v19FfetwR0kDq?(Wlcv4 ziRIMMjm#u4X)2a{tuL}Ec>!Vso%?_q&OC>nk99f{r2J=$?Fj_$z~-Qt2NrhD~`u^2;5KqVf}fp%|7Vb zKNLRo?i^J$F&#t>0RYt?EEp3G0?a_LU@SEX2!dfCh)g092#CZYer0N^yz-K%OI{0F zwNg?Stpxku&sEP#HkbPCpW8S8iK~-Og#OLDwe|Y9F4)zwjeb`2Mjw2CHcS0CyC3#F zzumlkAKXvE?S2^98@~zpPJ!|#Su=m04zttWY?OSJT}~KhRux%Ld~eR%>Z)h?1+-N5 z0-IB*?KhQ83puZ#!3ch9gsSl8EgKZCLNvC8(^p6oPOGr-N{I(FB!v}QG6Bb)J) z?SDdS7Xm<%0zp43Y&Wl%hSa%3>)zmc(RENBZ20;52Y z1V%9%#wgQwR;-zEO1(st6q4%ttq0Ei#`~Ix9EiTn9`7FZe68}DdRzLN8a=#ppZUkh z!<|)i^6x)UOPF~1e>UU!`R9Kg94O0|9zH%^9IwiN&zkYq=GE$N!Tt7NyL22e%sN|c z7V};o>%i&qDPNUUXOCr709)bopXdK4{vE$AXEDs=_$kgfOM)Y~`A5oxx?3< zlem6gcdga_Jbae->TgT=-_`cvo?pwy(?A82=Q95r<4dI{D?&C7=_{)Wz+8&uZqF$BXC0Tcyiqmd%PgI`#Uhr3YTqKk&T3dG-dhVBv z^X-ohZ`~HtThrj~MAn>T&WN?Q*?-Kx6?YlSVFAeWIy?sA?0mCDOe!Vlo8xtI<|yl( zIzJ5irhZth*_^OwliWdqr6#acXd}KLZ~*+7{!^JQWVu#lN($mg;C)ZvKg4srGf&O@ zHz)LvJ~^Lv^*x?*?f!G(o>&I23d$Z4gRl_;`?6` zI=>hH&_9;udC2g7%e3o07svX)6XN~LQaoqIxVoSLvrCD{{Ot8N@u#lq^)7byy80Sm z0UlfGcdMu2tpD%m@5e66umH^Ua=xzKn#WyVe<|$VZhJg4@b7%<000C`doHxN&bz*T zj!V2w*P6MFfB+glx!Y*(x Date: Fri, 20 Dec 2019 16:18:38 +0000 Subject: [PATCH 0569/1335] Throw more appropriate exceptions from extractors PiperOrigin-RevId: 286581465 --- .../android/exoplayer2/extractor/BinarySearchSeeker.java | 3 ++- .../exoplayer2/extractor/FlacSeekTableSeekMap.java | 2 +- .../exoplayer2/extractor/mkv/DefaultEbmlReader.java | 2 +- .../android/exoplayer2/extractor/mp3/XingSeeker.java | 4 ++-- .../android/exoplayer2/extractor/ogg/StreamReader.java | 2 +- .../android/exoplayer2/extractor/ts/AdtsExtractor.java | 9 ++++++--- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java index 9d5f7f1788..c4375e490c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java @@ -174,7 +174,8 @@ public abstract class BinarySearchSeeker { public int handlePendingSeek(ExtractorInput input, PositionHolder seekPositionHolder) throws InterruptedException, IOException { while (true) { - SeekOperationParams seekOperationParams = Assertions.checkNotNull(this.seekOperationParams); + SeekOperationParams seekOperationParams = + Assertions.checkStateNotNull(this.seekOperationParams); long floorPosition = seekOperationParams.getFloorBytePosition(); long ceilingPosition = seekOperationParams.getCeilingBytePosition(); long searchPosition = seekOperationParams.getNextSearchBytePosition(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java index a711f09e2f..be65e77c95 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java @@ -52,7 +52,7 @@ public final class FlacSeekTableSeekMap implements SeekMap { @Override public SeekPoints getSeekPoints(long timeUs) { - Assertions.checkNotNull(flacStreamMetadata.seekTable); + Assertions.checkStateNotNull(flacStreamMetadata.seekTable); long[] pointSampleNumbers = flacStreamMetadata.seekTable.pointSampleNumbers; long[] pointOffsets = flacStreamMetadata.seekTable.pointOffsets; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java index 5e6164249b..5aed97ec9a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java @@ -80,7 +80,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @Override public boolean read(ExtractorInput input) throws IOException, InterruptedException { - Assertions.checkNotNull(processor); + Assertions.checkStateNotNull(processor); while (true) { MasterElement head = masterElementsStack.peek(); if (head != null && input.getPosition() >= head.elementEndPosition) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java index aa1d8be316..816b647675 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java @@ -133,7 +133,7 @@ import com.google.android.exoplayer2.util.Util; scaledPosition = 256; } else { int prevTableIndex = (int) percent; - long[] tableOfContents = Assertions.checkNotNull(this.tableOfContents); + long[] tableOfContents = Assertions.checkStateNotNull(this.tableOfContents); double prevScaledPosition = tableOfContents[prevTableIndex]; double nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1]; // Linearly interpolate between the two scaled positions. @@ -153,7 +153,7 @@ import com.google.android.exoplayer2.util.Util; if (!isSeekable() || positionOffset <= xingFrameSize) { return 0L; } - long[] tableOfContents = Assertions.checkNotNull(this.tableOfContents); + long[] tableOfContents = Assertions.checkStateNotNull(this.tableOfContents); double scaledPosition = (positionOffset * 256d) / dataSize; int prevTableIndex = Util.binarySearchFloor(tableOfContents, (long) scaledPosition, true, true); long prevTimeUs = getTimeUsForTableIndex(prevTableIndex); 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 41ae394de2..ed0e3637da 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 @@ -177,7 +177,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } if (!seekMapSet) { - SeekMap seekMap = Assertions.checkNotNull(oggSeeker.createSeekMap()); + SeekMap seekMap = Assertions.checkStateNotNull(oggSeeker.createSeekMap()); extractorOutput.seekMap(seekMap); seekMapSet = true; } 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 86dacd8c30..d14b6a0b3e 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 @@ -20,7 +20,6 @@ import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_L import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG; import androidx.annotation.IntDef; -import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap; @@ -39,6 +38,8 @@ import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Extracts data from AAC bit streams with ADTS framing. @@ -86,7 +87,7 @@ public final class AdtsExtractor implements Extractor { private final ParsableByteArray scratch; private final ParsableBitArray scratchBits; - @Nullable private ExtractorOutput extractorOutput; + @MonotonicNonNull private ExtractorOutput extractorOutput; private long firstSampleTimestampUs; private long firstFramePosition; @@ -180,6 +181,8 @@ public final class AdtsExtractor implements Extractor { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + Assertions.checkStateNotNull(extractorOutput); // Asserts that init has been called. + long inputLength = input.getLength(); boolean canUseConstantBitrateSeeking = (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0 && inputLength != C.LENGTH_UNSET; @@ -230,6 +233,7 @@ public final class AdtsExtractor implements Extractor { return firstFramePosition; } + @RequiresNonNull("extractorOutput") private void maybeOutputSeekMap( long inputLength, boolean canUseConstantBitrateSeeking, boolean readEndOfStream) { if (hasOutputSeekMap) { @@ -244,7 +248,6 @@ public final class AdtsExtractor implements Extractor { return; } - ExtractorOutput extractorOutput = Assertions.checkNotNull(this.extractorOutput); if (useConstantBitrateSeeking && reader.getSampleDurationUs() != C.TIME_UNSET) { extractorOutput.seekMap(getConstantBitrateSeekMap(inputLength)); } else { From f88fbaf6555333775298d16716418584f2f4cdb0 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 20 Dec 2019 16:33:56 +0000 Subject: [PATCH 0570/1335] Add Cue.verticalType field Inspired by the `vertical` cue setting in WebVTT: https://www.w3.org/TR/webvtt1/#webvtt-vertical-text-cue-setting PiperOrigin-RevId: 286583621 --- .../google/android/exoplayer2/text/Cue.java | 54 ++++++++++++++++--- .../android/exoplayer2/text/CueTest.java | 2 + 2 files changed, 49 insertions(+), 7 deletions(-) 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 d35c444c18..889c61eb4a 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 @@ -49,9 +49,7 @@ public final class Cue { @IntDef({TYPE_UNSET, ANCHOR_TYPE_START, ANCHOR_TYPE_MIDDLE, ANCHOR_TYPE_END}) public @interface AnchorType {} - /** - * An unset anchor or line type value. - */ + /** An unset anchor, line, text size or vertical type value. */ public static final int TYPE_UNSET = Integer.MIN_VALUE; /** @@ -114,6 +112,25 @@ public final class Cue { /** Text size is measured in number of pixels. */ public static final int TEXT_SIZE_TYPE_ABSOLUTE = 2; + /** + * The type of vertical layout for this cue, which may be unset (i.e. horizontal). One of {@link + * #TYPE_UNSET}, {@link #VERTICAL_TYPE_RL} or {@link #VERTICAL_TYPE_LR}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + TYPE_UNSET, + VERTICAL_TYPE_RL, + VERTICAL_TYPE_LR, + }) + public @interface VerticalType {} + + /** Vertical right-to-left (e.g. for Japanese). */ + public static final int VERTICAL_TYPE_RL = 1; + + /** Vertical left-to-right (e.g. for Mongolian). */ + public static final int VERTICAL_TYPE_LR = 2; + /** * The cue text, or null if this is an image cue. Note the {@link CharSequence} may be decorated * with styling spans. @@ -241,6 +258,12 @@ public final class Cue { */ public final float textSize; + /** + * The vertical formatting of this Cue, or {@link #TYPE_UNSET} if the cue has no vertical setting + * (and so should be horizontal). + */ + public final @VerticalType int verticalType; + /** * Creates a text cue whose {@link #textAlignment} is null, whose type parameters are set to * {@link #TYPE_UNSET} and whose dimension parameters are set to {@link #DIMEN_UNSET}. @@ -338,7 +361,8 @@ public final class Cue { size, /* bitmapHeight= */ DIMEN_UNSET, /* windowColorSet= */ false, - /* windowColor= */ Color.BLACK); + /* windowColor= */ Color.BLACK, + /* verticalType= */ TYPE_UNSET); } /** @@ -382,7 +406,8 @@ public final class Cue { size, /* bitmapHeight= */ DIMEN_UNSET, windowColorSet, - windowColor); + windowColor, + /* verticalType= */ TYPE_UNSET); } private Cue( @@ -399,7 +424,8 @@ public final class Cue { float size, float bitmapHeight, boolean windowColorSet, - int windowColor) { + int windowColor, + @VerticalType int verticalType) { // Exactly one of text or bitmap should be set. if (text == null) { Assertions.checkNotNull(bitmap); @@ -420,6 +446,7 @@ public final class Cue { this.windowColor = windowColor; this.textSizeType = textSizeType; this.textSize = textSize; + this.verticalType = verticalType; } /** A builder for {@link Cue} objects. */ @@ -438,6 +465,7 @@ public final class Cue { private float bitmapHeight; private boolean windowColorSet; @ColorInt private int windowColor; + @VerticalType private int verticalType; public Builder() { text = null; @@ -454,6 +482,7 @@ public final class Cue { bitmapHeight = DIMEN_UNSET; windowColorSet = false; windowColor = Color.BLACK; + verticalType = TYPE_UNSET; } /** @@ -624,6 +653,16 @@ public final class Cue { return this; } + /** + * Sets the vertical formatting for this Cue. + * + * @see Cue#verticalType + */ + public Builder setVerticalType(@VerticalType int verticalType) { + this.verticalType = verticalType; + return this; + } + /** Build the cue. */ public Cue build() { return new Cue( @@ -640,7 +679,8 @@ public final class Cue { size, bitmapHeight, windowColorSet, - windowColor); + windowColor, + verticalType); } } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.java index bd1acdf02b..f014775a85 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.java @@ -43,6 +43,7 @@ public class CueTest { .setTextSize(0.2f, Cue.TEXT_SIZE_TYPE_FRACTIONAL) .setSize(0.8f) .setWindowColor(Color.CYAN) + .setVerticalType(Cue.VERTICAL_TYPE_RL) .build(); assertThat(cue.text).isEqualTo("text"); @@ -56,6 +57,7 @@ public class CueTest { assertThat(cue.size).isEqualTo(0.8f); assertThat(cue.windowColor).isEqualTo(Color.CYAN); assertThat(cue.windowColorSet).isTrue(); + assertThat(cue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_RL); } @Test From 14a0c9ebfdb3d1280c3136e6739cce12ceea98f8 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 20 Dec 2019 16:36:18 +0000 Subject: [PATCH 0571/1335] Add vertical support to WebvttCueParser PiperOrigin-RevId: 286583957 --- .../text/webvtt/WebvttCueParser.java | 20 ++++- .../core/src/test/assets/webvtt/with_vertical | 12 +++ .../text/webvtt/WebvttDecoderTest.java | 76 +++++++++++++++++-- 3 files changed, 99 insertions(+), 9 deletions(-) create mode 100644 library/core/src/test/assets/webvtt/with_vertical 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 565d324828..3a07a74042 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 @@ -313,6 +313,8 @@ public final class WebvttCueParser { parsePositionAttribute(value, builder); } else if ("size".equals(name)) { builder.size = WebvttParserUtil.parsePercentage(value); + } else if ("vertical".equals(name)) { + builder.verticalType = parseVerticalAttribute(value); } else { Log.w(TAG, "Unknown cue setting " + name + ":" + value); } @@ -368,6 +370,19 @@ public final class WebvttCueParser { } } + @Cue.VerticalType + private static int parseVerticalAttribute(String s) { + switch (s) { + case "rl": + return Cue.VERTICAL_TYPE_RL; + case "lr": + return Cue.VERTICAL_TYPE_LR; + default: + Log.w(TAG, "Invalid 'vertical' value: " + s); + return Cue.TYPE_UNSET; + } + } + @TextAlignment private static int parseTextAlignment(String s) { switch (s) { @@ -564,6 +579,7 @@ public final class WebvttCueParser { public float position; @Cue.AnchorType public int positionAnchor; public float size; + @Cue.VerticalType public int verticalType; public WebvttCueInfoBuilder() { startTimeUs = 0; @@ -579,6 +595,7 @@ public final class WebvttCueParser { positionAnchor = Cue.TYPE_UNSET; // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-size size = 1.0f; + verticalType = Cue.TYPE_UNSET; } public WebvttCueInfo build() { @@ -600,7 +617,8 @@ public final class WebvttCueParser { .setLineAnchor(lineAnchor) .setPosition(position) .setPositionAnchor(positionAnchor) - .setSize(Math.min(size, deriveMaxSize(positionAnchor, position))); + .setSize(Math.min(size, deriveMaxSize(positionAnchor, position))) + .setVerticalType(verticalType); if (text != null) { cueBuilder.setText(text); diff --git a/library/core/src/test/assets/webvtt/with_vertical b/library/core/src/test/assets/webvtt/with_vertical new file mode 100644 index 0000000000..2bfe9b7019 --- /dev/null +++ b/library/core/src/test/assets/webvtt/with_vertical @@ -0,0 +1,12 @@ +WEBVTT + +NOTE vertical spec: https://www.w3.org/TR/webvtt1/#webvtt-alignment-cue-setting + +00:00:00.000 --> 00:00:01.234 vertical:rl +Vertical right-to-left (e.g. Japanese) + +00:02.345 --> 00:03.456 vertical:lr +Vertical left-to-right (e.g. Mongolian) + +00:04.000 --> 00:05.000 +No vertical setting (i.e. horizontal) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index 06ac4d825c..f405f1c407 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -48,6 +48,7 @@ public class WebvttDecoderTest { private static final String TYPICAL_WITH_IDS_FILE = "webvtt/typical_with_identifiers"; private static final String TYPICAL_WITH_COMMENTS_FILE = "webvtt/typical_with_comments"; private static final String WITH_POSITIONING_FILE = "webvtt/with_positioning"; + private static final String WITH_VERTICAL_FILE = "webvtt/with_vertical"; private static final String WITH_BAD_CUE_HEADER_FILE = "webvtt/with_bad_cue_header"; private static final String WITH_TAGS_FILE = "webvtt/with_tags"; private static final String WITH_CSS_STYLES = "webvtt/with_css_styles"; @@ -231,7 +232,8 @@ public class WebvttDecoderTest { /* lineAnchor= */ Cue.ANCHOR_TYPE_START, /* position= */ 0.1f, /* positionAnchor= */ Cue.ANCHOR_TYPE_START, - /* size= */ 0.35f); + /* size= */ 0.35f, + /* verticalType= */ Cue.TYPE_UNSET); assertCue( subtitle, /* eventTimeIndex= */ 2, @@ -244,7 +246,8 @@ public class WebvttDecoderTest { /* lineAnchor= */ Cue.ANCHOR_TYPE_START, /* position= */ 0.5f, /* positionAnchor= */ Cue.ANCHOR_TYPE_END, - /* size= */ 0.35f); + /* size= */ 0.35f, + /* verticalType= */ Cue.TYPE_UNSET); assertCue( subtitle, /* eventTimeIndex= */ 4, @@ -257,7 +260,8 @@ public class WebvttDecoderTest { /* lineAnchor= */ Cue.ANCHOR_TYPE_END, /* position= */ 0.5f, /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 0.35f); + /* size= */ 0.35f, + /* verticalType= */ Cue.TYPE_UNSET); assertCue( subtitle, /* eventTimeIndex= */ 6, @@ -270,7 +274,8 @@ public class WebvttDecoderTest { /* lineAnchor= */ Cue.ANCHOR_TYPE_START, /* position= */ 0.5f, /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 1.0f); + /* size= */ 1.0f, + /* verticalType= */ Cue.TYPE_UNSET); assertCue( subtitle, /* eventTimeIndex= */ 8, @@ -283,7 +288,8 @@ public class WebvttDecoderTest { /* lineAnchor= */ Cue.ANCHOR_TYPE_START, /* position= */ 1.0f, /* positionAnchor= */ Cue.ANCHOR_TYPE_END, - /* size= */ 1.0f); + /* size= */ 1.0f, + /* verticalType= */ Cue.TYPE_UNSET); assertCue( subtitle, /* eventTimeIndex= */ 10, @@ -296,7 +302,58 @@ public class WebvttDecoderTest { /* lineAnchor= */ Cue.ANCHOR_TYPE_END, /* position= */ 0.5f, /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 0.35f); + /* size= */ 0.35f, + /* verticalType= */ Cue.TYPE_UNSET); + } + + @Test + public void testDecodeWithVertical() throws Exception { + WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_VERTICAL_FILE); + // Test event count. + assertThat(subtitle.getEventTimeCount()).isEqualTo(6); + // Test cues. + assertCue( + subtitle, + /* eventTimeIndex= */ 0, + /* startTimeUs= */ 0, + /* endTimeUs= */ 1234000, + "Vertical right-to-left (e.g. Japanese)", + Alignment.ALIGN_CENTER, + /* line= */ Cue.DIMEN_UNSET, + /* lineType= */ Cue.LINE_TYPE_NUMBER, + /* lineAnchor= */ Cue.ANCHOR_TYPE_START, + /* position= */ 0.5f, + /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, + /* size= */ 1.0f, + Cue.VERTICAL_TYPE_RL); + assertCue( + subtitle, + /* eventTimeIndex= */ 2, + /* startTimeUs= */ 2345000, + /* endTimeUs= */ 3456000, + "Vertical left-to-right (e.g. Mongolian)", + Alignment.ALIGN_CENTER, + /* line= */ Cue.DIMEN_UNSET, + /* lineType= */ Cue.LINE_TYPE_NUMBER, + /* lineAnchor= */ Cue.ANCHOR_TYPE_START, + /* position= */ 0.5f, + /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, + /* size= */ 1.0f, + Cue.VERTICAL_TYPE_LR); + assertCue( + subtitle, + /* eventTimeIndex= */ 4, + /* startTimeUs= */ 4000000, + /* endTimeUs= */ 5000000, + "No vertical setting (i.e. horizontal)", + Alignment.ALIGN_CENTER, + /* line= */ Cue.DIMEN_UNSET, + /* lineType= */ Cue.LINE_TYPE_NUMBER, + /* lineAnchor= */ Cue.ANCHOR_TYPE_START, + /* position= */ 0.5f, + /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, + /* size= */ 1.0f, + /* verticalType= */ Cue.TYPE_UNSET); } @Test @@ -421,7 +478,8 @@ public class WebvttDecoderTest { /* lineAnchor= */ Cue.ANCHOR_TYPE_START, /* position= */ 0.5f, /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 1.0f); + /* size= */ 1.0f, + /* verticalType= */ Cue.TYPE_UNSET); } private void assertCue( @@ -436,7 +494,8 @@ public class WebvttDecoderTest { @Cue.AnchorType int lineAnchor, float position, @Cue.AnchorType int positionAnchor, - float size) { + float size, + @Cue.VerticalType int verticalType) { expect .withMessage("startTimeUs") .that(subtitle.getEventTime(eventTimeIndex)) @@ -457,6 +516,7 @@ public class WebvttDecoderTest { expect.withMessage("cue.position").that(cue.position).isEqualTo(position); expect.withMessage("cue.positionAnchor").that(cue.positionAnchor).isEqualTo(positionAnchor); expect.withMessage("cue.size").that(cue.size).isEqualTo(size); + expect.withMessage("cue.verticalType").that(cue.verticalType).isEqualTo(verticalType); assertThat(expect.hasFailures()).isFalse(); } From a0044257b4b6e13cad1baac19f8f1e52c323b4fd Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 20 Dec 2019 16:51:02 +0000 Subject: [PATCH 0572/1335] Add troubleshooting instructions for decoding extensions PiperOrigin-RevId: 286585978 --- extensions/av1/README.md | 8 ++++++++ extensions/ffmpeg/README.md | 10 ++++++++++ extensions/flac/README.md | 8 ++++++++ extensions/opus/README.md | 8 ++++++++ extensions/vp9/README.md | 8 ++++++++ 5 files changed, 42 insertions(+) diff --git a/extensions/av1/README.md b/extensions/av1/README.md index 276daae4e2..54e27a3b87 100644 --- a/extensions/av1/README.md +++ b/extensions/av1/README.md @@ -96,6 +96,14 @@ a custom track selector the choice of `Renderer` is up to your implementation. You need to make sure you are passing a `Libgav1VideoRenderer` to the player and then you need to implement your own logic to use the renderer for a given track. +## Using the extension in the demo application ## + +To try out playback using the extension in the [demo application][], see +[enabling extension decoders][]. + +[demo application]: https://exoplayer.dev/demo-application.html +[enabling extension decoders]: https://exoplayer.dev/demo-application.html#enabling-extension-decoders + ## Rendering options ## There are two possibilities for rendering the output `Libgav1VideoRenderer` diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index fe4aca772a..1b2db8f0f4 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -106,9 +106,19 @@ then implement your own logic to use the renderer for a given track. [#2781]: https://github.com/google/ExoPlayer/issues/2781 [Supported formats]: https://exoplayer.dev/supported-formats.html#ffmpeg-extension +## Using the extension in the demo application ## + +To try out playback using the extension in the [demo application][], see +[enabling extension decoders][]. + +[demo application]: https://exoplayer.dev/demo-application.html +[enabling extension decoders]: https://exoplayer.dev/demo-application.html#enabling-extension-decoders + ## Links ## +* [Troubleshooting using extensions][] * [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ffmpeg.*` belong to this module. +[Troubleshooting using extensions]: https://exoplayer.dev/troubleshooting.html#how-can-i-get-a-decoding-extension-to-load-and-be-used-for-playback [Javadoc]: https://exoplayer.dev/doc/reference/index.html diff --git a/extensions/flac/README.md b/extensions/flac/README.md index 84a92f9586..a9d4c3094e 100644 --- a/extensions/flac/README.md +++ b/extensions/flac/README.md @@ -97,6 +97,14 @@ a custom track selector the choice of `Renderer` is up to your implementation, so you need to make sure you are passing an `LibflacAudioRenderer` to the player, then implement your own logic to use the renderer for a given track. +## Using the extension in the demo application ## + +To try out playback using the extension in the [demo application][], see +[enabling extension decoders][]. + +[demo application]: https://exoplayer.dev/demo-application.html +[enabling extension decoders]: https://exoplayer.dev/demo-application.html#enabling-extension-decoders + ## Links ## * [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.flac.*` diff --git a/extensions/opus/README.md b/extensions/opus/README.md index 05448f2073..d3691b07bd 100644 --- a/extensions/opus/README.md +++ b/extensions/opus/README.md @@ -101,6 +101,14 @@ a custom track selector the choice of `Renderer` is up to your implementation, so you need to make sure you are passing an `LibopusAudioRenderer` to the player, then implement your own logic to use the renderer for a given track. +## Using the extension in the demo application ## + +To try out playback using the extension in the [demo application][], see +[enabling extension decoders][]. + +[demo application]: https://exoplayer.dev/demo-application.html +[enabling extension decoders]: https://exoplayer.dev/demo-application.html#enabling-extension-decoders + ## Links ## * [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.opus.*` diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 71241d9a4f..fd0836648a 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -114,6 +114,14 @@ a custom track selector the choice of `Renderer` is up to your implementation, so you need to make sure you are passing an `LibvpxVideoRenderer` to the player, then implement your own logic to use the renderer for a given track. +## Using the extension in the demo application ## + +To try out playback using the extension in the [demo application][], see +[enabling extension decoders][]. + +[demo application]: https://exoplayer.dev/demo-application.html +[enabling extension decoders]: https://exoplayer.dev/demo-application.html#enabling-extension-decoders + ## Rendering options ## There are two possibilities for rendering the output `LibvpxVideoRenderer` From 945da46d81ae20ce434df16a167474a8f0b2003f Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 20 Dec 2019 16:57:42 +0000 Subject: [PATCH 0573/1335] Update release notes for #6776 PiperOrigin-RevId: 286586865 --- RELEASENOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 917f62ddf0..4180ca06fb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -49,6 +49,8 @@ `IllegalStateException` being thrown from `DefaultDownloadIndex.getDownloadForCurrentRow` ([#6785](https://github.com/google/ExoPlayer/issues/6785)). +* Fix `IndexOutOfBoundsException` in `SinglePeriodTimeline.getWindow` + ([#6776](https://github.com/google/ExoPlayer/issues/6776)). * Add missing @Nullable to `MediaCodecAudioRenderer.getMediaClock` and `SimpleDecoderAudioRenderer.getMediaClock` ([#6792](https://github.com/google/ExoPlayer/issues/6792)). From 96437611d837ed7164e7aab8b42841c3da409895 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 20 Dec 2019 17:04:06 +0000 Subject: [PATCH 0574/1335] Release notes tweak PiperOrigin-RevId: 286587978 --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4180ca06fb..67987f8852 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -51,7 +51,7 @@ ([#6785](https://github.com/google/ExoPlayer/issues/6785)). * Fix `IndexOutOfBoundsException` in `SinglePeriodTimeline.getWindow` ([#6776](https://github.com/google/ExoPlayer/issues/6776)). -* Add missing @Nullable to `MediaCodecAudioRenderer.getMediaClock` and +* Add missing `@Nullable` to `MediaCodecAudioRenderer.getMediaClock` and `SimpleDecoderAudioRenderer.getMediaClock` ([#6792](https://github.com/google/ExoPlayer/issues/6792)). From f50ed8fd9ccef82efe22353b4220092d837bf698 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 20 Dec 2019 20:15:27 +0000 Subject: [PATCH 0575/1335] Enable blacklisting for HTTP 416 Where media segments are specified using byte ranges, it makes sense that a server might return 416 (which we don't consider for blacklisting) if the segment is unavailable, rather than the 404 (which we do consider for blacklisting) that we expect when media segments are only specified using a URL. Issue: #6775 PiperOrigin-RevId: 286620698 --- .../exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java index 307652f456..435f4bf578 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java @@ -71,6 +71,7 @@ public class DefaultLoadErrorHandlingPolicy implements LoadErrorHandlingPolicy { int responseCode = ((InvalidResponseCodeException) exception).responseCode; return responseCode == 404 // HTTP 404 Not Found. || responseCode == 410 // HTTP 410 Gone. + || responseCode == 416 // HTTP 416 Range Not Satisfiable. ? DEFAULT_TRACK_BLACKLIST_MS : C.TIME_UNSET; } From 24a19264dbf35e326bbd7bb36c232eb180e1e26d Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 20 Dec 2019 20:23:02 +0000 Subject: [PATCH 0576/1335] Fix handling of network transitions in RequirementsWatcher Issue: #6733 PiperOrigin-RevId: 286621715 --- RELEASENOTES.md | 4 ++ .../exoplayer2/scheduler/Requirements.java | 13 +++-- .../scheduler/RequirementsWatcher.java | 54 +++++++++++-------- 3 files changed, 41 insertions(+), 30 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 67987f8852..d3f7cf8067 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -28,6 +28,10 @@ developers to handle data that's neither UTF-8 nor ISO-8859-1 ([#6753](https://github.com/google/ExoPlayer/issues/6753)). * Add playlist API ([#6161](https://github.com/google/ExoPlayer/issues/6161)). +* Fix handling of network transitions in `RequirementsWatcher` + ([#6733](https://github.com/google/ExoPlayer/issues/6733)). Incorrect handling + could previously cause downloads to be paused when they should have been able + to proceed. ### 2.11.1 (2019-12-20) ### 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 35f8e37dcf..87ea60bf73 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 @@ -163,9 +163,10 @@ public final class Requirements implements Parcelable { } private static boolean isInternetConnectivityValidated(ConnectivityManager connectivityManager) { - if (Util.SDK_INT < 23) { - // TODO Check internet connectivity using http://clients3.google.com/generate_204 on API - // levels prior to 23. + // It's possible to query NetworkCapabilities from API level 23, but RequirementsWatcher only + // fires an event to update its Requirements when NetworkCapabilities change from API level 24. + // Since Requirements wont be updated, we assume connectivity is validated on API level 23. + if (Util.SDK_INT < 24) { return true; } Network activeNetwork = connectivityManager.getActiveNetwork(); @@ -174,10 +175,8 @@ public final class Requirements implements Parcelable { } NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork); - boolean validated = - networkCapabilities == null - || !networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); - return !validated; + return networkCapabilities != null + && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); } @Override 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 0d9b8261d9..f55978c28a 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 @@ -23,7 +23,6 @@ import android.content.IntentFilter; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; -import android.net.NetworkRequest; import android.os.Handler; import android.os.Looper; import android.os.PowerManager; @@ -62,7 +61,7 @@ public final class RequirementsWatcher { @Nullable private DeviceStatusChangeReceiver receiver; @Requirements.RequirementFlags private int notMetRequirements; - @Nullable private CapabilityValidatedCallback networkCallback; + @Nullable private NetworkCallback networkCallback; /** * @param context Any context. @@ -88,8 +87,8 @@ public final class RequirementsWatcher { IntentFilter filter = new IntentFilter(); if (requirements.isNetworkRequired()) { - if (Util.SDK_INT >= 23) { - registerNetworkCallbackV23(); + if (Util.SDK_INT >= 24) { + registerNetworkCallbackV24(); } else { filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); } @@ -115,8 +114,8 @@ public final class RequirementsWatcher { public void stop() { context.unregisterReceiver(Assertions.checkNotNull(receiver)); receiver = null; - if (networkCallback != null) { - unregisterNetworkCallback(); + if (Util.SDK_INT >= 24 && networkCallback != null) { + unregisterNetworkCallbackV24(); } } @@ -125,26 +124,21 @@ public final class RequirementsWatcher { return requirements; } - @TargetApi(23) - private void registerNetworkCallbackV23() { + @TargetApi(24) + private void registerNetworkCallbackV24() { ConnectivityManager connectivityManager = Assertions.checkNotNull( (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)); - NetworkRequest request = - new NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - .build(); - networkCallback = new CapabilityValidatedCallback(); - connectivityManager.registerNetworkCallback(request, networkCallback); + networkCallback = new NetworkCallback(); + connectivityManager.registerDefaultNetworkCallback(networkCallback); } - private void unregisterNetworkCallback() { - if (Util.SDK_INT >= 21) { - ConnectivityManager connectivityManager = - (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - connectivityManager.unregisterNetworkCallback(Assertions.checkNotNull(networkCallback)); - networkCallback = null; - } + @TargetApi(24) + private void unregisterNetworkCallbackV24() { + ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + connectivityManager.unregisterNetworkCallback(Assertions.checkNotNull(networkCallback)); + networkCallback = null; } private void checkRequirements() { @@ -165,8 +159,11 @@ public final class RequirementsWatcher { } } - @RequiresApi(api = 21) - private final class CapabilityValidatedCallback extends ConnectivityManager.NetworkCallback { + @RequiresApi(24) + private final class NetworkCallback extends ConnectivityManager.NetworkCallback { + boolean receivedCapabilitiesChange; + boolean networkValidated; + @Override public void onAvailable(Network network) { onNetworkCallback(); @@ -177,6 +174,17 @@ public final class RequirementsWatcher { onNetworkCallback(); } + @Override + public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) { + boolean networkValidated = + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); + if (!receivedCapabilitiesChange || this.networkValidated != networkValidated) { + receivedCapabilitiesChange = true; + this.networkValidated = networkValidated; + onNetworkCallback(); + } + } + private void onNetworkCallback() { handler.post( () -> { From cc5e981e894b4e4ed6dd9d16a0537c87f7b162d6 Mon Sep 17 00:00:00 2001 From: ybai001 Date: Mon, 23 Dec 2019 10:49:03 +0800 Subject: [PATCH 0577/1335] Add AC-4 DRM Support --- .../extractor/mp4/FragmentedMp4Extractor.java | 4 +++- .../exoplayer2/source/SampleDataQueue.java | 17 ++++++++++++----- .../android/exoplayer2/source/SampleQueue.java | 3 ++- 3 files changed, 17 insertions(+), 7 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 1172f8665a..792545b610 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 @@ -1222,6 +1222,7 @@ public class FragmentedMp4Extractor implements Extractor { * @throws InterruptedException If the thread is interrupted. */ private boolean readSample(ExtractorInput input) throws IOException, InterruptedException { + int outputSampleEncryptionDataSize = 0; if (parserState == STATE_READING_SAMPLE_START) { if (currentTrackBundle == null) { @Nullable TrackBundle currentTrackBundle = getNextFragmentRun(trackBundles); @@ -1269,6 +1270,7 @@ public class FragmentedMp4Extractor implements Extractor { } sampleBytesWritten = currentTrackBundle.outputSampleEncryptionData(); sampleSize += sampleBytesWritten; + outputSampleEncryptionDataSize = sampleBytesWritten; parserState = STATE_READING_SAMPLE_CONTINUE; sampleCurrentNalBytesRemaining = 0; isAc4HeaderRequired = @@ -1338,7 +1340,7 @@ public class FragmentedMp4Extractor implements Extractor { } } else { if (isAc4HeaderRequired) { - Ac4Util.getAc4SampleHeader(sampleSize, scratch); + Ac4Util.getAc4SampleHeader(sampleSize - outputSampleEncryptionDataSize, scratch); int length = scratch.limit(); output.sampleData(scratch, length); sampleSize += length; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java index 68761cef19..26a95b8de2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.extractor.TrackOutput.CryptoData; import com.google.android.exoplayer2.source.SampleQueue.SampleExtrasHolder; import com.google.android.exoplayer2.upstream.Allocation; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.EOFException; import java.io.IOException; @@ -114,11 +115,13 @@ import java.nio.ByteBuffer; * * @param buffer The buffer to populate. * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. + * @param mimeType The MIME type. */ - public void readToBuffer(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { + public void readToBuffer(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder, + String mimeType) { // Read encryption data if the sample is encrypted. if (buffer.isEncrypted()) { - readEncryptionData(buffer, extrasHolder); + readEncryptionData(buffer, extrasHolder, mimeType); } // Read sample data, extracting supplemental data into a separate buffer if needed. if (buffer.hasSupplementalData()) { @@ -215,8 +218,10 @@ import java.nio.ByteBuffer; * * @param buffer The buffer into which the encryption data should be written. * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. + * @param mimeType The MIME type. */ - private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { + private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder, + String mimeType) { long offset = extrasHolder.offset; // Read the signal byte. @@ -265,8 +270,10 @@ import java.nio.ByteBuffer; encryptedDataSizes[i] = scratch.readUnsignedIntToInt(); } } else { - clearDataSizes[0] = 0; - encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset); + int addedHeaderSize = MimeTypes.AUDIO_AC4.equals(mimeType) ? 7 : 0; + clearDataSizes[0] = addedHeaderSize; + encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset) + - addedHeaderSize; } // Populate the cryptoInfo. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index cc15d9d275..bfd3d7c4ed 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -323,7 +323,8 @@ public class SampleQueue implements TrackOutput { readSampleMetadata( formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilUs, extrasHolder); if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream() && !buffer.isFlagsOnly()) { - sampleDataQueue.readToBuffer(buffer, extrasHolder); + sampleDataQueue.readToBuffer(buffer, extrasHolder, + downstreamFormat == null ? null : downstreamFormat.sampleMimeType); } return result; } From 035cb096d9c8a0200014993f6d275d7954401827 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 31 Dec 2019 16:30:35 +0000 Subject: [PATCH 0578/1335] Mark final field PiperOrigin-RevId: 287669425 --- .../exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java index 0d126ff27f..b5eb8efee3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java @@ -33,7 +33,7 @@ import com.google.android.exoplayer2.util.Assertions; */ @RequiresApi(21) /* package */ final class AsynchronousMediaCodecAdapter implements MediaCodecAdapter { - private MediaCodecAsyncCallback mediaCodecAsyncCallback; + private final MediaCodecAsyncCallback mediaCodecAsyncCallback; private final Handler handler; private final MediaCodec codec; @Nullable private IllegalStateException internalException; @@ -51,7 +51,7 @@ import com.google.android.exoplayer2.util.Assertions; @VisibleForTesting /* package */ AsynchronousMediaCodecAdapter(MediaCodec codec, Looper looper) { - this.mediaCodecAsyncCallback = new MediaCodecAsyncCallback(); + mediaCodecAsyncCallback = new MediaCodecAsyncCallback(); handler = new Handler(looper); this.codec = codec; this.codec.setCallback(mediaCodecAsyncCallback); From b77717ce91542dfb9ced8e13b1d15d36fc8ca3dd Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 2 Jan 2020 09:59:39 +0000 Subject: [PATCH 0579/1335] Remove buffer size based adaptation. An experiment with this algorithm didn't show positive results. We can therefore keep the simpler default algorithm. Startblock: is submitted PiperOrigin-RevId: 287807538 --- .../BufferSizeAdaptationBuilder.java | 494 ------------------ .../BufferSizeAdaptiveTrackSelectionTest.java | 248 --------- 2 files changed, 742 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java delete mode 100644 library/core/src/test/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptiveTrackSelectionTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java deleted file mode 100644 index b850a08aeb..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java +++ /dev/null @@ -1,494 +0,0 @@ -/* - * Copyright (C) 2018 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.trackselection; - -import android.util.Pair; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultLoadControl; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.LoadControl; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.chunk.MediaChunk; -import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import com.google.android.exoplayer2.trackselection.TrackSelection.Definition; -import com.google.android.exoplayer2.upstream.BandwidthMeter; -import com.google.android.exoplayer2.upstream.DefaultAllocator; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Clock; -import java.util.List; -import org.checkerframework.checker.nullness.compatqual.NullableType; - -/** - * Builder for a {@link TrackSelection.Factory} and {@link LoadControl} that implement buffer size - * based track adaptation. - */ -public final class BufferSizeAdaptationBuilder { - - /** Dynamic filter for formats, which is applied when selecting a new track. */ - public interface DynamicFormatFilter { - - /** Filter which allows all formats. */ - DynamicFormatFilter NO_FILTER = (format, trackBitrate, isInitialSelection) -> true; - - /** - * Called when updating the selected track to determine whether a candidate track is allowed. If - * no format is allowed or eligible, the lowest quality format will be used. - * - * @param format The {@link Format} of the candidate track. - * @param trackBitrate The estimated bitrate of the track. May differ from {@link - * Format#bitrate} if a more accurate estimate of the current track bitrate is available. - * @param isInitialSelection Whether this is for the initial track selection. - */ - boolean isFormatAllowed(Format format, int trackBitrate, boolean isInitialSelection); - } - - /** - * The default minimum duration of media that the player will attempt to ensure is buffered at all - * times, in milliseconds. - */ - public static final int DEFAULT_MIN_BUFFER_MS = 15000; - - /** - * The default maximum duration of media that the player will attempt to buffer, in milliseconds. - */ - public static final int DEFAULT_MAX_BUFFER_MS = 50000; - - /** - * The default duration of media that must be buffered for playback to start or resume following a - * user action such as a seek, in milliseconds. - */ - public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS = - DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS; - - /** - * The default duration of media that must be buffered for playback to resume after a rebuffer, in - * milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user action. - */ - public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = - DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; - - /** - * The default offset the current duration of buffered media must deviate from the ideal duration - * of buffered media for the currently selected format, before the selected format is changed. - */ - public static final int DEFAULT_HYSTERESIS_BUFFER_MS = 5000; - - /** - * During start-up phase, the default fraction of the available bandwidth that the selection - * should consider available for use. Setting to a value less than 1 is recommended to account for - * inaccuracies in the bandwidth estimator. - */ - public static final float DEFAULT_START_UP_BANDWIDTH_FRACTION = - AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION; - - /** - * During start-up phase, the default minimum duration of buffered media required for the selected - * track to switch to one of higher quality based on measured bandwidth. - */ - public static final int DEFAULT_START_UP_MIN_BUFFER_FOR_QUALITY_INCREASE_MS = - AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS; - - @Nullable private DefaultAllocator allocator; - private Clock clock; - private int minBufferMs; - private int maxBufferMs; - private int bufferForPlaybackMs; - private int bufferForPlaybackAfterRebufferMs; - private int hysteresisBufferMs; - private float startUpBandwidthFraction; - private int startUpMinBufferForQualityIncreaseMs; - private DynamicFormatFilter dynamicFormatFilter; - private boolean buildCalled; - - /** Creates builder with default values. */ - public BufferSizeAdaptationBuilder() { - clock = Clock.DEFAULT; - minBufferMs = DEFAULT_MIN_BUFFER_MS; - maxBufferMs = DEFAULT_MAX_BUFFER_MS; - bufferForPlaybackMs = DEFAULT_BUFFER_FOR_PLAYBACK_MS; - bufferForPlaybackAfterRebufferMs = DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; - hysteresisBufferMs = DEFAULT_HYSTERESIS_BUFFER_MS; - startUpBandwidthFraction = DEFAULT_START_UP_BANDWIDTH_FRACTION; - startUpMinBufferForQualityIncreaseMs = DEFAULT_START_UP_MIN_BUFFER_FOR_QUALITY_INCREASE_MS; - dynamicFormatFilter = DynamicFormatFilter.NO_FILTER; - } - - /** - * Set the clock to use. Should only be set for testing purposes. - * - * @param clock The {@link Clock}. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setClock(Clock clock) { - Assertions.checkState(!buildCalled); - this.clock = clock; - return this; - } - - /** - * Sets the {@link DefaultAllocator} used by the loader. - * - * @param allocator The {@link DefaultAllocator}. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setAllocator(DefaultAllocator allocator) { - Assertions.checkState(!buildCalled); - this.allocator = allocator; - return this; - } - - /** - * Sets the buffer duration parameters. - * - * @param minBufferMs The minimum duration of media that the player will attempt to ensure is - * buffered at all times, in milliseconds. - * @param maxBufferMs The maximum duration of media that the player will attempt to buffer, in - * milliseconds. - * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or - * resume following a user action such as a seek, in milliseconds. - * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for - * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by - * buffer depletion rather than a user action. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setBufferDurationsMs( - int minBufferMs, - int maxBufferMs, - int bufferForPlaybackMs, - int bufferForPlaybackAfterRebufferMs) { - Assertions.checkState(!buildCalled); - this.minBufferMs = minBufferMs; - this.maxBufferMs = maxBufferMs; - this.bufferForPlaybackMs = bufferForPlaybackMs; - this.bufferForPlaybackAfterRebufferMs = bufferForPlaybackAfterRebufferMs; - return this; - } - - /** - * Sets the hysteresis buffer used to prevent repeated format switching. - * - * @param hysteresisBufferMs The offset the current duration of buffered media must deviate from - * the ideal duration of buffered media for the currently selected format, before the selected - * format is changed. This value must be smaller than {@code maxBufferMs - minBufferMs}. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setHysteresisBufferMs(int hysteresisBufferMs) { - Assertions.checkState(!buildCalled); - this.hysteresisBufferMs = hysteresisBufferMs; - return this; - } - - /** - * Sets track selection parameters used during the start-up phase before the selection can be made - * purely on based on buffer size. During the start-up phase the selection is based on the current - * bandwidth estimate. - * - * @param bandwidthFraction The fraction of the available bandwidth that the selection should - * consider available for use. Setting to a value less than 1 is recommended to account for - * inaccuracies in the bandwidth estimator. - * @param minBufferForQualityIncreaseMs The minimum duration of buffered media required for the - * selected track to switch to one of higher quality. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setStartUpTrackSelectionParameters( - float bandwidthFraction, int minBufferForQualityIncreaseMs) { - Assertions.checkState(!buildCalled); - this.startUpBandwidthFraction = bandwidthFraction; - this.startUpMinBufferForQualityIncreaseMs = minBufferForQualityIncreaseMs; - return this; - } - - /** - * Sets the {@link DynamicFormatFilter} to use when updating the selected track. - * - * @param dynamicFormatFilter The {@link DynamicFormatFilter}. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setDynamicFormatFilter( - DynamicFormatFilter dynamicFormatFilter) { - Assertions.checkState(!buildCalled); - this.dynamicFormatFilter = dynamicFormatFilter; - return this; - } - - /** - * Builds player components for buffer size based track adaptation. - * - * @return A pair of a {@link TrackSelection.Factory} and a {@link LoadControl}, which should be - * used to construct the player. - */ - public Pair buildPlayerComponents() { - Assertions.checkArgument(hysteresisBufferMs < maxBufferMs - minBufferMs); - Assertions.checkState(!buildCalled); - buildCalled = true; - - DefaultLoadControl.Builder loadControlBuilder = - new DefaultLoadControl.Builder() - .setTargetBufferBytes(/* targetBufferBytes = */ Integer.MAX_VALUE) - .setBufferDurationsMs( - /* minBufferMs= */ maxBufferMs, - maxBufferMs, - bufferForPlaybackMs, - bufferForPlaybackAfterRebufferMs); - if (allocator != null) { - loadControlBuilder.setAllocator(allocator); - } - - TrackSelection.Factory trackSelectionFactory = - new TrackSelection.Factory() { - @Override - public @NullableType TrackSelection[] createTrackSelections( - @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) { - return TrackSelectionUtil.createTrackSelectionsForDefinitions( - definitions, - definition -> - new BufferSizeAdaptiveTrackSelection( - definition.group, - definition.tracks, - bandwidthMeter, - minBufferMs, - maxBufferMs, - hysteresisBufferMs, - startUpBandwidthFraction, - startUpMinBufferForQualityIncreaseMs, - dynamicFormatFilter, - clock)); - } - }; - - return Pair.create(trackSelectionFactory, loadControlBuilder.createDefaultLoadControl()); - } - - private static final class BufferSizeAdaptiveTrackSelection extends BaseTrackSelection { - - private static final int BITRATE_BLACKLISTED = Format.NO_VALUE; - - private final BandwidthMeter bandwidthMeter; - private final Clock clock; - private final DynamicFormatFilter dynamicFormatFilter; - private final int[] formatBitrates; - private final long minBufferUs; - private final long maxBufferUs; - private final long hysteresisBufferUs; - private final float startUpBandwidthFraction; - private final long startUpMinBufferForQualityIncreaseUs; - private final int minBitrate; - private final int maxBitrate; - private final double bitrateToBufferFunctionSlope; - private final double bitrateToBufferFunctionIntercept; - - private boolean isInSteadyState; - private int selectedIndex; - private int selectionReason; - private float playbackSpeed; - - private BufferSizeAdaptiveTrackSelection( - TrackGroup trackGroup, - int[] tracks, - BandwidthMeter bandwidthMeter, - int minBufferMs, - int maxBufferMs, - int hysteresisBufferMs, - float startUpBandwidthFraction, - int startUpMinBufferForQualityIncreaseMs, - DynamicFormatFilter dynamicFormatFilter, - Clock clock) { - super(trackGroup, tracks); - this.bandwidthMeter = bandwidthMeter; - this.minBufferUs = C.msToUs(minBufferMs); - this.maxBufferUs = C.msToUs(maxBufferMs); - this.hysteresisBufferUs = C.msToUs(hysteresisBufferMs); - this.startUpBandwidthFraction = startUpBandwidthFraction; - this.startUpMinBufferForQualityIncreaseUs = C.msToUs(startUpMinBufferForQualityIncreaseMs); - this.dynamicFormatFilter = dynamicFormatFilter; - this.clock = clock; - - formatBitrates = new int[length]; - maxBitrate = getFormat(/* index= */ 0).bitrate; - minBitrate = getFormat(/* index= */ length - 1).bitrate; - selectionReason = C.SELECTION_REASON_UNKNOWN; - playbackSpeed = 1.0f; - - // We use a log-linear function to map from bitrate to buffer size: - // buffer = slope * ln(bitrate) + intercept, - // with buffer(minBitrate) = minBuffer and buffer(maxBitrate) = maxBuffer - hysteresisBuffer. - bitrateToBufferFunctionSlope = - (maxBufferUs - hysteresisBufferUs - minBufferUs) - / Math.log((double) maxBitrate / minBitrate); - bitrateToBufferFunctionIntercept = - minBufferUs - bitrateToBufferFunctionSlope * Math.log(minBitrate); - } - - @Override - public void onPlaybackSpeed(float playbackSpeed) { - this.playbackSpeed = playbackSpeed; - } - - @Override - public void onDiscontinuity() { - isInSteadyState = false; - } - - @Override - public int getSelectedIndex() { - return selectedIndex; - } - - @Override - public int getSelectionReason() { - return selectionReason; - } - - @Override - @Nullable - public Object getSelectionData() { - return null; - } - - @Override - public void updateSelectedTrack( - long playbackPositionUs, - long bufferedDurationUs, - long availableDurationUs, - List queue, - MediaChunkIterator[] mediaChunkIterators) { - updateFormatBitrates(/* nowMs= */ clock.elapsedRealtime()); - - // Make initial selection - if (selectionReason == C.SELECTION_REASON_UNKNOWN) { - selectionReason = C.SELECTION_REASON_INITIAL; - selectedIndex = selectIdealIndexUsingBandwidth(/* isInitialSelection= */ true); - return; - } - - long bufferUs = getCurrentPeriodBufferedDurationUs(playbackPositionUs, bufferedDurationUs); - int oldSelectedIndex = selectedIndex; - if (isInSteadyState) { - selectIndexSteadyState(bufferUs); - } else { - selectIndexStartUpPhase(bufferUs); - } - if (selectedIndex != oldSelectedIndex) { - selectionReason = C.SELECTION_REASON_ADAPTIVE; - } - } - - // Steady state. - - private void selectIndexSteadyState(long bufferUs) { - if (isOutsideHysteresis(bufferUs)) { - selectedIndex = selectIdealIndexUsingBufferSize(bufferUs); - } - } - - private boolean isOutsideHysteresis(long bufferUs) { - if (formatBitrates[selectedIndex] == BITRATE_BLACKLISTED) { - return true; - } - long targetBufferForCurrentBitrateUs = - getTargetBufferForBitrateUs(formatBitrates[selectedIndex]); - long bufferDiffUs = bufferUs - targetBufferForCurrentBitrateUs; - return Math.abs(bufferDiffUs) > hysteresisBufferUs; - } - - private int selectIdealIndexUsingBufferSize(long bufferUs) { - int lowestBitrateNonBlacklistedIndex = 0; - for (int i = 0; i < formatBitrates.length; i++) { - if (formatBitrates[i] != BITRATE_BLACKLISTED) { - if (getTargetBufferForBitrateUs(formatBitrates[i]) <= bufferUs - && dynamicFormatFilter.isFormatAllowed( - getFormat(i), formatBitrates[i], /* isInitialSelection= */ false)) { - return i; - } - lowestBitrateNonBlacklistedIndex = i; - } - } - return lowestBitrateNonBlacklistedIndex; - } - - // Startup. - - private void selectIndexStartUpPhase(long bufferUs) { - int startUpSelectedIndex = selectIdealIndexUsingBandwidth(/* isInitialSelection= */ false); - int steadyStateSelectedIndex = selectIdealIndexUsingBufferSize(bufferUs); - if (steadyStateSelectedIndex <= selectedIndex) { - // Switch to steady state if we have enough buffer to maintain current selection. - selectedIndex = steadyStateSelectedIndex; - isInSteadyState = true; - } else { - if (bufferUs < startUpMinBufferForQualityIncreaseUs - && startUpSelectedIndex < selectedIndex - && formatBitrates[selectedIndex] != BITRATE_BLACKLISTED) { - // Switching up from a non-blacklisted track is only allowed if we have enough buffer. - return; - } - selectedIndex = startUpSelectedIndex; - } - } - - private int selectIdealIndexUsingBandwidth(boolean isInitialSelection) { - long effectiveBitrate = - (long) (bandwidthMeter.getBitrateEstimate() * startUpBandwidthFraction); - int lowestBitrateNonBlacklistedIndex = 0; - for (int i = 0; i < formatBitrates.length; i++) { - if (formatBitrates[i] != BITRATE_BLACKLISTED) { - if (Math.round(formatBitrates[i] * playbackSpeed) <= effectiveBitrate - && dynamicFormatFilter.isFormatAllowed( - getFormat(i), formatBitrates[i], isInitialSelection)) { - return i; - } - lowestBitrateNonBlacklistedIndex = i; - } - } - return lowestBitrateNonBlacklistedIndex; - } - - // Utility methods. - - private void updateFormatBitrates(long nowMs) { - for (int i = 0; i < length; i++) { - if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) { - formatBitrates[i] = getFormat(i).bitrate; - } else { - formatBitrates[i] = BITRATE_BLACKLISTED; - } - } - } - - private long getTargetBufferForBitrateUs(int bitrate) { - if (bitrate <= minBitrate) { - return minBufferUs; - } - if (bitrate >= maxBitrate) { - return maxBufferUs - hysteresisBufferUs; - } - return (int) - (bitrateToBufferFunctionSlope * Math.log(bitrate) + bitrateToBufferFunctionIntercept); - } - - private static long getCurrentPeriodBufferedDurationUs( - long playbackPositionUs, long bufferedDurationUs) { - return playbackPositionUs >= 0 ? bufferedDurationUs : playbackPositionUs + bufferedDurationUs; - } - } -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptiveTrackSelectionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptiveTrackSelectionTest.java deleted file mode 100644 index 8b20630a23..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptiveTrackSelectionTest.java +++ /dev/null @@ -1,248 +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.trackselection; - -import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; - -import android.util.Pair; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.LoadControl; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import com.google.android.exoplayer2.upstream.BandwidthMeter; -import com.google.android.exoplayer2.util.MimeTypes; -import java.util.Collections; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; - -/** Unit test for the track selection created by {@link BufferSizeAdaptationBuilder}. */ -@RunWith(AndroidJUnit4.class) -public final class BufferSizeAdaptiveTrackSelectionTest { - - private static final int MIN_BUFFER_MS = 15_000; - private static final int MAX_BUFFER_MS = 50_000; - private static final int HYSTERESIS_BUFFER_MS = 10_000; - private static final float BANDWIDTH_FRACTION = 0.5f; - private static final int MIN_BUFFER_FOR_QUALITY_INCREASE_MS = 10_000; - - /** - * Factor between bitrates is always the same (=2.2). That means buffer levels should be linearly - * distributed between MIN_BUFFER=15s and MAX_BUFFER-HYSTERESIS=50s-10s=40s. - */ - private static final Format format1 = - createVideoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); - - private static final Format format2 = - createVideoFormat(/* bitrate= */ 1100, /* width= */ 640, /* height= */ 480); - private static final Format format3 = - createVideoFormat(/* bitrate= */ 2420, /* width= */ 960, /* height= */ 720); - private static final int BUFFER_LEVEL_FORMAT_2 = - (MIN_BUFFER_MS + MAX_BUFFER_MS - HYSTERESIS_BUFFER_MS) / 2; - private static final int BUFFER_LEVEL_FORMAT_3 = MAX_BUFFER_MS - HYSTERESIS_BUFFER_MS; - - @Mock private BandwidthMeter mockBandwidthMeter; - private TrackSelection trackSelection; - - @Before - public void setUp() { - initMocks(this); - Pair trackSelectionFactoryAndLoadControl = - new BufferSizeAdaptationBuilder() - .setBufferDurationsMs( - MIN_BUFFER_MS, - MAX_BUFFER_MS, - /* bufferForPlaybackMs= */ 1000, - /* bufferForPlaybackAfterRebufferMs= */ 1000) - .setHysteresisBufferMs(HYSTERESIS_BUFFER_MS) - .setStartUpTrackSelectionParameters( - BANDWIDTH_FRACTION, MIN_BUFFER_FOR_QUALITY_INCREASE_MS) - .buildPlayerComponents(); - trackSelection = - trackSelectionFactoryAndLoadControl - .first - .createTrackSelections( - new TrackSelection.Definition[] { - new TrackSelection.Definition( - new TrackGroup(format1, format2, format3), /* tracks= */ 0, 1, 2) - }, - mockBandwidthMeter)[0]; - trackSelection.enable(); - } - - @Test - public void updateSelectedTrack_usesBandwidthEstimateForInitialSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - - updateSelectedTrack(/* bufferedDurationMs= */ 0); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void updateSelectedTrack_withLowerBandwidthEstimateDuringStartUp_switchesDown() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - updateSelectedTrack(/* bufferedDurationMs= */ 0); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format1); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - @Test - public void - updateSelectedTrack_withHigherBandwidthEstimateDuringStartUp_andLowBuffer_keepsSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format3)); - - updateSelectedTrack(/* bufferedDurationMs= */ MIN_BUFFER_FOR_QUALITY_INCREASE_MS - 1); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void - updateSelectedTrack_withHigherBandwidthEstimateDuringStartUp_andHighBuffer_switchesUp() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format3)); - - updateSelectedTrack(/* bufferedDurationMs= */ MIN_BUFFER_FOR_QUALITY_INCREASE_MS); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format3); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - @Test - public void - updateSelectedTrack_withIncreasedBandwidthEstimate_onceSteadyStateBufferIsReached_keepsSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format3)); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void - updateSelectedTrack_withDecreasedBandwidthEstimate_onceSteadyStateBufferIsReached_keepsSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void updateSelectedTrack_withIncreasedBufferInSteadyState_switchesUp() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_3); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format3); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - @Test - public void updateSelectedTrack_withDecreasedBufferInSteadyState_switchesDown() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2 - HYSTERESIS_BUFFER_MS - 1); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format1); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - @Test - public void - updateSelectedTrack_withDecreasedBufferInSteadyState_withinHysteresis_keepsSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2 - HYSTERESIS_BUFFER_MS); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void onDiscontinuity_switchesBackToStartUpState() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - trackSelection.onDiscontinuity(); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2 - 1); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format1); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - private void updateSelectedTrack(long bufferedDurationMs) { - trackSelection.updateSelectedTrack( - /* playbackPositionUs= */ 0, - /* bufferedDurationUs= */ C.msToUs(bufferedDurationMs), - /* availableDurationUs= */ C.TIME_UNSET, - /* queue= */ Collections.emptyList(), - /* mediaChunkIterators= */ new MediaChunkIterator[] { - MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY - }); - } - - private static Format createVideoFormat(int bitrate, int width, int height) { - return Format.createVideoSampleFormat( - /* id= */ null, - /* sampleMimeType= */ MimeTypes.VIDEO_H264, - /* codecs= */ null, - /* bitrate= */ bitrate, - /* maxInputSize= */ Format.NO_VALUE, - /* width= */ width, - /* height= */ height, - /* frameRate= */ Format.NO_VALUE, - /* initializationData= */ null, - /* drmInitData= */ null); - } - - private static long getBitrateEstimateEnoughFor(Format format) { - return (long) (format.bitrate / BANDWIDTH_FRACTION) + 1; - } -} From fefb1a17a0023e7c22ecf78ad63796f0e4668be3 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 2 Jan 2020 10:29:16 +0000 Subject: [PATCH 0580/1335] Fix typos PiperOrigin-RevId: 287810018 --- extensions/okhttp/build.gradle | 4 ++-- .../com/google/android/exoplayer2/scheduler/Requirements.java | 2 +- .../android/exoplayer2/upstream/cache/SimpleCacheTest.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index 3af38397a8..bde4e127df 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -39,8 +39,8 @@ dependencies { testImplementation 'org.robolectric:robolectric:' + robolectricVersion // Do not update to 3.13.X or later until minSdkVersion is increased to 21: // https://cashapp.github.io/2019-02-05/okhttp-3-13-requires-android-5 - // Since OkHttp is distributed as a jar rather than an aar, Gradle wont stop - // us from making this mistake! + // Since OkHttp is distributed as a jar rather than an aar, Gradle won't + // stop us from making this mistake! api 'com.squareup.okhttp3:okhttp:3.12.5' } 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 87ea60bf73..8919a26720 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 @@ -165,7 +165,7 @@ public final class Requirements implements Parcelable { private static boolean isInternetConnectivityValidated(ConnectivityManager connectivityManager) { // It's possible to query NetworkCapabilities from API level 23, but RequirementsWatcher only // fires an event to update its Requirements when NetworkCapabilities change from API level 24. - // Since Requirements wont be updated, we assume connectivity is validated on API level 23. + // Since Requirements won't be updated, we assume connectivity is validated on API level 23. if (Util.SDK_INT < 24) { return true; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index a8dbfe3b42..4d9a936c4e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -204,7 +204,7 @@ public class SimpleCacheTest { simpleCache.releaseHoleSpan(cacheSpan2); simpleCache.removeSpan(simpleCache.getCachedSpans(KEY_2).first()); - // Don't release the cache. This means the index file wont have been written to disk after the + // Don't release the cache. This means the index file won't have been written to disk after the // data for KEY_2 was removed. Move the cache instead, so we can reload it without failing the // folder locking check. File cacheDir2 = From 77f01da66089bb2a580c0810c9c6016ac593ed2b Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 2 Jan 2020 11:37:03 +0000 Subject: [PATCH 0581/1335] Don't use WavHeader.averageBytesPerSecond It's unreliable for at least one IMA ADPCM file I've found. Calculating the blockIndex to seek to using exact properties also seems more robust. Note this doesn't change anything for the existing PCM test, since averageBytesPerSecond is set correctly. It does make a difference for an upcoming IMA ADPCM test though. PiperOrigin-RevId: 287814947 --- .../android/exoplayer2/extractor/wav/WavExtractor.java | 2 +- .../android/exoplayer2/extractor/wav/WavSeekMap.java | 10 ++++------ 2 files changed, 5 insertions(+), 7 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 37edb07a1a..c1eb357bb9 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 @@ -203,7 +203,7 @@ public final class WavExtractor implements Extractor { /* id= */ null, MimeTypes.AUDIO_RAW, /* codecs= */ null, - /* bitrate= */ header.averageBytesPerSecond * 8, + /* bitrate= */ header.frameRateHz * bytesPerFrame * 8, targetSampleSize, header.numChannels, header.frameRateHz, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java index 53e0f45306..2a92c38431 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java @@ -49,20 +49,18 @@ import com.google.android.exoplayer2.util.Util; @Override public SeekPoints getSeekPoints(long timeUs) { - // Calculate the expected number of bytes of sample data corresponding to the requested time. - long positionOffset = (timeUs * wavHeader.averageBytesPerSecond) / C.MICROS_PER_SECOND; // Calculate the containing block index, constraining to valid indices. - long blockSize = wavHeader.blockSize; - long blockIndex = Util.constrainValue(positionOffset / blockSize, 0, blockCount - 1); + long blockIndex = (timeUs * wavHeader.frameRateHz) / (C.MICROS_PER_SECOND * framesPerBlock); + blockIndex = Util.constrainValue(blockIndex, 0, blockCount - 1); - long seekPosition = firstBlockPosition + (blockIndex * blockSize); + long seekPosition = firstBlockPosition + (blockIndex * wavHeader.blockSize); long seekTimeUs = blockIndexToTimeUs(blockIndex); SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition); if (seekTimeUs >= timeUs || blockIndex == blockCount - 1) { return new SeekPoints(seekPoint); } else { long secondBlockIndex = blockIndex + 1; - long secondSeekPosition = firstBlockPosition + (secondBlockIndex * blockSize); + long secondSeekPosition = firstBlockPosition + (secondBlockIndex * wavHeader.blockSize); long secondSeekTimeUs = blockIndexToTimeUs(secondBlockIndex); SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); return new SeekPoints(seekPoint, secondSeekPoint); From cafffcb812726d662b097a1f7bd31a69799669c0 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 2 Jan 2020 12:02:41 +0000 Subject: [PATCH 0582/1335] Fix handling of E-AC-3 streams with AC-3 frames Issue: #6602 PiperOrigin-RevId: 287816831 --- RELEASENOTES.md | 2 + .../android/exoplayer2/audio/Ac3Util.java | 73 +++++++++---------- .../exoplayer2/audio/DefaultAudioSink.java | 3 +- 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d3f7cf8067..1e736dd0da 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -32,6 +32,8 @@ ([#6733](https://github.com/google/ExoPlayer/issues/6733)). Incorrect handling could previously cause downloads to be paused when they should have been able to proceed. +* Fix handling of E-AC-3 streams that contain AC-3 syncframes + ([#6602](https://github.com/google/ExoPlayer/issues/6602)). ### 2.11.1 (2019-12-20) ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java index 05c20939ff..066c9f88ef 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java @@ -31,7 +31,7 @@ import java.nio.ByteBuffer; /** * Utility methods for parsing Dolby TrueHD and (E-)AC-3 syncframes. (E-)AC-3 parsing follows the - * definition in ETSI TS 102 366 V1.2.1. + * definition in ETSI TS 102 366 V1.4.1. */ public final class Ac3Util { @@ -39,8 +39,8 @@ public final class Ac3Util { public static final class SyncFrameInfo { /** - * AC3 stream types. See also ETSI TS 102 366 E.1.3.1.1. One of {@link #STREAM_TYPE_UNDEFINED}, - * {@link #STREAM_TYPE_TYPE0}, {@link #STREAM_TYPE_TYPE1} or {@link #STREAM_TYPE_TYPE2}. + * AC3 stream types. See also E.1.3.1.1. One of {@link #STREAM_TYPE_UNDEFINED}, {@link + * #STREAM_TYPE_TYPE0}, {@link #STREAM_TYPE_TYPE1} or {@link #STREAM_TYPE_TYPE2}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -114,9 +114,7 @@ public final class Ac3Util { * The number of new samples per (E-)AC-3 audio block. */ private static final int AUDIO_SAMPLES_PER_AUDIO_BLOCK = 256; - /** - * Each syncframe has 6 blocks that provide 256 new audio samples. See ETSI TS 102 366 4.1. - */ + /** Each syncframe has 6 blocks that provide 256 new audio samples. See subsection 4.1. */ private static final int AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT = 6 * AUDIO_SAMPLES_PER_AUDIO_BLOCK; /** * Number of audio blocks per E-AC-3 syncframe, indexed by numblkscod. @@ -134,20 +132,21 @@ public final class Ac3Util { * Channel counts, indexed by acmod. */ private static final int[] CHANNEL_COUNT_BY_ACMOD = new int[] {2, 1, 2, 3, 3, 4, 4, 5}; - /** - * Nominal bitrates in kbps, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.) - */ - private static final int[] BITRATE_BY_HALF_FRMSIZECOD = new int[] {32, 40, 48, 56, 64, 80, 96, - 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640}; - /** - * 16-bit words per syncframe, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.) - */ - private static final int[] SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1 = new int[] {69, 87, 104, - 121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, 1393}; + /** Nominal bitrates in kbps, indexed by frmsizecod / 2. (See table 4.13.) */ + private static final int[] BITRATE_BY_HALF_FRMSIZECOD = + new int[] { + 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640 + }; + /** 16-bit words per syncframe, indexed by frmsizecod / 2. (See table 4.13.) */ + private static final int[] SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1 = + new int[] { + 69, 87, 104, 121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, + 1393 + }; /** - * Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to ETSI TS - * 102 366 Annex F. The reading position of {@code data} will be modified. + * Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to Annex F. + * The reading position of {@code data} will be modified. * * @param data The AC3SpecificBox to parse. * @param trackId The track identifier to set on the format. @@ -179,8 +178,8 @@ public final class Ac3Util { } /** - * Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to ETSI TS - * 102 366 Annex F. The reading position of {@code data} will be modified. + * Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to Annex + * F. The reading position of {@code data} will be modified. * * @param data The EC3SpecificBox to parse. * @param trackId The track identifier to set on the format. @@ -243,9 +242,10 @@ public final class Ac3Util { public static SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) { int initialPosition = data.getPosition(); data.skipBits(40); - boolean isEac3 = data.readBits(5) == 16; // See bsid in subsection E.1.3.1.6. + // Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6). + boolean isEac3 = data.readBits(5) > 10; data.setPosition(initialPosition); - String mimeType; + @Nullable String mimeType; @StreamType int streamType = SyncFrameInfo.STREAM_TYPE_UNDEFINED; int sampleRate; int acmod; @@ -254,7 +254,7 @@ public final class Ac3Util { boolean lfeon; int channelCount; if (isEac3) { - // Syntax from ETSI TS 102 366 V1.2.1 subsections E.1.2.1 and E.1.2.2. + // Subsection E.1.2. data.skipBits(16); // syncword switch (data.readBits(2)) { // strmtyp case 0: @@ -472,7 +472,8 @@ public final class Ac3Util { if (data.length < 6) { return C.LENGTH_UNSET; } - boolean isEac3 = ((data[5] & 0xFF) >> 3) == 16; // See bsid in subsection E.1.3.1.6. + // Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6). + boolean isEac3 = ((data[5] & 0xF8) >> 3) > 10; if (isEac3) { int frmsiz = (data[2] & 0x07) << 8; // Most significant 3 bits. frmsiz |= data[3] & 0xFF; // Least significant 8 bits. @@ -485,24 +486,22 @@ public final class Ac3Util { } /** - * Returns the number of audio samples in an AC-3 syncframe. - */ - public static int getAc3SyncframeAudioSampleCount() { - return AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT; - } - - /** - * Reads the number of audio samples represented by the given E-AC-3 syncframe. The buffer's + * Reads the number of audio samples represented by the given (E-)AC-3 syncframe. The buffer's * position is not modified. * * @param buffer The {@link ByteBuffer} from which to read the syncframe. * @return The number of audio samples represented by the syncframe. */ - public static int parseEAc3SyncframeAudioSampleCount(ByteBuffer buffer) { - // See ETSI TS 102 366 subsection E.1.2.2. - int fscod = (buffer.get(buffer.position() + 4) & 0xC0) >> 6; - return AUDIO_SAMPLES_PER_AUDIO_BLOCK * (fscod == 0x03 ? 6 - : BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(buffer.get(buffer.position() + 4) & 0x30) >> 4]); + public static int parseAc3SyncframeAudioSampleCount(ByteBuffer buffer) { + // Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6). + boolean isEac3 = ((buffer.get(buffer.position() + 5) & 0xF8) >> 3) > 10; + if (isEac3) { + int fscod = (buffer.get(buffer.position() + 4) & 0xC0) >> 6; + int numblkscod = fscod == 0x03 ? 3 : (buffer.get(buffer.position() + 4) & 0x30) >> 4; + return BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[numblkscod] * AUDIO_SAMPLES_PER_AUDIO_BLOCK; + } else { + return AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT; + } } /** 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 240a8554b7..d73cf0be40 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 @@ -1166,10 +1166,9 @@ public final class DefaultAudioSink implements AudioSink { case C.ENCODING_DTS_HD: return DtsUtil.parseDtsAudioSampleCount(buffer); case C.ENCODING_AC3: - return Ac3Util.getAc3SyncframeAudioSampleCount(); case C.ENCODING_E_AC3: case C.ENCODING_E_AC3_JOC: - return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer); + return Ac3Util.parseAc3SyncframeAudioSampleCount(buffer); case C.ENCODING_AC4: return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer); case C.ENCODING_DOLBY_TRUEHD: From a3bad3680b8384d5ebf27e454668318c6311bc5a Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 2 Jan 2020 13:13:31 +0000 Subject: [PATCH 0583/1335] Document overriding drawables for notifications Issue: #6266 PiperOrigin-RevId: 287821640 --- .../ui/PlayerNotificationManager.java | 52 +++++++++++++------ 1 file changed, 37 insertions(+), 15 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 aeb5292187..e572bc5a11 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 @@ -55,28 +55,28 @@ import java.util.List; import java.util.Map; /** - * A notification manager to start, update and cancel a media style notification reflecting the - * player state. + * Starts, updates and cancels a media style notification reflecting the player state. The actions + * displayed and the drawables used can both be customized, as described below. * *

      The notification is cancelled when {@code null} is passed to {@link #setPlayer(Player)} or * when the notification is dismissed by the user. * *

      If the player is released it must be removed from the manager by calling {@code - * setPlayer(null)} which will cancel the notification. + * setPlayer(null)}. * *

      Action customization

      * - * Standard playback actions can be shown or omitted as follows: + * Playback actions can be displayed or omitted as follows: * *
        - *
      • {@code useNavigationActions} - Sets whether the navigation previous and next actions - * are displayed. + *
      • {@code useNavigationActions} - Sets whether the previous and next actions are + * displayed. *
          *
        • Corresponding setter: {@link #setUseNavigationActions(boolean)} *
        • Default: {@code true} *
        - *
      • {@code useNavigationActionsInCompactView} - Sets whether the navigation previous and - * next actions should are displayed in compact view (including the lock screen notification). + *
      • {@code useNavigationActionsInCompactView} - Sets whether the previous and next + * actions are displayed in compact view (including the lock screen notification). *
          *
        • Corresponding setter: {@link #setUseNavigationActionsInCompactView(boolean)} *
        • Default: {@code false} @@ -98,12 +98,35 @@ import java.util.Map; *
        • Default: {@link #DEFAULT_REWIND_MS} (5000) *
        *
      • {@code fastForwardIncrementMs} - Sets the fast forward increment. If set to zero the - * fast forward action is not included in the notification. + * fast forward action is not displayed. *
          *
        • Corresponding setter: {@link #setFastForwardIncrementMs(long)} - *
        • Default: {@link #DEFAULT_FAST_FORWARD_MS} (5000) + *
        • Default: {@link #DEFAULT_FAST_FORWARD_MS} (15000) *
        *
      + * + *

      Overriding drawables

      + * + * The drawables used by PlayerNotificationManager can be overridden by drawables with the same + * names defined in your application. The drawables that can be overridden are: + * + *
        + *
      • {@code exo_notification_small_icon} - The icon passed by default to {@link + * NotificationCompat.Builder#setSmallIcon(int)}. A different icon can also be specified + * programmatically by calling {@link #setSmallIcon(int)}. + *
      • {@code exo_notification_play} - The play icon. + *
      • {@code exo_notification_pause} - The pause icon. + *
      • {@code exo_notification_rewind} - The rewind icon. + *
      • {@code exo_notification_fastforward} - The fast forward icon. + *
      • {@code exo_notification_previous} - The previous icon. + *
      • {@code exo_notification_next} - The next icon. + *
      • {@code exo_notification_stop} - The stop icon. + *
      + * + * Unlike the drawables above, the large icon (i.e. the icon passed to {@link + * NotificationCompat.Builder#setLargeIcon(Bitmap)} cannot be overridden in this way. Instead, the + * large icon is obtained from the {@link MediaDescriptionAdapter} injected when creating the + * PlayerNotificationManager. */ public class PlayerNotificationManager { @@ -154,11 +177,10 @@ public class PlayerNotificationManager { /** * Gets the large icon for the current media item. * - *

      When a bitmap initially needs to be asynchronously loaded, a placeholder (or null) can be - * returned and the bitmap asynchronously passed to the {@link BitmapCallback} once it is - * loaded. Because the adapter may be called multiple times for the same media item, the bitmap - * should be cached by the app and whenever possible be returned synchronously at subsequent - * calls for the same media item. + *

      When a bitmap needs to be loaded asynchronously, a placeholder bitmap (or null) should be + * returned. The actual bitmap should be passed to the {@link BitmapCallback} once it has been + * loaded. Because the adapter may be called multiple times for the same media item, bitmaps + * should be cached by the app and returned synchronously when possible. * *

      See {@link NotificationCompat.Builder#setLargeIcon(Bitmap)}. * From 2380f937f3c06145676eaae3224d65f09a987cc4 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 2 Jan 2020 14:40:47 +0000 Subject: [PATCH 0584/1335] Document overriding of drawables for PlayerControlView Issue: #6779 PiperOrigin-RevId: 287828273 --- .../exoplayer2/ui/PlayerControlView.java | 53 +++++++++++++++---- .../android/exoplayer2/ui/PlayerView.java | 9 +++- 2 files changed, 51 insertions(+), 11 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 a6636d71be..248ac9fdaf 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 @@ -49,8 +49,8 @@ import java.util.concurrent.CopyOnWriteArrayList; * A view for controlling {@link Player} instances. * *

      A PlayerControlView can be customized by setting attributes (or calling corresponding - * methods), overriding the view's layout file or by specifying a custom view layout file, as - * outlined below. + * methods), overriding drawables, overriding the view's layout file, or by specifying a custom view + * layout file. * *

      Attributes

      * @@ -104,6 +104,30 @@ import java.util.concurrent.CopyOnWriteArrayList; * layout is overridden to specify a custom {@code exo_progress} (see below). *
    * + *

    Overriding drawables

    + * + * The drawables used by PlayerControlView (with its default layout file) can be overridden by + * drawables with the same names defined in your application. The drawables that can be overridden + * are: + * + *
      + *
    • {@code exo_controls_play} - The play icon. + *
    • {@code exo_controls_pause} - The pause icon. + *
    • {@code exo_controls_rewind} - The rewind icon. + *
    • {@code exo_controls_fastforward} - The fast forward icon. + *
    • {@code exo_controls_previous} - The previous icon. + *
    • {@code exo_controls_next} - The next icon. + *
    • {@code exo_controls_repeat_off} - The repeat icon for {@link + * Player#REPEAT_MODE_OFF}. + *
    • {@code exo_controls_repeat_one} - The repeat icon for {@link + * Player#REPEAT_MODE_ONE}. + *
    • {@code exo_controls_repeat_all} - The repeat icon for {@link + * Player#REPEAT_MODE_ALL}. + *
    • {@code exo_controls_shuffle_off} - The shuffle icon when shuffling is disabled. + *
    • {@code exo_controls_shuffle_on} - The shuffle icon when shuffling is enabled. + *
    • {@code exo_controls_vr} - The VR icon. + *
    + * *

    Overriding the layout file

    * * To customize the layout of PlayerControlView throughout your app, or just for certain @@ -123,29 +147,38 @@ import java.util.concurrent.CopyOnWriteArrayList; *
      *
    • Type: {@link View} *
    - *
  • {@code exo_ffwd} - The fast forward button. - *
      - *
    • Type: {@link View} - *
    *
  • {@code exo_rew} - The rewind button. *
      *
    • Type: {@link View} *
    - *
  • {@code exo_prev} - The previous track button. + *
  • {@code exo_ffwd} - The fast forward button. *
      *
    • Type: {@link View} *
    - *
  • {@code exo_next} - The next track button. + *
  • {@code exo_prev} - The previous button. + *
      + *
    • Type: {@link View} + *
    + *
  • {@code exo_next} - The next button. *
      *
    • Type: {@link View} *
    *
  • {@code exo_repeat_toggle} - The repeat toggle button. *
      - *
    • Type: {@link View} + *
    • Type: {@link ImageView} + *
    • Note: PlayerControlView will programmatically set the drawable on the repeat toggle + * button according to the player's current repeat mode. The drawables used are {@code + * exo_controls_repeat_off}, {@code exo_controls_repeat_one} and {@code + * exo_controls_repeat_all}. See the section above for information on overriding these + * drawables. *
    *
  • {@code exo_shuffle} - The shuffle button. *
      - *
    • Type: {@link View} + *
    • Type: {@link ImageView} + *
    • Note: PlayerControlView will programmatically set the drawable on the shuffle button + * according to the player's current repeat mode. The drawables used are {@code + * exo_controls_shuffle_off} and {@code exo_controls_shuffle_on}. See the section above + * for information on overriding these drawables. *
    *
  • {@code exo_vr} - The VR mode button. *
      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 c55fe09f76..03168643cf 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 @@ -79,7 +79,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * during playback, and displays playback controls using a {@link PlayerControlView}. * *

      A PlayerView can be customized by setting attributes (or calling corresponding methods), - * overriding the view's layout file or by specifying a custom view layout file, as outlined below. + * overriding drawables, overriding the view's layout file, or by specifying a custom view layout + * file. * *

      Attributes

      * @@ -172,6 +173,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * exo_controller} (see below). *
    * + *

    Overriding drawables

    + * + * The drawables used by {@link PlayerControlView} (with its default layout file) can be overridden + * by drawables with the same names defined in your application. See the {@link PlayerControlView} + * documentation for a list of drawables that can be overridden. + * *

    Overriding the layout file

    * * To customize the layout of PlayerView throughout your app, or just for certain configurations, From f0e0ee421f02595f89c9b228676f81f1796ca466 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 2 Jan 2020 14:44:16 +0000 Subject: [PATCH 0585/1335] Support twos codec in MP4 Issue: #5789 PiperOrigin-RevId: 287828559 --- RELEASENOTES.md | 2 ++ .../java/com/google/android/exoplayer2/C.java | 17 +++++++++----- .../audio/ResamplingAudioProcessor.java | 23 +++++++++++++++---- .../exoplayer2/extractor/mp4/Atom.java | 3 +++ .../exoplayer2/extractor/mp4/AtomParsers.java | 9 +++++--- .../google/android/exoplayer2/util/Util.java | 2 ++ 6 files changed, 42 insertions(+), 14 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1e736dd0da..12b08c3c80 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -34,6 +34,8 @@ to proceed. * Fix handling of E-AC-3 streams that contain AC-3 syncframes ([#6602](https://github.com/google/ExoPlayer/issues/6602)). +* Support "twos" codec (big endian PCM) in MP4 + ([#5789](https://github.com/google/ExoPlayer/issues/5789)). ### 2.11.1 (2019-12-20) ### 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 e431b2d899..776e79df97 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 @@ -150,10 +150,11 @@ public final class C { /** * Represents an audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE}, * {@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_MP3}, {@link - * #ENCODING_AC3}, {@link #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, - * {@link #ENCODING_DTS}, {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}. + * #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, + * {@link #ENCODING_PCM_FLOAT}, {@link #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link + * #ENCODING_MP3}, {@link #ENCODING_AC3}, {@link #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) @@ -162,6 +163,7 @@ public final class C { ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, + ENCODING_PCM_16BIT_BIG_ENDIAN, ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT, @@ -181,8 +183,8 @@ public final class C { /** * Represents a PCM audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE}, * {@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} or {@link #ENCODING_PCM_A_LAW}. + * #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, + * {@link #ENCODING_PCM_FLOAT}, {@link #ENCODING_PCM_MU_LAW} or {@link #ENCODING_PCM_A_LAW}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -191,6 +193,7 @@ public final class C { ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, + ENCODING_PCM_16BIT_BIG_ENDIAN, ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT, @@ -204,6 +207,8 @@ public final class C { public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT; /** @see AudioFormat#ENCODING_PCM_16BIT */ public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT; + /** Like {@link #ENCODING_PCM_16BIT}, but with the bytes in big endian order. */ + public static final int ENCODING_PCM_16BIT_BIG_ENDIAN = 0x08000000; /** PCM encoding with 24 bits per sample. */ public static final int ENCODING_PCM_24BIT = 0x80000000; /** PCM encoding with 32 bits per sample. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java index 1bfa1897c8..30bd4da472 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java @@ -29,8 +29,11 @@ import java.nio.ByteBuffer; public AudioFormat onConfigure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException { @C.PcmEncoding int encoding = inputAudioFormat.encoding; - if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT - && encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) { + if (encoding != C.ENCODING_PCM_8BIT + && encoding != C.ENCODING_PCM_16BIT + && encoding != C.ENCODING_PCM_16BIT_BIG_ENDIAN + && encoding != C.ENCODING_PCM_24BIT + && encoding != C.ENCODING_PCM_32BIT) { throw new UnhandledAudioFormatException(inputAudioFormat); } return encoding != C.ENCODING_PCM_16BIT @@ -50,6 +53,9 @@ import java.nio.ByteBuffer; case C.ENCODING_PCM_8BIT: resampledSize = size * 2; break; + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: + resampledSize = size; + break; case C.ENCODING_PCM_24BIT: resampledSize = (size / 3) * 2; break; @@ -70,21 +76,28 @@ import java.nio.ByteBuffer; ByteBuffer buffer = replaceOutputBuffer(resampledSize); switch (inputAudioFormat.encoding) { case C.ENCODING_PCM_8BIT: - // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. + // 8 -> 16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. for (int i = position; i < limit; i++) { buffer.put((byte) 0); buffer.put((byte) ((inputBuffer.get(i) & 0xFF) - 128)); } break; + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: + // Big endian to little endian resampling. Swap the byte order. + for (int i = position; i < limit; i += 2) { + buffer.put(inputBuffer.get(i + 1)); + buffer.put(inputBuffer.get(i)); + } + break; case C.ENCODING_PCM_24BIT: - // 24->16 bit resampling. Drop the least significant byte. + // 24 -> 16 bit resampling. Drop the least significant byte. for (int i = position; i < limit; i += 3) { buffer.put(inputBuffer.get(i + 1)); buffer.put(inputBuffer.get(i + 2)); } break; case C.ENCODING_PCM_32BIT: - // 32->16 bit resampling. Drop the two least significant bytes. + // 32 -> 16 bit resampling. Drop the two least significant bytes. for (int i = position; i < limit; i += 4) { buffer.put(inputBuffer.get(i + 2)); buffer.put(inputBuffer.get(i + 3)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index 572efed1af..e86a873ed5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -379,6 +379,9 @@ import java.util.List; @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_dfLa = 0x64664c61; + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_twos = 0x74776f73; + public final int type; public Atom(int type) { 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 f6b4f4d463..8f2a244d59 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 @@ -798,6 +798,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; || childAtomType == Atom.TYPE_sawb || childAtomType == Atom.TYPE_lpcm || childAtomType == Atom.TYPE_sowt + || childAtomType == Atom.TYPE_twos || childAtomType == Atom.TYPE__mp3 || childAtomType == Atom.TYPE_alac || childAtomType == Atom.TYPE_alaw @@ -1086,6 +1087,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; int channelCount; int sampleRate; + @C.PcmEncoding int pcmEncoding = Format.NO_VALUE; if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) { channelCount = parent.readUnsignedShort(); @@ -1147,6 +1149,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; mimeType = MimeTypes.AUDIO_AMR_WB; } else if (atomType == Atom.TYPE_lpcm || atomType == Atom.TYPE_sowt) { mimeType = MimeTypes.AUDIO_RAW; + pcmEncoding = C.ENCODING_PCM_16BIT; + } else if (atomType == Atom.TYPE_twos) { + mimeType = MimeTypes.AUDIO_RAW; + pcmEncoding = C.ENCODING_PCM_16BIT_BIG_ENDIAN; } else if (atomType == Atom.TYPE__mp3) { mimeType = MimeTypes.AUDIO_MPEG; } else if (atomType == Atom.TYPE_alac) { @@ -1233,9 +1239,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } if (out.format == null && mimeType != null) { - // TODO: Determine the correct PCM encoding. - @C.PcmEncoding int pcmEncoding = - MimeTypes.AUDIO_RAW.equals(mimeType) ? C.ENCODING_PCM_16BIT : Format.NO_VALUE; out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, pcmEncoding, initializationData == null ? null : Collections.singletonList(initializationData), 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 54e65797f0..aa87096ebb 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 @@ -1355,6 +1355,7 @@ public final class Util { public static boolean isEncodingLinearPcm(@C.Encoding int encoding) { return encoding == C.ENCODING_PCM_8BIT || encoding == C.ENCODING_PCM_16BIT + || encoding == C.ENCODING_PCM_16BIT_BIG_ENDIAN || encoding == C.ENCODING_PCM_24BIT || encoding == C.ENCODING_PCM_32BIT || encoding == C.ENCODING_PCM_FLOAT; @@ -1423,6 +1424,7 @@ public final class Util { case C.ENCODING_PCM_8BIT: return channelCount; case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: return channelCount * 2; case C.ENCODING_PCM_24BIT: return channelCount * 3; From 826083db923f26f2000ca42f7b54b9531726d713 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 2 Jan 2020 18:19:41 +0000 Subject: [PATCH 0586/1335] Add support for IMA ADPCM in WAV PiperOrigin-RevId: 287854701 --- RELEASENOTES.md | 1 + .../android/exoplayer2/audio/WavUtil.java | 12 +- .../extractor/wav/WavExtractor.java | 314 +++++++++++++++++- .../src/test/assets/wav/sample_ima_adpcm.wav | Bin 0 -> 22622 bytes .../assets/wav/sample_ima_adpcm.wav.0.dump | 75 +++++ .../assets/wav/sample_ima_adpcm.wav.1.dump | 59 ++++ .../assets/wav/sample_ima_adpcm.wav.2.dump | 47 +++ .../assets/wav/sample_ima_adpcm.wav.3.dump | 35 ++ .../extractor/wav/WavExtractorTest.java | 5 + 9 files changed, 526 insertions(+), 22 deletions(-) create mode 100644 library/core/src/test/assets/wav/sample_ima_adpcm.wav create mode 100644 library/core/src/test/assets/wav/sample_ima_adpcm.wav.0.dump create mode 100644 library/core/src/test/assets/wav/sample_ima_adpcm.wav.1.dump create mode 100644 library/core/src/test/assets/wav/sample_ima_adpcm.wav.2.dump create mode 100644 library/core/src/test/assets/wav/sample_ima_adpcm.wav.3.dump diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 12b08c3c80..2dba34486b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -36,6 +36,7 @@ ([#6602](https://github.com/google/ExoPlayer/issues/6602)). * Support "twos" codec (big endian PCM) in MP4 ([#5789](https://github.com/google/ExoPlayer/issues/5789)). +* WAV: Support IMA ADPCM encoded data. ### 2.11.1 (2019-12-20) ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java index 29b772f838..25261f1686 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java @@ -32,15 +32,17 @@ public final class WavUtil { public static final int DATA_FOURCC = 0x64617461; /** WAVE type value for integer PCM audio data. */ - private static final int TYPE_PCM = 0x0001; + public static final int TYPE_PCM = 0x0001; /** WAVE type value for float PCM audio data. */ - private static final int TYPE_FLOAT = 0x0003; + public static final int TYPE_FLOAT = 0x0003; /** WAVE type value for 8-bit ITU-T G.711 A-law audio data. */ - private static final int TYPE_A_LAW = 0x0006; + public static final int TYPE_A_LAW = 0x0006; /** WAVE type value for 8-bit ITU-T G.711 mu-law audio data. */ - private static final int TYPE_MU_LAW = 0x0007; + public static final int TYPE_MU_LAW = 0x0007; + /** WAVE type value for IMA ADPCM audio data. */ + public static final int TYPE_IMA_ADPCM = 0x0011; /** WAVE type value for extended WAVE format. */ - private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; + public static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; /** * Returns the WAVE format type value for the given {@link C.PcmEncoding}. 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 c1eb357bb9..0c6e538f43 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 @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -91,12 +92,16 @@ public final class WavExtractor implements Extractor { throw new ParserException("Unsupported or unrecognized wav header."); } - @C.PcmEncoding - int pcmEncoding = WavUtil.getPcmEncodingForType(header.formatType, header.bitsPerSample); - if (pcmEncoding == C.ENCODING_INVALID) { - throw new ParserException("Unsupported WAV format type: " + header.formatType); + if (header.formatType == WavUtil.TYPE_IMA_ADPCM) { + outputWriter = new ImaAdPcmOutputWriter(extractorOutput, trackOutput, header); + } else { + @C.PcmEncoding + int pcmEncoding = WavUtil.getPcmEncodingForType(header.formatType, header.bitsPerSample); + if (pcmEncoding == C.ENCODING_INVALID) { + throw new ParserException("Unsupported WAV format type: " + header.formatType); + } + outputWriter = new PcmOutputWriter(extractorOutput, trackOutput, header, pcmEncoding); } - outputWriter = new PcmOutputWriter(extractorOutput, trackOutput, header, pcmEncoding); } if (dataStartPosition == C.POSITION_UNSET) { @@ -156,11 +161,22 @@ public final class WavExtractor implements Extractor { private final TrackOutput trackOutput; private final WavHeader header; private final @C.PcmEncoding int pcmEncoding; - private final int targetSampleSize; + /** The target size of each output sample, in bytes. */ + private final int targetSampleSizeBytes; + /** The time at which the writer was last {@link #reset}. */ private long startTimeUs; + /** + * The number of bytes that have been written to {@link #trackOutput} but have yet to be + * included as part of a sample (i.e. the corresponding call to {@link + * TrackOutput#sampleMetadata} has yet to be made). + */ + private int pendingOutputBytes; + /** + * The total number of frames in samples that have been written to the trackOutput since the + * last call to {@link #reset}. + */ private long outputFrameCount; - private int pendingBytes; public PcmOutputWriter( ExtractorOutput extractorOutput, @@ -173,15 +189,15 @@ public final class WavExtractor implements Extractor { this.pcmEncoding = pcmEncoding; // For PCM blocks correspond to single frames. This is validated in init(int, long). int bytesPerFrame = header.blockSize; - targetSampleSize = + targetSampleSizeBytes = Math.max(bytesPerFrame, header.frameRateHz * bytesPerFrame / TARGET_SAMPLES_PER_SECOND); } @Override public void reset(long timeUs) { startTimeUs = timeUs; + pendingOutputBytes = 0; outputFrameCount = 0; - pendingBytes = 0; } @Override @@ -204,7 +220,7 @@ public final class WavExtractor implements Extractor { MimeTypes.AUDIO_RAW, /* codecs= */ null, /* bitrate= */ header.frameRateHz * bytesPerFrame * 8, - targetSampleSize, + /* maxInputSize= */ targetSampleSizeBytes, header.numChannels, header.frameRateHz, pcmEncoding, @@ -220,34 +236,298 @@ public final class WavExtractor implements Extractor { throws IOException, InterruptedException { // Write sample data until we've reached the target sample size, or the end of the data. boolean endOfSampleData = bytesLeft == 0; - while (!endOfSampleData && pendingBytes < targetSampleSize) { - int bytesToRead = (int) Math.min(targetSampleSize - pendingBytes, bytesLeft); + while (!endOfSampleData && pendingOutputBytes < targetSampleSizeBytes) { + int bytesToRead = (int) Math.min(targetSampleSizeBytes - pendingOutputBytes, bytesLeft); int bytesAppended = trackOutput.sampleData(input, bytesToRead, true); if (bytesAppended == RESULT_END_OF_INPUT) { endOfSampleData = true; } else { - pendingBytes += bytesAppended; + pendingOutputBytes += bytesAppended; } } // Write the corresponding sample metadata. Samples must be a whole number of frames. It's - // possible pendingBytes is not a whole number of frames if the stream ended unexpectedly. + // possible that the number of pending output bytes is not a whole number of frames if the + // stream ended unexpectedly. int bytesPerFrame = header.blockSize; - int pendingFrames = pendingBytes / bytesPerFrame; + int pendingFrames = pendingOutputBytes / bytesPerFrame; if (pendingFrames > 0) { long timeUs = startTimeUs + Util.scaleLargeTimestamp( outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz); int size = pendingFrames * bytesPerFrame; - int offset = pendingBytes - size; + int offset = pendingOutputBytes - size; trackOutput.sampleMetadata( timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null); outputFrameCount += pendingFrames; - pendingBytes = offset; + pendingOutputBytes = offset; } return endOfSampleData; } } + + private static final class ImaAdPcmOutputWriter implements OutputWriter { + + private static final int[] INDEX_TABLE = { + -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8 + }; + + private static final int[] STEP_TABLE = { + 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66, + 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, + 449, 494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, + 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, + 9493, 10442, 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, + 32767 + }; + + private final ExtractorOutput extractorOutput; + private final TrackOutput trackOutput; + private final WavHeader header; + /** The target size of each output sample, in frames. */ + private final int targetSampleSizeFrames; + + // Properties of the input (yet to be decoded) data. + private int framesPerBlock; + private byte[] inputData; + private int pendingInputBytes; + + // Target for decoded (yet to be output) data. + private ParsableByteArray decodedData; + + // Properties of the output. + /** The time at which the writer was last {@link #reset}. */ + private long startTimeUs; + /** + * The number of bytes that have been written to {@link #trackOutput} but have yet to be + * included as part of a sample (i.e. the corresponding call to {@link + * TrackOutput#sampleMetadata} has yet to be made). + */ + private int pendingOutputBytes; + /** + * The total number of frames in samples that have been written to the trackOutput since the + * last call to {@link #reset}. + */ + private long outputFrameCount; + + public ImaAdPcmOutputWriter( + ExtractorOutput extractorOutput, TrackOutput trackOutput, WavHeader header) { + this.extractorOutput = extractorOutput; + this.trackOutput = trackOutput; + this.header = header; + targetSampleSizeFrames = Math.max(1, header.frameRateHz / TARGET_SAMPLES_PER_SECOND); + } + + @Override + public void reset(long timeUs) { + // Reset the input side. + pendingInputBytes = 0; + // Reset the output side. + startTimeUs = timeUs; + pendingOutputBytes = 0; + outputFrameCount = 0; + } + + @Override + public void init(int dataStartPosition, long dataEndPosition) throws ParserException { + // Validate the header. + ParsableByteArray scratch = new ParsableByteArray(header.extraData); + scratch.readLittleEndianUnsignedShort(); + framesPerBlock = scratch.readLittleEndianUnsignedShort(); + // This calculation is defined in "Microsoft Multimedia Standards Update - New Multimedia + // Types and Data Techniques" (1994). See the "IMA ADPCM Wave Type" and + // "DVI ADPCM Wave Type" sections, and the calculation of wSamplesPerBlock in the latter. + int numChannels = header.numChannels; + int expectedFramesPerBlock = + (((header.blockSize - (4 * numChannels)) * 8) / (header.bitsPerSample * numChannels)) + 1; + if (framesPerBlock != expectedFramesPerBlock) { + throw new ParserException( + "Expected frames per block: " + expectedFramesPerBlock + "; got: " + framesPerBlock); + } + + // Calculate the number of blocks we'll need to decode to obtain an output sample of the + // target sample size, and allocate suitably sized buffers for input and decoded data. + int maxBlocksToDecode = Util.ceilDivide(targetSampleSizeFrames, framesPerBlock); + inputData = new byte[maxBlocksToDecode * header.blockSize]; + decodedData = + new ParsableByteArray(maxBlocksToDecode * numOutputFramesToBytes(framesPerBlock)); + + // Output the seek map. + extractorOutput.seekMap( + new WavSeekMap(header, framesPerBlock, dataStartPosition, dataEndPosition)); + + // Output the format. We calculate the bitrate of the data before decoding, since this is the + // bitrate of the stream itself. + int bitrate = header.frameRateHz * header.blockSize * 8 / framesPerBlock; + Format format = + Format.createAudioSampleFormat( + /* id= */ null, + MimeTypes.AUDIO_RAW, + /* codecs= */ null, + bitrate, + /* maxInputSize= */ numOutputFramesToBytes(targetSampleSizeFrames), + header.numChannels, + header.frameRateHz, + C.ENCODING_PCM_16BIT, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); + trackOutput.format(format); + } + + @Override + public boolean sampleData(ExtractorInput input, long bytesLeft) + throws IOException, InterruptedException { + // Calculate the number of additional frames that we need on the output side to complete a + // sample of the target size. + int targetFramesRemaining = + targetSampleSizeFrames - numOutputBytesToFrames(pendingOutputBytes); + // Calculate the whole number of blocks that we need to decode to obtain this many frames. + int blocksToDecode = Util.ceilDivide(targetFramesRemaining, framesPerBlock); + int targetReadBytes = blocksToDecode * header.blockSize; + + // Read input data until we've reached the target number of blocks, or the end of the data. + boolean endOfSampleData = bytesLeft == 0; + while (!endOfSampleData && pendingInputBytes < targetReadBytes) { + int bytesToRead = (int) Math.min(targetReadBytes - pendingInputBytes, bytesLeft); + int bytesAppended = input.read(inputData, pendingInputBytes, bytesToRead); + if (bytesAppended == RESULT_END_OF_INPUT) { + endOfSampleData = true; + } else { + pendingInputBytes += bytesAppended; + } + } + + int pendingBlockCount = pendingInputBytes / header.blockSize; + if (pendingBlockCount > 0) { + // We have at least one whole block to decode. + decode(inputData, pendingBlockCount, decodedData); + pendingInputBytes -= pendingBlockCount * header.blockSize; + + // Write all of the decoded data to the track output. + int decodedDataSize = decodedData.limit(); + trackOutput.sampleData(decodedData, decodedDataSize); + pendingOutputBytes += decodedDataSize; + + // Output the next sample at the target size. + int pendingOutputFrames = numOutputBytesToFrames(pendingOutputBytes); + if (pendingOutputFrames >= targetSampleSizeFrames) { + writeSampleMetadata(targetSampleSizeFrames); + } + } + + // If we've reached the end of the data, we might need to output a final partial sample. + if (endOfSampleData) { + int pendingOutputFrames = numOutputBytesToFrames(pendingOutputBytes); + if (pendingOutputFrames > 0) { + writeSampleMetadata(pendingOutputFrames); + } + } + + return endOfSampleData; + } + + private void writeSampleMetadata(int sampleFrames) { + long timeUs = + startTimeUs + + Util.scaleLargeTimestamp(outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz); + int size = numOutputFramesToBytes(sampleFrames); + int offset = pendingOutputBytes - size; + trackOutput.sampleMetadata( + timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null); + outputFrameCount += sampleFrames; + pendingOutputBytes -= size; + } + + /** + * Decodes IMA ADPCM data to 16 bit PCM. + * + * @param input The input data to decode. + * @param blockCount The number of blocks to decode. + * @param output The output into which the decoded data will be written. + */ + private void decode(byte[] input, int blockCount, ParsableByteArray output) { + for (int blockIndex = 0; blockIndex < blockCount; blockIndex++) { + for (int channelIndex = 0; channelIndex < header.numChannels; channelIndex++) { + decodeBlockForChannel(input, blockIndex, channelIndex, output.data); + } + } + int decodedDataSize = numOutputFramesToBytes(framesPerBlock * blockCount); + output.reset(decodedDataSize); + } + + private void decodeBlockForChannel( + byte[] input, int blockIndex, int channelIndex, byte[] output) { + int blockSize = header.blockSize; + int numChannels = header.numChannels; + + // The input data consists for a four byte header [Ci] for each of the N channels, followed + // by interleaved data segments [Ci-DATAj], each of which are four bytes long. + // + // [C1][C2]...[CN] [C1-Data0][C2-Data0]...[CN-Data0] [C1-Data1][C2-Data1]...[CN-Data1] etc + // + // Compute the start indices for the [Ci] and [Ci-Data0] for the current channel, as well as + // the number of data bytes for the channel in the block. + int blockStartIndex = blockIndex * blockSize; + int headerStartIndex = blockStartIndex + channelIndex * 4; + int dataStartIndex = headerStartIndex + numChannels * 4; + int dataSizeBytes = blockSize / numChannels - 4; + + // Decode initialization. Casting to a short is necessary for the most significant bit to be + // treated as -2^15 rather than 2^15. + int predictedSample = + (short) (((input[headerStartIndex + 1] & 0xFF) << 8) | (input[headerStartIndex] & 0xFF)); + int stepIndex = Math.min(input[headerStartIndex + 2] & 0xFF, 88); + int step = STEP_TABLE[stepIndex]; + + // Output the initial 16 bit PCM sample from the header. + int outputIndex = (blockIndex * framesPerBlock * numChannels + channelIndex) * 2; + output[outputIndex] = (byte) (predictedSample & 0xFF); + output[outputIndex + 1] = (byte) (predictedSample >> 8); + + // We examine each data byte twice during decode. + for (int i = 0; i < dataSizeBytes * 2; i++) { + int dataSegmentIndex = i / 8; + int dataSegmentOffset = (i / 2) % 4; + int dataIndex = dataStartIndex + (dataSegmentIndex * numChannels * 4) + dataSegmentOffset; + + int originalSample = input[dataIndex] & 0xFF; + if (i % 2 == 0) { + originalSample &= 0x0F; // Bottom four bits. + } else { + originalSample >>= 4; // Top four bits. + } + + int delta = originalSample & 0x07; + int difference = ((2 * delta + 1) * step) >> 3; + + if ((originalSample & 0x08) != 0) { + difference = -difference; + } + + predictedSample += difference; + predictedSample = Util.constrainValue(predictedSample, /* min= */ -32768, /* max= */ 32767); + + // Output the next 16 bit PCM sample to the correct position in the output. + outputIndex += 2 * numChannels; + output[outputIndex] = (byte) (predictedSample & 0xFF); + output[outputIndex + 1] = (byte) (predictedSample >> 8); + + stepIndex += INDEX_TABLE[originalSample]; + stepIndex = Util.constrainValue(stepIndex, /* min= */ 0, /* max= */ STEP_TABLE.length - 1); + step = STEP_TABLE[stepIndex]; + } + } + + private int numOutputBytesToFrames(int bytes) { + return bytes / (2 * header.numChannels); + } + + private int numOutputFramesToBytes(int frames) { + return frames * 2 * header.numChannels; + } + } } diff --git a/library/core/src/test/assets/wav/sample_ima_adpcm.wav b/library/core/src/test/assets/wav/sample_ima_adpcm.wav new file mode 100644 index 0000000000000000000000000000000000000000..661d54d1d716df8f4e7deef62e4cdf8a30783b44 GIT binary patch literal 22622 zcmeFZ-EZV(p6~aNr8$QzIRVM4-l>v-fN$!Ne2{~xBCFj!3j``wsXg9vkXTZCcF*0o zd(NJ#T*MyanpK_zB-I;`@;Ly>s&-1=T<}dj@*)>1lIrQXQ@zNZuw() zCwJ>vVDr`9=3iI?ZM0vy#3G;P`F($%@At?3_a8p|`JWN;r@f!={`r6VE3r-p;R!_s z86oC>g@0IEBQ*KDfBxqmJ^9rd{_)R$L&(2=|KQOEe*gY|{qVne|KQ=H{~W*n*B|}& zf8M?SFFUP&*>1JSFF*R#M+BexPk;T>9r&j^@K1N(|9=Ne2ImWYS@`91JVi0Qzzf8U z(_pqrnSD*TNw?^jrQK#%h?k}FSf^*_jVH3tiVFR@EQ0pkn$MDpq^Vbusy+<&RDmp3 zY2f4hl9XYje#<|EF9C~c+(MgHOiX4N*kRTPUu5Dy5v;&rWO|-5dRLg` z>@YFoWVYR<@WH(6bYxoe94i*-*b)1RFgq_As_Z2}B7RsqOVh>Torj7UZ>+@$kN+^7 zoe+7tEI9tED4KsRQ;ZqENu0;ZY02uUPoz81>ZZz37vV<(B|3K~I$o^v3~5ip>VDmv zO?t8*?aRU}B*7{b|CF}#aS#NoE=XpuELw&t(LLFXC%s2?ca;P|7&moIy*kgUqCVv) zSr<;fE}We~M@Uv19pyBne8y1RlQQAlW{m_bqh?9N(A-g?ctRx%#d~2QlAbbLEc5jz zDxEv~-A|WzOznF7A`Jtc^S%zZb_J91-Qc^i^jPCh(}iaYIwoqs5q~77bx~l57~NaJrb}59EO?8w#~Z%Z#H0%H@293St|p zgBM&;|1u8^Rpn(fFDGPqQ-r-;#Y|X^rXt)(`6{)gVf7-b)_$dol6=B>i;UYc3y3pbMkkunj zs{7oUXKAXefNl z8A6<#aK_MQkH*&y=^b4O>VyykhZS4 zF}n!gu*9^C%~877C1+*A3InY&4$@WA;+c*+HqXn6w5!rgkTlvt%(}}l!3x|_z4C9u zK$2=htoJ&WnXs=!Hkp@)SSTdgK2W_ynT1@v z75sccOn=ezs{A7%TG%|t4{s8YX5~6ZFN%hKPoXG3;HO_roYsDgpOzD*QOlbYG1kKJ zU+7qj%aHJc(LAxYHhJ1jqV2VDHISEIrU}2b?B%$tP*jsU-3DK{x;>b zJM#(Gr{gl*SWA`+>XBCCTMj(&-+~;$e6> z-cotmP)9c_han6l@scuqYH^nPl^qa6Kh7A3EErKH?K%!|I^h&A5bp#D>%OY;{ zh2s1mW;Hjok|f<2XkKE_oUDw3Cd5^JJ&C$hHPG^N`=UC!nCHbbNiD zY^!)aNw`YPd=+dx(uUWeI1t=%-u$-;S&SLOoHaGdr}Fuw-7qxa_?aYotI!zK{OJ6t zxI0{AEazJ+G3q=veO1PR>N3L1@>RIs878EoSK`E8tati+T#i|e$K@IIv-8DMv#H&q z;z=B5jMNR9m9Pzp&3pUWblIeNw|S2;Z^k<+KTSC9o4nDHx#=SD4?pSG zrb%cAO{G(dmuc%EbyhaD-<9dlc855~8=EG}lPF%~d9$zh@vB49`gK~abAn8nlO45w zv2w(Y;wIHjJqUZeZ6}GS9%s$I7KweGjxK|Ei|Y_q^djg1%5zB29LUqJtDMdcizaDn zyt`U8+p_yD+urnXvS?<|l&OZ$q}(w2K%=veRTUw&80B}k$D5P5ziBwA+i`tfu7 z5xzpRU&)iCQDG^Gc>b*>8m&&)UhS;NGDnnkUY>><_3zWxK(-Rx$&PUPlv>>6mhV@4 zGD_P1+H`5fJC77M3)q*JPP_UA)b;O$0ftvG0h-KcQoocOAUo{C!!LM_hjnQ zmgqPXk21FHm5fxMJtKWOC_bp0X3F)~;_HxTE)&=>bLOyyBJjUE4|i(P%{DJv*UuQH zEz`mL1Laox$%N-9bF~bIRaw7tRX!6t0v&{6#f`&H6p5NG&R30Ee-!6gGY+hmq5$&^fqBZuF)Md4(6OmMXn0GvczH!KR-%#B|HI2L{gIo3QnlPQEwf z*7BHhQNM*`VjLW;s!H9A?*F{zUyXfMuzZm-I>PG{UzA7ZK|`iQSqK)}{W|BRX@fq6 zoQSG5u5Jd40OdM-wC2Y_Q}Dz6zDkidPQ&B`Uj8Oc&1R?4t8>%K2{Vu>H^_X}l9;}# z{2!ArNyfdNPQ5DDRUz6KoR+rSRa`^y7w1VeXy__^b&hi*h}Z!o&699rVEWQ#-J3U# z&a1rlmCca@+AK4cMMWf{L8VPoj2NlE=3h4Z!+4Tx)Q!{7wz%?58jBz5k`-ODhT!@k zi6(|hFRIeu_|17k>5k&`AO4|2menUR9i3y*uEyd(rK60I<)g(aPx_sj%ku6`vc2j0 z3{TFQT3=bb!kSahRsm75n(PC;GCLpRS+2_i)Ru`z;^Y@K-j&@F>P7EV=F6>jKV6ko zN#G^99+fu1)!+YjH5>)}?e-&*L??vfN6k$VrA>N%&d3MC&0<|fkqfPXFw8G4u_MiF zzbP3qVLJL3i#JVKrKy`N6zMi}E(tiwt$NMlWziXUS-2&m=Ll?nPf@NXP5u-M7aMpO zd>!(VCZBG|2ct628v6phWF*yO;$#(mphiLQU{^i;D&+by31Wsme(t0G>330YLN(vsYuzIH*O}H%Z~N@73Im z&ZtCr)kncH$=& zZ!Pl%PKg&G=~q}$RwDDZ^69E~c_M1cF*d_0vs?FgGh?{nNw}@()BMG_-P^?TwkTeP z+;u{KwqnUH_0P!~33%o#+qyTX&B}mWhmCuZxh0E`=;OtBUlp#h#kN7m=lBsql(-^o z@^O&b?CEOK8qogYlI1zV?5nl>JTZ)}%UYC&Rozsj<5k);WcPDU=pSYH?VyhjMR^h~ zSg1l`m<=zE=O{h$Mab_fd6K=a4vX+{jXc)L*^5*(b7xPdf@DHkn`9P_dsCYW9+{*{ zqcHPBJdt>@jGKxkXvXxs=ye3nY{*LgWs+^Fm5XJ*t=E#Y2)X%ky!|2i2eh4?hXN<^ ze6So_qKXR1?Ru6Szpat^NlTzFiUhcTT&0ZzW%@dlc<#qN|9CE>U3Sd zME7Yw(1+!=7m8jUd?4^dc5lj-4wba5{!*(gigj%m1UQ3ao~=8 zt-RT@hM?BuAb6mn^l~zZPr~Y_lc4t)hfuYzT&%YHio0y$SHRIW*}462heAnE_#aAJ zqGX!tX=kq|ev_veiD#zO>AFVGUpeexGvh!+n+%3?A~u3D;T6*B>d<%VJAD{_V1<*WExms3Zk+k*){<6NA)9G^g>@vLWds zmc%QZ8beO1xp|tcOFgYtpkki|^DNj>WWIkeEGKqYj{P<*x`!e!lQ~OuBsGYNxN%P& zO_(IK0;vY&gp;LTGubG2cJ8>SkPwVjql?DBy01`YOGmC2Dqe(C6lLXldkx)4Ume;$Cwt|#*%E$8fj#`aeeGzqaty_su^Qj--jeUR zuN`(%BxL;ePenzqT%+nIFN$$bANntU(~xj9HH47Jw0To}pwgzz(NVd<#|?pg9kLC< z=Hwrt|Hp+d3Ri5uD-TJ0`aH0b;-sqdPuDx(b;b6(0{>;kx+1^HT_hH1E2L3x{Af|I zdz)0ea)OW@3~$gIa+H6awDuKu<~*UTJhaeA#q20tq4lB`AW5iO9JhAu2yHs>Erv%| z>Myt53&-qyq)TUSCaggG)^43Mm+?)=2R@#d94(%zn^C~f*VV^L?V^~3&0p5sCUuiu zgyk^DVG=MkjtYC&Fw=5uhX)4j`sZvi=A=G#gPJ?2S@R@r3)A0NenTNa zAPwV1@L^wb^K($k`D)!zjuIy5cVr5SkUbFG%S06Qcrj^jO2=%_#+Fa)S-8<3nAX;& za8T1Z!hr@vn9V22p9vD*p>WvAG2f<>L%|fqF6BSCC(O$1 zoi1S03O^o;V>;gNNJq;uL{a*^y>m}9Z&rb*AK_%;6gOB-Skjh9hZNd~=}nn#{9ENL zAMfZDC=a7fc~y_~Y{sG(9W5t+V~g@xbr>!tOb<;@6eRH^5P5D#ovt#UH#C)>Wo%_R z5iGKgPK5P}L)j245hqg|%*PZRO0rAxIOW`1Nv+ezlkG13269@jd8=FSx?FJWyVU7< zC@MT3dZ1InA_R2hH{EcaGvIp+9VTPuPCk3a>B8LRy2I;C><)v5>fv@yvjs}9d7~>IuY>+98{K;3?4W?JQjXyU9ixVi zG&@A&iEy^U*^mm7n{RI(J&*0SdJ}#`$IGg?KlGc&&l{Cym$DOaR|&v{`#!Y_KGN0E zB8-Co4at6AIn5f9w=AllMf;t~@3-{~`XU zg{rkEsynJO#}T8Af*?O(i0n&}I%lD>Vuy+oKt-t%=HbpJMO?;@L$1RcUiB*Bj+&e* zyXi^tjNH-`iDv)qgz40W5w_-WSp}J?b_In1BcGn9JE{gTnE|Xy!=#FqV3ua)_7Suf z$TmOAZE_J(2MYgP%D0E*x8s%~Q?pd$KdDK_=V|j`%|CnRk9g`bv<%)Aha5-DWUq3~ zn(i`%?jhzP=xO|H_9A9w#a)ei2MR|*m+2GXRq;SqaS7Fa-E0U{9N4@`O`ismV07re zPgzMjF5cp0j_2ojxxU{4_7}UtC^>N*=@ICC_*H62^&nkts9h>BonRbYPOM&C=11iv zC`~=Gw?6b1h11!T<78b&Rp$Ecs|jVOly8G-v3}D-foExnM|oqpQJPd;QR`HG02-G2 z$DuSmugcxp*`Wad{Ya-)q3H+YXlsoPoM5s&sC#8RX1a>^$x(u)-Vhc!^C8N2x-6R= zUJ)PfQbAGW`S{QZta+LM)o^XxK*6h;5{-5PxRzMZz?<6Iav8tBE6kPvX+k_dtclco ztanxFra+T$nm1)(QDk;p6^3Ej+#Mw`@GaNl88@tUYO}?d;_9NxMs^&cts%Tby_^Ie z3*0<01BUNZuEF&>_$p&o9pU*YQXOE0=^Jvfssb6j4zVTTB4b*D`-(Lx+$gw7+@4Ca zo5wdvvJ@Z6ZrIX~PeL2q$qL@lr{&uRG8%7RqWlKcI}KVRT*MRHO93bsW|jT@82Z0X zdKLD#JYg84D@>o^WWLH0Tl|TnMXQM+eResfB(;N*v0RKfZVI+8jzV#mgp>FtCB1v} zw0Hn@UL?-G4A#5RtCMd*bGtG@<*!ZiATE+UaB7ygi;39Prtt=3%Q>vjh$f<)@YvKWk`@>VA>I zv_nJvj>?i{j(tc`qCzbbNjQdyb_L2E^k`;AU{9_7fslQj&Z}VJRnifrVt)yk$)}=U3&g{ zLiUyE#FkFSRlW9d1q9se@A4#YOsNuwJ(--A+nc;T1ToO3Cm|~w)G6N)hu@cDW}wlj zL!wnth~0|W+_aV4^aQzMS~E9qb5vU+s);EhoV6z-5oK>tx0m^<+8rgRMrMs=bcdsl;RkHA26J z@KxPq!adR4%OZqrl8Aydoe=K3B8gK`*dL7YB-l{}lk?lP>AAE0Bc?x4V0jgG8@QcB zS++My!;jRc9QWwi^A_J0E}C-vn=BD4J-zeUZzl~%EPpJ@zduiV{ThFKa%(~4V)q_! z1fB@h^fG3VWIdONbRNb@Zy;7gaCWlRG(DQ-77*zb8=h!vneXax!bxglPtZyBt%GMo}5=^vz_9bEa`nv7)Bi zAaRqXD0x(d;u<_(=5*5Q(2FH(s&G*y;B=zkXE&^d2HRx%_0e^aG$;w6AiQ$qwoE=0 zhN}~Fi#)34VzQ^01-R%#VSZ9=A1J0{6X9&o%W8h zS3BdBj*8kqeyLqR^zbYrt%<(|kAM3vFHBKkaF;oXOtG*6?qS7D%e0D({pL{Y3)2?` z`-uul9fuN*^yWeT)7jS!rwrrq|AqaJKgNN;CoJVckUr7q+4du#hGkwAI=jl*GW7T8 zwEUCZni;!`{CW6TUvqE9CPl@7yz+GkvE?tFTb)KNqH4QC-;{9L=)JlShpDxrNW(E_ z24z*-70xc9y)~)s`azOp4f&xwyiSvFyz@vE;HVzvCskpf_<;59dCK#;A@CIG9WPda z4|7bKf)xbh71}7+lEGf5!v{%z4c|;x21@%H*#d}USvDI}7#MB_|46M7aatbkDL%Po z`9)^iJq|{yUOzq0w(fe2??d*zNrT2<7*%1NN}t|b24aVjhIVZEL8GH6evmUJJYb8{ zXCXyosEYc29T(u+QB~1AU{%=5XJ9yYO^!c;%9_5)s&c<#LQ}?ZVD8oVVX{Qo*aB-2 z4^(=2{gjbMHjy4u(<0$iFJyZUZvzW^3UzgEw<_@xnTqM8DbQyh#r^#Da8(8Bnx2>e*OuT#*XX^bax0BhWWHD^?k8+>W^KaTzm0<22lUsGQACI0Qhl@Z*{{ zgXoK!k_7WN5D{F+(;!Ya)C1+s#3qE=gJ{Fv9@K2O-Cnp%s}Le(ChDWbgxw!qubf_A z#v?-K5hUkLX;UyQR z72aI3vNp;sFtLU*sPjnHq)ot{dEAuIQm2esyIzHj-d!48bBp&F7)tZ;H2@kssi8GX z%4$y&W=YH%yEWK-oavmh;+@`6@gjs3C`fmS8T9&ei6ez?&;dt;z=CE;0Wxr-E{YX* zP$3`yl&F7tvrIN*nh1|5o-?Fbjk%z^^pnJ1b$ zeUswBftJ7<9;hB%r270;iJc{50^uE_nm-Xv9eM2nw*ujVXxZ#|WtLZU1w7BJ%24c4 z&=W*nqpzGK81LzV*`&?Ys@fS)ejb`Zs}5cvbS_q917gg~C$=4IjKr==R%tojkvkIX zg`1M~*$&ThaBi!FES_@xIzbo&j0#{wp}s}o)`i!3mE*xeimkywfY<5p+A#S@t(`rK zY&3cZ1P{mDk2VnXnzdsOSNHw;jn2V4nb3{zqHNv}TdANhl2}+G-P;1)4 zYgXAE=71Y$Fx$fHvb4lH*Y6*%zOvg;1I`Y}C5eHixzouk2yx6;mQiMjN01LHdXM5d zA^WNc-es~q>MDzEf?OAa^{4}hUX{y+Fvs`SN3YIp3Gq62!dUn>V`;w*8`Mk~*=$ot z34K*XcXl-~-?`%hn6p1u>Dld};LiSU)c;<8z(4`i{E<=+L;#CE??Y-r;5Xy*G_Q#rxqWD#4 zi87k{)AQt~Yf1Lgfqd<-f)|$rA+AGH)O1}l2K_uI7b+OLV;QEelivMI22uHBBdBINx}r z*W#pUh87lE(*Oe(q*3dyvXxC_3O9;7{k8x!hokAUZn{KwN-siA{h|PTn>8gKTiA~# ztQoJiaI(%>FJDchiqKc7s?7ax65J}Y)~2~gw-CYE!9Fwt3y^*vSx@n9ErQDH%T$7{ zS061wZhm*^urOU};?cTFV5K!t!RN(9^ez*t)33?sreZ|!p!GH$$-;ES-acGSKe~Rw z@E+8$zii6VfF?5*4IoEDug~oG`nL3V%f!M>LPjsi0Mf7NHADD1SD##V524kSj?N z%9Rry^wp?naZ@;xMyEcFCuXp->0XCYzjiU{4E?b?!6W@q%%3x-fB|}2 zW-wM{li?*CgxeKtMx3hUMjLIP9elG2(V+2X*@PK3&FyIwIQ^b1)_iH?K&2pY7lrWZ zdg?F^+n%$?v#_-}DykAnMx3_gP2w#f73rJM-&Z8%qS`t77L*BjO$L@{=Ja%Ex)n}) z<1bH^PNUa>Rcia}prib7*i)eKMZH7Mu)68%b>4S=p_1!K%2}kvW@WR{Jao}G~ z<%`RWC(vtI%E;X#gjd>#&?e#D9V^e+z984BSB`jxk5*%*v-Y}#CDc6V3T_dcho*=u zOrDlM>(d>YzhZgT+X3RArBxbHftKFU1pfwMFUboKGK)fc9yl_y8Rbtk9?;h-(NM1u zVJPbKb;vAUjTrzJ6L8T4M*gw=u0R^L99s;{iz;>)b(%4Ko+Zp$wD3WJVP60Nx;e}(LWS;fd*GLES^i?H zuL^DsIp@8|HuO*P<%5S1Em4{^g@@`8#kx&jjd2uuOP@^bbWhVRN=C2buN>0YuS`QK z!4iFqSi+!g#tmt7{=(VPNAXsN{w@opdS{wF5Filb)LEm)#bC3YNhvUW=ExF*-yK; zPi3(xoz^`S8+Vmut-f-GPzd_&YJ1a7;Jb?z!W&R3C;4F;DM>$mY7>`O#sHzWITA>c zCb)?>Fv~J+y(hfPYzha0s(VS8aHydu0U3gXg5TKJ>Wh41K>e_6AcLDF)iyssK5eTb z2?4Q9GS69&E*L?o1;IXrRP=*KHT9-KxeFIgwsX)HHMh_k=fm#q4b_4FTJio6Cu^q?EJby|d zFF}J!H!0Q)H`Q&Apl-!&KZmRxs}r~7N}Wk`sPdoWBFEC;!G{~;6&U!iD*H!blDuBQRm ze$BS~l{9baYsqRGA%h#_d>{r(rjL}t{B6~{Nez}%NPHdwhL~m|9=BYsw??@?9HKk2 zg8)4xvgIP&V~`qo28)>TrONbqqoWGjcg$s3JP^d*E`OGv+jc`B;=@t?s*0dcf=G#W zdhW0T5=0?FP;UMlJ}pWpf-Ou03EDO`gR|d+t-G{I5Ehlb$ylJ-Z;PY_xxw}o&Tk+V zbdCJ5#$Q(zgixNtJ*e69b+PWw;+16)%CrFhnE|rCvy_vFC3a6Q8%Ocj+L!x0x3*e9 zE$^$HX&E#sNzdC>)EQ15VGhaFlyLmQD?#;5I70=aOBObA>c%b&7rol<%k#9M%;5G0 z9aN~RG~}dRdKedQBZ$o?-U)I$e-tx>Uz^^*$v_TNR^nxFHDNn-!ZOMk8z%)qe90V~l8Ry;pLgbmXNtLOK3<)}ofi6ysu5Ora>Vjsz$G~puuY$ zsNd^M5>%MX#%GAIK9*(wB6|nP$g#kWc8<#Ds5zVqRMKp#<}zn}3Un7yKA44ZMh;M* zkTb9O*JJ+!RU9tPmv&p*N8)IaE`yjyM>|g--(53koY@SIT3>=^8E9+*uSs8@k2|^$ z#3D6Zg!+(yZHn0pSz;9u&Z=z<|92J|!rU>%mzfoh4S^HsBp=~x7qF^EHCQ0>-Te_2Hz7=~|BR+Fi=2O5D-hd>Y0 zU)1RUIbR(Bv!P;m<7TK4s=lYwsb2A9KiQe|C zsUNM<*~CVt)cUka@!TeCc?@cxpEuNl+Au26?O9>CO+|1@I+{^&;DcxDRIdgDXo?TzyUa*&Ys&Wl{6pKqJn`m!tIi{d4c-jIT#-xw`p>>d}PRJ5F7 zF`R~$4L5Z)M&g~6O{3mJah^CLfG=dWw>WqB{wUj)6>XY~;T>@7fR_vz`EE`UzK4=k z=)jr22=Ym*0~k9CgZTsp(lG{9Y8|xCiz+aqEg^KNsP;XGKRdm`f9L_+Z^jM9N3yfd!5e*9 zMa}F8n6R-Ji4u~XIzNTn1KRYG2cK2la_b0j=noOw=eg->%<>V>?NbAIY#>Hc zzSF(UJry=Tv=3Bwabh)1hNWcc;+bP&f z(!S%id6S*C`WkhEehJ*RGGq1Qb2v9JU|eqA(B=OE`u{Z?J|r4g9yFQ-3WGqOzB-iD z`f#3_t$Q&4pSsD4<$D9!$83mm06^OGnsC?gy< zJ327fDllWKtqd;@F^S^((!m;LYHUfPO-AO%wZ?J)XF$ z3IRpP0KzaDUu3MIPO_$MBfVFw~23(l8Fx zN}N|&w?`+o8OB+%bCho#*-4c+0}iGzcmY__cLgFQ@`b&RxsS_n6Dvd>omBZ?aTx%t z8q&aUvu?Hf26u ze>r)RI{TWY9w)0T$q|E_vip^@6~gR#w>HlYxsFnYZG-};D^z|nv3Ec~mW9L2kVjR$ zYa}*s3=_YnD#tHa?ues_zXNZW$M7Ske)?6CGUoio>D;~D)>D>27lVW4D2&#nV@De7 zYRDZTD0-aO`}FLpTwvbtp;WoG>E_Rwj`~H3ZpSb3s?R}k7+}8FS=d(YSTImxn8s~o zgf<(6-wd|yVc=`)ClZ~P4rfl3UhOgMW{do7RsP~R-1CYO;7%Tgr5#Wb#!;4OV}Pj% zOr!|X8fV(^PK_*IB@Rka%$O2tU_NWgl^nAZoGI#GG^c43u|T6EUu4cBI?U7Ux=FZ$ zE{&prFxG$$;wXfOu3`ekMV0STS7|0brl4^=v|b%3|LF@IhPD(tOnM5TT*N|O829fc*hH$D0E z#bM)JNJHirUFQ?6?j_k-)0?jgYUk)OMP*F?izIwdcWv?>@7rNuj-6-&Y9<&^v+u{~|^Q?N<601ycCejj;lmA&e+4F6Ve#kcKhJ7w5p~b*Kh`$2aGn)()R}nLmIVx z3?;D!|LJd-4S`*=7fZkn0`|SxKP}$YEAtm1DEzFjxyW9>2jv91vb}cjMIHv7i19M` zo=B30qI*mJ{IJal<{T!l24zeKkKNARl@Bl z`q3X!R6IQ2w+_@ZK=Jmwm{&>K`ti*P+qn}(liQ(1B+piX+wT54iHeLo5y*Ujn2RtE zx9+&FmtdvCfW;(@c5#Ewh{*GWy@$joE{!aMZ8JD&y@xg>vD^G@j0swGff21wxmkR_ zi#eg?gI$fzvoWu!rw*eHyc<~XC@W~@kDsG~k)q`}@<6j4ole=ffKle&q=%1a)6Y5m z%VZKAVsz)_5)(7LvPMN$U!^bs267+ss^iq@-IZhtgIWxZ3@d7PDejvPU@P;%!EUc4 zaC+#N4rMRpcyzSLCmf%EUDibe+miFDcXN3EJO)YTV;7#}^kx=!5+sz&@cc zLUGr_bfb(>t$&=3VxYu*#MWmCW=Gw0{{FkPUo79*1oI65QD*0kVD>0JW_Q3K$2KDf zVy!bDiw`UFWp)2QMi#$zsx`rTrR~EJb zm)D(zy?Xo3^m@_|qhv!zv=pRQ5-Kt0!FQyNSNS>9s-Gs=zv;@u#e$Xb>VRvT>Bx}? z|2>FXE(w9RY5QVg3qGqK`SXNF1dW@Ab5_&80RmJ07JU4jeix0$SGFii`TVURyCl6w z3dsTRUtr#*PjMI#FCKIyie(HwT@-oqT?|s^A-mT{Y|xoa{9d=~K{RF*M6a#;GF;?amkH2u8xcGC!udVK5DClJukltnoaw54`xd z;8xLi%A%!(eU3K~h{|-l*Tqcu6l(mX?QYz$Fi3(qH;w1N%ZR*Yh8Pc-J%t_yRY4Ph z*j4!NxQN<(h?`OZM`W=|VOlc0@!FsB~S(F(0whjTO12OYuW z2+bmKbGvF@J`Y6|D96P>Xa*maiiq8&gdhScKb#%j4n7pyUDcc(BK&V3P>xM){Mv~S zeL%9Z4a$fLqToD+5@O-{Mj1K2bdW=>!<-o#5{Yw+bz{h&>Hh4VHoZh=szxUcd~L9< zsS`B}_uI*qhTQbVK&ef~31e<6m=_fM`DGdtdLPM9LBJg8)k)ZZbuvraXj_o!5qXRR z$j8+Vd0pbjt_2PDY?ZTZd|5s-vD-sn6d&|U;%!wSyY=<;S3OsCDtrOm@NtRWlYAf z1nlp2IO`f6xTNtI<>RG`Rn|bFXpu2(ER}qWEC|&heBZNTTbw#TO=nx1l#TNhQ8?$Z z>N>VT-<%^Efnc?vYSVHxiJ7|fM42Xm(fJZaM!!CtIY`cpw{-0o_K}3p3}y|gw}aH{ z(%_c4^RnxRU6Vw1bW%O2k)Eu|wQsUjB0j23gA}>!d4bUYbX-M>mw+r*Rq-C0NpPbM zV$I)&=yZ{^!Sqg(0lS2;eCy~Z4C%=79hd-* zPSsy08~r-tTG4fxZaqYF!P$nJ^$I%GJU~3b9&~>HB5Y`0j;7{#l{DLH*5Z&yqJb66 ztU4^mAL%t3Oofwh0Rke@hd?EUT4_dpo^pe3B|5nYF^hVjsZgfp)ep7f)dcD@N*V}@ zMPH?un{ln2CH#H|<;Xk$ttFVXaQcsUk}o(4Ct&k2Ml$^wW`&T>$^nQm2=U;(+A!dN zYBI5NhQLnk%S#ryq>!+cpcH>uiQ~mv+F(ek-hX+$=uE%Pafk>QxoVR|v;AZK*iP|c5d8q8H<;&+ zykCF4L;{lcB4?QVc2zR$(4#sGrB@mtM1 zO=0bAUSNmURAmt=^KRvP+7S9sGFjM&Q#K&IX<6`j(g@0K%{rws>{=11y50^{h(qIepq-lJDTS%lj-pzs5?Ks)^ z{W4^fozI-DO*+10MNawvY1xwJUZOW`E0_~KOE5M=C1hul`yxx270dpIf3+sZuD?O) zptPdC*{lSY?u0hHi}5=i`IG-;ulV;B#2 z&n>($VbQT`4%Sph`TpTj-St~+|PMySS*vn>03uX z>rz0Tn+LE%9CT5p`x_p|*IU$w08wER@x|VmpZrG$eiVJyoM!n6f`VT>$0;JyHeUWj z`o6g7FWwqjcp1tsHIp4Rk6)eGVn?T&{Num+EqZ&oeOJY0Rp+Y=nbbS( zEDgldld|f^3}9!oJd z@5y)R(@T&_yn=?&h2wmc-XFlspPJ!(VgiM^W9e?Xhp-)) zbmco3WJAE1HjChA^_K95b;6JdYanjbRy=3^067R~a}VPo7{tXFDfaFu!}%n%^JHuc zD2R|M=RGq?a}zINX&PO4aLXzuUj?b(QxKNl?u}j`5_-~xa&Ha<`U8da=#;=NhG)2`#?#x&_eSwti=?mv06|>F>@+0y+(1HyW%iDPj4-{q} zA*b0*m|z5bjyJW?XP3+qUBI9rlNaybBk_deu9M%Ob@S8EZw`10x=H>2+BtjW#LX}Y zO9IbG&`83b6{+-RkG*L?$h+ANVZ573m9@Qn;>m>|w&AZ3n?^r)22!C9k=b-0WXI(q zLnf`Ua|7!;*i2?}xQi5V$BY}nC+U69InU9c+)3I`*m@QnT1c@5s;)C#Jjci|^xRE{ z{g4cPFq`f~lKTig3NX&G{`s*b4UOQ=e z;j7^vUC6V7%g&>l4y)?5OqV2FOP00%+V_=eVN5JohRG@~7@zI}>*RcQ4zxr*hG(VJ zjs7P2s-`0(!Ch*N2OqSC&_Gi~QM_Qhv}4$JL;@b4bmh!LTzZXYHRi$0Q<6jVfi6I_ z;h7nxx)8>#*&yp>mpT&xVk>)d4275j0|8(LW`4<*Ps;*AILJ3a(~!g&=b*?&oUg7Z zt=SHXaMKW)7ARRa`cpK8spy|j>tGdfX0s}W@xtfe&&?t%4B-knhe`p?8@61skkxFP zUamX~lW#(vt2mdOrBk>Qev7)+F~%b>TSKx9Z-acT#t3ZMaiBD0b>D{I06r$nQ+0G#OyW2v{KyF6?R8YdkAjhsSt9%< z@8FK2Y)e4=5sNHBx0{KDRu^ZI=Ei{As@Vp|8S)-IiwEpQVHC%f*+>W5K-g>zX`0BvM&0yvsbQO@ddF#?6ihGRMrT$+-Mz?FVMwQR6k7bisum32?kgbnzw|w0Z$hz_SY}b*VB{D z48CE(Vp@x3PwYPoiVolqH@*rF)ptmG+PIGn>m!^tf+bN#=YXq(ATmoBpueCcpz6pa|hHa$uB>Kn%4Ad=#iBV55*W1cXwTO{){&I+_*pE}PvX2U%PwhIdLuQ;2-f?TR4X1I0l=6= z65^*2-|-%Hv3#3M$7_d#!Zaf)wSp@d&LWVpk^SY%hN_AGL}*rE;#J$g!yu7hF1twc UP!6gOpHVE!w*LS7&wK;_0whkDivR!s literal 0 HcmV?d00001 diff --git a/library/core/src/test/assets/wav/sample_ima_adpcm.wav.0.dump b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.0.dump new file mode 100644 index 0000000000..a16ad68dfa --- /dev/null +++ b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.0.dump @@ -0,0 +1,75 @@ +seekMap: + isSeekable = true + duration = 1018185 + getPosition(0) = [[timeUs=0, position=94]] +numberOfTracks = 1 +track 0: + format: + bitrate = 177004 + id = null + containerMimeType = null + sampleMimeType = audio/raw + maxInputSize = 8820 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = 2 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + metadata = null + initializationData: + total output bytes = 89804 + sample count = 11 + sample 0: + time = 0 + flags = 1 + data = length 8820, hash E90A457C + sample 1: + time = 100000 + flags = 1 + data = length 8820, hash EA798370 + sample 2: + time = 200000 + flags = 1 + data = length 8820, hash A57ED989 + sample 3: + time = 300000 + flags = 1 + data = length 8820, hash 8B681816 + sample 4: + time = 400000 + flags = 1 + data = length 8820, hash 48177BEB + sample 5: + time = 500000 + flags = 1 + data = length 8820, hash 70197776 + sample 6: + time = 600000 + flags = 1 + data = length 8820, hash DB4A4704 + sample 7: + time = 700000 + flags = 1 + data = length 8820, hash 84A525D0 + sample 8: + time = 800000 + flags = 1 + data = length 8820, hash 197A4377 + sample 9: + time = 900000 + flags = 1 + data = length 8820, hash 6982BC91 + sample 10: + time = 1000000 + flags = 1 + data = length 1604, hash 3DED68ED +tracksEnded = true diff --git a/library/core/src/test/assets/wav/sample_ima_adpcm.wav.1.dump b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.1.dump new file mode 100644 index 0000000000..3eb13e82bf --- /dev/null +++ b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.1.dump @@ -0,0 +1,59 @@ +seekMap: + isSeekable = true + duration = 1018185 + getPosition(0) = [[timeUs=0, position=94]] +numberOfTracks = 1 +track 0: + format: + bitrate = 177004 + id = null + containerMimeType = null + sampleMimeType = audio/raw + maxInputSize = 8820 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = 2 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + metadata = null + initializationData: + total output bytes = 61230 + sample count = 7 + sample 0: + time = 339395 + flags = 1 + data = length 8820, hash 25FCA092 + sample 1: + time = 439395 + flags = 1 + data = length 8820, hash 9400B4BE + sample 2: + time = 539395 + flags = 1 + data = length 8820, hash 5BA7E45D + sample 3: + time = 639395 + flags = 1 + data = length 8820, hash 5AC42905 + sample 4: + time = 739395 + flags = 1 + data = length 8820, hash D57059C + sample 5: + time = 839395 + flags = 1 + data = length 8820, hash DEF5C480 + sample 6: + time = 939395 + flags = 1 + data = length 8310, hash 10B3FC93 +tracksEnded = true diff --git a/library/core/src/test/assets/wav/sample_ima_adpcm.wav.2.dump b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.2.dump new file mode 100644 index 0000000000..bef16523d4 --- /dev/null +++ b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.2.dump @@ -0,0 +1,47 @@ +seekMap: + isSeekable = true + duration = 1018185 + getPosition(0) = [[timeUs=0, position=94]] +numberOfTracks = 1 +track 0: + format: + bitrate = 177004 + id = null + containerMimeType = null + sampleMimeType = audio/raw + maxInputSize = 8820 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = 2 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + metadata = null + initializationData: + total output bytes = 32656 + sample count = 4 + sample 0: + time = 678790 + flags = 1 + data = length 8820, hash DB7FF64C + sample 1: + time = 778790 + flags = 1 + data = length 8820, hash B895DFDC + sample 2: + time = 878790 + flags = 1 + data = length 8820, hash E3AB416D + sample 3: + time = 978790 + flags = 1 + data = length 6196, hash E27E175A +tracksEnded = true diff --git a/library/core/src/test/assets/wav/sample_ima_adpcm.wav.3.dump b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.3.dump new file mode 100644 index 0000000000..085fe5e592 --- /dev/null +++ b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.3.dump @@ -0,0 +1,35 @@ +seekMap: + isSeekable = true + duration = 1018185 + getPosition(0) = [[timeUs=0, position=94]] +numberOfTracks = 1 +track 0: + format: + bitrate = 177004 + id = null + containerMimeType = null + sampleMimeType = audio/raw + maxInputSize = 8820 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = 2 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + metadata = null + initializationData: + total output bytes = 4082 + sample count = 1 + sample 0: + time = 1018185 + flags = 1 + data = length 4082, hash 4CB1A490 +tracksEnded = true diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java index c617b672e2..7f9549ea75 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java @@ -28,4 +28,9 @@ public final class WavExtractorTest { public void testSample() throws Exception { ExtractorAsserts.assertBehavior(WavExtractor::new, "wav/sample.wav"); } + + @Test + public void testSampleImaAdpcm() throws Exception { + ExtractorAsserts.assertBehavior(WavExtractor::new, "wav/sample_ima_adpcm.wav"); + } } From a67394b2308d1d65a4187f81280b8682d084410c Mon Sep 17 00:00:00 2001 From: ybai001 Date: Mon, 6 Jan 2020 15:21:43 +0800 Subject: [PATCH 0587/1335] Optimize AC-4 code and add related test cases -- Optimize Mp4Extractor for AC-4 -- Optimize FragmentedMp4Extractor for AC-4 -- Add test case for AC-4 in MP4 -- Add test case for AC-4 in Fragmented MP4 --- .../android/exoplayer2/audio/Ac4Util.java | 6 + .../extractor/mp4/FragmentedMp4Extractor.java | 20 ++-- .../extractor/mp4/Mp4Extractor.java | 19 ++-- .../core/src/test/assets/mp4/sample_ac4.mp4 | Bin 0 -> 8238 bytes .../src/test/assets/mp4/sample_ac4.mp4.0.dump | 107 ++++++++++++++++++ .../src/test/assets/mp4/sample_ac4.mp4.1.dump | 107 ++++++++++++++++++ .../src/test/assets/mp4/sample_ac4.mp4.2.dump | 107 ++++++++++++++++++ .../src/test/assets/mp4/sample_ac4.mp4.3.dump | 107 ++++++++++++++++++ .../test/assets/mp4/sample_ac4_fragmented.mp4 | Bin 0 -> 8404 bytes .../mp4/sample_ac4_fragmented.mp4.0.dump | 107 ++++++++++++++++++ .../mp4/sample_ac4_fragmented.mp4.1.dump | 83 ++++++++++++++ .../mp4/sample_ac4_fragmented.mp4.2.dump | 59 ++++++++++ .../mp4/sample_ac4_fragmented.mp4.3.dump | 35 ++++++ .../mp4/FragmentedMp4ExtractorTest.java | 6 + .../extractor/mp4/Mp4ExtractorTest.java | 5 + 15 files changed, 744 insertions(+), 24 deletions(-) create mode 100644 library/core/src/test/assets/mp4/sample_ac4.mp4 create mode 100644 library/core/src/test/assets/mp4/sample_ac4.mp4.0.dump create mode 100644 library/core/src/test/assets/mp4/sample_ac4.mp4.1.dump create mode 100644 library/core/src/test/assets/mp4/sample_ac4.mp4.2.dump create mode 100644 library/core/src/test/assets/mp4/sample_ac4.mp4.3.dump create mode 100644 library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4 create mode 100644 library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.0.dump create mode 100644 library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.1.dump create mode 100644 library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.2.dump create mode 100644 library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.3.dump diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java index c54e3844a3..f3b3a8fc1d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java @@ -57,6 +57,12 @@ public final class Ac4Util { /** The channel count of AC-4 stream. */ // TODO: Parse AC-4 stream channel count. private static final int CHANNEL_COUNT_2 = 2; + /** + * The AC-4 sync frame header size for extractor. + * The 7 bytes are 0xAC, 0x40, 0xFF, 0xFF, sizeByte1, sizeByte2, sizeByte3. + * See ETSI TS 103 190-1 V1.3.1, Annex G + */ + public static final int SAMPLE_HEADER_SIZE = 7; /** * The header size for AC-4 parser. Only needs to be as big as we need to read, not the full * header size. 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 1172f8665a..967f71b328 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 @@ -168,7 +168,6 @@ public class FragmentedMp4Extractor implements Extractor { private int sampleBytesWritten; private int sampleCurrentNalBytesRemaining; private boolean processSeiNalUnitPayload; - private boolean isAc4HeaderRequired; // Extractor output. @MonotonicNonNull private ExtractorOutput extractorOutput; @@ -302,7 +301,6 @@ public class FragmentedMp4Extractor implements Extractor { pendingMetadataSampleBytes = 0; pendingSeekTimeUs = timeUs; containerAtoms.clear(); - isAc4HeaderRequired = false; enterReadingAtomHeaderState(); } @@ -1267,12 +1265,18 @@ public class FragmentedMp4Extractor implements Extractor { sampleSize -= Atom.HEADER_SIZE; input.skipFully(Atom.HEADER_SIZE); } + boolean isAc4HeaderRequired = + MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType); sampleBytesWritten = currentTrackBundle.outputSampleEncryptionData(); sampleSize += sampleBytesWritten; + if (isAc4HeaderRequired) { + Ac4Util.getAc4SampleHeader(sampleSize, scratch); + currentTrackBundle.output.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE); + sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE; + sampleSize += Ac4Util.SAMPLE_HEADER_SIZE; + } parserState = STATE_READING_SAMPLE_CONTINUE; sampleCurrentNalBytesRemaining = 0; - isAc4HeaderRequired = - MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType); } TrackFragment fragment = currentTrackBundle.fragment; @@ -1337,14 +1341,6 @@ public class FragmentedMp4Extractor implements Extractor { } } } else { - if (isAc4HeaderRequired) { - Ac4Util.getAc4SampleHeader(sampleSize, scratch); - int length = scratch.limit(); - output.sampleData(scratch, length); - sampleSize += length; - sampleBytesWritten += length; - isAc4HeaderRequired = false; - } while (sampleBytesWritten < sampleSize) { int writtenBytes = output.sampleData(input, sampleSize - sampleBytesWritten, false); sampleBytesWritten += writtenBytes; 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 971cc27d13..651db26b5e 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 @@ -112,7 +112,6 @@ public final class Mp4Extractor implements Extractor, SeekMap { private int sampleTrackIndex; private int sampleBytesWritten; private int sampleCurrentNalBytesRemaining; - private boolean isAc4HeaderRequired; // Extractor outputs. @MonotonicNonNull private ExtractorOutput extractorOutput; @@ -162,7 +161,6 @@ public final class Mp4Extractor implements Extractor, SeekMap { sampleTrackIndex = C.INDEX_UNSET; sampleBytesWritten = 0; sampleCurrentNalBytesRemaining = 0; - isAc4HeaderRequired = false; if (position == 0) { enterReadingAtomHeaderState(); } else if (tracks != null) { @@ -507,8 +505,6 @@ public final class Mp4Extractor implements Extractor, SeekMap { if (sampleTrackIndex == C.INDEX_UNSET) { return RESULT_END_OF_INPUT; } - isAc4HeaderRequired = - MimeTypes.AUDIO_AC4.equals(tracks[sampleTrackIndex].track.format.sampleMimeType); } Mp4Track track = tracks[sampleTrackIndex]; TrackOutput trackOutput = track.trackOutput; @@ -527,6 +523,13 @@ public final class Mp4Extractor implements Extractor, SeekMap { sampleSize -= Atom.HEADER_SIZE; } input.skipFully((int) skipAmount); + if (MimeTypes.AUDIO_AC4.equals(track.track.format.sampleMimeType)) { + Ac4Util.getAc4SampleHeader(sampleSize, scratch); + int length = scratch.limit(); + trackOutput.sampleData(scratch, length); + sampleSize += length; + sampleBytesWritten += length; + } if (track.track.nalUnitLengthFieldLength != 0) { // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case // they're only 1 or 2 bytes long. @@ -562,14 +565,6 @@ public final class Mp4Extractor implements Extractor, SeekMap { } } } else { - if (isAc4HeaderRequired) { - Ac4Util.getAc4SampleHeader(sampleSize, scratch); - int length = scratch.limit(); - trackOutput.sampleData(scratch, length); - sampleSize += length; - sampleBytesWritten += length; - isAc4HeaderRequired = false; - } while (sampleBytesWritten < sampleSize) { int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten, false); sampleBytesWritten += writtenBytes; diff --git a/library/core/src/test/assets/mp4/sample_ac4.mp4 b/library/core/src/test/assets/mp4/sample_ac4.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..d649632c745ef055934dda90e750e1b99f991593 GIT binary patch literal 8238 zcmeHMc{r6_*S}36PLElMDDzAXk1;1xND_`AGM-~Tren%@l7z>Usgxp*!Vxl8#zLer zlSrsY9b?FR?)Sbcz2DdSz1Q`8SKs^3_q48STWhbq?zQ**TWhaZ^i z!vFwKz}yw@g#!S}L7bOY0HpK41-M`!4h$>$9fun@oS%?E(@^7q`4VGj2(d@lB$ z7!NFLL=lB^^>l&+COqz|SJ(<70nArDH^vtWkHZA7gVEpDgYTMVR1u;U)s<9AO1SP&B{jXuol|zw4W9 z2iXNf@(w{hDO~;+Z5nb9%|~ejJyQ%dXQ)L&&Fd=%xbc3Dko<+HAiK{Hl$`*;VGaO1 zC>+M#ud#D7i;q$axIA7)-7!jG6lonfv<6(i!*$z=zCAhDf;oLz>@*4x@Y>!(fy#j( zIsyQp0L800AZM6P7sFy?r!Y(`aw86)(YPwK*#MeIB#yWdi-|>OG`e~KZG|=?5({=- z@9)bA2i`$AYZExfWg(manFH3;Q05#1a*Qa(VN{UR1epDzpG1Hl1>=kYc5v4D0cYSi zUo0gRwDbQfk$-a1zv1JABLWc4I^Xk|`$wF0|AwIa2VR+Ai20Sz{{dWobV;y*asHLh zKjQL#-6wd$^_l;BeKv&a1~+^T0y)4MK(PQ%_^U?$H0*@%U-kK?LHr922#LSy^Iw4J zr%ebqe%0rn2JtUAAe6xMS@8RJUCGz)I?A2jDNt?xlM=xi%^*;1u-}Yzqy;6%GP`EK zPq5ssx_zT>c)G_3IT86}_mkdoN-l%cHsH+ri6~6QlUokYd9{3~wdcEeJXT_D9rgAKp0Shn zdoWSeVbZi8(U>O?;bYGzj?M-@p4#X&BxfdGvuwAplZ@x zZpFUe)}#NdOJq`a;*dzy37OyOSM5#d%`89$%oR4;8p`+xF_<++X^p;jY%K*zPsn9V z>LnSARyVhQ>uE?ru_yn*#*{1}jP%SYo4i%vWy6phVdFwgPxftX+$-4{P|>s8<}&l9tie+qRgsS$bb|1cq*ra|YzwgclxhS@+v6 zIwq=Cb!w=qCoUzYg;4@Bof3z}K0f6cea*ltE{()jN4$QZED-n>%pX0B0;s@<>EEZo z-m!$nRdPb3F!|5ru8oLpKo=l%!gWjPdz^>hI7iuHF?s39GRoX%mMK1zpkN((-?uI6>|Um~w<{aki>z9T9_!u=eleIJj?f3)s%DHT zOo0)Xn7a|Z8}TQ};4FfRPv1$D+f)lBb1tNI(rr#(TQT0-Nm;OiCQJ27#{FX*<7*lW zMnqO&@U%UwJgsP^7C zhHkR-n$-!v=bAU%N-$xZYxl*b2mw!;UQ(cCNs0;vXX=IOs2fFAozx_=Ov+{Q=HKm= z=5iJ;`0Q)e75B(bvE^VY-c6WtZM48`pS!~Zf13G9lSszq7ITPbt`wNgVto1?wGcwI1ODE z03eLQ31G960LTC+0jNE#@zldlNK?tilfqA;)wXhuC+QG^%#zt@;|)wOQ}h?>4=^cE#3! z#CFw5?6(i;6|4r2ZieQX*PZe(_2AVwpLmHsGIFbx39XEDV`tqb@c}iB7O?Kh;-;gp zY^sSnstmBB2x)xMHc~@Q^MboCHXRUV<$6Cn*?M+rWfy3v6Yjn7rl;%U3r#K(k)6oXpVlmem^0BVqAdst~^KU zc(Q{Vl8WzKw8%M!-OS89YC0c2%FqE+h?y-gyEXzOFRg~G%GHFC_k(BFR{}O)HZE;P zlnymI&jc5bMP!U^)~>8Y0dPT?g%9Z6JNYCh1LYItpe@!Rl7APS5W975Vh8nwX7baI zE6){1JkqU5B$BvjUHYIuj?UcNr7Q1k0Id^6GxowCLas5@+D|>WMbzfsBe>{gHCH}U zMMtGNM3dj!dTGov=VQXvRh@Kdgp890>i1lGJ~~qDe%?YPfKPNL|$EdA3y;u69S1Vl_5J7ZjOU9U&a$u7}kD{aLFKTK}X zzPKXfz`6=%3%;M09<}pD^+X*tW}3ctG87S2imedtx{jOf(9$5__^JDK31L;nV&|vF zL~(jSrRWP2vFn(pEGL|AH)IQox*Qq2tFM15g6O_3Gv?Uv==!|ZisB0lEpbvL#_yjMzL{3z5PVZIkK-0wm1O^_!q~QCKuOn2{Y}5hW$XNs z4lJ_LPqdDZ#~FHKJBd!ntqZFeFr*_O2F=O{7tqRXV;DKO_<-UuP^N7hBDxl`wSq)@9kYH<}+?TC-=Rx0%R_ zZSr)1wkFJx$DMLt#KycmUXYhGdou|gaW>m+IXGhNZX*EzHWN=aI)zoAY}|(T#5n|9 zV?}rN#NlBW<^o&CdH9m|Gr%R|5@|Q^hS6d-OV&(ibLQ0m(lw)u38`K;kC7P78iJ!< z5Le~pBMMJ7`+Z}q(}ZwXhX5g?Q7)2yLZK&^{f1{ldZc7=KU3!Iob>79T$@uTEUTp2 z3{n>3Lv#%gn@*n}&LP>Xuu`@{g0*YYUUgd90BU@5vYVC zaL3kh^=kwUowV=INT&KulxkkKH zmP68Y4Bjg*kd#eK+Tu8jYJEeOmwrCCv9?izxtiK?V24$n(X}nu`?<%6VImKsa$pC z%}S9M%T@BOloac*DrDhKGo$EZ@(ZE8CJ|QJ}_^1zt=&t%XV>SWb0!fJoP?vw<_O;k)6^%2}Rz z$9Fv_@FqxV&z3AQ*4(!pv7RVa+LQxq@hb!keMxvp_z*bp9qw-lkHzRQBpYzm7^I1^ z947ViP8}MHB2%fK=!%G)^yHE2D-XT6P#-A@CM}HVC8+vMnn|5GdYzh(^M{Z7?dVS? zR^-%23}wj16t_mRxr<_?stS&wgqY%Z?Yv<(rTY&LRy)r0(O<8%Ew#lBkW_l0cM>XUpP5;hPl zky>Lj?!6;KO9ev`+A774w0uYnh~pO4zV}G}p?i}TqqOY% b-oeDizO2Xc2Qp{?8omZ)=pUE=<}UId;b<$Z literal 0 HcmV?d00001 diff --git a/library/core/src/test/assets/mp4/sample_ac4.mp4.0.dump b/library/core/src/test/assets/mp4/sample_ac4.mp4.0.dump new file mode 100644 index 0000000000..92ba157e3f --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4.mp4.0.dump @@ -0,0 +1,107 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=758]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = 622 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 7613 + sample count = 19 + sample 0: + time = 0 + flags = 1 + data = length 367, hash D2762FA + sample 1: + time = 40000 + flags = 0 + data = length 367, hash BDD3224A + sample 2: + time = 80000 + flags = 0 + data = length 367, hash 9302227B + sample 3: + time = 120000 + flags = 0 + data = length 367, hash 72996003 + sample 4: + time = 160000 + flags = 0 + data = length 367, hash 88AE5A1B + sample 5: + time = 200000 + flags = 0 + data = length 367, hash E5346FE3 + sample 6: + time = 240000 + flags = 0 + data = length 367, hash CE558362 + sample 7: + time = 280000 + flags = 0 + data = length 367, hash 51AD3043 + sample 8: + time = 320000 + flags = 0 + data = length 367, hash EB72E95B + sample 9: + time = 360000 + flags = 0 + data = length 367, hash 47F8FF23 + sample 10: + time = 400000 + flags = 0 + data = length 367, hash 8133883D + sample 11: + time = 440000 + flags = 0 + data = length 495, hash E14BDFEE + sample 12: + time = 480000 + flags = 0 + data = length 520, hash FEE56928 + sample 13: + time = 519999 + flags = 0 + data = length 599, hash 41F496C5 + sample 14: + time = 560000 + flags = 0 + data = length 436, hash 76D6404 + sample 15: + time = 600000 + flags = 0 + data = length 366, hash 56D49D4D + sample 16: + time = 640000 + flags = 0 + data = length 393, hash 822FC8 + sample 17: + time = 680000 + flags = 0 + data = length 374, hash FA8AE217 + sample 18: + time = 720000 + flags = 536870912 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4.mp4.1.dump b/library/core/src/test/assets/mp4/sample_ac4.mp4.1.dump new file mode 100644 index 0000000000..92ba157e3f --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4.mp4.1.dump @@ -0,0 +1,107 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=758]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = 622 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 7613 + sample count = 19 + sample 0: + time = 0 + flags = 1 + data = length 367, hash D2762FA + sample 1: + time = 40000 + flags = 0 + data = length 367, hash BDD3224A + sample 2: + time = 80000 + flags = 0 + data = length 367, hash 9302227B + sample 3: + time = 120000 + flags = 0 + data = length 367, hash 72996003 + sample 4: + time = 160000 + flags = 0 + data = length 367, hash 88AE5A1B + sample 5: + time = 200000 + flags = 0 + data = length 367, hash E5346FE3 + sample 6: + time = 240000 + flags = 0 + data = length 367, hash CE558362 + sample 7: + time = 280000 + flags = 0 + data = length 367, hash 51AD3043 + sample 8: + time = 320000 + flags = 0 + data = length 367, hash EB72E95B + sample 9: + time = 360000 + flags = 0 + data = length 367, hash 47F8FF23 + sample 10: + time = 400000 + flags = 0 + data = length 367, hash 8133883D + sample 11: + time = 440000 + flags = 0 + data = length 495, hash E14BDFEE + sample 12: + time = 480000 + flags = 0 + data = length 520, hash FEE56928 + sample 13: + time = 519999 + flags = 0 + data = length 599, hash 41F496C5 + sample 14: + time = 560000 + flags = 0 + data = length 436, hash 76D6404 + sample 15: + time = 600000 + flags = 0 + data = length 366, hash 56D49D4D + sample 16: + time = 640000 + flags = 0 + data = length 393, hash 822FC8 + sample 17: + time = 680000 + flags = 0 + data = length 374, hash FA8AE217 + sample 18: + time = 720000 + flags = 536870912 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4.mp4.2.dump b/library/core/src/test/assets/mp4/sample_ac4.mp4.2.dump new file mode 100644 index 0000000000..92ba157e3f --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4.mp4.2.dump @@ -0,0 +1,107 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=758]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = 622 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 7613 + sample count = 19 + sample 0: + time = 0 + flags = 1 + data = length 367, hash D2762FA + sample 1: + time = 40000 + flags = 0 + data = length 367, hash BDD3224A + sample 2: + time = 80000 + flags = 0 + data = length 367, hash 9302227B + sample 3: + time = 120000 + flags = 0 + data = length 367, hash 72996003 + sample 4: + time = 160000 + flags = 0 + data = length 367, hash 88AE5A1B + sample 5: + time = 200000 + flags = 0 + data = length 367, hash E5346FE3 + sample 6: + time = 240000 + flags = 0 + data = length 367, hash CE558362 + sample 7: + time = 280000 + flags = 0 + data = length 367, hash 51AD3043 + sample 8: + time = 320000 + flags = 0 + data = length 367, hash EB72E95B + sample 9: + time = 360000 + flags = 0 + data = length 367, hash 47F8FF23 + sample 10: + time = 400000 + flags = 0 + data = length 367, hash 8133883D + sample 11: + time = 440000 + flags = 0 + data = length 495, hash E14BDFEE + sample 12: + time = 480000 + flags = 0 + data = length 520, hash FEE56928 + sample 13: + time = 519999 + flags = 0 + data = length 599, hash 41F496C5 + sample 14: + time = 560000 + flags = 0 + data = length 436, hash 76D6404 + sample 15: + time = 600000 + flags = 0 + data = length 366, hash 56D49D4D + sample 16: + time = 640000 + flags = 0 + data = length 393, hash 822FC8 + sample 17: + time = 680000 + flags = 0 + data = length 374, hash FA8AE217 + sample 18: + time = 720000 + flags = 536870912 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4.mp4.3.dump b/library/core/src/test/assets/mp4/sample_ac4.mp4.3.dump new file mode 100644 index 0000000000..92ba157e3f --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4.mp4.3.dump @@ -0,0 +1,107 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=758]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = 622 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 7613 + sample count = 19 + sample 0: + time = 0 + flags = 1 + data = length 367, hash D2762FA + sample 1: + time = 40000 + flags = 0 + data = length 367, hash BDD3224A + sample 2: + time = 80000 + flags = 0 + data = length 367, hash 9302227B + sample 3: + time = 120000 + flags = 0 + data = length 367, hash 72996003 + sample 4: + time = 160000 + flags = 0 + data = length 367, hash 88AE5A1B + sample 5: + time = 200000 + flags = 0 + data = length 367, hash E5346FE3 + sample 6: + time = 240000 + flags = 0 + data = length 367, hash CE558362 + sample 7: + time = 280000 + flags = 0 + data = length 367, hash 51AD3043 + sample 8: + time = 320000 + flags = 0 + data = length 367, hash EB72E95B + sample 9: + time = 360000 + flags = 0 + data = length 367, hash 47F8FF23 + sample 10: + time = 400000 + flags = 0 + data = length 367, hash 8133883D + sample 11: + time = 440000 + flags = 0 + data = length 495, hash E14BDFEE + sample 12: + time = 480000 + flags = 0 + data = length 520, hash FEE56928 + sample 13: + time = 519999 + flags = 0 + data = length 599, hash 41F496C5 + sample 14: + time = 560000 + flags = 0 + data = length 436, hash 76D6404 + sample 15: + time = 600000 + flags = 0 + data = length 366, hash 56D49D4D + sample 16: + time = 640000 + flags = 0 + data = length 393, hash 822FC8 + sample 17: + time = 680000 + flags = 0 + data = length 374, hash FA8AE217 + sample 18: + time = 720000 + flags = 536870912 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4 b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..2056348768808517cbbf07c417c230d701eb9d35 GIT binary patch literal 8404 zcmeHMc|29y+ushNI9;=wh%!$hhigb?AxSufDAPISV>ss+%5;*1YsyqgQCH!JGFON~ zq%u>HP?f(Zzu>QsKY%X-pLDUwkLXRjLGyD(XZN|asNz+4d!>_{Lkakp>Y@wCrE`5 z4)bMQf2yk{!DV#g21m&q4WA;Qm3m2<8i)AA`rj?Qnvp%^HCB7W!JQArPItp|T%=NZ0`UE8o${MHU5>;q>QH z5da&^_%dZ+KgP)gC6tTMf>?qOmcs}RDhZkTnT~aq;~NPX!l<4TkwR?B=>R3?)RwZ}4GJcH<>$kVs1YQojW`K6`ohByzZ(=o z#0U7$0SHE@ofFFC2)J-Mgb-4| zh2av0)SKu$li zA%?@uQMsR7=t=HHqj42zt6nsjOdjwg7m*9mXmn*S+74|+Cg*QV{y+Lb+Vvj7S)ar^ ztN`H*$nUaefV|-#kYh$S3a672^E z{SWL;Iv@h!Z1ByW`M<~6@IMfgf5VbVrkJ1p{BOYZdzGXUFwQ^y`FmXcuj?cqI6e!1 zi_fNT+~9}LK_CZ!Uf1W~0e{BmABLS2{xd%RFo=Ic0V(lkeEu6S{jdn>>d*N6!yx_* z1*Bp)K8t>PuPgrYUWeHHPJv?c9|$C8G>b^3$xbWIfkrJwj%ljX4$)G_%9fSx{)tXA z)JWu$ZBM#N5qu`8&A{>Tktj^YlN-*@1@-(H^k+K+ycc5~oPQPl8tzOg|WAgCPDC z*iII0P}99%hYvlcqCx<{^^65)E-lSs&Q^zo%v*86TWj01W?9<=&ty=989_4n4kn^y zgtx^>K9pl>E<8B+^_hkSq{Q7F=Z~<>b;)}*)SECgJc{r$`i)%X0J|HHmDR zt~@fS1|hR+J^Q;-x-#=ofinel4yN+{Vk}k-QF?>pE={*V$`e`%n`Uvwyxq0+UpgNo zX>lk2&c&82BaZUPDH*+y?|XtJIpTyn!&Vx;sc!vE5N`w15O}_RrEp2ajx(c;o7;KU z!V~S({d?S^Im$zmWjQIj0oO)PSSqAPbKvjTR8iO;ym`eLs2=RSTx}Yt9%XHzS6wLuFui+qBSUVjh54^)GR*th|j*qVcsQie?^;)re@+o za#|PykZ+UOGxXu9z~CDeL1{S@p)%sleKnDwcVOP&J}rPAjF|Xs4D1?8s9T~X)QQvn z_|(1<(E%6&q&7Hi?fquwm$03q9I%+&^kjLp^gTT`p0eDmzz#Z+FsYG^4#R!gObTHZ z#m(G^K`5}rhmw@-L+|06vrax{_YezHw2{^iV>(AXvMiC^s@!77kx?r!eJ$ruw~mFi zUYBzzM@eMGLiA9_>Zuof3DQVo(6eId`PQ2%_bDZ}RGlvDEu8FZ<&~-xUoLMqsx}cS z$Z&a>$s5X8>C6WKiXd2^#C$<|nIhTbdBU`uxc2>0j^NvdR=0aF4kY(X|qP zga%F{`GkyJCHXD&wB%2RRF8Vj80)LV`?;!#wlZezznF3FVCyhdhsDh7*zOdc8Ri%) zy8`Ro<6iG0$6hOB^E%#+*F`GDw2BNjD+HOV%UT#LP2jH2YX@nJo5wJZ-o9*inD|`x zs%J4KjF);(YK#>4r2Z8hT7jaXY;wF-tcIaZV#!rQHp`+^K6m#0uX22D;`yKPR_$?* zh$@Y{Qwg5ph?N^Q>)z)r@_VVaxh?(zJ8vl_h=)6iH~f|s7M_?57A$IB1b8AVE@)?F zl4rEwchvKdNX^9;YaQ&G;4*lYQ9=`>l^d3C(FE;j?j14B^>I3e8~{KXgdO0-rURe? zAqQaau_rL}KcS3K{R7c^A2RN?vC-Q`ztxFFA;HIGQU@6AdAq%RK4h8x7?T;1Y;wUu zJ+C0P$b8%O>$`Hg6?%pG3k05e7{|yw>rydgyCX1nsqCY$?R>=LKl+XZ?o=$%EsXMG zV~x(YFSOX*UJS@@xqQfb&XR{``oRI~nJ{YSRPZn-FWJ@tJMq5xs}0qb|ao;rlui{HHJ@n$)`&1gkSB;j}%-3728-E$Xo5%g|DjA zEPde771mn2Kw0dr@Hp1jS~xJ+Fov=*uPDmESkFmEoo6^3g{r@0X{D`WFDa7Anba&r zcYc9?`xWtomKU|}L>C`V6*ZPv86MPJzEab#{1lU)qWv1_h?TZJ5ulpAtete%(w8f} z#OP+|{SM2biAg2lqWam4w*@<3ABiPdlo-4oo#(poVFUktwl-` zk~^{YrR%KdwsZBn#5wuK`$wBjjxBBhZ4JWxR^E1&+~VcpaJ z+Ibs0yMTtlcFxpP=`0`@l>Z`hi^@!Uq>)fVJ6%9pX z46Rl#QlkJkpiILD^tMfZQj~}M3DM_(b&eF?!YsyZe?764;cNr#>4(MV$^+i%b`%Ol zTCyg+F964EZSCHk`!0~lm97DM_IELlm`eSp-ux08v*Sp4qGuEq7_(K^VoyCxZqz@wDCW$$1ZInl zPsj~A;#~ zn5P_v-EKa}7MFBC(09k!_-F*#YgvBC<-wyXv%ZTeFEF_DpgkJVX)+&^OKi~Ltdm-H zPHCyv+p{B#wtQ4iOKkIEvJW2VT-6n~?G<d@&itmNeh21DK0 z{b%{yS83VclBM9(ACeaV?#fr_Ew={Z;hmp~0RHKYyAVevSCWg+XsZF}Dja=`OwNHe z|2llis{&tdBlqRP#>#TQI?~-pkT768p|zN^nlffi!{SVw_|kQ1*d=v82VqfFV@+J- zx=(%v_x;rfJEz63rBykfx+a^;bAziwwr5FsXk9k2xc!y!DzW^NeO_@Z7FAA^EUO%H zKwfZDkd^8Q&_>M{#EQC6*)w;!RqH;OG_1^h7b~k0pMw}s+a}mq)^x7cu;z(Q{l$KR ztZYv8w`e~>Ic`p+wog7TjhX>+w50AQ`ugjRhvSmHjCPW9Ix(uxoI0g1$@-_@P{xm? z8D*-dF2Plw7gvu6Q3^{g;vQ)>5AqtGBYi%u3J?AyXLvu z8+leyX3ci#gxiFd5^+j3=3$FE8A-870G4BrL=O#^GOF~DS%=Y|rDuQ~ajsyU!iAU;O!zz!|t;2ibG!l-nlAC*CUq1|U zzJtpwe8>A4;FEWcbnJc0YO{?aYbvxM^HLz?vRTH+-Y!q?ff(H?l8aF=U-^Xt%1?EB z@GjCUzhBX7smNV$r}#BNj20_Cs;ZaKIteX+pq z!&-VA$!0Qq_jMQ?Nj@G2^Bg|Dm;I&j;9|kF+X}xZTkJql+v%!4z5!qLPa)|BCgW;z z6g3Nr<~Sa+YJ3Q-yxDd|vcm9lf=@z=lY`q}dGlK1X#Xq0%6bu&fVE0nK3&ff1^bc7 z9i6zg_y>doql>l)rJnwZsa2~fgvZnG7P*^GHtP2FX=mfE)t;3|12c{UuFcnhXdp}z zTnOhmkY@v!h~(r)MjkVdConMgi5dHRbYHvTdw#v;;Bj^Nk!KU{)h~GnW~C@eU03xh zmz8R@E8yTyv!d%}BZe?t7KtsJ@#n5+`Ym-31jS>0-aehlvzx4;d=hkTbj9s!H7^_Q z6R$jGqD0uQgyO#OC`jkxoFJx_$ws{nETzBGOJ?peSixCW30?K#{tA@VMRV7IDHM>qvw z=P^)b>yY-=*K*<%zWI4^ar!s?-{+qfhcbK472)-k?}?qD)+YHsB&{GhBK3x*yt)R+ zx`K9%W@pkgd}Uw84S+>uW9*)XMDI4(w?vjKAKcSNwLO#VFz}K+vn9zvh_TePv`={@q7P554MrS>+VQyZRFAy0acD z?aE*T7=<3FK|e14^28nL|idO$GP`*QP literal 0 HcmV?d00001 diff --git a/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.0.dump b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.0.dump new file mode 100644 index 0000000000..505c85e51f --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.0.dump @@ -0,0 +1,107 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=685]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 7613 + sample count = 19 + sample 0: + time = 0 + flags = 1 + data = length 367, hash D2762FA + sample 1: + time = 40000 + flags = 1 + data = length 367, hash BDD3224A + sample 2: + time = 80000 + flags = 1 + data = length 367, hash 9302227B + sample 3: + time = 120000 + flags = 1 + data = length 367, hash 72996003 + sample 4: + time = 160000 + flags = 1 + data = length 367, hash 88AE5A1B + sample 5: + time = 200000 + flags = 1 + data = length 367, hash E5346FE3 + sample 6: + time = 240000 + flags = 1 + data = length 367, hash CE558362 + sample 7: + time = 280000 + flags = 1 + data = length 367, hash 51AD3043 + sample 8: + time = 320000 + flags = 1 + data = length 367, hash EB72E95B + sample 9: + time = 360000 + flags = 1 + data = length 367, hash 47F8FF23 + sample 10: + time = 400000 + flags = 1 + data = length 367, hash 8133883D + sample 11: + time = 440000 + flags = 1 + data = length 495, hash E14BDFEE + sample 12: + time = 480000 + flags = 1 + data = length 520, hash FEE56928 + sample 13: + time = 520000 + flags = 1 + data = length 599, hash 41F496C5 + sample 14: + time = 560000 + flags = 1 + data = length 436, hash 76D6404 + sample 15: + time = 600000 + flags = 1 + data = length 366, hash 56D49D4D + sample 16: + time = 640000 + flags = 1 + data = length 393, hash 822FC8 + sample 17: + time = 680000 + flags = 1 + data = length 374, hash FA8AE217 + sample 18: + time = 720000 + flags = 1 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.1.dump b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.1.dump new file mode 100644 index 0000000000..8bee343bd9 --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.1.dump @@ -0,0 +1,83 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=685]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 5411 + sample count = 13 + sample 0: + time = 240000 + flags = 1 + data = length 367, hash CE558362 + sample 1: + time = 280000 + flags = 1 + data = length 367, hash 51AD3043 + sample 2: + time = 320000 + flags = 1 + data = length 367, hash EB72E95B + sample 3: + time = 360000 + flags = 1 + data = length 367, hash 47F8FF23 + sample 4: + time = 400000 + flags = 1 + data = length 367, hash 8133883D + sample 5: + time = 440000 + flags = 1 + data = length 495, hash E14BDFEE + sample 6: + time = 480000 + flags = 1 + data = length 520, hash FEE56928 + sample 7: + time = 520000 + flags = 1 + data = length 599, hash 41F496C5 + sample 8: + time = 560000 + flags = 1 + data = length 436, hash 76D6404 + sample 9: + time = 600000 + flags = 1 + data = length 366, hash 56D49D4D + sample 10: + time = 640000 + flags = 1 + data = length 393, hash 822FC8 + sample 11: + time = 680000 + flags = 1 + data = length 374, hash FA8AE217 + sample 12: + time = 720000 + flags = 1 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.2.dump b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.2.dump new file mode 100644 index 0000000000..ee1cf91a57 --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.2.dump @@ -0,0 +1,59 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=685]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 3081 + sample count = 7 + sample 0: + time = 480000 + flags = 1 + data = length 520, hash FEE56928 + sample 1: + time = 520000 + flags = 1 + data = length 599, hash 41F496C5 + sample 2: + time = 560000 + flags = 1 + data = length 436, hash 76D6404 + sample 3: + time = 600000 + flags = 1 + data = length 366, hash 56D49D4D + sample 4: + time = 640000 + flags = 1 + data = length 393, hash 822FC8 + sample 5: + time = 680000 + flags = 1 + data = length 374, hash FA8AE217 + sample 6: + time = 720000 + flags = 1 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.3.dump b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.3.dump new file mode 100644 index 0000000000..419f0444bf --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.3.dump @@ -0,0 +1,35 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=685]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 393 + sample count = 1 + sample 0: + time = 720000 + flags = 1 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java index a29dfcc310..1f49aee293 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java @@ -51,6 +51,12 @@ public final class FragmentedMp4ExtractorTest { ExtractorAsserts.assertBehavior(extractorFactory, "mp4/sample_fragmented_sei.mp4"); } + @Test + public void testSampleWithAc4Track() throws Exception { + ExtractorAsserts.assertBehavior( + getExtractorFactory(Collections.emptyList()), "mp4/sample_ac4_fragmented.mp4"); + } + private static ExtractorFactory getExtractorFactory(final List closedCaptionFormats) { return () -> new FragmentedMp4Extractor(0, null, null, null, closedCaptionFormats); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java index b5c3b26a23..6ddc74c797 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java @@ -42,4 +42,9 @@ public final class Mp4ExtractorTest { public void testMp4SampleWithMdatTooLong() throws Exception { ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_mdat_too_long.mp4"); } + + @Test + public void testMp4SampleWithAc4Track() throws Exception { + ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_ac4.mp4"); + } } From b5fa338367819c0a1338efbb3da0403c4ca91387 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 3 Jan 2020 10:22:27 +0000 Subject: [PATCH 0588/1335] Show ad markers after the window duration Issue: #6552 PiperOrigin-RevId: 287964221 --- RELEASENOTES.md | 3 +++ .../com/google/android/exoplayer2/ui/PlayerControlView.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2dba34486b..027f27da7a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -37,6 +37,9 @@ * Support "twos" codec (big endian PCM) in MP4 ([#5789](https://github.com/google/ExoPlayer/issues/5789)). * WAV: Support IMA ADPCM encoded data. +* Show ad group markers in `DefaultTimeBar` even if they are after the end of + the current window + ([#6552](https://github.com/google/ExoPlayer/issues/6552)). ### 2.11.1 (2019-12-20) ### 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 248ac9fdaf..bfb4e018f0 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 @@ -943,7 +943,7 @@ public class PlayerControlView extends FrameLayout { adGroupTimeInPeriodUs = period.durationUs; } long adGroupTimeInWindowUs = adGroupTimeInPeriodUs + period.getPositionInWindowUs(); - if (adGroupTimeInWindowUs >= 0 && adGroupTimeInWindowUs <= window.durationUs) { + if (adGroupTimeInWindowUs >= 0) { if (adGroupCount == adGroupTimesMs.length) { int newLength = adGroupTimesMs.length == 0 ? 1 : adGroupTimesMs.length * 2; adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, newLength); From f76b4fe63ece2df4d94319110f91186a390abffb Mon Sep 17 00:00:00 2001 From: kimvde Date: Fri, 3 Jan 2020 12:11:10 +0000 Subject: [PATCH 0589/1335] Add unit tests to FLAC extractor related classes PiperOrigin-RevId: 287973192 --- .../exoplayer2/extractor/FlacFrameReader.java | 2 +- .../extractor/FlacFrameReaderTest.java | 323 ++++++++++++++ .../extractor/FlacMetadataReaderTest.java | 408 ++++++++++++++++++ .../util/FlacStreamMetadataTest.java | 24 ++ 4 files changed, 756 insertions(+), 1 deletion(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/extractor/FlacFrameReaderTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/extractor/FlacMetadataReaderTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacFrameReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacFrameReader.java index 1e498cb677..f014eaa565 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacFrameReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacFrameReader.java @@ -167,7 +167,7 @@ public final class FlacFrameReader { * @param data The array to read the data from, whose position must correspond to the block size * bits. * @param blockSizeKey The key in the block size lookup table. - * @return The block size in samples. + * @return The block size in samples, or -1 if the {@code blockSizeKey} is invalid. */ public static int readFrameBlockSizeSamplesFromKey(ParsableByteArray data, int blockSizeKey) { switch (blockSizeKey) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/FlacFrameReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/FlacFrameReaderTest.java new file mode 100644 index 0000000000..87487a4199 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/FlacFrameReaderTest.java @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.extractor.FlacFrameReader.SampleNumberHolder; +import com.google.android.exoplayer2.extractor.FlacMetadataReader.FlacStreamMetadataHolder; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.FlacConstants; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Unit tests for {@link FlacFrameReader}. + * + *

    Some expected results in these tests have been retrieved using the flac command. + */ +@RunWith(AndroidJUnit4.class) +public class FlacFrameReaderTest { + + @Test + public void checkAndReadFrameHeader_validData_updatesPosition() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE); + input.read(scratch.data, 0, FlacConstants.MAX_FRAME_HEADER_SIZE); + + FlacFrameReader.checkAndReadFrameHeader( + scratch, + streamMetadataHolder.flacStreamMetadata, + frameStartMarker, + new SampleNumberHolder()); + + assertThat(scratch.getPosition()).isEqualTo(FlacConstants.MIN_FRAME_HEADER_SIZE); + } + + @Test + public void checkAndReadFrameHeader_validData_isTrue() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE); + input.read(scratch.data, 0, FlacConstants.MAX_FRAME_HEADER_SIZE); + + boolean result = + FlacFrameReader.checkAndReadFrameHeader( + scratch, + streamMetadataHolder.flacStreamMetadata, + frameStartMarker, + new SampleNumberHolder()); + + assertThat(result).isTrue(); + } + + @Test + public void checkAndReadFrameHeader_validData_writesSampleNumber() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + // Skip first frame. + input.skip(5030); + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE); + input.read(scratch.data, 0, FlacConstants.MAX_FRAME_HEADER_SIZE); + SampleNumberHolder sampleNumberHolder = new SampleNumberHolder(); + + FlacFrameReader.checkAndReadFrameHeader( + scratch, streamMetadataHolder.flacStreamMetadata, frameStartMarker, sampleNumberHolder); + + assertThat(sampleNumberHolder.sampleNumber).isEqualTo(4096); + } + + @Test + public void checkAndReadFrameHeader_invalidData_isFalse() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE); + input.read(scratch.data, 0, FlacConstants.MAX_FRAME_HEADER_SIZE); + + // The first bytes of the frame are not equal to the frame start marker. + boolean result = + FlacFrameReader.checkAndReadFrameHeader( + scratch, + streamMetadataHolder.flacStreamMetadata, + /* frameStartMarker= */ -1, + new SampleNumberHolder()); + + assertThat(result).isFalse(); + } + + @Test + public void checkFrameHeaderFromPeek_validData_doesNotUpdatePositions() throws Exception { + String file = "flac/bear_one_metadata_block.flac"; + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = buildExtractorInputReadingFromFirstFrame(file, streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + long peekPosition = input.getPosition(); + // Set read position to 0. + input = buildExtractorInput(file); + input.advancePeekPosition((int) peekPosition); + + FlacFrameReader.checkFrameHeaderFromPeek( + input, streamMetadataHolder.flacStreamMetadata, frameStartMarker, new SampleNumberHolder()); + + assertThat(input.getPosition()).isEqualTo(0); + assertThat(input.getPeekPosition()).isEqualTo(peekPosition); + } + + @Test + public void checkFrameHeaderFromPeek_validData_isTrue() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + + boolean result = + FlacFrameReader.checkFrameHeaderFromPeek( + input, + streamMetadataHolder.flacStreamMetadata, + frameStartMarker, + new SampleNumberHolder()); + + assertThat(result).isTrue(); + } + + @Test + public void checkFrameHeaderFromPeek_validData_writesSampleNumber() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + // Skip first frame. + input.skip(5030); + SampleNumberHolder sampleNumberHolder = new SampleNumberHolder(); + + FlacFrameReader.checkFrameHeaderFromPeek( + input, streamMetadataHolder.flacStreamMetadata, frameStartMarker, sampleNumberHolder); + + assertThat(sampleNumberHolder.sampleNumber).isEqualTo(4096); + } + + @Test + public void checkFrameHeaderFromPeek_invalidData_isFalse() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + + // The first bytes of the frame are not equal to the frame start marker. + boolean result = + FlacFrameReader.checkFrameHeaderFromPeek( + input, + streamMetadataHolder.flacStreamMetadata, + /* frameStartMarker= */ -1, + new SampleNumberHolder()); + + assertThat(result).isFalse(); + } + + @Test + public void checkFrameHeaderFromPeek_invalidData_doesNotUpdatePositions() throws Exception { + String file = "flac/bear_one_metadata_block.flac"; + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = buildExtractorInputReadingFromFirstFrame(file, streamMetadataHolder); + long peekPosition = input.getPosition(); + // Set read position to 0. + input = buildExtractorInput(file); + input.advancePeekPosition((int) peekPosition); + + // The first bytes of the frame are not equal to the frame start marker. + FlacFrameReader.checkFrameHeaderFromPeek( + input, + streamMetadataHolder.flacStreamMetadata, + /* frameStartMarker= */ -1, + new SampleNumberHolder()); + + assertThat(input.getPosition()).isEqualTo(0); + assertThat(input.getPeekPosition()).isEqualTo(peekPosition); + } + + @Test + public void getFirstSampleNumber_doesNotUpdateReadPositionAndAlignsPeekPosition() + throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + long initialReadPosition = input.getPosition(); + // Advance peek position after block size bits. + input.advancePeekPosition(FlacConstants.MAX_FRAME_HEADER_SIZE); + + FlacFrameReader.getFirstSampleNumber(input, streamMetadataHolder.flacStreamMetadata); + + assertThat(input.getPosition()).isEqualTo(initialReadPosition); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test + public void getFirstSampleNumber_returnsSampleNumber() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + // Skip first frame. + input.skip(5030); + + long result = + FlacFrameReader.getFirstSampleNumber(input, streamMetadataHolder.flacStreamMetadata); + + assertThat(result).isEqualTo(4096); + } + + @Test + public void readFrameBlockSizeSamplesFromKey_keyIs1_returnsCorrectBlockSize() { + int result = + FlacFrameReader.readFrameBlockSizeSamplesFromKey( + new ParsableByteArray(/* limit= */ 0), /* blockSizeKey= */ 1); + + assertThat(result).isEqualTo(192); + } + + @Test + public void readFrameBlockSizeSamplesFromKey_keyBetween2and5_returnsCorrectBlockSize() { + int result = + FlacFrameReader.readFrameBlockSizeSamplesFromKey( + new ParsableByteArray(/* limit= */ 0), /* blockSizeKey= */ 3); + + assertThat(result).isEqualTo(1152); + } + + @Test + public void readFrameBlockSizeSamplesFromKey_keyBetween6And7_returnsCorrectBlockSize() + throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_one_metadata_block.flac"); + // Skip to block size bits of last frame. + input.skipFully(164033); + ParsableByteArray scratch = new ParsableByteArray(2); + input.readFully(scratch.data, 0, 2); + + int result = FlacFrameReader.readFrameBlockSizeSamplesFromKey(scratch, /* blockSizeKey= */ 7); + + assertThat(result).isEqualTo(496); + } + + @Test + public void readFrameBlockSizeSamplesFromKey_keyBetween8and15_returnsCorrectBlockSize() { + int result = + FlacFrameReader.readFrameBlockSizeSamplesFromKey( + new ParsableByteArray(/* limit= */ 0), /* blockSizeKey= */ 11); + + assertThat(result).isEqualTo(2048); + } + + @Test + public void readFrameBlockSizeSamplesFromKey_invalidKey_returnsCorrectBlockSize() { + int result = + FlacFrameReader.readFrameBlockSizeSamplesFromKey( + new ParsableByteArray(/* limit= */ 0), /* blockSizeKey= */ 25); + + assertThat(result).isEqualTo(-1); + } + + private static ExtractorInput buildExtractorInput(String file) throws IOException { + byte[] fileData = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), file); + return new FakeExtractorInput.Builder().setData(fileData).build(); + } + + private ExtractorInput buildExtractorInputReadingFromFirstFrame( + String file, FlacStreamMetadataHolder streamMetadataHolder) + throws IOException, InterruptedException { + ExtractorInput input = buildExtractorInput(file); + + input.skipFully(FlacConstants.STREAM_MARKER_SIZE); + + boolean lastMetadataBlock = false; + while (!lastMetadataBlock) { + lastMetadataBlock = FlacMetadataReader.readMetadataBlock(input, streamMetadataHolder); + } + + return input; + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/FlacMetadataReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/FlacMetadataReaderTest.java new file mode 100644 index 0000000000..390e806807 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/FlacMetadataReaderTest.java @@ -0,0 +1,408 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.extractor.FlacMetadataReader.FlacStreamMetadataHolder; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.flac.PictureFrame; +import com.google.android.exoplayer2.metadata.flac.VorbisComment; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.FlacConstants; +import com.google.android.exoplayer2.util.FlacStreamMetadata; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import java.util.ArrayList; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Unit tests for {@link FlacMetadataReader}. + * + *

    Most expected results in these tests have been retrieved using the metaflac command. + */ +@RunWith(AndroidJUnit4.class) +public class FlacMetadataReaderTest { + + @Test + public void peekId3Metadata_updatesPeekPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3_enabled.flac"); + + FlacMetadataReader.peekId3Metadata(input, /* parseData= */ false); + + assertThat(input.getPosition()).isEqualTo(0); + assertThat(input.getPeekPosition()).isNotEqualTo(0); + } + + @Test + public void peekId3Metadata_parseData_returnsNonEmptyMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3_enabled.flac"); + + Metadata metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ true); + + assertThat(metadata).isNotNull(); + assertThat(metadata.length()).isNotEqualTo(0); + } + + @Test + public void peekId3Metadata_doNotParseData_returnsNull() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3_enabled.flac"); + + Metadata metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ false); + + assertThat(metadata).isNull(); + } + + @Test + public void peekId3Metadata_noId3Metadata_returnsNull() throws Exception { + String fileWithoutId3Metadata = "flac/bear.flac"; + ExtractorInput input = buildExtractorInput(fileWithoutId3Metadata); + + Metadata metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ true); + + assertThat(metadata).isNull(); + } + + @Test + public void checkAndPeekStreamMarker_updatesPeekPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + + FlacMetadataReader.checkAndPeekStreamMarker(input); + + assertThat(input.getPosition()).isEqualTo(0); + assertThat(input.getPeekPosition()).isEqualTo(FlacConstants.STREAM_MARKER_SIZE); + } + + @Test + public void checkAndPeekStreamMarker_validData_isTrue() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + + boolean result = FlacMetadataReader.checkAndPeekStreamMarker(input); + + assertThat(result).isTrue(); + } + + @Test + public void checkAndPeekStreamMarker_invalidData_isFalse() throws Exception { + ExtractorInput input = buildExtractorInput("mp3/bear.mp3"); + + boolean result = FlacMetadataReader.checkAndPeekStreamMarker(input); + + assertThat(result).isFalse(); + } + + @Test + public void readId3Metadata_updatesReadPositionAndAlignsPeekPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3_enabled.flac"); + // Advance peek position after ID3 metadata. + FlacMetadataReader.peekId3Metadata(input, /* parseData= */ false); + input.advancePeekPosition(1); + + FlacMetadataReader.readId3Metadata(input, /* parseData= */ false); + + assertThat(input.getPosition()).isNotEqualTo(0); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test + public void readId3Metadata_parseData_returnsNonEmptyMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3_enabled.flac"); + + Metadata metadata = FlacMetadataReader.readId3Metadata(input, /* parseData= */ true); + + assertThat(metadata).isNotNull(); + assertThat(metadata.length()).isNotEqualTo(0); + } + + @Test + public void readId3Metadata_doNotParseData_returnsNull() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3_enabled.flac"); + + Metadata metadata = FlacMetadataReader.readId3Metadata(input, /* parseData= */ false); + + assertThat(metadata).isNull(); + } + + @Test + public void readId3Metadata_noId3Metadata_returnsNull() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + + Metadata metadata = FlacMetadataReader.readId3Metadata(input, /* parseData= */ true); + + assertThat(metadata).isNull(); + } + + @Test + public void readStreamMarker_updatesReadPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + + FlacMetadataReader.readStreamMarker(input); + + assertThat(input.getPosition()).isEqualTo(FlacConstants.STREAM_MARKER_SIZE); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test(expected = ParserException.class) + public void readStreamMarker_invalidData_throwsException() throws Exception { + ExtractorInput input = buildExtractorInput("mp3/bear.mp3"); + + FlacMetadataReader.readStreamMarker(input); + } + + @Test + public void readMetadataBlock_updatesReadPositionAndAlignsPeekPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + input.skipFully(FlacConstants.STREAM_MARKER_SIZE); + // Advance peek position after metadata block. + input.advancePeekPosition(FlacConstants.STREAM_INFO_BLOCK_SIZE + 1); + + FlacMetadataReader.readMetadataBlock( + input, new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null)); + + assertThat(input.getPosition()).isNotEqualTo(0); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test + public void readMetadataBlock_lastMetadataBlock_isTrue() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_one_metadata_block.flac"); + input.skipFully(FlacConstants.STREAM_MARKER_SIZE); + + boolean result = + FlacMetadataReader.readMetadataBlock( + input, new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null)); + + assertThat(result).isTrue(); + } + + @Test + public void readMetadataBlock_notLastMetadataBlock_isFalse() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + input.skipFully(FlacConstants.STREAM_MARKER_SIZE); + + boolean result = + FlacMetadataReader.readMetadataBlock( + input, new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null)); + + assertThat(result).isFalse(); + } + + @Test + public void readMetadataBlock_streamInfoBlock_setsStreamMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + input.skipFully(FlacConstants.STREAM_MARKER_SIZE); + FlacStreamMetadataHolder metadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + + FlacMetadataReader.readMetadataBlock(input, metadataHolder); + + assertThat(metadataHolder.flacStreamMetadata).isNotNull(); + assertThat(metadataHolder.flacStreamMetadata.sampleRate).isEqualTo(48000); + } + + @Test + public void readMetadataBlock_seekTableBlock_updatesStreamMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to seek table block. + input.skipFully(FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE); + FlacStreamMetadataHolder metadataHolder = new FlacStreamMetadataHolder(buildStreamMetadata()); + long originalSampleRate = metadataHolder.flacStreamMetadata.sampleRate; + + FlacMetadataReader.readMetadataBlock(input, metadataHolder); + + assertThat(metadataHolder.flacStreamMetadata).isNotNull(); + // Check that metadata passed has not been erased. + assertThat(metadataHolder.flacStreamMetadata.sampleRate).isEqualTo(originalSampleRate); + assertThat(metadataHolder.flacStreamMetadata.seekTable).isNotNull(); + assertThat(metadataHolder.flacStreamMetadata.seekTable.pointSampleNumbers.length).isEqualTo(32); + } + + @Test + public void readMetadataBlock_vorbisCommentBlock_updatesStreamMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_vorbis_comments.flac"); + // Skip to Vorbis comment block. + input.skipFully(640); + FlacStreamMetadataHolder metadataHolder = new FlacStreamMetadataHolder(buildStreamMetadata()); + long originalSampleRate = metadataHolder.flacStreamMetadata.sampleRate; + + FlacMetadataReader.readMetadataBlock(input, metadataHolder); + + assertThat(metadataHolder.flacStreamMetadata).isNotNull(); + // Check that metadata passed has not been erased. + assertThat(metadataHolder.flacStreamMetadata.sampleRate).isEqualTo(originalSampleRate); + Metadata metadata = + metadataHolder.flacStreamMetadata.getMetadataCopyWithAppendedEntriesFrom(null); + assertThat(metadata).isNotNull(); + VorbisComment vorbisComment = (VorbisComment) metadata.get(0); + assertThat(vorbisComment.key).isEqualTo("TITLE"); + assertThat(vorbisComment.value).isEqualTo("test title"); + } + + @Test + public void readMetadataBlock_pictureBlock_updatesStreamMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_picture.flac"); + // Skip to picture block. + input.skipFully(640); + FlacStreamMetadataHolder metadataHolder = new FlacStreamMetadataHolder(buildStreamMetadata()); + long originalSampleRate = metadataHolder.flacStreamMetadata.sampleRate; + + FlacMetadataReader.readMetadataBlock(input, metadataHolder); + + assertThat(metadataHolder.flacStreamMetadata).isNotNull(); + // Check that metadata passed has not been erased. + assertThat(metadataHolder.flacStreamMetadata.sampleRate).isEqualTo(originalSampleRate); + Metadata metadata = + metadataHolder.flacStreamMetadata.getMetadataCopyWithAppendedEntriesFrom(null); + assertThat(metadata).isNotNull(); + PictureFrame pictureFrame = (PictureFrame) metadata.get(0); + assertThat(pictureFrame.pictureType).isEqualTo(3); + assertThat(pictureFrame.mimeType).isEqualTo("image/png"); + assertThat(pictureFrame.description).isEqualTo(""); + assertThat(pictureFrame.width).isEqualTo(371); + assertThat(pictureFrame.height).isEqualTo(320); + assertThat(pictureFrame.depth).isEqualTo(24); + assertThat(pictureFrame.colors).isEqualTo(0); + assertThat(pictureFrame.pictureData).hasLength(30943); + } + + @Test + public void readMetadataBlock_blockToSkip_updatesReadPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to padding block. + input.skipFully(640); + FlacStreamMetadataHolder metadataHolder = new FlacStreamMetadataHolder(buildStreamMetadata()); + + FlacMetadataReader.readMetadataBlock(input, metadataHolder); + + assertThat(input.getPosition()).isGreaterThan(640); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test(expected = IllegalArgumentException.class) + public void readMetadataBlock_nonStreamInfoBlockWithNullStreamMetadata_throwsException() + throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to seek table block. + input.skipFully(FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE); + + FlacMetadataReader.readMetadataBlock( + input, new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null)); + } + + @Test + public void readSeekTableMetadataBlock_updatesPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to seek table block. + input.skipFully(FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE); + int seekTableBlockSize = 598; + ParsableByteArray scratch = new ParsableByteArray(seekTableBlockSize); + input.read(scratch.data, 0, seekTableBlockSize); + + FlacMetadataReader.readSeekTableMetadataBlock(scratch); + + assertThat(scratch.getPosition()).isEqualTo(seekTableBlockSize); + } + + @Test + public void readSeekTableMetadataBlock_returnsCorrectSeekPoints() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to seek table block. + input.skipFully(FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE); + int seekTableBlockSize = 598; + ParsableByteArray scratch = new ParsableByteArray(seekTableBlockSize); + input.read(scratch.data, 0, seekTableBlockSize); + + FlacStreamMetadata.SeekTable seekTable = FlacMetadataReader.readSeekTableMetadataBlock(scratch); + + assertThat(seekTable.pointOffsets[0]).isEqualTo(0); + assertThat(seekTable.pointSampleNumbers[0]).isEqualTo(0); + assertThat(seekTable.pointOffsets[31]).isEqualTo(160602); + assertThat(seekTable.pointSampleNumbers[31]).isEqualTo(126976); + } + + @Test + public void readSeekTableMetadataBlock_ignoresPlaceholders() throws IOException { + byte[] fileData = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), "flac/bear.flac"); + ParsableByteArray scratch = new ParsableByteArray(fileData); + // Skip to seek table block. + scratch.skipBytes(FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE); + + FlacStreamMetadata.SeekTable seekTable = FlacMetadataReader.readSeekTableMetadataBlock(scratch); + + // Seek point at index 32 is a placeholder. + assertThat(seekTable.pointSampleNumbers).hasLength(32); + } + + @Test + public void getFrameStartMarker_doesNotUpdateReadPositionAndAlignsPeekPosition() + throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + int firstFramePosition = 8880; + input.skipFully(firstFramePosition); + // Advance the peek position after the frame start marker. + input.advancePeekPosition(3); + + FlacMetadataReader.getFrameStartMarker(input); + + assertThat(input.getPosition()).isEqualTo(firstFramePosition); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test + public void getFrameStartMarker_returnsCorrectFrameStartMarker() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to first frame. + input.skipFully(8880); + + int result = FlacMetadataReader.getFrameStartMarker(input); + + assertThat(result).isEqualTo(0xFFF8); + } + + @Test(expected = ParserException.class) + public void getFrameStartMarker_invalidData_throwsException() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + + // Input position is incorrect. + FlacMetadataReader.getFrameStartMarker(input); + } + + private static ExtractorInput buildExtractorInput(String file) throws IOException { + byte[] fileData = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), file); + return new FakeExtractorInput.Builder().setData(fileData).build(); + } + + private static FlacStreamMetadata buildStreamMetadata() { + return new FlacStreamMetadata( + /* minBlockSizeSamples= */ 10, + /* maxBlockSizeSamples= */ 20, + /* minFrameSize= */ 5, + /* maxFrameSize= */ 10, + /* sampleRate= */ 44100, + /* channels= */ 2, + /* bitsPerSample= */ 8, + /* totalSamples= */ 1000, + /* vorbisComments= */ new ArrayList<>(), + /* pictureFrames= */ new ArrayList<>()); + } +} 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 ddaa550b7f..d1b0363d20 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 @@ -17,9 +17,12 @@ package com.google.android.exoplayer2.util; import static com.google.common.truth.Truth.assertThat; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.flac.VorbisComment; +import com.google.android.exoplayer2.testutil.TestUtil; +import java.io.IOException; import java.util.ArrayList; import org.junit.Test; import org.junit.runner.RunWith; @@ -28,6 +31,27 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class FlacStreamMetadataTest { + @Test + public void constructFromByteArray_setsFieldsCorrectly() throws IOException { + byte[] fileData = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), "flac/bear.flac"); + + FlacStreamMetadata streamMetadata = + new FlacStreamMetadata( + fileData, FlacConstants.STREAM_MARKER_SIZE + FlacConstants.METADATA_BLOCK_HEADER_SIZE); + + assertThat(streamMetadata.minBlockSizeSamples).isEqualTo(4096); + assertThat(streamMetadata.maxBlockSizeSamples).isEqualTo(4096); + assertThat(streamMetadata.minFrameSize).isEqualTo(445); + assertThat(streamMetadata.maxFrameSize).isEqualTo(5776); + assertThat(streamMetadata.sampleRate).isEqualTo(48000); + assertThat(streamMetadata.sampleRateLookupKey).isEqualTo(10); + assertThat(streamMetadata.channels).isEqualTo(2); + assertThat(streamMetadata.bitsPerSample).isEqualTo(16); + assertThat(streamMetadata.bitsPerSampleLookupKey).isEqualTo(4); + assertThat(streamMetadata.totalSamples).isEqualTo(131568); + } + @Test public void parseVorbisComments() { ArrayList commentsList = new ArrayList<>(); From 1c0e69789fbf936b5ea2e066396d4886fd7cca98 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 3 Jan 2020 15:31:37 +0000 Subject: [PATCH 0590/1335] Clear existing Spans when applying CSS styles in WebvttCueParser Relying on the precedence of spans seems risky - I can't find it defined anywhere. It might have changed in Android 6.0? https://stackoverflow.com/q/34631851 PiperOrigin-RevId: 287989365 --- .../text/webvtt/WebvttCueParser.java | 46 ++++++++++++------- .../text/webvtt/WebvttDecoderTest.java | 2 +- 2 files changed, 31 insertions(+), 17 deletions(-) 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 3a07a74042..f4c0f26fc8 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 @@ -492,8 +492,7 @@ public final class WebvttCueParser { return; } if (style.getStyle() != WebvttCssStyle.UNSPECIFIED) { - spannedText.setSpan(new StyleSpan(style.getStyle()), start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + addOrReplaceSpan(spannedText, new StyleSpan(style.getStyle()), start, end); } if (style.isLinethrough()) { spannedText.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); @@ -502,34 +501,29 @@ public final class WebvttCueParser { spannedText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.hasFontColor()) { - spannedText.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + addOrReplaceSpan(spannedText, new ForegroundColorSpan(style.getFontColor()), start, end); } if (style.hasBackgroundColor()) { - spannedText.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + addOrReplaceSpan( + spannedText, new BackgroundColorSpan(style.getBackgroundColor()), start, end); } if (style.getFontFamily() != null) { - spannedText.setSpan(new TypefaceSpan(style.getFontFamily()), start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + addOrReplaceSpan(spannedText, new TypefaceSpan(style.getFontFamily()), start, end); } Layout.Alignment textAlign = style.getTextAlign(); if (textAlign != null) { - spannedText.setSpan( - new AlignmentSpan.Standard(textAlign), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + addOrReplaceSpan(spannedText, new AlignmentSpan.Standard(textAlign), start, end); } switch (style.getFontSizeUnit()) { case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL: - spannedText.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + addOrReplaceSpan( + spannedText, new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end); break; case WebvttCssStyle.FONT_SIZE_UNIT_EM: - spannedText.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + addOrReplaceSpan(spannedText, new RelativeSizeSpan(style.getFontSize()), start, end); break; case WebvttCssStyle.FONT_SIZE_UNIT_PERCENT: - spannedText.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + addOrReplaceSpan(spannedText, new RelativeSizeSpan(style.getFontSize() / 100), start, end); break; case WebvttCssStyle.UNSPECIFIED: // Do nothing. @@ -537,6 +531,26 @@ public final class WebvttCueParser { } } + /** + * Adds {@code span} to {@code spannedText} between {@code start} and {@code end}, removing any + * existing spans of the same type and with the same indices. + * + *

    This is useful for types of spans that don't make sense to duplicate and where the + * evaluation order might have an unexpected impact on the final text, e.g. {@link + * ForegroundColorSpan}. + */ + private static void addOrReplaceSpan( + SpannableStringBuilder spannedText, Object span, int start, int end) { + Object[] existingSpans = spannedText.getSpans(start, end, span.getClass()); + for (Object existingSpan : existingSpans) { + if (spannedText.getSpanStart(existingSpan) == start + && spannedText.getSpanEnd(existingSpan) == end) { + spannedText.removeSpan(existingSpan); + } + } + spannedText.setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + /** * Returns the tag name for the given tag contents. * diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index f405f1c407..5c044c029b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -405,7 +405,7 @@ public class WebvttDecoderTest { Spanned s4 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 25000000); assertThat(s1.getSpans(/* start= */ 0, s1.length(), ForegroundColorSpan.class)).hasLength(1); assertThat(s1.getSpans(/* start= */ 0, s1.length(), BackgroundColorSpan.class)).hasLength(1); - assertThat(s2.getSpans(/* start= */ 0, s2.length(), ForegroundColorSpan.class)).hasLength(2); + assertThat(s2.getSpans(/* start= */ 0, s2.length(), ForegroundColorSpan.class)).hasLength(1); assertThat(s3.getSpans(/* start= */ 10, s3.length(), UnderlineSpan.class)).hasLength(1); assertThat(s4.getSpans(/* start= */ 0, /* end= */ 16, BackgroundColorSpan.class)).hasLength(2); assertThat(s4.getSpans(/* start= */ 17, s4.length(), StyleSpan.class)).hasLength(1); From 0587180f147e842dc3e589826f4606cb3ef2ccd3 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 3 Jan 2020 15:32:06 +0000 Subject: [PATCH 0591/1335] Make SpannedSubject more fluent I decided the flags bit was a bit unclear so I played around with this It's also needed for more 'complex' assertions like colors - I didn't want to just chuck in a fourth int parameter to create: hasForegroundColorSpan(int start, int end, int flags, int color) PiperOrigin-RevId: 287989424 --- .../text/webvtt/WebvttCueParserTest.java | 16 +- .../text/webvtt/WebvttDecoderTest.java | 31 +- .../testutil/truth/SpannedSubject.java | 320 ++++++++++++++++-- .../testutil/truth/SpannedSubjectTest.java | 165 ++++++++- 4 files changed, 478 insertions(+), 54 deletions(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java index d23ed00e95..c9e8488c60 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java @@ -34,8 +34,7 @@ public final class WebvttCueParserTest { + "This is text with html tags"); assertThat(text.toString()).isEqualTo("This is text with html tags"); - assertThat(text) - .hasUnderlineSpan("This ".length(), "This is".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + assertThat(text).hasUnderlineSpanBetween("This ".length(), "This is".length()); assertThat(text) .hasBoldItalicSpan( "This is text with ".length(), @@ -59,10 +58,7 @@ public final class WebvttCueParserTest { assertThat(text.toString()).isEqualTo("An unclosed u tag with italic inside"); assertThat(text) - .hasUnderlineSpan( - "An ".length(), - "An unclosed u tag with italic inside".length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + .hasUnderlineSpanBetween("An ".length(), "An unclosed u tag with italic inside".length()); assertThat(text) .hasItalicSpan( "An unclosed u tag with ".length(), @@ -81,10 +77,9 @@ public final class WebvttCueParserTest { "An italic tag with unclosed underline".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); assertThat(text) - .hasUnderlineSpan( + .hasUnderlineSpanBetween( "An italic tag with unclosed ".length(), - "An italic tag with unclosed underline".length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + "An italic tag with unclosed underline".length()); } @Test @@ -95,8 +90,7 @@ public final class WebvttCueParserTest { assertThat(text.toString()).isEqualTo(expectedText); assertThat(text).hasBoldSpan(0, expectedText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // Text between the tags is underlined. - assertThat(text) - .hasUnderlineSpan(0, "Overlapping u and".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + assertThat(text).hasUnderlineSpanBetween(0, "Overlapping u and".length()); // Only text from to <\\u> is italic (unexpected - but simplifies the parsing). assertThat(text) .hasItalicSpan( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index 5c044c029b..a3ab3e8b1a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -15,23 +15,23 @@ */ package com.google.android.exoplayer2.text.webvtt; +import static com.google.android.exoplayer2.testutil.truth.SpannedSubject.assertThat; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import android.graphics.Typeface; import android.text.Layout.Alignment; import android.text.Spanned; -import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; -import android.text.style.UnderlineSpan; import androidx.annotation.Nullable; 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.SubtitleDecoderException; +import com.google.android.exoplayer2.util.ColorParser; import com.google.common.truth.Expect; import java.io.IOException; import java.util.List; @@ -403,14 +403,25 @@ public class WebvttDecoderTest { Spanned s2 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2345000); Spanned s3 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 20000000); Spanned s4 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 25000000); - assertThat(s1.getSpans(/* start= */ 0, s1.length(), ForegroundColorSpan.class)).hasLength(1); - assertThat(s1.getSpans(/* start= */ 0, s1.length(), BackgroundColorSpan.class)).hasLength(1); - assertThat(s2.getSpans(/* start= */ 0, s2.length(), ForegroundColorSpan.class)).hasLength(1); - assertThat(s3.getSpans(/* start= */ 10, s3.length(), UnderlineSpan.class)).hasLength(1); - assertThat(s4.getSpans(/* start= */ 0, /* end= */ 16, BackgroundColorSpan.class)).hasLength(2); - assertThat(s4.getSpans(/* start= */ 17, s4.length(), StyleSpan.class)).hasLength(1); - assertThat(s4.getSpans(/* start= */ 17, s4.length(), StyleSpan.class)[0].getStyle()) - .isEqualTo(Typeface.BOLD); + assertThat(s1) + .hasForegroundColorSpanBetween(0, s1.length()) + .withColor(ColorParser.parseCssColor("papayawhip")); + assertThat(s1) + .hasBackgroundColorSpanBetween(0, s1.length()) + .withColor(ColorParser.parseCssColor("green")); + assertThat(s2) + .hasForegroundColorSpanBetween(0, s2.length()) + .withColor(ColorParser.parseCssColor("peachpuff")); + + assertThat(s3).hasUnderlineSpanBetween(10, s3.length()); + assertThat(s4) + .hasBackgroundColorSpanBetween(0, 16) + .withColor(ColorParser.parseCssColor("lime")); + assertThat(s4) + .hasBoldSpan( + /* startIndex= */ 17, + /* endIndex= */ s4.length(), + /* flags= */ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } @Test diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java index 0015634c1f..84d40fb6f1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java @@ -23,8 +23,12 @@ import static com.google.common.truth.Truth.assertAbout; import android.graphics.Typeface; import android.text.Spanned; import android.text.TextUtils; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; +import androidx.annotation.CheckResult; +import androidx.annotation.ColorInt; import androidx.annotation.Nullable; import com.google.common.truth.FailureMetadata; import com.google.common.truth.Subject; @@ -64,7 +68,7 @@ public final class SpannedSubject extends Subject { failWithoutActual( simpleFact("Expected no spans"), fact("in text", actual), - fact("but found", actualSpansString())); + fact("but found", getAllSpansAsStringWithoutFlags(actual))); } } @@ -76,6 +80,7 @@ public final class SpannedSubject extends Subject { * @param flags The flags of the expected span. See constants on {@link Spanned} for more * information. */ + // TODO: swap this to fluent-style. public void hasItalicSpan(int startIndex, int endIndex, int flags) { hasStyleSpan(startIndex, endIndex, flags, Typeface.ITALIC); } @@ -88,6 +93,7 @@ public final class SpannedSubject extends Subject { * @param flags The flags of the expected span. See constants on {@link Spanned} for more * information. */ + // TODO: swap this to fluent-style. public void hasBoldSpan(int startIndex, int endIndex, int flags) { hasStyleSpan(startIndex, endIndex, flags, Typeface.BOLD); } @@ -104,7 +110,7 @@ public final class SpannedSubject extends Subject { } } - failWithExpectedSpan( + failWithExpectedSpanWithFlags( startIndex, endIndex, flags, @@ -129,6 +135,7 @@ public final class SpannedSubject extends Subject { * @param flags The flags of the expected span. See constants on {@link Spanned} for more * information. */ + // TODO: swap this to fluent-style. public void hasBoldItalicSpan(int startIndex, int endIndex, int flags) { if (actual == null) { failWithoutActual(simpleFact("Spanned must not be null")); @@ -149,11 +156,13 @@ public final class SpannedSubject extends Subject { String spannedSubstring = actual.toString().substring(startIndex, endIndex); String boldSpan = - spanToString(startIndex, endIndex, flags, new StyleSpan(Typeface.BOLD), spannedSubstring); + getSpanAsStringWithFlags( + startIndex, endIndex, flags, new StyleSpan(Typeface.BOLD), spannedSubstring); String italicSpan = - spanToString(startIndex, endIndex, flags, new StyleSpan(Typeface.ITALIC), spannedSubstring); + getSpanAsStringWithFlags( + startIndex, endIndex, flags, new StyleSpan(Typeface.ITALIC), spannedSubstring); String boldItalicSpan = - spanToString( + getSpanAsStringWithFlags( startIndex, endIndex, flags, new StyleSpan(Typeface.BOLD_ITALIC), spannedSubstring); failWithoutActual( @@ -161,34 +170,89 @@ public final class SpannedSubject extends Subject { fact("in text", actual.toString()), fact("expected either", boldItalicSpan), fact("or both", boldSpan + "\n" + italicSpan), - fact("but found", actualSpansString())); + fact("but found", getAllSpansAsStringWithFlags(actual))); } /** - * Checks that the subject has an underline span from {@code startIndex} to {@code endIndex}. + * Checks that the subject has an {@link UnderlineSpan} from {@code start} to {@code end}. * - * @param startIndex The start of the expected span. - * @param endIndex The end of the expected span. - * @param flags The flags of the expected span. See constants on {@link Spanned} for more - * information. + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. */ - public void hasUnderlineSpan(int startIndex, int endIndex, int flags) { + public WithSpanFlags hasUnderlineSpanBetween(int start, int end) { if (actual == null) { failWithoutActual(simpleFact("Spanned must not be null")); - return; + return ALREADY_FAILED_WITH_FLAGS; } - List underlineSpans = - findMatchingSpans(startIndex, endIndex, flags, UnderlineSpan.class); - if (underlineSpans.size() == 1) { - return; + List underlineSpans = findMatchingSpans(start, end, UnderlineSpan.class); + List allFlags = new ArrayList<>(); + for (UnderlineSpan span : underlineSpans) { + allFlags.add(actual.getSpanFlags(span)); } - failWithExpectedSpan( - startIndex, - endIndex, - flags, - new UnderlineSpan(), - actual.toString().substring(startIndex, endIndex)); + if (underlineSpans.size() == 1) { + return check("UnderlineSpan (start=%s,end=%s)", start, end).about(spanFlags()).that(allFlags); + } + failWithExpectedSpanWithoutFlags( + start, end, UnderlineSpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_WITH_FLAGS; + } + + /** + * Checks that the subject as a {@link ForegroundColorSpan} from {@code start} to {@code end}. + * + *

    The color is asserted in a follow-up method call on the return {@link Colored} object. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link Colored} object to assert on the color of the matching spans. + */ + @CheckResult + public Colored hasForegroundColorSpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_COLORED; + } + + List foregroundColorSpans = + findMatchingSpans(start, end, ForegroundColorSpan.class); + if (foregroundColorSpans.isEmpty()) { + failWithExpectedSpanWithoutFlags( + start, end, ForegroundColorSpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_COLORED; + } + return check("ForegroundColorSpan (start=%s,end=%s)", start, end) + .about(foregroundColorSpans(actual)) + .that(foregroundColorSpans); + } + + /** + * Checks that the subject as a {@link ForegroundColorSpan} from {@code start} to {@code end}. + * + *

    The color is asserted in a follow-up method call on the return {@link Colored} object. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link Colored} object to assert on the color of the matching spans. + */ + @CheckResult + public Colored hasBackgroundColorSpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_COLORED; + } + + List backgroundColorSpans = + findMatchingSpans(start, end, BackgroundColorSpan.class); + if (backgroundColorSpans.isEmpty()) { + failWithExpectedSpanWithoutFlags( + start, end, BackgroundColorSpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_COLORED; + } + return check("BackgroundColorSpan (start=%s,end=%s)", start, end) + .about(backgroundColorSpans(actual)) + .that(backgroundColorSpans); } private List findMatchingSpans( @@ -204,27 +268,46 @@ public final class SpannedSubject extends Subject { return spans; } - private void failWithExpectedSpan( + private List findMatchingSpans(int startIndex, int endIndex, Class spanClazz) { + List spans = new ArrayList<>(); + for (T span : actual.getSpans(startIndex, endIndex, spanClazz)) { + if (actual.getSpanStart(span) == startIndex && actual.getSpanEnd(span) == endIndex) { + spans.add(span); + } + } + return spans; + } + + private void failWithExpectedSpanWithFlags( int start, int end, int flags, Object span, String spannedSubstring) { failWithoutActual( simpleFact("No matching span found"), fact("in text", actual), - fact("expected", spanToString(start, end, flags, span, spannedSubstring)), - fact("but found", actualSpansString())); + fact("expected", getSpanAsStringWithFlags(start, end, flags, span, spannedSubstring)), + fact("but found", getAllSpansAsStringWithFlags(actual))); } - private String actualSpansString() { + private void failWithExpectedSpanWithoutFlags( + int start, int end, Class spanType, String spannedSubstring) { + failWithoutActual( + simpleFact("No matching span found"), + fact("in text", actual), + fact("expected", getSpanAsStringWithoutFlags(start, end, spanType, spannedSubstring)), + fact("but found", getAllSpansAsStringWithoutFlags(actual))); + } + + private static String getAllSpansAsStringWithFlags(Spanned spanned) { List actualSpanStrings = new ArrayList<>(); - for (Object span : actual.getSpans(0, actual.length(), /* type= */ Object.class)) { - actualSpanStrings.add(spanToString(span, actual)); + for (Object span : spanned.getSpans(0, spanned.length(), Object.class)) { + actualSpanStrings.add(getSpanAsStringWithFlags(span, spanned)); } return TextUtils.join("\n", actualSpanStrings); } - private static String spanToString(Object span, Spanned spanned) { + private static String getSpanAsStringWithFlags(Object span, Spanned spanned) { int spanStart = spanned.getSpanStart(span); int spanEnd = spanned.getSpanEnd(span); - return spanToString( + return getSpanAsStringWithFlags( spanStart, spanEnd, spanned.getSpanFlags(span), @@ -232,7 +315,7 @@ public final class SpannedSubject extends Subject { spanned.toString().substring(spanStart, spanEnd)); } - private static String spanToString( + private static String getSpanAsStringWithFlags( int start, int end, int flags, Object span, String spannedSubstring) { String suffix; if (span instanceof StyleSpan) { @@ -244,4 +327,177 @@ public final class SpannedSubject extends Subject { "start=%s\tend=%s\tflags=%s\ttype=%s\tsubstring='%s'%s", start, end, flags, span.getClass().getSimpleName(), spannedSubstring, suffix); } + + private static String getAllSpansAsStringWithoutFlags(Spanned spanned) { + List actualSpanStrings = new ArrayList<>(); + for (Object span : spanned.getSpans(0, spanned.length(), Object.class)) { + actualSpanStrings.add(getSpanAsStringWithoutFlags(span, spanned)); + } + return TextUtils.join("\n", actualSpanStrings); + } + + private static String getSpanAsStringWithoutFlags(Object span, Spanned spanned) { + int spanStart = spanned.getSpanStart(span); + int spanEnd = spanned.getSpanEnd(span); + return getSpanAsStringWithoutFlags( + spanStart, spanEnd, span.getClass(), spanned.toString().substring(spanStart, spanEnd)); + } + + private static String getSpanAsStringWithoutFlags( + int start, int end, Class span, String spannedSubstring) { + return String.format( + "start=%s\tend=%s\ttype=%s\tsubstring='%s'", + start, end, span.getSimpleName(), spannedSubstring); + } + + /** + * Allows additional assertions to be made on the flags of matching spans. + * + *

    Identical to {@link WithSpanFlags}, but this should be returned from {@code with...()} + * methods while {@link WithSpanFlags} should be returned from {@code has...()} methods. + * + *

    See Flag constants on {@link Spanned} for possible values. + */ + public interface AndSpanFlags { + + /** + * Checks that one of the matched spans has the expected {@code flags}. + * + * @param flags The expected flags. See SPAN_* constants on {@link Spanned} for possible values. + */ + void andFlags(int flags); + } + + private static final AndSpanFlags ALREADY_FAILED_AND_FLAGS = flags -> {}; + + /** + * Allows additional assertions to be made on the flags of matching spans. + * + *

    Identical to {@link AndSpanFlags}, but this should be returned from {@code has...()} methods + * while {@link AndSpanFlags} should be returned from {@code with...()} methods. + */ + public interface WithSpanFlags { + + /** + * Checks that one of the matched spans has the expected {@code flags}. + * + * @param flags The expected flags. See SPAN_* constants on {@link Spanned} for possible values. + */ + void withFlags(int flags); + } + + private static final WithSpanFlags ALREADY_FAILED_WITH_FLAGS = flags -> {}; + + private static Factory> spanFlags() { + return SpanFlagsSubject::new; + } + + private static final class SpanFlagsSubject extends Subject + implements AndSpanFlags, WithSpanFlags { + + private final List flags; + + private SpanFlagsSubject(FailureMetadata metadata, List flags) { + super(metadata, flags); + this.flags = flags; + } + + @Override + public void andFlags(int flags) { + check("contains()").that(this.flags).contains(flags); + } + + @Override + public void withFlags(int flags) { + andFlags(flags); + } + } + + /** Allows assertions about the color of a span. */ + public interface Colored { + + /** + * Checks that at least one of the matched spans has the expected {@code color}. + * + * @param color The expected color. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. + */ + AndSpanFlags withColor(@ColorInt int color); + } + + private static final Colored ALREADY_FAILED_COLORED = color -> ALREADY_FAILED_AND_FLAGS; + + private Factory> foregroundColorSpans( + Spanned actualSpanned) { + return (FailureMetadata metadata, List spans) -> + new ForegroundColorSpansSubject(metadata, spans, actualSpanned); + } + + private static final class ForegroundColorSpansSubject extends Subject implements Colored { + + private final List actualSpans; + private final Spanned actualSpanned; + + private ForegroundColorSpansSubject( + FailureMetadata metadata, List actualSpans, Spanned actualSpanned) { + super(metadata, actualSpans); + this.actualSpans = actualSpans; + this.actualSpanned = actualSpanned; + } + + @Override + public AndSpanFlags withColor(@ColorInt int color) { + List matchingSpanFlags = new ArrayList<>(); + // Use hex strings for comparison so the values in error messages are more human readable. + List spanColors = new ArrayList<>(); + + for (ForegroundColorSpan span : actualSpans) { + spanColors.add(String.format("0x%08X", span.getForegroundColor())); + if (span.getForegroundColor() == color) { + matchingSpanFlags.add(actualSpanned.getSpanFlags(span)); + } + } + + String expectedColorString = String.format("0x%08X", color); + check("foregroundColor").that(spanColors).containsExactly(expectedColorString); + return check("flags").about(spanFlags()).that(matchingSpanFlags); + } + } + + private Factory> backgroundColorSpans( + Spanned actualSpanned) { + return (FailureMetadata metadata, List spans) -> + new BackgroundColorSpansSubject(metadata, spans, actualSpanned); + } + + private static final class BackgroundColorSpansSubject extends Subject implements Colored { + + private final List actualSpans; + private final Spanned actualSpanned; + + private BackgroundColorSpansSubject( + FailureMetadata metadata, List actualSpans, Spanned actualSpanned) { + super(metadata, actualSpans); + this.actualSpans = actualSpans; + this.actualSpanned = actualSpanned; + } + + @Override + public AndSpanFlags withColor(@ColorInt int color) { + List matchingSpanFlags = new ArrayList<>(); + // Use hex strings for comparison so the values in error messages are more human readable. + List spanColors = new ArrayList<>(); + + for (BackgroundColorSpan span : actualSpans) { + spanColors.add(String.format("0x%08X", span.getBackgroundColor())); + if (span.getBackgroundColor() == color) { + matchingSpanFlags.add(actualSpanned.getSpanFlags(span)); + } + } + + String expectedColorString = String.format("0x%08X", color); + check("backgroundColor").that(spanColors).containsExactly(expectedColorString); + return check("flags").about(spanFlags()).that(matchingSpanFlags); + } + } } diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java index 37ccef6908..a01f6f66e0 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java @@ -21,9 +21,12 @@ import static com.google.android.exoplayer2.testutil.truth.SpannedSubject.spanne import static com.google.common.truth.ExpectFailure.assertThat; import static com.google.common.truth.ExpectFailure.expectFailureAbout; +import android.graphics.Color; import android.graphics.Typeface; import android.text.SpannableString; import android.text.Spanned; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -153,7 +156,167 @@ public class SpannedSubjectTest { int end = start + "underlined".length(); spannable.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - assertThat(spannable).hasUnderlineSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + assertThat(spannable) + .hasUnderlineSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void foregroundColorSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasForegroundColorSpanBetween(start, end) + .withColor(Color.CYAN) + .andFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void foregroundColorSpan_wrongEndIndex() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + int incorrectEnd = end + 2; + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasForegroundColorSpanBetween(start, incorrectEnd) + .withColor(Color.CYAN)); + assertThat(expected).factValue("expected").contains("end=" + incorrectEnd); + assertThat(expected).factValue("but found").contains("end=" + end); + } + + @Test + public void foregroundColorSpan_wrongColor() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasForegroundColorSpanBetween(start, end) + .withColor(Color.BLUE)); + assertThat(expected).factValue("value of").contains("foregroundColor"); + assertThat(expected).factValue("expected").contains("0xFF0000FF"); // Color.BLUE + assertThat(expected).factValue("but was").contains("0xFF00FFFF"); // Color.CYAN + } + + @Test + public void foregroundColorSpan_wrongFlags() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasForegroundColorSpanBetween(start, end) + .withColor(Color.CYAN) + .andFlags(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected).factValue("value of").contains("flags"); + assertThat(expected) + .factValue("expected to contain") + .contains(String.valueOf(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected) + .factValue("but was") + .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + } + + @Test + public void backgroundColorSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new BackgroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasBackgroundColorSpanBetween(start, end) + .withColor(Color.CYAN) + .andFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void backgroundColorSpan_wrongEndIndex() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new BackgroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + int incorrectEnd = end + 2; + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasBackgroundColorSpanBetween(start, incorrectEnd) + .withColor(Color.CYAN)); + assertThat(expected).factValue("expected").contains("end=" + incorrectEnd); + assertThat(expected).factValue("but found").contains("end=" + end); + } + + @Test + public void backgroundColorSpan_wrongColor() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new BackgroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasBackgroundColorSpanBetween(start, end) + .withColor(Color.BLUE)); + assertThat(expected).factValue("value of").contains("backgroundColor"); + assertThat(expected).factValue("expected").contains("0xFF0000FF"); // Color.BLUE + assertThat(expected).factValue("but was").contains("0xFF00FFFF"); // Color.CYAN + } + + @Test + public void backgroundColorSpan_wrongFlags() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new BackgroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasBackgroundColorSpanBetween(start, end) + .withColor(Color.CYAN) + .andFlags(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected).factValue("value of").contains("flags"); + assertThat(expected) + .factValue("expected to contain") + .contains(String.valueOf(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected) + .factValue("but was") + .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); } private static AssertionError expectFailure( From 88e70d7c1b237c25a21f063462defb665e54bae1 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 3 Jan 2020 15:32:35 +0000 Subject: [PATCH 0592/1335] Remove WebvttCssStyle.cascadeFrom() It's not used. I was trying to work out how to correctly cascade my text-combine-upright styling, but deleting the method seemed easier... PiperOrigin-RevId: 287989480 --- .../text/webvtt/WebvttCssStyle.java | 31 ------------------- 1 file changed, 31 deletions(-) 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 97c0acb1ec..1369859552 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 @@ -287,37 +287,6 @@ public final class WebvttCssStyle { return fontSize; } - public void cascadeFrom(WebvttCssStyle style) { - if (style.hasFontColor) { - setFontColor(style.fontColor); - } - if (style.bold != UNSPECIFIED) { - bold = style.bold; - } - if (style.italic != UNSPECIFIED) { - italic = style.italic; - } - if (style.fontFamily != null) { - fontFamily = style.fontFamily; - } - if (linethrough == UNSPECIFIED) { - linethrough = style.linethrough; - } - if (underline == UNSPECIFIED) { - underline = style.underline; - } - if (textAlign == null) { - textAlign = style.textAlign; - } - if (fontSizeUnit == UNSPECIFIED) { - fontSizeUnit = style.fontSizeUnit; - fontSize = style.fontSize; - } - if (style.hasBackgroundColor) { - setBackgroundColor(style.backgroundColor); - } - } - private static int updateScoreForMatch( int currentScore, String target, @Nullable String actual, int score) { if (target.isEmpty() || currentScore == -1) { From f1f0ff3a658e7734d58ea19810e27abe1c122dc4 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 3 Jan 2020 16:59:05 +0000 Subject: [PATCH 0593/1335] Use MIME types rather than PCM encodings for ALAW and MLAW PiperOrigin-RevId: 287999703 --- RELEASENOTES.md | 3 + .../ext/ffmpeg/FfmpegAudioRenderer.java | 3 +- .../exoplayer2/ext/ffmpeg/FfmpegDecoder.java | 12 +--- .../exoplayer2/ext/ffmpeg/FfmpegLibrary.java | 20 +++--- .../java/com/google/android/exoplayer2/C.java | 27 +++----- .../com/google/android/exoplayer2/Format.java | 8 +-- .../exoplayer2/audio/DefaultAudioSink.java | 2 - .../audio/ResamplingAudioProcessor.java | 4 -- .../android/exoplayer2/audio/WavUtil.java | 12 +--- .../extractor/flv/AudioTagPayloadReader.java | 17 ++++- .../extractor/wav/WavExtractor.java | 62 ++++++++++++------- .../google/android/exoplayer2/util/Util.java | 2 - 12 files changed, 79 insertions(+), 93 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 027f27da7a..9b2f0e7cbc 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -40,6 +40,9 @@ * Show ad group markers in `DefaultTimeBar` even if they are after the end of the current window ([#6552](https://github.com/google/ExoPlayer/issues/6552)). +* WAV: + * Support IMA ADPCM encoded data. + * Improve support for G.711 A-law and mu-law encoded data. ### 2.11.1 (2019-12-20) ### 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 17292cec34..0673f7893a 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 @@ -98,8 +98,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { Assertions.checkNotNull(format.sampleMimeType); if (!FfmpegLibrary.isAvailable()) { return FORMAT_UNSUPPORTED_TYPE; - } else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType, format.pcmEncoding) - || !isOutputSupported(format)) { + } else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType) || !isOutputSupported(format)) { return FORMAT_UNSUPPORTED_SUBTYPE; } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { return FORMAT_UNSUPPORTED_DRM; 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 5314835d1e..6fa3d888db 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 @@ -64,9 +64,7 @@ import java.util.List; throw new FfmpegDecoderException("Failed to load decoder native libraries."); } Assertions.checkNotNull(format.sampleMimeType); - codecName = - Assertions.checkNotNull( - FfmpegLibrary.getCodecName(format.sampleMimeType, format.pcmEncoding)); + codecName = Assertions.checkNotNull(FfmpegLibrary.getCodecName(format.sampleMimeType)); extraData = getExtraData(format.sampleMimeType, format.initializationData); encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT; outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT; @@ -145,16 +143,12 @@ import java.util.List; nativeContext = 0; } - /** - * Returns the channel count of output audio. May only be called after {@link #decode}. - */ + /** Returns the channel count of output audio. */ public int getChannelCount() { return channelCount; } - /** - * Returns the sample rate of output audio. May only be called after {@link #decode}. - */ + /** Returns the sample rate of output audio. */ public int getSampleRate() { return sampleRate; } 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 5b816b8c20..4639851263 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 @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.ext.ffmpeg; 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; @@ -65,13 +64,12 @@ public final class FfmpegLibrary { * Returns whether the underlying library supports the specified MIME type. * * @param mimeType The MIME type to check. - * @param encoding The PCM encoding for raw audio. */ - public static boolean supportsFormat(String mimeType, @C.PcmEncoding int encoding) { + public static boolean supportsFormat(String mimeType) { if (!isAvailable()) { return false; } - String codecName = getCodecName(mimeType, encoding); + String codecName = getCodecName(mimeType); if (codecName == null) { return false; } @@ -86,7 +84,7 @@ public final class FfmpegLibrary { * Returns the name of the FFmpeg decoder that could be used to decode the format, or {@code null} * if it's unsupported. */ - /* package */ static @Nullable String getCodecName(String mimeType, @C.PcmEncoding int encoding) { + /* package */ static @Nullable String getCodecName(String mimeType) { switch (mimeType) { case MimeTypes.AUDIO_AAC: return "aac"; @@ -116,14 +114,10 @@ public final class FfmpegLibrary { return "flac"; case MimeTypes.AUDIO_ALAC: return "alac"; - case MimeTypes.AUDIO_RAW: - if (encoding == C.ENCODING_PCM_MU_LAW) { - return "pcm_mulaw"; - } else if (encoding == C.ENCODING_PCM_A_LAW) { - return "pcm_alaw"; - } else { - return null; - } + case MimeTypes.AUDIO_MLAW: + return "pcm_mulaw"; + case MimeTypes.AUDIO_ALAW: + return "pcm_alaw"; default: return null; } 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 776e79df97..46f20a20f4 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 @@ -151,10 +151,9 @@ public final class C { * Represents an audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE}, * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link * #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, - * {@link #ENCODING_PCM_FLOAT}, {@link #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link - * #ENCODING_MP3}, {@link #ENCODING_AC3}, {@link #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, - * {@link #ENCODING_AC4}, {@link #ENCODING_DTS}, {@link #ENCODING_DTS_HD} or {@link - * #ENCODING_DOLBY_TRUEHD}. + * {@link #ENCODING_PCM_FLOAT}, {@link #ENCODING_MP3}, {@link #ENCODING_AC3}, {@link + * #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) @@ -167,8 +166,6 @@ public final class C { ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT, - ENCODING_PCM_MU_LAW, - ENCODING_PCM_A_LAW, ENCODING_MP3, ENCODING_AC3, ENCODING_E_AC3, @@ -176,7 +173,7 @@ public final class C { ENCODING_AC4, ENCODING_DTS, ENCODING_DTS_HD, - ENCODING_DOLBY_TRUEHD, + ENCODING_DOLBY_TRUEHD }) public @interface Encoding {} @@ -184,7 +181,7 @@ public final class C { * Represents a PCM audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE}, * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link * #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, - * {@link #ENCODING_PCM_FLOAT}, {@link #ENCODING_PCM_MU_LAW} or {@link #ENCODING_PCM_A_LAW}. + * {@link #ENCODING_PCM_FLOAT}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -196,9 +193,7 @@ public final class C { ENCODING_PCM_16BIT_BIG_ENDIAN, ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, - ENCODING_PCM_FLOAT, - ENCODING_PCM_MU_LAW, - ENCODING_PCM_A_LAW + ENCODING_PCM_FLOAT }) public @interface PcmEncoding {} /** @see AudioFormat#ENCODING_INVALID */ @@ -208,17 +203,13 @@ public final class C { /** @see AudioFormat#ENCODING_PCM_16BIT */ public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT; /** Like {@link #ENCODING_PCM_16BIT}, but with the bytes in big endian order. */ - public static final int ENCODING_PCM_16BIT_BIG_ENDIAN = 0x08000000; + public static final int ENCODING_PCM_16BIT_BIG_ENDIAN = 0x10000000; /** PCM encoding with 24 bits per sample. */ - public static final int ENCODING_PCM_24BIT = 0x80000000; + public static final int ENCODING_PCM_24BIT = 0x20000000; /** PCM encoding with 32 bits per sample. */ - public static final int ENCODING_PCM_32BIT = 0x40000000; + public static final int ENCODING_PCM_32BIT = 0x30000000; /** @see AudioFormat#ENCODING_PCM_FLOAT */ public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT; - /** Audio encoding for mu-law. */ - public static final int ENCODING_PCM_MU_LAW = 0x10000000; - /** Audio encoding for A-law. */ - public static final int ENCODING_PCM_A_LAW = 0x20000000; /** @see AudioFormat#ENCODING_MP3 */ public static final int ENCODING_MP3 = AudioFormat.ENCODING_MP3; /** @see AudioFormat#ENCODING_AC3 */ 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 4fb6cec1e8..19ed34405a 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 @@ -138,13 +138,7 @@ public final class Format implements Parcelable { * The audio sampling rate in Hz, or {@link #NO_VALUE} if unknown or not applicable. */ public final int sampleRate; - /** - * The encoding for PCM audio streams. If {@link #sampleMimeType} is {@link MimeTypes#AUDIO_RAW} - * then one of {@link C#ENCODING_PCM_8BIT}, {@link C#ENCODING_PCM_16BIT}, {@link - * C#ENCODING_PCM_24BIT}, {@link C#ENCODING_PCM_32BIT}, {@link C#ENCODING_PCM_FLOAT}, {@link - * C#ENCODING_PCM_MU_LAW} or {@link C#ENCODING_PCM_A_LAW}. Set to {@link #NO_VALUE} for other - * media types. - */ + /** The {@link C.PcmEncoding} for PCM audio. Set to {@link #NO_VALUE} for other media types. */ public final @C.PcmEncoding int pcmEncoding; /** * The number of frames to trim from the start of the decoded audio stream, or 0 if not 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 d73cf0be40..27abf486fa 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 @@ -1149,9 +1149,7 @@ public final class DefaultAudioSink implements AudioSink { case C.ENCODING_PCM_24BIT: case C.ENCODING_PCM_32BIT: case C.ENCODING_PCM_8BIT: - case C.ENCODING_PCM_A_LAW: case C.ENCODING_PCM_FLOAT: - case C.ENCODING_PCM_MU_LAW: case Format.NO_VALUE: default: throw new IllegalArgumentException(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java index 30bd4da472..7175b93614 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java @@ -64,8 +64,6 @@ import java.nio.ByteBuffer; break; case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_FLOAT: - case C.ENCODING_PCM_A_LAW: - case C.ENCODING_PCM_MU_LAW: case C.ENCODING_INVALID: case Format.NO_VALUE: default: @@ -105,8 +103,6 @@ import java.nio.ByteBuffer; break; case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_FLOAT: - case C.ENCODING_PCM_A_LAW: - case C.ENCODING_PCM_MU_LAW: case C.ENCODING_INVALID: case Format.NO_VALUE: default: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java index 25261f1686..dff81021de 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java @@ -36,9 +36,9 @@ public final class WavUtil { /** WAVE type value for float PCM audio data. */ public static final int TYPE_FLOAT = 0x0003; /** WAVE type value for 8-bit ITU-T G.711 A-law audio data. */ - public static final int TYPE_A_LAW = 0x0006; + public static final int TYPE_ALAW = 0x0006; /** WAVE type value for 8-bit ITU-T G.711 mu-law audio data. */ - public static final int TYPE_MU_LAW = 0x0007; + public static final int TYPE_MLAW = 0x0007; /** WAVE type value for IMA ADPCM audio data. */ public static final int TYPE_IMA_ADPCM = 0x0011; /** WAVE type value for extended WAVE format. */ @@ -59,10 +59,6 @@ public final class WavUtil { case C.ENCODING_PCM_24BIT: case C.ENCODING_PCM_32BIT: return TYPE_PCM; - case C.ENCODING_PCM_A_LAW: - return TYPE_A_LAW; - case C.ENCODING_PCM_MU_LAW: - return TYPE_MU_LAW; case C.ENCODING_PCM_FLOAT: return TYPE_FLOAT; case C.ENCODING_INVALID: @@ -83,10 +79,6 @@ public final class WavUtil { return Util.getPcmEncoding(bitsPerSample); case TYPE_FLOAT: return bitsPerSample == 32 ? C.ENCODING_PCM_FLOAT : C.ENCODING_INVALID; - case TYPE_A_LAW: - return C.ENCODING_PCM_A_LAW; - case TYPE_MU_LAW: - return C.ENCODING_PCM_MU_LAW; default: return C.ENCODING_INVALID; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java index b10f2bf80b..4a904844ee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java @@ -69,9 +69,20 @@ import java.util.Collections; } else if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) { String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW : MimeTypes.AUDIO_MLAW; - int pcmEncoding = (header & 0x01) == 1 ? C.ENCODING_PCM_16BIT : C.ENCODING_PCM_8BIT; - Format format = Format.createAudioSampleFormat(null, type, null, Format.NO_VALUE, - Format.NO_VALUE, 1, 8000, pcmEncoding, null, null, 0, null); + Format format = + Format.createAudioSampleFormat( + /* id= */ null, + /* sampleMimeType= */ type, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + /* channelCount= */ 1, + /* sampleRate= */ 8000, + /* pcmEncoding= */ Format.NO_VALUE, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); output.format(format); hasOutputFormat = true; } else if (audioFormat != AUDIO_FORMAT_AAC) { 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 0c6e538f43..d9989aeaf6 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 @@ -94,13 +94,31 @@ public final class WavExtractor implements Extractor { if (header.formatType == WavUtil.TYPE_IMA_ADPCM) { outputWriter = new ImaAdPcmOutputWriter(extractorOutput, trackOutput, header); + } else if (header.formatType == WavUtil.TYPE_ALAW) { + outputWriter = + new PassthroughOutputWriter( + extractorOutput, + trackOutput, + header, + MimeTypes.AUDIO_ALAW, + /* pcmEncoding= */ Format.NO_VALUE); + } else if (header.formatType == WavUtil.TYPE_MLAW) { + outputWriter = + new PassthroughOutputWriter( + extractorOutput, + trackOutput, + header, + MimeTypes.AUDIO_MLAW, + /* pcmEncoding= */ Format.NO_VALUE); } else { @C.PcmEncoding int pcmEncoding = WavUtil.getPcmEncodingForType(header.formatType, header.bitsPerSample); if (pcmEncoding == C.ENCODING_INVALID) { throw new ParserException("Unsupported WAV format type: " + header.formatType); } - outputWriter = new PcmOutputWriter(extractorOutput, trackOutput, header, pcmEncoding); + outputWriter = + new PassthroughOutputWriter( + extractorOutput, trackOutput, header, MimeTypes.AUDIO_RAW, pcmEncoding); } } @@ -155,12 +173,12 @@ public final class WavExtractor implements Extractor { throws IOException, InterruptedException; } - private static final class PcmOutputWriter implements OutputWriter { + private static final class PassthroughOutputWriter implements OutputWriter { private final ExtractorOutput extractorOutput; private final TrackOutput trackOutput; private final WavHeader header; - private final @C.PcmEncoding int pcmEncoding; + private final Format format; /** The target size of each output sample, in bytes. */ private final int targetSampleSizeBytes; @@ -178,19 +196,33 @@ public final class WavExtractor implements Extractor { */ private long outputFrameCount; - public PcmOutputWriter( + public PassthroughOutputWriter( ExtractorOutput extractorOutput, TrackOutput trackOutput, WavHeader header, + String mimeType, @C.PcmEncoding int pcmEncoding) { this.extractorOutput = extractorOutput; this.trackOutput = trackOutput; this.header = header; - this.pcmEncoding = pcmEncoding; - // For PCM blocks correspond to single frames. This is validated in init(int, long). + // Blocks are expected to correspond to single frames. This is validated in init(int, long). int bytesPerFrame = header.blockSize; targetSampleSizeBytes = Math.max(bytesPerFrame, header.frameRateHz * bytesPerFrame / TARGET_SAMPLES_PER_SECOND); + format = + Format.createAudioSampleFormat( + /* id= */ null, + mimeType, + /* codecs= */ null, + /* bitrate= */ header.frameRateHz * bytesPerFrame * 8, + /* maxInputSize= */ targetSampleSizeBytes, + header.numChannels, + header.frameRateHz, + pcmEncoding, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); } @Override @@ -209,25 +241,9 @@ public final class WavExtractor implements Extractor { "Expected block size: " + bytesPerFrame + "; got: " + header.blockSize); } - // Output the seek map. + // Output the seek map and format. extractorOutput.seekMap( new WavSeekMap(header, /* framesPerBlock= */ 1, dataStartPosition, dataEndPosition)); - - // Output the format. - Format format = - Format.createAudioSampleFormat( - /* id= */ null, - MimeTypes.AUDIO_RAW, - /* codecs= */ null, - /* bitrate= */ header.frameRateHz * bytesPerFrame * 8, - /* maxInputSize= */ targetSampleSizeBytes, - header.numChannels, - header.frameRateHz, - pcmEncoding, - /* initializationData= */ null, - /* drmInitData= */ null, - /* selectionFlags= */ 0, - /* language= */ null); trackOutput.format(format); } 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 aa87096ebb..3ca86ef13d 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 @@ -1431,8 +1431,6 @@ public final class Util { case C.ENCODING_PCM_32BIT: case C.ENCODING_PCM_FLOAT: return channelCount * 4; - case C.ENCODING_PCM_A_LAW: - case C.ENCODING_PCM_MU_LAW: case C.ENCODING_INVALID: case Format.NO_VALUE: default: From 1c1c0ed88a5755915d0a3b6da49d48cd56fcab39 Mon Sep 17 00:00:00 2001 From: christosts Date: Fri, 3 Jan 2020 17:44:27 +0000 Subject: [PATCH 0594/1335] Remove getDequeueOutputBufferTimeoutUs Remove unused method MediaCodecRenderer#getDequeueOutputBufferTimeoutUs(). PiperOrigin-RevId: 288005572 --- .../exoplayer2/mediacodec/MediaCodecRenderer.java | 11 +---------- .../mediacodec/SynchronousMediaCodecAdapter.java | 6 ++---- 2 files changed, 3 insertions(+), 14 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 e973b70204..dbfeed4063 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 @@ -1001,7 +1001,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecAdapter = new MultiLockAsyncMediaCodecAdapter(codec, getTrackType()); ((MultiLockAsyncMediaCodecAdapter) codecAdapter).start(); } else { - codecAdapter = new SynchronousMediaCodecAdapter(codec, getDequeueOutputBufferTimeoutUs()); + codecAdapter = new SynchronousMediaCodecAdapter(codec); } TraceUtil.endSection(); @@ -1460,15 +1460,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { && SystemClock.elapsedRealtime() < codecHotswapDeadlineMs)); } - /** - * Returns the maximum time to block whilst waiting for a decoded output buffer. - * - * @return The maximum time to block, in microseconds. - */ - protected long getDequeueOutputBufferTimeoutUs() { - return 0; - } - /** * Returns the {@link MediaFormat#KEY_OPERATING_RATE} value for a given renderer operating rate, * current {@link Format} and set of possible stream formats. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java index 8caf72ecf4..7dd7ef8f20 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java @@ -24,11 +24,9 @@ import android.media.MediaFormat; */ /* package */ final class SynchronousMediaCodecAdapter implements MediaCodecAdapter { private final MediaCodec codec; - private final long dequeueOutputBufferTimeoutMs; - public SynchronousMediaCodecAdapter(MediaCodec mediaCodec, long dequeueOutputBufferTimeoutMs) { + public SynchronousMediaCodecAdapter(MediaCodec mediaCodec) { this.codec = mediaCodec; - this.dequeueOutputBufferTimeoutMs = dequeueOutputBufferTimeoutMs; } @Override @@ -38,7 +36,7 @@ import android.media.MediaFormat; @Override public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { - return codec.dequeueOutputBuffer(bufferInfo, dequeueOutputBufferTimeoutMs); + return codec.dequeueOutputBuffer(bufferInfo, 0); } @Override From 29df73e2681e1dcab2227e08dfd2c949ebece942 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 6 Jan 2020 11:48:54 +0000 Subject: [PATCH 0595/1335] Fix MatroskaExtractor to use blockDurationUs not durationUs This typo was introduced in https://github.com/google/ExoPlayer/commit/ddb70d96ad99f07fe10f53a76ce3262fe625be70 when migrating a static method with parameter `durationUs` to an instance method where the correct field to use was `blockDurationUs` (but `durationUs` also exists). The test that catches this was only added in https://github.com/google/ExoPlayer/commit/45013ece1e3fe054ff8960355a89559241eeb288 (and therefore configured with the wrong expected output data). issue:#6833 PiperOrigin-RevId: 288274197 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/extractor/mkv/MatroskaExtractor.java | 4 ++-- library/core/src/test/assets/mkv/full_blocks.mkv.0.dump | 6 +++--- library/core/src/test/assets/mkv/full_blocks.mkv.1.dump | 6 +++--- library/core/src/test/assets/mkv/full_blocks.mkv.2.dump | 6 +++--- library/core/src/test/assets/mkv/full_blocks.mkv.3.dump | 6 +++--- 6 files changed, 16 insertions(+), 14 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9b2f0e7cbc..8df1b1f698 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -43,6 +43,8 @@ * WAV: * Support IMA ADPCM encoded data. * Improve support for G.711 A-law and mu-law encoded data. +* Fix MKV subtitles to disappear when intended instead of lasting until the + next cue ([#6833](https://github.com/google/ExoPlayer/issues/6833)). ### 2.11.1 (2019-12-20) ### 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 ed2acc5898..ee57bbec90 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 @@ -1250,10 +1250,10 @@ public class MatroskaExtractor implements Extractor { if (CODEC_ID_SUBRIP.equals(track.codecId) || CODEC_ID_ASS.equals(track.codecId)) { if (blockSampleCount > 1) { Log.w(TAG, "Skipping subtitle sample in laced block."); - } else if (durationUs == C.TIME_UNSET) { + } else if (blockDurationUs == C.TIME_UNSET) { Log.w(TAG, "Skipping subtitle sample with no duration."); } else { - setSubtitleEndTime(track.codecId, durationUs, subtitleSample.data); + setSubtitleEndTime(track.codecId, blockDurationUs, subtitleSample.data); // 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()); diff --git a/library/core/src/test/assets/mkv/full_blocks.mkv.0.dump b/library/core/src/test/assets/mkv/full_blocks.mkv.0.dump index ac111d0c62..70b22f16aa 100644 --- a/library/core/src/test/assets/mkv/full_blocks.mkv.0.dump +++ b/library/core/src/test/assets/mkv/full_blocks.mkv.0.dump @@ -31,13 +31,13 @@ track 1: sample 0: time = 0 flags = 1 - data = length 59, hash A0217393 + data = length 59, hash 1AD38625 sample 1: time = 2345000 flags = 1 - data = length 95, hash 4904F2 + data = length 95, hash F331C282 sample 2: time = 4567000 flags = 1 - data = length 59, hash EFAB6D8A + data = length 59, hash F8CD7C60 tracksEnded = true diff --git a/library/core/src/test/assets/mkv/full_blocks.mkv.1.dump b/library/core/src/test/assets/mkv/full_blocks.mkv.1.dump index ac111d0c62..70b22f16aa 100644 --- a/library/core/src/test/assets/mkv/full_blocks.mkv.1.dump +++ b/library/core/src/test/assets/mkv/full_blocks.mkv.1.dump @@ -31,13 +31,13 @@ track 1: sample 0: time = 0 flags = 1 - data = length 59, hash A0217393 + data = length 59, hash 1AD38625 sample 1: time = 2345000 flags = 1 - data = length 95, hash 4904F2 + data = length 95, hash F331C282 sample 2: time = 4567000 flags = 1 - data = length 59, hash EFAB6D8A + data = length 59, hash F8CD7C60 tracksEnded = true diff --git a/library/core/src/test/assets/mkv/full_blocks.mkv.2.dump b/library/core/src/test/assets/mkv/full_blocks.mkv.2.dump index ac111d0c62..70b22f16aa 100644 --- a/library/core/src/test/assets/mkv/full_blocks.mkv.2.dump +++ b/library/core/src/test/assets/mkv/full_blocks.mkv.2.dump @@ -31,13 +31,13 @@ track 1: sample 0: time = 0 flags = 1 - data = length 59, hash A0217393 + data = length 59, hash 1AD38625 sample 1: time = 2345000 flags = 1 - data = length 95, hash 4904F2 + data = length 95, hash F331C282 sample 2: time = 4567000 flags = 1 - data = length 59, hash EFAB6D8A + data = length 59, hash F8CD7C60 tracksEnded = true diff --git a/library/core/src/test/assets/mkv/full_blocks.mkv.3.dump b/library/core/src/test/assets/mkv/full_blocks.mkv.3.dump index ac111d0c62..70b22f16aa 100644 --- a/library/core/src/test/assets/mkv/full_blocks.mkv.3.dump +++ b/library/core/src/test/assets/mkv/full_blocks.mkv.3.dump @@ -31,13 +31,13 @@ track 1: sample 0: time = 0 flags = 1 - data = length 59, hash A0217393 + data = length 59, hash 1AD38625 sample 1: time = 2345000 flags = 1 - data = length 95, hash 4904F2 + data = length 95, hash F331C282 sample 2: time = 4567000 flags = 1 - data = length 59, hash EFAB6D8A + data = length 59, hash F8CD7C60 tracksEnded = true From 1e7db22ee256b473c6cb3c8dc16091530672299e Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 6 Jan 2020 11:56:24 +0000 Subject: [PATCH 0596/1335] Convert StyleSpan assertions in SpannedSubject to fluent style PiperOrigin-RevId: 288274998 --- .../text/webvtt/WebvttCueParserTest.java | 36 +--- .../text/webvtt/WebvttDecoderTest.java | 6 +- .../testutil/truth/SpannedSubject.java | 188 ++++++------------ .../testutil/truth/SpannedSubjectTest.java | 45 +++-- 4 files changed, 107 insertions(+), 168 deletions(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java index c9e8488c60..ec4ed10f3d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java @@ -36,10 +36,7 @@ public final class WebvttCueParserTest { assertThat(text.toString()).isEqualTo("This is text with html tags"); assertThat(text).hasUnderlineSpanBetween("This ".length(), "This is".length()); assertThat(text) - .hasBoldItalicSpan( - "This is text with ".length(), - "This is text with html".length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + .hasBoldItalicSpanBetween("This is text with ".length(), "This is text with html".length()); } @Test @@ -60,10 +57,8 @@ public final class WebvttCueParserTest { assertThat(text) .hasUnderlineSpanBetween("An ".length(), "An unclosed u tag with italic inside".length()); assertThat(text) - .hasItalicSpan( - "An unclosed u tag with ".length(), - "An unclosed u tag with italic".length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + .hasItalicSpanBetween( + "An unclosed u tag with ".length(), "An unclosed u tag with italic".length()); } @Test @@ -72,10 +67,9 @@ public final class WebvttCueParserTest { assertThat(text.toString()).isEqualTo("An italic tag with unclosed underline inside"); assertThat(text) - .hasItalicSpan( + .hasItalicSpanBetween( "An italic tag with unclosed ".length(), - "An italic tag with unclosed underline".length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + "An italic tag with unclosed underline".length()); assertThat(text) .hasUnderlineSpanBetween( "An italic tag with unclosed ".length(), @@ -88,15 +82,11 @@ public final class WebvttCueParserTest { String expectedText = "Overlapping u and i tags"; assertThat(text.toString()).isEqualTo(expectedText); - assertThat(text).hasBoldSpan(0, expectedText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + assertThat(text).hasBoldSpanBetween(0, expectedText.length()); // Text between the tags is underlined. assertThat(text).hasUnderlineSpanBetween(0, "Overlapping u and".length()); // Only text from to <\\u> is italic (unexpected - but simplifies the parsing). - assertThat(text) - .hasItalicSpan( - "Overlapping u ".length(), - "Overlapping u and".length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + assertThat(text).hasItalicSpanBetween("Overlapping u ".length(), "Overlapping u and".length()); } @Test @@ -105,8 +95,7 @@ public final class WebvttCueParserTest { assertThat(text.toString()).isEqualTo("foobarbazbuzz"); // endIndex should be 9 when valid (i.e. "foobarbaz".length() - assertThat(text) - .hasBoldSpan("foo".length(), "foobar".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + assertThat(text).hasBoldSpanBetween("foo".length(), "foobar".length()); } @Test @@ -156,13 +145,8 @@ public final class WebvttCueParserTest { Spanned text = parseCueText("blah blah blah foo"); assertThat(text.toString()).isEqualTo("blah blah blah foo"); - assertThat(text) - .hasBoldSpan("blah ".length(), "blah blah".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - assertThat(text) - .hasBoldSpan( - "blah blah blah ".length(), - "blah blah blah foo".length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + assertThat(text).hasBoldSpanBetween("blah ".length(), "blah blah".length()); + assertThat(text).hasBoldSpanBetween("blah blah blah ".length(), "blah blah blah foo".length()); } @Test diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index a3ab3e8b1a..063d4e1bfd 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -417,11 +417,7 @@ public class WebvttDecoderTest { assertThat(s4) .hasBackgroundColorSpanBetween(0, 16) .withColor(ColorParser.parseCssColor("lime")); - assertThat(s4) - .hasBoldSpan( - /* startIndex= */ 17, - /* endIndex= */ s4.length(), - /* flags= */ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + assertThat(s4).hasBoldSpanBetween(/* startIndex= */ 17, /* endIndex= */ s4.length()); } @Test diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java index 84d40fb6f1..16144a170b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java @@ -33,6 +33,7 @@ import androidx.annotation.Nullable; import com.google.common.truth.FailureMetadata; import com.google.common.truth.Subject; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** A Truth {@link Subject} for assertions on {@link Spanned} instances containing text styling. */ @@ -68,59 +69,59 @@ public final class SpannedSubject extends Subject { failWithoutActual( simpleFact("Expected no spans"), fact("in text", actual), - fact("but found", getAllSpansAsStringWithoutFlags(actual))); + fact("but found", getAllSpansAsString(actual))); } } /** - * Checks that the subject has an italic span from {@code startIndex} to {@code endIndex}. + * Checks that the subject has an italic span from {@code start} to {@code end}. * - * @param startIndex The start of the expected span. - * @param endIndex The end of the expected span. - * @param flags The flags of the expected span. See constants on {@link Spanned} for more - * information. + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. */ - // TODO: swap this to fluent-style. - public void hasItalicSpan(int startIndex, int endIndex, int flags) { - hasStyleSpan(startIndex, endIndex, flags, Typeface.ITALIC); + public WithSpanFlags hasItalicSpanBetween(int start, int end) { + return hasStyleSpan(start, end, Typeface.ITALIC); } /** - * Checks that the subject has a bold span from {@code startIndex} to {@code endIndex}. + * Checks that the subject has a bold span from {@code start} to {@code end}. * - * @param startIndex The start of the expected span. - * @param endIndex The end of the expected span. - * @param flags The flags of the expected span. See constants on {@link Spanned} for more - * information. + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. */ - // TODO: swap this to fluent-style. - public void hasBoldSpan(int startIndex, int endIndex, int flags) { - hasStyleSpan(startIndex, endIndex, flags, Typeface.BOLD); + public WithSpanFlags hasBoldSpanBetween(int start, int end) { + return hasStyleSpan(start, end, Typeface.BOLD); } - private void hasStyleSpan(int startIndex, int endIndex, int flags, int style) { + private WithSpanFlags hasStyleSpan(int start, int end, int style) { if (actual == null) { failWithoutActual(simpleFact("Spanned must not be null")); - return; + return ALREADY_FAILED_WITH_FLAGS; } - for (StyleSpan span : findMatchingSpans(startIndex, endIndex, flags, StyleSpan.class)) { + List allFlags = new ArrayList<>(); + boolean matchingSpanFound = false; + for (StyleSpan span : findMatchingSpans(start, end, StyleSpan.class)) { + allFlags.add(actual.getSpanFlags(span)); if (span.getStyle() == style) { - return; + matchingSpanFound = true; + break; } } + if (matchingSpanFound) { + return check("StyleSpan (start=%s,end=%s,style=%s)", start, end, style) + .about(spanFlags()) + .that(allFlags); + } - failWithExpectedSpanWithFlags( - startIndex, - endIndex, - flags, - new StyleSpan(style), - actual.toString().substring(startIndex, endIndex)); + failWithExpectedSpan(start, end, StyleSpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_WITH_FLAGS; } /** - * Checks that the subject has bold and italic styling from {@code startIndex} to {@code - * endIndex}. + * Checks that the subject has bold and italic styling from {@code start} to {@code end}. * *

    This can either be: * @@ -130,47 +131,41 @@ public final class SpannedSubject extends Subject { * with {@code span.getStyle() == Typeface.ITALIC}. * * - * @param startIndex The start of the expected span. - * @param endIndex The end of the expected span. - * @param flags The flags of the expected span. See constants on {@link Spanned} for more - * information. + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. */ - // TODO: swap this to fluent-style. - public void hasBoldItalicSpan(int startIndex, int endIndex, int flags) { + public WithSpanFlags hasBoldItalicSpanBetween(int start, int end) { if (actual == null) { failWithoutActual(simpleFact("Spanned must not be null")); - return; + return ALREADY_FAILED_WITH_FLAGS; } + List allFlags = new ArrayList<>(); List styles = new ArrayList<>(); - for (StyleSpan span : findMatchingSpans(startIndex, endIndex, flags, StyleSpan.class)) { + for (StyleSpan span : findMatchingSpans(start, end, StyleSpan.class)) { + allFlags.add(actual.getSpanFlags(span)); styles.add(span.getStyle()); } - if (styles.size() == 1 && styles.contains(Typeface.BOLD_ITALIC)) { - return; - } else if (styles.size() == 2 - && styles.contains(Typeface.BOLD) - && styles.contains(Typeface.ITALIC)) { - return; + if (styles.isEmpty()) { + failWithExpectedSpan(start, end, StyleSpan.class, actual.subSequence(start, end).toString()); + return ALREADY_FAILED_WITH_FLAGS; } - String spannedSubstring = actual.toString().substring(startIndex, endIndex); - String boldSpan = - getSpanAsStringWithFlags( - startIndex, endIndex, flags, new StyleSpan(Typeface.BOLD), spannedSubstring); - String italicSpan = - getSpanAsStringWithFlags( - startIndex, endIndex, flags, new StyleSpan(Typeface.ITALIC), spannedSubstring); - String boldItalicSpan = - getSpanAsStringWithFlags( - startIndex, endIndex, flags, new StyleSpan(Typeface.BOLD_ITALIC), spannedSubstring); - + if (styles.size() == 1 && styles.contains(Typeface.BOLD_ITALIC) + || styles.size() == 2 + && styles.contains(Typeface.BOLD) + && styles.contains(Typeface.ITALIC)) { + return check("StyleSpan (start=%s,end=%s)", start, end).about(spanFlags()).that(allFlags); + } failWithoutActual( - simpleFact("No matching span found"), + simpleFact( + String.format("No matching StyleSpans found between start=%s,end=%s", start, end)), fact("in text", actual.toString()), - fact("expected either", boldItalicSpan), - fact("or both", boldSpan + "\n" + italicSpan), - fact("but found", getAllSpansAsStringWithFlags(actual))); + fact("expected either styles", Arrays.asList(Typeface.BOLD_ITALIC)), + fact("or styles", Arrays.asList(Typeface.BOLD, Typeface.BOLD_ITALIC)), + fact("but found styles", styles)); + return ALREADY_FAILED_WITH_FLAGS; } /** @@ -194,8 +189,7 @@ public final class SpannedSubject extends Subject { if (underlineSpans.size() == 1) { return check("UnderlineSpan (start=%s,end=%s)", start, end).about(spanFlags()).that(allFlags); } - failWithExpectedSpanWithoutFlags( - start, end, UnderlineSpan.class, actual.toString().substring(start, end)); + failWithExpectedSpan(start, end, UnderlineSpan.class, actual.toString().substring(start, end)); return ALREADY_FAILED_WITH_FLAGS; } @@ -218,7 +212,7 @@ public final class SpannedSubject extends Subject { List foregroundColorSpans = findMatchingSpans(start, end, ForegroundColorSpan.class); if (foregroundColorSpans.isEmpty()) { - failWithExpectedSpanWithoutFlags( + failWithExpectedSpan( start, end, ForegroundColorSpan.class, actual.toString().substring(start, end)); return ALREADY_FAILED_COLORED; } @@ -246,7 +240,7 @@ public final class SpannedSubject extends Subject { List backgroundColorSpans = findMatchingSpans(start, end, BackgroundColorSpan.class); if (backgroundColorSpans.isEmpty()) { - failWithExpectedSpanWithoutFlags( + failWithExpectedSpan( start, end, BackgroundColorSpan.class, actual.toString().substring(start, end)); return ALREADY_FAILED_COLORED; } @@ -255,19 +249,6 @@ public final class SpannedSubject extends Subject { .that(backgroundColorSpans); } - private List findMatchingSpans( - int startIndex, int endIndex, int flags, Class spanClazz) { - List spans = new ArrayList<>(); - for (T span : actual.getSpans(startIndex, endIndex, spanClazz)) { - if (actual.getSpanStart(span) == startIndex - && actual.getSpanEnd(span) == endIndex - && actual.getSpanFlags(span) == flags) { - spans.add(span); - } - } - return spans; - } - private List findMatchingSpans(int startIndex, int endIndex, Class spanClazz) { List spans = new ArrayList<>(); for (T span : actual.getSpans(startIndex, endIndex, spanClazz)) { @@ -278,72 +259,31 @@ public final class SpannedSubject extends Subject { return spans; } - private void failWithExpectedSpanWithFlags( - int start, int end, int flags, Object span, String spannedSubstring) { - failWithoutActual( - simpleFact("No matching span found"), - fact("in text", actual), - fact("expected", getSpanAsStringWithFlags(start, end, flags, span, spannedSubstring)), - fact("but found", getAllSpansAsStringWithFlags(actual))); - } - - private void failWithExpectedSpanWithoutFlags( + private void failWithExpectedSpan( int start, int end, Class spanType, String spannedSubstring) { failWithoutActual( simpleFact("No matching span found"), fact("in text", actual), - fact("expected", getSpanAsStringWithoutFlags(start, end, spanType, spannedSubstring)), - fact("but found", getAllSpansAsStringWithoutFlags(actual))); + fact("expected", getSpanAsString(start, end, spanType, spannedSubstring)), + fact("but found", getAllSpansAsString(actual))); } - private static String getAllSpansAsStringWithFlags(Spanned spanned) { + private static String getAllSpansAsString(Spanned spanned) { List actualSpanStrings = new ArrayList<>(); for (Object span : spanned.getSpans(0, spanned.length(), Object.class)) { - actualSpanStrings.add(getSpanAsStringWithFlags(span, spanned)); + actualSpanStrings.add(getSpanAsString(span, spanned)); } return TextUtils.join("\n", actualSpanStrings); } - private static String getSpanAsStringWithFlags(Object span, Spanned spanned) { + private static String getSpanAsString(Object span, Spanned spanned) { int spanStart = spanned.getSpanStart(span); int spanEnd = spanned.getSpanEnd(span); - return getSpanAsStringWithFlags( - spanStart, - spanEnd, - spanned.getSpanFlags(span), - span, - spanned.toString().substring(spanStart, spanEnd)); - } - - private static String getSpanAsStringWithFlags( - int start, int end, int flags, Object span, String spannedSubstring) { - String suffix; - if (span instanceof StyleSpan) { - suffix = "\tstyle=" + ((StyleSpan) span).getStyle(); - } else { - suffix = ""; - } - return String.format( - "start=%s\tend=%s\tflags=%s\ttype=%s\tsubstring='%s'%s", - start, end, flags, span.getClass().getSimpleName(), spannedSubstring, suffix); - } - - private static String getAllSpansAsStringWithoutFlags(Spanned spanned) { - List actualSpanStrings = new ArrayList<>(); - for (Object span : spanned.getSpans(0, spanned.length(), Object.class)) { - actualSpanStrings.add(getSpanAsStringWithoutFlags(span, spanned)); - } - return TextUtils.join("\n", actualSpanStrings); - } - - private static String getSpanAsStringWithoutFlags(Object span, Spanned spanned) { - int spanStart = spanned.getSpanStart(span); - int spanEnd = spanned.getSpanEnd(span); - return getSpanAsStringWithoutFlags( + return getSpanAsString( spanStart, spanEnd, span.getClass(), spanned.toString().substring(spanStart, spanEnd)); } - private static String getSpanAsStringWithoutFlags( + private static String getSpanAsString( int start, int end, Class span, String spannedSubstring) { return String.format( "start=%s\tend=%s\ttype=%s\tsubstring='%s'", diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java index a01f6f66e0..c33a1128a0 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java @@ -63,7 +63,9 @@ public class SpannedSubjectTest { int end = start + "italic".length(); spannable.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - assertThat(spannable).hasItalicSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + assertThat(spannable) + .hasItalicSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } @Test @@ -78,14 +80,21 @@ public class SpannedSubjectTest { whenTesting -> whenTesting .that(spannable) - .hasItalicSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + .hasItalicSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); - assertThat(failure).factKeys().contains("No matching span found"); - assertThat(failure).factValue("in text").isEqualTo(spannable.toString()); - assertThat(failure).factValue("expected").contains("flags=" + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); assertThat(failure) - .factValue("but found") - .contains("flags=" + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + .factValue("value of") + .isEqualTo( + String.format( + "spanned.StyleSpan (start=%s,end=%s,style=%s).contains()", + start, end, Typeface.ITALIC)); + assertThat(failure) + .factValue("expected to contain") + .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + assertThat(failure) + .factValue("but was") + .contains(String.valueOf(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); } @Test @@ -93,7 +102,10 @@ public class SpannedSubjectTest { AssertionError failure = expectFailure( whenTesting -> - whenTesting.that(null).hasItalicSpan(0, 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + whenTesting + .that(null) + .hasItalicSpanBetween(0, 5) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); assertThat(failure).factKeys().containsExactly("Spanned must not be null"); } @@ -105,7 +117,9 @@ public class SpannedSubjectTest { int end = start + "bold".length(); spannable.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - assertThat(spannable).hasBoldSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + assertThat(spannable) + .hasBoldSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } @Test @@ -116,7 +130,9 @@ public class SpannedSubjectTest { spannable.setSpan( new StyleSpan(Typeface.BOLD_ITALIC), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - assertThat(spannable).hasBoldItalicSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + assertThat(spannable) + .hasBoldItalicSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } @Test @@ -127,7 +143,9 @@ public class SpannedSubjectTest { spannable.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); spannable.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - assertThat(spannable).hasBoldItalicSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + assertThat(spannable) + .hasBoldItalicSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } @Test @@ -144,8 +162,9 @@ public class SpannedSubjectTest { whenTesting -> whenTesting .that(spannable) - .hasBoldItalicSpan(incorrectStart, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); - assertThat(expected).factValue("expected either").contains("start=" + incorrectStart); + .hasBoldItalicSpanBetween(incorrectStart, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + assertThat(expected).factValue("expected").contains("start=" + incorrectStart); assertThat(expected).factValue("but found").contains("start=" + start); } From e55af3e3c8480eac8f9afb9658b8da0057feac76 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 6 Jan 2020 12:49:43 +0000 Subject: [PATCH 0597/1335] Add RubySpan This will be used when parsing Ruby info from WebVTT and TTML/IMSC subtitles. PiperOrigin-RevId: 288280181 --- .../exoplayer2/text/span/RubySpan.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/span/RubySpan.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/span/RubySpan.java b/library/core/src/main/java/com/google/android/exoplayer2/text/span/RubySpan.java new file mode 100644 index 0000000000..8ed84d6f6b --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/span/RubySpan.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.google.android.exoplayer2.text.span; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import androidx.annotation.IntDef; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +/** + * A styling span for ruby text. + * + *

    The text covered by this span is known as the "base text", and the ruby text is stored in + * {@link #rubyText}. + * + *

    More information on ruby characters + * and span styling. + */ +// NOTE: There's no Android layout support for rubies, so this span currently doesn't extend any +// styling superclasses (e.g. MetricAffectingSpan). The only way to render these rubies is to +// extract the spans and do the layout manually. +// TODO: Consider adding support for parenthetical text to be used when rendering doesn't support +// rubies (e.g. HTML tag). +public final class RubySpan { + + /** The ruby position is unknown. */ + public static final int POSITION_UNKNOWN = -1; + + /** + * The ruby text should be positioned above the base text. + * + *

    For vertical text it should be positioned to the right, same as CSS's ruby-position. + */ + public static final int POSITION_OVER = 1; + + /** + * The ruby text should be positioned below the base text. + * + *

    For vertical text it should be positioned to the left, same as CSS's ruby-position. + */ + public static final int POSITION_UNDER = 2; + + /** + * The possible positions of the ruby text relative to the base text. + * + *

    One of: + * + *

      + *
    • {@link #POSITION_UNKNOWN} + *
    • {@link #POSITION_OVER} + *
    • {@link #POSITION_UNDER} + *
    + */ + @Documented + @Retention(SOURCE) + @IntDef({POSITION_UNKNOWN, POSITION_OVER, POSITION_UNDER}) + public @interface Position {} + + /** The ruby text, i.e. the smaller explanatory characters. */ + public final String rubyText; + + /** The position of the ruby text relative to the base text. */ + @Position public final int position; + + public RubySpan(String rubyText, @Position int position) { + this.rubyText = rubyText; + this.position = position; + } +} From 2b1a06633903d4fbe1eec5f001cda502e2a712aa Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 6 Jan 2020 12:52:00 +0000 Subject: [PATCH 0598/1335] Add RubySpan support to SpannedSubject PiperOrigin-RevId: 288280332 --- .../testutil/truth/SpannedSubject.java | 117 +++++++++++++++++- .../testutil/truth/SpannedSubjectTest.java | 115 +++++++++++++++++ 2 files changed, 230 insertions(+), 2 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java index 16144a170b..55e2117e04 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java @@ -30,6 +30,7 @@ import android.text.style.UnderlineSpan; import androidx.annotation.CheckResult; import androidx.annotation.ColorInt; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.text.span.RubySpan; import com.google.common.truth.FailureMetadata; import com.google.common.truth.Subject; import java.util.ArrayList; @@ -194,7 +195,7 @@ public final class SpannedSubject extends Subject { } /** - * Checks that the subject as a {@link ForegroundColorSpan} from {@code start} to {@code end}. + * Checks that the subject has a {@link ForegroundColorSpan} from {@code start} to {@code end}. * *

    The color is asserted in a follow-up method call on the return {@link Colored} object. * @@ -222,7 +223,7 @@ public final class SpannedSubject extends Subject { } /** - * Checks that the subject as a {@link ForegroundColorSpan} from {@code start} to {@code end}. + * Checks that the subject has a {@link BackgroundColorSpan} from {@code start} to {@code end}. * *

    The color is asserted in a follow-up method call on the return {@link Colored} object. * @@ -249,6 +250,30 @@ public final class SpannedSubject extends Subject { .that(backgroundColorSpans); } + /** + * Checks that the subject has a {@link RubySpan} from {@code start} to {@code end}. + * + *

    The ruby-text is asserted in a follow-up method call on the return {@link RubyText} object. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link Colored} object to assert on the color of the matching spans. + */ + @CheckResult + public RubyText hasRubySpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_WITH_TEXT; + } + + List rubySpans = findMatchingSpans(start, end, RubySpan.class); + if (rubySpans.isEmpty()) { + failWithExpectedSpan(start, end, RubySpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_WITH_TEXT; + } + return check("RubySpan (start=%s,end=%s)", start, end).about(rubySpans(actual)).that(rubySpans); + } + private List findMatchingSpans(int startIndex, int endIndex, Class spanClazz) { List spans = new ArrayList<>(); for (T span : actual.getSpans(startIndex, endIndex, spanClazz)) { @@ -440,4 +465,92 @@ public final class SpannedSubject extends Subject { return check("flags").about(spanFlags()).that(matchingSpanFlags); } } + + /** Allows assertions about a span's ruby text and its position. */ + public interface RubyText { + + /** + * Checks that at least one of the matched spans has the expected {@code text}. + * + * @param text The expected text. + * @param position The expected position of the text. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. + */ + AndSpanFlags withTextAndPosition(String text, @RubySpan.Position int position); + } + + private static final RubyText ALREADY_FAILED_WITH_TEXT = + (text, position) -> ALREADY_FAILED_AND_FLAGS; + + private Factory> rubySpans(Spanned actualSpanned) { + return (FailureMetadata metadata, List spans) -> + new RubySpansSubject(metadata, spans, actualSpanned); + } + + private static final class RubySpansSubject extends Subject implements RubyText { + + private final List actualSpans; + private final Spanned actualSpanned; + + private RubySpansSubject( + FailureMetadata metadata, List actualSpans, Spanned actualSpanned) { + super(metadata, actualSpans); + this.actualSpans = actualSpans; + this.actualSpanned = actualSpanned; + } + + @Override + public AndSpanFlags withTextAndPosition(String text, @RubySpan.Position int position) { + List matchingSpanFlags = new ArrayList<>(); + List spanTextsAndPositions = new ArrayList<>(); + for (RubySpan span : actualSpans) { + spanTextsAndPositions.add(new TextAndPosition(span.rubyText, span.position)); + if (span.rubyText.equals(text)) { + matchingSpanFlags.add(actualSpanned.getSpanFlags(span)); + } + } + check("rubyTextAndPosition") + .that(spanTextsAndPositions) + .containsExactly(new TextAndPosition(text, position)); + return check("flags").about(spanFlags()).that(matchingSpanFlags); + } + + private static class TextAndPosition { + private final String text; + @RubySpan.Position private final int position; + + private TextAndPosition(String text, int position) { + this.text = text; + this.position = position; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TextAndPosition that = (TextAndPosition) o; + if (position != that.position) { + return false; + } + return text.equals(that.text); + } + + @Override + public int hashCode() { + int result = text.hashCode(); + result = 31 * result + position; + return result; + } + + @Override + public String toString() { + return String.format("{text='%s',position=%s}", text, position); + } + } + } } diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java index c33a1128a0..3eb0509eb4 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java @@ -30,6 +30,7 @@ import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.text.span.RubySpan; import com.google.common.truth.ExpectFailure; import org.junit.Test; import org.junit.runner.RunWith; @@ -338,6 +339,120 @@ public class SpannedSubjectTest { .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); } + @Test + public void rubySpan_success() { + SpannableString spannable = SpannableString.valueOf("test with rubied section"); + int start = "test with ".length(); + int end = start + "rubied".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasRubySpanBetween(start, end) + .withTextAndPosition("ruby text", RubySpan.POSITION_OVER) + .andFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void rubySpan_wrongEndIndex() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + int incorrectEnd = end + 2; + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasRubySpanBetween(start, incorrectEnd) + .withTextAndPosition("ruby text", RubySpan.POSITION_OVER)); + assertThat(expected).factValue("expected").contains("end=" + incorrectEnd); + assertThat(expected).factValue("but found").contains("end=" + end); + } + + @Test + public void rubySpan_wrongText() { + SpannableString spannable = SpannableString.valueOf("test with rubied section"); + int start = "test with ".length(); + int end = start + "rubied".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasRubySpanBetween(start, end) + .withTextAndPosition("incorrect text", RubySpan.POSITION_OVER)); + assertThat(expected).factValue("value of").contains("rubyTextAndPosition"); + assertThat(expected).factValue("expected").contains("text='incorrect text'"); + assertThat(expected).factValue("but was").contains("text='ruby text'"); + } + + @Test + public void rubySpan_wrongPosition() { + SpannableString spannable = SpannableString.valueOf("test with rubied section"); + int start = "test with ".length(); + int end = start + "rubied".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasRubySpanBetween(start, end) + .withTextAndPosition("ruby text", RubySpan.POSITION_UNDER)); + assertThat(expected).factValue("value of").contains("rubyTextAndPosition"); + assertThat(expected).factValue("expected").contains("position=" + RubySpan.POSITION_UNDER); + assertThat(expected).factValue("but was").contains("position=" + RubySpan.POSITION_OVER); + } + + @Test + public void rubySpan_wrongFlags() { + SpannableString spannable = SpannableString.valueOf("test with rubied section"); + int start = "test with ".length(); + int end = start + "rubied".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasRubySpanBetween(start, end) + .withTextAndPosition("ruby text", RubySpan.POSITION_OVER) + .andFlags(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected).factValue("value of").contains("flags"); + assertThat(expected) + .factValue("expected to contain") + .contains(String.valueOf(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected) + .factValue("but was") + .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + } + private static AssertionError expectFailure( ExpectFailure.SimpleSubjectBuilderCallback callback) { return expectFailureAbout(spanned(), callback); From 6f312c054ef2fe46b969584ecc3f089a3a21be06 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 6 Jan 2020 12:52:59 +0000 Subject: [PATCH 0599/1335] Add tag support to WebvttCueParser There's currently no rendering support for ruby text in SubtitleView or SubtitlePainter, but this does have a visible impact with the current implementation by stripping the ruby text from Cue.text meaning it doesn't show up at all under the 'naive' rendering. This is an improvement over the current behaviour of including the ruby text in-line with the base text (no rubies is better than wrongly rendered rubies). PiperOrigin-RevId: 288280416 --- RELEASENOTES.md | 2 + .../text/webvtt/WebvttCueParser.java | 71 +++++++++++++++++-- .../text/webvtt/WebvttCueParserTest.java | 31 ++++++++ 3 files changed, 97 insertions(+), 7 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8df1b1f698..0810d4fd97 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -45,6 +45,8 @@ * Improve support for G.711 A-law and mu-law encoded data. * Fix MKV subtitles to disappear when intended instead of lasting until the next cue ([#6833](https://github.com/google/ExoPlayer/issues/6833)). +* Parse \ and \ tags in WebVTT subtitles (rendering is coming + later). ### 2.11.1 (2019-12-20) ### 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 f4c0f26fc8..6de57783e0 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 @@ -37,6 +37,7 @@ import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.span.RubySpan; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -120,11 +121,13 @@ public final class WebvttCueParser { private static final String ENTITY_NON_BREAK_SPACE = "nbsp"; private static final String TAG_BOLD = "b"; - private static final String TAG_ITALIC = "i"; - private static final String TAG_UNDERLINE = "u"; private static final String TAG_CLASS = "c"; - private static final String TAG_VOICE = "v"; + private static final String TAG_ITALIC = "i"; private static final String TAG_LANG = "lang"; + private static final String TAG_RUBY = "ruby"; + private static final String TAG_RUBY_TEXT = "rt"; + private static final String TAG_UNDERLINE = "u"; + private static final String TAG_VOICE = "v"; private static final int STYLE_BOLD = Typeface.BOLD; private static final int STYLE_ITALIC = Typeface.ITALIC; @@ -197,6 +200,7 @@ public final class WebvttCueParser { ArrayDeque startTagStack = new ArrayDeque<>(); List scratchStyleMatches = new ArrayList<>(); int pos = 0; + List nestedElements = new ArrayList<>(); while (pos < markup.length()) { char curr = markup.charAt(pos); switch (curr) { @@ -225,8 +229,14 @@ public final class WebvttCueParser { break; } startTag = startTagStack.pop(); - applySpansForTag(id, startTag, spannedText, styles, scratchStyleMatches); - } while(!startTag.name.equals(tagName)); + applySpansForTag( + id, startTag, nestedElements, spannedText, styles, scratchStyleMatches); + if (!startTagStack.isEmpty()) { + nestedElements.add(new Element(startTag, spannedText.length())); + } else { + nestedElements.clear(); + } + } while (!startTag.name.equals(tagName)); } else if (!isVoidTag) { startTagStack.push(StartTag.buildStartTag(fullTagExpression, spannedText.length())); } @@ -256,9 +266,15 @@ public final class WebvttCueParser { } // apply unclosed tags while (!startTagStack.isEmpty()) { - applySpansForTag(id, startTagStack.pop(), spannedText, styles, scratchStyleMatches); + applySpansForTag( + id, startTagStack.pop(), nestedElements, spannedText, styles, scratchStyleMatches); } - applySpansForTag(id, StartTag.buildWholeCueVirtualTag(), spannedText, styles, + applySpansForTag( + id, + StartTag.buildWholeCueVirtualTag(), + /* nestedElements= */ Collections.emptyList(), + spannedText, + styles, scratchStyleMatches); return SpannedString.valueOf(spannedText); } @@ -442,6 +458,8 @@ public final class WebvttCueParser { case TAG_CLASS: case TAG_ITALIC: case TAG_LANG: + case TAG_RUBY: + case TAG_RUBY_TEXT: case TAG_UNDERLINE: case TAG_VOICE: return true; @@ -453,6 +471,7 @@ public final class WebvttCueParser { private static void applySpansForTag( @Nullable String cueId, StartTag startTag, + List nestedElements, SpannableStringBuilder text, List styles, List scratchStyleMatches) { @@ -467,6 +486,29 @@ public final class WebvttCueParser { text.setSpan(new StyleSpan(STYLE_ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; + case TAG_RUBY: + @Nullable Element rubyTextElement = null; + for (int i = 0; i < nestedElements.size(); i++) { + if (TAG_RUBY_TEXT.equals(nestedElements.get(i).startTag.name)) { + rubyTextElement = nestedElements.get(i); + // Behaviour of multiple tags inside is undefined, so use the first one. + break; + } + } + if (rubyTextElement == null) { + break; + } + // Move the rubyText from spannedText into the RubySpan. + CharSequence rubyText = + text.subSequence(rubyTextElement.startTag.position, rubyTextElement.endPosition); + text.delete(rubyTextElement.startTag.position, rubyTextElement.endPosition); + end -= rubyText.length(); + text.setSpan( + new RubySpan(rubyText.toString(), RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + break; case TAG_UNDERLINE: text.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; @@ -787,4 +829,19 @@ public final class WebvttCueParser { } } + + /** Information about a complete element (i.e. start tag and end position). */ + private static class Element { + private final StartTag startTag; + /** + * The position of the end of this element's text in the un-marked-up cue text (i.e. the + * corollary to {@link StartTag#position}). + */ + private final int endPosition; + + private Element(StartTag startTag, int endPosition) { + this.startTag = startTag; + this.endPosition = endPosition; + } + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java index ec4ed10f3d..aa83fbc8ed 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import android.text.Spanned; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.text.span.RubySpan; import java.util.Collections; import org.junit.Test; import org.junit.runner.RunWith; @@ -48,6 +49,36 @@ public final class WebvttCueParserTest { assertThat(text).hasNoSpans(); } + @Test + public void testParseRubyTag() throws Exception { + Spanned text = + parseCueText("Some base textwith ruby and undecorated text"); + + // The text between the tags is stripped from Cue.text and only present on the RubySpan. + assertThat(text.toString()).isEqualTo("Some base text and undecorated text"); + assertThat(text) + .hasRubySpanBetween("Some ".length(), "Some base text".length()) + .withTextAndPosition("with ruby", RubySpan.POSITION_OVER); + } + + @Test + public void testParseRubyTagWithNoTextTag() throws Exception { + Spanned text = parseCueText("Some base text with no ruby text"); + + assertThat(text.toString()).isEqualTo("Some base text with no ruby text"); + assertThat(text).hasNoSpans(); + } + + @Test + public void testParseRubyTagWithEmptyTextTag() throws Exception { + Spanned text = parseCueText("Some base text with empty ruby text"); + + assertThat(text.toString()).isEqualTo("Some base text with empty ruby text"); + assertThat(text) + .hasRubySpanBetween("Some ".length(), "Some base text with".length()) + .withTextAndPosition("", RubySpan.POSITION_OVER); + } + @Test public void testParseWellFormedUnclosedEndAtCueEnd() throws Exception { Spanned text = parseCueText("An unclosed u tag with " From 3a31bc17245974e667bcdb5eaa886a1d9a9999fb Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 6 Jan 2020 12:54:05 +0000 Subject: [PATCH 0600/1335] Support 5G in network type detection PiperOrigin-RevId: 288280500 --- .../main/java/com/google/android/exoplayer2/C.java | 12 ++++++------ .../exoplayer2/upstream/DefaultBandwidthMeter.java | 3 ++- .../com/google/android/exoplayer2/util/Util.java | 2 ++ 3 files changed, 10 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 46f20a20f4..e926e90d22 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 @@ -977,8 +977,8 @@ public final class C { /** * Network connection type. One of {@link #NETWORK_TYPE_UNKNOWN}, {@link #NETWORK_TYPE_OFFLINE}, * {@link #NETWORK_TYPE_WIFI}, {@link #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, {@link - * #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link #NETWORK_TYPE_ETHERNET} or - * {@link #NETWORK_TYPE_OTHER}. + * #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_5G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link + * #NETWORK_TYPE_ETHERNET} or {@link #NETWORK_TYPE_OTHER}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -989,6 +989,7 @@ public final class C { NETWORK_TYPE_2G, NETWORK_TYPE_3G, NETWORK_TYPE_4G, + NETWORK_TYPE_5G, NETWORK_TYPE_CELLULAR_UNKNOWN, NETWORK_TYPE_ETHERNET, NETWORK_TYPE_OTHER @@ -1006,6 +1007,8 @@ public final class C { public static final int NETWORK_TYPE_3G = 4; /** Network type for a 4G cellular connection. */ public static final int NETWORK_TYPE_4G = 5; + /** Network type for a 5G cellular connection. */ + public static final int NETWORK_TYPE_5G = 9; /** * Network type for cellular connections which cannot be mapped to one of {@link * #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, or {@link #NETWORK_TYPE_4G}. @@ -1013,10 +1016,7 @@ public final class C { public static final int NETWORK_TYPE_CELLULAR_UNKNOWN = 6; /** Network type for an Ethernet connection. */ public static final int NETWORK_TYPE_ETHERNET = 7; - /** - * Network type for other connections which are not Wifi or cellular (e.g. Ethernet, VPN, - * Bluetooth). - */ + /** Network type for other connections which are not Wifi or cellular (e.g. VPN, Bluetooth). */ public static final int NETWORK_TYPE_OTHER = 8; /** 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 1b69455695..2491cc93a9 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 @@ -203,9 +203,10 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList result.append(C.NETWORK_TYPE_2G, DEFAULT_INITIAL_BITRATE_ESTIMATES_2G[groupIndices[1]]); result.append(C.NETWORK_TYPE_3G, DEFAULT_INITIAL_BITRATE_ESTIMATES_3G[groupIndices[2]]); result.append(C.NETWORK_TYPE_4G, DEFAULT_INITIAL_BITRATE_ESTIMATES_4G[groupIndices[3]]); - // Assume default Wifi bitrate for Ethernet to prevent using the slower fallback bitrate. + // Assume default Wifi bitrate for Ethernet and 5G to prevent using the slower fallback. result.append( C.NETWORK_TYPE_ETHERNET, DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI[groupIndices[0]]); + result.append(C.NETWORK_TYPE_5G, DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI[groupIndices[0]]); return result; } 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 3ca86ef13d..65ffcf351e 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 @@ -2126,6 +2126,8 @@ public final class Util { return C.NETWORK_TYPE_3G; case TelephonyManager.NETWORK_TYPE_LTE: return C.NETWORK_TYPE_4G; + case TelephonyManager.NETWORK_TYPE_NR: + return C.NETWORK_TYPE_5G; case TelephonyManager.NETWORK_TYPE_IWLAN: return C.NETWORK_TYPE_WIFI; case TelephonyManager.NETWORK_TYPE_GSM: From 06fcf29edd527db2af535207497a25d6e12b1c66 Mon Sep 17 00:00:00 2001 From: kimvde Date: Mon, 6 Jan 2020 13:41:53 +0000 Subject: [PATCH 0601/1335] Simulate IO exceptions in all FlacExtractor tests - Simulate IO exceptions in the test using FlacBinarySearchSeeker for seeking in FlacExtractorTests. This makes the test slower but covers more test cases. PiperOrigin-RevId: 288285057 --- .../extractor/flac/FlacExtractorTest.java | 66 +------------------ 1 file changed, 1 insertion(+), 65 deletions(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java index 97bfc949de..061d0902b6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java @@ -15,13 +15,8 @@ */ package com.google.android.exoplayer2.extractor.flac; -import android.content.Context; -import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; -import com.google.android.exoplayer2.testutil.TestUtil; -import java.io.IOException; import org.junit.Test; import org.junit.runner.RunWith; @@ -66,9 +61,7 @@ public class FlacExtractorTest { @Test public void testOneMetadataBlock() throws Exception { - // Don't simulate IO errors as it is too slow when using the binary search seek map (see - // [Internal: b/145994869]). - assertBehaviorWithoutSimulatingIOErrors("flac/bear_one_metadata_block.flac"); + ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_one_metadata_block.flac"); } @Test @@ -85,61 +78,4 @@ public class FlacExtractorTest { public void testUncommonSampleRate() throws Exception { ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_uncommon_sample_rate.flac"); } - - private static void assertBehaviorWithoutSimulatingIOErrors(String file) - throws IOException, InterruptedException { - // Check behavior prior to initialization. - Extractor extractor = new FlacExtractor(); - extractor.seek(0, 0); - extractor.release(); - - // Assert output. - Context context = ApplicationProvider.getApplicationContext(); - byte[] data = TestUtil.getByteArray(context, file); - ExtractorAsserts.assertOutput( - new FlacExtractor(), - file, - data, - context, - /* sniffFirst= */ true, - /* simulateIOErrors= */ false, - /* simulateUnknownLength= */ false, - /* simulatePartialReads= */ false); - ExtractorAsserts.assertOutput( - new FlacExtractor(), - file, - data, - context, - /* sniffFirst= */ true, - /* simulateIOErrors= */ false, - /* simulateUnknownLength= */ false, - /* simulatePartialReads= */ true); - ExtractorAsserts.assertOutput( - new FlacExtractor(), - file, - data, - context, - /* sniffFirst= */ true, - /* simulateIOErrors= */ false, - /* simulateUnknownLength= */ true, - /* simulatePartialReads= */ false); - ExtractorAsserts.assertOutput( - new FlacExtractor(), - file, - data, - context, - /* sniffFirst= */ true, - /* simulateIOErrors= */ false, - /* simulateUnknownLength= */ true, - /* simulatePartialReads= */ true); - ExtractorAsserts.assertOutput( - new FlacExtractor(), - file, - data, - context, - /* sniffFirst= */ false, - /* simulateIOErrors= */ false, - /* simulateUnknownLength= */ false, - /* simulatePartialReads= */ false); - } } From a98fc7ca487269d6c803907ee3d9ced9a6f86620 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 6 Jan 2020 13:51:42 +0000 Subject: [PATCH 0602/1335] Add tate-chu-yoko support to WebVTT decoding PiperOrigin-RevId: 288285953 --- RELEASENOTES.md | 2 + .../HorizontalTextInVerticalContextSpan.java | 32 +++++++++++++++ .../exoplayer2/text/webvtt/CssParser.java | 11 +++++- .../text/webvtt/WebvttCssStyle.java | 10 +++++ .../text/webvtt/WebvttCueParser.java | 5 +++ .../webvtt/with_css_text_combine_upright | 18 +++++++++ .../text/webvtt/WebvttDecoderTest.java | 16 ++++++++ .../testutil/truth/SpannedSubject.java | 39 ++++++++++++++++--- .../testutil/truth/SpannedSubjectTest.java | 14 +++++++ 9 files changed, 140 insertions(+), 7 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/span/HorizontalTextInVerticalContextSpan.java create mode 100644 library/core/src/test/assets/webvtt/with_css_text_combine_upright diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0810d4fd97..5f411b7100 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -47,6 +47,8 @@ next cue ([#6833](https://github.com/google/ExoPlayer/issues/6833)). * Parse \ and \ tags in WebVTT subtitles (rendering is coming later). +* Parse `text-combine-upright` CSS property (i.e. tate-chu-yoko) in WebVTT + subtitles (rendering is coming later). ### 2.11.1 (2019-12-20) ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/span/HorizontalTextInVerticalContextSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/text/span/HorizontalTextInVerticalContextSpan.java new file mode 100644 index 0000000000..587e1647c6 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/span/HorizontalTextInVerticalContextSpan.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.google.android.exoplayer2.text.span; + +/** + * A styling span for horizontal text in a vertical context. + * + *

    This is used in vertical text to write some characters in a horizontal orientation, known in + * Japanese as tate-chu-yoko. + * + *

    More information on tate-chu-yoko and span styling. + */ +// NOTE: There's no Android layout support for this, so this span currently doesn't extend any +// styling superclasses (e.g. MetricAffectingSpan). The only way to render this styling is to +// extract the spans and do the layout manually. +public final class HorizontalTextInVerticalContextSpan {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java index 9a5ac40a05..7d5d51b706 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java @@ -31,14 +31,19 @@ import java.util.regex.Pattern; */ /* package */ final class CssParser { + private static final String TAG = "CssParser"; + + private static final String RULE_START = "{"; + private static final String RULE_END = "}"; private static final String PROPERTY_BGCOLOR = "background-color"; private static final String PROPERTY_FONT_FAMILY = "font-family"; private static final String PROPERTY_FONT_WEIGHT = "font-weight"; + private static final String PROPERTY_TEXT_COMBINE_UPRIGHT = "text-combine-upright"; + private static final String VALUE_ALL = "all"; + private static final String VALUE_DIGITS = "digits"; private static final String PROPERTY_TEXT_DECORATION = "text-decoration"; private static final String VALUE_BOLD = "bold"; private static final String VALUE_UNDERLINE = "underline"; - private static final String RULE_START = "{"; - private static final String RULE_END = "}"; private static final String PROPERTY_FONT_STYLE = "font-style"; private static final String VALUE_ITALIC = "italic"; @@ -182,6 +187,8 @@ import java.util.regex.Pattern; style.setFontColor(ColorParser.parseCssColor(value)); } else if (PROPERTY_BGCOLOR.equals(property)) { style.setBackgroundColor(ColorParser.parseCssColor(value)); + } else if (PROPERTY_TEXT_COMBINE_UPRIGHT.equals(property)) { + style.setCombineUpright(VALUE_ALL.equals(value) || value.startsWith(VALUE_DIGITS)); } else if (PROPERTY_TEXT_DECORATION.equals(property)) { if (VALUE_UNDERLINE.equals(value)) { style.setUnderline(true); 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 1369859552..cd08ad18cf 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 @@ -95,6 +95,7 @@ public final class WebvttCssStyle { @FontSizeUnit private int fontSizeUnit; private float fontSize; @Nullable private Layout.Alignment textAlign; + private boolean combineUpright; // Calling reset() is forbidden because `this` isn't initialized. This can be safely suppressed // because reset() only assigns fields, it doesn't read any. @@ -118,6 +119,7 @@ public final class WebvttCssStyle { italic = UNSPECIFIED; fontSizeUnit = UNSPECIFIED; textAlign = null; + combineUpright = false; } public void setTargetId(String targetId) { @@ -287,6 +289,14 @@ public final class WebvttCssStyle { return fontSize; } + public void setCombineUpright(boolean enabled) { + this.combineUpright = enabled; + } + + public boolean getCombineUpright() { + return combineUpright; + } + private static int updateScoreForMatch( int currentScore, String target, @Nullable String actual, int score) { if (target.isEmpty() || currentScore == -1) { 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 6de57783e0..fe36043800 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 @@ -37,6 +37,7 @@ import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; import com.google.android.exoplayer2.text.span.RubySpan; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; @@ -571,6 +572,10 @@ public final class WebvttCueParser { // Do nothing. break; } + if (style.getCombineUpright()) { + spannedText.setSpan( + new HorizontalTextInVerticalContextSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } } /** diff --git a/library/core/src/test/assets/webvtt/with_css_text_combine_upright b/library/core/src/test/assets/webvtt/with_css_text_combine_upright new file mode 100644 index 0000000000..fd198a9c71 --- /dev/null +++ b/library/core/src/test/assets/webvtt/with_css_text_combine_upright @@ -0,0 +1,18 @@ +WEBVTT + +NOTE https://developer.mozilla.org/en-US/docs/Web/CSS/text-combine-upright +NOTE The `digits` values are ignored in CssParser and all assumed to be `all` + +STYLE +::cue(.tcu-all) { + text-combine-upright: all; +} +::cue(.tcu-digits) { + text-combine-upright: digits 4; +} + +00:00:00.000 --> 00:00:01.000 vertical:rl +Combine all test + +00:03.000 --> 00:04.000 vertical:rl +Combine 0004 digits diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index 063d4e1bfd..b33439f4f3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -53,6 +53,8 @@ public class WebvttDecoderTest { private static final String WITH_TAGS_FILE = "webvtt/with_tags"; private static final String WITH_CSS_STYLES = "webvtt/with_css_styles"; private static final String WITH_CSS_COMPLEX_SELECTORS = "webvtt/with_css_complex_selectors"; + private static final String WITH_CSS_TEXT_COMBINE_UPRIGHT = + "webvtt/with_css_text_combine_upright"; private static final String WITH_BOM = "webvtt/with_bom"; private static final String EMPTY_FILE = "webvtt/empty"; @@ -460,6 +462,20 @@ public class WebvttDecoderTest { .isEqualTo(Typeface.ITALIC); } + @Test + public void testWebvttWithCssTextCombineUpright() throws Exception { + WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_TEXT_COMBINE_UPRIGHT); + + Spanned firstCueText = getUniqueSpanTextAt(subtitle, 500_000); + assertThat(firstCueText) + .hasHorizontalTextInVerticalContextSpanBetween("Combine ".length(), "Combine all".length()); + + Spanned secondCueText = getUniqueSpanTextAt(subtitle, 3_500_000); + assertThat(secondCueText) + .hasHorizontalTextInVerticalContextSpanBetween( + "Combine ".length(), "Combine 0004".length()); + } + private WebvttSubtitle getSubtitleForTestAsset(String asset) throws IOException, SubtitleDecoderException { WebvttDecoder decoder = new WebvttDecoder(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java index 55e2117e04..b6efa1e7b7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java @@ -30,11 +30,13 @@ import android.text.style.UnderlineSpan; import androidx.annotation.CheckResult; import androidx.annotation.ColorInt; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; import com.google.android.exoplayer2.text.span.RubySpan; import com.google.common.truth.FailureMetadata; import com.google.common.truth.Subject; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; /** A Truth {@link Subject} for assertions on {@link Spanned} instances containing text styling. */ @@ -183,12 +185,10 @@ public final class SpannedSubject extends Subject { } List underlineSpans = findMatchingSpans(start, end, UnderlineSpan.class); - List allFlags = new ArrayList<>(); - for (UnderlineSpan span : underlineSpans) { - allFlags.add(actual.getSpanFlags(span)); - } if (underlineSpans.size() == 1) { - return check("UnderlineSpan (start=%s,end=%s)", start, end).about(spanFlags()).that(allFlags); + return check("UnderlineSpan (start=%s,end=%s)", start, end) + .about(spanFlags()) + .that(Collections.singletonList(actual.getSpanFlags(underlineSpans.get(0)))); } failWithExpectedSpan(start, end, UnderlineSpan.class, actual.toString().substring(start, end)); return ALREADY_FAILED_WITH_FLAGS; @@ -274,6 +274,35 @@ public final class SpannedSubject extends Subject { return check("RubySpan (start=%s,end=%s)", start, end).about(rubySpans(actual)).that(rubySpans); } + /** + * Checks that the subject has an {@link HorizontalTextInVerticalContextSpan} from {@code start} + * to {@code end}. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. + */ + public WithSpanFlags hasHorizontalTextInVerticalContextSpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_WITH_FLAGS; + } + + List horizontalInVerticalSpans = + findMatchingSpans(start, end, HorizontalTextInVerticalContextSpan.class); + if (horizontalInVerticalSpans.size() == 1) { + return check("HorizontalTextInVerticalContextSpan (start=%s,end=%s)", start, end) + .about(spanFlags()) + .that(Collections.singletonList(actual.getSpanFlags(horizontalInVerticalSpans.get(0)))); + } + failWithExpectedSpan( + start, + end, + HorizontalTextInVerticalContextSpan.class, + actual.toString().substring(start, end)); + return ALREADY_FAILED_WITH_FLAGS; + } + private List findMatchingSpans(int startIndex, int endIndex, Class spanClazz) { List spans = new ArrayList<>(); for (T span : actual.getSpans(startIndex, endIndex, spanClazz)) { diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java index 3eb0509eb4..c3badd9bb9 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java @@ -30,6 +30,7 @@ import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; import com.google.android.exoplayer2.text.span.RubySpan; import com.google.common.truth.ExpectFailure; import org.junit.Test; @@ -453,6 +454,19 @@ public class SpannedSubjectTest { .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); } + @Test + public void horizontalTextInVerticalContextSpan_success() { + SpannableString spannable = SpannableString.valueOf("vertical text with horizontal section"); + int start = "vertical text with ".length(); + int end = start + "horizontal".length(); + spannable.setSpan( + new HorizontalTextInVerticalContextSpan(), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasHorizontalTextInVerticalContextSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + private static AssertionError expectFailure( ExpectFailure.SimpleSubjectBuilderCallback callback) { return expectFailureAbout(spanned(), callback); From 24743c77ce673492471557bf213bdee267f09b27 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 6 Jan 2020 14:51:34 +0000 Subject: [PATCH 0603/1335] Remove WavExtractor from the nullness blacklist PiperOrigin-RevId: 288292488 --- .../extractor/wav/WavExtractor.java | 113 ++++++++++-------- 1 file changed, 63 insertions(+), 50 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 d9989aeaf6..45a8c24e67 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 @@ -31,6 +31,8 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Extracts data from WAV byte streams. @@ -47,9 +49,9 @@ public final class WavExtractor implements Extractor { /** Factory for {@link WavExtractor} instances. */ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new WavExtractor()}; - private ExtractorOutput extractorOutput; - private TrackOutput trackOutput; - private OutputWriter outputWriter; + @MonotonicNonNull private ExtractorOutput extractorOutput; + @MonotonicNonNull private TrackOutput trackOutput; + @MonotonicNonNull private OutputWriter outputWriter; private int dataStartPosition; private long dataEndPosition; @@ -85,6 +87,7 @@ public final class WavExtractor implements Extractor { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + assertInitialized(); if (outputWriter == null) { WavHeader header = WavHeaderReader.peek(input); if (header == null) { @@ -136,6 +139,12 @@ public final class WavExtractor implements Extractor { return outputWriter.sampleData(input, bytesLeft) ? RESULT_END_OF_INPUT : RESULT_CONTINUE; } + @EnsuresNonNull({"extractorOutput", "trackOutput"}) + private void assertInitialized() { + Assertions.checkStateNotNull(trackOutput); + Util.castNonNull(extractorOutput); + } + /** Writes to the extractor's output. */ private interface OutputWriter { @@ -201,12 +210,19 @@ public final class WavExtractor implements Extractor { TrackOutput trackOutput, WavHeader header, String mimeType, - @C.PcmEncoding int pcmEncoding) { + @C.PcmEncoding int pcmEncoding) + throws ParserException { this.extractorOutput = extractorOutput; this.trackOutput = trackOutput; this.header = header; - // Blocks are expected to correspond to single frames. This is validated in init(int, long). - int bytesPerFrame = header.blockSize; + + int bytesPerFrame = header.numChannels * header.bitsPerSample / 8; + // Validate the header. Blocks are expected to correspond to single frames. + if (header.blockSize != bytesPerFrame) { + throw new ParserException( + "Expected block size: " + bytesPerFrame + "; got: " + header.blockSize); + } + targetSampleSizeBytes = Math.max(bytesPerFrame, header.frameRateHz * bytesPerFrame / TARGET_SAMPLES_PER_SECOND); format = @@ -233,15 +249,7 @@ public final class WavExtractor implements Extractor { } @Override - public void init(int dataStartPosition, long dataEndPosition) throws ParserException { - // Validate the header. - int bytesPerFrame = header.numChannels * header.bitsPerSample / 8; - if (header.blockSize != bytesPerFrame) { - throw new ParserException( - "Expected block size: " + bytesPerFrame + "; got: " + header.blockSize); - } - - // Output the seek map and format. + public void init(int dataStartPosition, long dataEndPosition) { extractorOutput.seekMap( new WavSeekMap(header, /* framesPerBlock= */ 1, dataStartPosition, dataEndPosition)); trackOutput.format(format); @@ -302,18 +310,20 @@ public final class WavExtractor implements Extractor { private final ExtractorOutput extractorOutput; private final TrackOutput trackOutput; private final WavHeader header; + + /** Number of frames per block of the input (yet to be decoded) data. */ + private final int framesPerBlock; + /** Target for the input (yet to be decoded) data. */ + private final byte[] inputData; + /** Target for decoded (yet to be output) data. */ + private final ParsableByteArray decodedData; /** The target size of each output sample, in frames. */ private final int targetSampleSizeFrames; + /** The output format. */ + private final Format format; - // Properties of the input (yet to be decoded) data. - private int framesPerBlock; - private byte[] inputData; + /** The number of pending bytes in {@link #inputData}. */ private int pendingInputBytes; - - // Target for decoded (yet to be output) data. - private ParsableByteArray decodedData; - - // Properties of the output. /** The time at which the writer was last {@link #reset}. */ private long startTimeUs; /** @@ -329,33 +339,21 @@ public final class WavExtractor implements Extractor { private long outputFrameCount; public ImaAdPcmOutputWriter( - ExtractorOutput extractorOutput, TrackOutput trackOutput, WavHeader header) { + ExtractorOutput extractorOutput, TrackOutput trackOutput, WavHeader header) + throws ParserException { this.extractorOutput = extractorOutput; this.trackOutput = trackOutput; this.header = header; targetSampleSizeFrames = Math.max(1, header.frameRateHz / TARGET_SAMPLES_PER_SECOND); - } - @Override - public void reset(long timeUs) { - // Reset the input side. - pendingInputBytes = 0; - // Reset the output side. - startTimeUs = timeUs; - pendingOutputBytes = 0; - outputFrameCount = 0; - } - - @Override - public void init(int dataStartPosition, long dataEndPosition) throws ParserException { - // Validate the header. ParsableByteArray scratch = new ParsableByteArray(header.extraData); scratch.readLittleEndianUnsignedShort(); framesPerBlock = scratch.readLittleEndianUnsignedShort(); - // This calculation is defined in "Microsoft Multimedia Standards Update - New Multimedia - // Types and Data Techniques" (1994). See the "IMA ADPCM Wave Type" and - // "DVI ADPCM Wave Type" sections, and the calculation of wSamplesPerBlock in the latter. + int numChannels = header.numChannels; + // Validate the header. This calculation is defined in "Microsoft Multimedia Standards Update + // - New Multimedia Types and Data Techniques" (1994). See the "IMA ADPCM Wave Type" and "DVI + // ADPCM Wave Type" sections, and the calculation of wSamplesPerBlock in the latter. int expectedFramesPerBlock = (((header.blockSize - (4 * numChannels)) * 8) / (header.bitsPerSample * numChannels)) + 1; if (framesPerBlock != expectedFramesPerBlock) { @@ -368,22 +366,19 @@ public final class WavExtractor implements Extractor { int maxBlocksToDecode = Util.ceilDivide(targetSampleSizeFrames, framesPerBlock); inputData = new byte[maxBlocksToDecode * header.blockSize]; decodedData = - new ParsableByteArray(maxBlocksToDecode * numOutputFramesToBytes(framesPerBlock)); + new ParsableByteArray( + maxBlocksToDecode * numOutputFramesToBytes(framesPerBlock, numChannels)); - // Output the seek map. - extractorOutput.seekMap( - new WavSeekMap(header, framesPerBlock, dataStartPosition, dataEndPosition)); - - // Output the format. We calculate the bitrate of the data before decoding, since this is the + // Create the format. We calculate the bitrate of the data before decoding, since this is the // bitrate of the stream itself. int bitrate = header.frameRateHz * header.blockSize * 8 / framesPerBlock; - Format format = + format = Format.createAudioSampleFormat( /* id= */ null, MimeTypes.AUDIO_RAW, /* codecs= */ null, bitrate, - /* maxInputSize= */ numOutputFramesToBytes(targetSampleSizeFrames), + /* maxInputSize= */ numOutputFramesToBytes(targetSampleSizeFrames, numChannels), header.numChannels, header.frameRateHz, C.ENCODING_PCM_16BIT, @@ -391,6 +386,20 @@ public final class WavExtractor implements Extractor { /* drmInitData= */ null, /* selectionFlags= */ 0, /* language= */ null); + } + + @Override + public void reset(long timeUs) { + pendingInputBytes = 0; + startTimeUs = timeUs; + pendingOutputBytes = 0; + outputFrameCount = 0; + } + + @Override + public void init(int dataStartPosition, long dataEndPosition) { + extractorOutput.seekMap( + new WavSeekMap(header, framesPerBlock, dataStartPosition, dataEndPosition)); trackOutput.format(format); } @@ -543,7 +552,11 @@ public final class WavExtractor implements Extractor { } private int numOutputFramesToBytes(int frames) { - return frames * 2 * header.numChannels; + return numOutputFramesToBytes(frames, header.numChannels); + } + + private static int numOutputFramesToBytes(int frames, int numChannels) { + return frames * 2 * numChannels; } } } From 9618e5e00f1577e3c73b7d44463c701fde337eb7 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 6 Jan 2020 16:21:38 +0000 Subject: [PATCH 0604/1335] FlacExtractor: Fix possible skipping of frame boundaries PiperOrigin-RevId: 288304477 --- .../google/android/exoplayer2/extractor/flac/FlacExtractor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java index 8a64d4243c..8c31bde2a2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java @@ -272,7 +272,7 @@ public final class FlacExtractor implements Extractor { // Skip frame search on the bytes within the minimum frame size. if (currentFrameBytesWritten < minFrameSize) { - buffer.skipBytes(Math.min(minFrameSize, buffer.bytesLeft())); + buffer.skipBytes(Math.min(minFrameSize - currentFrameBytesWritten, buffer.bytesLeft())); } long nextFrameFirstSampleNumber = findFrame(buffer, foundEndOfInput); From 09ca5c0783c9770879b389b15930d1987016a7c1 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 7 Jan 2020 10:55:13 +0000 Subject: [PATCH 0605/1335] Remove assertWithMessage() calls from SsaDecoderTest As discussed with Olly, these don't add much info and are liable to go stale. PiperOrigin-RevId: 288463027 --- .../exoplayer2/text/ssa/SsaDecoderTest.java | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) 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 9112bec398..65536f277e 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 @@ -70,10 +70,10 @@ public final class SsaDecoderTest { 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); + assertThat(firstCue.position).isEqualTo(0.5f); + assertThat(firstCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(firstCue.line).isEqualTo(0.95f); assertTypicalCue1(subtitle, 0); assertTypicalCue2(subtitle, 2); @@ -158,33 +158,33 @@ public final class SsaDecoderTest { // 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); + assertThat(firstCue.position).isEqualTo(0.5f); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(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); + assertThat(secondCue.position).isEqualTo(0.25f); + assertThat(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); + assertThat(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); + assertThat(fourthCue.position).isEqualTo(0.5f); + assertThat(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); + assertThat(fifthCue.position).isEqualTo(0.5f); + assertThat(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); + assertThat(fifthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); assertWithMessage("Cue.textAlignment") .that(fifthCue.textAlignment) .isEqualTo(Layout.Alignment.ALIGN_CENTER); @@ -192,12 +192,12 @@ public final class SsaDecoderTest { // 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); + assertThat(sixthCue.position).isEqualTo(0.5f); + assertThat(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); + assertThat(sixthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_START); assertWithMessage("Cue.textAlignment") .that(sixthCue.textAlignment) .isEqualTo(Layout.Alignment.ALIGN_OPPOSITE); @@ -212,31 +212,31 @@ public final class SsaDecoderTest { // 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); + assertThat(firstCue.position).isEqualTo(0.05f); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(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); + assertThat(secondCue.position).isEqualTo(0.05f); + assertThat(secondCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(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); + assertThat(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); + assertThat(fourthCue.position).isEqualTo(0.05f); + assertThat(fourthCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(fourthCue.line).isEqualTo(0.5f); } @Test @@ -250,9 +250,9 @@ public final class SsaDecoderTest { // 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); + assertThat(firstCue.position).isEqualTo(Cue.DIMEN_UNSET); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(firstCue.line).isEqualTo(Cue.DIMEN_UNSET); } @Test From 35fbb7f7cae26887c11d2a1972910782566c19ef Mon Sep 17 00:00:00 2001 From: kimvde Date: Tue, 7 Jan 2020 11:03:42 +0000 Subject: [PATCH 0606/1335] Add comment explaining FlacBinarySearchSeeker output PiperOrigin-RevId: 288464154 --- .../android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java | 2 ++ 1 file changed, 2 insertions(+) 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 cad5219883..34b3ad2df5 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 @@ -126,6 +126,8 @@ import java.nio.ByteBuffer; if (targetSampleInLastFrame) { // We are holding the target frame in outputFrameHolder. Set its presentation time now. outputFrameHolder.timeUs = decoderJni.getLastFrameTimestamp(); + // The input position is passed even though it does not indicate the frame containing the + // target sample because the extractor must continue to read from this position. return TimestampSearchResult.targetFoundResult(input.getPosition()); } else if (nextFrameSampleIndex <= targetSampleIndex) { return TimestampSearchResult.underestimatedResult( From 692c8ee0acfb23dfc24e690c365d2ed052e11f9b Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 7 Jan 2020 11:47:25 +0000 Subject: [PATCH 0607/1335] Fix playback for Vivo codecs that output non-16-bit audio PiperOrigin-RevId: 288468497 --- .../exoplayer2/audio/MediaCodecAudioRenderer.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 dfa13134ce..64a2dcfe37 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 @@ -79,6 +79,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private static final int MAX_PENDING_STREAM_CHANGE_COUNT = 10; private static final String TAG = "MediaCodecAudioRenderer"; + /** + * Custom key used to indicate bits per sample by some decoders on Vivo devices. For example + * OMX.vivo.alac.decoder on the Vivo Z1 Pro. + */ + private static final String VIVO_BITS_PER_SAMPLE_KEY = "v-bits-per-sample"; private final Context context; private final EventDispatcher eventDispatcher; @@ -566,7 +571,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media mediaFormat.getString(MediaFormat.KEY_MIME)); } else { mediaFormat = outputMediaFormat; - encoding = getPcmEncoding(inputFormat); + if (outputMediaFormat.containsKey(VIVO_BITS_PER_SAMPLE_KEY)) { + encoding = Util.getPcmEncoding(outputMediaFormat.getInteger(VIVO_BITS_PER_SAMPLE_KEY)); + } else { + encoding = getPcmEncoding(inputFormat); + } } int channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE); From 181606137dbd51af5cd5e593167884fc56f3f8f2 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 7 Jan 2020 12:06:43 +0000 Subject: [PATCH 0608/1335] Remove assertCues() helper methods from WebvttDecoderTest These make the interesting bits of each assertion harder to follow imo. Also remove all the assertWithMessage() calls at the same time, Olly convinced me these are rarely useful since you can click from the stack trace to the failing line in the IDE. PiperOrigin-RevId: 288470704 --- .../src/test/assets/webvtt/with_positioning | 8 +- .../text/webvtt/WebvttDecoderTest.java | 506 ++++++------------ 2 files changed, 180 insertions(+), 334 deletions(-) diff --git a/library/core/src/test/assets/webvtt/with_positioning b/library/core/src/test/assets/webvtt/with_positioning index 6bb86b7c93..7db327ca62 100644 --- a/library/core/src/test/assets/webvtt/with_positioning +++ b/library/core/src/test/assets/webvtt/with_positioning @@ -8,12 +8,12 @@ This is the first subtitle. NOTE Wrong position provided. It should be provided as a percentage value -00:02.345 --> 00:03.456 position:10 align:end size:35% +00:02.345 --> 00:03.456 position:10 align:end This is the second subtitle. NOTE Line as percentage and line alignment -00:04.000 --> 00:05.000 line:45%,end align:middle size:35% +00:04.000 --> 00:05.000 line:45%,end align:middle This is the third subtitle. NOTE Line as absolute negative number and without line alignment. @@ -23,10 +23,10 @@ This is the fourth subtitle. NOTE The position and positioning alignment should be inherited from align. -00:07.000 --> 00:08.000 align:right +00:08.000 --> 00:09.000 align:right This is the fifth subtitle. NOTE In newer drafts, align:middle has been replaced by align:center -00:10.000 --> 00:11.000 line:45%,end align:center size:35% +00:10.000 --> 00:11.000 align:center This is the sixth subtitle. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index b33439f4f3..e07c412fd7 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -25,16 +25,15 @@ import android.text.Spanned; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; -import androidx.annotation.Nullable; 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.SubtitleDecoderException; import com.google.android.exoplayer2.util.ColorParser; +import com.google.common.collect.Iterables; import com.google.common.truth.Expect; import java.io.IOException; -import java.util.List; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -76,350 +75,254 @@ public class WebvttDecoderTest { public void testDecodeTypical() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_FILE); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); } @Test public void testDecodeWithBom() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_BOM); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); } @Test public void testDecodeTypicalWithBadTimestamps() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_BAD_TIMESTAMPS); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); } @Test public void testDecodeTypicalWithIds() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_IDS_FILE); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); } @Test public void testDecodeTypicalWithComments() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_COMMENTS_FILE); - // test event count assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // test cues - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(0 + 1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(2 + 1)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); } @Test public void testDecodeWithTags() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_TAGS_FILE); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(8); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 4, - /* startTimeUs= */ 4000000, - /* endTimeUs= */ 5000000, - "This is the third subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 6, - /* startTimeUs= */ 6000000, - /* endTimeUs= */ 7000000, - "This is the &subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); + + assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L); + assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L); + Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); + assertThat(thirdCue.text.toString()).isEqualTo("This is the third subtitle."); + + assertThat(subtitle.getEventTime(6)).isEqualTo(6_000_000L); + assertThat(subtitle.getEventTime(7)).isEqualTo(7_000_000L); + Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); + assertThat(fourthCue.text.toString()).isEqualTo("This is the &subtitle."); } @Test public void testDecodeWithPositioning() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_POSITIONING_FILE); - // Test event count. + assertThat(subtitle.getEventTimeCount()).isEqualTo(12); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle.", - Alignment.ALIGN_NORMAL, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.1f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_START, - /* size= */ 0.35f, - /* verticalType= */ Cue.TYPE_UNSET); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle.", - Alignment.ALIGN_OPPOSITE, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_END, - /* size= */ 0.35f, - /* verticalType= */ Cue.TYPE_UNSET); - assertCue( - subtitle, - /* eventTimeIndex= */ 4, - /* startTimeUs= */ 4000000, - /* endTimeUs= */ 5000000, - "This is the third subtitle.", - Alignment.ALIGN_CENTER, - /* line= */ 0.45f, - /* lineType= */ Cue.LINE_TYPE_FRACTION, - /* lineAnchor= */ Cue.ANCHOR_TYPE_END, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 0.35f, - /* verticalType= */ Cue.TYPE_UNSET); - assertCue( - subtitle, - /* eventTimeIndex= */ 6, - /* startTimeUs= */ 6000000, - /* endTimeUs= */ 7000000, - "This is the fourth subtitle.", - Alignment.ALIGN_CENTER, - /* line= */ -11.0f, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 1.0f, - /* verticalType= */ Cue.TYPE_UNSET); - assertCue( - subtitle, - /* eventTimeIndex= */ 8, - /* startTimeUs= */ 7000000, - /* endTimeUs= */ 8000000, - "This is the fifth subtitle.", - Alignment.ALIGN_OPPOSITE, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 1.0f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_END, - /* size= */ 1.0f, - /* verticalType= */ Cue.TYPE_UNSET); - assertCue( - subtitle, - /* eventTimeIndex= */ 10, - /* startTimeUs= */ 10000000, - /* endTimeUs= */ 11000000, - "This is the sixth subtitle.", - Alignment.ALIGN_CENTER, - /* line= */ 0.45f, - /* lineType= */ Cue.LINE_TYPE_FRACTION, - /* lineAnchor= */ Cue.ANCHOR_TYPE_END, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 0.35f, - /* verticalType= */ Cue.TYPE_UNSET); + + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + assertThat(firstCue.position).isEqualTo(0.1f); + assertThat(firstCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_START); + assertThat(firstCue.textAlignment).isEqualTo(Alignment.ALIGN_NORMAL); + assertThat(firstCue.size).isEqualTo(0.35f); + // Unspecified values should use WebVTT defaults + assertThat(firstCue.line).isEqualTo(Cue.DIMEN_UNSET); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER); + assertThat(firstCue.verticalType).isEqualTo(Cue.TYPE_UNSET); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); + // Position is invalid so defaults to 0.5 + assertThat(secondCue.position).isEqualTo(0.5f); + assertThat(secondCue.textAlignment).isEqualTo(Alignment.ALIGN_OPPOSITE); + + assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L); + assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L); + Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); + assertThat(thirdCue.text.toString()).isEqualTo("This is the third subtitle."); + assertThat(thirdCue.line).isEqualTo(0.45f); + assertThat(thirdCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(thirdCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + assertThat(thirdCue.textAlignment).isEqualTo(Alignment.ALIGN_CENTER); + // Derived from `align:middle`: + assertThat(thirdCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + + assertThat(subtitle.getEventTime(6)).isEqualTo(6_000_000L); + assertThat(subtitle.getEventTime(7)).isEqualTo(7_000_000L); + Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); + assertThat(fourthCue.text.toString()).isEqualTo("This is the fourth subtitle."); + assertThat(fourthCue.line).isEqualTo(-11f); + assertThat(fourthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_START); + assertThat(fourthCue.textAlignment).isEqualTo(Alignment.ALIGN_CENTER); + // Derived from `align:middle`: + assertThat(fourthCue.position).isEqualTo(0.5f); + assertThat(fourthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + + assertThat(subtitle.getEventTime(8)).isEqualTo(8_000_000L); + assertThat(subtitle.getEventTime(9)).isEqualTo(9_000_000L); + Cue fifthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8))); + assertThat(fifthCue.text.toString()).isEqualTo("This is the fifth subtitle."); + assertThat(fifthCue.textAlignment).isEqualTo(Alignment.ALIGN_OPPOSITE); + // Derived from `align:right`: + assertThat(fifthCue.position).isEqualTo(1.0f); + assertThat(fifthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + + assertThat(subtitle.getEventTime(10)).isEqualTo(10_000_000L); + assertThat(subtitle.getEventTime(11)).isEqualTo(11_000_000L); + Cue sixthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10))); + assertThat(sixthCue.text.toString()).isEqualTo("This is the sixth subtitle."); + assertThat(sixthCue.textAlignment).isEqualTo(Alignment.ALIGN_CENTER); + // Derived from `align:center`: + assertThat(sixthCue.position).isEqualTo(0.5f); + assertThat(sixthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); } @Test public void testDecodeWithVertical() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_VERTICAL_FILE); - // Test event count. + assertThat(subtitle.getEventTimeCount()).isEqualTo(6); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "Vertical right-to-left (e.g. Japanese)", - Alignment.ALIGN_CENTER, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 1.0f, - Cue.VERTICAL_TYPE_RL); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "Vertical left-to-right (e.g. Mongolian)", - Alignment.ALIGN_CENTER, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 1.0f, - Cue.VERTICAL_TYPE_LR); - assertCue( - subtitle, - /* eventTimeIndex= */ 4, - /* startTimeUs= */ 4000000, - /* endTimeUs= */ 5000000, - "No vertical setting (i.e. horizontal)", - Alignment.ALIGN_CENTER, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 1.0f, - /* verticalType= */ Cue.TYPE_UNSET); + + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("Vertical right-to-left (e.g. Japanese)"); + assertThat(firstCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_RL); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("Vertical left-to-right (e.g. Mongolian)"); + assertThat(secondCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_LR); + + assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L); + assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L); + Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); + assertThat(thirdCue.text.toString()).isEqualTo("No vertical setting (i.e. horizontal)"); + assertThat(thirdCue.verticalType).isEqualTo(Cue.TYPE_UNSET); } @Test public void testDecodeWithBadCueHeader() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_BAD_CUE_HEADER_FILE); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 4000000, - /* endTimeUs= */ 5000000, - "This is the third subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(4_000_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(5_000_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the third subtitle."); } @Test public void testWebvttWithCssStyle() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_STYLES); - // Test event count. - assertThat(subtitle.getEventTimeCount()).isEqualTo(8); - - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); - - Spanned s1 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 0); - Spanned s2 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2345000); - Spanned s3 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 20000000); - Spanned s4 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 25000000); - assertThat(s1) - .hasForegroundColorSpanBetween(0, s1.length()) + Spanned firstCueText = getUniqueSpanTextAt(subtitle, 0); + assertThat(firstCueText.toString()).isEqualTo("This is the first subtitle."); + assertThat(firstCueText) + .hasForegroundColorSpanBetween(0, firstCueText.length()) .withColor(ColorParser.parseCssColor("papayawhip")); - assertThat(s1) - .hasBackgroundColorSpanBetween(0, s1.length()) + assertThat(firstCueText) + .hasBackgroundColorSpanBetween(0, firstCueText.length()) .withColor(ColorParser.parseCssColor("green")); - assertThat(s2) - .hasForegroundColorSpanBetween(0, s2.length()) + + Spanned secondCueText = getUniqueSpanTextAt(subtitle, 2_345_000); + assertThat(secondCueText.toString()).isEqualTo("This is the second subtitle."); + assertThat(secondCueText) + .hasForegroundColorSpanBetween(0, secondCueText.length()) .withColor(ColorParser.parseCssColor("peachpuff")); - assertThat(s3).hasUnderlineSpanBetween(10, s3.length()); - assertThat(s4) - .hasBackgroundColorSpanBetween(0, 16) + Spanned thirdCueText = getUniqueSpanTextAt(subtitle, 20_000_000); + assertThat(thirdCueText.toString()).isEqualTo("This is a reference by element"); + assertThat(thirdCueText).hasUnderlineSpanBetween("This is a ".length(), thirdCueText.length()); + + Spanned fourthCueText = getUniqueSpanTextAt(subtitle, 25_000_000); + assertThat(fourthCueText.toString()).isEqualTo("You are an idiot\nYou don't have the guts"); + assertThat(fourthCueText) + .hasBackgroundColorSpanBetween(0, "You are an idiot".length()) .withColor(ColorParser.parseCssColor("lime")); - assertThat(s4).hasBoldSpanBetween(/* startIndex= */ 17, /* endIndex= */ s4.length()); + assertThat(fourthCueText) + .hasBoldSpanBetween("You are an idiot\n".length(), fourthCueText.length()); } @Test @@ -486,61 +389,4 @@ public class WebvttDecoderTest { private Spanned getUniqueSpanTextAt(WebvttSubtitle sub, long timeUs) { return (Spanned) sub.getCues(timeUs).get(0).text; } - - private void assertCue( - WebvttSubtitle subtitle, int eventTimeIndex, long startTimeUs, long endTimeUs, String text) { - assertCue( - subtitle, - eventTimeIndex, - startTimeUs, - endTimeUs, - text, - /* textAlignment= */ Alignment.ALIGN_CENTER, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 1.0f, - /* verticalType= */ Cue.TYPE_UNSET); - } - - private void assertCue( - WebvttSubtitle subtitle, - int eventTimeIndex, - long startTimeUs, - long endTimeUs, - String text, - @Nullable Alignment textAlignment, - float line, - @Cue.LineType int lineType, - @Cue.AnchorType int lineAnchor, - float position, - @Cue.AnchorType int positionAnchor, - float size, - @Cue.VerticalType int verticalType) { - expect - .withMessage("startTimeUs") - .that(subtitle.getEventTime(eventTimeIndex)) - .isEqualTo(startTimeUs); - expect - .withMessage("endTimeUs") - .that(subtitle.getEventTime(eventTimeIndex + 1)) - .isEqualTo(endTimeUs); - List cues = subtitle.getCues(subtitle.getEventTime(eventTimeIndex)); - assertThat(cues).hasSize(1); - // Assert cue properties. - Cue cue = cues.get(0); - expect.withMessage("cue.text").that(cue.text.toString()).isEqualTo(text); - expect.withMessage("cue.textAlignment").that(cue.textAlignment).isEqualTo(textAlignment); - expect.withMessage("cue.line").that(cue.line).isEqualTo(line); - expect.withMessage("cue.lineType").that(cue.lineType).isEqualTo(lineType); - expect.withMessage("cue.lineAnchor").that(cue.lineAnchor).isEqualTo(lineAnchor); - expect.withMessage("cue.position").that(cue.position).isEqualTo(position); - expect.withMessage("cue.positionAnchor").that(cue.positionAnchor).isEqualTo(positionAnchor); - expect.withMessage("cue.size").that(cue.size).isEqualTo(size); - expect.withMessage("cue.verticalType").that(cue.verticalType).isEqualTo(verticalType); - - assertThat(expect.hasFailures()).isFalse(); - } } From c5535e825e993e43f2cca63bfed258971d21ecda Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 7 Jan 2020 12:54:13 +0000 Subject: [PATCH 0609/1335] Fix null-checker suppression introduced by 3.0.1 upgrade Suppression added in https://github.com/google/ExoPlayer/commit/6f9baffa0cc7daf8cbfd5e1f6c55a908190d2041 PiperOrigin-RevId: 288475120 --- .../com/google/android/exoplayer2/text/dvb/DvbParser.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 228973ce0c..8d99816ee1 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 @@ -481,8 +481,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * * @return The parsed object data. */ - // incompatible types in argument. - @SuppressWarnings("nullness:argument.type.incompatible") private static ObjectData parseObjectData(ParsableBitArray data) { int objectId = data.readBits(16); data.skipBits(4); // Skip object_version_number @@ -490,8 +488,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; boolean nonModifyingColorFlag = data.readBit(); data.skipBits(1); // Skip reserved. - @Nullable byte[] topFieldData = null; - @Nullable byte[] bottomFieldData = null; + byte[] topFieldData = Util.EMPTY_BYTE_ARRAY; + byte[] bottomFieldData = Util.EMPTY_BYTE_ARRAY; if (objectCodingMethod == OBJECT_CODING_STRING) { int numberOfCodes = data.readBits(8); From fb42f818ec15e810e67aade70e04e36a52de9860 Mon Sep 17 00:00:00 2001 From: christosts Date: Tue, 7 Jan 2020 13:05:14 +0000 Subject: [PATCH 0610/1335] Add start() method in MediaCodecAdapter PiperOrigin-RevId: 288476415 --- .../AsynchronousMediaCodecAdapter.java | 15 +- ...DedicatedThreadAsyncMediaCodecAdapter.java | 32 +--- .../mediacodec/MediaCodecAdapter.java | 7 + .../mediacodec/MediaCodecRenderer.java | 4 +- .../MultiLockAsyncMediaCodecAdapter.java | 32 +--- .../SynchronousMediaCodecAdapter.java | 6 + .../AsynchronousMediaCodecAdapterTest.java | 75 ++++---- ...catedThreadAsyncMediaCodecAdapterTest.java | 160 ++++------------- .../MultiLockAsyncMediaCodecAdapterTest.java | 161 ++++-------------- 9 files changed, 135 insertions(+), 357 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java index b5eb8efee3..18c7b1c201 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java @@ -38,7 +38,7 @@ import com.google.android.exoplayer2.util.Assertions; private final MediaCodec codec; @Nullable private IllegalStateException internalException; private boolean flushing; - private Runnable onCodecStart; + private Runnable codecStartRunnable; /** * Create a new {@code AsynchronousMediaCodecAdapter}. @@ -55,7 +55,12 @@ import com.google.android.exoplayer2.util.Assertions; handler = new Handler(looper); this.codec = codec; this.codec.setCallback(mediaCodecAsyncCallback); - onCodecStart = () -> codec.start(); + codecStartRunnable = codec::start; + } + + @Override + public void start() { + codecStartRunnable.run(); } @Override @@ -105,7 +110,7 @@ import com.google.android.exoplayer2.util.Assertions; flushing = false; mediaCodecAsyncCallback.flush(); try { - onCodecStart.run(); + codecStartRunnable.run(); } catch (IllegalStateException e) { // Catch IllegalStateException directly so that we don't have to wrap it. internalException = e; @@ -115,8 +120,8 @@ import com.google.android.exoplayer2.util.Assertions; } @VisibleForTesting - /* package */ void setOnCodecStart(Runnable onCodecStart) { - this.onCodecStart = onCodecStart; + /* package */ void setCodecStartRunnable(Runnable codecStartRunnable) { + this.codecStartRunnable = codecStartRunnable; } private void maybeThrowException() throws IllegalStateException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapter.java index bad21f91f8..b623811453 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapter.java @@ -26,7 +26,6 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -54,7 +53,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @MonotonicNonNull private Handler handler; private long pendingFlushCount; private @State int state; - private Runnable onCodecStart; + private Runnable codecStartRunnable; @Nullable private IllegalStateException internalException; /** @@ -77,31 +76,20 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; this.codec = codec; this.handlerThread = handlerThread; state = STATE_CREATED; - onCodecStart = codec::start; + codecStartRunnable = codec::start; } - /** - * Starts the operation of the instance. - * - *

    After a call to this method, make sure to call {@link #shutdown()} to terminate the internal - * Thread. You can only call this method once during the lifetime of this instance; calling this - * method again will throw an {@link IllegalStateException}. - * - * @throws IllegalStateException If this method has been called already. - */ + @Override public synchronized void start() { - Assertions.checkState(state == STATE_CREATED); - handlerThread.start(); handler = new Handler(handlerThread.getLooper()); codec.setCallback(this, handler); + codecStartRunnable.run(); state = STATE_STARTED; } @Override public synchronized int dequeueInputBufferIndex() { - Assertions.checkState(state == STATE_STARTED); - if (isFlushing()) { return MediaCodec.INFO_TRY_AGAIN_LATER; } else { @@ -112,8 +100,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public synchronized int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { - Assertions.checkState(state == STATE_STARTED); - if (isFlushing()) { return MediaCodec.INFO_TRY_AGAIN_LATER; } else { @@ -124,15 +110,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public synchronized MediaFormat getOutputFormat() { - Assertions.checkState(state == STATE_STARTED); - return mediaCodecAsyncCallback.getOutputFormat(); } @Override public synchronized void flush() { - Assertions.checkState(state == STATE_STARTED); - codec.flush(); ++pendingFlushCount; Util.castNonNull(handler).post(this::onFlushCompleted); @@ -177,8 +159,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } @VisibleForTesting - /* package */ void setOnCodecStart(Runnable onCodecStart) { - this.onCodecStart = onCodecStart; + /* package */ void setCodecStartRunnable(Runnable codecStartRunnable) { + this.codecStartRunnable = codecStartRunnable; } private synchronized void onFlushCompleted() { @@ -199,7 +181,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; mediaCodecAsyncCallback.flush(); try { - onCodecStart.run(); + codecStartRunnable.run(); } catch (IllegalStateException e) { internalException = e; } catch (Exception e) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java index c984443041..2f347de0ae 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java @@ -31,6 +31,13 @@ import android.media.MediaFormat; */ /* package */ interface MediaCodecAdapter { + /** + * Starts this instance. + * + * @see MediaCodec#start(). + */ + void start(); + /** * Returns the next available input buffer index from the underlying {@link MediaCodec} or {@link * MediaCodec#INFO_TRY_AGAIN_LATER} if no such buffer exists. 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 dbfeed4063..89a0cb5ae1 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 @@ -995,11 +995,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } else if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD && Util.SDK_INT >= 23) { codecAdapter = new DedicatedThreadAsyncMediaCodecAdapter(codec, getTrackType()); - ((DedicatedThreadAsyncMediaCodecAdapter) codecAdapter).start(); } else if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK && Util.SDK_INT >= 23) { codecAdapter = new MultiLockAsyncMediaCodecAdapter(codec, getTrackType()); - ((MultiLockAsyncMediaCodecAdapter) codecAdapter).start(); } else { codecAdapter = new SynchronousMediaCodecAdapter(codec); } @@ -1009,7 +1007,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { configureCodec(codecInfo, codec, inputFormat, crypto, codecOperatingRate); TraceUtil.endSection(); TraceUtil.beginSection("startCodec"); - codec.start(); + codecAdapter.start(); TraceUtil.endSection(); codecInitializedTimestamp = SystemClock.elapsedRealtime(); getCodecBuffers(codec); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapter.java index 56f503c71a..48d4ac9a55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapter.java @@ -27,7 +27,6 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.IntArrayQueue; import com.google.android.exoplayer2.util.Util; import java.util.ArrayDeque; @@ -94,7 +93,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final HandlerThread handlerThread; @MonotonicNonNull private Handler handler; - private Runnable onCodecStart; + private Runnable codecStartRunnable; /** Creates a new instance that wraps the specified {@link MediaCodec}. */ /* package */ MultiLockAsyncMediaCodecAdapter(MediaCodec codec, int trackType) { @@ -114,25 +113,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; codecException = null; state = STATE_CREATED; this.handlerThread = handlerThread; - onCodecStart = codec::start; + codecStartRunnable = codec::start; } - /** - * Starts the operation of this instance. - * - *

    After a call to this method, make sure to call {@link #shutdown()} to terminate the internal - * Thread. You can only call this method once during the lifetime of an instance; calling this - * method again will throw an {@link IllegalStateException}. - * - * @throws IllegalStateException If this method has been called already. - */ + @Override public void start() { synchronized (objectStateLock) { - Assertions.checkState(state == STATE_CREATED); - handlerThread.start(); handler = new Handler(handlerThread.getLooper()); codec.setCallback(this, handler); + codecStartRunnable.run(); state = STATE_STARTED; } } @@ -140,8 +130,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public int dequeueInputBufferIndex() { synchronized (objectStateLock) { - Assertions.checkState(state == STATE_STARTED); - if (isFlushing()) { return MediaCodec.INFO_TRY_AGAIN_LATER; } else { @@ -154,8 +142,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { synchronized (objectStateLock) { - Assertions.checkState(state == STATE_STARTED); - if (isFlushing()) { return MediaCodec.INFO_TRY_AGAIN_LATER; } else { @@ -168,8 +154,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public MediaFormat getOutputFormat() { synchronized (objectStateLock) { - Assertions.checkState(state == STATE_STARTED); - if (currentFormat == null) { throw new IllegalStateException(); } @@ -181,8 +165,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public void flush() { synchronized (objectStateLock) { - Assertions.checkState(state == STATE_STARTED); - codec.flush(); pendingFlush++; Util.castNonNull(handler).post(this::onFlushComplete); @@ -200,8 +182,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } @VisibleForTesting - /* package */ void setOnCodecStart(Runnable onCodecStart) { - this.onCodecStart = onCodecStart; + /* package */ void setCodecStartRunnable(Runnable codecStartRunnable) { + this.codecStartRunnable = codecStartRunnable; } private int dequeueAvailableInputBufferIndex() { @@ -307,7 +289,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; clearAvailableOutput(); codecException = null; try { - onCodecStart.run(); + codecStartRunnable.run(); } catch (IllegalStateException e) { codecException = e; } catch (Exception e) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java index 7dd7ef8f20..ee9ab857cc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java @@ -23,12 +23,18 @@ import android.media.MediaFormat; * A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in synchronous mode. */ /* package */ final class SynchronousMediaCodecAdapter implements MediaCodecAdapter { + private final MediaCodec codec; public SynchronousMediaCodecAdapter(MediaCodec mediaCodec) { this.codec = mediaCodec; } + @Override + public void start() { + codec.start(); + } + @Override public int dequeueInputBufferIndex() { return codec.dequeueInputBuffer(0); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java index d2bb0fcc5b..34ed88d2d0 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java @@ -19,7 +19,7 @@ package com.google.android.exoplayer2.mediacodec; import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.areEqual; import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.waitUntilAllEventsAreExecuted; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; +import static org.junit.Assert.assertThrows; import android.media.MediaCodec; import android.media.MediaFormat; @@ -29,7 +29,7 @@ import android.os.Looper; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.io.IOException; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -45,27 +45,32 @@ public class AsynchronousMediaCodecAdapterTest { private MediaCodec.BufferInfo bufferInfo; @Before - public void setup() throws IOException { + public void setUp() throws IOException { handlerThread = new HandlerThread("TestHandlerThread"); handlerThread.start(); looper = handlerThread.getLooper(); codec = MediaCodec.createByCodecName("h264"); adapter = new AsynchronousMediaCodecAdapter(codec, looper); + adapter.setCodecStartRunnable(() -> {}); bufferInfo = new MediaCodec.BufferInfo(); } @After public void tearDown() { + adapter.shutdown(); handlerThread.quit(); } @Test public void dequeueInputBufferIndex_withoutInputBuffer_returnsTryAgainLater() { + adapter.start(); + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); } @Test public void dequeueInputBufferIndex_withInputBuffer_returnsInputBuffer() { + adapter.start(); adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0); assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0); @@ -73,6 +78,7 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void dequeueInputBufferIndex_whileFlushing_returnsTryAgainLater() { + adapter.start(); adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0); adapter.flush(); adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 1); @@ -83,9 +89,7 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void dequeueInputBufferIndex_afterFlushCompletes_returnsNextInputBuffer() throws InterruptedException { - // Disable calling codec.start() after flush() completes to avoid receiving buffers from the - // shadow codec impl - adapter.setOnCodecStart(() -> {}); + adapter.start(); Handler handler = new Handler(looper); handler.post( () -> adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0)); @@ -100,28 +104,35 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void dequeueInputBufferIndex_afterFlushCompletesWithError_throwsException() throws InterruptedException { - adapter.setOnCodecStart( + AtomicInteger calls = new AtomicInteger(0); + adapter.setCodecStartRunnable( () -> { - throw new IllegalStateException("codec#start() exception"); + if (calls.incrementAndGet() == 2) { + throw new IllegalStateException(); + } }); + adapter.start(); adapter.flush(); assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows( + IllegalStateException.class, + () -> { + adapter.dequeueInputBufferIndex(); + }); } @Test public void dequeueOutputBufferIndex_withoutOutputBuffer_returnsTryAgainLater() { + adapter.start(); + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); } @Test public void dequeueOutputBufferIndex_withOutputBuffer_returnsOutputBuffer() { + adapter.start(); MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); outBufferInfo.presentationTimeUs = 10; adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, outBufferInfo); @@ -132,6 +143,7 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void dequeueOutputBufferIndex_whileFlushing_returnsTryAgainLater() { + adapter.start(); adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, bufferInfo); adapter.flush(); adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 1, bufferInfo); @@ -143,9 +155,7 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void dequeueOutputBufferIndex_afterFlushCompletes_returnsNextOutputBuffer() throws InterruptedException { - // Disable calling codec.start() after flush() completes to avoid receiving buffers from the - // shadow codec impl - adapter.setOnCodecStart(() -> {}); + adapter.start(); Handler handler = new Handler(looper); MediaCodec.BufferInfo info0 = new MediaCodec.BufferInfo(); handler.post( @@ -164,31 +174,23 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void dequeueOutputBufferIndex_afterFlushCompletesWithError_throwsException() throws InterruptedException { - adapter.setOnCodecStart( + AtomicInteger calls = new AtomicInteger(0); + adapter.setCodecStartRunnable( () -> { - throw new RuntimeException("codec#start() exception"); + if (calls.incrementAndGet() == 2) { + throw new RuntimeException("codec#start() exception"); + } }); + adapter.start(); adapter.flush(); assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void getOutputFormat_withoutFormat_throwsException() { - try { - adapter.getOutputFormat(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); } @Test public void getOutputFormat_withMultipleFormats_returnsFormatsInCorrectOrder() { + adapter.start(); MediaFormat[] formats = new MediaFormat[10]; MediaCodec.Callback mediaCodecCallback = adapter.getMediaCodecCallback(); for (int i = 0; i < formats.length; i++) { @@ -212,6 +214,7 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void getOutputFormat_afterFlush_returnsPreviousFormat() throws InterruptedException { + adapter.start(); MediaFormat format = new MediaFormat(); adapter.getMediaCodecCallback().onOutputFormatChanged(codec, format); adapter.dequeueOutputBufferIndex(bufferInfo); @@ -223,13 +226,13 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void shutdown_withPendingFlush_cancelsFlush() throws InterruptedException { - AtomicBoolean onCodecStartCalled = new AtomicBoolean(false); - Runnable onCodecStart = () -> onCodecStartCalled.set(true); - adapter.setOnCodecStart(onCodecStart); + AtomicInteger onCodecStartCalled = new AtomicInteger(0); + adapter.setCodecStartRunnable(() -> onCodecStartCalled.incrementAndGet()); + adapter.start(); adapter.flush(); adapter.shutdown(); assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); - assertThat(onCodecStartCalled.get()).isFalse(); + assertThat(onCodecStartCalled.get()).isEqualTo(1); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapterTest.java index 2cfb577579..f974144dd6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapterTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapterTest.java @@ -19,7 +19,7 @@ package com.google.android.exoplayer2.mediacodec; import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.areEqual; import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.waitUntilAllEventsAreExecuted; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; +import static org.junit.Assert.assertThrows; import static org.robolectric.Shadows.shadowOf; import android.media.MediaCodec; @@ -47,16 +47,18 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { private MediaCodec.BufferInfo bufferInfo = null; @Before - public void setup() throws IOException { + public void setUp() throws IOException { codec = MediaCodec.createByCodecName("h264"); handlerThread = new TestHandlerThread("TestHandlerThread"); adapter = new DedicatedThreadAsyncMediaCodecAdapter(codec, handlerThread); + adapter.setCodecStartRunnable(() -> {}); bufferInfo = new MediaCodec.BufferInfo(); } @After public void tearDown() { adapter.shutdown(); + assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0); } @@ -66,42 +68,15 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { adapter.shutdown(); } - @Test - public void start_calledTwice_throwsException() { - adapter.start(); - try { - adapter.start(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void dequeueInputBufferIndex_withoutStart_throwsException() { - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void dequeueInputBufferIndex_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } - } - @Test public void dequeueInputBufferIndex_withAfterFlushFailed_throwsException() throws InterruptedException { - adapter.setOnCodecStart( + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable( () -> { - throw new IllegalStateException("codec#start() exception"); + if (codecStartCalls.incrementAndGet() == 2) { + throw new IllegalStateException("codec#start() exception"); + } }); adapter.start(); adapter.flush(); @@ -110,11 +85,8 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { waitUntilAllEventsAreExecuted( handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS)) .isTrue(); - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } + + assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex()); } @Test @@ -144,9 +116,6 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { @Test public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer() throws InterruptedException { - // Disable calling codec.start() after flush to avoid receiving buffers from the - // shadow codec impl - adapter.setOnCodecStart(() -> {}); adapter.start(); Looper looper = handlerThread.getLooper(); Handler handler = new Handler(looper); @@ -169,39 +138,18 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { adapter.start(); adapter.onMediaCodecError(new IllegalStateException("error from codec")); - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void dequeueOutputBufferIndex_withoutStart_throwsException() { - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void dequeueOutputBufferIndex_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex()); } @Test public void dequeueOutputBufferIndex_withInternalException_throwsException() throws InterruptedException { - adapter.setOnCodecStart( + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable( () -> { - throw new RuntimeException("codec#start() exception"); + if (codecStartCalls.incrementAndGet() == 2) { + throw new RuntimeException("codec#start() exception"); + } }); adapter.start(); adapter.flush(); @@ -210,11 +158,7 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { waitUntilAllEventsAreExecuted( handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS)) .isTrue(); - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); } @Test @@ -275,42 +219,14 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { adapter.start(); adapter.onMediaCodecError(new IllegalStateException("error from codec")); - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void getOutputFormat_withoutStart_throwsException() { - try { - adapter.getOutputFormat(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void getOutputFormat_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.getOutputFormat(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); } @Test public void getOutputFormat_withoutFormatReceived_throwsException() { adapter.start(); - try { - adapter.getOutputFormat(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.getOutputFormat()); } @Test @@ -351,28 +267,10 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { assertThat(adapter.getOutputFormat()).isEqualTo(format); } - @Test - public void flush_withoutStarted_throwsException() { - try { - adapter.flush(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void flush_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.flush(); - } catch (IllegalStateException expected) { - } - } - @Test public void flush_multipleTimes_onlyLastFlushExecutes() throws InterruptedException { - AtomicInteger onCodecStartCount = new AtomicInteger(0); - adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet()); + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable(() -> codecStartCalls.incrementAndGet()); adapter.start(); Looper looper = handlerThread.getLooper(); Handler handler = new Handler(looper); @@ -384,23 +282,23 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { adapter.flush(); // Enqueues a second flush event handler.post(() -> adapter.onInputBufferAvailable(codec, 3)); - // Progress the looper until the milestoneCount is increased - first flush event - // should have been a no-op + // Progress the looper until the milestoneCount is increased. + // adapter.start() will call codec.start(). First flush event should not call codec.start(). ShadowLooper shadowLooper = shadowOf(looper); while (milestoneCount.get() < 1) { shadowLooper.runOneTask(); } - assertThat(onCodecStartCount.get()).isEqualTo(0); + assertThat(codecStartCalls.get()).isEqualTo(1); assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(3); - assertThat(onCodecStartCount.get()).isEqualTo(1); + assertThat(codecStartCalls.get()).isEqualTo(2); } @Test public void flush_andImmediatelyShutdown_flushIsNoOp() throws InterruptedException { AtomicInteger onCodecStartCount = new AtomicInteger(0); - adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet()); + adapter.setCodecStartRunnable(() -> onCodecStartCount.incrementAndGet()); adapter.start(); // Obtain looper when adapter is started Looper looper = handlerThread.getLooper(); @@ -408,8 +306,8 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { adapter.shutdown(); assertThat(waitUntilAllEventsAreExecuted(looper, 5, TimeUnit.SECONDS)).isTrue(); - // only shutdown flushes the MediaCodecAsync handler - assertThat(onCodecStartCount.get()).isEqualTo(0); + // Only adapter.start() calls onCodecStart. + assertThat(onCodecStartCount.get()).isEqualTo(1); } private static class TestHandlerThread extends HandlerThread { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java index b984d28914..c31b86db39 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java @@ -19,7 +19,7 @@ package com.google.android.exoplayer2.mediacodec; import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.areEqual; import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.waitUntilAllEventsAreExecuted; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; +import static org.junit.Assert.assertThrows; import static org.robolectric.Shadows.shadowOf; import android.media.MediaCodec; @@ -44,20 +44,21 @@ public class MultiLockAsyncMediaCodecAdapterTest { private MultiLockAsyncMediaCodecAdapter adapter; private MediaCodec codec; private MediaCodec.BufferInfo bufferInfo = null; - private MediaCodecAsyncCallback mediaCodecAsyncCallbackSpy; private TestHandlerThread handlerThread; @Before - public void setup() throws IOException { + public void setUp() throws IOException { codec = MediaCodec.createByCodecName("h264"); handlerThread = new TestHandlerThread("TestHandlerThread"); adapter = new MultiLockAsyncMediaCodecAdapter(codec, handlerThread); + adapter.setCodecStartRunnable(() -> {}); bufferInfo = new MediaCodec.BufferInfo(); } @After public void tearDown() { adapter.shutdown(); + assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0); } @@ -67,42 +68,15 @@ public class MultiLockAsyncMediaCodecAdapterTest { adapter.shutdown(); } - @Test - public void start_calledTwice_throwsException() { - adapter.start(); - try { - adapter.start(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void dequeueInputBufferIndex_withoutStart_throwsException() { - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void dequeueInputBufferIndex_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } - } - @Test public void dequeueInputBufferIndex_withAfterFlushFailed_throwsException() throws InterruptedException { - adapter.setOnCodecStart( + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable( () -> { - throw new IllegalStateException("codec#start() exception"); + if (codecStartCalls.incrementAndGet() == 2) { + throw new IllegalStateException("codec#start() exception"); + } }); adapter.start(); adapter.flush(); @@ -111,11 +85,7 @@ public class MultiLockAsyncMediaCodecAdapterTest { waitUntilAllEventsAreExecuted( handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS)) .isTrue(); - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex()); } @Test @@ -145,9 +115,6 @@ public class MultiLockAsyncMediaCodecAdapterTest { @Test public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer() throws InterruptedException { - // Disable calling codec.start() after flush to avoid receiving buffers from the - // shadow codec impl - adapter.setOnCodecStart(() -> {}); adapter.start(); Looper looper = handlerThread.getLooper(); Handler handler = new Handler(looper); @@ -170,39 +137,19 @@ public class MultiLockAsyncMediaCodecAdapterTest { adapter.start(); adapter.onMediaCodecError(new IllegalStateException("error from codec")); - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex()); } - @Test - public void dequeueOutputBufferIndex_withoutStart_throwsException() { - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void dequeueOutputBufferIndex_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } - } @Test public void dequeueOutputBufferIndex_withInternalException_throwsException() throws InterruptedException { - adapter.setOnCodecStart( + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable( () -> { - throw new RuntimeException("codec#start() exception"); + if (codecStartCalls.incrementAndGet() == 2) { + throw new RuntimeException("codec#start() exception"); + } }); adapter.start(); adapter.flush(); @@ -211,11 +158,7 @@ public class MultiLockAsyncMediaCodecAdapterTest { waitUntilAllEventsAreExecuted( handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS)) .isTrue(); - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); } @Test @@ -276,42 +219,14 @@ public class MultiLockAsyncMediaCodecAdapterTest { adapter.start(); adapter.onMediaCodecError(new IllegalStateException("error from codec")); - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void getOutputFormat_withoutStart_throwsException() { - try { - adapter.getOutputFormat(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void getOutputFormat_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.getOutputFormat(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); } @Test public void getOutputFormat_withoutFormatReceived_throwsException() { adapter.start(); - try { - adapter.getOutputFormat(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.getOutputFormat()); } @Test @@ -352,28 +267,10 @@ public class MultiLockAsyncMediaCodecAdapterTest { assertThat(adapter.getOutputFormat()).isEqualTo(format); } - @Test - public void flush_withoutStarted_throwsException() { - try { - adapter.flush(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void flush_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.flush(); - } catch (IllegalStateException expected) { - } - } - @Test public void flush_multipleTimes_onlyLastFlushExecutes() throws InterruptedException { - AtomicInteger onCodecStartCount = new AtomicInteger(0); - adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet()); + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable(() -> codecStartCalls.incrementAndGet()); adapter.start(); Looper looper = handlerThread.getLooper(); Handler handler = new Handler(looper); @@ -385,23 +282,23 @@ public class MultiLockAsyncMediaCodecAdapterTest { adapter.flush(); // Enqueues a second flush event handler.post(() -> adapter.onInputBufferAvailable(codec, 3)); - // Progress the looper until the milestoneCount is increased - first flush event - // should have been a no-op + // Progress the looper until the milestoneCount is increased: + // adapter.start() called codec.start() but first flush event should have been a no-op ShadowLooper shadowLooper = shadowOf(looper); while (milestoneCount.get() < 1) { shadowLooper.runOneTask(); } - assertThat(onCodecStartCount.get()).isEqualTo(0); + assertThat(codecStartCalls.get()).isEqualTo(1); assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(3); - assertThat(onCodecStartCount.get()).isEqualTo(1); + assertThat(codecStartCalls.get()).isEqualTo(2); } @Test public void flush_andImmediatelyShutdown_flushIsNoOp() throws InterruptedException { - AtomicInteger onCodecStartCount = new AtomicInteger(0); - adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet()); + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable(() -> codecStartCalls.incrementAndGet()); adapter.start(); // Obtain looper when adapter is started. Looper looper = handlerThread.getLooper(); @@ -409,8 +306,8 @@ public class MultiLockAsyncMediaCodecAdapterTest { adapter.shutdown(); assertThat(waitUntilAllEventsAreExecuted(looper, 5, TimeUnit.SECONDS)).isTrue(); - // Only shutdown flushes the MediaCodecAsync handler. - assertThat(onCodecStartCount.get()).isEqualTo(0); + // Only adapter.start() called codec#start() + assertThat(codecStartCalls.get()).isEqualTo(1); } private static class TestHandlerThread extends HandlerThread { From 63f90adef0fe26156fd5b48babb9a6332b707e83 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 7 Jan 2020 15:39:27 +0000 Subject: [PATCH 0611/1335] Add package level NonNull to extractor.ts Also remove most classes from the nullness blacklist PiperOrigin-RevId: 288494712 --- .../android/exoplayer2/audio/DtsUtil.java | 5 +- .../exoplayer2/extractor/ts/Ac3Reader.java | 45 ++++++++++++------ .../exoplayer2/extractor/ts/Ac4Reader.java | 21 +++++---- .../exoplayer2/extractor/ts/AdtsReader.java | 41 ++++++++++------ .../ts/DefaultTsPayloadReaderFactory.java | 4 +- .../exoplayer2/extractor/ts/DtsReader.java | 20 ++++---- .../extractor/ts/DvbSubtitleReader.java | 6 +-- .../exoplayer2/extractor/ts/H262Reader.java | 47 ++++++++++--------- .../exoplayer2/extractor/ts/H264Reader.java | 41 ++++++++++++---- .../exoplayer2/extractor/ts/H265Reader.java | 36 +++++++++++--- .../exoplayer2/extractor/ts/Id3Reader.java | 6 ++- .../exoplayer2/extractor/ts/LatmReader.java | 35 ++++++++++---- .../extractor/ts/MpegAudioReader.java | 42 ++++++++++------- .../exoplayer2/extractor/ts/PesReader.java | 14 ++++-- .../exoplayer2/extractor/ts/PsExtractor.java | 12 +++-- .../exoplayer2/extractor/ts/SeiReader.java | 4 +- .../extractor/ts/SpliceInfoSectionReader.java | 14 +++++- .../exoplayer2/extractor/ts/TsExtractor.java | 10 ++-- .../extractor/ts/TsPayloadReader.java | 17 ++++--- .../extractor/ts/UserDataReader.java | 3 +- .../exoplayer2/extractor/ts/package-info.java | 19 ++++++++ .../exoplayer2/offline/DownloadHelper.java | 2 +- .../extractor/ts/TsExtractorTest.java | 2 + 23 files changed, 307 insertions(+), 139 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/package-info.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java index 7af9d9f074..f57d3b2895 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java @@ -81,7 +81,10 @@ public final class DtsUtil { * @return The DTS format parsed from data in the header. */ public static Format parseDtsFormat( - byte[] frame, String trackId, @Nullable String language, @Nullable DrmInitData drmInitData) { + byte[] frame, + @Nullable String trackId, + @Nullable String language, + @Nullable DrmInitData drmInitData) { ParsableBitArray frameBits = getNormalizedFrameHeader(frame); frameBits.skipBits(32 + 1 + 5 + 1 + 7 + 14); // SYNC, FTYPE, SHORT, CPF, NBLKS, FSIZE int amode = frameBits.readBits(6); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java index cd07a40c6d..af5efc35a7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; 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.audio.Ac3Util; @@ -23,11 +24,15 @@ import com.google.android.exoplayer2.audio.Ac3Util.SyncFrameInfo; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +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.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous (E-)AC-3 byte stream and extracts individual samples. @@ -47,10 +52,10 @@ public final class Ac3Reader implements ElementaryStreamReader { private final ParsableBitArray headerScratchBits; private final ParsableByteArray headerScratchBytes; - private final String language; + @Nullable private final String language; - private String trackFormatId; - private TrackOutput output; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private TrackOutput output; @State private int state; private int bytesRead; @@ -60,7 +65,7 @@ public final class Ac3Reader implements ElementaryStreamReader { // Used when parsing the header. private long sampleDurationUs; - private Format format; + @MonotonicNonNull private Format format; private int sampleSize; // Used when reading the samples. @@ -78,7 +83,7 @@ public final class Ac3Reader implements ElementaryStreamReader { * * @param language Track language. */ - public Ac3Reader(String language) { + public Ac3Reader(@Nullable String language) { headerScratchBits = new ParsableBitArray(new byte[HEADER_SIZE]); headerScratchBytes = new ParsableByteArray(headerScratchBits.data); state = STATE_FINDING_SYNC; @@ -95,7 +100,7 @@ public final class Ac3Reader implements ElementaryStreamReader { @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) { generator.generateNewId(); - trackFormatId = generator.getFormatId(); + formatId = generator.getFormatId(); output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO); } @@ -106,6 +111,7 @@ public final class Ac3Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SYNC: @@ -185,19 +191,28 @@ public final class Ac3Reader implements ElementaryStreamReader { return false; } - /** - * Parses the sample header. - */ - @SuppressWarnings("ReferenceEquality") + /** Parses the sample header. */ + @RequiresNonNull("output") private void parseHeader() { headerScratchBits.setPosition(0); SyncFrameInfo frameInfo = Ac3Util.parseAc3SyncframeInfo(headerScratchBits); - if (format == null || frameInfo.channelCount != format.channelCount + if (format == null + || frameInfo.channelCount != format.channelCount || frameInfo.sampleRate != format.sampleRate - || frameInfo.mimeType != format.sampleMimeType) { - format = Format.createAudioSampleFormat(trackFormatId, frameInfo.mimeType, null, - Format.NO_VALUE, Format.NO_VALUE, frameInfo.channelCount, frameInfo.sampleRate, null, - null, 0, language); + || Util.areEqual(frameInfo.mimeType, format.sampleMimeType)) { + format = + Format.createAudioSampleFormat( + formatId, + frameInfo.mimeType, + null, + Format.NO_VALUE, + Format.NO_VALUE, + frameInfo.channelCount, + frameInfo.sampleRate, + null, + null, + 0, + language); output.format(format); } sampleSize = frameInfo.frameSize; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java index 48bd07fce4..096eb81119 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; 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.audio.Ac4Util; @@ -23,12 +24,15 @@ import com.google.android.exoplayer2.audio.Ac4Util.SyncFrameInfo; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** Parses a continuous AC-4 byte stream and extracts individual samples. */ public final class Ac4Reader implements ElementaryStreamReader { @@ -44,10 +48,10 @@ public final class Ac4Reader implements ElementaryStreamReader { private final ParsableBitArray headerScratchBits; private final ParsableByteArray headerScratchBytes; - private final String language; + @Nullable private final String language; - private String trackFormatId; - private TrackOutput output; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private TrackOutput output; @State private int state; private int bytesRead; @@ -58,7 +62,7 @@ public final class Ac4Reader implements ElementaryStreamReader { // Used when parsing the header. private long sampleDurationUs; - private Format format; + @MonotonicNonNull private Format format; private int sampleSize; // Used when reading the samples. @@ -74,7 +78,7 @@ public final class Ac4Reader implements ElementaryStreamReader { * * @param language Track language. */ - public Ac4Reader(String language) { + public Ac4Reader(@Nullable String language) { headerScratchBits = new ParsableBitArray(new byte[Ac4Util.HEADER_SIZE_FOR_PARSER]); headerScratchBytes = new ParsableByteArray(headerScratchBits.data); state = STATE_FINDING_SYNC; @@ -95,7 +99,7 @@ public final class Ac4Reader implements ElementaryStreamReader { @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) { generator.generateNewId(); - trackFormatId = generator.getFormatId(); + formatId = generator.getFormatId(); output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO); } @@ -106,6 +110,7 @@ public final class Ac4Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SYNC: @@ -185,7 +190,7 @@ public final class Ac4Reader implements ElementaryStreamReader { } /** Parses the sample header. */ - @SuppressWarnings("ReferenceEquality") + @RequiresNonNull("output") private void parseHeader() { headerScratchBits.setPosition(0); SyncFrameInfo frameInfo = Ac4Util.parseAc4SyncframeInfo(headerScratchBits); @@ -195,7 +200,7 @@ public final class Ac4Reader implements ElementaryStreamReader { || !MimeTypes.AUDIO_AC4.equals(format.sampleMimeType)) { format = Format.createAudioSampleFormat( - trackFormatId, + formatId, MimeTypes.AUDIO_AC4, /* codecs= */ null, /* bitrate= */ Format.NO_VALUE, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java index 589b543170..56ffc4500e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; @@ -23,13 +24,18 @@ import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.Collections; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous ADTS byte stream and extracts individual frames. @@ -62,11 +68,11 @@ public final class AdtsReader implements ElementaryStreamReader { private final boolean exposeId3; private final ParsableBitArray adtsScratch; private final ParsableByteArray id3HeaderBuffer; - private final String language; + @Nullable private final String language; - private String formatId; - private TrackOutput output; - private TrackOutput id3Output; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private TrackOutput output; + @MonotonicNonNull private TrackOutput id3Output; private int state; private int bytesRead; @@ -90,7 +96,7 @@ public final class AdtsReader implements ElementaryStreamReader { // Used when reading the samples. private long timeUs; - private TrackOutput currentOutput; + @MonotonicNonNull private TrackOutput currentOutput; private long currentSampleDuration; /** @@ -104,7 +110,7 @@ public final class AdtsReader implements ElementaryStreamReader { * @param exposeId3 True if the reader should expose ID3 information. * @param language Track language. */ - public AdtsReader(boolean exposeId3, String language) { + public AdtsReader(boolean exposeId3, @Nullable String language) { adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]); id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE)); setFindingSampleState(); @@ -130,6 +136,7 @@ public final class AdtsReader implements ElementaryStreamReader { idGenerator.generateNewId(); formatId = idGenerator.getFormatId(); output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); + currentOutput = output; if (exposeId3) { idGenerator.generateNewId(); id3Output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); @@ -147,6 +154,7 @@ public final class AdtsReader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) throws ParserException { + assertTracksCreated(); while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SAMPLE: @@ -425,9 +433,8 @@ public final class AdtsReader implements ElementaryStreamReader { return true; } - /** - * Parses the Id3 header. - */ + /** Parses the Id3 header. */ + @RequiresNonNull("id3Output") private void parseId3Header() { id3Output.sampleData(id3HeaderBuffer, ID3_HEADER_SIZE); id3HeaderBuffer.setPosition(ID3_SIZE_OFFSET); @@ -435,9 +442,8 @@ public final class AdtsReader implements ElementaryStreamReader { id3HeaderBuffer.readSynchSafeInt() + ID3_HEADER_SIZE); } - /** - * Parses the sample header. - */ + /** Parses the sample header. */ + @RequiresNonNull("output") private void parseAdtsHeader() throws ParserException { adtsScratch.setPosition(0); @@ -487,9 +493,8 @@ public final class AdtsReader implements ElementaryStreamReader { setReadingSampleState(output, sampleDurationUs, 0, sampleSize); } - /** - * Reads the rest of the sample - */ + /** Reads the rest of the sample */ + @RequiresNonNull("currentOutput") private void readSample(ParsableByteArray data) { int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); currentOutput.sampleData(data, bytesToRead); @@ -501,4 +506,10 @@ public final class AdtsReader implements ElementaryStreamReader { } } + @EnsuresNonNull({"output", "currentOutput", "id3Output"}) + private void assertTracksCreated() { + Assertions.checkNotNull(output); + Util.castNonNull(currentOutput); + Util.castNonNull(id3Output); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 24d17f4956..480edb0a19 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.SparseArray; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; import com.google.android.exoplayer2.text.cea.Cea708InitializationData; @@ -134,6 +135,7 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact return new SparseArray<>(); } + @Nullable @Override public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) { switch (streamType) { @@ -247,7 +249,7 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact // Skip reserved (8). scratchDescriptorData.skipBytes(1); - List initializationData = null; + @Nullable List initializationData = null; // The wide_aspect_ratio flag only has meaning for CEA-708. if (isDigital) { boolean isWideAspectRatio = (flags & 0x40) != 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java index 1f9b0e79d4..127405d661 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java @@ -15,13 +15,17 @@ */ package com.google.android.exoplayer2.extractor.ts; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.DtsUtil; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous DTS byte stream and extracts individual samples. @@ -35,10 +39,10 @@ public final class DtsReader implements ElementaryStreamReader { private static final int HEADER_SIZE = 18; private final ParsableByteArray headerScratchBytes; - private final String language; + @Nullable private final String language; - private String formatId; - private TrackOutput output; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private TrackOutput output; private int state; private int bytesRead; @@ -48,7 +52,7 @@ public final class DtsReader implements ElementaryStreamReader { // Used when parsing the header. private long sampleDurationUs; - private Format format; + @MonotonicNonNull private Format format; private int sampleSize; // Used when reading the samples. @@ -59,7 +63,7 @@ public final class DtsReader implements ElementaryStreamReader { * * @param language Track language. */ - public DtsReader(String language) { + public DtsReader(@Nullable String language) { headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]); state = STATE_FINDING_SYNC; this.language = language; @@ -86,6 +90,7 @@ public final class DtsReader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SYNC: @@ -162,9 +167,8 @@ public final class DtsReader implements ElementaryStreamReader { return false; } - /** - * Parses the sample header. - */ + /** Parses the sample header. */ + @RequiresNonNull("output") private void parseHeader() { byte[] frameData = headerScratchBytes.data; if (format == null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java index 3f0a772b1c..146f663bfd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java @@ -64,12 +64,12 @@ public final class DvbSubtitleReader implements ElementaryStreamReader { Format.createImageSampleFormat( idGenerator.getFormatId(), MimeTypes.APPLICATION_DVBSUBS, - null, + /* codecs= */ null, Format.NO_VALUE, - 0, + /* selectionFlags= */ 0, Collections.singletonList(subtitleInfo.initializationData), subtitleInfo.language, - null)); + /* drmInitData= */ null)); outputs[i] = output; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java index e7f2c1935b..4d2018ef86 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -16,16 +16,20 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.Collections; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Parses a continuous H262 byte stream and extracts individual frames. @@ -38,27 +42,27 @@ public final class H262Reader implements ElementaryStreamReader { private static final int START_GROUP = 0xB8; private static final int START_USER_DATA = 0xB2; - private String formatId; - private TrackOutput output; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private TrackOutput output; // Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4. private static final double[] FRAME_RATE_VALUES = new double[] { 24000d / 1001, 24, 25, 30000d / 1001, 30, 50, 60000d / 1001, 60}; + @Nullable private final UserDataReader userDataReader; + @Nullable private final ParsableByteArray userDataParsable; + + // State that should be reset on seek. + @Nullable private final NalUnitTargetBuffer userData; + private final boolean[] prefixFlags; + private final CsdBuffer csdBuffer; + private long totalBytesWritten; + private boolean startedFirstSample; + // State that should not be reset on seek. private boolean hasOutputFormat; private long frameDurationUs; - private final UserDataReader userDataReader; - private final ParsableByteArray userDataParsable; - - // State that should be reset on seek. - private final boolean[] prefixFlags; - private final CsdBuffer csdBuffer; - private final NalUnitTargetBuffer userData; - private long totalBytesWritten; - private boolean startedFirstSample; - // Per packet state that gets reset at the start of each packet. private long pesTimeUs; @@ -72,7 +76,7 @@ public final class H262Reader implements ElementaryStreamReader { this(null); } - /* package */ H262Reader(UserDataReader userDataReader) { + /* package */ H262Reader(@Nullable UserDataReader userDataReader) { this.userDataReader = userDataReader; prefixFlags = new boolean[4]; csdBuffer = new CsdBuffer(128); @@ -89,7 +93,7 @@ public final class H262Reader implements ElementaryStreamReader { public void seek() { NalUnitUtil.clearPrefixFlags(prefixFlags); csdBuffer.reset(); - if (userDataReader != null) { + if (userData != null) { userData.reset(); } totalBytesWritten = 0; @@ -114,6 +118,7 @@ public final class H262Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. int offset = data.getPosition(); int limit = data.limit(); byte[] dataArray = data.data; @@ -130,7 +135,7 @@ public final class H262Reader implements ElementaryStreamReader { if (!hasOutputFormat) { csdBuffer.onData(dataArray, offset, limit); } - if (userDataReader != null) { + if (userData != null) { userData.appendToNalUnit(dataArray, offset, limit); } return; @@ -157,7 +162,7 @@ public final class H262Reader implements ElementaryStreamReader { hasOutputFormat = true; } } - if (userDataReader != null) { + if (userData != null) { int bytesAlreadyPassed = 0; if (lengthToStartCode > 0) { userData.appendToNalUnit(dataArray, offset, startCodeOffset); @@ -167,8 +172,8 @@ public final class H262Reader implements ElementaryStreamReader { if (userData.endNalUnit(bytesAlreadyPassed)) { int unescapedLength = NalUnitUtil.unescapeStream(userData.nalData, userData.nalLength); - userDataParsable.reset(userData.nalData, unescapedLength); - userDataReader.consume(sampleTimeUs, userDataParsable); + Util.castNonNull(userDataParsable).reset(userData.nalData, unescapedLength); + Util.castNonNull(userDataReader).consume(sampleTimeUs, userDataParsable); } if (startCodeValue == START_USER_DATA && data.data[startCodeOffset + 2] == 0x1) { @@ -211,10 +216,10 @@ public final class H262Reader implements ElementaryStreamReader { * * @param csdBuffer The csd buffer. * @param formatId The id for the generated format. May be null. - * @return A pair consisting of the {@link Format} and the frame duration in microseconds, or - * 0 if the duration could not be determined. + * @return A pair consisting of the {@link Format} and the frame duration in microseconds, or 0 if + * the duration could not be determined. */ - private static Pair parseCsdBuffer(CsdBuffer csdBuffer, String formatId) { + private static Pair parseCsdBuffer(CsdBuffer csdBuffer, @Nullable String formatId) { byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length); int firstByte = csdData[4] & 0xFF; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java index d249c1b9da..011b3fd7b2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java @@ -23,15 +23,21 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.NalUnitUtil.SpsData; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; +import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous H264 byte stream and extracts individual frames. @@ -51,9 +57,9 @@ public final class H264Reader implements ElementaryStreamReader { private long totalBytesWritten; private final boolean[] prefixFlags; - private String formatId; - private TrackOutput output; - private SampleReader sampleReader; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private TrackOutput output; + @MonotonicNonNull private SampleReader sampleReader; // State that should not be reset on seek. private boolean hasOutputFormat; @@ -87,13 +93,15 @@ public final class H264Reader implements ElementaryStreamReader { @Override public void seek() { + totalBytesWritten = 0; + randomAccessIndicator = false; NalUnitUtil.clearPrefixFlags(prefixFlags); sps.reset(); pps.reset(); sei.reset(); - sampleReader.reset(); - totalBytesWritten = 0; - randomAccessIndicator = false; + if (sampleReader != null) { + sampleReader.reset(); + } } @Override @@ -113,6 +121,8 @@ public final class H264Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + assertTracksCreated(); + int offset = data.getPosition(); int limit = data.limit(); byte[] dataArray = data.data; @@ -159,6 +169,7 @@ public final class H264Reader implements ElementaryStreamReader { // Do nothing. } + @RequiresNonNull("sampleReader") private void startNalUnit(long position, int nalUnitType, long pesTimeUs) { if (!hasOutputFormat || sampleReader.needsSpsPps()) { sps.startNalUnit(nalUnitType); @@ -168,6 +179,7 @@ public final class H264Reader implements ElementaryStreamReader { sampleReader.startNalUnit(position, nalUnitType, pesTimeUs); } + @RequiresNonNull("sampleReader") private void nalUnitData(byte[] dataArray, int offset, int limit) { if (!hasOutputFormat || sampleReader.needsSpsPps()) { sps.appendToNalUnit(dataArray, offset, limit); @@ -177,6 +189,7 @@ public final class H264Reader implements ElementaryStreamReader { sampleReader.appendToNalUnit(dataArray, offset, limit); } + @RequiresNonNull({"output", "sampleReader"}) private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) { if (!hasOutputFormat || sampleReader.needsSpsPps()) { sps.endNalUnit(discardPadding); @@ -237,6 +250,12 @@ public final class H264Reader implements ElementaryStreamReader { } } + @EnsuresNonNull({"output", "sampleReader"}) + private void assertTracksCreated() { + Assertions.checkStateNotNull(output); + Util.castNonNull(sampleReader); + } + /** Consumes a stream of NAL units and outputs samples. */ private static final class SampleReader { @@ -478,7 +497,7 @@ public final class H264Reader implements ElementaryStreamReader { private boolean isComplete; private boolean hasSliceType; - private SpsData spsData; + @Nullable private SpsData spsData; private int nalRefIdc; private int sliceType; private int frameNum; @@ -542,6 +561,8 @@ public final class H264Reader implements ElementaryStreamReader { private boolean isFirstVclNalUnitOfPicture(SliceHeaderData other) { // See ISO 14496-10 subsection 7.4.1.2.4. + SpsData spsData = Assertions.checkStateNotNull(this.spsData); + SpsData otherSpsData = Assertions.checkStateNotNull(other.spsData); return isComplete && (!other.isComplete || frameNum != other.frameNum @@ -552,15 +573,15 @@ public final class H264Reader implements ElementaryStreamReader { && bottomFieldFlag != other.bottomFieldFlag) || (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0)) || (spsData.picOrderCountType == 0 - && other.spsData.picOrderCountType == 0 + && otherSpsData.picOrderCountType == 0 && (picOrderCntLsb != other.picOrderCntLsb || deltaPicOrderCntBottom != other.deltaPicOrderCntBottom)) || (spsData.picOrderCountType == 1 - && other.spsData.picOrderCountType == 1 + && otherSpsData.picOrderCountType == 1 && (deltaPicOrderCnt0 != other.deltaPicOrderCnt0 || deltaPicOrderCnt1 != other.deltaPicOrderCnt1)) || idrPicFlag != other.idrPicFlag - || (idrPicFlag && other.idrPicFlag && idrPicId != other.idrPicId)); + || (idrPicFlag && idrPicId != other.idrPicId)); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index 88bde53746..c86cf51866 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -20,12 +20,18 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; +import com.google.android.exoplayer2.util.Util; import java.util.Collections; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous H.265 byte stream and extracts individual frames. @@ -46,9 +52,9 @@ public final class H265Reader implements ElementaryStreamReader { private final SeiReader seiReader; - private String formatId; - private TrackOutput output; - private SampleReader sampleReader; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private TrackOutput output; + @MonotonicNonNull private SampleReader sampleReader; // State that should not be reset on seek. private boolean hasOutputFormat; @@ -84,14 +90,16 @@ public final class H265Reader implements ElementaryStreamReader { @Override public void seek() { + totalBytesWritten = 0; NalUnitUtil.clearPrefixFlags(prefixFlags); vps.reset(); sps.reset(); pps.reset(); prefixSei.reset(); suffixSei.reset(); - sampleReader.reset(); - totalBytesWritten = 0; + if (sampleReader != null) { + sampleReader.reset(); + } } @Override @@ -111,6 +119,8 @@ public final class H265Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + assertTracksCreated(); + while (data.bytesLeft() > 0) { int offset = data.getPosition(); int limit = data.limit(); @@ -160,6 +170,7 @@ public final class H265Reader implements ElementaryStreamReader { // Do nothing. } + @RequiresNonNull("sampleReader") private void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) { if (hasOutputFormat) { sampleReader.startNalUnit(position, offset, nalUnitType, pesTimeUs); @@ -172,6 +183,7 @@ public final class H265Reader implements ElementaryStreamReader { suffixSei.startNalUnit(nalUnitType); } + @RequiresNonNull("sampleReader") private void nalUnitData(byte[] dataArray, int offset, int limit) { if (hasOutputFormat) { sampleReader.readNalUnitData(dataArray, offset, limit); @@ -184,6 +196,7 @@ public final class H265Reader implements ElementaryStreamReader { suffixSei.appendToNalUnit(dataArray, offset, limit); } + @RequiresNonNull({"output", "sampleReader"}) private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) { if (hasOutputFormat) { sampleReader.endNalUnit(position, offset); @@ -214,8 +227,11 @@ public final class H265Reader implements ElementaryStreamReader { } } - private static Format parseMediaFormat(String formatId, NalUnitTargetBuffer vps, - NalUnitTargetBuffer sps, NalUnitTargetBuffer pps) { + private static Format parseMediaFormat( + @Nullable String formatId, + NalUnitTargetBuffer vps, + NalUnitTargetBuffer sps, + NalUnitTargetBuffer pps) { // Build codec-specific data. byte[] csd = new byte[vps.nalLength + sps.nalLength + pps.nalLength]; System.arraycopy(vps.nalData, 0, csd, 0, vps.nalLength); @@ -389,6 +405,12 @@ public final class H265Reader implements ElementaryStreamReader { } } + @EnsuresNonNull({"output", "sampleReader"}) + private void assertTracksCreated() { + Assertions.checkStateNotNull(output); + Util.castNonNull(sampleReader); + } + private static final class SampleReader { /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java index 77ec48d0a7..615d2f8c2e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java @@ -23,9 +23,11 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Parses ID3 data and extracts individual text information frames. @@ -36,7 +38,7 @@ public final class Id3Reader implements ElementaryStreamReader { private final ParsableByteArray id3Header; - private TrackOutput output; + @MonotonicNonNull private TrackOutput output; // State that should be reset on seek. private boolean writingSample; @@ -76,6 +78,7 @@ public final class Id3Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. if (!writingSample) { return; } @@ -106,6 +109,7 @@ public final class Id3Reader implements ElementaryStreamReader { @Override public void packetFinished() { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. if (!writingSample || sampleSize == 0 || sampleBytesRead != sampleSize) { return; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java index 4ad9adfa2a..1c8131feaa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java @@ -23,11 +23,14 @@ import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.Collections; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses and extracts samples from an AAC/LATM elementary stream. @@ -43,14 +46,14 @@ public final class LatmReader implements ElementaryStreamReader { private static final int SYNC_BYTE_FIRST = 0x56; private static final int SYNC_BYTE_SECOND = 0xE0; - private final String language; + @Nullable private final String language; private final ParsableByteArray sampleDataBuffer; private final ParsableBitArray sampleBitArray; // Track output info. - private TrackOutput output; - private Format format; - private String formatId; + @MonotonicNonNull private TrackOutput output; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private Format format; // Parser state info. private int state; @@ -99,6 +102,7 @@ public final class LatmReader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) throws ParserException { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. int bytesToRead; while (data.bytesLeft() > 0) { switch (state) { @@ -150,6 +154,7 @@ public final class LatmReader implements ElementaryStreamReader { * * @param data A {@link ParsableBitArray} containing the AudioMuxElement's bytes. */ + @RequiresNonNull("output") private void parseAudioMuxElement(ParsableBitArray data) throws ParserException { boolean useSameStreamMux = data.readBit(); if (!useSameStreamMux) { @@ -173,9 +178,8 @@ public final class LatmReader implements ElementaryStreamReader { } } - /** - * Parses a StreamMuxConfig as defined in ISO/IEC 14496-3:2009 Section 1.7.3.1, Table 1.42. - */ + /** Parses a StreamMuxConfig as defined in ISO/IEC 14496-3:2009 Section 1.7.3.1, Table 1.42. */ + @RequiresNonNull("output") private void parseStreamMuxConfig(ParsableBitArray data) throws ParserException { int audioMuxVersion = data.readBits(1); audioMuxVersionA = audioMuxVersion == 1 ? data.readBits(1) : 0; @@ -198,9 +202,19 @@ public final class LatmReader implements ElementaryStreamReader { data.setPosition(startPosition); byte[] initData = new byte[(readBits + 7) / 8]; data.readBits(initData, 0, readBits); - Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null, - Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRateHz, - Collections.singletonList(initData), null, 0, language); + Format format = + Format.createAudioSampleFormat( + formatId, + MimeTypes.AUDIO_AAC, + /* codecs= */ null, + Format.NO_VALUE, + Format.NO_VALUE, + channelCount, + sampleRateHz, + Collections.singletonList(initData), + /* drmInitData= */ null, + /* selectionFlags= */ 0, + language); if (!format.equals(this.format)) { this.format = format; sampleDurationUs = (C.MICROS_PER_SECOND * 1024) / format.sampleRate; @@ -280,6 +294,7 @@ public final class LatmReader implements ElementaryStreamReader { } } + @RequiresNonNull("output") private void parsePayloadMux(ParsableBitArray data, int muxLengthBytes) { // The start of sample data in int bitPosition = data.getPosition(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java index 393e297818..5f41a23246 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java @@ -21,7 +21,11 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous MPEG Audio byte stream and extracts individual frames. @@ -36,10 +40,10 @@ public final class MpegAudioReader implements ElementaryStreamReader { private final ParsableByteArray headerScratch; private final MpegAudioHeader header; - private final String language; + @Nullable private final String language; - private String formatId; - private TrackOutput output; + @MonotonicNonNull private TrackOutput output; + @MonotonicNonNull private String formatId; private int state; private int frameBytesRead; @@ -59,7 +63,7 @@ public final class MpegAudioReader implements ElementaryStreamReader { this(null); } - public MpegAudioReader(String language) { + public MpegAudioReader(@Nullable String language) { state = STATE_FINDING_HEADER; // The first byte of an MPEG Audio frame header is always 0xFF. headerScratch = new ParsableByteArray(4); @@ -89,6 +93,7 @@ public final class MpegAudioReader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_HEADER: @@ -146,20 +151,21 @@ public final class MpegAudioReader implements ElementaryStreamReader { /** * Attempts to read the remaining two bytes of the frame header. - *

    - * If a frame header is read in full then the state is changed to {@link #STATE_READING_FRAME}, + * + *

    If a frame header is read in full then the state is changed to {@link #STATE_READING_FRAME}, * the media format is output if this has not previously occurred, the four header bytes are * output as sample data, and the position of the source is advanced to the byte that immediately * follows the header. - *

    - * If a frame header is read in full but cannot be parsed then the state is changed to - * {@link #STATE_READING_HEADER}. - *

    - * If a frame header is not read in full then the position of the source is advanced to the limit, - * and the method should be called again with the next source to continue the read. + * + *

    If a frame header is read in full but cannot be parsed then the state is changed to {@link + * #STATE_READING_HEADER}. + * + *

    If a frame header is not read in full then the position of the source is advanced to the + * limit, and the method should be called again with the next source to continue the read. * * @param source The source from which to read. */ + @RequiresNonNull("output") private void readHeaderRemainder(ParsableByteArray source) { int bytesToRead = Math.min(source.bytesLeft(), HEADER_SIZE - frameBytesRead); source.readBytes(headerScratch.data, frameBytesRead, bytesToRead); @@ -195,16 +201,17 @@ public final class MpegAudioReader implements ElementaryStreamReader { /** * Attempts to read the remainder of the frame. - *

    - * If a frame is read in full then true is returned. The frame will have been output, and the + * + *

    If a frame is read in full then true is returned. The frame will have been output, and the * position of the source will have been advanced to the byte that immediately follows the end of * the frame. - *

    - * If a frame is not read in full then the position of the source will have been advanced to the - * limit, and the method should be called again with the next source to continue the read. + * + *

    If a frame is not read in full then the position of the source will have been advanced to + * the limit, and the method should be called again with the next source to continue the read. * * @param source The source from which to read. */ + @RequiresNonNull("output") private void readFrameRemainder(ParsableByteArray source) { int bytesToRead = Math.min(source.bytesLeft(), frameSize - frameBytesRead); output.sampleData(source, bytesToRead); @@ -219,5 +226,4 @@ public final class MpegAudioReader implements ElementaryStreamReader { frameBytesRead = 0; state = STATE_FINDING_HEADER; } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java index ff755f4ece..d5d32a6d96 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java @@ -15,13 +15,17 @@ */ package com.google.android.exoplayer2.extractor.ts; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses PES packet data and extracts samples. @@ -45,7 +49,7 @@ public final class PesReader implements TsPayloadReader { private int state; private int bytesRead; - private TimestampAdjuster timestampAdjuster; + @MonotonicNonNull private TimestampAdjuster timestampAdjuster; private boolean ptsFlag; private boolean dtsFlag; private boolean seenFirstDts; @@ -79,6 +83,8 @@ public final class PesReader implements TsPayloadReader { @Override public final void consume(ParsableByteArray data, @Flags int flags) throws ParserException { + Assertions.checkStateNotNull(timestampAdjuster); // Asserts init has been called. + if ((flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0) { switch (state) { case STATE_FINDING_HEADER: @@ -119,7 +125,7 @@ public final class PesReader implements TsPayloadReader { int readLength = Math.min(MAX_HEADER_EXTENSION_SIZE, extendedHeaderLength); // Read as much of the extended header as we're interested in, and skip the rest. if (continueRead(data, pesScratch.data, readLength) - && continueRead(data, null, extendedHeaderLength)) { + && continueRead(data, /* target= */ null, extendedHeaderLength)) { parseHeaderExtension(); flags |= dataAlignmentIndicator ? FLAG_DATA_ALIGNMENT_INDICATOR : 0; reader.packetStarted(timeUs, flags); @@ -162,7 +168,8 @@ public final class PesReader implements TsPayloadReader { * @param targetLength The target length of the read. * @return Whether the target length has been reached. */ - private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { + private boolean continueRead( + ParsableByteArray source, @Nullable byte[] target, int targetLength) { int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); if (bytesToRead <= 0) { return true; @@ -207,6 +214,7 @@ public final class PesReader implements TsPayloadReader { return true; } + @RequiresNonNull("timestampAdjuster") private void parseHeaderExtension() { pesScratch.setPosition(0); timeUs = C.TIME_UNSET; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java index fec108fd5f..3f10a454fc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.SparseArray; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.Extractor; @@ -25,10 +26,13 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +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.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Extracts data from the MPEG-2 PS container format. @@ -67,8 +71,8 @@ public final class PsExtractor implements Extractor { private long lastTrackPosition; // Accessed only by the loading thread. - private PsBinarySearchSeeker psBinarySearchSeeker; - private ExtractorOutput output; + @Nullable private PsBinarySearchSeeker psBinarySearchSeeker; + @MonotonicNonNull private ExtractorOutput output; private boolean hasOutputSeekMap; public PsExtractor() { @@ -160,6 +164,7 @@ public final class PsExtractor implements Extractor { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + Assertions.checkStateNotNull(output); // Asserts init has been called. long inputLength = input.getLength(); boolean canReadDuration = inputLength != C.LENGTH_UNSET; @@ -221,7 +226,7 @@ public final class PsExtractor implements Extractor { PesReader payloadReader = psPayloadReaders.get(streamId); if (!foundAllTracks) { if (payloadReader == null) { - ElementaryStreamReader elementaryStreamReader = null; + @Nullable ElementaryStreamReader elementaryStreamReader = null; if (streamId == PRIVATE_STREAM_1) { // Private stream, used for AC3 audio. // NOTE: This may need further parsing to determine if its DTS, but that's likely only @@ -278,6 +283,7 @@ public final class PsExtractor implements Extractor { // Internals. + @RequiresNonNull("output") private void maybeOutputSeekMap(long inputLength) { if (!hasOutputSeekMap) { hasOutputSeekMap = true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java index d032ef5883..2541db07a8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ts; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -45,7 +46,7 @@ public final class SeiReader { idGenerator.generateNewId(); TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); Format channelFormat = closedCaptionFormats.get(i); - String channelMimeType = channelFormat.sampleMimeType; + @Nullable String channelMimeType = channelFormat.sampleMimeType; Assertions.checkArgument(MimeTypes.APPLICATION_CEA608.equals(channelMimeType) || MimeTypes.APPLICATION_CEA708.equals(channelMimeType), "Invalid closed caption mime type provided: " + channelMimeType); @@ -69,5 +70,4 @@ public final class SeiReader { public void consume(long pesTimeUs, ParsableByteArray seiBuffer) { CeaUtil.consume(pesTimeUs, seiBuffer, outputs); } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java index 27838d4c25..6747a04916 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java @@ -19,17 +19,21 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; +import com.google.android.exoplayer2.util.Util; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Parses splice info sections as defined by SCTE35. */ public final class SpliceInfoSectionReader implements SectionPayloadReader { - private TimestampAdjuster timestampAdjuster; - private TrackOutput output; + @MonotonicNonNull private TimestampAdjuster timestampAdjuster; + @MonotonicNonNull private TrackOutput output; private boolean formatDeclared; @Override @@ -44,6 +48,7 @@ public final class SpliceInfoSectionReader implements SectionPayloadReader { @Override public void consume(ParsableByteArray sectionData) { + assertInitialized(); if (!formatDeclared) { if (timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET) { // There is not enough information to initialize the timestamp adjuster. @@ -59,4 +64,9 @@ public final class SpliceInfoSectionReader implements SectionPayloadReader { sampleSize, 0, null); } + @EnsuresNonNull({"timestampAdjuster", "output"}) + private void assertInitialized() { + Assertions.checkStateNotNull(timestampAdjuster); + Util.castNonNull(output); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 2cd7398d7c..35e8806a6f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -21,6 +21,7 @@ import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.Extractor; @@ -587,8 +588,11 @@ public final class TsExtractor implements Extractor { continue; } - TsPayloadReader reader = mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3 ? id3Reader - : payloadReaderFactory.createPayloadReader(streamType, esInfo); + @Nullable + TsPayloadReader reader = + mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3 + ? id3Reader + : payloadReaderFactory.createPayloadReader(streamType, esInfo); if (mode != MODE_HLS || elementaryPid < trackIdToPidScratch.get(trackId, MAX_PID_PLUS_ONE)) { trackIdToPidScratch.put(trackId, elementaryPid); @@ -602,7 +606,7 @@ public final class TsExtractor implements Extractor { int trackPid = trackIdToPidScratch.valueAt(i); trackIds.put(trackId, true); trackPids.put(trackPid, true); - TsPayloadReader reader = trackIdToReaderScratch.valueAt(i); + @Nullable TsPayloadReader reader = trackIdToReaderScratch.valueAt(i); if (reader != null) { if (reader != id3Reader) { reader.init(timestampAdjuster, output, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java index af27235257..03ed10ff0d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.SparseArray; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; @@ -53,11 +54,11 @@ public interface TsPayloadReader { * * @param streamType Stream type value as defined in the PMT entry or associated descriptors. * @param esInfo Information associated to the elementary stream provided in the PMT. - * @return A {@link TsPayloadReader} for the packet stream carried by the provided pid. + * @return A {@link TsPayloadReader} for the packet stream carried by the provided pid, or * {@code null} if the stream is not supported. */ + @Nullable TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo); - } /** @@ -66,18 +67,21 @@ public interface TsPayloadReader { final class EsInfo { public final int streamType; - public final String language; + @Nullable public final String language; public final List dvbSubtitleInfos; public final byte[] descriptorBytes; /** - * @param streamType The type of the stream as defined by the - * {@link TsExtractor}{@code .TS_STREAM_TYPE_*}. + * @param streamType The type of the stream as defined by the {@link TsExtractor}{@code + * .TS_STREAM_TYPE_*}. * @param language The language of the stream, as defined by ISO/IEC 13818-1, section 2.6.18. * @param dvbSubtitleInfos Information about DVB subtitles associated to the stream. * @param descriptorBytes The descriptor bytes associated to the stream. */ - public EsInfo(int streamType, String language, List dvbSubtitleInfos, + public EsInfo( + int streamType, + @Nullable String language, + @Nullable List dvbSubtitleInfos, byte[] descriptorBytes) { this.streamType = streamType; this.language = language; @@ -134,6 +138,7 @@ public interface TsPayloadReader { this.firstTrackId = firstTrackId; this.trackIdIncrement = trackIdIncrement; trackId = ID_UNSET; + formatId = ""; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java index 724eba1d9a..739e5341b8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ts; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -44,7 +45,7 @@ import java.util.List; idGenerator.generateNewId(); TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); Format channelFormat = closedCaptionFormats.get(i); - String channelMimeType = channelFormat.sampleMimeType; + @Nullable String channelMimeType = channelFormat.sampleMimeType; Assertions.checkArgument( MimeTypes.APPLICATION_CEA608.equals(channelMimeType) || MimeTypes.APPLICATION_CEA708.equals(channelMimeType), diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/package-info.java new file mode 100644 index 0000000000..4d93bd5ac5 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.extractor.ts; + +import com.google.android.exoplayer2.util.NonNullApi; 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 ff95afb1f6..1641b2aef6 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 @@ -773,7 +773,7 @@ public final class DownloadHelper { } // Initialization of array of Lists. - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "rawtypes"}) private void onMediaPrepared() { Assertions.checkNotNull(mediaPreparer); Assertions.checkNotNull(mediaPreparer.mediaPeriods); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index f1b962a712..93e3f30d0c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts; import static com.google.common.truth.Truth.assertThat; import android.util.SparseArray; +import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; @@ -172,6 +173,7 @@ public final class TsExtractorTest { } } + @Nullable @Override public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) { if (provideCustomEsReader && streamType == 3) { From 70fe6b45909dabc4daf3a57f1d0306feee68957c Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 8 Jan 2020 09:10:41 +0000 Subject: [PATCH 0612/1335] Upgrade OkHttp library to fix HTTP2 issue Issue: #4078 PiperOrigin-RevId: 288651166 --- RELEASENOTES.md | 3 +++ extensions/okhttp/build.gradle | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5f411b7100..d5dd9ddd0e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -49,6 +49,9 @@ later). * Parse `text-combine-upright` CSS property (i.e. tate-chu-yoko) in WebVTT subtitles (rendering is coming later). +* OkHttp extension: Upgrade OkHttp dependency to 3.12.7, which fixes a class of + `SocketTimeoutException` issues when using HTTP/2 + ([#4078](https://github.com/google/ExoPlayer/issues/4078)). ### 2.11.1 (2019-12-20) ### diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index bde4e127df..2b4b4854c3 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -41,7 +41,7 @@ dependencies { // https://cashapp.github.io/2019-02-05/okhttp-3-13-requires-android-5 // Since OkHttp is distributed as a jar rather than an aar, Gradle won't // stop us from making this mistake! - api 'com.squareup.okhttp3:okhttp:3.12.5' + api 'com.squareup.okhttp3:okhttp:3.12.7' } ext { From ee091e6a45854c0adbeda926c0bf52201b61f747 Mon Sep 17 00:00:00 2001 From: kimvde Date: Wed, 8 Jan 2020 11:48:58 +0000 Subject: [PATCH 0613/1335] Use FlacLibrary.isAvailable in FlacExtractor selection PiperOrigin-RevId: 288667790 --- .../flac/DefaultExtractorsFactoryTest.java | 76 ------------------- library/core/proguard-rules.txt | 6 ++ .../extractor/DefaultExtractorsFactory.java | 16 +++- 3 files changed, 18 insertions(+), 80 deletions(-) delete mode 100644 extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java diff --git a/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java deleted file mode 100644 index 611197bbe5..0000000000 --- a/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java +++ /dev/null @@ -1,76 +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.ext.flac; - -import static com.google.common.truth.Truth.assertThat; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.extractor.amr.AmrExtractor; -import com.google.android.exoplayer2.extractor.flv.FlvExtractor; -import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; -import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; -import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; -import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; -import com.google.android.exoplayer2.extractor.ogg.OggExtractor; -import com.google.android.exoplayer2.extractor.ts.Ac3Extractor; -import com.google.android.exoplayer2.extractor.ts.Ac4Extractor; -import com.google.android.exoplayer2.extractor.ts.AdtsExtractor; -import com.google.android.exoplayer2.extractor.ts.PsExtractor; -import com.google.android.exoplayer2.extractor.ts.TsExtractor; -import com.google.android.exoplayer2.extractor.wav.WavExtractor; -import java.util.ArrayList; -import java.util.List; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit test for {@link DefaultExtractorsFactory}. */ -@RunWith(AndroidJUnit4.class) -public final class DefaultExtractorsFactoryTest { - - @Test - public void testCreateExtractors_returnExpectedClasses() { - DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory(); - - Extractor[] extractors = defaultExtractorsFactory.createExtractors(); - List> listCreatedExtractorClasses = new ArrayList<>(); - for (Extractor extractor : extractors) { - listCreatedExtractorClasses.add(extractor.getClass()); - } - - Class[] expectedExtractorClassses = - new Class[] { - MatroskaExtractor.class, - FragmentedMp4Extractor.class, - Mp4Extractor.class, - Mp3Extractor.class, - AdtsExtractor.class, - Ac3Extractor.class, - Ac4Extractor.class, - TsExtractor.class, - FlvExtractor.class, - OggExtractor.class, - PsExtractor.class, - WavExtractor.class, - AmrExtractor.class, - FlacExtractor.class - }; - - assertThat(listCreatedExtractorClasses).containsNoDuplicates(); - assertThat(listCreatedExtractorClasses).containsExactlyElementsIn(expectedExtractorClassses); - } -} diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index fd4e196945..ff59046049 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -5,6 +5,12 @@ public static android.net.Uri buildRawResourceUri(int); } +# Methods accessed via reflection in DefaultExtractorsFactory +-dontnote com.google.android.exoplayer2.ext.flac.FlacLibrary +-keepclassmembers class com.google.android.exoplayer2.ext.flac.FlacLibrary { + public static boolean isAvailable(); +} + # Some members of this class are being accessed from native methods. Keep them unobfuscated. -keep class com.google.android.exoplayer2.video.VideoDecoderOutputBuffer { *; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java index 1f7b6f7098..cdbd37493b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -64,10 +64,18 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { @Nullable Constructor flacExtensionExtractorConstructor = null; try { // LINT.IfChange - flacExtensionExtractorConstructor = - Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor") - .asSubclass(Extractor.class) - .getConstructor(); + @SuppressWarnings("nullness:argument.type.incompatible") + boolean isFlacNativeLibraryAvailable = + Boolean.TRUE.equals( + Class.forName("com.google.android.exoplayer2.ext.flac.FlacLibrary") + .getMethod("isAvailable") + .invoke(/* obj= */ null)); + if (isFlacNativeLibraryAvailable) { + flacExtensionExtractorConstructor = + Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor") + .asSubclass(Extractor.class) + .getConstructor(); + } // LINT.ThenChange(../../../../../../../../proguard-rules.txt) } catch (ClassNotFoundException e) { // Expected if the app was built without the FLAC extension. From 448db89446bf91b63c5351b77c7f579a65bd0447 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 8 Jan 2020 15:04:09 +0000 Subject: [PATCH 0614/1335] Add TypefaceSpan and hasNoFooSpanBetween() support to SpannedSubject Use these to migrate the last WebvttDecoderTest method to SpannedSubject PiperOrigin-RevId: 288688620 --- .../assets/webvtt/with_css_complex_selectors | 10 +- .../text/webvtt/WebvttDecoderTest.java | 67 ++-- .../testutil/truth/SpannedSubject.java | 206 ++++++++++- .../testutil/truth/SpannedSubjectTest.java | 334 ++++++++++++++++++ 4 files changed, 570 insertions(+), 47 deletions(-) diff --git a/library/core/src/test/assets/webvtt/with_css_complex_selectors b/library/core/src/test/assets/webvtt/with_css_complex_selectors index 62e3348ae9..64d2a516d1 100644 --- a/library/core/src/test/assets/webvtt/with_css_complex_selectors +++ b/library/core/src/test/assets/webvtt/with_css_complex_selectors @@ -20,7 +20,7 @@ STYLE id 00:00.000 --> 00:01.001 -This should be underlined and courier and violet. +This should be underlined and courier and violet. íd 00:02.000 --> 00:02.001 @@ -31,10 +31,10 @@ _id This should be courier and bold. 00:04.000 --> 00:04.001 -This shouldn't be bold. -This should be bold. +This shouldn't be bold. +This should be bold. anId 00:05.000 --> 00:05.001 -This is specific - But this is more italic +This is specific +But this is more italic diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index e07c412fd7..97e3a8f7d5 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -19,17 +19,14 @@ import static com.google.android.exoplayer2.testutil.truth.SpannedSubject.assert import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; -import android.graphics.Typeface; import android.text.Layout.Alignment; import android.text.Spanned; -import android.text.style.ForegroundColorSpan; -import android.text.style.StyleSpan; -import android.text.style.TypefaceSpan; 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.SubtitleDecoderException; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ColorParser; import com.google.common.collect.Iterables; import com.google.common.truth.Expect; @@ -328,41 +325,39 @@ public class WebvttDecoderTest { @Test public void testWithComplexCssSelectors() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_COMPLEX_SELECTORS); - Spanned text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 0); - assertThat(text.getSpans(/* start= */ 30, text.length(), ForegroundColorSpan.class)) - .hasLength(1); - assertThat( - text.getSpans(/* start= */ 30, text.length(), ForegroundColorSpan.class)[0] - .getForegroundColor()) - .isEqualTo(0xFFEE82EE); - assertThat(text.getSpans(/* start= */ 30, text.length(), TypefaceSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 30, text.length(), TypefaceSpan.class)[0].getFamily()) - .isEqualTo("courier"); + Spanned firstCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 0); + assertThat(firstCueText) + .hasForegroundColorSpanBetween( + "This should be underlined and ".length(), firstCueText.length()) + .withColor(ColorParser.parseCssColor("violet")); + assertThat(firstCueText) + .hasTypefaceSpanBetween("This should be underlined and ".length(), firstCueText.length()) + .withFamily("courier"); - text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2000000); - assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)[0].getFamily()) - .isEqualTo("courier"); + Spanned secondCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2_000_000); + assertThat(secondCueText) + .hasTypefaceSpanBetween("This ".length(), secondCueText.length()) + .withFamily("courier"); + assertThat(secondCueText) + .hasNoForegroundColorSpanBetween("This ".length(), secondCueText.length()); - text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2500000); - assertThat(text.getSpans(/* start= */ 5, text.length(), StyleSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 5, text.length(), StyleSpan.class)[0].getStyle()) - .isEqualTo(Typeface.BOLD); - assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)[0].getFamily()) - .isEqualTo("courier"); + Spanned thirdCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2_500_000); + assertThat(thirdCueText).hasBoldSpanBetween("This ".length(), thirdCueText.length()); + assertThat(thirdCueText) + .hasTypefaceSpanBetween("This ".length(), thirdCueText.length()) + .withFamily("courier"); - text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 4000000); - assertThat(text.getSpans(/* start= */ 6, /* end= */ 22, StyleSpan.class)).hasLength(0); - assertThat(text.getSpans(/* start= */ 30, text.length(), StyleSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 30, text.length(), StyleSpan.class)[0].getStyle()) - .isEqualTo(Typeface.BOLD); + Spanned fourthCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 4_000_000); + assertThat(fourthCueText) + .hasNoStyleSpanBetween("This ".length(), "shouldn't be bold.".length()); + assertThat(fourthCueText) + .hasBoldSpanBetween("This shouldn't be bold.\nThis ".length(), fourthCueText.length()); - text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 5000000); - assertThat(text.getSpans(/* start= */ 9, /* end= */ 17, StyleSpan.class)).hasLength(0); - assertThat(text.getSpans(/* start= */ 19, text.length(), StyleSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 19, text.length(), StyleSpan.class)[0].getStyle()) - .isEqualTo(Typeface.ITALIC); + Spanned fifthCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 5_000_000); + assertThat(fifthCueText) + .hasNoStyleSpanBetween("This is ".length(), "This is specific".length()); + assertThat(fifthCueText) + .hasItalicSpanBetween("This is specific\n".length(), fifthCueText.length()); } @Test @@ -387,6 +382,6 @@ public class WebvttDecoderTest { } private Spanned getUniqueSpanTextAt(WebvttSubtitle sub, long timeUs) { - return (Spanned) sub.getCues(timeUs).get(0).text; + return (Spanned) Assertions.checkNotNull(sub.getCues(timeUs).get(0).text); } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java index b6efa1e7b7..78c41a43e8 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java @@ -26,12 +26,14 @@ import android.text.TextUtils; import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; +import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; import androidx.annotation.CheckResult; import androidx.annotation.ColorInt; import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; import com.google.android.exoplayer2.text.span.RubySpan; +import com.google.android.exoplayer2.util.Util; import com.google.common.truth.FailureMetadata; import com.google.common.truth.Subject; import java.util.ArrayList; @@ -171,6 +173,19 @@ public final class SpannedSubject extends Subject { return ALREADY_FAILED_WITH_FLAGS; } + /** + * Checks that the subject has no {@link StyleSpan}s on any of the text between {@code start} and + * {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + public void hasNoStyleSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(StyleSpan.class, start, end); + } + /** * Checks that the subject has an {@link UnderlineSpan} from {@code start} to {@code end}. * @@ -194,6 +209,19 @@ public final class SpannedSubject extends Subject { return ALREADY_FAILED_WITH_FLAGS; } + /** + * Checks that the subject has no {@link UnderlineSpan}s on any of the text between {@code start} + * and {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + public void hasNoUnderlineSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(UnderlineSpan.class, start, end); + } + /** * Checks that the subject has a {@link ForegroundColorSpan} from {@code start} to {@code end}. * @@ -222,6 +250,19 @@ public final class SpannedSubject extends Subject { .that(foregroundColorSpans); } + /** + * Checks that the subject has no {@link ForegroundColorSpan}s on any of the text between {@code + * start} and {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + public void hasNoForegroundColorSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(ForegroundColorSpan.class, start, end); + } + /** * Checks that the subject has a {@link BackgroundColorSpan} from {@code start} to {@code end}. * @@ -250,6 +291,58 @@ public final class SpannedSubject extends Subject { .that(backgroundColorSpans); } + /** + * Checks that the subject has no {@link BackgroundColorSpan}s on any of the text between {@code + * start} and {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + public void hasNoBackgroundColorSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(BackgroundColorSpan.class, start, end); + } + + /** + * Checks that the subject has a {@link TypefaceSpan} from {@code start} to {@code end}. + * + *

    The font is asserted in a follow-up method call on the return {@link Typefaced} object. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link Typefaced} object to assert on the font of the matching spans. + */ + @CheckResult + public Typefaced hasTypefaceSpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_TYPEFACED; + } + + List backgroundColorSpans = findMatchingSpans(start, end, TypefaceSpan.class); + if (backgroundColorSpans.isEmpty()) { + failWithExpectedSpan(start, end, TypefaceSpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_TYPEFACED; + } + return check("TypefaceSpan (start=%s,end=%s)", start, end) + .about(typefaceSpans(actual)) + .that(backgroundColorSpans); + } + + /** + * Checks that the subject has no {@link TypefaceSpan}s on any of the text between {@code start} + * and {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + public void hasNoTypefaceSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(TypefaceSpan.class, start, end); + } + /** * Checks that the subject has a {@link RubySpan} from {@code start} to {@code end}. * @@ -274,6 +367,19 @@ public final class SpannedSubject extends Subject { return check("RubySpan (start=%s,end=%s)", start, end).about(rubySpans(actual)).that(rubySpans); } + /** + * Checks that the subject has no {@link RubySpan}s on any of the text between {@code start} and + * {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + public void hasNoRubySpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(RubySpan.class, start, end); + } + /** * Checks that the subject has an {@link HorizontalTextInVerticalContextSpan} from {@code start} * to {@code end}. @@ -303,6 +409,45 @@ public final class SpannedSubject extends Subject { return ALREADY_FAILED_WITH_FLAGS; } + /** + * Checks that the subject has no {@link HorizontalTextInVerticalContextSpan}s on any of the text + * between {@code start} and {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + public void hasNoHorizontalTextInVerticalContextSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(HorizontalTextInVerticalContextSpan.class, start, end); + } + + /** + * Checks that the subject has no spans of type {@code spanClazz} on any of the text between + * {@code start} and {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + private void hasNoSpansOfTypeBetween(Class spanClazz, int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return; + } + Object[] matchingSpans = actual.getSpans(start, end, spanClazz); + if (matchingSpans.length != 0) { + failWithoutActual( + simpleFact( + String.format( + "Found unexpected %ss between start=%s,end=%s", + spanClazz.getSimpleName(), start, end)), + simpleFact("expected none"), + fact("but found", getAllSpansAsString(actual))); + } + } + private List findMatchingSpans(int startIndex, int endIndex, Class spanClazz) { List spans = new ArrayList<>(); for (T span : actual.getSpans(startIndex, endIndex, spanClazz)) { @@ -421,8 +566,8 @@ public final class SpannedSubject extends Subject { private static final Colored ALREADY_FAILED_COLORED = color -> ALREADY_FAILED_AND_FLAGS; - private Factory> foregroundColorSpans( - Spanned actualSpanned) { + private static Factory> + foregroundColorSpans(Spanned actualSpanned) { return (FailureMetadata metadata, List spans) -> new ForegroundColorSpansSubject(metadata, spans, actualSpanned); } @@ -458,8 +603,8 @@ public final class SpannedSubject extends Subject { } } - private Factory> backgroundColorSpans( - Spanned actualSpanned) { + private static Factory> + backgroundColorSpans(Spanned actualSpanned) { return (FailureMetadata metadata, List spans) -> new BackgroundColorSpansSubject(metadata, spans, actualSpanned); } @@ -495,6 +640,55 @@ public final class SpannedSubject extends Subject { } } + /** Allows assertions about the typeface of a span. */ + public interface Typefaced { + + /** + * Checks that at least one of the matched spans has the expected {@code fontFamily}. + * + * @param fontFamily The expected font family. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. + */ + AndSpanFlags withFamily(String fontFamily); + } + + private static final Typefaced ALREADY_FAILED_TYPEFACED = color -> ALREADY_FAILED_AND_FLAGS; + + private static Factory> typefaceSpans( + Spanned actualSpanned) { + return (FailureMetadata metadata, List spans) -> + new TypefaceSpansSubject(metadata, spans, actualSpanned); + } + + private static final class TypefaceSpansSubject extends Subject implements Typefaced { + + private final List actualSpans; + private final Spanned actualSpanned; + + private TypefaceSpansSubject( + FailureMetadata metadata, List actualSpans, Spanned actualSpanned) { + super(metadata, actualSpans); + this.actualSpans = actualSpans; + this.actualSpanned = actualSpanned; + } + + @Override + public AndSpanFlags withFamily(String fontFamily) { + List matchingSpanFlags = new ArrayList<>(); + List spanFontFamilies = new ArrayList<>(); + + for (TypefaceSpan span : actualSpans) { + spanFontFamilies.add(span.getFamily()); + if (Util.areEqual(span.getFamily(), fontFamily)) { + matchingSpanFlags.add(actualSpanned.getSpanFlags(span)); + } + } + + check("family").that(spanFontFamilies).containsExactly(fontFamily); + return check("flags").about(spanFlags()).that(matchingSpanFlags); + } + } + /** Allows assertions about a span's ruby text and its position. */ public interface RubyText { @@ -511,7 +705,7 @@ public final class SpannedSubject extends Subject { private static final RubyText ALREADY_FAILED_WITH_TEXT = (text, position) -> ALREADY_FAILED_AND_FLAGS; - private Factory> rubySpans(Spanned actualSpanned) { + private static Factory> rubySpans(Spanned actualSpanned) { return (FailureMetadata metadata, List spans) -> new RubySpansSubject(metadata, spans, actualSpanned); } @@ -544,7 +738,7 @@ public final class SpannedSubject extends Subject { return check("flags").about(spanFlags()).that(matchingSpanFlags); } - private static class TextAndPosition { + private static final class TextAndPosition { private final String text; @RubySpan.Position private final int position; diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java index c3badd9bb9..d1ee3ee81a 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java @@ -28,6 +28,7 @@ import android.text.Spanned; import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; +import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; @@ -170,6 +171,40 @@ public class SpannedSubjectTest { assertThat(expected).factValue("but found").contains("start=" + start); } + @Test + public void noStyleSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with underline then italic spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new StyleSpan(Typeface.ITALIC), + "test with underline then ".length(), + "test with underline then italic".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoStyleSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noStyleSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with italic section"); + int start = "test with ".length(); + int end = start + "italic".length(); + spannable.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> whenTesting.that(spannable).hasNoStyleSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains("Found unexpected StyleSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + @Test public void underlineSpan_success() { SpannableString spannable = SpannableString.valueOf("test with underlined section"); @@ -182,6 +217,40 @@ public class SpannedSubjectTest { .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } + @Test + public void noUnderlineSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with italic then underline spans"); + spannable.setSpan( + new StyleSpan(Typeface.ITALIC), + "test with ".length(), + "test with italic".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new UnderlineSpan(), + "test with italic then ".length(), + "test with italic then underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoUnderlineSpanBetween(0, "test with italic then".length()); + } + + @Test + public void noUnderlineSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with underline section"); + int start = "test with ".length(); + int end = start + "underline".length(); + spannable.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> whenTesting.that(spannable).hasNoUnderlineSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains("Found unexpected UnderlineSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + @Test public void foregroundColorSpan_success() { SpannableString spannable = SpannableString.valueOf("test with cyan section"); @@ -261,6 +330,43 @@ public class SpannedSubjectTest { .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); } + @Test + public void noForegroundColorSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with underline then cyan spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), + "test with underline then ".length(), + "test with underline then cyan".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoForegroundColorSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noForegroundColorSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting.that(spannable).hasNoForegroundColorSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains( + "Found unexpected ForegroundColorSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + @Test public void backgroundColorSpan_success() { SpannableString spannable = SpannableString.valueOf("test with cyan section"); @@ -340,6 +446,152 @@ public class SpannedSubjectTest { .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); } + @Test + public void noBackgroundColorSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with underline then cyan spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new BackgroundColorSpan(Color.CYAN), + "test with underline then ".length(), + "test with underline then cyan".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoBackgroundColorSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noBackgroundColorSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new BackgroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting.that(spannable).hasNoBackgroundColorSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains( + "Found unexpected BackgroundColorSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + + @Test + public void typefaceSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with courier section"); + int start = "test with ".length(); + int end = start + "courier".length(); + spannable.setSpan(new TypefaceSpan("courier"), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasTypefaceSpanBetween(start, end) + .withFamily("courier") + .andFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void typefaceSpan_wrongEndIndex() { + SpannableString spannable = SpannableString.valueOf("test with courier section"); + int start = "test with ".length(); + int end = start + "courier".length(); + spannable.setSpan(new TypefaceSpan("courier"), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + int incorrectEnd = end + 2; + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasTypefaceSpanBetween(start, incorrectEnd) + .withFamily("courier")); + assertThat(expected).factValue("expected").contains("end=" + incorrectEnd); + assertThat(expected).factValue("but found").contains("end=" + end); + } + + @Test + public void typefaceSpan_wrongFamily() { + SpannableString spannable = SpannableString.valueOf("test with courier section"); + int start = "test with ".length(); + int end = start + "courier".length(); + spannable.setSpan(new TypefaceSpan("courier"), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasTypefaceSpanBetween(start, end) + .withFamily("roboto")); + assertThat(expected).factValue("value of").contains("family"); + assertThat(expected).factValue("expected").contains("roboto"); + assertThat(expected).factValue("but was").contains("courier"); + } + + @Test + public void typefaceSpan_wrongFlags() { + SpannableString spannable = SpannableString.valueOf("test with courier section"); + int start = "test with ".length(); + int end = start + "courier".length(); + spannable.setSpan(new TypefaceSpan("courier"), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasTypefaceSpanBetween(start, end) + .withFamily("courier") + .andFlags(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected).factValue("value of").contains("flags"); + assertThat(expected) + .factValue("expected to contain") + .contains(String.valueOf(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected) + .factValue("but was") + .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + } + + @Test + public void noTypefaceSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with underline then courier spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new TypefaceSpan("courier"), + "test with underline then ".length(), + "test with underline then courier".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoTypefaceSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noTypefaceSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with courier section"); + int start = "test with ".length(); + int end = start + "courier".length(); + spannable.setSpan(new TypefaceSpan("courier"), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> whenTesting.that(spannable).hasNoTypefaceSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains("Found unexpected TypefaceSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + @Test public void rubySpan_success() { SpannableString spannable = SpannableString.valueOf("test with rubied section"); @@ -454,6 +706,44 @@ public class SpannedSubjectTest { .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); } + @Test + public void noRubySpan_success() { + SpannableString spannable = SpannableString.valueOf("test with underline then ruby spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + "test with underline then ".length(), + "test with underline then ruby".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoRubySpanBetween(0, "test with underline then".length()); + } + + @Test + public void noRubySpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with ruby section"); + int start = "test with ".length(); + int end = start + "ruby".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> whenTesting.that(spannable).hasNoRubySpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains("Found unexpected RubySpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + @Test public void horizontalTextInVerticalContextSpan_success() { SpannableString spannable = SpannableString.valueOf("vertical text with horizontal section"); @@ -467,6 +757,50 @@ public class SpannedSubjectTest { .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } + @Test + public void noHorizontalTextInVerticalContextSpan_success() { + SpannableString spannable = + SpannableString.valueOf("test with underline then tate-chu-yoko spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new HorizontalTextInVerticalContextSpan(), + "test with underline then ".length(), + "test with underline then tate-chu-yoko".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasNoHorizontalTextInVerticalContextSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noHorizontalTextInVerticalContextSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with tate-chu-yoko section"); + int start = "test with ".length(); + int end = start + "tate-chu-yoko".length(); + spannable.setSpan( + new HorizontalTextInVerticalContextSpan(), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasNoHorizontalTextInVerticalContextSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains( + "Found unexpected HorizontalTextInVerticalContextSpans between start=" + + (start + 1) + + ",end=" + + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + private static AssertionError expectFailure( ExpectFailure.SimpleSubjectBuilderCallback callback) { return expectFailureAbout(spanned(), callback); From 762bc18a28f131bcfb6b345f4fcfecf66125e6f0 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 8 Jan 2020 15:04:46 +0000 Subject: [PATCH 0615/1335] Fix TrueHD chunking in Matroska Issue: #6845 PiperOrigin-RevId: 288688716 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/extractor/mkv/MatroskaExtractor.java | 4 +--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d5dd9ddd0e..ac0fb108b9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -34,6 +34,8 @@ to proceed. * Fix handling of E-AC-3 streams that contain AC-3 syncframes ([#6602](https://github.com/google/ExoPlayer/issues/6602)). +* Fix playback of TrueHD streams in Matroska + ([#6845](https://github.com/google/ExoPlayer/issues/6845)). * Support "twos" codec (big endian PCM) in MP4 ([#5789](https://github.com/google/ExoPlayer/issues/5789)). * WAV: Support IMA ADPCM encoded data. 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 ee57bbec90..8812d2857e 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 @@ -1829,10 +1829,8 @@ public class MatroskaExtractor implements Extractor { chunkSize += size; 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; + outputPendingSampleMetadata(track); } - outputPendingSampleMetadata(track); } public void outputPendingSampleMetadata(Track track) { From 8e26505ee8984988f6f0015e6aadba6516b96a29 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 8 Jan 2020 16:07:50 +0000 Subject: [PATCH 0616/1335] Fix what I think is a typo in WebVTT test data Without the CSS tweak the additional test assertion fails. PiperOrigin-RevId: 288698323 --- library/core/src/test/assets/webvtt/with_css_complex_selectors | 2 +- .../android/exoplayer2/text/webvtt/WebvttDecoderTest.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/test/assets/webvtt/with_css_complex_selectors b/library/core/src/test/assets/webvtt/with_css_complex_selectors index 64d2a516d1..130d4a2529 100644 --- a/library/core/src/test/assets/webvtt/with_css_complex_selectors +++ b/library/core/src/test/assets/webvtt/with_css_complex_selectors @@ -1,7 +1,7 @@ WEBVTT STYLE -::cue(\n#id ){text-decoration:underline;} +::cue(#id ){text-decoration:underline;} STYLE ::cue(#id.class1.class2 ){ color: violet;} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index 97e3a8f7d5..b778953f01 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -326,6 +326,7 @@ public class WebvttDecoderTest { public void testWithComplexCssSelectors() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_COMPLEX_SELECTORS); Spanned firstCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 0); + assertThat(firstCueText).hasUnderlineSpanBetween(0, firstCueText.length()); assertThat(firstCueText) .hasForegroundColorSpanBetween( "This should be underlined and ".length(), firstCueText.length()) From 14e401f53a56a273c2ce784a1aff63b5da7eb2c8 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 8 Jan 2020 16:18:18 +0000 Subject: [PATCH 0617/1335] Update TtmlDecoder to keep only one Span of each type The current code relies on Android's evaluation order of spans, which doesn't seem to be defined anywhere. PiperOrigin-RevId: 288700011 --- .../android/exoplayer2/text/SpanUtil.java | 55 ++++++++++++ .../exoplayer2/text/ttml/TtmlRenderUtil.java | 48 ++++++++--- .../text/webvtt/WebvttCueParser.java | 76 +++++++++------- .../android/exoplayer2/text/SpanUtilTest.java | 86 +++++++++++++++++++ 4 files changed, 226 insertions(+), 39 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/SpanUtil.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/text/SpanUtilTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SpanUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SpanUtil.java new file mode 100644 index 0000000000..9e9f350dd7 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SpanUtil.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.text; + +import android.text.Spannable; +import android.text.style.ForegroundColorSpan; + +/** + * Utility methods for Android span + * styling. + */ +public final class SpanUtil { + + /** + * Adds {@code span} to {@code spannable} between {@code start} and {@code end}, removing any + * existing spans of the same type and with the same indices and flags. + * + *

    This is useful for types of spans that don't make sense to duplicate and where the + * evaluation order might have an unexpected impact on the final text, e.g. {@link + * ForegroundColorSpan}. + * + * @param spannable The {@link Spannable} to add {@code span} to. + * @param span The span object to be added. + * @param start The start index to add the new span at. + * @param end The end index to add the new span at. + * @param spanFlags The flags to pass to {@link Spannable#setSpan(Object, int, int, int)}. + */ + public static void addOrReplaceSpan( + Spannable spannable, Object span, int start, int end, int spanFlags) { + Object[] existingSpans = spannable.getSpans(start, end, span.getClass()); + for (Object existingSpan : existingSpans) { + if (spannable.getSpanStart(existingSpan) == start + && spannable.getSpanEnd(existingSpan) == end + && spannable.getSpanFlags(existingSpan) == spanFlags) { + spannable.removeSpan(existingSpan); + } + } + spannable.setSpan(span, start, end, spanFlags); + } + + private SpanUtil() {} +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java index 21333081c6..25395431de 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.text.ttml; -import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.AbsoluteSizeSpan; @@ -27,6 +26,7 @@ import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; +import com.google.android.exoplayer2.text.SpanUtil; import java.util.Map; /** @@ -77,32 +77,60 @@ import java.util.Map; builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.hasFontColor()) { - builder.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + SpanUtil.addOrReplaceSpan( + builder, + new ForegroundColorSpan(style.getFontColor()), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.hasBackgroundColor()) { - builder.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + SpanUtil.addOrReplaceSpan( + builder, + new BackgroundColorSpan(style.getBackgroundColor()), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.getFontFamily() != null) { - builder.setSpan(new TypefaceSpan(style.getFontFamily()), start, end, + SpanUtil.addOrReplaceSpan( + builder, + new TypefaceSpan(style.getFontFamily()), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.getTextAlign() != null) { - builder.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end, + SpanUtil.addOrReplaceSpan( + builder, + new AlignmentSpan.Standard(style.getTextAlign()), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } switch (style.getFontSizeUnit()) { case TtmlStyle.FONT_SIZE_UNIT_PIXEL: - builder.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end, + SpanUtil.addOrReplaceSpan( + builder, + new AbsoluteSizeSpan((int) style.getFontSize(), true), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case TtmlStyle.FONT_SIZE_UNIT_EM: - builder.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end, + SpanUtil.addOrReplaceSpan( + builder, + new RelativeSizeSpan(style.getFontSize()), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case TtmlStyle.FONT_SIZE_UNIT_PERCENT: - builder.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end, + SpanUtil.addOrReplaceSpan( + builder, + new RelativeSizeSpan(style.getFontSize() / 100), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case TtmlStyle.UNSPECIFIED: 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 fe36043800..f62b073f60 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 @@ -15,11 +15,11 @@ */ package com.google.android.exoplayer2.text.webvtt; +import static com.google.android.exoplayer2.text.SpanUtil.addOrReplaceSpan; import static java.lang.annotation.RetentionPolicy.SOURCE; import android.graphics.Typeface; import android.text.Layout; -import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.SpannedString; @@ -535,7 +535,12 @@ public final class WebvttCueParser { return; } if (style.getStyle() != WebvttCssStyle.UNSPECIFIED) { - addOrReplaceSpan(spannedText, new StyleSpan(style.getStyle()), start, end); + addOrReplaceSpan( + spannedText, + new StyleSpan(style.getStyle()), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.isLinethrough()) { spannedText.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); @@ -544,29 +549,62 @@ public final class WebvttCueParser { spannedText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.hasFontColor()) { - addOrReplaceSpan(spannedText, new ForegroundColorSpan(style.getFontColor()), start, end); + addOrReplaceSpan( + spannedText, + new ForegroundColorSpan(style.getFontColor()), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.hasBackgroundColor()) { addOrReplaceSpan( - spannedText, new BackgroundColorSpan(style.getBackgroundColor()), start, end); + spannedText, + new BackgroundColorSpan(style.getBackgroundColor()), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.getFontFamily() != null) { - addOrReplaceSpan(spannedText, new TypefaceSpan(style.getFontFamily()), start, end); + addOrReplaceSpan( + spannedText, + new TypefaceSpan(style.getFontFamily()), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } Layout.Alignment textAlign = style.getTextAlign(); if (textAlign != null) { - addOrReplaceSpan(spannedText, new AlignmentSpan.Standard(textAlign), start, end); + addOrReplaceSpan( + spannedText, + new AlignmentSpan.Standard(textAlign), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } switch (style.getFontSizeUnit()) { case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL: addOrReplaceSpan( - spannedText, new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end); + spannedText, + new AbsoluteSizeSpan((int) style.getFontSize(), true), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case WebvttCssStyle.FONT_SIZE_UNIT_EM: - addOrReplaceSpan(spannedText, new RelativeSizeSpan(style.getFontSize()), start, end); + addOrReplaceSpan( + spannedText, + new RelativeSizeSpan(style.getFontSize()), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case WebvttCssStyle.FONT_SIZE_UNIT_PERCENT: - addOrReplaceSpan(spannedText, new RelativeSizeSpan(style.getFontSize() / 100), start, end); + addOrReplaceSpan( + spannedText, + new RelativeSizeSpan(style.getFontSize() / 100), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case WebvttCssStyle.UNSPECIFIED: // Do nothing. @@ -578,26 +616,6 @@ public final class WebvttCueParser { } } - /** - * Adds {@code span} to {@code spannedText} between {@code start} and {@code end}, removing any - * existing spans of the same type and with the same indices. - * - *

    This is useful for types of spans that don't make sense to duplicate and where the - * evaluation order might have an unexpected impact on the final text, e.g. {@link - * ForegroundColorSpan}. - */ - private static void addOrReplaceSpan( - SpannableStringBuilder spannedText, Object span, int start, int end) { - Object[] existingSpans = spannedText.getSpans(start, end, span.getClass()); - for (Object existingSpan : existingSpans) { - if (spannedText.getSpanStart(existingSpan) == start - && spannedText.getSpanEnd(existingSpan) == end) { - spannedText.removeSpan(existingSpan); - } - } - spannedText.setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - /** * Returns the tag name for the given tag contents. * diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/SpanUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/SpanUtilTest.java new file mode 100644 index 0000000000..3a71925255 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/SpanUtilTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.text; + +import static com.google.common.truth.Truth.assertThat; + +import android.graphics.Color; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link SpanUtil}. */ +@RunWith(AndroidJUnit4.class) +public class SpanUtilTest { + + @Test + public void addOrReplaceSpan_replacesSameTypeAndIndexes() { + Spannable spannable = SpannableString.valueOf("test text"); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), + /* start= */ 2, + /* end= */ 5, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + ForegroundColorSpan newSpan = new ForegroundColorSpan(Color.BLUE); + SpanUtil.addOrReplaceSpan( + spannable, newSpan, /* start= */ 2, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + Object[] spans = spannable.getSpans(0, spannable.length(), Object.class); + assertThat(spans).asList().containsExactly(newSpan); + } + + @Test + public void addOrReplaceSpan_ignoresDifferentType() { + Spannable spannable = SpannableString.valueOf("test text"); + ForegroundColorSpan originalSpan = new ForegroundColorSpan(Color.CYAN); + spannable.setSpan(originalSpan, /* start= */ 2, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + BackgroundColorSpan newSpan = new BackgroundColorSpan(Color.BLUE); + SpanUtil.addOrReplaceSpan(spannable, newSpan, 2, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + Object[] spans = spannable.getSpans(0, spannable.length(), Object.class); + assertThat(spans).asList().containsExactly(originalSpan, newSpan).inOrder(); + } + + @Test + public void addOrReplaceSpan_ignoresDifferentStartEndAndFlags() { + Spannable spannable = SpannableString.valueOf("test text"); + ForegroundColorSpan originalSpan = new ForegroundColorSpan(Color.CYAN); + spannable.setSpan(originalSpan, /* start= */ 2, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + ForegroundColorSpan differentStart = new ForegroundColorSpan(Color.GREEN); + SpanUtil.addOrReplaceSpan( + spannable, differentStart, /* start= */ 3, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + ForegroundColorSpan differentEnd = new ForegroundColorSpan(Color.BLUE); + SpanUtil.addOrReplaceSpan( + spannable, differentEnd, /* start= */ 2, /* end= */ 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + ForegroundColorSpan differentFlags = new ForegroundColorSpan(Color.GREEN); + SpanUtil.addOrReplaceSpan( + spannable, differentFlags, /* start= */ 2, /* end= */ 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + Object[] spans = spannable.getSpans(0, spannable.length(), Object.class); + assertThat(spans) + .asList() + .containsExactly(originalSpan, differentStart, differentEnd, differentFlags) + .inOrder(); + } +} From fa9bf9c8280bf4853238388c9e94603e11798365 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 8 Jan 2020 16:51:25 +0000 Subject: [PATCH 0618/1335] Ensure seeks to new windows are reported as seeking. Currently, seeks are only tracked if both the start and the end of the seek is within the same window. This means no seeking state is reported if the playback switches to a new window during the seek. This problem is fixed by tracking seek start and end events in all cases, but only report seeking state once the window becomes the foreground window. PiperOrigin-RevId: 288706674 --- .../analytics/PlaybackStatsListener.java | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java index 5927b9dd6e..3f3803f5c0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java @@ -84,6 +84,7 @@ public final class PlaybackStatsListener @Player.State private int playbackState; private boolean isSuppressed; private float playbackSpeed; + private boolean isSeeking; /** * Creates listener for playback stats. @@ -169,6 +170,9 @@ public final class PlaybackStatsListener @Override public void onSessionCreated(EventTime eventTime, String session) { PlaybackStatsTracker tracker = new PlaybackStatsTracker(keepHistory, eventTime); + if (isSeeking) { + tracker.onSeekStarted(eventTime, /* belongsToPlayback= */ true); + } tracker.onPlayerStateChanged( eventTime, playWhenReady, playbackState, /* belongsToPlayback= */ true); tracker.onIsSuppressedChanged(eventTime, isSuppressed, /* belongsToPlayback= */ true); @@ -288,20 +292,20 @@ public final class PlaybackStatsListener public void onSeekStarted(EventTime eventTime) { sessionManager.updateSessions(eventTime); for (String session : playbackStatsTrackers.keySet()) { - if (sessionManager.belongsToSession(eventTime, session)) { - playbackStatsTrackers.get(session).onSeekStarted(eventTime); - } + boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); + playbackStatsTrackers.get(session).onSeekStarted(eventTime, belongsToPlayback); } + isSeeking = true; } @Override public void onSeekProcessed(EventTime eventTime) { sessionManager.updateSessions(eventTime); for (String session : playbackStatsTrackers.keySet()) { - if (sessionManager.belongsToSession(eventTime, session)) { - playbackStatsTrackers.get(session).onSeekProcessed(eventTime); - } + boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); + playbackStatsTrackers.get(session).onSeekProcessed(eventTime, belongsToPlayback); } + isSeeking = false; } @Override @@ -563,23 +567,27 @@ public final class PlaybackStatsListener } /** - * Notifies the tracker of the start of a seek in the current playback. + * Notifies the tracker of the start of a seek, including all seeks while the playback is not in + * the foreground. * * @param eventTime The {@link EventTime}. + * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. */ - public void onSeekStarted(EventTime eventTime) { + public void onSeekStarted(EventTime eventTime, boolean belongsToPlayback) { isSeeking = true; - maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + maybeUpdatePlaybackState(eventTime, belongsToPlayback); } /** - * Notifies the tracker of a seek has been processed in the current playback. + * Notifies the tracker that a seek has been processed, including all seeks while the playback + * is not in the foreground. * * @param eventTime The {@link EventTime}. + * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. */ - public void onSeekProcessed(EventTime eventTime) { + public void onSeekProcessed(EventTime eventTime, boolean belongsToPlayback) { isSeeking = false; - maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + maybeUpdatePlaybackState(eventTime, belongsToPlayback); } /** @@ -875,7 +883,7 @@ public final class PlaybackStatsListener return currentPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED ? PlaybackStats.PLAYBACK_STATE_ENDED : PlaybackStats.PLAYBACK_STATE_ABANDONED; - } else if (isSeeking) { + } else if (isSeeking && isForeground) { // Seeking takes precedence over errors such that we report a seek while in error state. return PlaybackStats.PLAYBACK_STATE_SEEKING; } else if (hasFatalError) { From e5eaacec20615b8c85eea520955d33d6c0fe7094 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 8 Jan 2020 17:15:03 +0000 Subject: [PATCH 0619/1335] Fix typo in SpannedSubject.hasBoldItalicSpanBetween PiperOrigin-RevId: 288710939 --- .../testutil/truth/SpannedSubject.java | 4 ++-- .../testutil/truth/SpannedSubjectTest.java | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java index 78c41a43e8..1751502ac4 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java @@ -167,8 +167,8 @@ public final class SpannedSubject extends Subject { simpleFact( String.format("No matching StyleSpans found between start=%s,end=%s", start, end)), fact("in text", actual.toString()), - fact("expected either styles", Arrays.asList(Typeface.BOLD_ITALIC)), - fact("or styles", Arrays.asList(Typeface.BOLD, Typeface.BOLD_ITALIC)), + fact("expected either styles", Collections.singletonList(Typeface.BOLD_ITALIC)), + fact("or styles", Arrays.asList(Typeface.BOLD, Typeface.ITALIC)), fact("but found styles", styles)); return ALREADY_FAILED_WITH_FLAGS; } diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java index d1ee3ee81a..32ce419c19 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java @@ -151,6 +151,23 @@ public class SpannedSubjectTest { .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } + @Test + public void boldItalicSpan_onlyItalic() { + SpannableString spannable = SpannableString.valueOf("test with italic section"); + int start = "test with ".length(); + int end = start + "italic".length(); + spannable.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> whenTesting.that(spannable).hasBoldItalicSpanBetween(start, end)); + assertThat(expected) + .factKeys() + .contains( + String.format("No matching StyleSpans found between start=%s,end=%s", start, end)); + assertThat(expected).factValue("but found styles").contains("[" + Typeface.ITALIC + "]"); + } + @Test public void boldItalicSpan_mismatchedStartIndex() { SpannableString spannable = SpannableString.valueOf("test with bold & italic section"); From 216518eb0ea1beaf567ae14eae00c0d19055c9cf Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 8 Jan 2020 17:20:19 +0000 Subject: [PATCH 0620/1335] Disable chronometer for playback speeds != 1.0 This doesn't work because the Chronometer text layout can only count in realtime. Issue:#6816 PiperOrigin-RevId: 288711702 --- RELEASENOTES.md | 2 ++ .../exoplayer2/ui/PlayerNotificationManager.java | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ac0fb108b9..bdef903be1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -54,6 +54,8 @@ * OkHttp extension: Upgrade OkHttp dependency to 3.12.7, which fixes a class of `SocketTimeoutException` issues when using HTTP/2 ([#4078](https://github.com/google/ExoPlayer/issues/4078)). +* Don't use notification chronometer if playback speed is != 1.0 + ([#6816](https://github.com/google/ExoPlayer/issues/6816)). ### 2.11.1 (2019-12-20) ### 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 e572bc5a11..9f0c8280c4 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 @@ -927,7 +927,18 @@ public class PlayerNotificationManager { } /** - * Sets whether the elapsed time of the media playback should be displayed + * Sets whether the elapsed time of the media playback should be displayed. + * + *

    Note that this setting only works if all of the following are true: + * + *

      + *
    • The media is {@link Player#isPlaying() actively playing}. + *
    • The media is not {@link Player#isCurrentWindowDynamic() dynamically changing its + * duration} (like for example a live stream). + *
    • The media is not {@link Player#isPlayingAd() interrupted by an ad}. + *
    • The media is played at {@link Player#getPlaybackParameters() regular speed}. + *
    • The device is running at least API 21 (Lollipop). + *
    * *

    See {@link NotificationCompat.Builder#setUsesChronometer(boolean)}. * @@ -1082,7 +1093,8 @@ public class PlayerNotificationManager { && useChronometer && player.isPlaying() && !player.isPlayingAd() - && !player.isCurrentWindowDynamic()) { + && !player.isCurrentWindowDynamic() + && player.getPlaybackParameters().speed == 1f) { builder .setWhen(System.currentTimeMillis() - player.getContentPosition()) .setShowWhen(true) From 79edf7cce23098a94901e1b351dd2ab49d0e8049 Mon Sep 17 00:00:00 2001 From: kimvde Date: Wed, 8 Jan 2020 17:21:09 +0000 Subject: [PATCH 0621/1335] FlacExtractor: add condition for zero-length reads This improves readability by making clearer that reading no bytes from the input in readFrames() is an expected case if the buffer is full. PiperOrigin-RevId: 288711841 --- .../extractor/flac/FlacExtractor.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java index 8c31bde2a2..79dd20065b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java @@ -256,15 +256,18 @@ public final class FlacExtractor implements Extractor { // Copy more bytes into the buffer. int currentLimit = buffer.limit(); - int bytesRead = - input.read( - buffer.data, /* offset= */ currentLimit, /* length= */ BUFFER_LENGTH - currentLimit); - boolean foundEndOfInput = bytesRead == C.RESULT_END_OF_INPUT; - if (!foundEndOfInput) { - buffer.setLimit(currentLimit + bytesRead); - } else if (buffer.bytesLeft() == 0) { - outputSampleMetadata(); - return Extractor.RESULT_END_OF_INPUT; + boolean foundEndOfInput = false; + if (currentLimit < BUFFER_LENGTH) { + int bytesRead = + input.read( + buffer.data, /* offset= */ currentLimit, /* length= */ BUFFER_LENGTH - currentLimit); + foundEndOfInput = bytesRead == C.RESULT_END_OF_INPUT; + if (!foundEndOfInput) { + buffer.setLimit(currentLimit + bytesRead); + } else if (buffer.bytesLeft() == 0) { + outputSampleMetadata(); + return Extractor.RESULT_END_OF_INPUT; + } } // Search for a frame. From 4ce72d9d6d7cab11169430d69809ec55be694ec2 Mon Sep 17 00:00:00 2001 From: ybai001 Date: Thu, 9 Jan 2020 15:23:05 +0800 Subject: [PATCH 0622/1335] Update AC-4 DRM code based on comments Update cleardatasize[0] in extractor rather than sampleDataQueue. --- .../extractor/mp4/FragmentedMp4Extractor.java | 75 ++++++++++++++++--- .../exoplayer2/source/SampleDataQueue.java | 17 ++--- .../exoplayer2/source/SampleQueue.java | 3 +- 3 files changed, 70 insertions(+), 25 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 a4a70ce7e5..66ed1a5c92 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 @@ -1265,10 +1265,17 @@ public class FragmentedMp4Extractor implements Extractor { sampleSize -= Atom.HEADER_SIZE; input.skipFully(Atom.HEADER_SIZE); } - sampleBytesWritten = currentTrackBundle.outputSampleEncryptionData(); - sampleSize += sampleBytesWritten; - if (MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType)) { - Ac4Util.getAc4SampleHeader(sampleSize, scratch); + + boolean isAc4HeaderRequired = + MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType); + + int encryptionDataBytesWritten = currentTrackBundle.outputSampleEncryptionData( + sampleSize, isAc4HeaderRequired ? Ac4Util.SAMPLE_HEADER_SIZE : 0); + sampleBytesWritten = encryptionDataBytesWritten; + sampleSize += encryptionDataBytesWritten; + + if (isAc4HeaderRequired) { + Ac4Util.getAc4SampleHeader(sampleSize - encryptionDataBytesWritten, scratch); currentTrackBundle.output.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE); sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE; sampleSize += Ac4Util.SAMPLE_HEADER_SIZE; @@ -1555,9 +1562,13 @@ public class FragmentedMp4Extractor implements Extractor { /** * Outputs the encryption data for the current sample. * + * @param sampleSize The size of the current sample in bytes, excluding any additional clear + * header that will be prefixed to the sample by the extractor. + * @param clearHeaderSize The size of a clear header that will be prefixed to the sample by the + * extractor, or 0. * @return The number of written bytes. */ - public int outputSampleEncryptionData() { + public int outputSampleEncryptionData(int sampleSize, int clearHeaderSize) { TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted(); if (encryptionBox == null) { return 0; @@ -1576,24 +1587,66 @@ public class FragmentedMp4Extractor implements Extractor { vectorSize = initVectorData.length; } - boolean subsampleEncryption = fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex); + boolean haveSubsampleEncryptionTable = + fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex); + boolean writeSubsampleEncryptionData = + haveSubsampleEncryptionTable | clearHeaderSize != 0; // Write the signal byte, containing the vector size and the subsample encryption flag. - encryptionSignalByte.data[0] = (byte) (vectorSize | (subsampleEncryption ? 0x80 : 0)); + encryptionSignalByte.data[0] = + (byte) (vectorSize | (writeSubsampleEncryptionData ? 0x80 : 0)); encryptionSignalByte.setPosition(0); output.sampleData(encryptionSignalByte, 1); // Write the vector. output.sampleData(initializationVectorData, vectorSize); - // If we don't have subsample encryption data, we're done. - if (!subsampleEncryption) { + + if (!writeSubsampleEncryptionData) { return 1 + vectorSize; } - // Write the subsample encryption data. + + if (!haveSubsampleEncryptionTable) { + // Need to synthesize subsample encryption data. The sample is fully encrypted except + // for the additional header that the extractor is going to prefix, so we need to write the + // following to output.sampleData: + // subsampleCount (unsigned short) = 1 + // clearDataSizes[0] (unsigned short) = clearHeaderSize + // encryptedDataSizes[0] (unsigned int) = sampleSize + ParsableByteArray encryptionData = new ParsableByteArray(8); + encryptionData.data[0] = (byte)0; + encryptionData.data[1] = (byte)1; + encryptionData.data[2] = (byte)((clearHeaderSize & 0xFF00) >>> 8); + encryptionData.data[3] = (byte)( clearHeaderSize & 0x00FF); + encryptionData.data[4] = (byte)((sampleSize & 0xFF000000) >>> 24); + encryptionData.data[5] = (byte)((sampleSize & 0x00FF0000) >>> 16); + encryptionData.data[6] = (byte)((sampleSize & 0x0000FF00) >>> 8); + encryptionData.data[7] = (byte)( sampleSize & 0x000000FF); + encryptionData.setPosition(0); + output.sampleData(encryptionData, 8); + return 1 + vectorSize + 8; + } + ParsableByteArray subsampleEncryptionData = fragment.sampleEncryptionData; int subsampleCount = subsampleEncryptionData.readUnsignedShort(); subsampleEncryptionData.skipBytes(-2); int subsampleDataLength = 2 + 6 * subsampleCount; - output.sampleData(subsampleEncryptionData, subsampleDataLength); + + if (clearHeaderSize > 0) { + // On the way through, we need to re-write the 3rd and 4th bytes, which hold + // clearDataSizes[0], so that clearHeaderSize is added into the value. This must be done + // without modifying subsampleEncryptionData itself. + ParsableByteArray subsampleEncryptionData2 = new ParsableByteArray(subsampleDataLength); + subsampleEncryptionData2.readBytes(subsampleEncryptionData.data, + subsampleEncryptionData.getPosition(), subsampleDataLength); + int clearDataSize = (subsampleEncryptionData2.data[2] & 0xFF) << 8 + | (subsampleEncryptionData2.data[3] & 0xFF) + clearHeaderSize; + subsampleEncryptionData2.data[2] = (byte)((clearDataSize & 0xFF00) >>> 8); + subsampleEncryptionData2.data[3] = (byte)( clearDataSize & 0x00FF); + subsampleEncryptionData2.setPosition(0); + output.sampleData(subsampleEncryptionData2, subsampleDataLength); + subsampleEncryptionData.skipBytes(subsampleDataLength); + } else { + output.sampleData(subsampleEncryptionData, subsampleDataLength); + } return 1 + vectorSize + subsampleDataLength; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java index 26a95b8de2..68761cef19 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java @@ -23,7 +23,6 @@ import com.google.android.exoplayer2.extractor.TrackOutput.CryptoData; import com.google.android.exoplayer2.source.SampleQueue.SampleExtrasHolder; import com.google.android.exoplayer2.upstream.Allocation; import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.EOFException; import java.io.IOException; @@ -115,13 +114,11 @@ import java.nio.ByteBuffer; * * @param buffer The buffer to populate. * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. - * @param mimeType The MIME type. */ - public void readToBuffer(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder, - String mimeType) { + public void readToBuffer(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { // Read encryption data if the sample is encrypted. if (buffer.isEncrypted()) { - readEncryptionData(buffer, extrasHolder, mimeType); + readEncryptionData(buffer, extrasHolder); } // Read sample data, extracting supplemental data into a separate buffer if needed. if (buffer.hasSupplementalData()) { @@ -218,10 +215,8 @@ import java.nio.ByteBuffer; * * @param buffer The buffer into which the encryption data should be written. * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. - * @param mimeType The MIME type. */ - private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder, - String mimeType) { + private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { long offset = extrasHolder.offset; // Read the signal byte. @@ -270,10 +265,8 @@ import java.nio.ByteBuffer; encryptedDataSizes[i] = scratch.readUnsignedIntToInt(); } } else { - int addedHeaderSize = MimeTypes.AUDIO_AC4.equals(mimeType) ? 7 : 0; - clearDataSizes[0] = addedHeaderSize; - encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset) - - addedHeaderSize; + clearDataSizes[0] = 0; + encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset); } // Populate the cryptoInfo. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index bfd3d7c4ed..cc15d9d275 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -323,8 +323,7 @@ public class SampleQueue implements TrackOutput { readSampleMetadata( formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilUs, extrasHolder); if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream() && !buffer.isFlagsOnly()) { - sampleDataQueue.readToBuffer(buffer, extrasHolder, - downstreamFormat == null ? null : downstreamFormat.sampleMimeType); + sampleDataQueue.readToBuffer(buffer, extrasHolder); } return result; } From 74e01f4e979941fe7be2985c3f921659ee55c1b1 Mon Sep 17 00:00:00 2001 From: ybai001 Date: Fri, 10 Jan 2020 14:19:30 +0800 Subject: [PATCH 0623/1335] Add protected AC-4 fmp4 test case --- .../test/assets/mp4/sample_ac4_protected.mp4 | Bin 0 -> 8815 bytes .../mp4/sample_ac4_protected.mp4.0.dump | 145 ++++++++++++++++++ .../mp4/sample_ac4_protected.mp4.1.dump | 109 +++++++++++++ .../mp4/sample_ac4_protected.mp4.2.dump | 73 +++++++++ .../mp4/sample_ac4_protected.mp4.3.dump | 37 +++++ .../mp4/FragmentedMp4ExtractorTest.java | 6 + 6 files changed, 370 insertions(+) create mode 100644 library/core/src/test/assets/mp4/sample_ac4_protected.mp4 create mode 100644 library/core/src/test/assets/mp4/sample_ac4_protected.mp4.0.dump create mode 100644 library/core/src/test/assets/mp4/sample_ac4_protected.mp4.1.dump create mode 100644 library/core/src/test/assets/mp4/sample_ac4_protected.mp4.2.dump create mode 100644 library/core/src/test/assets/mp4/sample_ac4_protected.mp4.3.dump diff --git a/library/core/src/test/assets/mp4/sample_ac4_protected.mp4 b/library/core/src/test/assets/mp4/sample_ac4_protected.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..e3a4f6d6c6a653ea15a59b56f17d439e8664afd8 GIT binary patch literal 8815 zcmb7oWmKEp(sponcPLJA*FteEE(MA^1WRxV!J#;X2QLt$#ogU$aat%E++AB}ffxGl zoO8bSynnt~nOrm1%-nnLnSHOU6#xK0YXkOiwFkKX0T1$k!a!Gk9xHQ@oh8u55&%GO z0J^w%K7^cro_1D`Ir9>Ig^vmVv_c+z0NkI?AM+po-_GCh|MaE))A?^3{(*`DT7%7> zLbP^PPVWDtDarEj{$oGkn}0Z>!3PWR4=bmkrKI;@i2(q1V6ZDll#9#O9&G1f!D;CN z;M1=S(1^7e+Z8+Smovh74)&KxJ9N695@!>(n zV8=g)NBQ^RACLXt@`wE2k^kjO{Xb=J*+46M^9PC+X!UpgrQM&KX!)@C)2b{Foc%+L7W~Nl5jfOD zelksaM-vJtugr(B?o+l08W#ixgZ_^H`F`+0Px*g*!Ux~?KjM$(Lyo1(e{i@U;NL@d z;)H>o*4~eKKHWzZZBIvbp^~O@CZ;^+7*@{xw_xK}p>q9O{ym?GxHPq5Ts& zJfY(gIz1uq37wzNRLYd453H*aQHP%)$d^! zpDikq3zzN%dKj_mER}+I6Z~M3H<>ezJ$!>SER+;Y6C(wOs4X`#`;elBVnB|U3zut> zk$Vp8t?9S}`C?yYbIEhDpHkvVhAFvmUx4)OcS$)W$Hc}4jZ&i$W2sH+*0*JvrutL5 zDrLggfrkB^0>PzckhOJdL^N1qt(rEDH_FTSRVra5HX)w8GF3E#Zi+6H5jLq(HAtUA zpw}VGf>$GvI)AoY-vyKBGTALyIeKbp{7q%$Fn6ALfmk7;{rNsLv znfC}*NjDjwFH0z~+sfsUnI{ZKwhQIiI7{9E+X%=gohiHfjf}?O>&W(Nfgb(Qv-0m= zjN)si(w?QvcIx=VouVGRHQWQO>X*CF_R%dqUn#SC2|VL-;}4>9mp-Vp+2&PZVQpHN z!*E|OrQ{@Tqi>ef|2ZQ$=*2hVbdz@DZzNe|Ky3;R~LZ0~^rF-KuCOyv!s=pNB+vT&G_0Jw<}Nt zo-O+OV0}^LRMPx2x|pu1Z-L-2=vNp)#t0zWRKe-#fxR-c^N%KCiaijq-fl=HDcH-J%>6Lx_{7kWFlT?EB?xoVZ+FNe3n6@z9Ot;_UZ z)FS4Lnqj=Tu1Qa%@(p{-4Ias9hd8%twrsDugbRLGem!+gj4^#{og2NMAW1qjeiMETn|#92Ow0@#5c_hqqQe~C(yDk3MxRHCmDNJMxQpv!cK9X9*OcA|5Z(syj1!sLrlj0TNsvu-$@8!#Bbb%% z@e{ckx#GSbYoQnsnb;_ZBA_9ue-Tvamjm+5ybhF?Kg`XQr7JhC(-!3!4^R#t8L=g@ zO&tG_Gipe*L3VM4h(}MD7?BY8eVl(k)+EifohtC^CDPZJMWfPgbAC+9wRhkSPz5~V zD+Z~aXHFSsiNsoX zM(&=ZyT6(=08w8L$xgGV)6d5?iDt|>`!S0PX<-Vk05MQn?FbV;l$5AlL~kMD5FY*c zxy3YMoHdVys}~htYOf9DWatTpOCAfLA!1DEI}~&*l{#Tqi2unHt)|_{*>T_~RR>LH z!irU9nb(k^2{$j1xH(uZW@uPwCv2-#3}wFnK5T?)aw}|}G(AZ}-;_?2oWtzJ7!8zT z=QVFSlGc%HZo=!^f#{A;Au1L5Ui5N~)`f&xj?4#lU8bHX`x~1Jm(GO}XAKSp(?1V1 z^uD{{YsBy8t)Z0-$&0SNI6Ji=9qicra@9$hDLtaS;T$e;{lOh%;5PT=c9-O`=!l9Z{) z!j~ZJ{Gh=aGWVk-lygKf?)e(Jr|uh`oL8VI!a(%f14qJ$0$Al}pZ6To_FT`#w3BUB zVdHxHan}`u$eXI1I-I-ZUEsI4bZNb?<=PGdo?EK26Z3KmITE$W@~WF6SejTFAbAsT zftvSQ94gIjvzDfnqKD)-yQUJCH%rD~F4oOO42H?1k#|FXM}RXaFt+4X1D{<3pnjVB z>zBb5246rkNG`k8ZTkF~@EQ#%%xQG0LTy6#pj+j60UD?n!t(3uV(vrjg_Xf8OU)SE zNQ)$)=XP76ijUq@zGQ4^z=g)x3<=Gpjg#rB;?L%!EghR-3a^UOJ5%w<{)t7~BkaO{8mDNnDu zJFz9^HdMxNFjqTRsY{+8B|v7ppgEf{nsD4pRu?f_Yo=CNSCry zFLLKn$FW4mmcF4oZ|X*Lup10zplE)>RopPJdiz$o9Nz@}xkb9FKWApQVk{Z|OT~Z> zI#7LSD-o!XXR8CnPs=1`cJ%D&9KR{Ic2T_WYJ%cwycq#C)lSEYwPy=0g5 zfNK6CmCy3qct6VxM6%Oi`l||p<;`n!G>KKc-)=PPjV(#WwJg}Qaf;CWn2*SS3|KM+ zx#5poRX;njmyngGYnb^Rm#2lkhcwQ(LsEj;8*Wa8vU?zMEzy*#12IM3{^gJ+aTW`w zZ?r)$BtbQRG(Hid@qU#do@()y(yP%T^HWF%W%s^SKEJQ1 zP3>f&{zjqo{sbd60)uXscNlub?FEQhJvY@p5kqBlV$|*p5&~N>Q*@>&WfPxD2ZmpB zd0uGp1^_CLnNpHw=}j(Jj(S?#UzWBeqdE&;MhsAd?|I)1i7Dgy5KDglTz+t+sS??c z?HL@a;4tOR5^RSljZnHDHV<`lDOK#5VEc(pL@c!-VQ)TA1ynv}s=N=PK0R_qL!w|< zc34WNaRUp{rdplr-a4(^=@?h!Eg8e4DTSwcI&E1jnkPH&rir-qse3qZ+~%I0o|&pl z`e0|1*%a|h?60JYwLYKDpP7Y2Pgo5)%-eOZp!>>!s$(hp#VX@J&kS&`I?36xO1gb(uM_+wm7?<1kI<{%I*VqN?nJE^od^i-{Y3QZdU$A ze8$R)(>cX{GF92z>l3t~ye3~BIA2oWJ8~-?*veq_gW?4XZhGj}#RWn=AL4BKrA~#5 zX(IXW*GCSpAq2?IqQ2Fv8$KL&MF%ji*19&vIB;W<#^+3Tt^;u0wDxc>3!=h3qE}CN zd9XF<%VZ6cqQ=!D$&!C0Z5vm6IajW$NTX=L@ik9p`|Vf1XbcI+&N6!8>qD+Gu67T5 zd)qx)@O*WNt&*8ZMl-!&4O%GeFCn|fNIz~#MIZ>rp5q-5=$8)qun6_#8s)WXB`<_~So??!sj9tTT;*w=p3fML#QNS&%LC@J5--$Qd$=J6HGQdinX;qy;Ap6Udnk%?z*j83p#lliq`vy zuah#;pgDi}&i+=gzpeF_E;Y$7#Y(J4=u&2lX6Ak2S6cF^v>C7fR}?d#)1R zpsFAfN_f=>`iNpxK}yriqYwDZJqa$1(N=?@x!q}RqMJ>{^@?L9Y13zPf0`|u_ApEIkC4B(Pz90Z?VN{-tP3;DtY5SKmg}})g>3J8y$Mm9 z#A0#E$A>Xu1?!wy`MTCG_@74!AcD3m?r`WSN$-6zYbq zQ0w%q6oWE|6IMj_X<%kse3mlQIZlhaZe0E-Ahu?OdiE(6rg{-(>2$#l0^xpgaD(%F zo>A4>E?QLY4=?lK7gbiG>l{v_N!hiZDIFc0fd!2EY=ld9+&<^5C?p)jpaH{@Qt(?C zN)(2>D|fNE@aQmN&%IXiMjPWj8kb5ZsyG!1aPq4YAWb(ny95MbnoupGS^WbNjGMwF zuj&I`8O2assnNHFGh-~xA)~3N~80z)V-*-ha5@ci~;~VmKLwcx+WriJPtGpUh zDi{^tp}^H`9uwrQK<(=DmP8d2DwDG}UFB_~i}pl}P=&?}wnJgj;h&Ov<0AD9qBb|* zqkpTLE{@9y?hl_HrXwJ+*C)o*SF)x}U++5m6d9PQRtlYCI1n{NyK@lhtkyZDrVn?N zlSG;^{PtVcXtOP*LWJ8PNlG%j`T&olpk?j51CF(?pY|n>`zMWB)ab3ObN?)y3`Q>P zuUUKZ*a7)r^1+P`Dr0B(T8HXMoXi_PL?_#53_NNZOGXve%96fiLb?Tl4{zRZmD{E< zH2AQU@y0>6_IWa*?H!O$d)vI5h0m>Fp!#vDae-OEB1O7qhM$-(e(5AzUNk~PXxW*2 za~6uUzD*u<#wcECv@Bv$U|6{BWJ0a;sm}KyVv!=5gaK-j2v*fbxhY!K9>>=ww@t^d zm%R$gJqnnV%QI>=WY&}MJaYl-KY(ZJj!twerQMZ*i>7J_lrQOyU#C?RHn;Y>jW zybAN)>>cZ5I{dBT)?L=3{1Tq$FiNz2)Av$cLB+DA-o1SS8KupbC)kg@eAwc?7lOxX z!2JVb4s(RLEf2o0r|zI1I-#K^@rh_TfOnwyMOuQW416C0ORJq4ILZ7>K0^Wyo+P|c z>L}!PI8WM7Wwe6uV-~ay5sanA#Yr`F*_~g~U5xdy)%tf7{()S3$SYF^TkVLoDj|xq zvEmx^_$FmEUxdPBb#Wv1qcVxOx02Xi{R?Uf(ONS+bTj@2CiQb@d69n51MAC0s_%0= zKb&XBm14zOl~MZ(R75f|am03R_jnaO^>+;mV9)d`v6G#D%-#yjC@B}4x+VJND0*3)%}v7^8R|># zNSNpXWIdhSLKFItuMg0HN1`YgmEx~fUImDa2Sx=Ip>(SYb!>e6oiLeTfMUcpRcBgd zgQPaf*l;h&%L%Oq$kV~!ed4M#^yMh{3iTRnlEXu)b4c^t(B`u^ojOZbLJWwcR|IOQ zSk8HEGo2%a!JF@^^2%RUF**bkA$)DROor@5{4$-43 zj0!CxsfG8RkJi>h%6OHV5EX9wj^KV#{jK3=JaKL04l{3ER6gB-8w?-xe3G?mNi5aR zD6c0IeJ?mp!%(_?85Wc1`DnioHeAL#*NUMXo$4PRwbv){=l5nBYUb_7YE-&T@v@tS zsxeYeLXhq&M!rMqc*HOCe9$NJ<~!!vRV2ZhZrN~b@4z&cOypL-Nes^lbpr_#q+dti zai6#b2LHN|yN|BIZKou%GVAhQlNHK2PZg-9#X(RgKrSIX{AJ8ZH9}@yq3~X&GKp{9 zIQoUhfD5BwlrN1^y$ctN_$}2<|B#E?u!jkf%R7a@w8Ck;>%L@x%o^JS~p&R7(n5} ze?Asei;v9i`AwPZ4e<){LS|Z&tF+~`dZ7w|{Bnl2ZkV)AtfLoReSrn9%OPI3$juoy zmd37&7wkDyu+y#>B0nlDi)PaDdyqYwqj;r&;VXBsv|e=O6mIgke?mhOqaQOYVGO zZi2up1w-K4+^H+c&HlV7-#EQ9z4T$X1daOjq4tQJp5Spg;nIEimUcOP!OUf!vE4$2 zGcH6LRZK&76`LbT02lS=FSM6;Ivu!^1ykaPhTYgoIqDXd0zps>hlop&`jsOAux+z) zL|(j`F1f|G{_q<(eu)e%;;>pMzVxZvyvV9Kp86Um*s-Bio%@3?i&fo$(v}Xn@%D1} z6_#>B{nGD_^5#K0~(4(oT{XkGo;11VmO4D0%c+{FI+O|*Tm9!M%deY z=4hcY0VJw;2KCFH{(l|c{Axq+X$<(BBN;hLkmqI)8IloyJ{cuJ^5eYmE*_~R%lPXk zm#o0>n{rr*ZXNDC()L02GmgIOTFo{TIZ^J`-FLaBBU@-Tx)n)!<1D)4pML|_qauvg zgHrh~m0mX4lz&TP?4x}b>j9IS=;@V$~gP@1k_P zaVDtDTj0TnzBhjd4}C#~Cx620Rm6GF|2=YXq*5uaX{u5`$Xl3`-YIaQHc!p11kom# zVK8pc+dJjsFRW>s=M;@npX@r3rYG%&5?5+{cceA7&(ovg1*07T-bM;)-nbW(_J%G* z8g~D@NW5!|Zd~I5nlZe;+C=6O7J}8#;3QcY~6AE@;USc#csj8bz`E<4XYV8 zq#^#otSR1OZ;_AM-h;%ep1;8qGriI`n=Z&lQ>umVx$j?PCYol4Z--L-zI}H(c(X?y zX_x;+UTk#I@MB%TD~T0e?G{wl<-xuE*n9<1>MO<|8s27-IHknuPLNF0MDfdXF}kA&pXbim^R8?03MFOQ&YPI!@ZjvF~4# zZ|X%5qIff@TC%e+TZWS+zQ5~GsvMLMxKqDfoGQ669_UqXslfFR$7dCG5_q`^+6A`# z{25A9OI9eF-t6%bl&C)m73WRFA@@BL7AfkJ(p*$JFwFX#EcJ%?vs1o`b&E&n_?L5L zB!a@@lSD?HFZ4pxY0W^==*usn3~ zHnv}&e~L>JAf)n6bSLgrwR-EIBpY!x`p_3iv_hSP7X_-Vq^r>>TSY6e9@c6m?VCs2 z0};k closedCaptionFormats) { return () -> new FragmentedMp4Extractor(0, null, null, null, closedCaptionFormats); } From 017a7cf38c60d8044277439a6aea3cc7c3dddae8 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 9 Jan 2020 09:32:42 +0000 Subject: [PATCH 0624/1335] Resolve TrueHD spec TODO PiperOrigin-RevId: 288855515 --- .../java/com/google/android/exoplayer2/audio/Ac3Util.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java index 066c9f88ef..53803ada4e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java @@ -534,8 +534,8 @@ public final class Ac3Util { * contain the start of a syncframe. */ public static int parseTrueHdSyncframeAudioSampleCount(byte[] syncframe) { - // TODO: Link to specification if available. - // The syncword ends 0xBA for TrueHD or 0xBB for MLP. + // See "Dolby TrueHD (MLP) high-level bitstream description" on the Dolby developer site, + // subsections 2.2 and 4.2.1. The syncword ends 0xBA for TrueHD or 0xBB for MLP. if (syncframe[4] != (byte) 0xF8 || syncframe[5] != (byte) 0x72 || syncframe[6] != (byte) 0x6F From 1a9b301f52ac8305f9778bcb15d66b5d6ab67cac Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 9 Jan 2020 10:13:25 +0000 Subject: [PATCH 0625/1335] Fix extension FLAC decoder to correctly handle non-16-bit depths PiperOrigin-RevId: 288860159 --- RELEASENOTES.md | 10 ++-- .../exoplayer2/ext/flac/FlacDecoder.java | 11 ++-- .../ext/flac/LibflacAudioRenderer.java | 54 ++++++++++++++++--- .../audio/SimpleDecoderAudioRenderer.java | 9 +--- .../exoplayer2/util/FlacStreamMetadata.java | 5 +- .../audio/SimpleDecoderAudioRendererTest.java | 8 ++- 6 files changed, 71 insertions(+), 26 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index bdef903be1..b6381e271f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -56,6 +56,10 @@ ([#4078](https://github.com/google/ExoPlayer/issues/4078)). * Don't use notification chronometer if playback speed is != 1.0 ([#6816](https://github.com/google/ExoPlayer/issues/6816)). +* FLAC extension: Fix handling of bit depths other than 16 in `FLACDecoder`. + This issue caused FLAC streams with other bit depths to sound like white noise + on earlier releases, but only when embedded in a non-FLAC container such as + Matroska or MP4. ### 2.11.1 (2019-12-20) ### @@ -222,7 +226,7 @@ `C.MSG_SET_OUTPUT_BUFFER_RENDERER`. * Use `VideoDecoderRenderer` as an implementation of `VideoDecoderOutputBufferRenderer`, instead of `VideoDecoderSurfaceView`. -* Flac extension: Update to use NDK r20. +* FLAC extension: Update to use NDK r20. * Opus extension: Update to use NDK r20. * FFmpeg extension: * Update to use NDK r20. @@ -359,7 +363,7 @@ ([#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 +* 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)). @@ -368,7 +372,7 @@ ([#6153](https://github.com/google/ExoPlayer/issues/6153)). * Fix `DataSchemeDataSource` re-opening and range requests ([#6192](https://github.com/google/ExoPlayer/issues/6192)). -* Fix Flac and ALAC playback on some LG devices +* 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` 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 e1f6112319..013b23ef21 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 @@ -33,7 +33,7 @@ import java.util.List; /* package */ final class FlacDecoder extends SimpleDecoder { - private final int maxOutputBufferSize; + private final FlacStreamMetadata streamMetadata; private final FlacDecoderJni decoderJni; /** @@ -59,7 +59,6 @@ import java.util.List; } decoderJni = new FlacDecoderJni(); decoderJni.setData(ByteBuffer.wrap(initializationData.get(0))); - FlacStreamMetadata streamMetadata; try { streamMetadata = decoderJni.decodeStreamMetadata(); } catch (ParserException e) { @@ -72,7 +71,6 @@ import java.util.List; int initialInputBufferSize = maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize; setInitialInputBufferSize(initialInputBufferSize); - maxOutputBufferSize = streamMetadata.getMaxDecodedFrameSize(); } @Override @@ -103,7 +101,8 @@ import java.util.List; decoderJni.flush(); } decoderJni.setData(Util.castNonNull(inputBuffer.data)); - ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize); + ByteBuffer outputData = + outputBuffer.init(inputBuffer.timeUs, streamMetadata.getMaxDecodedFrameSize()); try { decoderJni.decodeSample(outputData); } catch (FlacDecoderJni.FlacFrameDecodeException e) { @@ -121,4 +120,8 @@ import java.util.List; decoderJni.release(); } + /** Returns the {@link FlacStreamMetadata} decoded from the initialization data. */ + public FlacStreamMetadata getStreamMetadata() { + return streamMetadata; + } } 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 3e8d727476..7d9f6253e3 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 @@ -24,15 +24,20 @@ import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.FlacConstants; +import com.google.android.exoplayer2.util.FlacStreamMetadata; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -/** - * Decodes and renders audio using the native Flac decoder. - */ -public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { +/** Decodes and renders audio using the native Flac decoder. */ +public final class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { private static final int NUM_BUFFERS = 16; + @MonotonicNonNull private FlacStreamMetadata streamMetadata; + public LibflacAudioRenderer() { this(/* eventHandler= */ null, /* eventListener= */ null); } @@ -57,7 +62,23 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { if (!FlacLibrary.isAvailable() || !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) { return FORMAT_UNSUPPORTED_TYPE; - } else if (!supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) { + } + // Compute the PCM encoding that the FLAC decoder will output. + @C.PcmEncoding int pcmEncoding; + if (format.initializationData.isEmpty()) { + // The initialization data might not be set if the format was obtained from a manifest (e.g. + // for DASH playbacks) rather than directly from the media. In this case we assume + // ENCODING_PCM_16BIT. If the actual encoding is different, playback will still succeed as + // long as the AudioSink supports it (which will always be true when using DefaultAudioSink). + pcmEncoding = C.ENCODING_PCM_16BIT; + } else { + int streamMetadataOffset = + FlacConstants.STREAM_MARKER_SIZE + FlacConstants.METADATA_BLOCK_HEADER_SIZE; + FlacStreamMetadata streamMetadata = + new FlacStreamMetadata(format.initializationData.get(0), streamMetadataOffset); + pcmEncoding = Util.getPcmEncoding(streamMetadata.bitsPerSample); + } + if (!supportsOutput(format.channelCount, pcmEncoding)) { return FORMAT_UNSUPPORTED_SUBTYPE; } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { return FORMAT_UNSUPPORTED_DRM; @@ -69,8 +90,27 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { @Override protected FlacDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) throws FlacDecoderException { - return new FlacDecoder( - NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData); + FlacDecoder decoder = + new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData); + streamMetadata = decoder.getStreamMetadata(); + return decoder; } + @Override + protected Format getOutputFormat() { + Assertions.checkNotNull(streamMetadata); + return Format.createAudioSampleFormat( + /* id= */ null, + MimeTypes.AUDIO_RAW, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + streamMetadata.channels, + streamMetadata.sampleRate, + Util.getPcmEncoding(streamMetadata.bitsPerSample), + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); + } } 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 60870204cc..3977650146 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 @@ -351,15 +351,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements /** * Returns the format of audio buffers output by the decoder. Will not be called until the first * output buffer has been dequeued, so the decoder may use input data to determine the format. - *

    - * The default implementation returns a 16-bit PCM format with the same channel count and sample - * rate as the input. */ - protected Format getOutputFormat() { - return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE, - Format.NO_VALUE, inputFormat.channelCount, inputFormat.sampleRate, C.ENCODING_PCM_16BIT, - null, null, 0, null); - } + protected abstract Format getOutputFormat(); /** * Returns whether the existing decoder can be kept for a new format. 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 deced8ebe6..470e82c13f 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 @@ -102,8 +102,9 @@ public final class FlacStreamMetadata { /** * Parses binary FLAC stream info metadata. * - * @param data An array containing binary FLAC stream info block (with or without header). - * @param offset The offset of the stream info block in {@code data} (header excluded). + * @param data An array containing binary FLAC stream info block. + * @param offset The offset of the stream info block in {@code data}, excluding the header (i.e. + * the offset points to the first byte of the minimum block size). */ public FlacStreamMetadata(byte[] data, int offset) { ParsableBitArray scratch = new ParsableBitArray(data); 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 f8fd2fc9ca..0d8d2dabb1 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 @@ -67,10 +67,14 @@ public class SimpleDecoderAudioRendererTest { @Override protected SimpleDecoder< DecoderInputBuffer, ? extends SimpleOutputBuffer, ? extends AudioDecoderException> - createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) - throws AudioDecoderException { + createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) { return new FakeDecoder(); } + + @Override + protected Format getOutputFormat() { + return FORMAT; + } }; } From f22ac32c2c82db78f86fd936ec0d87f89b270a76 Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 9 Jan 2020 10:34:28 +0000 Subject: [PATCH 0626/1335] Relax the check in SpannedSubject.hasBoldItalicSpanBetween Ultimately we only care if the style is both bold & italic, if some of those are specified multiple times there's no problem. PiperOrigin-RevId: 288862235 --- .../exoplayer2/testutil/truth/SpannedSubject.java | 10 ++++------ .../testutil/truth/SpannedSubjectTest.java | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java index 1751502ac4..db1c984635 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java @@ -157,18 +157,16 @@ public final class SpannedSubject extends Subject { return ALREADY_FAILED_WITH_FLAGS; } - if (styles.size() == 1 && styles.contains(Typeface.BOLD_ITALIC) - || styles.size() == 2 - && styles.contains(Typeface.BOLD) - && styles.contains(Typeface.ITALIC)) { + if (styles.contains(Typeface.BOLD_ITALIC) + || (styles.contains(Typeface.BOLD) && styles.contains(Typeface.ITALIC))) { return check("StyleSpan (start=%s,end=%s)", start, end).about(spanFlags()).that(allFlags); } failWithoutActual( simpleFact( String.format("No matching StyleSpans found between start=%s,end=%s", start, end)), fact("in text", actual.toString()), - fact("expected either styles", Collections.singletonList(Typeface.BOLD_ITALIC)), - fact("or styles", Arrays.asList(Typeface.BOLD, Typeface.ITALIC)), + fact("expected to contain either", Collections.singletonList(Typeface.BOLD_ITALIC)), + fact("or both", Arrays.asList(Typeface.BOLD, Typeface.ITALIC)), fact("but found styles", styles)); return ALREADY_FAILED_WITH_FLAGS; } diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java index 32ce419c19..33a67f1c64 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java @@ -151,6 +151,21 @@ public class SpannedSubjectTest { .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } + @Test + // If the span is both BOLD and BOLD_ITALIC then the assertion should still succeed. + public void boldItalicSpan_withRepeatSpans() { + SpannableString spannable = SpannableString.valueOf("test with bold & italic section"); + int start = "test with ".length(); + int end = start + "bold & italic".length(); + spannable.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new StyleSpan(Typeface.BOLD_ITALIC), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasBoldItalicSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + @Test public void boldItalicSpan_onlyItalic() { SpannableString spannable = SpannableString.valueOf("test with italic section"); From bae4d786e247ed9c8057ba948c1659d5cd75af9a Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 9 Jan 2020 10:35:01 +0000 Subject: [PATCH 0627/1335] Add {Strikethrough,Alignment}Span support to SpannedSubject I'm going to use these in TtmlDecoderTest PiperOrigin-RevId: 288862274 --- .../testutil/truth/SpannedSubject.java | 130 +++++++++++++ .../testutil/truth/SpannedSubjectTest.java | 182 ++++++++++++++++++ 2 files changed, 312 insertions(+) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java index db1c984635..b161df701b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java @@ -21,10 +21,13 @@ import static com.google.common.truth.Fact.simpleFact; import static com.google.common.truth.Truth.assertAbout; import android.graphics.Typeface; +import android.text.Layout.Alignment; import android.text.Spanned; import android.text.TextUtils; +import android.text.style.AlignmentSpan; import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; +import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; @@ -220,6 +223,84 @@ public final class SpannedSubject extends Subject { hasNoSpansOfTypeBetween(UnderlineSpan.class, start, end); } + /** + * Checks that the subject has an {@link StrikethroughSpan} from {@code start} to {@code end}. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. + */ + public WithSpanFlags hasStrikethroughSpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_WITH_FLAGS; + } + + List strikethroughSpans = + findMatchingSpans(start, end, StrikethroughSpan.class); + if (strikethroughSpans.size() == 1) { + return check("StrikethroughSpan (start=%s,end=%s)", start, end) + .about(spanFlags()) + .that(Collections.singletonList(actual.getSpanFlags(strikethroughSpans.get(0)))); + } + failWithExpectedSpan( + start, end, StrikethroughSpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_WITH_FLAGS; + } + + /** + * Checks that the subject has no {@link StrikethroughSpan}s on any of the text between {@code + * start} and {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + public void hasNoStrikethroughSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(StrikethroughSpan.class, start, end); + } + + /** + * Checks that the subject has a {@link AlignmentSpan} from {@code start} to {@code end}. + * + *

    The alignment is asserted in a follow-up method call on the return {@link Aligned} object. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link Aligned} object to assert on the alignment of the matching spans. + */ + @CheckResult + public Aligned hasAlignmentSpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_ALIGNED; + } + + List alignmentSpans = findMatchingSpans(start, end, AlignmentSpan.class); + if (alignmentSpans.isEmpty()) { + failWithExpectedSpan( + start, end, AlignmentSpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_ALIGNED; + } + return check("AlignmentSpan (start=%s,end=%s)", start, end) + .about(alignmentSpans(actual)) + .that(alignmentSpans); + } + + /** + * Checks that the subject has no {@link AlignmentSpan}s on any of the text between {@code start} + * and {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + public void hasNoAlignmentSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(AlignmentSpan.class, start, end); + } + /** * Checks that the subject has a {@link ForegroundColorSpan} from {@code start} to {@code end}. * @@ -550,6 +631,55 @@ public final class SpannedSubject extends Subject { } } + /** Allows assertions about the alignment of a span. */ + public interface Aligned { + + /** + * Checks that at least one of the matched spans has the expected {@code alignment}. + * + * @param alignment The expected alignment. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. + */ + AndSpanFlags withAlignment(Alignment alignment); + } + + private static final Aligned ALREADY_FAILED_ALIGNED = alignment -> ALREADY_FAILED_AND_FLAGS; + + private static Factory> alignmentSpans( + Spanned actualSpanned) { + return (FailureMetadata metadata, List spans) -> + new AlignmentSpansSubject(metadata, spans, actualSpanned); + } + + private static final class AlignmentSpansSubject extends Subject implements Aligned { + + private final List actualSpans; + private final Spanned actualSpanned; + + private AlignmentSpansSubject( + FailureMetadata metadata, List actualSpans, Spanned actualSpanned) { + super(metadata, actualSpans); + this.actualSpans = actualSpans; + this.actualSpanned = actualSpanned; + } + + @Override + public AndSpanFlags withAlignment(Alignment alignment) { + List matchingSpanFlags = new ArrayList<>(); + List spanAlignments = new ArrayList<>(); + + for (AlignmentSpan span : actualSpans) { + spanAlignments.add(span.getAlignment()); + if (span.getAlignment().equals(alignment)) { + matchingSpanFlags.add(actualSpanned.getSpanFlags(span)); + } + } + + check("alignment").that(spanAlignments).containsExactly(alignment); + return check("flags").about(spanFlags()).that(matchingSpanFlags); + } + } + /** Allows assertions about the color of a span. */ public interface Colored { diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java index 33a67f1c64..1c37853c4b 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java @@ -23,10 +23,13 @@ import static com.google.common.truth.ExpectFailure.expectFailureAbout; import android.graphics.Color; import android.graphics.Typeface; +import android.text.Layout.Alignment; import android.text.SpannableString; import android.text.Spanned; +import android.text.style.AlignmentSpan; import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; +import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; @@ -283,6 +286,185 @@ public class SpannedSubjectTest { assertThat(expected).factValue("but found").contains("start=" + start); } + @Test + public void strikethroughSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with crossed-out section"); + int start = "test with ".length(); + int end = start + "crossed-out".length(); + spannable.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasStrikethroughSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void noStrikethroughSpan_success() { + SpannableString spannable = + SpannableString.valueOf("test with underline then crossed-out spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new UnderlineSpan(), + "test with underline then ".length(), + "test with italic then crossed-out".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoStrikethroughSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noStrikethroughSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with crossed-out section"); + int start = "test with ".length(); + int end = start + "crossed-out".length(); + spannable.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting.that(spannable).hasNoStrikethroughSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains( + "Found unexpected StrikethroughSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + + @Test + public void alignmentSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with right-aligned section"); + int start = "test with ".length(); + int end = start + "right-aligned".length(); + spannable.setSpan( + new AlignmentSpan.Standard(Alignment.ALIGN_OPPOSITE), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasAlignmentSpanBetween(start, end) + .withAlignment(Alignment.ALIGN_OPPOSITE) + .andFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void alignmentSpan_wrongEndIndex() { + SpannableString spannable = SpannableString.valueOf("test with right-aligned section"); + int start = "test with ".length(); + int end = start + "right-aligned".length(); + spannable.setSpan( + new AlignmentSpan.Standard(Alignment.ALIGN_OPPOSITE), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + int incorrectEnd = end + 2; + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasAlignmentSpanBetween(start, incorrectEnd) + .withAlignment(Alignment.ALIGN_OPPOSITE)); + assertThat(expected).factValue("expected").contains("end=" + incorrectEnd); + assertThat(expected).factValue("but found").contains("end=" + end); + } + + @Test + public void alignmentSpan_wrongAlignment() { + SpannableString spannable = SpannableString.valueOf("test with right-aligned section"); + int start = "test with ".length(); + int end = start + "right-aligned".length(); + spannable.setSpan( + new AlignmentSpan.Standard(Alignment.ALIGN_OPPOSITE), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasAlignmentSpanBetween(start, end) + .withAlignment(Alignment.ALIGN_CENTER)); + assertThat(expected).factValue("value of").contains("alignment"); + assertThat(expected).factValue("expected").contains("ALIGN_CENTER"); + assertThat(expected).factValue("but was").contains("ALIGN_OPPOSITE"); + } + + @Test + public void alignmentSpan_wrongFlags() { + SpannableString spannable = SpannableString.valueOf("test with right-aligned section"); + int start = "test with ".length(); + int end = start + "right-aligned".length(); + spannable.setSpan( + new AlignmentSpan.Standard(Alignment.ALIGN_OPPOSITE), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasAlignmentSpanBetween(start, end) + .withAlignment(Alignment.ALIGN_OPPOSITE) + .andFlags(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected).factValue("value of").contains("flags"); + assertThat(expected) + .factValue("expected to contain") + .contains(String.valueOf(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected) + .factValue("but was") + .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + } + + @Test + public void noAlignmentSpan_success() { + SpannableString spannable = + SpannableString.valueOf("test with underline then right-aligned spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new AlignmentSpan.Standard(Alignment.ALIGN_OPPOSITE), + "test with underline then ".length(), + "test with underline then cyan".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoAlignmentSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noAlignmentSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with right-aligned section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new AlignmentSpan.Standard(Alignment.ALIGN_OPPOSITE), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> whenTesting.that(spannable).hasNoAlignmentSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains("Found unexpected AlignmentSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + @Test public void foregroundColorSpan_success() { SpannableString spannable = SpannableString.valueOf("test with cyan section"); From 2c02f787bda975a4d3e9b403bdf634197e4f2c43 Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 9 Jan 2020 10:41:12 +0000 Subject: [PATCH 0628/1335] Remove unused styles from TTML test data PiperOrigin-RevId: 288862795 --- library/core/src/test/assets/ttml/font_size.xml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/library/core/src/test/assets/ttml/font_size.xml b/library/core/src/test/assets/ttml/font_size.xml index a25fff1cf9..986931e547 100644 --- a/library/core/src/test/assets/ttml/font_size.xml +++ b/library/core/src/test/assets/ttml/font_size.xml @@ -3,14 +3,6 @@ xmlns:tts="http://www.w3.org/2006/10/ttaf1#style" xmlns="http://www.w3.org/ns/ttml" xmlns="http://www.w3.org/2006/10/ttaf1"> - - -