diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index f2cc754221..690069ffa8 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -10,11 +10,11 @@ Before filing a bug: ----------------------- - Search existing issues, including issues that are closed. - Consult our FAQs, supported devices and supported formats pages. These can be - found at https://google.github.io/ExoPlayer/. + found at https://exoplayer.dev/. - Rule out issues in your own code. A good way to do this is to try and reproduce the issue in the ExoPlayer demo app. Information about the ExoPlayer demo app can be found here: - http://google.github.io/ExoPlayer/demo-application.html. + http://exoplayer.dev/demo-application.html. When reporting a bug: ----------------------- diff --git a/.github/ISSUE_TEMPLATE/content_not_playing.md b/.github/ISSUE_TEMPLATE/content_not_playing.md index a8265c4bd6..f326e7cd46 100644 --- a/.github/ISSUE_TEMPLATE/content_not_playing.md +++ b/.github/ISSUE_TEMPLATE/content_not_playing.md @@ -10,10 +10,10 @@ Before filing a content issue: ------------------------------ - Search existing issues, including issues that are closed. - Consult our supported formats page, which can be found at - https://google.github.io/ExoPlayer/supported-formats.html. + https://exoplayer.dev/supported-formats.html. - Try playing your content in the ExoPlayer demo app. Information about the ExoPlayer demo app can be found here: - http://google.github.io/ExoPlayer/demo-application.html. + http://exoplayer.dev/demo-application.html. When reporting a content issue: ----------------------------- diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 57202f17dd..3ed569862f 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -13,7 +13,7 @@ Before filing a question: - Search existing issues, including issues that are closed. It’s often the quickest way to get an answer! - Consult our FAQs, developer guide and the class reference of ExoPlayer. These - can be found at https://google.github.io/ExoPlayer/. + can be found at https://exoplayer.dev/. When filing a question: ----------------------- diff --git a/.gitignore b/.gitignore index db5a8c4305..4731d5ba99 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,12 @@ local.properties proguard.cfg proguard-project.txt +# Bazel +bazel-bin +bazel-genfiles +bazel-out +bazel-testlogs + # Other .DS_Store cmake-build-debug @@ -66,3 +72,6 @@ extensions/cronet/jniLibs/* extensions/cronet/libs/* !extensions/cronet/libs/README.md +# Cast receiver +cast_receiver_app/external-js +cast_receiver_app/bazel-cast_receiver_app diff --git a/.hgignore b/.hgignore index f7c3656f65..36d3268005 100644 --- a/.hgignore +++ b/.hgignore @@ -44,6 +44,12 @@ local.properties proguard.cfg proguard-project.txt +# Bazel +bazel-bin +bazel-genfiles +bazel-out +bazel-testlogs + # Other .DS_Store cmake-build-debug @@ -69,3 +75,7 @@ extensions/cronet/jniLibs/* !extensions/cronet/jniLibs/README.md extensions/cronet/libs/* !extensions/cronet/libs/README.md + +# Cast receiver +cast_receiver_app/external-js +cast_receiver_app/bazel-cast_receiver_app diff --git a/README.md b/README.md index 03f16bd655..a369b077f4 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ and extend, and can be updated through Play Store application updates. * Follow our [developer blog][] to keep up to date with the latest ExoPlayer developments! -[developer guide]: https://google.github.io/ExoPlayer/guide.html -[class reference]: https://google.github.io/ExoPlayer/doc/reference +[developer guide]: https://exoplayer.dev/guide.html +[class reference]: https://exoplayer.dev/doc/reference [release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md [developer blog]: https://medium.com/google-exoplayer @@ -95,20 +95,6 @@ compileOptions { } ``` -Note that if you want to use Java 8 features in your own code, the following -additional options need to be set: - -```gradle -// For Java compilers: -compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 -} -// For Kotlin compilers: -kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8 -} -``` - ### Locally ### Cloning the repository and depending on the modules locally is required when diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6c87dd02b4..9e69bcc917 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,129 @@ # Release notes # +### 2.10.0 ### + +* Core library: + * Improve decoder re-use between playbacks + ([#2826](https://github.com/google/ExoPlayer/issues/2826)). Read + [this blog post](https://medium.com/google-exoplayer/improved-decoder-reuse-in-exoplayer-ef4c6d99591d) + for more details. + * Rename `ExtractorMediaSource` to `ProgressiveMediaSource`. + * Fix issue where using `ProgressiveMediaSource.Factory` would mean that + `DefaultExtractorsFactory` would be kept by proguard. Custom + `ExtractorsFactory` instances must now be passed via the + `ProgressiveMediaSource.Factory` constructor, and `setExtractorsFactory` is + deprecated. + * Move `PriorityTaskManager` from `DefaultLoadControl` to `SimpleExoPlayer`. + * Add new `ExoPlaybackException` types for remote exceptions and out-of-memory + errors. + * Use full BCP 47 language tags in `Format`. + * Do not retry failed loads whose error is `FileNotFoundException`. + * Fix issue where not resetting the position for a new `MediaSource` in calls + to `ExoPlayer.prepare` causes an `IndexOutOfBoundsException` + ([#5520](https://github.com/google/ExoPlayer/issues/5520)). +* Offline: + * Improve offline support. `DownloadManager` now tracks all offline content, + not just tasks in progress. Read + [this page](https://exoplayer.dev/downloading-media.html) for more details. +* Caching: + * Improve performance of `SimpleCache` + ([#4253](https://github.com/google/ExoPlayer/issues/4253)). + * Cache data with unknown length by default. The previous flag to opt in to + this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been + replaced with an opt out flag + (`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`). +* Extractors: + * MP4/FMP4: Add support for Dolby Vision. + * MP4: Fix issue handling meta atoms in some streams + ([#5698](https://github.com/google/ExoPlayer/issues/5698), + [#5694](https://github.com/google/ExoPlayer/issues/5694)). + * MP3: Add support for SHOUTcast ICY metadata + ([#3735](https://github.com/google/ExoPlayer/issues/3735)). + * MP3: Fix ID3 frame unsychronization + ([#5673](https://github.com/google/ExoPlayer/issues/5673)). + * MP3: Fix playback of badly clipped files + ([#5772](https://github.com/google/ExoPlayer/issues/5772)). + * MPEG-TS: Enable HDMV DTS stream detection only if a flag is set. By default + (i.e. if the flag is not set), the 0x82 elementary stream type is now + treated as an SCTE subtitle track + ([#5330](https://github.com/google/ExoPlayer/issues/5330)). +* Track selection: + * Add options for controlling audio track selections to `DefaultTrackSelector` + ([#3314](https://github.com/google/ExoPlayer/issues/3314)). + * Update `TrackSelection.Factory` interface to support creating all track + selections together. + * Allow to specify a selection reason for a `SelectionOverride`. + * When no text language preference matches, only select forced text tracks + whose language matches the selected audio language. +* UI: + * Update `DefaultTimeBar` based on duration of media and add parameter to set + the minimum update interval to control the smoothness of the updates + ([#5040](https://github.com/google/ExoPlayer/issues/5040)). + * Move creation of dialogs for `TrackSelectionView`s to + `TrackSelectionDialogBuilder` and add option to select multiple overrides. + * Change signature of `PlayerNotificationManager.NotificationListener` to + better fit service requirements. + * Add option to include navigation actions in the compact mode of + notifications created using `PlayerNotificationManager`. + * Fix issues with flickering notifications on KitKat when using + `PlayerNotificationManager` and `DownloadNotificationUtil`. For the latter, + applications should switch to using `DownloadNotificationHelper`. + * Fix accuracy of D-pad seeking in `DefaultTimeBar` + ([#5767](https://github.com/google/ExoPlayer/issues/5767)). +* Audio: + * Allow `AudioProcessor`s to be drained of pending output after they are + reconfigured. + * Fix an issue that caused audio to be truncated at the end of a period + when switching to a new period where gapless playback information was newly + present or newly absent. + * Add support for reading AC-4 streams + ([#5303](https://github.com/google/ExoPlayer/pull/5303)). +* Video: + * Remove `MediaCodecSelector.DEFAULT_WITH_FALLBACK`. Apps should instead + signal that fallback should be used by passing `true` as the + `enableDecoderFallback` parameter when instantiating the video renderer. + * Support video tunneling when the decoder is not listed first for the MIME + type ([#3100](https://github.com/google/ExoPlayer/issues/3100)). + * Query `MediaCodecList.ALL_CODECS` when selecting a tunneling decoder + ([#5547](https://github.com/google/ExoPlayer/issues/5547)). +* DRM: + * Fix black flicker when keys rotate in DRM protected content + ([#3561](https://github.com/google/ExoPlayer/issues/3561)). + * Work around lack of LA_URL attribute in PlayReady key request init data. +* CEA-608: Improved conformance to the specification + ([#3860](https://github.com/google/ExoPlayer/issues/3860)). +* DASH: + * Parse role and accessibility descriptors into `Format.roleFlags`. + * Support multiple CEA-608 channels muxed into FMP4 representations + ([#5656](https://github.com/google/ExoPlayer/issues/5656)). +* HLS: + * Prevent unnecessary reloads of initialization segments. + * Form an adaptive track group out of audio renditions with matching name. + * Support encrypted initialization segments + ([#5441](https://github.com/google/ExoPlayer/issues/5441)). + * Parse `EXT-X-MEDIA` `CHARACTERISTICS` attribute into `Format.roleFlags`. + * Add metadata entry for HLS tracks to expose master playlist information. + * Prevent `IndexOutOfBoundsException` in some live HLS scenarios + ([#5816](https://github.com/google/ExoPlayer/issues/5816)). +* Support for playing spherical videos on Daydream. +* Cast extension: Work around Cast framework returning a limited-size queue + items list ([#4964](https://github.com/google/ExoPlayer/issues/4964)). +* VP9 extension: Remove RGB output mode and libyuv dependency, and switch to + surface YUV output as the default. Remove constructor parameters `scaleToFit` + and `useSurfaceYuvOutput`. +* MediaSession extension: + * Let apps intercept media button events + ([#5179](https://github.com/google/ExoPlayer/issues/5179)). + * Fix issue with `TimelineQueueNavigator` not publishing the queue in shuffled + order when in shuffle mode. + * Allow handling of custom commands via `registerCustomCommandReceiver`. + * Add ability to include an extras `Bundle` when reporting a custom error. +* LoadControl: Set minimum buffer for playbacks with video equal to maximum + buffer ([#2083](https://github.com/google/ExoPlayer/issues/2083)). +* Log warnings when extension native libraries can't be used, to help with + diagnosing playback failures + ([#5788](https://github.com/google/ExoPlayer/issues/5788)). + ### 2.9.6 ### * Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`. diff --git a/build.gradle b/build.gradle index 96eade1aa3..4761a1fbe0 100644 --- a/build.gradle +++ b/build.gradle @@ -17,9 +17,9 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.4' - classpath 'com.novoda:bintray-release:0.8.1' - classpath 'com.google.android.gms:strict-version-matcher-plugin:1.0.3' + classpath 'com.android.tools.build:gradle:3.4.0' + classpath 'com.novoda:bintray-release:0.9' + classpath 'com.google.android.gms:strict-version-matcher-plugin:1.1.0' } // Workaround for the following test coverage issue. Remove when fixed: // https://code.google.com/p/android/issues/detail?id=226070 diff --git a/constants.gradle b/constants.gradle index d7349c3c66..5063c59141 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,26 +13,17 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.9.6' - releaseVersionCode = 2009006 - // Important: ExoPlayer specifies a minSdkVersion of 14 because various - // components provided by the library may be of use on older devices. - // However, please note that the core media playback functionality provided - // by the library requires API level 16 or greater. - minSdkVersion = 14 + releaseVersion = '2.10.0' + releaseVersionCode = 2010000 + minSdkVersion = 16 targetSdkVersion = 28 compileSdkVersion = 28 - buildToolsVersion = '28.0.2' - testSupportLibraryVersion = '0.5' - supportLibraryVersion = '27.1.1' - dexmakerVersion = '1.2' - mockitoVersion = '1.9.5' - junitVersion = '4.12' - truthVersion = '0.39' - robolectricVersion = '3.7.1' + dexmakerVersion = '2.21.0' + mockitoVersion = '2.25.0' + robolectricVersion = '4.2' autoValueVersion = '1.6' checkerframeworkVersion = '2.5.0' - testRunnerVersion = '1.1.0-alpha3' + androidXTestVersion = '1.1.0' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle index 915bc10b7c..03a54947cf 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -16,7 +16,6 @@ apply plugin: 'com.android.application' android { compileSdkVersion project.ext.compileSdkVersion - buildToolsVersion project.ext.buildToolsVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -26,7 +25,7 @@ android { defaultConfig { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode - minSdkVersion 16 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion } @@ -45,8 +44,18 @@ android { } lintOptions { - // The demo app does not have translations. - disable 'MissingTranslation' + // The demo app isn't indexed and doesn't have translations. + disable 'GoogleAppIndexingWarning','MissingTranslation' + } + + flavorDimensions "receiver" + + productFlavors { + defaultCast { + dimension "receiver" + manifestPlaceholders = + [castOptionsProvider: "com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"] + } } } @@ -58,9 +67,10 @@ dependencies { implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'extension-cast') - implementation 'com.android.support:support-v4:' + supportLibraryVersion - implementation 'com.android.support:appcompat-v7:' + supportLibraryVersion - implementation 'com.android.support:recyclerview-v7:' + supportLibraryVersion + 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.recyclerview:recyclerview:1.0.0' } apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/cast/proguard-rules.txt b/demos/cast/proguard-rules.txt index 3221818080..e6bf2dd3bf 100644 --- a/demos/cast/proguard-rules.txt +++ b/demos/cast/proguard-rules.txt @@ -1,6 +1,6 @@ # Proguard rules specific to the Cast demo app. # Accessed via menu.xml --keep class android.support.v7.app.MediaRouteActionProvider { +-keep class androidx.mediarouter.app.MediaRouteActionProvider { *; } diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml index ae16776333..856b0b1235 100644 --- a/demos/cast/src/main/AndroidManifest.xml +++ b/demos/cast/src/main/AndroidManifest.xml @@ -17,13 +17,15 @@ package="com.google.android.exoplayer2.castdemo"> + + + android:value="${castOptionsProvider}" /> mediaQueue; + private final Listener listener; + private final ConcatenatingMediaSource concatenatingMediaSource; + + private boolean castMediaQueueCreationPending; + 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(); + if (currentPlayer == castPlayer && timeline.isEmpty()) { + castMediaQueueCreationPending = true; + } + } + + // 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. + castMediaQueueCreationPending = currentPlayer == castPlayer; + 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 (castMediaQueueCreationPending) { + MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; + for (int i = 0; i < items.length; i++) { + items[i] = buildMediaQueueItem(mediaQueue.get(i)); + } + castMediaQueueCreationPending = false; + castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); + } else { + currentPlayer.seekTo(itemIndex, positionMs); + 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 77f6a6fc1a..9625304252 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 @@ -15,44 +15,37 @@ */ 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.util.MimeTypes; -import com.google.android.gms.cast.MediaInfo; 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. - */ +/** Utility methods and constants for the Cast demo application. */ /* package */ final class DemoUtil { - 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; - - /** - * Represents a media sample. - */ + /** Represents a media sample. */ public static final class Sample { - /** - * The uri from which the media sample is obtained. - */ + /** The uri of the media content. */ public final String uri; - /** - * A descriptive name for the sample. - */ + /** The name of the sample. */ public final String name; - /** - * The mime type of the media sample, as required by {@link MediaInfo#setContentType}. - */ + /** 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}. @@ -60,31 +53,53 @@ import java.util.List; * @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; + static { // App samples. ArrayList samples = new ArrayList<>(); - samples.add(new Sample("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd", - "DASH (clear,MP4,H264)", MIME_TYPE_DASH)); - samples.add(new Sample("https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/" - + "hls/TearsOfSteel.m3u8", "Tears of Steel (HLS)", MIME_TYPE_HLS)); - samples.add(new Sample("https://html5demos.com/assets/dizzy.mp4", "Dizzy (MP4)", - MIME_TYPE_VIDEO_MP4)); - SAMPLES = Collections.unmodifiableList(samples); + // Clear content. + samples.add( + new Sample( + "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd", + "Clear DASH: Tears", + MIME_TYPE_DASH)); + samples.add( + new Sample( + "https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4)); + + SAMPLES = Collections.unmodifiableList(samples); } private DemoUtil() {} - } 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 6589685124..17eeed2da7 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,13 @@ package com.google.android.exoplayer2.castdemo; import android.content.Context; import android.os.Bundle; -import android.support.v4.graphics.ColorUtils; -import android.support.v7.app.AlertDialog; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.RecyclerView.ViewHolder; -import android.support.v7.widget.helper.ItemTouchHelper; +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; @@ -33,21 +33,26 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; +import android.widget.Toast; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.castdemo.DemoUtil.Sample; -import com.google.android.exoplayer2.ext.cast.CastPlayer; +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 {@link CastPlayer}. + * An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's + * Cast extension. */ -public class MainActivity extends AppCompatActivity implements OnClickListener, - PlayerManager.QueuePositionListener { +public class MainActivity extends AppCompatActivity + implements OnClickListener, PlayerManager.Listener { + + private final MediaItem.Builder mediaItemBuilder; private PlayerView localPlayerView; private PlayerControlView castControlView; @@ -56,6 +61,10 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, private MediaQueueListAdapter mediaQueueListAdapter; private CastContext castContext; + public MainActivity() { + mediaItemBuilder = new MediaItem.Builder(); + } + // Activity lifecycle methods. @Override @@ -68,7 +77,7 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, Throwable cause = e.getCause(); while (cause != null) { if (cause instanceof DynamiteModule.LoadingException) { - setContentView(R.layout.cast_context_error_message_layout); + setContentView(R.layout.cast_context_error); return; } cause = cause.getCause(); @@ -109,13 +118,20 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, // There is no Cast context to work with. Do nothing. return; } - playerManager = - PlayerManager.createPlayerManager( - /* queuePositionListener= */ this, - localPlayerView, - castControlView, - /* context= */ this, - castContext); + 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); + } mediaQueueList.setAdapter(mediaQueueListAdapter); } @@ -129,6 +145,7 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount()); mediaQueueList.setAdapter(null); playerManager.release(); + playerManager = null; } // Activity input. @@ -141,12 +158,15 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, @Override public void onClick(View view) { - new AlertDialog.Builder(this).setTitle(R.string.sample_list_dialog_title) - .setView(buildSampleListView()).setPositiveButton(android.R.string.ok, null).create() + new AlertDialog.Builder(this) + .setTitle(R.string.add_samples) + .setView(buildSampleListView()) + .setPositiveButton(android.R.string.ok, null) + .create() .show(); } - // PlayerManager.QueuePositionListener implementation. + // PlayerManager.Listener implementation. @Override public void onQueuePositionChanged(int previousIndex, int newIndex) { @@ -158,6 +178,16 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, } } + @Override + public void onQueueContentsExternallyChanged() { + mediaQueueListAdapter.notifyDataSetChanged(); + } + + @Override + public void onPlayerError() { + Toast.makeText(getApplicationContext(), R.string.player_error_msg, Toast.LENGTH_LONG).show(); + } + // Internal methods. private View buildSampleListView() { @@ -166,7 +196,19 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, sampleList.setAdapter(new SampleListAdapter(this)); sampleList.setOnItemClickListener( (parent, view, position, id) -> { - playerManager.addItem(DemoUtil.SAMPLES.get(position)); + 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()); mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); }); return dialogList; @@ -174,23 +216,6 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, // Internal classes. - private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener { - - public final TextView textView; - - public QueueItemViewHolder(TextView textView) { - super(textView); - this.textView = textView; - textView.setOnClickListener(this); - } - - @Override - public void onClick(View v) { - playerManager.selectQueueItem(getAdapterPosition()); - } - - } - private class MediaQueueListAdapter extends RecyclerView.Adapter { @Override @@ -202,8 +227,9 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, @Override public void onBindViewHolder(QueueItemViewHolder holder, int position) { + holder.item = playerManager.getItem(position); TextView view = holder.textView; - view.setText(playerManager.getItem(position).name); + view.setText(holder.item.title); // TODO: Solve coloring using the theme's ColorStateList. view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(), position == playerManager.getCurrentItemIndex() ? 255 : 100)); @@ -244,8 +270,11 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { int position = viewHolder.getAdapterPosition(); - if (playerManager.removeItem(position)) { + QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder; + if (playerManager.removeItem(queueItemHolder.item)) { mediaQueueListAdapter.notifyItemRemoved(position); + // Update whichever item took its place, in case it became the new selected item. + mediaQueueListAdapter.notifyItemChanged(position); } } @@ -253,8 +282,9 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) { super.clearView(recyclerView, viewHolder); if (draggingFromPosition != C.INDEX_UNSET) { + QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder; // A drag has ended. We reflect the media queue change in the player. - if (!playerManager.moveItem(draggingFromPosition, draggingToPosition)) { + if (!playerManager.moveItem(queueItemHolder.item, draggingToPosition)) { // The move failed. The entire sequence of onMove calls since the drag started needs to be // invalidated. mediaQueueListAdapter.notifyDataSetChanged(); @@ -263,15 +293,30 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, draggingFromPosition = C.INDEX_UNSET; draggingToPosition = C.INDEX_UNSET; } - } - private static final class SampleListAdapter extends ArrayAdapter { + private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener { + + public final TextView textView; + public MediaItem item; + + public QueueItemViewHolder(TextView textView) { + super(textView); + this.textView = textView; + textView.setOnClickListener(this); + } + + @Override + public void onClick(View v) { + playerManager.selectQueueItem(getAdapterPosition()); + } + } + + private static final class SampleListAdapter extends ArrayAdapter { public SampleListAdapter(Context context) { super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); } - } } 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 d188469de8..c9a728b3ff 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 @@ -1,5 +1,5 @@ /* - * 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. @@ -15,402 +15,53 @@ */ package com.google.android.exoplayer2.castdemo; -import android.content.Context; -import android.net.Uri; -import android.support.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.castdemo.DemoUtil.Sample; -import com.google.android.exoplayer2.ext.cast.CastPlayer; -import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.MediaSource; -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; +import com.google.android.exoplayer2.ext.cast.MediaItem; -/** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */ -/* package */ final class PlayerManager - implements EventListener, CastPlayer.SessionAvailabilityListener { +/** Manages the players in the Cast demo app. */ +/* package */ interface PlayerManager { - /** - * Listener for changes in the media queue playback position. - */ - public interface QueuePositionListener { + /** Listener for events. */ + interface Listener { - /** - * Called when the currently played item of the media queue changes. - */ + /** 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 an error occurs in the current player. */ + void onPlayerError(); } - private static final String USER_AGENT = "ExoCastDemoPlayer"; - private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = - new DefaultHttpDataSourceFactory(USER_AGENT); + /** Redirects the given {@code keyEvent} to the active player. */ + boolean dispatchKeyEvent(KeyEvent keyEvent); - private final PlayerView localPlayerView; - private final PlayerControlView castControlView; - private final SimpleExoPlayer exoPlayer; - private final CastPlayer castPlayer; - private final ArrayList mediaQueue; - private final QueuePositionListener queuePositionListener; - private final ConcatenatingMediaSource concatenatingMediaSource; + /** Appends the given {@link MediaItem} to the media queue. */ + void addItem(MediaItem mediaItem); - private boolean castMediaQueueCreationPending; - private int currentItemIndex; - private Player currentPlayer; + /** Returns the number of items in the media queue. */ + int getMediaQueueSize(); + + /** Selects the item at the given position for playback. */ + void selectQueueItem(int position); /** - * @param queuePositionListener A {@link QueuePositionListener} 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}. + * Returns the position of the item currently being played, or {@link C#INDEX_UNSET} if no item is + * being played. */ - public static PlayerManager createPlayerManager( - QueuePositionListener queuePositionListener, - PlayerView localPlayerView, - PlayerControlView castControlView, - Context context, - CastContext castContext) { - PlayerManager playerManager = - new PlayerManager( - queuePositionListener, localPlayerView, castControlView, context, castContext); - playerManager.init(); - return playerManager; - } + int getCurrentItemIndex(); - private PlayerManager( - QueuePositionListener queuePositionListener, - PlayerView localPlayerView, - PlayerControlView castControlView, - Context context, - CastContext castContext) { - this.queuePositionListener = queuePositionListener; - this.localPlayerView = localPlayerView; - this.castControlView = castControlView; - mediaQueue = new ArrayList<>(); - currentItemIndex = C.INDEX_UNSET; - concatenatingMediaSource = new ConcatenatingMediaSource(); + /** Returns the {@link MediaItem} at the given {@code position}. */ + MediaItem getItem(int position); - DefaultTrackSelector trackSelector = new DefaultTrackSelector(); - RenderersFactory renderersFactory = new DefaultRenderersFactory(context); - exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector); - exoPlayer.addListener(this); - localPlayerView.setPlayer(exoPlayer); + /** Moves the item at position {@code from} to position {@code to}. */ + boolean moveItem(MediaItem item, int to); - castPlayer = new CastPlayer(castContext); - castPlayer.addListener(this); - castPlayer.setSessionAvailabilityListener(this); - castControlView.setPlayer(castPlayer); - } - - // 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 sample} to the media queue. - * - * @param sample The {@link Sample} to append. - */ - public void addItem(Sample sample) { - mediaQueue.add(sample); - concatenatingMediaSource.addMediaSource(buildMediaSource(sample)); - if (currentPlayer == castPlayer) { - castPlayer.addItems(buildMediaQueueItem(sample)); - } - } - - /** - * Returns the size of the media queue. - */ - 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. - */ - public Sample getItem(int position) { - return mediaQueue.get(position); - } - - /** - * Removes the item at the given index from the media queue. - * - * @param itemIndex The index of the item to remove. - * @return Whether the removal was successful. - */ - public boolean removeItem(int itemIndex) { - 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 fromIndex The index of the item to move. - * @param toIndex The target index of the item in the queue. - * @return Whether the item move was successful. - */ - public boolean moveItem(int fromIndex, int toIndex) { - // 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; - } - - // Miscellaneous methods. - - /** - * 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(); - } - - // 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(); - if (timeline.isEmpty()) { - castMediaQueueCreationPending = true; - } - } - - // CastPlayer.SessionAvailabilityListener implementation. - - @Override - public void onCastSessionAvailable() { - setCurrentPlayer(castPlayer); - } - - @Override - public void onCastSessionUnavailable() { - setCurrentPlayer(exoPlayer); - } - - // Internal methods. - - private void init() { - setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer); - } - - 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. - castMediaQueueCreationPending = currentPlayer == castPlayer; - 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 (castMediaQueueCreationPending) { - MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; - for (int i = 0; i < items.length; i++) { - items[i] = buildMediaQueueItem(mediaQueue.get(i)); - } - castMediaQueueCreationPending = false; - castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); - } else { - currentPlayer.seekTo(itemIndex, positionMs); - currentPlayer.setPlayWhenReady(playWhenReady); - } - } - - private void maybeSetCurrentItemAndNotify(int currentItemIndex) { - if (this.currentItemIndex != currentItemIndex) { - int oldIndex = this.currentItemIndex; - this.currentItemIndex = currentItemIndex; - queuePositionListener.onQueuePositionChanged(oldIndex, currentItemIndex); - } - } - - private static MediaSource buildMediaSource(DemoUtil.Sample sample) { - Uri uri = Uri.parse(sample.uri); - switch (sample.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 ExtractorMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - default: { - throw new IllegalStateException("Unsupported type: " + sample.mimeType); - } - } - } - - private static MediaQueueItem buildMediaQueueItem(DemoUtil.Sample sample) { - MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); - movieMetadata.putString(MediaMetadata.KEY_TITLE, sample.name); - MediaInfo mediaInfo = new MediaInfo.Builder(sample.uri) - .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED).setContentType(sample.mimeType) - .setMetadata(movieMetadata).build(); - return new MediaQueueItem.Builder(mediaInfo).build(); - } + /** Removes the item at position {@code index}. */ + boolean removeItem(MediaItem item); + /** Releases any acquired resources. */ + void release(); } diff --git a/demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml b/demos/cast/src/main/res/drawable/ic_plus.xml similarity index 59% rename from demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml rename to demos/cast/src/main/res/drawable/ic_plus.xml index 5f3c8961ef..5a5a5154c9 100644 --- a/demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml +++ b/demos/cast/src/main/res/drawable/ic_plus.xml @@ -13,8 +13,12 @@ See the License for the specific language governing permissions and limitations under the License. --> - - + + diff --git a/demos/cast/src/main/res/layout/cast_context_error_message_layout.xml b/demos/cast/src/main/res/layout/cast_context_error.xml similarity index 77% rename from demos/cast/src/main/res/layout/cast_context_error_message_layout.xml rename to demos/cast/src/main/res/layout/cast_context_error.xml index 6d3260de38..0b3fdb63d2 100644 --- a/demos/cast/src/main/res/layout/cast_context_error_message_layout.xml +++ b/demos/cast/src/main/res/layout/cast_context_error.xml @@ -13,17 +13,10 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - - - diff --git a/demos/cast/src/main/res/layout/main_activity.xml b/demos/cast/src/main/res/layout/main_activity.xml index 01e48cdea7..71dbcdcd9c 100644 --- a/demos/cast/src/main/res/layout/main_activity.xml +++ b/demos/cast/src/main/res/layout/main_activity.xml @@ -19,34 +19,42 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:keepScreenOn="true"> + + - + + - + android:layout_margin="16dp" + android:contentDescription="@string/add_samples"/> + + + diff --git a/demos/cast/src/main/res/layout/sample_list.xml b/demos/cast/src/main/res/layout/sample_list.xml index 910db9e058..183c74eb3a 100644 --- a/demos/cast/src/main/res/layout/sample_list.xml +++ b/demos/cast/src/main/res/layout/sample_list.xml @@ -14,7 +14,7 @@ limitations under the License. --> diff --git a/demos/cast/src/main/res/values/strings.xml b/demos/cast/src/main/res/values/strings.xml index 58f5233412..013b50a175 100644 --- a/demos/cast/src/main/res/values/strings.xml +++ b/demos/cast/src/main/res/values/strings.xml @@ -20,8 +20,10 @@ Cast - Add samples + Add samples 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. + diff --git a/demos/ima/build.gradle b/demos/ima/build.gradle index 33cca6ef46..33161b4121 100644 --- a/demos/ima/build.gradle +++ b/demos/ima/build.gradle @@ -16,7 +16,6 @@ apply plugin: 'com.android.application' android { compileSdkVersion project.ext.compileSdkVersion - buildToolsVersion project.ext.buildToolsVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -26,7 +25,7 @@ android { defaultConfig { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode - minSdkVersion 16 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion } @@ -42,8 +41,8 @@ android { } lintOptions { - // The demo app does not have translations. - disable 'MissingTranslation' + // The demo app isn't indexed and doesn't have translations. + disable 'GoogleAppIndexingWarning','MissingTranslation' } } @@ -54,7 +53,7 @@ dependencies { implementation project(modulePrefix + 'library-hls') implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'extension-ima') - implementation 'com.android.support:support-annotations:' + supportLibraryVersion + implementation 'androidx.annotation:annotation:1.0.2' } apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' 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 index fb13073840..05c804c7a8 100644 --- 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 @@ -23,8 +23,8 @@ 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.ExtractorMediaSource; 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; @@ -114,7 +114,7 @@ import com.google.android.exoplayer2.util.Util; case C.TYPE_HLS: return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); case C.TYPE_OTHER: - return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri); + return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri); default: throw new IllegalStateException("Unsupported type: " + type); } diff --git a/demos/main/build.gradle b/demos/main/build.gradle index c516ba297f..7089d4d731 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -16,7 +16,6 @@ apply plugin: 'com.android.application' android { compileSdkVersion project.ext.compileSdkVersion - buildToolsVersion project.ext.buildToolsVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -26,7 +25,7 @@ android { defaultConfig { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode - minSdkVersion 16 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion } @@ -45,8 +44,9 @@ android { } lintOptions { - // The demo app does not have translations. - disable 'MissingTranslation' + // The demo app isn't indexed, doesn't have translations, and has a + // banner for AndroidTV that's only in xhdpi density. + disable 'GoogleAppIndexingWarning','MissingTranslation','IconDensities' } flavorDimensions "extensions" @@ -62,7 +62,10 @@ android { } dependencies { - implementation 'com.android.support:support-annotations:' + supportLibraryVersion + implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.legacy:legacy-support-core-ui:1.0.0' + implementation 'androidx.fragment:fragment:1.0.0' + implementation 'com.google.android.material:material:1.0.0' implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-dash') implementation project(modulePrefix + 'library-hls') diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index e80e37688d..355ba43405 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -15,6 +15,7 @@ --> @@ -33,11 +34,13 @@ android:banner="@drawable/ic_banner" android:largeHeap="true" android:allowBackup="false" - android:name="com.google.android.exoplayer2.demo.DemoApplication"> + android:name="com.google.android.exoplayer2.demo.DemoApplication" + tools:ignore="UnusedAttribute"> + android:label="@string/application_name" + android:theme="@style/Theme.AppCompat"> diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index c2acf3990b..bcb3ef4ad1 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -330,11 +330,11 @@ "samples": [ { "name": "Super speed", - "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism" + "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest" }, { "name": "Super speed (PlayReady)", - "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism", + "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest", "drm_scheme": "playready" } ] 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 ac8be7dc16..6985d42b36 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java @@ -16,6 +16,13 @@ package com.google.android.exoplayer2.demo; import android.app.Application; +import com.google.android.exoplayer2.DefaultRenderersFactory; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.database.DatabaseProvider; +import com.google.android.exoplayer2.database.ExoDatabaseProvider; +import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil; +import com.google.android.exoplayer2.offline.DefaultDownloadIndex; +import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.upstream.DataSource; @@ -28,21 +35,24 @@ import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.SimpleCache; +import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.io.File; +import java.io.IOException; /** * Placeholder application to facilitate overriding Application methods for debugging and testing. */ public class DemoApplication extends Application { + private static final String TAG = "DemoApplication"; private static final String DOWNLOAD_ACTION_FILE = "actions"; private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions"; private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; - private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2; protected String userAgent; + private DatabaseProvider databaseProvider; private File downloadDirectory; private Cache downloadCache; private DownloadManager downloadManager; @@ -71,6 +81,18 @@ public class DemoApplication extends Application { return "withExtensions".equals(BuildConfig.FLAVOR); } + public RenderersFactory buildRenderersFactory(boolean preferExtensionRenderer) { + @DefaultRenderersFactory.ExtensionRendererMode + int extensionRendererMode = + useExtensionRenderers() + ? (preferExtensionRenderer + ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER + : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) + : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF; + return new DefaultRenderersFactory(/* context= */ this) + .setExtensionRendererMode(extensionRendererMode); + } + public DownloadManager getDownloadManager() { initDownloadManager(); return downloadManager; @@ -81,31 +103,51 @@ public class DemoApplication extends Application { return downloadTracker; } + protected synchronized Cache getDownloadCache() { + if (downloadCache == null) { + File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY); + downloadCache = + new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider()); + } + return downloadCache; + } + private synchronized void initDownloadManager() { if (downloadManager == null) { + DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider()); + upgradeActionFile( + DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false); + upgradeActionFile( + DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true); DownloaderConstructorHelper downloaderConstructorHelper = new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory()); downloadManager = new DownloadManager( - downloaderConstructorHelper, - MAX_SIMULTANEOUS_DOWNLOADS, - DownloadManager.DEFAULT_MIN_RETRY_COUNT, - new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE)); + this, downloadIndex, new DefaultDownloaderFactory(downloaderConstructorHelper)); downloadTracker = - new DownloadTracker( - /* context= */ this, - buildDataSourceFactory(), - new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE)); - downloadManager.addListener(downloadTracker); + new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadManager); } } - private synchronized Cache getDownloadCache() { - if (downloadCache == null) { - File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY); - downloadCache = new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor()); + private void upgradeActionFile( + String fileName, DefaultDownloadIndex downloadIndex, boolean addNewDownloadsAsCompleted) { + try { + ActionFileUpgradeUtil.upgradeAndDelete( + new File(getDownloadDirectory(), fileName), + /* downloadIdProvider= */ null, + downloadIndex, + /* deleteOnFailure= */ true, + addNewDownloadsAsCompleted); + } catch (IOException e) { + Log.e(TAG, "Failed to upgrade action file: " + fileName, e); } - return downloadCache; + } + + private DatabaseProvider getDatabaseProvider() { + if (databaseProvider == null) { + databaseProvider = new ExoDatabaseProvider(this); + } + return databaseProvider; } private File getDownloadDirectory() { @@ -118,8 +160,8 @@ public class DemoApplication extends Application { return downloadDirectory; } - private static CacheDataSourceFactory buildReadOnlyCacheDataSource( - DefaultDataSourceFactory upstreamFactory, Cache cache) { + protected static CacheDataSourceFactory buildReadOnlyCacheDataSource( + DataSource.Factory upstreamFactory, Cache cache) { return new CacheDataSourceFactory( cache, upstreamFactory, diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java index 7d1ab16ce4..3886ef5c44 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java @@ -16,13 +16,14 @@ package com.google.android.exoplayer2.demo; import android.app.Notification; +import com.google.android.exoplayer2.offline.Download; import com.google.android.exoplayer2.offline.DownloadManager; -import com.google.android.exoplayer2.offline.DownloadManager.TaskState; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.scheduler.PlatformScheduler; -import com.google.android.exoplayer2.ui.DownloadNotificationUtil; +import com.google.android.exoplayer2.ui.DownloadNotificationHelper; import com.google.android.exoplayer2.util.NotificationUtil; import com.google.android.exoplayer2.util.Util; +import java.util.List; /** A service for downloading media. */ public class DemoDownloadService extends DownloadService { @@ -31,12 +32,23 @@ public class DemoDownloadService extends DownloadService { private static final int JOB_ID = 1; private static final int FOREGROUND_NOTIFICATION_ID = 1; + private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1; + + private DownloadNotificationHelper notificationHelper; + public DemoDownloadService() { super( FOREGROUND_NOTIFICATION_ID, DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL, CHANNEL_ID, R.string.exo_download_notification_channel_name); + nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1; + } + + @Override + public void onCreate() { + super.onCreate(); + notificationHelper = new DownloadNotificationHelper(this, CHANNEL_ID); } @Override @@ -50,40 +62,29 @@ public class DemoDownloadService extends DownloadService { } @Override - protected Notification getForegroundNotification(TaskState[] taskStates) { - return DownloadNotificationUtil.buildProgressNotification( - /* context= */ this, - R.drawable.exo_controls_play, - CHANNEL_ID, - /* contentIntent= */ null, - /* message= */ null, - taskStates); + protected Notification getForegroundNotification(List downloads) { + return notificationHelper.buildProgressNotification( + R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads); } @Override - protected void onTaskStateChanged(TaskState taskState) { - if (taskState.action.isRemoveAction) { + protected void onDownloadChanged(Download download) { + Notification notification; + if (download.state == Download.STATE_COMPLETED) { + notification = + notificationHelper.buildDownloadCompletedNotification( + R.drawable.ic_download_done, + /* contentIntent= */ null, + Util.fromUtf8Bytes(download.request.data)); + } else if (download.state == Download.STATE_FAILED) { + notification = + notificationHelper.buildDownloadFailedNotification( + R.drawable.ic_download_done, + /* contentIntent= */ null, + Util.fromUtf8Bytes(download.request.data)); + } else { return; } - Notification notification = null; - if (taskState.state == TaskState.STATE_COMPLETED) { - notification = - DownloadNotificationUtil.buildDownloadCompletedNotification( - /* context= */ this, - R.drawable.exo_controls_play, - CHANNEL_ID, - /* contentIntent= */ null, - Util.fromUtf8Bytes(taskState.action.data)); - } else if (taskState.state == TaskState.STATE_FAILED) { - notification = - DownloadNotificationUtil.buildDownloadFailedNotification( - /* context= */ this, - R.drawable.exo_controls_play, - CHANNEL_ID, - /* contentIntent= */ null, - Util.fromUtf8Bytes(taskState.action.data)); - } - int notificationId = FOREGROUND_NOTIFICATION_ID + 1 + taskState.taskId; - NotificationUtil.setNotification(this, notificationId, notification); + NotificationUtil.setNotification(this, nextNotificationId++, notification); } } 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 b0619a82fd..f372a47df6 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java @@ -15,54 +15,34 @@ */ package com.google.android.exoplayer2.demo; -import android.app.Activity; -import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.net.Uri; -import android.os.Handler; -import android.os.HandlerThread; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ArrayAdapter; -import android.widget.ListView; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentManager; import android.widget.Toast; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.offline.ActionFile; -import com.google.android.exoplayer2.offline.DownloadAction; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.offline.Download; +import com.google.android.exoplayer2.offline.DownloadCursor; import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.offline.DownloadIndex; import com.google.android.exoplayer2.offline.DownloadManager; -import com.google.android.exoplayer2.offline.DownloadManager.TaskState; +import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.DownloadService; -import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper; import com.google.android.exoplayer2.offline.StreamKey; -import com.google.android.exoplayer2.offline.TrackKey; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.dash.offline.DashDownloadHelper; -import com.google.android.exoplayer2.source.hls.offline.HlsDownloadHelper; -import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadHelper; -import com.google.android.exoplayer2.ui.DefaultTrackNameProvider; -import com.google.android.exoplayer2.ui.TrackNameProvider; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; -import java.io.File; import java.io.IOException; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; -/** - * Tracks media that has been downloaded. - * - *

Tracked downloads are persisted using an {@link ActionFile}, however in a real application - * it's expected that state will be stored directly in the application's media database, so that it - * can be queried efficiently together with other information about the media. - */ -public class DownloadTracker implements DownloadManager.Listener { +/** Tracks media that has been downloaded. */ +public class DownloadTracker { /** Listens for changes in the tracked downloads. */ public interface Listener { @@ -75,28 +55,21 @@ public class DownloadTracker implements DownloadManager.Listener { private final Context context; private final DataSource.Factory dataSourceFactory; - private final TrackNameProvider trackNameProvider; private final CopyOnWriteArraySet listeners; - private final HashMap trackedDownloadStates; - private final ActionFile actionFile; - private final Handler actionFileWriteHandler; + private final HashMap downloads; + private final DownloadIndex downloadIndex; + + @Nullable private StartDownloadDialogHelper startDownloadDialogHelper; public DownloadTracker( - Context context, - DataSource.Factory dataSourceFactory, - File actionFile, - DownloadAction.Deserializer... deserializers) { + Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) { this.context = context.getApplicationContext(); this.dataSourceFactory = dataSourceFactory; - this.actionFile = new ActionFile(actionFile); - trackNameProvider = new DefaultTrackNameProvider(context.getResources()); listeners = new CopyOnWriteArraySet<>(); - trackedDownloadStates = new HashMap<>(); - HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker"); - actionFileWriteThread.start(); - actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper()); - loadTrackedActions( - deserializers.length > 0 ? deserializers : DownloadAction.getDefaultDeserializers()); + downloads = new HashMap<>(); + downloadIndex = downloadManager.getDownloadIndex(); + downloadManager.addListener(new DownloadManagerListener()); + loadDownloads(); } public void addListener(Listener listener) { @@ -108,167 +81,139 @@ public class DownloadTracker implements DownloadManager.Listener { } public boolean isDownloaded(Uri uri) { - return trackedDownloadStates.containsKey(uri); + Download download = downloads.get(uri); + return download != null && download.state != Download.STATE_FAILED; } @SuppressWarnings("unchecked") public List getOfflineStreamKeys(Uri uri) { - if (!trackedDownloadStates.containsKey(uri)) { - return Collections.emptyList(); - } - return trackedDownloadStates.get(uri).getKeys(); + Download download = downloads.get(uri); + return download != null && download.state != Download.STATE_FAILED + ? download.request.streamKeys + : Collections.emptyList(); } - public void toggleDownload(Activity activity, String name, Uri uri, String extension) { - if (isDownloaded(uri)) { - DownloadAction removeAction = - getDownloadHelper(uri, extension).getRemoveAction(Util.getUtf8Bytes(name)); - startServiceWithAction(removeAction); + public void toggleDownload( + FragmentManager fragmentManager, + String name, + Uri uri, + String extension, + RenderersFactory renderersFactory) { + Download download = downloads.get(uri); + if (download != null) { + DownloadService.sendRemoveDownload( + context, DemoDownloadService.class, download.request.id, /* foreground= */ false); } else { - StartDownloadDialogHelper helper = - new StartDownloadDialogHelper(activity, getDownloadHelper(uri, extension), name); - helper.prepare(); - } - } - - // DownloadManager.Listener - - @Override - public void onInitialized(DownloadManager downloadManager) { - // Do nothing. - } - - @Override - public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) { - DownloadAction action = taskState.action; - Uri uri = action.uri; - if ((action.isRemoveAction && taskState.state == TaskState.STATE_COMPLETED) - || (!action.isRemoveAction && taskState.state == TaskState.STATE_FAILED)) { - // A download has been removed, or has failed. Stop tracking it. - if (trackedDownloadStates.remove(uri) != null) { - handleTrackedDownloadStatesChanged(); + if (startDownloadDialogHelper != null) { + startDownloadDialogHelper.release(); } + startDownloadDialogHelper = + new StartDownloadDialogHelper( + fragmentManager, getDownloadHelper(uri, extension, renderersFactory), name); } } - @Override - public void onIdle(DownloadManager downloadManager) { - // Do nothing. - } - - // Internal methods - - private void loadTrackedActions(DownloadAction.Deserializer[] deserializers) { - try { - DownloadAction[] allActions = actionFile.load(deserializers); - for (DownloadAction action : allActions) { - trackedDownloadStates.put(action.uri, action); + private void loadDownloads() { + try (DownloadCursor loadedDownloads = downloadIndex.getDownloads()) { + while (loadedDownloads.moveToNext()) { + Download download = loadedDownloads.getDownload(); + downloads.put(download.request.uri, download); } } catch (IOException e) { - Log.e(TAG, "Failed to load tracked actions", e); + Log.w(TAG, "Failed to query downloads", e); } } - private void handleTrackedDownloadStatesChanged() { - for (Listener listener : listeners) { - listener.onDownloadsChanged(); - } - final DownloadAction[] actions = trackedDownloadStates.values().toArray(new DownloadAction[0]); - actionFileWriteHandler.post( - () -> { - try { - actionFile.store(actions); - } catch (IOException e) { - Log.e(TAG, "Failed to store tracked actions", e); - } - }); - } - - private void startDownload(DownloadAction action) { - if (trackedDownloadStates.containsKey(action.uri)) { - // This content is already being downloaded. Do nothing. - return; - } - trackedDownloadStates.put(action.uri, action); - handleTrackedDownloadStatesChanged(); - startServiceWithAction(action); - } - - private void startServiceWithAction(DownloadAction action) { - DownloadService.startWithAction(context, DemoDownloadService.class, action, false); - } - - private DownloadHelper getDownloadHelper(Uri uri, String extension) { + private DownloadHelper getDownloadHelper( + Uri uri, String extension, RenderersFactory renderersFactory) { int type = Util.inferContentType(uri, extension); switch (type) { case C.TYPE_DASH: - return new DashDownloadHelper(uri, dataSourceFactory); + return DownloadHelper.forDash(uri, dataSourceFactory, renderersFactory); case C.TYPE_SS: - return new SsDownloadHelper(uri, dataSourceFactory); + return DownloadHelper.forSmoothStreaming(uri, dataSourceFactory, renderersFactory); case C.TYPE_HLS: - return new HlsDownloadHelper(uri, dataSourceFactory); + return DownloadHelper.forHls(uri, dataSourceFactory, renderersFactory); case C.TYPE_OTHER: - return new ProgressiveDownloadHelper(uri); + return DownloadHelper.forProgressive(uri); default: throw new IllegalStateException("Unsupported type: " + type); } } - private final class StartDownloadDialogHelper - implements DownloadHelper.Callback, DialogInterface.OnClickListener { + private class DownloadManagerListener implements DownloadManager.Listener { - private final DownloadHelper downloadHelper; - private final String name; - - private final AlertDialog.Builder builder; - private final View dialogView; - private final List trackKeys; - private final ArrayAdapter trackTitles; - private final ListView representationList; - - public StartDownloadDialogHelper( - Activity activity, DownloadHelper downloadHelper, String name) { - this.downloadHelper = downloadHelper; - this.name = name; - builder = - new AlertDialog.Builder(activity) - .setTitle(R.string.exo_download_description) - .setPositiveButton(android.R.string.ok, this) - .setNegativeButton(android.R.string.cancel, null); - - // Inflate with the builder's context to ensure the correct style is used. - LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext()); - dialogView = dialogInflater.inflate(R.layout.start_download_dialog, null); - - trackKeys = new ArrayList<>(); - trackTitles = - new ArrayAdapter<>( - builder.getContext(), android.R.layout.simple_list_item_multiple_choice); - representationList = dialogView.findViewById(R.id.representation_list); - representationList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); - representationList.setAdapter(trackTitles); - } - - public void prepare() { - downloadHelper.prepare(this); + @Override + public void onDownloadChanged(DownloadManager downloadManager, Download download) { + downloads.put(download.request.uri, download); + for (Listener listener : listeners) { + listener.onDownloadsChanged(); + } } + @Override + public void onDownloadRemoved(DownloadManager downloadManager, Download download) { + downloads.remove(download.request.uri); + for (Listener listener : listeners) { + listener.onDownloadsChanged(); + } + } + } + + private final class StartDownloadDialogHelper + implements DownloadHelper.Callback, + DialogInterface.OnClickListener, + DialogInterface.OnDismissListener { + + private final FragmentManager fragmentManager; + private final DownloadHelper downloadHelper; + private final String name; + + private TrackSelectionDialog trackSelectionDialog; + private MappedTrackInfo mappedTrackInfo; + + public StartDownloadDialogHelper( + FragmentManager fragmentManager, DownloadHelper downloadHelper, String name) { + this.fragmentManager = fragmentManager; + this.downloadHelper = downloadHelper; + this.name = name; + downloadHelper.prepare(this); + } + + public void release() { + downloadHelper.release(); + if (trackSelectionDialog != null) { + trackSelectionDialog.dismiss(); + } + } + + // DownloadHelper.Callback implementation. + @Override public void onPrepared(DownloadHelper helper) { - for (int i = 0; i < downloadHelper.getPeriodCount(); i++) { - TrackGroupArray trackGroups = downloadHelper.getTrackGroups(i); - for (int j = 0; j < trackGroups.length; j++) { - TrackGroup trackGroup = trackGroups.get(j); - for (int k = 0; k < trackGroup.length; k++) { - trackKeys.add(new TrackKey(i, j, k)); - trackTitles.add(trackNameProvider.getTrackName(trackGroup.getFormat(k))); - } - } + if (helper.getPeriodCount() == 0) { + Log.d(TAG, "No periods found. Downloading entire stream."); + startDownload(); + downloadHelper.release(); + return; } - if (!trackKeys.isEmpty()) { - builder.setView(dialogView); + mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0); + if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) { + Log.d(TAG, "No dialog content. Downloading entire stream."); + startDownload(); + downloadHelper.release(); + return; } - builder.create().show(); + trackSelectionDialog = + TrackSelectionDialog.createForMappedTrackInfoAndParameters( + /* titleId= */ R.string.exo_download_description, + mappedTrackInfo, + /* initialParameters= */ DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, + /* allowAdaptiveSelections =*/ false, + /* allowMultipleOverrides= */ true, + /* onClickListener= */ this, + /* onDismissListener= */ this); + trackSelectionDialog.show(fragmentManager, /* tag= */ null); } @Override @@ -279,20 +224,51 @@ public class DownloadTracker implements DownloadManager.Listener { Log.e(TAG, "Failed to start download", e); } + // DialogInterface.OnClickListener implementation. + @Override public void onClick(DialogInterface dialog, int which) { - ArrayList selectedTrackKeys = new ArrayList<>(); - for (int i = 0; i < representationList.getChildCount(); i++) { - if (representationList.isItemChecked(i)) { - selectedTrackKeys.add(trackKeys.get(i)); + for (int periodIndex = 0; periodIndex < downloadHelper.getPeriodCount(); periodIndex++) { + downloadHelper.clearTrackSelections(periodIndex); + for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { + if (!trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i)) { + downloadHelper.addTrackSelectionForSingleRenderer( + periodIndex, + /* rendererIndex= */ i, + DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, + trackSelectionDialog.getOverrides(/* rendererIndex= */ i)); + } } } - if (!selectedTrackKeys.isEmpty() || trackKeys.isEmpty()) { - // We have selected keys, or we're dealing with single stream content. - DownloadAction downloadAction = - downloadHelper.getDownloadAction(Util.getUtf8Bytes(name), selectedTrackKeys); - startDownload(downloadAction); + DownloadRequest downloadRequest = buildDownloadRequest(); + if (downloadRequest.streamKeys.isEmpty()) { + // All tracks were deselected in the dialog. Don't start the download. + return; } + startDownload(downloadRequest); + } + + // DialogInterface.OnDismissListener implementation. + + @Override + public void onDismiss(DialogInterface dialogInterface) { + trackSelectionDialog = null; + downloadHelper.release(); + } + + // Internal methods. + + private void startDownload() { + startDownload(buildDownloadRequest()); + } + + private void startDownload(DownloadRequest downloadRequest) { + DownloadService.sendAddDownload( + context, DemoDownloadService.class, downloadRequest, /* foreground= */ false); + } + + private DownloadRequest buildDownloadRequest() { + return downloadHelper.getDownloadRequest(Util.getUtf8Bytes(name)); } } } 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 483ab37369..acb24adebe 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 @@ -15,14 +15,13 @@ */ package com.google.android.exoplayer2.demo; -import android.app.Activity; -import android.app.AlertDialog; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +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; @@ -33,11 +32,11 @@ import android.widget.TextView; import android.widget.Toast; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C.ContentType; -import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerFactory; 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.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; @@ -46,21 +45,17 @@ import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; -import com.google.android.exoplayer2.offline.FilteringManifestParser; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.ExtractorMediaSource; 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.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistParserFactory; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; @@ -70,7 +65,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; 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.TrackSelectionView; import com.google.android.exoplayer2.ui.spherical.SphericalSurfaceView; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.HttpDataSource; @@ -85,7 +79,7 @@ import java.util.List; import java.util.UUID; /** An activity that plays media using {@link SimpleExoPlayer}. */ -public class PlayerActivity extends Activity +public class PlayerActivity extends AppCompatActivity implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener { public static final String DRM_SCHEME_EXTRA = "drm_scheme"; @@ -130,7 +124,9 @@ public class PlayerActivity extends Activity private PlayerView playerView; private LinearLayout debugRootView; + private Button selectTracksButton; private TextView debugTextView; + private boolean isShowingTrackSelectionDialog; private DataSource.Factory dataSourceFactory; private SimpleExoPlayer player; @@ -165,10 +161,10 @@ public class PlayerActivity extends Activity } setContentView(R.layout.player_activity); - View rootView = findViewById(R.id.root); - rootView.setOnClickListener(this); debugRootView = findViewById(R.id.controls_root); debugTextView = findViewById(R.id.debug_text_view); + selectTracksButton = findViewById(R.id.select_tracks_button); + selectTracksButton.setOnClickListener(this); playerView = findViewById(R.id.player_view); playerView.setControllerVisibilityListener(this); @@ -203,6 +199,7 @@ public class PlayerActivity extends Activity @Override public void onNewIntent(Intent intent) { + super.onNewIntent(intent); releasePlayer(); releaseAdsLoader(); clearStartPosition(); @@ -277,6 +274,7 @@ public class PlayerActivity extends Activity @Override public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); updateTrackSelectorParameters(); updateStartPosition(); outState.putParcelable(KEY_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters); @@ -297,23 +295,15 @@ public class PlayerActivity extends Activity @Override public void onClick(View view) { - if (view.getParent() == debugRootView) { - MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); - if (mappedTrackInfo != null) { - CharSequence title = ((Button) view).getText(); - int rendererIndex = (int) view.getTag(); - int rendererType = mappedTrackInfo.getRendererType(rendererIndex); - boolean allowAdaptiveSelections = - rendererType == C.TRACK_TYPE_VIDEO - || (rendererType == C.TRACK_TYPE_AUDIO - && mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO) - == MappedTrackInfo.RENDERER_SUPPORT_NO_TRACKS); - Pair dialogPair = - TrackSelectionView.getDialog(this, title, trackSelector, rendererIndex); - dialogPair.second.setShowDisableOption(true); - dialogPair.second.setAllowAdaptiveSelections(allowAdaptiveSelections); - dialogPair.first.show(); - } + if (view == selectTracksButton + && !isShowingTrackSelectionDialog + && TrackSelectionDialog.willHaveContent(trackSelector)) { + isShowingTrackSelectionDialog = true; + TrackSelectionDialog trackSelectionDialog = + TrackSelectionDialog.createForTrackSelector( + trackSelector, + /* onDismissListener= */ dismissedDialog -> isShowingTrackSelectionDialog = false); + trackSelectionDialog.show(getSupportFragmentManager(), /* tag= */ null); } } @@ -321,7 +311,7 @@ public class PlayerActivity extends Activity @Override public void preparePlayback() { - initializePlayer(); + player.retry(); } // PlaybackControlView.VisibilityListener implementation @@ -413,13 +403,8 @@ public class PlayerActivity extends Activity boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false); - @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode = - ((DemoApplication) getApplication()).useExtensionRenderers() - ? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER - : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) - : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF; - DefaultRenderersFactory renderersFactory = - new DefaultRenderersFactory(this, extensionRendererMode); + RenderersFactory renderersFactory = + ((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders); trackSelector = new DefaultTrackSelector(trackSelectionFactory); trackSelector.setParameters(trackSelectorParameters); @@ -464,7 +449,7 @@ public class PlayerActivity extends Activity player.seekTo(startWindow, startPosition); } player.prepare(mediaSource, !haveStartPosition, false); - updateButtonVisibilities(); + updateButtonVisibility(); } private MediaSource buildMediaSource(Uri uri) { @@ -473,24 +458,22 @@ public class PlayerActivity extends Activity private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { @ContentType int type = Util.inferContentType(uri, overrideExtension); + List offlineStreamKeys = getOfflineStreamKeys(uri); switch (type) { case C.TYPE_DASH: return new DashMediaSource.Factory(dataSourceFactory) - .setManifestParser( - new FilteringManifestParser<>(new DashManifestParser(), getOfflineStreamKeys(uri))) + .setStreamKeys(offlineStreamKeys) .createMediaSource(uri); case C.TYPE_SS: return new SsMediaSource.Factory(dataSourceFactory) - .setManifestParser( - new FilteringManifestParser<>(new SsManifestParser(), getOfflineStreamKeys(uri))) + .setStreamKeys(offlineStreamKeys) .createMediaSource(uri); case C.TYPE_HLS: return new HlsMediaSource.Factory(dataSourceFactory) - .setPlaylistParserFactory( - new DefaultHlsPlaylistParserFactory(getOfflineStreamKeys(uri))) + .setStreamKeys(offlineStreamKeys) .createMediaSource(uri); case C.TYPE_OTHER: - return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri); + return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri); default: { throw new IllegalStateException("Unsupported type: " + type); } @@ -617,41 +600,9 @@ public class PlayerActivity extends Activity // User controls - private void updateButtonVisibilities() { - debugRootView.removeAllViews(); - if (player == null) { - return; - } - - MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); - if (mappedTrackInfo == null) { - return; - } - - for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { - TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(i); - if (trackGroups.length != 0) { - Button button = new Button(this); - int label; - switch (player.getRendererType(i)) { - case C.TRACK_TYPE_AUDIO: - label = R.string.exo_track_selection_title_audio; - break; - case C.TRACK_TYPE_VIDEO: - label = R.string.exo_track_selection_title_video; - break; - case C.TRACK_TYPE_TEXT: - label = R.string.exo_track_selection_title_text; - break; - default: - continue; - } - button.setText(label); - button.setTag(i); - button.setOnClickListener(this); - debugRootView.addView(button); - } - } + private void updateButtonVisibility() { + selectTracksButton.setEnabled( + player != null && TrackSelectionDialog.willHaveContent(trackSelector)); } private void showControls() { @@ -687,16 +638,7 @@ public class PlayerActivity extends Activity if (playbackState == Player.STATE_ENDED) { showControls(); } - updateButtonVisibilities(); - } - - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - if (player.getPlaybackError() != null) { - // The user has performed a seek whilst in the error state. Update the resume position so - // that if the user then retries, playback resumes from the position to which they seeked. - updateStartPosition(); - } + updateButtonVisibility(); } @Override @@ -705,8 +647,7 @@ public class PlayerActivity extends Activity clearStartPosition(); initializePlayer(); } else { - updateStartPosition(); - updateButtonVisibilities(); + updateButtonVisibility(); showControls(); } } @@ -714,7 +655,7 @@ public class PlayerActivity extends Activity @Override @SuppressWarnings("ReferenceEquality") public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - updateButtonVisibilities(); + updateButtonVisibility(); if (trackGroups != lastSeenTrackGroupArray) { MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); if (mappedTrackInfo != null) { 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 6395ea4c24..7245de01c6 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 @@ -15,14 +15,14 @@ */ package com.google.android.exoplayer2.demo; -import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.res.AssetManager; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import android.util.JsonReader; import android.view.Menu; import android.view.MenuInflater; @@ -37,6 +37,7 @@ import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceInputStream; @@ -54,7 +55,7 @@ import java.util.Collections; import java.util.List; /** An activity for selecting from a list of media samples. */ -public class SampleChooserActivity extends Activity +public class SampleChooserActivity extends AppCompatActivity implements DownloadTracker.Listener, OnChildClickListener { private static final String TAG = "SampleChooserActivity"; @@ -177,7 +178,15 @@ public class SampleChooserActivity extends Activity .show(); } else { UriSample uriSample = (UriSample) sample; - downloadTracker.toggleDownload(this, sample.name, uriSample.uri, uriSample.extension); + RenderersFactory renderersFactory = + ((DemoApplication) getApplication()) + .buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem)); + downloadTracker.toggleDownload( + getSupportFragmentManager(), + sample.name, + uriSample.uri, + uriSample.extension, + renderersFactory); } } @@ -350,8 +359,7 @@ public class SampleChooserActivity extends Activity ? null : new DrmInfo(drmScheme, drmLicenseUrl, drmKeyRequestProperties, drmMultiSession); if (playlistSamples != null) { - UriSample[] playlistSamplesArray = playlistSamples.toArray( - new UriSample[playlistSamples.size()]); + UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]); return new PlaylistSample(sampleName, drmInfo, playlistSamplesArray); } else { return new UriSample( 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 new file mode 100644 index 0000000000..a7dd1a0df8 --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java @@ -0,0 +1,368 @@ +/* + * 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 android.app.Dialog; +import android.content.DialogInterface; +import android.content.res.Resources; +import android.os.Bundle; +import androidx.annotation.Nullable; +import com.google.android.material.tabs.TabLayout; +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; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; +import com.google.android.exoplayer2.ui.TrackSelectionView; +import com.google.android.exoplayer2.util.Assertions; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** Dialog to select tracks. */ +public final class TrackSelectionDialog extends DialogFragment { + + private final SparseArray tabFragments; + private final ArrayList tabTrackTypes; + + private int titleId; + private DialogInterface.OnClickListener onClickListener; + private DialogInterface.OnDismissListener onDismissListener; + + /** + * Returns whether a track selection dialog will have content to display if initialized with the + * specified {@link DefaultTrackSelector} in its current state. + */ + public static boolean willHaveContent(DefaultTrackSelector trackSelector) { + MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); + return mappedTrackInfo != null && willHaveContent(mappedTrackInfo); + } + + /** + * Returns whether a track selection dialog will have content to display if initialized with the + * specified {@link MappedTrackInfo}. + */ + public static boolean willHaveContent(MappedTrackInfo mappedTrackInfo) { + for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { + if (showTabForRenderer(mappedTrackInfo, i)) { + return true; + } + } + return false; + } + + /** + * Creates a dialog for a given {@link DefaultTrackSelector}, whose parameters will be + * automatically updated when tracks are selected. + * + * @param trackSelector The {@link DefaultTrackSelector}. + * @param onDismissListener A {@link DialogInterface.OnDismissListener} to call when the dialog is + * dismissed. + */ + public static TrackSelectionDialog createForTrackSelector( + DefaultTrackSelector trackSelector, DialogInterface.OnDismissListener onDismissListener) { + MappedTrackInfo mappedTrackInfo = + Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo()); + TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog(); + DefaultTrackSelector.Parameters parameters = trackSelector.getParameters(); + trackSelectionDialog.init( + /* titleId= */ R.string.track_selection_title, + mappedTrackInfo, + /* initialParameters = */ parameters, + /* allowAdaptiveSelections =*/ true, + /* allowMultipleOverrides= */ false, + /* onClickListener= */ (dialog, which) -> { + DefaultTrackSelector.ParametersBuilder builder = parameters.buildUpon(); + for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { + builder + .clearSelectionOverrides(/* rendererIndex= */ i) + .setRendererDisabled( + /* rendererIndex= */ i, + trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i)); + List overrides = + trackSelectionDialog.getOverrides(/* rendererIndex= */ i); + if (!overrides.isEmpty()) { + builder.setSelectionOverride( + /* rendererIndex= */ i, + mappedTrackInfo.getTrackGroups(/* rendererIndex= */ i), + overrides.get(0)); + } + } + trackSelector.setParameters(builder); + }, + onDismissListener); + return trackSelectionDialog; + } + + /** + * Creates a dialog for given {@link MappedTrackInfo} and {@link DefaultTrackSelector.Parameters}. + * + * @param titleId The resource id of the dialog title. + * @param mappedTrackInfo The {@link MappedTrackInfo} to display. + * @param initialParameters The {@link DefaultTrackSelector.Parameters} describing the initial + * track selection. + * @param allowAdaptiveSelections Whether adaptive selections (consisting of more than one track) + * can be made. + * @param allowMultipleOverrides Whether tracks from multiple track groups can be selected. + * @param onClickListener {@link DialogInterface.OnClickListener} called when tracks are selected. + * @param onDismissListener {@link DialogInterface.OnDismissListener} called when the dialog is + * dismissed. + */ + public static TrackSelectionDialog createForMappedTrackInfoAndParameters( + int titleId, + MappedTrackInfo mappedTrackInfo, + DefaultTrackSelector.Parameters initialParameters, + boolean allowAdaptiveSelections, + boolean allowMultipleOverrides, + DialogInterface.OnClickListener onClickListener, + DialogInterface.OnDismissListener onDismissListener) { + TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog(); + trackSelectionDialog.init( + titleId, + mappedTrackInfo, + initialParameters, + allowAdaptiveSelections, + allowMultipleOverrides, + onClickListener, + onDismissListener); + return trackSelectionDialog; + } + + public TrackSelectionDialog() { + tabFragments = new SparseArray<>(); + tabTrackTypes = new ArrayList<>(); + // Retain instance across activity re-creation to prevent losing access to init data. + setRetainInstance(true); + } + + private void init( + int titleId, + MappedTrackInfo mappedTrackInfo, + DefaultTrackSelector.Parameters initialParameters, + boolean allowAdaptiveSelections, + boolean allowMultipleOverrides, + DialogInterface.OnClickListener onClickListener, + DialogInterface.OnDismissListener onDismissListener) { + this.titleId = titleId; + this.onClickListener = onClickListener; + this.onDismissListener = onDismissListener; + for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { + if (showTabForRenderer(mappedTrackInfo, i)) { + int trackType = mappedTrackInfo.getRendererType(/* rendererIndex= */ i); + TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(i); + TrackSelectionViewFragment tabFragment = new TrackSelectionViewFragment(); + tabFragment.init( + mappedTrackInfo, + /* rendererIndex= */ i, + initialParameters.getRendererDisabled(/* rendererIndex= */ i), + initialParameters.getSelectionOverride(/* rendererIndex= */ i, trackGroupArray), + allowAdaptiveSelections, + allowMultipleOverrides); + tabFragments.put(i, tabFragment); + tabTrackTypes.add(trackType); + } + } + } + + /** + * Returns whether a renderer is disabled. + * + * @param rendererIndex Renderer index. + * @return Whether the renderer is disabled. + */ + public boolean getIsDisabled(int rendererIndex) { + TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex); + return rendererView != null && rendererView.isDisabled; + } + + /** + * Returns the list of selected track selection overrides for the specified renderer. There will + * be at most one override for each track group. + * + * @param rendererIndex Renderer index. + * @return The list of track selection overrides for this renderer. + */ + public List getOverrides(int rendererIndex) { + TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex); + return rendererView == null ? Collections.emptyList() : rendererView.overrides; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + // We need to own the view to let tab layout work correctly on all API levels. We can't use + // AlertDialog because it owns the view itself, so we use AppCompatDialog instead, themed using + // the AlertDialog theme overlay with force-enabled title. + AppCompatDialog dialog = + new AppCompatDialog(getActivity(), R.style.TrackSelectionDialogThemeOverlay); + dialog.setTitle(titleId); + return dialog; + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + onDismissListener.onDismiss(dialog); + } + + @Nullable + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + + View dialogView = inflater.inflate(R.layout.track_selection_dialog, container, false); + TabLayout tabLayout = dialogView.findViewById(R.id.track_selection_dialog_tab_layout); + ViewPager viewPager = dialogView.findViewById(R.id.track_selection_dialog_view_pager); + Button cancelButton = dialogView.findViewById(R.id.track_selection_dialog_cancel_button); + Button okButton = dialogView.findViewById(R.id.track_selection_dialog_ok_button); + viewPager.setAdapter(new FragmentAdapter(getChildFragmentManager())); + tabLayout.setupWithViewPager(viewPager); + tabLayout.setVisibility(tabFragments.size() > 1 ? View.VISIBLE : View.GONE); + cancelButton.setOnClickListener(view -> dismiss()); + okButton.setOnClickListener( + view -> { + onClickListener.onClick(getDialog(), DialogInterface.BUTTON_POSITIVE); + dismiss(); + }); + return dialogView; + } + + private static boolean showTabForRenderer(MappedTrackInfo mappedTrackInfo, int rendererIndex) { + TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); + if (trackGroupArray.length == 0) { + return false; + } + int trackType = mappedTrackInfo.getRendererType(rendererIndex); + return isSupportedTrackType(trackType); + } + + private static boolean isSupportedTrackType(int trackType) { + switch (trackType) { + case C.TRACK_TYPE_VIDEO: + case C.TRACK_TYPE_AUDIO: + case C.TRACK_TYPE_TEXT: + return true; + default: + return false; + } + } + + private static String getTrackTypeString(Resources resources, int trackType) { + switch (trackType) { + case C.TRACK_TYPE_VIDEO: + return resources.getString(R.string.exo_track_selection_title_video); + case C.TRACK_TYPE_AUDIO: + return resources.getString(R.string.exo_track_selection_title_audio); + case C.TRACK_TYPE_TEXT: + return resources.getString(R.string.exo_track_selection_title_text); + default: + throw new IllegalArgumentException(); + } + } + + private final class FragmentAdapter extends FragmentPagerAdapter { + + public FragmentAdapter(FragmentManager fragmentManager) { + super(fragmentManager); + } + + @Override + public Fragment getItem(int position) { + return tabFragments.valueAt(position); + } + + @Override + public int getCount() { + return tabFragments.size(); + } + + @Nullable + @Override + public CharSequence getPageTitle(int position) { + return getTrackTypeString(getResources(), tabTrackTypes.get(position)); + } + } + + /** Fragment to show a track seleciton in tab of the track selection dialog. */ + public static final class TrackSelectionViewFragment extends Fragment + implements TrackSelectionView.TrackSelectionListener { + + private MappedTrackInfo mappedTrackInfo; + private int rendererIndex; + private boolean allowAdaptiveSelections; + private boolean allowMultipleOverrides; + + /* package */ boolean isDisabled; + /* package */ List overrides; + + public TrackSelectionViewFragment() { + // Retain instance across activity re-creation to prevent losing access to init data. + setRetainInstance(true); + } + + public void init( + MappedTrackInfo mappedTrackInfo, + int rendererIndex, + boolean initialIsDisabled, + @Nullable SelectionOverride initialOverride, + boolean allowAdaptiveSelections, + boolean allowMultipleOverrides) { + this.mappedTrackInfo = mappedTrackInfo; + this.rendererIndex = rendererIndex; + this.isDisabled = initialIsDisabled; + this.overrides = + initialOverride == null + ? Collections.emptyList() + : Collections.singletonList(initialOverride); + this.allowAdaptiveSelections = allowAdaptiveSelections; + this.allowMultipleOverrides = allowMultipleOverrides; + } + + @Nullable + @Override + public View onCreateView( + LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View rootView = + inflater.inflate( + R.layout.exo_track_selection_dialog, container, /* attachToRoot= */ false); + TrackSelectionView trackSelectionView = rootView.findViewById(R.id.exo_track_selection_view); + trackSelectionView.setShowDisableOption(true); + trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides); + trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections); + trackSelectionView.init( + mappedTrackInfo, rendererIndex, isDisabled, overrides, /* listener= */ this); + return rootView; + } + + @Override + public void onTrackSelectionChanged(boolean isDisabled, List overrides) { + this.isDisabled = isDisabled; + this.overrides = overrides; + } + } +} diff --git a/demos/main/src/main/res/layout/player_activity.xml b/demos/main/src/main/res/layout/player_activity.xml index 6b84033273..ea3de257e2 100644 --- a/demos/main/src/main/res/layout/player_activity.xml +++ b/demos/main/src/main/res/layout/player_activity.xml @@ -42,7 +42,15 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" - android:visibility="gone"/> + android:visibility="gone"> + +