Merge pull request #5833 from google/dev-v2-r2.10.0

r2.10.0
This commit is contained in:
Oliver Woodman 2019-05-06 10:29:21 -07:00 committed by GitHub
commit 44293e8f45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
869 changed files with 35644 additions and 15725 deletions

View File

@ -10,11 +10,11 @@ Before filing a bug:
----------------------- -----------------------
- Search existing issues, including issues that are closed. - Search existing issues, including issues that are closed.
- Consult our FAQs, supported devices and supported formats pages. These can be - 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 - 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 reproduce the issue in the ExoPlayer demo app. Information about the ExoPlayer
demo app can be found here: demo app can be found here:
http://google.github.io/ExoPlayer/demo-application.html. http://exoplayer.dev/demo-application.html.
When reporting a bug: When reporting a bug:
----------------------- -----------------------

View File

@ -10,10 +10,10 @@ Before filing a content issue:
------------------------------ ------------------------------
- Search existing issues, including issues that are closed. - Search existing issues, including issues that are closed.
- Consult our supported formats page, which can be found at - 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 - Try playing your content in the ExoPlayer demo app. Information about the
ExoPlayer demo app can be found here: 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: When reporting a content issue:
----------------------------- -----------------------------

View File

@ -13,7 +13,7 @@ Before filing a question:
- Search existing issues, including issues that are closed. Its often the - Search existing issues, including issues that are closed. Its often the
quickest way to get an answer! quickest way to get an answer!
- Consult our FAQs, developer guide and the class reference of ExoPlayer. These - 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: When filing a question:
----------------------- -----------------------

9
.gitignore vendored
View File

@ -37,6 +37,12 @@ local.properties
proguard.cfg proguard.cfg
proguard-project.txt proguard-project.txt
# Bazel
bazel-bin
bazel-genfiles
bazel-out
bazel-testlogs
# Other # Other
.DS_Store .DS_Store
cmake-build-debug cmake-build-debug
@ -66,3 +72,6 @@ extensions/cronet/jniLibs/*
extensions/cronet/libs/* extensions/cronet/libs/*
!extensions/cronet/libs/README.md !extensions/cronet/libs/README.md
# Cast receiver
cast_receiver_app/external-js
cast_receiver_app/bazel-cast_receiver_app

View File

@ -44,6 +44,12 @@ local.properties
proguard.cfg proguard.cfg
proguard-project.txt proguard-project.txt
# Bazel
bazel-bin
bazel-genfiles
bazel-out
bazel-testlogs
# Other # Other
.DS_Store .DS_Store
cmake-build-debug cmake-build-debug
@ -69,3 +75,7 @@ extensions/cronet/jniLibs/*
!extensions/cronet/jniLibs/README.md !extensions/cronet/jniLibs/README.md
extensions/cronet/libs/* extensions/cronet/libs/*
!extensions/cronet/libs/README.md !extensions/cronet/libs/README.md
# Cast receiver
cast_receiver_app/external-js
cast_receiver_app/bazel-cast_receiver_app

View File

@ -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 * Follow our [developer blog][] to keep up to date with the latest ExoPlayer
developments! developments!
[developer guide]: https://google.github.io/ExoPlayer/guide.html [developer guide]: https://exoplayer.dev/guide.html
[class reference]: https://google.github.io/ExoPlayer/doc/reference [class reference]: https://exoplayer.dev/doc/reference
[release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md [release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
[developer blog]: https://medium.com/google-exoplayer [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 ### ### Locally ###
Cloning the repository and depending on the modules locally is required when Cloning the repository and depending on the modules locally is required when

View File

@ -1,5 +1,129 @@
# Release notes # # 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 ### ### 2.9.6 ###
* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`. * Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`.

View File

@ -17,9 +17,9 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.1.4' classpath 'com.android.tools.build:gradle:3.4.0'
classpath 'com.novoda:bintray-release:0.8.1' classpath 'com.novoda:bintray-release:0.9'
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.0.3' classpath 'com.google.android.gms:strict-version-matcher-plugin:1.1.0'
} }
// Workaround for the following test coverage issue. Remove when fixed: // Workaround for the following test coverage issue. Remove when fixed:
// https://code.google.com/p/android/issues/detail?id=226070 // https://code.google.com/p/android/issues/detail?id=226070

View File

@ -13,26 +13,17 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.9.6' releaseVersion = '2.10.0'
releaseVersionCode = 2009006 releaseVersionCode = 2010000
// Important: ExoPlayer specifies a minSdkVersion of 14 because various minSdkVersion = 16
// 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
targetSdkVersion = 28 targetSdkVersion = 28
compileSdkVersion = 28 compileSdkVersion = 28
buildToolsVersion = '28.0.2' dexmakerVersion = '2.21.0'
testSupportLibraryVersion = '0.5' mockitoVersion = '2.25.0'
supportLibraryVersion = '27.1.1' robolectricVersion = '4.2'
dexmakerVersion = '1.2'
mockitoVersion = '1.9.5'
junitVersion = '4.12'
truthVersion = '0.39'
robolectricVersion = '3.7.1'
autoValueVersion = '1.6' autoValueVersion = '1.6'
checkerframeworkVersion = '2.5.0' checkerframeworkVersion = '2.5.0'
testRunnerVersion = '1.1.0-alpha3' androidXTestVersion = '1.1.0'
modulePrefix = ':' modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) { if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix modulePrefix += gradle.ext.exoplayerModulePrefix

View File

@ -16,7 +16,6 @@ apply plugin: 'com.android.application'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -26,7 +25,7 @@ android {
defaultConfig { defaultConfig {
versionName project.ext.releaseVersion versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion 16 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} }
@ -45,8 +44,18 @@ android {
} }
lintOptions { lintOptions {
// The demo app does not have translations. // The demo app isn't indexed and doesn't have translations.
disable 'MissingTranslation' 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-smoothstreaming')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
implementation project(modulePrefix + 'extension-cast') implementation project(modulePrefix + 'extension-cast')
implementation 'com.android.support:support-v4:' + supportLibraryVersion implementation 'com.google.android.material:material:1.0.0'
implementation 'com.android.support:appcompat-v7:' + supportLibraryVersion implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.android.support:recyclerview-v7:' + supportLibraryVersion implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
} }
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'

View File

@ -1,6 +1,6 @@
# Proguard rules specific to the Cast demo app. # Proguard rules specific to the Cast demo app.
# Accessed via menu.xml # Accessed via menu.xml
-keep class android.support.v7.app.MediaRouteActionProvider { -keep class androidx.mediarouter.app.MediaRouteActionProvider {
*; *;
} }

View File

@ -17,13 +17,15 @@
package="com.google.android.exoplayer2.castdemo"> package="com.google.android.exoplayer2.castdemo">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-sdk/> <uses-sdk/>
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher" <application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
android:largeHeap="true" android:allowBackup="false"> android:largeHeap="true" android:allowBackup="false">
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" <meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider" /> android:value="${castOptionsProvider}" />
<activity android:name="com.google.android.exoplayer2.castdemo.MainActivity" <activity android:name="com.google.android.exoplayer2.castdemo.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"

View File

@ -0,0 +1,405 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.castdemo;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.Nullable;
import android.view.KeyEvent;
import android.view.View;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.Player.EventListener;
import com.google.android.exoplayer2.Player.TimelineChangeReason;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.ext.cast.CastPlayer;
import com.google.android.exoplayer2.ext.cast.MediaItem;
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaMetadata;
import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.framework.CastContext;
import java.util.ArrayList;
/** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */
/* package */ class DefaultReceiverPlayerManager
implements PlayerManager, EventListener, SessionAvailabilityListener {
private static final String USER_AGENT = "ExoCastDemoPlayer";
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
new DefaultHttpDataSourceFactory(USER_AGENT);
private final PlayerView localPlayerView;
private final PlayerControlView castControlView;
private final SimpleExoPlayer exoPlayer;
private final CastPlayer castPlayer;
private final ArrayList<MediaItem> 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();
}
}

View File

@ -15,44 +15,37 @@
*/ */
package com.google.android.exoplayer2.castdemo; 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.exoplayer2.util.MimeTypes;
import com.google.android.gms.cast.MediaInfo;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; 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 { /* package */ final class DemoUtil {
public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD; /** Represents a media sample. */
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<Sample> SAMPLES;
/**
* Represents a media sample.
*/
public static final class Sample { public static final class Sample {
/** /** The uri of the media content. */
* The uri from which the media sample is obtained.
*/
public final String uri; public final String uri;
/** /** The name of the sample. */
* A descriptive name for the sample.
*/
public final String name; public final String name;
/** /** The mime type of the sample media content. */
* The mime type of the media sample, as required by {@link MediaInfo#setContentType}.
*/
public final String mimeType; public final String mimeType;
/**
* The {@link UUID} of the DRM scheme that protects the content, or null if the content is not
* DRM-protected.
*/
@Nullable public final UUID drmSchemeUuid;
/**
* The url from which players should obtain DRM licenses, or null if the content is not
* DRM-protected.
*/
@Nullable public final Uri licenseServerUri;
/** /**
* @param uri See {@link #uri}. * @param uri See {@link #uri}.
@ -60,31 +53,53 @@ import java.util.List;
* @param mimeType See {@link #mimeType}. * @param mimeType See {@link #mimeType}.
*/ */
public Sample(String uri, String name, String 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.uri = uri;
this.name = name; this.name = name;
this.mimeType = mimeType; this.mimeType = mimeType;
this.drmSchemeUuid = drmSchemeUuid;
this.licenseServerUri =
licenseServerUriString != null ? Uri.parse(licenseServerUriString) : null;
} }
@Override @Override
public String toString() { public String toString() {
return name; 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<Sample> SAMPLES;
static { static {
// App samples. // App samples.
ArrayList<Sample> samples = new ArrayList<>(); ArrayList<Sample> 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() {} private DemoUtil() {}
} }

View File

@ -17,13 +17,13 @@ package com.google.android.exoplayer2.castdemo;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.graphics.ColorUtils; import androidx.core.graphics.ColorUtils;
import android.support.v7.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import android.support.v7.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder; import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import android.support.v7.widget.helper.ItemTouchHelper; import androidx.recyclerview.widget.ItemTouchHelper;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
@ -33,21 +33,26 @@ import android.view.ViewGroup;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.ListView; import android.widget.ListView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.castdemo.DemoUtil.Sample; import com.google.android.exoplayer2.ext.cast.MediaItem;
import com.google.android.exoplayer2.ext.cast.CastPlayer;
import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.PlayerView; 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.CastButtonFactory;
import com.google.android.gms.cast.framework.CastContext; import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.dynamite.DynamiteModule; 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, public class MainActivity extends AppCompatActivity
PlayerManager.QueuePositionListener { implements OnClickListener, PlayerManager.Listener {
private final MediaItem.Builder mediaItemBuilder;
private PlayerView localPlayerView; private PlayerView localPlayerView;
private PlayerControlView castControlView; private PlayerControlView castControlView;
@ -56,6 +61,10 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
private MediaQueueListAdapter mediaQueueListAdapter; private MediaQueueListAdapter mediaQueueListAdapter;
private CastContext castContext; private CastContext castContext;
public MainActivity() {
mediaItemBuilder = new MediaItem.Builder();
}
// Activity lifecycle methods. // Activity lifecycle methods.
@Override @Override
@ -68,7 +77,7 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
Throwable cause = e.getCause(); Throwable cause = e.getCause();
while (cause != null) { while (cause != null) {
if (cause instanceof DynamiteModule.LoadingException) { if (cause instanceof DynamiteModule.LoadingException) {
setContentView(R.layout.cast_context_error_message_layout); setContentView(R.layout.cast_context_error);
return; return;
} }
cause = cause.getCause(); cause = cause.getCause();
@ -109,13 +118,20 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
// There is no Cast context to work with. Do nothing. // There is no Cast context to work with. Do nothing.
return; return;
} }
playerManager = String applicationId = castContext.getCastOptions().getReceiverApplicationId();
PlayerManager.createPlayerManager( switch (applicationId) {
/* queuePositionListener= */ this, case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID:
localPlayerView, playerManager =
castControlView, new DefaultReceiverPlayerManager(
/* context= */ this, /* listener= */ this,
castContext); localPlayerView,
castControlView,
/* context= */ this,
castContext);
break;
default:
throw new IllegalStateException("Illegal receiver app id: " + applicationId);
}
mediaQueueList.setAdapter(mediaQueueListAdapter); mediaQueueList.setAdapter(mediaQueueListAdapter);
} }
@ -129,6 +145,7 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount()); mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount());
mediaQueueList.setAdapter(null); mediaQueueList.setAdapter(null);
playerManager.release(); playerManager.release();
playerManager = null;
} }
// Activity input. // Activity input.
@ -141,12 +158,15 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
@Override @Override
public void onClick(View view) { public void onClick(View view) {
new AlertDialog.Builder(this).setTitle(R.string.sample_list_dialog_title) new AlertDialog.Builder(this)
.setView(buildSampleListView()).setPositiveButton(android.R.string.ok, null).create() .setTitle(R.string.add_samples)
.setView(buildSampleListView())
.setPositiveButton(android.R.string.ok, null)
.create()
.show(); .show();
} }
// PlayerManager.QueuePositionListener implementation. // PlayerManager.Listener implementation.
@Override @Override
public void onQueuePositionChanged(int previousIndex, int newIndex) { 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. // Internal methods.
private View buildSampleListView() { private View buildSampleListView() {
@ -166,7 +196,19 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
sampleList.setAdapter(new SampleListAdapter(this)); sampleList.setAdapter(new SampleListAdapter(this));
sampleList.setOnItemClickListener( sampleList.setOnItemClickListener(
(parent, view, position, id) -> { (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); mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
}); });
return dialogList; return dialogList;
@ -174,23 +216,6 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
// Internal classes. // 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<QueueItemViewHolder> { private class MediaQueueListAdapter extends RecyclerView.Adapter<QueueItemViewHolder> {
@Override @Override
@ -202,8 +227,9 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
@Override @Override
public void onBindViewHolder(QueueItemViewHolder holder, int position) { public void onBindViewHolder(QueueItemViewHolder holder, int position) {
holder.item = playerManager.getItem(position);
TextView view = holder.textView; TextView view = holder.textView;
view.setText(playerManager.getItem(position).name); view.setText(holder.item.title);
// TODO: Solve coloring using the theme's ColorStateList. // TODO: Solve coloring using the theme's ColorStateList.
view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(), view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(),
position == playerManager.getCurrentItemIndex() ? 255 : 100)); position == playerManager.getCurrentItemIndex() ? 255 : 100));
@ -244,8 +270,11 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
@Override @Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAdapterPosition(); int position = viewHolder.getAdapterPosition();
if (playerManager.removeItem(position)) { QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
if (playerManager.removeItem(queueItemHolder.item)) {
mediaQueueListAdapter.notifyItemRemoved(position); 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) { public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder); super.clearView(recyclerView, viewHolder);
if (draggingFromPosition != C.INDEX_UNSET) { if (draggingFromPosition != C.INDEX_UNSET) {
QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
// A drag has ended. We reflect the media queue change in the player. // 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 // The move failed. The entire sequence of onMove calls since the drag started needs to be
// invalidated. // invalidated.
mediaQueueListAdapter.notifyDataSetChanged(); mediaQueueListAdapter.notifyDataSetChanged();
@ -263,15 +293,30 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
draggingFromPosition = C.INDEX_UNSET; draggingFromPosition = C.INDEX_UNSET;
draggingToPosition = C.INDEX_UNSET; draggingToPosition = C.INDEX_UNSET;
} }
} }
private static final class SampleListAdapter extends ArrayAdapter<Sample> { 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<DemoUtil.Sample> {
public SampleListAdapter(Context context) { public SampleListAdapter(Context context) {
super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);
} }
} }
} }

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -15,402 +15,53 @@
*/ */
package com.google.android.exoplayer2.castdemo; 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.KeyEvent;
import android.view.View;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ext.cast.MediaItem;
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;
/** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */ /** Manages the players in the Cast demo app. */
/* package */ final class PlayerManager /* package */ interface PlayerManager {
implements EventListener, CastPlayer.SessionAvailabilityListener {
/** /** Listener for events. */
* Listener for changes in the media queue playback position. interface Listener {
*/
public interface QueuePositionListener {
/** /** 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); 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"; /** Redirects the given {@code keyEvent} to the active player. */
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = boolean dispatchKeyEvent(KeyEvent keyEvent);
new DefaultHttpDataSourceFactory(USER_AGENT);
private final PlayerView localPlayerView; /** Appends the given {@link MediaItem} to the media queue. */
private final PlayerControlView castControlView; void addItem(MediaItem mediaItem);
private final SimpleExoPlayer exoPlayer;
private final CastPlayer castPlayer;
private final ArrayList<DemoUtil.Sample> mediaQueue;
private final QueuePositionListener queuePositionListener;
private final ConcatenatingMediaSource concatenatingMediaSource;
private boolean castMediaQueueCreationPending; /** Returns the number of items in the media queue. */
private int currentItemIndex; int getMediaQueueSize();
private Player currentPlayer;
/** Selects the item at the given position for playback. */
void selectQueueItem(int position);
/** /**
* @param queuePositionListener A {@link QueuePositionListener} for queue position changes. * Returns the position of the item currently being played, or {@link C#INDEX_UNSET} if no item is
* @param localPlayerView The {@link PlayerView} for local playback. * being played.
* @param castControlView The {@link PlayerControlView} to control remote playback.
* @param context A {@link Context}.
* @param castContext The {@link CastContext}.
*/ */
public static PlayerManager createPlayerManager( int getCurrentItemIndex();
QueuePositionListener queuePositionListener,
PlayerView localPlayerView,
PlayerControlView castControlView,
Context context,
CastContext castContext) {
PlayerManager playerManager =
new PlayerManager(
queuePositionListener, localPlayerView, castControlView, context, castContext);
playerManager.init();
return playerManager;
}
private PlayerManager( /** Returns the {@link MediaItem} at the given {@code position}. */
QueuePositionListener queuePositionListener, MediaItem getItem(int position);
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();
DefaultTrackSelector trackSelector = new DefaultTrackSelector(); /** Moves the item at position {@code from} to position {@code to}. */
RenderersFactory renderersFactory = new DefaultRenderersFactory(context); boolean moveItem(MediaItem item, int to);
exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector);
exoPlayer.addListener(this);
localPlayerView.setPlayer(exoPlayer);
castPlayer = new CastPlayer(castContext); /** Removes the item at position {@code index}. */
castPlayer.addListener(this); boolean removeItem(MediaItem item);
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();
}
/** Releases any acquired resources. */
void release();
} }

View File

@ -13,8 +13,12 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<vector android:alpha="0.8" android:height="24dp" android:viewportHeight="24.0" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportWidth="24.0" android:width="24dp" android:height="24.0dp"
xmlns:android="http://schemas.android.com/apk/res/android"> android:viewportHeight="24.0"
<path android:fillColor="#FFFFFF" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z"/> android:viewportWidth="24.0"
android:width="24.0dp" >
<path
android:fillColor="#FFFFFFFF"
android:pathData="M18,13h-5v5c0,0.55 -0.45,1 -1,1h0c-0.55,0 -1,-0.45 -1,-1v-5H6c-0.55,0 -1,-0.45 -1,-1v0c0,-0.55 0.45,-1 1,-1h5V6c0,-0.55 0.45,-1 1,-1h0c0.55,0 1,0.45 1,1v5h5c0.55,0 1,0.45 1,1v0C19,12.55 18.55,13 18,13z"/>
</vector> </vector>

View File

@ -13,17 +13,10 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView" android:id="@+id/textView"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_weight="1"
android:textSize="20sp"
android:gravity="center" android:gravity="center"
android:textSize="20sp"
android:text="@string/cast_context_error"/> android:text="@string/cast_context_error"/>
</LinearLayout>

View File

@ -19,34 +19,42 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:keepScreenOn="true"> android:keepScreenOn="true">
<com.google.android.exoplayer2.ui.PlayerView android:id="@+id/local_player_view" <com.google.android.exoplayer2.ui.PlayerView android:id="@+id/local_player_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="12" android:layout_weight="1"
android:background="@android:color/black"
app:repeat_toggle_modes="all|one"/> app:repeat_toggle_modes="all|one"/>
<RelativeLayout android:layout_width="match_parent" <RelativeLayout android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="12"> android:layout_weight="1">
<android.support.v7.widget.RecyclerView android:id="@+id/sample_list"
<androidx.recyclerview.widget.RecyclerView android:id="@+id/sample_list"
android:choiceMode="singleChoice" android:choiceMode="singleChoice"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:scrollbars="vertical" android:scrollbars="vertical"
android:fadeScrollbars="false"/> android:fadeScrollbars="false"/>
<ImageButton android:id="@+id/add_sample_button"
android:background="@drawable/ic_add_circle_white_24dp" <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/add_sample_button"
android:src="@drawable/ic_plus"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:padding="30dp"/> android:layout_margin="16dp"
android:contentDescription="@string/add_samples"/>
</RelativeLayout> </RelativeLayout>
<com.google.android.exoplayer2.ui.PlayerControlView android:id="@+id/cast_control_view" <com.google.android.exoplayer2.ui.PlayerControlView android:id="@+id/cast_control_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="wrap_content"
android:layout_weight="2"
android:visibility="gone" android:visibility="gone"
app:repeat_toggle_modes="all|one" app:repeat_toggle_modes="all|one"
app:show_timeout="-1"/> app:show_timeout="-1"/>
</LinearLayout> </LinearLayout>

View File

@ -14,7 +14,7 @@
limitations under the License. limitations under the License.
--> -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<ListView android:id="@+id/sample_list" <ListView android:id="@+id/sample_list"

View File

@ -19,7 +19,7 @@
<item <item
android:id="@+id/media_route_menu_item" android:id="@+id/media_route_menu_item"
android:title="@string/media_route_menu_title" android:title="@string/media_route_menu_title"
app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider" app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
app:showAsAction="always" /> app:showAsAction="always" />
</menu> </menu>

View File

@ -20,8 +20,10 @@
<string name="media_route_menu_title">Cast</string> <string name="media_route_menu_title">Cast</string>
<string name="sample_list_dialog_title">Add samples</string> <string name="add_samples">Add samples</string>
<string name="cast_context_error">Failed to get Cast context. Try updating Google Play Services and restart the app.</string> <string name="cast_context_error">Failed to get Cast context. Try updating Google Play Services and restart the app.</string>
<string name="player_error_msg">Player error encountered. Select a queue item to reprepare. Check the logcat and receiver app\'s console for more info.</string>
</resources> </resources>

View File

@ -16,7 +16,6 @@ apply plugin: 'com.android.application'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -26,7 +25,7 @@ android {
defaultConfig { defaultConfig {
versionName project.ext.releaseVersion versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion 16 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} }
@ -42,8 +41,8 @@ android {
} }
lintOptions { lintOptions {
// The demo app does not have translations. // The demo app isn't indexed and doesn't have translations.
disable 'MissingTranslation' disable 'GoogleAppIndexingWarning','MissingTranslation'
} }
} }
@ -54,7 +53,7 @@ dependencies {
implementation project(modulePrefix + 'library-hls') implementation project(modulePrefix + 'library-hls')
implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'extension-ima') 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' apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'

View File

@ -23,8 +23,8 @@ import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; 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.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource;
@ -114,7 +114,7 @@ import com.google.android.exoplayer2.util.Util;
case C.TYPE_HLS: case C.TYPE_HLS:
return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
case C.TYPE_OTHER: case C.TYPE_OTHER:
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri); return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
default: default:
throw new IllegalStateException("Unsupported type: " + type); throw new IllegalStateException("Unsupported type: " + type);
} }

View File

@ -16,7 +16,6 @@ apply plugin: 'com.android.application'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -26,7 +25,7 @@ android {
defaultConfig { defaultConfig {
versionName project.ext.releaseVersion versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion 16 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} }
@ -45,8 +44,9 @@ android {
} }
lintOptions { lintOptions {
// The demo app does not have translations. // The demo app isn't indexed, doesn't have translations, and has a
disable 'MissingTranslation' // banner for AndroidTV that's only in xhdpi density.
disable 'GoogleAppIndexingWarning','MissingTranslation','IconDensities'
} }
flavorDimensions "extensions" flavorDimensions "extensions"
@ -62,7 +62,10 @@ android {
} }
dependencies { 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-core')
implementation project(modulePrefix + 'library-dash') implementation project(modulePrefix + 'library-dash')
implementation project(modulePrefix + 'library-hls') implementation project(modulePrefix + 'library-hls')

View File

@ -15,6 +15,7 @@
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.demo"> package="com.google.android.exoplayer2.demo">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
@ -33,11 +34,13 @@
android:banner="@drawable/ic_banner" android:banner="@drawable/ic_banner"
android:largeHeap="true" android:largeHeap="true"
android:allowBackup="false" android:allowBackup="false"
android:name="com.google.android.exoplayer2.demo.DemoApplication"> android:name="com.google.android.exoplayer2.demo.DemoApplication"
tools:ignore="UnusedAttribute">
<activity android:name="com.google.android.exoplayer2.demo.SampleChooserActivity" <activity android:name="com.google.android.exoplayer2.demo.SampleChooserActivity"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:label="@string/application_name"> android:label="@string/application_name"
android:theme="@style/Theme.AppCompat">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>

View File

@ -330,11 +330,11 @@
"samples": [ "samples": [
{ {
"name": "Super speed", "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)", "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" "drm_scheme": "playready"
} }
] ]

View File

@ -16,6 +16,13 @@
package com.google.android.exoplayer2.demo; package com.google.android.exoplayer2.demo;
import android.app.Application; 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.DownloadManager;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.upstream.DataSource; 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.CacheDataSourceFactory;
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.io.IOException;
/** /**
* Placeholder application to facilitate overriding Application methods for debugging and testing. * Placeholder application to facilitate overriding Application methods for debugging and testing.
*/ */
public class DemoApplication extends Application { public class DemoApplication extends Application {
private static final String TAG = "DemoApplication";
private static final String DOWNLOAD_ACTION_FILE = "actions"; private static final String DOWNLOAD_ACTION_FILE = "actions";
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions"; private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2;
protected String userAgent; protected String userAgent;
private DatabaseProvider databaseProvider;
private File downloadDirectory; private File downloadDirectory;
private Cache downloadCache; private Cache downloadCache;
private DownloadManager downloadManager; private DownloadManager downloadManager;
@ -71,6 +81,18 @@ public class DemoApplication extends Application {
return "withExtensions".equals(BuildConfig.FLAVOR); 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() { public DownloadManager getDownloadManager() {
initDownloadManager(); initDownloadManager();
return downloadManager; return downloadManager;
@ -81,31 +103,51 @@ public class DemoApplication extends Application {
return downloadTracker; 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() { private synchronized void initDownloadManager() {
if (downloadManager == null) { 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 = DownloaderConstructorHelper downloaderConstructorHelper =
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory()); new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
downloadManager = downloadManager =
new DownloadManager( new DownloadManager(
downloaderConstructorHelper, this, downloadIndex, new DefaultDownloaderFactory(downloaderConstructorHelper));
MAX_SIMULTANEOUS_DOWNLOADS,
DownloadManager.DEFAULT_MIN_RETRY_COUNT,
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE));
downloadTracker = downloadTracker =
new DownloadTracker( new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadManager);
/* context= */ this,
buildDataSourceFactory(),
new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE));
downloadManager.addListener(downloadTracker);
} }
} }
private synchronized Cache getDownloadCache() { private void upgradeActionFile(
if (downloadCache == null) { String fileName, DefaultDownloadIndex downloadIndex, boolean addNewDownloadsAsCompleted) {
File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY); try {
downloadCache = new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor()); 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() { private File getDownloadDirectory() {
@ -118,8 +160,8 @@ public class DemoApplication extends Application {
return downloadDirectory; return downloadDirectory;
} }
private static CacheDataSourceFactory buildReadOnlyCacheDataSource( protected static CacheDataSourceFactory buildReadOnlyCacheDataSource(
DefaultDataSourceFactory upstreamFactory, Cache cache) { DataSource.Factory upstreamFactory, Cache cache) {
return new CacheDataSourceFactory( return new CacheDataSourceFactory(
cache, cache,
upstreamFactory, upstreamFactory,

View File

@ -16,13 +16,14 @@
package com.google.android.exoplayer2.demo; package com.google.android.exoplayer2.demo;
import android.app.Notification; 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;
import com.google.android.exoplayer2.offline.DownloadManager.TaskState;
import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.scheduler.PlatformScheduler; 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.NotificationUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.List;
/** A service for downloading media. */ /** A service for downloading media. */
public class DemoDownloadService extends DownloadService { 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 JOB_ID = 1;
private static final int FOREGROUND_NOTIFICATION_ID = 1; private static final int FOREGROUND_NOTIFICATION_ID = 1;
private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
private DownloadNotificationHelper notificationHelper;
public DemoDownloadService() { public DemoDownloadService() {
super( super(
FOREGROUND_NOTIFICATION_ID, FOREGROUND_NOTIFICATION_ID,
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL, DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
CHANNEL_ID, CHANNEL_ID,
R.string.exo_download_notification_channel_name); 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 @Override
@ -50,40 +62,29 @@ public class DemoDownloadService extends DownloadService {
} }
@Override @Override
protected Notification getForegroundNotification(TaskState[] taskStates) { protected Notification getForegroundNotification(List<Download> downloads) {
return DownloadNotificationUtil.buildProgressNotification( return notificationHelper.buildProgressNotification(
/* context= */ this, R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads);
R.drawable.exo_controls_play,
CHANNEL_ID,
/* contentIntent= */ null,
/* message= */ null,
taskStates);
} }
@Override @Override
protected void onTaskStateChanged(TaskState taskState) { protected void onDownloadChanged(Download download) {
if (taskState.action.isRemoveAction) { 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; return;
} }
Notification notification = null; NotificationUtil.setNotification(this, nextNotificationId++, notification);
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);
} }
} }

View File

@ -15,54 +15,34 @@
*/ */
package com.google.android.exoplayer2.demo; package com.google.android.exoplayer2.demo;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.net.Uri; import android.net.Uri;
import android.os.Handler; import androidx.annotation.Nullable;
import android.os.HandlerThread; import androidx.fragment.app.FragmentManager;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast; import android.widget.Toast;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.offline.ActionFile; import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.offline.DownloadAction; 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.DownloadHelper;
import com.google.android.exoplayer2.offline.DownloadIndex;
import com.google.android.exoplayer2.offline.DownloadManager; 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.DownloadService;
import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper;
import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.offline.TrackKey; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
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.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
/** /** Tracks media that has been downloaded. */
* Tracks media that has been downloaded. public class DownloadTracker {
*
* <p>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 {
/** Listens for changes in the tracked downloads. */ /** Listens for changes in the tracked downloads. */
public interface Listener { public interface Listener {
@ -75,28 +55,21 @@ public class DownloadTracker implements DownloadManager.Listener {
private final Context context; private final Context context;
private final DataSource.Factory dataSourceFactory; private final DataSource.Factory dataSourceFactory;
private final TrackNameProvider trackNameProvider;
private final CopyOnWriteArraySet<Listener> listeners; private final CopyOnWriteArraySet<Listener> listeners;
private final HashMap<Uri, DownloadAction> trackedDownloadStates; private final HashMap<Uri, Download> downloads;
private final ActionFile actionFile; private final DownloadIndex downloadIndex;
private final Handler actionFileWriteHandler;
@Nullable private StartDownloadDialogHelper startDownloadDialogHelper;
public DownloadTracker( public DownloadTracker(
Context context, Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) {
DataSource.Factory dataSourceFactory,
File actionFile,
DownloadAction.Deserializer... deserializers) {
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
this.dataSourceFactory = dataSourceFactory; this.dataSourceFactory = dataSourceFactory;
this.actionFile = new ActionFile(actionFile);
trackNameProvider = new DefaultTrackNameProvider(context.getResources());
listeners = new CopyOnWriteArraySet<>(); listeners = new CopyOnWriteArraySet<>();
trackedDownloadStates = new HashMap<>(); downloads = new HashMap<>();
HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker"); downloadIndex = downloadManager.getDownloadIndex();
actionFileWriteThread.start(); downloadManager.addListener(new DownloadManagerListener());
actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper()); loadDownloads();
loadTrackedActions(
deserializers.length > 0 ? deserializers : DownloadAction.getDefaultDeserializers());
} }
public void addListener(Listener listener) { public void addListener(Listener listener) {
@ -108,167 +81,139 @@ public class DownloadTracker implements DownloadManager.Listener {
} }
public boolean isDownloaded(Uri uri) { public boolean isDownloaded(Uri uri) {
return trackedDownloadStates.containsKey(uri); Download download = downloads.get(uri);
return download != null && download.state != Download.STATE_FAILED;
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public List<StreamKey> getOfflineStreamKeys(Uri uri) { public List<StreamKey> getOfflineStreamKeys(Uri uri) {
if (!trackedDownloadStates.containsKey(uri)) { Download download = downloads.get(uri);
return Collections.emptyList(); return download != null && download.state != Download.STATE_FAILED
} ? download.request.streamKeys
return trackedDownloadStates.get(uri).getKeys(); : Collections.emptyList();
} }
public void toggleDownload(Activity activity, String name, Uri uri, String extension) { public void toggleDownload(
if (isDownloaded(uri)) { FragmentManager fragmentManager,
DownloadAction removeAction = String name,
getDownloadHelper(uri, extension).getRemoveAction(Util.getUtf8Bytes(name)); Uri uri,
startServiceWithAction(removeAction); String extension,
RenderersFactory renderersFactory) {
Download download = downloads.get(uri);
if (download != null) {
DownloadService.sendRemoveDownload(
context, DemoDownloadService.class, download.request.id, /* foreground= */ false);
} else { } else {
StartDownloadDialogHelper helper = if (startDownloadDialogHelper != null) {
new StartDownloadDialogHelper(activity, getDownloadHelper(uri, extension), name); startDownloadDialogHelper.release();
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();
} }
startDownloadDialogHelper =
new StartDownloadDialogHelper(
fragmentManager, getDownloadHelper(uri, extension, renderersFactory), name);
} }
} }
@Override private void loadDownloads() {
public void onIdle(DownloadManager downloadManager) { try (DownloadCursor loadedDownloads = downloadIndex.getDownloads()) {
// Do nothing. while (loadedDownloads.moveToNext()) {
} Download download = loadedDownloads.getDownload();
downloads.put(download.request.uri, download);
// Internal methods
private void loadTrackedActions(DownloadAction.Deserializer[] deserializers) {
try {
DownloadAction[] allActions = actionFile.load(deserializers);
for (DownloadAction action : allActions) {
trackedDownloadStates.put(action.uri, action);
} }
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "Failed to load tracked actions", e); Log.w(TAG, "Failed to query downloads", e);
} }
} }
private void handleTrackedDownloadStatesChanged() { private DownloadHelper getDownloadHelper(
for (Listener listener : listeners) { Uri uri, String extension, RenderersFactory renderersFactory) {
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) {
int type = Util.inferContentType(uri, extension); int type = Util.inferContentType(uri, extension);
switch (type) { switch (type) {
case C.TYPE_DASH: case C.TYPE_DASH:
return new DashDownloadHelper(uri, dataSourceFactory); return DownloadHelper.forDash(uri, dataSourceFactory, renderersFactory);
case C.TYPE_SS: case C.TYPE_SS:
return new SsDownloadHelper(uri, dataSourceFactory); return DownloadHelper.forSmoothStreaming(uri, dataSourceFactory, renderersFactory);
case C.TYPE_HLS: case C.TYPE_HLS:
return new HlsDownloadHelper(uri, dataSourceFactory); return DownloadHelper.forHls(uri, dataSourceFactory, renderersFactory);
case C.TYPE_OTHER: case C.TYPE_OTHER:
return new ProgressiveDownloadHelper(uri); return DownloadHelper.forProgressive(uri);
default: default:
throw new IllegalStateException("Unsupported type: " + type); throw new IllegalStateException("Unsupported type: " + type);
} }
} }
private final class StartDownloadDialogHelper private class DownloadManagerListener implements DownloadManager.Listener {
implements DownloadHelper.Callback, DialogInterface.OnClickListener {
private final DownloadHelper downloadHelper; @Override
private final String name; public void onDownloadChanged(DownloadManager downloadManager, Download download) {
downloads.put(download.request.uri, download);
private final AlertDialog.Builder builder; for (Listener listener : listeners) {
private final View dialogView; listener.onDownloadsChanged();
private final List<TrackKey> trackKeys; }
private final ArrayAdapter<String> 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 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 @Override
public void onPrepared(DownloadHelper helper) { public void onPrepared(DownloadHelper helper) {
for (int i = 0; i < downloadHelper.getPeriodCount(); i++) { if (helper.getPeriodCount() == 0) {
TrackGroupArray trackGroups = downloadHelper.getTrackGroups(i); Log.d(TAG, "No periods found. Downloading entire stream.");
for (int j = 0; j < trackGroups.length; j++) { startDownload();
TrackGroup trackGroup = trackGroups.get(j); downloadHelper.release();
for (int k = 0; k < trackGroup.length; k++) { return;
trackKeys.add(new TrackKey(i, j, k));
trackTitles.add(trackNameProvider.getTrackName(trackGroup.getFormat(k)));
}
}
} }
if (!trackKeys.isEmpty()) { mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
builder.setView(dialogView); 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 @Override
@ -279,20 +224,51 @@ public class DownloadTracker implements DownloadManager.Listener {
Log.e(TAG, "Failed to start download", e); Log.e(TAG, "Failed to start download", e);
} }
// DialogInterface.OnClickListener implementation.
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
ArrayList<TrackKey> selectedTrackKeys = new ArrayList<>(); for (int periodIndex = 0; periodIndex < downloadHelper.getPeriodCount(); periodIndex++) {
for (int i = 0; i < representationList.getChildCount(); i++) { downloadHelper.clearTrackSelections(periodIndex);
if (representationList.isItemChecked(i)) { for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
selectedTrackKeys.add(trackKeys.get(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()) { DownloadRequest downloadRequest = buildDownloadRequest();
// We have selected keys, or we're dealing with single stream content. if (downloadRequest.streamKeys.isEmpty()) {
DownloadAction downloadAction = // All tracks were deselected in the dialog. Don't start the download.
downloadHelper.getDownloadAction(Util.getUtf8Bytes(name), selectedTrackKeys); return;
startDownload(downloadAction);
} }
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));
} }
} }
} }

View File

@ -15,14 +15,13 @@
*/ */
package com.google.android.exoplayer2.demo; package com.google.android.exoplayer2.demo;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import androidx.annotation.NonNull;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.util.Pair; import android.util.Pair;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.View; import android.view.View;
@ -33,11 +32,11 @@ import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.ContentType; import com.google.android.exoplayer2.C.ContentType;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; 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.drm.UnsupportedDrmException;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; 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.offline.StreamKey;
import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource; 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.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource; 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.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.SsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; 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.DebugTextViewHelper;
import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.PlayerView; 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.ui.spherical.SphericalSurfaceView;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
@ -85,7 +79,7 @@ import java.util.List;
import java.util.UUID; import java.util.UUID;
/** An activity that plays media using {@link SimpleExoPlayer}. */ /** An activity that plays media using {@link SimpleExoPlayer}. */
public class PlayerActivity extends Activity public class PlayerActivity extends AppCompatActivity
implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener { implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener {
public static final String DRM_SCHEME_EXTRA = "drm_scheme"; public static final String DRM_SCHEME_EXTRA = "drm_scheme";
@ -130,7 +124,9 @@ public class PlayerActivity extends Activity
private PlayerView playerView; private PlayerView playerView;
private LinearLayout debugRootView; private LinearLayout debugRootView;
private Button selectTracksButton;
private TextView debugTextView; private TextView debugTextView;
private boolean isShowingTrackSelectionDialog;
private DataSource.Factory dataSourceFactory; private DataSource.Factory dataSourceFactory;
private SimpleExoPlayer player; private SimpleExoPlayer player;
@ -165,10 +161,10 @@ public class PlayerActivity extends Activity
} }
setContentView(R.layout.player_activity); setContentView(R.layout.player_activity);
View rootView = findViewById(R.id.root);
rootView.setOnClickListener(this);
debugRootView = findViewById(R.id.controls_root); debugRootView = findViewById(R.id.controls_root);
debugTextView = findViewById(R.id.debug_text_view); debugTextView = findViewById(R.id.debug_text_view);
selectTracksButton = findViewById(R.id.select_tracks_button);
selectTracksButton.setOnClickListener(this);
playerView = findViewById(R.id.player_view); playerView = findViewById(R.id.player_view);
playerView.setControllerVisibilityListener(this); playerView.setControllerVisibilityListener(this);
@ -203,6 +199,7 @@ public class PlayerActivity extends Activity
@Override @Override
public void onNewIntent(Intent intent) { public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
releasePlayer(); releasePlayer();
releaseAdsLoader(); releaseAdsLoader();
clearStartPosition(); clearStartPosition();
@ -277,6 +274,7 @@ public class PlayerActivity extends Activity
@Override @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
updateTrackSelectorParameters(); updateTrackSelectorParameters();
updateStartPosition(); updateStartPosition();
outState.putParcelable(KEY_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters); outState.putParcelable(KEY_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters);
@ -297,23 +295,15 @@ public class PlayerActivity extends Activity
@Override @Override
public void onClick(View view) { public void onClick(View view) {
if (view.getParent() == debugRootView) { if (view == selectTracksButton
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); && !isShowingTrackSelectionDialog
if (mappedTrackInfo != null) { && TrackSelectionDialog.willHaveContent(trackSelector)) {
CharSequence title = ((Button) view).getText(); isShowingTrackSelectionDialog = true;
int rendererIndex = (int) view.getTag(); TrackSelectionDialog trackSelectionDialog =
int rendererType = mappedTrackInfo.getRendererType(rendererIndex); TrackSelectionDialog.createForTrackSelector(
boolean allowAdaptiveSelections = trackSelector,
rendererType == C.TRACK_TYPE_VIDEO /* onDismissListener= */ dismissedDialog -> isShowingTrackSelectionDialog = false);
|| (rendererType == C.TRACK_TYPE_AUDIO trackSelectionDialog.show(getSupportFragmentManager(), /* tag= */ null);
&& mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
== MappedTrackInfo.RENDERER_SUPPORT_NO_TRACKS);
Pair<AlertDialog, TrackSelectionView> dialogPair =
TrackSelectionView.getDialog(this, title, trackSelector, rendererIndex);
dialogPair.second.setShowDisableOption(true);
dialogPair.second.setAllowAdaptiveSelections(allowAdaptiveSelections);
dialogPair.first.show();
}
} }
} }
@ -321,7 +311,7 @@ public class PlayerActivity extends Activity
@Override @Override
public void preparePlayback() { public void preparePlayback() {
initializePlayer(); player.retry();
} }
// PlaybackControlView.VisibilityListener implementation // PlaybackControlView.VisibilityListener implementation
@ -413,13 +403,8 @@ public class PlayerActivity extends Activity
boolean preferExtensionDecoders = boolean preferExtensionDecoders =
intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false); intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false);
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode = RenderersFactory renderersFactory =
((DemoApplication) getApplication()).useExtensionRenderers() ((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders);
? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
DefaultRenderersFactory renderersFactory =
new DefaultRenderersFactory(this, extensionRendererMode);
trackSelector = new DefaultTrackSelector(trackSelectionFactory); trackSelector = new DefaultTrackSelector(trackSelectionFactory);
trackSelector.setParameters(trackSelectorParameters); trackSelector.setParameters(trackSelectorParameters);
@ -464,7 +449,7 @@ public class PlayerActivity extends Activity
player.seekTo(startWindow, startPosition); player.seekTo(startWindow, startPosition);
} }
player.prepare(mediaSource, !haveStartPosition, false); player.prepare(mediaSource, !haveStartPosition, false);
updateButtonVisibilities(); updateButtonVisibility();
} }
private MediaSource buildMediaSource(Uri uri) { private MediaSource buildMediaSource(Uri uri) {
@ -473,24 +458,22 @@ public class PlayerActivity extends Activity
private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
@ContentType int type = Util.inferContentType(uri, overrideExtension); @ContentType int type = Util.inferContentType(uri, overrideExtension);
List<StreamKey> offlineStreamKeys = getOfflineStreamKeys(uri);
switch (type) { switch (type) {
case C.TYPE_DASH: case C.TYPE_DASH:
return new DashMediaSource.Factory(dataSourceFactory) return new DashMediaSource.Factory(dataSourceFactory)
.setManifestParser( .setStreamKeys(offlineStreamKeys)
new FilteringManifestParser<>(new DashManifestParser(), getOfflineStreamKeys(uri)))
.createMediaSource(uri); .createMediaSource(uri);
case C.TYPE_SS: case C.TYPE_SS:
return new SsMediaSource.Factory(dataSourceFactory) return new SsMediaSource.Factory(dataSourceFactory)
.setManifestParser( .setStreamKeys(offlineStreamKeys)
new FilteringManifestParser<>(new SsManifestParser(), getOfflineStreamKeys(uri)))
.createMediaSource(uri); .createMediaSource(uri);
case C.TYPE_HLS: case C.TYPE_HLS:
return new HlsMediaSource.Factory(dataSourceFactory) return new HlsMediaSource.Factory(dataSourceFactory)
.setPlaylistParserFactory( .setStreamKeys(offlineStreamKeys)
new DefaultHlsPlaylistParserFactory(getOfflineStreamKeys(uri)))
.createMediaSource(uri); .createMediaSource(uri);
case C.TYPE_OTHER: case C.TYPE_OTHER:
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri); return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
default: { default: {
throw new IllegalStateException("Unsupported type: " + type); throw new IllegalStateException("Unsupported type: " + type);
} }
@ -617,41 +600,9 @@ public class PlayerActivity extends Activity
// User controls // User controls
private void updateButtonVisibilities() { private void updateButtonVisibility() {
debugRootView.removeAllViews(); selectTracksButton.setEnabled(
if (player == null) { player != null && TrackSelectionDialog.willHaveContent(trackSelector));
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 showControls() { private void showControls() {
@ -687,16 +638,7 @@ public class PlayerActivity extends Activity
if (playbackState == Player.STATE_ENDED) { if (playbackState == Player.STATE_ENDED) {
showControls(); showControls();
} }
updateButtonVisibilities(); updateButtonVisibility();
}
@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();
}
} }
@Override @Override
@ -705,8 +647,7 @@ public class PlayerActivity extends Activity
clearStartPosition(); clearStartPosition();
initializePlayer(); initializePlayer();
} else { } else {
updateStartPosition(); updateButtonVisibility();
updateButtonVisibilities();
showControls(); showControls();
} }
} }
@ -714,7 +655,7 @@ public class PlayerActivity extends Activity
@Override @Override
@SuppressWarnings("ReferenceEquality") @SuppressWarnings("ReferenceEquality")
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
updateButtonVisibilities(); updateButtonVisibility();
if (trackGroups != lastSeenTrackGroupArray) { if (trackGroups != lastSeenTrackGroupArray) {
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo != null) { if (mappedTrackInfo != null) {

View File

@ -15,14 +15,14 @@
*/ */
package com.google.android.exoplayer2.demo; package com.google.android.exoplayer2.demo;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.res.AssetManager; import android.content.res.AssetManager;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.util.JsonReader; import android.util.JsonReader;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
@ -37,6 +37,7 @@ import android.widget.ImageButton;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSourceInputStream;
@ -54,7 +55,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
/** An activity for selecting from a list of media samples. */ /** An activity for selecting from a list of media samples. */
public class SampleChooserActivity extends Activity public class SampleChooserActivity extends AppCompatActivity
implements DownloadTracker.Listener, OnChildClickListener { implements DownloadTracker.Listener, OnChildClickListener {
private static final String TAG = "SampleChooserActivity"; private static final String TAG = "SampleChooserActivity";
@ -177,7 +178,15 @@ public class SampleChooserActivity extends Activity
.show(); .show();
} else { } else {
UriSample uriSample = (UriSample) sample; 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 ? null
: new DrmInfo(drmScheme, drmLicenseUrl, drmKeyRequestProperties, drmMultiSession); : new DrmInfo(drmScheme, drmLicenseUrl, drmKeyRequestProperties, drmMultiSession);
if (playlistSamples != null) { if (playlistSamples != null) {
UriSample[] playlistSamplesArray = playlistSamples.toArray( UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]);
new UriSample[playlistSamples.size()]);
return new PlaylistSample(sampleName, drmInfo, playlistSamplesArray); return new PlaylistSample(sampleName, drmInfo, playlistSamplesArray);
} else { } else {
return new UriSample( return new UriSample(

View File

@ -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<TrackSelectionViewFragment> tabFragments;
private final ArrayList<Integer> 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<SelectionOverride> 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<SelectionOverride> 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<SelectionOverride> 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<SelectionOverride> overrides) {
this.isDisabled = isDisabled;
this.overrides = overrides;
}
}
}

View File

@ -42,7 +42,15 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:visibility="gone"/> android:visibility="gone">
<Button android:id="@+id/select_tracks_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/track_selection_title"
android:enabled="false"/>
</LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.viewpager.widget.ViewPager
android:id="@+id/track_selection_dialog_view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<com.google.android.material.tabs.TabLayout
android:id="@+id/track_selection_dialog_tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabGravity="fill"
app:tabMode="fixed"/>
</androidx.viewpager.widget.ViewPager>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end">
<Button
android:id="@+id/track_selection_dialog_cancel_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/cancel"
style="?android:attr/borderlessButtonStyle"/>
<Button
android:id="@+id/track_selection_dialog_ok_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/ok"
style="?android:attr/borderlessButtonStyle"/>
</LinearLayout>
</LinearLayout>

View File

@ -13,13 +13,14 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/prefer_extension_decoders" <item android:id="@+id/prefer_extension_decoders"
android:title="@string/prefer_extension_decoders" android:title="@string/prefer_extension_decoders"
android:showAsAction="never" android:checkable="true"
android:checkable="true"/> app:showAsAction="never"/>
<item android:id="@+id/random_abr" <item android:id="@+id/random_abr"
android:title="@string/random_abr" android:title="@string/random_abr"
android:showAsAction="never" android:checkable="true"
android:checkable="true"/> app:showAsAction="never"/>
</menu> </menu>

View File

@ -17,6 +17,8 @@
<string name="application_name">ExoPlayer</string> <string name="application_name">ExoPlayer</string>
<string name="track_selection_title">Select tracks</string>
<string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string> <string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string>
<string name="error_cleartext_not_permitted">Cleartext traffic not permitted</string> <string name="error_cleartext_not_permitted">Cleartext traffic not permitted</string>

View File

@ -15,8 +15,11 @@
--> -->
<resources xmlns:android="http://schemas.android.com/apk/res/android"> <resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="PlayerTheme" parent="android:Theme.Holo"> <style name="TrackSelectionDialogThemeOverlay" parent="ThemeOverlay.AppCompat.Dialog.Alert">
<item name="android:windowNoTitle">true</item> <item name="windowNoTitle">false</item>
</style>
<style name="PlayerTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">@android:color/black</item> <item name="android:windowBackground">@android:color/black</item>
</style> </style>

View File

@ -5,7 +5,7 @@
The cast extension is a [Player][] implementation that controls playback on a The cast extension is a [Player][] implementation that controls playback on a
Cast receiver app. Cast receiver app.
[Player]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/Player.html [Player]: https://exoplayer.dev/doc/reference/index.html?com/google/android/exoplayer2/Player.html
## Getting the extension ## ## Getting the extension ##

View File

@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -24,32 +23,21 @@ android {
} }
defaultConfig { defaultConfig {
minSdkVersion 14 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
} }
testOptions.unitTests.includeAndroidResources = true
} }
dependencies { dependencies {
api 'com.google.android.gms:play-services-cast-framework:16.1.2' api 'com.google.android.gms:play-services-cast-framework:16.1.2'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion implementation 'androidx.annotation:annotation:1.0.2'
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
testImplementation project(modulePrefix + 'testutils') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
testImplementation 'junit:junit:' + junitVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils-robolectric')
// These dependencies are necessary to force the supportLibraryVersion of
// com.android.support:support-v4, com.android.support:appcompat-v7 and
// com.android.support:mediarouter-v7 to be used. Else older versions are
// used, for example via:
// com.google.android.gms:play-services-cast-framework:15.0.1
// |-- com.android.support:mediarouter-v7:26.1.0
api 'com.android.support:support-v4:' + supportLibraryVersion
api 'com.android.support:mediarouter-v7:' + supportLibraryVersion
api 'com.android.support:recyclerview-v7:' + supportLibraryVersion
} }
ext { ext {

View File

@ -1,4 +0,0 @@
# Proguard rules specific to the Cast extension.
# DefaultCastOptionsProvider is commonly referred to only by the app's manifest.
-keep class com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider

View File

@ -16,8 +16,8 @@
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import android.os.Looper; import android.os.Looper;
import android.support.annotation.NonNull; import androidx.annotation.NonNull;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.BasePlayer; import com.google.android.exoplayer2.BasePlayer;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
@ -52,35 +52,18 @@ import java.util.concurrent.CopyOnWriteArraySet;
* {@link Player} implementation that communicates with a Cast receiver app. * {@link Player} implementation that communicates with a Cast receiver app.
* *
* <p>The behavior of this class depends on the underlying Cast session, which is obtained from the * <p>The behavior of this class depends on the underlying Cast session, which is obtained from the
* Cast context passed to {@link #CastPlayer}. To keep track of the session, * Cast context passed to {@link #CastPlayer}. To keep track of the session, {@link
* {@link #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be * #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be
* implemented and attached to the player.</p> * implemented and attached to the player.
* *
* <p> If no session is available, the player state will remain unchanged and calls to methods that * <p>If no session is available, the player state will remain unchanged and calls to methods that
* alter it will be ignored. Querying the player state is possible even when no session is * alter it will be ignored. Querying the player state is possible even when no session is
* available, in which case, the last observed receiver app state is reported.</p> * available, in which case, the last observed receiver app state is reported.
* *
* <p>Methods should be called on the application's main thread.</p> * <p>Methods should be called on the application's main thread.
*/ */
public final class CastPlayer extends BasePlayer { public final class CastPlayer extends BasePlayer {
/**
* Listener of changes in the cast session availability.
*/
public interface SessionAvailabilityListener {
/**
* Called when a cast session becomes available to the player.
*/
void onCastSessionAvailable();
/**
* Called when the cast session becomes unavailable.
*/
void onCastSessionUnavailable();
}
private static final String TAG = "CastPlayer"; private static final String TAG = "CastPlayer";
private static final int RENDERER_COUNT = 3; private static final int RENDERER_COUNT = 3;
@ -591,7 +574,9 @@ public final class CastPlayer extends BasePlayer {
CastTimeline oldTimeline = currentTimeline; CastTimeline oldTimeline = currentTimeline;
MediaStatus status = getMediaStatus(); MediaStatus status = getMediaStatus();
currentTimeline = currentTimeline =
status != null ? timelineTracker.getCastTimeline(status) : CastTimeline.EMPTY_CAST_TIMELINE; status != null
? timelineTracker.getCastTimeline(remoteMediaClient)
: CastTimeline.EMPTY_CAST_TIMELINE;
return !oldTimeline.equals(currentTimeline); return !oldTimeline.equals(currentTimeline);
} }

View File

@ -15,24 +15,66 @@
*/ */
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import android.util.SparseArray;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/** /**
* A {@link Timeline} for Cast media queues. * A {@link Timeline} for Cast media queues.
*/ */
/* package */ final class CastTimeline extends Timeline { /* package */ final class CastTimeline extends Timeline {
/** Holds {@link Timeline} related data for a Cast media item. */
public static final class ItemData {
/** Holds no media information. */
public static final ItemData EMPTY = new ItemData();
/** The duration of the item in microseconds, or {@link C#TIME_UNSET} if unknown. */
public final long durationUs;
/**
* The default start position of the item in microseconds, or {@link C#TIME_UNSET} if unknown.
*/
public final long defaultPositionUs;
private ItemData() {
this(/* durationUs= */ C.TIME_UNSET, /* defaultPositionUs */ C.TIME_UNSET);
}
/**
* Creates an instance.
*
* @param durationUs See {@link #durationsUs}.
* @param defaultPositionUs See {@link #defaultPositionUs}.
*/
public ItemData(long durationUs, long defaultPositionUs) {
this.durationUs = durationUs;
this.defaultPositionUs = defaultPositionUs;
}
/** Returns an instance with the given {@link #durationsUs}. */
public ItemData copyWithDurationUs(long durationUs) {
if (durationUs == this.durationUs) {
return this;
}
return new ItemData(durationUs, defaultPositionUs);
}
/** Returns an instance with the given {@link #defaultPositionsUs}. */
public ItemData copyWithDefaultPositionUs(long defaultPositionUs) {
if (defaultPositionUs == this.defaultPositionUs) {
return this;
}
return new ItemData(durationUs, defaultPositionUs);
}
}
/** {@link Timeline} for a cast queue that has no items. */
public static final CastTimeline EMPTY_CAST_TIMELINE = public static final CastTimeline EMPTY_CAST_TIMELINE =
new CastTimeline(Collections.emptyList(), Collections.emptyMap()); new CastTimeline(new int[0], new SparseArray<>());
private final SparseIntArray idsToIndex; private final SparseIntArray idsToIndex;
private final int[] ids; private final int[] ids;
@ -40,28 +82,23 @@ import java.util.Map;
private final long[] defaultPositionsUs; private final long[] defaultPositionsUs;
/** /**
* @param items A list of cast media queue items to represent. * Creates a Cast timeline from the given data.
* @param contentIdToDurationUsMap A map of content id to duration in microseconds. *
* @param itemIds The ids of the items in the timeline.
* @param itemIdToData Maps item ids to {@link ItemData}.
*/ */
public CastTimeline(List<MediaQueueItem> items, Map<String, Long> contentIdToDurationUsMap) { public CastTimeline(int[] itemIds, SparseArray<ItemData> itemIdToData) {
int itemCount = items.size(); int itemCount = itemIds.length;
int index = 0;
idsToIndex = new SparseIntArray(itemCount); idsToIndex = new SparseIntArray(itemCount);
ids = new int[itemCount]; ids = Arrays.copyOf(itemIds, itemCount);
durationsUs = new long[itemCount]; durationsUs = new long[itemCount];
defaultPositionsUs = new long[itemCount]; defaultPositionsUs = new long[itemCount];
for (MediaQueueItem item : items) { for (int i = 0; i < ids.length; i++) {
int itemId = item.getItemId(); int id = ids[i];
ids[index] = itemId; idsToIndex.put(id, i);
idsToIndex.put(itemId, index); ItemData data = itemIdToData.get(id, ItemData.EMPTY);
MediaInfo mediaInfo = item.getMedia(); durationsUs[i] = data.durationUs;
String contentId = mediaInfo.getContentId(); defaultPositionsUs[i] = data.defaultPositionUs;
durationsUs[index] =
contentIdToDurationUsMap.containsKey(contentId)
? contentIdToDurationUsMap.get(contentId)
: CastUtils.getStreamDurationUs(mediaInfo);
defaultPositionsUs[index] = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
index++;
} }
} }
@ -108,7 +145,7 @@ import java.util.Map;
} }
@Override @Override
public Object getUidOfPeriod(int periodIndex) { public Integer getUidOfPeriod(int periodIndex) {
return ids[periodIndex]; return ids[periodIndex];
} }

View File

@ -15,53 +15,84 @@
*/ */
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import com.google.android.gms.cast.MediaInfo; import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.MediaStatus; import com.google.android.gms.cast.MediaStatus;
import java.util.HashMap; import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
/** /**
* Creates {@link CastTimeline}s from cast receiver app media status. * Creates {@link CastTimeline CastTimelines} from cast receiver app status updates.
* *
* <p>This class keeps track of the duration reported by the current item to fill any missing * <p>This class keeps track of the duration reported by the current item to fill any missing
* durations in the media queue items [See internal: b/65152553]. * durations in the media queue items [See internal: b/65152553].
*/ */
/* package */ final class CastTimelineTracker { /* package */ final class CastTimelineTracker {
private final HashMap<String, Long> contentIdToDurationUsMap; private final SparseArray<CastTimeline.ItemData> itemIdToData;
private final HashSet<String> scratchContentIdSet;
public CastTimelineTracker() { public CastTimelineTracker() {
contentIdToDurationUsMap = new HashMap<>(); itemIdToData = new SparseArray<>();
scratchContentIdSet = new HashSet<>();
} }
/** /**
* Returns a {@link CastTimeline} that represent the given {@code status}. * Returns a {@link CastTimeline} that represents the state of the given {@code
* remoteMediaClient}.
* *
* @param status The Cast media status. * <p>Returned timelines may contain values obtained from {@code remoteMediaClient} in previous
* @return A {@link CastTimeline} that represent the given {@code status}. * invocations of this method.
*
* @param remoteMediaClient The Cast media client.
* @return A {@link CastTimeline} that represents the given {@code remoteMediaClient} status.
*/ */
public CastTimeline getCastTimeline(MediaStatus status) { public CastTimeline getCastTimeline(RemoteMediaClient remoteMediaClient) {
MediaInfo mediaInfo = status.getMediaInfo(); int[] itemIds = remoteMediaClient.getMediaQueue().getItemIds();
List<MediaQueueItem> items = status.getQueueItems(); if (itemIds.length > 0) {
removeUnusedDurationEntries(items); // Only remove unused items when there is something in the queue to avoid removing all entries
// if the remote media client clears the queue temporarily. See [Internal ref: b/128825216].
if (mediaInfo != null) { removeUnusedItemDataEntries(itemIds);
String contentId = mediaInfo.getContentId();
long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
contentIdToDurationUsMap.put(contentId, durationUs);
} }
return new CastTimeline(items, contentIdToDurationUsMap);
// TODO: Reset state when the app instance changes [Internal ref: b/129672468].
MediaStatus mediaStatus = remoteMediaClient.getMediaStatus();
if (mediaStatus == null) {
return CastTimeline.EMPTY_CAST_TIMELINE;
}
int currentItemId = mediaStatus.getCurrentItemId();
long durationUs = CastUtils.getStreamDurationUs(mediaStatus.getMediaInfo());
itemIdToData.put(
currentItemId,
itemIdToData
.get(currentItemId, CastTimeline.ItemData.EMPTY)
.copyWithDurationUs(durationUs));
for (MediaQueueItem item : mediaStatus.getQueueItems()) {
int itemId = item.getItemId();
itemIdToData.put(
itemId,
itemIdToData
.get(itemId, CastTimeline.ItemData.EMPTY)
.copyWithDefaultPositionUs((long) (item.getStartTime() * C.MICROS_PER_SECOND)));
}
return new CastTimeline(itemIds, itemIdToData);
} }
private void removeUnusedDurationEntries(List<MediaQueueItem> items) { private void removeUnusedItemDataEntries(int[] itemIds) {
scratchContentIdSet.clear(); HashSet<Integer> scratchItemIds = new HashSet<>(/* initialCapacity= */ itemIds.length * 2);
for (MediaQueueItem item : items) { for (int id : itemIds) {
scratchContentIdSet.add(item.getMedia().getContentId()); scratchItemIds.add(id);
}
int index = 0;
while (index < itemIdToData.size()) {
if (!scratchItemIds.contains(itemIdToData.keyAt(index))) {
itemIdToData.removeAt(index);
} else {
index++;
}
} }
contentIdToDurationUsMap.keySet().retainAll(scratchContentIdSet);
} }
} }

View File

@ -31,11 +31,13 @@ import com.google.android.gms.cast.MediaTrack;
* unknown or not applicable. * unknown or not applicable.
* *
* @param mediaInfo The media info to get the duration from. * @param mediaInfo The media info to get the duration from.
* @return The duration in microseconds. * @return The duration in microseconds, or {@link C#TIME_UNSET} if unknown or not applicable.
*/ */
public static long getStreamDurationUs(MediaInfo mediaInfo) { public static long getStreamDurationUs(MediaInfo mediaInfo) {
long durationMs = if (mediaInfo == null) {
mediaInfo != null ? mediaInfo.getStreamDuration() : MediaInfo.UNKNOWN_DURATION; return C.TIME_UNSET;
}
long durationMs = mediaInfo.getStreamDuration();
return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET; return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET;
} }
@ -109,6 +111,7 @@ import com.google.android.gms.cast.MediaTrack;
/* codecs= */ null, /* codecs= */ null,
/* bitrate= */ Format.NO_VALUE, /* bitrate= */ Format.NO_VALUE,
/* selectionFlags= */ 0, /* selectionFlags= */ 0,
/* roleFlags= */ 0,
mediaTrack.getLanguage()); mediaTrack.getLanguage());
} }

View File

@ -0,0 +1,368 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cast;
import android.net.Uri;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.checkerframework.checker.initialization.qual.UnknownInitialization;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
/** Representation of an item that can be played by a media player. */
public final class MediaItem {
/** A builder for {@link MediaItem} instances. */
public static final class Builder {
@Nullable private UUID uuid;
private String title;
private String description;
private MediaItem.UriBundle media;
@Nullable private Object attachment;
private List<MediaItem.DrmScheme> drmSchemes;
private long startPositionUs;
private long endPositionUs;
private String mimeType;
/** Creates an builder with default field values. */
public Builder() {
clearInternal();
}
/** See {@link MediaItem#uuid}. */
public Builder setUuid(UUID uuid) {
this.uuid = uuid;
return this;
}
/** See {@link MediaItem#title}. */
public Builder setTitle(String title) {
this.title = title;
return this;
}
/** See {@link MediaItem#description}. */
public Builder setDescription(String description) {
this.description = description;
return this;
}
/** Equivalent to {@link #setMedia(UriBundle) setMedia(new UriBundle(Uri.parse(uri)))}. */
public Builder setMedia(String uri) {
return setMedia(new UriBundle(Uri.parse(uri)));
}
/** See {@link MediaItem#media}. */
public Builder setMedia(UriBundle media) {
this.media = media;
return this;
}
/** See {@link MediaItem#attachment}. */
public Builder setAttachment(Object attachment) {
this.attachment = attachment;
return this;
}
/** See {@link MediaItem#drmSchemes}. */
public Builder setDrmSchemes(List<MediaItem.DrmScheme> drmSchemes) {
this.drmSchemes = Collections.unmodifiableList(new ArrayList<>(drmSchemes));
return this;
}
/** See {@link MediaItem#startPositionUs}. */
public Builder setStartPositionUs(long startPositionUs) {
this.startPositionUs = startPositionUs;
return this;
}
/** See {@link MediaItem#endPositionUs}. */
public Builder setEndPositionUs(long endPositionUs) {
Assertions.checkArgument(endPositionUs != C.TIME_END_OF_SOURCE);
this.endPositionUs = endPositionUs;
return this;
}
/** See {@link MediaItem#mimeType}. */
public Builder setMimeType(String mimeType) {
this.mimeType = mimeType;
return this;
}
/**
* Equivalent to {@link #build()}, except it also calls {@link #clear()} after creating the
* {@link MediaItem}.
*/
public MediaItem buildAndClear() {
MediaItem item = build();
clearInternal();
return item;
}
/** Returns the builder to default values. */
public Builder clear() {
clearInternal();
return this;
}
/**
* Returns a new {@link MediaItem} instance with the current builder values. This method also
* clears any values passed to {@link #setUuid(UUID)}.
*/
public MediaItem build() {
UUID uuid = this.uuid;
this.uuid = null;
return new MediaItem(
uuid != null ? uuid : UUID.randomUUID(),
title,
description,
media,
attachment,
drmSchemes,
startPositionUs,
endPositionUs,
mimeType);
}
@EnsuresNonNull({"title", "description", "media", "drmSchemes", "mimeType"})
private void clearInternal(@UnknownInitialization Builder this) {
uuid = null;
title = "";
description = "";
media = UriBundle.EMPTY;
attachment = null;
drmSchemes = Collections.emptyList();
startPositionUs = C.TIME_UNSET;
endPositionUs = C.TIME_UNSET;
mimeType = "";
}
}
/** Bundles a resource's URI with headers to attach to any request to that URI. */
public static final class UriBundle {
/** An empty {@link UriBundle}. */
public static final UriBundle EMPTY = new UriBundle(Uri.EMPTY);
/** A URI. */
public final Uri uri;
/** The headers to attach to any request for the given URI. */
public final Map<String, String> requestHeaders;
/**
* Creates an instance with no request headers.
*
* @param uri See {@link #uri}.
*/
public UriBundle(Uri uri) {
this(uri, Collections.emptyMap());
}
/**
* Creates an instance with the given URI and request headers.
*
* @param uri See {@link #uri}.
* @param requestHeaders See {@link #requestHeaders}.
*/
public UriBundle(Uri uri, Map<String, String> requestHeaders) {
this.uri = uri;
this.requestHeaders = Collections.unmodifiableMap(new HashMap<>(requestHeaders));
}
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
UriBundle uriBundle = (UriBundle) other;
return uri.equals(uriBundle.uri) && requestHeaders.equals(uriBundle.requestHeaders);
}
@Override
public int hashCode() {
int result = uri.hashCode();
result = 31 * result + requestHeaders.hashCode();
return result;
}
}
/**
* Represents a DRM protection scheme, and optionally provides information about how to acquire
* the license for the media.
*/
public static final class DrmScheme {
/** The UUID of the protection scheme. */
public final UUID uuid;
/**
* Optional {@link UriBundle} for the license server. If no license server is provided, the
* server must be provided by the media.
*/
@Nullable public final UriBundle licenseServer;
/**
* Creates an instance.
*
* @param uuid See {@link #uuid}.
* @param licenseServer See {@link #licenseServer}.
*/
public DrmScheme(UUID uuid, @Nullable UriBundle licenseServer) {
this.uuid = uuid;
this.licenseServer = licenseServer;
}
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
DrmScheme drmScheme = (DrmScheme) other;
return uuid.equals(drmScheme.uuid) && Util.areEqual(licenseServer, drmScheme.licenseServer);
}
@Override
public int hashCode() {
int result = uuid.hashCode();
result = 31 * result + (licenseServer != null ? licenseServer.hashCode() : 0);
return result;
}
}
/**
* A UUID that identifies this item, potentially across different devices. The default value is
* obtained by calling {@link UUID#randomUUID()}.
*/
public final UUID uuid;
/** The title of the item. The default value is an empty string. */
public final String title;
/** A description for the item. The default value is an empty string. */
public final String description;
/**
* A {@link UriBundle} to fetch the media content. The default value is {@link UriBundle#EMPTY}.
*/
public final UriBundle media;
/**
* An optional opaque object to attach to the media item. Handling of this attachment is
* implementation specific. The default value is null.
*/
@Nullable public final Object attachment;
/**
* Immutable list of {@link DrmScheme} instances sorted in decreasing order of preference. The
* default value is an empty list.
*/
public final List<DrmScheme> drmSchemes;
/**
* The position in microseconds at which playback of this media item should start. {@link
* C#TIME_UNSET} if playback should start at the default position. The default value is {@link
* C#TIME_UNSET}.
*/
public final long startPositionUs;
/**
* The position in microseconds at which playback of this media item should end. {@link
* C#TIME_UNSET} if playback should end at the end of the media. The default value is {@link
* C#TIME_UNSET}.
*/
public final long endPositionUs;
/**
* The mime type of this media item. The default value is an empty string.
*
* <p>The usage of this mime type is optional and player implementation specific.
*/
public final String mimeType;
// TODO: Add support for sideloaded tracks, artwork, icon, and subtitle.
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
MediaItem mediaItem = (MediaItem) other;
return startPositionUs == mediaItem.startPositionUs
&& endPositionUs == mediaItem.endPositionUs
&& uuid.equals(mediaItem.uuid)
&& title.equals(mediaItem.title)
&& description.equals(mediaItem.description)
&& media.equals(mediaItem.media)
&& Util.areEqual(attachment, mediaItem.attachment)
&& drmSchemes.equals(mediaItem.drmSchemes)
&& mimeType.equals(mediaItem.mimeType);
}
@Override
public int hashCode() {
int result = uuid.hashCode();
result = 31 * result + title.hashCode();
result = 31 * result + description.hashCode();
result = 31 * result + media.hashCode();
result = 31 * result + (attachment != null ? attachment.hashCode() : 0);
result = 31 * result + drmSchemes.hashCode();
result = 31 * result + (int) (startPositionUs ^ (startPositionUs >>> 32));
result = 31 * result + (int) (endPositionUs ^ (endPositionUs >>> 32));
result = 31 * result + mimeType.hashCode();
return result;
}
private MediaItem(
UUID uuid,
String title,
String description,
UriBundle media,
@Nullable Object attachment,
List<DrmScheme> drmSchemes,
long startPositionUs,
long endPositionUs,
String mimeType) {
this.uuid = uuid;
this.title = title;
this.description = description;
this.media = media;
this.attachment = attachment;
this.drmSchemes = drmSchemes;
this.startPositionUs = startPositionUs;
this.endPositionUs = endPositionUs;
this.mimeType = mimeType;
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cast;
/** Represents a sequence of {@link MediaItem MediaItems}. */
public interface MediaItemQueue {
/**
* Returns the item at the given index.
*
* @param index The index of the item to retrieve.
* @return The item at the given index.
* @throws IndexOutOfBoundsException If {@code index < 0 || index >= getSize()}.
*/
MediaItem get(int index);
/** Returns the number of items in this queue. */
int getSize();
/**
* Appends the given sequence of items to the queue.
*
* @param items The sequence of items to append.
*/
void add(MediaItem... items);
/**
* Adds the given sequence of items to the queue at the given position, so that the first of
* {@code items} is placed at the given index.
*
* @param index The index at which {@code items} will be inserted.
* @param items The sequence of items to append.
* @throws IndexOutOfBoundsException If {@code index < 0 || index > getSize()}.
*/
void add(int index, MediaItem... items);
/**
* Moves an existing item within the playlist.
*
* <p>Calling this method is equivalent to removing the item at position {@code indexFrom} and
* immediately inserting it at position {@code indexTo}. If the moved item is being played at the
* moment of the invocation, playback will stick with the moved item.
*
* @param indexFrom The index of the item to move.
* @param indexTo The index at which the item will be placed after this operation.
* @throws IndexOutOfBoundsException If for either index, {@code index < 0 || index >= getSize()}.
*/
void move(int indexFrom, int indexTo);
/**
* Removes an item from the queue.
*
* @param index The index of the item to remove from the queue.
* @throws IndexOutOfBoundsException If {@code index < 0 || index >= getSize()}.
*/
void remove(int index);
/**
* Removes a range of items from the queue.
*
* <p>Does nothing if an empty range ({@code from == exclusiveTo}) is passed.
*
* @param from The inclusive index at which the range to remove starts.
* @param exclusiveTo The exclusive index at which the range to remove ends.
* @throws IndexOutOfBoundsException If {@code from < 0 || exclusiveTo > getSize() || from >
* exclusiveTo}.
*/
void removeRange(int from, int exclusiveTo);
/** Removes all items in the queue. */
void clear();
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cast;
/** Listener of changes in the cast session availability. */
public interface SessionAvailabilityListener {
/** Called when a cast session becomes available to the player. */
void onCastSessionAvailable();
/** Called when the cast session becomes unavailable. */
void onCastSessionUnavailable();
}

View File

@ -15,23 +15,23 @@
*/ */
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TimelineAsserts; import com.google.android.exoplayer2.testutil.TimelineAsserts;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.MediaStatus; import com.google.android.gms.cast.MediaStatus;
import java.util.ArrayList; import com.google.android.gms.cast.framework.media.MediaQueue;
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import java.util.Collections;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link CastTimelineTracker}. */ /** Tests for {@link CastTimelineTracker}. */
@RunWith(RobolectricTestRunner.class) @RunWith(AndroidJUnit4.class)
public class CastTimelineTrackerTest { public class CastTimelineTrackerTest {
private static final long DURATION_1_MS = 1000;
private static final long DURATION_2_MS = 2000; private static final long DURATION_2_MS = 2000;
private static final long DURATION_3_MS = 3000; private static final long DURATION_3_MS = 3000;
private static final long DURATION_4_MS = 4000; private static final long DURATION_4_MS = 4000;
@ -39,91 +39,89 @@ public class CastTimelineTrackerTest {
/** Tests that duration of the current media info is correctly propagated to the timeline. */ /** Tests that duration of the current media info is correctly propagated to the timeline. */
@Test @Test
public void testGetCastTimeline() { public void testGetCastTimelinePersistsDuration() {
MediaInfo mediaInfo;
MediaStatus status =
mockMediaStatus(
new int[] {1, 2, 3},
new String[] {"contentId1", "contentId2", "contentId3"},
new long[] {DURATION_1_MS, MediaInfo.UNKNOWN_DURATION, MediaInfo.UNKNOWN_DURATION});
CastTimelineTracker tracker = new CastTimelineTracker(); CastTimelineTracker tracker = new CastTimelineTracker();
mediaInfo = getMediaInfo("contentId1", DURATION_1_MS);
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(status), C.msToUs(DURATION_1_MS), C.TIME_UNSET, C.TIME_UNSET);
mediaInfo = getMediaInfo("contentId3", DURATION_3_MS); RemoteMediaClient remoteMediaClient =
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo); mockRemoteMediaClient(
/* itemIds= */ new int[] {1, 2, 3, 4, 5},
/* currentItemId= */ 2,
/* currentDurationMs= */ DURATION_2_MS);
TimelineAsserts.assertPeriodDurations( TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(status), tracker.getCastTimeline(remoteMediaClient),
C.msToUs(DURATION_1_MS),
C.TIME_UNSET, C.TIME_UNSET,
C.msToUs(DURATION_3_MS)); C.msToUs(DURATION_2_MS),
C.TIME_UNSET,
C.TIME_UNSET,
C.TIME_UNSET);
mediaInfo = getMediaInfo("contentId2", DURATION_2_MS); remoteMediaClient =
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo); mockRemoteMediaClient(
/* itemIds= */ new int[] {1, 2, 3},
/* currentItemId= */ 3,
/* currentDurationMs= */ DURATION_3_MS);
TimelineAsserts.assertPeriodDurations( TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(status), tracker.getCastTimeline(remoteMediaClient),
C.msToUs(DURATION_1_MS), C.TIME_UNSET,
C.msToUs(DURATION_2_MS), C.msToUs(DURATION_2_MS),
C.msToUs(DURATION_3_MS)); C.msToUs(DURATION_3_MS));
MediaStatus newStatus = remoteMediaClient =
mockMediaStatus( mockRemoteMediaClient(
new int[] {4, 1, 5, 3}, /* itemIds= */ new int[] {1, 3},
new String[] {"contentId4", "contentId1", "contentId5", "contentId3"}, /* currentItemId= */ 3,
new long[] { /* currentDurationMs= */ DURATION_3_MS);
MediaInfo.UNKNOWN_DURATION,
MediaInfo.UNKNOWN_DURATION,
DURATION_5_MS,
MediaInfo.UNKNOWN_DURATION
});
mediaInfo = getMediaInfo("contentId5", DURATION_5_MS);
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
TimelineAsserts.assertPeriodDurations( TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(newStatus), tracker.getCastTimeline(remoteMediaClient), C.TIME_UNSET, C.msToUs(DURATION_3_MS));
C.TIME_UNSET,
C.msToUs(DURATION_1_MS),
C.msToUs(DURATION_5_MS),
C.msToUs(DURATION_3_MS));
mediaInfo = getMediaInfo("contentId3", DURATION_3_MS); remoteMediaClient =
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo); mockRemoteMediaClient(
/* itemIds= */ new int[] {1, 2, 3, 4, 5},
/* currentItemId= */ 4,
/* currentDurationMs= */ DURATION_4_MS);
TimelineAsserts.assertPeriodDurations( TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(newStatus), tracker.getCastTimeline(remoteMediaClient),
C.TIME_UNSET, C.TIME_UNSET,
C.msToUs(DURATION_1_MS), C.TIME_UNSET,
C.msToUs(DURATION_5_MS), C.msToUs(DURATION_3_MS),
C.msToUs(DURATION_3_MS));
mediaInfo = getMediaInfo("contentId4", DURATION_4_MS);
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(newStatus),
C.msToUs(DURATION_4_MS), C.msToUs(DURATION_4_MS),
C.msToUs(DURATION_1_MS), C.TIME_UNSET);
C.msToUs(DURATION_5_MS),
C.msToUs(DURATION_3_MS)); remoteMediaClient =
mockRemoteMediaClient(
/* itemIds= */ new int[] {1, 2, 3, 4, 5},
/* currentItemId= */ 5,
/* currentDurationMs= */ DURATION_5_MS);
TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(remoteMediaClient),
C.TIME_UNSET,
C.TIME_UNSET,
C.msToUs(DURATION_3_MS),
C.msToUs(DURATION_4_MS),
C.msToUs(DURATION_5_MS));
} }
private static MediaStatus mockMediaStatus( private static RemoteMediaClient mockRemoteMediaClient(
int[] itemIds, String[] contentIds, long[] durationsMs) { int[] itemIds, int currentItemId, long currentDurationMs) {
ArrayList<MediaQueueItem> items = new ArrayList<>(); RemoteMediaClient remoteMediaClient = Mockito.mock(RemoteMediaClient.class);
for (int i = 0; i < contentIds.length; i++) {
MediaInfo mediaInfo = getMediaInfo(contentIds[i], durationsMs[i]);
MediaQueueItem item = Mockito.mock(MediaQueueItem.class);
Mockito.when(item.getMedia()).thenReturn(mediaInfo);
Mockito.when(item.getItemId()).thenReturn(itemIds[i]);
items.add(item);
}
MediaStatus status = Mockito.mock(MediaStatus.class); MediaStatus status = Mockito.mock(MediaStatus.class);
Mockito.when(status.getQueueItems()).thenReturn(items); Mockito.when(status.getQueueItems()).thenReturn(Collections.emptyList());
return status; Mockito.when(remoteMediaClient.getMediaStatus()).thenReturn(status);
Mockito.when(status.getMediaInfo()).thenReturn(getMediaInfo(currentDurationMs));
Mockito.when(status.getCurrentItemId()).thenReturn(currentItemId);
MediaQueue mediaQueue = mockMediaQueue(itemIds);
Mockito.when(remoteMediaClient.getMediaQueue()).thenReturn(mediaQueue);
return remoteMediaClient;
} }
private static MediaInfo getMediaInfo(String contentId, long durationMs) { private static MediaQueue mockMediaQueue(int[] itemIds) {
return new MediaInfo.Builder(contentId) MediaQueue mediaQueue = Mockito.mock(MediaQueue.class);
Mockito.when(mediaQueue.getItemIds()).thenReturn(itemIds);
return mediaQueue;
}
private static MediaInfo getMediaInfo(long durationMs) {
return new MediaInfo.Builder(/*contentId= */ "")
.setStreamDuration(durationMs) .setStreamDuration(durationMs)
.setContentType(MimeTypes.APPLICATION_MP4) .setContentType(MimeTypes.APPLICATION_MP4)
.setStreamType(MediaInfo.STREAM_TYPE_NONE) .setStreamType(MediaInfo.STREAM_TYPE_NONE)

View File

@ -0,0 +1,144 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cast;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.MimeTypes;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Test for {@link MediaItem}. */
@RunWith(AndroidJUnit4.class)
public class MediaItemTest {
@Test
public void buildMediaItem_resetsUuid() {
MediaItem.Builder builder = new MediaItem.Builder();
UUID uuid = new UUID(1, 1);
MediaItem item1 = builder.setUuid(uuid).build();
MediaItem item2 = builder.build();
MediaItem item3 = builder.build();
assertThat(item1.uuid).isEqualTo(uuid);
assertThat(item2.uuid).isNotEqualTo(uuid);
assertThat(item3.uuid).isNotEqualTo(item2.uuid);
assertThat(item3.uuid).isNotEqualTo(uuid);
}
@Test
public void buildMediaItem_doesNotChangeState() {
MediaItem.Builder builder = new MediaItem.Builder();
MediaItem item1 =
builder
.setUuid(new UUID(0, 1))
.setMedia("http://example.com")
.setTitle("title")
.setMimeType(MimeTypes.AUDIO_MP4)
.setStartPositionUs(3)
.setEndPositionUs(4)
.build();
MediaItem item2 = builder.setUuid(new UUID(0, 1)).build();
assertThat(item1).isEqualTo(item2);
}
@Test
public void buildMediaItem_assertDefaultValues() {
assertDefaultValues(new MediaItem.Builder().build());
}
@Test
public void buildAndClear_assertDefaultValues() {
MediaItem.Builder builder = new MediaItem.Builder();
builder
.setMedia("http://example.com")
.setTitle("title")
.setMimeType(MimeTypes.AUDIO_MP4)
.setStartPositionUs(3)
.setEndPositionUs(4)
.buildAndClear();
assertDefaultValues(builder.build());
}
@Test
public void equals_withEqualDrmSchemes_returnsTrue() {
MediaItem.Builder builder = new MediaItem.Builder();
MediaItem mediaItem1 =
builder
.setUuid(new UUID(0, 1))
.setMedia("www.google.com")
.setDrmSchemes(createDummyDrmSchemes(1))
.buildAndClear();
MediaItem mediaItem2 =
builder
.setUuid(new UUID(0, 1))
.setMedia("www.google.com")
.setDrmSchemes(createDummyDrmSchemes(1))
.buildAndClear();
assertThat(mediaItem1).isEqualTo(mediaItem2);
}
@Test
public void equals_withDifferentDrmRequestHeaders_returnsFalse() {
MediaItem.Builder builder = new MediaItem.Builder();
MediaItem mediaItem1 =
builder
.setUuid(new UUID(0, 1))
.setMedia("www.google.com")
.setDrmSchemes(createDummyDrmSchemes(1))
.buildAndClear();
MediaItem mediaItem2 =
builder
.setUuid(new UUID(0, 1))
.setMedia("www.google.com")
.setDrmSchemes(createDummyDrmSchemes(2))
.buildAndClear();
assertThat(mediaItem1).isNotEqualTo(mediaItem2);
}
private static void assertDefaultValues(MediaItem item) {
assertThat(item.title).isEmpty();
assertThat(item.description).isEmpty();
assertThat(item.media.uri).isEqualTo(Uri.EMPTY);
assertThat(item.attachment).isNull();
assertThat(item.drmSchemes).isEmpty();
assertThat(item.startPositionUs).isEqualTo(C.TIME_UNSET);
assertThat(item.endPositionUs).isEqualTo(C.TIME_UNSET);
assertThat(item.mimeType).isEmpty();
}
private static List<MediaItem.DrmScheme> createDummyDrmSchemes(int seed) {
HashMap<String, String> requestHeaders1 = new HashMap<>();
requestHeaders1.put("key1", "value1");
requestHeaders1.put("key2", "value1");
MediaItem.UriBundle uriBundle1 =
new MediaItem.UriBundle(Uri.parse("www.uri1.com"), requestHeaders1);
MediaItem.DrmScheme drmScheme1 = new MediaItem.DrmScheme(C.WIDEVINE_UUID, uriBundle1);
HashMap<String, String> requestHeaders2 = new HashMap<>();
requestHeaders2.put("key3", "value3");
requestHeaders2.put("key4", "valueWithSeed" + seed);
MediaItem.UriBundle uriBundle2 =
new MediaItem.UriBundle(Uri.parse("www.uri2.com"), requestHeaders2);
MediaItem.DrmScheme drmScheme2 = new MediaItem.DrmScheme(C.PLAYREADY_UUID, uriBundle2);
return Arrays.asList(drmScheme1, drmScheme2);
}
}

View File

@ -1 +0,0 @@
manifest=src/test/AndroidManifest.xml

View File

@ -2,7 +2,7 @@
The Cronet extension is an [HttpDataSource][] implementation using [Cronet][]. The Cronet extension is an [HttpDataSource][] implementation using [Cronet][].
[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html [HttpDataSource]: https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
[Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F [Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F
## Getting the extension ## ## Getting the extension ##
@ -52,4 +52,4 @@ respectively.
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.cronet.*` * [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.cronet.*`
belong to this module. belong to this module.
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html

View File

@ -16,10 +16,9 @@ apply plugin: 'com.android.library'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} }
@ -27,12 +26,14 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
testOptions.unitTests.includeAndroidResources = true
} }
dependencies { dependencies {
api 'org.chromium.net:cronet-embedded:71.3578.98' api 'org.chromium.net:cronet-embedded:73.3683.76'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'androidx.annotation:annotation:1.0.2'
testImplementation project(modulePrefix + 'library') testImplementation project(modulePrefix + 'library')
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils-robolectric')
} }

View File

@ -16,10 +16,11 @@
package com.google.android.exoplayer2.ext.cronet; package com.google.android.exoplayer2.ext.cronet;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.metadata.icy.IcyHeaders;
import com.google.android.exoplayer2.upstream.BaseDataSource; import com.google.android.exoplayer2.upstream.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
@ -493,6 +494,11 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
if (dataSpec.httpBody != null && !isContentTypeHeaderSet) { if (dataSpec.httpBody != null && !isContentTypeHeaderSet) {
throw new IOException("HTTP request with non-empty body must set Content-Type"); throw new IOException("HTTP request with non-empty body must set Content-Type");
} }
if (dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA)) {
requestBuilder.addHeader(
IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME,
IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE);
}
// Set the Range header. // Set the Range header.
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) { if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
StringBuilder rangeValue = new StringBuilder(); StringBuilder rangeValue = new StringBuilder();

View File

@ -15,7 +15,7 @@
*/ */
package com.google.android.exoplayer2.ext.cronet; package com.google.android.exoplayer2.ext.cronet;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;

View File

@ -16,7 +16,7 @@
package com.google.android.exoplayer2.ext.cronet; package com.google.android.exoplayer2.ext.cronet;
import android.content.Context; import android.content.Context;
import android.support.annotation.IntDef; import androidx.annotation.IntDef;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;

View File

@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Arrays; import java.util.Arrays;
@ -28,10 +29,9 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link ByteArrayUploadDataProvider}. */ /** Tests for {@link ByteArrayUploadDataProvider}. */
@RunWith(RobolectricTestRunner.class) @RunWith(AndroidJUnit4.class)
public final class ByteArrayUploadDataProviderTest { public final class ByteArrayUploadDataProviderTest {
private static final byte[] TEST_DATA = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; private static final byte[] TEST_DATA = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

View File

@ -31,6 +31,7 @@ import static org.mockito.Mockito.when;
import android.net.Uri; import android.net.Uri;
import android.os.ConditionVariable; import android.os.ConditionVariable;
import android.os.SystemClock; import android.os.SystemClock;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
@ -62,10 +63,9 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link CronetDataSource}. */ /** Tests for {@link CronetDataSource}. */
@RunWith(RobolectricTestRunner.class) @RunWith(AndroidJUnit4.class)
public final class CronetDataSourceTest { public final class CronetDataSourceTest {
private static final int TEST_CONNECT_TIMEOUT_MS = 100; private static final int TEST_CONNECT_TIMEOUT_MS = 100;

View File

@ -1 +0,0 @@
manifest=src/test/AndroidManifest.xml

View File

@ -147,11 +147,11 @@ then implement your own logic to use the renderer for a given track.
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
[#2781]: https://github.com/google/ExoPlayer/issues/2781 [#2781]: https://github.com/google/ExoPlayer/issues/2781
[Supported formats]: https://google.github.io/ExoPlayer/supported-formats.html#ffmpeg-extension [Supported formats]: https://exoplayer.dev/supported-formats.html#ffmpeg-extension
## Links ## ## Links ##
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ffmpeg.*` * [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ffmpeg.*`
belong to this module. belong to this module.
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html

View File

@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -33,12 +32,15 @@ android {
jniLibs.srcDir 'src/main/libs' jniLibs.srcDir 'src/main/libs'
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio. jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
} }
testOptions.unitTests.includeAndroidResources = true
} }
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'androidx.annotation:annotation:1.0.2'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'testutils-robolectric')
} }
ext { ext {

View File

@ -16,7 +16,7 @@
package com.google.android.exoplayer2.ext.ffmpeg; package com.google.android.exoplayer2.ext.ffmpeg;
import android.os.Handler; import android.os.Handler;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;

View File

@ -15,7 +15,7 @@
*/ */
package com.google.android.exoplayer2.ext.ffmpeg; package com.google.android.exoplayer2.ext.ffmpeg;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;

View File

@ -15,10 +15,11 @@
*/ */
package com.google.android.exoplayer2.ext.ffmpeg; package com.google.android.exoplayer2.ext.ffmpeg;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader; import com.google.android.exoplayer2.util.LibraryLoader;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
/** /**
@ -30,6 +31,8 @@ public final class FfmpegLibrary {
ExoPlayerLibraryInfo.registerModule("goog.exo.ffmpeg"); ExoPlayerLibraryInfo.registerModule("goog.exo.ffmpeg");
} }
private static final String TAG = "FfmpegLibrary";
private static final LibraryLoader LOADER = private static final LibraryLoader LOADER =
new LibraryLoader("avutil", "avresample", "avcodec", "ffmpeg"); new LibraryLoader("avutil", "avresample", "avcodec", "ffmpeg");
@ -69,7 +72,14 @@ public final class FfmpegLibrary {
return false; return false;
} }
String codecName = getCodecName(mimeType, encoding); String codecName = getCodecName(mimeType, encoding);
return codecName != null && ffmpegHasDecoder(codecName); if (codecName == null) {
return false;
}
if (!ffmpegHasDecoder(codecName)) {
Log.w(TAG, "No " + codecName + " decoder available. Check the FFmpeg build configuration.");
return false;
}
return true;
} }
/** /**

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest package="com.google.android.exoplayer2.ext.ffmpeg"/>

View File

@ -0,0 +1,33 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.ffmpeg;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.DefaultRenderersFactoryAsserts;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link DefaultRenderersFactoryTest} with {@link FfmpegAudioRenderer}. */
@RunWith(AndroidJUnit4.class)
public final class DefaultRenderersFactoryTest {
@Test
public void createRenderers_instantiatesVpxRenderer() {
DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
FfmpegAudioRenderer.class, C.TRACK_TYPE_AUDIO);
}
}

View File

@ -95,4 +95,4 @@ player, then implement your own logic to use the renderer for a given track.
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.flac.*` * [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.flac.*`
belong to this module. belong to this module.
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html

View File

@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -34,13 +33,15 @@ android {
jniLibs.srcDir 'src/main/libs' jniLibs.srcDir 'src/main/libs'
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio. jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
} }
testOptions.unitTests.includeAndroidResources = true
} }
dependencies { dependencies {
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
androidTestImplementation 'androidx.test:runner:' + testRunnerVersion implementation 'androidx.annotation:annotation:1.0.2'
androidTestImplementation project(modulePrefix + 'testutils') androidTestImplementation project(modulePrefix + 'testutils')
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils-robolectric')
} }

View File

@ -18,6 +18,9 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.ext.flac.test"> package="com.google.android.exoplayer2.ext.flac.test">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-sdk/>
<application android:debuggable="true" <application android:debuggable="true"
android:allowBackup="false" android:allowBackup="false"
tools:ignore="MissingApplicationIcon,HardcodedDebugMode"> tools:ignore="MissingApplicationIcon,HardcodedDebugMode">

View File

@ -16,22 +16,26 @@
package com.google.android.exoplayer2.ext.flac; package com.google.android.exoplayer2.ext.flac;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.test.InstrumentationTestCase; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.IOException; import java.io.IOException;
import org.junit.Before;
import org.junit.runner.RunWith;
/** Unit test for {@link FlacBinarySearchSeeker}. */ /** Unit test for {@link FlacBinarySearchSeeker}. */
public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase { @RunWith(AndroidJUnit4.class)
public final class FlacBinarySearchSeekerTest {
private static final String NOSEEKTABLE_FLAC = "bear_no_seek.flac"; private static final String NOSEEKTABLE_FLAC = "bear_no_seek.flac";
private static final int DURATION_US = 2_741_000; private static final int DURATION_US = 2_741_000;
@Override @Before
protected void setUp() throws Exception { public void setUp() {
super.setUp();
if (!FlacLibrary.isAvailable()) { if (!FlacLibrary.isAvailable()) {
fail("Flac library not available."); fail("Flac library not available.");
} }
@ -39,7 +43,8 @@ public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
public void testGetSeekMap_returnsSeekMapWithCorrectDuration() public void testGetSeekMap_returnsSeekMapWithCorrectDuration()
throws IOException, FlacDecoderException, InterruptedException { throws IOException, FlacDecoderException, InterruptedException {
byte[] data = TestUtil.getByteArray(getInstrumentation().getContext(), NOSEEKTABLE_FLAC); byte[] data =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NOSEEKTABLE_FLAC);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
FlacDecoderJni decoderJni = new FlacDecoderJni(); FlacDecoderJni decoderJni = new FlacDecoderJni();
@ -57,7 +62,8 @@ public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
public void testSetSeekTargetUs_returnsSeekPending() public void testSetSeekTargetUs_returnsSeekPending()
throws IOException, FlacDecoderException, InterruptedException { throws IOException, FlacDecoderException, InterruptedException {
byte[] data = TestUtil.getByteArray(getInstrumentation().getContext(), NOSEEKTABLE_FLAC); byte[] data =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NOSEEKTABLE_FLAC);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
FlacDecoderJni decoderJni = new FlacDecoderJni(); FlacDecoderJni decoderJni = new FlacDecoderJni();

View File

@ -16,11 +16,13 @@
package com.google.android.exoplayer2.ext.flac; package com.google.android.exoplayer2.ext.flac;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import android.test.InstrumentationTestCase; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
@ -38,9 +40,12 @@ import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import org.junit.Before;
import org.junit.runner.RunWith;
/** Seeking tests for {@link FlacExtractor} when the FLAC stream does not have a SEEKTABLE. */ /** Seeking tests for {@link FlacExtractor} when the FLAC stream does not have a SEEKTABLE. */
public final class FlacExtractorSeekTest extends InstrumentationTestCase { @RunWith(AndroidJUnit4.class)
public final class FlacExtractorSeekTest {
private static final String NO_SEEKTABLE_FLAC = "bear_no_seek.flac"; private static final String NO_SEEKTABLE_FLAC = "bear_no_seek.flac";
private static final int DURATION_US = 2_741_000; private static final int DURATION_US = 2_741_000;
@ -54,18 +59,18 @@ public final class FlacExtractorSeekTest extends InstrumentationTestCase {
private PositionHolder positionHolder; private PositionHolder positionHolder;
private long totalInputLength; private long totalInputLength;
@Override @Before
protected void setUp() throws Exception { public void setUp() throws Exception {
super.setUp();
if (!FlacLibrary.isAvailable()) { if (!FlacLibrary.isAvailable()) {
fail("Flac library not available."); fail("Flac library not available.");
} }
expectedOutput = new FakeExtractorOutput(); expectedOutput = new FakeExtractorOutput();
extractAllSamplesFromFileToExpectedOutput(getInstrumentation().getContext(), NO_SEEKTABLE_FLAC); extractAllSamplesFromFileToExpectedOutput(
ApplicationProvider.getApplicationContext(), NO_SEEKTABLE_FLAC);
expectedTrackOutput = expectedOutput.trackOutputs.get(0); expectedTrackOutput = expectedOutput.trackOutputs.get(0);
dataSource = dataSource =
new DefaultDataSourceFactory(getInstrumentation().getContext(), "UserAgent") new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), "UserAgent")
.createDataSource(); .createDataSource();
totalInputLength = readInputLength(); totalInputLength = readInputLength();
positionHolder = new PositionHolder(); positionHolder = new PositionHolder();

View File

@ -15,17 +15,20 @@
*/ */
package com.google.android.exoplayer2.ext.flac; package com.google.android.exoplayer2.ext.flac;
import android.test.InstrumentationTestCase; import static org.junit.Assert.fail;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import org.junit.Before;
import org.junit.runner.RunWith;
/** /** Unit test for {@link FlacExtractor}. */
* Unit test for {@link FlacExtractor}. @RunWith(AndroidJUnit4.class)
*/ public class FlacExtractorTest {
public class FlacExtractorTest extends InstrumentationTestCase {
@Override @Before
protected void setUp() throws Exception { public void setUp() throws Exception {
super.setUp();
if (!FlacLibrary.isAvailable()) { if (!FlacLibrary.isAvailable()) {
fail("Flac library not available."); fail("Flac library not available.");
} }
@ -33,11 +36,11 @@ public class FlacExtractorTest extends InstrumentationTestCase {
public void testExtractFlacSample() throws Exception { public void testExtractFlacSample() throws Exception {
ExtractorAsserts.assertBehavior( ExtractorAsserts.assertBehavior(
FlacExtractor::new, "bear.flac", getInstrumentation().getContext()); FlacExtractor::new, "bear.flac", ApplicationProvider.getApplicationContext());
} }
public void testExtractFlacSampleWithId3Header() throws Exception { public void testExtractFlacSampleWithId3Header() throws Exception {
ExtractorAsserts.assertBehavior( ExtractorAsserts.assertBehavior(
FlacExtractor::new, "bear_with_id3.flac", getInstrumentation().getContext()); FlacExtractor::new, "bear_with_id3.flac", ApplicationProvider.getApplicationContext());
} }
} }

View File

@ -15,21 +15,21 @@
*/ */
package com.google.android.exoplayer2.ext.flac; package com.google.android.exoplayer2.ext.flac;
import static androidx.test.InstrumentationRegistry.getContext;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.os.Looper; import android.os.Looper;
import androidx.test.runner.AndroidJUnit4; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.junit.Before; import org.junit.Before;
@ -56,7 +56,7 @@ public class FlacPlaybackTest {
private void playUri(String uri) throws Exception { private void playUri(String uri) throws Exception {
TestPlaybackRunnable testPlaybackRunnable = TestPlaybackRunnable testPlaybackRunnable =
new TestPlaybackRunnable(Uri.parse(uri), getContext()); new TestPlaybackRunnable(Uri.parse(uri), ApplicationProvider.getApplicationContext());
Thread thread = new Thread(testPlaybackRunnable); Thread thread = new Thread(testPlaybackRunnable);
thread.start(); thread.start();
thread.join(); thread.join();
@ -83,12 +83,12 @@ public class FlacPlaybackTest {
Looper.prepare(); Looper.prepare();
LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer(); LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer();
DefaultTrackSelector trackSelector = new DefaultTrackSelector(); DefaultTrackSelector trackSelector = new DefaultTrackSelector();
player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector); player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this); player.addListener(this);
MediaSource mediaSource = MediaSource mediaSource =
new ExtractorMediaSource.Factory( new ProgressiveMediaSource.Factory(
new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest")) new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"),
.setExtractorsFactory(MatroskaExtractor.FACTORY) MatroskaExtractor.FACTORY)
.createMediaSource(uri); .createMediaSource(uri);
player.prepare(mediaSource); player.prepare(mediaSource);
player.setPlayWhenReady(true); player.setPlayWhenReady(true);

View File

@ -17,8 +17,8 @@ package com.google.android.exoplayer2.ext.flac;
import static com.google.android.exoplayer2.util.Util.getPcmEncoding; import static com.google.android.exoplayer2.util.Util.getPcmEncoding;
import android.support.annotation.IntDef; import androidx.annotation.IntDef;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.BinarySearchSeeker; import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
@ -94,7 +94,7 @@ public final class FlacExtractor implements Extractor {
/** Constructs an instance with flags = 0. */ /** Constructs an instance with flags = 0. */
public FlacExtractor() { public FlacExtractor() {
this(0); this(/* flags= */ 0);
} }
/** /**

View File

@ -42,7 +42,9 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
* @param eventListener A listener of events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/ */
public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, public LibflacAudioRenderer(
Handler eventHandler,
AudioRendererEventListener eventListener,
AudioProcessor... audioProcessors) { AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, audioProcessors); super(eventHandler, eventListener, audioProcessors);
} }

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.flac;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.amr.AmrExtractor; import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
@ -27,6 +28,7 @@ import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
import com.google.android.exoplayer2.extractor.ogg.OggExtractor; import com.google.android.exoplayer2.extractor.ogg.OggExtractor;
import com.google.android.exoplayer2.extractor.ts.Ac3Extractor; import com.google.android.exoplayer2.extractor.ts.Ac3Extractor;
import com.google.android.exoplayer2.extractor.ts.Ac4Extractor;
import com.google.android.exoplayer2.extractor.ts.AdtsExtractor; import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
import com.google.android.exoplayer2.extractor.ts.PsExtractor; import com.google.android.exoplayer2.extractor.ts.PsExtractor;
import com.google.android.exoplayer2.extractor.ts.TsExtractor; import com.google.android.exoplayer2.extractor.ts.TsExtractor;
@ -35,10 +37,9 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Unit test for {@link DefaultExtractorsFactory}. */ /** Unit test for {@link DefaultExtractorsFactory}. */
@RunWith(RobolectricTestRunner.class) @RunWith(AndroidJUnit4.class)
public final class DefaultExtractorsFactoryTest { public final class DefaultExtractorsFactoryTest {
@Test @Test
@ -59,6 +60,7 @@ public final class DefaultExtractorsFactoryTest {
Mp3Extractor.class, Mp3Extractor.class,
AdtsExtractor.class, AdtsExtractor.class,
Ac3Extractor.class, Ac3Extractor.class,
Ac4Extractor.class,
TsExtractor.class, TsExtractor.class,
FlvExtractor.class, FlvExtractor.class,
OggExtractor.class, OggExtractor.class,

View File

@ -0,0 +1,33 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.flac;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.DefaultRenderersFactoryAsserts;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link DefaultRenderersFactoryTest} with {@link LibflacAudioRenderer}. */
@RunWith(AndroidJUnit4.class)
public final class DefaultRenderersFactoryTest {
@Test
public void createRenderers_instantiatesVpxRenderer() {
DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
LibflacAudioRenderer.class, C.TRACK_TYPE_AUDIO);
}
}

View File

@ -1 +0,0 @@
manifest=src/test/AndroidManifest.xml

View File

@ -37,4 +37,4 @@ locally. Instructions for doing this can be found in ExoPlayer's
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.gvr.*` * [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.gvr.*`
belong to this module. belong to this module.
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html

View File

@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -27,11 +26,14 @@ android {
minSdkVersion 19 minSdkVersion 19
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} }
testOptions.unitTests.includeAndroidResources = true
} }
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation project(modulePrefix + 'library-ui')
implementation 'androidx.annotation:annotation:1.0.2'
api 'com.google.vr:sdk-base:1.190.0' api 'com.google.vr:sdk-base:1.190.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
} }

View File

@ -15,7 +15,7 @@
*/ */
package com.google.android.exoplayer2.ext.gvr; package com.google.android.exoplayer2.ext.gvr;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
@ -38,9 +38,11 @@ public final class GvrAudioProcessor implements AudioProcessor {
private static final int FRAMES_PER_OUTPUT_BUFFER = 1024; private static final int FRAMES_PER_OUTPUT_BUFFER = 1024;
private static final int OUTPUT_CHANNEL_COUNT = 2; private static final int OUTPUT_CHANNEL_COUNT = 2;
private static final int OUTPUT_FRAME_SIZE = OUTPUT_CHANNEL_COUNT * 2; // 16-bit stereo output. private static final int OUTPUT_FRAME_SIZE = OUTPUT_CHANNEL_COUNT * 2; // 16-bit stereo output.
private static final int NO_SURROUND_FORMAT = GvrAudioSurround.SurroundFormat.INVALID;
private int sampleRateHz; private int sampleRateHz;
private int channelCount; private int channelCount;
private int pendingGvrAudioSurroundFormat;
@Nullable private GvrAudioSurround gvrAudioSurround; @Nullable private GvrAudioSurround gvrAudioSurround;
private ByteBuffer buffer; private ByteBuffer buffer;
private boolean inputEnded; private boolean inputEnded;
@ -57,6 +59,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
sampleRateHz = Format.NO_VALUE; sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE; channelCount = Format.NO_VALUE;
buffer = EMPTY_BUFFER; buffer = EMPTY_BUFFER;
pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;
} }
/** /**
@ -92,33 +95,28 @@ public final class GvrAudioProcessor implements AudioProcessor {
} }
this.sampleRateHz = sampleRateHz; this.sampleRateHz = sampleRateHz;
this.channelCount = channelCount; this.channelCount = channelCount;
maybeReleaseGvrAudioSurround();
int surroundFormat;
switch (channelCount) { switch (channelCount) {
case 1: case 1:
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_MONO; pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_MONO;
break; break;
case 2: case 2:
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO; pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO;
break; break;
case 4: case 4:
surroundFormat = GvrAudioSurround.SurroundFormat.FIRST_ORDER_AMBISONICS; pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.FIRST_ORDER_AMBISONICS;
break; break;
case 6: case 6:
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_FIVE_DOT_ONE; pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_FIVE_DOT_ONE;
break; break;
case 9: case 9:
surroundFormat = GvrAudioSurround.SurroundFormat.SECOND_ORDER_AMBISONICS; pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SECOND_ORDER_AMBISONICS;
break; break;
case 16: case 16:
surroundFormat = GvrAudioSurround.SurroundFormat.THIRD_ORDER_AMBISONICS; pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.THIRD_ORDER_AMBISONICS;
break; break;
default: default:
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
} }
gvrAudioSurround = new GvrAudioSurround(surroundFormat, sampleRateHz, channelCount,
FRAMES_PER_OUTPUT_BUFFER);
gvrAudioSurround.updateNativeOrientation(w, x, y, z);
if (buffer == EMPTY_BUFFER) { if (buffer == EMPTY_BUFFER) {
buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE) buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE)
.order(ByteOrder.nativeOrder()); .order(ByteOrder.nativeOrder());
@ -128,7 +126,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
@Override @Override
public boolean isActive() { public boolean isActive() {
return gvrAudioSurround != null; return pendingGvrAudioSurroundFormat != NO_SURROUND_FORMAT || gvrAudioSurround != null;
} }
@Override @Override
@ -156,14 +154,17 @@ public final class GvrAudioProcessor implements AudioProcessor {
@Override @Override
public void queueEndOfStream() { public void queueEndOfStream() {
Assertions.checkNotNull(gvrAudioSurround); if (gvrAudioSurround != null) {
gvrAudioSurround.triggerProcessing();
}
inputEnded = true; inputEnded = true;
gvrAudioSurround.triggerProcessing();
} }
@Override @Override
public ByteBuffer getOutput() { public ByteBuffer getOutput() {
Assertions.checkNotNull(gvrAudioSurround); if (gvrAudioSurround == null) {
return EMPTY_BUFFER;
}
int writtenBytes = gvrAudioSurround.getOutput(buffer, 0, buffer.capacity()); int writtenBytes = gvrAudioSurround.getOutput(buffer, 0, buffer.capacity());
buffer.position(0).limit(writtenBytes); buffer.position(0).limit(writtenBytes);
return buffer; return buffer;
@ -171,13 +172,20 @@ public final class GvrAudioProcessor implements AudioProcessor {
@Override @Override
public boolean isEnded() { public boolean isEnded() {
Assertions.checkNotNull(gvrAudioSurround); return inputEnded
return inputEnded && gvrAudioSurround.getAvailableOutputSize() == 0; && (gvrAudioSurround == null || gvrAudioSurround.getAvailableOutputSize() == 0);
} }
@Override @Override
public void flush() { public void flush() {
if (gvrAudioSurround != null) { if (pendingGvrAudioSurroundFormat != NO_SURROUND_FORMAT) {
maybeReleaseGvrAudioSurround();
gvrAudioSurround =
new GvrAudioSurround(
pendingGvrAudioSurroundFormat, sampleRateHz, channelCount, FRAMES_PER_OUTPUT_BUFFER);
gvrAudioSurround.updateNativeOrientation(w, x, y, z);
pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;
} else if (gvrAudioSurround != null) {
gvrAudioSurround.flush(); gvrAudioSurround.flush();
} }
inputEnded = false; inputEnded = false;
@ -191,13 +199,13 @@ public final class GvrAudioProcessor implements AudioProcessor {
sampleRateHz = Format.NO_VALUE; sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE; channelCount = Format.NO_VALUE;
buffer = EMPTY_BUFFER; buffer = EMPTY_BUFFER;
pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;
} }
private void maybeReleaseGvrAudioSurround() { private void maybeReleaseGvrAudioSurround() {
if (this.gvrAudioSurround != null) { if (gvrAudioSurround != null) {
GvrAudioSurround gvrAudioSurround = this.gvrAudioSurround;
this.gvrAudioSurround = null;
gvrAudioSurround.release(); gvrAudioSurround.release();
gvrAudioSurround = null;
} }
} }

View File

@ -0,0 +1,355 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.gvr;
import android.content.Context;
import android.content.Intent;
import android.graphics.SurfaceTexture;
import android.opengl.Matrix;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.BinderThread;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import android.view.ContextThemeWrapper;
import android.view.MotionEvent;
import android.view.Surface;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.spherical.GlViewGroup;
import com.google.android.exoplayer2.ui.spherical.PointerRenderer;
import com.google.android.exoplayer2.ui.spherical.SceneRenderer;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import com.google.vr.ndk.base.DaydreamApi;
import com.google.vr.sdk.base.AndroidCompat;
import com.google.vr.sdk.base.Eye;
import com.google.vr.sdk.base.GvrActivity;
import com.google.vr.sdk.base.GvrView;
import com.google.vr.sdk.base.HeadTransform;
import com.google.vr.sdk.base.Viewport;
import com.google.vr.sdk.controller.Controller;
import com.google.vr.sdk.controller.ControllerManager;
import javax.microedition.khronos.egl.EGLConfig;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Base activity for VR 360 video playback. */
public abstract class GvrPlayerActivity extends GvrActivity {
private static final int EXIT_FROM_VR_REQUEST_CODE = 42;
private final Handler mainHandler;
@Nullable private Player player;
@MonotonicNonNull private GlViewGroup glView;
@MonotonicNonNull private ControllerManager controllerManager;
@MonotonicNonNull private SurfaceTexture surfaceTexture;
@MonotonicNonNull private Surface surface;
@MonotonicNonNull private SceneRenderer scene;
@MonotonicNonNull private PlayerControlView playerControl;
public GvrPlayerActivity() {
mainHandler = new Handler(Looper.getMainLooper());
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setScreenAlwaysOn(true);
GvrView gvrView = new GvrView(this);
// Since videos typically have fewer pixels per degree than the phones, reducing the render
// target scaling factor reduces the work required to render the scene.
gvrView.setRenderTargetScale(.5f);
// If a custom theme isn't specified, the Context's theme is used. For VR Activities, this is
// the old Android default theme rather than a modern theme. Override this with a custom theme.
Context theme = new ContextThemeWrapper(this, R.style.VrTheme);
glView = new GlViewGroup(theme, R.layout.vr_ui);
playerControl = Assertions.checkNotNull(glView.findViewById(R.id.controller));
playerControl.setShowVrButton(true);
playerControl.setVrButtonListener(v -> exit());
PointerRenderer pointerRenderer = new PointerRenderer();
scene = new SceneRenderer();
Renderer renderer = new Renderer(scene, glView, pointerRenderer);
// Attach glView to gvrView in order to properly handle UI events.
gvrView.addView(glView, 0);
// Standard GvrView configuration
gvrView.setEGLConfigChooser(
8, 8, 8, 8, // RGBA bits.
16, // Depth bits.
0); // Stencil bits.
gvrView.setRenderer(renderer);
setContentView(gvrView);
// Most Daydream phones can render a 4k video at 60fps in sustained performance mode. These
// options can be tweaked along with the render target scale.
if (gvrView.setAsyncReprojectionEnabled(true)) {
AndroidCompat.setSustainedPerformanceMode(this, true);
}
// Handle the user clicking on the 'X' in the top left corner. Since this is done when the user
// has taken the headset out of VR, it should launch the app's exit flow directly rather than
// using the transition flow.
gvrView.setOnCloseButtonListener(this::finish);
ControllerManager.EventListener listener =
new ControllerManager.EventListener() {
@Override
public void onApiStatusChanged(int status) {
// Do nothing.
}
@Override
public void onRecentered() {
// TODO if in cardboard mode call gvrView.recenterHeadTracker();
glView.post(() -> Util.castNonNull(playerControl).show());
}
};
controllerManager = new ControllerManager(this, listener);
Controller controller = controllerManager.getController();
ControllerEventListener controllerEventListener =
new ControllerEventListener(controller, pointerRenderer, glView);
controller.setEventListener(controllerEventListener);
}
/**
* Sets the {@link Player} to use.
*
* @param newPlayer The {@link Player} to use, or {@code null} to detach the current player.
*/
protected void setPlayer(@Nullable Player newPlayer) {
Assertions.checkNotNull(scene);
if (player == newPlayer) {
return;
}
if (player != null) {
Player.VideoComponent videoComponent = player.getVideoComponent();
if (videoComponent != null) {
if (surface != null) {
videoComponent.clearVideoSurface(surface);
}
videoComponent.clearVideoFrameMetadataListener(scene);
videoComponent.clearCameraMotionListener(scene);
}
}
player = newPlayer;
if (player != null) {
Player.VideoComponent videoComponent = player.getVideoComponent();
if (videoComponent != null) {
videoComponent.setVideoFrameMetadataListener(scene);
videoComponent.setCameraMotionListener(scene);
videoComponent.setVideoSurface(surface);
}
}
Assertions.checkNotNull(playerControl).setPlayer(player);
}
/**
* Sets the default stereo mode. If the played video doesn't contain a stereo mode the default one
* is used.
*
* @param stereoMode A {@link C.StereoMode} value.
*/
protected void setDefaultStereoMode(@C.StereoMode int stereoMode) {
Assertions.checkNotNull(scene).setDefaultStereoMode(stereoMode);
}
@CallSuper
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent unused) {
if (requestCode == EXIT_FROM_VR_REQUEST_CODE && resultCode == RESULT_OK) {
finish();
}
}
@Override
protected void onResume() {
super.onResume();
Util.castNonNull(controllerManager).start();
}
@Override
protected void onPause() {
Util.castNonNull(controllerManager).stop();
super.onPause();
}
@Override
protected void onDestroy() {
setPlayer(null);
releaseSurface(surfaceTexture, surface);
super.onDestroy();
}
/** Tries to exit gracefully from VR using a VR transition dialog. */
@SuppressWarnings("nullness:argument.type.incompatible")
protected void exit() {
// This needs to use GVR's exit transition to avoid disorienting the user.
DaydreamApi api = DaydreamApi.create(this);
if (api != null) {
api.exitFromVr(this, EXIT_FROM_VR_REQUEST_CODE, null);
// Eventually, the Activity's onActivityResult will be called.
api.close();
} else {
finish();
}
}
/** Toggles PlayerControl visibility. */
@UiThread
protected void togglePlayerControlVisibility() {
if (Assertions.checkNotNull(playerControl).isVisible()) {
playerControl.hide();
} else {
playerControl.show();
}
}
// Called on GL thread.
private void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture) {
mainHandler.post(
() -> {
SurfaceTexture oldSurfaceTexture = this.surfaceTexture;
Surface oldSurface = this.surface;
this.surfaceTexture = surfaceTexture;
this.surface = new Surface(surfaceTexture);
if (player != null) {
Player.VideoComponent videoComponent = player.getVideoComponent();
if (videoComponent != null) {
videoComponent.setVideoSurface(surface);
}
}
releaseSurface(oldSurfaceTexture, oldSurface);
});
}
private static void releaseSurface(
@Nullable SurfaceTexture oldSurfaceTexture, @Nullable Surface oldSurface) {
if (oldSurfaceTexture != null) {
oldSurfaceTexture.release();
}
if (oldSurface != null) {
oldSurface.release();
}
}
private class Renderer implements GvrView.StereoRenderer {
private static final float Z_NEAR = .1f;
private static final float Z_FAR = 100;
private final float[] viewProjectionMatrix = new float[16];
private final SceneRenderer scene;
private final GlViewGroup glView;
private final PointerRenderer pointerRenderer;
public Renderer(SceneRenderer scene, GlViewGroup glView, PointerRenderer pointerRenderer) {
this.scene = scene;
this.glView = glView;
this.pointerRenderer = pointerRenderer;
}
@Override
public void onNewFrame(HeadTransform headTransform) {}
@Override
public void onDrawEye(Eye eye) {
Matrix.multiplyMM(
viewProjectionMatrix, 0, eye.getPerspective(Z_NEAR, Z_FAR), 0, eye.getEyeView(), 0);
scene.drawFrame(viewProjectionMatrix, eye.getType() == Eye.Type.RIGHT);
if (glView.isVisible()) {
glView.getRenderer().draw(viewProjectionMatrix);
pointerRenderer.draw(viewProjectionMatrix);
}
}
@Override
public void onFinishFrame(Viewport viewport) {}
@Override
public void onSurfaceCreated(EGLConfig config) {
onSurfaceTextureAvailable(scene.init());
glView.getRenderer().init();
pointerRenderer.init();
}
@Override
public void onSurfaceChanged(int width, int height) {}
@Override
public void onRendererShutdown() {
glView.getRenderer().shutdown();
pointerRenderer.shutdown();
scene.shutdown();
}
}
private class ControllerEventListener extends Controller.EventListener {
private final Controller controller;
private final PointerRenderer pointerRenderer;
private final GlViewGroup glView;
private final float[] controllerOrientationMatrix;
private boolean clickButtonDown;
private boolean appButtonDown;
public ControllerEventListener(
Controller controller, PointerRenderer pointerRenderer, GlViewGroup glView) {
this.controller = controller;
this.pointerRenderer = pointerRenderer;
this.glView = glView;
controllerOrientationMatrix = new float[16];
}
@Override
@BinderThread
public void onUpdate() {
controller.update();
controller.orientation.toRotationMatrix(controllerOrientationMatrix);
pointerRenderer.setControllerOrientation(controllerOrientationMatrix);
if (clickButtonDown || controller.clickButtonState) {
int action;
if (clickButtonDown != controller.clickButtonState) {
clickButtonDown = controller.clickButtonState;
action = clickButtonDown ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP;
} else {
action = MotionEvent.ACTION_MOVE;
}
glView.post(
() -> {
float[] angles = controller.orientation.toYawPitchRollRadians(new float[3]);
boolean clickedOnView = glView.simulateClick(action, angles[0], angles[1]);
if (action == MotionEvent.ACTION_DOWN && !clickedOnView) {
togglePlayerControlVisibility();
}
});
} else if (!appButtonDown && controller.appButtonState) {
glView.post(GvrPlayerActivity.this::togglePlayerControlVisibility);
}
appButtonDown = controller.appButtonState;
}
}
}

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/video_ui_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/black"
android:orientation="horizontal"
tools:ignore="Overdraw">
<com.google.android.exoplayer2.ui.PlayerControlView
android:id="@+id/controller"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</merge>

View File

@ -13,7 +13,6 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<ListView xmlns:android="http://schemas.android.com/apk/res/android" <resources>
android:id="@+id/representation_list" <style name="VrTheme" parent="android:Theme.Material"/>
android:layout_width="match_parent" </resources>
android:layout_height="match_parent"/>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources>
<style name="VrTheme" parent="android:Theme.Holo"/>
</resources>

View File

@ -5,7 +5,7 @@ The IMA extension is an [AdsLoader][] implementation wrapping the
alongside content. alongside content.
[IMA]: https://developers.google.com/interactive-media-ads/docs/sdks/android/ [IMA]: https://developers.google.com/interactive-media-ads/docs/sdks/android/
[AdsLoader]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/source/ads/AdsLoader.html [AdsLoader]: https://exoplayer.dev/doc/reference/index.html?com/google/android/exoplayer2/source/ads/AdsLoader.html
## Getting the extension ## ## Getting the extension ##
@ -61,4 +61,4 @@ playback.
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ima.*` * [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ima.*`
belong to this module. belong to this module.
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html

View File

@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -28,23 +27,14 @@ android {
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt' consumerProguardFiles 'proguard-rules.txt'
} }
testOptions.unitTests.includeAndroidResources = true
} }
dependencies { dependencies {
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.10.6' api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'com.google.android.gms:play-services-ads:17.1.2' implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0'
// These dependencies are necessary to force the supportLibraryVersion of
// com.android.support:support-v4 and com.android.support:customtabs to be
// used. Else older versions are used, for example via:
// com.google.android.gms:play-services-ads:17.1.2
// |-- com.android.support:customtabs:26.1.0
implementation 'com.android.support:support-v4:' + supportLibraryVersion
implementation 'com.android.support:customtabs:' + supportLibraryVersion
testImplementation 'com.google.truth:truth:' + truthVersion
testImplementation 'junit:junit:' + junitVersion
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils-robolectric')
} }

View File

@ -19,8 +19,9 @@ import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.os.Looper; import android.os.Looper;
import android.os.SystemClock; import android.os.SystemClock;
import android.support.annotation.IntDef; import androidx.annotation.IntDef;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.google.ads.interactivemedia.v3.api.Ad; import com.google.ads.interactivemedia.v3.api.Ad;
@ -216,7 +217,7 @@ public final class ImaAdsLoader
return this; return this;
} }
// @VisibleForTesting @VisibleForTesting
/* package */ Builder setImaFactory(ImaFactory imaFactory) { /* package */ Builder setImaFactory(ImaFactory imaFactory) {
this.imaFactory = Assertions.checkNotNull(imaFactory); this.imaFactory = Assertions.checkNotNull(imaFactory);
return this; return this;
@ -755,7 +756,8 @@ public final class ImaAdsLoader
// until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered // until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered
// just after an ad group isn't incorrectly attributed to the next ad group. // just after an ad group isn't incorrectly attributed to the next ad group.
int nextAdGroupIndex = int nextAdGroupIndex =
adPlaybackState.getAdGroupIndexAfterPositionUs(C.msToUs(contentPositionMs)); adPlaybackState.getAdGroupIndexAfterPositionUs(
C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));
if (nextAdGroupIndex != expectedAdGroupIndex && nextAdGroupIndex != C.INDEX_UNSET) { if (nextAdGroupIndex != expectedAdGroupIndex && nextAdGroupIndex != C.INDEX_UNSET) {
long nextAdGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[nextAdGroupIndex]); long nextAdGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[nextAdGroupIndex]);
if (nextAdGroupTimeMs == C.TIME_END_OF_SOURCE) { if (nextAdGroupTimeMs == C.TIME_END_OF_SOURCE) {
@ -1389,7 +1391,7 @@ public final class ImaAdsLoader
} }
/** Factory for objects provided by the IMA SDK. */ /** Factory for objects provided by the IMA SDK. */
// @VisibleForTesting @VisibleForTesting
/* package */ interface ImaFactory { /* package */ interface ImaFactory {
/** @see ImaSdkSettings */ /** @see ImaSdkSettings */
ImaSdkSettings createImaSdkSettings(); ImaSdkSettings createImaSdkSettings();

View File

@ -22,10 +22,12 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.ads.interactivemedia.v3.api.Ad; import com.google.ads.interactivemedia.v3.api.Ad;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
import com.google.ads.interactivemedia.v3.api.AdEvent; import com.google.ads.interactivemedia.v3.api.AdEvent;
@ -54,11 +56,9 @@ import org.junit.runner.RunWith;
import org.mockito.InOrder; import org.mockito.InOrder;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
/** Test for {@link ImaAdsLoader}. */ /** Test for {@link ImaAdsLoader}. */
@RunWith(RobolectricTestRunner.class) @RunWith(AndroidJUnit4.class)
public class ImaAdsLoaderTest { public class ImaAdsLoaderTest {
private static final long CONTENT_DURATION_US = 10 * C.MICROS_PER_SECOND; private static final long CONTENT_DURATION_US = 10 * C.MICROS_PER_SECOND;
@ -95,8 +95,8 @@ public class ImaAdsLoaderTest {
adDisplayContainer, adDisplayContainer,
fakeAdsRequest, fakeAdsRequest,
fakeAdsLoader); fakeAdsLoader);
adViewGroup = new FrameLayout(RuntimeEnvironment.application); adViewGroup = new FrameLayout(ApplicationProvider.getApplicationContext());
adOverlayView = new View(RuntimeEnvironment.application); adOverlayView = new View(ApplicationProvider.getApplicationContext());
adViewProvider = adViewProvider =
new AdsLoader.AdViewProvider() { new AdsLoader.AdViewProvider() {
@Override @Override
@ -237,7 +237,7 @@ public class ImaAdsLoaderTest {
adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline, adDurationsUs); adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline, adDurationsUs);
when(adsManager.getAdCuePoints()).thenReturn(Arrays.asList(cuePoints)); when(adsManager.getAdCuePoints()).thenReturn(Arrays.asList(cuePoints));
imaAdsLoader = imaAdsLoader =
new ImaAdsLoader.Builder(RuntimeEnvironment.application) new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
.setImaFactory(testImaFactory) .setImaFactory(testImaFactory)
.setImaSdkSettings(imaSdkSettings) .setImaSdkSettings(imaSdkSettings)
.buildForAdTag(TEST_URI); .buildForAdTag(TEST_URI);

View File

@ -1 +0,0 @@
manifest=src/test/AndroidManifest.xml

View File

@ -18,7 +18,6 @@ apply plugin: 'com.android.library'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -29,6 +28,8 @@ android {
minSdkVersion project.ext.minSdkVersion minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} }
testOptions.unitTests.includeAndroidResources = true
} }
dependencies { dependencies {

View File

@ -57,6 +57,7 @@ import com.google.android.exoplayer2.util.Util;
*/ */
public final class JobDispatcherScheduler implements Scheduler { public final class JobDispatcherScheduler implements Scheduler {
private static final boolean DEBUG = false;
private static final String TAG = "JobDispatcherScheduler"; private static final String TAG = "JobDispatcherScheduler";
private static final String KEY_SERVICE_ACTION = "service_action"; private static final String KEY_SERVICE_ACTION = "service_action";
private static final String KEY_SERVICE_PACKAGE = "service_package"; private static final String KEY_SERVICE_PACKAGE = "service_package";
@ -78,8 +79,8 @@ public final class JobDispatcherScheduler implements Scheduler {
} }
@Override @Override
public boolean schedule(Requirements requirements, String serviceAction, String servicePackage) { public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) {
Job job = buildJob(jobDispatcher, requirements, jobTag, serviceAction, servicePackage); Job job = buildJob(jobDispatcher, requirements, jobTag, servicePackage, serviceAction);
int result = jobDispatcher.schedule(job); int result = jobDispatcher.schedule(job);
logd("Scheduling job: " + jobTag + " result: " + result); logd("Scheduling job: " + jobTag + " result: " + result);
return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS; return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS;
@ -96,26 +97,18 @@ public final class JobDispatcherScheduler implements Scheduler {
FirebaseJobDispatcher dispatcher, FirebaseJobDispatcher dispatcher,
Requirements requirements, Requirements requirements,
String tag, String tag,
String serviceAction, String servicePackage,
String servicePackage) { String serviceAction) {
Job.Builder builder = Job.Builder builder =
dispatcher dispatcher
.newJobBuilder() .newJobBuilder()
.setService(JobDispatcherSchedulerService.class) // the JobService that will be called .setService(JobDispatcherSchedulerService.class) // the JobService that will be called
.setTag(tag); .setTag(tag);
switch (requirements.getRequiredNetworkType()) { if (requirements.isUnmeteredNetworkRequired()) {
case Requirements.NETWORK_TYPE_NONE: builder.addConstraint(Constraint.ON_UNMETERED_NETWORK);
// do nothing. } else if (requirements.isNetworkRequired()) {
break; builder.addConstraint(Constraint.ON_ANY_NETWORK);
case Requirements.NETWORK_TYPE_ANY:
builder.addConstraint(Constraint.ON_ANY_NETWORK);
break;
case Requirements.NETWORK_TYPE_UNMETERED:
builder.addConstraint(Constraint.ON_UNMETERED_NETWORK);
break;
default:
throw new UnsupportedOperationException();
} }
if (requirements.isIdleRequired()) { if (requirements.isIdleRequired()) {
@ -129,7 +122,7 @@ public final class JobDispatcherScheduler implements Scheduler {
Bundle extras = new Bundle(); Bundle extras = new Bundle();
extras.putString(KEY_SERVICE_ACTION, serviceAction); extras.putString(KEY_SERVICE_ACTION, serviceAction);
extras.putString(KEY_SERVICE_PACKAGE, servicePackage); extras.putString(KEY_SERVICE_PACKAGE, servicePackage);
extras.putInt(KEY_REQUIREMENTS, requirements.getRequirementsData()); extras.putInt(KEY_REQUIREMENTS, requirements.getRequirements());
builder.setExtras(extras); builder.setExtras(extras);
return builder.build(); return builder.build();

View File

@ -28,4 +28,4 @@ locally. Instructions for doing this can be found in ExoPlayer's
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.leanback.*` * [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.leanback.*`
belong to this module. belong to this module.
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html

View File

@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -27,11 +26,14 @@ android {
minSdkVersion 17 minSdkVersion 17
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} }
testOptions.unitTests.includeAndroidResources = true
} }
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation('com.android.support:leanback-v17:' + supportLibraryVersion) implementation 'androidx.annotation:annotation:1.0.2'
implementation 'androidx.leanback:leanback:1.0.0'
} }
ext { ext {

View File

@ -17,11 +17,11 @@ package com.google.android.exoplayer2.ext.leanback;
import android.content.Context; import android.content.Context;
import android.os.Handler; import android.os.Handler;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import android.support.v17.leanback.R; import androidx.leanback.R;
import android.support.v17.leanback.media.PlaybackGlueHost; import androidx.leanback.media.PlaybackGlueHost;
import android.support.v17.leanback.media.PlayerAdapter; import androidx.leanback.media.PlayerAdapter;
import android.support.v17.leanback.media.SurfaceHolderGlueHost; import androidx.leanback.media.SurfaceHolderGlueHost;
import android.util.Pair; import android.util.Pair;
import android.view.Surface; import android.view.Surface;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;

View File

@ -29,4 +29,4 @@ locally. Instructions for doing this can be found in ExoPlayer's
* [Javadoc][]: Classes matching * [Javadoc][]: Classes matching
`com.google.android.exoplayer2.ext.mediasession.*` belong to this module. `com.google.android.exoplayer2.ext.mediasession.*` belong to this module.
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html

View File

@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -27,11 +26,13 @@ android {
minSdkVersion project.ext.minSdkVersion minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} }
testOptions.unitTests.includeAndroidResources = true
} }
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
api 'com.android.support:support-media-compat:' + supportLibraryVersion api 'androidx.media:media:1.0.1'
} }
ext { ext {

View File

@ -1,173 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.mediasession;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.support.v4.media.session.PlaybackStateCompat;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.util.RepeatModeUtil;
/**
* A default implementation of {@link MediaSessionConnector.PlaybackController}.
* <p>
* Methods can be safely overridden by subclasses to intercept calls for given actions.
*/
public class DefaultPlaybackController implements MediaSessionConnector.PlaybackController {
/**
* The default fast forward increment, in milliseconds.
*/
public static final int DEFAULT_FAST_FORWARD_MS = 15000;
/**
* The default rewind increment, in milliseconds.
*/
public static final int DEFAULT_REWIND_MS = 5000;
private static final long BASE_ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE
| PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE
| PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
| PlaybackStateCompat.ACTION_SET_REPEAT_MODE;
protected final long rewindIncrementMs;
protected final long fastForwardIncrementMs;
protected final int repeatToggleModes;
/**
* Creates a new instance.
* <p>
* Equivalent to {@code DefaultPlaybackController(DefaultPlaybackController.DEFAULT_REWIND_MS,
* DefaultPlaybackController.DEFAULT_FAST_FORWARD_MS,
* MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES)}.
*/
public DefaultPlaybackController() {
this(DEFAULT_REWIND_MS, DEFAULT_FAST_FORWARD_MS,
MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES);
}
/**
* Creates a new instance with the given fast forward and rewind increments.
* @param rewindIncrementMs The rewind increment in milliseconds. A zero or negative value will
* cause the rewind action to be disabled.
* @param fastForwardIncrementMs The fast forward increment in milliseconds. A zero or negative
* @param repeatToggleModes The available repeatToggleModes.
*/
public DefaultPlaybackController(long rewindIncrementMs, long fastForwardIncrementMs,
@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
this.rewindIncrementMs = rewindIncrementMs;
this.fastForwardIncrementMs = fastForwardIncrementMs;
this.repeatToggleModes = repeatToggleModes;
}
@Override
public long getSupportedPlaybackActions(Player player) {
if (player == null || player.getCurrentTimeline().isEmpty()) {
return 0;
} else if (!player.isCurrentWindowSeekable()) {
return BASE_ACTIONS;
}
long actions = BASE_ACTIONS | PlaybackStateCompat.ACTION_SEEK_TO;
if (fastForwardIncrementMs > 0) {
actions |= PlaybackStateCompat.ACTION_FAST_FORWARD;
}
if (rewindIncrementMs > 0) {
actions |= PlaybackStateCompat.ACTION_REWIND;
}
return actions;
}
@Override
public void onPlay(Player player) {
player.setPlayWhenReady(true);
}
@Override
public void onPause(Player player) {
player.setPlayWhenReady(false);
}
@Override
public void onSeekTo(Player player, long position) {
long duration = player.getDuration();
if (duration != C.TIME_UNSET) {
position = Math.min(position, duration);
}
player.seekTo(Math.max(position, 0));
}
@Override
public void onFastForward(Player player) {
if (fastForwardIncrementMs <= 0) {
return;
}
onSeekTo(player, player.getCurrentPosition() + fastForwardIncrementMs);
}
@Override
public void onRewind(Player player) {
if (rewindIncrementMs <= 0) {
return;
}
onSeekTo(player, player.getCurrentPosition() - rewindIncrementMs);
}
@Override
public void onStop(Player player) {
player.stop(true);
}
@Override
public void onSetShuffleMode(Player player, int shuffleMode) {
player.setShuffleModeEnabled(shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL
|| shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP);
}
@Override
public void onSetRepeatMode(Player player, int repeatMode) {
int selectedExoPlayerRepeatMode = player.getRepeatMode();
switch (repeatMode) {
case PlaybackStateCompat.REPEAT_MODE_ALL:
case PlaybackStateCompat.REPEAT_MODE_GROUP:
if ((repeatToggleModes & RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL) != 0) {
selectedExoPlayerRepeatMode = Player.REPEAT_MODE_ALL;
}
break;
case PlaybackStateCompat.REPEAT_MODE_ONE:
if ((repeatToggleModes & RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE) != 0) {
selectedExoPlayerRepeatMode = Player.REPEAT_MODE_ONE;
}
break;
default:
selectedExoPlayerRepeatMode = Player.REPEAT_MODE_OFF;
break;
}
player.setRepeatMode(selectedExoPlayerRepeatMode);
}
// CommandReceiver implementation.
@Override
public String[] getCommands() {
return null;
}
@Override
public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
// Do nothing.
}
}

View File

@ -18,17 +18,20 @@ package com.google.android.exoplayer2.ext.mediasession;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.media.session.PlaybackStateCompat; import android.support.v4.media.session.PlaybackStateCompat;
import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.util.RepeatModeUtil; import com.google.android.exoplayer2.util.RepeatModeUtil;
/** /** Provides a custom action for toggling repeat modes. */
* Provides a custom action for toggling repeat modes.
*/
public final class RepeatModeActionProvider implements MediaSessionConnector.CustomActionProvider { public final class RepeatModeActionProvider implements MediaSessionConnector.CustomActionProvider {
/** The default repeat toggle modes. */
@RepeatModeUtil.RepeatToggleModes
public static final int DEFAULT_REPEAT_TOGGLE_MODES =
RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL;
private static final String ACTION_REPEAT_MODE = "ACTION_EXO_REPEAT_MODE"; private static final String ACTION_REPEAT_MODE = "ACTION_EXO_REPEAT_MODE";
private final Player player;
@RepeatModeUtil.RepeatToggleModes @RepeatModeUtil.RepeatToggleModes
private final int repeatToggleModes; private final int repeatToggleModes;
private final CharSequence repeatAllDescription; private final CharSequence repeatAllDescription;
@ -37,27 +40,23 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus
/** /**
* Creates a new instance. * Creates a new instance.
* <p> *
* Equivalent to {@code RepeatModeActionProvider(context, player, * <p>Equivalent to {@code RepeatModeActionProvider(context, DEFAULT_REPEAT_TOGGLE_MODES)}.
* MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES)}.
* *
* @param context The context. * @param context The context.
* @param player The player on which to toggle the repeat mode.
*/ */
public RepeatModeActionProvider(Context context, Player player) { public RepeatModeActionProvider(Context context) {
this(context, player, MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES); this(context, DEFAULT_REPEAT_TOGGLE_MODES);
} }
/** /**
* Creates a new instance enabling the given repeat toggle modes. * Creates a new instance enabling the given repeat toggle modes.
* *
* @param context The context. * @param context The context.
* @param player The player on which to toggle the repeat mode.
* @param repeatToggleModes The toggle modes to enable. * @param repeatToggleModes The toggle modes to enable.
*/ */
public RepeatModeActionProvider(Context context, Player player, public RepeatModeActionProvider(
@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { Context context, @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
this.player = player;
this.repeatToggleModes = repeatToggleModes; this.repeatToggleModes = repeatToggleModes;
repeatAllDescription = context.getString(R.string.exo_media_action_repeat_all_description); repeatAllDescription = context.getString(R.string.exo_media_action_repeat_all_description);
repeatOneDescription = context.getString(R.string.exo_media_action_repeat_one_description); repeatOneDescription = context.getString(R.string.exo_media_action_repeat_one_description);
@ -65,16 +64,17 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus
} }
@Override @Override
public void onCustomAction(String action, Bundle extras) { public void onCustomAction(
Player player, ControlDispatcher controlDispatcher, String action, Bundle extras) {
int mode = player.getRepeatMode(); int mode = player.getRepeatMode();
int proposedMode = RepeatModeUtil.getNextRepeatMode(mode, repeatToggleModes); int proposedMode = RepeatModeUtil.getNextRepeatMode(mode, repeatToggleModes);
if (mode != proposedMode) { if (mode != proposedMode) {
player.setRepeatMode(proposedMode); controlDispatcher.dispatchSetRepeatMode(player, proposedMode);
} }
} }
@Override @Override
public PlaybackStateCompat.CustomAction getCustomAction() { public PlaybackStateCompat.CustomAction getCustomAction(Player player) {
CharSequence actionLabel; CharSequence actionLabel;
int iconResourceId; int iconResourceId;
switch (player.getRepeatMode()) { switch (player.getRepeatMode()) {

Some files were not shown because too many files have changed in this diff Show More