commit
44293e8f45
4
.github/ISSUE_TEMPLATE/bug.md
vendored
4
.github/ISSUE_TEMPLATE/bug.md
vendored
@ -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:
|
||||
-----------------------
|
||||
|
@ -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:
|
||||
-----------------------------
|
||||
|
2
.github/ISSUE_TEMPLATE/question.md
vendored
2
.github/ISSUE_TEMPLATE/question.md
vendored
@ -13,7 +13,7 @@ Before filing a question:
|
||||
- Search existing issues, including issues that are closed. It’s often the
|
||||
quickest way to get an answer!
|
||||
- Consult our FAQs, developer guide and the class reference of ExoPlayer. These
|
||||
can be found at https://google.github.io/ExoPlayer/.
|
||||
can be found at https://exoplayer.dev/.
|
||||
|
||||
When filing a question:
|
||||
-----------------------
|
||||
|
9
.gitignore
vendored
9
.gitignore
vendored
@ -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
|
||||
|
10
.hgignore
10
.hgignore
@ -44,6 +44,12 @@ local.properties
|
||||
proguard.cfg
|
||||
proguard-project.txt
|
||||
|
||||
# Bazel
|
||||
bazel-bin
|
||||
bazel-genfiles
|
||||
bazel-out
|
||||
bazel-testlogs
|
||||
|
||||
# Other
|
||||
.DS_Store
|
||||
cmake-build-debug
|
||||
@ -69,3 +75,7 @@ extensions/cronet/jniLibs/*
|
||||
!extensions/cronet/jniLibs/README.md
|
||||
extensions/cronet/libs/*
|
||||
!extensions/cronet/libs/README.md
|
||||
|
||||
# Cast receiver
|
||||
cast_receiver_app/external-js
|
||||
cast_receiver_app/bazel-cast_receiver_app
|
||||
|
18
README.md
18
README.md
@ -15,8 +15,8 @@ and extend, and can be updated through Play Store application updates.
|
||||
* Follow our [developer blog][] to keep up to date with the latest ExoPlayer
|
||||
developments!
|
||||
|
||||
[developer guide]: https://google.github.io/ExoPlayer/guide.html
|
||||
[class reference]: https://google.github.io/ExoPlayer/doc/reference
|
||||
[developer guide]: https://exoplayer.dev/guide.html
|
||||
[class reference]: https://exoplayer.dev/doc/reference
|
||||
[release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
|
||||
[developer blog]: https://medium.com/google-exoplayer
|
||||
|
||||
@ -95,20 +95,6 @@ compileOptions {
|
||||
}
|
||||
```
|
||||
|
||||
Note that if you want to use Java 8 features in your own code, the following
|
||||
additional options need to be set:
|
||||
|
||||
```gradle
|
||||
// For Java compilers:
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
// For Kotlin compilers:
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8
|
||||
}
|
||||
```
|
||||
|
||||
### Locally ###
|
||||
|
||||
Cloning the repository and depending on the modules locally is required when
|
||||
|
124
RELEASENOTES.md
124
RELEASENOTES.md
@ -1,5 +1,129 @@
|
||||
# Release notes #
|
||||
|
||||
### 2.10.0 ###
|
||||
|
||||
* Core library:
|
||||
* Improve decoder re-use between playbacks
|
||||
([#2826](https://github.com/google/ExoPlayer/issues/2826)). Read
|
||||
[this blog post](https://medium.com/google-exoplayer/improved-decoder-reuse-in-exoplayer-ef4c6d99591d)
|
||||
for more details.
|
||||
* Rename `ExtractorMediaSource` to `ProgressiveMediaSource`.
|
||||
* Fix issue where using `ProgressiveMediaSource.Factory` would mean that
|
||||
`DefaultExtractorsFactory` would be kept by proguard. Custom
|
||||
`ExtractorsFactory` instances must now be passed via the
|
||||
`ProgressiveMediaSource.Factory` constructor, and `setExtractorsFactory` is
|
||||
deprecated.
|
||||
* Move `PriorityTaskManager` from `DefaultLoadControl` to `SimpleExoPlayer`.
|
||||
* Add new `ExoPlaybackException` types for remote exceptions and out-of-memory
|
||||
errors.
|
||||
* Use full BCP 47 language tags in `Format`.
|
||||
* Do not retry failed loads whose error is `FileNotFoundException`.
|
||||
* Fix issue where not resetting the position for a new `MediaSource` in calls
|
||||
to `ExoPlayer.prepare` causes an `IndexOutOfBoundsException`
|
||||
([#5520](https://github.com/google/ExoPlayer/issues/5520)).
|
||||
* Offline:
|
||||
* Improve offline support. `DownloadManager` now tracks all offline content,
|
||||
not just tasks in progress. Read
|
||||
[this page](https://exoplayer.dev/downloading-media.html) for more details.
|
||||
* Caching:
|
||||
* Improve performance of `SimpleCache`
|
||||
([#4253](https://github.com/google/ExoPlayer/issues/4253)).
|
||||
* Cache data with unknown length by default. The previous flag to opt in to
|
||||
this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been
|
||||
replaced with an opt out flag
|
||||
(`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`).
|
||||
* Extractors:
|
||||
* MP4/FMP4: Add support for Dolby Vision.
|
||||
* MP4: Fix issue handling meta atoms in some streams
|
||||
([#5698](https://github.com/google/ExoPlayer/issues/5698),
|
||||
[#5694](https://github.com/google/ExoPlayer/issues/5694)).
|
||||
* MP3: Add support for SHOUTcast ICY metadata
|
||||
([#3735](https://github.com/google/ExoPlayer/issues/3735)).
|
||||
* MP3: Fix ID3 frame unsychronization
|
||||
([#5673](https://github.com/google/ExoPlayer/issues/5673)).
|
||||
* MP3: Fix playback of badly clipped files
|
||||
([#5772](https://github.com/google/ExoPlayer/issues/5772)).
|
||||
* MPEG-TS: Enable HDMV DTS stream detection only if a flag is set. By default
|
||||
(i.e. if the flag is not set), the 0x82 elementary stream type is now
|
||||
treated as an SCTE subtitle track
|
||||
([#5330](https://github.com/google/ExoPlayer/issues/5330)).
|
||||
* Track selection:
|
||||
* Add options for controlling audio track selections to `DefaultTrackSelector`
|
||||
([#3314](https://github.com/google/ExoPlayer/issues/3314)).
|
||||
* Update `TrackSelection.Factory` interface to support creating all track
|
||||
selections together.
|
||||
* Allow to specify a selection reason for a `SelectionOverride`.
|
||||
* When no text language preference matches, only select forced text tracks
|
||||
whose language matches the selected audio language.
|
||||
* UI:
|
||||
* Update `DefaultTimeBar` based on duration of media and add parameter to set
|
||||
the minimum update interval to control the smoothness of the updates
|
||||
([#5040](https://github.com/google/ExoPlayer/issues/5040)).
|
||||
* Move creation of dialogs for `TrackSelectionView`s to
|
||||
`TrackSelectionDialogBuilder` and add option to select multiple overrides.
|
||||
* Change signature of `PlayerNotificationManager.NotificationListener` to
|
||||
better fit service requirements.
|
||||
* Add option to include navigation actions in the compact mode of
|
||||
notifications created using `PlayerNotificationManager`.
|
||||
* Fix issues with flickering notifications on KitKat when using
|
||||
`PlayerNotificationManager` and `DownloadNotificationUtil`. For the latter,
|
||||
applications should switch to using `DownloadNotificationHelper`.
|
||||
* Fix accuracy of D-pad seeking in `DefaultTimeBar`
|
||||
([#5767](https://github.com/google/ExoPlayer/issues/5767)).
|
||||
* Audio:
|
||||
* Allow `AudioProcessor`s to be drained of pending output after they are
|
||||
reconfigured.
|
||||
* Fix an issue that caused audio to be truncated at the end of a period
|
||||
when switching to a new period where gapless playback information was newly
|
||||
present or newly absent.
|
||||
* Add support for reading AC-4 streams
|
||||
([#5303](https://github.com/google/ExoPlayer/pull/5303)).
|
||||
* Video:
|
||||
* Remove `MediaCodecSelector.DEFAULT_WITH_FALLBACK`. Apps should instead
|
||||
signal that fallback should be used by passing `true` as the
|
||||
`enableDecoderFallback` parameter when instantiating the video renderer.
|
||||
* Support video tunneling when the decoder is not listed first for the MIME
|
||||
type ([#3100](https://github.com/google/ExoPlayer/issues/3100)).
|
||||
* Query `MediaCodecList.ALL_CODECS` when selecting a tunneling decoder
|
||||
([#5547](https://github.com/google/ExoPlayer/issues/5547)).
|
||||
* DRM:
|
||||
* Fix black flicker when keys rotate in DRM protected content
|
||||
([#3561](https://github.com/google/ExoPlayer/issues/3561)).
|
||||
* Work around lack of LA_URL attribute in PlayReady key request init data.
|
||||
* CEA-608: Improved conformance to the specification
|
||||
([#3860](https://github.com/google/ExoPlayer/issues/3860)).
|
||||
* DASH:
|
||||
* Parse role and accessibility descriptors into `Format.roleFlags`.
|
||||
* Support multiple CEA-608 channels muxed into FMP4 representations
|
||||
([#5656](https://github.com/google/ExoPlayer/issues/5656)).
|
||||
* HLS:
|
||||
* Prevent unnecessary reloads of initialization segments.
|
||||
* Form an adaptive track group out of audio renditions with matching name.
|
||||
* Support encrypted initialization segments
|
||||
([#5441](https://github.com/google/ExoPlayer/issues/5441)).
|
||||
* Parse `EXT-X-MEDIA` `CHARACTERISTICS` attribute into `Format.roleFlags`.
|
||||
* Add metadata entry for HLS tracks to expose master playlist information.
|
||||
* Prevent `IndexOutOfBoundsException` in some live HLS scenarios
|
||||
([#5816](https://github.com/google/ExoPlayer/issues/5816)).
|
||||
* Support for playing spherical videos on Daydream.
|
||||
* Cast extension: Work around Cast framework returning a limited-size queue
|
||||
items list ([#4964](https://github.com/google/ExoPlayer/issues/4964)).
|
||||
* VP9 extension: Remove RGB output mode and libyuv dependency, and switch to
|
||||
surface YUV output as the default. Remove constructor parameters `scaleToFit`
|
||||
and `useSurfaceYuvOutput`.
|
||||
* MediaSession extension:
|
||||
* Let apps intercept media button events
|
||||
([#5179](https://github.com/google/ExoPlayer/issues/5179)).
|
||||
* Fix issue with `TimelineQueueNavigator` not publishing the queue in shuffled
|
||||
order when in shuffle mode.
|
||||
* Allow handling of custom commands via `registerCustomCommandReceiver`.
|
||||
* Add ability to include an extras `Bundle` when reporting a custom error.
|
||||
* LoadControl: Set minimum buffer for playbacks with video equal to maximum
|
||||
buffer ([#2083](https://github.com/google/ExoPlayer/issues/2083)).
|
||||
* Log warnings when extension native libraries can't be used, to help with
|
||||
diagnosing playback failures
|
||||
([#5788](https://github.com/google/ExoPlayer/issues/5788)).
|
||||
|
||||
### 2.9.6 ###
|
||||
|
||||
* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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 {
|
||||
*;
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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() {}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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'
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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')
|
||||
|
@ -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"/>
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
||||
|
59
demos/main/src/main/res/layout/track_selection_dialog.xml
Normal file
59
demos/main/src/main/res/layout/track_selection_dialog.xml
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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 ##
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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];
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
manifest=src/test/AndroidManifest.xml
|
@ -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
|
||||
|
@ -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')
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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};
|
||||
|
@ -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;
|
||||
|
@ -1 +0,0 @@
|
||||
manifest=src/test/AndroidManifest.xml
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
17
extensions/ffmpeg/src/test/AndroidManifest.xml
Normal file
17
extensions/ffmpeg/src/test/AndroidManifest.xml
Normal 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"/>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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')
|
||||
}
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
manifest=src/test/AndroidManifest.xml
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
28
extensions/gvr/src/main/res/layout/vr_ui.xml
Normal file
28
extensions/gvr/src/main/res/layout/vr_ui.xml
Normal 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>
|
@ -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>
|
18
extensions/gvr/src/main/res/values/styles.xml
Normal file
18
extensions/gvr/src/main/res/values/styles.xml
Normal 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>
|
@ -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
|
||||
|
@ -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')
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -1 +0,0 @@
|
||||
manifest=src/test/AndroidManifest.xml
|
@ -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 {
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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
Loading…
x
Reference in New Issue
Block a user