diff --git a/README.md b/README.md index d488f4113e..ac4c41b0fe 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ExoPlayer # +# ExoPlayer # ExoPlayer is an application level media player for Android. It provides an alternative to Android’s MediaPlayer API for playing audio and video both diff --git a/RELEASENOTES.md b/RELEASENOTES.md index cff7ef660b..c5e43a5e49 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,138 +2,137 @@ ### dev-v2 (not yet released) +* New release notes go here! -### 2.12.0 (not yet released - targeted for 2020-08-TBD) ### +### 2.12.0 (not yet released - targeted for 2020-09-03) ### * Core library: - * Implement getTag for SilenceMediaSource. - * Added `TextComponent.getCurrentCues` because the current cues are no - longer forwarded to a new `TextOutput` in `SimpleExoPlayer` - automatically. - * Add additional options to `SimpleExoPlayer.Builder` that were previously - only accessible via setters. - * Add opt-in to verify correct thread usage with - `SimpleExoPlayer.setThrowsWhenUsingWrongThread(true)` - ([#4463](https://github.com/google/ExoPlayer/issues/4463)). - * Add playbackPositionUs parameter to 'LoadControl.shouldContinueLoading'. - * The `DefaultLoadControl` default minimum buffer is set to 50 seconds, - equal to the default maximum buffer. `DefaultLoadControl` applies the - same behavior for audio and video. - * Add API in `AnalyticsListener` to report video frame processing offset. - `MediaCodecVideoRenderer` reports the event. - * Add fields `videoFrameProcessingOffsetUsSum` and - `videoFrameProcessingOffsetUsCount` in `DecoderCounters` to compute the - average video frame processing offset. - * Add playlist API - ([#6161](https://github.com/google/ExoPlayer/issues/6161)). + * `Player`: + * Add a top level playlist API based on a new `MediaItem` class + ([#6161](https://github.com/google/ExoPlayer/issues/6161)). The + new methods for playlist manipulation are `setMediaItem(s)`, + `addMediaItem(s)`, `moveMediaItem(s)`, `removeMediaItem(s)` and + `clearMediaItems`. This API should be used instead of + `ConcatenatingMediaSource` in most cases. + * Add `getCurrentMediaItem` for getting the currently playing item + in the playlist. + * Add `EventListener.onMediaItemTransition` to report when + playback transitions from one item to another in the playlist. + * Add `play` and `pause` convenience methods. They are equivalent to + `setPlayWhenReady(true)` and `setPlayWhenReady(false)` respectively. + * Add `getCurrentLiveOffset` for getting the offset of the current + playback position from the live edge of a live stream. + * Add `getTrackSelector` for getting the `TrackSelector` used by the + player. + * Add `AudioComponent.setAudioSessionId` to set the audio session ID. + This method is also available on `SimpleExoPlayer`. + * Add `TextComponent.getCurrentCues` to get the current cues. This + method is also available on `SimpleExoPlayer`. The current cues are + no longer automatically forwarded to a `TextOutput` when it's added + to a `SimpleExoPlayer`. + * Add `Player.DeviceComponent` to query and control the device volume. + `SimpleExoPlayer` implements this interface. + * Deprecate and rename `getPlaybackError` to `getPlayerError` for + consistency. + * Deprecate and rename `onLoadingChanged` to `onIsLoadingChanged` for + consistency. + * Deprecate `EventListener.onPlayerStateChanged`, replacing it with + `EventListener.onPlayWhenReadyChanged` and + `EventListener.onPlaybackStateChanged`. + * Deprecate `EventListener.onSeekProcessed` because seek changes now + happen instantly and listening to `onPositionDiscontinuity` is + sufficient. + * `ExoPlayer`: + * Add `setMediaSource(s)` and `addMediaSource(s)` to `ExoPlayer`, for + adding `MediaSource` instances directly to the playlist. + * Add `ExoPlayer.setPauseAtEndOfMediaItems` to let the player pause at + the end of each media item + ([#5660](https://github.com/google/ExoPlayer/issues/5660)). + * Allow passing `C.TIME_END_OF_SOURCE` to `PlayerMessage.setPosition` + to send a `PlayerMessage` at the end of a stream. + * `SimpleExoPlayer`: + * `SimpleExoPlayer` implements the new `MediaItem` based playlist API, + using a `MediaSourceFactory` to convert `MediaItem` instances to + playable `MediaSource` instances. A `DefaultMediaSourceFactory` is + used by default. `Builder.setMediaSourceFactory` allows setting a + custom factory. + * Add additional options to `Builder` that were previously only + accessible via setters. + * Add opt-in to verify correct thread usage with + `setThrowsWhenUsingWrongThread(true)` + ([#4463](https://github.com/google/ExoPlayer/issues/4463)). + * `Format`: + * Add a `Builder` and deprecate all `create` methods and most + `Format.copyWith` methods. + * Split `bitrate` into `averageBitrate` and `peakBitrate` + ([#2863](https://github.com/google/ExoPlayer/issues/2863)). + * `LoadControl`: + * Add a `playbackPositionUs` parameter to `shouldContinueLoading`. + * Set the default minimum buffer duration in `DefaultLoadControl` to + 50 seconds (equal to the default maximum buffer), and treat audio + and video the same. + * Add a `MetadataRetriever` API for retrieving track information and + static metadata for a media item + ([#3609](https://github.com/google/ExoPlayer/issues/3609)). * Attach an identifier and extra information to load error events passed - to `LoadErrorHandlingPolicy`. `LoadErrorHandlingPolicy` implementations - must migrate to overriding the non-deprecated methods of the interface - in preparation for deprecated methods' removal in a future ExoPlayer - version ([#7309](https://github.com/google/ExoPlayer/issues/7309)). - * Add `play` and `pause` methods to `Player`. - * Add `Player.getCurrentLiveOffset` to conveniently return the live - offset. - * Add `Player.EventListener.onPlayWhenReadyChanged` with reasons. - * Add `Player.EventListener.onPlaybackStateChanged` and deprecate - `Player.EventListener.onPlayerStateChanged`. - * Add `Player.EventListener.onMediaItemTransition` with reasons. - * Add `Player.setAudioSessionId` to set the session ID attached to the - `AudioTrack`. - * Add `Player.getTrackSelector`. - * Deprecate and rename `getPlaybackError` to `getPlayerError` for - consistency. - * Deprecate and rename `onLoadingChanged` to `onIsLoadingChanged` for - consistency. - * Deprecate `onSeekProcessed` because all seek changes happen instantly - now and listening to `onPositionDiscontinuity` is sufficient. - * Add `ExoPlayer.setPauseAtEndOfMediaItems` to let the player pause at the - end of each media item - ([#5660](https://github.com/google/ExoPlayer/issues/5660)). - * Split `setPlaybackParameter` into `setPlaybackSpeed` and - `AudioComponent.setSkipSilenceEnabled` with callbacks - `onPlaybackSpeedChanged` and - `AudioListener.onSkipSilenceEnabledChanged`. - * Make `MediaSourceEventListener.LoadEventInfo` and - `MediaSourceEventListener.MediaLoadData` top-level classes. - * Rename `MediaCodecRenderer.onOutputFormatChanged` to - `MediaCodecRenderer.onOutputMediaFormatChanged`, further clarifying the - distinction between `Format` and `MediaFormat`. - * Improve `Format` propagation within the media codec renderer - ([#6646](https://github.com/google/ExoPlayer/issues/6646)). - * Move player message-related constants from `C` to `Renderer`, to avoid - having the constants class depend on player/renderer classes. - * Split out `common` and `extractor` submodules. - * Allow to explicitly send `PlayerMessage`s at the end of a stream. - * Add `DataSpec.Builder` and deprecate most `DataSpec` constructors. - * Add `DataSpec.customData` to allow applications to pass custom data - through `DataSource` chains. - * Add a `Format.Builder` and deprecate all `Format.create*` methods and - most `Format.copyWith*` methods. - * Split `Format.bitrate` into `Format.averageBitrate` and - `Format.peakBitrate` - ([#2863](https://github.com/google/ExoPlayer/issues/2863)). - * Add option to `MergingMediaSource` to adjust the time offsets between - the merged sources + to `LoadErrorHandlingPolicy` + ([#7309](https://github.com/google/ExoPlayer/issues/7309)). + `LoadErrorHandlingPolicy` implementations should migrate to implementing + the non-deprecated methods of the interface. + * Add an option to `MergingMediaSource` to adjust the time offsets + between the merged sources ([#6103](https://github.com/google/ExoPlayer/issues/6103)). - * `SimpleDecoderVideoRenderer` and `SimpleDecoderAudioRenderer` renamed to + * Move `MediaSourceEventListener.LoadEventInfo` and + `MediaSourceEventListener.MediaLoadData` to be top-level classes in + `com.google.android.exoplayer2.source`. + * Move `SimpleDecoderVideoRenderer` and `SimpleDecoderAudioRenderer` to `DecoderVideoRenderer` and `DecoderAudioRenderer` respectively, and - generalized to work with `Decoder` rather than `SimpleDecoder`. - * Add media item based playlist API to `Player`. - * Add `getCurrentMediaItem` to `Player`. - * Remove deprecated members in `DefaultTrackSelector`. - * Add `DefaultTrackSelector` constraints for minimum video resolution, - bitrate and frame rate - ([#4511](https://github.com/google/ExoPlayer/issues/4511)). - * Add `Player.DeviceComponent` and implement it for `SimpleExoPlayer` so - that the device volume can be controlled by player. - * Parse track titles from Matroska files - ([#7247](https://github.com/google/ExoPlayer/pull/7247)). - * Replace `CacheDataSinkFactory` and `CacheDataSourceFactory` with - `CacheDataSink.Factory` and `CacheDataSource.Factory` respectively. - * Extend `EventTime` with more details about the current player state for - easier access - ([#7332](https://github.com/google/ExoPlayer/issues/7332)). - * Add `HttpDataSource.InvalidResponseCodeException#responseBody` field - ([#6853](https://github.com/google/ExoPlayer/issues/6853)). - * Add `TrackSelection.shouldCancelMediaChunkLoad` to check whether an - ongoing load should be canceled. Only supported by HLS streams so far. - ([#2848](https://github.com/google/ExoPlayer/issues/2848)). - * Remove throws clause from Renderer.stop. - * Don't clear `exception` in `SimpleDecoder#flush()` - ([#7590](https://github.com/google/ExoPlayer/issues/7590)). - * Remove `AdaptiveTrackSelection.minTimeBetweenBufferReevaluationMs` - parameter ([#7582](https://github.com/google/ExoPlayer/issues/7582)). - * Fix wrong `MediaPeriodId` for some renderer errors reported by - `AnalyticsListener.onPlayerError`. - * Remove onMediaPeriodCreated/Released/ReadingStarted from - `MediaSourceEventListener` and `AnalyticsListener`. - * Dispatch previous, next, fast forward and rewind actions through - `ControlDispatcher` - ([#6926](https://github.com/google/ExoPlayer/issues/6926)). - * Add Guava dependency. - * Add MetadataRetriever API to retrieve the static metadata of a media - item ([#3609](https://github.com/google/ExoPlayer/issues/3609)). -* Video: Pass frame rate hint to `Surface.setFrameRate` on Android R devices. + generalize them to work with `Decoder` rather than `SimpleDecoder`. + * Deprecate `C.MSG_*` constants, replacing them with constants in + `Renderer`. + * Split the `library-core` module into `library-core`, + `library-common` and `library-extractor`. The `library-core` module + has an API dependency on both of the new modules, so this change + should be transparent to developers including ExoPlayer using Gradle + dependencies. + * Add a dependency on Guava. +* Video: + * Pass frame rate hint to `Surface.setFrameRate` on Android 11. + * Fix incorrect aspect ratio when transitioning from one video to another + with the same resolution, but a different pixel aspect ratio + ([#6646](https://github.com/google/ExoPlayer/issues/6646)). * Audio: - * Add a sample count parameter to `MediaCodecRenderer.processOutputBuffer` - and `AudioSink.handleBuffer` to allow batching multiple encoded frames - in one buffer. - * No longer use a `MediaCodec` in audio passthrough mode. + * Add experimental support for power efficient playback using audio + offload. + * Add support for using framework audio speed adjustment instead of + ExoPlayer's implementation + ([#7502](https://github.com/google/ExoPlayer/issues/7502)). This option + can be set using + `DefaultRenderersFactory.setEnableAudioTrackPlaybackParams`. + * Add an event for the audio position starting to advance, to make it + easier for apps to determine when audio playout started + ([#7577](https://github.com/google/ExoPlayer/issues/7577)). + * Generalize support for floating point audio. + * Add an option to `DefaultAudioSink` for enabling floating point + output. This option can also be set using + `DefaultRenderersFactory.setEnableAudioFloatOutput`. + * Add floating point output capability to `MediaCodecAudioRenderer` + and `LibopusAudioRenderer`, which is enabled automatically if the + audio sink supports floating point output and if it makes sense for + the content being played. + * Enable the floating point output capability of `FfmpegAudioRenderer` + automatically if the audio sink supports floating point output and + if it makes sense for the content being played. The option to + manually enable floating point output has been removed, since this + now done with the generalized option on `DefaultAudioSink`. + * In `MediaCodecAudioRenderer`, stop passing audio samples through + `MediaCodec` when playing PCM audio or encoded audio using passthrough + mode. + * Reuse audio decoders when transitioning through playlists of gapless + audio, rather than reinstantiating them. * Check `DefaultAudioSink` supports passthrough, in addition to checking the `AudioCapabilities` - * Add an experimental scheduling mode to save power in offload. ([#7404](https://github.com/google/ExoPlayer/issues/7404)). - * Adjust input timestamps in `MediaCodecRenderer` to account for the - Codec2 MP3 decoder having lower timestamps on the output side. - * Propagate gapless audio metadata without the need to recreate the audio - decoders. - * Add floating point PCM output capability in `MediaCodecAudioRenderer`, - and `LibopusAudioRenderer`. - * Do not use a MediaCodec for PCM formats if AudioTrack supports it. - * Add optional support for using framework audio speed adjustment instead - of application-level audio speed adjustment - ([#7502](https://github.com/google/ExoPlayer/issues/7502)). * Text: * Add a WebView-based output option to `SubtitleView`. This can display some features not supported by the existing Canvas-based output such as @@ -209,21 +208,19 @@ `Mp3Extractor`. A significant portion of the file may need to be scanned when a seek is performed, which may be costly for large files. * MP4: Fix playback of MP4 streams that contain Opus audio. - * FMP4: - * Add support for partially fragmented MP4s - ([#7308](https://github.com/google/ExoPlayer/issues/7308)). - * Fix handling of `traf` boxes containing multiple `sbgp` or `sgpd` - boxes ([#7716](https://github.com/google/ExoPlayer/issues/7716)). - * Matroska: Remove support for the `Invisible` block header flag. + * FMP4: Add support for partially fragmented MP4s + ([#7308](https://github.com/google/ExoPlayer/issues/7308)). + * Matroska: + * Support Dolby Vision + ([#7267](https://github.com/google/ExoPlayer/issues/7267). + * Populate `Format.label` with track titles. + * Remove support for the `Invisible` block header flag. * MPEG-TS: Add support for MPEG-4 Part 2 and H.263 ([#1603](https://github.com/google/ExoPlayer/issues/1603), [#5107](https://github.com/google/ExoPlayer/issues/5107)). * Ogg: Fix handling of non-contiguous pages ([#7230](https://github.com/google/ExoPlayer/issues/7230)). - * FLV: Ignore `SCRIPTDATA` segments with invalid name types, rather than - failing playback - ([#7675](https://github.com/google/ExoPlayer/issues/7675)). -* UI +* UI: * Add `StyledPlayerView` and `StyledPlayerControlView`, which provide a more polished user experience than `PlayerView` and `PlayerControlView` at the cost of decreased customizability. @@ -238,6 +235,9 @@ * Update `TrackSelectionDialogBuilder` to use the AndroidX app compat `AlertDialog` rather than the platform version, if available ([#7357](https://github.com/google/ExoPlayer/issues/7357)). + * Make UI components dispatch previous, next, fast forward and rewind + actions via their `ControlDispatcher` + ([#6926](https://github.com/google/ExoPlayer/issues/6926)). * Downloads and caching: * Add `DownloadRequest.Builder`. * Add `DownloadRequest.keySetId` to make it easier to store an offline @@ -269,7 +269,39 @@ ([#7011](https://github.com/google/ExoPlayer/issues/7011), [#6725](https://github.com/google/ExoPlayer/issues/6725), [#7066](https://github.com/google/ExoPlayer/issues/7066)). + * Remove support for `cbc1` and `cens` encrytion schemes. Support for + these schemes was removed from the Android platform from API level 30, + and the range of API levels for which they are supported is too small to + be useful. * Remove generic types from DRM components. +* Track selection: + * Add `TrackSelection.shouldCancelMediaChunkLoad` to check whether an + ongoing load should be canceled + ([#2848](https://github.com/google/ExoPlayer/issues/2848)). + * Add `DefaultTrackSelector` constraints for minimum video resolution, + bitrate and frame rate + ([#4511](https://github.com/google/ExoPlayer/issues/4511)). + * Remove previously deprecated `DefaultTrackSelector` members. +* Data sources: + * Add `HttpDataSource.InvalidResponseCodeException#responseBody` field + ([#6853](https://github.com/google/ExoPlayer/issues/6853)). + * Add `DataSpec.Builder` and deprecate most `DataSpec` constructors. + * Add `DataSpec.customData` to allow applications to pass custom data + through `DataSource` chains. + * Deprecate `CacheDataSinkFactory` and `CacheDataSourceFactory`, which are + replaced by `CacheDataSink.Factory` and `CacheDataSource.Factory` + respectively. +* Analytics: + * Extend `EventTime` with more details about the current player state + ([#7332](https://github.com/google/ExoPlayer/issues/7332)). + * Add `AnalyticsListener.onVideoFrameProcessingOffset` to report how + early or late video frames are processed relative to them needing to be + presented. Video frame processing offset fields are also added to + `DecoderCounters`. + * Fix incorrect `MediaPeriodId` for some renderer errors reported by + `AnalyticsListener.onPlayerError`. + * Remove `onMediaPeriodCreated`, `onMediaPeriodReleased` and + `onReadingStarted` from `AnalyticsListener`. * Test utils: Add `TestExoPlayer`, a utility class with APIs to create `SimpleExoPlayer` instances with fake components for testing. * Media2 extension: This is a new extension that makes it easy to use @@ -277,8 +309,6 @@ * Cast extension: Implement playlist API and deprecate the old queue manipulation API. * IMA extension: - * Upgrade to IMA SDK 3.19.4, bringing in a fix for setting the media load - timeout ([#7170](https://github.com/google/ExoPlayer/issues/7170)). * Migrate to new 'friendly obstruction' IMA SDK APIs, and allow apps to register a purpose and detail reason for overlay views via `AdsLoader.AdViewProvider`. @@ -304,8 +334,32 @@ * Remove support for media tunneling, random ABR and playback of spherical video. Developers wishing to experiment with these features can enable them by modifying the demo app source code. - * Fix playback of ClearKey protected content on API level 26 and earlier - ([#7735](https://github.com/google/ExoPlayer/issues/7735)). + +### 2.11.8 (2020-08-25) ### + +* Fix distorted playback of floating point audio when samples exceed the + `[-1, 1]` nominal range. +* MP4: + * Add support for `piff` and `isml` brands + ([#7584](https://github.com/google/ExoPlayer/issues/7584)). + * Fix playback of very short MP4 files. +* FMP4: + * Fix `saiz` and `senc` sample count checks, resolving a "length + mismatch" `ParserException` when playing certain protected FMP4 streams + ([#7592](https://github.com/google/ExoPlayer/issues/7592)). + * Fix handling of `traf` boxes containing multiple `sbgp` or `sgpd` + boxes. +* FLV: Ignore `SCRIPTDATA` segments with invalid name types, rather than + failing playback ([#7675](https://github.com/google/ExoPlayer/issues/7675)). +* Better infer the content type of `.ism` and `.isml` streaming URLs. +* Workaround an issue on Broadcom based devices where playbacks would not + transition to `STATE_ENDED` when using video tunneling mode + ([#7647](https://github.com/google/ExoPlayer/issues/7647)). +* IMA extension: Upgrade to IMA SDK 3.19.4, bringing in a fix for setting the + media load timeout + ([#7170](https://github.com/google/ExoPlayer/issues/7170)). +* Demo app: Fix playback of ClearKey protected content on API level 26 and + earlier ([#7735](https://github.com/google/ExoPlayer/issues/7735)). ### 2.11.7 (2020-06-29) ### diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index d21c58e0cc..ce1854db85 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -124,18 +124,6 @@ "drm_scheme": "widevine", "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, - { - "name": "Secure (cbc1)", - "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd", - "drm_scheme": "widevine", - "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "Secure UHD (cbc1)", - "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_uhd.mpd", - "drm_scheme": "widevine", - "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, { "name": "Secure (cbcs)", "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd", 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 7d8d05bbaa..80d9817a46 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 @@ -458,32 +458,16 @@ public final class CastPlayer extends BasePlayer { flushNotifications(); } - /** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */ - @SuppressWarnings("deprecation") - @Deprecated @Override public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { // Unsupported by the RemoteMediaClient API. Do nothing. } - /** @deprecated Use {@link #getPlaybackSpeed()} instead. */ - @SuppressWarnings("deprecation") - @Deprecated @Override public PlaybackParameters getPlaybackParameters() { return PlaybackParameters.DEFAULT; } - @Override - public void setPlaybackSpeed(float playbackSpeed) { - // Unsupported by the RemoteMediaClient API. Do nothing. - } - - @Override - public float getPlaybackSpeed() { - return Player.DEFAULT_PLAYBACK_SPEED; - } - @Override public void stop(boolean reset) { playbackState = STATE_IDLE; diff --git a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/MediaSessionUtilTest.java b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/MediaSessionUtilTest.java index a60c231dfc..8cf586b846 100644 --- a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/MediaSessionUtilTest.java +++ b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/MediaSessionUtilTest.java @@ -19,7 +19,6 @@ import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.content.Context; -import android.os.Looper; import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; import androidx.annotation.NonNull; @@ -46,13 +45,6 @@ public class MediaSessionUtilTest { @Test public void getSessionCompatToken_withMediaControllerCompat_returnsValidToken() throws Exception { - // Workaround to instantiate MediaSession with public androidx.media dependency. - // TODO(b/146536708): Remove this workaround when the relevant change is released via - // androidx.media 1.2.0. - if (Looper.myLooper() == null) { - Looper.prepare(); - } - Context context = ApplicationProvider.getApplicationContext(); SessionPlayerConnector sessionPlayerConnector = playerTestRule.getSessionPlayerConnector(); diff --git a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/PlayerTestRule.java b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/PlayerTestRule.java index 546501412c..345985f862 100644 --- a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/PlayerTestRule.java +++ b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/PlayerTestRule.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.ext.media2; import android.content.Context; @@ -55,15 +54,23 @@ import org.junit.rules.ExternalResource; @Override protected void before() { + // Workaround limitation in androidx.media2.session:1.0.3 which session can only be instantiated + // on thread with prepared Looper. + // TODO: Remove when androidx.media2.session:1.1.0 is released without the limitation + // [Internal: b/146536708] + if (Looper.myLooper() == null) { + Looper.prepare(); + } + context = ApplicationProvider.getApplicationContext(); executor = Executors.newFixedThreadPool(1); InstrumentationRegistry.getInstrumentation() .runOnMainSync( () -> { - // Initialize AudioManager on the main thread to workaround b/78617702 that + // Initialize AudioManager on the main thread to workaround that // audio focus listener is called on the thread where the AudioManager was - // originally initialized. + // originally initialized. [Internal: b/78617702] // Without posting this, audio focus listeners wouldn't be called because the // listeners would be posted to the test thread (here) where it waits until the // tests are finished. @@ -75,8 +82,7 @@ import org.junit.rules.ExternalResource; .setLooper(Looper.myLooper()) .setMediaSourceFactory(new DefaultMediaSourceFactory(dataSourceFactory, null)) .build(); - sessionPlayerConnector = - new SessionPlayerConnector(exoPlayer, new DefaultMediaItemConverter()); + sessionPlayerConnector = new SessionPlayerConnector(exoPlayer); }); } diff --git a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionCallbackBuilderTest.java b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionCallbackBuilderTest.java index df15d706f6..c578b0ba8c 100644 --- a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionCallbackBuilderTest.java +++ b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionCallbackBuilderTest.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.ext.media2; import static com.google.android.exoplayer2.ext.media2.TestUtils.assertPlayerResultSuccess; @@ -29,6 +28,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.media2.common.MediaItem; +import androidx.media2.common.MediaMetadata; import androidx.media2.common.Rating; import androidx.media2.common.SessionPlayer; import androidx.media2.common.UriMediaItem; @@ -136,7 +136,7 @@ public class SessionCallbackBuilderTest { SessionResult.RESULT_ERROR_BAD_VALUE) .setRewindIncrementMs(testRewindIncrementMs) .setFastForwardIncrementMs(testFastForwardIncrementMs) - .setMediaItemProvider(new SessionCallbackBuilder.DefaultMediaItemProvider()) + .setMediaItemProvider(new SessionCallbackBuilder.MediaIdMediaItemProvider()) .build())) { assertPlayerResultSuccess(sessionPlayerConnector.setMediaItem(TestUtils.createMediaItem())); assertPlayerResultSuccess(sessionPlayerConnector.prepare()); @@ -179,7 +179,7 @@ public class SessionCallbackBuilderTest { SessionResult.RESULT_ERROR_BAD_VALUE) .setRewindIncrementMs(testRewindIncrementMs) .setFastForwardIncrementMs(testFastForwardIncrementMs) - .setMediaItemProvider(new SessionCallbackBuilder.DefaultMediaItemProvider()) + .setMediaItemProvider(new SessionCallbackBuilder.MediaIdMediaItemProvider()) .build())) { assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(testPlaylist, null)); @@ -455,13 +455,13 @@ public class SessionCallbackBuilderTest { Uri testMediaUri = RawResourceDataSource.buildRawResourceUri(R.raw.audio); CountDownLatch providerLatch = new CountDownLatch(1); - SessionCallbackBuilder.DefaultMediaItemProvider defaultMediaItemProvider = - new SessionCallbackBuilder.DefaultMediaItemProvider(); + SessionCallbackBuilder.MediaIdMediaItemProvider mediaIdMediaItemProvider = + new SessionCallbackBuilder.MediaIdMediaItemProvider(); SessionCallbackBuilder.MediaItemProvider provider = (session, controllerInfo, mediaId) -> { assertThat(mediaId).isEqualTo(testMediaUri.toString()); providerLatch.countDown(); - return defaultMediaItemProvider.onCreateMediaItem(session, controllerInfo, mediaId); + return mediaIdMediaItemProvider.onCreateMediaItem(session, controllerInfo, mediaId); }; CountDownLatch currentMediaItemChangedLatch = new CountDownLatch(1); @@ -471,7 +471,9 @@ public class SessionCallbackBuilderTest { @Override public void onCurrentMediaItemChanged( @NonNull SessionPlayer player, @NonNull MediaItem item) { - assertThat(((UriMediaItem) item).getUri()).isEqualTo(testMediaUri); + MediaMetadata metadata = item.getMetadata(); + assertThat(metadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID)) + .isEqualTo(testMediaUri.toString()); currentMediaItemChangedLatch.countDown(); } }); 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 7dd9cfdc0f..45a0c59645 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 @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.ext.media2; import static androidx.media2.common.SessionPlayer.PLAYER_STATE_PAUSED; @@ -33,6 +32,7 @@ import android.os.Build.VERSION_CODES; import android.os.Looper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.util.ObjectsCompat; import androidx.media.AudioAttributesCompat; import androidx.media2.common.MediaItem; import androidx.media2.common.MediaMetadata; @@ -61,6 +61,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -273,17 +274,17 @@ public class SessionPlayerConnectorTest { @Test @SmallTest @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) - public void getCurrentPosition_whenIdleState_returnsUnknownTime() { + public void getCurrentPosition_whenIdleState_returnsDefaultPosition() { assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_IDLE); - assertThat(sessionPlayerConnector.getCurrentPosition()).isEqualTo(SessionPlayer.UNKNOWN_TIME); + assertThat(sessionPlayerConnector.getCurrentPosition()).isEqualTo(0); } @Test @SmallTest @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) - public void getBufferedPosition_whenIdleState_returnsUnknownTime() { + public void getBufferedPosition_whenIdleState_returnsDefaultPosition() { assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_IDLE); - assertThat(sessionPlayerConnector.getBufferedPosition()).isEqualTo(SessionPlayer.UNKNOWN_TIME); + assertThat(sessionPlayerConnector.getBufferedPosition()).isEqualTo(0); } @Test @@ -795,6 +796,73 @@ public class SessionPlayerConnectorTest { assertThat(onPlaylistChangedLatch.getCount()).isEqualTo(1); } + @Test + @LargeTest + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) + public void setPlaylist_byUnderlyingPlayerBeforePrepare_notifiesOnPlaylistChanged() + throws Exception { + List playlistToSessionPlayer = TestUtils.createPlaylist(2); + List playlistToExoPlayer = TestUtils.createPlaylist(4); + DefaultMediaItemConverter converter = new DefaultMediaItemConverter(); + List exoMediaItems = new ArrayList<>(); + for (MediaItem mediaItem : playlistToExoPlayer) { + exoMediaItems.add(converter.convertToExoPlayerMediaItem(mediaItem)); + } + + CountDownLatch onPlaylistChangedLatch = new CountDownLatch(1); + sessionPlayerConnector.registerPlayerCallback( + executor, + new SessionPlayer.PlayerCallback() { + @Override + public void onPlaylistChanged( + @NonNull SessionPlayer player, + @Nullable List list, + @Nullable MediaMetadata metadata) { + if (ObjectsCompat.equals(list, playlistToExoPlayer)) { + onPlaylistChangedLatch.countDown(); + } + } + }); + sessionPlayerConnector.setPlaylist(playlistToSessionPlayer, /* metadata= */ null); + InstrumentationRegistry.getInstrumentation() + .runOnMainSync(() -> playerTestRule.getSimpleExoPlayer().setMediaItems(exoMediaItems)); + assertThat(onPlaylistChangedLatch.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue(); + } + + @Test + @LargeTest + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) + public void setPlaylist_byUnderlyingPlayerAfterPrepare_notifiesOnPlaylistChanged() + throws Exception { + List playlistToSessionPlayer = TestUtils.createPlaylist(2); + List playlistToExoPlayer = TestUtils.createPlaylist(4); + DefaultMediaItemConverter converter = new DefaultMediaItemConverter(); + List exoMediaItems = new ArrayList<>(); + for (MediaItem mediaItem : playlistToExoPlayer) { + exoMediaItems.add(converter.convertToExoPlayerMediaItem(mediaItem)); + } + + CountDownLatch onPlaylistChangedLatch = new CountDownLatch(1); + sessionPlayerConnector.registerPlayerCallback( + executor, + new SessionPlayer.PlayerCallback() { + @Override + public void onPlaylistChanged( + @NonNull SessionPlayer player, + @Nullable List list, + @Nullable MediaMetadata metadata) { + if (ObjectsCompat.equals(list, playlistToExoPlayer)) { + onPlaylistChangedLatch.countDown(); + } + } + }); + sessionPlayerConnector.prepare(); + sessionPlayerConnector.setPlaylist(playlistToSessionPlayer, /* metadata= */ null); + InstrumentationRegistry.getInstrumentation() + .runOnMainSync(() -> playerTestRule.getSimpleExoPlayer().setMediaItems(exoMediaItems)); + assertThat(onPlaylistChangedLatch.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue(); + } + @Test @LargeTest @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) @@ -862,7 +930,7 @@ public class SessionPlayerConnectorTest { CountDownLatch onPlaylistChangedLatch = new CountDownLatch(2); int replaceIndex = 2; - MediaItem newMediaItem = TestUtils.createMediaItem(); + MediaItem newMediaItem = TestUtils.createMediaItem(R.raw.video_big_buck_bunny); playlist.set(replaceIndex, newMediaItem); sessionPlayerConnector.registerPlayerCallback( executor, @@ -1185,6 +1253,32 @@ public class SessionPlayerConnectorTest { assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(PLAYER_STATE_PLAYING); } + @Test + @LargeTest + public void getPlaylist_returnsPlaylistInUnderlyingPlayer() { + List playlistToExoPlayer = TestUtils.createPlaylist(4); + DefaultMediaItemConverter converter = new DefaultMediaItemConverter(); + List exoMediaItems = new ArrayList<>(); + for (MediaItem mediaItem : playlistToExoPlayer) { + exoMediaItems.add(converter.convertToExoPlayerMediaItem(mediaItem)); + } + + AtomicReference> playlistFromSessionPlayer = new AtomicReference<>(); + InstrumentationRegistry.getInstrumentation() + .runOnMainSync( + () -> { + SimpleExoPlayer simpleExoPlayer = playerTestRule.getSimpleExoPlayer(); + simpleExoPlayer.setMediaItems(exoMediaItems); + + try (SessionPlayerConnector sessionPlayer = + new SessionPlayerConnector(simpleExoPlayer)) { + List playlist = sessionPlayer.getPlaylist(); + playlistFromSessionPlayer.set(playlist); + } + }); + assertThat(playlistFromSessionPlayer.get()).isEqualTo(playlistToExoPlayer); + } + private class PlayerCallbackForPlaylist extends SessionPlayer.PlayerCallback { private List playlist; private CountDownLatch onCurrentMediaItemChangedLatch; diff --git a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/TestUtils.java b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/TestUtils.java index 5a8e87de22..a7eb058ee6 100644 --- a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/TestUtils.java +++ b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/TestUtils.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.ext.media2; import static androidx.media2.common.SessionPlayer.PlayerResult.RESULT_SUCCESS; diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java index 19b1130c13..c23bdd5669 100644 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java @@ -13,79 +13,125 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.ext.media2; +import static androidx.media2.common.MediaMetadata.METADATA_KEY_DISPLAY_TITLE; +import static androidx.media2.common.MediaMetadata.METADATA_KEY_MEDIA_ID; +import static androidx.media2.common.MediaMetadata.METADATA_KEY_MEDIA_URI; +import static androidx.media2.common.MediaMetadata.METADATA_KEY_TITLE; + import android.net.Uri; import androidx.annotation.Nullable; import androidx.media2.common.CallbackMediaItem; import androidx.media2.common.FileMediaItem; -import androidx.media2.common.MediaMetadata; import androidx.media2.common.UriMediaItem; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.util.Assertions; -/** Default implementation of {@link MediaItemConverter}. */ -public final class DefaultMediaItemConverter implements MediaItemConverter { +/** + * Default implementation of {@link MediaItemConverter}. + * + *

Note that {@link #getMetadata} can be overridden to fill in additional metadata when + * converting {@link MediaItem ExoPlayer MediaItems} to their AndroidX equivalents. + */ +public class DefaultMediaItemConverter implements MediaItemConverter { @Override - public MediaItem convertToExoPlayerMediaItem(androidx.media2.common.MediaItem androidXMediaItem) { - if (androidXMediaItem instanceof FileMediaItem) { + public MediaItem convertToExoPlayerMediaItem(androidx.media2.common.MediaItem media2MediaItem) { + if (media2MediaItem instanceof FileMediaItem) { throw new IllegalStateException("FileMediaItem isn't supported"); } - if (androidXMediaItem instanceof CallbackMediaItem) { + if (media2MediaItem instanceof CallbackMediaItem) { throw new IllegalStateException("CallbackMediaItem isn't supported"); } - - MediaItem.Builder exoplayerMediaItemBuilder = new MediaItem.Builder(); - - // Set mediaItem as tag for creating MediaSource via MediaSourceFactory methods. - exoplayerMediaItemBuilder.setTag(androidXMediaItem); - - // Media ID or URI must be present. Get it from androidx.MediaItem if possible. + @Nullable Uri uri = null; @Nullable String mediaId = null; - if (androidXMediaItem instanceof UriMediaItem) { - UriMediaItem uriMediaItem = (UriMediaItem) androidXMediaItem; + @Nullable String title = null; + if (media2MediaItem instanceof UriMediaItem) { + UriMediaItem uriMediaItem = (UriMediaItem) media2MediaItem; uri = uriMediaItem.getUri(); } - @Nullable MediaMetadata metadata = androidXMediaItem.getMetadata(); + @Nullable androidx.media2.common.MediaMetadata metadata = media2MediaItem.getMetadata(); if (metadata != null) { - mediaId = metadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); - @Nullable String uriString = metadata.getString(MediaMetadata.METADATA_KEY_MEDIA_URI); - if (uri == null && uriString != null) { - uri = Uri.parse(uriString); + @Nullable String uriString = metadata.getString(METADATA_KEY_MEDIA_URI); + mediaId = metadata.getString(METADATA_KEY_MEDIA_ID); + if (uri == null) { + if (uriString != null) { + uri = Uri.parse(uriString); + } else if (mediaId != null) { + uri = Uri.parse("media2:///" + mediaId); + } + } + title = metadata.getString(METADATA_KEY_DISPLAY_TITLE); + if (title == null) { + title = metadata.getString(METADATA_KEY_TITLE); } } if (uri == null) { - // Generate a Uri to make it non-null. If not, tag will be ignored. - uri = Uri.parse("exoplayer://" + androidXMediaItem.hashCode()); + // Generate a URI to make it non-null. If not, then the tag passed to setTag will be ignored. + uri = Uri.parse("media2:///"); } - exoplayerMediaItemBuilder.setUri(uri); - exoplayerMediaItemBuilder.setMediaId(mediaId); - - if (androidXMediaItem.getStartPosition() != androidx.media2.common.MediaItem.POSITION_UNKNOWN) { - exoplayerMediaItemBuilder.setClipStartPositionMs(androidXMediaItem.getStartPosition()); - exoplayerMediaItemBuilder.setClipRelativeToDefaultPosition(true); + long startPositionMs = media2MediaItem.getStartPosition(); + if (startPositionMs == androidx.media2.common.MediaItem.POSITION_UNKNOWN) { + startPositionMs = 0; } - if (androidXMediaItem.getEndPosition() != androidx.media2.common.MediaItem.POSITION_UNKNOWN) { - exoplayerMediaItemBuilder.setClipEndPositionMs(androidXMediaItem.getEndPosition()); - exoplayerMediaItemBuilder.setClipRelativeToDefaultPosition(true); + long endPositionMs = media2MediaItem.getEndPosition(); + if (endPositionMs == androidx.media2.common.MediaItem.POSITION_UNKNOWN) { + endPositionMs = C.TIME_END_OF_SOURCE; } - return exoplayerMediaItemBuilder.build(); + return new MediaItem.Builder() + .setUri(uri) + .setMediaId(mediaId) + .setMediaMetadata( + new com.google.android.exoplayer2.MediaMetadata.Builder().setTitle(title).build()) + .setTag(media2MediaItem) + .setClipStartPositionMs(startPositionMs) + .setClipEndPositionMs(endPositionMs) + .build(); } @Override - public androidx.media2.common.MediaItem convertToAndroidXMediaItem(MediaItem exoplayerMediaItem) { - Assertions.checkNotNull(exoplayerMediaItem); + public androidx.media2.common.MediaItem convertToMedia2MediaItem(MediaItem exoPlayerMediaItem) { + Assertions.checkNotNull(exoPlayerMediaItem); MediaItem.PlaybackProperties playbackProperties = - Assertions.checkNotNull(exoplayerMediaItem.playbackProperties); + Assertions.checkNotNull(exoPlayerMediaItem.playbackProperties); + @Nullable Object tag = playbackProperties.tag; - if (!(tag instanceof androidx.media2.common.MediaItem)) { - throw new IllegalStateException( - "MediaItem tag must be an instance of androidx.media2.common.MediaItem"); + if (tag instanceof androidx.media2.common.MediaItem) { + return (androidx.media2.common.MediaItem) tag; } - return (androidx.media2.common.MediaItem) tag; + + androidx.media2.common.MediaMetadata metadata = getMetadata(exoPlayerMediaItem); + long startPositionMs = exoPlayerMediaItem.clippingProperties.startPositionMs; + long endPositionMs = exoPlayerMediaItem.clippingProperties.endPositionMs; + if (endPositionMs == C.TIME_END_OF_SOURCE) { + endPositionMs = androidx.media2.common.MediaItem.POSITION_UNKNOWN; + } + + return new androidx.media2.common.MediaItem.Builder() + .setMetadata(metadata) + .setStartPosition(startPositionMs) + .setEndPosition(endPositionMs) + .build(); + } + + /** + * Returns a {@link androidx.media2.common.MediaMetadata} corresponding to the given {@link + * MediaItem ExoPlayer MediaItem}. + */ + protected androidx.media2.common.MediaMetadata getMetadata(MediaItem exoPlayerMediaItem) { + @Nullable String title = exoPlayerMediaItem.mediaMetadata.title; + + androidx.media2.common.MediaMetadata.Builder metadataBuilder = + new androidx.media2.common.MediaMetadata.Builder() + .putString(METADATA_KEY_MEDIA_ID, exoPlayerMediaItem.mediaId); + if (title != null) { + metadataBuilder.putString(METADATA_KEY_TITLE, title); + metadataBuilder.putString(METADATA_KEY_DISPLAY_TITLE, title); + } + return metadataBuilder.build(); } } diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/MediaItemConverter.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/MediaItemConverter.java index 34a3d1c314..218c2a737e 100644 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/MediaItemConverter.java +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/MediaItemConverter.java @@ -13,25 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.ext.media2; import com.google.android.exoplayer2.MediaItem; /** - * Converter for between {@link MediaItem AndroidX MediaItem} and {@link - * com.google.android.exoplayer2.MediaItem ExoPlayer MediaItem}. + * Converts between {@link androidx.media2.common.MediaItem Media2 MediaItem} and {@link MediaItem + * ExoPlayer MediaItem}. */ public interface MediaItemConverter { /** - * Converts {@link androidx.media2.common.MediaItem AndroidX MediaItem} to {@link MediaItem + * Converts an {@link androidx.media2.common.MediaItem Media2 MediaItem} to an {@link MediaItem * ExoPlayer MediaItem}. */ - MediaItem convertToExoPlayerMediaItem(androidx.media2.common.MediaItem androidXMediaItem); + MediaItem convertToExoPlayerMediaItem(androidx.media2.common.MediaItem media2MediaItem); /** - * Converts {@link MediaItem ExoPlayer MediaItem} to {@link androidx.media2.common.MediaItem - * AndroidX MediaItem}. + * Converts an {@link MediaItem ExoPlayer MediaItem} to an {@link androidx.media2.common.MediaItem + * Media2 MediaItem}. */ - androidx.media2.common.MediaItem convertToAndroidXMediaItem(MediaItem exoplayerMediaItem); + androidx.media2.common.MediaItem convertToMedia2MediaItem(MediaItem exoPlayerMediaItem); } diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java index 6089d2c5d4..453a7b6d55 100644 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.ext.media2; import androidx.annotation.IntRange; @@ -27,6 +26,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioAttributes; @@ -56,44 +56,44 @@ import java.util.List; void onPlayerStateChanged(/* @SessionPlayer.PlayerState */ int playerState); /** Called when the player is prepared. */ - void onPrepared(androidx.media2.common.MediaItem androidXMediaItem, int bufferingPercentage); + void onPrepared(androidx.media2.common.MediaItem media2MediaItem, int bufferingPercentage); /** Called when a seek request has completed. */ void onSeekCompleted(); /** Called when the player rebuffers. */ - void onBufferingStarted(androidx.media2.common.MediaItem androidXMediaItem); + void onBufferingStarted(androidx.media2.common.MediaItem media2MediaItem); /** Called when the player becomes ready again after rebuffering. */ void onBufferingEnded( - androidx.media2.common.MediaItem androidXMediaItem, int bufferingPercentage); + androidx.media2.common.MediaItem media2MediaItem, int bufferingPercentage); /** Called periodically with the player's buffered position as a percentage. */ void onBufferingUpdate( - androidx.media2.common.MediaItem androidXMediaItem, int bufferingPercentage); + androidx.media2.common.MediaItem media2MediaItem, int bufferingPercentage); /** Called when current media item is changed. */ - void onCurrentMediaItemChanged(androidx.media2.common.MediaItem androidXMediaItem); + void onCurrentMediaItemChanged(androidx.media2.common.MediaItem media2MediaItem); /** Called when playback of the item list has ended. */ void onPlaybackEnded(); /** Called when the player encounters an error. */ - void onError(@Nullable androidx.media2.common.MediaItem androidXMediaItem); + void onError(@Nullable androidx.media2.common.MediaItem media2MediaItem); - /** Called when the playlist is changed */ + /** Called when the playlist is changed. */ void onPlaylistChanged(); - /** Called when the shuffle mode is changed */ + /** Called when the shuffle mode is changed. */ void onShuffleModeChanged(int shuffleMode); - /** Called when the repeat mode is changed */ + /** Called when the repeat mode is changed. */ void onRepeatModeChanged(int repeatMode); - /** Called when the audio attributes is changed */ + /** Called when the audio attributes is changed. */ void onAudioAttributesChanged(AudioAttributesCompat audioAttributes); - /** Called when the playback speed is changed */ + /** Called when the playback speed is changed. */ void onPlaybackSpeedChanged(float playbackSpeed); } @@ -108,14 +108,15 @@ import java.util.List; private final ControlDispatcher controlDispatcher; private final ComponentListener componentListener; - private final List cachedPlaylist; @Nullable private MediaMetadata playlistMetadata; - private final List cachedMediaItems; + + // These should be only updated in TimelineChanges. + private final List media2Playlist; + private final List exoPlayerPlaylist; private boolean prepared; private boolean rebuffering; private int currentWindowIndex; - private boolean loggedUnexpectedTimelineChanges; private boolean ignoreTimelineUpdates; /** @@ -146,79 +147,69 @@ import java.util.List; handler = new PlayerHandler(player.getApplicationLooper()); pollBufferRunnable = new PollBufferRunnable(); - cachedPlaylist = new ArrayList<>(); - cachedMediaItems = new ArrayList<>(); + media2Playlist = new ArrayList<>(); + exoPlayerPlaylist = new ArrayList<>(); currentWindowIndex = C.INDEX_UNSET; + + prepared = player.getPlaybackState() != Player.STATE_IDLE; + rebuffering = player.getPlaybackState() == Player.STATE_BUFFERING; + + updatePlaylist(player.getCurrentTimeline()); } - public boolean setMediaItem(androidx.media2.common.MediaItem androidXMediaItem) { - return setPlaylist(Collections.singletonList(androidXMediaItem), /* metadata= */ null); + public boolean setMediaItem(androidx.media2.common.MediaItem media2MediaItem) { + return setPlaylist(Collections.singletonList(media2MediaItem), /* metadata= */ null); } public boolean setPlaylist( List playlist, @Nullable MediaMetadata metadata) { // Check for duplication. for (int i = 0; i < playlist.size(); i++) { - androidx.media2.common.MediaItem androidXMediaItem = playlist.get(i); - Assertions.checkArgument(playlist.indexOf(androidXMediaItem) == i); + androidx.media2.common.MediaItem media2MediaItem = playlist.get(i); + Assertions.checkArgument(playlist.indexOf(media2MediaItem) == i); } - this.cachedPlaylist.clear(); - this.cachedPlaylist.addAll(playlist); this.playlistMetadata = metadata; - this.cachedMediaItems.clear(); - List exoplayerMediaItems = new ArrayList<>(); + List exoPlayerMediaItems = new ArrayList<>(); for (int i = 0; i < playlist.size(); i++) { - androidx.media2.common.MediaItem androidXMediaItem = playlist.get(i); - MediaItem exoplayerMediaItem = - Assertions.checkNotNull( - mediaItemConverter.convertToExoPlayerMediaItem(androidXMediaItem)); - exoplayerMediaItems.add(exoplayerMediaItem); + androidx.media2.common.MediaItem media2MediaItem = playlist.get(i); + MediaItem exoPlayerMediaItem = + Assertions.checkNotNull(mediaItemConverter.convertToExoPlayerMediaItem(media2MediaItem)); + exoPlayerMediaItems.add(exoPlayerMediaItem); } - this.cachedMediaItems.addAll(exoplayerMediaItems); - player.setMediaItems(exoplayerMediaItems, /* resetPosition= */ true); + player.setMediaItems(exoPlayerMediaItems, /* resetPosition= */ true); currentWindowIndex = getCurrentMediaItemIndex(); return true; } - public boolean addPlaylistItem(int index, androidx.media2.common.MediaItem androidXMediaItem) { - Assertions.checkArgument(!cachedPlaylist.contains(androidXMediaItem)); - index = Util.constrainValue(index, 0, cachedPlaylist.size()); + public boolean addPlaylistItem(int index, androidx.media2.common.MediaItem media2MediaItem) { + Assertions.checkArgument(!media2Playlist.contains(media2MediaItem)); + index = Util.constrainValue(index, 0, media2Playlist.size()); - cachedPlaylist.add(index, androidXMediaItem); - MediaItem exoplayerMediaItem = - Assertions.checkNotNull(mediaItemConverter.convertToExoPlayerMediaItem(androidXMediaItem)); - cachedMediaItems.add(index, exoplayerMediaItem); - player.addMediaItem(index, exoplayerMediaItem); + MediaItem exoPlayerMediaItem = + Assertions.checkNotNull(mediaItemConverter.convertToExoPlayerMediaItem(media2MediaItem)); + player.addMediaItem(index, exoPlayerMediaItem); return true; } public boolean removePlaylistItem(@IntRange(from = 0) int index) { - androidx.media2.common.MediaItem androidXMediaItemToRemove = cachedPlaylist.remove(index); - releaseMediaItem(androidXMediaItemToRemove); - cachedMediaItems.remove(index); player.removeMediaItem(index); return true; } - public boolean replacePlaylistItem( - int index, androidx.media2.common.MediaItem androidXMediaItem) { - Assertions.checkArgument(!cachedPlaylist.contains(androidXMediaItem)); - index = Util.constrainValue(index, 0, cachedPlaylist.size()); + public boolean replacePlaylistItem(int index, androidx.media2.common.MediaItem media2MediaItem) { + Assertions.checkArgument(!media2Playlist.contains(media2MediaItem)); + index = Util.constrainValue(index, 0, media2Playlist.size()); - androidx.media2.common.MediaItem androidXMediaItemToRemove = cachedPlaylist.get(index); - cachedPlaylist.set(index, androidXMediaItem); - releaseMediaItem(androidXMediaItemToRemove); - MediaItem exoplayerMediaItemToAdd = - Assertions.checkNotNull(mediaItemConverter.convertToExoPlayerMediaItem(androidXMediaItem)); - cachedMediaItems.set(index, exoplayerMediaItemToAdd); + MediaItem exoPlayerMediaItemToAdd = + Assertions.checkNotNull(mediaItemConverter.convertToExoPlayerMediaItem(media2MediaItem)); ignoreTimelineUpdates = true; player.removeMediaItem(index); ignoreTimelineUpdates = false; - player.addMediaItem(index, exoplayerMediaItemToAdd); + player.addMediaItem(index, exoPlayerMediaItemToAdd); return true; } @@ -272,8 +263,8 @@ import java.util.List; } @Nullable - public List getCachedPlaylist() { - return new ArrayList<>(cachedPlaylist); + public List getPlaylist() { + return new ArrayList<>(media2Playlist); } @Nullable @@ -290,7 +281,7 @@ import java.util.List; } public int getCurrentMediaItemIndex() { - return cachedPlaylist.isEmpty() ? C.INDEX_UNSET : player.getCurrentWindowIndex(); + return media2Playlist.isEmpty() ? C.INDEX_UNSET : player.getCurrentWindowIndex(); } public int getPreviousMediaItemIndex() { @@ -304,7 +295,7 @@ import java.util.List; @Nullable public androidx.media2.common.MediaItem getCurrentMediaItem() { int index = getCurrentMediaItemIndex(); - return (index != C.INDEX_UNSET) ? cachedPlaylist.get(index) : null; + return index == C.INDEX_UNSET ? null : media2Playlist.get(index); } public boolean prepare() { @@ -317,9 +308,9 @@ import java.util.List; public boolean play() { if (player.getPlaybackState() == Player.STATE_ENDED) { - int currentWindowIndex = getCurrentMediaItemIndex(); boolean seekHandled = - controlDispatcher.dispatchSeekTo(player, currentWindowIndex, /* positionMs= */ 0); + controlDispatcher.dispatchSeekTo( + player, player.getCurrentWindowIndex(), /* positionMs= */ 0); if (!seekHandled) { return false; } @@ -342,23 +333,19 @@ import java.util.List; } public boolean seekTo(long position) { - int currentWindowIndex = getCurrentMediaItemIndex(); - return controlDispatcher.dispatchSeekTo(player, currentWindowIndex, position); + return controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), position); } public long getCurrentPosition() { - Assertions.checkState(getState() != SessionPlayer.PLAYER_STATE_IDLE); - return Math.max(0, player.getCurrentPosition()); + return player.getCurrentPosition(); } public long getDuration() { - Assertions.checkState(getState() != SessionPlayer.PLAYER_STATE_IDLE); long duration = player.getDuration(); - return duration == C.TIME_UNSET ? -1 : duration; + return duration == C.TIME_UNSET ? SessionPlayer.UNKNOWN_TIME : duration; } public long getBufferedPosition() { - Assertions.checkState(getState() != SessionPlayer.PLAYER_STATE_IDLE); return player.getBufferedPosition(); } @@ -397,11 +384,11 @@ import java.util.List; } public void setPlaybackSpeed(float playbackSpeed) { - player.setPlaybackSpeed(playbackSpeed); + player.setPlaybackParameters(new PlaybackParameters(playbackSpeed)); } public float getPlaybackSpeed() { - return player.getPlaybackSpeed(); + return player.getPlaybackParameters().speed; } public void reset() { @@ -427,7 +414,7 @@ import java.util.List; } public boolean canSkipToPlaylistItem() { - @Nullable List playlist = getCachedPlaylist(); + @Nullable List playlist = getPlaylist(); return playlist != null && playlist.size() > 1; } @@ -497,57 +484,57 @@ import java.util.List; listener.onShuffleModeChanged(Utils.getShuffleMode(shuffleModeEnabled)); } - private void handlePlaybackSpeedChanged(float playbackSpeed) { - listener.onPlaybackSpeedChanged(playbackSpeed); + private void handlePlaybackParametersChanged(PlaybackParameters playbackParameters) { + listener.onPlaybackSpeedChanged(playbackParameters.speed); } private void handleTimelineChanged(Timeline timeline) { if (ignoreTimelineUpdates) { return; } - updateCachedPlaylistAndMediaItems(timeline); + if (!isExoPlayerMediaItemsChanged(timeline)) { + return; + } + updatePlaylist(timeline); listener.onPlaylistChanged(); } - // Update cached playlist, if the ExoPlayer Player's Timeline is unexpectedly changed without - // using SessionPlayer interface. - private void updateCachedPlaylistAndMediaItems(Timeline currentTimeline) { - // Check whether ExoPlayer media items are the same as expected. + // Check whether Timeline is changed by media item changes or not + private boolean isExoPlayerMediaItemsChanged(Timeline timeline) { + if (exoPlayerPlaylist.size() != timeline.getWindowCount()) { + return true; + } Timeline.Window window = new Timeline.Window(); - int windowCount = currentTimeline.getWindowCount(); + int windowCount = timeline.getWindowCount(); for (int i = 0; i < windowCount; i++) { - currentTimeline.getWindow(i, window); - if (i >= cachedMediaItems.size() - || !ObjectsCompat.equals(cachedMediaItems.get(i), window.mediaItem)) { - if (!loggedUnexpectedTimelineChanges) { - Log.w(TAG, "Timeline was unexpectedly changed. Playlist will be rebuilt."); - loggedUnexpectedTimelineChanges = true; - } - - androidx.media2.common.MediaItem oldAndroidXMediaItem = cachedPlaylist.get(i); - releaseMediaItem(oldAndroidXMediaItem); - - androidx.media2.common.MediaItem androidXMediaItem = - Assertions.checkNotNull( - mediaItemConverter.convertToAndroidXMediaItem(window.mediaItem)); - if (i < cachedMediaItems.size()) { - cachedMediaItems.set(i, window.mediaItem); - cachedPlaylist.set(i, androidXMediaItem); - } else { - cachedMediaItems.add(window.mediaItem); - cachedPlaylist.add(androidXMediaItem); - } + timeline.getWindow(i, window); + if (!ObjectsCompat.equals(exoPlayerPlaylist.get(i), window.mediaItem)) { + return true; } } - if (cachedMediaItems.size() > windowCount) { - if (!loggedUnexpectedTimelineChanges) { - Log.w(TAG, "Timeline was unexpectedly changed. Playlist will be rebuilt."); - loggedUnexpectedTimelineChanges = true; - } - while (cachedMediaItems.size() > windowCount) { - cachedMediaItems.remove(windowCount); - cachedPlaylist.remove(windowCount); - } + return false; + } + + private void updatePlaylist(Timeline timeline) { + List media2MediaItemToBeRemoved = + new ArrayList<>(media2Playlist); + media2Playlist.clear(); + exoPlayerPlaylist.clear(); + + Timeline.Window window = new Timeline.Window(); + int windowCount = timeline.getWindowCount(); + for (int i = 0; i < windowCount; i++) { + timeline.getWindow(i, window); + MediaItem exoPlayerMediaItem = window.mediaItem; + androidx.media2.common.MediaItem media2MediaItem = + Assertions.checkNotNull(mediaItemConverter.convertToMedia2MediaItem(exoPlayerMediaItem)); + exoPlayerPlaylist.add(exoPlayerMediaItem); + media2Playlist.add(media2MediaItem); + media2MediaItemToBeRemoved.remove(media2MediaItem); + } + + for (androidx.media2.common.MediaItem item : media2MediaItemToBeRemoved) { + releaseMediaItem(item); } } @@ -556,35 +543,35 @@ import java.util.List; } private void updateBufferingAndScheduleNextPollBuffer() { - androidx.media2.common.MediaItem androidXMediaItem = + androidx.media2.common.MediaItem media2MediaItem = Assertions.checkNotNull(getCurrentMediaItem()); - listener.onBufferingUpdate(androidXMediaItem, player.getBufferedPercentage()); + listener.onBufferingUpdate(media2MediaItem, player.getBufferedPercentage()); handler.removeCallbacks(pollBufferRunnable); handler.postDelayed(pollBufferRunnable, POLL_BUFFER_INTERVAL_MS); } private void maybeNotifyBufferingEvents() { - androidx.media2.common.MediaItem androidXMediaItem = + androidx.media2.common.MediaItem media2MediaItem = Assertions.checkNotNull(getCurrentMediaItem()); if (prepared && !rebuffering) { rebuffering = true; - listener.onBufferingStarted(androidXMediaItem); + listener.onBufferingStarted(media2MediaItem); } } private void maybeNotifyReadyEvents() { - androidx.media2.common.MediaItem androidXMediaItem = + androidx.media2.common.MediaItem media2MediaItem = Assertions.checkNotNull(getCurrentMediaItem()); boolean prepareComplete = !prepared; if (prepareComplete) { prepared = true; handlePositionDiscontinuity(Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); listener.onPlayerStateChanged(SessionPlayer.PLAYER_STATE_PAUSED); - listener.onPrepared(androidXMediaItem, player.getBufferedPercentage()); + listener.onPrepared(media2MediaItem, player.getBufferedPercentage()); } if (rebuffering) { rebuffering = false; - listener.onBufferingEnded(androidXMediaItem, player.getBufferedPercentage()); + listener.onBufferingEnded(media2MediaItem, player.getBufferedPercentage()); } } @@ -596,13 +583,13 @@ import java.util.List; } } - private void releaseMediaItem(androidx.media2.common.MediaItem androidXMediaItem) { + private void releaseMediaItem(androidx.media2.common.MediaItem media2MediaItem) { try { - if (androidXMediaItem instanceof CallbackMediaItem) { - ((CallbackMediaItem) androidXMediaItem).getDataSourceCallback().close(); + if (media2MediaItem instanceof CallbackMediaItem) { + ((CallbackMediaItem) media2MediaItem).getDataSourceCallback().close(); } } catch (IOException e) { - Log.w(TAG, "Error releasing media item " + androidXMediaItem, e); + Log.w(TAG, "Error releasing media item " + media2MediaItem, e); } } @@ -641,8 +628,8 @@ import java.util.List; } @Override - public void onPlaybackSpeedChanged(float playbackSpeed) { - handlePlaybackSpeedChanged(playbackSpeed); + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + handlePlaybackParametersChanged(playbackParameters); } @Override diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionCallback.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionCallback.java index 986478f1a9..1f60db947e 100644 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionCallback.java +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionCallback.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.ext.media2; import static java.util.concurrent.TimeUnit.MILLISECONDS; diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionCallbackBuilder.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionCallbackBuilder.java index e334dbd0ad..516ec20b3b 100644 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionCallbackBuilder.java +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionCallbackBuilder.java @@ -13,14 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.ext.media2; import android.Manifest; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; -import android.net.Uri; import android.os.Bundle; import android.provider.Settings; import android.text.TextUtils; @@ -31,7 +29,6 @@ import androidx.media2.common.MediaItem; import androidx.media2.common.MediaMetadata; import androidx.media2.common.Rating; import androidx.media2.common.SessionPlayer; -import androidx.media2.common.UriMediaItem; import androidx.media2.session.MediaController; import androidx.media2.session.MediaSession; import androidx.media2.session.MediaSession.ControllerInfo; @@ -39,13 +36,11 @@ import androidx.media2.session.SessionCommand; import androidx.media2.session.SessionCommandGroup; import androidx.media2.session.SessionResult; import com.google.android.exoplayer2.util.Assertions; -import java.net.URI; -import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; /** - * Builds {@link MediaSession.SessionCallback} with various collaborators. + * Builds a {@link MediaSession.SessionCallback} with various collaborators. * * @see MediaSession.SessionCallback */ @@ -351,10 +346,8 @@ public final class SessionCallbackBuilder { } } - /** - * Default implementation of {@link MediaItemProvider} that assumes the media id is a URI string. - */ - public static final class DefaultMediaItemProvider implements MediaItemProvider { + /** A {@link MediaItemProvider} that creates media items containing only a media ID. */ + public static final class MediaIdMediaItemProvider implements MediaItemProvider { @Override @Nullable public MediaItem onCreateMediaItem( @@ -362,17 +355,11 @@ public final class SessionCallbackBuilder { if (TextUtils.isEmpty(mediaId)) { return null; } - try { - new URI(mediaId); - } catch (URISyntaxException e) { - // Ignore if mediaId isn't a URI. - return null; - } MediaMetadata metadata = new MediaMetadata.Builder() .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, mediaId) .build(); - return new UriMediaItem.Builder(Uri.parse(mediaId)).setMetadata(metadata).build(); + return new MediaItem.Builder().setMetadata(metadata).build(); } } diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java index 99a5f1bcfc..d4aa888a1a 100644 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.ext.media2; import androidx.annotation.FloatRange; @@ -23,6 +22,7 @@ import androidx.annotation.Nullable; import androidx.core.util.ObjectsCompat; import androidx.core.util.Pair; import androidx.media.AudioAttributesCompat; +import androidx.media2.common.CallbackMediaItem; import androidx.media2.common.FileMediaItem; import androidx.media2.common.MediaItem; import androidx.media2.common.MediaMetadata; @@ -41,25 +41,11 @@ import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; -import org.checkerframework.checker.initialization.qual.Initialized; import org.checkerframework.checker.nullness.compatqual.NullableType; /** * An implementation of {@link SessionPlayer} that wraps a given ExoPlayer {@link Player} instance. * - *

Ownership

- * - *

{@code SessionPlayerConnector} takes ownership of the provided ExoPlayer {@link Player} - * instance between when it's constructed and when it's {@link #close() closed}. No other components - * should interact with the wrapped player (otherwise, unexpected event callbacks from the wrapped - * player may put the session player in an inconsistent state). - * - *

Call {@link SessionPlayer#close()} when the {@code SessionPlayerConnector} is no longer needed - * to regain ownership of the wrapped player. It is the caller's responsibility to release the - * wrapped player via {@link Player#release()}. - * - *

Threading model

- * *

Internally this implementation posts operations to and receives callbacks on the thread * associated with {@link Player#getApplicationLooper()}, so it is important not to block this * thread. In particular, when awaiting the result of an asynchronous session player operation, apps @@ -95,16 +81,15 @@ public final class SessionPlayerConnector extends SessionPlayer { // Should be only accessed on the executor, which is currently single-threaded. @Nullable private MediaItem currentMediaItem; - @Nullable private List currentPlaylist; /** - * Creates an instance using {@link DefaultControlDispatcher} to dispatch player commands. + * Creates an instance using {@link DefaultMediaItemConverter} to convert between ExoPlayer and + * media2 MediaItems and {@link DefaultControlDispatcher} to dispatch player commands. * * @param player The player to wrap. - * @param mediaItemConverter The {@link MediaItemConverter}. */ - public SessionPlayerConnector(Player player, MediaItemConverter mediaItemConverter) { - this(player, mediaItemConverter, new DefaultControlDispatcher()); + public SessionPlayerConnector(Player player) { + this(player, new DefaultMediaItemConverter(), new DefaultControlDispatcher()); } /** @@ -124,19 +109,8 @@ public final class SessionPlayerConnector extends SessionPlayer { taskHandler = new PlayerHandler(player.getApplicationLooper()); taskHandlerExecutor = taskHandler::postOrRun; ExoPlayerWrapperListener playerListener = new ExoPlayerWrapperListener(); - PlayerWrapper playerWrapper = - new PlayerWrapper(playerListener, player, mediaItemConverter, controlDispatcher); - this.player = playerWrapper; + this.player = new PlayerWrapper(playerListener, player, mediaItemConverter, controlDispatcher); playerCommandQueue = new PlayerCommandQueue(this.player, taskHandler); - - @SuppressWarnings("assignment.type.incompatible") - @Initialized - SessionPlayerConnector initializedThis = this; - initializedThis.runPlayerCallableBlocking( - /* callable= */ () -> { - playerWrapper.reset(); - return null; - }); } @Override @@ -251,17 +225,27 @@ public final class SessionPlayerConnector extends SessionPlayer { return runPlayerCallableBlockingWithNullOnException(/* callable= */ player::getAudioAttributes); } + /** + * {@inheritDoc} + * + *

{@link FileMediaItem} and {@link CallbackMediaItem} are not supported. + */ @Override public ListenableFuture setMediaItem(MediaItem item) { Assertions.checkNotNull(item); Assertions.checkArgument(!(item instanceof FileMediaItem)); + Assertions.checkArgument(!(item instanceof CallbackMediaItem)); ListenableFuture result = playerCommandQueue.addCommand( PlayerCommandQueue.COMMAND_CODE_PLAYER_SET_MEDIA_ITEM, () -> player.setMediaItem(item)); - result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor); return result; } + /** + * {@inheritDoc} + * + *

{@link FileMediaItem} and {@link CallbackMediaItem} are not supported. + */ @Override public ListenableFuture setPlaylist( final List playlist, @Nullable MediaMetadata metadata) { @@ -271,6 +255,7 @@ public final class SessionPlayerConnector extends SessionPlayer { MediaItem item = playlist.get(i); Assertions.checkNotNull(item); Assertions.checkArgument(!(item instanceof FileMediaItem)); + Assertions.checkArgument(!(item instanceof CallbackMediaItem)); for (int j = 0; j < i; j++) { Assertions.checkArgument( item != playlist.get(j), @@ -281,20 +266,24 @@ public final class SessionPlayerConnector extends SessionPlayer { playerCommandQueue.addCommand( PlayerCommandQueue.COMMAND_CODE_PLAYER_SET_PLAYLIST, /* command= */ () -> player.setPlaylist(playlist, metadata)); - result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor); return result; } + /** + * {@inheritDoc} + * + *

{@link FileMediaItem} and {@link CallbackMediaItem} are not supported. + */ @Override public ListenableFuture addPlaylistItem(int index, MediaItem item) { Assertions.checkArgument(index >= 0); Assertions.checkNotNull(item); Assertions.checkArgument(!(item instanceof FileMediaItem)); + Assertions.checkArgument(!(item instanceof CallbackMediaItem)); ListenableFuture result = playerCommandQueue.addCommand( PlayerCommandQueue.COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM, /* command= */ () -> player.addPlaylistItem(index, item)); - result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor); return result; } @@ -305,20 +294,24 @@ public final class SessionPlayerConnector extends SessionPlayer { playerCommandQueue.addCommand( PlayerCommandQueue.COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM, /* command= */ () -> player.removePlaylistItem(index)); - result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor); return result; } + /** + * {@inheritDoc} + * + *

{@link FileMediaItem} and {@link CallbackMediaItem} are not supported. + */ @Override public ListenableFuture replacePlaylistItem(int index, MediaItem item) { Assertions.checkArgument(index >= 0); Assertions.checkNotNull(item); Assertions.checkArgument(!(item instanceof FileMediaItem)); + Assertions.checkArgument(!(item instanceof CallbackMediaItem)); ListenableFuture result = playerCommandQueue.addCommand( PlayerCommandQueue.COMMAND_CODE_PLAYER_REPLACE_PLAYLIST_ITEM, /* command= */ () -> player.replacePlaylistItem(index, item)); - result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor); return result; } @@ -385,7 +378,7 @@ public final class SessionPlayerConnector extends SessionPlayer { @Override @Nullable public List getPlaylist() { - return runPlayerCallableBlockingWithNullOnException(/* callable= */ player::getCachedPlaylist); + return runPlayerCallableBlockingWithNullOnException(/* callable= */ player::getPlaylist); } @Override @@ -447,7 +440,7 @@ public final class SessionPlayerConnector extends SessionPlayer { } reset(); - this.runPlayerCallableBlockingInternal( + this.runPlayerCallableBlocking( /* callable= */ () -> { player.close(); return null; @@ -511,7 +504,7 @@ public final class SessionPlayerConnector extends SessionPlayer { state = PLAYER_STATE_IDLE; mediaItemToBuffState.clear(); } - this.runPlayerCallableBlockingInternal( + this.runPlayerCallableBlocking( /* callable= */ () -> { player.reset(); return null; @@ -558,25 +551,18 @@ public final class SessionPlayerConnector extends SessionPlayer { } private void handlePlaylistChangedOnHandler() { - List currentPlaylist = player.getCachedPlaylist(); - boolean notifyCurrentPlaylist = !ObjectsCompat.equals(this.currentPlaylist, currentPlaylist); - this.currentPlaylist = currentPlaylist; + List currentPlaylist = player.getPlaylist(); MediaMetadata playlistMetadata = player.getPlaylistMetadata(); MediaItem currentMediaItem = player.getCurrentMediaItem(); boolean notifyCurrentMediaItem = !ObjectsCompat.equals(this.currentMediaItem, currentMediaItem); this.currentMediaItem = currentMediaItem; - if (!notifyCurrentMediaItem && !notifyCurrentPlaylist) { - return; - } long currentPosition = getCurrentPosition(); notifySessionPlayerCallback( callback -> { - if (notifyCurrentPlaylist) { - callback.onPlaylistChanged( - SessionPlayerConnector.this, currentPlaylist, playlistMetadata); - } + callback.onPlaylistChanged( + SessionPlayerConnector.this, currentPlaylist, playlistMetadata); if (notifyCurrentMediaItem) { Assertions.checkNotNull( currentMediaItem, "PlaylistManager#currentMediaItem() cannot be changed to null"); @@ -610,13 +596,6 @@ public final class SessionPlayerConnector extends SessionPlayer { } private T runPlayerCallableBlocking(Callable callable) { - synchronized (stateLock) { - Assertions.checkState(!closed); - } - return runPlayerCallableBlockingInternal(callable); - } - - private T runPlayerCallableBlockingInternal(Callable callable) { SettableFuture future = SettableFuture.create(); boolean success = taskHandler.postOrRun( diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index f3edfa3545..85d0155bd7 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -38,6 +38,7 @@ import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.DefaultControlDispatcher; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.Assertions; @@ -127,8 +128,8 @@ public final class MediaSessionConnector { @PlaybackActions public static final long DEFAULT_PLAYBACK_ACTIONS = ALL_PLAYBACK_ACTIONS; /** - * The name of the {@link PlaybackStateCompat} float extra with the value of {@link - * Player#getPlaybackSpeed()}. + * The name of the {@link PlaybackStateCompat} float extra with the value of {@code + * Player.getPlaybackParameters().speed}. */ public static final String EXTRAS_SPEED = "EXO_SPEED"; @@ -765,7 +766,7 @@ public final class MediaSessionConnector { queueNavigator != null ? queueNavigator.getActiveQueueItemId(player) : MediaSessionCompat.QueueItem.UNKNOWN_ID; - float playbackSpeed = player.getPlaybackSpeed(); + float playbackSpeed = player.getPlaybackParameters().speed; extras.putFloat(EXTRAS_SPEED, playbackSpeed); float sessionPlaybackSpeed = player.isPlaying() ? playbackSpeed : 0f; builder @@ -1134,7 +1135,7 @@ public final class MediaSessionConnector { } @Override - public void onPlaybackSpeedChanged(float playbackSpeed) { + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { invalidateMediaSessionPlaybackState(); } diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Format.java b/library/common/src/main/java/com/google/android/exoplayer2/Format.java index fa11f0c2a9..05062727c3 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/Format.java @@ -592,37 +592,7 @@ public final class Format implements Parcelable { // Build. public Format build() { - return new Format( - id, - label, - language, - selectionFlags, - roleFlags, - averageBitrate, - peakBitrate, - codecs, - metadata, - containerMimeType, - sampleMimeType, - maxInputSize, - initializationData, - drmInitData, - subsampleOffsetUs, - width, - height, - frameRate, - rotationDegrees, - pixelWidthHeightRatio, - projectionData, - stereoMode, - colorInfo, - channelCount, - sampleRate, - pcmEncoding, - encoderDelay, - encoderPadding, - accessibilityChannel, - exoMediaCryptoType); + return new Format(/* builder= */ this); } } @@ -1211,86 +1181,51 @@ public final class Format implements Parcelable { return new Builder().setId(id).setSampleMimeType(sampleMimeType).build(); } - // Some fields are deprecated but they're still assigned below. - /* package */ Format( - @Nullable String id, - @Nullable String label, - @Nullable String language, - @C.SelectionFlags int selectionFlags, - @C.RoleFlags int roleFlags, - int averageBitrate, - int peakBitrate, - @Nullable String codecs, - @Nullable Metadata metadata, - // Container specific. - @Nullable String containerMimeType, - // Sample specific. - @Nullable String sampleMimeType, - int maxInputSize, - @Nullable List initializationData, - @Nullable DrmInitData drmInitData, - long subsampleOffsetUs, - // Video specific. - int width, - int height, - float frameRate, - int rotationDegrees, - float pixelWidthHeightRatio, - @Nullable byte[] projectionData, - @C.StereoMode int stereoMode, - @Nullable ColorInfo colorInfo, - // Audio specific. - int channelCount, - int sampleRate, - @C.PcmEncoding int pcmEncoding, - int encoderDelay, - int encoderPadding, - // Text specific. - int accessibilityChannel, - // Provided by source. - @Nullable Class exoMediaCryptoType) { - this.id = id; - this.label = label; - this.language = Util.normalizeLanguageCode(language); - this.selectionFlags = selectionFlags; - this.roleFlags = roleFlags; - this.averageBitrate = averageBitrate; - this.peakBitrate = peakBitrate; - this.bitrate = peakBitrate != NO_VALUE ? peakBitrate : averageBitrate; - this.codecs = codecs; - this.metadata = metadata; + private Format(Builder builder) { + id = builder.id; + label = builder.label; + language = Util.normalizeLanguageCode(builder.language); + selectionFlags = builder.selectionFlags; + roleFlags = builder.roleFlags; + averageBitrate = builder.averageBitrate; + peakBitrate = builder.peakBitrate; + bitrate = peakBitrate != NO_VALUE ? peakBitrate : averageBitrate; + codecs = builder.codecs; + metadata = builder.metadata; // Container specific. - this.containerMimeType = containerMimeType; + containerMimeType = builder.containerMimeType; // Sample specific. - this.sampleMimeType = sampleMimeType; - this.maxInputSize = maxInputSize; - this.initializationData = - initializationData == null ? Collections.emptyList() : initializationData; - this.drmInitData = drmInitData; - this.subsampleOffsetUs = subsampleOffsetUs; + sampleMimeType = builder.sampleMimeType; + maxInputSize = builder.maxInputSize; + initializationData = + builder.initializationData == null ? Collections.emptyList() : builder.initializationData; + drmInitData = builder.drmInitData; + subsampleOffsetUs = builder.subsampleOffsetUs; // Video specific. - this.width = width; - this.height = height; - this.frameRate = frameRate; - this.rotationDegrees = rotationDegrees == NO_VALUE ? 0 : rotationDegrees; - this.pixelWidthHeightRatio = pixelWidthHeightRatio == NO_VALUE ? 1 : pixelWidthHeightRatio; - this.projectionData = projectionData; - this.stereoMode = stereoMode; - this.colorInfo = colorInfo; + width = builder.width; + height = builder.height; + frameRate = builder.frameRate; + rotationDegrees = builder.rotationDegrees == NO_VALUE ? 0 : builder.rotationDegrees; + pixelWidthHeightRatio = + builder.pixelWidthHeightRatio == NO_VALUE ? 1 : builder.pixelWidthHeightRatio; + projectionData = builder.projectionData; + stereoMode = builder.stereoMode; + colorInfo = builder.colorInfo; // Audio specific. - this.channelCount = channelCount; - this.sampleRate = sampleRate; - this.pcmEncoding = pcmEncoding; - this.encoderDelay = encoderDelay == NO_VALUE ? 0 : encoderDelay; - this.encoderPadding = encoderPadding == NO_VALUE ? 0 : encoderPadding; + channelCount = builder.channelCount; + sampleRate = builder.sampleRate; + pcmEncoding = builder.pcmEncoding; + encoderDelay = builder.encoderDelay == NO_VALUE ? 0 : builder.encoderDelay; + encoderPadding = builder.encoderPadding == NO_VALUE ? 0 : builder.encoderPadding; // Text specific. - this.accessibilityChannel = accessibilityChannel; + accessibilityChannel = builder.accessibilityChannel; // Provided by source. - if (exoMediaCryptoType == null && drmInitData != null) { + if (builder.exoMediaCryptoType == null && drmInitData != null) { // Encrypted content must always have a non-null exoMediaCryptoType. exoMediaCryptoType = UnsupportedMediaCrypto.class; + } else { + exoMediaCryptoType = builder.exoMediaCryptoType; } - this.exoMediaCryptoType = exoMediaCryptoType; } // Some fields are deprecated but they're still assigned below. diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java b/library/common/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java index 6edbecf1ea..4831ec59e2 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.util; +import androidx.annotation.Nullable; import java.nio.ByteBuffer; import java.util.Arrays; @@ -219,11 +220,12 @@ public final class NalUnitUtil { * Returns whether the NAL unit with the specified header contains supplemental enhancement * information. * - * @param mimeType The sample MIME type. + * @param mimeType The sample MIME type, or {@code null} if unknown. * @param nalUnitHeaderFirstByte The first byte of nal_unit(). - * @return Whether the NAL unit with the specified header is an SEI NAL unit. + * @return Whether the NAL unit with the specified header is an SEI NAL unit. False is returned if + * the {@code MimeType} is {@code null}. */ - public static boolean isNalUnitSei(String mimeType, byte nalUnitHeaderFirstByte) { + public static boolean isNalUnitSei(@Nullable String mimeType, byte nalUnitHeaderFirstByte) { return (MimeTypes.VIDEO_H264.equals(mimeType) && (nalUnitHeaderFirstByte & 0x1F) == H264_NAL_UNIT_TYPE_SEI) || (MimeTypes.VIDEO_H265.equals(mimeType) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java index 213745f93d..7fa26a94f4 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -405,6 +405,21 @@ public final class Util { return concatenation; } + /** + * Copies the contents of {@code list} into {@code array}. + * + *

{@code list.size()} must be the same as {@code array.length} to ensure the contents can be + * copied into {@code array} without leaving any nulls at the end. + * + * @param list The list to copy items from. + * @param array The array to copy items to. + */ + @SuppressWarnings("nullness:toArray.nullable.elements.not.newarray") + public static void nullSafeListToArray(List list, T[] array) { + Assertions.checkState(list.size() == array.length); + list.toArray(array); + } + /** * Creates a {@link Handler} on the current {@link Looper} thread. * @@ -2362,7 +2377,7 @@ public final class Util { case TelephonyManager.NETWORK_TYPE_LTE: return C.NETWORK_TYPE_4G; case TelephonyManager.NETWORK_TYPE_NR: - return C.NETWORK_TYPE_5G; + return SDK_INT >= 29 ? C.NETWORK_TYPE_5G : C.NETWORK_TYPE_UNKNOWN; case TelephonyManager.NETWORK_TYPE_IWLAN: return C.NETWORK_TYPE_WIFI; case TelephonyManager.NETWORK_TYPE_GSM: diff --git a/library/common/src/test/java/com/google/android/exoplayer2/FormatTest.java b/library/common/src/test/java/com/google/android/exoplayer2/FormatTest.java index 608d1bb104..1ad888c868 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/FormatTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/FormatTest.java @@ -90,37 +90,38 @@ public final class FormatTest { C.COLOR_TRANSFER_SDR, new byte[] {1, 2, 3, 4, 5, 6, 7}); - return new Format( - "id", - "label", - "language", - C.SELECTION_FLAG_DEFAULT, - C.ROLE_FLAG_MAIN, - /* averageBitrate= */ 1024, - /* peakBitrate= */ 2048, - "codec", - metadata, - /* containerMimeType= */ MimeTypes.VIDEO_MP4, - /* sampleMimeType= */ MimeTypes.VIDEO_H264, - /* maxInputSize= */ 5000, - initializationData, - drmInitData, - Format.OFFSET_SAMPLE_RELATIVE, - /* width= */ 1920, - /* height= */ 1080, - /* frameRate= */ 24, - /* rotationDegrees= */ 90, - /* pixelWidthHeightRatio= */ 4, - projectionData, - C.STEREO_MODE_TOP_BOTTOM, - colorInfo, - /* channelCount= */ 6, - /* sampleRate= */ 44100, - C.ENCODING_PCM_24BIT, - /* encoderDelay= */ 1001, - /* encoderPadding= */ 1002, - /* accessibilityChannel= */ 2, - /* exoMediaCryptoType= */ ExoMediaCrypto.class); + return new Format.Builder() + .setId("id") + .setLabel("label") + .setLanguage("language") + .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) + .setRoleFlags(C.ROLE_FLAG_MAIN) + .setAverageBitrate(1024) + .setPeakBitrate(2048) + .setCodecs("codec") + .setMetadata(metadata) + .setContainerMimeType(MimeTypes.VIDEO_MP4) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setMaxInputSize(5000) + .setInitializationData(initializationData) + .setDrmInitData(drmInitData) + .setSubsampleOffsetUs(Format.OFFSET_SAMPLE_RELATIVE) + .setWidth(1920) + .setHeight(1080) + .setFrameRate(24) + .setRotationDegrees(90) + .setPixelWidthHeightRatio(4) + .setProjectionData(projectionData) + .setStereoMode(C.STEREO_MODE_TOP_BOTTOM) + .setColorInfo(colorInfo) + .setChannelCount(6) + .setSampleRate(44100) + .setPcmEncoding(C.ENCODING_PCM_24BIT) + .setEncoderDelay(1001) + .setEncoderPadding(1002) + .setAccessibilityChannel(2) + .setExoMediaCryptoType(ExoMediaCrypto.class) + .build(); } /** Generates an array of random bytes with the specified length. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java index 6276f3bca8..9ee1846fc1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java @@ -27,20 +27,20 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; */ /* package */ final class DefaultMediaClock implements MediaClock { - /** Listener interface to be notified of changes to the active playback speed. */ - public interface PlaybackSpeedListener { + /** Listener interface to be notified of changes to the active playback parameters. */ + public interface PlaybackParametersListener { /** - * Called when the active playback speed changed. Will not be called for {@link - * #setPlaybackSpeed(float)}. + * Called when the active playback parameters changed. Will not be called for {@link + * #setPlaybackParameters(PlaybackParameters)}. * - * @param newPlaybackSpeed The newly active playback speed. + * @param newPlaybackParameters The newly active playback parameters. */ - void onPlaybackSpeedChanged(float newPlaybackSpeed); + void onPlaybackParametersChanged(PlaybackParameters newPlaybackParameters); } private final StandaloneMediaClock standaloneClock; - private final PlaybackSpeedListener listener; + private final PlaybackParametersListener listener; @Nullable private Renderer rendererClockSource; @Nullable private MediaClock rendererClock; @@ -48,13 +48,13 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; private boolean standaloneClockIsStarted; /** - * Creates a new instance with listener for playback speed changes and a {@link Clock} to use for - * the standalone clock implementation. + * Creates a new instance with a listener for playback parameters changes and a {@link Clock} to + * use for the standalone clock implementation. * - * @param listener A {@link PlaybackSpeedListener} to listen for playback speed changes. + * @param listener A {@link PlaybackParametersListener} to listen for playback parameters changes. * @param clock A {@link Clock}. */ - public DefaultMediaClock(PlaybackSpeedListener listener, Clock clock) { + public DefaultMediaClock(PlaybackParametersListener listener, Clock clock) { this.listener = listener; this.standaloneClock = new StandaloneMediaClock(clock); isUsingStandaloneClock = true; @@ -102,7 +102,7 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; } this.rendererClock = rendererMediaClock; this.rendererClockSource = renderer; - rendererClock.setPlaybackSpeed(standaloneClock.getPlaybackSpeed()); + rendererClock.setPlaybackParameters(standaloneClock.getPlaybackParameters()); } } @@ -140,19 +140,19 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; } @Override - public void setPlaybackSpeed(float playbackSpeed) { + public void setPlaybackParameters(PlaybackParameters playbackParameters) { if (rendererClock != null) { - rendererClock.setPlaybackSpeed(playbackSpeed); - playbackSpeed = rendererClock.getPlaybackSpeed(); + rendererClock.setPlaybackParameters(playbackParameters); + playbackParameters = rendererClock.getPlaybackParameters(); } - standaloneClock.setPlaybackSpeed(playbackSpeed); + standaloneClock.setPlaybackParameters(playbackParameters); } @Override - public float getPlaybackSpeed() { + public PlaybackParameters getPlaybackParameters() { return rendererClock != null - ? rendererClock.getPlaybackSpeed() - : standaloneClock.getPlaybackSpeed(); + ? rendererClock.getPlaybackParameters() + : standaloneClock.getPlaybackParameters(); } private void syncClocks(boolean isReadingAhead) { @@ -180,10 +180,10 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; } // Continuously sync stand-alone clock to renderer clock so that it can take over if needed. standaloneClock.resetPosition(rendererClockPositionUs); - float playbackSpeed = rendererClock.getPlaybackSpeed(); - if (playbackSpeed != standaloneClock.getPlaybackSpeed()) { - standaloneClock.setPlaybackSpeed(playbackSpeed); - listener.onPlaybackSpeedChanged(playbackSpeed); + PlaybackParameters playbackParameters = rendererClock.getPlaybackParameters(); + if (!playbackParameters.equals(standaloneClock.getPlaybackParameters())) { + standaloneClock.setPlaybackParameters(playbackParameters); + listener.onPlaybackParametersChanged(playbackParameters); } } 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 76be46eb40..b1f5736465 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 @@ -619,32 +619,17 @@ import java.util.concurrent.TimeoutException; /* seekProcessed= */ true); } - /** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */ - @SuppressWarnings("deprecation") - @Deprecated @Override public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { - setPlaybackSpeed( - playbackParameters != null ? playbackParameters.speed : Player.DEFAULT_PLAYBACK_SPEED); - } - - /** @deprecated Use {@link #getPlaybackSpeed()} instead. */ - @SuppressWarnings("deprecation") - @Deprecated - @Override - public PlaybackParameters getPlaybackParameters() { - return new PlaybackParameters(playbackInfo.playbackSpeed); - } - - @Override - public void setPlaybackSpeed(float playbackSpeed) { - checkState(playbackSpeed > 0); - if (playbackInfo.playbackSpeed == playbackSpeed) { + if (playbackParameters == null) { + playbackParameters = PlaybackParameters.DEFAULT; + } + if (playbackInfo.playbackParameters.equals(playbackParameters)) { return; } - PlaybackInfo newPlaybackInfo = playbackInfo.copyWithPlaybackSpeed(playbackSpeed); + PlaybackInfo newPlaybackInfo = playbackInfo.copyWithPlaybackParameters(playbackParameters); pendingOperationAcks++; - internalPlayer.setPlaybackSpeed(playbackSpeed); + internalPlayer.setPlaybackParameters(playbackParameters); updatePlaybackInfo( newPlaybackInfo, /* positionDiscontinuity= */ false, @@ -655,8 +640,8 @@ import java.util.concurrent.TimeoutException; } @Override - public float getPlaybackSpeed() { - return playbackInfo.playbackSpeed; + public PlaybackParameters getPlaybackParameters() { + return playbackInfo.playbackParameters; } @Override @@ -1366,7 +1351,7 @@ import java.util.concurrent.TimeoutException; private final boolean playWhenReadyChanged; private final boolean playbackSuppressionReasonChanged; private final boolean isPlayingChanged; - private final boolean playbackSpeedChanged; + private final boolean playbackParametersChanged; private final boolean offloadSchedulingEnabledChanged; public PlaybackInfoUpdate( @@ -1405,7 +1390,8 @@ import java.util.concurrent.TimeoutException; playbackSuppressionReasonChanged = previousPlaybackInfo.playbackSuppressionReason != playbackInfo.playbackSuppressionReason; isPlayingChanged = isPlaying(previousPlaybackInfo) != isPlaying(playbackInfo); - playbackSpeedChanged = previousPlaybackInfo.playbackSpeed != playbackInfo.playbackSpeed; + playbackParametersChanged = + !previousPlaybackInfo.playbackParameters.equals(playbackInfo.playbackParameters); offloadSchedulingEnabledChanged = previousPlaybackInfo.offloadSchedulingEnabled != playbackInfo.offloadSchedulingEnabled; } @@ -1473,13 +1459,11 @@ import java.util.concurrent.TimeoutException; invokeAll( listenerSnapshot, listener -> listener.onIsPlayingChanged(isPlaying(playbackInfo))); } - if (playbackSpeedChanged) { - PlaybackParameters playbackParameters = new PlaybackParameters(playbackInfo.playbackSpeed); + if (playbackParametersChanged) { invokeAll( listenerSnapshot, listener -> { - listener.onPlaybackSpeedChanged(playbackInfo.playbackSpeed); - listener.onPlaybackParametersChanged(playbackParameters); + listener.onPlaybackParametersChanged(playbackInfo.playbackParameters); }); } if (seekProcessed) { 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 77637f1ef0..9739680e79 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 @@ -27,7 +27,7 @@ import android.os.SystemClock; import android.util.Pair; import androidx.annotation.CheckResult; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.DefaultMediaClock.PlaybackSpeedListener; +import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParametersListener; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Player.PlayWhenReadyChangeReason; import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; @@ -61,7 +61,7 @@ import java.util.concurrent.atomic.AtomicBoolean; MediaPeriod.Callback, TrackSelector.InvalidationListener, MediaSourceList.MediaSourceListInfoRefreshListener, - PlaybackSpeedListener, + PlaybackParametersListener, PlayerMessage.Sender { private static final String TAG = "ExoPlayerImplInternal"; @@ -121,7 +121,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private static final int MSG_SET_PLAY_WHEN_READY = 1; private static final int MSG_DO_SOME_WORK = 2; private static final int MSG_SEEK_TO = 3; - private static final int MSG_SET_PLAYBACK_SPEED = 4; + private static final int MSG_SET_PLAYBACK_PARAMETERS = 4; private static final int MSG_SET_SEEK_PARAMETERS = 5; private static final int MSG_STOP = 6; private static final int MSG_RELEASE = 7; @@ -133,7 +133,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private static final int MSG_SET_FOREGROUND_MODE = 13; private static final int MSG_SEND_MESSAGE = 14; private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 15; - private static final int MSG_PLAYBACK_SPEED_CHANGED_INTERNAL = 16; + private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 16; private static final int MSG_SET_MEDIA_SOURCES = 17; private static final int MSG_ADD_MEDIA_SOURCES = 18; private static final int MSG_MOVE_MEDIA_SOURCES = 19; @@ -163,6 +163,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private final BandwidthMeter bandwidthMeter; private final HandlerWrapper handler; private final HandlerThread internalPlaybackThread; + private final Looper playbackLooper; private final Timeline.Window window; private final Timeline.Period period; private final long backBufferDurationUs; @@ -252,7 +253,8 @@ import java.util.concurrent.atomic.AtomicBoolean; // not normally change to this priority" is incorrect. internalPlaybackThread = new HandlerThread("ExoPlayer:Playback", Process.THREAD_PRIORITY_AUDIO); internalPlaybackThread.start(); - handler = clock.createHandler(internalPlaybackThread.getLooper(), this); + playbackLooper = internalPlaybackThread.getLooper(); + handler = clock.createHandler(playbackLooper, this); } public void experimentalSetReleaseTimeoutMs(long releaseTimeoutMs) { @@ -301,8 +303,8 @@ import java.util.concurrent.atomic.AtomicBoolean; .sendToTarget(); } - public void setPlaybackSpeed(float playbackSpeed) { - handler.obtainMessage(MSG_SET_PLAYBACK_SPEED, playbackSpeed).sendToTarget(); + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + handler.obtainMessage(MSG_SET_PLAYBACK_PARAMETERS, playbackParameters).sendToTarget(); } public void setSeekParameters(SeekParameters seekParameters) { @@ -403,7 +405,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } public Looper getPlaybackLooper() { - return internalPlaybackThread.getLooper(); + return playbackLooper; } // Playlist.PlaylistInfoRefreshListener implementation. @@ -432,11 +434,11 @@ import java.util.concurrent.atomic.AtomicBoolean; handler.sendEmptyMessage(MSG_TRACK_SELECTION_INVALIDATED); } - // DefaultMediaClock.PlaybackSpeedListener implementation. + // DefaultMediaClock.PlaybackParametersListener implementation. @Override - public void onPlaybackSpeedChanged(float playbackSpeed) { - sendPlaybackSpeedChangedInternal(playbackSpeed, /* acknowledgeCommand= */ false); + public void onPlaybackParametersChanged(PlaybackParameters newPlaybackParameters) { + sendPlaybackParametersChangedInternal(newPlaybackParameters, /* acknowledgeCommand= */ false); } // Handler.Callback implementation. @@ -467,8 +469,8 @@ import java.util.concurrent.atomic.AtomicBoolean; case MSG_SEEK_TO: seekToInternal((SeekPosition) msg.obj); break; - case MSG_SET_PLAYBACK_SPEED: - setPlaybackSpeedInternal((Float) msg.obj); + case MSG_SET_PLAYBACK_PARAMETERS: + setPlaybackParametersInternal((PlaybackParameters) msg.obj); break; case MSG_SET_SEEK_PARAMETERS: setSeekParametersInternal((SeekParameters) msg.obj); @@ -489,8 +491,9 @@ import java.util.concurrent.atomic.AtomicBoolean; case MSG_TRACK_SELECTION_INVALIDATED: reselectTracksInternal(); break; - case MSG_PLAYBACK_SPEED_CHANGED_INTERNAL: - handlePlaybackSpeed((Float) msg.obj, /* acknowledgeCommand= */ msg.arg1 != 0); + case MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL: + handlePlaybackParameters( + (PlaybackParameters) msg.obj, /* acknowledgeCommand= */ msg.arg1 != 0); break; case MSG_SEND_MESSAGE: sendMessageInternal((PlayerMessage) msg.obj); @@ -733,11 +736,13 @@ import java.util.concurrent.atomic.AtomicBoolean; private void setPauseAtEndOfWindowInternal(boolean pauseAtEndOfWindow) throws ExoPlaybackException { this.pauseAtEndOfWindow = pauseAtEndOfWindow; - if (queue.getReadingPeriod() != queue.getPlayingPeriod()) { - seekToCurrentPosition(/* sendDiscontinuity= */ true); - } resetPendingPauseAtEndOfPeriod(); - handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); + if (pendingPauseAtEndOfPeriod && queue.getReadingPeriod() != queue.getPlayingPeriod()) { + // When pausing is required, we need to set the streams of the playing period final. If we + // already started reading the next period, we need to flush the renderers. + seekToCurrentPosition(/* sendDiscontinuity= */ true); + handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); + } } private void setOffloadSchedulingEnabledInternal(boolean offloadSchedulingEnabled) { @@ -1182,9 +1187,10 @@ import java.util.concurrent.atomic.AtomicBoolean; notifyTrackSelectionDiscontinuity(); } - private void setPlaybackSpeedInternal(float playbackSpeed) { - mediaClock.setPlaybackSpeed(playbackSpeed); - sendPlaybackSpeedChangedInternal(mediaClock.getPlaybackSpeed(), /* acknowledgeCommand= */ true); + private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) { + mediaClock.setPlaybackParameters(playbackParameters); + sendPlaybackParametersChangedInternal( + mediaClock.getPlaybackParameters(), /* acknowledgeCommand= */ true); } private void setSeekParametersInternal(SeekParameters seekParameters) { @@ -1301,7 +1307,7 @@ import java.util.concurrent.atomic.AtomicBoolean; mediaPeriodId, playbackInfo.playWhenReady, playbackInfo.playbackSuppressionReason, - playbackInfo.playbackSpeed, + playbackInfo.playbackParameters, startPositionUs, /* totalBufferedDurationUs= */ 0, startPositionUs, @@ -1361,7 +1367,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void sendMessageToTarget(PlayerMessage message) throws ExoPlaybackException { - if (message.getHandler().getLooper() == handler.getLooper()) { + if (message.getHandler().getLooper() == playbackLooper) { deliverMessage(message); if (playbackInfo.playbackState == Player.STATE_READY || playbackInfo.playbackState == Player.STATE_BUFFERING) { @@ -1506,7 +1512,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void reselectTracksInternal() throws ExoPlaybackException { - float playbackSpeed = mediaClock.getPlaybackSpeed(); + float playbackSpeed = mediaClock.getPlaybackParameters().speed; // Reselect tracks on each period in turn, until the selection changes. MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); @@ -1624,7 +1630,7 @@ import java.util.concurrent.atomic.AtomicBoolean; boolean bufferedToEnd = loadingHolder.isFullyBuffered() && loadingHolder.info.isFinal; return bufferedToEnd || loadControl.shouldStartPlayback( - getTotalBufferedDurationUs(), mediaClock.getPlaybackSpeed(), rebuffering); + getTotalBufferedDurationUs(), mediaClock.getPlaybackParameters().speed, rebuffering); } private boolean isTimelineReady() { @@ -1960,7 +1966,8 @@ import java.util.concurrent.atomic.AtomicBoolean; return; } MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); - loadingPeriodHolder.handlePrepared(mediaClock.getPlaybackSpeed(), playbackInfo.timeline); + loadingPeriodHolder.handlePrepared( + mediaClock.getPlaybackParameters().speed, playbackInfo.timeline); updateLoadControlTrackSelection( loadingPeriodHolder.getTrackGroups(), loadingPeriodHolder.getTrackSelectorResult()); if (loadingPeriodHolder == queue.getPlayingPeriod()) { @@ -1985,14 +1992,15 @@ import java.util.concurrent.atomic.AtomicBoolean; maybeContinueLoading(); } - private void handlePlaybackSpeed(float playbackSpeed, boolean acknowledgeCommand) + private void handlePlaybackParameters( + PlaybackParameters playbackParameters, boolean acknowledgeCommand) throws ExoPlaybackException { playbackInfoUpdate.incrementPendingOperationAcks(acknowledgeCommand ? 1 : 0); - playbackInfo = playbackInfo.copyWithPlaybackSpeed(playbackSpeed); - updateTrackSelectionPlaybackSpeed(playbackSpeed); + playbackInfo = playbackInfo.copyWithPlaybackParameters(playbackParameters); + updateTrackSelectionPlaybackSpeed(playbackParameters.speed); for (Renderer renderer : renderers) { if (renderer != null) { - renderer.setOperatingRate(playbackSpeed); + renderer.setOperatingRate(playbackParameters.speed); } } } @@ -2018,7 +2026,7 @@ import java.util.concurrent.atomic.AtomicBoolean; : loadingPeriodHolder.toPeriodTime(rendererPositionUs) - loadingPeriodHolder.info.startPositionUs; return loadControl.shouldContinueLoading( - playbackPositionUs, bufferedDurationUs, mediaClock.getPlaybackSpeed()); + playbackPositionUs, bufferedDurationUs, mediaClock.getPlaybackParameters().speed); } private boolean isLoadingPossible() { @@ -2194,10 +2202,14 @@ import java.util.concurrent.atomic.AtomicBoolean; loadControl.onTracksSelected(renderers, trackGroups, trackSelectorResult.selections); } - private void sendPlaybackSpeedChangedInternal(float playbackSpeed, boolean acknowledgeCommand) { + private void sendPlaybackParametersChangedInternal( + PlaybackParameters playbackParameters, boolean acknowledgeCommand) { handler .obtainMessage( - MSG_PLAYBACK_SPEED_CHANGED_INTERNAL, acknowledgeCommand ? 1 : 0, 0, playbackSpeed) + MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL, + acknowledgeCommand ? 1 : 0, + 0, + playbackParameters) .sendToTarget(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java index 57295c54fc..9fb6563005 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java @@ -63,8 +63,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; public final boolean playWhenReady; /** Reason why playback is suppressed even though {@link #playWhenReady} is {@code true}. */ @PlaybackSuppressionReason public final int playbackSuppressionReason; - /** The playback speed. */ - public final float playbackSpeed; + /** The playback parameters. */ + public final PlaybackParameters playbackParameters; /** Whether offload scheduling is enabled for the main player loop. */ public final boolean offloadSchedulingEnabled; @@ -105,7 +105,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; PLACEHOLDER_MEDIA_PERIOD_ID, /* playWhenReady= */ false, Player.PLAYBACK_SUPPRESSION_REASON_NONE, - Player.DEFAULT_PLAYBACK_SPEED, + PlaybackParameters.DEFAULT, /* bufferedPositionUs= */ 0, /* totalBufferedDurationUs= */ 0, /* positionUs= */ 0, @@ -119,10 +119,14 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; * @param periodId See {@link #periodId}. * @param requestedContentPositionUs See {@link #requestedContentPositionUs}. * @param playbackState See {@link #playbackState}. + * @param playbackError See {@link #playbackError}. * @param isLoading See {@link #isLoading}. * @param trackGroups See {@link #trackGroups}. * @param trackSelectorResult See {@link #trackSelectorResult}. * @param loadingMediaPeriodId See {@link #loadingMediaPeriodId}. + * @param playWhenReady See {@link #playWhenReady}. + * @param playbackSuppressionReason See {@link #playbackSuppressionReason}. + * @param playbackParameters See {@link #playbackParameters}. * @param bufferedPositionUs See {@link #bufferedPositionUs}. * @param totalBufferedDurationUs See {@link #totalBufferedDurationUs}. * @param positionUs See {@link #positionUs}. @@ -140,7 +144,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; MediaPeriodId loadingMediaPeriodId, boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason, - float playbackSpeed, + PlaybackParameters playbackParameters, long bufferedPositionUs, long totalBufferedDurationUs, long positionUs, @@ -156,7 +160,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; this.loadingMediaPeriodId = loadingMediaPeriodId; this.playWhenReady = playWhenReady; this.playbackSuppressionReason = playbackSuppressionReason; - this.playbackSpeed = playbackSpeed; + this.playbackParameters = playbackParameters; this.bufferedPositionUs = bufferedPositionUs; this.totalBufferedDurationUs = totalBufferedDurationUs; this.positionUs = positionUs; @@ -201,7 +205,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; loadingMediaPeriodId, playWhenReady, playbackSuppressionReason, - playbackSpeed, + playbackParameters, bufferedPositionUs, totalBufferedDurationUs, positionUs, @@ -228,7 +232,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; loadingMediaPeriodId, playWhenReady, playbackSuppressionReason, - playbackSpeed, + playbackParameters, bufferedPositionUs, totalBufferedDurationUs, positionUs, @@ -255,7 +259,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; loadingMediaPeriodId, playWhenReady, playbackSuppressionReason, - playbackSpeed, + playbackParameters, bufferedPositionUs, totalBufferedDurationUs, positionUs, @@ -282,7 +286,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; loadingMediaPeriodId, playWhenReady, playbackSuppressionReason, - playbackSpeed, + playbackParameters, bufferedPositionUs, totalBufferedDurationUs, positionUs, @@ -309,7 +313,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; loadingMediaPeriodId, playWhenReady, playbackSuppressionReason, - playbackSpeed, + playbackParameters, bufferedPositionUs, totalBufferedDurationUs, positionUs, @@ -336,7 +340,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; loadingMediaPeriodId, playWhenReady, playbackSuppressionReason, - playbackSpeed, + playbackParameters, bufferedPositionUs, totalBufferedDurationUs, positionUs, @@ -367,7 +371,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; loadingMediaPeriodId, playWhenReady, playbackSuppressionReason, - playbackSpeed, + playbackParameters, bufferedPositionUs, totalBufferedDurationUs, positionUs, @@ -375,13 +379,13 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; } /** - * Copies playback info with new playback speed. + * Copies playback info with new playback parameters. * - * @param playbackSpeed New playback speed. See {@link #playbackSpeed}. - * @return Copied playback info with new playback speed. + * @param playbackParameters New playback parameters. See {@link #playbackParameters}. + * @return Copied playback info with new playback parameters. */ @CheckResult - public PlaybackInfo copyWithPlaybackSpeed(float playbackSpeed) { + public PlaybackInfo copyWithPlaybackParameters(PlaybackParameters playbackParameters) { return new PlaybackInfo( timeline, periodId, @@ -394,7 +398,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; loadingMediaPeriodId, playWhenReady, playbackSuppressionReason, - playbackSpeed, + playbackParameters, bufferedPositionUs, totalBufferedDurationUs, positionUs, @@ -422,7 +426,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; loadingMediaPeriodId, playWhenReady, playbackSuppressionReason, - playbackSpeed, + playbackParameters, bufferedPositionUs, totalBufferedDurationUs, positionUs, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java index afa0a7ebc4..7dcd6f80aa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java @@ -17,13 +17,9 @@ package com.google.android.exoplayer2; import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; -/** - * @deprecated Use {@link Player#setPlaybackSpeed(float)} and {@link - * Player.AudioComponent#setSkipSilenceEnabled(boolean)} instead. - */ -@SuppressWarnings("deprecation") -@Deprecated +/** Parameters that apply to playback, including speed setting. */ public final class PlaybackParameters { /** The default playback parameters: real-time playback with no silence skipping. */ @@ -32,16 +28,34 @@ public final class PlaybackParameters { /** The factor by which playback will be sped up. */ public final float speed; + /** The factor by which pitch will be shifted. */ + public final float pitch; + private final int scaledUsPerMs; /** - * Creates new playback parameters that set the playback speed. + * Creates new playback parameters that set the playback speed. The pitch of audio will not be + * adjusted, so the effect is to time-stretch the audio. * * @param speed The factor by which playback will be sped up. Must be greater than zero. */ public PlaybackParameters(float speed) { + this(speed, /* pitch= */ 1f); + } + + /** + * Creates new playback parameters that set the playback speed/pitch. + * + * @param speed The factor by which playback will be sped up. Must be greater than zero. + * @param pitch The factor by which the pitch of audio will be adjusted. Must be greater than + * zero. Useful values are {@code 1} (to time-stretch audio) and the same value as passed in + * as the {@code speed} (to resample audio, which is useful for slow-motion videos). + */ + public PlaybackParameters(float speed, float pitch) { Assertions.checkArgument(speed > 0); + Assertions.checkArgument(pitch > 0); this.speed = speed; + this.pitch = pitch; scaledUsPerMs = Math.round(speed * 1000f); } @@ -65,11 +79,19 @@ public final class PlaybackParameters { return false; } PlaybackParameters other = (PlaybackParameters) obj; - return this.speed == other.speed; + return this.speed == other.speed && this.pitch == other.pitch; } @Override public int hashCode() { - return Float.floatToRawIntBits(speed); + int result = 17; + result = 31 * result + Float.floatToRawIntBits(speed); + result = 31 * result + Float.floatToRawIntBits(pitch); + return result; + } + + @Override + public String toString() { + return Util.formatInvariant("PlaybackParameters(speed=%.2f, pitch=%.2f)", speed, pitch); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 9344345375..490022de93 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -583,21 +583,15 @@ public interface Player { default void onPositionDiscontinuity(@DiscontinuityReason int reason) {} /** - * @deprecated Use {@link #onPlaybackSpeedChanged(float)} and {@link - * AudioListener#onSkipSilenceEnabledChanged(boolean)} instead. + * Called when the current playback parameters change. The playback parameters may change due to + * a call to {@link #setPlaybackParameters(PlaybackParameters)}, or the player itself may change + * them (for example, if audio playback switches to passthrough or offload mode, where speed + * adjustment is no longer possible). + * + * @param playbackParameters The playback parameters. */ - @SuppressWarnings("deprecation") - @Deprecated default void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {} - /** - * Called when the current playback speed changes. The normal playback speed is 1. The speed may - * change due to a call to {@link #setPlaybackSpeed(float)}, or the player itself may change it - * (for example, if audio playback switches to passthrough mode, where speed adjustment is no - * longer possible). - */ - default void onPlaybackSpeedChanged(float playbackSpeed) {} - /** * @deprecated Seeks are processed without delay. Listen to {@link * #onPositionDiscontinuity(int)} with reason {@link #DISCONTINUITY_REASON_SEEK} instead. @@ -810,9 +804,6 @@ public interface Player { */ int MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED = 3; - /** The default playback speed. */ - float DEFAULT_PLAYBACK_SPEED = 1.0f; - /** Returns the component of this player for audio output, or null if audio is not supported. */ @Nullable AudioComponent getAudioComponent(); @@ -1161,39 +1152,24 @@ public interface Player { void next(); /** - * @deprecated Use {@link #setPlaybackSpeed(float)} or {@link - * AudioComponent#setSkipSilenceEnabled(boolean)} instead. + * Attempts to set the playback parameters. Passing {@code null} sets the parameters to the + * default, {@link PlaybackParameters#DEFAULT}, which means there is no speed or pitch adjustment. + * + *

Playback parameters changes may cause the player to buffer. {@link + * EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever the + * currently active playback parameters change. + * + * @param playbackParameters The playback parameters, or {@code null} to use the defaults. */ - @SuppressWarnings("deprecation") - @Deprecated void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters); /** - * @deprecated Use {@link #getPlaybackSpeed()} or {@link AudioComponent#getSkipSilenceEnabled()} - * instead. + * Returns the currently active playback parameters. + * + * @see EventListener#onPlaybackParametersChanged(PlaybackParameters) */ - @SuppressWarnings("deprecation") - @Deprecated PlaybackParameters getPlaybackParameters(); - /** - * Attempts to set the playback speed. - * - *

Playback speed changes may cause the player to buffer. {@link - * EventListener#onPlaybackSpeedChanged(float)} will be called whenever the currently active - * playback speed change. - * - * @param playbackSpeed The playback speed. - */ - void setPlaybackSpeed(float playbackSpeed); - - /** - * Returns the currently active playback speed. - * - * @see EventListener#onPlaybackSpeedChanged(float) - */ - float getPlaybackSpeed(); - /** * Stops playback without resetting the player. Use {@link #pause()} rather than this method if * the intention is to pause playback. 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 00c1e0bcc5..a43973b31c 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 @@ -1030,18 +1030,23 @@ public class SimpleExoPlayer extends BasePlayer this.priorityTaskManager = priorityTaskManager; } - /** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */ + /** + * Sets the {@link PlaybackParams} governing audio playback. + * + * @param params The {@link PlaybackParams}, or null to clear any previously set parameters. + * @deprecated Use {@link #setPlaybackParameters(PlaybackParameters)}. + */ @Deprecated @RequiresApi(23) public void setPlaybackParams(@Nullable PlaybackParams params) { - float playbackSpeed; + PlaybackParameters playbackParameters; if (params != null) { params.allowDefaults(); - playbackSpeed = params.getSpeed(); + playbackParameters = new PlaybackParameters(params.getSpeed(), params.getPitch()); } else { - playbackSpeed = 1.0f; + playbackParameters = null; } - setPlaybackSpeed(playbackSpeed); + setPlaybackParameters(playbackParameters); } /** Returns the video format currently being played, or null if no video is being played. */ @@ -1623,39 +1628,18 @@ public class SimpleExoPlayer extends BasePlayer player.seekTo(windowIndex, positionMs); } - /** - * @deprecated Use {@link #setPlaybackSpeed(float)} and {@link #setSkipSilenceEnabled(boolean)} - * instead. - */ - @SuppressWarnings("deprecation") - @Deprecated @Override public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { verifyApplicationThread(); player.setPlaybackParameters(playbackParameters); } - /** @deprecated Use {@link #getPlaybackSpeed()} and {@link #getSkipSilenceEnabled()} instead. */ - @SuppressWarnings("deprecation") - @Deprecated @Override public PlaybackParameters getPlaybackParameters() { verifyApplicationThread(); return player.getPlaybackParameters(); } - @Override - public void setPlaybackSpeed(float playbackSpeed) { - verifyApplicationThread(); - player.setPlaybackSpeed(playbackSpeed); - } - - @Override - public float getPlaybackSpeed() { - verifyApplicationThread(); - return player.getPlaybackSpeed(); - } - @Override public void setSeekParameters(@Nullable SeekParameters seekParameters) { verifyApplicationThread(); @@ -2231,6 +2215,13 @@ public class SimpleExoPlayer extends BasePlayer } } + @Override + public void onAudioPositionAdvancing(long playoutStartSystemTimeMs) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { + audioDebugListener.onAudioPositionAdvancing(playoutStartSystemTimeMs); + } + } + @Override public void onAudioUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { 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 7c170742d7..35f3099dc9 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 @@ -205,6 +205,14 @@ public class AnalyticsCollector } } + @Override + public final void onAudioPositionAdvancing(long playoutStartSystemTimeMs) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onAudioPositionAdvancing(eventTime, playoutStartSystemTimeMs); + } + } + @Override public final void onAudioUnderrun( int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { @@ -544,12 +552,6 @@ public class AnalyticsCollector } } - /** - * @deprecated Use {@link #onPlaybackSpeedChanged(float)} and {@link - * #onSkipSilenceEnabledChanged(boolean)} instead. - */ - @SuppressWarnings("deprecation") - @Deprecated @Override public final void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); @@ -558,14 +560,6 @@ public class AnalyticsCollector } } - @Override - public void onPlaybackSpeedChanged(float playbackSpeed) { - EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - for (AnalyticsListener listener : listeners) { - listener.onPlaybackSpeedChanged(eventTime, playbackSpeed); - } - } - @SuppressWarnings("deprecation") @Override public final void onSeekProcessed() { 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 f01d11ec25..2e26019541 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 @@ -279,21 +279,13 @@ public interface AnalyticsListener { default void onSeekProcessed(EventTime eventTime) {} /** - * @deprecated Use {@link #onPlaybackSpeedChanged(EventTime, float)} and {@link - * #onSkipSilenceEnabledChanged(EventTime, boolean)} instead. - */ - @SuppressWarnings("deprecation") - @Deprecated - default void onPlaybackParametersChanged( - EventTime eventTime, PlaybackParameters playbackParameters) {} - - /** - * Called when the playback speed changes. + * Called when the playback parameters changed. * * @param eventTime The event time. - * @param playbackSpeed The playback speed. + * @param playbackParameters The new playback parameters. */ - default void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) {} + default void onPlaybackParametersChanged( + EventTime eventTime, PlaybackParameters playbackParameters) {} /** * Called when the repeat mode changed. @@ -479,6 +471,16 @@ public interface AnalyticsListener { */ default void onAudioInputFormatChanged(EventTime eventTime, Format format) {} + /** + * Called when the audio position has increased for the first time since the last pause or + * position reset. + * + * @param eventTime The event time. + * @param playoutStartSystemTimeMs The approximate derived {@link System#currentTimeMillis()} at + * which playout started. + */ + default void onAudioPositionAdvancing(EventTime eventTime, long playoutStartSystemTimeMs) {} + /** * Called when an audio underrun occurs. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java index 1efb072ef0..ab137f98e1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java @@ -22,6 +22,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; @@ -334,8 +335,9 @@ public final class PlaybackStatsListener } @Override - public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) { - this.playbackSpeed = playbackSpeed; + public void onPlaybackParametersChanged( + EventTime eventTime, PlaybackParameters playbackParameters) { + playbackSpeed = playbackParameters.speed; maybeAddSession(eventTime); for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) { tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed); 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 c366f27f81..f921141f24 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 @@ -65,6 +65,15 @@ public interface AudioRendererEventListener { */ default void onAudioInputFormatChanged(Format format) {} + /** + * Called when the audio position has increased for the first time since the last pause or + * position reset. + * + * @param playoutStartSystemTimeMs The approximate derived {@link System#currentTimeMillis()} at + * which playout started. + */ + default void onAudioPositionAdvancing(long playoutStartSystemTimeMs) {} + /** * Called when an audio underrun occurs. * @@ -89,7 +98,7 @@ public interface AudioRendererEventListener { */ default void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) {} - /** Dispatches events to a {@link AudioRendererEventListener}. */ + /** Dispatches events to an {@link AudioRendererEventListener}. */ final class EventDispatcher { @Nullable private final Handler handler; @@ -106,20 +115,16 @@ public interface AudioRendererEventListener { this.listener = listener; } - /** - * Invokes {@link AudioRendererEventListener#onAudioEnabled(DecoderCounters)}. - */ - public void enabled(final DecoderCounters decoderCounters) { + /** Invokes {@link AudioRendererEventListener#onAudioEnabled(DecoderCounters)}. */ + public void enabled(DecoderCounters decoderCounters) { if (handler != null) { handler.post(() -> castNonNull(listener).onAudioEnabled(decoderCounters)); } } - /** - * Invokes {@link AudioRendererEventListener#onAudioDecoderInitialized(String, long, long)}. - */ - public void decoderInitialized(final String decoderName, - final long initializedTimestampMs, final long initializationDurationMs) { + /** Invokes {@link AudioRendererEventListener#onAudioDecoderInitialized(String, long, long)}. */ + public void decoderInitialized( + String decoderName, long initializedTimestampMs, long initializationDurationMs) { if (handler != null) { handler.post( () -> @@ -129,18 +134,23 @@ public interface AudioRendererEventListener { } } - /** - * Invokes {@link AudioRendererEventListener#onAudioInputFormatChanged(Format)}. - */ - public void inputFormatChanged(final Format format) { + /** Invokes {@link AudioRendererEventListener#onAudioInputFormatChanged(Format)}. */ + public void inputFormatChanged(Format format) { if (handler != null) { handler.post(() -> castNonNull(listener).onAudioInputFormatChanged(format)); } } + /** Invokes {@link AudioRendererEventListener#onAudioPositionAdvancing(long)}. */ + public void positionAdvancing(long playoutStartSystemTimeMs) { + if (handler != null) { + handler.post( + () -> castNonNull(listener).onAudioPositionAdvancing(playoutStartSystemTimeMs)); + } + } + /** Invokes {@link AudioRendererEventListener#onAudioUnderrun(int, long, long)}. */ - public void underrun( - final int bufferSize, final long bufferSizeMs, final long elapsedSinceLastFeedMs) { + public void underrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { if (handler != null) { handler.post( () -> @@ -149,10 +159,8 @@ public interface AudioRendererEventListener { } } - /** - * Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}. - */ - public void disabled(final DecoderCounters counters) { + /** Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}. */ + public void disabled(DecoderCounters counters) { counters.ensureUpdated(); if (handler != null) { handler.post( @@ -163,17 +171,15 @@ public interface AudioRendererEventListener { } } - /** - * Invokes {@link AudioRendererEventListener#onAudioSessionId(int)}. - */ - public void audioSessionId(final int audioSessionId) { + /** Invokes {@link AudioRendererEventListener#onAudioSessionId(int)}. */ + public void audioSessionId(int audioSessionId) { if (handler != null) { handler.post(() -> castNonNull(listener).onAudioSessionId(audioSessionId)); } } /** Invokes {@link AudioRendererEventListener#onSkipSilenceEnabledChanged(boolean)}. */ - public void skipSilenceEnabledChanged(final boolean skipSilenceEnabled) { + public void skipSilenceEnabledChanged(boolean skipSilenceEnabled) { if (handler != null) { handler.post(() -> castNonNull(listener).onSkipSilenceEnabledChanged(skipSilenceEnabled)); } 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 b0f76c0afb..b7d375fd9d 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 @@ -20,6 +20,7 @@ import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -72,10 +73,19 @@ public interface AudioSink { */ void onPositionDiscontinuity(); + /** + * Called when the audio sink's position has increased for the first time since it was last + * paused or flushed. + * + * @param playoutStartSystemTimeMs The approximate derived {@link System#currentTimeMillis()} at + * which playout started. Only valid if the audio track has not underrun. + */ + default void onPositionAdvancing(long playoutStartSystemTimeMs) {} + /** * Called when the audio sink runs out of data. - *

- * An audio sink implementation may never call this method (for example, if audio data is + * + *

An audio sink implementation may never call this method (for example, if audio data is * consumed in batches rather than based on the sink's own clock). * * @param bufferSize The size of the sink's buffer, in bytes. @@ -297,16 +307,21 @@ public interface AudioSink { */ boolean hasPendingData(); - /** Sets the playback speed. */ - void setPlaybackSpeed(float playbackSpeed); + /** + * Attempts to set the playback parameters. The audio sink may override these parameters if they + * are not supported. + * + * @param playbackParameters The new playback parameters to attempt to set. + */ + void setPlaybackParameters(PlaybackParameters playbackParameters); - /** Gets the playback speed. */ - float getPlaybackSpeed(); + /** Returns the active {@link PlaybackParameters}. */ + PlaybackParameters getPlaybackParameters(); /** Sets whether silences should be skipped in the audio stream. */ void setSkipSilenceEnabled(boolean skipSilenceEnabled); - /** Gets whether silences are skipped in the audio stream. */ + /** Returns whether silences are skipped in the audio stream. */ boolean getSkipSilenceEnabled(); /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java index c1d8df5c75..540ee098ee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java @@ -48,6 +48,15 @@ import java.lang.reflect.Method; /** Listener for position tracker events. */ public interface Listener { + /** + * Called when the position tracker's position has increased for the first time since it was + * last paused or reset. + * + * @param playoutStartSystemTimeMs The approximate derived {@link System#currentTimeMillis()} at + * which playout started. + */ + void onPositionAdvancing(long playoutStartSystemTimeMs); + /** * Called when the frame position is too far from the expected frame position. * @@ -145,6 +154,7 @@ import java.lang.reflect.Method; private boolean needsPassthroughWorkarounds; private long bufferSizeUs; private float audioTrackPlaybackSpeed; + private boolean notifiedPositionIncreasing; private long smoothedPlayheadOffsetUs; private long lastPlayheadSampleTimeUs; @@ -287,9 +297,21 @@ import java.lang.reflect.Method; positionUs /= 1000; } + if (!notifiedPositionIncreasing && positionUs > lastPositionUs) { + notifiedPositionIncreasing = true; + long mediaDurationSinceLastPositionUs = C.usToMs(positionUs - lastPositionUs); + long playoutDurationSinceLastPositionUs = + Util.getPlayoutDurationForMediaDuration( + mediaDurationSinceLastPositionUs, audioTrackPlaybackSpeed); + long playoutStartSystemTimeMs = + System.currentTimeMillis() - C.usToMs(playoutDurationSinceLastPositionUs); + listener.onPositionAdvancing(playoutStartSystemTimeMs); + } + lastSystemTimeUs = systemTimeUs; lastPositionUs = positionUs; lastSampleUsedGetTimestampMode = useGetTimestampMode; + return positionUs; } @@ -512,6 +534,7 @@ import java.lang.reflect.Method; lastPlayheadSampleTimeUs = 0; lastSystemTimeUs = 0; previousModeSystemTimeUs = 0; + notifiedPositionIncreasing = false; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java index bc8237c911..1c1e593e22 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; @@ -498,13 +499,13 @@ public abstract class DecoderAudioRenderer< } @Override - public void setPlaybackSpeed(float playbackSpeed) { - audioSink.setPlaybackSpeed(playbackSpeed); + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + audioSink.setPlaybackParameters(playbackParameters); } @Override - public float getPlaybackSpeed() { - return audioSink.getPlaybackSpeed(); + public PlaybackParameters getPlaybackParameters() { + return audioSink.getPlaybackParameters(); } @Override @@ -708,6 +709,11 @@ public abstract class DecoderAudioRenderer< DecoderAudioRenderer.this.onPositionDiscontinuity(); } + @Override + public void onPositionAdvancing(long playoutStartSystemTimeMs) { + eventDispatcher.positionAdvancing(playoutStartSystemTimeMs); + } + @Override public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { eventDispatcher.underrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); 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 2da648b303..1e04b1e8d7 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 @@ -32,6 +32,7 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.audio.AudioProcessor.UnhandledAudioFormatException; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; @@ -92,14 +93,14 @@ public final class DefaultAudioSink implements AudioSink { AudioProcessor[] getAudioProcessors(); /** - * Configures audio processors to apply the specified playback speed immediately, returning the - * new playback speed, which may differ from the speed passed in. Only called when processors - * have no input pending. + * Configures audio processors to apply the specified playback parameters immediately, returning + * the new playback parameters, which may differ from those passed in. Only called when + * processors have no input pending. * - * @param playbackSpeed The playback speed to try to apply. - * @return The playback speed that was actually applied. + * @param playbackParameters The playback parameters to try to apply. + * @return The playback parameters that were actually applied. */ - float applyPlaybackSpeed(float playbackSpeed); + PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters); /** * Configures audio processors to apply whether to skip silences immediately, returning the new @@ -170,8 +171,10 @@ public final class DefaultAudioSink implements AudioSink { } @Override - public float applyPlaybackSpeed(float playbackSpeed) { - return sonicAudioProcessor.setSpeed(playbackSpeed); + public PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters) { + float speed = sonicAudioProcessor.setSpeed(playbackParameters.speed); + float pitch = sonicAudioProcessor.setPitch(playbackParameters.pitch); + return new PlaybackParameters(speed, pitch); } @Override @@ -197,6 +200,10 @@ public final class DefaultAudioSink implements AudioSink { public static final float MIN_PLAYBACK_SPEED = 0.1f; /** The maximum allowed playback speed. Higher values will be constrained to fall in range. */ public static final float MAX_PLAYBACK_SPEED = 8f; + /** The minimum allowed pitch factor. Lower values will be constrained to fall in range. */ + public static final float MIN_PITCH = 0.1f; + /** The maximum allowed pitch factor. Higher values will be constrained to fall in range. */ + public static final float MAX_PITCH = 8f; /** The default skip silence flag. */ private static final boolean DEFAULT_SKIP_SILENCE = false; @@ -296,7 +303,7 @@ public final class DefaultAudioSink implements AudioSink { private AudioAttributes audioAttributes; @Nullable private MediaPositionParameters afterDrainParameters; private MediaPositionParameters mediaPositionParameters; - private float audioTrackPlaybackSpeed; + private PlaybackParameters audioTrackPlaybackParameters; @Nullable private ByteBuffer avSyncHeader; private int bytesUntilNextAvSync; @@ -418,11 +425,11 @@ public final class DefaultAudioSink implements AudioSink { auxEffectInfo = new AuxEffectInfo(AuxEffectInfo.NO_AUX_EFFECT_ID, 0f); mediaPositionParameters = new MediaPositionParameters( - DEFAULT_PLAYBACK_SPEED, + PlaybackParameters.DEFAULT, DEFAULT_SKIP_SILENCE, /* mediaTimeUs= */ 0, /* audioTrackPositionUs= */ 0); - audioTrackPlaybackSpeed = 1f; + audioTrackPlaybackParameters = PlaybackParameters.DEFAULT; drainingAudioProcessorIndex = C.INDEX_UNSET; activeAudioProcessors = new AudioProcessor[0]; outputBuffers = new ByteBuffer[0]; @@ -707,7 +714,7 @@ public final class DefaultAudioSink implements AudioSink { } } // Re-apply playback parameters. - applyAudioProcessorPlaybackSpeedAndSkipSilence(presentationTimeUs); + applyAudioProcessorPlaybackParametersAndSkipSilence(presentationTimeUs); } if (!isAudioTrackInitialized()) { @@ -720,9 +727,9 @@ public final class DefaultAudioSink implements AudioSink { startMediaTimeUsNeedsInit = false; if (enableAudioTrackPlaybackParams && Util.SDK_INT >= 23) { - setAudioTrackPlaybackSpeedV23(audioTrackPlaybackSpeed); + setAudioTrackPlaybackParametersV23(audioTrackPlaybackParameters); } - applyAudioProcessorPlaybackSpeedAndSkipSilence(presentationTimeUs); + applyAudioProcessorPlaybackParametersAndSkipSilence(presentationTimeUs); if (playing) { play(); @@ -758,7 +765,7 @@ public final class DefaultAudioSink implements AudioSink { // Don't process any more input until draining completes. return false; } - applyAudioProcessorPlaybackSpeedAndSkipSilence(presentationTimeUs); + applyAudioProcessorPlaybackParametersAndSkipSilence(presentationTimeUs); afterDrainParameters = null; } @@ -789,7 +796,7 @@ public final class DefaultAudioSink implements AudioSink { startMediaTimeUs += adjustmentUs; startMediaTimeUsNeedsSync = false; // Re-apply playback parameters because the startMediaTimeUs changed. - applyAudioProcessorPlaybackSpeedAndSkipSilence(presentationTimeUs); + applyAudioProcessorPlaybackParametersAndSkipSilence(presentationTimeUs); if (listener != null && adjustmentUs != 0) { listener.onPositionDiscontinuity(); } @@ -1011,26 +1018,30 @@ public final class DefaultAudioSink implements AudioSink { } @Override - public void setPlaybackSpeed(float playbackSpeed) { - playbackSpeed = Util.constrainValue(playbackSpeed, MIN_PLAYBACK_SPEED, MAX_PLAYBACK_SPEED); + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + playbackParameters = + new PlaybackParameters( + Util.constrainValue(playbackParameters.speed, MIN_PLAYBACK_SPEED, MAX_PLAYBACK_SPEED), + Util.constrainValue(playbackParameters.pitch, MIN_PITCH, MAX_PITCH)); if (enableAudioTrackPlaybackParams && Util.SDK_INT >= 23) { - setAudioTrackPlaybackSpeedV23(playbackSpeed); + setAudioTrackPlaybackParametersV23(playbackParameters); } else { - setAudioProcessorPlaybackSpeedAndSkipSilence(playbackSpeed, getSkipSilenceEnabled()); + setAudioProcessorPlaybackParametersAndSkipSilence( + playbackParameters, getSkipSilenceEnabled()); } } @Override - public float getPlaybackSpeed() { - // We use either audio processor speed adjustment or AudioTrack playback parameters, so one of - // the operands is always 1f. - return getAudioProcessorPlaybackSpeed() * audioTrackPlaybackSpeed; + public PlaybackParameters getPlaybackParameters() { + return enableAudioTrackPlaybackParams + ? audioTrackPlaybackParameters + : getAudioProcessorPlaybackParameters(); } @Override public void setSkipSilenceEnabled(boolean skipSilenceEnabled) { - setAudioProcessorPlaybackSpeedAndSkipSilence( - getAudioProcessorPlaybackSpeed(), skipSilenceEnabled); + setAudioProcessorPlaybackParametersAndSkipSilence( + getAudioProcessorPlaybackParameters(), skipSilenceEnabled); } @Override @@ -1212,7 +1223,7 @@ public final class DefaultAudioSink implements AudioSink { framesPerEncodedSample = 0; mediaPositionParameters = new MediaPositionParameters( - getAudioProcessorPlaybackSpeed(), + getAudioProcessorPlaybackParameters(), getSkipSilenceEnabled(), /* mediaTimeUs= */ 0, /* audioTrackPositionUs= */ 0); @@ -1249,12 +1260,13 @@ public final class DefaultAudioSink implements AudioSink { } @RequiresApi(23) - private void setAudioTrackPlaybackSpeedV23(float audioTrackPlaybackSpeed) { + private void setAudioTrackPlaybackParametersV23(PlaybackParameters audioTrackPlaybackParameters) { if (isAudioTrackInitialized()) { PlaybackParams playbackParams = new PlaybackParams() .allowDefaults() - .setSpeed(audioTrackPlaybackSpeed) + .setSpeed(audioTrackPlaybackParameters.speed) + .setPitch(audioTrackPlaybackParameters.pitch) .setAudioFallbackMode(PlaybackParams.AUDIO_FALLBACK_MODE_FAIL); try { audioTrack.setPlaybackParams(playbackParams); @@ -1262,20 +1274,22 @@ public final class DefaultAudioSink implements AudioSink { Log.w(TAG, "Failed to set playback params", e); } // Update the speed using the actual effective speed from the audio track. - audioTrackPlaybackSpeed = audioTrack.getPlaybackParams().getSpeed(); - audioTrackPositionTracker.setAudioTrackPlaybackSpeed(audioTrackPlaybackSpeed); + audioTrackPlaybackParameters = + new PlaybackParameters( + audioTrack.getPlaybackParams().getSpeed(), audioTrack.getPlaybackParams().getPitch()); + audioTrackPositionTracker.setAudioTrackPlaybackSpeed(audioTrackPlaybackParameters.speed); } - this.audioTrackPlaybackSpeed = audioTrackPlaybackSpeed; + this.audioTrackPlaybackParameters = audioTrackPlaybackParameters; } - private void setAudioProcessorPlaybackSpeedAndSkipSilence( - float playbackSpeed, boolean skipSilence) { + private void setAudioProcessorPlaybackParametersAndSkipSilence( + PlaybackParameters playbackParameters, boolean skipSilence) { MediaPositionParameters currentMediaPositionParameters = getMediaPositionParameters(); - if (playbackSpeed != currentMediaPositionParameters.playbackSpeed + if (!playbackParameters.equals(currentMediaPositionParameters.playbackParameters) || skipSilence != currentMediaPositionParameters.skipSilence) { MediaPositionParameters mediaPositionParameters = new MediaPositionParameters( - playbackSpeed, + playbackParameters, skipSilence, /* mediaTimeUs= */ C.TIME_UNSET, /* audioTrackPositionUs= */ C.TIME_UNSET); @@ -1291,8 +1305,8 @@ public final class DefaultAudioSink implements AudioSink { } } - private float getAudioProcessorPlaybackSpeed() { - return getMediaPositionParameters().playbackSpeed; + private PlaybackParameters getAudioProcessorPlaybackParameters() { + return getMediaPositionParameters().playbackParameters; } private MediaPositionParameters getMediaPositionParameters() { @@ -1304,18 +1318,18 @@ public final class DefaultAudioSink implements AudioSink { : mediaPositionParameters; } - private void applyAudioProcessorPlaybackSpeedAndSkipSilence(long presentationTimeUs) { - float playbackSpeed = + private void applyAudioProcessorPlaybackParametersAndSkipSilence(long presentationTimeUs) { + PlaybackParameters playbackParameters = configuration.canApplyPlaybackParameters - ? audioProcessorChain.applyPlaybackSpeed(getAudioProcessorPlaybackSpeed()) - : DEFAULT_PLAYBACK_SPEED; + ? audioProcessorChain.applyPlaybackParameters(getAudioProcessorPlaybackParameters()) + : PlaybackParameters.DEFAULT; boolean skipSilenceEnabled = configuration.canApplyPlaybackParameters ? audioProcessorChain.applySkipSilenceEnabled(getSkipSilenceEnabled()) : DEFAULT_SKIP_SILENCE; mediaPositionParametersCheckpoints.add( new MediaPositionParameters( - playbackSpeed, + playbackParameters, skipSilenceEnabled, /* mediaTimeUs= */ max(0, presentationTimeUs), /* audioTrackPositionUs= */ configuration.framesToDurationUs(getWrittenFrames()))); @@ -1340,7 +1354,7 @@ public final class DefaultAudioSink implements AudioSink { long playoutDurationSinceLastCheckpoint = positionUs - mediaPositionParameters.audioTrackPositionUs; - if (mediaPositionParameters.playbackSpeed != 1f) { + if (!mediaPositionParameters.playbackParameters.equals(PlaybackParameters.DEFAULT)) { if (mediaPositionParametersCheckpoints.isEmpty()) { playoutDurationSinceLastCheckpoint = audioProcessorChain.getMediaDuration(playoutDurationSinceLastCheckpoint); @@ -1348,7 +1362,8 @@ public final class DefaultAudioSink implements AudioSink { // Playing data at a previous playback speed, so fall back to multiplying by the speed. playoutDurationSinceLastCheckpoint = Util.getMediaDurationForPlayoutDuration( - playoutDurationSinceLastCheckpoint, mediaPositionParameters.playbackSpeed); + playoutDurationSinceLastCheckpoint, + mediaPositionParameters.playbackParameters.speed); } } return mediaPositionParameters.mediaTimeUs + playoutDurationSinceLastCheckpoint; @@ -1692,8 +1707,8 @@ public final class DefaultAudioSink implements AudioSink { /** Stores parameters used to calculate the current media position. */ private static final class MediaPositionParameters { - /** The playback speed. */ - public final float playbackSpeed; + /** The playback parameters. */ + public final PlaybackParameters playbackParameters; /** Whether to skip silences. */ public final boolean skipSilence; /** The media time from which the playback parameters apply, in microseconds. */ @@ -1702,8 +1717,11 @@ public final class DefaultAudioSink implements AudioSink { public final long audioTrackPositionUs; private MediaPositionParameters( - float playbackSpeed, boolean skipSilence, long mediaTimeUs, long audioTrackPositionUs) { - this.playbackSpeed = playbackSpeed; + PlaybackParameters playbackParameters, + boolean skipSilence, + long mediaTimeUs, + long audioTrackPositionUs) { + this.playbackParameters = playbackParameters; this.skipSilence = skipSilence; this.mediaTimeUs = mediaTimeUs; this.audioTrackPositionUs = audioTrackPositionUs; @@ -1776,6 +1794,13 @@ public final class DefaultAudioSink implements AudioSink { Log.w(TAG, "Ignoring impossibly large audio latency: " + latencyUs); } + @Override + public void onPositionAdvancing(long playoutStartSystemTimeMs) { + if (listener != null) { + listener.onPositionAdvancing(playoutStartSystemTimeMs); + } + } + @Override public void onUnderrun(int bufferSize, long bufferSizeMs) { if (listener != null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ForwardingAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ForwardingAudioSink.java index 3f755a7130..7460d12457 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ForwardingAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ForwardingAudioSink.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.audio; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; import java.nio.ByteBuffer; /** An overridable {@link AudioSink} implementation forwarding all methods to another sink. */ @@ -88,13 +89,13 @@ public class ForwardingAudioSink implements AudioSink { } @Override - public void setPlaybackSpeed(float playbackSpeed) { - sink.setPlaybackSpeed(playbackSpeed); + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + sink.setPlaybackParameters(playbackParameters); } @Override - public float getPlaybackSpeed() { - return sink.getPlaybackSpeed(); + public PlaybackParameters getPlaybackParameters() { + return sink.getPlaybackParameters(); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 91c0f946ce..2d034335c8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; @@ -545,13 +546,13 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - public void setPlaybackSpeed(float playbackSpeed) { - audioSink.setPlaybackSpeed(playbackSpeed); + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + audioSink.setPlaybackParameters(playbackParameters); } @Override - public float getPlaybackSpeed() { - return audioSink.getPlaybackSpeed(); + public PlaybackParameters getPlaybackParameters() { + return audioSink.getPlaybackParameters(); } @Override @@ -828,6 +829,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media MediaCodecAudioRenderer.this.onPositionDiscontinuity(); } + @Override + public void onPositionAdvancing(long playoutStartSystemTimeMs) { + eventDispatcher.positionAdvancing(playoutStartSystemTimeMs); + } + @Override public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { eventDispatcher.underrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java index a2cdaa8b74..ae65eacd13 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java @@ -37,6 +37,7 @@ import java.util.Arrays; private final int inputSampleRateHz; private final int channelCount; private final float speed; + private final float pitch; private final float rate; private final int minPeriod; private final int maxPeriod; @@ -63,12 +64,15 @@ import java.util.Arrays; * @param inputSampleRateHz The sample rate of input audio, in hertz. * @param channelCount The number of channels in the input audio. * @param speed The speedup factor for output audio. + * @param pitch The pitch factor for output audio. * @param outputSampleRateHz The sample rate for output audio, in hertz. */ - public Sonic(int inputSampleRateHz, int channelCount, float speed, int outputSampleRateHz) { + public Sonic( + int inputSampleRateHz, int channelCount, float speed, float pitch, int outputSampleRateHz) { this.inputSampleRateHz = inputSampleRateHz; this.channelCount = channelCount; this.speed = speed; + this.pitch = pitch; rate = (float) inputSampleRateHz / outputSampleRateHz; minPeriod = inputSampleRateHz / MAXIMUM_PITCH; maxPeriod = inputSampleRateHz / MINIMUM_PITCH; @@ -118,8 +122,10 @@ import java.util.Arrays; */ public void queueEndOfStream() { int remainingFrameCount = inputFrameCount; + float s = speed / pitch; + float r = rate * pitch; int expectedOutputFrames = - outputFrameCount + (int) ((remainingFrameCount / speed + pitchFrameCount) / rate + 0.5f); + outputFrameCount + (int) ((remainingFrameCount / s + pitchFrameCount) / r + 0.5f); // Add enough silence to flush both input and pitch buffers. inputBuffer = @@ -464,14 +470,16 @@ import java.util.Arrays; private void processStreamInput() { // Resample as many pitch periods as we have buffered on the input. int originalOutputFrameCount = outputFrameCount; - if (speed > 1.00001 || speed < 0.99999) { - changeSpeed(speed); + float s = speed / pitch; + float r = rate * pitch; + if (s > 1.00001 || s < 0.99999) { + changeSpeed(s); } else { copyToOutput(inputBuffer, 0, inputFrameCount); inputFrameCount = 0; } - if (rate != 1.0f) { - adjustRate(rate, originalOutputFrameCount); + if (r != 1.0f) { + adjustRate(r, originalOutputFrameCount); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java index d582461c81..5c3c1db0c7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java @@ -43,6 +43,7 @@ public final class SonicAudioProcessor implements AudioProcessor { private int pendingOutputSampleRate; private float speed; + private float pitch; private AudioFormat pendingInputAudioFormat; private AudioFormat pendingOutputAudioFormat; @@ -61,6 +62,7 @@ public final class SonicAudioProcessor implements AudioProcessor { /** Creates a new Sonic audio processor. */ public SonicAudioProcessor() { speed = 1f; + pitch = 1f; pendingInputAudioFormat = AudioFormat.NOT_SET; pendingOutputAudioFormat = AudioFormat.NOT_SET; inputAudioFormat = AudioFormat.NOT_SET; @@ -87,6 +89,22 @@ public final class SonicAudioProcessor implements AudioProcessor { return speed; } + /** + * Sets the playback pitch. This method may only be called after draining data through the + * processor. The value returned by {@link #isActive()} may change, and the processor must be + * {@link #flush() flushed} before queueing more data. + * + * @param pitch The requested new pitch. + * @return The actual new pitch. + */ + public float setPitch(float pitch) { + if (this.pitch != pitch) { + this.pitch = pitch; + pendingSonicRecreation = true; + } + return pitch; + } + /** * Sets the sample rate for output audio, in Hertz. Pass {@link #SAMPLE_RATE_NO_CHANGE} to output * audio at the same sample rate as the input. After calling this method, call {@link @@ -140,6 +158,7 @@ public final class SonicAudioProcessor implements AudioProcessor { public boolean isActive() { return pendingOutputAudioFormat.sampleRate != Format.NO_VALUE && (Math.abs(speed - 1f) >= CLOSE_THRESHOLD + || Math.abs(pitch - 1f) >= CLOSE_THRESHOLD || pendingOutputAudioFormat.sampleRate != pendingInputAudioFormat.sampleRate); } @@ -200,6 +219,7 @@ public final class SonicAudioProcessor implements AudioProcessor { inputAudioFormat.sampleRate, inputAudioFormat.channelCount, speed, + pitch, outputAudioFormat.sampleRate); } else if (sonic != null) { sonic.flush(); @@ -214,6 +234,7 @@ public final class SonicAudioProcessor implements AudioProcessor { @Override public void reset() { speed = 1f; + pitch = 1f; pendingInputAudioFormat = AudioFormat.NOT_SET; pendingOutputAudioFormat = AudioFormat.NOT_SET; inputAudioFormat = AudioFormat.NOT_SET; 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 a593943e57..be02faeba8 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 @@ -409,7 +409,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { /** * Sets the mode, which determines the role of sessions acquired from the instance. This must be * called before {@link #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)} - * or {@link #acquirePlaceholderSession} is called. + * is called. * *

By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when * required. @@ -469,34 +469,6 @@ public class DefaultDrmSessionManager implements DrmSessionManager { @Override @Nullable - public DrmSession acquirePlaceholderSession(Looper playbackLooper, int trackType) { - initPlaybackLooper(playbackLooper); - ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm); - boolean avoidPlaceholderDrmSessions = - FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType()) - && FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC; - // Avoid attaching a session to sparse formats. - if (avoidPlaceholderDrmSessions - || Util.linearSearch(useDrmSessionsForClearContentTrackTypes, trackType) == C.INDEX_UNSET - || exoMediaDrm.getExoMediaCryptoType() == null) { - return null; - } - maybeCreateMediaDrmHandler(playbackLooper); - if (placeholderDrmSession == null) { - DefaultDrmSession placeholderDrmSession = - createAndAcquireSessionWithRetry( - /* schemeDatas= */ ImmutableList.of(), - /* isPlaceholderSession= */ true, - /* eventDispatcher= */ null); - sessions.add(placeholderDrmSession); - this.placeholderDrmSession = placeholderDrmSession; - } else { - placeholderDrmSession.acquire(/* eventDispatcher= */ null); - } - return placeholderDrmSession; - } - - @Override public DrmSession acquireSession( Looper playbackLooper, @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, @@ -504,6 +476,11 @@ public class DefaultDrmSessionManager implements DrmSessionManager { initPlaybackLooper(playbackLooper); maybeCreateMediaDrmHandler(playbackLooper); + if (format.drmInitData == null) { + // Content is not encrypted. + return maybeAcquirePlaceholderSession(MimeTypes.getTrackType(format.sampleMimeType)); + } + @Nullable List schemeDatas = null; if (offlineLicenseKeySetId == null) { schemeDatas = getSchemeDatas(Assertions.checkNotNull(format.drmInitData), uuid, false); @@ -565,6 +542,32 @@ public class DefaultDrmSessionManager implements DrmSessionManager { // Internal methods. + @Nullable + private DrmSession maybeAcquirePlaceholderSession(int trackType) { + ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm); + boolean avoidPlaceholderDrmSessions = + FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType()) + && FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC; + // Avoid attaching a session to sparse formats. + if (avoidPlaceholderDrmSessions + || Util.linearSearch(useDrmSessionsForClearContentTrackTypes, trackType) == C.INDEX_UNSET + || UnsupportedMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType())) { + return null; + } + if (placeholderDrmSession == null) { + DefaultDrmSession placeholderDrmSession = + createAndAcquireSessionWithRetry( + /* schemeDatas= */ ImmutableList.of(), + /* isPlaceholderSession= */ true, + /* eventDispatcher= */ null); + sessions.add(placeholderDrmSession); + this.placeholderDrmSession = placeholderDrmSession; + } else { + placeholderDrmSession.acquire(/* eventDispatcher= */ null); + } + return placeholderDrmSession; + } + private boolean canAcquireSession(DrmInitData drmInitData) { if (offlineLicenseKeySetId != null) { // An offline license can be restored so a session can always be acquired. @@ -585,12 +588,16 @@ public class DefaultDrmSessionManager implements DrmSessionManager { if (schemeType == null || C.CENC_TYPE_cenc.equals(schemeType)) { // If there is no scheme information, assume patternless AES-CTR. return true; - } else if (C.CENC_TYPE_cbc1.equals(schemeType) - || C.CENC_TYPE_cbcs.equals(schemeType) - || C.CENC_TYPE_cens.equals(schemeType)) { - // API support for AES-CBC and pattern encryption was added in API 24. However, the + } else if (C.CENC_TYPE_cbcs.equals(schemeType)) { + // Support for cbcs (AES-CBC with pattern encryption) was added in API 24. However, the // implementation was not stable until API 25. return Util.SDK_INT >= 25; + } else if (C.CENC_TYPE_cbc1.equals(schemeType) || C.CENC_TYPE_cens.equals(schemeType)) { + // Support for cbc1 (AES-CTR with pattern encryption) and cens (AES-CBC without pattern + // encryption) was also added in API 24 and made stable from API 25, however support was + // removed from API 30. Since the range of API levels for which these modes are usable is too + // small to be useful, we don't indicate support on any API level. + return false; } // Unknown schemes, assume one of them is supported. return true; 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 7c26142216..1168884d76 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 @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.drm; import android.os.Looper; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; /** Manages a DRM session. */ @@ -33,13 +32,19 @@ public interface DrmSessionManager { new DrmSessionManager() { @Override + @Nullable public DrmSession acquireSession( Looper playbackLooper, @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, Format format) { - return new ErrorStateDrmSession( - new DrmSession.DrmSessionException( - new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME))); + if (format.drmInitData == null) { + return null; + } else { + return new ErrorStateDrmSession( + new DrmSession.DrmSessionException( + new UnsupportedDrmException( + UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME))); + } } @Override @@ -64,39 +69,27 @@ public interface DrmSessionManager { // Do nothing. } - /** - * Returns a {@link DrmSession} that does not execute key requests, with an incremented reference - * count. When the caller no longer needs to use the instance, it must call {@link - * DrmSession#release(DrmSessionEventListener.EventDispatcher)} to decrement the reference count. - * - *

Placeholder {@link DrmSession DrmSessions} may be used to configure secure decoders for - * playback of clear content periods. This can reduce the cost of transitioning between clear and - * encrypted content periods. - * - * @param playbackLooper The looper associated with the media playback thread. - * @param trackType The type of the track to acquire a placeholder session for. Must be one of the - * {@link C}{@code .TRACK_TYPE_*} constants. - * @return The placeholder DRM session, or null if this DRM session manager does not support - * placeholder sessions. - */ - @Nullable - default DrmSession acquirePlaceholderSession(Looper playbackLooper, int trackType) { - return null; - } - /** * Returns a {@link DrmSession} for the specified {@link Format}, with an incremented reference - * count. When the caller no longer needs to use the instance, it must call {@link + * count. May return null if the {@link Format#drmInitData} is null and the DRM session manager is + * not configured to attach a {@link DrmSession} to clear content. When the caller no longer needs + * to use a returned {@link DrmSession}, it must call {@link * DrmSession#release(DrmSessionEventListener.EventDispatcher)} to decrement the reference count. * + *

If the provided {@link Format} contains a null {@link Format#drmInitData}, the returned + * {@link DrmSession} (if not null) will be a placeholder session which does not execute key + * requests, and cannot be used to handle encrypted content. However, a placeholder session may be + * used to configure secure decoders for playback of clear content periods, which can reduce the + * cost of transitioning between clear and encrypted content. + * * @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 acquire a {@link DrmSession}. Must contain a - * non-null {@link Format#drmInitData}. - * @return The DRM session. + * @param format The {@link Format} for which to acquire a {@link DrmSession}. + * @return The DRM session. May be null if the given {@link Format#drmInitData} is null. */ + @Nullable DrmSession acquireSession( Looper playbackLooper, @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, @@ -105,16 +98,16 @@ public interface DrmSessionManager { /** * Returns the {@link ExoMediaCrypto} type associated to sessions acquired for the given {@link * Format}. Returns the {@link UnsupportedMediaCrypto} type if this DRM session manager does not - * support any of the DRM schemes defined in the given {@link Format}. If the {@link Format} - * describes unencrypted content, returns an {@link ExoMediaCrypto} type if this DRM session - * manager would associate a {@link #acquirePlaceholderSession placeholder session} to the given - * {@link Format}, or null otherwise. + * support any of the DRM schemes defined in the given {@link Format}. Returns null if {@link + * Format#drmInitData} is null and {@link #acquireSession} would return null for the given {@link + * Format}. * * @param format The {@link Format} for which to return the {@link ExoMediaCrypto} type. - * @return The {@link ExoMediaCrypto} type associated to sessions acquired using the given - * parameters, or the {@link UnsupportedMediaCrypto} type if the provided {@code drmInitData} - * is not supported, or {@code null} if {@code drmInitData} is null and no DRM session will be - * associated to the given {@code trackType}. + * @return The {@link ExoMediaCrypto} type associated to sessions acquired using the given {@link + * Format}, or {@link UnsupportedMediaCrypto} if this DRM session manager does not support any + * of the DRM schemes defined in the given {@link Format}. May be null if {@link + * Format#drmInitData} is null and {@link #acquireSession} would return null for the given + * {@link Format}. */ @Nullable Class getExoMediaCryptoType(Format format); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 71091c878d..b218d0cadb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -184,7 +184,8 @@ public final class OfflineLicenseHelper { /** * Downloads an offline license. * - * @param format The {@link Format} of the content whose license is to be downloaded. + * @param format The {@link Format} of the content whose license is to be downloaded. Must contain + * a non-null {@link Format#drmInitData}. * @return The key set id for the downloaded license. * @throws DrmSessionException Thrown when a DRM session error occurs. */ @@ -278,13 +279,14 @@ public final class OfflineLicenseHelper { private DrmSession openBlockingKeyRequest( @Mode int licenseMode, @Nullable byte[] offlineLicenseKeySetId, Format format) { + Assertions.checkNotNull(format.drmInitData); drmSessionManager.setMode(licenseMode, offlineLicenseKeySetId); conditionVariable.close(); DrmSession drmSession = drmSessionManager.acquireSession(handlerThread.getLooper(), eventDispatcher, format); // Block current thread until key loading is finished conditionVariable.block(); - return drmSession; + return Assertions.checkNotNull(drmSession); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java index afbf05fa9b..566f7fb1c7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java @@ -15,28 +15,19 @@ */ package com.google.android.exoplayer2.source; -import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_PLAYBACK; - import android.content.Context; import android.net.Uri; -import android.os.Build; import android.util.SparseArray; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.MediaItem; -import com.google.android.exoplayer2.MediaItem.DrmConfiguration; -import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaDrm; -import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; -import com.google.android.exoplayer2.drm.MediaDrmCallback; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; @@ -44,10 +35,8 @@ 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; -import com.google.common.primitives.Ints; import java.util.Arrays; import java.util.List; -import java.util.Map; /** * The default {@link MediaSourceFactory} implementation. @@ -79,21 +68,6 @@ import java.util.Map; * the stream. * * - *

DrmSessionManager creation for protected content

- * - *

For a media item with a {@link DrmConfiguration}, a {@link DefaultDrmSessionManager} is - * created based on that configuration. The following setter can be used to optionally configure the - * creation: - * - *

    - *
  • {@link #setDrmHttpDataSourceFactory(HttpDataSource.Factory)}: Sets the data source factory - * to be used by the {@link HttpMediaDrmCallback} for network requests (default: {@link - * DefaultHttpDataSourceFactory}). - *
- * - *

For media items without a {@link DrmConfiguration}, the {@link DrmSessionManager} passed to - * {@link #setDrmSessionManager(DrmSessionManager)} will be used. - * *

Ad support for media items with ad tag uri

* *

For a media item with an ad tag uri, an {@link AdSupportProvider} needs to be passed to {@link @@ -167,21 +141,14 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { } private static final String TAG = "DefaultMediaSourceFactory"; - private static final String DEFAULT_USER_AGENT = - ExoPlayerLibraryInfo.VERSION_SLASHY - + " (Linux;Android " - + Build.VERSION.RELEASE - + ") " - + ExoPlayerLibraryInfo.VERSION_SLASHY; + private final MediaSourceDrmHelper mediaSourceDrmHelper; private final DataSource.Factory dataSourceFactory; @Nullable private final AdSupportProvider adSupportProvider; private final SparseArray mediaSourceFactories; @C.ContentType private final int[] supportedTypes; - private DrmSessionManager drmSessionManager; - @Nullable private HttpDataSource.Factory drmHttpDataSourceFactory; - private String userAgent; + @Nullable private DrmSessionManager drmSessionManager; @Nullable private List streamKeys; /** @@ -196,8 +163,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { DataSource.Factory dataSourceFactory, @Nullable AdSupportProvider adSupportProvider) { this.dataSourceFactory = dataSourceFactory; this.adSupportProvider = adSupportProvider; - drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); - userAgent = DEFAULT_USER_AGENT; + mediaSourceDrmHelper = new MediaSourceDrmHelper(); mediaSourceFactories = loadDelegates(dataSourceFactory); supportedTypes = new int[mediaSourceFactories.size()]; for (int i = 0; i < mediaSourceFactories.size(); i++) { @@ -205,49 +171,23 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { } } - /** - * Sets the {@link HttpDataSource.Factory} to be used for creating {@link HttpMediaDrmCallback - * HttpMediaDrmCallbacks} which executes key and provisioning requests over HTTP. If {@code null} - * is passed the {@link DefaultHttpDataSourceFactory} is used. - * - * @param drmHttpDataSourceFactory The HTTP data source factory or {@code null} to use {@link - * DefaultHttpDataSourceFactory}. - * @return This factory, for convenience. - */ + @Override public DefaultMediaSourceFactory setDrmHttpDataSourceFactory( @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { - this.drmHttpDataSourceFactory = drmHttpDataSourceFactory; + mediaSourceDrmHelper.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory); return this; } - /** - * Sets the optional user agent to be used for DRM requests. - * - *

In case a factory has been set by {@link - * #setDrmHttpDataSourceFactory(HttpDataSource.Factory)}, this user agent is ignored. - * - * @param userAgent The user agent to be used for DRM requests. - * @return This factory, for convenience. - */ + @Override public DefaultMediaSourceFactory setDrmUserAgent(@Nullable String userAgent) { - this.userAgent = userAgent != null ? userAgent : DEFAULT_USER_AGENT; + mediaSourceDrmHelper.setDrmUserAgent(userAgent); return this; } - /** - * Sets the {@link DrmSessionManager} to use for media items that do not specify a {@link - * DrmConfiguration}. The default value is {@link DrmSessionManager#DUMMY}. - * - * @param drmSessionManager The {@link DrmSessionManager}. - * @return This factory, for convenience. - */ @Override public DefaultMediaSourceFactory setDrmSessionManager( @Nullable DrmSessionManager drmSessionManager) { - this.drmSessionManager = - drmSessionManager != null - ? drmSessionManager - : DrmSessionManager.getDummyDrmSessionManager(); + this.drmSessionManager = drmSessionManager; return this; } @@ -292,7 +232,8 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { @Nullable MediaSourceFactory mediaSourceFactory = mediaSourceFactories.get(type); Assertions.checkNotNull( mediaSourceFactory, "No suitable media source factory found for content type: " + type); - mediaSourceFactory.setDrmSessionManager(createDrmSessionManager(mediaItem)); + mediaSourceFactory.setDrmSessionManager( + drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem)); mediaSourceFactory.setStreamKeys( !mediaItem.playbackProperties.streamKeys.isEmpty() ? mediaItem.playbackProperties.streamKeys @@ -318,46 +259,6 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { // internal methods - private DrmSessionManager createDrmSessionManager(MediaItem mediaItem) { - Assertions.checkNotNull(mediaItem.playbackProperties); - if (mediaItem.playbackProperties.drmConfiguration == null - || mediaItem.playbackProperties.drmConfiguration.licenseUri == null - || Util.SDK_INT < 18) { - return drmSessionManager; - } - DefaultDrmSessionManager drmSessionManager = - new DefaultDrmSessionManager.Builder() - .setUuidAndExoMediaDrmProvider( - mediaItem.playbackProperties.drmConfiguration.uuid, - FrameworkMediaDrm.DEFAULT_PROVIDER) - .setMultiSession(mediaItem.playbackProperties.drmConfiguration.multiSession) - .setPlayClearSamplesWithoutKeys( - mediaItem.playbackProperties.drmConfiguration.playClearContentWithoutKey) - .setUseDrmSessionsForClearContent( - Ints.toArray(mediaItem.playbackProperties.drmConfiguration.sessionForClearTypes)) - .build(createHttpMediaDrmCallback(mediaItem.playbackProperties.drmConfiguration)); - - drmSessionManager.setMode( - MODE_PLAYBACK, mediaItem.playbackProperties.drmConfiguration.getKeySetId()); - - return drmSessionManager; - } - - private MediaDrmCallback createHttpMediaDrmCallback(MediaItem.DrmConfiguration drmConfiguration) { - Assertions.checkNotNull(drmConfiguration.licenseUri); - HttpMediaDrmCallback drmCallback = - new HttpMediaDrmCallback( - drmConfiguration.licenseUri.toString(), - drmConfiguration.forceDefaultLicenseUri, - drmHttpDataSourceFactory != null - ? drmHttpDataSourceFactory - : new DefaultHttpDataSourceFactory(userAgent)); - for (Map.Entry entry : drmConfiguration.requestHeaders.entrySet()) { - drmCallback.setKeyRequestProperty(entry.getKey(), entry.getValue()); - } - return drmCallback; - } - private static MediaSource maybeClipMediaSource(MediaItem mediaItem, MediaSource mediaSource) { if (mediaItem.clippingProperties.startPositionMs == 0 && mediaItem.clippingProperties.endPositionMs == C.TIME_END_OF_SOURCE 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 1e8129bf3a..38146c92b2 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 @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; @@ -168,6 +169,23 @@ public final class ExtractorMediaSource extends CompositeMediaSource { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link ProgressiveMediaSource.Factory#setDrmHttpDataSourceFactory} instead. + */ + @Deprecated + @Override + public MediaSourceFactory setDrmHttpDataSourceFactory( + @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { + throw new UnsupportedOperationException(); + } + + /** @deprecated Use {@link ProgressiveMediaSource.Factory#setDrmUserAgent} instead. */ + @Deprecated + @Override + public MediaSourceFactory setDrmUserAgent(@Nullable String userAgent) { + throw new UnsupportedOperationException(); + } + /** @deprecated Use {@link #createMediaSource(MediaItem)} instead. */ @SuppressWarnings("deprecation") @Deprecated diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceDrmHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceDrmHelper.java new file mode 100644 index 0000000000..29325d789e --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceDrmHelper.java @@ -0,0 +1,105 @@ +/* + * 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.source; + +import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_PLAYBACK; +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Build; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.FrameworkMediaDrm; +import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import com.google.common.primitives.Ints; +import java.util.Map; + +/** A helper to create a {@link DrmSessionManager} from a {@link MediaItem}. */ +public final class MediaSourceDrmHelper { + + private static final String DEFAULT_USER_AGENT = + ExoPlayerLibraryInfo.VERSION_SLASHY + + " (Linux;Android " + + Build.VERSION.RELEASE + + ") " + + ExoPlayerLibraryInfo.VERSION_SLASHY; + + @Nullable private HttpDataSource.Factory drmHttpDataSourceFactory; + @Nullable private String userAgent; + + /** + * Sets the {@link HttpDataSource.Factory} to be used for creating {@link HttpMediaDrmCallback + * HttpMediaDrmCallbacks} which executes key and provisioning requests over HTTP. If {@code null} + * is passed the {@link DefaultHttpDataSourceFactory} is used. + * + * @param drmHttpDataSourceFactory The HTTP data source factory or {@code null} to use {@link + * DefaultHttpDataSourceFactory}. + */ + public void setDrmHttpDataSourceFactory( + @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { + this.drmHttpDataSourceFactory = drmHttpDataSourceFactory; + } + + /** + * Sets the optional user agent to be used for DRM requests. + * + *

In case a factory has been set by {@link + * #setDrmHttpDataSourceFactory(HttpDataSource.Factory)}, this user agent is ignored. + * + * @param userAgent The user agent to be used for DRM requests. + */ + public void setDrmUserAgent(@Nullable String userAgent) { + this.userAgent = userAgent; + } + + /** Creates a {@link DrmSessionManager} for the given media item. */ + public DrmSessionManager create(MediaItem mediaItem) { + Assertions.checkNotNull(mediaItem.playbackProperties); + @Nullable + MediaItem.DrmConfiguration drmConfiguration = mediaItem.playbackProperties.drmConfiguration; + if (drmConfiguration == null || drmConfiguration.licenseUri == null || Util.SDK_INT < 18) { + return DrmSessionManager.getDummyDrmSessionManager(); + } + HttpDataSource.Factory dataSourceFactory = + drmHttpDataSourceFactory != null + ? drmHttpDataSourceFactory + : new DefaultHttpDataSourceFactory(userAgent != null ? userAgent : DEFAULT_USER_AGENT); + HttpMediaDrmCallback httpDrmCallback = + new HttpMediaDrmCallback( + castNonNull(drmConfiguration.licenseUri).toString(), + drmConfiguration.forceDefaultLicenseUri, + dataSourceFactory); + for (Map.Entry entry : drmConfiguration.requestHeaders.entrySet()) { + httpDrmCallback.setKeyRequestProperty(entry.getKey(), entry.getValue()); + } + DefaultDrmSessionManager drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider( + drmConfiguration.uuid, FrameworkMediaDrm.DEFAULT_PROVIDER) + .setMultiSession(drmConfiguration.multiSession) + .setPlayClearSamplesWithoutKeys(drmConfiguration.playClearContentWithoutKey) + .setUseDrmSessionsForClearContent(Ints.toArray(drmConfiguration.sessionForClearTypes)) + .build(httpDrmCallback); + drmSessionManager.setMode(MODE_PLAYBACK, drmConfiguration.getKeySetId()); + return drmSessionManager; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java index e1c52c097b..4175121d38 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java @@ -19,13 +19,34 @@ import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.MediaItem; -import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import java.util.List; -/** Factory for creating {@link MediaSource}s from URIs. */ +/** + * Factory for creating {@link MediaSource}s from URIs. + * + *

DrmSessionManager creation for protected content

+ * + *

In case a {@link DrmSessionManager} is passed to {@link + * #setDrmSessionManager(DrmSessionManager)}, it will be used regardless of the drm configuration of + * the media item. + * + *

For a media item with a {@link MediaItem.DrmConfiguration}, a {@link DefaultDrmSessionManager} + * is created based on that configuration. The following setter can be used to optionally configure + * the creation: + * + *

    + *
  • {@link #setDrmHttpDataSourceFactory(HttpDataSource.Factory)}: Sets the data source factory + * to be used by the {@link HttpMediaDrmCallback} for network requests (default: {@link + * DefaultHttpDataSourceFactory}). + *
+ */ public interface MediaSourceFactory { /** @deprecated Use {@link MediaItem.PlaybackProperties#streamKeys} instead. */ @@ -35,13 +56,40 @@ public interface MediaSourceFactory { } /** - * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. + * Sets the {@link DrmSessionManager} to use for all media items regardless of their {@link + * MediaItem.DrmConfiguration}. * * @param drmSessionManager The {@link DrmSessionManager}. * @return This factory, for convenience. */ MediaSourceFactory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager); + /** + * Sets the {@link HttpDataSource.Factory} to be used for creating {@link HttpMediaDrmCallback + * HttpMediaDrmCallbacks} to execute key and provisioning requests over HTTP. + * + *

In case a {@link DrmSessionManager} has been set by {@link + * #setDrmSessionManager(DrmSessionManager)}, this data source factory is ignored. + * + * @param drmHttpDataSourceFactory The HTTP data source factory, or {@code null} to use {@link + * DefaultHttpDataSourceFactory}. + * @return This factory, for convenience. + */ + MediaSourceFactory setDrmHttpDataSourceFactory( + @Nullable HttpDataSource.Factory drmHttpDataSourceFactory); + + /** + * Sets the optional user agent to be used for DRM requests. + * + *

In case a factory has been set by {@link + * #setDrmHttpDataSourceFactory(HttpDataSource.Factory)} or a {@link DrmSessionManager} has been + * set by {@link #setDrmSessionManager(DrmSessionManager)}, this user agent is ignored. + * + * @param userAgent The user agent to be used for DRM requests. + * @return This factory, for convenience. + */ + MediaSourceFactory setDrmUserAgent(@Nullable String userAgent); + /** * Sets an optional {@link LoadErrorHandlingPolicy}. * 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 125891f09c..4d7230cc3a 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 @@ -22,7 +22,6 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; @@ -30,6 +29,7 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.TransferListener; @@ -51,9 +51,10 @@ public final class ProgressiveMediaSource extends BaseMediaSource public static final class Factory implements MediaSourceFactory { private final DataSource.Factory dataSourceFactory; + private final MediaSourceDrmHelper mediaSourceDrmHelper; private ExtractorsFactory extractorsFactory; - private DrmSessionManager drmSessionManager; + @Nullable private DrmSessionManager drmSessionManager; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private int continueLoadingCheckIntervalBytes; @Nullable private String customCacheKey; @@ -78,7 +79,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource public Factory(DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) { this.dataSourceFactory = dataSourceFactory; this.extractorsFactory = extractorsFactory; - drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); + mediaSourceDrmHelper = new MediaSourceDrmHelper(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES; } @@ -146,19 +147,22 @@ public final class ProgressiveMediaSource extends BaseMediaSource return this; } - /** - * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The - * default value is {@link DrmSessionManager#DUMMY}. - * - * @param drmSessionManager The {@link DrmSessionManager}. - * @return This factory, for convenience. - */ @Override public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { - this.drmSessionManager = - drmSessionManager != null - ? drmSessionManager - : DrmSessionManager.getDummyDrmSessionManager(); + this.drmSessionManager = drmSessionManager; + return this; + } + + @Override + public Factory setDrmHttpDataSourceFactory( + @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { + mediaSourceDrmHelper.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory); + return this; + } + + @Override + public Factory setDrmUserAgent(@Nullable String userAgent) { + mediaSourceDrmHelper.setDrmUserAgent(userAgent); return this; } @@ -194,7 +198,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource mediaItem, dataSourceFactory, extractorsFactory, - drmSessionManager, + drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem), loadErrorHandlingPolicy, continueLoadingCheckIntervalBytes); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index 515f805845..28d25feb03 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -327,13 +327,7 @@ public class SampleQueue implements TrackOutput { * Attempts to read from the queue. * *

{@link Format Formats} read from this method may be associated to a {@link DrmSession} - * through {@link FormatHolder#drmSession}, which is populated in two scenarios: - * - *

    - *
  • The {@link Format} has a non-null {@link Format#drmInitData}. - *
  • The {@link DrmSessionManager} provides placeholder sessions for this queue's track type. - * See {@link DrmSessionManager#acquirePlaceholderSession(Looper, int)}. - *
+ * through {@link FormatHolder#drmSession}. * * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the @@ -842,10 +836,7 @@ public class SampleQueue implements TrackOutput { // is being used for both DrmInitData. @Nullable DrmSession previousSession = currentDrmSession; currentDrmSession = - newDrmInitData != null - ? drmSessionManager.acquireSession(playbackLooper, drmEventDispatcher, newFormat) - : drmSessionManager.acquirePlaceholderSession( - playbackLooper, MimeTypes.getTrackType(newFormat.sampleMimeType)); + drmSessionManager.acquireSession(playbackLooper, drmEventDispatcher, newFormat); outputFormatHolder.drmSession = currentDrmSession; if (previousSession != null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java index 65abcb4059..4c3f58f2c6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream.cache; 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 static java.lang.Math.max; import static java.lang.Math.min; @@ -219,9 +220,9 @@ import java.util.TreeSet; public SimpleCacheSpan setLastTouchTimestamp( SimpleCacheSpan cacheSpan, long lastTouchTimestamp, boolean updateFile) { checkState(cachedSpans.remove(cacheSpan)); - File file = cacheSpan.file; + File file = checkNotNull(cacheSpan.file); if (updateFile) { - File directory = file.getParentFile(); + File directory = checkNotNull(file.getParentFile()); long position = cacheSpan.position; File newFile = SimpleCacheSpan.getCacheFile(directory, id, position, lastTouchTimestamp); if (file.renameTo(newFile)) { @@ -244,7 +245,9 @@ import java.util.TreeSet; /** Removes the given span from cache. */ public boolean removeSpan(CacheSpan span) { if (cachedSpans.remove(span)) { - span.file.delete(); + if (span.file != null) { + span.file.delete(); + } return true; } return false; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index 452abca7c2..850ac59f04 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2.upstream.cache; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkState; +import static com.google.android.exoplayer2.util.Util.castNonNull; import static java.lang.Math.min; import android.annotation.SuppressLint; @@ -61,6 +64,7 @@ import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** Maintains the index of cached content. */ /* package */ class CachedContentIndex { @@ -155,13 +159,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Nullable byte[] legacyStorageSecretKey, boolean legacyStorageEncrypt, boolean preferLegacyStorage) { - Assertions.checkState(databaseProvider != null || legacyStorageDir != null); + checkState(databaseProvider != null || legacyStorageDir != null); keyToContent = new HashMap<>(); idToKey = new SparseArray<>(); removedIds = new SparseBooleanArray(); newIds = new SparseBooleanArray(); + @Nullable Storage databaseStorage = databaseProvider != null ? new DatabaseStorage(databaseProvider) : null; + @Nullable Storage legacyStorage = legacyStorageDir != null ? new LegacyStorage( @@ -170,7 +176,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; legacyStorageEncrypt) : null; if (databaseStorage == null || (legacyStorage != null && preferLegacyStorage)) { - storage = legacyStorage; + storage = castNonNull(legacyStorage); previousStorage = databaseStorage; } else { storage = databaseStorage; @@ -325,7 +331,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Returns a {@link ContentMetadata} for the given key. */ public ContentMetadata getContentMetadata(String key) { - CachedContent cachedContent = get(key); + @Nullable CachedContent cachedContent = get(key); return cachedContent != null ? cachedContent.getMetadata() : DefaultContentMetadata.EMPTY; } @@ -358,7 +364,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; * returns the smallest unused non-negative integer. */ @VisibleForTesting - /* package */ static int getNewId(SparseArray idToKey) { + /* package */ static int getNewId(SparseArray<@NullableType String> idToKey) { int size = idToKey.size(); int id = size == 0 ? 0 : (idToKey.keyAt(size - 1) + 1); if (id < 0) { // In case if we pass max int value. @@ -512,8 +518,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Nullable private ReusableBufferedOutputStream bufferedOutputStream; public LegacyStorage(File file, @Nullable byte[] secretKey, boolean encrypt) { - Cipher cipher = null; - SecretKeySpec secretKeySpec = null; + checkState(secretKey != null || !encrypt); + @Nullable Cipher cipher = null; + @Nullable SecretKeySpec secretKeySpec = null; if (secretKey != null) { Assertions.checkArgument(secretKey.length == 16); try { @@ -550,7 +557,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public void load( HashMap content, SparseArray<@NullableType String> idToKey) { - Assertions.checkState(!changed); + checkState(!changed); if (!readFile(content, idToKey)) { content.clear(); idToKey.clear(); @@ -588,7 +595,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return true; } - DataInputStream input = null; + @Nullable DataInputStream input = null; try { InputStream inputStream = new BufferedInputStream(atomicFile.openRead()); input = new DataInputStream(inputStream); @@ -606,7 +613,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; input.readFully(initializationVector); IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector); try { - cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); + cipher.init(Cipher.DECRYPT_MODE, castNonNull(secretKeySpec), ivParameterSpec); } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { throw new IllegalStateException(e); } @@ -647,6 +654,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } else { bufferedOutputStream.reset(outputStream); } + ReusableBufferedOutputStream bufferedOutputStream = this.bufferedOutputStream; output = new DataOutputStream(bufferedOutputStream); output.writeInt(VERSION); @@ -655,11 +663,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; if (encrypt) { byte[] initializationVector = new byte[16]; - random.nextBytes(initializationVector); + castNonNull(random).nextBytes(initializationVector); output.write(initializationVector); IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector); try { - cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); + castNonNull(cipher) + .init(Cipher.ENCRYPT_MODE, castNonNull(secretKeySpec), ivParameterSpec); } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { throw new IllegalStateException(e); // Should never happen. } @@ -762,16 +771,17 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; + " BLOB NOT NULL)"; private final DatabaseProvider databaseProvider; - private final SparseArray pendingUpdates; + private final SparseArray<@NullableType CachedContent> pendingUpdates; - private String hexUid; - private String tableName; + private @MonotonicNonNull String hexUid; + private @MonotonicNonNull String tableName; public static void delete(DatabaseProvider databaseProvider, long uid) throws DatabaseIOException { delete(databaseProvider, Long.toHexString(uid)); } + @SuppressWarnings("nullness:initialization.fields.uninitialized") public DatabaseStorage(DatabaseProvider databaseProvider) { this.databaseProvider = databaseProvider; pendingUpdates = new SparseArray<>(); @@ -788,26 +798,26 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return VersionTable.getVersion( databaseProvider.getReadableDatabase(), VersionTable.FEATURE_CACHE_CONTENT_METADATA, - hexUid) + checkNotNull(hexUid)) != VersionTable.VERSION_UNSET; } @Override public void delete() throws DatabaseIOException { - delete(databaseProvider, hexUid); + delete(databaseProvider, checkNotNull(hexUid)); } @Override public void load( HashMap content, SparseArray<@NullableType String> idToKey) throws IOException { - Assertions.checkState(pendingUpdates.size() == 0); + checkState(pendingUpdates.size() == 0); try { int version = VersionTable.getVersion( databaseProvider.getReadableDatabase(), VersionTable.FEATURE_CACHE_CONTENT_METADATA, - hexUid); + checkNotNull(hexUid)); if (version != TABLE_VERSION) { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); writableDatabase.beginTransactionNonExclusive(); @@ -871,7 +881,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; writableDatabase.beginTransactionNonExclusive(); try { for (int i = 0; i < pendingUpdates.size(); i++) { - CachedContent cachedContent = pendingUpdates.valueAt(i); + @Nullable CachedContent cachedContent = pendingUpdates.valueAt(i); if (cachedContent == null) { deleteRow(writableDatabase, pendingUpdates.keyAt(i)); } else { @@ -906,7 +916,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return databaseProvider .getReadableDatabase() .query( - tableName, + checkNotNull(tableName), COLUMNS, /* selection= */ null, /* selectionArgs= */ null, @@ -917,13 +927,17 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private void initializeTable(SQLiteDatabase writableDatabase) throws DatabaseIOException { VersionTable.setVersion( - writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA, hexUid, TABLE_VERSION); - dropTable(writableDatabase, tableName); + writableDatabase, + VersionTable.FEATURE_CACHE_CONTENT_METADATA, + checkNotNull(hexUid), + TABLE_VERSION); + dropTable(writableDatabase, checkNotNull(tableName)); writableDatabase.execSQL("CREATE TABLE " + tableName + " " + TABLE_SCHEMA); } private void deleteRow(SQLiteDatabase writableDatabase, int key) { - writableDatabase.delete(tableName, WHERE_ID_EQUALS, new String[] {Integer.toString(key)}); + writableDatabase.delete( + checkNotNull(tableName), WHERE_ID_EQUALS, new String[] {Integer.toString(key)}); } private void addOrUpdateRow(SQLiteDatabase writableDatabase, CachedContent cachedContent) @@ -936,7 +950,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; values.put(COLUMN_ID, cachedContent.id); values.put(COLUMN_KEY, cachedContent.key); values.put(COLUMN_METADATA, data); - writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values); + writableDatabase.replaceOrThrow(checkNotNull(tableName), /* nullColumnHack= */ null, values); } private static void delete(DatabaseProvider databaseProvider, String hexUid) 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 293264504f..aa82d41414 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; import com.google.android.exoplayer2.RendererCapabilities; @@ -147,8 +148,9 @@ public class EventLogger implements AnalyticsListener { } @Override - public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) { - logd(eventTime, "playbackSpeed", Float.toString(playbackSpeed)); + public void onPlaybackParametersChanged( + EventTime eventTime, PlaybackParameters playbackParameters) { + logd(eventTime, "playbackParameters", playbackParameters.toString()); } @Override @@ -316,13 +318,19 @@ public class EventLogger implements AnalyticsListener { logd(eventTime, "audioInputFormat", Format.toLogString(format)); } + @Override + public void onAudioPositionAdvancing(EventTime eventTime, long playoutStartSystemTimeMs) { + long timeSincePlayoutStartMs = System.currentTimeMillis() - playoutStartSystemTimeMs; + logd(eventTime, "audioPositionAdvancing", "timeSincePlayoutStartMs=" + timeSincePlayoutStartMs); + } + @Override public void onAudioUnderrun( EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { loge( eventTime, "audioTrackUnderrun", - bufferSize + ", " + bufferSizeMs + ", " + elapsedSinceLastFeedMs + "]", + bufferSize + ", " + bufferSizeMs + ", " + elapsedSinceLastFeedMs, /* throwable= */ null); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java index 44c3c5e7fa..df335908c0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.util; +import com.google.android.exoplayer2.PlaybackParameters; + /** * Tracks the progression of media time. */ @@ -26,13 +28,13 @@ public interface MediaClock { long getPositionUs(); /** - * Attempts to set the playback speed. The media clock may override the speed if changing the - * speed is not supported. + * Attempts to set the playback parameters. The media clock may override the speed if changing the + * playback parameters is not supported. * - * @param playbackSpeed The playback speed to attempt to set. + * @param playbackParameters The playback parameters to attempt to set. */ - void setPlaybackSpeed(float playbackSpeed); + void setPlaybackParameters(PlaybackParameters playbackParameters); - /** Returns the active playback speed. */ - float getPlaybackSpeed(); + /** Returns the active playback parameters. */ + PlaybackParameters getPlaybackParameters(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java index e1df77a200..87970d3c00 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java @@ -16,7 +16,7 @@ package com.google.android.exoplayer2.util; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.PlaybackParameters; /** * A {@link MediaClock} whose position advances with real time based on the playback parameters when @@ -29,8 +29,7 @@ public final class StandaloneMediaClock implements MediaClock { private boolean started; private long baseUs; private long baseElapsedMs; - private float playbackSpeed; - private int scaledUsPerMs; + private PlaybackParameters playbackParameters; /** * Creates a new standalone media clock using the given {@link Clock} implementation. @@ -39,8 +38,7 @@ public final class StandaloneMediaClock implements MediaClock { */ public StandaloneMediaClock(Clock clock) { this.clock = clock; - playbackSpeed = Player.DEFAULT_PLAYBACK_SPEED; - scaledUsPerMs = getScaledUsPerMs(playbackSpeed); + playbackParameters = PlaybackParameters.DEFAULT; } /** @@ -80,33 +78,29 @@ public final class StandaloneMediaClock implements MediaClock { long positionUs = baseUs; if (started) { long elapsedSinceBaseMs = clock.elapsedRealtime() - baseElapsedMs; - if (playbackSpeed == 1f) { + if (playbackParameters.speed == 1f) { positionUs += C.msToUs(elapsedSinceBaseMs); } else { // Add the media time in microseconds that will elapse in elapsedSinceBaseMs milliseconds of // wallclock time - positionUs += elapsedSinceBaseMs * scaledUsPerMs; + positionUs += playbackParameters.getMediaTimeUsForPlayoutTimeMs(elapsedSinceBaseMs); } } return positionUs; } @Override - public void setPlaybackSpeed(float playbackSpeed) { + public void setPlaybackParameters(PlaybackParameters playbackParameters) { // Store the current position as the new base, in case the playback speed has changed. if (started) { resetPosition(getPositionUs()); } - this.playbackSpeed = playbackSpeed; - scaledUsPerMs = getScaledUsPerMs(playbackSpeed); + this.playbackParameters = playbackParameters; } @Override - public float getPlaybackSpeed() { - return playbackSpeed; + public PlaybackParameters getPlaybackParameters() { + return playbackParameters; } - private static int getScaledUsPerMs(float playbackSpeed) { - return Math.round(playbackSpeed * 1000f); - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 56e5457044..5b26588244 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -1611,6 +1611,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { case "ELUGA_Prim": case "ELUGA_Ray_X": case "EverStar_S": + case "F02H": + case "F03H": case "F3111": case "F3113": case "F3116": diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java index 217df762f6..867857cbe5 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java @@ -22,7 +22,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.MockitoAnnotations.initMocks; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.DefaultMediaClock.PlaybackSpeedListener; +import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParametersListener; import com.google.android.exoplayer2.testutil.FakeClock; import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; import org.junit.Before; @@ -36,9 +36,10 @@ public class DefaultMediaClockTest { private static final long TEST_POSITION_US = 123456789012345678L; private static final long SLEEP_TIME_MS = 1_000; - private static final float TEST_PLAYBACK_SPEED = 2f; + private static final PlaybackParameters TEST_PLAYBACK_PARAMETERS = + new PlaybackParameters(/* speed= */ 2f); - @Mock private PlaybackSpeedListener listener; + @Mock private PlaybackParametersListener listener; private FakeClock fakeClock; private DefaultMediaClock mediaClock; @@ -109,44 +110,44 @@ public class DefaultMediaClockTest { } @Test - public void standaloneGetPlaybackSpeed_initializedWithDefaultPlaybackSpeed() { - assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(Player.DEFAULT_PLAYBACK_SPEED); + public void standaloneGetPlaybackParameters_initializedWithDefaultPlaybackParameters() { + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); } @Test - public void standaloneSetPlaybackSpeed_getPlaybackSpeedShouldReturnSameValue() { - mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED); - assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(TEST_PLAYBACK_SPEED); + public void standaloneSetPlaybackParameters_getPlaybackParametersShouldReturnSameValue() { + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); } @Test - public void standaloneSetPlaybackSpeed_shouldNotTriggerCallback() { - mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED); + public void standaloneSetPlaybackParameters_shouldNotTriggerCallback() { + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); verifyNoMoreInteractions(listener); } @Test - public void standaloneSetPlaybackSpeed_shouldApplyNewPlaybackSpeed() { - mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED); + public void standaloneSetPlaybackParameters_shouldApplyNewPlaybackParameters() { + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); mediaClock.start(); - // Asserts that clock is running with speed declared in getPlaybackSpeed(). + // Asserts that clock is running with speed declared in getPlaybackParameters(). assertClockIsRunning(/* isReadingAhead= */ false); } @Test - public void standaloneSetOtherPlaybackSpeed_getPlaybackSpeedShouldReturnSameValue() { - mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED); - mediaClock.setPlaybackSpeed(Player.DEFAULT_PLAYBACK_SPEED); - assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(Player.DEFAULT_PLAYBACK_SPEED); + public void standaloneSetOtherPlaybackParameters_getPlaybackParametersShouldReturnSameValue() { + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + mediaClock.setPlaybackParameters(PlaybackParameters.DEFAULT); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); } @Test - public void enableRendererMediaClock_shouldOverwriteRendererPlaybackSpeedIfPossible() + public void enableRendererMediaClock_shouldOverwriteRendererPlaybackParametersIfPossible() throws ExoPlaybackException { FakeMediaClockRenderer mediaClockRenderer = - new MediaClockRenderer(TEST_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ true); + new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ true); mediaClock.onRendererEnabled(mediaClockRenderer); - assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(Player.DEFAULT_PLAYBACK_SPEED); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); verifyNoMoreInteractions(listener); } @@ -154,26 +155,27 @@ public class DefaultMediaClockTest { public void enableRendererMediaClockWithFixedPlaybackSpeed_usesRendererPlaybackSpeed() throws ExoPlaybackException { FakeMediaClockRenderer mediaClockRenderer = - new MediaClockRenderer(TEST_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ false); + new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false); mediaClock.onRendererEnabled(mediaClockRenderer); - assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(TEST_PLAYBACK_SPEED); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); } @Test public void enableRendererMediaClockWithFixedPlaybackSpeed_shouldTriggerCallback() throws ExoPlaybackException { FakeMediaClockRenderer mediaClockRenderer = - new MediaClockRenderer(TEST_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ false); + new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false); mediaClock.onRendererEnabled(mediaClockRenderer); mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); - verify(listener).onPlaybackSpeedChanged(TEST_PLAYBACK_SPEED); + verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); } @Test public void enableRendererMediaClockWithFixedButSamePlaybackSpeed_shouldNotTriggerCallback() throws ExoPlaybackException { FakeMediaClockRenderer mediaClockRenderer = - new MediaClockRenderer(Player.DEFAULT_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ false); + new MediaClockRenderer( + PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ false); mediaClock.onRendererEnabled(mediaClockRenderer); mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); verifyNoMoreInteractions(listener); @@ -182,44 +184,47 @@ public class DefaultMediaClockTest { @Test public void disableRendererMediaClock_shouldKeepPlaybackSpeed() throws ExoPlaybackException { FakeMediaClockRenderer mediaClockRenderer = - new MediaClockRenderer(TEST_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ false); + new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false); mediaClock.onRendererEnabled(mediaClockRenderer); mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); mediaClock.onRendererDisabled(mediaClockRenderer); mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); - assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(TEST_PLAYBACK_SPEED); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); } @Test - public void rendererClockSetPlaybackSpeed_getPlaybackSpeedShouldReturnSameValue() + public void rendererClockSetPlaybackSpeed_getPlaybackParametersShouldReturnSameValue() throws ExoPlaybackException { FakeMediaClockRenderer mediaClockRenderer = - new MediaClockRenderer(Player.DEFAULT_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ true); + new MediaClockRenderer( + PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ true); mediaClock.onRendererEnabled(mediaClockRenderer); mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); - mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED); - assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(TEST_PLAYBACK_SPEED); + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); } @Test public void rendererClockSetPlaybackSpeed_shouldNotTriggerCallback() throws ExoPlaybackException { FakeMediaClockRenderer mediaClockRenderer = - new MediaClockRenderer(Player.DEFAULT_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ true); + new MediaClockRenderer( + PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ true); mediaClock.onRendererEnabled(mediaClockRenderer); mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); - mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED); + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); verifyNoMoreInteractions(listener); } @Test - public void rendererClockSetPlaybackSpeedOverwrite_getPlaybackSpeedShouldReturnSameValue() + public void rendererClockSetPlaybackSpeedOverwrite_getPlaybackParametersShouldReturnSameValue() throws ExoPlaybackException { FakeMediaClockRenderer mediaClockRenderer = - new MediaClockRenderer(Player.DEFAULT_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ false); + new MediaClockRenderer( + PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ false); mediaClock.onRendererEnabled(mediaClockRenderer); mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); - mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED); - assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(Player.DEFAULT_PLAYBACK_SPEED); + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); } @Test @@ -266,12 +271,13 @@ public class DefaultMediaClockTest { public void getPositionWithPlaybackSpeedChange_shouldTriggerCallback() throws ExoPlaybackException { MediaClockRenderer mediaClockRenderer = - new MediaClockRenderer(Player.DEFAULT_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ true); + new MediaClockRenderer( + PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ true); mediaClock.onRendererEnabled(mediaClockRenderer); // Silently change playback speed of renderer clock. - mediaClockRenderer.playbackSpeed = TEST_PLAYBACK_SPEED; + mediaClockRenderer.playbackParameters = TEST_PLAYBACK_PARAMETERS; mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); - verify(listener).onPlaybackSpeedChanged(TEST_PLAYBACK_SPEED); + verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); } @Test @@ -356,7 +362,7 @@ public class DefaultMediaClockTest { private void assertClockIsRunning(boolean isReadingAhead) { long clockStartUs = mediaClock.syncAndGetPositionUs(isReadingAhead); fakeClock.advanceTime(SLEEP_TIME_MS); - int scaledUsPerMs = Math.round(mediaClock.getPlaybackSpeed() * 1000f); + int scaledUsPerMs = Math.round(mediaClock.getPlaybackParameters().speed * 1000f); assertThat(mediaClock.syncAndGetPositionUs(isReadingAhead)) .isEqualTo(clockStartUs + (SLEEP_TIME_MS * scaledUsPerMs)); } @@ -371,37 +377,53 @@ public class DefaultMediaClockTest { @SuppressWarnings("HidingField") private static class MediaClockRenderer extends FakeMediaClockRenderer { - private final boolean playbackSpeedIsMutable; + private final boolean playbackParametersAreMutable; private final boolean isReady; private final boolean isEnded; - public float playbackSpeed; + public PlaybackParameters playbackParameters; public long positionUs; public MediaClockRenderer() throws ExoPlaybackException { - this(Player.DEFAULT_PLAYBACK_SPEED, false, true, false, false); + this( + PlaybackParameters.DEFAULT, + /* playbackParametersAreMutable= */ false, + /* isReady= */ true, + /* isEnded= */ false, + /* hasReadStreamToEnd= */ false); } - public MediaClockRenderer(float playbackSpeed, boolean playbackSpeedIsMutable) + public MediaClockRenderer( + PlaybackParameters playbackParameters, boolean playbackParametersAreMutable) throws ExoPlaybackException { - this(playbackSpeed, playbackSpeedIsMutable, true, false, false); + this( + playbackParameters, + playbackParametersAreMutable, + /* isReady= */ true, + /* isEnded= */ false, + /* hasReadStreamToEnd= */ false); } public MediaClockRenderer(boolean isReady, boolean isEnded, boolean hasReadStreamToEnd) throws ExoPlaybackException { - this(Player.DEFAULT_PLAYBACK_SPEED, false, isReady, isEnded, hasReadStreamToEnd); + this( + PlaybackParameters.DEFAULT, + /* playbackParametersAreMutable= */ false, + isReady, + isEnded, + hasReadStreamToEnd); } private MediaClockRenderer( - float playbackSpeed, - boolean playbackSpeedIsMutable, + PlaybackParameters playbackParameters, + boolean playbackParametersAreMutable, boolean isReady, boolean isEnded, boolean hasReadStreamToEnd) throws ExoPlaybackException { super(C.TRACK_TYPE_UNKNOWN); - this.playbackSpeed = playbackSpeed; - this.playbackSpeedIsMutable = playbackSpeedIsMutable; + this.playbackParameters = playbackParameters; + this.playbackParametersAreMutable = playbackParametersAreMutable; this.isReady = isReady; this.isEnded = isEnded; this.positionUs = TEST_POSITION_US; @@ -416,15 +438,15 @@ public class DefaultMediaClockTest { } @Override - public void setPlaybackSpeed(float playbackSpeed) { - if (playbackSpeedIsMutable) { - this.playbackSpeed = playbackSpeed; + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + if (playbackParametersAreMutable) { + this.playbackParameters = playbackParameters; } } @Override - public float getPlaybackSpeed() { - return playbackSpeed; + public PlaybackParameters getPlaybackParameters() { + return playbackParameters; } @Override 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 3c43d3c22f..0043cb9e74 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 @@ -328,11 +328,11 @@ public final class ExoPlayerTest { } @Override - public void setPlaybackSpeed(float playbackSpeed) {} + public void setPlaybackParameters(PlaybackParameters playbackParameters) {} @Override - public float getPlaybackSpeed() { - return Player.DEFAULT_PLAYBACK_SPEED; + public PlaybackParameters getPlaybackParameters() { + return PlaybackParameters.DEFAULT; } @Override @@ -1010,7 +1010,7 @@ public final class ExoPlayerTest { } }) // Set playback speed (while the fake media period is not yet prepared). - .setPlaybackSpeed(2f) + .setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f)) // Complete preparation of the fake media period. .executeRunnable(() -> fakeMediaPeriodHolder[0].setPreparationComplete()) .build(); @@ -3378,18 +3378,18 @@ public final class ExoPlayerTest { SimpleExoPlayer player, DefaultTrackSelector trackSelector, @Nullable Surface surface) { - maskedPlaybackSpeeds.add(player.getPlaybackSpeed()); + maskedPlaybackSpeeds.add(player.getPlaybackParameters().speed); } }; ActionSchedule actionSchedule = new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) - .setPlaybackSpeed(1.1f) + .setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.1f)) .apply(getPlaybackSpeedAction) - .setPlaybackSpeed(1.2f) + .setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.2f)) .apply(getPlaybackSpeedAction) - .setPlaybackSpeed(1.3f) + .setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.3f)) .apply(getPlaybackSpeedAction) .play() .build(); @@ -3397,8 +3397,8 @@ public final class ExoPlayerTest { EventListener listener = new EventListener() { @Override - public void onPlaybackSpeedChanged(float playbackSpeed) { - reportedPlaybackSpeeds.add(playbackSpeed); + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + reportedPlaybackSpeeds.add(playbackParameters.speed); } }; new ExoPlayerTestRunner.Builder(context) @@ -3424,28 +3424,28 @@ public final class ExoPlayerTest { } @Override - public void setPlaybackSpeed(float playbackSpeed) {} + public void setPlaybackParameters(PlaybackParameters playbackParameters) {} @Override - public float getPlaybackSpeed() { - return Player.DEFAULT_PLAYBACK_SPEED; + public PlaybackParameters getPlaybackParameters() { + return PlaybackParameters.DEFAULT; } }; ActionSchedule actionSchedule = new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) - .setPlaybackSpeed(1.1f) - .setPlaybackSpeed(1.2f) - .setPlaybackSpeed(1.3f) + .setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.1f)) + .setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.2f)) + .setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.3f)) .play() .build(); - List reportedPlaybackParameters = new ArrayList<>(); + List reportedPlaybackParameters = new ArrayList<>(); EventListener listener = new EventListener() { @Override - public void onPlaybackSpeedChanged(float playbackSpeed) { - reportedPlaybackParameters.add(playbackSpeed); + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + reportedPlaybackParameters.add(playbackParameters); } }; new ExoPlayerTestRunner.Builder(context) @@ -3458,7 +3458,11 @@ public final class ExoPlayerTest { .blockUntilEnded(TIMEOUT_MS); assertThat(reportedPlaybackParameters) - .containsExactly(1.1f, 1.2f, 1.3f, Player.DEFAULT_PLAYBACK_SPEED) + .containsExactly( + new PlaybackParameters(/* speed= */ 1.1f), + new PlaybackParameters(/* speed= */ 1.2f), + new PlaybackParameters(/* speed= */ 1.3f), + PlaybackParameters.DEFAULT) .inOrder(); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java index 7fa586c323..20be8fe12b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java @@ -435,7 +435,7 @@ public final class MediaPeriodQueueTest { /* loadingMediaPeriodId= */ null, /* playWhenReady= */ false, Player.PLAYBACK_SUPPRESSION_REASON_NONE, - /* playbackSpeed= */ Player.DEFAULT_PLAYBACK_SPEED, + /* playbackParameters= */ PlaybackParameters.DEFAULT, /* bufferedPositionUs= */ 0, /* totalBufferedDurationUs= */ 0, /* positionUs= */ 0, 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 7d3dfdf103..1238831cbc 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 @@ -26,6 +26,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RenderersFactory; @@ -82,7 +83,7 @@ public final class AnalyticsCollectorTest { private static final int EVENT_POSITION_DISCONTINUITY = 2; private static final int EVENT_SEEK_STARTED = 3; private static final int EVENT_SEEK_PROCESSED = 4; - private static final int EVENT_PLAYBACK_SPEED_CHANGED = 5; + private static final int EVENT_PLAYBACK_PARAMETERS_CHANGED = 5; private static final int EVENT_REPEAT_MODE_CHANGED = 6; private static final int EVENT_SHUFFLE_MODE_CHANGED = 7; private static final int EVENT_LOADING_CHANGED = 8; @@ -106,21 +107,22 @@ public final class AnalyticsCollectorTest { private static final int EVENT_AUDIO_INPUT_FORMAT_CHANGED = 26; private static final int EVENT_AUDIO_DISABLED = 27; private static final int EVENT_AUDIO_SESSION_ID = 28; - private static final int EVENT_AUDIO_UNDERRUN = 29; - private static final int EVENT_VIDEO_ENABLED = 30; - private static final int EVENT_VIDEO_DECODER_INIT = 31; - private static final int EVENT_VIDEO_INPUT_FORMAT_CHANGED = 32; - private static final int EVENT_DROPPED_FRAMES = 33; - private static final int EVENT_VIDEO_DISABLED = 34; - private static final int EVENT_RENDERED_FIRST_FRAME = 35; - private static final int EVENT_VIDEO_FRAME_PROCESSING_OFFSET = 36; - private static final int EVENT_VIDEO_SIZE_CHANGED = 37; - private static final int EVENT_DRM_KEYS_LOADED = 38; - private static final int EVENT_DRM_ERROR = 39; - private static final int EVENT_DRM_KEYS_RESTORED = 40; - private static final int EVENT_DRM_KEYS_REMOVED = 41; - private static final int EVENT_DRM_SESSION_ACQUIRED = 42; - private static final int EVENT_DRM_SESSION_RELEASED = 43; + private static final int EVENT_AUDIO_POSITION_ADVANCING = 29; + private static final int EVENT_AUDIO_UNDERRUN = 30; + private static final int EVENT_VIDEO_ENABLED = 31; + private static final int EVENT_VIDEO_DECODER_INIT = 32; + private static final int EVENT_VIDEO_INPUT_FORMAT_CHANGED = 33; + private static final int EVENT_DROPPED_FRAMES = 34; + private static final int EVENT_VIDEO_DISABLED = 35; + private static final int EVENT_RENDERED_FIRST_FRAME = 36; + private static final int EVENT_VIDEO_FRAME_PROCESSING_OFFSET = 37; + private static final int EVENT_VIDEO_SIZE_CHANGED = 38; + private static final int EVENT_DRM_KEYS_LOADED = 39; + private static final int EVENT_DRM_ERROR = 40; + private static final int EVENT_DRM_KEYS_RESTORED = 41; + private static final int EVENT_DRM_KEYS_REMOVED = 42; + private static final int EVENT_DRM_SESSION_ACQUIRED = 43; + private static final int EVENT_DRM_SESSION_RELEASED = 44; private static final UUID DRM_SCHEME_UUID = UUID.nameUUIDFromBytes(TestUtil.createByteArray(7, 8, 9)); @@ -226,6 +228,7 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_AUDIO_DECODER_INIT)).containsExactly(period0); assertThat(listener.getEvents(EVENT_AUDIO_INPUT_FORMAT_CHANGED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_INPUT_FORMAT_CHANGED)).containsExactly(period0); @@ -305,6 +308,7 @@ public final class AnalyticsCollectorTest { .containsExactly(period0, period1) .inOrder(); assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)) .containsExactly(period0, period1) @@ -380,6 +384,7 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_AUDIO_DECODER_INIT)).containsExactly(period1); assertThat(listener.getEvents(EVENT_AUDIO_INPUT_FORMAT_CHANGED)).containsExactly(period1); assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period1); + assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING)).containsExactly(period1); assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_INPUT_FORMAT_CHANGED)).containsExactly(period0); @@ -476,6 +481,9 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)) .containsExactly(period0, period1) .inOrder(); + assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING)) + .containsExactly(period0, period1) + .inOrder(); assertThat(listener.getEvents(EVENT_AUDIO_DISABLED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)).containsExactly(period0); @@ -576,6 +584,9 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)) .containsExactly(period1Seq1, period1Seq2) .inOrder(); + assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING)) + .containsExactly(period1Seq1, period1Seq2) + .inOrder(); assertThat(listener.getEvents(EVENT_AUDIO_DISABLED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0, period0); assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)) @@ -1785,8 +1796,9 @@ public final class AnalyticsCollectorTest { } @Override - public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) { - reportedEvents.add(new ReportedEvent(EVENT_PLAYBACK_SPEED_CHANGED, eventTime)); + public void onPlaybackParametersChanged( + EventTime eventTime, PlaybackParameters playbackParameters) { + reportedEvents.add(new ReportedEvent(EVENT_PLAYBACK_PARAMETERS_CHANGED, eventTime)); } @Override @@ -1922,6 +1934,11 @@ public final class AnalyticsCollectorTest { reportedEvents.add(new ReportedEvent(EVENT_AUDIO_SESSION_ID, eventTime)); } + @Override + public void onAudioPositionAdvancing(EventTime eventTime, long playoutStartSystemTimeMs) { + reportedEvents.add(new ReportedEvent(EVENT_AUDIO_POSITION_ADVANCING, eventTime)); + } + @Override public void onAudioUnderrun( EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java index 8a1f8807ea..1f19c2af58 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.verify; import android.os.SystemClock; import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; @@ -75,8 +76,8 @@ public final class PlaybackStatsListenerTest { playbackStatsListener.onPositionDiscontinuity( EMPTY_TIMELINE_EVENT_TIME, Player.DISCONTINUITY_REASON_SEEK); - playbackStatsListener.onPlaybackSpeedChanged( - EMPTY_TIMELINE_EVENT_TIME, /* playbackSpeed= */ 2.0f); + playbackStatsListener.onPlaybackParametersChanged( + EMPTY_TIMELINE_EVENT_TIME, new PlaybackParameters(/* speed= */ 2.0f)); playbackStatsListener.onPlayWhenReadyChanged( EMPTY_TIMELINE_EVENT_TIME, /* playWhenReady= */ true, 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 54628f91be..2f86988d42 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 @@ -25,6 +25,7 @@ import static org.robolectric.annotation.Config.TARGET_SDK; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.util.MimeTypes; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -89,7 +90,7 @@ public final class DefaultAudioSinkTest { @Test public void handlesBufferAfterReset_withPlaybackSpeed() throws Exception { - defaultAudioSink.setPlaybackSpeed(/* playbackSpeed= */ 1.5f); + defaultAudioSink.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.5f)); configureDefaultAudioSink(CHANNEL_COUNT_STEREO); defaultAudioSink.handleBuffer( createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); @@ -99,7 +100,8 @@ public final class DefaultAudioSinkTest { configureDefaultAudioSink(CHANNEL_COUNT_STEREO); defaultAudioSink.handleBuffer( createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); - assertThat(defaultAudioSink.getPlaybackSpeed()).isEqualTo(1.5f); + assertThat(defaultAudioSink.getPlaybackParameters()) + .isEqualTo(new PlaybackParameters(/* speed= */ 1.5f)); } @Test @@ -117,7 +119,7 @@ public final class DefaultAudioSinkTest { @Test public void handlesBufferAfterReset_withFormatChangeAndPlaybackSpeed() throws Exception { - defaultAudioSink.setPlaybackSpeed(/* playbackSpeed= */ 1.5f); + defaultAudioSink.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.5f)); configureDefaultAudioSink(CHANNEL_COUNT_STEREO); defaultAudioSink.handleBuffer( createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); @@ -127,7 +129,8 @@ public final class DefaultAudioSinkTest { configureDefaultAudioSink(CHANNEL_COUNT_MONO); defaultAudioSink.handleBuffer( createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); - assertThat(defaultAudioSink.getPlaybackSpeed()).isEqualTo(1.5f); + assertThat(defaultAudioSink.getPlaybackParameters()) + .isEqualTo(new PlaybackParameters(/* speed= */ 1.5f)); } @Test diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/MediaCodecAudioRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/MediaCodecAudioRendererTest.java index 82c8824de1..922431d210 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/MediaCodecAudioRendererTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/MediaCodecAudioRendererTest.java @@ -49,8 +49,10 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import org.robolectric.annotation.Config; /** Unit tests for {@link MediaCodecAudioRenderer} */ +@Config(sdk = 29) @RunWith(AndroidJUnit4.class) public class MediaCodecAudioRendererTest { @Rule public final MockitoRule mockito = MockitoJUnit.rule(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java new file mode 100644 index 0000000000..684399d845 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java @@ -0,0 +1,64 @@ +/* + * 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.e2etest; + +import android.graphics.SurfaceTexture; +import android.view.Surface; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock; +import com.google.android.exoplayer2.testutil.DumpFileAsserts; +import com.google.android.exoplayer2.testutil.PlaybackOutput; +import com.google.android.exoplayer2.testutil.ShadowMediaCodecConfig; +import com.google.android.exoplayer2.testutil.TestExoPlayer; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +/** End-to-end tests using MP4 samples. */ +// TODO(b/143232359): Remove once https://issuetracker.google.com/143232359 is resolved. +@Config(sdk = 29) +@RunWith(AndroidJUnit4.class) +public class Mp4PlaybackTest { + @Rule + public ShadowMediaCodecConfig mediaCodecConfig = + ShadowMediaCodecConfig.forAllSupportedMimeTypes(); + + @Test + public void h264VideoAacAudio() throws Exception { + SimpleExoPlayer player = + new SimpleExoPlayer.Builder(ApplicationProvider.getApplicationContext()) + .setClock(new AutoAdvancingFakeClock()) + .build(); + player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1))); + PlaybackOutput playbackOutput = PlaybackOutput.register(player, mediaCodecConfig); + + player.setMediaItem(MediaItem.fromUri("asset:///media/mp4/sample.mp4")); + player.prepare(); + player.play(); + TestExoPlayer.runUntilPlaybackState(player, Player.STATE_ENDED); + + DumpFileAsserts.assertOutput( + ApplicationProvider.getApplicationContext(), + playbackOutput, + "playbackdumps/mp4/sample.mp4.dump"); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/TsPlaybackTest.java b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/TsPlaybackTest.java new file mode 100644 index 0000000000..c78e4cfe96 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/TsPlaybackTest.java @@ -0,0 +1,64 @@ +/* + * 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.e2etest; + +import android.graphics.SurfaceTexture; +import android.view.Surface; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock; +import com.google.android.exoplayer2.testutil.DumpFileAsserts; +import com.google.android.exoplayer2.testutil.PlaybackOutput; +import com.google.android.exoplayer2.testutil.ShadowMediaCodecConfig; +import com.google.android.exoplayer2.testutil.TestExoPlayer; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +/** End-to-end tests using TS samples. */ +// TODO(b/143232359): Remove once https://issuetracker.google.com/143232359 is resolved. +@Config(sdk = 29) +@RunWith(AndroidJUnit4.class) +public class TsPlaybackTest { + + @Rule + public ShadowMediaCodecConfig mediaCodecConfig = + ShadowMediaCodecConfig.forAllSupportedMimeTypes(); + + @Test + public void mpegVideoMpegAudioScte35() throws Exception { + SimpleExoPlayer player = + new SimpleExoPlayer.Builder(ApplicationProvider.getApplicationContext()) + .setClock(new AutoAdvancingFakeClock()) + .build(); + player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1))); + PlaybackOutput playbackOutput = PlaybackOutput.register(player, mediaCodecConfig); + + player.setMediaItem(MediaItem.fromUri("asset:///media/ts/sample_scte35.ts")); + player.prepare(); + player.play(); + TestExoPlayer.runUntilPlaybackState(player, Player.STATE_ENDED); + + DumpFileAsserts.assertOutput( + ApplicationProvider.getApplicationContext(), + playbackOutput, + "playbackdumps/ts/sample_scte35.ts.dump"); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/MediaSourceDrmHelperTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/MediaSourceDrmHelperTest.java new file mode 100644 index 0000000000..45384f05ec --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/MediaSourceDrmHelperTest.java @@ -0,0 +1,53 @@ +/* + * 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.source; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link MediaSourceDrmHelper}. */ +@RunWith(AndroidJUnit4.class) +public class MediaSourceDrmHelperTest { + + @Test + public void create_noDrmProperties_createsNoopManager() { + DrmSessionManager drmSessionManager = + new MediaSourceDrmHelper().create(MediaItem.fromUri(Uri.EMPTY)); + + assertThat(drmSessionManager).isEqualTo(DrmSessionManager.DUMMY); + } + + @Test + public void create_createsManager() { + MediaItem mediaItem = + new MediaItem.Builder() + .setUri(Uri.EMPTY) + .setDrmLicenseUri(Uri.EMPTY) + .setDrmUuid(C.WIDEVINE_UUID) + .build(); + + DrmSessionManager drmSessionManager = new MediaSourceDrmHelper().create(mediaItem); + + assertThat(drmSessionManager).isNotEqualTo(DrmSessionManager.DUMMY); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java index 4583c542b3..54e2dd902d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -38,6 +38,7 @@ import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.Allocator; @@ -54,7 +55,6 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentMatchers; import org.mockito.Mockito; /** Test for {@link SampleQueue}. */ @@ -69,6 +69,8 @@ public final class SampleQueueTest { private static final Format FORMAT_SPLICED = buildFormat(/* id= */ "spliced"); private static final Format FORMAT_ENCRYPTED = new Format.Builder().setId(/* id= */ "encrypted").setDrmInitData(new DrmInitData()).build(); + private static final Format FORMAT_ENCRYPTED_WITH_EXO_MEDIA_CRYPTO_TYPE = + FORMAT_ENCRYPTED.copyWithExoMediaCryptoType(MockExoMediaCrypto.class); private static final byte[] DATA = TestUtil.buildTestData(ALLOCATION_SIZE * 10); /* @@ -128,7 +130,7 @@ public final class SampleQueueTest { new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, new byte[16], 0, 0); private Allocator allocator; - private DrmSessionManager mockDrmSessionManager; + private MockDrmSessionManager mockDrmSessionManager; private DrmSession mockDrmSession; private DrmSessionEventListener.EventDispatcher eventDispatcher; private SampleQueue sampleQueue; @@ -138,11 +140,8 @@ public final class SampleQueueTest { @Before public void setUp() { allocator = new DefaultAllocator(false, ALLOCATION_SIZE); - mockDrmSessionManager = Mockito.mock(DrmSessionManager.class); mockDrmSession = Mockito.mock(DrmSession.class); - when(mockDrmSessionManager.acquireSession( - ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any())) - .thenReturn(mockDrmSession); + mockDrmSessionManager = new MockDrmSessionManager(mockDrmSession); eventDispatcher = new DrmSessionEventListener.EventDispatcher(); sampleQueue = new SampleQueue( @@ -399,7 +398,7 @@ public final class SampleQueueTest { @Test public void isReadyReturnsTrueForValidDrmSession() { writeTestDataWithEncryptedSections(); - assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED); + assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED_WITH_EXO_MEDIA_CRYPTO_TYPE); assertThat(sampleQueue.isReady(/* loadingFinished= */ false)).isFalse(); when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); assertThat(sampleQueue.isReady(/* loadingFinished= */ false)).isTrue(); @@ -424,7 +423,7 @@ public final class SampleQueueTest { when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED); writeTestDataWithEncryptedSections(); - assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED); + assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED_WITH_EXO_MEDIA_CRYPTO_TYPE); assertReadNothing(/* formatRequired= */ false); assertThat(inputBuffer.waitingForKeys).isTrue(); when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); @@ -464,9 +463,7 @@ public final class SampleQueueTest { when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); DrmSession mockPlaceholderDrmSession = Mockito.mock(DrmSession.class); when(mockPlaceholderDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); - when(mockDrmSessionManager.acquirePlaceholderSession( - ArgumentMatchers.any(), ArgumentMatchers.anyInt())) - .thenReturn(mockPlaceholderDrmSession); + mockDrmSessionManager.mockPlaceholderDrmSession = mockPlaceholderDrmSession; writeTestDataWithEncryptedSections(); int result = @@ -497,9 +494,7 @@ public final class SampleQueueTest { when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); DrmSession mockPlaceholderDrmSession = Mockito.mock(DrmSession.class); when(mockPlaceholderDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); - when(mockDrmSessionManager.acquirePlaceholderSession( - ArgumentMatchers.any(), ArgumentMatchers.anyInt())) - .thenReturn(mockPlaceholderDrmSession); + mockDrmSessionManager.mockPlaceholderDrmSession = mockPlaceholderDrmSession; writeFormat(ENCRYPTED_SAMPLE_FORMATS[0]); byte[] sampleData = new byte[] {0, 1, 2}; @@ -540,7 +535,7 @@ public final class SampleQueueTest { when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED); writeTestDataWithEncryptedSections(); - assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED); + assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED_WITH_EXO_MEDIA_CRYPTO_TYPE); assertReadNothing(/* formatRequired= */ false); sampleQueue.maybeThrowError(); when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_ERROR); @@ -569,7 +564,7 @@ public final class SampleQueueTest { when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED); writeTestDataWithEncryptedSections(); - assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED); + assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED_WITH_EXO_MEDIA_CRYPTO_TYPE); assertReadEncryptedSample(/* sampleIndex= */ 0); } @@ -1497,4 +1492,33 @@ public final class SampleQueueTest { private static Format copyWithLabel(Format format, String label) { return format.buildUpon().setLabel(label).build(); } + + private static final class MockExoMediaCrypto implements ExoMediaCrypto {} + + private static final class MockDrmSessionManager implements DrmSessionManager { + + private final DrmSession mockDrmSession; + @Nullable private DrmSession mockPlaceholderDrmSession; + + private MockDrmSessionManager(DrmSession mockDrmSession) { + this.mockDrmSession = mockDrmSession; + } + + @Nullable + @Override + public DrmSession acquireSession( + Looper playbackLooper, + @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, + Format format) { + return format.drmInitData != null ? mockDrmSession : mockPlaceholderDrmSession; + } + + @Nullable + @Override + public Class getExoMediaCryptoType(Format format) { + return mockPlaceholderDrmSession != null || format.drmInitData != null + ? MockExoMediaCrypto.class + : null; + } + } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 260c11fa62..2f5b169e30 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -30,7 +30,6 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.ParserException; 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.drm.DrmSessionManager; import com.google.android.exoplayer2.offline.FilteringManifestParser; @@ -42,6 +41,7 @@ import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceDrmHelper; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.MediaSourceFactory; @@ -54,6 +54,7 @@ import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy.LoadErrorInfo; import com.google.android.exoplayer2.upstream.Loader; @@ -91,9 +92,10 @@ public final class DashMediaSource extends BaseMediaSource { public static final class Factory implements MediaSourceFactory { private final DashChunkSource.Factory chunkSourceFactory; + private final MediaSourceDrmHelper mediaSourceDrmHelper; @Nullable private final DataSource.Factory manifestDataSourceFactory; - private DrmSessionManager drmSessionManager; + @Nullable private DrmSessionManager drmSessionManager; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private long livePresentationDelayMs; @@ -126,7 +128,7 @@ public final class DashMediaSource extends BaseMediaSource { @Nullable DataSource.Factory manifestDataSourceFactory) { this.chunkSourceFactory = checkNotNull(chunkSourceFactory); this.manifestDataSourceFactory = manifestDataSourceFactory; - drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); + mediaSourceDrmHelper = new MediaSourceDrmHelper(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); @@ -155,19 +157,22 @@ public final class DashMediaSource extends BaseMediaSource { return this; } - /** - * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The - * default value is {@link DrmSessionManager#DUMMY}. - * - * @param drmSessionManager The {@link DrmSessionManager}. - * @return This factory, for convenience. - */ @Override public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { - this.drmSessionManager = - drmSessionManager != null - ? drmSessionManager - : DrmSessionManager.getDummyDrmSessionManager(); + this.drmSessionManager = drmSessionManager; + return this; + } + + @Override + public Factory setDrmHttpDataSourceFactory( + @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { + mediaSourceDrmHelper.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory); + return this; + } + + @Override + public Factory setDrmUserAgent(@Nullable String userAgent) { + mediaSourceDrmHelper.setDrmUserAgent(userAgent); return this; } @@ -312,7 +317,7 @@ public final class DashMediaSource extends BaseMediaSource { /* manifestParser= */ null, chunkSourceFactory, compositeSequenceableLoaderFactory, - drmSessionManager, + drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem), loadErrorHandlingPolicy, livePresentationDelayMs, livePresentationDelayOverridesManifest); @@ -403,7 +408,7 @@ public final class DashMediaSource extends BaseMediaSource { manifestParser, chunkSourceFactory, compositeSequenceableLoaderFactory, - drmSessionManager, + drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem), loadErrorHandlingPolicy, livePresentationDelayMs, livePresentationDelayOverridesManifest); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java index a59cb1d1f2..95b1daeb6e 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java @@ -20,10 +20,34 @@ package com.google.android.exoplayer2.extractor; */ public interface ExtractorOutput { + /** + * Placeholder {@link ExtractorOutput} implementation throwing an {@link + * UnsupportedOperationException} in each method. + */ + ExtractorOutput PLACEHOLDER = + new ExtractorOutput() { + + @Override + public TrackOutput track(int id, int type) { + throw new UnsupportedOperationException(); + } + + @Override + public void endTracks() { + throw new UnsupportedOperationException(); + } + + @Override + public void seekMap(SeekMap seekMap) { + throw new UnsupportedOperationException(); + } + }; + /** * Called by the {@link Extractor} to get the {@link TrackOutput} for a specific track. - *

- * The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}. + * + *

The same {@link TrackOutput} is returned if multiple calls are made with the same {@code + * id}. * * @param id A track identifier. * @param type The type of the track. Typically one of the {@link com.google.android.exoplayer2.C} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 287ebc6ce6..660605ebe5 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -48,6 +48,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.AvcConfig; import com.google.android.exoplayer2.video.ColorInfo; +import com.google.android.exoplayer2.video.DolbyVisionConfig; import com.google.android.exoplayer2.video.HevcConfig; import java.io.IOException; import java.lang.annotation.Documented; @@ -170,6 +171,9 @@ public class MatroskaExtractor implements Extractor { private static final int ID_FLAG_FORCED = 0x55AA; private static final int ID_DEFAULT_DURATION = 0x23E383; private static final int ID_MAX_BLOCK_ADDITION_ID = 0x55EE; + private static final int ID_BLOCK_ADDITION_MAPPING = 0x41E4; + private static final int ID_BLOCK_ADD_ID_TYPE = 0x41E7; + private static final int ID_BLOCK_ADD_ID_EXTRA_DATA = 0x41ED; private static final int ID_NAME = 0x536E; private static final int ID_CODEC_ID = 0x86; private static final int ID_CODEC_PRIVATE = 0x63A2; @@ -234,6 +238,17 @@ public class MatroskaExtractor implements Extractor { */ private static final int BLOCK_ADDITIONAL_ID_VP9_ITU_T_35 = 4; + /** + * BlockAddIdType value for Dolby Vision configuration with profile <= 7. See also + * https://www.matroska.org/technical/codec_specs.html. + */ + private static final int BLOCK_ADD_ID_TYPE_DVCC = 0x64766343; + /** + * BlockAddIdType value for Dolby Vision configuration with profile > 7. See also + * https://www.matroska.org/technical/codec_specs.html. + */ + private static final int BLOCK_ADD_ID_TYPE_DVVC = 0x64767643; + private static final int LACING_NONE = 0; private static final int LACING_XIPH = 1; private static final int LACING_FIXED_SIZE = 2; @@ -501,6 +516,7 @@ public class MatroskaExtractor implements Extractor { case ID_CLUSTER: case ID_TRACKS: case ID_TRACK_ENTRY: + case ID_BLOCK_ADDITION_MAPPING: case ID_AUDIO: case ID_VIDEO: case ID_CONTENT_ENCODINGS: @@ -535,6 +551,7 @@ public class MatroskaExtractor implements Extractor { case ID_FLAG_FORCED: case ID_DEFAULT_DURATION: case ID_MAX_BLOCK_ADDITION_ID: + case ID_BLOCK_ADD_ID_TYPE: case ID_CODEC_DELAY: case ID_SEEK_PRE_ROLL: case ID_CHANNELS: @@ -562,6 +579,7 @@ public class MatroskaExtractor implements Extractor { case ID_LANGUAGE: return EbmlProcessor.ELEMENT_TYPE_STRING; case ID_SEEK_ID: + case ID_BLOCK_ADD_ID_EXTRA_DATA: case ID_CONTENT_COMPRESSION_SETTINGS: case ID_CONTENT_ENCRYPTION_KEY_ID: case ID_SIMPLE_BLOCK: @@ -814,6 +832,9 @@ public class MatroskaExtractor implements Extractor { case ID_MAX_BLOCK_ADDITION_ID: currentTrack.maxBlockAdditionId = (int) value; break; + case ID_BLOCK_ADD_ID_TYPE: + currentTrack.blockAddIdType = (int) value; + break; case ID_CODEC_DELAY: currentTrack.codecDelayNs = value; break; @@ -1076,6 +1097,9 @@ public class MatroskaExtractor implements Extractor { seekEntryIdBytes.setPosition(0); seekEntryId = (int) seekEntryIdBytes.readUnsignedInt(); break; + case ID_BLOCK_ADD_ID_EXTRA_DATA: + handleBlockAddIDExtraData(currentTrack, input, contentSize); + break; case ID_CODEC_PRIVATE: currentTrack.codecPrivate = new byte[contentSize]; input.readFully(currentTrack.codecPrivate, 0, contentSize); @@ -1244,6 +1268,18 @@ public class MatroskaExtractor implements Extractor { } } + protected void handleBlockAddIDExtraData(Track track, ExtractorInput input, int contentSize) + throws IOException { + if (track.blockAddIdType == BLOCK_ADD_ID_TYPE_DVVC + || track.blockAddIdType == BLOCK_ADD_ID_TYPE_DVCC) { + track.dolbyVisionConfigBytes = new byte[contentSize]; + input.readFully(track.dolbyVisionConfigBytes, 0, contentSize); + } else { + // Unhandled BlockAddIDExtraData. + input.skipFully(contentSize); + } + } + protected void handleBlockAdditionalData( Track track, int blockAdditionalId, ExtractorInput input, int contentSize) throws IOException { @@ -1883,6 +1919,7 @@ public class MatroskaExtractor implements Extractor { public int type; public int defaultSampleDurationNs; public int maxBlockAdditionId; + private int blockAddIdType; public boolean hasContentEncryption; public byte[] sampleStrippedBytes; public TrackOutput.CryptoData cryptoData; @@ -1921,6 +1958,7 @@ public class MatroskaExtractor implements Extractor { public float whitePointChromaticityY = Format.NO_VALUE; public float maxMasteringLuminance = Format.NO_VALUE; public float minMasteringLuminance = Format.NO_VALUE; + @Nullable public byte[] dolbyVisionConfigBytes; // Audio elements. Initially set to their default values. public int channelCount = 1; @@ -2091,6 +2129,16 @@ public class MatroskaExtractor implements Extractor { throw new ParserException("Unrecognized codec identifier."); } + if (dolbyVisionConfigBytes != null) { + @Nullable + DolbyVisionConfig dolbyVisionConfig = + DolbyVisionConfig.parse(new ParsableByteArray(this.dolbyVisionConfigBytes)); + if (dolbyVisionConfig != null) { + codecs = dolbyVisionConfig.codecs; + mimeType = MimeTypes.VIDEO_DOLBY_VISION; + } + } + @C.SelectionFlags int selectionFlags = 0; selectionFlags |= flagDefault ? C.SELECTION_FLAG_DEFAULT : 0; selectionFlags |= flagForced ? C.SELECTION_FLAG_FORCED : 0; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index 756cd43fcc..859ce49b26 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -16,6 +16,10 @@ package com.google.android.exoplayer2.extractor.mp4; import static com.google.android.exoplayer2.extractor.mp4.AtomParsers.parseTraks; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkState; +import static com.google.android.exoplayer2.util.Util.castNonNull; +import static com.google.android.exoplayer2.util.Util.nullSafeArrayCopy; import static java.lang.Math.max; import android.util.Pair; @@ -42,7 +46,6 @@ import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom; import com.google.android.exoplayer2.extractor.mp4.Atom.LeafAtom; import com.google.android.exoplayer2.metadata.emsg.EventMessage; import com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; @@ -59,7 +62,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.UUID; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** Extracts data from the FMP4 container format. */ @SuppressWarnings("ConstantField") @@ -175,7 +177,7 @@ public class FragmentedMp4Extractor implements Extractor { private boolean processSeiNalUnitPayload; // Outputs. - private @MonotonicNonNull ExtractorOutput extractorOutput; + private ExtractorOutput extractorOutput; private TrackOutput[] emsgTrackOutputs; private TrackOutput[] ceaTrackOutputs; @@ -270,9 +272,9 @@ public class FragmentedMp4Extractor implements Extractor { durationUs = C.TIME_UNSET; pendingSeekTimeUs = C.TIME_UNSET; segmentIndexEarliestPresentationTimeUs = C.TIME_UNSET; + extractorOutput = ExtractorOutput.PLACEHOLDER; emsgTrackOutputs = new TrackOutput[0]; ceaTrackOutputs = new TrackOutput[0]; - enterReadingAtomHeaderState(); } @Override @@ -283,6 +285,7 @@ public class FragmentedMp4Extractor implements Extractor { @Override public void init(ExtractorOutput output) { extractorOutput = output; + enterReadingAtomHeaderState(); initExtraTracks(); if (sideloadedTrack != null) { TrackBundle bundle = @@ -429,8 +432,9 @@ public class FragmentedMp4Extractor implements Extractor { if (atomSize > Integer.MAX_VALUE) { throw new ParserException("Leaf atom with length > 2147483647 (unsupported)."); } - atomData = new ParsableByteArray((int) atomSize); + ParsableByteArray atomData = new ParsableByteArray((int) atomSize); System.arraycopy(atomHeader.getData(), 0, atomData.getData(), 0, Atom.HEADER_SIZE); + this.atomData = atomData; parserState = STATE_READING_ATOM_PAYLOAD; } else { if (atomSize > Integer.MAX_VALUE) { @@ -445,6 +449,7 @@ public class FragmentedMp4Extractor implements Extractor { private void readAtomPayload(ExtractorInput input) throws IOException { int atomPayloadSize = (int) atomSize - atomHeaderBytesRead; + @Nullable ParsableByteArray atomData = this.atomData; if (atomData != null) { input.readFully(atomData.getData(), Atom.HEADER_SIZE, atomPayloadSize); onLeafAtomRead(new LeafAtom(atomType, atomData), input.getPosition()); @@ -485,12 +490,12 @@ public class FragmentedMp4Extractor implements Extractor { } private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException { - Assertions.checkState(sideloadedTrack == null, "Unexpected moov box."); + checkState(sideloadedTrack == null, "Unexpected moov box."); @Nullable DrmInitData drmInitData = getDrmInitDataFromAtoms(moov.leafChildren); - // Read declaration of track fragments in the Moov box. - ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex); + // Read declaration of track fragments in the moov box. + ContainerAtom mvex = checkNotNull(moov.getContainerAtomOfType(Atom.TYPE_mvex)); SparseArray defaultSampleValuesArray = new SparseArray<>(); long duration = C.TIME_UNSET; int mvexChildrenSize = mvex.leafChildren.size(); @@ -531,7 +536,7 @@ public class FragmentedMp4Extractor implements Extractor { } extractorOutput.endTracks(); } else { - Assertions.checkState(trackBundles.size() == trackCount); + checkState(trackBundles.size() == trackCount); for (int i = 0; i < trackCount; i++) { TrackSampleTable sampleTable = sampleTables.get(i); Track track = sampleTable.track; @@ -554,7 +559,7 @@ public class FragmentedMp4Extractor implements Extractor { // See https://github.com/google/ExoPlayer/issues/4477. return defaultSampleValuesArray.valueAt(/* index= */ 0); } - return Assertions.checkNotNull(defaultSampleValuesArray.get(trackId)); + return checkNotNull(defaultSampleValuesArray.get(trackId)); } private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException { @@ -589,7 +594,7 @@ public class FragmentedMp4Extractor implements Extractor { emsgTrackOutputs[emsgTrackOutputCount++] = extractorOutput.track(nextExtraTrackId++, C.TRACK_TYPE_METADATA); } - emsgTrackOutputs = Arrays.copyOf(emsgTrackOutputs, emsgTrackOutputCount); + emsgTrackOutputs = nullSafeArrayCopy(emsgTrackOutputs, emsgTrackOutputCount); for (TrackOutput eventMessageTrackOutput : emsgTrackOutputs) { eventMessageTrackOutput.format(EMSG_FORMAT); } @@ -604,7 +609,7 @@ public class FragmentedMp4Extractor implements Extractor { /** Handles an emsg atom (defined in 23009-1). */ private void onEmsgLeafAtomRead(ParsableByteArray atom) { - if (emsgTrackOutputs == null || emsgTrackOutputs.length == 0) { + if (emsgTrackOutputs.length == 0) { return; } atom.setPosition(Atom.HEADER_SIZE); @@ -619,8 +624,8 @@ public class FragmentedMp4Extractor implements Extractor { long id; switch (version) { case 0: - schemeIdUri = Assertions.checkNotNull(atom.readNullTerminatedString()); - value = Assertions.checkNotNull(atom.readNullTerminatedString()); + schemeIdUri = checkNotNull(atom.readNullTerminatedString()); + value = checkNotNull(atom.readNullTerminatedString()); timescale = atom.readUnsignedInt(); presentationTimeDeltaUs = Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale); @@ -638,8 +643,8 @@ public class FragmentedMp4Extractor implements Extractor { durationMs = Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); id = atom.readUnsignedInt(); - schemeIdUri = Assertions.checkNotNull(atom.readNullTerminatedString()); - value = Assertions.checkNotNull(atom.readNullTerminatedString()); + schemeIdUri = checkNotNull(atom.readNullTerminatedString()); + value = checkNotNull(atom.readNullTerminatedString()); break; default: Log.w(TAG, "Skipping unsupported emsg version: " + version); @@ -717,7 +722,7 @@ public class FragmentedMp4Extractor implements Extractor { */ private static void parseTraf(ContainerAtom traf, SparseArray trackBundleArray, @Flags int flags, byte[] extendedTypeScratch) throws ParserException { - LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd); + LeafAtom tfhd = checkNotNull(traf.getLeafAtomOfType(Atom.TYPE_tfhd)); @Nullable TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray); if (trackBundle == null) { return; @@ -730,7 +735,7 @@ public class FragmentedMp4Extractor implements Extractor { trackBundle.currentlyInFragment = true; @Nullable LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt); if (tfdtAtom != null && (flags & FLAG_WORKAROUND_IGNORE_TFDT_BOX) == 0) { - fragment.nextFragmentDecodeTime = parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data); + fragment.nextFragmentDecodeTime = parseTfdt(tfdtAtom.data); fragment.nextFragmentDecodeTimeIncludesMoov = true; } else { fragment.nextFragmentDecodeTime = fragmentDecodeTime; @@ -742,11 +747,11 @@ public class FragmentedMp4Extractor implements Extractor { @Nullable TrackEncryptionBox encryptionBox = trackBundle.moovSampleTable.track.getSampleDescriptionEncryptionBox( - fragment.header.sampleDescriptionIndex); + checkNotNull(fragment.header).sampleDescriptionIndex); @Nullable LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz); if (saiz != null) { - parseSaiz(encryptionBox, saiz.data, fragment); + parseSaiz(checkNotNull(encryptionBox), saiz.data, fragment); } @Nullable LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio); @@ -964,7 +969,7 @@ public class FragmentedMp4Extractor implements Extractor { Track track = trackBundle.moovSampleTable.track; TrackFragment fragment = trackBundle.fragment; - DefaultSampleValues defaultSampleValues = fragment.header; + DefaultSampleValues defaultSampleValues = castNonNull(fragment.header); fragment.trunLength[index] = trun.readUnsignedIntToInt(); fragment.trunDataPosition[index] = fragment.dataPosition; @@ -994,7 +999,7 @@ public class FragmentedMp4Extractor implements Extractor { && track.editListDurations[0] == 0) { edtsOffsetUs = Util.scaleLargeTimestamp( - track.editListMediaTimes[0], C.MICROS_PER_SECOND, track.timescale); + castNonNull(track.editListMediaTimes)[0], C.MICROS_PER_SECOND, track.timescale); } int[] sampleSizeTable = fragment.sampleSizeTable; @@ -1161,7 +1166,7 @@ public class FragmentedMp4Extractor implements Extractor { int perSampleIvSize = sgpd.readUnsignedByte(); byte[] keyId = new byte[16]; sgpd.readBytes(keyId, 0, keyId.length); - byte[] constantIv = null; + @Nullable byte[] constantIv = null; if (perSampleIvSize == 0) { int constantIvSize = sgpd.readUnsignedByte(); constantIv = new byte[constantIvSize]; @@ -1238,7 +1243,7 @@ public class FragmentedMp4Extractor implements Extractor { } private void readEncryptionData(ExtractorInput input) throws IOException { - TrackBundle nextTrackBundle = null; + @Nullable TrackBundle nextTrackBundle = null; long nextDataOffset = Long.MAX_VALUE; int trackBundlesSize = trackBundles.size(); for (int i = 0; i < trackBundlesSize; i++) { @@ -1277,71 +1282,70 @@ public class FragmentedMp4Extractor implements Extractor { * @throws IOException If an error occurs reading from the input. */ private boolean readSample(ExtractorInput input) throws IOException { - if (parserState == STATE_READING_SAMPLE_START) { - if (currentTrackBundle == null) { - @Nullable TrackBundle currentTrackBundle = getNextTrackBundle(trackBundles); - if (currentTrackBundle == null) { - // We've run out of samples in the current mdat. Discard any trailing data and prepare to - // read the header of the next atom. - int bytesToSkip = (int) (endOfMdatPosition - input.getPosition()); - if (bytesToSkip < 0) { - throw new ParserException("Offset to end of mdat was negative."); - } - input.skipFully(bytesToSkip); - enterReadingAtomHeaderState(); - return false; - } - - long nextDataPosition = currentTrackBundle.getCurrentSampleOffset(); - // We skip bytes preceding the next sample to read. - int bytesToSkip = (int) (nextDataPosition - input.getPosition()); + @Nullable TrackBundle trackBundle = currentTrackBundle; + if (trackBundle == null) { + trackBundle = getNextTrackBundle(trackBundles); + if (trackBundle == null) { + // We've run out of samples in the current mdat. Discard any trailing data and prepare to + // read the header of the next atom. + int bytesToSkip = (int) (endOfMdatPosition - input.getPosition()); if (bytesToSkip < 0) { - // Assume the sample data must be contiguous in the mdat with no preceding data. - Log.w(TAG, "Ignoring negative offset to sample data."); - bytesToSkip = 0; + throw new ParserException("Offset to end of mdat was negative."); } input.skipFully(bytesToSkip); - this.currentTrackBundle = currentTrackBundle; + enterReadingAtomHeaderState(); + return false; } - sampleSize = currentTrackBundle.getCurrentSampleSize(); + long nextDataPosition = trackBundle.getCurrentSampleOffset(); + // We skip bytes preceding the next sample to read. + int bytesToSkip = (int) (nextDataPosition - input.getPosition()); + if (bytesToSkip < 0) { + // Assume the sample data must be contiguous in the mdat with no preceding data. + Log.w(TAG, "Ignoring negative offset to sample data."); + bytesToSkip = 0; + } + input.skipFully(bytesToSkip); + currentTrackBundle = trackBundle; + } + if (parserState == STATE_READING_SAMPLE_START) { + sampleSize = trackBundle.getCurrentSampleSize(); - if (currentTrackBundle.currentSampleIndex < currentTrackBundle.firstSampleToOutputIndex) { + if (trackBundle.currentSampleIndex < trackBundle.firstSampleToOutputIndex) { input.skipFully(sampleSize); - currentTrackBundle.skipSampleEncryptionData(); - if (!currentTrackBundle.next()) { + trackBundle.skipSampleEncryptionData(); + if (!trackBundle.next()) { currentTrackBundle = null; } parserState = STATE_READING_SAMPLE_START; return true; } - if (currentTrackBundle.moovSampleTable.track.sampleTransformation + if (trackBundle.moovSampleTable.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) { sampleSize -= Atom.HEADER_SIZE; input.skipFully(Atom.HEADER_SIZE); } - if (MimeTypes.AUDIO_AC4.equals( - currentTrackBundle.moovSampleTable.track.format.sampleMimeType)) { + if (MimeTypes.AUDIO_AC4.equals(trackBundle.moovSampleTable.track.format.sampleMimeType)) { // AC4 samples need to be prefixed with a clear sample header. sampleBytesWritten = - currentTrackBundle.outputSampleEncryptionData(sampleSize, Ac4Util.SAMPLE_HEADER_SIZE); + trackBundle.outputSampleEncryptionData(sampleSize, Ac4Util.SAMPLE_HEADER_SIZE); Ac4Util.getAc4SampleHeader(sampleSize, scratch); - currentTrackBundle.output.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE); + trackBundle.output.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE); sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE; } else { sampleBytesWritten = - currentTrackBundle.outputSampleEncryptionData(sampleSize, /* clearHeaderSize= */ 0); + trackBundle.outputSampleEncryptionData(sampleSize, /* clearHeaderSize= */ 0); } sampleSize += sampleBytesWritten; parserState = STATE_READING_SAMPLE_CONTINUE; sampleCurrentNalBytesRemaining = 0; } - Track track = currentTrackBundle.moovSampleTable.track; - TrackOutput output = currentTrackBundle.output; - long sampleTimeUs = currentTrackBundle.getCurrentSamplePresentationTimeUs(); + Track track = trackBundle.moovSampleTable.track; + TrackOutput output = trackBundle.output; + long sampleTimeUs = trackBundle.getCurrentSamplePresentationTimeUs(); if (timestampAdjuster != null) { sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs); } @@ -1407,11 +1411,11 @@ public class FragmentedMp4Extractor implements Extractor { } } - @C.BufferFlags int sampleFlags = currentTrackBundle.getCurrentSampleFlags(); + @C.BufferFlags int sampleFlags = trackBundle.getCurrentSampleFlags(); // Encryption data. - TrackOutput.CryptoData cryptoData = null; - TrackEncryptionBox encryptionBox = currentTrackBundle.getEncryptionBoxIfEncrypted(); + @Nullable TrackOutput.CryptoData cryptoData = null; + @Nullable TrackEncryptionBox encryptionBox = trackBundle.getEncryptionBoxIfEncrypted(); if (encryptionBox != null) { cryptoData = encryptionBox.cryptoData; } @@ -1420,7 +1424,7 @@ public class FragmentedMp4Extractor implements Extractor { // After we have the sampleTimeUs, we can commit all the pending metadata samples outputPendingMetadataSamples(sampleTimeUs); - if (!currentTrackBundle.next()) { + if (!trackBundle.next()) { currentTrackBundle = null; } parserState = STATE_READING_SAMPLE_START; @@ -1452,7 +1456,7 @@ public class FragmentedMp4Extractor implements Extractor { */ @Nullable private static TrackBundle getNextTrackBundle(SparseArray trackBundles) { - TrackBundle nextTrackBundle = null; + @Nullable TrackBundle nextTrackBundle = null; long nextSampleOffset = Long.MAX_VALUE; int trackBundlesSize = trackBundles.size(); @@ -1579,6 +1583,8 @@ public class FragmentedMp4Extractor implements Extractor { TrackSampleTable moovSampleTable, DefaultSampleValues defaultSampleValues) { this.output = output; + this.moovSampleTable = moovSampleTable; + this.defaultSampleValues = defaultSampleValues; fragment = new TrackFragment(); scratch = new ParsableByteArray(); encryptionSignalByte = new ParsableByteArray(1); @@ -1587,9 +1593,8 @@ public class FragmentedMp4Extractor implements Extractor { } public void reset(TrackSampleTable moovSampleTable, DefaultSampleValues defaultSampleValues) { - Assertions.checkNotNull(moovSampleTable.track); this.moovSampleTable = moovSampleTable; - this.defaultSampleValues = Assertions.checkNotNull(defaultSampleValues); + this.defaultSampleValues = defaultSampleValues; output.format(moovSampleTable.track.format); resetFragmentInfo(); } @@ -1598,7 +1603,7 @@ public class FragmentedMp4Extractor implements Extractor { @Nullable TrackEncryptionBox encryptionBox = moovSampleTable.track.getSampleDescriptionEncryptionBox( - fragment.header.sampleDescriptionIndex); + castNonNull(fragment.header).sampleDescriptionIndex); @Nullable String schemeType = encryptionBox != null ? encryptionBox.schemeType : null; DrmInitData updatedDrmInitData = drmInitData.copyWithSchemeType(schemeType); Format format = @@ -1706,7 +1711,7 @@ public class FragmentedMp4Extractor implements Extractor { * @return The number of written bytes. */ public int outputSampleEncryptionData(int sampleSize, int clearHeaderSize) { - TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted(); + @Nullable TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted(); if (encryptionBox == null) { return 0; } @@ -1718,7 +1723,7 @@ public class FragmentedMp4Extractor implements Extractor { vectorSize = encryptionBox.perSampleIvSize; } else { // The default initialization vector should be used. - byte[] initVectorData = encryptionBox.defaultInitializationVector; + byte[] initVectorData = castNonNull(encryptionBox.defaultInitializationVector); defaultInitializationVector.reset(initVectorData, initVectorData.length); initializationVectorData = defaultInitializationVector; vectorSize = initVectorData.length; @@ -1815,7 +1820,7 @@ public class FragmentedMp4Extractor implements Extractor { // Encryption is not supported yet for samples specified in the sample table. return null; } - int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex; + int sampleDescriptionIndex = castNonNull(fragment.header).sampleDescriptionIndex; @Nullable TrackEncryptionBox encryptionBox = fragment.trackEncryptionBox != null diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 02d7ebd9a0..b58f3da928 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -26,7 +26,6 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.MediaItem; -import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.Extractor; @@ -36,6 +35,7 @@ import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceDrmHelper; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.SequenceableLoader; @@ -49,6 +49,7 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.MimeTypes; @@ -93,12 +94,13 @@ public final class HlsMediaSource extends BaseMediaSource public static final class Factory implements MediaSourceFactory { private final HlsDataSourceFactory hlsDataSourceFactory; + private final MediaSourceDrmHelper mediaSourceDrmHelper; private HlsExtractorFactory extractorFactory; private HlsPlaylistParserFactory playlistParserFactory; private HlsPlaylistTracker.Factory playlistTrackerFactory; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private DrmSessionManager drmSessionManager; + @Nullable private DrmSessionManager drmSessionManager; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private boolean allowChunklessPreparation; @MetadataType private int metadataType; @@ -125,10 +127,10 @@ public final class HlsMediaSource extends BaseMediaSource */ public Factory(HlsDataSourceFactory hlsDataSourceFactory) { this.hlsDataSourceFactory = checkNotNull(hlsDataSourceFactory); + mediaSourceDrmHelper = new MediaSourceDrmHelper(); playlistParserFactory = new DefaultHlsPlaylistParserFactory(); playlistTrackerFactory = DefaultHlsPlaylistTracker.FACTORY; extractorFactory = HlsExtractorFactory.DEFAULT; - drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); metadataType = METADATA_TYPE_ID3; @@ -285,19 +287,22 @@ public final class HlsMediaSource extends BaseMediaSource return this; } - /** - * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The - * default value is {@link DrmSessionManager#DUMMY}. - * - * @param drmSessionManager The {@link DrmSessionManager}. - * @return This factory, for convenience. - */ @Override public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { - this.drmSessionManager = - drmSessionManager != null - ? drmSessionManager - : DrmSessionManager.getDummyDrmSessionManager(); + this.drmSessionManager = drmSessionManager; + return this; + } + + @Override + public Factory setDrmHttpDataSourceFactory( + @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { + mediaSourceDrmHelper.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory); + return this; + } + + @Override + public MediaSourceFactory setDrmUserAgent(@Nullable String userAgent) { + mediaSourceDrmHelper.setDrmUserAgent(userAgent); return this; } @@ -374,7 +379,7 @@ public final class HlsMediaSource extends BaseMediaSource hlsDataSourceFactory, extractorFactory, compositeSequenceableLoaderFactory, - drmSessionManager, + drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem), loadErrorHandlingPolicy, playlistTrackerFactory.createTracker( hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory), diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index 4a1f5c353c..a2ebb06936 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.MediaItem; 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.drm.DrmSessionManager; import com.google.android.exoplayer2.offline.FilteringManifestParser; @@ -39,6 +38,7 @@ import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceDrmHelper; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.MediaSourceFactory; @@ -50,6 +50,7 @@ import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestP import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy.LoadErrorInfo; import com.google.android.exoplayer2.upstream.Loader; @@ -77,10 +78,11 @@ public final class SsMediaSource extends BaseMediaSource public static final class Factory implements MediaSourceFactory { private final SsChunkSource.Factory chunkSourceFactory; + private final MediaSourceDrmHelper mediaSourceDrmHelper; @Nullable private final DataSource.Factory manifestDataSourceFactory; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private DrmSessionManager drmSessionManager; + @Nullable private DrmSessionManager drmSessionManager; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private long livePresentationDelayMs; @Nullable private ParsingLoadable.Parser manifestParser; @@ -111,7 +113,7 @@ public final class SsMediaSource extends BaseMediaSource @Nullable DataSource.Factory manifestDataSourceFactory) { this.chunkSourceFactory = checkNotNull(chunkSourceFactory); this.manifestDataSourceFactory = manifestDataSourceFactory; - drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); + mediaSourceDrmHelper = new MediaSourceDrmHelper(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); @@ -197,19 +199,22 @@ public final class SsMediaSource extends BaseMediaSource return this; } - /** - * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The - * default value is {@link DrmSessionManager#DUMMY}. - * - * @param drmSessionManager The {@link DrmSessionManager}. - * @return This factory, for convenience. - */ @Override public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { - this.drmSessionManager = - drmSessionManager != null - ? drmSessionManager - : DrmSessionManager.getDummyDrmSessionManager(); + this.drmSessionManager = drmSessionManager; + return this; + } + + @Override + public Factory setDrmHttpDataSourceFactory( + @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { + mediaSourceDrmHelper.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory); + return this; + } + + @Override + public Factory setDrmUserAgent(@Nullable String userAgent) { + mediaSourceDrmHelper.setDrmUserAgent(userAgent); return this; } @@ -280,7 +285,7 @@ public final class SsMediaSource extends BaseMediaSource /* manifestParser= */ null, chunkSourceFactory, compositeSequenceableLoaderFactory, - drmSessionManager, + drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem), loadErrorHandlingPolicy, livePresentationDelayMs); } @@ -357,7 +362,7 @@ public final class SsMediaSource extends BaseMediaSource manifestParser, chunkSourceFactory, compositeSequenceableLoaderFactory, - drmSessionManager, + drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem), loadErrorHandlingPolicy, livePresentationDelayMs); } diff --git a/library/ui/proguard-rules.txt b/library/ui/proguard-rules.txt index 9bfde914b2..ad7c139ea8 100644 --- a/library/ui/proguard-rules.txt +++ b/library/ui/proguard-rules.txt @@ -3,7 +3,7 @@ # Constructor method accessed via reflection in TrackSelectionDialogBuilder -dontnote androidx.appcompat.app.AlertDialog.Builder -keepclassmembers class androidx.appcompat.app.AlertDialog$Builder { - (android.content.Context); + (android.content.Context, int); public android.content.Context getContext(); public androidx.appcompat.app.AlertDialog$Builder setTitle(java.lang.CharSequence); public androidx.appcompat.app.AlertDialog$Builder setView(android.view.View); 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 62e9094cf0..fe802f9c0e 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 @@ -1085,8 +1085,8 @@ public class PlayerControlView extends FrameLayout { long mediaTimeUntilNextFullSecondMs = 1000 - position % 1000; mediaTimeDelayMs = Math.min(mediaTimeDelayMs, mediaTimeUntilNextFullSecondMs); - // Calculate the delay until the next update in real time, taking playbackSpeed into account. - float playbackSpeed = player.getPlaybackSpeed(); + // Calculate the delay until the next update in real time, taking playback speed into account. + float playbackSpeed = player.getPlaybackParameters().speed; long delayMs = playbackSpeed > 0 ? (long) (mediaTimeDelayMs / playbackSpeed) : MAX_UPDATE_INTERVAL_MS; 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 d0e7b0da9e..06a5341499 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 @@ -37,6 +37,7 @@ import androidx.media.app.NotificationCompat.MediaStyle; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.DefaultControlDispatcher; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; @@ -923,7 +924,7 @@ public class PlayerNotificationManager { *

  • The media is not {@link Player#isCurrentWindowDynamic() dynamically changing its * duration} (like for example a live stream). *
  • The media is not {@link Player#isPlayingAd() interrupted by an ad}. - *
  • The media is played at {@link Player#getPlaybackSpeed() regular speed}. + *
  • The media is played at {@link Player#getPlaybackParameters() regular speed}. *
  • The device is running at least API 21 (Lollipop). * * @@ -1086,7 +1087,7 @@ public class PlayerNotificationManager { && player.isPlaying() && !player.isPlayingAd() && !player.isCurrentWindowDynamic() - && player.getPlaybackSpeed() == 1f) { + && player.getPlaybackParameters().speed == 1f) { builder .setWhen(System.currentTimeMillis() - player.getContentPosition()) .setShowWhen(true) @@ -1336,7 +1337,7 @@ public class PlayerNotificationManager { } @Override - public void onPlaybackSpeedChanged(float playbackSpeed) { + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { postStartOrUpdateNotification(); } 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 ae523b4387..07106686ad 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 @@ -42,6 +42,7 @@ import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.DefaultControlDispatcher; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RendererCapabilities; @@ -1408,8 +1409,8 @@ public class StyledPlayerControlView extends FrameLayout { long mediaTimeUntilNextFullSecondMs = 1000 - position % 1000; mediaTimeDelayMs = Math.min(mediaTimeDelayMs, mediaTimeUntilNextFullSecondMs); - // Calculate the delay until the next update in real time, taking playbackSpeed into account. - float playbackSpeed = player.getPlaybackSpeed(); + // Calculate the delay until the next update in real time, taking playback speed into account. + float playbackSpeed = player.getPlaybackParameters().speed; long delayMs = playbackSpeed > 0 ? (long) (mediaTimeDelayMs / playbackSpeed) : MAX_UPDATE_INTERVAL_MS; @@ -1425,7 +1426,7 @@ public class StyledPlayerControlView extends FrameLayout { if (player == null) { return; } - float speed = player.getPlaybackSpeed(); + float speed = player.getPlaybackParameters().speed; int currentSpeedMultBy100 = Math.round(speed * 100); int indexForCurrentSpeed = playbackSpeedMultBy100List.indexOf(currentSpeedMultBy100); if (indexForCurrentSpeed == UNDEFINED_POSITION) { @@ -1481,7 +1482,7 @@ public class StyledPlayerControlView extends FrameLayout { if (player == null) { return; } - player.setPlaybackSpeed(speed); + player.setPlaybackParameters(new PlaybackParameters(speed)); } /* package */ void requestPlayPauseFocus() { @@ -1771,7 +1772,7 @@ public class StyledPlayerControlView extends FrameLayout { } @Override - public void onPlaybackSpeedChanged(float playbackSpeed) { + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { updateSettingsPlaybackSpeedLists(); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java index ec8b3562a8..a230744511 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java @@ -25,6 +25,7 @@ import android.view.LayoutInflater; import android.view.View; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; +import androidx.annotation.StyleRes; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; @@ -51,6 +52,7 @@ public final class TrackSelectionDialogBuilder { } private final Context context; + @StyleRes private int themeResId; private final CharSequence title; private final MappedTrackInfo mappedTrackInfo; private final int rendererIndex; @@ -124,6 +126,17 @@ public final class TrackSelectionDialogBuilder { newOverrides.isEmpty() ? null : newOverrides.get(0))); } + /** + * Sets the resource ID of the theme used to inflate this dialog. + * + * @param themeResId The resource ID of the theme. + * @return This builder, for convenience. + */ + public TrackSelectionDialogBuilder setTheme(@StyleRes int themeResId) { + this.themeResId = themeResId; + return this; + } + /** * Sets whether the selection is initially shown as disabled. * @@ -221,7 +234,7 @@ public final class TrackSelectionDialogBuilder { } private Dialog buildForPlatform() { - AlertDialog.Builder builder = new AlertDialog.Builder(context); + AlertDialog.Builder builder = new AlertDialog.Builder(context, themeResId); // Inflate with the builder's context to ensure the correct style is used. LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext()); @@ -245,8 +258,8 @@ public final class TrackSelectionDialogBuilder { // the APK size even with shrinking. See https://issuetracker.google.com/161514204. // LINT.IfChange Class builderClazz = Class.forName("androidx.appcompat.app.AlertDialog$Builder"); - Constructor builderConstructor = builderClazz.getConstructor(Context.class); - Object builder = builderConstructor.newInstance(context); + Constructor builderConstructor = builderClazz.getConstructor(Context.class, int.class); + Object builder = builderConstructor.newInstance(context, themeResId); // Inflate with the builder's context to ensure the correct style is used. Context builderContext = (Context) builderClazz.getMethod("getContext").invoke(builder); diff --git a/library/ui/src/main/res/layout/exo_styled_player_control_view.xml b/library/ui/src/main/res/layout/exo_styled_player_control_view.xml index 083ce48d06..0b0dd84b1f 100644 --- a/library/ui/src/main/res/layout/exo_styled_player_control_view.xml +++ b/library/ui/src/main/res/layout/exo_styled_player_control_view.xml @@ -134,6 +134,7 @@ android:layout_height="wrap_content" android:orientation="vertical" android:layout_gravity="bottom|end" + android:layout_marginBottom="@dimen/exo_custom_progress_thumb_size" android:visibility="invisible"> diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java index d25836eee3..0f2c856dd7 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.playbacktests.gts; +import static com.google.android.exoplayer2.playbacktests.gts.GtsTestUtil.shouldSkipWidevineTest; + import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.rule.ActivityTestRule; import com.google.android.exoplayer2.Player; @@ -64,7 +66,7 @@ public final class CommonEncryptionDrmTest { @Test public void cencSchemeTypeV18() { - if (Util.SDK_INT < 18) { + if (Util.SDK_INT < 18 || shouldSkipWidevineTest(testRule.getActivity())) { // Pass. return; } @@ -74,23 +76,9 @@ public final class CommonEncryptionDrmTest { .run(); } - @Test - public void cbc1SchemeTypeV25() { - if (Util.SDK_INT < 25) { - // cbc1 support was added in API 24, but it is stable from API 25 onwards. - // See [internal: b/65634809]. - // Pass. - return; - } - testRunner - .setStreamName("test_widevine_h264_scheme_cbc1") - .setManifestUrl(DashTestData.WIDEVINE_SCHEME_CBC1) - .run(); - } - @Test public void cbcsSchemeTypeV25() { - if (Util.SDK_INT < 25) { + if (Util.SDK_INT < 25 || shouldSkipWidevineTest(testRule.getActivity())) { // cbcs support was added in API 24, but it is stable from API 25 onwards. // See [internal: b/65634809]. // Pass. @@ -101,9 +89,4 @@ public final class CommonEncryptionDrmTest { .setManifestUrl(DashTestData.WIDEVINE_SCHEME_CBCS) .run(); } - - @Test - public void censSchemeTypeV25() { - // TODO: Implement once content is available. Track [internal: b/31219813]. - } } diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java index 259c2e61f6..a2f557ca0d 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.playbacktests.gts; +import static com.google.android.exoplayer2.playbacktests.gts.GtsTestUtil.shouldSkipWidevineTest; import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -356,7 +357,7 @@ public final class DashStreamingTest { @Test public void widevineH264FixedV18() throws Exception { - if (Util.SDK_INT < 18) { + if (Util.SDK_INT < 18 || shouldSkipWidevineTest(testRule.getActivity())) { // Pass. return; } @@ -373,7 +374,9 @@ public final class DashStreamingTest { @Test public void widevineH264AdaptiveV18() throws Exception { - if (Util.SDK_INT < 18 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { + if (Util.SDK_INT < 18 + || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264) + || shouldSkipWidevineTest(testRule.getActivity())) { // Pass. return; } @@ -390,7 +393,9 @@ public final class DashStreamingTest { @Test public void widevineH264AdaptiveWithSeekingV18() throws Exception { - if (Util.SDK_INT < 18 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { + if (Util.SDK_INT < 18 + || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264) + || shouldSkipWidevineTest(testRule.getActivity())) { // Pass. return; } @@ -408,7 +413,9 @@ public final class DashStreamingTest { @Test public void widevineH264AdaptiveWithRendererDisablingV18() throws Exception { - if (Util.SDK_INT < 18 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { + if (Util.SDK_INT < 18 + || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264) + || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { // Pass. return; } @@ -428,7 +435,7 @@ public final class DashStreamingTest { @Test public void widevineH265FixedV23() throws Exception { - if (Util.SDK_INT < 23) { + if (Util.SDK_INT < 23 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { // Pass. return; } @@ -445,7 +452,7 @@ public final class DashStreamingTest { @Test public void widevineH265AdaptiveV24() throws Exception { - if (Util.SDK_INT < 24) { + if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { // Pass. return; } @@ -462,7 +469,7 @@ public final class DashStreamingTest { @Test public void widevineH265AdaptiveWithSeekingV24() throws Exception { - if (Util.SDK_INT < 24) { + if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { // Pass. return; } @@ -480,7 +487,7 @@ public final class DashStreamingTest { @Test public void widevineH265AdaptiveWithRendererDisablingV24() throws Exception { - if (Util.SDK_INT < 24) { + if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { // Pass. return; } @@ -500,7 +507,7 @@ public final class DashStreamingTest { @Test public void widevineVp9Fixed360pV23() throws Exception { - if (Util.SDK_INT < 23) { + if (Util.SDK_INT < 23 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { // Pass. return; } @@ -517,7 +524,7 @@ public final class DashStreamingTest { @Test public void widevineVp9AdaptiveV24() throws Exception { - if (Util.SDK_INT < 24) { + if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { // Pass. return; } @@ -534,7 +541,7 @@ public final class DashStreamingTest { @Test public void widevineVp9AdaptiveWithSeekingV24() throws Exception { - if (Util.SDK_INT < 24) { + if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { // Pass. return; } @@ -552,7 +559,7 @@ public final class DashStreamingTest { @Test public void widevineVp9AdaptiveWithRendererDisablingV24() throws Exception { - if (Util.SDK_INT < 24) { + if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { // Pass. return; } @@ -573,7 +580,7 @@ public final class DashStreamingTest { // 23.976 fps. @Test public void widevine23FpsH264FixedV23() throws Exception { - if (Util.SDK_INT < 23) { + if (Util.SDK_INT < 23 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { // Pass. return; } @@ -591,7 +598,7 @@ public final class DashStreamingTest { // 24 fps. @Test public void widevine24FpsH264FixedV23() throws Exception { - if (Util.SDK_INT < 23) { + if (Util.SDK_INT < 23 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { // Pass. return; } @@ -609,7 +616,7 @@ public final class DashStreamingTest { // 29.97 fps. @Test public void widevine29FpsH264FixedV23() throws Exception { - if (Util.SDK_INT < 23) { + if (Util.SDK_INT < 23 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { // Pass. return; } diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java index 2033ef3096..c148010fdb 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java @@ -45,8 +45,6 @@ import com.google.android.exoplayer2.util.Util; // Widevine encrypted content manifests using different common encryption schemes. public static final String WIDEVINE_SCHEME_CENC = BASE_URL_COMMON_ENCRYPTION + "tears-cenc.mpd"; - public static final String WIDEVINE_SCHEME_CBC1 = - BASE_URL_COMMON_ENCRYPTION + "tears-aes-cbc1.mpd"; public static final String WIDEVINE_SCHEME_CBCS = BASE_URL_COMMON_ENCRYPTION + "tears-aes-cbcs.mpd"; diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java index 23b5cc7f17..81425c34ea 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java @@ -100,7 +100,7 @@ public final class DashWidevineOfflineTest { @Test public void widevineOfflineLicenseV22() throws Exception { - if (Util.SDK_INT < 22) { + if (Util.SDK_INT < 22 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { return; // Pass. } downloadLicense(); @@ -113,7 +113,9 @@ public final class DashWidevineOfflineTest { @Test public void widevineOfflineReleasedLicenseV22() throws Throwable { - if (Util.SDK_INT < 22 || Util.SDK_INT > 28) { + if (Util.SDK_INT < 22 + || Util.SDK_INT > 28 + || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { return; // Pass. } downloadLicense(); @@ -136,7 +138,7 @@ public final class DashWidevineOfflineTest { @Test public void widevineOfflineReleasedLicenseV29() throws Throwable { - if (Util.SDK_INT < 29) { + if (Util.SDK_INT < 29 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { return; // Pass. } downloadLicense(); @@ -158,7 +160,7 @@ public final class DashWidevineOfflineTest { @Test public void widevineOfflineExpiredLicenseV22() throws Exception { - if (Util.SDK_INT < 22) { + if (Util.SDK_INT < 22 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { return; // Pass. } downloadLicense(); @@ -188,7 +190,7 @@ public final class DashWidevineOfflineTest { @Test public void widevineOfflineLicenseExpiresOnPauseV22() throws Exception { - if (Util.SDK_INT < 22) { + if (Util.SDK_INT < 22 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { return; // Pass. } downloadLicense(); @@ -198,7 +200,7 @@ public final class DashWidevineOfflineTest { offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId); long licenseDuration = licenseDurationRemainingSec.first; assertWithMessage( - "License duration should be less than 30 sec. " + "Server settings might have changed.") + "License duration should be less than 30 sec. Server settings might have changed.") .that(licenseDuration < 30) .isTrue(); ActionSchedule schedule = new ActionSchedule.Builder(TAG) diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java index 36c58d46b2..71270d21c5 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java @@ -24,6 +24,7 @@ import android.media.MediaFormat; import android.os.Handler; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; @@ -84,13 +85,14 @@ import java.util.ArrayList; private final long[] timestampsList; private final ArrayDeque inputFormatChangeTimesUs; - private final boolean enableMediaFormatChangeTimeCheck; + + private boolean skipToPositionBeforeRenderingFirstFrame; + private boolean shouldMediaFormatChangeTimesBeChecked; private int startIndex; private int queueSize; private int bufferCount; private int minimumInsertIndex; - private boolean skipToPositionBeforeRenderingFirstFrame; private boolean inputFormatChanged; private boolean outputMediaFormatChanged; @@ -112,10 +114,6 @@ import java.util.ArrayList; maxDroppedFrameCountToNotify); timestampsList = new long[ARRAY_SIZE]; inputFormatChangeTimesUs = new ArrayDeque<>(); - - // As per [Internal ref: b/149818050, b/149751672], MediaFormat changes can occur early for - // SDK 29 and 30. Should be fixed for SDK 31 onwards. - enableMediaFormatChangeTimeCheck = Util.SDK_INT < 29 || Util.SDK_INT >= 31; } @Override @@ -137,6 +135,10 @@ import java.util.ArrayList; // frames up to the current playback position [Internal: b/66494991]. skipToPositionBeforeRenderingFirstFrame = getState() == Renderer.STATE_STARTED; super.configureCodec(codecInfo, codecAdapter, format, crypto, operatingRate); + + // Output MediaFormat changes are known to occur too early until API 30 (see [internal: + // b/149818050, b/149751672]). + shouldMediaFormatChangeTimesBeChecked = Util.SDK_INT > 30; } @Override @@ -247,10 +249,12 @@ import java.util.ArrayList; } if (outputMediaFormatChanged) { - long inputFormatChangeTimeUs = inputFormatChangeTimesUs.remove(); + long inputFormatChangeTimeUs = + inputFormatChangeTimesUs.isEmpty() ? C.TIME_UNSET : inputFormatChangeTimesUs.remove(); outputMediaFormatChanged = false; - if (enableMediaFormatChangeTimeCheck && presentationTimeUs != inputFormatChangeTimeUs) { + if (shouldMediaFormatChangeTimesBeChecked + && presentationTimeUs != inputFormatChangeTimeUs) { throw new IllegalStateException( "Expected output MediaFormat change timestamp (" + presentationTimeUs diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/GtsTestUtil.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/GtsTestUtil.java new file mode 100644 index 0000000000..6223539056 --- /dev/null +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/GtsTestUtil.java @@ -0,0 +1,50 @@ +/* + * 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.playbacktests.gts; + +import static com.google.android.exoplayer2.C.WIDEVINE_UUID; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.media.MediaDrm; + +/** Utility methods for GTS tests. */ +public final class GtsTestUtil { + + private GtsTestUtil() {} + + /** Returns true if the device doesn't support Widevine and this is permitted. */ + public static boolean shouldSkipWidevineTest(Context context) { + if (isGmsInstalled(context)) { + // GMS devices are required to support Widevine. + return false; + } + // For non-GMS devices Widevine is optional. + return !MediaDrm.isCryptoSchemeSupported(WIDEVINE_UUID); + } + + private static boolean isGmsInstalled(Context context) { + try { + context + .getPackageManager() + .getPackageInfo("com.google.android.gms", PackageManager.GET_SIGNATURES); + } catch (PackageManager.NameNotFoundException e) { + return false; + } + return true; + } +} diff --git a/testdata/src/test/assets/playbackdumps/mp4/sample.mp4.dump b/testdata/src/test/assets/playbackdumps/mp4/sample.mp4.dump new file mode 100644 index 0000000000..5256ea561e --- /dev/null +++ b/testdata/src/test/assets/playbackdumps/mp4/sample.mp4.dump @@ -0,0 +1,81 @@ +MediaCodec (audio/mp4a-latm): + buffers.length = 46 + buffers[0] = length 23, hash 47DE9131 + buffers[1] = length 6, hash 31EC5206 + buffers[2] = length 148, hash 894A176B + buffers[3] = length 189, hash CEF235A1 + buffers[4] = length 205, hash BBF5F7B0 + buffers[5] = length 210, hash F278B193 + buffers[6] = length 210, hash 82DA1589 + buffers[7] = length 207, hash 5BE231DF + buffers[8] = length 225, hash 18819EE1 + buffers[9] = length 215, hash CA7FA67B + buffers[10] = length 211, hash 581A1C18 + buffers[11] = length 216, hash ADB88187 + buffers[12] = length 229, hash 2E8BA4DC + buffers[13] = length 232, hash 22F0C510 + buffers[14] = length 235, hash 867AD0DC + buffers[15] = length 231, hash 84E823A8 + buffers[16] = length 226, hash 1BEF3A95 + buffers[17] = length 216, hash EAA345AE + buffers[18] = length 229, hash 6957411F + buffers[19] = length 219, hash 41275022 + buffers[20] = length 241, hash 6495DF96 + buffers[21] = length 228, hash 63D95906 + buffers[22] = length 238, hash 34F676F9 + buffers[23] = length 234, hash E5CBC045 + buffers[24] = length 231, hash 5FC43661 + buffers[25] = length 217, hash 682708ED + buffers[26] = length 239, hash D43780FC + buffers[27] = length 243, hash C5E17980 + buffers[28] = length 231, hash AC5837BA + buffers[29] = length 230, hash 169EE895 + buffers[30] = length 238, hash C48FF3F1 + buffers[31] = length 225, hash 531E4599 + buffers[32] = length 232, hash CB3C6B8D + buffers[33] = length 243, hash F8C94C7 + buffers[34] = length 232, hash A646A7D0 + buffers[35] = length 237, hash E8B787A5 + buffers[36] = length 228, hash 3FA7A29F + buffers[37] = length 235, hash B9B33B0A + buffers[38] = length 264, hash 71A4869E + buffers[39] = length 257, hash D049B54C + buffers[40] = length 227, hash 66757231 + buffers[41] = length 227, hash BD374F1B + buffers[42] = length 235, hash 999477F6 + buffers[43] = length 229, hash FFF98DF0 + buffers[44] = length 6, hash 31B22286 + buffers[45] = length 0, hash 1 +MediaCodec (video/avc): + buffers.length = 31 + buffers[0] = length 36692, hash D216076E + buffers[1] = length 5312, hash D45D3CA0 + buffers[2] = length 599, hash 1BE7812D + buffers[3] = length 7735, hash 4490F110 + buffers[4] = length 987, hash 560B5036 + buffers[5] = length 673, hash ED7CD8C7 + buffers[6] = length 523, hash 3020DF50 + buffers[7] = length 6061, hash 736C72B2 + buffers[8] = length 992, hash FE132F23 + buffers[9] = length 623, hash 5B2C1816 + buffers[10] = length 421, hash 742E69C1 + buffers[11] = length 4899, hash F72F86A1 + buffers[12] = length 568, hash 519A8E50 + buffers[13] = length 620, hash 3990AA39 + buffers[14] = length 5450, hash F06EC4AA + buffers[15] = length 1051, hash 92DFA63A + buffers[16] = length 874, hash 69587FB4 + buffers[17] = length 781, hash 36BE495B + buffers[18] = length 4725, hash AC0C8CD3 + buffers[19] = length 1022, hash 5D8BFF34 + buffers[20] = length 790, hash 99413A99 + buffers[21] = length 610, hash 5E129290 + buffers[22] = length 2751, hash 769974CB + buffers[23] = length 745, hash B78A477A + buffers[24] = length 621, hash CF741E7A + buffers[25] = length 505, hash 1DB4894E + buffers[26] = length 1268, hash C15348DC + buffers[27] = length 880, hash C2DE85D0 + buffers[28] = length 530, hash C98BC6A8 + buffers[29] = length 568, hash 4FE5C8EA + buffers[30] = length 0, hash 1 diff --git a/testdata/src/test/assets/playbackdumps/ts/sample_scte35.ts.dump b/testdata/src/test/assets/playbackdumps/ts/sample_scte35.ts.dump new file mode 100644 index 0000000000..9e850d0f14 --- /dev/null +++ b/testdata/src/test/assets/playbackdumps/ts/sample_scte35.ts.dump @@ -0,0 +1,19 @@ +MediaCodec (audio/mpeg-L2): + buffers.length = 5 + buffers[0] = length 1253, hash 727FD1C6 + buffers[1] = length 1254, hash 73FB07B8 + buffers[2] = length 1254, hash 73FB07B8 + buffers[3] = length 1254, hash 73FB07B8 + buffers[4] = length 0, hash 1 +MediaCodec (video/mpeg2): + buffers.length = 3 + buffers[0] = length 20711, hash 34341E8 + buffers[1] = length 18112, hash EC44B35B + buffers[2] = length 0, hash 1 +MetadataOutput: + Metadata[0]: + entry[0] = SpliceInsertCommand + Metadata[1]: + entry[0] = SpliceInsertCommand + Metadata[2]: + entry[0] = SpliceInsertCommand diff --git a/testutils/build.gradle b/testutils/build.gradle index 93b3acf53f..8cd443e07f 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -24,6 +24,7 @@ dependencies { compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation project(modulePrefix + 'library-core') + implementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index 5b8d501d00..ca514432f2 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.IllegalSeekPositionException; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.PlayerMessage.Target; @@ -608,26 +609,26 @@ public abstract class Action { } } - /** Calls {@link Player#setPlaybackSpeed(float)}. */ - public static final class SetPlaybackSpeed extends Action { + /** Calls {@link Player#setPlaybackParameters(PlaybackParameters)}. */ + public static final class SetPlaybackParameters extends Action { - private final float playbackSpeed; + private final PlaybackParameters playbackParameters; /** - * Creates a set playback speed action instance. + * Creates a set playback parameters action instance. * * @param tag A tag to use for logging. - * @param playbackSpeed The playback speed. + * @param playbackParameters The playback parameters. */ - public SetPlaybackSpeed(String tag, float playbackSpeed) { - super(tag, "SetPlaybackSpeed:" + playbackSpeed); - this.playbackSpeed = playbackSpeed; + public SetPlaybackParameters(String tag, PlaybackParameters playbackParameters) { + super(tag, "SetPlaybackParameters:" + playbackParameters); + this.playbackParameters = playbackParameters; } @Override protected void doActionImpl( SimpleExoPlayer player, DefaultTrackSelector trackSelector, @Nullable Surface surface) { - player.setPlaybackSpeed(playbackSpeed); + player.setPlaybackParameters(playbackParameters); } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 8051e997b3..fa672b844a 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -20,6 +20,7 @@ import android.view.Surface; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.PlayerMessage.Target; @@ -35,7 +36,7 @@ import com.google.android.exoplayer2.testutil.Action.Seek; import com.google.android.exoplayer2.testutil.Action.SendMessages; import com.google.android.exoplayer2.testutil.Action.SetAudioAttributes; import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady; -import com.google.android.exoplayer2.testutil.Action.SetPlaybackSpeed; +import com.google.android.exoplayer2.testutil.Action.SetPlaybackParameters; import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled; import com.google.android.exoplayer2.testutil.Action.SetRepeatMode; import com.google.android.exoplayer2.testutil.Action.SetShuffleModeEnabled; @@ -214,14 +215,14 @@ public final class ActionSchedule { } /** - * Schedules a playback speed setting action. + * Schedules a playback parameters setting action. * - * @param playbackSpeed The playback speed to set. + * @param playbackParameters The playback parameters to set. * @return The builder, for convenience. - * @see Player#setPlaybackSpeed(float) + * @see Player#setPlaybackParameters(PlaybackParameters) */ - public Builder setPlaybackSpeed(float playbackSpeed) { - return apply(new SetPlaybackSpeed(tag, playbackSpeed)); + public Builder setPlaybackParameters(PlaybackParameters playbackParameters) { + return apply(new SetPlaybackParameters(tag, playbackParameters)); } /** diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index 7444d35e8e..e66a30935e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -52,7 +52,7 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest { DefaultAudioSink.failOnSpuriousAudioTimestamp = true; } - public static final long MAX_PLAYING_TIME_DISCREPANCY_MS = 2000; + public static final long MAX_PLAYING_TIME_DISCREPANCY_MS = 5000; public static final long EXPECTED_PLAYING_TIME_MEDIA_DURATION_MS = -2; public static final long EXPECTED_PLAYING_TIME_UNSET = -1; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java index d3eec0b85b..4a3b9e923e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.List; import org.checkerframework.checker.nullness.compatqual.NullableType; @@ -48,7 +49,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod @Nullable private final TransferListener transferListener; private final long durationUs; - @MonotonicNonNull private Callback callback; + private @MonotonicNonNull Callback callback; private ChunkSampleStream[] sampleStreams; private SequenceableLoader sequenceableLoader; @@ -99,7 +100,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod } } sampleStreams = newSampleStreamArray(validStreams.size()); - validStreams.toArray(sampleStreams); + Util.nullSafeListToArray(validStreams, sampleStreams); this.sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); return returnPositionUs; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAudioRenderer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAudioRenderer.java index 5ed4e5bf5f..1e2f6159a5 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAudioRenderer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAudioRenderer.java @@ -30,6 +30,7 @@ public class FakeAudioRenderer extends FakeRenderer { private final AudioRendererEventListener.EventDispatcher eventDispatcher; private final DecoderCounters decoderCounters; private boolean notifiedAudioSessionId; + private boolean notifiedPositionAdvancing; public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) { super(C.TRACK_TYPE_AUDIO); @@ -43,6 +44,7 @@ public class FakeAudioRenderer extends FakeRenderer { super.onEnabled(joining, mayRenderStartOfStream); eventDispatcher.enabled(decoderCounters); notifiedAudioSessionId = false; + notifiedPositionAdvancing = false; } @Override @@ -67,6 +69,10 @@ public class FakeAudioRenderer extends FakeRenderer { eventDispatcher.audioSessionId(/* audioSessionId= */ 1); notifiedAudioSessionId = true; } + if (shouldProcess && !notifiedPositionAdvancing) { + eventDispatcher.positionAdvancing(System.currentTimeMillis()); + notifiedPositionAdvancing = true; + } return shouldProcess; } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java index a9ca00ac64..5f858bea99 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java @@ -217,8 +217,7 @@ public class FakeDataSource extends BaseDataSource { * this method. */ public final DataSpec[] getAndClearOpenedDataSpecs() { - DataSpec[] dataSpecs = new DataSpec[openedDataSpecs.size()]; - openedDataSpecs.toArray(dataSpecs); + DataSpec[] dataSpecs = openedDataSpecs.toArray(new DataSpec[0]); openedDataSpecs.clear(); return dataSpecs; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java index 824d3c02e3..7d63e129db 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java @@ -30,7 +30,6 @@ import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import com.google.common.collect.Iterables; import java.io.IOException; @@ -274,10 +273,7 @@ public class FakeSampleStream implements SampleStream { @Nullable DrmSession previousSession = currentDrmSession; Looper playbackLooper = Assertions.checkNotNull(Looper.myLooper()); currentDrmSession = - newDrmInitData != null - ? drmSessionManager.acquireSession(playbackLooper, drmEventDispatcher, newFormat) - : drmSessionManager.acquirePlaceholderSession( - playbackLooper, MimeTypes.getTrackType(newFormat.sampleMimeType)); + drmSessionManager.acquireSession(playbackLooper, drmEventDispatcher, newFormat); outputFormatHolder.drmSession = currentDrmSession; if (previousSession != null) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/PlaybackOutput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/PlaybackOutput.java new file mode 100644 index 0000000000..69429709a4 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/PlaybackOutput.java @@ -0,0 +1,93 @@ +/* + * 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.testutil; + +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.util.Assertions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Class to capture output from a playback test. + * + *

    Implements {@link Dumper.Dumpable} so the output can be easily dumped to a string for + * comparison against previous test runs. + */ +public final class PlaybackOutput implements Dumper.Dumpable { + + private final ShadowMediaCodecConfig codecConfig; + + // TODO: Add support for subtitles too + private final List metadatas; + + private PlaybackOutput(SimpleExoPlayer player, ShadowMediaCodecConfig codecConfig) { + this.codecConfig = codecConfig; + + metadatas = Collections.synchronizedList(new ArrayList<>()); + // TODO: Consider passing playback position into MetadataOutput and TextOutput. Calling + // player.getCurrentPosition() inside onMetadata/Cues will likely be non-deterministic + // because renderer-thread != playback-thread. + player.addMetadataOutput(metadatas::add); + } + + /** + * Create an instance that captures the metadata and text output from {@code player} and the audio + * and video output via the {@link TeeCodec TeeCodecs} exposed by {@code mediaCodecConfig}. + * + *

    Must be called before playback to ensure metadata and text output is captured + * correctly. + * + * @param player The {@link SimpleExoPlayer} to capture metadata and text output from. + * @param mediaCodecConfig The {@link ShadowMediaCodecConfig} to capture audio and video output + * from. + * @return A new instance that can be used to dump the playback output. + */ + public static PlaybackOutput register( + SimpleExoPlayer player, ShadowMediaCodecConfig mediaCodecConfig) { + return new PlaybackOutput(player, mediaCodecConfig); + } + + @Override + public void dump(Dumper dumper) { + ImmutableMap codecs = codecConfig.getCodecs(); + ImmutableList mimeTypes = ImmutableList.sortedCopyOf(codecs.keySet()); + for (String mimeType : mimeTypes) { + dumper.add(Assertions.checkNotNull(codecs.get(mimeType))); + } + + dumpMetadata(dumper); + } + + private void dumpMetadata(Dumper dumper) { + if (metadatas.isEmpty()) { + return; + } + dumper.startBlock("MetadataOutput"); + for (int i = 0; i < metadatas.size(); i++) { + dumper.startBlock("Metadata[" + i + "]"); + Metadata metadata = metadatas.get(i); + for (int j = 0; j < metadata.length(); j++) { + dumper.add("entry[" + j + "]", metadata.get(j).getClass().getSimpleName()); + } + dumper.endBlock(); + } + dumper.endBlock(); + } +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ShadowMediaCodecConfig.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ShadowMediaCodecConfig.java new file mode 100644 index 0000000000..d1b4e784b8 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ShadowMediaCodecConfig.java @@ -0,0 +1,133 @@ +/* + * 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.testutil; + +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.primitives.Ints; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.rules.ExternalResource; +import org.robolectric.shadows.MediaCodecInfoBuilder; +import org.robolectric.shadows.ShadowMediaCodec; +import org.robolectric.shadows.ShadowMediaCodecList; + +/** + * A JUnit @Rule to configure Roboelectric's {@link ShadowMediaCodec}. + * + *

    Registers a {@link org.robolectric.shadows.ShadowMediaCodec.CodecConfig} for each audio/video + * MIME type known by ExoPlayer, and provides access to the bytes passed to these via {@link + * TeeCodec}. + */ +public final class ShadowMediaCodecConfig extends ExternalResource { + + private final Map codecsByMimeType; + + private ShadowMediaCodecConfig() { + this.codecsByMimeType = new HashMap<>(); + } + + public static ShadowMediaCodecConfig forAllSupportedMimeTypes() { + return new ShadowMediaCodecConfig(); + } + + public ImmutableMap getCodecs() { + return ImmutableMap.copyOf(codecsByMimeType); + } + + @Override + protected void before() throws Throwable { + // Video codecs + MediaCodecInfo.CodecProfileLevel avcProfileLevel = + createProfileLevel( + MediaCodecInfo.CodecProfileLevel.AVCProfileHigh, + MediaCodecInfo.CodecProfileLevel.AVCLevel62); + configureCodec( + /* codecName= */ "exotest.video.avc", + MimeTypes.VIDEO_H264, + ImmutableList.of(avcProfileLevel), + ImmutableList.of(MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible)); + MediaCodecInfo.CodecProfileLevel mpeg2ProfileLevel = + createProfileLevel( + MediaCodecInfo.CodecProfileLevel.MPEG2ProfileMain, + MediaCodecInfo.CodecProfileLevel.MPEG2LevelML); + configureCodec( + /* codecName= */ "exotest.video.mpeg2", + MimeTypes.VIDEO_MPEG2, + ImmutableList.of(mpeg2ProfileLevel), + ImmutableList.of(MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible)); + + // Audio codecs + configureCodec("exotest.audio.aac", MimeTypes.AUDIO_AAC); + configureCodec("exotest.audio.mpegl2", MimeTypes.AUDIO_MPEG_L2); + } + + @Override + protected void after() { + codecsByMimeType.clear(); + ShadowMediaCodecList.reset(); + ShadowMediaCodec.clearCodecs(); + } + + private void configureCodec(String codecName, String mimeType) { + configureCodec( + codecName, + mimeType, + /* profileLevels= */ ImmutableList.of(), + /* colorFormats= */ ImmutableList.of()); + } + + private void configureCodec( + String codecName, + String mimeType, + List profileLevels, + List colorFormats) { + MediaFormat mediaFormat = new MediaFormat(); + mediaFormat.setString(MediaFormat.KEY_MIME, mimeType); + MediaCodecInfoBuilder.CodecCapabilitiesBuilder capabilities = + MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder().setMediaFormat(mediaFormat); + if (!profileLevels.isEmpty()) { + capabilities.setProfileLevels(profileLevels.toArray(new MediaCodecInfo.CodecProfileLevel[0])); + } + if (!colorFormats.isEmpty()) { + capabilities.setColorFormats(Ints.toArray(colorFormats)); + } + ShadowMediaCodecList.addCodec( + MediaCodecInfoBuilder.newBuilder() + .setName(codecName) + .setCapabilities(capabilities.build()) + .build()); + // TODO: Update ShadowMediaCodec to consider the MediaFormat.KEY_MAX_INPUT_SIZE value passed + // to configure() so we don't have to specify large buffers here. + TeeCodec codec = new TeeCodec(mimeType); + ShadowMediaCodec.addDecoder( + codecName, + new ShadowMediaCodec.CodecConfig( + /* inputBufferSize= */ 50_000, /* outputBufferSize= */ 50_000, codec)); + codecsByMimeType.put(mimeType, codec); + } + + private static MediaCodecInfo.CodecProfileLevel createProfileLevel(int profile, int level) { + MediaCodecInfo.CodecProfileLevel profileLevel = new MediaCodecInfo.CodecProfileLevel(); + profileLevel.profile = profile; + profileLevel.level = level; + return profileLevel; + } +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 250dd01c0f..7a96e1c797 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -314,32 +314,16 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } - /** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */ - @SuppressWarnings("deprecation") - @Deprecated @Override public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { throw new UnsupportedOperationException(); } - /** @deprecated Use {@link #getPlaybackSpeed()} instead. */ - @SuppressWarnings("deprecation") - @Deprecated @Override public PlaybackParameters getPlaybackParameters() { throw new UnsupportedOperationException(); } - @Override - public void setPlaybackSpeed(float playbackSpeed) { - throw new UnsupportedOperationException(); - } - - @Override - public float getPlaybackSpeed() { - throw new UnsupportedOperationException(); - } - @Override public void setSeekParameters(@Nullable SeekParameters seekParameters) { throw new UnsupportedOperationException(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TeeCodec.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TeeCodec.java new file mode 100644 index 0000000000..fd9b374d46 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TeeCodec.java @@ -0,0 +1,80 @@ +/* + * 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.testutil; + +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.common.collect.ImmutableList; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.robolectric.shadows.ShadowMediaCodec; + +/** + * A {@link ShadowMediaCodec.CodecConfig.Codec} for Robolectric's {@link ShadowMediaCodec} that + * records the contents of buffers passed to it before copying the contents into the output buffer. + * + *

    This also implements {@link Dumper.Dumpable} so the recorded buffers can be written out to a + * dump file. + */ +public final class TeeCodec implements ShadowMediaCodec.CodecConfig.Codec, Dumper.Dumpable { + + private final String mimeType; + private final List receivedBuffers; + + public TeeCodec(String mimeType) { + this.mimeType = mimeType; + this.receivedBuffers = Collections.synchronizedList(new ArrayList<>()); + } + + @Override + public void process(ByteBuffer in, ByteBuffer out) { + byte[] bytes = new byte[in.remaining()]; + in.get(bytes); + receivedBuffers.add(bytes); + + if (!MimeTypes.isAudio(mimeType)) { + // Don't output audio bytes, because ShadowAudioTrack doesn't advance the playback position so + // playback never completes. + // TODO: Update ShadowAudioTrack to advance the playback position in a realistic way. + out.put(bytes); + } + } + + @Override + public void dump(Dumper dumper) { + if (receivedBuffers.isEmpty()) { + return; + } + dumper.startBlock("MediaCodec (" + mimeType + ")"); + dumper.add("buffers.length", receivedBuffers.size()); + for (int i = 0; i < receivedBuffers.size(); i++) { + dumper.add("buffers[" + i + "]", receivedBuffers.get(i)); + } + + dumper.endBlock(); + } + + /** + * Return the buffers received by this codec. + * + *

    The list is sorted in the order the buffers were passed to {@link #process(ByteBuffer, + * ByteBuffer)}. + */ + public ImmutableList getReceivedBuffers() { + return ImmutableList.copyOf(receivedBuffers); + } +}