diff --git a/.gitignore b/.gitignore index 2ec73a6fd8d..790a44c22f0 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,9 @@ extensions/vp9/src/main/jni/libvpx extensions/vp9/src/main/jni/libvpx_android_configs extensions/vp9/src/main/jni/libyuv +# AV1 extension +extensions/av1/src/main/jni/libgav1 + # Opus extension extensions/opus/src/main/jni/libopus diff --git a/.hgignore b/.hgignore index 5889f43b8df..7819a90ac54 100644 --- a/.hgignore +++ b/.hgignore @@ -62,6 +62,9 @@ extensions/vp9/src/main/jni/libvpx extensions/vp9/src/main/jni/libvpx_android_configs extensions/vp9/src/main/jni/libyuv +# AV1 extension +extensions/av1/src/main/jni/libgav1 + # Opus extension extensions/opus/src/main/jni/libopus diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6df173c7d40..5add56ca086 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,167 @@ # Release notes # +### 2.11.0 (2019-12-11) ### + +* Core library: + * Replace `ExoPlayerFactory` by `SimpleExoPlayer.Builder` and + `ExoPlayer.Builder`. + * Add automatic `WakeLock` handling to `SimpleExoPlayer`, which can be enabled + by calling `SimpleExoPlayer.setHandleWakeLock` + ([#5846](https://github.com/google/ExoPlayer/issues/5846)). To use this + feature, you must add the + [WAKE_LOCK](https://developer.android.com/reference/android/Manifest.permission.html#WAKE_LOCK) + permission to your application's manifest file. + * Add automatic "audio becoming noisy" handling to `SimpleExoPlayer`, which + can be enabled by calling `SimpleExoPlayer.setHandleAudioBecomingNoisy`. + * Wrap decoder exceptions in a new `DecoderException` class and report them as + renderer errors. + * Add `Timeline.Window.isLive` to indicate that a window is a live stream + ([#2668](https://github.com/google/ExoPlayer/issues/2668) and + [#5973](https://github.com/google/ExoPlayer/issues/5973)). + * Add `Timeline.Window.uid` to uniquely identify window instances. + * Deprecate `setTag` parameter of `Timeline.getWindow`. Tags will always be + set. + * Deprecate passing the manifest directly to + `Player.EventListener.onTimelineChanged`. It can be accessed through + `Timeline.Window.manifest` or `Player.getCurrentManifest()` + * Add `MediaSource.enable` and `MediaSource.disable` to improve resource + management in playlists. + * Add `MediaPeriod.isLoading` to improve `Player.isLoading` state. + * Fix issue where player errors are thrown too early at playlist transitions + ([#5407](https://github.com/google/ExoPlayer/issues/5407)). + * Add `Format` and renderer support flags to renderer `ExoPlaybackException`s. +* DRM: + * Inject `DrmSessionManager` into the `MediaSources` instead of `Renderers`. + This allows each `MediaSource` in a `ConcatenatingMediaSource` to use a + different `DrmSessionManager` + ([#5619](https://github.com/google/ExoPlayer/issues/5619)). + * Add `DefaultDrmSessionManager.Builder`, and remove + `DefaultDrmSessionManager` static factory methods that leaked + `ExoMediaDrm` instances + ([#4721](https://github.com/google/ExoPlayer/issues/4721)). + * Add support for the use of secure decoders when playing clear content + ([#4867](https://github.com/google/ExoPlayer/issues/4867)). This can + be enabled using `DefaultDrmSessionManager.Builder`'s + `setUseDrmSessionsForClearContent` method. + * Add support for custom `LoadErrorHandlingPolicies` in key and provisioning + requests ([#6334](https://github.com/google/ExoPlayer/issues/6334)). Custom + policies can be passed via `DefaultDrmSessionManager.Builder`'s + `setLoadErrorHandlingPolicy` method. + * Use `ExoMediaDrm.Provider` in `OfflineLicenseHelper` to avoid leaking + `ExoMediaDrm` instances + ([#4721](https://github.com/google/ExoPlayer/issues/4721)). +* Track selection: + * Update `DefaultTrackSelector` to set a viewport constraint for the default + display by default. + * Update `DefaultTrackSelector` to set text language and role flag + constraints for the device's accessibility settings by default + ([#5749](https://github.com/google/ExoPlayer/issues/5749)). + * Add option to set preferred text role flags using + `DefaultTrackSelector.ParametersBuilder.setPreferredTextRoleFlags`. +* Android 10: + * Set `compileSdkVersion` to 29 to enable use of Android 10 APIs. + * Expose new `isHardwareAccelerated`, `isSoftwareOnly` and `isVendor` flags + in `MediaCodecInfo` + ([#5839](https://github.com/google/ExoPlayer/issues/5839)). + * Add `allowedCapturePolicy` field to `AudioAttributes` to allow to + configuration of the audio capture policy. +* Video: + * Pass the codec output `MediaFormat` to `VideoFrameMetadataListener`. + * Fix byte order of HDR10+ static metadata to match CTA-861.3. + * Support out-of-band HDR10+ dynamic metadata for VP9 in WebM/Matroska. + * Assume that protected content requires a secure decoder when evaluating + whether `MediaCodecVideoRenderer` supports a given video format + ([#5568](https://github.com/google/ExoPlayer/issues/5568)). + * Fix Dolby Vision fallback to AVC and HEVC. + * Fix early end-of-stream detection when using video tunneling, on API level + 23 and above. + * Fix an issue where a keyframe was rendered rather than skipped when + performing an exact seek to a non-zero position close to the start of the + stream. +* Audio: + * Fix the start of audio getting truncated when transitioning to a new + item in a playlist of Opus streams. + * Workaround broken raw audio decoding on Oppo R9 + ([#5782](https://github.com/google/ExoPlayer/issues/5782)). + * Reconfigure audio sink when PCM encoding changes + ([#6601](https://github.com/google/ExoPlayer/issues/6601)). + * Allow `AdtsExtractor` to encounter EOF when calculating average frame size + ([#6700](https://github.com/google/ExoPlayer/issues/6700)). +* Text: + * Add support for position and overlapping start/end times in SSA/ASS + subtitles ([#6320](https://github.com/google/ExoPlayer/issues/6320)). + * Require an end time or duration for SubRip (SRT) and SubStation Alpha + (SSA/ASS) subtitles. This applies to both sidecar files & subtitles + [embedded in Matroska streams](https://matroska.org/technical/specs/subtitles/index.html). +* UI: + * Make showing and hiding player controls accessible to TalkBack in + `PlayerView`. + * Rename `spherical_view` surface type to `spherical_gl_surface_view`. + * Make it easier to override the shuffle, repeat, fullscreen, VR and small + notification icon assets + ([#6709](https://github.com/google/ExoPlayer/issues/6709)). +* Analytics: + * Remove `AnalyticsCollector.Factory`. Instances should be created directly, + and the `Player` should be set by calling `AnalyticsCollector.setPlayer`. + * Add `PlaybackStatsListener` to collect `PlaybackStats` for analysis and + analytics reporting. +* DataSource + * Add `DataSpec.httpRequestHeaders` to support setting per-request headers for + HTTP and HTTPS. + * Remove the `DataSpec.FLAG_ALLOW_ICY_METADATA` flag. Use is replaced by + setting the `IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME` header in + `DataSpec.httpRequestHeaders`. + * Fail more explicitly when local file URIs contain invalid parts (e.g. a + fragment) ([#6470](https://github.com/google/ExoPlayer/issues/6470)). +* DASH: Support negative @r values in segment timelines + ([#1787](https://github.com/google/ExoPlayer/issues/1787)). +* HLS: + * Use peak bitrate rather than average bitrate for adaptive track selection. + * Fix issue where streams could get stuck in an infinite buffering state + after a postroll ad + ([#6314](https://github.com/google/ExoPlayer/issues/6314)). +* Matroska: Support lacing in Blocks + ([#3026](https://github.com/google/ExoPlayer/issues/3026)). +* AV1 extension: + * New in this release. The AV1 extension allows use of the + [libgav1 software decoder](https://chromium.googlesource.com/codecs/libgav1/) + in ExoPlayer. You can read more about playing AV1 videos with ExoPlayer + [here](https://medium.com/google-exoplayer/playing-av1-videos-with-exoplayer-a7cb19bedef9). +* VP9 extension: + * Update to use NDK r20. + * Rename `VpxVideoSurfaceView` to `VideoDecoderSurfaceView` and move it to the + core library. + * Move `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` to + `C.MSG_SET_OUTPUT_BUFFER_RENDERER`. + * Use `VideoDecoderRenderer` as an implementation of + `VideoDecoderOutputBufferRenderer`, instead of `VideoDecoderSurfaceView`. +* Flac extension: Update to use NDK r20. +* Opus extension: Update to use NDK r20. +* FFmpeg extension: + * Update to use NDK r20. + * Update to use FFmpeg version 4.2. It is necessary to rebuild the native part + of the extension after this change, following the instructions in the + extension's readme. +* MediaSession extension: Add `MediaSessionConnector.setCaptionCallback` to + support `ACTION_SET_CAPTIONING_ENABLED` events. +* GVR extension: This extension is now deprecated. +* Demo apps: + * Add [SurfaceControl demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/surface) + to show how to use the Android 10 `SurfaceControl` API with ExoPlayer + ([#677](https://github.com/google/ExoPlayer/issues/677)). + * Add support for subtitle files to the + [Main demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/main) + ([#5523](https://github.com/google/ExoPlayer/issues/5523)). + * Remove the IMA demo app. IMA functionality is demonstrated by the + [main demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/main). + * Add basic DRM support to the + [Cast demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/cast). +* TestUtils: Publish the `testutils` module to simplify unit testing with + ExoPlayer ([#6267](https://github.com/google/ExoPlayer/issues/6267)). +* IMA extension: Remove `AdsManager` listeners on release to avoid leaking an + `AdEventListener` provided by the app + ([#6687](https://github.com/google/ExoPlayer/issues/6687)). + ### 2.10.8 (2019-11-19) ### * E-AC3 JOC @@ -23,7 +185,7 @@ * MediaSession extension: Update shuffle and repeat modes when playback state is invalidated ([#6582](https://github.com/google/ExoPlayer/issues/6582)). * Fix the start of audio getting truncated when transitioning to a new - item in a playlist of opus streams. + item in a playlist of Opus streams. ### 2.10.6 (2019-10-17) ### @@ -264,6 +426,7 @@ * Update `TrackSelection.Factory` interface to support creating all track selections together. * Allow to specify a selection reason for a `SelectionOverride`. + * Select audio track based on system language if no preference is provided. * When no text language preference matches, only select forced text tracks whose language matches the selected audio language. * UI: @@ -592,7 +755,7 @@ and `AnalyticsListener` callbacks ([#4361](https://github.com/google/ExoPlayer/issues/4361) and [#4615](https://github.com/google/ExoPlayer/issues/4615)). -* UI components: +* UI: * Add option to `PlayerView` to show buffering view when playWhenReady is false ([#4304](https://github.com/google/ExoPlayer/issues/4304)). * Allow any `Drawable` to be used as `PlayerView` default artwork. @@ -748,7 +911,7 @@ * OkHttp extension: Fix to correctly include response headers in thrown `InvalidResponseCodeException`s. * Add possibility to cancel `PlayerMessage`s. -* UI components: +* UI: * Add `PlayerView.setKeepContentOnPlayerReset` to keep the currently displayed video frame or media artwork visible when the player is reset ([#2843](https://github.com/google/ExoPlayer/issues/2843)). @@ -798,7 +961,7 @@ * Support live stream clipping with `ClippingMediaSource`. * Allow setting tags for all media sources in their factories. The tag of the current window can be retrieved with `Player.getCurrentTag`. -* UI components: +* UI: * Add support for displaying error messages and a buffering spinner in `PlayerView`. * Add support for listening to `AspectRatioFrameLayout`'s aspect ratio update @@ -962,7 +1125,7 @@ `SsMediaSource.Factory`, and `MergingMediaSource`. * Play out existing buffer before retrying for progressive live streams ([#1606](https://github.com/google/ExoPlayer/issues/1606)). -* UI components: +* UI: * Generalized player and control views to allow them to bind with any `Player`, and renamed them to `PlayerView` and `PlayerControlView` respectively. diff --git a/constants.gradle b/constants.gradle index edb099b7e8b..599af54dde2 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,17 +13,30 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.10.8' - releaseVersionCode = 2010008 + releaseVersion = '2.11.0' + releaseVersionCode = 2011000 minSdkVersion = 16 - targetSdkVersion = 28 - compileSdkVersion = 28 + appTargetSdkVersion = 29 + targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved + compileSdkVersion = 29 dexmakerVersion = '2.21.0' + guavaVersion = '23.5-android' mockitoVersion = '2.25.0' - robolectricVersion = '4.2' + robolectricVersion = '4.3' autoValueVersion = '1.6' + autoServiceVersion = '1.0-rc4' checkerframeworkVersion = '2.5.0' - androidXTestVersion = '1.1.0' + jsr305Version = '3.0.2' + kotlinAnnotationsVersion = '1.3.31' + androidxAnnotationVersion = '1.1.0' + androidxAppCompatVersion = '1.1.0' + androidxCollectionVersion = '1.1.0' + androidxMediaVersion = '1.0.1' + androidxTestCoreVersion = '1.2.0' + androidxTestJUnitVersion = '1.1.1' + androidxTestRunnerVersion = '1.2.0' + androidxTestRulesVersion = '1.2.0' + truthVersion = '0.44' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/core_settings.gradle b/core_settings.gradle index 38889e1a21a..0f9746af96e 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -24,7 +24,7 @@ include modulePrefix + 'library-hls' include modulePrefix + 'library-smoothstreaming' include modulePrefix + 'library-ui' include modulePrefix + 'testutils' -include modulePrefix + 'testutils-robolectric' +include modulePrefix + 'extension-av1' include modulePrefix + 'extension-ffmpeg' include modulePrefix + 'extension-flac' include modulePrefix + 'extension-gvr' @@ -47,7 +47,7 @@ project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hl project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming') project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui') project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils') -project(modulePrefix + 'testutils-robolectric').projectDir = new File(rootDir, 'testutils_robolectric') +project(modulePrefix + 'extension-av1').projectDir = new File(rootDir, 'extensions/av1') project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg') project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac') project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr') diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle index 85e60f27969..f9228e4b792 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -26,7 +26,7 @@ android { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode minSdkVersion project.ext.minSdkVersion - targetSdkVersion project.ext.targetSdkVersion + targetSdkVersion project.ext.appTargetSdkVersion } buildTypes { @@ -56,10 +56,9 @@ dependencies { implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'extension-cast') - implementation 'com.google.android.material:material:1.0.0' - implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion implementation 'androidx.recyclerview:recyclerview:1.0.0' + implementation 'com.google.android.material:material:1.0.0' } apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java deleted file mode 100644 index df153a14232..00000000000 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java +++ /dev/null @@ -1,399 +0,0 @@ -/* - * Copyright (C) 2017 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.castdemo; - -import android.content.Context; -import android.net.Uri; -import androidx.annotation.Nullable; -import android.view.KeyEvent; -import android.view.View; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultRenderersFactory; -import com.google.android.exoplayer2.ExoPlayerFactory; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Player.DiscontinuityReason; -import com.google.android.exoplayer2.Player.EventListener; -import com.google.android.exoplayer2.Player.TimelineChangeReason; -import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.Timeline.Period; -import com.google.android.exoplayer2.ext.cast.CastPlayer; -import com.google.android.exoplayer2.ext.cast.MediaItem; -import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; -import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.ProgressiveMediaSource; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.ui.PlayerControlView; -import com.google.android.exoplayer2.ui.PlayerView; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.gms.cast.MediaInfo; -import com.google.android.gms.cast.MediaMetadata; -import com.google.android.gms.cast.MediaQueueItem; -import com.google.android.gms.cast.framework.CastContext; -import java.util.ArrayList; - -/** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */ -/* package */ class DefaultReceiverPlayerManager - implements PlayerManager, EventListener, SessionAvailabilityListener { - - private static final String USER_AGENT = "ExoCastDemoPlayer"; - private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = - new DefaultHttpDataSourceFactory(USER_AGENT); - - private final PlayerView localPlayerView; - private final PlayerControlView castControlView; - private final SimpleExoPlayer exoPlayer; - private final CastPlayer castPlayer; - private final ArrayList mediaQueue; - private final Listener listener; - private final ConcatenatingMediaSource concatenatingMediaSource; - - private int currentItemIndex; - private Player currentPlayer; - - /** - * Creates a new manager for {@link SimpleExoPlayer} and {@link CastPlayer}. - * - * @param listener A {@link Listener} for queue position changes. - * @param localPlayerView The {@link PlayerView} for local playback. - * @param castControlView The {@link PlayerControlView} to control remote playback. - * @param context A {@link Context}. - * @param castContext The {@link CastContext}. - */ - public DefaultReceiverPlayerManager( - Listener listener, - PlayerView localPlayerView, - PlayerControlView castControlView, - Context context, - CastContext castContext) { - this.listener = listener; - this.localPlayerView = localPlayerView; - this.castControlView = castControlView; - mediaQueue = new ArrayList<>(); - currentItemIndex = C.INDEX_UNSET; - concatenatingMediaSource = new ConcatenatingMediaSource(); - - DefaultTrackSelector trackSelector = new DefaultTrackSelector(); - RenderersFactory renderersFactory = new DefaultRenderersFactory(context); - exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector); - exoPlayer.addListener(this); - localPlayerView.setPlayer(exoPlayer); - - castPlayer = new CastPlayer(castContext); - castPlayer.addListener(this); - castPlayer.setSessionAvailabilityListener(this); - castControlView.setPlayer(castPlayer); - - setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer); - } - - // Queue manipulation methods. - - /** - * Plays a specified queue item in the current player. - * - * @param itemIndex The index of the item to play. - */ - @Override - public void selectQueueItem(int itemIndex) { - setCurrentItem(itemIndex, C.TIME_UNSET, true); - } - - /** Returns the index of the currently played item. */ - @Override - public int getCurrentItemIndex() { - return currentItemIndex; - } - - /** - * Appends {@code item} to the media queue. - * - * @param item The {@link MediaItem} to append. - */ - @Override - public void addItem(MediaItem item) { - mediaQueue.add(item); - concatenatingMediaSource.addMediaSource(buildMediaSource(item)); - if (currentPlayer == castPlayer) { - castPlayer.addItems(buildMediaQueueItem(item)); - } - } - - /** Returns the size of the media queue. */ - @Override - public int getMediaQueueSize() { - return mediaQueue.size(); - } - - /** - * Returns the item at the given index in the media queue. - * - * @param position The index of the item. - * @return The item at the given index in the media queue. - */ - @Override - public MediaItem getItem(int position) { - return mediaQueue.get(position); - } - - /** - * Removes the item at the given index from the media queue. - * - * @param item The item to remove. - * @return Whether the removal was successful. - */ - @Override - public boolean removeItem(MediaItem item) { - int itemIndex = mediaQueue.indexOf(item); - if (itemIndex == -1) { - return false; - } - concatenatingMediaSource.removeMediaSource(itemIndex); - if (currentPlayer == castPlayer) { - if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { - Timeline castTimeline = castPlayer.getCurrentTimeline(); - if (castTimeline.getPeriodCount() <= itemIndex) { - return false; - } - castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id); - } - } - mediaQueue.remove(itemIndex); - if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) { - maybeSetCurrentItemAndNotify(C.INDEX_UNSET); - } else if (itemIndex < currentItemIndex) { - maybeSetCurrentItemAndNotify(currentItemIndex - 1); - } - return true; - } - - /** - * Moves an item within the queue. - * - * @param item The item to move. - * @param toIndex The target index of the item in the queue. - * @return Whether the item move was successful. - */ - @Override - public boolean moveItem(MediaItem item, int toIndex) { - int fromIndex = mediaQueue.indexOf(item); - if (fromIndex == -1) { - return false; - } - // Player update. - concatenatingMediaSource.moveMediaSource(fromIndex, toIndex); - if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) { - Timeline castTimeline = castPlayer.getCurrentTimeline(); - int periodCount = castTimeline.getPeriodCount(); - if (periodCount <= fromIndex || periodCount <= toIndex) { - return false; - } - int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id; - castPlayer.moveItem(elementId, toIndex); - } - - mediaQueue.add(toIndex, mediaQueue.remove(fromIndex)); - - // Index update. - if (fromIndex == currentItemIndex) { - maybeSetCurrentItemAndNotify(toIndex); - } else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) { - maybeSetCurrentItemAndNotify(currentItemIndex - 1); - } else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) { - maybeSetCurrentItemAndNotify(currentItemIndex + 1); - } - - return true; - } - - /** - * Dispatches a given {@link KeyEvent} to the corresponding view of the current player. - * - * @param event The {@link KeyEvent}. - * @return Whether the event was handled by the target view. - */ - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - if (currentPlayer == exoPlayer) { - return localPlayerView.dispatchKeyEvent(event); - } else /* currentPlayer == castPlayer */ { - return castControlView.dispatchKeyEvent(event); - } - } - - /** Releases the manager and the players that it holds. */ - @Override - public void release() { - currentItemIndex = C.INDEX_UNSET; - mediaQueue.clear(); - concatenatingMediaSource.clear(); - castPlayer.setSessionAvailabilityListener(null); - castPlayer.release(); - localPlayerView.setPlayer(null); - exoPlayer.release(); - } - - // Player.EventListener implementation. - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - updateCurrentItemIndex(); - } - - @Override - public void onPositionDiscontinuity(@DiscontinuityReason int reason) { - updateCurrentItemIndex(); - } - - @Override - public void onTimelineChanged( - Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { - updateCurrentItemIndex(); - } - - // CastPlayer.SessionAvailabilityListener implementation. - - @Override - public void onCastSessionAvailable() { - setCurrentPlayer(castPlayer); - } - - @Override - public void onCastSessionUnavailable() { - setCurrentPlayer(exoPlayer); - } - - // Internal methods. - - private void updateCurrentItemIndex() { - int playbackState = currentPlayer.getPlaybackState(); - maybeSetCurrentItemAndNotify( - playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED - ? currentPlayer.getCurrentWindowIndex() - : C.INDEX_UNSET); - } - - private void setCurrentPlayer(Player currentPlayer) { - if (this.currentPlayer == currentPlayer) { - return; - } - - // View management. - if (currentPlayer == exoPlayer) { - localPlayerView.setVisibility(View.VISIBLE); - castControlView.hide(); - } else /* currentPlayer == castPlayer */ { - localPlayerView.setVisibility(View.GONE); - castControlView.show(); - } - - // Player state management. - long playbackPositionMs = C.TIME_UNSET; - int windowIndex = C.INDEX_UNSET; - boolean playWhenReady = false; - if (this.currentPlayer != null) { - int playbackState = this.currentPlayer.getPlaybackState(); - if (playbackState != Player.STATE_ENDED) { - playbackPositionMs = this.currentPlayer.getCurrentPosition(); - playWhenReady = this.currentPlayer.getPlayWhenReady(); - windowIndex = this.currentPlayer.getCurrentWindowIndex(); - if (windowIndex != currentItemIndex) { - playbackPositionMs = C.TIME_UNSET; - windowIndex = currentItemIndex; - } - } - this.currentPlayer.stop(true); - } else { - // This is the initial setup. No need to save any state. - } - - this.currentPlayer = currentPlayer; - - // Media queue management. - if (currentPlayer == exoPlayer) { - exoPlayer.prepare(concatenatingMediaSource); - } - - // Playback transition. - if (windowIndex != C.INDEX_UNSET) { - setCurrentItem(windowIndex, playbackPositionMs, playWhenReady); - } - } - - /** - * Starts playback of the item at the given position. - * - * @param itemIndex The index of the item to play. - * @param positionMs The position at which playback should start. - * @param playWhenReady Whether the player should proceed when ready to do so. - */ - private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { - maybeSetCurrentItemAndNotify(itemIndex); - if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) { - MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; - for (int i = 0; i < items.length; i++) { - items[i] = buildMediaQueueItem(mediaQueue.get(i)); - } - castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); - } else { - currentPlayer.seekTo(itemIndex, positionMs); - currentPlayer.setPlayWhenReady(playWhenReady); - } - } - - private void maybeSetCurrentItemAndNotify(int currentItemIndex) { - if (this.currentItemIndex != currentItemIndex) { - int oldIndex = this.currentItemIndex; - this.currentItemIndex = currentItemIndex; - listener.onQueuePositionChanged(oldIndex, currentItemIndex); - } - } - - private static MediaSource buildMediaSource(MediaItem item) { - Uri uri = item.media.uri; - switch (item.mimeType) { - case DemoUtil.MIME_TYPE_SS: - return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - case DemoUtil.MIME_TYPE_DASH: - return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - case DemoUtil.MIME_TYPE_HLS: - return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - case DemoUtil.MIME_TYPE_VIDEO_MP4: - return new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - default: - { - throw new IllegalStateException("Unsupported type: " + item.mimeType); - } - } - } - - private static MediaQueueItem buildMediaQueueItem(MediaItem item) { - MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); - movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title); - MediaInfo mediaInfo = - new MediaInfo.Builder(item.media.uri.toString()) - .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) - .setContentType(item.mimeType) - .setMetadata(movieMetadata) - .build(); - return new MediaQueueItem.Builder(mediaInfo).build(); - } -} diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java index 96253042523..dacdbfe6160 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java @@ -16,87 +16,86 @@ package com.google.android.exoplayer2.castdemo; import android.net.Uri; -import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ext.cast.MediaItem; +import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration; import com.google.android.exoplayer2.util.MimeTypes; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.UUID; /** Utility methods and constants for the Cast demo application. */ /* package */ final class DemoUtil { - /** Represents a media sample. */ - public static final class Sample { - - /** The uri of the media content. */ - public final String uri; - /** The name of the sample. */ - public final String name; - /** The mime type of the sample media content. */ - public final String mimeType; - /** - * The {@link UUID} of the DRM scheme that protects the content, or null if the content is not - * DRM-protected. - */ - @Nullable public final UUID drmSchemeUuid; - /** - * The url from which players should obtain DRM licenses, or null if the content is not - * DRM-protected. - */ - @Nullable public final Uri licenseServerUri; - - /** - * @param uri See {@link #uri}. - * @param name See {@link #name}. - * @param mimeType See {@link #mimeType}. - */ - public Sample(String uri, String name, String mimeType) { - this(uri, name, mimeType, /* drmSchemeUuid= */ null, /* licenseServerUriString= */ null); - } - - public Sample( - String uri, - String name, - String mimeType, - @Nullable UUID drmSchemeUuid, - @Nullable String licenseServerUriString) { - this.uri = uri; - this.name = name; - this.mimeType = mimeType; - this.drmSchemeUuid = drmSchemeUuid; - this.licenseServerUri = - licenseServerUriString != null ? Uri.parse(licenseServerUriString) : null; - } - - @Override - public String toString() { - return name; - } - } - public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD; public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8; public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS; public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4; /** The list of samples available in the cast demo app. */ - public static final List SAMPLES; + public static final List SAMPLES; static { - // App samples. - ArrayList samples = new ArrayList<>(); + ArrayList samples = new ArrayList<>(); // Clear content. samples.add( - new Sample( - "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd", - "Clear DASH: Tears", - MIME_TYPE_DASH)); + new MediaItem.Builder() + .setUri("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd") + .setTitle("Clear DASH: Tears") + .setMimeType(MIME_TYPE_DASH) + .build()); + samples.add( + new MediaItem.Builder() + .setUri("https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8") + .setTitle("Clear HLS: Angel one") + .setMimeType(MIME_TYPE_HLS) + .build()); + samples.add( + new MediaItem.Builder() + .setUri("https://html5demos.com/assets/dizzy.mp4") + .setTitle("Clear MP4: Dizzy") + .setMimeType(MIME_TYPE_VIDEO_MP4) + .build()); + + // DRM content. + samples.add( + new MediaItem.Builder() + .setUri(Uri.parse("https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd")) + .setTitle("Widevine DASH cenc: Tears") + .setMimeType(MIME_TYPE_DASH) + .setDrmConfiguration( + new DrmConfiguration( + C.WIDEVINE_UUID, + Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"), + Collections.emptyMap())) + .build()); + samples.add( + new MediaItem.Builder() + .setUri( + Uri.parse( + "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd")) + .setTitle("Widevine DASH cbc1: Tears") + .setMimeType(MIME_TYPE_DASH) + .setDrmConfiguration( + new DrmConfiguration( + C.WIDEVINE_UUID, + Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"), + Collections.emptyMap())) + .build()); samples.add( - new Sample( - "https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4)); + new MediaItem.Builder() + .setUri( + Uri.parse( + "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd")) + .setTitle("Widevine DASH cbcs: Tears") + .setMimeType(MIME_TYPE_DASH) + .setDrmConfiguration( + new DrmConfiguration( + C.WIDEVINE_UUID, + Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"), + Collections.emptyMap())) + .build()); SAMPLES = Collections.unmodifiableList(samples); } diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 17eeed2da76..0c5b5037f56 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -17,13 +17,6 @@ import android.content.Context; import android.os.Bundle; -import androidx.core.graphics.ColorUtils; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.RecyclerView.ViewHolder; -import androidx.recyclerview.widget.ItemTouchHelper; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; @@ -34,16 +27,23 @@ import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.ColorUtils; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.ext.cast.MediaItem; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; -import com.google.android.gms.cast.CastMediaControlIntent; import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.gms.cast.framework.CastContext; import com.google.android.gms.dynamite.DynamiteModule; -import java.util.Collections; /** * An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's @@ -52,8 +52,6 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, PlayerManager.Listener { - private final MediaItem.Builder mediaItemBuilder; - private PlayerView localPlayerView; private PlayerControlView castControlView; private PlayerManager playerManager; @@ -61,10 +59,6 @@ public class MainActivity extends AppCompatActivity private MediaQueueListAdapter mediaQueueListAdapter; private CastContext castContext; - public MainActivity() { - mediaItemBuilder = new MediaItem.Builder(); - } - // Activity lifecycle methods. @Override @@ -118,20 +112,13 @@ public void onResume() { // There is no Cast context to work with. Do nothing. return; } - String applicationId = castContext.getCastOptions().getReceiverApplicationId(); - switch (applicationId) { - case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID: - playerManager = - new DefaultReceiverPlayerManager( - /* listener= */ this, - localPlayerView, - castControlView, - /* context= */ this, - castContext); - break; - default: - throw new IllegalStateException("Illegal receiver app id: " + applicationId); - } + playerManager = + new PlayerManager( + /* listener= */ this, + localPlayerView, + castControlView, + /* context= */ this, + castContext); mediaQueueList.setAdapter(mediaQueueListAdapter); } @@ -179,36 +166,29 @@ public void onQueuePositionChanged(int previousIndex, int newIndex) { } @Override - public void onQueueContentsExternallyChanged() { - mediaQueueListAdapter.notifyDataSetChanged(); - } - - @Override - public void onPlayerError() { - Toast.makeText(getApplicationContext(), R.string.player_error_msg, Toast.LENGTH_LONG).show(); + public void onUnsupportedTrack(int trackType) { + if (trackType == C.TRACK_TYPE_AUDIO) { + showToast(R.string.error_unsupported_audio); + } else if (trackType == C.TRACK_TYPE_VIDEO) { + showToast(R.string.error_unsupported_video); + } else { + // Do nothing. + } } // Internal methods. + private void showToast(int messageId) { + Toast.makeText(getApplicationContext(), messageId, Toast.LENGTH_LONG).show(); + } + private View buildSampleListView() { View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null); ListView sampleList = dialogList.findViewById(R.id.sample_list); sampleList.setAdapter(new SampleListAdapter(this)); sampleList.setOnItemClickListener( (parent, view, position, id) -> { - DemoUtil.Sample sample = DemoUtil.SAMPLES.get(position); - mediaItemBuilder - .clear() - .setMedia(sample.uri) - .setTitle(sample.name) - .setMimeType(sample.mimeType); - if (sample.drmSchemeUuid != null) { - mediaItemBuilder.setDrmSchemes( - Collections.singletonList( - new MediaItem.DrmScheme( - sample.drmSchemeUuid, new MediaItem.UriBundle(sample.licenseServerUri)))); - } - playerManager.addItem(mediaItemBuilder.build()); + playerManager.addItem(DemoUtil.SAMPLES.get(position)); mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); }); return dialogList; @@ -231,8 +211,10 @@ public void onBindViewHolder(QueueItemViewHolder holder, int position) { TextView view = holder.textView; view.setText(holder.item.title); // TODO: Solve coloring using the theme's ColorStateList. - view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(), - position == playerManager.getCurrentItemIndex() ? 255 : 100)); + view.setTextColor( + ColorUtils.setAlphaComponent( + view.getCurrentTextColor(), + position == playerManager.getCurrentItemIndex() ? 255 : 100)); } @Override @@ -312,11 +294,18 @@ public void onClick(View v) { } } - private static final class SampleListAdapter extends ArrayAdapter { + private static final class SampleListAdapter extends ArrayAdapter { public SampleListAdapter(Context context) { super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); } - } + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + View view = super.getView(position, convertView, parent); + ((TextView) view).setText(getItem(position).title); + return view; + } + } } diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index c9a728b3ffd..85104e0d188 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -15,12 +15,49 @@ */ package com.google.android.exoplayer2.castdemo; +import android.content.Context; +import android.net.Uri; import android.view.KeyEvent; +import android.view.View; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.EventListener; +import com.google.android.exoplayer2.Player.TimelineChangeReason; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.drm.FrameworkMediaDrm; +import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; +import com.google.android.exoplayer2.ext.cast.CastPlayer; +import com.google.android.exoplayer2.ext.cast.DefaultMediaItemConverter; import com.google.android.exoplayer2.ext.cast.MediaItem; +import com.google.android.exoplayer2.ext.cast.MediaItemConverter; +import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; +import com.google.android.exoplayer2.source.ConcatenatingMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.ui.PlayerControlView; +import com.google.android.exoplayer2.ui.PlayerView; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.util.Util; +import com.google.android.gms.cast.MediaQueueItem; +import com.google.android.gms.cast.framework.CastContext; +import java.util.ArrayList; +import java.util.Map; -/** Manages the players in the Cast demo app. */ -/* package */ interface PlayerManager { +/** Manages players and an internal media queue for the demo app. */ +/* package */ class PlayerManager implements EventListener, SessionAvailabilityListener { /** Listener for events. */ interface Listener { @@ -28,40 +65,395 @@ interface Listener { /** Called when the currently played item of the media queue changes. */ void onQueuePositionChanged(int previousIndex, int newIndex); - /** Called when the media queue changes due to modifications not caused by this manager. */ - void onQueueContentsExternallyChanged(); + /** + * Called when a track of type {@code trackType} is not supported by the player. + * + * @param trackType One of the {@link C}{@code .TRACK_TYPE_*} constants. + */ + void onUnsupportedTrack(int trackType); + } + + private static final String USER_AGENT = "ExoCastDemoPlayer"; + private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = + new DefaultHttpDataSourceFactory(USER_AGENT); + + private final PlayerView localPlayerView; + private final PlayerControlView castControlView; + private final DefaultTrackSelector trackSelector; + private final SimpleExoPlayer exoPlayer; + private final CastPlayer castPlayer; + private final ArrayList mediaQueue; + private final Listener listener; + private final ConcatenatingMediaSource concatenatingMediaSource; + private final MediaItemConverter mediaItemConverter; + + private TrackGroupArray lastSeenTrackGroupArray; + private int currentItemIndex; + private Player currentPlayer; + + /** + * Creates a new manager for {@link SimpleExoPlayer} and {@link CastPlayer}. + * + * @param listener A {@link Listener} for queue position changes. + * @param localPlayerView The {@link PlayerView} for local playback. + * @param castControlView The {@link PlayerControlView} to control remote playback. + * @param context A {@link Context}. + * @param castContext The {@link CastContext}. + */ + public PlayerManager( + Listener listener, + PlayerView localPlayerView, + PlayerControlView castControlView, + Context context, + CastContext castContext) { + this.listener = listener; + this.localPlayerView = localPlayerView; + this.castControlView = castControlView; + mediaQueue = new ArrayList<>(); + currentItemIndex = C.INDEX_UNSET; + concatenatingMediaSource = new ConcatenatingMediaSource(); + mediaItemConverter = new DefaultMediaItemConverter(); + + trackSelector = new DefaultTrackSelector(context); + exoPlayer = new SimpleExoPlayer.Builder(context).setTrackSelector(trackSelector).build(); + exoPlayer.addListener(this); + localPlayerView.setPlayer(exoPlayer); + + castPlayer = new CastPlayer(castContext); + castPlayer.addListener(this); + castPlayer.setSessionAvailabilityListener(this); + castControlView.setPlayer(castPlayer); - /** Called when an error occurs in the current player. */ - void onPlayerError(); + setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer); } - /** Redirects the given {@code keyEvent} to the active player. */ - boolean dispatchKeyEvent(KeyEvent keyEvent); + // Queue manipulation methods. + + /** + * Plays a specified queue item in the current player. + * + * @param itemIndex The index of the item to play. + */ + public void selectQueueItem(int itemIndex) { + setCurrentItem(itemIndex, C.TIME_UNSET, true); + } + + /** Returns the index of the currently played item. */ + public int getCurrentItemIndex() { + return currentItemIndex; + } + + /** + * Appends {@code item} to the media queue. + * + * @param item The {@link MediaItem} to append. + */ + public void addItem(MediaItem item) { + mediaQueue.add(item); + concatenatingMediaSource.addMediaSource(buildMediaSource(item)); + if (currentPlayer == castPlayer) { + castPlayer.addItems(mediaItemConverter.toMediaQueueItem(item)); + } + } - /** Appends the given {@link MediaItem} to the media queue. */ - void addItem(MediaItem mediaItem); + /** Returns the size of the media queue. */ + public int getMediaQueueSize() { + return mediaQueue.size(); + } - /** Returns the number of items in the media queue. */ - int getMediaQueueSize(); + /** + * Returns the item at the given index in the media queue. + * + * @param position The index of the item. + * @return The item at the given index in the media queue. + */ + public MediaItem getItem(int position) { + return mediaQueue.get(position); + } - /** Selects the item at the given position for playback. */ - void selectQueueItem(int position); + /** + * Removes the item at the given index from the media queue. + * + * @param item The item to remove. + * @return Whether the removal was successful. + */ + public boolean removeItem(MediaItem item) { + int itemIndex = mediaQueue.indexOf(item); + if (itemIndex == -1) { + return false; + } + concatenatingMediaSource.removeMediaSource(itemIndex); + if (currentPlayer == castPlayer) { + if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { + Timeline castTimeline = castPlayer.getCurrentTimeline(); + if (castTimeline.getPeriodCount() <= itemIndex) { + return false; + } + castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id); + } + } + mediaQueue.remove(itemIndex); + if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) { + maybeSetCurrentItemAndNotify(C.INDEX_UNSET); + } else if (itemIndex < currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex - 1); + } + return true; + } /** - * Returns the position of the item currently being played, or {@link C#INDEX_UNSET} if no item is - * being played. + * Moves an item within the queue. + * + * @param item The item to move. + * @param toIndex The target index of the item in the queue. + * @return Whether the item move was successful. */ - int getCurrentItemIndex(); + public boolean moveItem(MediaItem item, int toIndex) { + int fromIndex = mediaQueue.indexOf(item); + if (fromIndex == -1) { + return false; + } + // Player update. + concatenatingMediaSource.moveMediaSource(fromIndex, toIndex); + if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) { + Timeline castTimeline = castPlayer.getCurrentTimeline(); + int periodCount = castTimeline.getPeriodCount(); + if (periodCount <= fromIndex || periodCount <= toIndex) { + return false; + } + int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id; + castPlayer.moveItem(elementId, toIndex); + } - /** Returns the {@link MediaItem} at the given {@code position}. */ - MediaItem getItem(int position); + mediaQueue.add(toIndex, mediaQueue.remove(fromIndex)); - /** Moves the item at position {@code from} to position {@code to}. */ - boolean moveItem(MediaItem item, int to); + // Index update. + if (fromIndex == currentItemIndex) { + maybeSetCurrentItemAndNotify(toIndex); + } else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex - 1); + } else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex + 1); + } + + return true; + } + + /** + * Dispatches a given {@link KeyEvent} to the corresponding view of the current player. + * + * @param event The {@link KeyEvent}. + * @return Whether the event was handled by the target view. + */ + public boolean dispatchKeyEvent(KeyEvent event) { + if (currentPlayer == exoPlayer) { + return localPlayerView.dispatchKeyEvent(event); + } else /* currentPlayer == castPlayer */ { + return castControlView.dispatchKeyEvent(event); + } + } + + /** Releases the manager and the players that it holds. */ + public void release() { + currentItemIndex = C.INDEX_UNSET; + mediaQueue.clear(); + concatenatingMediaSource.clear(); + castPlayer.setSessionAvailabilityListener(null); + castPlayer.release(); + localPlayerView.setPlayer(null); + exoPlayer.release(); + } - /** Removes the item at position {@code index}. */ - boolean removeItem(MediaItem item); + // Player.EventListener implementation. - /** Releases any acquired resources. */ - void release(); + @Override + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + updateCurrentItemIndex(); + } + + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + updateCurrentItemIndex(); + } + + @Override + public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) { + updateCurrentItemIndex(); + } + + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + if (currentPlayer == exoPlayer && trackGroups != lastSeenTrackGroupArray) { + MappingTrackSelector.MappedTrackInfo mappedTrackInfo = + trackSelector.getCurrentMappedTrackInfo(); + if (mappedTrackInfo != null) { + if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO) + == MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { + listener.onUnsupportedTrack(C.TRACK_TYPE_VIDEO); + } + if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO) + == MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { + listener.onUnsupportedTrack(C.TRACK_TYPE_AUDIO); + } + } + lastSeenTrackGroupArray = trackGroups; + } + } + + // CastPlayer.SessionAvailabilityListener implementation. + + @Override + public void onCastSessionAvailable() { + setCurrentPlayer(castPlayer); + } + + @Override + public void onCastSessionUnavailable() { + setCurrentPlayer(exoPlayer); + } + + // Internal methods. + + private void updateCurrentItemIndex() { + int playbackState = currentPlayer.getPlaybackState(); + maybeSetCurrentItemAndNotify( + playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED + ? currentPlayer.getCurrentWindowIndex() + : C.INDEX_UNSET); + } + + private void setCurrentPlayer(Player currentPlayer) { + if (this.currentPlayer == currentPlayer) { + return; + } + + // View management. + if (currentPlayer == exoPlayer) { + localPlayerView.setVisibility(View.VISIBLE); + castControlView.hide(); + } else /* currentPlayer == castPlayer */ { + localPlayerView.setVisibility(View.GONE); + castControlView.show(); + } + + // Player state management. + long playbackPositionMs = C.TIME_UNSET; + int windowIndex = C.INDEX_UNSET; + boolean playWhenReady = false; + + Player previousPlayer = this.currentPlayer; + if (previousPlayer != null) { + // Save state from the previous player. + int playbackState = previousPlayer.getPlaybackState(); + if (playbackState != Player.STATE_ENDED) { + playbackPositionMs = previousPlayer.getCurrentPosition(); + playWhenReady = previousPlayer.getPlayWhenReady(); + windowIndex = previousPlayer.getCurrentWindowIndex(); + if (windowIndex != currentItemIndex) { + playbackPositionMs = C.TIME_UNSET; + windowIndex = currentItemIndex; + } + } + previousPlayer.stop(true); + } + + this.currentPlayer = currentPlayer; + + // Media queue management. + if (currentPlayer == exoPlayer) { + exoPlayer.prepare(concatenatingMediaSource); + } + + // Playback transition. + if (windowIndex != C.INDEX_UNSET) { + setCurrentItem(windowIndex, playbackPositionMs, playWhenReady); + } + } + + /** + * Starts playback of the item at the given position. + * + * @param itemIndex The index of the item to play. + * @param positionMs The position at which playback should start. + * @param playWhenReady Whether the player should proceed when ready to do so. + */ + private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { + maybeSetCurrentItemAndNotify(itemIndex); + if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) { + MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; + for (int i = 0; i < items.length; i++) { + items[i] = mediaItemConverter.toMediaQueueItem(mediaQueue.get(i)); + } + castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); + } else { + currentPlayer.seekTo(itemIndex, positionMs); + currentPlayer.setPlayWhenReady(playWhenReady); + } + } + + private void maybeSetCurrentItemAndNotify(int currentItemIndex) { + if (this.currentItemIndex != currentItemIndex) { + int oldIndex = this.currentItemIndex; + this.currentItemIndex = currentItemIndex; + listener.onQueuePositionChanged(oldIndex, currentItemIndex); + } + } + + private MediaSource buildMediaSource(MediaItem item) { + Uri uri = item.uri; + String mimeType = item.mimeType; + if (mimeType == null) { + throw new IllegalArgumentException("mimeType is required"); + } + + DrmSessionManager drmSessionManager = + DrmSessionManager.getDummyDrmSessionManager(); + MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration; + if (drmConfiguration != null && Util.SDK_INT >= 18) { + String licenseServerUrl = + drmConfiguration.licenseUri != null ? drmConfiguration.licenseUri.toString() : ""; + HttpMediaDrmCallback drmCallback = + new HttpMediaDrmCallback(licenseServerUrl, DATA_SOURCE_FACTORY); + for (Map.Entry requestHeader : drmConfiguration.requestHeaders.entrySet()) { + drmCallback.setKeyRequestProperty(requestHeader.getKey(), requestHeader.getValue()); + } + drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setMultiSession(/* multiSession= */ true) + .setUuidAndExoMediaDrmProvider( + drmConfiguration.uuid, FrameworkMediaDrm.DEFAULT_PROVIDER) + .build(drmCallback); + } + + MediaSource createdMediaSource; + switch (mimeType) { + case DemoUtil.MIME_TYPE_SS: + createdMediaSource = + new SsMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; + case DemoUtil.MIME_TYPE_DASH: + createdMediaSource = + new DashMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; + case DemoUtil.MIME_TYPE_HLS: + createdMediaSource = + new HlsMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; + case DemoUtil.MIME_TYPE_VIDEO_MP4: + createdMediaSource = + new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; + default: + throw new IllegalArgumentException("mimeType is unsupported: " + mimeType); + } + return createdMediaSource; + } } diff --git a/demos/cast/src/main/res/values/strings.xml b/demos/cast/src/main/res/values/strings.xml index 013b50a175c..69f0691630e 100644 --- a/demos/cast/src/main/res/values/strings.xml +++ b/demos/cast/src/main/res/values/strings.xml @@ -24,6 +24,8 @@ Failed to get Cast context. Try updating Google Play Services and restart the app. - Player error encountered. Select a queue item to reprepare. Check the logcat and receiver app\'s console for more info. + Media includes video tracks, but none are playable by this device + + Media includes audio tracks, but none are playable by this device diff --git a/demos/ima/README.md b/demos/ima/README.md deleted file mode 100644 index 8002b566678..00000000000 --- a/demos/ima/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# IMA demo application # - -This folder contains a demo application that showcases ExoPlayer integration -with the IMA SDK. diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/MainActivity.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/MainActivity.java deleted file mode 100644 index 9988108f32c..00000000000 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/MainActivity.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2017 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.imademo; - -import android.app.Activity; -import android.os.Bundle; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.ui.PlayerView; - -/** - * Main Activity for the IMA plugin demo. {@link ExoPlayer} objects are created by - * {@link PlayerManager}, which this class instantiates. - */ -public final class MainActivity extends Activity { - - private PlayerView playerView; - private PlayerManager player; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.main_activity); - playerView = findViewById(R.id.player_view); - player = new PlayerManager(this); - } - - @Override - public void onResume() { - super.onResume(); - player.init(this, playerView); - } - - @Override - public void onPause() { - super.onPause(); - player.reset(); - } - - @Override - public void onDestroy() { - player.release(); - super.onDestroy(); - } - -} diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java deleted file mode 100644 index 05c804c7a82..00000000000 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (C) 2017 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.imademo; - -import android.content.Context; -import android.net.Uri; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.C.ContentType; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.ExoPlayerFactory; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.ProgressiveMediaSource; -import com.google.android.exoplayer2.source.ads.AdsMediaSource; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.ui.PlayerView; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.util.Util; - -/** Manages the {@link ExoPlayer}, the IMA plugin and all video playback. */ -/* package */ final class PlayerManager implements AdsMediaSource.MediaSourceFactory { - - private final ImaAdsLoader adsLoader; - private final DataSource.Factory dataSourceFactory; - - private SimpleExoPlayer player; - private long contentPosition; - - public PlayerManager(Context context) { - String adTag = context.getString(R.string.ad_tag_url); - adsLoader = new ImaAdsLoader(context, Uri.parse(adTag)); - dataSourceFactory = - new DefaultDataSourceFactory( - context, Util.getUserAgent(context, context.getString(R.string.application_name))); - } - - public void init(Context context, PlayerView playerView) { - // Create a player instance. - player = ExoPlayerFactory.newSimpleInstance(context); - adsLoader.setPlayer(player); - playerView.setPlayer(player); - - // This is the MediaSource representing the content media (i.e. not the ad). - String contentUrl = context.getString(R.string.content_url); - MediaSource contentMediaSource = buildMediaSource(Uri.parse(contentUrl)); - - // Compose the content media source into a new AdsMediaSource with both ads and content. - MediaSource mediaSourceWithAds = - new AdsMediaSource( - contentMediaSource, /* adMediaSourceFactory= */ this, adsLoader, playerView); - - // Prepare the player with the source. - player.seekTo(contentPosition); - player.prepare(mediaSourceWithAds); - player.setPlayWhenReady(true); - } - - public void reset() { - if (player != null) { - contentPosition = player.getContentPosition(); - player.release(); - player = null; - adsLoader.setPlayer(null); - } - } - - public void release() { - if (player != null) { - player.release(); - player = null; - } - adsLoader.release(); - } - - // AdsMediaSource.MediaSourceFactory implementation. - - @Override - public MediaSource createMediaSource(Uri uri) { - return buildMediaSource(uri); - } - - @Override - public int[] getSupportedTypes() { - // IMA does not support Smooth Streaming ads. - return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER}; - } - - // Internal methods. - - private MediaSource buildMediaSource(Uri uri) { - @ContentType int type = Util.inferContentType(uri); - switch (type) { - case C.TYPE_DASH: - return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri); - case C.TYPE_SS: - return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); - case C.TYPE_HLS: - return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); - case C.TYPE_OTHER: - return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri); - default: - throw new IllegalStateException("Unsupported type: " + type); - } - } - -} diff --git a/demos/ima/src/main/res/values/strings.xml b/demos/ima/src/main/res/values/strings.xml deleted file mode 100644 index 2eb5700bf0a..00000000000 --- a/demos/ima/src/main/res/values/strings.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - Exo IMA Demo - - - - - - diff --git a/demos/ima/src/main/res/values/styles.xml b/demos/ima/src/main/res/values/styles.xml deleted file mode 100644 index 1c78ad58df8..00000000000 --- a/demos/ima/src/main/res/values/styles.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - diff --git a/demos/main/build.gradle b/demos/main/build.gradle index 06c5d1ffb72..ab47b6de811 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -26,7 +26,7 @@ android { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode minSdkVersion project.ext.minSdkVersion - targetSdkVersion project.ext.targetSdkVersion + targetSdkVersion project.ext.appTargetSdkVersion } buildTypes { @@ -62,15 +62,15 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.1.0' - implementation 'androidx.legacy:legacy-support-core-ui:1.0.0' - implementation 'androidx.fragment:fragment:1.0.0' + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion implementation 'com.google.android.material:material:1.0.0' implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-dash') implementation project(modulePrefix + 'library-hls') implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'library-ui') + withExtensionsImplementation project(path: modulePrefix + 'extension-av1') withExtensionsImplementation project(path: modulePrefix + 'extension-ffmpeg') withExtensionsImplementation project(path: modulePrefix + 'extension-flac') withExtensionsImplementation project(path: modulePrefix + 'extension-ima') diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 355ba434058..0240a377ac3 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -34,6 +34,7 @@ android:banner="@drawable/ic_banner" android:largeHeap="true" android:allowBackup="false" + android:requestLegacyExternalStorage="true" android:name="com.google.android.exoplayer2.demo.DemoApplication" tools:ignore="UnusedAttribute"> diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index bcb3ef4ad1a..01980c2f369 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -376,44 +376,48 @@ "uri": "https://html5demos.com/assets/dizzy.mp4" }, { - "name": "Apple AAC 10s", + "name": "Apple 10s (AAC)", "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac" }, { - "name": "Apple TS 10s", + "name": "Apple 10s (TS)", "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts" }, { - "name": "Android screens (Matroska)", + "name": "Android screens (MKV)", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" }, { - "name": "Screens 360P (WebM,VP9,No Audio)", + "name": "Screens 360p video (WebM,VP9)", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm" }, { - "name": "Screens 480p (FMP4,H264,No Audio)", + "name": "Screens 480p video (FMP4,H264)", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4" }, { - "name": "Screens 1080p (FMP4,H264, No Audio)", + "name": "Screens 1080p video (FMP4,H264)", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-137.mp4" }, { - "name": "Screens (FMP4,AAC Audio)", + "name": "Screens audio (FMP4)", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" }, { - "name": "Google Play (MP3 Audio)", + "name": "Google Play (MP3)", "uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3" }, { - "name": "Google Play (Ogg/Vorbis Audio)", + "name": "Google Play (Ogg/Vorbis)", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg" }, { - "name": "Big Buck Bunny (FLV Video)", + "name": "Big Buck Bunny video (FLV)", "uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0" + }, + { + "name": "Big Buck Bunny 480p video (MP4,AV1)", + "uri": "https://storage.googleapis.com/downloads.webmproject.org/av1/exoplayer/bbb-av1-480p.mp4" } ] }, @@ -447,23 +451,27 @@ }, { "name": "Clear -> Enc -> Clear -> Enc -> Enc", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test", "playlist": [ { "uri": "https://html5demos.com/assets/dizzy.mp4" }, { - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, { "uri": "https://html5demos.com/assets/dizzy.mp4" }, { - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, { - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" } ] } @@ -578,5 +586,17 @@ "spherical_stereo_mode": "top_bottom" } ] + }, + { + "name": "Subtitles", + "samples": [ + { + "name": "TTML", + "uri": "https://html5demos.com/assets/dizzy.mp4", + "subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_ttml_sample.xml", + "subtitle_mime_type": "application/ttml+xml", + "subtitle_language": "en" + } + ] } ] diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java index 6985d42b36f..d83d7076c53 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java @@ -28,7 +28,7 @@ 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.FileDataSourceFactory; +import com.google.android.exoplayer2.upstream.FileDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.CacheDataSource; @@ -165,7 +165,7 @@ protected static CacheDataSourceFactory buildReadOnlyCacheDataSource( return new CacheDataSourceFactory( cache, upstreamFactory, - new FileDataSourceFactory(), + new FileDataSource.Factory(), /* cacheWriteDataSinkFactory= */ null, CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR, /* eventListener= */ null); diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java index a913a9b891f..143eda93df6 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java @@ -18,9 +18,9 @@ import android.content.Context; import android.content.DialogInterface; import android.net.Uri; +import android.widget.Toast; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentManager; -import android.widget.Toast; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.offline.Download; @@ -30,6 +30,7 @@ import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.DownloadService; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Log; @@ -55,6 +56,7 @@ public interface Listener { private final CopyOnWriteArraySet listeners; private final HashMap downloads; private final DownloadIndex downloadIndex; + private final DefaultTrackSelector.Parameters trackSelectorParameters; @Nullable private StartDownloadDialogHelper startDownloadDialogHelper; @@ -65,6 +67,7 @@ public DownloadTracker( listeners = new CopyOnWriteArraySet<>(); downloads = new HashMap<>(); downloadIndex = downloadManager.getDownloadIndex(); + trackSelectorParameters = DownloadHelper.getDefaultTrackSelectorParameters(context); downloadManager.addListener(new DownloadManagerListener()); loadDownloads(); } @@ -82,7 +85,6 @@ public boolean isDownloaded(Uri uri) { return download != null && download.state != Download.STATE_FAILED; } - @SuppressWarnings("unchecked") public DownloadRequest getDownloadRequest(Uri uri) { Download download = downloads.get(uri); return download != null && download.state != Download.STATE_FAILED ? download.request : null; @@ -124,13 +126,13 @@ private DownloadHelper getDownloadHelper( int type = Util.inferContentType(uri, extension); switch (type) { case C.TYPE_DASH: - return DownloadHelper.forDash(uri, dataSourceFactory, renderersFactory); + return DownloadHelper.forDash(context, uri, dataSourceFactory, renderersFactory); case C.TYPE_SS: - return DownloadHelper.forSmoothStreaming(uri, dataSourceFactory, renderersFactory); + return DownloadHelper.forSmoothStreaming(context, uri, dataSourceFactory, renderersFactory); case C.TYPE_HLS: - return DownloadHelper.forHls(uri, dataSourceFactory, renderersFactory); + return DownloadHelper.forHls(context, uri, dataSourceFactory, renderersFactory); case C.TYPE_OTHER: - return DownloadHelper.forProgressive(uri); + return DownloadHelper.forProgressive(context, uri); default: throw new IllegalStateException("Unsupported type: " + type); } @@ -203,7 +205,7 @@ public void onPrepared(DownloadHelper helper) { TrackSelectionDialog.createForMappedTrackInfoAndParameters( /* titleId= */ R.string.exo_download_description, mappedTrackInfo, - /* initialParameters= */ DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, + trackSelectorParameters, /* allowAdaptiveSelections =*/ false, /* allowMultipleOverrides= */ true, /* onClickListener= */ this, @@ -213,10 +215,13 @@ public void onPrepared(DownloadHelper helper) { @Override public void onPrepareError(DownloadHelper helper, IOException e) { - Toast.makeText( - context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG) - .show(); - Log.e(TAG, "Failed to start download", e); + Toast.makeText(context, R.string.download_start_error, Toast.LENGTH_LONG).show(); + Log.e( + TAG, + e instanceof DownloadHelper.LiveContentUnsupportedException + ? "Downloading live content unsupported" + : "Failed to start download", + e); } // DialogInterface.OnClickListener implementation. @@ -230,7 +235,7 @@ public void onClick(DialogInterface dialog, int which) { downloadHelper.addTrackSelectionForSingleRenderer( periodIndex, /* rendererIndex= */ i, - DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, + trackSelectorParameters, trackSelectionDialog.getOverrides(/* rendererIndex= */ i)); } } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 35307eb5d8e..2f8d0045d3b 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -17,11 +17,9 @@ import android.content.Intent; import android.content.pm.PackageManager; +import android.media.MediaDrm; import android.net.Uri; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; import android.util.Pair; import android.view.KeyEvent; import android.view.View; @@ -30,19 +28,24 @@ import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C.ContentType; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.demo.Sample.UriSample; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; -import com.google.android.exoplayer2.drm.UnsupportedDrmException; +import com.google.android.exoplayer2.drm.MediaDrmCallback; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.offline.DownloadHelper; @@ -50,7 +53,10 @@ import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceFactory; +import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.SingleSampleMediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; @@ -66,7 +72,7 @@ import com.google.android.exoplayer2.ui.DebugTextViewHelper; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; -import com.google.android.exoplayer2.ui.spherical.SphericalSurfaceView; +import com.google.android.exoplayer2.ui.spherical.SphericalGLSurfaceView; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.ErrorMessageProvider; @@ -76,41 +82,51 @@ import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; -import java.util.UUID; /** An activity that plays media using {@link SimpleExoPlayer}. */ public class PlayerActivity extends AppCompatActivity implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener { - public static final String DRM_SCHEME_EXTRA = "drm_scheme"; - public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url"; - public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties"; - public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session"; - public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders"; + // Activity extras. - public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; - public static final String EXTENSION_EXTRA = "extension"; + public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode"; + public static final String SPHERICAL_STEREO_MODE_MONO = "mono"; + public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom"; + public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right"; + + // Actions. + public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; public static final String ACTION_VIEW_LIST = "com.google.android.exoplayer.demo.action.VIEW_LIST"; - public static final String URI_LIST_EXTRA = "uri_list"; - public static final String EXTENSION_LIST_EXTRA = "extension_list"; - public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; + // Player configuration extras. public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm"; public static final String ABR_ALGORITHM_DEFAULT = "default"; public static final String ABR_ALGORITHM_RANDOM = "random"; - public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode"; - public static final String SPHERICAL_STEREO_MODE_MONO = "mono"; - public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom"; - public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right"; + // Media item configuration extras. + public static final String URI_EXTRA = "uri"; + public static final String EXTENSION_EXTRA = "extension"; + public static final String IS_LIVE_EXTRA = "is_live"; + + public static final String DRM_SCHEME_EXTRA = "drm_scheme"; + public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url"; + public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties"; + public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session"; + public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders"; + public static final String TUNNELING_EXTRA = "tunneling"; + public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; + public static final String SUBTITLE_URI_EXTRA = "subtitle_uri"; + public static final String SUBTITLE_MIME_TYPE_EXTRA = "subtitle_mime_type"; + public static final String SUBTITLE_LANGUAGE_EXTRA = "subtitle_language"; // For backwards compatibility only. - private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; + public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; // Saved instance state keys. + private static final String KEY_TRACK_SELECTOR_PARAMETERS = "track_selector_parameters"; private static final String KEY_WINDOW = "window"; private static final String KEY_POSITION = "position"; @@ -130,7 +146,6 @@ public class PlayerActivity extends AppCompatActivity private DataSource.Factory dataSourceFactory; private SimpleExoPlayer player; - private FrameworkMediaDrm mediaDrm; private MediaSource mediaSource; private DefaultTrackSelector trackSelector; private DefaultTrackSelector.Parameters trackSelectorParameters; @@ -150,7 +165,8 @@ public class PlayerActivity extends AppCompatActivity @Override public void onCreate(Bundle savedInstanceState) { - String sphericalStereoMode = getIntent().getStringExtra(SPHERICAL_STEREO_MODE_EXTRA); + Intent intent = getIntent(); + String sphericalStereoMode = intent.getStringExtra(SPHERICAL_STEREO_MODE_EXTRA); if (sphericalStereoMode != null) { setTheme(R.style.PlayerTheme_Spherical); } @@ -183,7 +199,7 @@ public void onCreate(Bundle savedInstanceState) { finish(); return; } - ((SphericalSurfaceView) playerView.getVideoSurfaceView()).setDefaultStereoMode(stereoMode); + ((SphericalGLSurfaceView) playerView.getVideoSurfaceView()).setDefaultStereoMode(stereoMode); } if (savedInstanceState != null) { @@ -192,7 +208,13 @@ public void onCreate(Bundle savedInstanceState) { startWindow = savedInstanceState.getInt(KEY_WINDOW); startPosition = savedInstanceState.getLong(KEY_POSITION); } else { - trackSelectorParameters = new DefaultTrackSelector.ParametersBuilder().build(); + DefaultTrackSelector.ParametersBuilder builder = + new DefaultTrackSelector.ParametersBuilder(/* context= */ this); + boolean tunneling = intent.getBooleanExtra(TUNNELING_EXTRA, false); + if (Util.SDK_INT >= 21 && tunneling) { + builder.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(/* context= */ this)); + } + trackSelectorParameters = builder.build(); clearStartPosition(); } } @@ -326,67 +348,10 @@ public void onVisibilityChange(int visibility) { private void initializePlayer() { if (player == null) { Intent intent = getIntent(); - String action = intent.getAction(); - Uri[] uris; - String[] extensions; - if (ACTION_VIEW.equals(action)) { - uris = new Uri[] {intent.getData()}; - extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)}; - } else if (ACTION_VIEW_LIST.equals(action)) { - String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA); - uris = new Uri[uriStrings.length]; - for (int i = 0; i < uriStrings.length; i++) { - uris[i] = Uri.parse(uriStrings[i]); - } - extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA); - if (extensions == null) { - extensions = new String[uriStrings.length]; - } - } else { - showToast(getString(R.string.unexpected_intent_action, action)); - finish(); - return; - } - if (!Util.checkCleartextTrafficPermitted(uris)) { - showToast(R.string.error_cleartext_not_permitted); - return; - } - if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, uris)) { - // The player will be reinitialized if the permission is granted. - return; - } - DefaultDrmSessionManager drmSessionManager = null; - if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) { - String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA); - String[] keyRequestPropertiesArray = - intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA); - boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA, false); - int errorStringId = R.string.error_drm_unknown; - if (Util.SDK_INT < 18) { - errorStringId = R.string.error_drm_not_supported; - } else { - try { - String drmSchemeExtra = intent.hasExtra(DRM_SCHEME_EXTRA) ? DRM_SCHEME_EXTRA - : DRM_SCHEME_UUID_EXTRA; - UUID drmSchemeUuid = Util.getDrmUuid(intent.getStringExtra(drmSchemeExtra)); - if (drmSchemeUuid == null) { - errorStringId = R.string.error_drm_unsupported_scheme; - } else { - drmSessionManager = - buildDrmSessionManagerV18( - drmSchemeUuid, drmLicenseUrl, keyRequestPropertiesArray, multiSession); - } - } catch (UnsupportedDrmException e) { - errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME - ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown; - } - } - if (drmSessionManager == null) { - showToast(errorStringId); - finish(); - return; - } + mediaSource = createTopLevelMediaSource(intent); + if (mediaSource == null) { + return; } TrackSelection.Factory trackSelectionFactory; @@ -406,13 +371,14 @@ private void initializePlayer() { RenderersFactory renderersFactory = ((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders); - trackSelector = new DefaultTrackSelector(trackSelectionFactory); + trackSelector = new DefaultTrackSelector(/* context= */ this, trackSelectionFactory); trackSelector.setParameters(trackSelectorParameters); lastSeenTrackGroupArray = null; player = - ExoPlayerFactory.newSimpleInstance( - /* context= */ this, renderersFactory, trackSelector, drmSessionManager); + new SimpleExoPlayer.Builder(/* context= */ this, renderersFactory) + .setTrackSelector(trackSelector) + .build(); player.addListener(new PlayerEventListener()); player.setPlayWhenReady(startAutoPlay); player.addAnalyticsListener(new EventLogger(trackSelector)); @@ -420,66 +386,153 @@ private void initializePlayer() { playerView.setPlaybackPreparer(this); debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper.start(); + if (adsLoader != null) { + adsLoader.setPlayer(player); + } + } + boolean haveStartPosition = startWindow != C.INDEX_UNSET; + if (haveStartPosition) { + player.seekTo(startWindow, startPosition); + } + player.prepare(mediaSource, !haveStartPosition, false); + updateButtonVisibility(); + } - MediaSource[] mediaSources = new MediaSource[uris.length]; - for (int i = 0; i < uris.length; i++) { - mediaSources[i] = buildMediaSource(uris[i], extensions[i]); + @Nullable + private MediaSource createTopLevelMediaSource(Intent intent) { + String action = intent.getAction(); + boolean actionIsListView = ACTION_VIEW_LIST.equals(action); + if (!actionIsListView && !ACTION_VIEW.equals(action)) { + showToast(getString(R.string.unexpected_intent_action, action)); + finish(); + return null; + } + + Sample intentAsSample = Sample.createFromIntent(intent); + UriSample[] samples = + intentAsSample instanceof Sample.PlaylistSample + ? ((Sample.PlaylistSample) intentAsSample).children + : new UriSample[] {(UriSample) intentAsSample}; + + boolean seenAdsTagUri = false; + for (UriSample sample : samples) { + seenAdsTagUri |= sample.adTagUri != null; + if (!Util.checkCleartextTrafficPermitted(sample.uri)) { + showToast(R.string.error_cleartext_not_permitted); + return null; } - mediaSource = - mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); - String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA); - if (adTagUriString != null) { - Uri adTagUri = Uri.parse(adTagUriString); + if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, sample.uri)) { + // The player will be reinitialized if the permission is granted. + return null; + } + } + + MediaSource[] mediaSources = new MediaSource[samples.length]; + for (int i = 0; i < samples.length; i++) { + mediaSources[i] = createLeafMediaSource(samples[i]); + Sample.SubtitleInfo subtitleInfo = samples[i].subtitleInfo; + if (subtitleInfo != null) { + Format subtitleFormat = + Format.createTextSampleFormat( + /* id= */ null, + subtitleInfo.mimeType, + C.SELECTION_FLAG_DEFAULT, + subtitleInfo.language); + MediaSource subtitleMediaSource = + new SingleSampleMediaSource.Factory(dataSourceFactory) + .createMediaSource(subtitleInfo.uri, subtitleFormat, C.TIME_UNSET); + mediaSources[i] = new MergingMediaSource(mediaSources[i], subtitleMediaSource); + } + } + MediaSource mediaSource = + mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); + + if (seenAdsTagUri) { + Uri adTagUri = samples[0].adTagUri; + if (actionIsListView) { + showToast(R.string.unsupported_ads_in_concatenation); + } else { if (!adTagUri.equals(loadedAdTagUri)) { releaseAdsLoader(); loadedAdTagUri = adTagUri; } - MediaSource adsMediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString)); + MediaSource adsMediaSource = createAdsMediaSource(mediaSource, adTagUri); if (adsMediaSource != null) { mediaSource = adsMediaSource; } else { showToast(R.string.ima_not_loaded); } - } else { - releaseAdsLoader(); } + } else { + releaseAdsLoader(); } - boolean haveStartPosition = startWindow != C.INDEX_UNSET; - if (haveStartPosition) { - player.seekTo(startWindow, startPosition); - } - player.prepare(mediaSource, !haveStartPosition, false); - updateButtonVisibility(); - } - private MediaSource buildMediaSource(Uri uri) { - return buildMediaSource(uri, null); + return mediaSource; } - private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { + private MediaSource createLeafMediaSource(UriSample parameters) { + Sample.DrmInfo drmInfo = parameters.drmInfo; + int errorStringId = R.string.error_drm_unknown; + DrmSessionManager drmSessionManager = null; + if (drmInfo == null) { + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); + } else if (Util.SDK_INT < 18) { + errorStringId = R.string.error_drm_unsupported_before_api_18; + } else if (!MediaDrm.isCryptoSchemeSupported(drmInfo.drmScheme)) { + errorStringId = R.string.error_drm_unsupported_scheme; + } else { + MediaDrmCallback mediaDrmCallback = + createMediaDrmCallback(drmInfo.drmLicenseUrl, drmInfo.drmKeyRequestProperties); + drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(drmInfo.drmScheme, FrameworkMediaDrm.DEFAULT_PROVIDER) + .setMultiSession(drmInfo.drmMultiSession) + .build(mediaDrmCallback); + } + + if (drmSessionManager == null) { + showToast(errorStringId); + finish(); + return null; + } + DownloadRequest downloadRequest = - ((DemoApplication) getApplication()).getDownloadTracker().getDownloadRequest(uri); + ((DemoApplication) getApplication()) + .getDownloadTracker() + .getDownloadRequest(parameters.uri); if (downloadRequest != null) { return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory); } - @ContentType int type = Util.inferContentType(uri, overrideExtension); + return createLeafMediaSource(parameters.uri, parameters.extension, drmSessionManager); + } + + private MediaSource createLeafMediaSource( + Uri uri, String extension, DrmSessionManager drmSessionManager) { + @ContentType int type = Util.inferContentType(uri, extension); switch (type) { case C.TYPE_DASH: - return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri); + return new DashMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); case C.TYPE_SS: - return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); + return new SsMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); case C.TYPE_HLS: - return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); + return new HlsMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); case C.TYPE_OTHER: - return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri); + return new ProgressiveMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); default: throw new IllegalStateException("Unsupported type: " + type); } } - private DefaultDrmSessionManager buildDrmSessionManagerV18( - UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession) - throws UnsupportedDrmException { + private HttpMediaDrmCallback createMediaDrmCallback( + String licenseUrl, String[] keyRequestPropertiesArray) { HttpDataSource.Factory licenseDataSourceFactory = ((DemoApplication) getApplication()).buildHttpDataSourceFactory(); HttpMediaDrmCallback drmCallback = @@ -490,9 +543,7 @@ private DefaultDrmSessionManager buildDrmSessionManagerV18 keyRequestPropertiesArray[i + 1]); } } - releaseMediaDrm(); - mediaDrm = FrameworkMediaDrm.newInstance(uuid); - return new DefaultDrmSessionManager<>(uuid, mediaDrm, drmCallback, null, multiSession); + return drmCallback; } private void releasePlayer() { @@ -509,14 +560,6 @@ private void releasePlayer() { if (adsLoader != null) { adsLoader.setPlayer(null); } - releaseMediaDrm(); - } - - private void releaseMediaDrm() { - if (mediaDrm != null) { - mediaDrm.release(); - mediaDrm = null; - } } private void releaseAdsLoader() { @@ -554,7 +597,8 @@ private DataSource.Factory buildDataSourceFactory() { } /** Returns an ads media source, reusing the ads loader if one exists. */ - private @Nullable MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) { + @Nullable + private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) { // Load the extension source using reflection so the demo app doesn't have to depend on it. // The ads loader is reused for multiple playbacks, so that ad playback can resume. try { @@ -569,12 +613,12 @@ private DataSource.Factory buildDataSourceFactory() { // LINT.ThenChange(../../../../../../../../proguard-rules.txt) adsLoader = loaderConstructor.newInstance(this, adTagUri); } - adsLoader.setPlayer(player); - AdsMediaSource.MediaSourceFactory adMediaSourceFactory = - new AdsMediaSource.MediaSourceFactory() { + MediaSourceFactory adMediaSourceFactory = + new MediaSourceFactory() { @Override public MediaSource createMediaSource(Uri uri) { - return PlayerActivity.this.buildMediaSource(uri); + return PlayerActivity.this.createLeafMediaSource( + uri, /* extension=*/ null, DrmSessionManager.getDummyDrmSessionManager()); } @Override @@ -627,7 +671,7 @@ private static boolean isBehindLiveWindow(ExoPlaybackException e) { private class PlayerEventListener implements Player.EventListener { @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { if (playbackState == Player.STATE_ENDED) { showControls(); } @@ -677,7 +721,7 @@ public Pair getErrorMessage(ExoPlaybackException e) { // Special case for decoder initialization failures. DecoderInitializationException decoderInitializationException = (DecoderInitializationException) cause; - if (decoderInitializationException.decoderName == null) { + if (decoderInitializationException.codecInfo == null) { if (decoderInitializationException.getCause() instanceof DecoderQueryException) { errorString = getString(R.string.error_querying_decoders); } else if (decoderInitializationException.secureDecoderRequired) { @@ -692,12 +736,11 @@ public Pair getErrorMessage(ExoPlaybackException e) { errorString = getString( R.string.error_instantiating_decoder, - decoderInitializationException.decoderName); + decoderInitializationException.codecInfo.name); } } } return Pair.create(0, errorString); } } - } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java new file mode 100644 index 00000000000..85530b993bc --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.demo; + +import static com.google.android.exoplayer2.demo.PlayerActivity.ACTION_VIEW_LIST; +import static com.google.android.exoplayer2.demo.PlayerActivity.AD_TAG_URI_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_LICENSE_URL_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_MULTI_SESSION_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_UUID_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.EXTENSION_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.IS_LIVE_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_LANGUAGE_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_MIME_TYPE_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_URI_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.URI_EXTRA; + +import android.content.Intent; +import android.net.Uri; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.UUID; + +/* package */ abstract class Sample { + + public static final class UriSample extends Sample { + + public static UriSample createFromIntent(Uri uri, Intent intent, String extrasKeySuffix) { + String extension = intent.getStringExtra(EXTENSION_EXTRA + extrasKeySuffix); + String adsTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix); + boolean isLive = + intent.getBooleanExtra(IS_LIVE_EXTRA + extrasKeySuffix, /* defaultValue= */ false); + Uri adTagUri = adsTagUriString != null ? Uri.parse(adsTagUriString) : null; + return new UriSample( + /* name= */ null, + uri, + extension, + isLive, + DrmInfo.createFromIntent(intent, extrasKeySuffix), + adTagUri, + /* sphericalStereoMode= */ null, + SubtitleInfo.createFromIntent(intent, extrasKeySuffix)); + } + + public final Uri uri; + public final String extension; + public final boolean isLive; + public final DrmInfo drmInfo; + public final Uri adTagUri; + @Nullable public final String sphericalStereoMode; + @Nullable SubtitleInfo subtitleInfo; + + public UriSample( + String name, + Uri uri, + String extension, + boolean isLive, + DrmInfo drmInfo, + Uri adTagUri, + @Nullable String sphericalStereoMode, + @Nullable SubtitleInfo subtitleInfo) { + super(name); + this.uri = uri; + this.extension = extension; + this.isLive = isLive; + this.drmInfo = drmInfo; + this.adTagUri = adTagUri; + this.sphericalStereoMode = sphericalStereoMode; + this.subtitleInfo = subtitleInfo; + } + + @Override + public void addToIntent(Intent intent) { + intent.setAction(PlayerActivity.ACTION_VIEW).setData(uri); + intent.putExtra(PlayerActivity.IS_LIVE_EXTRA, isLive); + intent.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode); + addPlayerConfigToIntent(intent, /* extrasKeySuffix= */ ""); + } + + public void addToPlaylistIntent(Intent intent, String extrasKeySuffix) { + intent.putExtra(PlayerActivity.URI_EXTRA + extrasKeySuffix, uri.toString()); + intent.putExtra(PlayerActivity.IS_LIVE_EXTRA + extrasKeySuffix, isLive); + addPlayerConfigToIntent(intent, extrasKeySuffix); + } + + private void addPlayerConfigToIntent(Intent intent, String extrasKeySuffix) { + intent + .putExtra(EXTENSION_EXTRA + extrasKeySuffix, extension) + .putExtra( + AD_TAG_URI_EXTRA + extrasKeySuffix, adTagUri != null ? adTagUri.toString() : null); + if (drmInfo != null) { + drmInfo.addToIntent(intent, extrasKeySuffix); + } + if (subtitleInfo != null) { + subtitleInfo.addToIntent(intent, extrasKeySuffix); + } + } + } + + public static final class PlaylistSample extends Sample { + + public final UriSample[] children; + + public PlaylistSample(String name, UriSample... children) { + super(name); + this.children = children; + } + + @Override + public void addToIntent(Intent intent) { + intent.setAction(PlayerActivity.ACTION_VIEW_LIST); + for (int i = 0; i < children.length; i++) { + children[i].addToPlaylistIntent(intent, /* extrasKeySuffix= */ "_" + i); + } + } + } + + public static final class DrmInfo { + + public static DrmInfo createFromIntent(Intent intent, String extrasKeySuffix) { + String schemeKey = DRM_SCHEME_EXTRA + extrasKeySuffix; + String schemeUuidKey = DRM_SCHEME_UUID_EXTRA + extrasKeySuffix; + if (!intent.hasExtra(schemeKey) && !intent.hasExtra(schemeUuidKey)) { + return null; + } + String drmSchemeExtra = + intent.hasExtra(schemeKey) + ? intent.getStringExtra(schemeKey) + : intent.getStringExtra(schemeUuidKey); + UUID drmScheme = Util.getDrmUuid(drmSchemeExtra); + String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix); + String[] keyRequestPropertiesArray = + intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix); + boolean drmMultiSession = + intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false); + return new DrmInfo(drmScheme, drmLicenseUrl, keyRequestPropertiesArray, drmMultiSession); + } + + public final UUID drmScheme; + public final String drmLicenseUrl; + public final String[] drmKeyRequestProperties; + public final boolean drmMultiSession; + + public DrmInfo( + UUID drmScheme, + String drmLicenseUrl, + String[] drmKeyRequestProperties, + boolean drmMultiSession) { + this.drmScheme = drmScheme; + this.drmLicenseUrl = drmLicenseUrl; + this.drmKeyRequestProperties = drmKeyRequestProperties; + this.drmMultiSession = drmMultiSession; + } + + public void addToIntent(Intent intent, String extrasKeySuffix) { + Assertions.checkNotNull(intent); + intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmScheme.toString()); + intent.putExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix, drmLicenseUrl); + intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties); + intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmMultiSession); + } + } + + public static final class SubtitleInfo { + + @Nullable + public static SubtitleInfo createFromIntent(Intent intent, String extrasKeySuffix) { + if (!intent.hasExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)) { + return null; + } + return new SubtitleInfo( + Uri.parse(intent.getStringExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)), + intent.getStringExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix), + intent.getStringExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix)); + } + + public final Uri uri; + public final String mimeType; + @Nullable public final String language; + + public SubtitleInfo(Uri uri, String mimeType, @Nullable String language) { + this.uri = Assertions.checkNotNull(uri); + this.mimeType = Assertions.checkNotNull(mimeType); + this.language = language; + } + + public void addToIntent(Intent intent, String extrasKeySuffix) { + intent.putExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix, uri.toString()); + intent.putExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix, mimeType); + intent.putExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix, language); + } + } + + public static Sample createFromIntent(Intent intent) { + if (ACTION_VIEW_LIST.equals(intent.getAction())) { + ArrayList intentUris = new ArrayList<>(); + int index = 0; + while (intent.hasExtra(URI_EXTRA + "_" + index)) { + intentUris.add(intent.getStringExtra(URI_EXTRA + "_" + index)); + index++; + } + UriSample[] children = new UriSample[intentUris.size()]; + for (int i = 0; i < children.length; i++) { + Uri uri = Uri.parse(intentUris.get(i)); + children[i] = UriSample.createFromIntent(uri, intent, /* extrasKeySuffix= */ "_" + i); + } + return new PlaylistSample(/* name= */ null, children); + } else { + return UriSample.createFromIntent(intent.getData(), intent, /* extrasKeySuffix= */ ""); + } + } + + @Nullable public final String name; + + public Sample(String name) { + this.name = name; + } + + public abstract void addToIntent(Intent intent); +} diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 7245de01c64..cdce29aa5ea 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -21,8 +21,6 @@ import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; import android.util.JsonReader; import android.view.Menu; import android.view.MenuInflater; @@ -36,8 +34,13 @@ import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.demo.Sample.DrmInfo; +import com.google.android.exoplayer2.demo.Sample.PlaylistSample; +import com.google.android.exoplayer2.demo.Sample.UriSample; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceInputStream; @@ -65,6 +68,7 @@ public class SampleChooserActivity extends AppCompatActivity private SampleAdapter sampleAdapter; private MenuItem preferExtensionDecodersMenuItem; private MenuItem randomAbrMenuItem; + private MenuItem tunnelingMenuItem; @Override public void onCreate(Bundle savedInstanceState) { @@ -122,6 +126,10 @@ public boolean onCreateOptionsMenu(Menu menu) { preferExtensionDecodersMenuItem = menu.findItem(R.id.prefer_extension_decoders); preferExtensionDecodersMenuItem.setVisible(useExtensionRenderers); randomAbrMenuItem = menu.findItem(R.id.random_abr); + tunnelingMenuItem = menu.findItem(R.id.tunneling); + if (Util.SDK_INT < 21) { + tunnelingMenuItem.setEnabled(false); + } return true; } @@ -161,13 +169,18 @@ private void onSampleGroups(final List groups, boolean sawError) { public boolean onChildClick( ExpandableListView parent, View view, int groupPosition, int childPosition, long id) { Sample sample = (Sample) view.getTag(); - startActivity( - sample.buildIntent( - /* context= */ this, - isNonNullAndChecked(preferExtensionDecodersMenuItem), - isNonNullAndChecked(randomAbrMenuItem) - ? PlayerActivity.ABR_ALGORITHM_RANDOM - : PlayerActivity.ABR_ALGORITHM_DEFAULT)); + Intent intent = new Intent(this, PlayerActivity.class); + intent.putExtra( + PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, + isNonNullAndChecked(preferExtensionDecodersMenuItem)); + String abrAlgorithm = + isNonNullAndChecked(randomAbrMenuItem) + ? PlayerActivity.ABR_ALGORITHM_RANDOM + : PlayerActivity.ABR_ALGORITHM_DEFAULT; + intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm); + intent.putExtra(PlayerActivity.TUNNELING_EXTRA, isNonNullAndChecked(tunnelingMenuItem)); + sample.addToIntent(intent); + startActivity(intent); return true; } @@ -198,6 +211,9 @@ private int getDownloadUnsupportedStringId(Sample sample) { if (uriSample.drmInfo != null) { return R.string.download_drm_unsupported; } + if (uriSample.isLive) { + return R.string.download_live_unsupported; + } if (uriSample.adTagUri != null) { return R.string.download_ads_unsupported; } @@ -287,6 +303,7 @@ private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOExc String sampleName = null; Uri uri = null; String extension = null; + boolean isLive = false; String drmScheme = null; String drmLicenseUrl = null; String[] drmKeyRequestProperties = null; @@ -294,6 +311,10 @@ private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOExc ArrayList playlistSamples = null; String adTagUri = null; String sphericalStereoMode = null; + List subtitleInfos = new ArrayList<>(); + Uri subtitleUri = null; + String subtitleMimeType = null; + String subtitleLanguage = null; reader.beginObject(); while (reader.hasNext()) { @@ -309,17 +330,15 @@ private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOExc extension = reader.nextString(); break; case "drm_scheme": - Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme"); drmScheme = reader.nextString(); break; + case "is_live": + isLive = reader.nextBoolean(); + break; case "drm_license_url": - Assertions.checkState(!insidePlaylist, - "Invalid attribute on nested item: drm_license_url"); drmLicenseUrl = reader.nextString(); break; case "drm_key_request_properties": - Assertions.checkState(!insidePlaylist, - "Invalid attribute on nested item: drm_key_request_properties"); ArrayList drmKeyRequestPropertiesList = new ArrayList<>(); reader.beginObject(); while (reader.hasNext()) { @@ -337,7 +356,7 @@ private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOExc playlistSamples = new ArrayList<>(); reader.beginArray(); while (reader.hasNext()) { - playlistSamples.add((UriSample) readEntry(reader, true)); + playlistSamples.add((UriSample) readEntry(reader, /* insidePlaylist= */ true)); } reader.endArray(); break; @@ -349,6 +368,15 @@ private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOExc !insidePlaylist, "Invalid attribute on nested item: spherical_stereo_mode"); sphericalStereoMode = reader.nextString(); break; + case "subtitle_uri": + subtitleUri = Uri.parse(reader.nextString()); + break; + case "subtitle_mime_type": + subtitleMimeType = reader.nextString(); + break; + case "subtitle_language": + subtitleLanguage = reader.nextString(); + break; default: throw new ParserException("Unsupported attribute name: " + name); } @@ -357,18 +385,32 @@ private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOExc DrmInfo drmInfo = drmScheme == null ? null - : new DrmInfo(drmScheme, drmLicenseUrl, drmKeyRequestProperties, drmMultiSession); + : new DrmInfo( + Util.getDrmUuid(drmScheme), + drmLicenseUrl, + drmKeyRequestProperties, + drmMultiSession); + Sample.SubtitleInfo subtitleInfo = + subtitleUri == null + ? null + : new Sample.SubtitleInfo( + subtitleUri, + Assertions.checkNotNull( + subtitleMimeType, "subtitle_mime_type is required if subtitle_uri is set."), + subtitleLanguage); if (playlistSamples != null) { UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]); - return new PlaylistSample(sampleName, drmInfo, playlistSamplesArray); + return new PlaylistSample(sampleName, playlistSamplesArray); } else { return new UriSample( sampleName, - drmInfo, uri, extension, - adTagUri, - sphericalStereoMode); + isLive, + drmInfo, + adTagUri != null ? Uri.parse(adTagUri) : null, + sphericalStereoMode, + subtitleInfo); } } @@ -480,7 +522,7 @@ private void initializeChildView(View view, Sample sample) { ImageButton downloadButton = view.findViewById(R.id.download_button); downloadButton.setTag(sample); downloadButton.setColorFilter( - canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFFEEEEEE); + canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFF666666); downloadButton.setImageResource( isDownloaded ? R.drawable.ic_download_done : R.drawable.ic_download); } @@ -497,116 +539,4 @@ public SampleGroup(String title) { } } - - private static final class DrmInfo { - public final String drmScheme; - public final String drmLicenseUrl; - public final String[] drmKeyRequestProperties; - public final boolean drmMultiSession; - - public DrmInfo( - String drmScheme, - String drmLicenseUrl, - String[] drmKeyRequestProperties, - boolean drmMultiSession) { - this.drmScheme = drmScheme; - this.drmLicenseUrl = drmLicenseUrl; - this.drmKeyRequestProperties = drmKeyRequestProperties; - this.drmMultiSession = drmMultiSession; - } - - public void updateIntent(Intent intent) { - Assertions.checkNotNull(intent); - intent.putExtra(PlayerActivity.DRM_SCHEME_EXTRA, drmScheme); - intent.putExtra(PlayerActivity.DRM_LICENSE_URL_EXTRA, drmLicenseUrl); - intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA, drmKeyRequestProperties); - intent.putExtra(PlayerActivity.DRM_MULTI_SESSION_EXTRA, drmMultiSession); - } - } - - private abstract static class Sample { - public final String name; - public final DrmInfo drmInfo; - - public Sample(String name, DrmInfo drmInfo) { - this.name = name; - this.drmInfo = drmInfo; - } - - public Intent buildIntent( - Context context, boolean preferExtensionDecoders, String abrAlgorithm) { - Intent intent = new Intent(context, PlayerActivity.class); - intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, preferExtensionDecoders); - intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm); - if (drmInfo != null) { - drmInfo.updateIntent(intent); - } - return intent; - } - - } - - private static final class UriSample extends Sample { - - public final Uri uri; - public final String extension; - public final String adTagUri; - public final String sphericalStereoMode; - - public UriSample( - String name, - DrmInfo drmInfo, - Uri uri, - String extension, - String adTagUri, - String sphericalStereoMode) { - super(name, drmInfo); - this.uri = uri; - this.extension = extension; - this.adTagUri = adTagUri; - this.sphericalStereoMode = sphericalStereoMode; - } - - @Override - public Intent buildIntent( - Context context, boolean preferExtensionDecoders, String abrAlgorithm) { - return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm) - .setData(uri) - .putExtra(PlayerActivity.EXTENSION_EXTRA, extension) - .putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri) - .putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode) - .setAction(PlayerActivity.ACTION_VIEW); - } - - } - - private static final class PlaylistSample extends Sample { - - public final UriSample[] children; - - public PlaylistSample( - String name, - DrmInfo drmInfo, - UriSample... children) { - super(name, drmInfo); - this.children = children; - } - - @Override - public Intent buildIntent( - Context context, boolean preferExtensionDecoders, String abrAlgorithm) { - String[] uris = new String[children.length]; - String[] extensions = new String[children.length]; - for (int i = 0; i < children.length; i++) { - uris[i] = children[i].uri.toString(); - extensions[i] = children[i].extension; - } - return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm) - .putExtra(PlayerActivity.URI_LIST_EXTRA, uris) - .putExtra(PlayerActivity.EXTENSION_LIST_EXTRA, extensions) - .setAction(PlayerActivity.ACTION_VIEW_LIST); - } - - } - } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java index bc409410c3b..9e8009388eb 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java @@ -19,19 +19,18 @@ import android.content.DialogInterface; import android.content.res.Resources; import android.os.Bundle; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; import androidx.annotation.Nullable; -import com.google.android.material.tabs.TabLayout; +import androidx.appcompat.app.AppCompatDialog; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; import androidx.viewpager.widget.ViewPager; -import androidx.appcompat.app.AppCompatDialog; -import android.util.SparseArray; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; @@ -39,6 +38,7 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.ui.TrackSelectionView; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.material.tabs.TabLayout; import java.util.ArrayList; import java.util.Collections; import java.util.List; diff --git a/demos/main/src/main/res/menu/sample_chooser_menu.xml b/demos/main/src/main/res/menu/sample_chooser_menu.xml index 9934e9db95a..f95c0b64607 100644 --- a/demos/main/src/main/res/menu/sample_chooser_menu.xml +++ b/demos/main/src/main/res/menu/sample_chooser_menu.xml @@ -23,4 +23,8 @@ android:title="@string/random_abr" android:checkable="true" app:showAsAction="never"/> + diff --git a/demos/main/src/main/res/values/strings.xml b/demos/main/src/main/res/values/strings.xml index 0729da2fc6a..671303a5225 100644 --- a/demos/main/src/main/res/values/strings.xml +++ b/demos/main/src/main/res/values/strings.xml @@ -29,7 +29,7 @@ Unrecognized stereo mode - Protected content not supported on API levels below 18 + Protected content not supported on API levels below 18 This device does not support the required DRM scheme @@ -53,6 +53,8 @@ Playing sample without ads, as the IMA extension was not loaded + Playing sample without ads, as ads are not supported in concatenations + Failed to start download This demo app does not support downloading playlists @@ -61,10 +63,14 @@ This demo app only supports downloading http streams + This demo app does not support downloading live content + IMA does not support offline ads Prefer extension decoders Enable random ABR + Request multimedia tunneling + diff --git a/demos/main/src/main/res/values/styles.xml b/demos/main/src/main/res/values/styles.xml index 04c5b90edc4..a2ebde37bd8 100644 --- a/demos/main/src/main/res/values/styles.xml +++ b/demos/main/src/main/res/values/styles.xml @@ -24,7 +24,7 @@ diff --git a/demos/surface/README.md b/demos/surface/README.md new file mode 100644 index 00000000000..312259dbf68 --- /dev/null +++ b/demos/surface/README.md @@ -0,0 +1,21 @@ +# ExoPlayer SurfaceControl demo + +This app demonstrates how to use the [SurfaceControl][] API to redirect video +output from ExoPlayer between different views or off-screen. `SurfaceControl` +is new in Android 10, so the app requires `minSdkVersion` 29. + +The app layout has a grid of `SurfaceViews`. Initially video is output to one +of the views. Tap a `SurfaceView` to move video output to it. You can also tap +the buttons at the top of the activity to move video output off-screen, to a +full-screen `SurfaceView` or to a new activity. + +When using `SurfaceControl`, the `MediaCodec` always has the same surface +attached to it, which can be freely 'reparented' to any `SurfaceView` (or +off-screen) without any interruptions to playback. This works better than +calling `MediaCodec.setOutputSurface` to change the output surface of the codec +because `MediaCodec` does not re-render its last frame when that method is +called, and because you can move output off-screen easily (`setOutputSurface` +can't take a `null` surface, so the player has to use a `DummySurface`, which +doesn't handle protected output on all devices). + +[SurfaceControl]: https://developer.android.com/reference/android/view/SurfaceControl diff --git a/demos/ima/build.gradle b/demos/surface/build.gradle similarity index 67% rename from demos/ima/build.gradle rename to demos/surface/build.gradle index 124555d9b5a..bff05901b5f 100644 --- a/demos/ima/build.gradle +++ b/demos/surface/build.gradle @@ -1,4 +1,4 @@ -// Copyright (C) 2017 The Android Open Source Project +// Copyright (C) 2019 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,8 +25,8 @@ android { defaultConfig { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode - minSdkVersion project.ext.minSdkVersion - targetSdkVersion project.ext.targetSdkVersion + minSdkVersion 29 + targetSdkVersion project.ext.appTargetSdkVersion } buildTypes { @@ -35,14 +35,11 @@ android { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt') } - debug { - jniDebuggable = true - } } lintOptions { - // The demo app isn't indexed and doesn't have translations. - disable 'GoogleAppIndexingWarning','MissingTranslation' + // This demo app does not have translations. + disable 'MissingTranslation' } } @@ -50,10 +47,5 @@ dependencies { implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-dash') - implementation project(modulePrefix + 'library-hls') - implementation project(modulePrefix + 'library-smoothstreaming') - implementation project(modulePrefix + 'extension-ima') - implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion } - -apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/ima/src/main/AndroidManifest.xml b/demos/surface/src/main/AndroidManifest.xml similarity index 56% rename from demos/ima/src/main/AndroidManifest.xml rename to demos/surface/src/main/AndroidManifest.xml index 85439018fd3..c33a9e646bc 100644 --- a/demos/ima/src/main/AndroidManifest.xml +++ b/demos/surface/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ - - - - + package="com.google.android.exoplayer2.surfacedemo"> - - - - + + + + + + + + + + + + + - - diff --git a/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java b/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java new file mode 100644 index 00000000000..99bc0d7abc5 --- /dev/null +++ b/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.surfacedemo; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.view.Surface; +import android.view.SurfaceControl; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.widget.Button; +import android.widget.GridLayout; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.drm.FrameworkMediaDrm; +import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.ui.PlayerControlView; +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.HttpDataSource; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.util.UUID; + +/** Activity that demonstrates use of {@link SurfaceControl} with ExoPlayer. */ +public final class MainActivity extends Activity { + + private static final String DEFAULT_MEDIA_URI = + "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"; + private static final String SURFACE_CONTROL_NAME = "surfacedemo"; + + private static final String ACTION_VIEW = "com.google.android.exoplayer.surfacedemo.action.VIEW"; + private static final String EXTENSION_EXTRA = "extension"; + private static final String DRM_SCHEME_EXTRA = "drm_scheme"; + private static final String DRM_LICENSE_URL_EXTRA = "drm_license_url"; + private static final String OWNER_EXTRA = "owner"; + + private boolean isOwner; + @Nullable private PlayerControlView playerControlView; + @Nullable private SurfaceView fullScreenView; + @Nullable private SurfaceView nonFullScreenView; + @Nullable private SurfaceView currentOutputView; + + @Nullable private static SimpleExoPlayer player; + @Nullable private static SurfaceControl surfaceControl; + @Nullable private static Surface videoSurface; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main_activity); + playerControlView = findViewById(R.id.player_control_view); + fullScreenView = findViewById(R.id.full_screen_view); + fullScreenView.setOnClickListener( + v -> { + setCurrentOutputView(nonFullScreenView); + Assertions.checkNotNull(fullScreenView).setVisibility(View.GONE); + }); + attachSurfaceListener(fullScreenView); + isOwner = getIntent().getBooleanExtra(OWNER_EXTRA, /* defaultValue= */ true); + GridLayout gridLayout = findViewById(R.id.grid_layout); + for (int i = 0; i < 9; i++) { + View view; + if (i == 0) { + Button button = new Button(/* context= */ this); + view = button; + button.setText(getString(R.string.no_output_label)); + button.setOnClickListener(v -> reparent(/* surfaceView= */ null)); + } else if (i == 1) { + Button button = new Button(/* context= */ this); + view = button; + button.setText(getString(R.string.full_screen_label)); + button.setOnClickListener( + v -> { + setCurrentOutputView(fullScreenView); + Assertions.checkNotNull(fullScreenView).setVisibility(View.VISIBLE); + }); + } else if (i == 2) { + Button button = new Button(/* context= */ this); + view = button; + button.setText(getString(R.string.new_activity_label)); + button.setOnClickListener( + v -> + startActivity( + new Intent(MainActivity.this, MainActivity.class) + .putExtra(OWNER_EXTRA, /* value= */ false))); + } else { + SurfaceView surfaceView = new SurfaceView(this); + view = surfaceView; + attachSurfaceListener(surfaceView); + surfaceView.setOnClickListener( + v -> { + setCurrentOutputView(surfaceView); + nonFullScreenView = surfaceView; + }); + if (nonFullScreenView == null) { + nonFullScreenView = surfaceView; + } + } + gridLayout.addView(view); + GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams(); + layoutParams.width = 0; + layoutParams.height = 0; + layoutParams.columnSpec = GridLayout.spec(i % 3, 1f); + layoutParams.rowSpec = GridLayout.spec(i / 3, 1f); + layoutParams.bottomMargin = 10; + layoutParams.leftMargin = 10; + layoutParams.topMargin = 10; + layoutParams.rightMargin = 10; + view.setLayoutParams(layoutParams); + } + } + + @Override + public void onResume() { + super.onResume(); + + if (isOwner && player == null) { + initializePlayer(); + } + + setCurrentOutputView(nonFullScreenView); + + PlayerControlView playerControlView = Assertions.checkNotNull(this.playerControlView); + playerControlView.setPlayer(player); + playerControlView.show(); + } + + @Override + public void onPause() { + super.onPause(); + + Assertions.checkNotNull(playerControlView).setPlayer(null); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (isOwner && isFinishing()) { + if (surfaceControl != null) { + surfaceControl.release(); + surfaceControl = null; + } + if (videoSurface != null) { + videoSurface.release(); + videoSurface = null; + } + if (player != null) { + player.release(); + player = null; + } + } + } + + private void initializePlayer() { + Intent intent = getIntent(); + String action = intent.getAction(); + Uri uri = + ACTION_VIEW.equals(action) + ? Assertions.checkNotNull(intent.getData()) + : Uri.parse(DEFAULT_MEDIA_URI); + String userAgent = Util.getUserAgent(this, getString(R.string.application_name)); + DrmSessionManager drmSessionManager; + if (intent.hasExtra(DRM_SCHEME_EXTRA)) { + String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA)); + String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA)); + UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme)); + HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent); + HttpMediaDrmCallback drmCallback = + new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory); + drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(drmSchemeUuid, FrameworkMediaDrm.DEFAULT_PROVIDER) + .build(drmCallback); + } else { + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); + } + + DataSource.Factory dataSourceFactory = + new DefaultDataSourceFactory( + this, Util.getUserAgent(this, getString(R.string.application_name))); + MediaSource mediaSource; + @C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA)); + if (type == C.TYPE_DASH) { + mediaSource = + new DashMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + } else if (type == C.TYPE_OTHER) { + mediaSource = + new ProgressiveMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + } else { + throw new IllegalStateException(); + } + SimpleExoPlayer player = new SimpleExoPlayer.Builder(getApplicationContext()).build(); + player.prepare(mediaSource); + player.setPlayWhenReady(true); + player.setRepeatMode(Player.REPEAT_MODE_ALL); + + surfaceControl = + new SurfaceControl.Builder() + .setName(SURFACE_CONTROL_NAME) + .setBufferSize(/* width= */ 0, /* height= */ 0) + .build(); + videoSurface = new Surface(surfaceControl); + player.setVideoSurface(videoSurface); + MainActivity.player = player; + } + + private void setCurrentOutputView(@Nullable SurfaceView surfaceView) { + currentOutputView = surfaceView; + if (surfaceView != null && surfaceView.getHolder().getSurface() != null) { + reparent(surfaceView); + } + } + + private void attachSurfaceListener(SurfaceView surfaceView) { + surfaceView + .getHolder() + .addCallback( + new SurfaceHolder.Callback() { + @Override + public void surfaceCreated(SurfaceHolder surfaceHolder) { + if (surfaceView == currentOutputView) { + reparent(surfaceView); + } + } + + @Override + public void surfaceChanged( + SurfaceHolder surfaceHolder, int format, int width, int height) {} + + @Override + public void surfaceDestroyed(SurfaceHolder surfaceHolder) {} + }); + } + + private static void reparent(@Nullable SurfaceView surfaceView) { + SurfaceControl surfaceControl = Assertions.checkNotNull(MainActivity.surfaceControl); + if (surfaceView == null) { + new SurfaceControl.Transaction() + .reparent(surfaceControl, /* newParent= */ null) + .setBufferSize(surfaceControl, /* w= */ 0, /* h= */ 0) + .setVisibility(surfaceControl, /* visible= */ false) + .apply(); + } else { + SurfaceControl newParentSurfaceControl = surfaceView.getSurfaceControl(); + new SurfaceControl.Transaction() + .reparent(surfaceControl, newParentSurfaceControl) + .setBufferSize(surfaceControl, surfaceView.getWidth(), surfaceView.getHeight()) + .setVisibility(surfaceControl, /* visible= */ true) + .apply(); + } + } +} diff --git a/demos/surface/src/main/res/layout/main_activity.xml b/demos/surface/src/main/res/layout/main_activity.xml new file mode 100644 index 00000000000..829602275d2 --- /dev/null +++ b/demos/surface/src/main/res/layout/main_activity.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + diff --git a/demos/ima/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/surface/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from demos/ima/src/main/res/mipmap-hdpi/ic_launcher.png rename to demos/surface/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/demos/ima/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/surface/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from demos/ima/src/main/res/mipmap-mdpi/ic_launcher.png rename to demos/surface/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/demos/ima/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/surface/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from demos/ima/src/main/res/mipmap-xhdpi/ic_launcher.png rename to demos/surface/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/demos/ima/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/surface/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from demos/ima/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to demos/surface/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/demos/ima/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/surface/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from demos/ima/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to demos/surface/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/extensions/gvr/src/main/res/values-v21/styles.xml b/demos/surface/src/main/res/values/strings.xml similarity index 68% rename from extensions/gvr/src/main/res/values-v21/styles.xml rename to demos/surface/src/main/res/values/strings.xml index 276db1b42d5..9ba24bd3685 100644 --- a/extensions/gvr/src/main/res/values-v21/styles.xml +++ b/demos/surface/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - - diff --git a/library/ui/src/test/AndroidManifest.xml b/library/ui/src/test/AndroidManifest.xml index 1a749dc82cf..b8f75629695 100644 --- a/library/ui/src/test/AndroidManifest.xml +++ b/library/ui/src/test/AndroidManifest.xml @@ -15,4 +15,6 @@ ~ limitations under the License. --> - + + + diff --git a/library/ui/src/test/java/com/google/android/exoplayer2/ui/spherical/CanvasRendererTest.java b/library/ui/src/test/java/com/google/android/exoplayer2/ui/spherical/CanvasRendererTest.java deleted file mode 100644 index 098f4157ef1..00000000000 --- a/library/ui/src/test/java/com/google/android/exoplayer2/ui/spherical/CanvasRendererTest.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.ui.spherical; - -import static com.google.common.truth.Truth.assertThat; - -import android.graphics.PointF; -import androidx.annotation.Nullable; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Tests for {@link CanvasRenderer}. */ -@RunWith(AndroidJUnit4.class) -public class CanvasRendererTest { - - private static final float JUST_BELOW_45_DEGREES = (float) (Math.PI / 4 - 1.0E-08); - private static final float JUST_ABOVE_45_DEGREES = (float) (Math.PI / 4 + 1.0E-08); - private static final float TOLERANCE = .00001f; - - @Test - public void testClicksOnCanvas() { - assertClick(translateClick(JUST_BELOW_45_DEGREES, JUST_BELOW_45_DEGREES), 0, 0); - assertClick(translateClick(JUST_BELOW_45_DEGREES, -JUST_BELOW_45_DEGREES), 0, 100); - assertClick(translateClick(0, 0), 50, 50); - assertClick(translateClick(-JUST_BELOW_45_DEGREES, JUST_BELOW_45_DEGREES), 100, 0); - assertClick(translateClick(-JUST_BELOW_45_DEGREES, -JUST_BELOW_45_DEGREES), 100, 100); - } - - @Test - public void testClicksNotOnCanvas() { - assertThat(translateClick(JUST_ABOVE_45_DEGREES, JUST_ABOVE_45_DEGREES)).isNull(); - assertThat(translateClick(JUST_ABOVE_45_DEGREES, -JUST_ABOVE_45_DEGREES)).isNull(); - assertThat(translateClick(-JUST_ABOVE_45_DEGREES, JUST_ABOVE_45_DEGREES)).isNull(); - assertThat(translateClick(-JUST_ABOVE_45_DEGREES, -JUST_ABOVE_45_DEGREES)).isNull(); - assertThat(translateClick((float) (Math.PI / 2), 0)).isNull(); - assertThat(translateClick(0, (float) Math.PI)).isNull(); - } - - private static PointF translateClick(float yaw, float pitch) { - return CanvasRenderer.internalTranslateClick( - yaw, - pitch, - /* xUnit= */ -1, - /* yUnit= */ -1, - /* widthUnit= */ 2, - /* heightUnit= */ 2, - /* widthPixel= */ 100, - /* heightPixel= */ 100); - } - - private static void assertClick(@Nullable PointF actual, float expectedX, float expectedY) { - assertThat(actual).isNotNull(); - assertThat(actual.x).isWithin(TOLERANCE).of(expectedX); - assertThat(actual.y).isWithin(TOLERANCE).of(expectedY); - } -} diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle index 5865d3c36dd..0e93b97f5ef 100644 --- a/playbacktests/build.gradle +++ b/playbacktests/build.gradle @@ -32,9 +32,9 @@ android { } dependencies { - androidTestImplementation 'androidx.test:rules:' + androidXTestVersion - androidTestImplementation 'androidx.test:runner:' + androidXTestVersion - androidTestImplementation 'androidx.annotation:annotation:1.1.0' + androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion + androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion + androidTestImplementation 'androidx.annotation:annotation:' + androidxAnnotationVersion androidTestImplementation project(modulePrefix + 'library-core') androidTestImplementation project(modulePrefix + 'library-dash') androidTestImplementation project(modulePrefix + 'library-hls') 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 598138126b6..5ae4708006f 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 @@ -101,7 +101,7 @@ public void tearDown() { // H264 CDD. @Test - public void testH264Fixed() { + public void testH264Fixed() throws Exception { testRunner .setStreamName("test_h264_fixed") .setManifestUrl(DashTestData.H264_MANIFEST) @@ -112,7 +112,7 @@ public void testH264Fixed() { } @Test - public void testH264Adaptive() throws DecoderQueryException { + public void testH264Adaptive() throws Exception { if (shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { // Pass. return; @@ -128,7 +128,7 @@ public void testH264Adaptive() throws DecoderQueryException { } @Test - public void testH264AdaptiveWithSeeking() throws DecoderQueryException { + public void testH264AdaptiveWithSeeking() throws Exception { if (shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { // Pass. return; @@ -146,7 +146,7 @@ public void testH264AdaptiveWithSeeking() throws DecoderQueryException { } @Test - public void testH264AdaptiveWithRendererDisabling() throws DecoderQueryException { + public void testH264AdaptiveWithRendererDisabling() throws Exception { if (shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { // Pass. return; @@ -166,7 +166,7 @@ public void testH264AdaptiveWithRendererDisabling() throws DecoderQueryException // H265 CDD. @Test - public void testH265FixedV23() { + public void testH265FixedV23() throws Exception { if (Util.SDK_INT < 23) { // Pass. return; @@ -181,7 +181,7 @@ public void testH265FixedV23() { } @Test - public void testH265AdaptiveV24() throws DecoderQueryException { + public void testH265AdaptiveV24() throws Exception { if (Util.SDK_INT < 24) { // Pass. return; @@ -197,7 +197,7 @@ public void testH265AdaptiveV24() throws DecoderQueryException { } @Test - public void testH265AdaptiveWithSeekingV24() throws DecoderQueryException { + public void testH265AdaptiveWithSeekingV24() throws Exception { if (Util.SDK_INT < 24) { // Pass. return; @@ -214,7 +214,7 @@ public void testH265AdaptiveWithSeekingV24() throws DecoderQueryException { } @Test - public void testH265AdaptiveWithRendererDisablingV24() throws DecoderQueryException { + public void testH265AdaptiveWithRendererDisablingV24() throws Exception { if (Util.SDK_INT < 24) { // Pass. return; @@ -233,7 +233,7 @@ public void testH265AdaptiveWithRendererDisablingV24() throws DecoderQueryExcept // VP9 (CDD). @Test - public void testVp9Fixed360pV23() { + public void testVp9Fixed360pV23() throws Exception { if (Util.SDK_INT < 23) { // Pass. return; @@ -249,7 +249,7 @@ public void testVp9Fixed360pV23() { } @Test - public void testVp9AdaptiveV24() throws DecoderQueryException { + public void testVp9AdaptiveV24() throws Exception { if (Util.SDK_INT < 24) { // Pass. return; @@ -265,7 +265,7 @@ public void testVp9AdaptiveV24() throws DecoderQueryException { } @Test - public void testVp9AdaptiveWithSeekingV24() throws DecoderQueryException { + public void testVp9AdaptiveWithSeekingV24() throws Exception { if (Util.SDK_INT < 24) { // Pass. return; @@ -282,7 +282,7 @@ public void testVp9AdaptiveWithSeekingV24() throws DecoderQueryException { } @Test - public void testVp9AdaptiveWithRendererDisablingV24() throws DecoderQueryException { + public void testVp9AdaptiveWithRendererDisablingV24() throws Exception { if (Util.SDK_INT < 24) { // Pass. return; @@ -302,7 +302,7 @@ public void testVp9AdaptiveWithRendererDisablingV24() throws DecoderQueryExcepti // 23.976 fps. @Test - public void test23FpsH264FixedV23() { + public void test23FpsH264FixedV23() throws Exception { if (Util.SDK_INT < 23) { // Pass. return; @@ -319,7 +319,7 @@ public void test23FpsH264FixedV23() { // 24 fps. @Test - public void test24FpsH264FixedV23() { + public void test24FpsH264FixedV23() throws Exception { if (Util.SDK_INT < 23) { // Pass. return; @@ -336,7 +336,7 @@ public void test24FpsH264FixedV23() { // 29.97 fps. @Test - public void test29FpsH264FixedV23() { + public void test29FpsH264FixedV23() throws Exception { if (Util.SDK_INT < 23) { // Pass. return; @@ -355,7 +355,7 @@ public void test29FpsH264FixedV23() { // H264 CDD. @Test - public void testWidevineH264FixedV18() throws DecoderQueryException { + public void testWidevineH264FixedV18() throws Exception { if (Util.SDK_INT < 18) { // Pass. return; @@ -372,7 +372,7 @@ public void testWidevineH264FixedV18() throws DecoderQueryException { } @Test - public void testWidevineH264AdaptiveV18() throws DecoderQueryException { + public void testWidevineH264AdaptiveV18() throws Exception { if (Util.SDK_INT < 18 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { // Pass. return; @@ -389,7 +389,7 @@ public void testWidevineH264AdaptiveV18() throws DecoderQueryException { } @Test - public void testWidevineH264AdaptiveWithSeekingV18() throws DecoderQueryException { + public void testWidevineH264AdaptiveWithSeekingV18() throws Exception { if (Util.SDK_INT < 18 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { // Pass. return; @@ -407,7 +407,7 @@ public void testWidevineH264AdaptiveWithSeekingV18() throws DecoderQueryExceptio } @Test - public void testWidevineH264AdaptiveWithRendererDisablingV18() throws DecoderQueryException { + public void testWidevineH264AdaptiveWithRendererDisablingV18() throws Exception { if (Util.SDK_INT < 18 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { // Pass. return; @@ -427,7 +427,7 @@ public void testWidevineH264AdaptiveWithRendererDisablingV18() throws DecoderQue // H265 CDD. @Test - public void testWidevineH265FixedV23() throws DecoderQueryException { + public void testWidevineH265FixedV23() throws Exception { if (Util.SDK_INT < 23) { // Pass. return; @@ -444,7 +444,7 @@ public void testWidevineH265FixedV23() throws DecoderQueryException { } @Test - public void testWidevineH265AdaptiveV24() throws DecoderQueryException { + public void testWidevineH265AdaptiveV24() throws Exception { if (Util.SDK_INT < 24) { // Pass. return; @@ -461,7 +461,7 @@ public void testWidevineH265AdaptiveV24() throws DecoderQueryException { } @Test - public void testWidevineH265AdaptiveWithSeekingV24() throws DecoderQueryException { + public void testWidevineH265AdaptiveWithSeekingV24() throws Exception { if (Util.SDK_INT < 24) { // Pass. return; @@ -479,7 +479,7 @@ public void testWidevineH265AdaptiveWithSeekingV24() throws DecoderQueryExceptio } @Test - public void testWidevineH265AdaptiveWithRendererDisablingV24() throws DecoderQueryException { + public void testWidevineH265AdaptiveWithRendererDisablingV24() throws Exception { if (Util.SDK_INT < 24) { // Pass. return; @@ -499,7 +499,7 @@ public void testWidevineH265AdaptiveWithRendererDisablingV24() throws DecoderQue // VP9 (CDD). @Test - public void testWidevineVp9Fixed360pV23() throws DecoderQueryException { + public void testWidevineVp9Fixed360pV23() throws Exception { if (Util.SDK_INT < 23) { // Pass. return; @@ -516,7 +516,7 @@ public void testWidevineVp9Fixed360pV23() throws DecoderQueryException { } @Test - public void testWidevineVp9AdaptiveV24() throws DecoderQueryException { + public void testWidevineVp9AdaptiveV24() throws Exception { if (Util.SDK_INT < 24) { // Pass. return; @@ -533,7 +533,7 @@ public void testWidevineVp9AdaptiveV24() throws DecoderQueryException { } @Test - public void testWidevineVp9AdaptiveWithSeekingV24() throws DecoderQueryException { + public void testWidevineVp9AdaptiveWithSeekingV24() throws Exception { if (Util.SDK_INT < 24) { // Pass. return; @@ -551,7 +551,7 @@ public void testWidevineVp9AdaptiveWithSeekingV24() throws DecoderQueryException } @Test - public void testWidevineVp9AdaptiveWithRendererDisablingV24() throws DecoderQueryException { + public void testWidevineVp9AdaptiveWithRendererDisablingV24() throws Exception { if (Util.SDK_INT < 24) { // Pass. return; @@ -572,7 +572,7 @@ public void testWidevineVp9AdaptiveWithRendererDisablingV24() throws DecoderQuer // 23.976 fps. @Test - public void testWidevine23FpsH264FixedV23() throws DecoderQueryException { + public void testWidevine23FpsH264FixedV23() throws Exception { if (Util.SDK_INT < 23) { // Pass. return; @@ -590,7 +590,7 @@ public void testWidevine23FpsH264FixedV23() throws DecoderQueryException { // 24 fps. @Test - public void testWidevine24FpsH264FixedV23() throws DecoderQueryException { + public void testWidevine24FpsH264FixedV23() throws Exception { if (Util.SDK_INT < 23) { // Pass. return; @@ -608,7 +608,7 @@ public void testWidevine24FpsH264FixedV23() throws DecoderQueryException { // 29.97 fps. @Test - public void testWidevine29FpsH264FixedV23() throws DecoderQueryException { + public void testWidevine29FpsH264FixedV23() throws Exception { if (Util.SDK_INT < 23) { // Pass. return; @@ -627,7 +627,7 @@ public void testWidevine29FpsH264FixedV23() throws DecoderQueryException { // Decoder info. @Test - public void testDecoderInfoH264() throws DecoderQueryException { + public void testDecoderInfoH264() throws Exception { MediaCodecInfo decoderInfo = MediaCodecUtil.getDecoderInfo( MimeTypes.VIDEO_H264, /* secure= */ false, /* tunneling= */ false); @@ -636,7 +636,7 @@ public void testDecoderInfoH264() throws DecoderQueryException { } @Test - public void testDecoderInfoH265V24() throws DecoderQueryException { + public void testDecoderInfoH265V24() throws Exception { if (Util.SDK_INT < 24) { // Pass. return; @@ -649,7 +649,7 @@ public void testDecoderInfoH265V24() throws DecoderQueryException { } @Test - public void testDecoderInfoVP9V24() throws DecoderQueryException { + public void testDecoderInfoVP9V24() throws Exception { if (Util.SDK_INT < 24) { // 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 45cdf34b6ca..2033ef3096a 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 @@ -17,10 +17,8 @@ import com.google.android.exoplayer2.util.Util; -/** - * Test data for DASH tests. - */ -public final class DashTestData { +/** Test data for DASH tests. */ +/* package */ final class DashTestData { private static final String BASE_URL = "https://storage.googleapis.com/exoplayer-test-media-1/gen-4/"; diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java index b2a49a31fee..5deed11699f 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java @@ -23,9 +23,6 @@ import android.net.Uri; import android.view.Surface; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultLoadControl; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.SimpleExoPlayer; @@ -33,6 +30,7 @@ import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +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.drm.UnsupportedDrmException; @@ -42,12 +40,10 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.testutil.ActionSchedule; -import com.google.android.exoplayer2.testutil.DebugRenderersFactory; import com.google.android.exoplayer2.testutil.DecoderCountersUtil; import com.google.android.exoplayer2.testutil.ExoHostedTest; import com.google.android.exoplayer2.testutil.HostActivity; import com.google.android.exoplayer2.testutil.HostActivity.HostedTest; -import com.google.android.exoplayer2.testutil.MetricsLogger; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.RandomTrackSelection; @@ -64,7 +60,7 @@ import java.util.List; /** {@link DashHostedTest} builder. */ -public final class DashTestRunner { +/* package */ final class DashTestRunner { static final int VIDEO_RENDERER_INDEX = 0; static final int AUDIO_RENDERER_INDEX = 1; @@ -260,19 +256,25 @@ protected DefaultTrackSelector buildTrackSelector(HostActivity host) { } @Override - protected DefaultDrmSessionManager buildDrmSessionManager( + protected DrmSessionManager buildDrmSessionManager( final String userAgent) { if (widevineLicenseUrl == null) { - return null; + return DrmSessionManager.getDummyDrmSessionManager(); } try { MediaDrmCallback drmCallback = new HttpMediaDrmCallback(widevineLicenseUrl, new DefaultHttpDataSourceFactory(userAgent)); + FrameworkMediaDrm frameworkMediaDrm = FrameworkMediaDrm.newInstance(WIDEVINE_UUID); DefaultDrmSessionManager drmSessionManager = - DefaultDrmSessionManager.newWidevineInstance(drmCallback, null); + new DefaultDrmSessionManager<>( + C.WIDEVINE_UUID, + frameworkMediaDrm, + drmCallback, + /* optionalKeyRequestParameters= */ null, + /* multiSession= */ false, + DefaultDrmSessionManager.INITIAL_DRM_REQUEST_RETRY_COUNT); if (!useL1Widevine) { - drmSessionManager.setPropertyString( - SECURITY_LEVEL_PROPERTY, WIDEVINE_SECURITY_LEVEL_3); + frameworkMediaDrm.setPropertyString(SECURITY_LEVEL_PROPERTY, WIDEVINE_SECURITY_LEVEL_3); } if (offlineLicenseKeySetId != null) { drmSessionManager.setMode(DefaultDrmSessionManager.MODE_PLAYBACK, @@ -286,29 +288,25 @@ protected DefaultDrmSessionManager buildDrmSessionManager( @Override protected SimpleExoPlayer buildExoPlayer( - HostActivity host, - Surface surface, - MappingTrackSelector trackSelector, - DrmSessionManager drmSessionManager) { + HostActivity host, Surface surface, MappingTrackSelector trackSelector) { SimpleExoPlayer player = - ExoPlayerFactory.newSimpleInstance( - host, - new DebugRenderersFactory(host), - trackSelector, - new DefaultLoadControl(), - drmSessionManager); + new SimpleExoPlayer.Builder(host, new DebugRenderersFactory(host)) + .setTrackSelector(trackSelector) + .build(); player.setVideoSurface(surface); return player; } @Override - protected MediaSource buildSource(HostActivity host, String userAgent) { + protected MediaSource buildSource( + HostActivity host, String userAgent, DrmSessionManager drmSessionManager) { DataSource.Factory dataSourceFactory = this.dataSourceFactory != null ? this.dataSourceFactory : new DefaultDataSourceFactory(host, userAgent); Uri manifestUri = Uri.parse(manifestUrl); return new DashMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) .setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(MIN_LOADABLE_RETRY_COUNT)) .createMediaSource(manifestUri); } @@ -385,8 +383,7 @@ protected TrackSelection.Definition[] selectAllTracks( MappedTrackInfo mappedTrackInfo, int[][][] rendererFormatSupports, int[] rendererMixedMimeTypeAdaptationSupports, - Parameters parameters) - throws ExoPlaybackException { + Parameters parameters) { Assertions.checkState( mappedTrackInfo.getRendererType(VIDEO_RENDERER_INDEX) == C.TRACK_TYPE_VIDEO); Assertions.checkState( @@ -454,7 +451,7 @@ private static int getTrackIndex(TrackGroup trackGroup, String formatId) { } private static boolean isFormatHandled(int formatSupport) { - return (formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK) + return RendererCapabilities.getFormatSupport(formatSupport) == RendererCapabilities.FORMAT_HANDLED; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java similarity index 96% rename from testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java rename to playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java index 8b11a89d8d1..affd762f614 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.testutil; +package com.google.android.exoplayer2.playbacktests.gts; import android.annotation.TargetApi; import android.content.Context; @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -40,7 +41,7 @@ * video buffer timestamp assertions, and modifies the default value for {@link * #setAllowedVideoJoiningTimeMs(long)} to be {@code 0}. */ -public class DebugRenderersFactory extends DefaultRenderersFactory { +/* package */ final class DebugRenderersFactory extends DefaultRenderersFactory { public DebugRenderersFactory(Context context) { super(context); @@ -140,8 +141,8 @@ protected boolean flushOrReleaseCodec() { } @Override - protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { - super.onInputFormatChanged(newFormat); + protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { + super.onInputFormatChanged(formatHolder); // Ensure timestamps of buffers queued after this format change are never inserted into the // queue of expected output timestamps before those of buffers that have already been queued. minimumInsertIndex = startIndex + queueSize; diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/EnumerateDecodersTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/EnumerateDecodersTest.java index d256db8c301..f7b376d7add 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/EnumerateDecodersTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/EnumerateDecodersTest.java @@ -20,12 +20,11 @@ import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecInfo.VideoCapabilities; +import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; -import com.google.android.exoplayer2.testutil.MetricsLogger; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -54,20 +53,24 @@ public void testEnumerateDecoders() throws Exception { enumerateDecoders(MimeTypes.VIDEO_H265); enumerateDecoders(MimeTypes.VIDEO_VP8); enumerateDecoders(MimeTypes.VIDEO_VP9); + enumerateDecoders(MimeTypes.VIDEO_AV1); enumerateDecoders(MimeTypes.VIDEO_MP4V); enumerateDecoders(MimeTypes.VIDEO_MPEG); enumerateDecoders(MimeTypes.VIDEO_MPEG2); enumerateDecoders(MimeTypes.VIDEO_VC1); + enumerateDecoders(MimeTypes.VIDEO_DIVX); + enumerateDecoders(MimeTypes.VIDEO_DOLBY_VISION); enumerateDecoders(MimeTypes.AUDIO_AAC); + enumerateDecoders(MimeTypes.AUDIO_MPEG); enumerateDecoders(MimeTypes.AUDIO_MPEG_L1); enumerateDecoders(MimeTypes.AUDIO_MPEG_L2); - enumerateDecoders(MimeTypes.AUDIO_MPEG); enumerateDecoders(MimeTypes.AUDIO_RAW); enumerateDecoders(MimeTypes.AUDIO_ALAW); enumerateDecoders(MimeTypes.AUDIO_MLAW); enumerateDecoders(MimeTypes.AUDIO_AC3); enumerateDecoders(MimeTypes.AUDIO_E_AC3); enumerateDecoders(MimeTypes.AUDIO_E_AC3_JOC); + enumerateDecoders(MimeTypes.AUDIO_AC4); enumerateDecoders(MimeTypes.AUDIO_TRUEHD); enumerateDecoders(MimeTypes.AUDIO_DTS); enumerateDecoders(MimeTypes.AUDIO_DTS_HD); @@ -93,14 +96,17 @@ private void logDecoderInfos(String mimeType, boolean secure, boolean tunneling) List mediaCodecInfos = MediaCodecUtil.getDecoderInfos(mimeType, secure, tunneling); for (MediaCodecInfo mediaCodecInfo : mediaCodecInfos) { - CodecCapabilities capabilities = Assertions.checkNotNull(mediaCodecInfo.capabilities); + CodecCapabilities capabilities = mediaCodecInfo.capabilities; metricsLogger.logMetric( "capabilities_" + mediaCodecInfo.name, codecCapabilitiesToString(mimeType, capabilities)); } } private static String codecCapabilitiesToString( - String requestedMimeType, CodecCapabilities codecCapabilities) { + String requestedMimeType, @Nullable CodecCapabilities codecCapabilities) { + if (codecCapabilities == null) { + return "[null]"; + } boolean isVideo = MimeTypes.isVideo(requestedMimeType); boolean isAudio = MimeTypes.isAudio(requestedMimeType); StringBuilder result = new StringBuilder(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/LogcatMetricsLogger.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/LogcatMetricsLogger.java similarity index 78% rename from testutils/src/main/java/com/google/android/exoplayer2/testutil/LogcatMetricsLogger.java rename to playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/LogcatMetricsLogger.java index f3432749d0d..94a07b9aafe 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/LogcatMetricsLogger.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/LogcatMetricsLogger.java @@ -13,14 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.testutil; +package com.google.android.exoplayer2.playbacktests.gts; import com.google.android.exoplayer2.util.Log; -/** - * Implementation of {@link MetricsLogger} that prints the metrics to logcat. - */ -public final class LogcatMetricsLogger implements MetricsLogger { +/** Implementation of {@link MetricsLogger} that prints the metrics to logcat. */ +/* package */ final class LogcatMetricsLogger implements MetricsLogger { private final String tag; @@ -33,11 +31,6 @@ public void logMetric(String key, int value) { Log.d(tag, key + ": " + value); } - @Override - public void logMetric(String key, double value) { - Log.d(tag, key + ": " + value); - } - @Override public void logMetric(String key, String value) { Log.d(tag, key + ": " + value); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MetricsLogger.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/MetricsLogger.java similarity index 85% rename from testutils/src/main/java/com/google/android/exoplayer2/testutil/MetricsLogger.java rename to playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/MetricsLogger.java index 9edccadcab0..1aeb73a29dc 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MetricsLogger.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/MetricsLogger.java @@ -13,12 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.testutil; +package com.google.android.exoplayer2.playbacktests.gts; -/** - * Metric Logging interface for ExoPlayer playback tests. - */ -public interface MetricsLogger { +/** Metric logging interface for playback tests. */ +/* package */ interface MetricsLogger { String KEY_FRAMES_DROPPED_COUNT = "frames_dropped_count"; String KEY_FRAMES_RENDERED_COUNT = "frames_rendered_count"; @@ -35,14 +33,6 @@ public interface MetricsLogger { */ void logMetric(String key, int value); - /** - * Logs a double metric provided from a test. - * - * @param key The key of the metric to be logged. - * @param value The value of the metric to be logged. - */ - void logMetric(String key, double value); - /** * Logs a string metric provided from a test. * diff --git a/settings.gradle b/settings.gradle index d4530d67b74..39e4791bb59 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,11 +20,11 @@ if (gradle.ext.has('exoplayerModulePrefix')) { include modulePrefix + 'demo' include modulePrefix + 'demo-cast' -include modulePrefix + 'demo-ima' +include modulePrefix + 'demo-surface' include modulePrefix + 'playbacktests' project(modulePrefix + 'demo').projectDir = new File(rootDir, 'demos/main') project(modulePrefix + 'demo-cast').projectDir = new File(rootDir, 'demos/cast') -project(modulePrefix + 'demo-ima').projectDir = new File(rootDir, 'demos/ima') +project(modulePrefix + 'demo-surface').projectDir = new File(rootDir, 'demos/surface') project(modulePrefix + 'playbacktests').projectDir = new File(rootDir, 'playbacktests') apply from: 'core_settings.gradle' diff --git a/testutils/README.md b/testutils/README.md new file mode 100644 index 00000000000..d1ab6b1af50 --- /dev/null +++ b/testutils/README.md @@ -0,0 +1,10 @@ +# ExoPlayer test utils # + +Provides utility classes for ExoPlayer unit and instrumentation tests. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.testutil` + belong to this module. + +[Javadoc]: https://exoplayer.dev/doc/reference/index.html diff --git a/testutils/build.gradle b/testutils/build.gradle index 1ec358b83d7..204e089bd0f 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -39,11 +39,22 @@ android { dependencies { api 'org.mockito:mockito-core:' + mockitoVersion - api 'androidx.test.ext:junit:' + androidXTestVersion - api 'androidx.test.ext:truth:' + androidXTestVersion - implementation 'androidx.annotation:annotation:1.1.0' + api 'androidx.test:core:' + androidxTestCoreVersion + api 'androidx.test.ext:junit:' + androidxTestJUnitVersion + api 'com.google.truth:truth:' + truthVersion + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation project(modulePrefix + 'library-core') - implementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion - annotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion - testImplementation project(modulePrefix + 'testutils-robolectric') + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion } + +ext { + javadocTitle = 'Test utils' +} +apply from: '../javadoc_library.gradle' + +ext { + releaseArtifact = 'exoplayer-testutils' + releaseDescription = 'Test utils for ExoPlayer.' +} +apply from: '../publish.gradle' 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 f1fdfc42aa6..b65accdf3f8 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 @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; -import androidx.annotation.Nullable; 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.ExoPlayer; @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.testutil.ActionSchedule.ActionNode; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; @@ -37,13 +38,11 @@ import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.Log; -/** - * Base class for actions to perform during playback tests. - */ +/** Base class for actions to perform during playback tests. */ public abstract class Action { private final String tag; - private final @Nullable String description; + @Nullable private final String description; /** * @param tag A tag to use for logging. @@ -109,9 +108,7 @@ protected void doActionAndScheduleNextImpl( protected abstract void doActionImpl( SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface); - /** - * Calls {@link Player#seekTo(long)} or {@link Player#seekTo(int, long)}. - */ + /** Calls {@link Player#seekTo(long)} or {@link Player#seekTo(int, long)}. */ public static final class Seek extends Action { private final Integer windowIndex; @@ -151,12 +148,9 @@ protected void doActionImpl( player.seekTo(windowIndex, positionMs); } } - } - /** - * Calls {@link Player#stop()} or {@link Player#stop(boolean)}. - */ + /** Calls {@link Player#stop()} or {@link Player#stop(boolean)}. */ public static final class Stop extends Action { private static final String STOP_ACTION_TAG = "Stop"; @@ -192,14 +186,10 @@ protected void doActionImpl( } else { player.stop(reset); } - } - } - /** - * Calls {@link Player#setPlayWhenReady(boolean)}. - */ + /** Calls {@link Player#setPlayWhenReady(boolean)}. */ public static final class SetPlayWhenReady extends Action { private final boolean playWhenReady; @@ -247,17 +237,12 @@ protected void doActionImpl( trackSelector.setParameters( trackSelector.buildUponParameters().setRendererDisabled(rendererIndex, disabled)); } - } - /** - * Calls {@link SimpleExoPlayer#clearVideoSurface()}. - */ + /** Calls {@link SimpleExoPlayer#clearVideoSurface()}. */ public static final class ClearVideoSurface extends Action { - /** - * @param tag A tag to use for logging. - */ + /** @param tag A tag to use for logging. */ public ClearVideoSurface(String tag) { super(tag, "ClearVideoSurface"); } @@ -267,17 +252,12 @@ protected void doActionImpl( SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { player.clearVideoSurface(); } - } - /** - * Calls {@link SimpleExoPlayer#setVideoSurface(Surface)}. - */ + /** Calls {@link SimpleExoPlayer#setVideoSurface(Surface)}. */ public static final class SetVideoSurface extends Action { - /** - * @param tag A tag to use for logging. - */ + /** @param tag A tag to use for logging. */ public SetVideoSurface(String tag) { super(tag, "SetVideoSurface"); } @@ -287,12 +267,34 @@ protected void doActionImpl( SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { player.setVideoSurface(surface); } + } + + /** Calls {@link SimpleExoPlayer#setAudioAttributes(AudioAttributes, boolean)}. */ + public static final class SetAudioAttributes extends Action { + + private final AudioAttributes audioAttributes; + private final boolean handleAudioFocus; + + /** + * @param tag A tag to use for logging. + * @param audioAttributes The attributes to use for audio playback. + * @param handleAudioFocus True if the player should handle audio focus, false otherwise. + */ + public SetAudioAttributes( + String tag, AudioAttributes audioAttributes, boolean handleAudioFocus) { + super(tag, "SetAudioAttributes"); + this.audioAttributes = audioAttributes; + this.handleAudioFocus = handleAudioFocus; + } + @Override + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { + player.setAudioAttributes(audioAttributes, handleAudioFocus); + } } - /** - * Calls {@link ExoPlayer#prepare(MediaSource)}. - */ + /** Calls {@link ExoPlayer#prepare(MediaSource)}. */ public static final class PrepareSource extends Action { private final MediaSource mediaSource; @@ -301,6 +303,7 @@ public static final class PrepareSource extends Action { /** * @param tag A tag to use for logging. + * @param mediaSource The {@link MediaSource} to prepare the player with. */ public PrepareSource(String tag, MediaSource mediaSource) { this(tag, mediaSource, true, true); @@ -308,9 +311,11 @@ public PrepareSource(String tag, MediaSource mediaSource) { /** * @param tag A tag to use for logging. + * @param mediaSource The {@link MediaSource} to prepare the player with. + * @param resetPosition Whether the player's position should be reset. */ - public PrepareSource(String tag, MediaSource mediaSource, boolean resetPosition, - boolean resetState) { + public PrepareSource( + String tag, MediaSource mediaSource, boolean resetPosition, boolean resetState) { super(tag, "PrepareSource"); this.mediaSource = mediaSource; this.resetPosition = resetPosition; @@ -322,18 +327,16 @@ protected void doActionImpl( SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { player.prepare(mediaSource, resetPosition, resetState); } - } - /** - * Calls {@link Player#setRepeatMode(int)}. - */ + /** Calls {@link Player#setRepeatMode(int)}. */ public static final class SetRepeatMode extends Action { private final @Player.RepeatMode int repeatMode; /** * @param tag A tag to use for logging. + * @param repeatMode The repeat mode. */ public SetRepeatMode(String tag, @Player.RepeatMode int repeatMode) { super(tag, "SetRepeatMode:" + repeatMode); @@ -345,18 +348,16 @@ protected void doActionImpl( SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { player.setRepeatMode(repeatMode); } - } - /** - * Calls {@link Player#setShuffleModeEnabled(boolean)}. - */ + /** Calls {@link Player#setShuffleModeEnabled(boolean)}. */ public static final class SetShuffleModeEnabled extends Action { private final boolean shuffleModeEnabled; /** * @param tag A tag to use for logging. + * @param shuffleModeEnabled Whether shuffling is enabled. */ public SetShuffleModeEnabled(String tag, boolean shuffleModeEnabled) { super(tag, "SetShuffleModeEnabled:" + shuffleModeEnabled); @@ -427,9 +428,7 @@ protected void doActionImpl( } } - /** - * Calls {@link Player#setPlaybackParameters(PlaybackParameters)}. - */ + /** Calls {@link Player#setPlaybackParameters(PlaybackParameters)}. */ public static final class SetPlaybackParameters extends Action { private final PlaybackParameters playbackParameters; @@ -542,12 +541,10 @@ protected void doActionImpl( } } - /** - * Waits for {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)}. - */ + /** Waits for {@link Player.EventListener#onTimelineChanged(Timeline, int)}. */ public static final class WaitForTimelineChanged extends Action { - private final @Nullable Timeline expectedTimeline; + @Nullable private final Timeline expectedTimeline; /** * Creates action waiting for a timeline change. @@ -575,9 +572,7 @@ protected void doActionAndScheduleNextImpl( new Player.EventListener() { @Override public void onTimelineChanged( - Timeline timeline, - @Nullable Object manifest, - @Player.TimelineChangeReason int reason) { + Timeline timeline, @Player.TimelineChangeReason int reason) { if (expectedTimeline == null || timeline.equals(expectedTimeline)) { player.removeListener(this); nextAction.schedule(player, trackSelector, surface, handler); @@ -598,14 +593,10 @@ protected void doActionImpl( } } - /** - * Waits for {@link Player.EventListener#onPositionDiscontinuity(int)}. - */ + /** Waits for {@link Player.EventListener#onPositionDiscontinuity(int)}. */ public static final class WaitForPositionDiscontinuity extends Action { - /** - * @param tag A tag to use for logging. - */ + /** @param tag A tag to use for logging. */ public WaitForPositionDiscontinuity(String tag) { super(tag, "WaitForPositionDiscontinuity"); } @@ -638,15 +629,67 @@ protected void doActionImpl( } /** - * Waits for a specified playback state, returning either immediately or after a call to + * Waits for a specified playWhenReady value, returning either immediately or after a call to * {@link Player.EventListener#onPlayerStateChanged(boolean, int)}. */ + public static final class WaitForPlayWhenReady extends Action { + + private final boolean targetPlayWhenReady; + + /** + * @param tag A tag to use for logging. + * @param playWhenReady The playWhenReady value to wait for. + */ + public WaitForPlayWhenReady(String tag, boolean playWhenReady) { + super(tag, "WaitForPlayWhenReady"); + targetPlayWhenReady = playWhenReady; + } + + @Override + protected void doActionAndScheduleNextImpl( + SimpleExoPlayer player, + DefaultTrackSelector trackSelector, + Surface surface, + HandlerWrapper handler, + ActionNode nextAction) { + if (nextAction == null) { + return; + } + if (targetPlayWhenReady == player.getPlayWhenReady()) { + nextAction.schedule(player, trackSelector, surface, handler); + } else { + player.addListener( + new Player.EventListener() { + @Override + public void onPlayerStateChanged( + boolean playWhenReady, @Player.State int playbackState) { + if (targetPlayWhenReady == playWhenReady) { + player.removeListener(this); + nextAction.schedule(player, trackSelector, surface, handler); + } + } + }); + } + } + + @Override + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { + // Not triggered. + } + } + + /** + * Waits for a specified playback state, returning either immediately or after a call to {@link + * Player.EventListener#onPlayerStateChanged(boolean, int)}. + */ public static final class WaitForPlaybackState extends Action { private final int targetPlaybackState; /** * @param tag A tag to use for logging. + * @param targetPlaybackState The playback state to wait for. */ public WaitForPlaybackState(String tag, int targetPlaybackState) { super(tag, "WaitForPlaybackState"); @@ -669,7 +712,8 @@ protected void doActionAndScheduleNextImpl( player.addListener( new Player.EventListener() { @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged( + boolean playWhenReady, @Player.State int playbackState) { if (targetPlaybackState == playbackState) { player.removeListener(this); nextAction.schedule(player, trackSelector, surface, handler); @@ -736,14 +780,10 @@ protected void doActionImpl( } } - /** - * Waits for {@link Player.EventListener#onSeekProcessed()}. - */ + /** Waits for {@link Player.EventListener#onSeekProcessed()}. */ public static final class WaitForSeekProcessed extends Action { - /** - * @param tag A tag to use for logging. - */ + /** @param tag A tag to use for logging. */ public WaitForSeekProcessed(String tag) { super(tag, "WaitForSeekProcessed"); } @@ -775,16 +815,12 @@ protected void doActionImpl( } } - /** - * Calls {@link Runnable#run()}. - */ + /** Calls {@link Runnable#run()}. */ public static final class ExecuteRunnable extends Action { private final Runnable runnable; - /** - * @param tag A tag to use for logging. - */ + /** @param tag A tag to use for logging. */ public ExecuteRunnable(String tag, Runnable runnable) { super(tag, "ExecuteRunnable"); this.runnable = runnable; @@ -798,7 +834,5 @@ protected void doActionImpl( } runnable.run(); } - } - } 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 7f688cacf71..f6ab4b9924e 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 @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.testutil; import android.os.Looper; -import androidx.annotation.Nullable; 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; @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.testutil.Action.ClearVideoSurface; import com.google.android.exoplayer2.testutil.Action.ExecuteRunnable; @@ -33,6 +34,7 @@ import com.google.android.exoplayer2.testutil.Action.PrepareSource; import com.google.android.exoplayer2.testutil.Action.Seek; import com.google.android.exoplayer2.testutil.Action.SendMessages; +import com.google.android.exoplayer2.testutil.Action.SetAudioAttributes; import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady; import com.google.android.exoplayer2.testutil.Action.SetPlaybackParameters; import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled; @@ -42,6 +44,7 @@ import com.google.android.exoplayer2.testutil.Action.Stop; import com.google.android.exoplayer2.testutil.Action.ThrowPlaybackException; import com.google.android.exoplayer2.testutil.Action.WaitForIsLoading; +import com.google.android.exoplayer2.testutil.Action.WaitForPlayWhenReady; import com.google.android.exoplayer2.testutil.Action.WaitForPlaybackState; import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuity; import com.google.android.exoplayer2.testutil.Action.WaitForSeekProcessed; @@ -131,7 +134,7 @@ public Builder delay(long delayMs) { } /** - * Schedules an action to be executed. + * Schedules an action. * * @param action The action to schedule. * @return The builder, for convenience. @@ -141,7 +144,7 @@ public Builder apply(Action action) { } /** - * Schedules an action to be executed repeatedly. + * Schedules an action repeatedly. * * @param action The action to schedule. * @param intervalMs The interval between each repetition in milliseconds. @@ -152,7 +155,7 @@ public Builder repeat(Action action, long intervalMs) { } /** - * Schedules a seek action to be executed. + * Schedules a seek action. * * @param positionMs The seek position. * @return The builder, for convenience. @@ -162,7 +165,7 @@ public Builder seek(long positionMs) { } /** - * Schedules a seek action to be executed. + * Schedules a seek action. * * @param windowIndex The window to seek to. * @param positionMs The seek position. @@ -173,7 +176,7 @@ public Builder seek(int windowIndex, long positionMs) { } /** - * Schedules a seek action to be executed and waits until playback resumes after the seek. + * Schedules a seek action and waits until playback resumes after the seek. * * @param positionMs The seek position. * @return The builder, for convenience. @@ -194,7 +197,7 @@ public Builder waitForSeekProcessed() { } /** - * Schedules a playback parameters setting action to be executed. + * Schedules a playback parameters setting action. * * @param playbackParameters The playback parameters to set. * @return The builder, for convenience. @@ -205,7 +208,7 @@ public Builder setPlaybackParameters(PlaybackParameters playbackParameters) { } /** - * Schedules a stop action to be executed. + * Schedules a stop action. * * @return The builder, for convenience. */ @@ -214,7 +217,7 @@ public Builder stop() { } /** - * Schedules a stop action to be executed. + * Schedules a stop action. * * @param reset Whether the player should be reset. * @return The builder, for convenience. @@ -224,7 +227,7 @@ public Builder stop(boolean reset) { } /** - * Schedules a play action to be executed. + * Schedules a play action. * * @return The builder, for convenience. */ @@ -233,8 +236,8 @@ public Builder play() { } /** - * Schedules a play action to be executed, waits until the player reaches the specified - * position, and pauses the player again. + * Schedules a play action, waits until the player reaches the specified position, and pauses + * the player again. * * @param windowIndex The window index at which the player should be paused again. * @param positionMs The position in that window at which the player should be paused again. @@ -245,8 +248,8 @@ public Builder playUntilPosition(int windowIndex, long positionMs) { } /** - * Schedules a play action to be executed, waits until the player reaches the start of the - * specified window, and pauses the player again. + * Schedules a play action, waits until the player reaches the start of the specified window, + * and pauses the player again. * * @param windowIndex The window index at which the player should be paused again. * @return The builder, for convenience. @@ -256,7 +259,7 @@ public Builder playUntilStartOfWindow(int windowIndex) { } /** - * Schedules a pause action to be executed. + * Schedules a pause action. * * @return The builder, for convenience. */ @@ -265,7 +268,7 @@ public Builder pause() { } /** - * Schedules a renderer enable action to be executed. + * Schedules a renderer enable action. * * @return The builder, for convenience. */ @@ -274,7 +277,7 @@ public Builder enableRenderer(int index) { } /** - * Schedules a renderer disable action to be executed. + * Schedules a renderer disable action. * * @return The builder, for convenience. */ @@ -283,7 +286,7 @@ public Builder disableRenderer(int index) { } /** - * Schedules a clear video surface action to be executed. + * Schedules a clear video surface action. * * @return The builder, for convenience. */ @@ -292,7 +295,7 @@ public Builder clearVideoSurface() { } /** - * Schedules a set video surface action to be executed. + * Schedules a set video surface action. * * @return The builder, for convenience. */ @@ -301,7 +304,16 @@ public Builder setVideoSurface() { } /** - * Schedules a new source preparation action to be executed. + * Schedules application of audio attributes. + * + * @return The builder, for convenience. + */ + public Builder setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus) { + return apply(new SetAudioAttributes(tag, audioAttributes, handleAudioFocus)); + } + + /** + * Schedules a new source preparation action. * * @return The builder, for convenience. */ @@ -310,18 +322,18 @@ public Builder prepareSource(MediaSource mediaSource) { } /** - * Schedules a new source preparation action to be executed. - * @see com.google.android.exoplayer2.ExoPlayer#prepare(MediaSource, boolean, boolean). + * Schedules a new source preparation action. * + * @see com.google.android.exoplayer2.ExoPlayer#prepare(MediaSource, boolean, boolean) * @return The builder, for convenience. */ - public Builder prepareSource(MediaSource mediaSource, boolean resetPosition, - boolean resetState) { + public Builder prepareSource( + MediaSource mediaSource, boolean resetPosition, boolean resetState) { return apply(new PrepareSource(tag, mediaSource, resetPosition, resetState)); } /** - * Schedules a repeat mode setting action to be executed. + * Schedules a repeat mode setting action. * * @return The builder, for convenience. */ @@ -330,7 +342,7 @@ public Builder setRepeatMode(@Player.RepeatMode int repeatMode) { } /** - * Schedules a shuffle setting action to be executed. + * Schedules a shuffle setting action. * * @return The builder, for convenience. */ @@ -405,6 +417,16 @@ public Builder waitForPositionDiscontinuity() { return apply(new WaitForPositionDiscontinuity(tag)); } + /** + * Schedules a delay until playWhenReady has the specified value. + * + * @param targetPlayWhenReady The target playWhenReady value. + * @return The builder, for convenience. + */ + public Builder waitForPlayWhenReady(boolean targetPlayWhenReady) { + return apply(new WaitForPlayWhenReady(tag, targetPlayWhenReady)); + } + /** * Schedules a delay until the playback state changed to the specified state. * @@ -426,7 +448,7 @@ public Builder waitForIsLoading(boolean targetIsLoading) { } /** - * Schedules a {@link Runnable} to be executed. + * Schedules a {@link Runnable}. * * @return The builder, for convenience. */ @@ -444,6 +466,7 @@ public Builder throwPlaybackException(ExoPlaybackException exception) { return apply(new ThrowPlaybackException(tag, exception)); } + /** Builds the schedule. */ public ActionSchedule build() { CallbackAction callbackAction = new CallbackAction(tag); apply(callbackAction); @@ -504,9 +527,7 @@ public final void run() { } } - /** - * Wraps an {@link Action}, allowing a delay and a next {@link Action} to be specified. - */ + /** Wraps an {@link Action}, allowing a delay and a next {@link Action} to be specified. */ /* package */ static final class ActionNode implements Runnable { private final Action action; @@ -550,8 +571,8 @@ public void setNext(ActionNode next) { } /** - * Schedules {@link #action} to be executed after {@link #delayMs}. The {@link #next} node will - * be scheduled immediately after {@link #action} is executed. + * Schedules {@link #action} after {@link #delayMs}. The {@link #next} node will be scheduled + * immediately after {@link #action} is executed. * * @param player The player to which actions should be applied. * @param trackSelector The track selector to which actions should be applied. @@ -613,7 +634,7 @@ protected void doActionImpl( */ private static final class CallbackAction extends Action { - private @Nullable Callback callback; + @Nullable private Callback callback; public CallbackAction(String tag) { super(tag, "FinishedCallback"); diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java similarity index 97% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java index 00c9e60bd51..4ea4c0844eb 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java @@ -148,14 +148,11 @@ public static void assertDataCached(Cache cache, DataSpec dataSpec, byte[] expec */ public static void assertReadData(DataSource dataSource, DataSpec dataSpec, byte[] expected) throws IOException { - DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec); byte[] bytes = null; - try { + try (DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec)) { bytes = Util.toByteArray(inputStream); } catch (IOException e) { // Ignore - } finally { - inputStream.close(); } assertThat(bytes).isEqualTo(expected); } diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/DefaultRenderersFactoryAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DefaultRenderersFactoryAsserts.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/DefaultRenderersFactoryAsserts.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/DefaultRenderersFactoryAsserts.java diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java index 63a761a5cf4..9678e03b405 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java @@ -17,11 +17,12 @@ import static com.google.common.truth.Truth.assertThat; -import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import com.google.android.exoplayer2.util.Util; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; /** Helper class to simulate main/UI thread in tests. */ @@ -90,7 +91,7 @@ public void runTestOnMainThread(int timeoutMs, final TestRunnable runnable) { Util.sneakyThrow(e); } } else { - ConditionVariable finishedCondition = new ConditionVariable(); + CountDownLatch finishedLatch = new CountDownLatch(1); AtomicReference thrown = new AtomicReference<>(); handler.post( () -> { @@ -99,9 +100,13 @@ public void runTestOnMainThread(int timeoutMs, final TestRunnable runnable) { } catch (Throwable t) { thrown.set(t); } - finishedCondition.open(); + finishedLatch.countDown(); }); - assertThat(finishedCondition.block(timeoutMs)).isTrue(); + try { + assertThat(finishedLatch.await(timeoutMs, TimeUnit.MILLISECONDS)).isTrue(); + } catch (InterruptedException e) { + Util.sneakyThrow(e); + } if (thrown.get() != null) { Util.sneakyThrow(thrown.get()); } 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 74c0d4bb439..5f01d7724b2 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 @@ -22,13 +22,10 @@ import android.os.SystemClock; import android.view.Surface; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.DefaultAudioSink; @@ -37,7 +34,6 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.testutil.HostActivity.HostedTest; -import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.util.Clock; @@ -134,8 +130,7 @@ public final void onStart(HostActivity host, Surface surface) { // Build the player. trackSelector = buildTrackSelector(host); String userAgent = "ExoPlayerPlaybackTests"; - DrmSessionManager drmSessionManager = buildDrmSessionManager(userAgent); - player = buildExoPlayer(host, surface, trackSelector, drmSessionManager); + player = buildExoPlayer(host, surface, trackSelector); player.setPlayWhenReady(true); player.addAnalyticsListener(this); player.addAnalyticsListener(new EventLogger(trackSelector, tag)); @@ -145,7 +140,8 @@ public final void onStart(HostActivity host, Surface surface) { pendingSchedule.start(player, trackSelector, surface, actionHandler, /* callback= */ null); pendingSchedule = null; } - player.prepare(buildSource(host, Util.getUserAgent(host, userAgent))); + DrmSessionManager drmSessionManager = buildDrmSessionManager(userAgent); + player.prepare(buildSource(host, Util.getUserAgent(host, userAgent), drmSessionManager)); } @Override @@ -183,7 +179,7 @@ public final void onFinished() { @Override public final void onPlayerStateChanged( - EventTime eventTime, boolean playWhenReady, int playbackState) { + EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) { Log.d(tag, "state [" + playWhenReady + ", " + playbackState + "]"); playerWasPrepared |= playbackState != Player.STATE_IDLE; if (playbackState == Player.STATE_ENDED @@ -234,31 +230,28 @@ private boolean stopTest() { protected DrmSessionManager buildDrmSessionManager(String userAgent) { // Do nothing. Interested subclasses may override. - return null; + return DrmSessionManager.getDummyDrmSessionManager(); } protected DefaultTrackSelector buildTrackSelector(HostActivity host) { - return new DefaultTrackSelector(new AdaptiveTrackSelection.Factory()); + return new DefaultTrackSelector(host); } protected SimpleExoPlayer buildExoPlayer( - HostActivity host, - Surface surface, - MappingTrackSelector trackSelector, - DrmSessionManager drmSessionManager) { - RenderersFactory renderersFactory = - new DefaultRenderersFactory( - host, - DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF, - /* allowedVideoJoiningTimeMs= */ 0); + HostActivity host, Surface surface, MappingTrackSelector trackSelector) { + DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(host); + renderersFactory.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF); + renderersFactory.setAllowedVideoJoiningTimeMs(/* allowedVideoJoiningTimeMs= */ 0); SimpleExoPlayer player = - ExoPlayerFactory.newSimpleInstance( - host, renderersFactory, trackSelector, new DefaultLoadControl(), drmSessionManager); + new SimpleExoPlayer.Builder(host, renderersFactory) + .setTrackSelector(trackSelector) + .build(); player.setVideoSurface(surface); return player; } - protected abstract MediaSource buildSource(HostActivity host, String userAgent); + protected abstract MediaSource buildSource( + HostActivity host, String userAgent, DrmSessionManager drmSessionManager); protected void onPlayerErrorInternal(ExoPlaybackException error) { // Do nothing. Interested subclasses may override. diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 2f91c1926cb..d64a44ac046 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -36,7 +36,6 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.util.Clock; @@ -87,9 +86,9 @@ public static final class Builder { /** * Sets a {@link Timeline} to be used by a {@link FakeMediaSource} in the test runner. The - * default value is a seekable, non-dynamic {@link FakeTimeline} with a duration of - * {@link FakeTimeline.TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US}. Setting the - * timeline is not allowed after a call to {@link #setMediaSource(MediaSource)}. + * default value is a seekable, non-dynamic {@link FakeTimeline} with a duration of {@link + * FakeTimeline.TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US}. Setting the timeline is + * not allowed after a call to {@link #setMediaSource(MediaSource)}. * * @param timeline A {@link Timeline} to be used by a {@link FakeMediaSource} in the test * runner. @@ -103,8 +102,8 @@ public Builder setTimeline(Timeline timeline) { /** * Sets a manifest to be used by a {@link FakeMediaSource} in the test runner. The default value - * is null. Setting the manifest is not allowed after a call to - * {@link #setMediaSource(MediaSource)}. + * is null. Setting the manifest is not allowed after a call to {@link + * #setMediaSource(MediaSource)}. * * @param manifest A manifest to be used by a {@link FakeMediaSource} in the test runner. * @return This builder. @@ -116,11 +115,10 @@ public Builder setManifest(Object manifest) { } /** - * Sets a {@link MediaSource} to be used by the test runner. The default value is a - * {@link FakeMediaSource} with the timeline and manifest provided by - * {@link #setTimeline(Timeline)} and {@link #setManifest(Object)}. Setting the media source is - * not allowed after calls to {@link #setTimeline(Timeline)} and/or - * {@link #setManifest(Object)}. + * Sets a {@link MediaSource} to be used by the test runner. The default value is a {@link + * FakeMediaSource} with the timeline and manifest provided by {@link #setTimeline(Timeline)} + * and {@link #setManifest(Object)}. Setting the media source is not allowed after calls to + * {@link #setTimeline(Timeline)} and/or {@link #setManifest(Object)}. * * @param mediaSource A {@link MediaSource} to be used by the test runner. * @return This builder. @@ -170,10 +168,10 @@ public Builder setBandwidthMeter(BandwidthMeter bandwidthMeter) { /** * Sets a list of {@link Format}s to be used by a {@link FakeMediaSource} to create media - * periods and for setting up a {@link FakeRenderer}. The default value is a single - * {@link #VIDEO_FORMAT}. Note that this parameter doesn't have any influence if both a media - * source with {@link #setMediaSource(MediaSource)} and renderers with - * {@link #setRenderers(Renderer...)} or {@link #setRenderersFactory(RenderersFactory)} are set. + * periods and for setting up a {@link FakeRenderer}. The default value is a single {@link + * #VIDEO_FORMAT}. Note that this parameter doesn't have any influence if both a media source + * with {@link #setMediaSource(MediaSource)} and renderers with {@link + * #setRenderers(Renderer...)} or {@link #setRenderersFactory(RenderersFactory)} are set. * * @param supportedFormats A list of supported {@link Format}s. * @return This builder. @@ -284,7 +282,7 @@ public ExoPlayerTestRunner build(Context context) { supportedFormats = new Format[] {VIDEO_FORMAT}; } if (trackSelector == null) { - trackSelector = new DefaultTrackSelector(); + trackSelector = new DefaultTrackSelector(context); } if (bandwidthMeter == null) { bandwidthMeter = new DefaultBandwidthMeter.Builder(context).build(); @@ -309,9 +307,9 @@ public ExoPlayerTestRunner build(Context context) { } if (mediaSource == null) { if (timeline == null) { - timeline = new FakeTimeline(1); + timeline = new FakeTimeline(/* windowCount= */ 1, manifest); } - mediaSource = new FakeMediaSource(timeline, manifest, supportedFormats); + mediaSource = new FakeMediaSource(timeline, supportedFormats); } if (expectedPlayerEndedCount == null) { expectedPlayerEndedCount = 1; @@ -338,16 +336,15 @@ public ExoPlayerTestRunner build(Context context) { private final DefaultTrackSelector trackSelector; private final LoadControl loadControl; private final BandwidthMeter bandwidthMeter; - private final @Nullable ActionSchedule actionSchedule; - private final @Nullable Player.EventListener eventListener; - private final @Nullable AnalyticsListener analyticsListener; + @Nullable private final ActionSchedule actionSchedule; + @Nullable private final Player.EventListener eventListener; + @Nullable private final AnalyticsListener analyticsListener; private final HandlerThread playerThread; private final HandlerWrapper handler; private final CountDownLatch endedCountDownLatch; private final CountDownLatch actionScheduleFinishedCountDownLatch; private final ArrayList timelines; - private final ArrayList manifests; private final ArrayList timelineChangeReasons; private final ArrayList periodIndices; private final ArrayList discontinuityReasons; @@ -380,7 +377,6 @@ private ExoPlayerTestRunner( this.eventListener = eventListener; this.analyticsListener = analyticsListener; this.timelines = new ArrayList<>(); - this.manifests = new ArrayList<>(); this.timelineChangeReasons = new ArrayList<>(); this.periodIndices = new ArrayList<>(); this.discontinuityReasons = new ArrayList<>(); @@ -395,18 +391,35 @@ private ExoPlayerTestRunner( /** * Starts the test runner on its own thread. This will trigger the creation of the player, the - * listener registration, the start of the action schedule, and the preparation of the player - * with the provided media source. + * listener registration, the start of the action schedule, the initial set of media items and the + * preparation of the player. * * @return This test runner. */ public ExoPlayerTestRunner start() { + return start(/* doPrepare= */ true); + } + + /** + * Starts the test runner on its own thread. This will trigger the creation of the player, the + * listener registration, the start of the action schedule and the initial set of media items. + * + * @param doPrepare Whether the player should be prepared. + * @return This test runner. + */ + public ExoPlayerTestRunner start(boolean doPrepare) { handler.post( () -> { try { player = - new TestSimpleExoPlayer( - context, renderersFactory, trackSelector, loadControl, bandwidthMeter, clock); + new SimpleExoPlayer.Builder(context, renderersFactory) + .setTrackSelector(trackSelector) + .setLoadControl(loadControl) + .setBandwidthMeter(bandwidthMeter) + .setAnalyticsCollector(new AnalyticsCollector(clock)) + .setClock(clock) + .setLooper(Looper.myLooper()) + .build(); player.addListener(ExoPlayerTestRunner.this); if (eventListener != null) { player.addListener(eventListener); @@ -469,9 +482,8 @@ public ExoPlayerTestRunner blockUntilActionScheduleFinished(long timeoutMs) // Assertions called on the test thread after test finished. /** - * Asserts that the timelines reported by - * {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)} are equal to the provided - * timelines. + * Asserts that the timelines reported by {@link Player.EventListener#onTimelineChanged(Timeline, + * int)} are equal to the provided timelines. * * @param timelines A list of expected {@link Timeline}s. */ @@ -479,30 +491,19 @@ public void assertTimelinesEqual(Timeline... timelines) { assertThat(this.timelines).containsExactlyElementsIn(Arrays.asList(timelines)).inOrder(); } - /** - * Asserts that the manifests reported by - * {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)} are equal to the provided - * manifest. - * - * @param manifests A list of expected manifests. - */ - public void assertManifestsEqual(Object... manifests) { - assertThat(this.manifests).containsExactlyElementsIn(Arrays.asList(manifests)).inOrder(); - } - /** * Asserts that the timeline change reasons reported by {@link - * Player.EventListener#onTimelineChanged(Timeline, Object, int)} are equal to the provided - * timeline change reasons. + * Player.EventListener#onTimelineChanged(Timeline, int)} are equal to the provided timeline + * change reasons. */ public void assertTimelineChangeReasonsEqual(Integer... reasons) { assertThat(timelineChangeReasons).containsExactlyElementsIn(Arrays.asList(reasons)).inOrder(); } /** - * Asserts that the last track group array reported by - * {@link Player.EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)} is equal to - * the provided track group array. + * Asserts that the last track group array reported by {@link + * Player.EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)} is equal to the + * provided track group array. * * @param trackGroupArray The expected {@link TrackGroupArray}. */ @@ -573,10 +574,8 @@ private void handleException(Exception exception) { // Player.EventListener @Override - public void onTimelineChanged( - Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { + public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { timelines.add(timeline); - manifests.add(manifest); timelineChangeReasons.add(reason); if (reason == Player.TIMELINE_CHANGE_REASON_PREPARED) { periodIndices.add(player.getCurrentPeriodIndex()); @@ -589,7 +588,7 @@ public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray tra } @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { playerWasPrepared |= playbackState != Player.STATE_IDLE; if (playbackState == Player.STATE_ENDED || (playbackState == Player.STATE_IDLE && playerWasPrepared)) { @@ -620,27 +619,4 @@ public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { public void onActionScheduleFinished() { actionScheduleFinishedCountDownLatch.countDown(); } - - /** SimpleExoPlayer implementation using a custom Clock. */ - private static final class TestSimpleExoPlayer extends SimpleExoPlayer { - - public TestSimpleExoPlayer( - Context context, - RenderersFactory renderersFactory, - TrackSelector trackSelector, - LoadControl loadControl, - BandwidthMeter bandwidthMeter, - Clock clock) { - super( - context, - renderersFactory, - trackSelector, - loadControl, - /* drmSessionManager= */ null, - bandwidthMeter, - new AnalyticsCollector.Factory(), - clock, - Looper.myLooper()); - } - } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java index a933121bc5e..1ca4f1fb187 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java @@ -45,6 +45,29 @@ public interface ExtractorFactory { private static final String DUMP_EXTENSION = ".dump"; private static final String UNKNOWN_LENGTH_EXTENSION = ".unklen" + DUMP_EXTENSION; + /** + * Asserts that {@link Extractor#sniff(ExtractorInput)} returns the {@code expectedResult} for a + * given {@code input}, retrying repeatedly when {@link SimulatedIOException} is thrown. + * + * @param extractor The extractor to test. + * @param input The extractor input. + * @param expectedResult The expected return value. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + */ + public static void assertSniff( + Extractor extractor, FakeExtractorInput input, boolean expectedResult) + throws IOException, InterruptedException { + while (true) { + try { + assertThat(extractor.sniff(input)).isEqualTo(expectedResult); + return; + } catch (SimulatedIOException e) { + // Ignore. + } + } + } + /** * Asserts that an extractor behaves correctly given valid input data. Can only be used from * Robolectric tests. @@ -164,7 +187,7 @@ private static FakeExtractorOutput assertOutput( .setSimulatePartialReads(simulatePartialReads).build(); if (sniffFirst) { - assertThat(TestUtil.sniffTestData(extractor, input)).isTrue(); + assertSniff(extractor, input, /* expectedResult= */ true); input.resetPeekPosition(); } 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 a9c19fbc8d3..011270d543f 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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.testutil; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.source.CompositeSequenceableLoader; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; @@ -33,14 +34,14 @@ /** * Fake {@link MediaPeriod} that provides tracks from the given {@link TrackGroupArray}. Selecting a - * track will give the player a {@link ChunkSampleStream}. + * track will give the player a {@link ChunkSampleStream}. */ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod implements SequenceableLoader.Callback> { private final Allocator allocator; private final FakeChunkSource.Factory chunkSourceFactory; - private final @Nullable TransferListener transferListener; + @Nullable private final TransferListener transferListener; private final long durationUs; private Callback callback; @@ -138,6 +139,11 @@ public boolean continueLoading(long positionUs) { return sequenceableLoader.continueLoading(positionUs); } + @Override + public boolean isLoading() { + return sequenceableLoader.isLoading(); + } + @Override protected SampleStream createSampleStream(TrackSelection trackSelection) { FakeChunkSource chunkSource = @@ -150,6 +156,7 @@ protected SampleStream createSampleStream(TrackSelection trackSelection) { /* callback= */ this, allocator, /* positionUs= */ 0, + /* drmSessionManager= */ DrmSessionManager.getDummyDrmSessionManager(), new DefaultLoadErrorHandlingPolicy(/* minimumLoadableRetryCount= */ 3), eventDispatcher); } @@ -159,7 +166,8 @@ public void onContinueLoadingRequested(ChunkSampleStream source callback.onContinueLoadingRequested(this); } - @SuppressWarnings("unchecked") + // We won't assign the array to a variable that erases the generic type, and then write into it. + @SuppressWarnings({"unchecked", "rawtypes"}) private static ChunkSampleStream[] newSampleStreamArray(int length) { return new ChunkSampleStream[length]; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java index 5a158a36598..0d97b7a20ff 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java @@ -34,10 +34,9 @@ public class FakeAdaptiveMediaSource extends FakeMediaSource { public FakeAdaptiveMediaSource( Timeline timeline, - Object manifest, TrackGroupArray trackGroupArray, FakeChunkSource.Factory chunkSourceFactory) { - super(timeline, manifest, trackGroupArray); + super(timeline, trackGroupArray); this.chunkSourceFactory = chunkSourceFactory; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java index 77ae19f0834..286ef15b159 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java @@ -79,11 +79,11 @@ public static final class FakeData { */ public static final class Segment { - public @Nullable final IOException exception; - public @Nullable final byte[] data; + @Nullable public final IOException exception; + @Nullable public final byte[] data; public final int length; public final long byteOffset; - public @Nullable final Runnable action; + @Nullable public final Runnable action; public boolean exceptionThrown; public boolean exceptionCleared; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java index 1a127eeab5e..443ffdb12c8 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java @@ -95,7 +95,8 @@ public void reset() { * @param position The position to set. */ public void setPosition(int position) { - assertThat(0 <= position && position <= data.length).isTrue(); + assertThat(0 <= position).isTrue(); + assertThat(position <= data.length).isTrue(); readPosition = position; peekPosition = position; } diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunkIterator.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunkIterator.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunkIterator.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunkIterator.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java index d524d381fa7..bcc96ef47e1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java @@ -217,6 +217,11 @@ public boolean continueLoading(long positionUs) { return false; } + @Override + public boolean isLoading() { + return false; + } + protected SampleStream createSampleStream(TrackSelection selection) { return new FakeSampleStream( selection.getSelectedFormat(), eventDispatcher, /* shouldOutputSample= */ true); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index b89acae6c8b..8e5ba230ac2 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -56,11 +56,10 @@ public class FakeMediaSource extends BaseMediaSource { private final ArrayList createdMediaPeriods; protected Timeline timeline; - private Object manifest; private boolean preparedSource; private boolean releasedSource; private Handler sourceInfoRefreshHandler; - private @Nullable TransferListener transferListener; + @Nullable private TransferListener transferListener; /** * Creates a {@link FakeMediaSource}. This media source creates {@link FakeMediaPeriod}s with a @@ -68,8 +67,8 @@ public class FakeMediaSource extends BaseMediaSource { * null to prevent an immediate source info refresh message when preparing the media source. It * can be manually set later using {@link #setNewSourceInfo(Timeline, Object)}. */ - public FakeMediaSource(@Nullable Timeline timeline, Object manifest, Format... formats) { - this(timeline, manifest, buildTrackGroupArray(formats)); + public FakeMediaSource(@Nullable Timeline timeline, Format... formats) { + this(timeline, buildTrackGroupArray(formats)); } /** @@ -78,10 +77,8 @@ public FakeMediaSource(@Nullable Timeline timeline, Object manifest, Format... f * immediate source info refresh message when preparing the media source. It can be manually set * later using {@link #setNewSourceInfo(Timeline, Object)}. */ - public FakeMediaSource(@Nullable Timeline timeline, Object manifest, - TrackGroupArray trackGroupArray) { + public FakeMediaSource(@Nullable Timeline timeline, TrackGroupArray trackGroupArray) { this.timeline = timeline; - this.manifest = manifest; this.activeMediaPeriods = new ArrayList<>(); this.createdMediaPeriods = new ArrayList<>(); this.trackGroupArray = trackGroupArray; @@ -137,7 +134,7 @@ public void releasePeriod(MediaPeriod mediaPeriod) { } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { assertThat(preparedSource).isTrue(); assertThat(releasedSource).isFalse(); assertThat(activeMediaPeriods.isEmpty()).isTrue(); @@ -158,12 +155,10 @@ public synchronized void setNewSourceInfo(final Timeline newTimeline, final Obje assertThat(releasedSource).isFalse(); assertThat(preparedSource).isTrue(); timeline = newTimeline; - manifest = newManifest; finishSourcePreparation(); }); } else { timeline = newTimeline; - manifest = newManifest; } } @@ -212,7 +207,7 @@ protected FakeMediaPeriod createFakeMediaPeriod( } private void finishSourcePreparation() { - refreshSourceInfo(timeline, manifest); + refreshSourceInfo(timeline); if (!timeline.isEmpty()) { MediaLoadData mediaLoadData = new MediaLoadData( diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java index 0d65d7fcc7d..987a9e33c15 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; @@ -45,7 +46,6 @@ public class FakeRenderer extends BaseRenderer { private final List expectedFormats; private final DecoderInputBuffer buffer; - private final FormatHolder formatHolder; private long playbackPositionUs; private long lastSamplePositionUs; @@ -60,7 +60,6 @@ public FakeRenderer(Format... expectedFormats) { : MimeTypes.getTrackType(expectedFormats[0].sampleMimeType)); this.expectedFormats = Collections.unmodifiableList(Arrays.asList(expectedFormats)); buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); - formatHolder = new FormatHolder(); lastSamplePositionUs = Long.MIN_VALUE; } @@ -79,7 +78,7 @@ public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackEx } playbackPositionUs = positionUs; while (lastSamplePositionUs < positionUs + SOURCE_READAHEAD_US) { - formatHolder.format = null; + FormatHolder formatHolder = getFormatHolder(); buffer.clear(); int result = readSource(formatHolder, buffer, false); if (result == C.RESULT_FORMAT_READ) { @@ -112,9 +111,11 @@ public boolean isEnded() { } @Override + @Capabilities public int supportsFormat(Format format) throws ExoPlaybackException { return getTrackType() == MimeTypes.getTrackType(format.sampleMimeType) - ? (FORMAT_HANDLED | ADAPTIVE_SEAMLESS) : FORMAT_UNSUPPORTED_TYPE; + ? RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED) + : RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } /** Called when the renderer reads a new format. */ 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 ba604cb0874..8b05b270463 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 @@ -31,7 +31,7 @@ public final class FakeSampleStream implements SampleStream { private final Format format; - private final @Nullable EventDispatcher eventDispatcher; + @Nullable private final EventDispatcher eventDispatcher; private final byte[] sampleData; private boolean notifiedDownstreamFormat; diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index 56438a51efc..401fcf80340 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -112,6 +112,7 @@ public TimelineWindowDefinition( private static final long AD_DURATION_US = 10 * C.MICROS_PER_SECOND; private final TimelineWindowDefinition[] windowDefinitions; + private final Object[] manifests; private final int[] periodOffsets; /** @@ -140,9 +141,10 @@ public static AdPlaybackState createAdPlaybackState(int adsPerAdGroup, long... a * with a duration of {@link TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US} each. * * @param windowCount The number of windows. + * @param manifests The manifests of the windows. */ - public FakeTimeline(int windowCount) { - this(createDefaultWindowDefinitions(windowCount)); + public FakeTimeline(int windowCount, Object... manifests) { + this(manifests, createDefaultWindowDefinitions(windowCount)); } /** @@ -151,6 +153,18 @@ public FakeTimeline(int windowCount) { * @param windowDefinitions A list of {@link TimelineWindowDefinition}s. */ public FakeTimeline(TimelineWindowDefinition... windowDefinitions) { + this(new Object[0], windowDefinitions); + } + + /** + * Creates a fake timeline with the given window definitions. + * + * @param windowDefinitions A list of {@link TimelineWindowDefinition}s. + */ + public FakeTimeline(Object[] manifests, TimelineWindowDefinition... windowDefinitions) { + this.manifests = new Object[windowDefinitions.length]; + System.arraycopy( + manifests, 0, this.manifests, 0, Math.min(this.manifests.length, manifests.length)); this.windowDefinitions = windowDefinitions; periodOffsets = new int[windowDefinitions.length + 1]; periodOffsets[0] = 0; @@ -165,16 +179,17 @@ public int getWindowCount() { } @Override - public Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex]; - Object tag = setTag ? windowDefinition.id : null; return window.set( - tag, + /* uid= */ windowDefinition.id, + /* tag= */ windowDefinition.id, + manifests[windowIndex], /* presentationStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET, windowDefinition.isSeekable, windowDefinition.isDynamic, + /* isLive= */ windowDefinition.isDynamic, /* defaultPositionUs= */ 0, windowDefinition.durationUs, periodOffsets[windowIndex], diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java similarity index 89% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java index 681f1668372..42fc40e72dd 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java @@ -26,6 +26,8 @@ import com.google.android.exoplayer2.source.MediaPeriod.Callback; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.trackselection.BaseTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.util.ConditionVariable; @@ -176,8 +178,8 @@ private static boolean containsFormats(TrackGroup trackGroup, Format[] formats) for (int i = 0; i < trackGroup.length; i++) { allFormats.add(trackGroup.getFormat(i)); } - for (int i = 0; i < formats.length; i++) { - if (!allFormats.remove(formats[i])) { + for (Format format : formats) { + if (!allFormats.remove(format)) { return false; } } @@ -189,22 +191,21 @@ private static TrackGroupArray getTrackGroups(MediaPeriod mediaPeriod) { DummyMainThread dummyMainThread = new DummyMainThread(); ConditionVariable preparedCondition = new ConditionVariable(); dummyMainThread.runOnMainThread( - () -> { - mediaPeriod.prepare( - new Callback() { - @Override - public void onPrepared(MediaPeriod mediaPeriod) { - trackGroupArray.set(mediaPeriod.getTrackGroups()); - preparedCondition.open(); - } - - @Override - public void onContinueLoadingRequested(MediaPeriod source) { - // Ignore. - } - }, - /* positionUs= */ 0); - }); + () -> + mediaPeriod.prepare( + new Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + trackGroupArray.set(mediaPeriod.getTrackGroups()); + preparedCondition.open(); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + // Ignore. + } + }, + /* positionUs= */ 0)); try { preparedCondition.block(); } catch (InterruptedException e) { @@ -235,5 +236,15 @@ public int getSelectionReason() { public Object getSelectionData() { return null; } + + @Override + public void updateSelectedTrack( + long playbackPositionUs, + long bufferedDurationUs, + long availableDurationUs, + List queue, + MediaChunkIterator[] mediaChunkIterators) { + // Do nothing. + } } } diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java similarity index 93% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 0873dbd1455..8eaa1df9c6e 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -22,12 +22,13 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; @@ -88,8 +89,8 @@ public MediaSourceTestRunner(MediaSource mediaSource, Allocator allocator) { * @param runnable The {@link Runnable} to run. */ public void runOnPlaybackThread(final Runnable runnable) { - final Throwable[] throwable = new Throwable[1]; - final ConditionVariable finishedCondition = new ConditionVariable(); + Throwable[] throwable = new Throwable[1]; + CountDownLatch finishedLatch = new CountDownLatch(1); playbackHandler.post( () -> { try { @@ -97,10 +98,14 @@ public void runOnPlaybackThread(final Runnable runnable) { } catch (Throwable e) { throwable[0] = e; } finally { - finishedCondition.open(); + finishedLatch.countDown(); } }); - assertThat(finishedCondition.block(TIMEOUT_MS)).isTrue(); + try { + assertThat(finishedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); + } catch (InterruptedException e) { + Util.sneakyThrow(e); + } if (throwable[0] != null) { Util.sneakyThrow(throwable[0]); } @@ -168,14 +173,14 @@ public MediaPeriod createPeriod(final MediaPeriodId periodId, long startPosition */ public CountDownLatch preparePeriod(final MediaPeriod mediaPeriod, final long positionUs) { final ConditionVariable prepareCalled = new ConditionVariable(); - final CountDownLatch preparedCountDown = new CountDownLatch(1); + final CountDownLatch preparedLatch = new CountDownLatch(1); runOnPlaybackThread( () -> { mediaPeriod.prepare( new MediaPeriod.Callback() { @Override public void onPrepared(MediaPeriod mediaPeriod1) { - preparedCountDown.countDown(); + preparedLatch.countDown(); } @Override @@ -187,7 +192,7 @@ public void onContinueLoadingRequested(MediaPeriod source) { prepareCalled.open(); }); prepareCalled.block(); - return preparedCountDown; + return preparedLatch; } /** @@ -199,10 +204,7 @@ public void releasePeriod(final MediaPeriod mediaPeriod) { runOnPlaybackThread(() -> mediaSource.releasePeriod(mediaPeriod)); } - /** - * Calls {@link MediaSource#releaseSource(MediaSource.SourceInfoRefreshListener)} on the playback - * thread. - */ + /** Calls {@link MediaSource#releaseSource(MediaSourceCaller)} on the playback thread. */ public void releaseSource() { runOnPlaybackThread(() -> mediaSource.releaseSource(mediaSourceListener)); } @@ -269,8 +271,8 @@ private void assertPrepareAndReleasePeriod(MediaPeriodId mediaPeriodId) throws InterruptedException { MediaPeriod mediaPeriod = createPeriod(mediaPeriodId); assertThat(lastCreatedMediaPeriod.getAndSet(/* newValue= */ null)).isEqualTo(mediaPeriodId); - CountDownLatch preparedCondition = preparePeriod(mediaPeriod, 0); - assertThat(preparedCondition.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); + CountDownLatch preparedLatch = preparePeriod(mediaPeriod, 0); + assertThat(preparedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); // MediaSource is supposed to support multiple calls to createPeriod without an intervening call // to releasePeriod. MediaPeriodId secondMediaPeriodId = @@ -282,8 +284,8 @@ private void assertPrepareAndReleasePeriod(MediaPeriodId mediaPeriodId) MediaPeriod secondMediaPeriod = createPeriod(secondMediaPeriodId); assertThat(lastCreatedMediaPeriod.getAndSet(/* newValue= */ null)) .isEqualTo(secondMediaPeriodId); - CountDownLatch secondPreparedCondition = preparePeriod(secondMediaPeriod, 0); - assertThat(secondPreparedCondition.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); + CountDownLatch secondPreparedLatch = preparePeriod(secondMediaPeriod, 0); + assertThat(secondPreparedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); // Release the periods. releasePeriod(mediaPeriod); assertThat(lastReleasedMediaPeriod.getAndSet(/* newValue= */ null)).isEqualTo(mediaPeriodId); @@ -339,13 +341,12 @@ public void release() { playbackThread.quit(); } - private class MediaSourceListener - implements MediaSource.SourceInfoRefreshListener, MediaSourceEventListener { + private class MediaSourceListener implements MediaSourceCaller, MediaSourceEventListener { - // SourceInfoRefreshListener methods. + // MediaSourceCaller methods. @Override - public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) { + public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { Assertions.checkState(Looper.myLooper() == playbackThread.getLooper()); timelines.addLast(timeline); } diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java similarity index 93% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 51ac5a986d1..18eaec2cd7a 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -75,6 +75,7 @@ public void removeListener(Player.EventListener listener) { } @Override + @State public int getPlaybackState() { throw new UnsupportedOperationException(); } @@ -180,20 +181,6 @@ public PlayerMessage createMessage(PlayerMessage.Target target) { throw new UnsupportedOperationException(); } - @Override - @Deprecated - @SuppressWarnings("deprecation") - public void sendMessages(ExoPlayerMessage... messages) { - throw new UnsupportedOperationException(); - } - - @Override - @Deprecated - @SuppressWarnings("deprecation") - public void blockingSendMessages(ExoPlayerMessage... messages) { - throw new UnsupportedOperationException(); - } - @Override public int getRendererCount() { throw new UnsupportedOperationException(); @@ -214,11 +201,6 @@ public TrackSelectionArray getCurrentTrackSelections() { throw new UnsupportedOperationException(); } - @Override - public Object getCurrentManifest() { - throw new UnsupportedOperationException(); - } - @Override public Timeline getCurrentTimeline() { throw new UnsupportedOperationException(); diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java similarity index 87% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java index 4c334992b50..c42071d0a2c 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java @@ -18,7 +18,6 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; -import android.os.ConditionVariable; import com.google.android.exoplayer2.offline.Download; import com.google.android.exoplayer2.offline.Download.State; import com.google.android.exoplayer2.offline.DownloadManager; @@ -30,31 +29,31 @@ /** A {@link DownloadManager.Listener} for testing. */ public final class TestDownloadManagerListener implements DownloadManager.Listener { - private static final int TIMEOUT = 1000; - private static final int INITIALIZATION_TIMEOUT = 10000; + private static final int TIMEOUT_MS = 1000; + private static final int INITIALIZATION_TIMEOUT_MS = 10_000; private static final int STATE_REMOVED = -1; private final DownloadManager downloadManager; private final DummyMainThread dummyMainThread; private final HashMap> downloadStates; - private final ConditionVariable initializedCondition; - private final int timeout; + private final CountDownLatch initializedCondition; + private final int timeoutMs; private CountDownLatch downloadFinishedCondition; @Download.FailureReason private int failureReason; public TestDownloadManagerListener( DownloadManager downloadManager, DummyMainThread dummyMainThread) { - this(downloadManager, dummyMainThread, TIMEOUT); + this(downloadManager, dummyMainThread, TIMEOUT_MS); } public TestDownloadManagerListener( - DownloadManager downloadManager, DummyMainThread dummyMainThread, int timeout) { + DownloadManager downloadManager, DummyMainThread dummyMainThread, int timeoutMs) { this.downloadManager = downloadManager; this.dummyMainThread = dummyMainThread; - this.timeout = timeout; + this.timeoutMs = timeoutMs; downloadStates = new HashMap<>(); - initializedCondition = new ConditionVariable(); + initializedCondition = new CountDownLatch(1); downloadManager.addListener(this); } @@ -64,12 +63,13 @@ public Integer pollStateChange(String taskId, long timeoutMs) throws Interrupted @Override public void onInitialized(DownloadManager downloadManager) { - initializedCondition.open(); + initializedCondition.countDown(); } - public void waitUntilInitialized() { + public void waitUntilInitialized() throws InterruptedException { if (!downloadManager.isInitialized()) { - assertThat(initializedCondition.block(INITIALIZATION_TIMEOUT)).isTrue(); + assertThat(initializedCondition.await(INITIALIZATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) + .isTrue(); } } @@ -115,7 +115,7 @@ public void blockUntilTasksComplete() throws InterruptedException { downloadFinishedCondition.countDown(); } }); - assertThat(downloadFinishedCondition.await(timeout, TimeUnit.MILLISECONDS)).isTrue(); + assertThat(downloadFinishedCondition.await(timeoutMs, TimeUnit.MILLISECONDS)).isTrue(); } private ArrayBlockingQueue getStateQueue(String taskId) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index facfa0d7e4b..66ea480cc39 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -33,7 +33,6 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; @@ -50,17 +49,14 @@ public class TestUtil { private TestUtil() {} - public static boolean sniffTestData(Extractor extractor, FakeExtractorInput input) - throws IOException, InterruptedException { - while (true) { - try { - return extractor.sniff(input); - } catch (SimulatedIOException e) { - // Ignore. - } - } - } - + /** + * Given an open {@link DataSource}, repeatedly calls {@link DataSource#read(byte[], int, int)} + * until {@link C#RESULT_END_OF_INPUT} is returned. + * + * @param dataSource The source from which to read. + * @return The concatenation of all read data. + * @throws IOException If an error occurs reading from the source. + */ public static byte[] readToEnd(DataSource dataSource) throws IOException { byte[] data = new byte[1024]; int position = 0; @@ -77,6 +73,14 @@ public static byte[] readToEnd(DataSource dataSource) throws IOException { return Arrays.copyOf(data, position); } + /** + * Given an open {@link DataSource}, repeatedly calls {@link DataSource#read(byte[], int, int)} + * until exactly {@code length} bytes have been read. + * + * @param dataSource The source from which to read. + * @return The read data. + * @throws IOException If an error occurs reading from the source. + */ public static byte[] readExactly(DataSource dataSource, int length) throws IOException { byte[] data = new byte[length]; int position = 0; @@ -91,22 +95,49 @@ public static byte[] readExactly(DataSource dataSource, int length) throws IOExc return data; } + /** + * Equivalent to {@code buildTestData(length, length)}. + * + * @param length The length of the array. + * @return The generated array. + */ public static byte[] buildTestData(int length) { return buildTestData(length, length); } + /** + * Generates an array of random bytes with the specified length. + * + * @param length The length of the array. + * @param seed A seed for an internally created {@link Random source of randomness}. + * @return The generated array. + */ public static byte[] buildTestData(int length, int seed) { return buildTestData(length, new Random(seed)); } + /** + * Generates an array of random bytes with the specified length. + * + * @param length The length of the array. + * @param random A source of randomness. + * @return The generated array. + */ public static byte[] buildTestData(int length, Random random) { byte[] source = new byte[length]; random.nextBytes(source); return source; } - public static String buildTestString(int maxLength, Random random) { - int length = random.nextInt(maxLength); + /** + * Generates a random string with the specified maximum length. + * + * @param maximumLength The maximum length of the string. + * @param random A source of randomness. + * @return The generated string. + */ + public static String buildTestString(int maximumLength, Random random) { + int length = random.nextInt(maximumLength); StringBuilder builder = new StringBuilder(length); for (int i = 0; i < length; i++) { builder.append((char) random.nextInt()); @@ -129,6 +160,12 @@ public static byte[] createByteArray(int... intArray) { return byteArray; } + /** + * Concatenates the provided byte arrays. + * + * @param byteArrays The byte arrays to concatenate. + * @return The concatenated result. + */ public static byte[] joinByteArrays(byte[]... byteArrays) { int length = 0; for (byte[] byteArray : byteArrays) { @@ -143,24 +180,28 @@ public static byte[] joinByteArrays(byte[]... byteArrays) { return joined; } + /** Returns the bytes of an asset file. */ public static byte[] getByteArray(Context context, String fileName) throws IOException { return Util.toByteArray(getInputStream(context, fileName)); } + /** Returns an {@link InputStream} for reading from an asset file. */ public static InputStream getInputStream(Context context, String fileName) throws IOException { return context.getResources().getAssets().open(fileName); } + /** Returns a {@link String} read from an asset file. */ public static String getString(Context context, String fileName) throws IOException { return Util.fromUtf8Bytes(getByteArray(context, fileName)); } - public static Bitmap readBitmapFromFile(Context context, String fileName) throws IOException { + /** Returns a {@link Bitmap} read from an asset file. */ + public static Bitmap getBitmap(Context context, String fileName) throws IOException { return BitmapFactory.decodeStream(getInputStream(context, fileName)); } - public static DatabaseProvider getTestDatabaseProvider() { - // Provides an in-memory database. + /** Returns a {@link DatabaseProvider} that provides an in-memory database. */ + public static DatabaseProvider getInMemoryDatabaseProvider() { return new DefaultDatabaseProvider( new SQLiteOpenHelper( /* context= */ null, /* name= */ null, /* factory= */ null, /* version= */ 1) { diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java similarity index 98% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java index 1e3c9c61d9f..f3ec47a88b5 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java @@ -52,7 +52,7 @@ public static void assertWindowTags(Timeline timeline, Object... expectedWindowT Window window = new Window(); assertThat(timeline.getWindowCount()).isEqualTo(expectedWindowTags.length); for (int i = 0; i < timeline.getWindowCount(); i++) { - timeline.getWindow(i, window, true); + timeline.getWindow(i, window); if (expectedWindowTags[i] != null) { assertThat(window.tag).isEqualTo(expectedWindowTags[i]); } @@ -63,7 +63,7 @@ public static void assertWindowTags(Timeline timeline, Object... expectedWindowT public static void assertWindowIsDynamic(Timeline timeline, boolean... windowIsDynamic) { Window window = new Window(); for (int i = 0; i < timeline.getWindowCount(); i++) { - timeline.getWindow(i, window, true); + timeline.getWindow(i, window); assertThat(window.isDynamic).isEqualTo(windowIsDynamic[i]); } } @@ -129,7 +129,7 @@ public static void assertPeriodCounts(Timeline timeline, int... expectedPeriodCo Window window = new Window(); Period period = new Period(); for (int i = 0; i < windowCount; i++) { - timeline.getWindow(i, window, true); + timeline.getWindow(i, window); assertThat(window.firstPeriodIndex).isEqualTo(accumulatedPeriodCounts[i]); assertThat(window.lastPeriodIndex).isEqualTo(accumulatedPeriodCounts[i + 1] - 1); } diff --git a/testutils/src/test/AndroidManifest.xml b/testutils/src/test/AndroidManifest.xml index e30ea1c3ca4..edb8bcafde5 100644 --- a/testutils/src/test/AndroidManifest.xml +++ b/testutils/src/test/AndroidManifest.xml @@ -14,4 +14,6 @@ limitations under the License. --> - + + + diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSetTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSetTest.java index 714c77f8d35..c0351967bf4 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSetTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSetTest.java @@ -90,7 +90,7 @@ public void testAdaptiveDataSetTrailingSmallChunk() { for (int i = 0; i < dataSet.getChunkCount() - 1; i++) { assertThat(dataSet.getChunkDuration(i)).isEqualTo(chunkDuration); } - assertThat(dataSet.getChunkDuration(3)).isEqualTo(1 * C.MICROS_PER_SECOND); + assertThat(dataSet.getChunkDuration(3)).isEqualTo(C.MICROS_PER_SECOND); assertChunkData(dataSet, chunkDuration); } @@ -101,7 +101,7 @@ public void testAdaptiveDataSetChunkSizeDistribution() { new FakeAdaptiveDataSet( TRACK_GROUP, 100000 * C.MICROS_PER_SECOND, - 1 * C.MICROS_PER_SECOND, + C.MICROS_PER_SECOND, expectedStdDev, new Random(0)); for (int i = 0; i < TEST_FORMATS.length; i++) { diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java index 65b0efa72ad..c82980d7a4f 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java @@ -26,11 +26,11 @@ import java.util.concurrent.TimeUnit; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; /** Unit test for {@link FakeClock}. */ @RunWith(AndroidJUnit4.class) -@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +@LooperMode(LooperMode.Mode.PAUSED) public final class FakeClockTest { private static final long TIMEOUT_MS = 10000; diff --git a/testutils_robolectric/build.gradle b/testutils_robolectric/build.gradle deleted file mode 100644 index 758d22b5d91..00000000000 --- a/testutils_robolectric/build.gradle +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (C) 2018 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -apply from: '../constants.gradle' -apply plugin: 'com.android.library' - -android { - compileSdkVersion project.ext.compileSdkVersion - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - defaultConfig { - minSdkVersion project.ext.minSdkVersion - targetSdkVersion project.ext.targetSdkVersion - } - - lintOptions { - // Robolectric depends on BouncyCastle, which depends on javax.naming, - // which is not part of Android. - disable 'InvalidPackage' - } - - testOptions.unitTests.includeAndroidResources = true -} - -dependencies { - api 'androidx.test:core:' + androidXTestVersion - api 'org.robolectric:robolectric:' + robolectricVersion - api project(modulePrefix + 'testutils') - implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.1.0' -} diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java deleted file mode 100644 index ad1fa6bb296..00000000000 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.testutil; - -import static org.robolectric.Shadows.shadowOf; -import static org.robolectric.util.ReflectionHelpers.callInstanceMethod; - -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.MessageQueue; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.util.Util; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.concurrent.PriorityBlockingQueue; -import java.util.concurrent.atomic.AtomicLong; -import org.robolectric.annotation.Implementation; -import org.robolectric.annotation.Implements; -import org.robolectric.shadows.ShadowLooper; -import org.robolectric.shadows.ShadowMessageQueue; - -/** Collection of shadow classes used to run tests with Robolectric which require Loopers. */ -public final class RobolectricUtil { - - private static final AtomicLong sequenceNumberGenerator = new AtomicLong(0); - private static final int ANY_MESSAGE = Integer.MIN_VALUE; - - private RobolectricUtil() {} - - /** - * A custom implementation of Robolectric's ShadowLooper which runs all scheduled messages in the - * loop method of the looper. Also ensures to correctly emulate the message order of the real - * message loop and to avoid blocking caused by Robolectric's default implementation. - * - *

Only works in conjunction with {@link CustomMessageQueue}. Note that the test's {@code - * SystemClock} is not advanced automatically. - */ - @Implements(Looper.class) - public static final class CustomLooper extends ShadowLooper { - - private final PriorityBlockingQueue pendingMessages; - private final CopyOnWriteArraySet removedMessages; - - public CustomLooper() { - pendingMessages = new PriorityBlockingQueue<>(); - removedMessages = new CopyOnWriteArraySet<>(); - } - - @Implementation - public static void loop() { - Looper looper = Looper.myLooper(); - if (shadowOf(looper) instanceof CustomLooper) { - ((CustomLooper) shadowOf(looper)).doLoop(); - } - } - - @Implementation - @Override - public void quitUnchecked() { - super.quitUnchecked(); - // Insert message at the front of the queue to quit loop as soon as possible. - addPendingMessage(/* message= */ null, /* when= */ Long.MIN_VALUE); - } - - private void addPendingMessage(@Nullable Message message, long when) { - pendingMessages.put(new PendingMessage(message, when)); - } - - private void removeMessages(Handler handler, int what, Object object) { - RemovedMessage newRemovedMessage = new RemovedMessage(handler, what, object); - removedMessages.add(newRemovedMessage); - for (RemovedMessage removedMessage : removedMessages) { - if (removedMessage != newRemovedMessage - && removedMessage.handler == handler - && removedMessage.what == what - && removedMessage.object == object) { - removedMessages.remove(removedMessage); - } - } - } - - private void doLoop() { - boolean wasInterrupted = false; - while (true) { - try { - PendingMessage pendingMessage = pendingMessages.take(); - if (pendingMessage.message == null) { - // Null message is signal to end message loop. - return; - } - // Call through to real {@code Message.markInUse()} and {@code Message.recycle()} to - // ensure message recycling works. This is also done in Robolectric's own implementation - // of the message queue. - callInstanceMethod(pendingMessage.message, "markInUse"); - Handler target = pendingMessage.message.getTarget(); - if (target != null) { - boolean isRemoved = false; - for (RemovedMessage removedMessage : removedMessages) { - if (removedMessage.handler == target - && (removedMessage.what == ANY_MESSAGE - || removedMessage.what == pendingMessage.message.what) - && (removedMessage.object == null - || removedMessage.object == pendingMessage.message.obj) - && pendingMessage.sequenceNumber < removedMessage.sequenceNumber) { - isRemoved = true; - } - } - if (!isRemoved) { - try { - if (wasInterrupted) { - wasInterrupted = false; - // Restore the interrupt status flag, so long-running messages will exit early. - Thread.currentThread().interrupt(); - } - target.dispatchMessage(pendingMessage.message); - } catch (Throwable t) { - // Interrupt the main thread to terminate the test. Robolectric's HandlerThread will - // print the rethrown error to standard output. - Looper.getMainLooper().getThread().interrupt(); - throw t; - } - } - } - if (Util.SDK_INT >= 21) { - callInstanceMethod(pendingMessage.message, "recycleUnchecked"); - } else { - callInstanceMethod(pendingMessage.message, "recycle"); - } - } catch (InterruptedException e) { - wasInterrupted = true; - } - } - } - } - - /** - * Custom implementation of Robolectric's ShadowMessageQueue which is needed to let {@link - * CustomLooper} work as intended. - */ - @Implements(MessageQueue.class) - public static final class CustomMessageQueue extends ShadowMessageQueue { - - private final Thread looperThread; - - public CustomMessageQueue() { - looperThread = Thread.currentThread(); - } - - @Implementation - @Override - public boolean enqueueMessage(Message msg, long when) { - Looper looper = ShadowLooper.getLooperForThread(looperThread); - if (shadowOf(looper) instanceof CustomLooper - && shadowOf(looper) != shadowOf(Looper.getMainLooper())) { - ((CustomLooper) shadowOf(looper)).addPendingMessage(msg, when); - } else { - super.enqueueMessage(msg, when); - } - return true; - } - - @Implementation - public void removeMessages(Handler handler, int what, Object object) { - Looper looper = ShadowLooper.getLooperForThread(looperThread); - if (shadowOf(looper) instanceof CustomLooper - && shadowOf(looper) != shadowOf(Looper.getMainLooper())) { - ((CustomLooper) shadowOf(looper)).removeMessages(handler, what, object); - } - } - - @Implementation - public void removeCallbacksAndMessages(Handler handler, Object object) { - Looper looper = ShadowLooper.getLooperForThread(looperThread); - if (shadowOf(looper) instanceof CustomLooper - && shadowOf(looper) != shadowOf(Looper.getMainLooper())) { - ((CustomLooper) shadowOf(looper)).removeMessages(handler, ANY_MESSAGE, object); - } - } - } - - private static final class PendingMessage implements Comparable { - - public final @Nullable Message message; - public final long when; - public final long sequenceNumber; - - public PendingMessage(@Nullable Message message, long when) { - this.message = message; - this.when = when; - sequenceNumber = sequenceNumberGenerator.getAndIncrement(); - } - - @Override - public int compareTo(@NonNull PendingMessage other) { - int res = Util.compareLong(this.when, other.when); - if (res == 0 && this != other) { - res = Util.compareLong(this.sequenceNumber, other.sequenceNumber); - } - return res; - } - } - - private static final class RemovedMessage { - - public final Handler handler; - public final int what; - public final Object object; - public final long sequenceNumber; - - public RemovedMessage(Handler handler, int what, Object object) { - this.handler = handler; - this.what = what; - this.object = object; - this.sequenceNumber = sequenceNumberGenerator.get(); - } - } -}