diff --git a/.gitignore b/.gitignore index cb4cfaada1..3ab16a94fd 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ bazel-testlogs .DS_Store cmake-build-debug dist +jacoco.exec tmp # External native builds diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7d1191199c..b59b9798a4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,22 +2,29 @@ ### dev-v2 (not yet released) -* Extractors: - * Add support for MP4 and QuickTime meta atoms that are not full atoms. * UI: * Add builder for `PlayerNotificationManager`. + * Add group setting to `PlayerNotificationManager`. * Audio: - * Fix `SimpleExoPlayer` reporting audio session ID as 0 in some cases - ([#8585](https://github.com/google/ExoPlayer/issues/8585)). * Report unexpected discontinuities in `AnalyticsListener.onAudioSinkError` ([#6384](https://github.com/google/ExoPlayer/issues/6384)). + * Allow forcing offload for gapless content even if gapless playback is + not supported. + * Allow fall back from DTS-HD to DTS when playing via passthrough. * Analytics: * Add `onAudioCodecError` and `onVideoCodecError` to `AnalyticsListener`. +* Downloads and caching: + * Fix `CacheWriter` to correctly handle `DataSource.close` failures, for + which it cannot be assumed that data was successfully written to the + cache. * Library restructuring: * `DebugTextViewHelper` moved from `ui` package to `util` package. * Spherical UI components moved from `video.spherical` package to `ui.spherical` package, and made package private. +* Core + * Move `getRendererCount` and `getRendererType` methods from `Player` to + `ExoPlayer`. * Remove deprecated symbols: * Remove `Player.DefaultEventListener`. Use `Player.EventListener` instead. @@ -25,6 +32,33 @@ instead. * Remove `extension-jobdispatcher` module. Use the `extension-workmanager` module instead. +* DRM: + * Only dispatch DRM session acquire and release events once per period + when playing content that uses the same encryption keys for both audio & + video tracks (previously separate acquire and release events were + dispatched for each track in each period). + * Include the session state in DRM session-acquired listener methods. +* UI + * Fix `StyledPlayerView` scrubber not reappearing correctly in some cases + ([#8646](https://github.com/google/ExoPlayer/issues/8646)). +* MediaSession extension: Remove dependency to core module and rely on common + only. The `TimelineQueueEditor` uses a new `MediaDescriptionConverter` for + this purpose and does not rely on the `ConcatenatingMediaSource` anymore. + +### 2.13.2 (2021-02-25) + +* Extractors: + * Add support for MP4 and QuickTime meta atoms that are not full atoms. +* UI: + * Make conditions to enable UI actions consistent in + `DefaultControlDispatcher`, `PlayerControlView`, + `StyledPlayerControlView`, `PlayerNotificationManager` and + `TimelineQueueNavigator`. + * Fix conditions to enable seeking to next/previous media item to handle + the case where a live stream has ended. +* Audio: + * Fix `SimpleExoPlayer` reporting audio session ID as 0 in some cases + ([#8585](https://github.com/google/ExoPlayer/issues/8585)). * IMA extension: * Fix a bug where playback could get stuck when seeking into a playlist item with ads, if the preroll ad had preloaded but the window position @@ -32,13 +66,16 @@ * Fix a bug with playback of ads in playlists, where the incorrect period index was used when deciding whether to trigger playback of an ad after a seek. -* VP9 extension: Update to use NDK r22 +* Text: + * Parse SSA/ASS font size in `Style:` lines + ([#8435](https://github.com/google/ExoPlayer/issues/8435)). +* VP9 extension: Update to use NDK r21 ([#8581](https://github.com/google/ExoPlayer/issues/8581)). -* FLAC extension: Update to use NDK r22 +* FLAC extension: Update to use NDK r21 ([#8581](https://github.com/google/ExoPlayer/issues/8581)). -* Opus extension: Update to use NDK r22 +* Opus extension: Update to use NDK r21 ([#8581](https://github.com/google/ExoPlayer/issues/8581)). -* FFmpeg extension: Update to use NDK r22 +* FFmpeg extension: Update to use NDK r21 ([#8581](https://github.com/google/ExoPlayer/issues/8581)). ### 2.13.1 (2021-02-12) diff --git a/constants.gradle b/constants.gradle index 56c8621def..0c018a940f 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.13.1' - releaseVersionCode = 2013001 + releaseVersion = '2.13.2' + releaseVersionCode = 2013002 minSdkVersion = 16 appTargetSdkVersion = 29 targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest. diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle index 868e3c7b43..a3c13b382d 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -38,6 +38,7 @@ android { "proguard-rules.txt", getDefaultProguardFile('proguard-android.txt') ] + signingConfig signingConfigs.debug } debug { jniDebuggable = true diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml index d92d9e2303..6bb16ed734 100644 --- a/demos/cast/src/main/AndroidManifest.xml +++ b/demos/cast/src/main/AndroidManifest.xml @@ -31,7 +31,8 @@ + android:theme="@style/Theme.AppCompat" + android:exported="true"> diff --git a/demos/gl/build.gradle b/demos/gl/build.gradle index e065f9b8f2..a2ffa7f41f 100644 --- a/demos/gl/build.gradle +++ b/demos/gl/build.gradle @@ -34,6 +34,7 @@ android { shrinkResources true minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt') + signingConfig signingConfigs.debug } } diff --git a/demos/main/build.gradle b/demos/main/build.gradle index c5554993dc..d9ec74e2c2 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -38,6 +38,7 @@ android { "proguard-rules.txt", getDefaultProguardFile('proguard-android.txt') ] + signingConfig signingConfigs.debug } debug { jniDebuggable = true diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 053665502b..39a2ec1709 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -41,7 +41,8 @@ + android:theme="@style/Theme.AppCompat" + android:exported="true"> @@ -65,7 +66,8 @@ android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode" android:launchMode="singleTop" android:label="@string/application_name" - android:theme="@style/PlayerTheme"> + android:theme="@style/PlayerTheme" + android:exported="true"> diff --git a/demos/surface/build.gradle b/demos/surface/build.gradle index bff05901b5..38de169ae5 100644 --- a/demos/surface/build.gradle +++ b/demos/surface/build.gradle @@ -34,6 +34,7 @@ android { shrinkResources true minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt') + signingConfig signingConfigs.debug } } diff --git a/demos/surface/src/main/AndroidManifest.xml b/demos/surface/src/main/AndroidManifest.xml index c33a9e646b..5fd2890915 100644 --- a/demos/surface/src/main/AndroidManifest.xml +++ b/demos/surface/src/main/AndroidManifest.xml @@ -21,7 +21,8 @@ + android:label="@string/application_name" + android:exported="true"> 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 d20b84cbc3..888a7ceb74 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 @@ -484,26 +484,6 @@ public final class CastPlayer extends BasePlayer { sessionManager.endCurrentSession(false); } - @Override - public int getRendererCount() { - // We assume there are three renderers: video, audio, and text. - return RENDERER_COUNT; - } - - @Override - public int getRendererType(int index) { - switch (index) { - case RENDERER_INDEX_VIDEO: - return C.TRACK_TYPE_VIDEO; - case RENDERER_INDEX_AUDIO: - return C.TRACK_TYPE_AUDIO; - case RENDERER_INDEX_TEXT: - return C.TRACK_TYPE_TEXT; - default: - throw new IndexOutOfBoundsException(); - } - } - @Override public void setRepeatMode(@RepeatMode int repeatMode) { if (remoteMediaClient == null) { @@ -708,15 +688,19 @@ public final class CastPlayer extends BasePlayer { } } + @SuppressWarnings("deprecation") // Calling deprecated listener method. private void updateTimelineAndNotifyIfChanged() { if (updateTimeline()) { // TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and // TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553]. + Timeline timeline = currentTimeline; listeners.queueEvent( Player.EVENT_TIMELINE_CHANGED, - listener -> - listener.onTimelineChanged( - currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)); + listener -> { + listener.onTimelineChanged( + timeline, /* manifest= */ null, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + listener.onTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + }); } } 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 2726b00c73..060c70090a 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 @@ -321,7 +321,6 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { // Accessed by the calling thread only. private boolean opened; - private long bytesToSkip; private long bytesRemaining; // Written from the calling thread only. currentUrlRequest.start() calls ensure writes are visible @@ -577,7 +576,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { byte[] responseBody; try { responseBody = readResponseBody(); - } catch (HttpDataSourceException e) { + } catch (IOException e) { responseBody = Util.EMPTY_BYTE_ARRAY; } @@ -607,7 +606,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { // If we requested a range starting from a non-zero position and received a 200 rather than a // 206, then the server does not support partial requests. We'll need to manually skip to the // requested position. - bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0; + long bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0; // Calculate the content length. if (!isCompressed(responseInfo)) { @@ -627,6 +626,14 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { opened = true; transferStarted(dataSpec); + try { + if (!skipFully(bytesToSkip)) { + throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE); + } + } catch (IOException e) { + throw new OpenException(e, dataSpec, Status.READING_RESPONSE); + } + return bytesRemaining; } @@ -641,25 +648,25 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } ByteBuffer readBuffer = getOrCreateReadBuffer(); - while (!readBuffer.hasRemaining()) { + if (!readBuffer.hasRemaining()) { // Fill readBuffer with more data from Cronet. operation.close(); readBuffer.clear(); - readInternal(readBuffer); + try { + readInternal(readBuffer); + } catch (IOException e) { + throw new HttpDataSourceException( + e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ); + } if (finished) { bytesRemaining = 0; return C.RESULT_END_OF_INPUT; - } else { - // The operation didn't time out, fail or finish, and therefore data must have been read. - readBuffer.flip(); - Assertions.checkState(readBuffer.hasRemaining()); - if (bytesToSkip > 0) { - int bytesSkipped = (int) Math.min(readBuffer.remaining(), bytesToSkip); - readBuffer.position(readBuffer.position() + bytesSkipped); - bytesToSkip -= bytesSkipped; - } } + + // The operation didn't time out, fail or finish, and therefore data must have been read. + readBuffer.flip(); + Assertions.checkState(readBuffer.hasRemaining()); } // Ensure we read up to bytesRemaining, in case this was a Range request with finite end, but @@ -718,17 +725,6 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { int readLength = buffer.remaining(); if (readBuffer != null) { - // Skip all the bytes we can from readBuffer if there are still bytes to skip. - if (bytesToSkip != 0) { - if (bytesToSkip >= readBuffer.remaining()) { - bytesToSkip -= readBuffer.remaining(); - readBuffer.position(readBuffer.limit()); - } else { - readBuffer.position(readBuffer.position() + (int) bytesToSkip); - bytesToSkip = 0; - } - } - // If there is existing data in the readBuffer, read as much as possible. Return if any read. int copyBytes = copyByteBuffer(/* src= */ readBuffer, /* dst= */ buffer); if (copyBytes != 0) { @@ -740,44 +736,23 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } } - boolean readMore = true; - while (readMore) { - // If bytesToSkip > 0, read into intermediate buffer that we can discard instead of caller's - // buffer. If we do not need to skip bytes, we may write to buffer directly. - final boolean useCallerBuffer = bytesToSkip == 0; - - operation.close(); - - if (!useCallerBuffer) { - ByteBuffer readBuffer = getOrCreateReadBuffer(); - readBuffer.clear(); - if (bytesToSkip < READ_BUFFER_SIZE_BYTES) { - readBuffer.limit((int) bytesToSkip); - } - } - - // Fill buffer with more data from Cronet. - readInternal(useCallerBuffer ? buffer : castNonNull(readBuffer)); - - if (finished) { - bytesRemaining = 0; - return C.RESULT_END_OF_INPUT; - } else { - // The operation didn't time out, fail or finish, and therefore data must have been read. - Assertions.checkState( - useCallerBuffer - ? readLength > buffer.remaining() - : castNonNull(readBuffer).position() > 0); - // If we meant to skip bytes, subtract what was left and repeat, otherwise, continue. - if (useCallerBuffer) { - readMore = false; - } else { - bytesToSkip -= castNonNull(readBuffer).position(); - } - } + // Fill buffer with more data from Cronet. + operation.close(); + try { + readInternal(buffer); + } catch (IOException e) { + throw new HttpDataSourceException( + e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ); } - final int bytesRead = readLength - buffer.remaining(); + if (finished) { + bytesRemaining = 0; + return C.RESULT_END_OF_INPUT; + } + + // The operation didn't time out, fail or finish, and therefore data must have been read. + Assertions.checkState(readLength > buffer.remaining()); + int bytesRead = readLength - buffer.remaining(); if (bytesRemaining != C.LENGTH_UNSET) { bytesRemaining -= bytesRead; } @@ -885,13 +860,49 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs; } + /** + * Attempts to skip the specified number of bytes in full. + * + * @param bytesToSkip The number of bytes to skip. + * @throws InterruptedIOException If the thread is interrupted during the operation. + * @throws IOException If an error occurs reading from the source. + * @return Whether the bytes were skipped in full. If {@code false} then the data ended before the + * specified number of bytes were skipped. Always {@code true} if {@code bytesToSkip == 0}. + */ + private boolean skipFully(long bytesToSkip) throws IOException { + if (bytesToSkip == 0) { + return true; + } + ByteBuffer readBuffer = getOrCreateReadBuffer(); + while (bytesToSkip > 0) { + // Fill readBuffer with more data from Cronet. + operation.close(); + readBuffer.clear(); + readInternal(readBuffer); + if (Thread.currentThread().isInterrupted()) { + throw new InterruptedIOException(); + } + if (finished) { + return false; + } else { + // The operation didn't time out, fail or finish, and therefore data must have been read. + readBuffer.flip(); + Assertions.checkState(readBuffer.hasRemaining()); + int bytesSkipped = (int) Math.min(readBuffer.remaining(), bytesToSkip); + readBuffer.position(readBuffer.position() + bytesSkipped); + bytesToSkip -= bytesSkipped; + } + } + return true; + } + /** * Reads the whole response body. * * @return The response body. - * @throws HttpDataSourceException If an error occurs reading from the source. + * @throws IOException If an error occurs reading from the source. */ - private byte[] readResponseBody() throws HttpDataSourceException { + private byte[] readResponseBody() throws IOException { byte[] responseBody = Util.EMPTY_BYTE_ARRAY; ByteBuffer readBuffer = getOrCreateReadBuffer(); while (!finished) { @@ -914,10 +925,10 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * the current {@code readBuffer} object so that it is not reused in the future. * * @param buffer The ByteBuffer into which the read data is stored. Must be a direct ByteBuffer. - * @throws HttpDataSourceException If an error occurs reading from the source. + * @throws IOException If an error occurs reading from the source. */ @SuppressWarnings("ReferenceEquality") - private void readInternal(ByteBuffer buffer) throws HttpDataSourceException { + private void readInternal(ByteBuffer buffer) throws IOException { castNonNull(currentUrlRequest).read(buffer); try { if (!operation.block(readTimeoutMs)) { @@ -930,23 +941,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { readBuffer = null; } Thread.currentThread().interrupt(); - throw new HttpDataSourceException( - new InterruptedIOException(), - castNonNull(currentDataSpec), - HttpDataSourceException.TYPE_READ); + throw new InterruptedIOException(); } catch (SocketTimeoutException e) { // The operation is ongoing so replace buffer to avoid it being written to by this // operation during a subsequent request. if (buffer == readBuffer) { readBuffer = null; } - throw new HttpDataSourceException( - e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ); + throw e; } if (exception != null) { - throw new HttpDataSourceException( - exception, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ); + throw exception; } } diff --git a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index 631e1300d6..52d5c3fbe8 100644 --- a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -256,6 +256,7 @@ public final class CronetDataSourceTest { public void requestSetsRangeHeader() throws HttpDataSourceException { testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000); mockResponseStartSuccess(); + mockReadSuccess(0, 1000); dataSourceUnderTest.open(testDataSpec); // The header value to add is current position to current position + length - 1. @@ -287,8 +288,6 @@ public final class CronetDataSourceTest { testDataSpec = new DataSpec.Builder() .setUri(TEST_URL) - .setPosition(1000) - .setLength(5000) .setHttpRequestHeaders(dataSpecRequestProperties) .build(); mockResponseStartSuccess(); @@ -1198,6 +1197,7 @@ public final class CronetDataSourceTest { dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE); mockSingleRedirectSuccess(); + mockReadSuccess(0, 1000); testResponseHeader.put("Set-Cookie", "testcookie=testcookie; Path=/video"); @@ -1368,7 +1368,7 @@ public final class CronetDataSourceTest { @Test public void allowDirectExecutor() throws HttpDataSourceException { - testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000); + testDataSpec = new DataSpec(Uri.parse(TEST_URL)); mockResponseStartSuccess(); dataSourceUnderTest.open(testDataSpec); diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index 68eafd2926..da2fa40141 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -30,7 +30,7 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main" ``` * Download the [Android NDK][] and set its location in a shell variable. - This build configuration has been tested on NDK r22. + This build configuration has been tested on NDK r21. ``` NDK_PATH="" diff --git a/extensions/flac/README.md b/extensions/flac/README.md index 5b98e33364..074daca71e 100644 --- a/extensions/flac/README.md +++ b/extensions/flac/README.md @@ -29,7 +29,7 @@ FLAC_EXT_PATH="${EXOPLAYER_ROOT}/extensions/flac/src/main" ``` * Download the [Android NDK][] and set its location in an environment variable. - This build configuration has been tested on NDK r22. + This build configuration has been tested on NDK r21. ``` NDK_PATH="" diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java index 39f9f36fd4..f3f9c1749f 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java @@ -60,6 +60,7 @@ import com.google.android.exoplayer2.source.ads.AdsLoader.EventListener; import com.google.android.exoplayer2.source.ads.AdsLoader.OverlayInfo; import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionUtil; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; @@ -700,12 +701,7 @@ import java.util.Map; // Check for a selected track using an audio renderer. TrackSelectionArray trackSelections = player.getCurrentTrackSelections(); - for (int i = 0; i < player.getRendererCount() && i < trackSelections.length; i++) { - if (player.getRendererType(i) == C.TRACK_TYPE_AUDIO && trackSelections.get(i) != null) { - return 100; - } - } - return 0; + return TrackSelectionUtil.hasTrackOfType(trackSelections, C.TRACK_TYPE_AUDIO) ? 100 : 0; } private void handleAdEvent(AdEvent adEvent) { diff --git a/extensions/media2/build.gradle b/extensions/media2/build.gradle index 49621da3a8..da70210bd6 100644 --- a/extensions/media2/build.gradle +++ b/extensions/media2/build.gradle @@ -13,8 +13,6 @@ // limitations under the License. apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle" -android.defaultConfig.minSdkVersion 19 - dependencies { implementation project(modulePrefix + 'library-common') implementation 'androidx.collection:collection:' + androidxCollectionVersion diff --git a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnectorTest.java b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnectorTest.java index edabd55812..34cf19f86f 100644 --- a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnectorTest.java +++ b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnectorTest.java @@ -28,8 +28,6 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.content.Context; import android.media.AudioManager; -import android.os.Build; -import android.os.Build.VERSION_CODES; import android.os.Looper; import androidx.annotation.Nullable; import androidx.core.util.ObjectsCompat; @@ -43,7 +41,6 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; import androidx.test.filters.MediumTest; -import androidx.test.filters.SdkSuppress; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; import com.google.android.exoplayer2.ControlDispatcher; @@ -93,7 +90,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void play_onceWithAudioResource_changesPlayerStateToPlaying() throws Exception { TestUtils.loadResource(R.raw.audio, sessionPlayerConnector); @@ -120,7 +116,6 @@ public class SessionPlayerConnectorTest { @Test @MediumTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void play_onceWithAudioResourceOnMainThread_notifiesOnPlayerStateChanged() throws Exception { CountDownLatch onPlayerStatePlayingLatch = new CountDownLatch(1); @@ -158,7 +153,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void play_withCustomControlDispatcher_isSkipped() throws Exception { if (Looper.myLooper() == null) { Looper.prepare(); @@ -194,7 +188,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void setMediaItem_withAudioResource_notifiesOnPlaybackCompleted() throws Exception { TestUtils.loadResource(R.raw.audio, sessionPlayerConnector); @@ -219,7 +212,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void setMediaItem_withVideoResource_notifiesOnPlaybackCompleted() throws Exception { TestUtils.loadResource(R.raw.video_desks, sessionPlayerConnector); CountDownLatch onPlaybackCompletedLatch = new CountDownLatch(1); @@ -243,7 +235,6 @@ public class SessionPlayerConnectorTest { @Test @SmallTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void getDuration_whenIdleState_returnsUnknownTime() { assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_IDLE); assertThat(sessionPlayerConnector.getDuration()).isEqualTo(SessionPlayer.UNKNOWN_TIME); @@ -251,7 +242,6 @@ public class SessionPlayerConnectorTest { @Test @MediumTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void getDuration_afterPrepared_returnsDuration() throws Exception { TestUtils.loadResource(R.raw.video_desks, sessionPlayerConnector); @@ -263,7 +253,6 @@ public class SessionPlayerConnectorTest { @Test @SmallTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void getCurrentPosition_whenIdleState_returnsDefaultPosition() { assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_IDLE); assertThat(sessionPlayerConnector.getCurrentPosition()).isEqualTo(0); @@ -271,7 +260,6 @@ public class SessionPlayerConnectorTest { @Test @SmallTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void getBufferedPosition_whenIdleState_returnsDefaultPosition() { assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_IDLE); assertThat(sessionPlayerConnector.getBufferedPosition()).isEqualTo(0); @@ -279,7 +267,6 @@ public class SessionPlayerConnectorTest { @Test @SmallTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void getPlaybackSpeed_whenIdleState_throwsNoException() { assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_IDLE); try { @@ -291,7 +278,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void play_withDataSourceCallback_changesPlayerState() throws Exception { sessionPlayerConnector.setMediaItem(TestUtils.createMediaItem(R.raw.video_big_buck_bunny)); sessionPlayerConnector.prepare(); @@ -308,7 +294,6 @@ public class SessionPlayerConnectorTest { @Test @SmallTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void setMediaItem_withNullMediaItem_throwsException() { try { sessionPlayerConnector.setMediaItem(null); @@ -320,7 +305,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void setPlaybackSpeed_afterPlayback_remainsSame() throws Exception { int resId1 = R.raw.video_big_buck_bunny; MediaItem mediaItem1 = @@ -363,7 +347,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void seekTo_withSeriesOfSeek_succeeds() throws Exception { TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector); @@ -378,7 +361,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void seekTo_skipsUnnecessarySeek() throws Exception { CountDownLatch readAllowedLatch = new CountDownLatch(1); playerTestRule.setDataSourceInstrumentation( @@ -435,7 +417,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void seekTo_whenUnderlyingPlayerAlsoSeeks_throwsNoException() throws Exception { TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector); assertPlayerResultSuccess(sessionPlayerConnector.prepare()); @@ -456,7 +437,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void seekTo_byUnderlyingPlayer_notifiesOnSeekCompleted() throws Exception { TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector); assertPlayerResultSuccess(sessionPlayerConnector.prepare()); @@ -484,7 +464,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void getPlayerState_withCallingPrepareAndPlayAndPause_reflectsPlayerState() throws Throwable { TestUtils.loadResource(R.raw.video_desks, sessionPlayerConnector); @@ -521,7 +500,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = VERSION_CODES.KITKAT) public void prepare_twice_finishes() throws Exception { TestUtils.loadResource(R.raw.audio, sessionPlayerConnector); assertPlayerResultSuccess(sessionPlayerConnector.prepare()); @@ -530,7 +508,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void prepare_notifiesOnPlayerStateChanged() throws Throwable { TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector); @@ -552,7 +529,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void prepare_notifiesBufferingCompletedOnce() throws Throwable { TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector); @@ -587,7 +563,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void seekTo_whenPrepared_notifiesOnSeekCompleted() throws Throwable { long mp4DurationMs = 8_484L; TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector); @@ -611,7 +586,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void setPlaybackSpeed_whenPrepared_notifiesOnPlaybackSpeedChanged() throws Throwable { TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector); @@ -636,7 +610,6 @@ public class SessionPlayerConnectorTest { @Test @SmallTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void setPlaybackSpeed_withZeroSpeed_throwsException() { try { sessionPlayerConnector.setPlaybackSpeed(0.0f); @@ -648,7 +621,6 @@ public class SessionPlayerConnectorTest { @Test @SmallTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void setPlaybackSpeed_withNegativeSpeed_throwsException() { try { sessionPlayerConnector.setPlaybackSpeed(-1.0f); @@ -660,7 +632,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void close_throwsNoExceptionAndDoesNotCrash() throws Exception { TestUtils.loadResource(R.raw.audio, sessionPlayerConnector); AudioAttributesCompat attributes = @@ -679,7 +650,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void cancelReturnedFuture_withSeekTo_cancelsPendingCommand() throws Exception { CountDownLatch readRequestedLatch = new CountDownLatch(1); CountDownLatch readAllowedLatch = new CountDownLatch(1); @@ -719,7 +689,6 @@ public class SessionPlayerConnectorTest { @Test @SmallTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void setPlaylist_withNullPlaylist_throwsException() throws Exception { try { sessionPlayerConnector.setPlaylist(null, null); @@ -731,7 +700,6 @@ public class SessionPlayerConnectorTest { @Test @SmallTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void setPlaylist_withPlaylistContainingNullItem_throwsException() { try { List list = new ArrayList<>(); @@ -745,7 +713,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void setPlaylist_setsPlaylistAndCurrentMediaItem() throws Exception { List playlist = TestUtils.createPlaylist(10); PlayerCallbackForPlaylist callback = new PlayerCallbackForPlaylist(playlist, 1); @@ -760,7 +727,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void setPlaylistAndRemoveAllPlaylistItem_playerStateBecomesIdle() throws Exception { List playlist = new ArrayList<>(); playlist.add(TestUtils.createMediaItem(R.raw.video_1)); @@ -786,7 +752,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void setPlaylist_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception { List playlist = TestUtils.createPlaylist(10); CountDownLatch onPlaylistChangedLatch = new CountDownLatch(2); @@ -811,7 +776,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void setPlaylist_byUnderlyingPlayerBeforePrepare_notifiesOnPlaylistChanged() throws Exception { List playlistToExoPlayer = TestUtils.createPlaylist(4); @@ -842,7 +806,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void setPlaylist_byUnderlyingPlayerAfterPrepare_notifiesOnPlaylistChanged() throws Exception { List playlistToSessionPlayer = TestUtils.createPlaylist(2); @@ -876,7 +839,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void addPlaylistItem_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception { List playlist = TestUtils.createPlaylist(10); assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, /* metadata= */ null)); @@ -905,7 +867,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void removePlaylistItem_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception { List playlist = TestUtils.createPlaylist(10); assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, /* metadata= */ null)); @@ -933,7 +894,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void movePlaylistItem_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception { List playlist = new ArrayList<>(); playlist.add(TestUtils.createMediaItem(R.raw.video_1)); @@ -967,7 +927,6 @@ public class SessionPlayerConnectorTest { @Ignore @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void replacePlaylistItem_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception { List playlist = TestUtils.createPlaylist(10); assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, /* metadata= */ null)); @@ -996,7 +955,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void setPlaylist_withPlaylist_notifiesOnCurrentMediaItemChanged() throws Exception { int listSize = 2; List playlist = TestUtils.createPlaylist(listSize); @@ -1011,7 +969,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void play_twice_finishes() throws Exception { TestUtils.loadResource(R.raw.audio, sessionPlayerConnector); assertPlayerResultSuccess(sessionPlayerConnector.prepare()); @@ -1021,7 +978,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void play_withPlaylist_notifiesOnCurrentMediaItemChangedAndOnPlaybackCompleted() throws Exception { List playlist = new ArrayList<>(); @@ -1060,7 +1016,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void play_byUnderlyingPlayer_notifiesOnPlayerStateChanges() throws Exception { TestUtils.loadResource(R.raw.audio, sessionPlayerConnector); SimpleExoPlayer simpleExoPlayer = playerTestRule.getSimpleExoPlayer(); @@ -1086,7 +1041,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void pause_twice_finishes() throws Exception { TestUtils.loadResource(R.raw.audio, sessionPlayerConnector); assertPlayerResultSuccess(sessionPlayerConnector.prepare()); @@ -1097,7 +1051,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void pause_byUnderlyingPlayer_notifiesOnPlayerStateChanges() throws Exception { TestUtils.loadResource(R.raw.audio, sessionPlayerConnector); SimpleExoPlayer simpleExoPlayer = playerTestRule.getSimpleExoPlayer(); @@ -1124,7 +1077,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void pause_byUnderlyingPlayerInListener_changesToPlayerStatePaused() throws Exception { TestUtils.loadResource(R.raw.audio, sessionPlayerConnector); SimpleExoPlayer simpleExoPlayer = playerTestRule.getSimpleExoPlayer(); @@ -1169,7 +1121,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void skipToNextAndPrevious_calledInARow_notifiesOnCurrentMediaItemChanged() throws Exception { List playlist = new ArrayList<>(); @@ -1221,7 +1172,6 @@ public class SessionPlayerConnectorTest { @Test @LargeTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void setRepeatMode_withRepeatAll_continuesToPlayPlaylistWithoutBeingCompleted() throws Exception { List playlist = new ArrayList<>(); diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle index 5c827084da..9b812911ab 100644 --- a/extensions/mediasession/build.gradle +++ b/extensions/mediasession/build.gradle @@ -14,7 +14,7 @@ apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle" dependencies { - implementation project(modulePrefix + 'library-core') + implementation project(modulePrefix + 'library-common') api 'androidx.media:media:' + androidxMediaVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java index 7f60d5e715..cab16744b9 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java @@ -23,15 +23,13 @@ import android.support.v4.media.session.MediaSessionCompat; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ControlDispatcher; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.util.Util; import java.util.List; /** - * A {@link MediaSessionConnector.QueueEditor} implementation based on the {@link - * ConcatenatingMediaSource}. + * A {@link MediaSessionConnector.QueueEditor} implementation. * *

This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles * the {@link #COMMAND_MOVE_QUEUE_ITEM} to move a queue item instead of removing and inserting it. @@ -44,18 +42,17 @@ public final class TimelineQueueEditor public static final String EXTRA_FROM_INDEX = "from_index"; public static final String EXTRA_TO_INDEX = "to_index"; - /** - * Factory to create {@link MediaSource}s. - */ - public interface MediaSourceFactory { + /** Converts a {@link MediaDescriptionCompat} to a {@link MediaItem}. */ + public interface MediaDescriptionConverter { /** - * Creates a {@link MediaSource} for the given {@link MediaDescriptionCompat}. + * Returns a {@link MediaItem} for the given {@link MediaDescriptionCompat} or null if the + * description can't be converted. * - * @param description The {@link MediaDescriptionCompat} to create a media source for. - * @return A {@link MediaSource} or {@code null} if no source can be created for the given - * description. + *

If not null, the media item that is returned will be used to call {@link + * Player#addMediaItem(MediaItem)}. */ - @Nullable MediaSource createMediaSource(MediaDescriptionCompat description); + @Nullable + MediaItem convert(MediaDescriptionCompat description); } /** @@ -110,51 +107,46 @@ public final class TimelineQueueEditor public boolean equals(MediaDescriptionCompat d1, MediaDescriptionCompat d2) { return Util.areEqual(d1.getMediaId(), d2.getMediaId()); } - } private final MediaControllerCompat mediaController; private final QueueDataAdapter queueDataAdapter; - private final MediaSourceFactory sourceFactory; + private final MediaDescriptionConverter mediaDescriptionConverter; private final MediaDescriptionEqualityChecker equalityChecker; - private final ConcatenatingMediaSource queueMediaSource; /** * Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory. * * @param mediaController A {@link MediaControllerCompat} to read the current queue. - * @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate. * @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data. - * @param sourceFactory The {@link MediaSourceFactory} to build media sources. + * @param mediaDescriptionConverter The {@link MediaDescriptionConverter} for converting media + * descriptions to {@link MediaItem MediaItems}. */ public TimelineQueueEditor( MediaControllerCompat mediaController, - ConcatenatingMediaSource queueMediaSource, QueueDataAdapter queueDataAdapter, - MediaSourceFactory sourceFactory) { - this(mediaController, queueMediaSource, queueDataAdapter, sourceFactory, - new MediaIdEqualityChecker()); + MediaDescriptionConverter mediaDescriptionConverter) { + this( + mediaController, queueDataAdapter, mediaDescriptionConverter, new MediaIdEqualityChecker()); } /** * Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory. * * @param mediaController A {@link MediaControllerCompat} to read the current queue. - * @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate. * @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data. - * @param sourceFactory The {@link MediaSourceFactory} to build media sources. + * @param mediaDescriptionConverter The {@link MediaDescriptionConverter} for converting media + * descriptions to {@link MediaItem MediaItems}. * @param equalityChecker The {@link MediaDescriptionEqualityChecker} to match queue items. */ public TimelineQueueEditor( MediaControllerCompat mediaController, - ConcatenatingMediaSource queueMediaSource, QueueDataAdapter queueDataAdapter, - MediaSourceFactory sourceFactory, + MediaDescriptionConverter mediaDescriptionConverter, MediaDescriptionEqualityChecker equalityChecker) { this.mediaController = mediaController; - this.queueMediaSource = queueMediaSource; this.queueDataAdapter = queueDataAdapter; - this.sourceFactory = sourceFactory; + this.mediaDescriptionConverter = mediaDescriptionConverter; this.equalityChecker = equalityChecker; } @@ -165,10 +157,10 @@ public final class TimelineQueueEditor @Override public void onAddQueueItem(Player player, MediaDescriptionCompat description, int index) { - @Nullable MediaSource mediaSource = sourceFactory.createMediaSource(description); - if (mediaSource != null) { + @Nullable MediaItem mediaItem = mediaDescriptionConverter.convert(description); + if (mediaItem != null) { queueDataAdapter.add(index, description); - queueMediaSource.addMediaSource(index, mediaSource); + player.addMediaItem(index, mediaItem); } } @@ -178,7 +170,7 @@ public final class TimelineQueueEditor for (int i = 0; i < queue.size(); i++) { if (equalityChecker.equals(queue.get(i).getDescription(), description)) { queueDataAdapter.remove(i); - queueMediaSource.removeMediaSource(i); + player.removeMediaItem(i); return; } } @@ -200,9 +192,8 @@ public final class TimelineQueueEditor int to = extras.getInt(EXTRA_TO_INDEX, C.INDEX_UNSET); if (from != C.INDEX_UNSET && to != C.INDEX_UNSET) { queueDataAdapter.move(from, to); - queueMediaSource.moveMediaSource(from, to); + player.moveMediaItem(from, to); } return true; } - } diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index 203479a7ed..4c737268a2 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -98,8 +98,10 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu if (!timeline.isEmpty() && !player.isPlayingAd()) { timeline.getWindow(player.getCurrentWindowIndex(), window); enableSkipTo = timeline.getWindowCount() > 1; - enablePrevious = window.isSeekable || !window.isDynamic || player.hasPrevious(); - enableNext = window.isDynamic || player.hasNext(); + enablePrevious = window.isSeekable || !window.isLive() || player.hasPrevious(); + enableNext = + (window.isLive() && window.isDynamic) + || player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM); } long actions = 0; 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 d23dd22574..0d741b583d 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 @@ -168,8 +168,6 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { } } - private static final byte[] SKIP_BUFFER = new byte[4096]; - private final Call.Factory callFactory; private final RequestProperties requestProperties; @@ -183,10 +181,8 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { @Nullable private InputStream responseByteStream; private boolean opened; - private long bytesToSkip; - private long bytesToRead; - private long bytesSkipped; + private long bytesToRead; private long bytesRead; /** @deprecated Use {@link OkHttpDataSource.Factory} instead. */ @@ -332,7 +328,7 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { // If we requested a range starting from a non-zero position and received a 200 rather than a // 206, then the server does not support partial requests. We'll need to manually skip to the // requested position. - bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0; + long bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0; // Determine the length of the data to be read, after skipping. if (dataSpec.length != C.LENGTH_UNSET) { @@ -345,13 +341,21 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { opened = true; transferStarted(dataSpec); + try { + if (!skipFully(bytesToSkip)) { + throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE); + } + } catch (IOException e) { + closeConnectionQuietly(); + throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_OPEN); + } + return bytesToRead; } @Override public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException { try { - skipInternal(); return readInternal(buffer, offset, readLength); } catch (IOException e) { throw new HttpDataSourceException( @@ -369,8 +373,8 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { } /** - * Returns the number of bytes that have been skipped since the most recent call to - * {@link #open(DataSpec)}. + * Returns the number of bytes that were skipped during the most recent call to {@link + * #open(DataSpec)}. * * @return The number of bytes skipped. */ @@ -454,30 +458,32 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { } /** - * Skips any bytes that need skipping. Else does nothing. - *

- * This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}. + * Attempts to skip the specified number of bytes in full. * + * @param bytesToSkip The number of bytes to skip. * @throws InterruptedIOException If the thread is interrupted during the operation. - * @throws EOFException If the end of the input stream is reached before the bytes are skipped. + * @throws IOException If an error occurs reading from the source. + * @return Whether the bytes were skipped in full. If {@code false} then the data ended before the + * specified number of bytes were skipped. Always {@code true} if {@code bytesToSkip == 0}. */ - private void skipInternal() throws IOException { - if (bytesSkipped == bytesToSkip) { - return; + private boolean skipFully(long bytesToSkip) throws IOException { + if (bytesToSkip == 0) { + return true; } - + byte[] skipBuffer = new byte[4096]; while (bytesSkipped != bytesToSkip) { - int readLength = (int) min(bytesToSkip - bytesSkipped, SKIP_BUFFER.length); - int read = castNonNull(responseByteStream).read(SKIP_BUFFER, 0, readLength); + int readLength = (int) min(bytesToSkip - bytesSkipped, skipBuffer.length); + int read = castNonNull(responseByteStream).read(skipBuffer, 0, readLength); if (Thread.currentThread().isInterrupted()) { throw new InterruptedIOException(); } if (read == -1) { - throw new EOFException(); + return false; } bytesSkipped += read; bytesTransferred(read); } + return true; } /** diff --git a/extensions/opus/README.md b/extensions/opus/README.md index 6a68a1946b..4daff54abf 100644 --- a/extensions/opus/README.md +++ b/extensions/opus/README.md @@ -29,7 +29,7 @@ OPUS_EXT_PATH="${EXOPLAYER_ROOT}/extensions/opus/src/main" ``` * Download the [Android NDK][] and set its location in an environment variable. - This build configuration has been tested on NDK r22. + This build configuration has been tested on NDK r21. ``` NDK_PATH="" diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 3abf72758d..30b7a252d0 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -29,7 +29,7 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main" ``` * Download the [Android NDK][] and set its location in an environment variable. - This build configuration has been tested on NDK r22. + This build configuration has been tested on NDK r21. ``` NDK_PATH="" diff --git a/library/common/src/main/java/com/google/android/exoplayer2/BasePlayer.java b/library/common/src/main/java/com/google/android/exoplayer2/BasePlayer.java index e402fa7306..9b09c0988f 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/BasePlayer.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/BasePlayer.java @@ -30,44 +30,44 @@ public abstract class BasePlayer implements Player { } @Override - public void setMediaItem(MediaItem mediaItem) { + public final void setMediaItem(MediaItem mediaItem) { setMediaItems(Collections.singletonList(mediaItem)); } @Override - public void setMediaItem(MediaItem mediaItem, long startPositionMs) { + public final void setMediaItem(MediaItem mediaItem, long startPositionMs) { setMediaItems(Collections.singletonList(mediaItem), /* startWindowIndex= */ 0, startPositionMs); } @Override - public void setMediaItem(MediaItem mediaItem, boolean resetPosition) { + public final void setMediaItem(MediaItem mediaItem, boolean resetPosition) { setMediaItems(Collections.singletonList(mediaItem), resetPosition); } @Override - public void setMediaItems(List mediaItems) { + public final void setMediaItems(List mediaItems) { setMediaItems(mediaItems, /* resetPosition= */ true); } @Override - public void addMediaItem(int index, MediaItem mediaItem) { + public final void addMediaItem(int index, MediaItem mediaItem) { addMediaItems(index, Collections.singletonList(mediaItem)); } @Override - public void addMediaItem(MediaItem mediaItem) { + public final void addMediaItem(MediaItem mediaItem) { addMediaItems(Collections.singletonList(mediaItem)); } @Override - public void moveMediaItem(int currentIndex, int newIndex) { + public final void moveMediaItem(int currentIndex, int newIndex) { if (currentIndex != newIndex) { moveMediaItems(/* fromIndex= */ currentIndex, /* toIndex= */ currentIndex + 1, newIndex); } } @Override - public void removeMediaItem(int index) { + public final void removeMediaItem(int index) { removeMediaItems(/* fromIndex= */ index, /* toIndex= */ index + 1); } @@ -137,6 +137,11 @@ public abstract class BasePlayer implements Player { } } + @Override + public final void setPlaybackSpeed(float speed) { + setPlaybackParameters(getPlaybackParameters().withSpeed(speed)); + } + @Override public final void stop() { stop(/* reset= */ false); @@ -188,12 +193,12 @@ public abstract class BasePlayer implements Player { } @Override - public int getMediaItemCount() { + public final int getMediaItemCount() { return getCurrentTimeline().getWindowCount(); } @Override - public MediaItem getMediaItemAt(int index) { + public final MediaItem getMediaItemAt(int index) { return getCurrentTimeline().getWindow(index, window).mediaItem; } diff --git a/library/common/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java b/library/common/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java index fe23f28db7..03f03d5903 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java @@ -79,11 +79,12 @@ public class DefaultControlDispatcher implements ControlDispatcher { int windowIndex = player.getCurrentWindowIndex(); timeline.getWindow(windowIndex, window); int previousWindowIndex = player.getPreviousWindowIndex(); + boolean isUnseekableLiveStream = window.isLive() && !window.isSeekable; if (previousWindowIndex != C.INDEX_UNSET && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS - || (window.isDynamic && !window.isSeekable))) { + || isUnseekableLiveStream)) { player.seekTo(previousWindowIndex, C.TIME_UNSET); - } else { + } else if (!isUnseekableLiveStream) { player.seekTo(windowIndex, /* positionMs= */ 0); } return true; @@ -96,10 +97,11 @@ public class DefaultControlDispatcher implements ControlDispatcher { return true; } int windowIndex = player.getCurrentWindowIndex(); + timeline.getWindow(windowIndex, window); int nextWindowIndex = player.getNextWindowIndex(); if (nextWindowIndex != C.INDEX_UNSET) { player.seekTo(nextWindowIndex, C.TIME_UNSET); - } else if (timeline.getWindow(windowIndex, window).isLive()) { + } else if (window.isLive() && window.isDynamic) { player.seekTo(windowIndex, C.TIME_UNSET); } return true; diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 0315cfc9cd..24c8505d98 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -30,11 +30,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.13.1"; + public static final String VERSION = "2.13.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.13.1"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.13.2"; /** * The version of the library expressed as an integer, for example 1002003. @@ -44,7 +44,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 = 2013001; + public static final int VERSION_INT = 2013002; /** * The default user agent for requests made by the library. diff --git a/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java b/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java index b51d8f7446..ebc41366b2 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java @@ -25,6 +25,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; @@ -937,6 +938,7 @@ public final class MediaItem implements Bundleable { // Bundleable implementation. + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({ FIELD_TARGET_OFFSET_MS, @@ -1148,6 +1150,7 @@ public final class MediaItem implements Bundleable { // Bundleable implementation. + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({ FIELD_START_POSITION_MS, @@ -1254,6 +1257,7 @@ public final class MediaItem implements Bundleable { // Bundleable implementation. + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({ FIELD_MEDIA_ID, diff --git a/library/common/src/main/java/com/google/android/exoplayer2/MediaMetadata.java b/library/common/src/main/java/com/google/android/exoplayer2/MediaMetadata.java index 0094258564..aeb0c87976 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/MediaMetadata.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/MediaMetadata.java @@ -19,6 +19,7 @@ import android.os.Bundle; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -69,10 +70,9 @@ public final class MediaMetadata implements Bundleable { // Bundleable implementation. + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({ - FIELD_TITLE, - }) + @IntDef({FIELD_TITLE}) private @interface FieldNumber {} private static final int FIELD_TITLE = 0; diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Player.java b/library/common/src/main/java/com/google/android/exoplayer2/Player.java index ad4875a5fe..d444105656 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/Player.java @@ -55,11 +55,8 @@ import java.util.List; * which can be obtained by calling {@link #getCurrentTimeline()}. *

  • They can provide a {@link TrackGroupArray} defining the currently available tracks, which * can be obtained by calling {@link #getCurrentTrackGroups()}. - *
  • They contain a number of renderers, each of which is able to render tracks of a single type - * (e.g. audio, video or text). The number of renderers and their respective track types can - * be obtained by calling {@link #getRendererCount()} and {@link #getRendererType(int)}. *
  • They can provide a {@link TrackSelectionArray} defining which of the currently available - * tracks are selected to be rendered by each renderer. This can be obtained by calling {@link + * tracks are selected to be rendered. This can be obtained by calling {@link * #getCurrentTrackSelections()}}. * */ @@ -130,13 +127,17 @@ public interface Player { void clearAuxEffectInfo(); /** - * Sets the audio volume, with 0 being silence and 1 being unity gain. + * Sets the audio volume, with 0 being silence and 1 being unity gain (signal unchanged). * - * @param audioVolume The audio volume. + * @param audioVolume Linear output gain to apply to all audio channels. */ void setVolume(float audioVolume); - /** Returns the audio volume, with 0 being silence and 1 being unity gain. */ + /** + * Returns the audio volume, with 0 being silence and 1 being unity gain (signal unchanged). + * + * @return The linear gain applied to all audio channels. + */ float getVolume(); /** @@ -400,30 +401,9 @@ public interface Player { * @param timeline The latest timeline. Never null, but may be empty. * @param reason The {@link TimelineChangeReason} responsible for this timeline change. */ - @SuppressWarnings("deprecation") - default void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) { - Object manifest = null; - if (timeline.getWindowCount() == 1) { - // Legacy behavior was to report the manifest for single window timelines only. - Timeline.Window window = new Timeline.Window(); - manifest = timeline.getWindow(0, window).manifest; - } - // Call deprecated version. - onTimelineChanged(timeline, manifest, reason); - } + default void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {} /** - * Called when the timeline and/or manifest has been refreshed. - * - *

    Note that if the timeline has changed then a position discontinuity may also have - * occurred. For example, the current period index may have changed as a result of periods being - * added or removed from the timeline. This will not be reported via a separate call to - * {@link #onPositionDiscontinuity(int)}. - * - * @param timeline The latest timeline. Never null, but may be empty. - * @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, * window).manifest} for a given window index. @@ -455,8 +435,10 @@ public interface Player { * other events that happen in the same {@link Looper} message queue iteration. * * @param trackGroups The available tracks. Never null, but may be of length zero. - * @param trackSelections The track selections for each renderer. Never null and always of - * length {@link #getRendererCount()}, but may contain null elements. + * @param trackSelections The selected tracks. Never null, but may contain null elements. A + * concrete implementation may include null elements if it has a fixed number of renderer + * components, wishes to report a TrackSelection for each of them, and has one or more + * renderer components that is not assigned any selected tracks. */ default void onTracksChanged( TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {} @@ -488,10 +470,7 @@ public interface Player { * * @param isLoading Whether the source is currently being loaded. */ - @SuppressWarnings("deprecation") - default void onIsLoadingChanged(boolean isLoading) { - onLoadingChanged(isLoading); - } + default void onIsLoadingChanged(boolean isLoading) {} /** @deprecated Use {@link #onIsLoadingChanged(boolean)} instead. */ @Deprecated @@ -1131,6 +1110,7 @@ public interface Player { * Returns the current {@link State playback state} of the player. * * @return The current {@link State playback state}. + * @see EventListener#onPlaybackStateChanged(int) */ @State int getPlaybackState(); @@ -1140,6 +1120,7 @@ public interface Player { * true}, or {@link #PLAYBACK_SUPPRESSION_REASON_NONE} if playback is not suppressed. * * @return The current {@link PlaybackSuppressionReason playback suppression reason}. + * @see EventListener#onPlaybackSuppressionReasonChanged(int) */ @PlaybackSuppressionReason int getPlaybackSuppressionReason(); @@ -1156,6 +1137,7 @@ public interface Player { * * * @return Whether the player is playing. + * @see EventListener#onIsPlayingChanged(boolean) */ boolean isPlaying(); @@ -1168,6 +1150,7 @@ public interface Player { * {@link #STATE_IDLE}. * * @return The error, or {@code null}. + * @see EventListener#onPlayerError(ExoPlaybackException) */ @Nullable ExoPlaybackException getPlayerError(); @@ -1199,6 +1182,7 @@ public interface Player { * Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}. * * @return Whether playback will proceed when ready. + * @see EventListener#onPlayWhenReadyChanged(boolean, int) */ boolean getPlayWhenReady(); @@ -1213,6 +1197,7 @@ public interface Player { * Returns the current {@link RepeatMode} used for playback. * * @return The current repeat mode. + * @see EventListener#onRepeatModeChanged(int) */ @RepeatMode int getRepeatMode(); @@ -1224,13 +1209,18 @@ public interface Player { */ void setShuffleModeEnabled(boolean shuffleModeEnabled); - /** Returns whether shuffling of windows is enabled. */ + /** + * Returns whether shuffling of windows is enabled. + * + * @see EventListener#onShuffleModeEnabledChanged(boolean) + */ boolean getShuffleModeEnabled(); /** * Whether the player is currently loading the source. * * @return Whether the player is currently loading the source. + * @see EventListener#onIsLoadingChanged(boolean) */ boolean isLoading(); @@ -1325,6 +1315,18 @@ public interface Player { */ void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters); + /** + * Changes the rate at which playback occurs. + * + *

    The pitch is not changed. + * + *

    This is equivalent to {@code setPlaybackParameter(getPlaybackParameter().withSpeed(speed))}. + * + * @param speed The linear factor by which playback will be sped up. Must be higher than 0. 1 is + * normal speed, 2 is twice as fast, 0.5 is half normal speed... + */ + void setPlaybackSpeed(float speed); + /** * Returns the currently active playback parameters. * @@ -1359,24 +1361,22 @@ public interface Player { */ void release(); - /** Returns the number of renderers. */ - int getRendererCount(); - /** - * Returns the track type that the renderer at a given index handles. + * Returns the available track groups. * - *

    For example, a video renderer will return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will - * return {@link C#TRACK_TYPE_AUDIO} and a text renderer will return {@link C#TRACK_TYPE_TEXT}. - * - * @param index The index of the renderer. - * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}. + * @see EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray) */ - int getRendererType(int index); - - /** Returns the available track groups. */ TrackGroupArray getCurrentTrackGroups(); - /** Returns the current track selections for each renderer. */ + /** + * Returns the current track selections. + * + *

    A concrete implementation may include null elements if it has a fixed number of renderer + * components, wishes to report a TrackSelection for each of them, and has one or more renderer + * components that is not assigned any selected tracks. + * + * @see EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray) + */ TrackSelectionArray getCurrentTrackSelections(); /** @@ -1389,6 +1389,8 @@ public interface Player { * *

    This metadata is considered static in that it comes from the tracks' declared Formats, * rather than being timed (or dynamic) metadata, which is represented within a metadata track. + * + * @see EventListener#onStaticMetadataChanged(List) */ List getCurrentStaticMetadata(); @@ -1398,7 +1400,11 @@ public interface Player { @Nullable Object getCurrentManifest(); - /** Returns the current {@link Timeline}. Never null, but may be empty. */ + /** + * Returns the current {@link Timeline}. Never null, but may be empty. + * + * @see EventListener#onTimelineChanged(Timeline, int) + */ Timeline getCurrentTimeline(); /** Returns the index of the period currently being played. */ @@ -1444,6 +1450,8 @@ public interface Player { /** * Returns the media item of the current window in the timeline. May be null if the timeline is * empty. + * + * @see EventListener#onMediaItemTransition(MediaItem, int) */ @Nullable MediaItem getCurrentMediaItem(); diff --git a/library/common/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java b/library/common/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java index c190b42a7d..c2da130ba9 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java @@ -22,6 +22,7 @@ import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.Bundleable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -166,6 +167,7 @@ public final class AudioAttributes implements Bundleable { // Bundleable implementation. + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({FIELD_CONTENT_TYPE, FIELD_FLAGS, FIELD_USAGE, FIELD_ALLOWED_CAPTURE_POLICY}) private @interface FieldNumber {} diff --git a/library/common/src/main/java/com/google/android/exoplayer2/device/DeviceInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/device/DeviceInfo.java index bb0386314f..20210c5635 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/device/DeviceInfo.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/device/DeviceInfo.java @@ -85,6 +85,7 @@ public final class DeviceInfo implements Bundleable { // Bundleable implementation. + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({FIELD_PLAYBACK_TYPE, FIELD_MIN_VOLUME, FIELD_MAX_VOLUME}) private @interface FieldNumber {} diff --git a/library/common/src/main/java/com/google/android/exoplayer2/upstream/DataSourceException.java b/library/common/src/main/java/com/google/android/exoplayer2/upstream/DataSourceException.java index a45b7db2f2..4718568168 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/upstream/DataSourceException.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/upstream/DataSourceException.java @@ -41,6 +41,10 @@ public final class DataSourceException extends IOException { return false; } + /** + * Indicates that the {@link DataSpec#position starting position} of the request was outside the + * bounds of the data. + */ public static final int POSITION_OUT_OF_RANGE = 0; /** @@ -56,5 +60,4 @@ public final class DataSourceException extends IOException { public DataSourceException(int reason) { this.reason = reason; } - } diff --git a/library/common/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/common/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index 575a10b6cd..e7df4c0599 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -46,7 +46,6 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * An {@link HttpDataSource} that uses Android's {@link HttpURLConnection}. @@ -221,14 +220,11 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou @Nullable private DataSpec dataSpec; @Nullable private HttpURLConnection connection; @Nullable private InputStream inputStream; - private byte @MonotonicNonNull [] skipBuffer; private boolean opened; private int responseCode; - private long bytesToSkip; - private long bytesToRead; - private long bytesSkipped; + private long bytesToRead; private long bytesRead; /** @deprecated Use {@link DefaultHttpDataSource.Factory} instead. */ @@ -400,7 +396,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou // If we requested a range starting from a non-zero position and received a 200 rather than a // 206, then the server does not support partial requests. We'll need to manually skip to the // requested position. - bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0; + long bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0; // Determine the length of the data to be read, after skipping. boolean isCompressed = isCompressed(connection); @@ -432,13 +428,21 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou opened = true; transferStarted(dataSpec); + try { + if (!skipFully(bytesToSkip)) { + throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE); + } + } catch (IOException e) { + closeConnectionQuietly(); + throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_OPEN); + } + return bytesToRead; } @Override public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException { try { - skipInternal(); return readInternal(buffer, offset, readLength); } catch (IOException e) { throw new HttpDataSourceException( @@ -480,8 +484,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou } /** - * Returns the number of bytes that have been skipped since the most recent call to - * {@link #open(DataSpec)}. + * Returns the number of bytes that were skipped during the most recent call to {@link + * #open(DataSpec)}. * * @return The number of bytes skipped. */ @@ -725,22 +729,19 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou } /** - * Skips any bytes that need skipping. Else does nothing. - *

    - * This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}. + * Attempts to skip the specified number of bytes in full. * + * @param bytesToSkip The number of bytes to skip. * @throws InterruptedIOException If the thread is interrupted during the operation. - * @throws EOFException If the end of the input stream is reached before the bytes are skipped. + * @throws IOException If an error occurs reading from the source. + * @return Whether the bytes were skipped in full. If {@code false} then the data ended before the + * specified number of bytes were skipped. Always {@code true} if {@code bytesToSkip == 0}. */ - private void skipInternal() throws IOException { - if (bytesSkipped == bytesToSkip) { - return; + private boolean skipFully(long bytesToSkip) throws IOException { + if (bytesToSkip == 0) { + return true; } - - if (skipBuffer == null) { - skipBuffer = new byte[4096]; - } - + byte[] skipBuffer = new byte[4096]; while (bytesSkipped != bytesToSkip) { int readLength = (int) min(bytesToSkip - bytesSkipped, skipBuffer.length); int read = castNonNull(inputStream).read(skipBuffer, 0, readLength); @@ -748,11 +749,12 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou throw new InterruptedIOException(); } if (read == -1) { - throw new EOFException(); + return false; } bytesSkipped += read; bytesTransferred(read); } + return true; } /** diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/CopyOnWriteMultiset.java b/library/common/src/main/java/com/google/android/exoplayer2/util/CopyOnWriteMultiset.java index 505ff55cbe..c473e2206b 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/CopyOnWriteMultiset.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/CopyOnWriteMultiset.java @@ -138,4 +138,11 @@ public final class CopyOnWriteMultiset implements Iterable return elements.iterator(); } } + + /** Returns the number of occurrences of an element in this multiset. */ + public int count(E element) { + synchronized (lock) { + return elementCounts.containsKey(element) ? elementCounts.get(element) : 0; + } + } } diff --git a/library/common/src/test/java/com/google/android/exoplayer2/util/CopyOnWriteMultisetTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/CopyOnWriteMultisetTest.java index 92e4124a6a..1b41a2a79d 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/util/CopyOnWriteMultisetTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/util/CopyOnWriteMultisetTest.java @@ -107,4 +107,44 @@ public final class CopyOnWriteMultisetTest { assertThrows(UnsupportedOperationException.class, () -> elementSet.remove("a string")); } + + @Test + public void count() { + CopyOnWriteMultiset multiset = new CopyOnWriteMultiset<>(); + multiset.add("a string"); + multiset.add("a string"); + + assertThat(multiset.count("a string")).isEqualTo(2); + assertThat(multiset.count("another string")).isEqualTo(0); + } + + @Test + public void modifyingWhileIteratingElements_succeeds() { + CopyOnWriteMultiset multiset = new CopyOnWriteMultiset<>(); + multiset.add("a string"); + multiset.add("a string"); + multiset.add("another string"); + + // A traditional collection would throw a ConcurrentModificationException here. + for (String element : multiset) { + multiset.remove(element); + } + + assertThat(multiset).isEmpty(); + } + + @Test + public void modifyingWhileIteratingElementSet_succeeds() { + CopyOnWriteMultiset multiset = new CopyOnWriteMultiset<>(); + multiset.add("a string"); + multiset.add("a string"); + multiset.add("another string"); + + // A traditional collection would throw a ConcurrentModificationException here. + for (String element : multiset.elementSet()) { + multiset.remove(element); + } + + assertThat(multiset).containsExactly("a string"); + } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java index 79b661e717..b59a6e1618 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java @@ -17,15 +17,12 @@ package com.google.android.exoplayer2.upstream; import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.fail; -import static org.junit.Assert.assertThrows; 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.testutil.TestUtil; -import com.google.android.exoplayer2.upstream.ContentDataSource.ContentDataSourceException; -import java.io.EOFException; import java.io.FileNotFoundException; import java.io.IOException; import java.util.Arrays; @@ -85,36 +82,6 @@ public final class ContentDataSourceTest { } } - @Test - public void read_positionPastEndOfContent_throwsEOFException() throws Exception { - Uri contentUri = TestContentProvider.buildUri(DATA_PATH, /* pipeMode= */ false); - ContentDataSource dataSource = - new ContentDataSource(ApplicationProvider.getApplicationContext()); - DataSpec dataSpec = new DataSpec(contentUri, /* position= */ 1025, C.LENGTH_UNSET); - try { - ContentDataSourceException exception = - assertThrows(ContentDataSourceException.class, () -> dataSource.open(dataSpec)); - assertThat(exception).hasCauseThat().isInstanceOf(EOFException.class); - } finally { - dataSource.close(); - } - } - - @Test - public void readPipeMode_positionPastEndOfContent_throwsEOFException() throws Exception { - Uri contentUri = TestContentProvider.buildUri(DATA_PATH, /* pipeMode= */ true); - ContentDataSource dataSource = - new ContentDataSource(ApplicationProvider.getApplicationContext()); - DataSpec dataSpec = new DataSpec(contentUri, /* position= */ 1025, C.LENGTH_UNSET); - try { - ContentDataSourceException exception = - assertThrows(ContentDataSourceException.class, () -> dataSource.open(dataSpec)); - assertThat(exception).hasCauseThat().isInstanceOf(EOFException.class); - } finally { - dataSource.close(); - } - } - private static void assertData(int offset, int length, boolean pipeMode) throws IOException { Uri contentUri = TestContentProvider.buildUri(DATA_PATH, pipeMode); ContentDataSource dataSource = @@ -130,5 +97,4 @@ public final class ContentDataSourceTest { dataSource.close(); } } - } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/RawResourceDataSourceContractTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/RawResourceDataSourceContractTest.java new file mode 100644 index 0000000000..d55e162a49 --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/RawResourceDataSourceContractTest.java @@ -0,0 +1,85 @@ +/* + * 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.upstream; + +import android.content.res.Resources; +import android.net.Uri; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.core.test.R; +import com.google.android.exoplayer2.testutil.DataSourceContractTest; +import com.google.android.exoplayer2.util.Util; +import com.google.common.collect.ImmutableList; +import org.junit.runner.RunWith; + +/** {@link DataSource} contract tests for {@link RawResourceDataSource}. */ +@RunWith(AndroidJUnit4.class) +public final class RawResourceDataSourceContractTest extends DataSourceContractTest { + + private static final byte[] RESOURCE_1_DATA = Util.getUtf8Bytes("resource1 abc\n"); + private static final byte[] RESOURCE_2_DATA = Util.getUtf8Bytes("resource2 abcdef\n"); + + @Override + protected DataSource createDataSource() { + return new RawResourceDataSource(ApplicationProvider.getApplicationContext()); + } + + @Override + protected ImmutableList getTestResources() { + // Android packages raw resources into a single file. When reading a resource other than the + // last one, Android does not prevent accidentally reading beyond the end of the resource and + // into the next one. We use two resources in this test to ensure that when packaged, at least + // one of them has a subsequent resource. This allows the contract test to enforce that the + // RawResourceDataSource implementation doesn't erroneously read into the second resource when + // opened to read the first. + return ImmutableList.of( + new TestResource.Builder() + .setName("resource 1") + .setUri(RawResourceDataSource.buildRawResourceUri(R.raw.resource1)) + .setExpectedBytes(RESOURCE_1_DATA) + .build(), + new TestResource.Builder() + .setName("resource 2") + .setUri(RawResourceDataSource.buildRawResourceUri(R.raw.resource2)) + .setExpectedBytes(RESOURCE_2_DATA) + .build(), + // Additional resources using different URI schemes. + new TestResource.Builder() + .setName("android.resource:// with path") + .setUri( + Uri.parse( + "android.resource://" + + ApplicationProvider.getApplicationContext().getPackageName() + + "/raw/resource1")) + .setExpectedBytes(RESOURCE_1_DATA) + .build(), + new TestResource.Builder() + .setName("android.resource:// with ID") + .setUri( + Uri.parse( + "android.resource://" + + ApplicationProvider.getApplicationContext().getPackageName() + + "/" + + R.raw.resource1)) + .setExpectedBytes(RESOURCE_1_DATA) + .build()); + } + + @Override + protected Uri getNotFoundUri() { + return RawResourceDataSource.buildRawResourceUri(Resources.ID_NULL); + } +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/TestContentProvider.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/TestContentProvider.java index 42bfd178e2..a7ddd7118e 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/TestContentProvider.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/TestContentProvider.java @@ -24,7 +24,6 @@ import android.net.Uri; import android.os.Bundle; import android.os.ParcelFileDescriptor; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -73,7 +72,7 @@ public final class TestContentProvider extends ContentProvider openPipeHelper( uri, /* mimeType= */ null, /* opts= */ null, /* args= */ null, /* func= */ this); return new AssetFileDescriptor( - fileDescriptor, /* startOffset= */ 0, /* length= */ C.LENGTH_UNSET); + fileDescriptor, /* startOffset= */ 0, AssetFileDescriptor.UNKNOWN_LENGTH); } else { return getContext().getAssets().openFd(fileName); } diff --git a/library/core/src/androidTest/res/raw/resource1 b/library/core/src/androidTest/res/raw/resource1 new file mode 100644 index 0000000000..8b59777753 --- /dev/null +++ b/library/core/src/androidTest/res/raw/resource1 @@ -0,0 +1 @@ +resource1 abc diff --git a/library/core/src/androidTest/res/raw/resource2 b/library/core/src/androidTest/res/raw/resource2 new file mode 100644 index 0000000000..37927749f2 --- /dev/null +++ b/library/core/src/androidTest/res/raw/resource2 @@ -0,0 +1 @@ +resource2 abcdef 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 57f7f65e1f..0fc968b5ba 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 @@ -669,6 +669,8 @@ public class DefaultRenderersFactory implements RenderersFactory { new DefaultAudioProcessorChain(), enableFloatOutput, enableAudioTrackPlaybackParams, - enableOffload); + enableOffload + ? DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED + : DefaultAudioSink.OFFLOAD_MODE_DISABLED); } } 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 9169271d12..7536fd65b9 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 @@ -74,7 +74,8 @@ import java.util.List; * provides default implementations for common media types ({@link MediaCodecVideoRenderer}, * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A * Renderer consumes media from the MediaSource being played. Renderers are injected when the - * player is created. + * player is created. The number of renderers and their respective track types can be obtained + * by calling {@link #getRendererCount()} and {@link #getRendererType(int)}. *

  • A {@link TrackSelector} that selects tracks provided by the MediaSource to be * consumed by each of the available Renderers. The library provides a default implementation * ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected @@ -449,6 +450,20 @@ public interface ExoPlayer extends Player { } } + /** Returns the number of renderers. */ + int getRendererCount(); + + /** + * Returns the track type that the renderer at a given index handles. + * + *

    For example, a video renderer will return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will + * return {@link C#TRACK_TYPE_AUDIO} and a text renderer will return {@link C#TRACK_TYPE_TEXT}. + * + * @param index The index of the renderer. + * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}. + */ + int getRendererType(int index); + /** * Returns the track selector that this player uses, or null if track selection is not supported. */ @@ -663,7 +678,7 @@ public interface ExoPlayer extends Player { *

  • Audio offload rendering is enabled in {@link * DefaultRenderersFactory#setEnableAudioOffload} or the equivalent option passed to {@link * DefaultAudioSink#DefaultAudioSink(AudioCapabilities, - * DefaultAudioSink.AudioProcessorChain, boolean, boolean, boolean)}. + * DefaultAudioSink.AudioProcessorChain, boolean, boolean, int)}. *
  • An audio track is playing in a format that the device supports offloading (for example, * MP3 or AAC). *
  • The {@link AudioSink} is playing with an offload {@link AudioTrack}. @@ -682,6 +697,7 @@ public interface ExoPlayer extends Player { * Returns whether the player has paused its main loop to save power in offload scheduling mode. * * @see #experimentalSetOffloadSchedulingEnabled(boolean) + * @see EventListener#onExperimentalSleepingForOffloadChanged(boolean) */ boolean experimentalIsSleepingForOffload(); } 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 de8aa48891..5c52e27343 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 @@ -999,7 +999,16 @@ import java.util.List; if (!previousPlaybackInfo.timeline.equals(newPlaybackInfo.timeline)) { listeners.queueEvent( Player.EVENT_TIMELINE_CHANGED, - listener -> listener.onTimelineChanged(newPlaybackInfo.timeline, timelineChangeReason)); + listener -> { + @Nullable Object manifest = null; + if (newPlaybackInfo.timeline.getWindowCount() == 1) { + // Legacy behavior was to report the manifest for single window timelines only. + Timeline.Window window = new Timeline.Window(); + manifest = newPlaybackInfo.timeline.getWindow(0, window).manifest; + } + listener.onTimelineChanged(newPlaybackInfo.timeline, manifest, timelineChangeReason); + listener.onTimelineChanged(newPlaybackInfo.timeline, timelineChangeReason); + }); } if (positionDiscontinuity) { listeners.queueEvent( @@ -1042,7 +1051,10 @@ import java.util.List; if (previousPlaybackInfo.isLoading != newPlaybackInfo.isLoading) { listeners.queueEvent( Player.EVENT_IS_LOADING_CHANGED, - listener -> listener.onIsLoadingChanged(newPlaybackInfo.isLoading)); + listener -> { + listener.onLoadingChanged(newPlaybackInfo.isLoading); + listener.onIsLoadingChanged(newPlaybackInfo.isLoading); + }); } if (previousPlaybackInfo.playbackState != newPlaybackInfo.playbackState || previousPlaybackInfo.playWhenReady != newPlaybackInfo.playWhenReady) { 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 f2d0d53444..b518455eed 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 @@ -1980,7 +1980,7 @@ import java.util.concurrent.atomic.AtomicBoolean; @Nullable MediaPeriodHolder readingPeriod = queue.getReadingPeriod(); if (readingPeriod == null || queue.getPlayingPeriod() == readingPeriod - || readingPeriod.allRenderersEnabled) { + || readingPeriod.allRenderersInCorrectState) { // Not reading ahead or all renderers updated. return; } @@ -2075,7 +2075,7 @@ import java.util.concurrent.atomic.AtomicBoolean; MediaPeriodHolder nextPlayingPeriodHolder = playingPeriodHolder.getNext(); return nextPlayingPeriodHolder != null && rendererPositionUs >= nextPlayingPeriodHolder.getStartPositionRendererTime() - && nextPlayingPeriodHolder.allRenderersEnabled; + && nextPlayingPeriodHolder.allRenderersInCorrectState; } private boolean hasReadingPeriodFinishedReading() { @@ -2294,7 +2294,7 @@ import java.util.concurrent.atomic.AtomicBoolean; enableRenderer(i, rendererWasEnabledFlags[i]); } } - readingMediaPeriod.allRenderersEnabled = true; + readingMediaPeriod.allRenderersInCorrectState = true; } private void enableRenderer(int rendererIndex, boolean wasRendererEnabled) 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 e8639e1f9a..d8569a544d 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 @@ -53,12 +53,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** {@link MediaPeriodInfo} about this media period. */ public MediaPeriodInfo info; /** - * Whether all required renderers have been enabled with the {@link #sampleStreams} for this + * Whether all renderers are in the correct state for this {@link #mediaPeriod}. + * + *

    Renderers that are needed must have been enabled with the {@link #sampleStreams} for this * {@link #mediaPeriod}. This means either {@link Renderer#enable(RendererConfiguration, Format[], - * SampleStream, long, boolean, boolean, long)} or {@link Renderer#replaceStream(Format[], - * SampleStream, long)} has been called. + * SampleStream, long, boolean, boolean, long, long)} or {@link Renderer#replaceStream(Format[], + * SampleStream, long, long)} has been called. + * + *

    Renderers that are not needed must have been {@link Renderer#disable() disabled}. */ - public boolean allRenderersEnabled; + public boolean allRenderersInCorrectState; private final boolean[] mayRetainStreamFlags; private final RendererCapabilities[] rendererCapabilities; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaSourceList.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaSourceList.java index 1227dbb397..1418a03a02 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaSourceList.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaSourceList.java @@ -21,6 +21,7 @@ import static java.lang.Math.min; import android.os.Handler; import androidx.annotation.Nullable; import com.google.android.exoplayer2.analytics.AnalyticsCollector; +import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.MaskingMediaPeriod; @@ -600,9 +601,11 @@ import java.util.Set; @Override public void onDrmSessionAcquired( - int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) { + int windowIndex, + @Nullable MediaSource.MediaPeriodId mediaPeriodId, + @DrmSession.State int state) { if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - drmEventDispatcher.drmSessionAcquired(); + drmEventDispatcher.drmSessionAcquired(state); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java index 8578a23929..0aba970a25 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java @@ -240,7 +240,7 @@ public interface Renderer extends PlayerMessage.Target { /** * Returns the track type that the renderer handles. * - * @see Player#getRendererType(int) + * @see ExoPlayer#getRendererType(int) * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}. */ int getTrackType(); 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 49feb74b8b..320f4825ba 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 @@ -1293,13 +1293,6 @@ public class SimpleExoPlayer extends BasePlayer prepare(); } - @Override - public void setMediaItems(List mediaItems) { - verifyApplicationThread(); - analyticsCollector.resetForNewPlaylist(); - player.setMediaItems(mediaItems); - } - @Override public void setMediaItems(List mediaItems, boolean resetPosition) { verifyApplicationThread(); @@ -1315,27 +1308,6 @@ public class SimpleExoPlayer extends BasePlayer player.setMediaItems(mediaItems, startWindowIndex, startPositionMs); } - @Override - public void setMediaItem(MediaItem mediaItem) { - verifyApplicationThread(); - analyticsCollector.resetForNewPlaylist(); - player.setMediaItem(mediaItem); - } - - @Override - public void setMediaItem(MediaItem mediaItem, boolean resetPosition) { - verifyApplicationThread(); - analyticsCollector.resetForNewPlaylist(); - player.setMediaItem(mediaItem, resetPosition); - } - - @Override - public void setMediaItem(MediaItem mediaItem, long startPositionMs) { - verifyApplicationThread(); - analyticsCollector.resetForNewPlaylist(); - player.setMediaItem(mediaItem, startPositionMs); - } - @Override public void setMediaSources(List mediaSources) { verifyApplicationThread(); @@ -1391,18 +1363,6 @@ public class SimpleExoPlayer extends BasePlayer player.addMediaItems(index, mediaItems); } - @Override - public void addMediaItem(MediaItem mediaItem) { - verifyApplicationThread(); - player.addMediaItem(mediaItem); - } - - @Override - public void addMediaItem(int index, MediaItem mediaItem) { - verifyApplicationThread(); - player.addMediaItem(index, mediaItem); - } - @Override public void addMediaSource(MediaSource mediaSource) { verifyApplicationThread(); @@ -1427,24 +1387,12 @@ public class SimpleExoPlayer extends BasePlayer 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 void removeMediaItem(int index) { - verifyApplicationThread(); - player.removeMediaItem(index); - } - @Override public void removeMediaItems(int fromIndex, int toIndex) { verifyApplicationThread(); @@ -2072,8 +2020,8 @@ public class SimpleExoPlayer extends BasePlayer } @Override - public void onRenderedFirstFrame(Surface surface) { - analyticsCollector.onRenderedFirstFrame(surface); + public void onRenderedFirstFrame(@Nullable Surface surface, long renderTimeMs) { + analyticsCollector.onRenderedFirstFrame(surface, renderTimeMs); if (SimpleExoPlayer.this.surface == surface) { for (VideoListener videoListener : videoListeners) { videoListener.onRenderedFirstFrame(); 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 ff613b85a9..92f4ff78ff 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 @@ -37,6 +37,7 @@ import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; +import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.LoadEventInfo; @@ -207,7 +208,7 @@ public class AnalyticsCollector // AudioRendererEventListener implementation. - @SuppressWarnings("deprecation") + @SuppressWarnings("deprecation") // Calling deprecated listener method. @Override public final void onAudioEnabled(DecoderCounters counters) { EventTime eventTime = generateReadingMediaPeriodEventTime(); @@ -220,7 +221,7 @@ public class AnalyticsCollector }); } - @SuppressWarnings("deprecation") + @SuppressWarnings("deprecation") // Calling deprecated listener method. @Override public final void onAudioDecoderInitialized( String decoderName, long initializedTimestampMs, long initializationDurationMs) { @@ -230,12 +231,14 @@ public class AnalyticsCollector AnalyticsListener.EVENT_AUDIO_DECODER_INITIALIZED, listener -> { listener.onAudioDecoderInitialized(eventTime, decoderName, initializationDurationMs); + listener.onAudioDecoderInitialized( + eventTime, decoderName, initializedTimestampMs, initializationDurationMs); listener.onDecoderInitialized( eventTime, C.TRACK_TYPE_AUDIO, decoderName, initializationDurationMs); }); } - @SuppressWarnings("deprecation") + @SuppressWarnings("deprecation") // Calling deprecated listener method. @Override public final void onAudioInputFormatChanged( Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { @@ -244,6 +247,7 @@ public class AnalyticsCollector eventTime, AnalyticsListener.EVENT_AUDIO_INPUT_FORMAT_CHANGED, listener -> { + listener.onAudioInputFormatChanged(eventTime, format); listener.onAudioInputFormatChanged(eventTime, format, decoderReuseEvaluation); listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_AUDIO, format); }); @@ -278,7 +282,7 @@ public class AnalyticsCollector listener -> listener.onAudioDecoderReleased(eventTime, decoderName)); } - @SuppressWarnings("deprecation") + @SuppressWarnings("deprecation") // Calling deprecated listener method. @Override public final void onAudioDisabled(DecoderCounters counters) { EventTime eventTime = generatePlayingMediaPeriodEventTime(); @@ -361,7 +365,7 @@ public class AnalyticsCollector // VideoRendererEventListener implementation. - @SuppressWarnings("deprecation") + @SuppressWarnings("deprecation") // Calling deprecated listener method. @Override public final void onVideoEnabled(DecoderCounters counters) { EventTime eventTime = generateReadingMediaPeriodEventTime(); @@ -374,7 +378,7 @@ public class AnalyticsCollector }); } - @SuppressWarnings("deprecation") + @SuppressWarnings("deprecation") // Calling deprecated listener method. @Override public final void onVideoDecoderInitialized( String decoderName, long initializedTimestampMs, long initializationDurationMs) { @@ -384,12 +388,14 @@ public class AnalyticsCollector AnalyticsListener.EVENT_VIDEO_DECODER_INITIALIZED, listener -> { listener.onVideoDecoderInitialized(eventTime, decoderName, initializationDurationMs); + listener.onVideoDecoderInitialized( + eventTime, decoderName, initializedTimestampMs, initializationDurationMs); listener.onDecoderInitialized( eventTime, C.TRACK_TYPE_VIDEO, decoderName, initializationDurationMs); }); } - @SuppressWarnings("deprecation") + @SuppressWarnings("deprecation") // Calling deprecated listener method. @Override public final void onVideoInputFormatChanged( Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { @@ -398,6 +404,7 @@ public class AnalyticsCollector eventTime, AnalyticsListener.EVENT_VIDEO_INPUT_FORMAT_CHANGED, listener -> { + listener.onVideoInputFormatChanged(eventTime, format); listener.onVideoInputFormatChanged(eventTime, format, decoderReuseEvaluation); listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_VIDEO, format); }); @@ -421,7 +428,7 @@ public class AnalyticsCollector listener -> listener.onVideoDecoderReleased(eventTime, decoderName)); } - @SuppressWarnings("deprecation") + @SuppressWarnings("deprecation") // Calling deprecated listener method. @Override public final void onVideoDisabled(DecoderCounters counters) { EventTime eventTime = generatePlayingMediaPeriodEventTime(); @@ -446,13 +453,17 @@ public class AnalyticsCollector eventTime, width, height, unappliedRotationDegrees, pixelWidthHeightRatio)); } + @SuppressWarnings("deprecation") // Calling deprecated listener method. @Override - public final void onRenderedFirstFrame(@Nullable Surface surface) { + public final void onRenderedFirstFrame(@Nullable Surface surface, long renderTimeMs) { EventTime eventTime = generateReadingMediaPeriodEventTime(); sendEvent( eventTime, AnalyticsListener.EVENT_RENDERED_FIRST_FRAME, - listener -> listener.onRenderedFirstFrame(eventTime, surface)); + listener -> { + listener.onRenderedFirstFrame(eventTime, surface); + listener.onRenderedFirstFrame(eventTime, surface, renderTimeMs); + }); } @Override @@ -615,16 +626,20 @@ public class AnalyticsCollector listener -> listener.onStaticMetadataChanged(eventTime, metadataList)); } + @SuppressWarnings("deprecation") // Calling deprecated listener method. @Override public final void onIsLoadingChanged(boolean isLoading) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); sendEvent( eventTime, AnalyticsListener.EVENT_IS_LOADING_CHANGED, - listener -> listener.onIsLoadingChanged(eventTime, isLoading)); + listener -> { + listener.onLoadingChanged(eventTime, isLoading); + listener.onIsLoadingChanged(eventTime, isLoading); + }); } - @SuppressWarnings("deprecation") + @SuppressWarnings("deprecation") // Implementing and calling deprecated listener method. @Override public final void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); @@ -725,7 +740,7 @@ public class AnalyticsCollector listener -> listener.onPlaybackParametersChanged(eventTime, playbackParameters)); } - @SuppressWarnings("deprecation") + @SuppressWarnings("deprecation") // Implementing and calling deprecated listener method. @Override public final void onSeekProcessed() { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); @@ -747,12 +762,17 @@ public class AnalyticsCollector // DefaultDrmSessionManager.EventListener implementation. @Override - public final void onDrmSessionAcquired(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { + @SuppressWarnings("deprecation") // Calls deprecated listener method. + public final void onDrmSessionAcquired( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, @DrmSession.State int state) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); sendEvent( eventTime, AnalyticsListener.EVENT_DRM_SESSION_ACQUIRED, - listener -> listener.onDrmSessionAcquired(eventTime)); + listener -> { + listener.onDrmSessionAcquired(eventTime); + listener.onDrmSessionAcquired(eventTime, state); + }); } @Override 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 a68e2f7fa2..d9978ed6a4 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 @@ -20,6 +20,7 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import android.media.MediaCodec; import android.media.MediaCodec.CodecException; import android.os.Looper; +import android.os.SystemClock; import android.util.SparseArray; import android.view.Surface; import androidx.annotation.IntDef; @@ -39,6 +40,7 @@ import com.google.android.exoplayer2.audio.AudioSink; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderException; import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; +import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.MediaLoadData; @@ -583,10 +585,7 @@ public interface AnalyticsListener { * @param eventTime The event time. * @param isLoading Whether the player is loading. */ - @SuppressWarnings("deprecation") - default void onIsLoadingChanged(EventTime eventTime, boolean isLoading) { - onLoadingChanged(eventTime, isLoading); - } + default void onIsLoadingChanged(EventTime eventTime, boolean isLoading) {} /** @deprecated Use {@link #onIsLoadingChanged(EventTime, boolean)} instead. */ @Deprecated @@ -755,8 +754,18 @@ public interface AnalyticsListener { * * @param eventTime The event time. * @param decoderName The decoder that was created. + * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization + * finished. * @param initializationDurationMs The time taken to initialize the decoder in milliseconds. */ + default void onAudioDecoderInitialized( + EventTime eventTime, + String decoderName, + long initializedTimestampMs, + long initializationDurationMs) {} + + /** @deprecated Use {@link #onAudioDecoderInitialized(EventTime, String, long, long)}. */ + @Deprecated default void onAudioDecoderInitialized( EventTime eventTime, String decoderName, long initializationDurationMs) {} @@ -775,11 +784,10 @@ public interface AnalyticsListener { * decoder instance can be reused for the new format, or {@code null} if the renderer did not * have a decoder. */ - @SuppressWarnings("deprecation") default void onAudioInputFormatChanged( - EventTime eventTime, Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { - onAudioInputFormatChanged(eventTime, format); - } + EventTime eventTime, + Format format, + @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {} /** * Called when the audio position has increased for the first time since the last pause or @@ -898,8 +906,18 @@ public interface AnalyticsListener { * * @param eventTime The event time. * @param decoderName The decoder that was created. + * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization + * finished. * @param initializationDurationMs The time taken to initialize the decoder in milliseconds. */ + default void onVideoDecoderInitialized( + EventTime eventTime, + String decoderName, + long initializedTimestampMs, + long initializationDurationMs) {} + + /** @deprecated Use {@link #onVideoDecoderInitialized(EventTime, String, long, long)}. */ + @Deprecated default void onVideoDecoderInitialized( EventTime eventTime, String decoderName, long initializationDurationMs) {} @@ -918,11 +936,10 @@ public interface AnalyticsListener { * decoder instance can be reused for the new format, or {@code null} if the renderer did not * have a decoder. */ - @SuppressWarnings("deprecation") default void onVideoInputFormatChanged( - EventTime eventTime, Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { - onVideoInputFormatChanged(eventTime, format); - } + EventTime eventTime, + Format format, + @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {} /** * Called after video frames have been dropped. @@ -992,7 +1009,13 @@ public interface AnalyticsListener { * @param eventTime The event time. * @param surface The {@link Surface} to which a frame has been rendered, or {@code null} if the * renderer renders to something that isn't a {@link Surface}. + * @param renderTimeMs {@link SystemClock#elapsedRealtime()} when the first frame was rendered. */ + default void onRenderedFirstFrame( + EventTime eventTime, @Nullable Surface surface, long renderTimeMs) {} + + /** @deprecated Use {@link #onRenderedFirstFrame(EventTime, Surface, long)} instead. */ + @Deprecated default void onRenderedFirstFrame(EventTime eventTime, @Nullable Surface surface) {} /** @@ -1026,12 +1049,17 @@ public interface AnalyticsListener { */ default void onSurfaceSizeChanged(EventTime eventTime, int width, int height) {} + /** @deprecated Implement {@link #onDrmSessionAcquired(EventTime, int)} instead. */ + @Deprecated + default void onDrmSessionAcquired(EventTime eventTime) {} + /** * Called each time a drm session is acquired. * * @param eventTime The event time. + * @param state The {@link DrmSession.State} of the session when the acquisition completed. */ - default void onDrmSessionAcquired(EventTime eventTime) {} + default void onDrmSessionAcquired(EventTime eventTime, @DrmSession.State int state) {} /** * Called each time drm keys are loaded. 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 69803ceef6..e3f2e11be4 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 @@ -69,11 +69,8 @@ public interface AudioRendererEventListener { * decoder instance can be reused for the new format, or {@code null} if the renderer did not * have a decoder. */ - @SuppressWarnings("deprecation") default void onAudioInputFormatChanged( - Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { - onAudioInputFormatChanged(format); - } + Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {} /** * Called when the audio position has increased for the first time since the last pause or @@ -186,11 +183,15 @@ public interface AudioRendererEventListener { } /** Invokes {@link AudioRendererEventListener#onAudioInputFormatChanged(Format)}. */ + @SuppressWarnings("deprecation") // Calling deprecated listener method. public void inputFormatChanged( Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { if (handler != null) { handler.post( - () -> castNonNull(listener).onAudioInputFormatChanged(format, decoderReuseEvaluation)); + () -> { + castNonNull(listener).onAudioInputFormatChanged(format); + castNonNull(listener).onAudioInputFormatChanged(format, decoderReuseEvaluation); + }); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java index f2aa0d6d52..0abf326538 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java @@ -428,7 +428,7 @@ public interface AudioSink { /** * Sets the playback volume. * - * @param volume A volume in the range [0.0, 1.0]. + * @param volume Linear output gain to apply to all channels. Should be in the range [0.0, 1.0]. */ void setVolume(float volume); 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 919870c24e..47bfba9efe 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 @@ -215,6 +215,35 @@ public final class DefaultAudioSink implements AudioSink { /** The default skip silence flag. */ private static final boolean DEFAULT_SKIP_SILENCE = false; + /** Audio offload mode configuration. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + OFFLOAD_MODE_DISABLED, + OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED, + OFFLOAD_MODE_ENABLED_GAPLESS_NOT_REQUIRED + }) + public @interface OffloadMode {} + + /** The audio sink will never play in offload mode. */ + public static final int OFFLOAD_MODE_DISABLED = 0; + /** + * The audio sink will prefer offload playback except if the track is gapless and the device does + * not advertise support for gapless playback in offload. + * + *

    Use this option to prioritize seamless transitions between tracks of the same album to power + * savings. + */ + public static final int OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED = 1; + /** + * The audio sink will prefer offload playback even if this might result in silence gaps between + * tracks. + * + *

    Use this option to prioritize battery saving at the cost of a possible non seamless + * transitions between tracks of the same album. + */ + public static final int OFFLOAD_MODE_ENABLED_GAPLESS_NOT_REQUIRED = 2; + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({OUTPUT_MODE_PCM, OUTPUT_MODE_OFFLOAD, OUTPUT_MODE_PASSTHROUGH}) @@ -281,7 +310,7 @@ public final class DefaultAudioSink implements AudioSink { private final AudioTrackPositionTracker audioTrackPositionTracker; private final ArrayDeque mediaPositionParametersCheckpoints; private final boolean enableAudioTrackPlaybackParams; - private final boolean enableOffload; + @OffloadMode private final int offloadMode; @MonotonicNonNull private StreamEventCallbackV29 offloadStreamEventCallbackV29; private final PendingExceptionHolder initializationExceptionPendingExceptionHolder; @@ -364,7 +393,7 @@ public final class DefaultAudioSink implements AudioSink { new DefaultAudioProcessorChain(audioProcessors), enableFloatOutput, /* enableAudioTrackPlaybackParams= */ false, - /* enableOffload= */ false); + OFFLOAD_MODE_DISABLED); } /** @@ -382,8 +411,8 @@ public final class DefaultAudioSink implements AudioSink { * use. * @param enableAudioTrackPlaybackParams Whether to enable setting playback speed using {@link * android.media.AudioTrack#setPlaybackParams(PlaybackParams)}, if supported. - * @param enableOffload Whether to enable audio offload. If an audio format can be both played - * with offload and encoded audio passthrough, it will be played in offload. Audio offload is + * @param offloadMode Audio offload configuration. If an audio format can be both played with + * offload and encoded audio passthrough, it will be played in offload. Audio offload is * supported from API level 29. Most Android devices can only support one offload {@link * android.media.AudioTrack} at a time and can invalidate it at any time. Thus an app can * never be guaranteed that it will be able to play in offload. Audio processing (for example, @@ -394,12 +423,12 @@ public final class DefaultAudioSink implements AudioSink { AudioProcessorChain audioProcessorChain, boolean enableFloatOutput, boolean enableAudioTrackPlaybackParams, - boolean enableOffload) { + @OffloadMode int offloadMode) { this.audioCapabilities = audioCapabilities; this.audioProcessorChain = Assertions.checkNotNull(audioProcessorChain); this.enableFloatOutput = Util.SDK_INT >= 21 && enableFloatOutput; this.enableAudioTrackPlaybackParams = Util.SDK_INT >= 23 && enableAudioTrackPlaybackParams; - this.enableOffload = Util.SDK_INT >= 29 && enableOffload; + this.offloadMode = Util.SDK_INT >= 29 ? offloadMode : OFFLOAD_MODE_DISABLED; releasingConditionVariable = new ConditionVariable(true); audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener()); channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); @@ -462,9 +491,7 @@ public final class DefaultAudioSink implements AudioSink { // guaranteed to support. return SINK_FORMAT_SUPPORTED_WITH_TRANSCODING; } - if (enableOffload - && !offloadDisabledUntilNextConfiguration - && isOffloadedPlaybackSupported(format, audioAttributes)) { + if (!offloadDisabledUntilNextConfiguration && useOffloadedPlayback(format, audioAttributes)) { return SINK_FORMAT_SUPPORTED_DIRECTLY; } if (isPassthroughPlaybackSupported(format, audioCapabilities)) { @@ -541,7 +568,7 @@ public final class DefaultAudioSink implements AudioSink { availableAudioProcessors = new AudioProcessor[0]; outputSampleRate = inputFormat.sampleRate; outputPcmFrameSize = C.LENGTH_UNSET; - if (enableOffload && isOffloadedPlaybackSupported(inputFormat, audioAttributes)) { + if (useOffloadedPlayback(inputFormat, audioAttributes)) { outputMode = OUTPUT_MODE_OFFLOAD; outputEncoding = MimeTypes.getEncoding( @@ -1478,6 +1505,10 @@ public final class DefaultAudioSink implements AudioSink { && !audioCapabilities.supportsEncoding(C.ENCODING_E_AC3_JOC)) { // E-AC3 receivers support E-AC3 JOC streams (but decode only the base layer). encoding = C.ENCODING_E_AC3; + } else if (encoding == C.ENCODING_DTS_HD + && !audioCapabilities.supportsEncoding(C.ENCODING_DTS_HD)) { + // DTS receivers support DTS-HD streams (but decode only the core layer). + encoding = C.ENCODING_DTS; } if (!audioCapabilities.supportsEncoding(encoding)) { return null; @@ -1561,9 +1592,8 @@ public final class DefaultAudioSink implements AudioSink { return Util.getAudioTrackChannelConfig(channelCount); } - private static boolean isOffloadedPlaybackSupported( - Format format, AudioAttributes audioAttributes) { - if (Util.SDK_INT < 29) { + private boolean useOffloadedPlayback(Format format, AudioAttributes audioAttributes) { + if (Util.SDK_INT < 29 || offloadMode == OFFLOAD_MODE_DISABLED) { return false; } @C.Encoding @@ -1581,8 +1611,12 @@ public final class DefaultAudioSink implements AudioSink { audioFormat, audioAttributes.getAudioAttributesV21())) { return false; } - boolean notGapless = format.encoderDelay == 0 && format.encoderPadding == 0; - return notGapless || isOffloadedGaplessPlaybackSupported(); + boolean isGapless = format.encoderDelay != 0 || format.encoderPadding != 0; + boolean offloadRequiresGaplessSupport = offloadMode == OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED; + if (isGapless && offloadRequiresGaplessSupport && !isOffloadedGaplessPlaybackSupported()) { + return false; + } + return true; } private static boolean isOffloadedPlayback(AudioTrack audioTrack) { 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 037ce49171..4325e677de 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 @@ -293,11 +293,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; if (openInternal(true)) { doLicense(true); } - } else if (eventDispatcher != null && isOpen()) { - // If the session is already open then send the acquire event only to the provided dispatcher. - // TODO: Add a parameter to onDrmSessionAcquired to indicate whether the session is being - // re-used or not. - eventDispatcher.drmSessionAcquired(); + } else if (eventDispatcher != null + && isOpen() + && eventDispatchers.count(eventDispatcher) == 1) { + // If the session is already open and this is the first instance of eventDispatcher we've + // seen, then send the acquire event only to the provided dispatcher. + eventDispatcher.drmSessionAcquired(state); } referenceCountListener.onReferenceCountIncremented(this, referenceCount); } @@ -321,15 +322,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; mediaDrm.closeSession(sessionId); sessionId = null; } - dispatchEvent(DrmSessionEventListener.EventDispatcher::drmSessionReleased); } if (eventDispatcher != null) { - if (isOpen()) { - // If the session is still open then send the release event only to the provided dispatcher - // before removing it. + eventDispatchers.remove(eventDispatcher); + if (eventDispatchers.count(eventDispatcher) == 0) { + // Release events are only sent to the last-attached instance of each EventDispatcher. eventDispatcher.drmSessionReleased(); } - eventDispatchers.remove(eventDispatcher); } referenceCountListener.onReferenceCountDecremented(this, referenceCount); } @@ -353,8 +352,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; try { sessionId = mediaDrm.openSession(); mediaCrypto = mediaDrm.createMediaCrypto(sessionId); - dispatchEvent(DrmSessionEventListener.EventDispatcher::drmSessionAcquired); state = STATE_OPENED; + // Capture state into a local so a consistent value is seen by the lambda. + int localState = state; + dispatchEvent(eventDispatcher -> eventDispatcher.drmSessionAcquired(localState)); Assertions.checkNotNull(sessionId); return true; } catch (NotProvisionedException e) { 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 10d6accc51..e8fb9d9781 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 @@ -15,6 +15,10 @@ */ package com.google.android.exoplayer2.drm; +import static com.google.android.exoplayer2.util.Assertions.checkArgument; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkState; + import android.annotation.SuppressLint; import android.media.ResourceBusyException; import android.os.Handler; @@ -31,7 +35,6 @@ import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; -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.Util; @@ -47,9 +50,16 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; -/** A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. */ +/** + * A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. + * + *

    This implementation supports pre-acquisition of sessions using {@link + * #preacquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)}. + */ @RequiresApi(18) public class DefaultDrmSessionManager implements DrmSessionManager { @@ -120,8 +130,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager { */ public Builder setUuidAndExoMediaDrmProvider( UUID uuid, ExoMediaDrm.Provider exoMediaDrmProvider) { - this.uuid = Assertions.checkNotNull(uuid); - this.exoMediaDrmProvider = Assertions.checkNotNull(exoMediaDrmProvider); + this.uuid = checkNotNull(uuid); + this.exoMediaDrmProvider = checkNotNull(exoMediaDrmProvider); return this; } @@ -157,8 +167,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { public Builder setUseDrmSessionsForClearContent( int... useDrmSessionsForClearContentTrackTypes) { for (int trackType : useDrmSessionsForClearContentTrackTypes) { - Assertions.checkArgument( - trackType == C.TRACK_TYPE_VIDEO || trackType == C.TRACK_TYPE_AUDIO); + checkArgument(trackType == C.TRACK_TYPE_VIDEO || trackType == C.TRACK_TYPE_AUDIO); } this.useDrmSessionsForClearContentTrackTypes = useDrmSessionsForClearContentTrackTypes.clone(); @@ -185,7 +194,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { * @return This builder. */ public Builder setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) { - this.loadErrorHandlingPolicy = Assertions.checkNotNull(loadErrorHandlingPolicy); + this.loadErrorHandlingPolicy = checkNotNull(loadErrorHandlingPolicy); return this; } @@ -205,7 +214,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { * @return This builder. */ public Builder setSessionKeepaliveMs(long sessionKeepaliveMs) { - Assertions.checkArgument(sessionKeepaliveMs > 0 || sessionKeepaliveMs == C.TIME_UNSET); + checkArgument(sessionKeepaliveMs > 0 || sessionKeepaliveMs == C.TIME_UNSET); this.sessionKeepaliveMs = sessionKeepaliveMs; return this; } @@ -282,14 +291,15 @@ public class DefaultDrmSessionManager implements DrmSessionManager { private final List sessions; private final List provisioningSessions; + private final Set preacquiredSessionReferences; private final Set keepaliveSessions; private int prepareCallsCount; @Nullable private ExoMediaDrm exoMediaDrm; @Nullable private DefaultDrmSession placeholderDrmSession; @Nullable private DefaultDrmSession noMultiSessionDrmSession; - @Nullable private Looper playbackLooper; - private @MonotonicNonNull Handler sessionReleasingHandler; + private @MonotonicNonNull Looper playbackLooper; + private @MonotonicNonNull Handler playbackHandler; private int mode; @Nullable private byte[] offlineLicenseKeySetId; @@ -388,8 +398,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager { boolean playClearSamplesWithoutKeys, LoadErrorHandlingPolicy loadErrorHandlingPolicy, long sessionKeepaliveMs) { - Assertions.checkNotNull(uuid); - Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead"); + checkNotNull(uuid); + checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead"); this.uuid = uuid; this.exoMediaDrmProvider = exoMediaDrmProvider; this.callback = callback; @@ -403,6 +413,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { mode = MODE_PLAYBACK; sessions = new ArrayList<>(); provisioningSessions = new ArrayList<>(); + preacquiredSessionReferences = Sets.newIdentityHashSet(); keepaliveSessions = Sets.newIdentityHashSet(); this.sessionKeepaliveMs = sessionKeepaliveMs; } @@ -432,9 +443,9 @@ public class DefaultDrmSessionManager implements DrmSessionManager { * @param offlineLicenseKeySetId The key set id of the license to be used with the given mode. */ public void setMode(@Mode int mode, @Nullable byte[] offlineLicenseKeySetId) { - Assertions.checkState(sessions.isEmpty()); + checkState(sessions.isEmpty()); if (mode == MODE_QUERY || mode == MODE_RELEASE) { - Assertions.checkNotNull(offlineLicenseKeySetId); + checkNotNull(offlineLicenseKeySetId); } this.mode = mode; this.offlineLicenseKeySetId = offlineLicenseKeySetId; @@ -447,7 +458,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { if (prepareCallsCount++ != 0) { return; } - Assertions.checkState(exoMediaDrm == null); + checkState(exoMediaDrm == null); exoMediaDrm = exoMediaDrmProvider.acquireExoMediaDrm(uuid); exoMediaDrm.setOnEventListener(new MediaDrmEventListener()); } @@ -466,10 +477,24 @@ public class DefaultDrmSessionManager implements DrmSessionManager { sessions.get(i).release(/* eventDispatcher= */ null); } } - Assertions.checkNotNull(exoMediaDrm).release(); + releaseAllPreacquiredSessions(); + + checkNotNull(exoMediaDrm).release(); exoMediaDrm = null; } + @Override + public DrmSessionReference preacquireSession( + Looper playbackLooper, + @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, + Format format) { + initPlaybackLooper(playbackLooper); + PreacquiredSessionReference preacquiredSessionReference = + new PreacquiredSessionReference(eventDispatcher); + preacquiredSessionReference.acquire(format); + return preacquiredSessionReference; + } + @Override @Nullable public DrmSession acquireSession( @@ -477,16 +502,32 @@ public class DefaultDrmSessionManager implements DrmSessionManager { @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, Format format) { initPlaybackLooper(playbackLooper); + return acquireSession( + playbackLooper, + eventDispatcher, + format, + /* shouldReleasePreacquiredSessionsBeforeRetrying= */ true); + } + + // Must be called on the playback thread. + @Nullable + private DrmSession acquireSession( + Looper playbackLooper, + @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, + Format format, + boolean shouldReleasePreacquiredSessionsBeforeRetrying) { maybeCreateMediaDrmHandler(playbackLooper); if (format.drmInitData == null) { // Content is not encrypted. - return maybeAcquirePlaceholderSession(MimeTypes.getTrackType(format.sampleMimeType)); + return maybeAcquirePlaceholderSession( + MimeTypes.getTrackType(format.sampleMimeType), + shouldReleasePreacquiredSessionsBeforeRetrying); } @Nullable List schemeDatas = null; if (offlineLicenseKeySetId == null) { - schemeDatas = getSchemeDatas(Assertions.checkNotNull(format.drmInitData), uuid, false); + schemeDatas = getSchemeDatas(checkNotNull(format.drmInitData), uuid, false); if (schemeDatas.isEmpty()) { final MissingSchemeDataException error = new MissingSchemeDataException(uuid); Log.e(TAG, "DRM error", error); @@ -515,7 +556,10 @@ public class DefaultDrmSessionManager implements DrmSessionManager { // Create a new session. session = createAndAcquireSessionWithRetry( - schemeDatas, /* isPlaceholderSession= */ false, eventDispatcher); + schemeDatas, + /* isPlaceholderSession= */ false, + eventDispatcher, + shouldReleasePreacquiredSessionsBeforeRetrying); if (!multiSession) { noMultiSessionDrmSession = session; } @@ -531,7 +575,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { @Nullable public Class getExoMediaCryptoType(Format format) { Class exoMediaCryptoType = - Assertions.checkNotNull(exoMediaDrm).getExoMediaCryptoType(); + checkNotNull(exoMediaDrm).getExoMediaCryptoType(); if (format.drmInitData == null) { int trackType = MimeTypes.getTrackType(format.sampleMimeType); return Util.linearSearch(useDrmSessionsForClearContentTrackTypes, trackType) != C.INDEX_UNSET @@ -547,8 +591,9 @@ public class DefaultDrmSessionManager implements DrmSessionManager { // Internal methods. @Nullable - private DrmSession maybeAcquirePlaceholderSession(int trackType) { - ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm); + private DrmSession maybeAcquirePlaceholderSession( + int trackType, boolean shouldReleasePreacquiredSessionsBeforeRetrying) { + ExoMediaDrm exoMediaDrm = checkNotNull(this.exoMediaDrm); boolean avoidPlaceholderDrmSessions = FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType()) && FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC; @@ -563,7 +608,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager { createAndAcquireSessionWithRetry( /* schemeDatas= */ ImmutableList.of(), /* isPlaceholderSession= */ true, - /* eventDispatcher= */ null); + /* eventDispatcher= */ null, + shouldReleasePreacquiredSessionsBeforeRetrying); sessions.add(placeholderDrmSession); this.placeholderDrmSession = placeholderDrmSession; } else { @@ -607,12 +653,14 @@ public class DefaultDrmSessionManager implements DrmSessionManager { return true; } - private void initPlaybackLooper(Looper playbackLooper) { + @EnsuresNonNull({"this.playbackLooper", "this.playbackHandler"}) + private synchronized void initPlaybackLooper(Looper playbackLooper) { if (this.playbackLooper == null) { this.playbackLooper = playbackLooper; - this.sessionReleasingHandler = new Handler(playbackLooper); + this.playbackHandler = new Handler(playbackLooper); } else { - Assertions.checkState(this.playbackLooper == playbackLooper); + checkState(this.playbackLooper == playbackLooper); + checkNotNull(playbackHandler); } } @@ -625,35 +673,67 @@ public class DefaultDrmSessionManager implements DrmSessionManager { private DefaultDrmSession createAndAcquireSessionWithRetry( @Nullable List schemeDatas, boolean isPlaceholderSession, - @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) { + @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, + boolean shouldReleasePreacquiredSessionsBeforeRetrying) { DefaultDrmSession session = createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher); - if (session.getState() == DrmSession.STATE_ERROR - && (Util.SDK_INT < 19 - || Assertions.checkNotNull(session.getError()).getCause() - instanceof ResourceBusyException)) { - // We're short on DRM session resources, so eagerly release all our keepalive sessions. - // ResourceBusyException is only available at API 19, so on earlier versions we always - // eagerly release regardless of the underlying error. - if (!keepaliveSessions.isEmpty()) { - // Make a local copy, because sessions are removed from this.keepaliveSessions during - // release (via callback). - ImmutableSet keepaliveSessions = - ImmutableSet.copyOf(this.keepaliveSessions); - for (DrmSession keepaliveSession : keepaliveSessions) { - keepaliveSession.release(/* eventDispatcher= */ null); - } - // Undo the acquisitions from createAndAcquireSession(). - session.release(eventDispatcher); - if (sessionKeepaliveMs != C.TIME_UNSET) { - session.release(/* eventDispatcher= */ null); - } - session = createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher); + // If we're short on DRM session resources, first try eagerly releasing all our keepalive + // sessions and then retry the acquisition. + if (acquisitionFailedIndicatingResourceShortage(session) && !keepaliveSessions.isEmpty()) { + // Make a local copy, because sessions are removed from this.keepaliveSessions during + // release (via callback). + ImmutableSet keepaliveSessions = + ImmutableSet.copyOf(this.keepaliveSessions); + for (DrmSession keepaliveSession : keepaliveSessions) { + keepaliveSession.release(/* eventDispatcher= */ null); } + undoAcquisition(session, eventDispatcher); + session = createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher); + } + + // If the acquisition failed again due to continued resource shortage, and + // shouldReleasePreacquiredSessionsBeforeRetrying is true, try releasing all pre-acquired + // sessions and then retry the acquisition. + if (acquisitionFailedIndicatingResourceShortage(session) + && shouldReleasePreacquiredSessionsBeforeRetrying + && !preacquiredSessionReferences.isEmpty()) { + releaseAllPreacquiredSessions(); + undoAcquisition(session, eventDispatcher); + session = createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher); } return session; } + private static boolean acquisitionFailedIndicatingResourceShortage(DrmSession session) { + // ResourceBusyException is only available at API 19, so on earlier versions we + // assume any error indicates resource shortage (ensuring we retry). + return session.getState() == DrmSession.STATE_ERROR + && (Util.SDK_INT < 19 + || checkNotNull(session.getError()).getCause() instanceof ResourceBusyException); + } + + /** + * Undoes the acquisitions from {@link #createAndAcquireSession(List, boolean, + * DrmSessionEventListener.EventDispatcher)}. + */ + private void undoAcquisition( + DrmSession session, @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) { + session.release(eventDispatcher); + if (sessionKeepaliveMs != C.TIME_UNSET) { + session.release(/* eventDispatcher= */ null); + } + } + + private void releaseAllPreacquiredSessions() { + // Make a local copy, because sessions are removed from this.preacquiredSessionReferences + // during release (via callback). + ImmutableSet preacquiredSessionReferences = + ImmutableSet.copyOf(this.preacquiredSessionReferences); + for (PreacquiredSessionReference preacquiredSessionReference : preacquiredSessionReferences) { + preacquiredSessionReference.release(); + } + } + /** * Creates a new {@link DefaultDrmSession} and acquires it on behalf of the caller (passing in * {@code eventDispatcher}). @@ -665,7 +745,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { @Nullable List schemeDatas, boolean isPlaceholderSession, @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) { - Assertions.checkNotNull(exoMediaDrm); + checkNotNull(exoMediaDrm); // Placeholder sessions should always play clear samples without keys. boolean playClearSamplesWithoutKeys = this.playClearSamplesWithoutKeys | isPlaceholderSession; DefaultDrmSession session = @@ -681,7 +761,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { offlineLicenseKeySetId, keyRequestParameters, callback, - Assertions.checkNotNull(playbackLooper), + checkNotNull(playbackLooper), loadErrorHandlingPolicy); // Acquire the session once on behalf of the caller to DrmSessionManager - this is the // reference 'assigned' to the caller which they're responsible for releasing. Do this first, @@ -782,7 +862,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { if (sessionKeepaliveMs != C.TIME_UNSET) { // The session has been acquired elsewhere so we want to cancel our timeout. keepaliveSessions.remove(session); - Assertions.checkNotNull(sessionReleasingHandler).removeCallbacksAndMessages(session); + checkNotNull(playbackHandler).removeCallbacksAndMessages(session); } } @@ -791,7 +871,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { if (newReferenceCount == 1 && sessionKeepaliveMs != C.TIME_UNSET) { // Only the internal keep-alive reference remains, so we can start the timeout. keepaliveSessions.add(session); - Assertions.checkNotNull(sessionReleasingHandler) + checkNotNull(playbackHandler) .postAtTime( () -> session.release(/* eventDispatcher= */ null), session, @@ -812,7 +892,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { } provisioningSessions.remove(session); if (sessionKeepaliveMs != C.TIME_UNSET) { - Assertions.checkNotNull(sessionReleasingHandler).removeCallbacksAndMessages(session); + checkNotNull(playbackHandler).removeCallbacksAndMessages(session); keepaliveSessions.remove(session); } } @@ -824,7 +904,78 @@ public class DefaultDrmSessionManager implements DrmSessionManager { @Override public void onEvent( ExoMediaDrm md, @Nullable byte[] sessionId, int event, int extra, @Nullable byte[] data) { - Assertions.checkNotNull(mediaDrmHandler).obtainMessage(event, sessionId).sendToTarget(); + checkNotNull(mediaDrmHandler).obtainMessage(event, sessionId).sendToTarget(); + } + } + + /** + * An implementation of {@link DrmSessionReference} that lazily acquires the underlying {@link + * DrmSession}. + * + *

    A new instance is needed for each reference (compared to maintaining exactly one instance + * for each {@link DrmSession}) because each associated {@link + * DrmSessionEventListener.EventDispatcher} might be different. The {@link + * DrmSessionEventListener.EventDispatcher} is required to implement the zero-arg {@link + * DrmSessionReference#release()} method. + */ + private class PreacquiredSessionReference implements DrmSessionReference { + + @Nullable private final DrmSessionEventListener.EventDispatcher eventDispatcher; + + @Nullable private DrmSession session; + private boolean isReleased; + + /** + * Constructs an instance. + * + * @param eventDispatcher The {@link DrmSessionEventListener.EventDispatcher} passed to {@link + * #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)}. + */ + public PreacquiredSessionReference( + @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) { + this.eventDispatcher = eventDispatcher; + } + + /** + * Acquires the underlying session. + * + *

    Must be called at most once. Can be called from any thread. + */ + @RequiresNonNull("playbackHandler") + public void acquire(Format format) { + playbackHandler.post( + () -> { + if (prepareCallsCount == 0 || isReleased) { + // The manager has been fully released or this reference has already been released. + // Abort the acquisition attempt. + return; + } + this.session = + acquireSession( + checkNotNull(playbackLooper), + eventDispatcher, + format, + /* shouldReleasePreacquiredSessionsBeforeRetrying= */ false); + preacquiredSessionReferences.add(this); + }); + } + + @Override + public void release() { + // Ensure the underlying session is released immediately if we're already on the playback + // thread, to allow a failed session opening to be immediately retried. + Util.postOrRun( + checkNotNull(playbackHandler), + () -> { + if (isReleased) { + return; + } + if (session != null) { + session.release(eventDispatcher); + } + preacquiredSessionReferences.remove(this); + isReleased = true; + }); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionEventListener.java index 0720d9677f..d0c6dea4fd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionEventListener.java @@ -28,13 +28,19 @@ import java.util.concurrent.CopyOnWriteArrayList; /** Listener of {@link DrmSessionManager} events. */ public interface DrmSessionEventListener { + /** @deprecated Implement {@link #onDrmSessionAcquired(int, MediaPeriodId, int)} instead. */ + @Deprecated + default void onDrmSessionAcquired(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {} + /** * Called each time a drm session is acquired. * * @param windowIndex The window index in the timeline this media period belongs to. * @param mediaPeriodId The {@link MediaPeriodId} associated with the drm session. + * @param state The {@link DrmSession.State} of the session when the acquisition completed. */ - default void onDrmSessionAcquired(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {} + default void onDrmSessionAcquired( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, @DrmSession.State int state) {} /** * Called each time keys are loaded. @@ -149,13 +155,20 @@ public interface DrmSessionEventListener { } } - /** Dispatches {@link #onDrmSessionAcquired(int, MediaPeriodId)}. */ - public void drmSessionAcquired() { + /** + * Dispatches {@link #onDrmSessionAcquired(int, MediaPeriodId, int)} and {@link + * #onDrmSessionAcquired(int, MediaPeriodId)}. + */ + @SuppressWarnings("deprecation") // Calls deprecated listener method. + public void drmSessionAcquired(@DrmSession.State int state) { for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { DrmSessionEventListener listener = listenerAndHandler.listener; postOrRun( listenerAndHandler.handler, - () -> listener.onDrmSessionAcquired(windowIndex, mediaPeriodId)); + () -> { + listener.onDrmSessionAcquired(windowIndex, mediaPeriodId); + listener.onDrmSessionAcquired(windowIndex, mediaPeriodId, state); + }); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java index 70dc4fa7f5..4b3ee553d8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java @@ -22,6 +22,23 @@ import com.google.android.exoplayer2.Format; /** Manages a DRM session. */ public interface DrmSessionManager { + /** + * Represents a single reference count of a {@link DrmSession}, while deliberately not giving + * access to the underlying session. + */ + interface DrmSessionReference { + /** A reference that is never populated with an underlying {@link DrmSession}. */ + DrmSessionReference EMPTY = () -> {}; + + /** + * Releases the underlying session at most once. + * + *

    Can be called from any thread. Calling this method more than once will only release the + * underlying session once. + */ + void release(); + } + /** An instance that supports no DRM schemes. */ DrmSessionManager DRM_UNSUPPORTED = new DrmSessionManager() { @@ -81,6 +98,51 @@ public interface DrmSessionManager { // Do nothing. } + /** + * Pre-acquires a DRM session for the specified {@link Format}. + * + *

    This notifies the manager that a subsequent call to {@link #acquireSession(Looper, + * DrmSessionEventListener.EventDispatcher, Format)} with the same {@link Format} is likely, + * allowing a manager that supports pre-acquisition to get the required {@link DrmSession} ready + * in the background. + * + *

    The caller must call {@link DrmSessionReference#release()} on the returned instance when + * they no longer require the pre-acquisition (i.e. they know they won't be making a matching call + * to {@link #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)} in the near + * future). + * + *

    This manager may silently release the underlying session in order to allow another operation + * to complete. This will result in a subsequent call to {@link #acquireSession(Looper, + * DrmSessionEventListener.EventDispatcher, Format)} re-initializing a new session, including + * repeating key loads and other async initialization steps. + * + *

    The caller must separately call {@link #acquireSession(Looper, + * DrmSessionEventListener.EventDispatcher, Format)} in order to obtain a session suitable for + * playback. The pre-acquired {@link DrmSessionReference} and full {@link DrmSession} instances + * are distinct. The caller must release both, and can release the {@link DrmSessionReference} + * before the {@link DrmSession} without affecting playback. + * + *

    This can be called from any thread. + * + *

    Implementations that do not support pre-acquisition always return an empty {@link + * DrmSessionReference} instance. + * + * @param playbackLooper The looper associated with the media playback thread. + * @param eventDispatcher The {@link DrmSessionEventListener.EventDispatcher} used to distribute + * events, and passed on to {@link + * DrmSession#acquire(DrmSessionEventListener.EventDispatcher)}. + * @param format The {@link Format} for which to pre-acquire a {@link DrmSession}. + * @return A releaser for the pre-acquired session. Guaranteed to be non-null even if the matching + * {@link #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)} would + * return null. + */ + default DrmSessionReference preacquireSession( + Looper playbackLooper, + @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, + Format format) { + return DrmSessionReference.EMPTY; + } + /** * Returns a {@link DrmSession} for the specified {@link Format}, with an incremented reference * count. May return null if the {@link Format#drmInitData} is null and the DRM session manager is 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 f51869ce43..80d5cc0866 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 @@ -733,11 +733,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { throws ExoPlaybackException { this.currentPlaybackSpeed = currentPlaybackSpeed; this.targetPlaybackSpeed = targetPlaybackSpeed; - if (codec != null - && codecDrainAction != DRAIN_ACTION_REINITIALIZE - && getState() != STATE_DISABLED) { - updateCodecOperatingRate(codecInputFormat); - } + updateCodecOperatingRate(codecInputFormat); } @Override @@ -1689,6 +1685,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return CODEC_OPERATING_RATE_UNSET; } + /** + * Updates the codec operating rate, or triggers codec release and re-initialization if a + * previously set operating rate needs to be cleared. + * + * @throws ExoPlaybackException If an error occurs releasing or initializing a codec. + * @return False if codec release and re-initialization was triggered. True in all other cases. + */ + protected final boolean updateCodecOperatingRate() throws ExoPlaybackException { + return updateCodecOperatingRate(codecInputFormat); + } + /** * Updates the codec operating rate, or triggers codec release and re-initialization if a * previously set operating rate needs to be cleared. @@ -1702,6 +1709,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return true; } + if (codec == null + || codecDrainAction == DRAIN_ACTION_REINITIALIZE + || getState() == STATE_DISABLED) { + // No need to update the operating rate. + return true; + } + float newCodecOperatingRate = getCodecOperatingRateV23(targetPlaybackSpeed, format, getStreamFormats()); if (codecOperatingRate == newCodecOperatingRate) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java index 5f1464721c..e6bba9ae46 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java @@ -19,6 +19,7 @@ import android.os.Handler; import androidx.annotation.CallSuper; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; @@ -290,9 +291,10 @@ public abstract class CompositeMediaSource extends BaseMediaSource { // DrmSessionEventListener implementation @Override - public void onDrmSessionAcquired(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { + public void onDrmSessionAcquired( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, @DrmSession.State int state) { if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - drmEventDispatcher.drmSessionAcquired(); + drmEventDispatcher.drmSessionAcquired(state); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index c2fa35275c..c0548791fc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -336,7 +336,7 @@ public final class ExtractorMediaSource extends CompositeMediaSource { .setTag(tag) .build(), dataSourceFactory, - extractorsFactory, + () -> new BundledExtractorsAdapter(extractorsFactory), DrmSessionManager.DRM_UNSUPPORTED, loadableLoadErrorHandlingPolicy, continueLoadingCheckIntervalBytes); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaExtractor.java index 9efe6acba1..ed4f6ac604 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaExtractor.java @@ -26,7 +26,14 @@ import java.util.List; import java.util.Map; /** Extracts the contents of a container file from a progressive media stream. */ -/* package */ interface ProgressiveMediaExtractor { +public interface ProgressiveMediaExtractor { + + /** Creates {@link ProgressiveMediaExtractor} instances. */ + interface Factory { + + /** Returns a new {@link ProgressiveMediaExtractor} instance. */ + ProgressiveMediaExtractor createProgressiveMediaExtractor(); + } /** * Initializes the underlying infrastructure for reading from the input. 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 57b4d2be2b..66af0e5eee 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 @@ -31,7 +31,6 @@ import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.Extractor; 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.extractor.SeekMap.SeekPoints; @@ -147,7 +146,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * @param uri The {@link Uri} of the media stream. * @param dataSource The data source to read the media. - * @param extractorsFactory The {@link ExtractorsFactory} to use to read the data source. + * @param progressiveMediaExtractor The {@link ProgressiveMediaExtractor} to use to read the data + * source. * @param drmSessionManager A {@link DrmSessionManager} to allow DRM interactions. * @param drmEventDispatcher A dispatcher to notify of {@link DrmSessionEventListener} events. * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}. @@ -168,7 +168,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; public ProgressiveMediaPeriod( Uri uri, DataSource dataSource, - ExtractorsFactory extractorsFactory, + ProgressiveMediaExtractor progressiveMediaExtractor, DrmSessionManager drmSessionManager, DrmSessionEventListener.EventDispatcher drmEventDispatcher, LoadErrorHandlingPolicy loadErrorHandlingPolicy, @@ -188,7 +188,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; this.customCacheKey = customCacheKey; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; loader = new Loader("ProgressiveMediaPeriod"); - this.progressiveMediaExtractor = new BundledExtractorsAdapter(extractorsFactory); + this.progressiveMediaExtractor = progressiveMediaExtractor; loadCondition = new ConditionVariable(); maybeFinishPrepareRunnable = this::maybeFinishPrepare; onContinueLoadingRequestedRunnable = diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java index fe249df6ff..e7d97dbf8b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java @@ -54,7 +54,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource private final DataSource.Factory dataSourceFactory; - private ExtractorsFactory extractorsFactory; + private ProgressiveMediaExtractor.Factory progressiveMediaExtractorFactory; private boolean usingCustomDrmSessionManagerProvider; private DrmSessionManagerProvider drmSessionManagerProvider; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; @@ -72,15 +72,26 @@ public final class ProgressiveMediaSource extends BaseMediaSource this(dataSourceFactory, new DefaultExtractorsFactory()); } + /** + * Equivalent to {@link #Factory(DataSource.Factory, ProgressiveMediaExtractor.Factory) new + * Factory(dataSourceFactory, () -> new BundledExtractorsAdapter(extractorsFactory)}. + */ + public Factory(DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) { + this(dataSourceFactory, () -> new BundledExtractorsAdapter(extractorsFactory)); + } + /** * Creates a new factory for {@link ProgressiveMediaSource}s. * * @param dataSourceFactory A factory for {@link DataSource}s to read the media. - * @param extractorsFactory A factory for extractors used to extract media from its container. + * @param progressiveMediaExtractorFactory A factory for the {@link ProgressiveMediaExtractor} + * to extract media from its container. */ - public Factory(DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) { + public Factory( + DataSource.Factory dataSourceFactory, + ProgressiveMediaExtractor.Factory progressiveMediaExtractorFactory) { this.dataSourceFactory = dataSourceFactory; - this.extractorsFactory = extractorsFactory; + this.progressiveMediaExtractorFactory = progressiveMediaExtractorFactory; drmSessionManagerProvider = new DefaultDrmSessionManagerProvider(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES; @@ -93,8 +104,10 @@ public final class ProgressiveMediaSource extends BaseMediaSource */ @Deprecated public Factory setExtractorsFactory(@Nullable ExtractorsFactory extractorsFactory) { - this.extractorsFactory = - extractorsFactory != null ? extractorsFactory : new DefaultExtractorsFactory(); + this.progressiveMediaExtractorFactory = + () -> + new BundledExtractorsAdapter( + extractorsFactory != null ? extractorsFactory : new DefaultExtractorsFactory()); return this; } @@ -220,7 +233,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource return new ProgressiveMediaSource( mediaItem, dataSourceFactory, - extractorsFactory, + progressiveMediaExtractorFactory, drmSessionManagerProvider.get(mediaItem), loadErrorHandlingPolicy, continueLoadingCheckIntervalBytes); @@ -241,7 +254,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource private final MediaItem mediaItem; private final MediaItem.PlaybackProperties playbackProperties; private final DataSource.Factory dataSourceFactory; - private final ExtractorsFactory extractorsFactory; + private final ProgressiveMediaExtractor.Factory progressiveMediaExtractorFactory; private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy; private final int continueLoadingCheckIntervalBytes; @@ -256,14 +269,14 @@ public final class ProgressiveMediaSource extends BaseMediaSource /* package */ ProgressiveMediaSource( MediaItem mediaItem, DataSource.Factory dataSourceFactory, - ExtractorsFactory extractorsFactory, + ProgressiveMediaExtractor.Factory progressiveMediaExtractorFactory, DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy, int continueLoadingCheckIntervalBytes) { this.playbackProperties = checkNotNull(mediaItem.playbackProperties); this.mediaItem = mediaItem; this.dataSourceFactory = dataSourceFactory; - this.extractorsFactory = extractorsFactory; + this.progressiveMediaExtractorFactory = progressiveMediaExtractorFactory; this.drmSessionManager = drmSessionManager; this.loadableLoadErrorHandlingPolicy = loadableLoadErrorHandlingPolicy; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; @@ -308,7 +321,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource return new ProgressiveMediaPeriod( playbackProperties.uri, dataSource, - extractorsFactory, + progressiveMediaExtractorFactory.createProgressiveMediaExtractor(), drmSessionManager, createDrmEventDispatcher(id), loadableLoadErrorHandlingPolicy, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BundledChunkExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BundledChunkExtractor.java index f02329d5d5..ff19ee1d26 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BundledChunkExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BundledChunkExtractor.java @@ -29,8 +29,12 @@ 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.extractor.mkv.MatroskaExtractor; +import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; +import com.google.android.exoplayer2.extractor.rawcc.RawCcExtractor; import com.google.android.exoplayer2.upstream.DataReader; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -41,6 +45,41 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; */ public final class BundledChunkExtractor implements ExtractorOutput, ChunkExtractor { + /** {@link ChunkExtractor.Factory} for instances of this class. */ + public static final ChunkExtractor.Factory FACTORY = + (primaryTrackType, + format, + enableEventMessageTrack, + closedCaptionFormats, + playerEmsgTrackOutput) -> { + @Nullable String containerMimeType = format.containerMimeType; + Extractor extractor; + if (MimeTypes.isText(containerMimeType)) { + if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) { + // RawCC is special because it's a text specific container format. + extractor = new RawCcExtractor(format); + } else { + // All other text types are raw formats that do not need an extractor. + return null; + } + } else if (MimeTypes.isMatroska(containerMimeType)) { + extractor = new MatroskaExtractor(MatroskaExtractor.FLAG_DISABLE_SEEK_FOR_CUES); + } else { + int flags = 0; + if (enableEventMessageTrack) { + flags |= FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK; + } + extractor = + new FragmentedMp4Extractor( + flags, + /* timestampAdjuster= */ null, + /* sideloadedTrack= */ null, + closedCaptionFormats, + playerEmsgTrackOutput); + } + return new BundledChunkExtractor(extractor, primaryTrackType, format); + }; + private static final PositionHolder POSITION_HOLDER = new PositionHolder(); private final Extractor extractor; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractor.java index 6bfe9590db..60774c3ea6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractor.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.TrackOutput; import java.io.IOException; +import java.util.List; /** * Extracts samples and track {@link Format Formats} from chunks. @@ -31,6 +32,27 @@ import java.io.IOException; */ public interface ChunkExtractor { + /** Creates {@link ChunkExtractor} instances. */ + interface Factory { + + /** + * Returns a new {@link ChunkExtractor} instance. + * + * @param primaryTrackType The type of the primary track. One of {@link C C.TRACK_TYPE_*}. + * @param representationFormat The format of the representation to extract from. + * @param enableEventMessageTrack Whether to enable the event message track. + * @param closedCaptionFormats The {@link Format Formats} of the Closed-Caption tracks. + * @return A new {@link ChunkExtractor} instance, or null if not applicable. + */ + @Nullable + ChunkExtractor createProgressiveMediaExtractor( + int primaryTrackType, + Format representationFormat, + boolean enableEventMessageTrack, + List closedCaptionFormats, + @Nullable TrackOutput playerEmsgTrackOutput); + } + /** Provides {@link TrackOutput} instances to be written to during extraction. */ interface TrackOutputProvider { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaParserChunkExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaParserChunkExtractor.java index 7c440b46d7..ffedb4b1ff 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaParserChunkExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaParserChunkExtractor.java @@ -26,6 +26,7 @@ import static com.google.android.exoplayer2.source.mediaparser.MediaParserUtil.P import android.annotation.SuppressLint; import android.media.MediaFormat; import android.media.MediaParser; +import android.util.Log; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; @@ -49,6 +50,25 @@ import java.util.List; @RequiresApi(30) public final class MediaParserChunkExtractor implements ChunkExtractor { + // Maximum TAG length is 23 characters. + private static final String TAG = "MediaPrsrChunkExtractor"; + + public static final ChunkExtractor.Factory FACTORY = + (primaryTrackType, + format, + enableEventMessageTrack, + closedCaptionFormats, + playerEmsgTrackOutput) -> { + if (!MimeTypes.isText(format.containerMimeType)) { + // Container is either Matroska or Fragmented MP4. + return new MediaParserChunkExtractor(primaryTrackType, format, closedCaptionFormats); + } else { + // This is either RAWCC (unsupported) or a text track that does not require an extractor. + Log.w(TAG, "Ignoring an unsupported text track."); + return null; + } + }; + private final OutputConsumerAdapterV30 outputConsumerAdapter; private final InputReaderAdapterV30 inputReaderAdapter; private final MediaParser mediaParser; 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 777290e0fc..72755e62f3 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 @@ -318,8 +318,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { } if (style.fontSize != Cue.DIMEN_UNSET && screenHeight != Cue.DIMEN_UNSET) { cue.setTextSize( - style.fontSize / screenHeight, - Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING); + style.fontSize / screenHeight, Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING); } if (style.bold && style.italic) { spannableText.setSpan( 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 index 03c025cd94..df3db09d73 100644 --- 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 @@ -76,7 +76,9 @@ import com.google.android.exoplayer2.util.Util; break; } } - return (startTimeIndex != C.INDEX_UNSET && endTimeIndex != C.INDEX_UNSET) + return (startTimeIndex != C.INDEX_UNSET + && endTimeIndex != C.INDEX_UNSET + && textIndex != 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 index 3e0279fca0..4873ee9d2b 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 @@ -125,11 +125,21 @@ import java.util.regex.Pattern; try { return new SsaStyle( styleValues[format.nameIndex].trim(), - parseAlignment(styleValues[format.alignmentIndex].trim()), - parseColor(styleValues[format.primaryColorIndex].trim()), - parseFontSize(styleValues[format.fontSizeIndex].trim()), - parseBold(styleValues[format.boldIndex].trim()), - parseItalic(styleValues[format.italicIndex].trim())); + format.alignmentIndex != C.INDEX_UNSET + ? parseAlignment(styleValues[format.alignmentIndex].trim()) + : SSA_ALIGNMENT_UNKNOWN, + format.primaryColorIndex != C.INDEX_UNSET + ? parseColor(styleValues[format.primaryColorIndex].trim()) + : null, + format.fontSizeIndex != C.INDEX_UNSET + ? parseFontSize(styleValues[format.fontSizeIndex].trim()) + : Cue.DIMEN_UNSET, + format.boldIndex != C.INDEX_UNSET) + ? parseBold(styleValues[format.boldIndex].trim()) + : false, + format.italicIndex != C.INDEX_UNSET) + ? parseItalic(styleValues[format.italicIndex].trim()) + : false); } catch (RuntimeException e) { Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e); return null; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java index 4a9fbe193e..53ae2e9cd6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java @@ -19,6 +19,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; import com.google.android.exoplayer2.trackselection.ExoTrackSelection.Definition; +import com.google.android.exoplayer2.util.MimeTypes; import org.checkerframework.checker.nullness.compatqual.NullableType; /** Track selection related utility methods. */ @@ -97,4 +98,20 @@ public final class TrackSelectionUtil { } return builder.build(); } + + /** Returns if a {@link TrackSelectionArray} has at least one track of the given type. */ + public static boolean hasTrackOfType(TrackSelectionArray trackSelections, int trackType) { + for (int i = 0; i < trackSelections.length; i++) { + @Nullable TrackSelection trackSelection = trackSelections.get(i); + if (trackSelection == null) { + continue; + } + for (int j = 0; j < trackSelection.length(); j++) { + if (MimeTypes.getTrackType(trackSelection.getFormat(j).sampleMimeType) == trackType) { + return true; + } + } + } + return false; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java index e529e28846..a7ec8fd81e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java @@ -71,7 +71,7 @@ public final class AssetDataSource extends BaseDataSource { if (skipped < dataSpec.position) { // assetManager.open() returns an AssetInputStream, whose skip() implementation only skips // fewer bytes than requested if the skip is beyond the end of the asset's data. - throw new EOFException(); + throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE); } if (dataSpec.length != C.LENGTH_UNSET) { bytesRemaining = dataSpec.length; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java index 17e9073128..9f9f11b67d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java @@ -47,13 +47,13 @@ public final class ByteArrayDataSource extends BaseDataSource { public long open(DataSpec dataSpec) throws IOException { uri = dataSpec.uri; transferInitializing(dataSpec); - readPosition = (int) dataSpec.position; - bytesRemaining = (int) ((dataSpec.length == C.LENGTH_UNSET) - ? (data.length - dataSpec.position) : dataSpec.length); - if (bytesRemaining <= 0 || readPosition + bytesRemaining > data.length) { - throw new IOException("Unsatisfiable range: [" + readPosition + ", " + dataSpec.length - + "], length: " + data.length); + if (dataSpec.position >= data.length) { + throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE); } + readPosition = (int) dataSpec.position; + bytesRemaining = + (int) + (dataSpec.length == C.LENGTH_UNSET ? data.length - dataSpec.position : dataSpec.length); opened = true; transferStarted(dataSpec); return bytesRemaining; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java index b659c5ca98..75359504bd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java @@ -80,7 +80,7 @@ public final class ContentDataSource extends BaseDataSource { if (skipped != dataSpec.position) { // We expect the skip to be satisfied in full. If it isn't then we're probably trying to // skip beyond the end of the data. - throw new EOFException(); + throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE); } if (dataSpec.length != C.LENGTH_UNSET) { bytesRemaining = dataSpec.length; @@ -96,13 +96,13 @@ public final class ContentDataSource extends BaseDataSource { } else { bytesRemaining = channelSize - channel.position(); if (bytesRemaining < 0) { - throw new EOFException(); + throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE); } } } else { bytesRemaining = assetFileDescriptorLength - skipped; if (bytesRemaining < 0) { - throw new EOFException(); + throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE); } } } 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 2b9cf00e47..30752626fa 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 @@ -69,7 +69,7 @@ public final class DataSchemeDataSource extends BaseDataSource { } endPosition = dataSpec.length != C.LENGTH_UNSET ? (int) dataSpec.length + readPosition : data.length; - if (endPosition > data.length || readPosition > endPosition) { + if (readPosition >= endPosition) { data = null; throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE); } 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 d34e43eb46..7fba170f36 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 @@ -23,7 +23,6 @@ import android.text.TextUtils; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; -import java.io.EOFException; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; @@ -91,7 +90,7 @@ public final class FileDataSource extends BaseDataSource { bytesRemaining = dataSpec.length == C.LENGTH_UNSET ? file.length() - dataSpec.position : dataSpec.length; if (bytesRemaining < 0) { - throw new EOFException(); + throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE); } } catch (IOException e) { throw new FileDataSourceException(e); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java index 7538cc67a4..2568d49c3a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java @@ -60,7 +60,7 @@ public final class RawResourceDataSource extends BaseDataSource { super(message); } - public RawResourceDataSourceException(IOException e) { + public RawResourceDataSourceException(Throwable e) { super(e); } } @@ -133,21 +133,39 @@ public final class RawResourceDataSource extends BaseDataSource { } transferInitializing(dataSpec); - AssetFileDescriptor assetFileDescriptor = resources.openRawResourceFd(resourceId); + + AssetFileDescriptor assetFileDescriptor; + try { + assetFileDescriptor = resources.openRawResourceFd(resourceId); + } catch (Resources.NotFoundException e) { + throw new RawResourceDataSourceException(e); + } + this.assetFileDescriptor = assetFileDescriptor; if (assetFileDescriptor == null) { throw new RawResourceDataSourceException("Resource is compressed: " + uri); } + long assetFileDescriptorLength = assetFileDescriptor.getLength(); FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); this.inputStream = inputStream; try { + // We can't rely only on the "skipped < dataSpec.position" check below to detect whether the + // position is beyond the end of the resource being read. This is because the file will + // typically contain multiple resources, and there's nothing to prevent InputStream.skip() + // from succeeding by skipping into the data of the next resource. Hence we also need to check + // against the resource length explicitly, which is guaranteed to be set unless the resource + // extends to the end of the file. + if (assetFileDescriptorLength != AssetFileDescriptor.UNKNOWN_LENGTH + && dataSpec.position > assetFileDescriptorLength) { + throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE); + } inputStream.skip(assetFileDescriptor.getStartOffset()); long skipped = inputStream.skip(dataSpec.position); if (skipped < dataSpec.position) { // We expect the skip to be satisfied in full. If it isn't then we're probably trying to - // skip beyond the end of the data. - throw new EOFException(); + // read beyond the end of the last resource in the file. + throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE); } } catch (IOException e) { throw new RawResourceDataSourceException(e); @@ -156,7 +174,6 @@ public final class RawResourceDataSource extends BaseDataSource { if (dataSpec.length != C.LENGTH_UNSET) { bytesRemaining = dataSpec.length; } else { - long assetFileDescriptorLength = assetFileDescriptor.getLength(); // If the length is UNKNOWN_LENGTH then the asset extends to the end of the file. bytesRemaining = assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheWriter.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheWriter.java index 8ea2b4e280..0d2266b7db 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheWriter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheWriter.java @@ -55,7 +55,6 @@ public final class CacheWriter { private final byte[] temporaryBuffer; @Nullable private final ProgressListener progressListener; - private boolean initialized; private long nextPosition; private long endPosition; private long bytesCached; @@ -118,18 +117,15 @@ public final class CacheWriter { public void cache() throws IOException { throwIfCanceled(); - if (!initialized) { - if (dataSpec.length != C.LENGTH_UNSET) { - endPosition = dataSpec.position + dataSpec.length; - } else { - long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(cacheKey)); - endPosition = contentLength == C.LENGTH_UNSET ? C.POSITION_UNSET : contentLength; - } - bytesCached = cache.getCachedBytes(cacheKey, dataSpec.position, dataSpec.length); - if (progressListener != null) { - progressListener.onProgress(getLength(), bytesCached, /* newBytesCached= */ 0); - } - initialized = true; + bytesCached = cache.getCachedBytes(cacheKey, dataSpec.position, dataSpec.length); + if (dataSpec.length != C.LENGTH_UNSET) { + endPosition = dataSpec.position + dataSpec.length; + } else { + long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(cacheKey)); + endPosition = contentLength == C.LENGTH_UNSET ? C.POSITION_UNSET : contentLength; + } + if (progressListener != null) { + progressListener.onProgress(getLength(), bytesCached, /* newBytesCached= */ 0); } while (endPosition == C.POSITION_UNSET || nextPosition < endPosition) { @@ -158,42 +154,50 @@ public final class CacheWriter { */ private long readBlockToCache(long position, long length) throws IOException { boolean isLastBlock = position + length == endPosition || length == C.LENGTH_UNSET; - try { - long resolvedLength = C.LENGTH_UNSET; - boolean isDataSourceOpen = false; - if (length != C.LENGTH_UNSET) { - // If the length is specified, try to open the data source with a bounded request to avoid - // the underlying network stack requesting more data than required. - try { - DataSpec boundedDataSpec = - dataSpec.buildUpon().setPosition(position).setLength(length).build(); - resolvedLength = dataSource.open(boundedDataSpec); - isDataSourceOpen = true; - } catch (IOException exception) { - if (allowShortContent - && isLastBlock - && DataSourceException.isCausedByPositionOutOfRange(exception)) { - // The length of the request exceeds the length of the content. If we allow shorter - // content and are reading the last block, fall through and try again with an unbounded - // request to read up to the end of the content. - Util.closeQuietly(dataSource); - } else { - throw exception; - } + + long resolvedLength = C.LENGTH_UNSET; + boolean isDataSourceOpen = false; + if (length != C.LENGTH_UNSET) { + // If the length is specified, try to open the data source with a bounded request to avoid + // the underlying network stack requesting more data than required. + DataSpec boundedDataSpec = + dataSpec.buildUpon().setPosition(position).setLength(length).build(); + try { + resolvedLength = dataSource.open(boundedDataSpec); + isDataSourceOpen = true; + } catch (IOException e) { + Util.closeQuietly(dataSource); + if (allowShortContent + && isLastBlock + && DataSourceException.isCausedByPositionOutOfRange(e)) { + // The length of the request exceeds the length of the content. If we allow shorter + // content and are reading the last block, fall through and try again with an unbounded + // request to read up to the end of the content. + } else { + throw e; } } - if (!isDataSourceOpen) { - // Either the length was unspecified, or we allow short content and our attempt to open the - // DataSource with the specified length failed. - throwIfCanceled(); - DataSpec unboundedDataSpec = - dataSpec.buildUpon().setPosition(position).setLength(C.LENGTH_UNSET).build(); + } + + if (!isDataSourceOpen) { + // Either the length was unspecified, or we allow short content and our attempt to open the + // DataSource with the specified length failed. + throwIfCanceled(); + DataSpec unboundedDataSpec = + dataSpec.buildUpon().setPosition(position).setLength(C.LENGTH_UNSET).build(); + try { resolvedLength = dataSource.open(unboundedDataSpec); + } catch (IOException e) { + Util.closeQuietly(dataSource); + throw e; } + } + + int totalBytesRead = 0; + try { if (isLastBlock && resolvedLength != C.LENGTH_UNSET) { onRequestEndPosition(position + resolvedLength); } - int totalBytesRead = 0; int bytesRead = 0; while (bytesRead != C.RESULT_END_OF_INPUT) { throwIfCanceled(); @@ -206,10 +210,16 @@ public final class CacheWriter { if (isLastBlock) { onRequestEndPosition(position + totalBytesRead); } - return totalBytesRead; - } finally { + } catch (IOException e) { Util.closeQuietly(dataSource); + throw e; } + + // Util.closeQuietly(dataSource) is not used here because it's important that an exception is + // thrown if DataSource.close fails. This is because there's no way of knowing whether the block + // was successfully cached in this case. + dataSource.close(); + return totalBytesRead; } private void onRequestEndPosition(long endPosition) { 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 6e25f1f0a2..38aab00d43 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 @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; +import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.MediaLoadData; @@ -479,8 +480,8 @@ public class EventLogger implements AnalyticsListener { } @Override - public void onDrmSessionAcquired(EventTime eventTime) { - logd(eventTime, "drmSessionAcquired"); + public void onDrmSessionAcquired(EventTime eventTime, @DrmSession.State int state) { + logd(eventTime, "drmSessionAcquired", "state=" + state); } @Override 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 7638aadca8..78b1a72867 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 @@ -69,11 +69,8 @@ public interface VideoRendererEventListener { * decoder instance can be reused for the new format, or {@code null} if the renderer did not * have a decoder. */ - @SuppressWarnings("deprecation") default void onVideoInputFormatChanged( - Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { - onVideoInputFormatChanged(format); - } + Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {} /** * Called to report the number of frames dropped by the renderer. Dropped frames are reported @@ -133,7 +130,12 @@ public interface VideoRendererEventListener { * * @param surface The {@link Surface} to which a first frame has been rendered, or {@code null} if * the renderer renders to something that isn't a {@link Surface}. + * @param renderTimeMs The {@link SystemClock#elapsedRealtime()} when the frame was rendered. */ + default void onRenderedFirstFrame(@Nullable Surface surface, long renderTimeMs) {} + + /** @deprecated Use {@link #onRenderedFirstFrame(Surface, long)}. */ + @Deprecated default void onRenderedFirstFrame(@Nullable Surface surface) {} /** @@ -205,11 +207,15 @@ public interface VideoRendererEventListener { * Invokes {@link VideoRendererEventListener#onVideoInputFormatChanged(Format, * DecoderReuseEvaluation)}. */ + @SuppressWarnings("deprecation") // Calling deprecated listener method. public void inputFormatChanged( Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { if (handler != null) { handler.post( - () -> castNonNull(listener).onVideoInputFormatChanged(format, decoderReuseEvaluation)); + () -> { + castNonNull(listener).onVideoInputFormatChanged(format); + castNonNull(listener).onVideoInputFormatChanged(format, decoderReuseEvaluation); + }); } } @@ -245,10 +251,16 @@ public interface VideoRendererEventListener { } } - /** Invokes {@link VideoRendererEventListener#onRenderedFirstFrame(Surface)}. */ + /** Invokes {@link VideoRendererEventListener#onRenderedFirstFrame(Surface, long)}. */ public void renderedFirstFrame(@Nullable Surface surface) { if (handler != null) { - handler.post(() -> castNonNull(listener).onRenderedFirstFrame(surface)); + // TODO: Replace this timestamp with the actual frame release time. + long renderTimeMs = SystemClock.elapsedRealtime(); + handler.post( + () -> { + castNonNull(listener).onRenderedFirstFrame(surface); + castNonNull(listener).onRenderedFirstFrame(surface, renderTimeMs); + }); } } 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 d5a00201ac..07388f6b5e 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 @@ -8605,6 +8605,20 @@ public final class ExoPlayerTest { assertThat(liveOffsetAtEnd).isIn(Range.closed(3_900L, 4_100L)); } + @Test + public void playerIdle_withSetPlaybackSpeed_usesPlaybackParameterSpeedWithPitchUnchanged() { + ExoPlayer player = new TestExoPlayerBuilder(context).build(); + player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1, /* pitch= */ 2)); + Player.EventListener mockListener = mock(Player.EventListener.class); + player.addListener(mockListener); + player.prepare(); + + player.setPlaybackSpeed(2); + + verify(mockListener) + .onPlaybackParametersChanged(new PlaybackParameters(/* speed= */ 2, /* pitch= */ 2)); + } + @Test public void targetLiveOffsetInMedia_withSetPlaybackParameters_usesPlaybackParameterSpeed() throws Exception { 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 bc7d149007..3a753d6e2b 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 @@ -80,6 +80,7 @@ import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaDrm; import com.google.android.exoplayer2.drm.MediaDrmCallback; @@ -1700,12 +1701,12 @@ public final class AnalyticsCollectorTest { ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); verify(listener, atLeastOnce()) .onVideoDecoderInitialized( - individualVideoDecoderInitializedEventTimes.capture(), any(), anyLong()); + individualVideoDecoderInitializedEventTimes.capture(), any(), anyLong(), anyLong()); ArgumentCaptor individualAudioDecoderInitializedEventTimes = ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); verify(listener, atLeastOnce()) .onAudioDecoderInitialized( - individualAudioDecoderInitializedEventTimes.capture(), any(), anyLong()); + individualAudioDecoderInitializedEventTimes.capture(), any(), anyLong(), anyLong()); ArgumentCaptor individualVideoDisabledEventTimes = ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); verify(listener, atLeastOnce()) @@ -1717,7 +1718,7 @@ public final class AnalyticsCollectorTest { ArgumentCaptor individualRenderedFirstFrameEventTimes = ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); verify(listener, atLeastOnce()) - .onRenderedFirstFrame(individualRenderedFirstFrameEventTimes.capture(), any()); + .onRenderedFirstFrame(individualRenderedFirstFrameEventTimes.capture(), any(), anyLong()); ArgumentCaptor individualVideoSizeChangedEventTimes = ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); verify(listener, atLeastOnce()) @@ -2183,7 +2184,10 @@ public final class AnalyticsCollectorTest { @Override public void onAudioDecoderInitialized( - EventTime eventTime, String decoderName, long initializationDurationMs) { + EventTime eventTime, + String decoderName, + long initializedTimestampMs, + long initializationDurationMs) { reportedEvents.add(new ReportedEvent(EVENT_AUDIO_DECODER_INITIALIZED, eventTime)); } @@ -2220,7 +2224,10 @@ public final class AnalyticsCollectorTest { @Override public void onVideoDecoderInitialized( - EventTime eventTime, String decoderName, long initializationDurationMs) { + EventTime eventTime, + String decoderName, + long initializedTimestampMs, + long initializationDurationMs) { reportedEvents.add(new ReportedEvent(EVENT_VIDEO_DECODER_INITIALIZED, eventTime)); } @@ -2246,7 +2253,8 @@ public final class AnalyticsCollectorTest { } @Override - public void onRenderedFirstFrame(EventTime eventTime, @Nullable Surface surface) { + public void onRenderedFirstFrame( + EventTime eventTime, @Nullable Surface surface, long renderTimeMs) { reportedEvents.add(new ReportedEvent(EVENT_RENDERED_FIRST_FRAME, eventTime)); } @@ -2261,7 +2269,7 @@ public final class AnalyticsCollectorTest { } @Override - public void onDrmSessionAcquired(EventTime eventTime) { + public void onDrmSessionAcquired(EventTime eventTime, @DrmSession.State int state) { reportedEvents.add(new ReportedEvent(EVENT_DRM_SESSION_ACQUIRED, eventTime)); } 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 13c62c96b9..f1a4fb91f6 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 @@ -66,7 +66,7 @@ public final class DefaultAudioSinkTest { new DefaultAudioSink.DefaultAudioProcessorChain(teeAudioProcessor), /* enableFloatOutput= */ false, /* enableAudioTrackPlaybackParams= */ false, - /* enableOffload= */ false); + DefaultAudioSink.OFFLOAD_MODE_DISABLED); } @Test diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerTest.java index c0b83e7a65..07398e0bf1 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerTest.java @@ -20,14 +20,18 @@ import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.SECONDS; import android.os.Looper; +import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.testutil.FakeExoMediaDrm; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableList; import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.shadows.ShadowLooper; @@ -38,6 +42,7 @@ import org.robolectric.shadows.ShadowLooper; // - Multiple acquisitions & releases for same keys -> multiple requests. // - Provisioning. // - Key denial. +// - Handling of ResourceBusyException (indicating session scarcity). @RunWith(AndroidJUnit4.class) public class DefaultDrmSessionManagerTest { @@ -252,6 +257,156 @@ public class DefaultDrmSessionManagerTest { assertThat(secondDrmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS); } + @Test(timeout = 10_000) + public void preacquireSession_loadsKeysBeforeFullAcquisition() throws Exception { + AtomicInteger keyLoadCount = new AtomicInteger(0); + DrmSessionEventListener.EventDispatcher eventDispatcher = + new DrmSessionEventListener.EventDispatcher(); + eventDispatcher.addEventListener( + Util.createHandlerForCurrentLooper(), + new DrmSessionEventListener() { + @Override + public void onDrmKeysLoaded( + int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) { + keyLoadCount.incrementAndGet(); + } + }); + FakeExoMediaDrm.LicenseServer licenseServer = + FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS); + DrmSessionManager drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm()) + // Disable keepalive + .setSessionKeepaliveMs(C.TIME_UNSET) + .build(/* mediaDrmCallback= */ licenseServer); + + drmSessionManager.prepare(); + + DrmSessionManager.DrmSessionReference sessionReference = + drmSessionManager.preacquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + eventDispatcher, + FORMAT_WITH_DRM_INIT_DATA); + + // Wait for the key load event to propagate, indicating the pre-acquired session is in + // STATE_OPENED_WITH_KEYS. + while (keyLoadCount.get() == 0) { + // Allow the key response to be handled. + ShadowLooper.idleMainLooper(); + } + + DrmSession drmSession = + checkNotNull( + drmSessionManager.acquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + /* eventDispatcher= */ null, + FORMAT_WITH_DRM_INIT_DATA)); + + // Without idling the main/playback looper, we assert the session is already in OPENED_WITH_KEYS + assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS); + assertThat(keyLoadCount.get()).isEqualTo(1); + + // After releasing our concrete session reference, the session is held open by the pre-acquired + // reference. + drmSession.release(/* eventDispatcher= */ null); + assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS); + + // Releasing the pre-acquired reference allows the session to be fully released. + sessionReference.release(); + assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED); + } + + @Test(timeout = 10_000) + public void + preacquireSession_releaseBeforeUnderlyingAcquisitionCompletesReleasesSessionOnceAcquired() + throws Exception { + FakeExoMediaDrm.LicenseServer licenseServer = + FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS); + DrmSessionManager drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm()) + // Disable keepalive + .setSessionKeepaliveMs(C.TIME_UNSET) + .build(/* mediaDrmCallback= */ licenseServer); + + drmSessionManager.prepare(); + + DrmSessionManager.DrmSessionReference sessionReference = + drmSessionManager.preacquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + /* eventDispatcher= */ null, + FORMAT_WITH_DRM_INIT_DATA); + + // Release the pre-acquired reference before the underlying session has had a chance to be + // constructed. + sessionReference.release(); + + // Acquiring the same session triggers a second key load (because the pre-acquired session was + // fully released). + DrmSession drmSession = + checkNotNull( + drmSessionManager.acquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + /* eventDispatcher= */ null, + FORMAT_WITH_DRM_INIT_DATA)); + assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED); + + waitForOpenedWithKeys(drmSession); + + drmSession.release(/* eventDispatcher= */ null); + assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED); + } + + @Test(timeout = 10_000) + public void preacquireSession_releaseManagerBeforeAcquisition_acquisitionDoesntHappen() + throws Exception { + FakeExoMediaDrm.LicenseServer licenseServer = + FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS); + DrmSessionManager drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm()) + // Disable keepalive + .setSessionKeepaliveMs(C.TIME_UNSET) + .build(/* mediaDrmCallback= */ licenseServer); + + drmSessionManager.prepare(); + + DrmSessionManager.DrmSessionReference sessionReference = + drmSessionManager.preacquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + /* eventDispatcher= */ null, + FORMAT_WITH_DRM_INIT_DATA); + + // Release the manager before the underlying session has had a chance to be constructed. This + // will release all pre-acquired sessions. + drmSessionManager.release(); + + // Allow the acquisition event to be handled on the main/playback thread. + ShadowLooper.idleMainLooper(); + + // Re-prepare the manager so we can fully acquire the same session, and check the previous + // pre-acquisition didn't do anything. + drmSessionManager.prepare(); + DrmSession drmSession = + checkNotNull( + drmSessionManager.acquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + /* eventDispatcher= */ null, + FORMAT_WITH_DRM_INIT_DATA)); + assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED); + waitForOpenedWithKeys(drmSession); + + drmSession.release(/* eventDispatcher= */ null); + // If the (still unreleased) pre-acquired session above was linked to the same underlying + // session then the state would still be OPENED_WITH_KEYS. + assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED); + + // Release the pre-acquired session from above (this is a no-op, but we do it anyway for + // correctness). + sessionReference.release(); + drmSessionManager.release(); + } + private static void waitForOpenedWithKeys(DrmSession drmSession) { // Check the error first, so we get a meaningful failure if there's been an error. assertThat(drmSession.getError()).isNull(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloaderTest.java index 52d83c133a..b80483a4e6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloaderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloaderTest.java @@ -23,6 +23,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.database.DatabaseProvider; +import com.google.android.exoplayer2.testutil.FailOnCloseDataSink; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.TestUtil; @@ -34,6 +35,7 @@ import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -66,7 +68,7 @@ public class ProgressiveDownloaderTest { } @Test - public void download_afterSingleFailure_succeeds() throws Exception { + public void download_afterReadFailure_succeeds() throws Exception { Uri uri = Uri.parse("test:///test.mp4"); // Fake data has a built in failure after 10 bytes. @@ -92,6 +94,39 @@ public class ProgressiveDownloaderTest { assertThat(progressListener.bytesDownloaded).isEqualTo(30); } + @Test + public void download_afterWriteFailureOnClose_succeeds() throws Exception { + Uri uri = Uri.parse("test:///test.mp4"); + + FakeDataSet data = new FakeDataSet(); + data.newData(uri).appendReadData(1024); + DataSource.Factory upstreamDataSource = new FakeDataSource.Factory().setFakeDataSet(data); + + AtomicBoolean failOnClose = new AtomicBoolean(/* initialValue= */ true); + FailOnCloseDataSink.Factory dataSinkFactory = + new FailOnCloseDataSink.Factory(downloadCache, failOnClose); + + MediaItem mediaItem = MediaItem.fromUri(uri); + CacheDataSource.Factory cacheDataSourceFactory = + new CacheDataSource.Factory() + .setCache(downloadCache) + .setCacheWriteDataSinkFactory(dataSinkFactory) + .setUpstreamDataSourceFactory(upstreamDataSource); + ProgressiveDownloader downloader = new ProgressiveDownloader(mediaItem, cacheDataSourceFactory); + + TestProgressListener progressListener = new TestProgressListener(); + + // Failure expected after 1024 bytes. + assertThrows(IOException.class, () -> downloader.download(progressListener)); + assertThat(progressListener.bytesDownloaded).isEqualTo(1024); + + failOnClose.set(false); + + // Retry should succeed. + downloader.download(progressListener); + assertThat(progressListener.bytesDownloaded).isEqualTo(1024); + } + private static final class TestProgressListener implements Downloader.ProgressListener { public long bytesDownloaded; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriodTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriodTest.java index aaf00388f6..2ca6ee6343 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriodTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriodTest.java @@ -24,22 +24,37 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.upstream.AssetDataSource; import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; /** Unit test for {@link ProgressiveMediaPeriod}. */ @RunWith(AndroidJUnit4.class) public final class ProgressiveMediaPeriodTest { @Test - public void prepare_updatesSourceInfoBeforeOnPreparedCallback() throws Exception { + public void prepareUsingBundledExtractors_updatesSourceInfoBeforeOnPreparedCallback() + throws TimeoutException { + testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback( + new BundledExtractorsAdapter(Mp4Extractor.FACTORY)); + } + + @Test + @Config(sdk = 30) + public void prepareUsingMediaParser_updatesSourceInfoBeforeOnPreparedCallback() + throws TimeoutException { + testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback(new MediaParserExtractorAdapter()); + } + + private static void testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback( + ProgressiveMediaExtractor extractor) throws TimeoutException { AtomicBoolean sourceInfoRefreshCalled = new AtomicBoolean(false); ProgressiveMediaPeriod.Listener sourceInfoRefreshListener = (durationUs, isSeekable, isLive) -> sourceInfoRefreshCalled.set(true); @@ -48,7 +63,7 @@ public final class ProgressiveMediaPeriodTest { new ProgressiveMediaPeriod( Uri.parse("asset://android_asset/media/mp4/sample.mp4"), new AssetDataSource(ApplicationProvider.getApplicationContext()), - () -> new Extractor[] {new Mp4Extractor()}, + extractor, DrmSessionManager.DRM_UNSUPPORTED, new DrmSessionEventListener.EventDispatcher() .withParameters(/* windowIndex= */ 0, mediaPeriodId), 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 3bdebdf82b..faa5df5140 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 @@ -323,17 +323,18 @@ public final class SsaDecoderTest { } @Test - public void decodeFontSize() throws IOException{ + public void decodeFontSize() throws IOException { SsaDecoder decoder = new SsaDecoder(); - byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_FONT_SIZE); + byte[] bytes = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_FONT_SIZE); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(4); Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); - assertThat(firstCue.textSize).isEqualTo(30f/720f); + assertThat(firstCue.textSize).isWithin(1.0e-8f).of(30f / 720f); assertThat(firstCue.textSizeType).isEqualTo(Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING); Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); - assertThat(secondCue.textSize).isEqualTo(72.2f/720f); + assertThat(secondCue.textSize).isWithin(1.0e-8f).of(72.2f / 720f); assertThat(secondCue.textSizeType).isEqualTo(Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java index 564973f51c..c370630815 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java @@ -87,14 +87,6 @@ public final class ByteArrayDataSourceTest { readTestData(TEST_DATA, TEST_DATA.length, 1, 1, 0, 1, true); } - @Test - public void readWithInvalidLength() { - // Read more data than is available. - readTestData(TEST_DATA, 0, TEST_DATA.length + 1, 1, 0, 1, true); - // And with bound. - readTestData(TEST_DATA, 1, TEST_DATA.length, 1, 0, 1, true); - } - /** * Tests reading from a {@link ByteArrayDataSource} with various parameters. * diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/CacheDataSourceContractTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/CacheDataSourceContractTest.java index 2c7e94ef8c..fc2f364124 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/CacheDataSourceContractTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/CacheDataSourceContractTest.java @@ -50,7 +50,6 @@ public class CacheDataSourceContractTest extends DataSourceContractTest { File file = tempFolder.newFile(); Files.write(Paths.get(file.getAbsolutePath()), DATA); simpleUri = Uri.fromFile(file); - fileDataSource = new FileDataSource(); } @Override @@ -74,6 +73,7 @@ public class CacheDataSourceContractTest extends DataSourceContractTest { Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest"); SimpleCache cache = new SimpleCache(tempFolder, new NoOpCacheEvictor(), TestUtil.getInMemoryDatabaseProvider()); + fileDataSource = new FileDataSource(); return new CacheDataSource(cache, fileDataSource); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceContractTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceContractTest.java index 97bd701865..861fe358f6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceContractTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceContractTest.java @@ -39,12 +39,12 @@ public class DataSchemeDataSourceContractTest extends DataSourceContractTest { return ImmutableList.of( new TestResource.Builder() .setName("plain text") - .setUri(Uri.parse("data:text/plain," + DATA)) + .setUri("data:text/plain," + DATA) .setExpectedBytes(DATA.getBytes(UTF_8)) .build(), new TestResource.Builder() .setName("base64 encoded text") - .setUri(Uri.parse("data:text/plain;base64," + BASE64_ENCODED_DATA)) + .setUri("data:text/plain;base64," + BASE64_ENCODED_DATA) .setExpectedBytes(Base64.decode(BASE64_ENCODED_DATA, Base64.DEFAULT)) .build()); } 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 7a99b97bd5..119d473e7d 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 @@ -107,18 +107,6 @@ public final class DataSchemeDataSourceTest { } } - @Test - public void rangeExceedingResourceLengthRequest() 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 incorrectScheme() { try { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/ResolvingDataSourceContractTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/ResolvingDataSourceContractTest.java new file mode 100644 index 0000000000..1c550497c4 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/ResolvingDataSourceContractTest.java @@ -0,0 +1,83 @@ +/* + * Copyright 2021 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 androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.testutil.DataSourceContractTest; +import com.google.android.exoplayer2.testutil.FakeDataSet; +import com.google.android.exoplayer2.testutil.FakeDataSource; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.upstream.ResolvingDataSource.Resolver; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import org.junit.Before; +import org.junit.runner.RunWith; + +/** {@link DataSource} contract tests for {@link ResolvingDataSourceContractTest}. */ +@RunWith(AndroidJUnit4.class) +public class ResolvingDataSourceContractTest extends DataSourceContractTest { + + private static final String URI = "test://simple.test"; + private static final String RESOLVED_URI = "resolved://simple.resolved"; + + private byte[] simpleData; + private FakeDataSet fakeDataSet; + private FakeDataSource fakeDataSource; + + @Before + public void setUp() { + simpleData = TestUtil.buildTestData(/* length= */ 20); + fakeDataSet = new FakeDataSet().newData(RESOLVED_URI).appendReadData(simpleData).endData(); + } + + @Override + protected ImmutableList getTestResources() { + return ImmutableList.of( + new TestResource.Builder() + .setName("simple") + .setUri(URI) + .setExpectedBytes(simpleData) + .build()); + } + + @Override + protected Uri getNotFoundUri() { + return Uri.parse("test://not-found.test"); + } + + @Override + protected DataSource createDataSource() { + fakeDataSource = new FakeDataSource(fakeDataSet); + return new ResolvingDataSource( + fakeDataSource, + new Resolver() { + @Override + public DataSpec resolveDataSpec(DataSpec dataSpec) throws IOException { + return URI.equals(dataSpec.uri.toString()) + ? dataSpec.buildUpon().setUri(RESOLVED_URI).build() + : dataSpec; + } + }); + } + + @Override + @Nullable + protected DataSource getTransferListenerDataSource() { + return fakeDataSource; + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/UdpDataSourceContractTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/UdpDataSourceContractTest.java index c8dd082e67..4b2115da50 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/UdpDataSourceContractTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/UdpDataSourceContractTest.java @@ -53,14 +53,18 @@ public class UdpDataSourceContractTest extends DataSourceContractTest { return udpDataSource; } + @Override + protected boolean unboundedReadsAreIndefinite() { + return true; + } + @Override protected ImmutableList getTestResources() { return ImmutableList.of( new TestResource.Builder() .setName("local-udp-unicast-socket") - .setUri(Uri.parse("udp://localhost:" + findFreeUdpPort())) + .setUri("udp://localhost:" + findFreeUdpPort()) .setExpectedBytes(data) - .setEndOfInputExpected(false) .build()); } @@ -84,6 +88,26 @@ public class UdpDataSourceContractTest extends DataSourceContractTest { @Override public void dataSpecWithPositionAndLength_readExpectedRange() {} + @Test + @Ignore("UdpDataSource doesn't support DataSpec's position or length [internal: b/175856954]") + @Override + public void dataSpecWithPositionAtEnd_throwsPositionOutOfRangeException() {} + + @Test + @Ignore("UdpDataSource doesn't support DataSpec's position or length [internal: b/175856954]") + @Override + public void dataSpecWithPositionAtEndAndLength_throwsPositionOutOfRangeException() {} + + @Test + @Ignore("UdpDataSource doesn't support DataSpec's position or length [internal: b/175856954]") + @Override + public void dataSpecWithPositionOutOfRange_throwsPositionOutOfRangeException() {} + + @Test + @Ignore("UdpDataSource doesn't support DataSpec's position or length [internal: b/175856954]") + @Override + public void dataSpecWithEndPositionOutOfRange_readsToEnd() {} + /** * Finds a free UDP port in the range of unreserved ports 50000-60000 that can be used from the * test or throws an {@link IllegalStateException} if no port is available. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheWriterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheWriterTest.java index 6064783e08..265ac7c28f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheWriterTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheWriterTest.java @@ -17,84 +17,40 @@ package com.google.android.exoplayer2.upstream.cache; import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData; import static com.google.common.truth.Truth.assertThat; -import static java.lang.Math.min; import static org.junit.Assert.assertThrows; 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.testutil.FailOnCloseDataSink; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.FileDataSource; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Answers; -import org.mockito.Mock; import org.mockito.MockitoAnnotations; /** Unit tests for {@link CacheWriter}. */ @RunWith(AndroidJUnit4.class) public final class CacheWriterTest { - /** - * Abstract fake Cache implementation used by the test. This class must be public so Mockito can - * create a proxy for it. - */ - public abstract static class AbstractFakeCache implements Cache { - - // This array is set to alternating length of cached and not cached regions in tests: - // spansAndGaps = {, , - // , , ... } - // Ideally it should end with a cached region but it shouldn't matter for any code. - private int[] spansAndGaps; - private long contentLength; - - private void init() { - spansAndGaps = new int[] {}; - contentLength = C.LENGTH_UNSET; - } - - @Override - public long getCachedLength(String key, long position, long length) { - if (length == C.LENGTH_UNSET) { - length = Long.MAX_VALUE; - } - for (int i = 0; i < spansAndGaps.length; i++) { - int spanOrGap = spansAndGaps[i]; - if (position < spanOrGap) { - long left = min(spanOrGap - position, length); - return (i & 1) == 1 ? -left : left; - } - position -= spanOrGap; - } - return -length; - } - - @Override - public ContentMetadata getContentMetadata(String key) { - DefaultContentMetadata metadata = new DefaultContentMetadata(); - ContentMetadataMutations mutations = new ContentMetadataMutations(); - ContentMetadataMutations.setContentLength(mutations, contentLength); - return metadata.copyWithMutationsApplied(mutations); - } - } - - @Mock(answer = Answers.CALLS_REAL_METHODS) private AbstractFakeCache mockCache; private File tempFolder; private SimpleCache cache; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - mockCache.init(); tempFolder = Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest"); cache = @@ -219,6 +175,7 @@ public final class CacheWriterTest { assertCachedData(cache, fakeDataSet); } + @Ignore("Currently broken. See https://github.com/google/ExoPlayer/issues/7326.") @Test public void cacheLengthExceedsActualDataLength() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); @@ -263,6 +220,50 @@ public final class CacheWriterTest { assertThat(DataSourceException.isCausedByPositionOutOfRange(exception)).isTrue(); } + @Test + public void cache_afterFailureOnClose_succeeds() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); + FakeDataSource upstreamDataSource = new FakeDataSource(fakeDataSet); + + AtomicBoolean failOnClose = new AtomicBoolean(/* initialValue= */ true); + FailOnCloseDataSink dataSink = new FailOnCloseDataSink(cache, failOnClose); + + CacheDataSource cacheDataSource = + new CacheDataSource( + cache, + upstreamDataSource, + new FileDataSource(), + dataSink, + /* flags= */ 0, + /* eventListener= */ null); + + CachingCounters counters = new CachingCounters(); + + CacheWriter cacheWriter = + new CacheWriter( + cacheDataSource, + new DataSpec(Uri.parse("test_data")), + /* allowShortContent= */ false, + /* temporaryBuffer= */ null, + counters); + + // DataSink.close failing must cause the operation to fail rather than succeed. + assertThrows(IOException.class, cacheWriter::cache); + // Since all of the bytes were read through the DataSource chain successfully before the sink + // was closed, the progress listener will have seen all of the bytes being cached, even though + // this may not really be the case. + counters.assertValues( + /* bytesAlreadyCached= */ 0, /* bytesNewlyCached= */ 100, /* contentLength= */ 100); + + failOnClose.set(false); + + // The bytes will be downloaded again, but cached successfully this time. + cacheWriter.cache(); + counters.assertValues( + /* bytesAlreadyCached= */ 0, /* bytesNewlyCached= */ 100, /* contentLength= */ 100); + assertCachedData(cache, fakeDataSet); + } + @Test public void cachePolling() throws Exception { final CachingCounters counters = new CachingCounters(); 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 2225589950..809f0d10c3 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 @@ -26,11 +26,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.extractor.ChunkIndex; -import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; -import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; -import com.google.android.exoplayer2.extractor.rawcc.RawCcExtractor; import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator; import com.google.android.exoplayer2.source.chunk.BundledChunkExtractor; @@ -53,7 +48,6 @@ import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; import com.google.android.exoplayer2.upstream.TransferListener; -import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; @@ -180,11 +174,15 @@ public class DefaultDashChunkSource implements DashChunkSource { representationHolders[i] = new RepresentationHolder( periodDurationUs, - trackType, representation, - enableEventMessageTrack, - closedCaptionFormats, - playerTrackEmsgHandler); + BundledChunkExtractor.FACTORY.createProgressiveMediaExtractor( + trackType, + representation.format, + enableEventMessageTrack, + closedCaptionFormats, + playerTrackEmsgHandler), + /* segmentNumShift= */ 0, + representation.getIndex()); } } @@ -665,26 +663,6 @@ public class DefaultDashChunkSource implements DashChunkSource { private final long segmentNumShift; /* package */ RepresentationHolder( - long periodDurationUs, - int trackType, - Representation representation, - boolean enableEventMessageTrack, - List closedCaptionFormats, - @Nullable TrackOutput playerEmsgTrackOutput) { - this( - periodDurationUs, - representation, - createChunkExtractor( - trackType, - representation, - enableEventMessageTrack, - closedCaptionFormats, - playerEmsgTrackOutput), - /* segmentNumShift= */ 0, - representation.getIndex()); - } - - private RepresentationHolder( long periodDurationUs, Representation representation, @Nullable ChunkExtractor chunkExtractor, @@ -800,40 +778,5 @@ public class DefaultDashChunkSource implements DashChunkSource { public boolean isSegmentAvailableAtFullNetworkSpeed(long segmentNum, long nowPeriodTimeUs) { return nowPeriodTimeUs == C.TIME_UNSET || getSegmentEndTimeUs(segmentNum) <= nowPeriodTimeUs; } - - @Nullable - private static ChunkExtractor createChunkExtractor( - int trackType, - Representation representation, - boolean enableEventMessageTrack, - List closedCaptionFormats, - @Nullable TrackOutput playerEmsgTrackOutput) { - String containerMimeType = representation.format.containerMimeType; - Extractor extractor; - if (MimeTypes.isText(containerMimeType)) { - if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) { - // RawCC is special because it's a text specific container format. - extractor = new RawCcExtractor(representation.format); - } else { - // All other text types are raw formats that do not need an extractor. - return null; - } - } else if (MimeTypes.isMatroska(containerMimeType)) { - extractor = new MatroskaExtractor(MatroskaExtractor.FLAG_DISABLE_SEEK_FOR_CUES); - } else { - int flags = 0; - if (enableEventMessageTrack) { - flags |= FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK; - } - extractor = - new FragmentedMp4Extractor( - flags, - /* timestampAdjuster= */ null, - /* sideloadedTrack= */ null, - closedCaptionFormats, - playerEmsgTrackOutput); - } - return new BundledChunkExtractor(extractor, trackType, representation.format); - } } } 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 2759a61bb9..d7633f6c56 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 @@ -913,11 +913,12 @@ public class PlayerControlView extends FrameLayout { timeline.getWindow(player.getCurrentWindowIndex(), window); boolean isSeekable = window.isSeekable; enableSeeking = isSeekable; - enablePrevious = isSeekable || !window.isDynamic || player.hasPrevious(); + enablePrevious = isSeekable || !window.isLive() || player.hasPrevious(); enableRewind = isSeekable && controlDispatcher.isRewindEnabled(); enableFastForward = isSeekable && controlDispatcher.isFastForwardEnabled(); enableNext = - window.isLive() || player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM); + (window.isLive() && window.isDynamic) + || player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM); } } 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 a1183c8a3f..f683a41cb8 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 @@ -320,6 +320,7 @@ public class PlayerNotificationManager { private int fastForwardActionIconResourceId; private int previousActionIconResourceId; private int nextActionIconResourceId; + @Nullable private String groupKey; /** * Creates an instance. @@ -514,6 +515,18 @@ public class PlayerNotificationManager { return this; } + /** + * The key of the group the media notification should belong to. + * + *

    The default is {@code null} + * + * @return This builder. + */ + public Builder setGroup(String groupKey) { + this.groupKey = groupKey; + return this; + } + /** Builds the {@link PlayerNotificationManager}. */ public PlayerNotificationManager build() { if (channelNameResourceId != 0) { @@ -538,7 +551,8 @@ public class PlayerNotificationManager { rewindActionIconResourceId, fastForwardActionIconResourceId, previousActionIconResourceId, - nextActionIconResourceId); + nextActionIconResourceId, + groupKey); } } @@ -662,6 +676,7 @@ public class PlayerNotificationManager { private int visibility; @Priority private int priority; private boolean useChronometer; + @Nullable private String groupKey; /** @deprecated Use the {@link Builder} instead. */ @SuppressWarnings("deprecation") @@ -805,7 +820,8 @@ public class PlayerNotificationManager { R.drawable.exo_notification_rewind, R.drawable.exo_notification_fastforward, R.drawable.exo_notification_previous, - R.drawable.exo_notification_next); + R.drawable.exo_notification_next, + null); } private PlayerNotificationManager( @@ -822,7 +838,8 @@ public class PlayerNotificationManager { int rewindActionIconResourceId, int fastForwardActionIconResourceId, int previousActionIconResourceId, - int nextActionIconResourceId) { + int nextActionIconResourceId, + @Nullable String groupKey) { context = context.getApplicationContext(); this.context = context; this.channelId = channelId; @@ -831,6 +848,7 @@ public class PlayerNotificationManager { this.notificationListener = notificationListener; this.customActionReceiver = customActionReceiver; this.smallIconResourceId = smallIconResourceId; + this.groupKey = groupKey; controlDispatcher = new DefaultControlDispatcher(); window = new Timeline.Window(); instanceId = instanceIdCounter++; @@ -1407,6 +1425,10 @@ public class PlayerNotificationManager { setLargeIcon(builder, largeIcon); builder.setContentIntent(mediaDescriptionAdapter.createCurrentContentIntent(player)); + if (groupKey != null) { + builder.setGroup(groupKey); + } + return builder; } @@ -1437,10 +1459,13 @@ public class PlayerNotificationManager { Timeline timeline = player.getCurrentTimeline(); if (!timeline.isEmpty() && !player.isPlayingAd()) { timeline.getWindow(player.getCurrentWindowIndex(), window); - enablePrevious = window.isSeekable || !window.isDynamic || player.hasPrevious(); - enableRewind = controlDispatcher.isRewindEnabled(); - enableFastForward = controlDispatcher.isFastForwardEnabled(); - enableNext = window.isDynamic || player.hasNext(); + boolean isSeekable = window.isSeekable; + enablePrevious = isSeekable || !window.isLive() || player.hasPrevious(); + enableRewind = isSeekable && controlDispatcher.isRewindEnabled(); + enableFastForward = isSeekable && controlDispatcher.isFastForwardEnabled(); + enableNext = + (window.isLive() && window.isDynamic) + || player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM); } List stringActions = new ArrayList<>(); 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 b913d32211..67a11554e4 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 @@ -58,6 +58,7 @@ import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionUtil; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode; import com.google.android.exoplayer2.ui.spherical.SingleTapListener; import com.google.android.exoplayer2.ui.spherical.SphericalGLSurfaceView; @@ -1332,14 +1333,11 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider closeShutter(); } - TrackSelectionArray selections = player.getCurrentTrackSelections(); - for (int i = 0; i < selections.length; i++) { - if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) { - // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in - // onRenderedFirstFrame(). - hideArtwork(); - return; - } + if (TrackSelectionUtil.hasTrackOfType(player.getCurrentTrackSelections(), C.TRACK_TYPE_VIDEO)) { + // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in + // onRenderedFirstFrame(). + hideArtwork(); + return; } // Video disabled so the shutter must be closed. diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java index 678d844038..91d00ab9e3 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java @@ -436,14 +436,10 @@ public class StyledPlayerControlView extends FrameLayout { private StyledPlayerControlViewLayoutManager controlViewLayoutManager; private Resources resources; - private int selectedMainSettingsPosition; private RecyclerView settingsView; private SettingsAdapter settingsAdapter; - private SubSettingsAdapter subSettingsAdapter; + private PlaybackSpeedAdapter playbackSpeedAdapter; private PopupWindow settingsWindow; - private String[] playbackSpeedTexts; - private int[] playbackSpeedsMultBy100; - private int selectedPlaybackSpeedIndex; private boolean needToHideBars; private int settingsWindowMargin; @@ -457,6 +453,8 @@ public class StyledPlayerControlView extends FrameLayout { @Nullable private ImageView fullScreenButton; @Nullable private ImageView minimalFullScreenButton; @Nullable private View settingsButton; + @Nullable private View playbackSpeedButton; + @Nullable private View audioTrackButton; public StyledPlayerControlView(Context context) { this(context, /* attrs= */ null); @@ -575,6 +573,16 @@ public class StyledPlayerControlView extends FrameLayout { settingsButton.setOnClickListener(componentListener); } + playbackSpeedButton = findViewById(R.id.exo_playback_speed); + if (playbackSpeedButton != null) { + playbackSpeedButton.setOnClickListener(componentListener); + } + + audioTrackButton = findViewById(R.id.exo_audio_track); + if (audioTrackButton != null) { + audioTrackButton.setOnClickListener(componentListener); + } + TimeBar customTimeBar = findViewById(R.id.exo_progress); View timeBarPlaceholder = findViewById(R.id.exo_progress_placeholder); if (customTimeBar != null) { @@ -663,12 +671,7 @@ public class StyledPlayerControlView extends FrameLayout { settingIcons[SETTINGS_AUDIO_TRACK_SELECTION_POSITION] = resources.getDrawable(R.drawable.exo_styled_controls_audiotrack); settingsAdapter = new SettingsAdapter(settingTexts, settingIcons); - - playbackSpeedTexts = resources.getStringArray(R.array.exo_playback_speeds); - playbackSpeedsMultBy100 = resources.getIntArray(R.array.exo_speed_multiplied_by_100); settingsWindowMargin = resources.getDimensionPixelSize(R.dimen.exo_settings_offset); - - subSettingsAdapter = new SubSettingsAdapter(); settingsView = (RecyclerView) LayoutInflater.from(context).inflate(R.layout.exo_styled_settings_list, null); @@ -693,6 +696,10 @@ public class StyledPlayerControlView extends FrameLayout { resources.getString(R.string.exo_controls_cc_disabled_description); textTrackSelectionAdapter = new TextTrackSelectionAdapter(); audioTrackSelectionAdapter = new AudioTrackSelectionAdapter(); + playbackSpeedAdapter = + new PlaybackSpeedAdapter( + resources.getStringArray(R.array.exo_playback_speeds), + resources.getIntArray(R.array.exo_speed_multiplied_by_100)); fullScreenExitDrawable = resources.getDrawable(R.drawable.exo_styled_controls_fullscreen_exit); fullScreenEnterDrawable = @@ -770,7 +777,6 @@ public class StyledPlayerControlView extends FrameLayout { this.trackSelector = null; } updateAll(); - updateSettingsPlaybackSpeedLists(); } /** @@ -1102,6 +1108,7 @@ public class StyledPlayerControlView extends FrameLayout { updateRepeatModeButton(); updateShuffleButton(); updateTrackLists(); + updatePlaybackSpeedList(); updateTimeline(); } @@ -1141,11 +1148,12 @@ public class StyledPlayerControlView extends FrameLayout { timeline.getWindow(player.getCurrentWindowIndex(), window); boolean isSeekable = window.isSeekable; enableSeeking = isSeekable; - enablePrevious = isSeekable || !window.isDynamic || player.hasPrevious(); + enablePrevious = isSeekable || !window.isLive() || player.hasPrevious(); enableRewind = isSeekable && controlDispatcher.isRewindEnabled(); enableFastForward = isSeekable && controlDispatcher.isFastForwardEnabled(); enableNext = - window.isLive() || player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM); + (window.isLive() && window.isDynamic) + || player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM); } } @@ -1438,24 +1446,13 @@ public class StyledPlayerControlView extends FrameLayout { } } - private void updateSettingsPlaybackSpeedLists() { + private void updatePlaybackSpeedList() { if (player == null) { return; } - float speed = player.getPlaybackParameters().speed; - int currentSpeedMultBy100 = Math.round(speed * 100); - int closestMatchIndex = 0; - int closestMatchDifference = Integer.MAX_VALUE; - for (int i = 0; i < playbackSpeedsMultBy100.length; i++) { - int difference = Math.abs(currentSpeedMultBy100 - playbackSpeedsMultBy100[i]); - if (difference < closestMatchDifference) { - closestMatchIndex = i; - closestMatchDifference = difference; - } - } - selectedPlaybackSpeedIndex = closestMatchIndex; + playbackSpeedAdapter.updateSelectedIndex(player.getPlaybackParameters().speed); settingsAdapter.setSubTextAtPosition( - SETTINGS_PLAYBACK_SPEED_POSITION, playbackSpeedTexts[closestMatchIndex]); + SETTINGS_PLAYBACK_SPEED_POSITION, playbackSpeedAdapter.getSelectedText()); } private void updateSettingsWindowSize() { @@ -1571,27 +1568,14 @@ public class StyledPlayerControlView extends FrameLayout { private void onSettingViewClicked(int position) { if (position == SETTINGS_PLAYBACK_SPEED_POSITION) { - subSettingsAdapter.init(playbackSpeedTexts, selectedPlaybackSpeedIndex); - selectedMainSettingsPosition = SETTINGS_PLAYBACK_SPEED_POSITION; - displaySettingsWindow(subSettingsAdapter); + displaySettingsWindow(playbackSpeedAdapter); } else if (position == SETTINGS_AUDIO_TRACK_SELECTION_POSITION) { - selectedMainSettingsPosition = SETTINGS_AUDIO_TRACK_SELECTION_POSITION; displaySettingsWindow(audioTrackSelectionAdapter); } else { settingsWindow.dismiss(); } } - private void onSubSettingViewClicked(int position) { - if (selectedMainSettingsPosition == SETTINGS_PLAYBACK_SPEED_POSITION) { - if (position != selectedPlaybackSpeedIndex) { - float speed = playbackSpeedsMultBy100[position] / 100.0f; - setPlaybackSpeed(speed); - } - } - settingsWindow.dismiss(); - } - private void onLayoutChange( View v, int left, @@ -1836,7 +1820,7 @@ public class StyledPlayerControlView extends FrameLayout { updateTimeline(); } if (events.contains(EVENT_PLAYBACK_PARAMETERS_CHANGED)) { - updateSettingsPlaybackSpeedLists(); + updatePlaybackSpeedList(); } if (events.contains(EVENT_TRACKS_CHANGED)) { updateTrackLists(); @@ -1877,6 +1861,12 @@ public class StyledPlayerControlView extends FrameLayout { } else if (settingsButton == view) { controlViewLayoutManager.removeHideCallbacks(); displaySettingsWindow(settingsAdapter); + } else if (playbackSpeedButton == view) { + controlViewLayoutManager.removeHideCallbacks(); + displaySettingsWindow(playbackSpeedAdapter); + } else if (audioTrackButton == view) { + controlViewLayoutManager.removeHideCallbacks(); + displaySettingsWindow(audioTrackSelectionAdapter); } else if (subtitleButton == view) { controlViewLayoutManager.removeHideCallbacks(); displaySettingsWindow(textTrackSelectionAdapter); @@ -1950,18 +1940,33 @@ public class StyledPlayerControlView extends FrameLayout { } } - private class SubSettingsAdapter extends RecyclerView.Adapter { + private final class PlaybackSpeedAdapter extends RecyclerView.Adapter { - private String[] texts; + private final String[] playbackSpeedTexts; + private final int[] playbackSpeedsMultBy100; private int selectedIndex; - public SubSettingsAdapter() { - texts = new String[0]; + public PlaybackSpeedAdapter(String[] playbackSpeedTexts, int[] playbackSpeedsMultBy100) { + this.playbackSpeedTexts = playbackSpeedTexts; + this.playbackSpeedsMultBy100 = playbackSpeedsMultBy100; } - public void init(String[] texts, int selectedIndex) { - this.texts = texts; - this.selectedIndex = selectedIndex; + public void updateSelectedIndex(float playbackSpeed) { + int currentSpeedMultBy100 = Math.round(playbackSpeed * 100); + int closestMatchIndex = 0; + int closestMatchDifference = Integer.MAX_VALUE; + for (int i = 0; i < playbackSpeedsMultBy100.length; i++) { + int difference = Math.abs(currentSpeedMultBy100 - playbackSpeedsMultBy100[i]); + if (difference < closestMatchDifference) { + closestMatchIndex = i; + closestMatchDifference = difference; + } + } + selectedIndex = closestMatchIndex; + } + + public String getSelectedText() { + return playbackSpeedTexts[selectedIndex]; } @Override @@ -1974,27 +1979,23 @@ public class StyledPlayerControlView extends FrameLayout { @Override public void onBindViewHolder(SubSettingViewHolder holder, int position) { - if (position < texts.length) { - holder.textView.setText(texts[position]); + if (position < playbackSpeedTexts.length) { + holder.textView.setText(playbackSpeedTexts[position]); } holder.checkView.setVisibility(position == selectedIndex ? VISIBLE : INVISIBLE); + holder.itemView.setOnClickListener( + v -> { + if (position != selectedIndex) { + float speed = playbackSpeedsMultBy100[position] / 100.0f; + setPlaybackSpeed(speed); + } + settingsWindow.dismiss(); + }); } @Override public int getItemCount() { - return texts.length; - } - } - - private final class SubSettingViewHolder extends RecyclerView.ViewHolder { - private final TextView textView; - private final View checkView; - - public SubSettingViewHolder(View itemView) { - super(itemView); - textView = itemView.findViewById(R.id.exo_text); - checkView = itemView.findViewById(R.id.exo_check); - itemView.setOnClickListener(v -> onSubSettingViewClicked(getAdapterPosition())); + return playbackSpeedTexts.length; } } @@ -2042,7 +2043,7 @@ public class StyledPlayerControlView extends FrameLayout { } @Override - public void onBindViewHolderAtZeroPosition(TrackSelectionViewHolder holder) { + public void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder) { // CC options include "Off" at the first position, which disables text rendering. holder.textView.setText(R.string.exo_track_selection_none); boolean isTrackSelectionOff = true; @@ -2071,7 +2072,7 @@ public class StyledPlayerControlView extends FrameLayout { } @Override - public void onBindViewHolder(TrackSelectionViewHolder holder, int position) { + public void onBindViewHolder(SubSettingViewHolder holder, int position) { super.onBindViewHolder(holder, position); if (position > 0) { TrackInfo track = tracks.get(position - 1); @@ -2088,7 +2089,7 @@ public class StyledPlayerControlView extends FrameLayout { private final class AudioTrackSelectionAdapter extends TrackSelectionAdapter { @Override - public void onBindViewHolderAtZeroPosition(TrackSelectionViewHolder holder) { + public void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder) { // Audio track selection option includes "Auto" at the top. holder.textView.setText(R.string.exo_track_selection_auto); // hasSelectionOverride is true means there is an explicit track selection, not "Auto". @@ -2167,8 +2168,7 @@ public class StyledPlayerControlView extends FrameLayout { } } - private abstract class TrackSelectionAdapter - extends RecyclerView.Adapter { + private abstract class TrackSelectionAdapter extends RecyclerView.Adapter { protected List rendererIndices; protected List tracks; @@ -2184,19 +2184,19 @@ public class StyledPlayerControlView extends FrameLayout { List rendererIndices, List trackInfos, MappedTrackInfo mappedTrackInfo); @Override - public TrackSelectionViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + public SubSettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View v = LayoutInflater.from(getContext()) .inflate(R.layout.exo_styled_sub_settings_list_item, null); - return new TrackSelectionViewHolder(v); + return new SubSettingViewHolder(v); } - public abstract void onBindViewHolderAtZeroPosition(TrackSelectionViewHolder holder); + public abstract void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder); public abstract void onTrackSelection(String subtext); @Override - public void onBindViewHolder(TrackSelectionViewHolder holder, int position) { + public void onBindViewHolder(SubSettingViewHolder holder, int position) { if (trackSelector == null || mappedTrackInfo == null) { return; } @@ -2252,12 +2252,12 @@ public class StyledPlayerControlView extends FrameLayout { } } - private static class TrackSelectionViewHolder extends RecyclerView.ViewHolder { + private static class SubSettingViewHolder extends RecyclerView.ViewHolder { public final TextView textView; public final View checkView; - public TrackSelectionViewHolder(View itemView) { + public SubSettingViewHolder(View itemView) { super(itemView); textView = itemView.findViewById(R.id.exo_text); checkView = itemView.findViewById(R.id.exo_check); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlViewLayoutManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlViewLayoutManager.java index 4045559ccb..3b901fc6ea 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlViewLayoutManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlViewLayoutManager.java @@ -607,7 +607,7 @@ import java.util.List; defaultTimeBar.hideScrubber(/* disableScrubberPadding= */ true); } else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) { defaultTimeBar.hideScrubber(/* disableScrubberPadding= */ false); - } else if (uxState != UX_STATE_ANIMATING_HIDE && uxState != UX_STATE_ANIMATING_SHOW) { + } else if (uxState != UX_STATE_ANIMATING_HIDE) { defaultTimeBar.showScrubber(); } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java index 36e4a2ed62..3b64890791 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java @@ -60,6 +60,7 @@ import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionUtil; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode; import com.google.android.exoplayer2.ui.spherical.SingleTapListener; import com.google.android.exoplayer2.ui.spherical.SphericalGLSurfaceView; @@ -1354,14 +1355,11 @@ public class StyledPlayerView extends FrameLayout implements AdsLoader.AdViewPro closeShutter(); } - TrackSelectionArray selections = player.getCurrentTrackSelections(); - for (int i = 0; i < selections.length; i++) { - if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) { - // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in - // onRenderedFirstFrame(). - hideArtwork(); - return; - } + if (TrackSelectionUtil.hasTrackOfType(player.getCurrentTrackSelections(), C.TRACK_TYPE_VIDEO)) { + // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in + // onRenderedFirstFrame(). + hideArtwork(); + return; } // Video disabled so the shutter must be closed. diff --git a/library/ui/src/main/res/layout/exo_styled_player_control_ffwd_button.xml b/library/ui/src/main/res/layout/exo_styled_player_control_ffwd_button.xml index f4f7dd6e91..bf4a0f94f0 100644 --- a/library/ui/src/main/res/layout/exo_styled_player_control_ffwd_button.xml +++ b/library/ui/src/main/res/layout/exo_styled_player_control_ffwd_button.xml @@ -22,7 +22,7 @@ android:addStatesFromChildren="true" style="@style/ExoStyledControls.Button.Center"> -