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.
- Consult our FAQs, supported devices and supported formats pages. These can be
found at https://google.github.io/ExoPlayer/.
found at https://exoplayer.dev/.
- Rule out issues in your own code. A good way to do this is to try and
reproduce the issue in the ExoPlayer demo app. Information about the ExoPlayer
demo app can be found here:
http://google.github.io/ExoPlayer/demo-application.html.
http://exoplayer.dev/demo-application.html.
When reporting a bug:
-----------------------

View File

@ -10,10 +10,10 @@ Before filing a content issue:
------------------------------
- Search existing issues, including issues that are closed.
- Consult our supported formats page, which can be found at
https://google.github.io/ExoPlayer/supported-formats.html.
https://exoplayer.dev/supported-formats.html.
- Try playing your content in the ExoPlayer demo app. Information about the
ExoPlayer demo app can be found here:
http://google.github.io/ExoPlayer/demo-application.html.
http://exoplayer.dev/demo-application.html.
When reporting a content issue:
-----------------------------

View File

@ -13,7 +13,7 @@ Before filing a question:
- Search existing issues, including issues that are closed. Its often the
quickest way to get an answer!
- Consult our FAQs, developer guide and the class reference of ExoPlayer. These
can be found at https://google.github.io/ExoPlayer/.
can be found at https://exoplayer.dev/.
When filing a question:
-----------------------

9
.gitignore vendored
View File

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

View File

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

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
developments!
[developer guide]: https://google.github.io/ExoPlayer/guide.html
[class reference]: https://google.github.io/ExoPlayer/doc/reference
[developer guide]: https://exoplayer.dev/guide.html
[class reference]: https://exoplayer.dev/doc/reference
[release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
[developer blog]: https://medium.com/google-exoplayer
@ -95,20 +95,6 @@ compileOptions {
}
```
Note that if you want to use Java 8 features in your own code, the following
additional options need to be set:
```gradle
// For Java compilers:
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
}
// For Kotlin compilers:
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
```
### Locally ###
Cloning the repository and depending on the modules locally is required when

View File

@ -1,5 +1,129 @@
# Release notes #
### 2.10.0 ###
* Core library:
* Improve decoder re-use between playbacks
([#2826](https://github.com/google/ExoPlayer/issues/2826)). Read
[this blog post](https://medium.com/google-exoplayer/improved-decoder-reuse-in-exoplayer-ef4c6d99591d)
for more details.
* Rename `ExtractorMediaSource` to `ProgressiveMediaSource`.
* Fix issue where using `ProgressiveMediaSource.Factory` would mean that
`DefaultExtractorsFactory` would be kept by proguard. Custom
`ExtractorsFactory` instances must now be passed via the
`ProgressiveMediaSource.Factory` constructor, and `setExtractorsFactory` is
deprecated.
* Move `PriorityTaskManager` from `DefaultLoadControl` to `SimpleExoPlayer`.
* Add new `ExoPlaybackException` types for remote exceptions and out-of-memory
errors.
* Use full BCP 47 language tags in `Format`.
* Do not retry failed loads whose error is `FileNotFoundException`.
* Fix issue where not resetting the position for a new `MediaSource` in calls
to `ExoPlayer.prepare` causes an `IndexOutOfBoundsException`
([#5520](https://github.com/google/ExoPlayer/issues/5520)).
* Offline:
* Improve offline support. `DownloadManager` now tracks all offline content,
not just tasks in progress. Read
[this page](https://exoplayer.dev/downloading-media.html) for more details.
* Caching:
* Improve performance of `SimpleCache`
([#4253](https://github.com/google/ExoPlayer/issues/4253)).
* Cache data with unknown length by default. The previous flag to opt in to
this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been
replaced with an opt out flag
(`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`).
* Extractors:
* MP4/FMP4: Add support for Dolby Vision.
* MP4: Fix issue handling meta atoms in some streams
([#5698](https://github.com/google/ExoPlayer/issues/5698),
[#5694](https://github.com/google/ExoPlayer/issues/5694)).
* MP3: Add support for SHOUTcast ICY metadata
([#3735](https://github.com/google/ExoPlayer/issues/3735)).
* MP3: Fix ID3 frame unsychronization
([#5673](https://github.com/google/ExoPlayer/issues/5673)).
* MP3: Fix playback of badly clipped files
([#5772](https://github.com/google/ExoPlayer/issues/5772)).
* MPEG-TS: Enable HDMV DTS stream detection only if a flag is set. By default
(i.e. if the flag is not set), the 0x82 elementary stream type is now
treated as an SCTE subtitle track
([#5330](https://github.com/google/ExoPlayer/issues/5330)).
* Track selection:
* Add options for controlling audio track selections to `DefaultTrackSelector`
([#3314](https://github.com/google/ExoPlayer/issues/3314)).
* Update `TrackSelection.Factory` interface to support creating all track
selections together.
* Allow to specify a selection reason for a `SelectionOverride`.
* When no text language preference matches, only select forced text tracks
whose language matches the selected audio language.
* UI:
* Update `DefaultTimeBar` based on duration of media and add parameter to set
the minimum update interval to control the smoothness of the updates
([#5040](https://github.com/google/ExoPlayer/issues/5040)).
* Move creation of dialogs for `TrackSelectionView`s to
`TrackSelectionDialogBuilder` and add option to select multiple overrides.
* Change signature of `PlayerNotificationManager.NotificationListener` to
better fit service requirements.
* Add option to include navigation actions in the compact mode of
notifications created using `PlayerNotificationManager`.
* Fix issues with flickering notifications on KitKat when using
`PlayerNotificationManager` and `DownloadNotificationUtil`. For the latter,
applications should switch to using `DownloadNotificationHelper`.
* Fix accuracy of D-pad seeking in `DefaultTimeBar`
([#5767](https://github.com/google/ExoPlayer/issues/5767)).
* Audio:
* Allow `AudioProcessor`s to be drained of pending output after they are
reconfigured.
* Fix an issue that caused audio to be truncated at the end of a period
when switching to a new period where gapless playback information was newly
present or newly absent.
* Add support for reading AC-4 streams
([#5303](https://github.com/google/ExoPlayer/pull/5303)).
* Video:
* Remove `MediaCodecSelector.DEFAULT_WITH_FALLBACK`. Apps should instead
signal that fallback should be used by passing `true` as the
`enableDecoderFallback` parameter when instantiating the video renderer.
* Support video tunneling when the decoder is not listed first for the MIME
type ([#3100](https://github.com/google/ExoPlayer/issues/3100)).
* Query `MediaCodecList.ALL_CODECS` when selecting a tunneling decoder
([#5547](https://github.com/google/ExoPlayer/issues/5547)).
* DRM:
* Fix black flicker when keys rotate in DRM protected content
([#3561](https://github.com/google/ExoPlayer/issues/3561)).
* Work around lack of LA_URL attribute in PlayReady key request init data.
* CEA-608: Improved conformance to the specification
([#3860](https://github.com/google/ExoPlayer/issues/3860)).
* DASH:
* Parse role and accessibility descriptors into `Format.roleFlags`.
* Support multiple CEA-608 channels muxed into FMP4 representations
([#5656](https://github.com/google/ExoPlayer/issues/5656)).
* HLS:
* Prevent unnecessary reloads of initialization segments.
* Form an adaptive track group out of audio renditions with matching name.
* Support encrypted initialization segments
([#5441](https://github.com/google/ExoPlayer/issues/5441)).
* Parse `EXT-X-MEDIA` `CHARACTERISTICS` attribute into `Format.roleFlags`.
* Add metadata entry for HLS tracks to expose master playlist information.
* Prevent `IndexOutOfBoundsException` in some live HLS scenarios
([#5816](https://github.com/google/ExoPlayer/issues/5816)).
* Support for playing spherical videos on Daydream.
* Cast extension: Work around Cast framework returning a limited-size queue
items list ([#4964](https://github.com/google/ExoPlayer/issues/4964)).
* VP9 extension: Remove RGB output mode and libyuv dependency, and switch to
surface YUV output as the default. Remove constructor parameters `scaleToFit`
and `useSurfaceYuvOutput`.
* MediaSession extension:
* Let apps intercept media button events
([#5179](https://github.com/google/ExoPlayer/issues/5179)).
* Fix issue with `TimelineQueueNavigator` not publishing the queue in shuffled
order when in shuffle mode.
* Allow handling of custom commands via `registerCustomCommandReceiver`.
* Add ability to include an extras `Bundle` when reporting a custom error.
* LoadControl: Set minimum buffer for playbacks with video equal to maximum
buffer ([#2083](https://github.com/google/ExoPlayer/issues/2083)).
* Log warnings when extension native libraries can't be used, to help with
diagnosing playback failures
([#5788](https://github.com/google/ExoPlayer/issues/5788)).
### 2.9.6 ###
* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`.

View File

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

View File

@ -13,26 +13,17 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.9.6'
releaseVersionCode = 2009006
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
// components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality provided
// by the library requires API level 16 or greater.
minSdkVersion = 14
releaseVersion = '2.10.0'
releaseVersionCode = 2010000
minSdkVersion = 16
targetSdkVersion = 28
compileSdkVersion = 28
buildToolsVersion = '28.0.2'
testSupportLibraryVersion = '0.5'
supportLibraryVersion = '27.1.1'
dexmakerVersion = '1.2'
mockitoVersion = '1.9.5'
junitVersion = '4.12'
truthVersion = '0.39'
robolectricVersion = '3.7.1'
dexmakerVersion = '2.21.0'
mockitoVersion = '2.25.0'
robolectricVersion = '4.2'
autoValueVersion = '1.6'
checkerframeworkVersion = '2.5.0'
testRunnerVersion = '1.1.0-alpha3'
androidXTestVersion = '1.1.0'
modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix

View File

@ -16,7 +16,6 @@ apply plugin: 'com.android.application'
android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -26,7 +25,7 @@ android {
defaultConfig {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
minSdkVersion 16
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
@ -45,8 +44,18 @@ android {
}
lintOptions {
// The demo app does not have translations.
disable 'MissingTranslation'
// The demo app isn't indexed and doesn't have translations.
disable 'GoogleAppIndexingWarning','MissingTranslation'
}
flavorDimensions "receiver"
productFlavors {
defaultCast {
dimension "receiver"
manifestPlaceholders =
[castOptionsProvider: "com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"]
}
}
}
@ -58,9 +67,10 @@ dependencies {
implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'library-ui')
implementation project(modulePrefix + 'extension-cast')
implementation 'com.android.support:support-v4:' + supportLibraryVersion
implementation 'com.android.support:appcompat-v7:' + supportLibraryVersion
implementation 'com.android.support:recyclerview-v7:' + supportLibraryVersion
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
}
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'

View File

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

View File

@ -17,13 +17,15 @@
package="com.google.android.exoplayer2.castdemo">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-sdk/>
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
android:largeHeap="true" android:allowBackup="false">
<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"
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;
import android.net.Uri;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.gms.cast.MediaInfo;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
/**
* Utility methods and constants for the Cast demo application.
*/
/** Utility methods and constants for the Cast demo application. */
/* package */ final class DemoUtil {
public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD;
public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8;
public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS;
public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4;
/**
* The list of samples available in the cast demo app.
*/
public static final List<Sample> SAMPLES;
/**
* Represents a media sample.
*/
/** Represents a media sample. */
public static final class Sample {
/**
* The uri from which the media sample is obtained.
*/
/** The uri of the media content. */
public final String uri;
/**
* A descriptive name for the sample.
*/
/** The name of the sample. */
public final String name;
/**
* The mime type of the media sample, as required by {@link MediaInfo#setContentType}.
*/
/** The mime type of the sample media content. */
public final String mimeType;
/**
* The {@link UUID} of the DRM scheme that protects the content, or null if the content is not
* DRM-protected.
*/
@Nullable public final UUID drmSchemeUuid;
/**
* The url from which players should obtain DRM licenses, or null if the content is not
* DRM-protected.
*/
@Nullable public final Uri licenseServerUri;
/**
* @param uri See {@link #uri}.
@ -60,31 +53,53 @@ import java.util.List;
* @param mimeType See {@link #mimeType}.
*/
public Sample(String uri, String name, String mimeType) {
this(uri, name, mimeType, /* drmSchemeUuid= */ null, /* licenseServerUriString= */ null);
}
public Sample(
String uri,
String name,
String mimeType,
@Nullable UUID drmSchemeUuid,
@Nullable String licenseServerUriString) {
this.uri = uri;
this.name = name;
this.mimeType = mimeType;
this.drmSchemeUuid = drmSchemeUuid;
this.licenseServerUri =
licenseServerUriString != null ? Uri.parse(licenseServerUriString) : null;
}
@Override
public String toString() {
return name;
}
}
public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD;
public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8;
public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS;
public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4;
/** The list of samples available in the cast demo app. */
public static final List<Sample> SAMPLES;
static {
// App samples.
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() {}
}

View File

@ -17,13 +17,13 @@ package com.google.android.exoplayer2.castdemo;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.graphics.ColorUtils;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.support.v7.widget.helper.ItemTouchHelper;
import androidx.core.graphics.ColorUtils;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import androidx.recyclerview.widget.ItemTouchHelper;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
@ -33,21 +33,26 @@ import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.castdemo.DemoUtil.Sample;
import com.google.android.exoplayer2.ext.cast.CastPlayer;
import com.google.android.exoplayer2.ext.cast.MediaItem;
import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.framework.CastButtonFactory;
import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.dynamite.DynamiteModule;
import java.util.Collections;
/**
* An activity that plays video using {@link SimpleExoPlayer} and {@link CastPlayer}.
* An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's
* Cast extension.
*/
public class MainActivity extends AppCompatActivity implements OnClickListener,
PlayerManager.QueuePositionListener {
public class MainActivity extends AppCompatActivity
implements OnClickListener, PlayerManager.Listener {
private final MediaItem.Builder mediaItemBuilder;
private PlayerView localPlayerView;
private PlayerControlView castControlView;
@ -56,6 +61,10 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
private MediaQueueListAdapter mediaQueueListAdapter;
private CastContext castContext;
public MainActivity() {
mediaItemBuilder = new MediaItem.Builder();
}
// Activity lifecycle methods.
@Override
@ -68,7 +77,7 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
Throwable cause = e.getCause();
while (cause != null) {
if (cause instanceof DynamiteModule.LoadingException) {
setContentView(R.layout.cast_context_error_message_layout);
setContentView(R.layout.cast_context_error);
return;
}
cause = cause.getCause();
@ -109,13 +118,20 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
// There is no Cast context to work with. Do nothing.
return;
}
String applicationId = castContext.getCastOptions().getReceiverApplicationId();
switch (applicationId) {
case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID:
playerManager =
PlayerManager.createPlayerManager(
/* queuePositionListener= */ this,
new DefaultReceiverPlayerManager(
/* listener= */ this,
localPlayerView,
castControlView,
/* context= */ this,
castContext);
break;
default:
throw new IllegalStateException("Illegal receiver app id: " + applicationId);
}
mediaQueueList.setAdapter(mediaQueueListAdapter);
}
@ -129,6 +145,7 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount());
mediaQueueList.setAdapter(null);
playerManager.release();
playerManager = null;
}
// Activity input.
@ -141,12 +158,15 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
@Override
public void onClick(View view) {
new AlertDialog.Builder(this).setTitle(R.string.sample_list_dialog_title)
.setView(buildSampleListView()).setPositiveButton(android.R.string.ok, null).create()
new AlertDialog.Builder(this)
.setTitle(R.string.add_samples)
.setView(buildSampleListView())
.setPositiveButton(android.R.string.ok, null)
.create()
.show();
}
// PlayerManager.QueuePositionListener implementation.
// PlayerManager.Listener implementation.
@Override
public void onQueuePositionChanged(int previousIndex, int newIndex) {
@ -158,6 +178,16 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
}
}
@Override
public void onQueueContentsExternallyChanged() {
mediaQueueListAdapter.notifyDataSetChanged();
}
@Override
public void onPlayerError() {
Toast.makeText(getApplicationContext(), R.string.player_error_msg, Toast.LENGTH_LONG).show();
}
// Internal methods.
private View buildSampleListView() {
@ -166,7 +196,19 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
sampleList.setAdapter(new SampleListAdapter(this));
sampleList.setOnItemClickListener(
(parent, view, position, id) -> {
playerManager.addItem(DemoUtil.SAMPLES.get(position));
DemoUtil.Sample sample = DemoUtil.SAMPLES.get(position);
mediaItemBuilder
.clear()
.setMedia(sample.uri)
.setTitle(sample.name)
.setMimeType(sample.mimeType);
if (sample.drmSchemeUuid != null) {
mediaItemBuilder.setDrmSchemes(
Collections.singletonList(
new MediaItem.DrmScheme(
sample.drmSchemeUuid, new MediaItem.UriBundle(sample.licenseServerUri))));
}
playerManager.addItem(mediaItemBuilder.build());
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
});
return dialogList;
@ -174,23 +216,6 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
// Internal classes.
private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener {
public final TextView textView;
public QueueItemViewHolder(TextView textView) {
super(textView);
this.textView = textView;
textView.setOnClickListener(this);
}
@Override
public void onClick(View v) {
playerManager.selectQueueItem(getAdapterPosition());
}
}
private class MediaQueueListAdapter extends RecyclerView.Adapter<QueueItemViewHolder> {
@Override
@ -202,8 +227,9 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
@Override
public void onBindViewHolder(QueueItemViewHolder holder, int position) {
holder.item = playerManager.getItem(position);
TextView view = holder.textView;
view.setText(playerManager.getItem(position).name);
view.setText(holder.item.title);
// TODO: Solve coloring using the theme's ColorStateList.
view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(),
position == playerManager.getCurrentItemIndex() ? 255 : 100));
@ -244,8 +270,11 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAdapterPosition();
if (playerManager.removeItem(position)) {
QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
if (playerManager.removeItem(queueItemHolder.item)) {
mediaQueueListAdapter.notifyItemRemoved(position);
// Update whichever item took its place, in case it became the new selected item.
mediaQueueListAdapter.notifyItemChanged(position);
}
}
@ -253,8 +282,9 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
if (draggingFromPosition != C.INDEX_UNSET) {
QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
// A drag has ended. We reflect the media queue change in the player.
if (!playerManager.moveItem(draggingFromPosition, draggingToPosition)) {
if (!playerManager.moveItem(queueItemHolder.item, draggingToPosition)) {
// The move failed. The entire sequence of onMove calls since the drag started needs to be
// invalidated.
mediaQueueListAdapter.notifyDataSetChanged();
@ -263,15 +293,30 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
draggingFromPosition = C.INDEX_UNSET;
draggingToPosition = C.INDEX_UNSET;
}
}
private static final class SampleListAdapter extends ArrayAdapter<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) {
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");
* you may not use this file except in compliance with the License.
@ -15,402 +15,53 @@
*/
package com.google.android.exoplayer2.castdemo;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.view.KeyEvent;
import android.view.View;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.Player.EventListener;
import com.google.android.exoplayer2.Player.TimelineChangeReason;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.castdemo.DemoUtil.Sample;
import com.google.android.exoplayer2.ext.cast.CastPlayer;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaMetadata;
import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.framework.CastContext;
import java.util.ArrayList;
import com.google.android.exoplayer2.ext.cast.MediaItem;
/** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */
/* package */ final class PlayerManager
implements EventListener, CastPlayer.SessionAvailabilityListener {
/** Manages the players in the Cast demo app. */
/* package */ interface PlayerManager {
/**
* Listener for changes in the media queue playback position.
*/
public interface QueuePositionListener {
/** Listener for events. */
interface Listener {
/**
* Called when the currently played item of the media queue changes.
*/
/** Called when the currently played item of the media queue changes. */
void onQueuePositionChanged(int previousIndex, int newIndex);
/** Called when the media queue changes due to modifications not caused by this manager. */
void onQueueContentsExternallyChanged();
/** Called when an error occurs in the current player. */
void onPlayerError();
}
private static final String USER_AGENT = "ExoCastDemoPlayer";
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
new DefaultHttpDataSourceFactory(USER_AGENT);
/** Redirects the given {@code keyEvent} to the active player. */
boolean dispatchKeyEvent(KeyEvent keyEvent);
private final PlayerView localPlayerView;
private final PlayerControlView castControlView;
private final SimpleExoPlayer exoPlayer;
private final CastPlayer castPlayer;
private final ArrayList<DemoUtil.Sample> mediaQueue;
private final QueuePositionListener queuePositionListener;
private final ConcatenatingMediaSource concatenatingMediaSource;
/** Appends the given {@link MediaItem} to the media queue. */
void addItem(MediaItem mediaItem);
private boolean castMediaQueueCreationPending;
private int currentItemIndex;
private Player currentPlayer;
/** Returns the number of items in the media queue. */
int getMediaQueueSize();
/** Selects the item at the given position for playback. */
void selectQueueItem(int position);
/**
* @param queuePositionListener A {@link QueuePositionListener} for queue position changes.
* @param localPlayerView The {@link PlayerView} for local playback.
* @param castControlView The {@link PlayerControlView} to control remote playback.
* @param context A {@link Context}.
* @param castContext The {@link CastContext}.
* Returns the position of the item currently being played, or {@link C#INDEX_UNSET} if no item is
* being played.
*/
public static PlayerManager createPlayerManager(
QueuePositionListener queuePositionListener,
PlayerView localPlayerView,
PlayerControlView castControlView,
Context context,
CastContext castContext) {
PlayerManager playerManager =
new PlayerManager(
queuePositionListener, localPlayerView, castControlView, context, castContext);
playerManager.init();
return playerManager;
}
int getCurrentItemIndex();
private PlayerManager(
QueuePositionListener queuePositionListener,
PlayerView localPlayerView,
PlayerControlView castControlView,
Context context,
CastContext castContext) {
this.queuePositionListener = queuePositionListener;
this.localPlayerView = localPlayerView;
this.castControlView = castControlView;
mediaQueue = new ArrayList<>();
currentItemIndex = C.INDEX_UNSET;
concatenatingMediaSource = new ConcatenatingMediaSource();
/** Returns the {@link MediaItem} at the given {@code position}. */
MediaItem getItem(int position);
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector);
exoPlayer.addListener(this);
localPlayerView.setPlayer(exoPlayer);
/** Moves the item at position {@code from} to position {@code to}. */
boolean moveItem(MediaItem item, int to);
castPlayer = new CastPlayer(castContext);
castPlayer.addListener(this);
castPlayer.setSessionAvailabilityListener(this);
castControlView.setPlayer(castPlayer);
}
// Queue manipulation methods.
/**
* Plays a specified queue item in the current player.
*
* @param itemIndex The index of the item to play.
*/
public void selectQueueItem(int itemIndex) {
setCurrentItem(itemIndex, C.TIME_UNSET, true);
}
/**
* Returns the index of the currently played item.
*/
public int getCurrentItemIndex() {
return currentItemIndex;
}
/**
* Appends {@code sample} to the media queue.
*
* @param sample The {@link Sample} to append.
*/
public void addItem(Sample sample) {
mediaQueue.add(sample);
concatenatingMediaSource.addMediaSource(buildMediaSource(sample));
if (currentPlayer == castPlayer) {
castPlayer.addItems(buildMediaQueueItem(sample));
}
}
/**
* Returns the size of the media queue.
*/
public int getMediaQueueSize() {
return mediaQueue.size();
}
/**
* Returns the item at the given index in the media queue.
*
* @param position The index of the item.
* @return The item at the given index in the media queue.
*/
public Sample getItem(int position) {
return mediaQueue.get(position);
}
/**
* Removes the item at the given index from the media queue.
*
* @param itemIndex The index of the item to remove.
* @return Whether the removal was successful.
*/
public boolean removeItem(int itemIndex) {
concatenatingMediaSource.removeMediaSource(itemIndex);
if (currentPlayer == castPlayer) {
if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
Timeline castTimeline = castPlayer.getCurrentTimeline();
if (castTimeline.getPeriodCount() <= itemIndex) {
return false;
}
castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id);
}
}
mediaQueue.remove(itemIndex);
if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) {
maybeSetCurrentItemAndNotify(C.INDEX_UNSET);
} else if (itemIndex < currentItemIndex) {
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
}
return true;
}
/**
* Moves an item within the queue.
*
* @param fromIndex The index of the item to move.
* @param toIndex The target index of the item in the queue.
* @return Whether the item move was successful.
*/
public boolean moveItem(int fromIndex, int toIndex) {
// Player update.
concatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) {
Timeline castTimeline = castPlayer.getCurrentTimeline();
int periodCount = castTimeline.getPeriodCount();
if (periodCount <= fromIndex || periodCount <= toIndex) {
return false;
}
int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id;
castPlayer.moveItem(elementId, toIndex);
}
mediaQueue.add(toIndex, mediaQueue.remove(fromIndex));
// Index update.
if (fromIndex == currentItemIndex) {
maybeSetCurrentItemAndNotify(toIndex);
} else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) {
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
} else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) {
maybeSetCurrentItemAndNotify(currentItemIndex + 1);
}
return true;
}
// Miscellaneous methods.
/**
* Dispatches a given {@link KeyEvent} to the corresponding view of the current player.
*
* @param event The {@link KeyEvent}.
* @return Whether the event was handled by the target view.
*/
public boolean dispatchKeyEvent(KeyEvent event) {
if (currentPlayer == exoPlayer) {
return localPlayerView.dispatchKeyEvent(event);
} else /* currentPlayer == castPlayer */ {
return castControlView.dispatchKeyEvent(event);
}
}
/**
* Releases the manager and the players that it holds.
*/
public void release() {
currentItemIndex = C.INDEX_UNSET;
mediaQueue.clear();
concatenatingMediaSource.clear();
castPlayer.setSessionAvailabilityListener(null);
castPlayer.release();
localPlayerView.setPlayer(null);
exoPlayer.release();
}
// Player.EventListener implementation.
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
updateCurrentItemIndex();
}
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
updateCurrentItemIndex();
}
@Override
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
updateCurrentItemIndex();
if (timeline.isEmpty()) {
castMediaQueueCreationPending = true;
}
}
// CastPlayer.SessionAvailabilityListener implementation.
@Override
public void onCastSessionAvailable() {
setCurrentPlayer(castPlayer);
}
@Override
public void onCastSessionUnavailable() {
setCurrentPlayer(exoPlayer);
}
// Internal methods.
private void init() {
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
}
private void updateCurrentItemIndex() {
int playbackState = currentPlayer.getPlaybackState();
maybeSetCurrentItemAndNotify(
playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED
? currentPlayer.getCurrentWindowIndex() : C.INDEX_UNSET);
}
private void setCurrentPlayer(Player currentPlayer) {
if (this.currentPlayer == currentPlayer) {
return;
}
// View management.
if (currentPlayer == exoPlayer) {
localPlayerView.setVisibility(View.VISIBLE);
castControlView.hide();
} else /* currentPlayer == castPlayer */ {
localPlayerView.setVisibility(View.GONE);
castControlView.show();
}
// Player state management.
long playbackPositionMs = C.TIME_UNSET;
int windowIndex = C.INDEX_UNSET;
boolean playWhenReady = false;
if (this.currentPlayer != null) {
int playbackState = this.currentPlayer.getPlaybackState();
if (playbackState != Player.STATE_ENDED) {
playbackPositionMs = this.currentPlayer.getCurrentPosition();
playWhenReady = this.currentPlayer.getPlayWhenReady();
windowIndex = this.currentPlayer.getCurrentWindowIndex();
if (windowIndex != currentItemIndex) {
playbackPositionMs = C.TIME_UNSET;
windowIndex = currentItemIndex;
}
}
this.currentPlayer.stop(true);
} else {
// This is the initial setup. No need to save any state.
}
this.currentPlayer = currentPlayer;
// Media queue management.
castMediaQueueCreationPending = currentPlayer == castPlayer;
if (currentPlayer == exoPlayer) {
exoPlayer.prepare(concatenatingMediaSource);
}
// Playback transition.
if (windowIndex != C.INDEX_UNSET) {
setCurrentItem(windowIndex, playbackPositionMs, playWhenReady);
}
}
/**
* Starts playback of the item at the given position.
*
* @param itemIndex The index of the item to play.
* @param positionMs The position at which playback should start.
* @param playWhenReady Whether the player should proceed when ready to do so.
*/
private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
maybeSetCurrentItemAndNotify(itemIndex);
if (castMediaQueueCreationPending) {
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
for (int i = 0; i < items.length; i++) {
items[i] = buildMediaQueueItem(mediaQueue.get(i));
}
castMediaQueueCreationPending = false;
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
} else {
currentPlayer.seekTo(itemIndex, positionMs);
currentPlayer.setPlayWhenReady(playWhenReady);
}
}
private void maybeSetCurrentItemAndNotify(int currentItemIndex) {
if (this.currentItemIndex != currentItemIndex) {
int oldIndex = this.currentItemIndex;
this.currentItemIndex = currentItemIndex;
queuePositionListener.onQueuePositionChanged(oldIndex, currentItemIndex);
}
}
private static MediaSource buildMediaSource(DemoUtil.Sample sample) {
Uri uri = Uri.parse(sample.uri);
switch (sample.mimeType) {
case DemoUtil.MIME_TYPE_SS:
return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
case DemoUtil.MIME_TYPE_DASH:
return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
case DemoUtil.MIME_TYPE_HLS:
return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
case DemoUtil.MIME_TYPE_VIDEO_MP4:
return new ExtractorMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
default: {
throw new IllegalStateException("Unsupported type: " + sample.mimeType);
}
}
}
private static MediaQueueItem buildMediaQueueItem(DemoUtil.Sample sample) {
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
movieMetadata.putString(MediaMetadata.KEY_TITLE, sample.name);
MediaInfo mediaInfo = new MediaInfo.Builder(sample.uri)
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED).setContentType(sample.mimeType)
.setMetadata(movieMetadata).build();
return new MediaQueueItem.Builder(mediaInfo).build();
}
/** Removes the item at position {@code index}. */
boolean removeItem(MediaItem item);
/** Releases any acquired resources. */
void release();
}

View File

@ -13,8 +13,12 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector android:alpha="0.8" android:height="24dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<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"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24.0dp"
android:viewportHeight="24.0"
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>

View File

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

View File

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

View File

@ -14,7 +14,7 @@
limitations under the License.
-->
<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">
<ListView android:id="@+id/sample_list"

View File

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

View File

@ -20,8 +20,10 @@
<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="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>

View File

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

View File

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

View File

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

View File

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

View File

@ -330,11 +330,11 @@
"samples": [
{
"name": "Super speed",
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism"
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest"
},
{
"name": "Super speed (PlayReady)",
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism",
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest",
"drm_scheme": "playready"
}
]

View File

@ -16,6 +16,13 @@
package com.google.android.exoplayer2.demo;
import android.app.Application;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.database.DatabaseProvider;
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil;
import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.upstream.DataSource;
@ -28,21 +35,24 @@ import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException;
/**
* Placeholder application to facilitate overriding Application methods for debugging and testing.
*/
public class DemoApplication extends Application {
private static final String TAG = "DemoApplication";
private static final String DOWNLOAD_ACTION_FILE = "actions";
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2;
protected String userAgent;
private DatabaseProvider databaseProvider;
private File downloadDirectory;
private Cache downloadCache;
private DownloadManager downloadManager;
@ -71,6 +81,18 @@ public class DemoApplication extends Application {
return "withExtensions".equals(BuildConfig.FLAVOR);
}
public RenderersFactory buildRenderersFactory(boolean preferExtensionRenderer) {
@DefaultRenderersFactory.ExtensionRendererMode
int extensionRendererMode =
useExtensionRenderers()
? (preferExtensionRenderer
? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
return new DefaultRenderersFactory(/* context= */ this)
.setExtensionRendererMode(extensionRendererMode);
}
public DownloadManager getDownloadManager() {
initDownloadManager();
return downloadManager;
@ -81,31 +103,51 @@ public class DemoApplication extends Application {
return downloadTracker;
}
protected synchronized Cache getDownloadCache() {
if (downloadCache == null) {
File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY);
downloadCache =
new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider());
}
return downloadCache;
}
private synchronized void initDownloadManager() {
if (downloadManager == null) {
DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider());
upgradeActionFile(
DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false);
upgradeActionFile(
DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true);
DownloaderConstructorHelper downloaderConstructorHelper =
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
downloadManager =
new DownloadManager(
downloaderConstructorHelper,
MAX_SIMULTANEOUS_DOWNLOADS,
DownloadManager.DEFAULT_MIN_RETRY_COUNT,
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE));
this, downloadIndex, new DefaultDownloaderFactory(downloaderConstructorHelper));
downloadTracker =
new DownloadTracker(
/* context= */ this,
buildDataSourceFactory(),
new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE));
downloadManager.addListener(downloadTracker);
new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadManager);
}
}
private synchronized Cache getDownloadCache() {
if (downloadCache == null) {
File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY);
downloadCache = new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor());
private void upgradeActionFile(
String fileName, DefaultDownloadIndex downloadIndex, boolean addNewDownloadsAsCompleted) {
try {
ActionFileUpgradeUtil.upgradeAndDelete(
new File(getDownloadDirectory(), fileName),
/* downloadIdProvider= */ null,
downloadIndex,
/* deleteOnFailure= */ true,
addNewDownloadsAsCompleted);
} catch (IOException e) {
Log.e(TAG, "Failed to upgrade action file: " + fileName, e);
}
return downloadCache;
}
private DatabaseProvider getDatabaseProvider() {
if (databaseProvider == null) {
databaseProvider = new ExoDatabaseProvider(this);
}
return databaseProvider;
}
private File getDownloadDirectory() {
@ -118,8 +160,8 @@ public class DemoApplication extends Application {
return downloadDirectory;
}
private static CacheDataSourceFactory buildReadOnlyCacheDataSource(
DefaultDataSourceFactory upstreamFactory, Cache cache) {
protected static CacheDataSourceFactory buildReadOnlyCacheDataSource(
DataSource.Factory upstreamFactory, Cache cache) {
return new CacheDataSourceFactory(
cache,
upstreamFactory,

View File

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

View File

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

View File

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

View File

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

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_height="wrap_content"
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>

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
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"
android:title="@string/prefer_extension_decoders"
android:showAsAction="never"
android:checkable="true"/>
android:checkable="true"
app:showAsAction="never"/>
<item android:id="@+id/random_abr"
android:title="@string/random_abr"
android:showAsAction="never"
android:checkable="true"/>
android:checkable="true"
app:showAsAction="never"/>
</menu>

View File

@ -17,6 +17,8 @@
<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="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">
<style name="PlayerTheme" parent="android:Theme.Holo">
<item name="android:windowNoTitle">true</item>
<style name="TrackSelectionDialogThemeOverlay" parent="ThemeOverlay.AppCompat.Dialog.Alert">
<item name="windowNoTitle">false</item>
</style>
<style name="PlayerTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">@android:color/black</item>
</style>

View File

@ -5,7 +5,7 @@
The cast extension is a [Player][] implementation that controls playback on a
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 ##

View File

@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -24,32 +23,21 @@ android {
}
defaultConfig {
minSdkVersion 14
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
}
testOptions.unitTests.includeAndroidResources = true
}
dependencies {
api 'com.google.android.gms:play-services-cast-framework:16.1.2'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
implementation 'androidx.annotation:annotation:1.0.2'
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui')
testImplementation project(modulePrefix + 'testutils')
testImplementation 'junit:junit:' + junitVersion
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
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 {

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;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.BasePlayer;
import com.google.android.exoplayer2.C;
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.
*
* <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,
* {@link #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be
* implemented and attached to the player.</p>
* Cast context passed to {@link #CastPlayer}. To keep track of the session, {@link
* #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be
* implemented and attached to the player.
*
* <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
* 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 {
/**
* 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 int RENDERER_COUNT = 3;
@ -591,7 +574,9 @@ public final class CastPlayer extends BasePlayer {
CastTimeline oldTimeline = currentTimeline;
MediaStatus status = getMediaStatus();
currentTimeline =
status != null ? timelineTracker.getCastTimeline(status) : CastTimeline.EMPTY_CAST_TIMELINE;
status != null
? timelineTracker.getCastTimeline(remoteMediaClient)
: CastTimeline.EMPTY_CAST_TIMELINE;
return !oldTimeline.equals(currentTimeline);
}

View File

@ -15,24 +15,66 @@
*/
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 com.google.android.exoplayer2.C;
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.Collections;
import java.util.List;
import java.util.Map;
/**
* A {@link Timeline} for Cast media queues.
*/
/* 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 =
new CastTimeline(Collections.emptyList(), Collections.emptyMap());
new CastTimeline(new int[0], new SparseArray<>());
private final SparseIntArray idsToIndex;
private final int[] ids;
@ -40,28 +82,23 @@ import java.util.Map;
private final long[] defaultPositionsUs;
/**
* @param items A list of cast media queue items to represent.
* @param contentIdToDurationUsMap A map of content id to duration in microseconds.
* Creates a Cast timeline from the given data.
*
* @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) {
int itemCount = items.size();
int index = 0;
public CastTimeline(int[] itemIds, SparseArray<ItemData> itemIdToData) {
int itemCount = itemIds.length;
idsToIndex = new SparseIntArray(itemCount);
ids = new int[itemCount];
ids = Arrays.copyOf(itemIds, itemCount);
durationsUs = new long[itemCount];
defaultPositionsUs = new long[itemCount];
for (MediaQueueItem item : items) {
int itemId = item.getItemId();
ids[index] = itemId;
idsToIndex.put(itemId, index);
MediaInfo mediaInfo = item.getMedia();
String contentId = mediaInfo.getContentId();
durationsUs[index] =
contentIdToDurationUsMap.containsKey(contentId)
? contentIdToDurationUsMap.get(contentId)
: CastUtils.getStreamDurationUs(mediaInfo);
defaultPositionsUs[index] = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
index++;
for (int i = 0; i < ids.length; i++) {
int id = ids[i];
idsToIndex.put(id, i);
ItemData data = itemIdToData.get(id, ItemData.EMPTY);
durationsUs[i] = data.durationUs;
defaultPositionsUs[i] = data.defaultPositionUs;
}
}
@ -108,7 +145,7 @@ import java.util.Map;
}
@Override
public Object getUidOfPeriod(int periodIndex) {
public Integer getUidOfPeriod(int periodIndex) {
return ids[periodIndex];
}

View File

@ -15,53 +15,84 @@
*/
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.MediaStatus;
import java.util.HashMap;
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
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
* durations in the media queue items [See internal: b/65152553].
*/
/* package */ final class CastTimelineTracker {
private final HashMap<String, Long> contentIdToDurationUsMap;
private final HashSet<String> scratchContentIdSet;
private final SparseArray<CastTimeline.ItemData> itemIdToData;
public CastTimelineTracker() {
contentIdToDurationUsMap = new HashMap<>();
scratchContentIdSet = new HashSet<>();
itemIdToData = new SparseArray<>();
}
/**
* 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.
* @return A {@link CastTimeline} that represent the given {@code status}.
* <p>Returned timelines may contain values obtained from {@code remoteMediaClient} in previous
* 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) {
MediaInfo mediaInfo = status.getMediaInfo();
List<MediaQueueItem> items = status.getQueueItems();
removeUnusedDurationEntries(items);
if (mediaInfo != null) {
String contentId = mediaInfo.getContentId();
long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
contentIdToDurationUsMap.put(contentId, durationUs);
}
return new CastTimeline(items, contentIdToDurationUsMap);
public CastTimeline getCastTimeline(RemoteMediaClient remoteMediaClient) {
int[] itemIds = remoteMediaClient.getMediaQueue().getItemIds();
if (itemIds.length > 0) {
// 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].
removeUnusedItemDataEntries(itemIds);
}
private void removeUnusedDurationEntries(List<MediaQueueItem> items) {
scratchContentIdSet.clear();
for (MediaQueueItem item : items) {
scratchContentIdSet.add(item.getMedia().getContentId());
// 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 removeUnusedItemDataEntries(int[] itemIds) {
HashSet<Integer> scratchItemIds = new HashSet<>(/* initialCapacity= */ itemIds.length * 2);
for (int id : itemIds) {
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.
*
* @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) {
long durationMs =
mediaInfo != null ? mediaInfo.getStreamDuration() : MediaInfo.UNKNOWN_DURATION;
if (mediaInfo == null) {
return C.TIME_UNSET;
}
long durationMs = mediaInfo.getStreamDuration();
return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET;
}
@ -109,6 +111,7 @@ import com.google.android.gms.cast.MediaTrack;
/* codecs= */ null,
/* bitrate= */ Format.NO_VALUE,
/* selectionFlags= */ 0,
/* roleFlags= */ 0,
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;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem;
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.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link CastTimelineTracker}. */
@RunWith(RobolectricTestRunner.class)
@RunWith(AndroidJUnit4.class)
public class CastTimelineTrackerTest {
private static final long DURATION_1_MS = 1000;
private static final long DURATION_2_MS = 2000;
private static final long DURATION_3_MS = 3000;
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. */
@Test
public void testGetCastTimeline() {
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});
public void testGetCastTimelinePersistsDuration() {
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);
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
RemoteMediaClient remoteMediaClient =
mockRemoteMediaClient(
/* itemIds= */ new int[] {1, 2, 3, 4, 5},
/* currentItemId= */ 2,
/* currentDurationMs= */ DURATION_2_MS);
TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(status),
C.msToUs(DURATION_1_MS),
tracker.getCastTimeline(remoteMediaClient),
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);
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
remoteMediaClient =
mockRemoteMediaClient(
/* itemIds= */ new int[] {1, 2, 3},
/* currentItemId= */ 3,
/* currentDurationMs= */ DURATION_3_MS);
TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(status),
C.msToUs(DURATION_1_MS),
tracker.getCastTimeline(remoteMediaClient),
C.TIME_UNSET,
C.msToUs(DURATION_2_MS),
C.msToUs(DURATION_3_MS));
MediaStatus newStatus =
mockMediaStatus(
new int[] {4, 1, 5, 3},
new String[] {"contentId4", "contentId1", "contentId5", "contentId3"},
new long[] {
MediaInfo.UNKNOWN_DURATION,
MediaInfo.UNKNOWN_DURATION,
DURATION_5_MS,
MediaInfo.UNKNOWN_DURATION
});
mediaInfo = getMediaInfo("contentId5", DURATION_5_MS);
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
remoteMediaClient =
mockRemoteMediaClient(
/* itemIds= */ new int[] {1, 3},
/* currentItemId= */ 3,
/* currentDurationMs= */ DURATION_3_MS);
TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(newStatus),
C.TIME_UNSET,
C.msToUs(DURATION_1_MS),
C.msToUs(DURATION_5_MS),
C.msToUs(DURATION_3_MS));
tracker.getCastTimeline(remoteMediaClient), C.TIME_UNSET, C.msToUs(DURATION_3_MS));
mediaInfo = getMediaInfo("contentId3", DURATION_3_MS);
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
remoteMediaClient =
mockRemoteMediaClient(
/* itemIds= */ new int[] {1, 2, 3, 4, 5},
/* currentItemId= */ 4,
/* currentDurationMs= */ DURATION_4_MS);
TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(newStatus),
tracker.getCastTimeline(remoteMediaClient),
C.TIME_UNSET,
C.msToUs(DURATION_1_MS),
C.msToUs(DURATION_5_MS),
C.msToUs(DURATION_3_MS));
mediaInfo = getMediaInfo("contentId4", DURATION_4_MS);
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(newStatus),
C.TIME_UNSET,
C.msToUs(DURATION_3_MS),
C.msToUs(DURATION_4_MS),
C.msToUs(DURATION_1_MS),
C.msToUs(DURATION_5_MS),
C.msToUs(DURATION_3_MS));
C.TIME_UNSET);
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(
int[] itemIds, String[] contentIds, long[] durationsMs) {
ArrayList<MediaQueueItem> items = new ArrayList<>();
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);
}
private static RemoteMediaClient mockRemoteMediaClient(
int[] itemIds, int currentItemId, long currentDurationMs) {
RemoteMediaClient remoteMediaClient = Mockito.mock(RemoteMediaClient.class);
MediaStatus status = Mockito.mock(MediaStatus.class);
Mockito.when(status.getQueueItems()).thenReturn(items);
return status;
Mockito.when(status.getQueueItems()).thenReturn(Collections.emptyList());
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) {
return new MediaInfo.Builder(contentId)
private static MediaQueue mockMediaQueue(int[] itemIds) {
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)
.setContentType(MimeTypes.APPLICATION_MP4)
.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][].
[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
## Getting the extension ##
@ -52,4 +52,4 @@ respectively.
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.cronet.*`
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 {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion 16
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
@ -27,12 +26,14 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
testOptions.unitTests.includeAndroidResources = true
}
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 'com.android.support:support-annotations:' + supportLibraryVersion
implementation 'androidx.annotation:annotation:1.0.2'
testImplementation project(modulePrefix + 'library')
testImplementation project(modulePrefix + 'testutils-robolectric')
}

View File

@ -16,10 +16,11 @@
package com.google.android.exoplayer2.ext.cronet;
import android.net.Uri;
import android.support.annotation.Nullable;
import androidx.annotation.Nullable;
import android.text.TextUtils;
import com.google.android.exoplayer2.C;
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.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
@ -493,6 +494,11 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
if (dataSpec.httpBody != null && !isContentTypeHeaderSet) {
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.
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
StringBuilder rangeValue = new StringBuilder();

View File

@ -15,7 +15,7 @@
*/
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.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;

View File

@ -16,7 +16,7 @@
package com.google.android.exoplayer2.ext.cronet;
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.Util;
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.verify;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
@ -28,10 +29,9 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link ByteArrayUploadDataProvider}. */
@RunWith(RobolectricTestRunner.class)
@RunWith(AndroidJUnit4.class)
public final class ByteArrayUploadDataProviderTest {
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.os.ConditionVariable;
import android.os.SystemClock;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource;
@ -62,10 +63,9 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link CronetDataSource}. */
@RunWith(RobolectricTestRunner.class)
@RunWith(AndroidJUnit4.class)
public final class CronetDataSourceTest {
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
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
[#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 ##
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ffmpeg.*`
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 {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -33,12 +32,15 @@ android {
jniLibs.srcDir 'src/main/libs'
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
}
testOptions.unitTests.includeAndroidResources = true
}
dependencies {
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
testImplementation project(modulePrefix + 'testutils-robolectric')
}
ext {

View File

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

View File

@ -15,7 +15,7 @@
*/
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.Format;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;

View File

@ -15,10 +15,11 @@
*/
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.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
/**
@ -30,6 +31,8 @@ public final class FfmpegLibrary {
ExoPlayerLibraryInfo.registerModule("goog.exo.ffmpeg");
}
private static final String TAG = "FfmpegLibrary";
private static final LibraryLoader LOADER =
new LibraryLoader("avutil", "avresample", "avcodec", "ffmpeg");
@ -69,7 +72,14 @@ public final class FfmpegLibrary {
return false;
}
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.*`
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 {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -34,13 +33,15 @@ android {
jniLibs.srcDir 'src/main/libs'
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
}
testOptions.unitTests.includeAndroidResources = true
}
dependencies {
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
implementation project(modulePrefix + 'library-core')
androidTestImplementation 'androidx.test:runner:' + testRunnerVersion
implementation 'androidx.annotation:annotation:1.0.2'
androidTestImplementation project(modulePrefix + 'testutils')
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
testImplementation project(modulePrefix + 'testutils-robolectric')
}

View File

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

View File

@ -16,22 +16,26 @@
package com.google.android.exoplayer2.ext.flac;
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.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.IOException;
import org.junit.Before;
import org.junit.runner.RunWith;
/** 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 int DURATION_US = 2_741_000;
@Override
protected void setUp() throws Exception {
super.setUp();
@Before
public void setUp() {
if (!FlacLibrary.isAvailable()) {
fail("Flac library not available.");
}
@ -39,7 +43,8 @@ public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
public void testGetSeekMap_returnsSeekMapWithCorrectDuration()
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();
FlacDecoderJni decoderJni = new FlacDecoderJni();
@ -57,7 +62,8 @@ public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
public void testSetSeekTargetUs_returnsSeekPending()
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();
FlacDecoderJni decoderJni = new FlacDecoderJni();

View File

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

View File

@ -15,17 +15,20 @@
*/
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 org.junit.Before;
import org.junit.runner.RunWith;
/**
* Unit test for {@link FlacExtractor}.
*/
public class FlacExtractorTest extends InstrumentationTestCase {
/** Unit test for {@link FlacExtractor}. */
@RunWith(AndroidJUnit4.class)
public class FlacExtractorTest {
@Override
protected void setUp() throws Exception {
super.setUp();
@Before
public void setUp() throws Exception {
if (!FlacLibrary.isAvailable()) {
fail("Flac library not available.");
}
@ -33,11 +36,11 @@ public class FlacExtractorTest extends InstrumentationTestCase {
public void testExtractFlacSample() throws Exception {
ExtractorAsserts.assertBehavior(
FlacExtractor::new, "bear.flac", getInstrumentation().getContext());
FlacExtractor::new, "bear.flac", ApplicationProvider.getApplicationContext());
}
public void testExtractFlacSampleWithId3Header() throws Exception {
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;
import static androidx.test.InstrumentationRegistry.getContext;
import static org.junit.Assert.fail;
import android.content.Context;
import android.net.Uri;
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.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
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.ProgressiveMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.junit.Before;
@ -56,7 +56,7 @@ public class FlacPlaybackTest {
private void playUri(String uri) throws Exception {
TestPlaybackRunnable testPlaybackRunnable =
new TestPlaybackRunnable(Uri.parse(uri), getContext());
new TestPlaybackRunnable(Uri.parse(uri), ApplicationProvider.getApplicationContext());
Thread thread = new Thread(testPlaybackRunnable);
thread.start();
thread.join();
@ -83,12 +83,12 @@ public class FlacPlaybackTest {
Looper.prepare();
LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer();
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector);
player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this);
MediaSource mediaSource =
new ExtractorMediaSource.Factory(
new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"))
.setExtractorsFactory(MatroskaExtractor.FACTORY)
new ProgressiveMediaSource.Factory(
new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"),
MatroskaExtractor.FACTORY)
.createMediaSource(uri);
player.prepare(mediaSource);
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 android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
@ -94,7 +94,7 @@ public final class FlacExtractor implements Extractor {
/** Constructs an instance with flags = 0. */
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 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) {
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 androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.Extractor;
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.ogg.OggExtractor;
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.PsExtractor;
import com.google.android.exoplayer2.extractor.ts.TsExtractor;
@ -35,10 +37,9 @@ import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Unit test for {@link DefaultExtractorsFactory}. */
@RunWith(RobolectricTestRunner.class)
@RunWith(AndroidJUnit4.class)
public final class DefaultExtractorsFactoryTest {
@Test
@ -59,6 +60,7 @@ public final class DefaultExtractorsFactoryTest {
Mp3Extractor.class,
AdtsExtractor.class,
Ac3Extractor.class,
Ac4Extractor.class,
TsExtractor.class,
FlvExtractor.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.*`
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 {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -27,11 +26,14 @@ android {
minSdkVersion 19
targetSdkVersion project.ext.targetSdkVersion
}
testOptions.unitTests.includeAndroidResources = true
}
dependencies {
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'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
}

View File

@ -15,7 +15,7 @@
*/
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.ExoPlayerLibraryInfo;
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 OUTPUT_CHANNEL_COUNT = 2;
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 channelCount;
private int pendingGvrAudioSurroundFormat;
@Nullable private GvrAudioSurround gvrAudioSurround;
private ByteBuffer buffer;
private boolean inputEnded;
@ -57,6 +59,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE;
buffer = EMPTY_BUFFER;
pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;
}
/**
@ -92,33 +95,28 @@ public final class GvrAudioProcessor implements AudioProcessor {
}
this.sampleRateHz = sampleRateHz;
this.channelCount = channelCount;
maybeReleaseGvrAudioSurround();
int surroundFormat;
switch (channelCount) {
case 1:
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_MONO;
pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_MONO;
break;
case 2:
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO;
pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO;
break;
case 4:
surroundFormat = GvrAudioSurround.SurroundFormat.FIRST_ORDER_AMBISONICS;
pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.FIRST_ORDER_AMBISONICS;
break;
case 6:
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_FIVE_DOT_ONE;
pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_FIVE_DOT_ONE;
break;
case 9:
surroundFormat = GvrAudioSurround.SurroundFormat.SECOND_ORDER_AMBISONICS;
pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SECOND_ORDER_AMBISONICS;
break;
case 16:
surroundFormat = GvrAudioSurround.SurroundFormat.THIRD_ORDER_AMBISONICS;
pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.THIRD_ORDER_AMBISONICS;
break;
default:
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) {
buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE)
.order(ByteOrder.nativeOrder());
@ -128,7 +126,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
@Override
public boolean isActive() {
return gvrAudioSurround != null;
return pendingGvrAudioSurroundFormat != NO_SURROUND_FORMAT || gvrAudioSurround != null;
}
@Override
@ -156,14 +154,17 @@ public final class GvrAudioProcessor implements AudioProcessor {
@Override
public void queueEndOfStream() {
Assertions.checkNotNull(gvrAudioSurround);
inputEnded = true;
if (gvrAudioSurround != null) {
gvrAudioSurround.triggerProcessing();
}
inputEnded = true;
}
@Override
public ByteBuffer getOutput() {
Assertions.checkNotNull(gvrAudioSurround);
if (gvrAudioSurround == null) {
return EMPTY_BUFFER;
}
int writtenBytes = gvrAudioSurround.getOutput(buffer, 0, buffer.capacity());
buffer.position(0).limit(writtenBytes);
return buffer;
@ -171,13 +172,20 @@ public final class GvrAudioProcessor implements AudioProcessor {
@Override
public boolean isEnded() {
Assertions.checkNotNull(gvrAudioSurround);
return inputEnded && gvrAudioSurround.getAvailableOutputSize() == 0;
return inputEnded
&& (gvrAudioSurround == null || gvrAudioSurround.getAvailableOutputSize() == 0);
}
@Override
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();
}
inputEnded = false;
@ -191,13 +199,13 @@ public final class GvrAudioProcessor implements AudioProcessor {
sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE;
buffer = EMPTY_BUFFER;
pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;
}
private void maybeReleaseGvrAudioSurround() {
if (this.gvrAudioSurround != null) {
GvrAudioSurround gvrAudioSurround = this.gvrAudioSurround;
this.gvrAudioSurround = null;
if (gvrAudioSurround != null) {
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
limitations under the License.
-->
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/representation_list"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<resources>
<style name="VrTheme" parent="android:Theme.Material"/>
</resources>

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.
[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 ##
@ -61,4 +61,4 @@ playback.
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ima.*`
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 {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -28,23 +27,14 @@ android {
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
}
testOptions.unitTests.includeAndroidResources = true
}
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 'com.google.android.gms:play-services-ads:17.1.2'
// 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
implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0'
testImplementation project(modulePrefix + 'testutils-robolectric')
}

View File

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

View File

@ -22,10 +22,12 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.net.Uri;
import android.support.annotation.Nullable;
import androidx.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
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.AdDisplayContainer;
import com.google.ads.interactivemedia.v3.api.AdEvent;
@ -54,11 +56,9 @@ import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
/** Test for {@link ImaAdsLoader}. */
@RunWith(RobolectricTestRunner.class)
@RunWith(AndroidJUnit4.class)
public class ImaAdsLoaderTest {
private static final long CONTENT_DURATION_US = 10 * C.MICROS_PER_SECOND;
@ -95,8 +95,8 @@ public class ImaAdsLoaderTest {
adDisplayContainer,
fakeAdsRequest,
fakeAdsLoader);
adViewGroup = new FrameLayout(RuntimeEnvironment.application);
adOverlayView = new View(RuntimeEnvironment.application);
adViewGroup = new FrameLayout(ApplicationProvider.getApplicationContext());
adOverlayView = new View(ApplicationProvider.getApplicationContext());
adViewProvider =
new AdsLoader.AdViewProvider() {
@Override
@ -237,7 +237,7 @@ public class ImaAdsLoaderTest {
adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline, adDurationsUs);
when(adsManager.getAdCuePoints()).thenReturn(Arrays.asList(cuePoints));
imaAdsLoader =
new ImaAdsLoader.Builder(RuntimeEnvironment.application)
new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
.setImaFactory(testImaFactory)
.setImaSdkSettings(imaSdkSettings)
.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 {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -29,6 +28,8 @@ android {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
testOptions.unitTests.includeAndroidResources = true
}
dependencies {

View File

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

View File

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

View File

@ -29,4 +29,4 @@ locally. Instructions for doing this can be found in ExoPlayer's
* [Javadoc][]: Classes matching
`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 {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -27,11 +26,13 @@ android {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
testOptions.unitTests.includeAndroidResources = true
}
dependencies {
implementation project(modulePrefix + 'library-core')
api 'com.android.support:support-media-compat:' + supportLibraryVersion
api 'androidx.media:media:1.0.1'
}
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.os.Bundle;
import android.support.v4.media.session.PlaybackStateCompat;
import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.Player;
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 {
/** 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 final Player player;
@RepeatModeUtil.RepeatToggleModes
private final int repeatToggleModes;
private final CharSequence repeatAllDescription;
@ -37,27 +40,23 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus
/**
* Creates a new instance.
* <p>
* Equivalent to {@code RepeatModeActionProvider(context, player,
* MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES)}.
*
* <p>Equivalent to {@code RepeatModeActionProvider(context, DEFAULT_REPEAT_TOGGLE_MODES)}.
*
* @param context The context.
* @param player The player on which to toggle the repeat mode.
*/
public RepeatModeActionProvider(Context context, Player player) {
this(context, player, MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES);
public RepeatModeActionProvider(Context context) {
this(context, DEFAULT_REPEAT_TOGGLE_MODES);
}
/**
* Creates a new instance enabling the given repeat toggle modes.
*
* @param context The context.
* @param player The player on which to toggle the repeat mode.
* @param repeatToggleModes The toggle modes to enable.
*/
public RepeatModeActionProvider(Context context, Player player,
@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
this.player = player;
public RepeatModeActionProvider(
Context context, @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
this.repeatToggleModes = repeatToggleModes;
repeatAllDescription = context.getString(R.string.exo_media_action_repeat_all_description);
repeatOneDescription = context.getString(R.string.exo_media_action_repeat_one_description);
@ -65,16 +64,17 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus
}
@Override
public void onCustomAction(String action, Bundle extras) {
public void onCustomAction(
Player player, ControlDispatcher controlDispatcher, String action, Bundle extras) {
int mode = player.getRepeatMode();
int proposedMode = RepeatModeUtil.getNextRepeatMode(mode, repeatToggleModes);
if (mode != proposedMode) {
player.setRepeatMode(proposedMode);
controlDispatcher.dispatchSetRepeatMode(player, proposedMode);
}
}
@Override
public PlaybackStateCompat.CustomAction getCustomAction() {
public PlaybackStateCompat.CustomAction getCustomAction(Player player) {
CharSequence actionLabel;
int iconResourceId;
switch (player.getRepeatMode()) {

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