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.
|
- Search existing issues, including issues that are closed.
|
||||||
- Consult our FAQs, supported devices and supported formats pages. These can be
|
- Consult our FAQs, supported devices and supported formats pages. These can be
|
||||||
found at https://google.github.io/ExoPlayer/.
|
found at https://exoplayer.dev/.
|
||||||
- Rule out issues in your own code. A good way to do this is to try and
|
- Rule out issues in your own code. A good way to do this is to try and
|
||||||
reproduce the issue in the ExoPlayer demo app. Information about the ExoPlayer
|
reproduce the issue in the ExoPlayer demo app. Information about the ExoPlayer
|
||||||
demo app can be found here:
|
demo app can be found here:
|
||||||
http://google.github.io/ExoPlayer/demo-application.html.
|
http://exoplayer.dev/demo-application.html.
|
||||||
|
|
||||||
When reporting a bug:
|
When reporting a bug:
|
||||||
-----------------------
|
-----------------------
|
||||||
|
@ -10,10 +10,10 @@ Before filing a content issue:
|
|||||||
------------------------------
|
------------------------------
|
||||||
- Search existing issues, including issues that are closed.
|
- Search existing issues, including issues that are closed.
|
||||||
- Consult our supported formats page, which can be found at
|
- Consult our supported formats page, which can be found at
|
||||||
https://google.github.io/ExoPlayer/supported-formats.html.
|
https://exoplayer.dev/supported-formats.html.
|
||||||
- Try playing your content in the ExoPlayer demo app. Information about the
|
- Try playing your content in the ExoPlayer demo app. Information about the
|
||||||
ExoPlayer demo app can be found here:
|
ExoPlayer demo app can be found here:
|
||||||
http://google.github.io/ExoPlayer/demo-application.html.
|
http://exoplayer.dev/demo-application.html.
|
||||||
|
|
||||||
When reporting a content issue:
|
When reporting a content issue:
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
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
|
- Search existing issues, including issues that are closed. It’s often the
|
||||||
quickest way to get an answer!
|
quickest way to get an answer!
|
||||||
- Consult our FAQs, developer guide and the class reference of ExoPlayer. These
|
- Consult our FAQs, developer guide and the class reference of ExoPlayer. These
|
||||||
can be found at https://google.github.io/ExoPlayer/.
|
can be found at https://exoplayer.dev/.
|
||||||
|
|
||||||
When filing a question:
|
When filing a question:
|
||||||
-----------------------
|
-----------------------
|
||||||
|
9
.gitignore
vendored
9
.gitignore
vendored
@ -37,6 +37,12 @@ local.properties
|
|||||||
proguard.cfg
|
proguard.cfg
|
||||||
proguard-project.txt
|
proguard-project.txt
|
||||||
|
|
||||||
|
# Bazel
|
||||||
|
bazel-bin
|
||||||
|
bazel-genfiles
|
||||||
|
bazel-out
|
||||||
|
bazel-testlogs
|
||||||
|
|
||||||
# Other
|
# Other
|
||||||
.DS_Store
|
.DS_Store
|
||||||
cmake-build-debug
|
cmake-build-debug
|
||||||
@ -66,3 +72,6 @@ extensions/cronet/jniLibs/*
|
|||||||
extensions/cronet/libs/*
|
extensions/cronet/libs/*
|
||||||
!extensions/cronet/libs/README.md
|
!extensions/cronet/libs/README.md
|
||||||
|
|
||||||
|
# Cast receiver
|
||||||
|
cast_receiver_app/external-js
|
||||||
|
cast_receiver_app/bazel-cast_receiver_app
|
||||||
|
10
.hgignore
10
.hgignore
@ -44,6 +44,12 @@ local.properties
|
|||||||
proguard.cfg
|
proguard.cfg
|
||||||
proguard-project.txt
|
proguard-project.txt
|
||||||
|
|
||||||
|
# Bazel
|
||||||
|
bazel-bin
|
||||||
|
bazel-genfiles
|
||||||
|
bazel-out
|
||||||
|
bazel-testlogs
|
||||||
|
|
||||||
# Other
|
# Other
|
||||||
.DS_Store
|
.DS_Store
|
||||||
cmake-build-debug
|
cmake-build-debug
|
||||||
@ -69,3 +75,7 @@ extensions/cronet/jniLibs/*
|
|||||||
!extensions/cronet/jniLibs/README.md
|
!extensions/cronet/jniLibs/README.md
|
||||||
extensions/cronet/libs/*
|
extensions/cronet/libs/*
|
||||||
!extensions/cronet/libs/README.md
|
!extensions/cronet/libs/README.md
|
||||||
|
|
||||||
|
# Cast receiver
|
||||||
|
cast_receiver_app/external-js
|
||||||
|
cast_receiver_app/bazel-cast_receiver_app
|
||||||
|
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
|
* Follow our [developer blog][] to keep up to date with the latest ExoPlayer
|
||||||
developments!
|
developments!
|
||||||
|
|
||||||
[developer guide]: https://google.github.io/ExoPlayer/guide.html
|
[developer guide]: https://exoplayer.dev/guide.html
|
||||||
[class reference]: https://google.github.io/ExoPlayer/doc/reference
|
[class reference]: https://exoplayer.dev/doc/reference
|
||||||
[release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
|
[release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
|
||||||
[developer blog]: https://medium.com/google-exoplayer
|
[developer blog]: https://medium.com/google-exoplayer
|
||||||
|
|
||||||
@ -95,20 +95,6 @@ compileOptions {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that if you want to use Java 8 features in your own code, the following
|
|
||||||
additional options need to be set:
|
|
||||||
|
|
||||||
```gradle
|
|
||||||
// For Java compilers:
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
// For Kotlin compilers:
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Locally ###
|
### Locally ###
|
||||||
|
|
||||||
Cloning the repository and depending on the modules locally is required when
|
Cloning the repository and depending on the modules locally is required when
|
||||||
|
124
RELEASENOTES.md
124
RELEASENOTES.md
@ -1,5 +1,129 @@
|
|||||||
# Release notes #
|
# Release notes #
|
||||||
|
|
||||||
|
### 2.10.0 ###
|
||||||
|
|
||||||
|
* Core library:
|
||||||
|
* Improve decoder re-use between playbacks
|
||||||
|
([#2826](https://github.com/google/ExoPlayer/issues/2826)). Read
|
||||||
|
[this blog post](https://medium.com/google-exoplayer/improved-decoder-reuse-in-exoplayer-ef4c6d99591d)
|
||||||
|
for more details.
|
||||||
|
* Rename `ExtractorMediaSource` to `ProgressiveMediaSource`.
|
||||||
|
* Fix issue where using `ProgressiveMediaSource.Factory` would mean that
|
||||||
|
`DefaultExtractorsFactory` would be kept by proguard. Custom
|
||||||
|
`ExtractorsFactory` instances must now be passed via the
|
||||||
|
`ProgressiveMediaSource.Factory` constructor, and `setExtractorsFactory` is
|
||||||
|
deprecated.
|
||||||
|
* Move `PriorityTaskManager` from `DefaultLoadControl` to `SimpleExoPlayer`.
|
||||||
|
* Add new `ExoPlaybackException` types for remote exceptions and out-of-memory
|
||||||
|
errors.
|
||||||
|
* Use full BCP 47 language tags in `Format`.
|
||||||
|
* Do not retry failed loads whose error is `FileNotFoundException`.
|
||||||
|
* Fix issue where not resetting the position for a new `MediaSource` in calls
|
||||||
|
to `ExoPlayer.prepare` causes an `IndexOutOfBoundsException`
|
||||||
|
([#5520](https://github.com/google/ExoPlayer/issues/5520)).
|
||||||
|
* Offline:
|
||||||
|
* Improve offline support. `DownloadManager` now tracks all offline content,
|
||||||
|
not just tasks in progress. Read
|
||||||
|
[this page](https://exoplayer.dev/downloading-media.html) for more details.
|
||||||
|
* Caching:
|
||||||
|
* Improve performance of `SimpleCache`
|
||||||
|
([#4253](https://github.com/google/ExoPlayer/issues/4253)).
|
||||||
|
* Cache data with unknown length by default. The previous flag to opt in to
|
||||||
|
this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been
|
||||||
|
replaced with an opt out flag
|
||||||
|
(`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`).
|
||||||
|
* Extractors:
|
||||||
|
* MP4/FMP4: Add support for Dolby Vision.
|
||||||
|
* MP4: Fix issue handling meta atoms in some streams
|
||||||
|
([#5698](https://github.com/google/ExoPlayer/issues/5698),
|
||||||
|
[#5694](https://github.com/google/ExoPlayer/issues/5694)).
|
||||||
|
* MP3: Add support for SHOUTcast ICY metadata
|
||||||
|
([#3735](https://github.com/google/ExoPlayer/issues/3735)).
|
||||||
|
* MP3: Fix ID3 frame unsychronization
|
||||||
|
([#5673](https://github.com/google/ExoPlayer/issues/5673)).
|
||||||
|
* MP3: Fix playback of badly clipped files
|
||||||
|
([#5772](https://github.com/google/ExoPlayer/issues/5772)).
|
||||||
|
* MPEG-TS: Enable HDMV DTS stream detection only if a flag is set. By default
|
||||||
|
(i.e. if the flag is not set), the 0x82 elementary stream type is now
|
||||||
|
treated as an SCTE subtitle track
|
||||||
|
([#5330](https://github.com/google/ExoPlayer/issues/5330)).
|
||||||
|
* Track selection:
|
||||||
|
* Add options for controlling audio track selections to `DefaultTrackSelector`
|
||||||
|
([#3314](https://github.com/google/ExoPlayer/issues/3314)).
|
||||||
|
* Update `TrackSelection.Factory` interface to support creating all track
|
||||||
|
selections together.
|
||||||
|
* Allow to specify a selection reason for a `SelectionOverride`.
|
||||||
|
* When no text language preference matches, only select forced text tracks
|
||||||
|
whose language matches the selected audio language.
|
||||||
|
* UI:
|
||||||
|
* Update `DefaultTimeBar` based on duration of media and add parameter to set
|
||||||
|
the minimum update interval to control the smoothness of the updates
|
||||||
|
([#5040](https://github.com/google/ExoPlayer/issues/5040)).
|
||||||
|
* Move creation of dialogs for `TrackSelectionView`s to
|
||||||
|
`TrackSelectionDialogBuilder` and add option to select multiple overrides.
|
||||||
|
* Change signature of `PlayerNotificationManager.NotificationListener` to
|
||||||
|
better fit service requirements.
|
||||||
|
* Add option to include navigation actions in the compact mode of
|
||||||
|
notifications created using `PlayerNotificationManager`.
|
||||||
|
* Fix issues with flickering notifications on KitKat when using
|
||||||
|
`PlayerNotificationManager` and `DownloadNotificationUtil`. For the latter,
|
||||||
|
applications should switch to using `DownloadNotificationHelper`.
|
||||||
|
* Fix accuracy of D-pad seeking in `DefaultTimeBar`
|
||||||
|
([#5767](https://github.com/google/ExoPlayer/issues/5767)).
|
||||||
|
* Audio:
|
||||||
|
* Allow `AudioProcessor`s to be drained of pending output after they are
|
||||||
|
reconfigured.
|
||||||
|
* Fix an issue that caused audio to be truncated at the end of a period
|
||||||
|
when switching to a new period where gapless playback information was newly
|
||||||
|
present or newly absent.
|
||||||
|
* Add support for reading AC-4 streams
|
||||||
|
([#5303](https://github.com/google/ExoPlayer/pull/5303)).
|
||||||
|
* Video:
|
||||||
|
* Remove `MediaCodecSelector.DEFAULT_WITH_FALLBACK`. Apps should instead
|
||||||
|
signal that fallback should be used by passing `true` as the
|
||||||
|
`enableDecoderFallback` parameter when instantiating the video renderer.
|
||||||
|
* Support video tunneling when the decoder is not listed first for the MIME
|
||||||
|
type ([#3100](https://github.com/google/ExoPlayer/issues/3100)).
|
||||||
|
* Query `MediaCodecList.ALL_CODECS` when selecting a tunneling decoder
|
||||||
|
([#5547](https://github.com/google/ExoPlayer/issues/5547)).
|
||||||
|
* DRM:
|
||||||
|
* Fix black flicker when keys rotate in DRM protected content
|
||||||
|
([#3561](https://github.com/google/ExoPlayer/issues/3561)).
|
||||||
|
* Work around lack of LA_URL attribute in PlayReady key request init data.
|
||||||
|
* CEA-608: Improved conformance to the specification
|
||||||
|
([#3860](https://github.com/google/ExoPlayer/issues/3860)).
|
||||||
|
* DASH:
|
||||||
|
* Parse role and accessibility descriptors into `Format.roleFlags`.
|
||||||
|
* Support multiple CEA-608 channels muxed into FMP4 representations
|
||||||
|
([#5656](https://github.com/google/ExoPlayer/issues/5656)).
|
||||||
|
* HLS:
|
||||||
|
* Prevent unnecessary reloads of initialization segments.
|
||||||
|
* Form an adaptive track group out of audio renditions with matching name.
|
||||||
|
* Support encrypted initialization segments
|
||||||
|
([#5441](https://github.com/google/ExoPlayer/issues/5441)).
|
||||||
|
* Parse `EXT-X-MEDIA` `CHARACTERISTICS` attribute into `Format.roleFlags`.
|
||||||
|
* Add metadata entry for HLS tracks to expose master playlist information.
|
||||||
|
* Prevent `IndexOutOfBoundsException` in some live HLS scenarios
|
||||||
|
([#5816](https://github.com/google/ExoPlayer/issues/5816)).
|
||||||
|
* Support for playing spherical videos on Daydream.
|
||||||
|
* Cast extension: Work around Cast framework returning a limited-size queue
|
||||||
|
items list ([#4964](https://github.com/google/ExoPlayer/issues/4964)).
|
||||||
|
* VP9 extension: Remove RGB output mode and libyuv dependency, and switch to
|
||||||
|
surface YUV output as the default. Remove constructor parameters `scaleToFit`
|
||||||
|
and `useSurfaceYuvOutput`.
|
||||||
|
* MediaSession extension:
|
||||||
|
* Let apps intercept media button events
|
||||||
|
([#5179](https://github.com/google/ExoPlayer/issues/5179)).
|
||||||
|
* Fix issue with `TimelineQueueNavigator` not publishing the queue in shuffled
|
||||||
|
order when in shuffle mode.
|
||||||
|
* Allow handling of custom commands via `registerCustomCommandReceiver`.
|
||||||
|
* Add ability to include an extras `Bundle` when reporting a custom error.
|
||||||
|
* LoadControl: Set minimum buffer for playbacks with video equal to maximum
|
||||||
|
buffer ([#2083](https://github.com/google/ExoPlayer/issues/2083)).
|
||||||
|
* Log warnings when extension native libraries can't be used, to help with
|
||||||
|
diagnosing playback failures
|
||||||
|
([#5788](https://github.com/google/ExoPlayer/issues/5788)).
|
||||||
|
|
||||||
### 2.9.6 ###
|
### 2.9.6 ###
|
||||||
|
|
||||||
* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`.
|
* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`.
|
||||||
|
@ -17,9 +17,9 @@ buildscript {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.1.4'
|
classpath 'com.android.tools.build:gradle:3.4.0'
|
||||||
classpath 'com.novoda:bintray-release:0.8.1'
|
classpath 'com.novoda:bintray-release:0.9'
|
||||||
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.0.3'
|
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.1.0'
|
||||||
}
|
}
|
||||||
// Workaround for the following test coverage issue. Remove when fixed:
|
// Workaround for the following test coverage issue. Remove when fixed:
|
||||||
// https://code.google.com/p/android/issues/detail?id=226070
|
// https://code.google.com/p/android/issues/detail?id=226070
|
||||||
|
@ -13,26 +13,17 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
project.ext {
|
project.ext {
|
||||||
// ExoPlayer version and version code.
|
// ExoPlayer version and version code.
|
||||||
releaseVersion = '2.9.6'
|
releaseVersion = '2.10.0'
|
||||||
releaseVersionCode = 2009006
|
releaseVersionCode = 2010000
|
||||||
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
|
minSdkVersion = 16
|
||||||
// components provided by the library may be of use on older devices.
|
|
||||||
// However, please note that the core media playback functionality provided
|
|
||||||
// by the library requires API level 16 or greater.
|
|
||||||
minSdkVersion = 14
|
|
||||||
targetSdkVersion = 28
|
targetSdkVersion = 28
|
||||||
compileSdkVersion = 28
|
compileSdkVersion = 28
|
||||||
buildToolsVersion = '28.0.2'
|
dexmakerVersion = '2.21.0'
|
||||||
testSupportLibraryVersion = '0.5'
|
mockitoVersion = '2.25.0'
|
||||||
supportLibraryVersion = '27.1.1'
|
robolectricVersion = '4.2'
|
||||||
dexmakerVersion = '1.2'
|
|
||||||
mockitoVersion = '1.9.5'
|
|
||||||
junitVersion = '4.12'
|
|
||||||
truthVersion = '0.39'
|
|
||||||
robolectricVersion = '3.7.1'
|
|
||||||
autoValueVersion = '1.6'
|
autoValueVersion = '1.6'
|
||||||
checkerframeworkVersion = '2.5.0'
|
checkerframeworkVersion = '2.5.0'
|
||||||
testRunnerVersion = '1.1.0-alpha3'
|
androidXTestVersion = '1.1.0'
|
||||||
modulePrefix = ':'
|
modulePrefix = ':'
|
||||||
if (gradle.ext.has('exoplayerModulePrefix')) {
|
if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||||
modulePrefix += gradle.ext.exoplayerModulePrefix
|
modulePrefix += gradle.ext.exoplayerModulePrefix
|
||||||
|
@ -16,7 +16,6 @@ apply plugin: 'com.android.application'
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
@ -26,7 +25,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
versionName project.ext.releaseVersion
|
versionName project.ext.releaseVersion
|
||||||
versionCode project.ext.releaseVersionCode
|
versionCode project.ext.releaseVersionCode
|
||||||
minSdkVersion 16
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,8 +44,18 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
// The demo app does not have translations.
|
// The demo app isn't indexed and doesn't have translations.
|
||||||
disable 'MissingTranslation'
|
disable 'GoogleAppIndexingWarning','MissingTranslation'
|
||||||
|
}
|
||||||
|
|
||||||
|
flavorDimensions "receiver"
|
||||||
|
|
||||||
|
productFlavors {
|
||||||
|
defaultCast {
|
||||||
|
dimension "receiver"
|
||||||
|
manifestPlaceholders =
|
||||||
|
[castOptionsProvider: "com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -58,9 +67,10 @@ dependencies {
|
|||||||
implementation project(modulePrefix + 'library-smoothstreaming')
|
implementation project(modulePrefix + 'library-smoothstreaming')
|
||||||
implementation project(modulePrefix + 'library-ui')
|
implementation project(modulePrefix + 'library-ui')
|
||||||
implementation project(modulePrefix + 'extension-cast')
|
implementation project(modulePrefix + 'extension-cast')
|
||||||
implementation 'com.android.support:support-v4:' + supportLibraryVersion
|
implementation 'com.google.android.material:material:1.0.0'
|
||||||
implementation 'com.android.support:appcompat-v7:' + supportLibraryVersion
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
implementation 'com.android.support:recyclerview-v7:' + supportLibraryVersion
|
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||||
|
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Proguard rules specific to the Cast demo app.
|
# Proguard rules specific to the Cast demo app.
|
||||||
|
|
||||||
# Accessed via menu.xml
|
# Accessed via menu.xml
|
||||||
-keep class android.support.v7.app.MediaRouteActionProvider {
|
-keep class androidx.mediarouter.app.MediaRouteActionProvider {
|
||||||
*;
|
*;
|
||||||
}
|
}
|
||||||
|
@ -17,13 +17,15 @@
|
|||||||
package="com.google.android.exoplayer2.castdemo">
|
package="com.google.android.exoplayer2.castdemo">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
|
|
||||||
<uses-sdk/>
|
<uses-sdk/>
|
||||||
|
|
||||||
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
|
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
|
||||||
android:largeHeap="true" android:allowBackup="false">
|
android:largeHeap="true" android:allowBackup="false">
|
||||||
|
|
||||||
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||||
android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider" />
|
android:value="${castOptionsProvider}" />
|
||||||
|
|
||||||
<activity android:name="com.google.android.exoplayer2.castdemo.MainActivity"
|
<activity android:name="com.google.android.exoplayer2.castdemo.MainActivity"
|
||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
||||||
|
@ -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;
|
package com.google.android.exoplayer2.castdemo;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import com.google.android.gms.cast.MediaInfo;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/** Utility methods and constants for the Cast demo application. */
|
||||||
* Utility methods and constants for the Cast demo application.
|
|
||||||
*/
|
|
||||||
/* package */ final class DemoUtil {
|
/* package */ final class DemoUtil {
|
||||||
|
|
||||||
public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD;
|
/** Represents a media sample. */
|
||||||
public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8;
|
|
||||||
public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS;
|
|
||||||
public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The list of samples available in the cast demo app.
|
|
||||||
*/
|
|
||||||
public static final List<Sample> SAMPLES;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a media sample.
|
|
||||||
*/
|
|
||||||
public static final class Sample {
|
public static final class Sample {
|
||||||
|
|
||||||
/**
|
/** The uri of the media content. */
|
||||||
* The uri from which the media sample is obtained.
|
|
||||||
*/
|
|
||||||
public final String uri;
|
public final String uri;
|
||||||
/**
|
/** The name of the sample. */
|
||||||
* A descriptive name for the sample.
|
|
||||||
*/
|
|
||||||
public final String name;
|
public final String name;
|
||||||
/**
|
/** The mime type of the sample media content. */
|
||||||
* The mime type of the media sample, as required by {@link MediaInfo#setContentType}.
|
|
||||||
*/
|
|
||||||
public final String mimeType;
|
public final String mimeType;
|
||||||
|
/**
|
||||||
|
* The {@link UUID} of the DRM scheme that protects the content, or null if the content is not
|
||||||
|
* DRM-protected.
|
||||||
|
*/
|
||||||
|
@Nullable public final UUID drmSchemeUuid;
|
||||||
|
/**
|
||||||
|
* The url from which players should obtain DRM licenses, or null if the content is not
|
||||||
|
* DRM-protected.
|
||||||
|
*/
|
||||||
|
@Nullable public final Uri licenseServerUri;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param uri See {@link #uri}.
|
* @param uri See {@link #uri}.
|
||||||
@ -60,31 +53,53 @@ import java.util.List;
|
|||||||
* @param mimeType See {@link #mimeType}.
|
* @param mimeType See {@link #mimeType}.
|
||||||
*/
|
*/
|
||||||
public Sample(String uri, String name, String mimeType) {
|
public Sample(String uri, String name, String mimeType) {
|
||||||
|
this(uri, name, mimeType, /* drmSchemeUuid= */ null, /* licenseServerUriString= */ null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Sample(
|
||||||
|
String uri,
|
||||||
|
String name,
|
||||||
|
String mimeType,
|
||||||
|
@Nullable UUID drmSchemeUuid,
|
||||||
|
@Nullable String licenseServerUriString) {
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.mimeType = mimeType;
|
this.mimeType = mimeType;
|
||||||
|
this.drmSchemeUuid = drmSchemeUuid;
|
||||||
|
this.licenseServerUri =
|
||||||
|
licenseServerUriString != null ? Uri.parse(licenseServerUriString) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD;
|
||||||
|
public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8;
|
||||||
|
public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS;
|
||||||
|
public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4;
|
||||||
|
|
||||||
|
/** The list of samples available in the cast demo app. */
|
||||||
|
public static final List<Sample> SAMPLES;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
// App samples.
|
// App samples.
|
||||||
ArrayList<Sample> samples = new ArrayList<>();
|
ArrayList<Sample> samples = new ArrayList<>();
|
||||||
samples.add(new Sample("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
|
|
||||||
"DASH (clear,MP4,H264)", MIME_TYPE_DASH));
|
|
||||||
samples.add(new Sample("https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/"
|
|
||||||
+ "hls/TearsOfSteel.m3u8", "Tears of Steel (HLS)", MIME_TYPE_HLS));
|
|
||||||
samples.add(new Sample("https://html5demos.com/assets/dizzy.mp4", "Dizzy (MP4)",
|
|
||||||
MIME_TYPE_VIDEO_MP4));
|
|
||||||
SAMPLES = Collections.unmodifiableList(samples);
|
|
||||||
|
|
||||||
|
// Clear content.
|
||||||
|
samples.add(
|
||||||
|
new Sample(
|
||||||
|
"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
|
||||||
|
"Clear DASH: Tears",
|
||||||
|
MIME_TYPE_DASH));
|
||||||
|
samples.add(
|
||||||
|
new Sample(
|
||||||
|
"https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4));
|
||||||
|
|
||||||
|
SAMPLES = Collections.unmodifiableList(samples);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DemoUtil() {}
|
private DemoUtil() {}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -17,13 +17,13 @@ package com.google.android.exoplayer2.castdemo;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.graphics.ColorUtils;
|
import androidx.core.graphics.ColorUtils;
|
||||||
import android.support.v7.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import android.support.v7.widget.RecyclerView.ViewHolder;
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
@ -33,21 +33,26 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
import android.widget.ListView;
|
import android.widget.ListView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.castdemo.DemoUtil.Sample;
|
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
||||||
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
|
||||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||||
import com.google.android.exoplayer2.ui.PlayerView;
|
import com.google.android.exoplayer2.ui.PlayerView;
|
||||||
|
import com.google.android.gms.cast.CastMediaControlIntent;
|
||||||
import com.google.android.gms.cast.framework.CastButtonFactory;
|
import com.google.android.gms.cast.framework.CastButtonFactory;
|
||||||
import com.google.android.gms.cast.framework.CastContext;
|
import com.google.android.gms.cast.framework.CastContext;
|
||||||
import com.google.android.gms.dynamite.DynamiteModule;
|
import com.google.android.gms.dynamite.DynamiteModule;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An activity that plays video using {@link SimpleExoPlayer} and {@link CastPlayer}.
|
* An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's
|
||||||
|
* Cast extension.
|
||||||
*/
|
*/
|
||||||
public class MainActivity extends AppCompatActivity implements OnClickListener,
|
public class MainActivity extends AppCompatActivity
|
||||||
PlayerManager.QueuePositionListener {
|
implements OnClickListener, PlayerManager.Listener {
|
||||||
|
|
||||||
|
private final MediaItem.Builder mediaItemBuilder;
|
||||||
|
|
||||||
private PlayerView localPlayerView;
|
private PlayerView localPlayerView;
|
||||||
private PlayerControlView castControlView;
|
private PlayerControlView castControlView;
|
||||||
@ -56,6 +61,10 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||||||
private MediaQueueListAdapter mediaQueueListAdapter;
|
private MediaQueueListAdapter mediaQueueListAdapter;
|
||||||
private CastContext castContext;
|
private CastContext castContext;
|
||||||
|
|
||||||
|
public MainActivity() {
|
||||||
|
mediaItemBuilder = new MediaItem.Builder();
|
||||||
|
}
|
||||||
|
|
||||||
// Activity lifecycle methods.
|
// Activity lifecycle methods.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -68,7 +77,7 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||||||
Throwable cause = e.getCause();
|
Throwable cause = e.getCause();
|
||||||
while (cause != null) {
|
while (cause != null) {
|
||||||
if (cause instanceof DynamiteModule.LoadingException) {
|
if (cause instanceof DynamiteModule.LoadingException) {
|
||||||
setContentView(R.layout.cast_context_error_message_layout);
|
setContentView(R.layout.cast_context_error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cause = cause.getCause();
|
cause = cause.getCause();
|
||||||
@ -109,13 +118,20 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||||||
// There is no Cast context to work with. Do nothing.
|
// There is no Cast context to work with. Do nothing.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
playerManager =
|
String applicationId = castContext.getCastOptions().getReceiverApplicationId();
|
||||||
PlayerManager.createPlayerManager(
|
switch (applicationId) {
|
||||||
/* queuePositionListener= */ this,
|
case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID:
|
||||||
localPlayerView,
|
playerManager =
|
||||||
castControlView,
|
new DefaultReceiverPlayerManager(
|
||||||
/* context= */ this,
|
/* listener= */ this,
|
||||||
castContext);
|
localPlayerView,
|
||||||
|
castControlView,
|
||||||
|
/* context= */ this,
|
||||||
|
castContext);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException("Illegal receiver app id: " + applicationId);
|
||||||
|
}
|
||||||
mediaQueueList.setAdapter(mediaQueueListAdapter);
|
mediaQueueList.setAdapter(mediaQueueListAdapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,6 +145,7 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||||||
mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount());
|
mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount());
|
||||||
mediaQueueList.setAdapter(null);
|
mediaQueueList.setAdapter(null);
|
||||||
playerManager.release();
|
playerManager.release();
|
||||||
|
playerManager = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activity input.
|
// Activity input.
|
||||||
@ -141,12 +158,15 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
new AlertDialog.Builder(this).setTitle(R.string.sample_list_dialog_title)
|
new AlertDialog.Builder(this)
|
||||||
.setView(buildSampleListView()).setPositiveButton(android.R.string.ok, null).create()
|
.setTitle(R.string.add_samples)
|
||||||
|
.setView(buildSampleListView())
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.create()
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlayerManager.QueuePositionListener implementation.
|
// PlayerManager.Listener implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onQueuePositionChanged(int previousIndex, int newIndex) {
|
public void onQueuePositionChanged(int previousIndex, int newIndex) {
|
||||||
@ -158,6 +178,16 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onQueueContentsExternallyChanged() {
|
||||||
|
mediaQueueListAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayerError() {
|
||||||
|
Toast.makeText(getApplicationContext(), R.string.player_error_msg, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
private View buildSampleListView() {
|
private View buildSampleListView() {
|
||||||
@ -166,7 +196,19 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||||||
sampleList.setAdapter(new SampleListAdapter(this));
|
sampleList.setAdapter(new SampleListAdapter(this));
|
||||||
sampleList.setOnItemClickListener(
|
sampleList.setOnItemClickListener(
|
||||||
(parent, view, position, id) -> {
|
(parent, view, position, id) -> {
|
||||||
playerManager.addItem(DemoUtil.SAMPLES.get(position));
|
DemoUtil.Sample sample = DemoUtil.SAMPLES.get(position);
|
||||||
|
mediaItemBuilder
|
||||||
|
.clear()
|
||||||
|
.setMedia(sample.uri)
|
||||||
|
.setTitle(sample.name)
|
||||||
|
.setMimeType(sample.mimeType);
|
||||||
|
if (sample.drmSchemeUuid != null) {
|
||||||
|
mediaItemBuilder.setDrmSchemes(
|
||||||
|
Collections.singletonList(
|
||||||
|
new MediaItem.DrmScheme(
|
||||||
|
sample.drmSchemeUuid, new MediaItem.UriBundle(sample.licenseServerUri))));
|
||||||
|
}
|
||||||
|
playerManager.addItem(mediaItemBuilder.build());
|
||||||
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
|
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
|
||||||
});
|
});
|
||||||
return dialogList;
|
return dialogList;
|
||||||
@ -174,23 +216,6 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||||||
|
|
||||||
// Internal classes.
|
// Internal classes.
|
||||||
|
|
||||||
private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener {
|
|
||||||
|
|
||||||
public final TextView textView;
|
|
||||||
|
|
||||||
public QueueItemViewHolder(TextView textView) {
|
|
||||||
super(textView);
|
|
||||||
this.textView = textView;
|
|
||||||
textView.setOnClickListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
playerManager.selectQueueItem(getAdapterPosition());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private class MediaQueueListAdapter extends RecyclerView.Adapter<QueueItemViewHolder> {
|
private class MediaQueueListAdapter extends RecyclerView.Adapter<QueueItemViewHolder> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -202,8 +227,9 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(QueueItemViewHolder holder, int position) {
|
public void onBindViewHolder(QueueItemViewHolder holder, int position) {
|
||||||
|
holder.item = playerManager.getItem(position);
|
||||||
TextView view = holder.textView;
|
TextView view = holder.textView;
|
||||||
view.setText(playerManager.getItem(position).name);
|
view.setText(holder.item.title);
|
||||||
// TODO: Solve coloring using the theme's ColorStateList.
|
// TODO: Solve coloring using the theme's ColorStateList.
|
||||||
view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(),
|
view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(),
|
||||||
position == playerManager.getCurrentItemIndex() ? 255 : 100));
|
position == playerManager.getCurrentItemIndex() ? 255 : 100));
|
||||||
@ -244,8 +270,11 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||||||
@Override
|
@Override
|
||||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
|
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
|
||||||
int position = viewHolder.getAdapterPosition();
|
int position = viewHolder.getAdapterPosition();
|
||||||
if (playerManager.removeItem(position)) {
|
QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
|
||||||
|
if (playerManager.removeItem(queueItemHolder.item)) {
|
||||||
mediaQueueListAdapter.notifyItemRemoved(position);
|
mediaQueueListAdapter.notifyItemRemoved(position);
|
||||||
|
// Update whichever item took its place, in case it became the new selected item.
|
||||||
|
mediaQueueListAdapter.notifyItemChanged(position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,8 +282,9 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||||||
public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
|
public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
|
||||||
super.clearView(recyclerView, viewHolder);
|
super.clearView(recyclerView, viewHolder);
|
||||||
if (draggingFromPosition != C.INDEX_UNSET) {
|
if (draggingFromPosition != C.INDEX_UNSET) {
|
||||||
|
QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
|
||||||
// A drag has ended. We reflect the media queue change in the player.
|
// A drag has ended. We reflect the media queue change in the player.
|
||||||
if (!playerManager.moveItem(draggingFromPosition, draggingToPosition)) {
|
if (!playerManager.moveItem(queueItemHolder.item, draggingToPosition)) {
|
||||||
// The move failed. The entire sequence of onMove calls since the drag started needs to be
|
// The move failed. The entire sequence of onMove calls since the drag started needs to be
|
||||||
// invalidated.
|
// invalidated.
|
||||||
mediaQueueListAdapter.notifyDataSetChanged();
|
mediaQueueListAdapter.notifyDataSetChanged();
|
||||||
@ -263,15 +293,30 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||||||
draggingFromPosition = C.INDEX_UNSET;
|
draggingFromPosition = C.INDEX_UNSET;
|
||||||
draggingToPosition = C.INDEX_UNSET;
|
draggingToPosition = C.INDEX_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class SampleListAdapter extends ArrayAdapter<Sample> {
|
private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener {
|
||||||
|
|
||||||
|
public final TextView textView;
|
||||||
|
public MediaItem item;
|
||||||
|
|
||||||
|
public QueueItemViewHolder(TextView textView) {
|
||||||
|
super(textView);
|
||||||
|
this.textView = textView;
|
||||||
|
textView.setOnClickListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
playerManager.selectQueueItem(getAdapterPosition());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class SampleListAdapter extends ArrayAdapter<DemoUtil.Sample> {
|
||||||
|
|
||||||
public SampleListAdapter(Context context) {
|
public SampleListAdapter(Context context) {
|
||||||
super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);
|
super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2017 The Android Open Source Project
|
* Copyright (C) 2019 The Android Open Source Project
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -15,402 +15,53 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.castdemo;
|
package com.google.android.exoplayer2.castdemo;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
||||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
|
||||||
import com.google.android.exoplayer2.Player;
|
|
||||||
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
|
||||||
import com.google.android.exoplayer2.Player.EventListener;
|
|
||||||
import com.google.android.exoplayer2.Player.TimelineChangeReason;
|
|
||||||
import com.google.android.exoplayer2.RenderersFactory;
|
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
|
||||||
import com.google.android.exoplayer2.Timeline;
|
|
||||||
import com.google.android.exoplayer2.Timeline.Period;
|
|
||||||
import com.google.android.exoplayer2.castdemo.DemoUtil.Sample;
|
|
||||||
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
|
||||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
|
||||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
|
||||||
import com.google.android.exoplayer2.ui.PlayerView;
|
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
|
||||||
import com.google.android.gms.cast.MediaInfo;
|
|
||||||
import com.google.android.gms.cast.MediaMetadata;
|
|
||||||
import com.google.android.gms.cast.MediaQueueItem;
|
|
||||||
import com.google.android.gms.cast.framework.CastContext;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
/** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */
|
/** Manages the players in the Cast demo app. */
|
||||||
/* package */ final class PlayerManager
|
/* package */ interface PlayerManager {
|
||||||
implements EventListener, CastPlayer.SessionAvailabilityListener {
|
|
||||||
|
|
||||||
/**
|
/** Listener for events. */
|
||||||
* Listener for changes in the media queue playback position.
|
interface Listener {
|
||||||
*/
|
|
||||||
public interface QueuePositionListener {
|
|
||||||
|
|
||||||
/**
|
/** Called when the currently played item of the media queue changes. */
|
||||||
* Called when the currently played item of the media queue changes.
|
|
||||||
*/
|
|
||||||
void onQueuePositionChanged(int previousIndex, int newIndex);
|
void onQueuePositionChanged(int previousIndex, int newIndex);
|
||||||
|
|
||||||
|
/** Called when the media queue changes due to modifications not caused by this manager. */
|
||||||
|
void onQueueContentsExternallyChanged();
|
||||||
|
|
||||||
|
/** Called when an error occurs in the current player. */
|
||||||
|
void onPlayerError();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String USER_AGENT = "ExoCastDemoPlayer";
|
/** Redirects the given {@code keyEvent} to the active player. */
|
||||||
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
|
boolean dispatchKeyEvent(KeyEvent keyEvent);
|
||||||
new DefaultHttpDataSourceFactory(USER_AGENT);
|
|
||||||
|
|
||||||
private final PlayerView localPlayerView;
|
/** Appends the given {@link MediaItem} to the media queue. */
|
||||||
private final PlayerControlView castControlView;
|
void addItem(MediaItem mediaItem);
|
||||||
private final SimpleExoPlayer exoPlayer;
|
|
||||||
private final CastPlayer castPlayer;
|
|
||||||
private final ArrayList<DemoUtil.Sample> mediaQueue;
|
|
||||||
private final QueuePositionListener queuePositionListener;
|
|
||||||
private final ConcatenatingMediaSource concatenatingMediaSource;
|
|
||||||
|
|
||||||
private boolean castMediaQueueCreationPending;
|
/** Returns the number of items in the media queue. */
|
||||||
private int currentItemIndex;
|
int getMediaQueueSize();
|
||||||
private Player currentPlayer;
|
|
||||||
|
/** Selects the item at the given position for playback. */
|
||||||
|
void selectQueueItem(int position);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param queuePositionListener A {@link QueuePositionListener} for queue position changes.
|
* Returns the position of the item currently being played, or {@link C#INDEX_UNSET} if no item is
|
||||||
* @param localPlayerView The {@link PlayerView} for local playback.
|
* being played.
|
||||||
* @param castControlView The {@link PlayerControlView} to control remote playback.
|
|
||||||
* @param context A {@link Context}.
|
|
||||||
* @param castContext The {@link CastContext}.
|
|
||||||
*/
|
*/
|
||||||
public static PlayerManager createPlayerManager(
|
int getCurrentItemIndex();
|
||||||
QueuePositionListener queuePositionListener,
|
|
||||||
PlayerView localPlayerView,
|
|
||||||
PlayerControlView castControlView,
|
|
||||||
Context context,
|
|
||||||
CastContext castContext) {
|
|
||||||
PlayerManager playerManager =
|
|
||||||
new PlayerManager(
|
|
||||||
queuePositionListener, localPlayerView, castControlView, context, castContext);
|
|
||||||
playerManager.init();
|
|
||||||
return playerManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
private PlayerManager(
|
/** Returns the {@link MediaItem} at the given {@code position}. */
|
||||||
QueuePositionListener queuePositionListener,
|
MediaItem getItem(int position);
|
||||||
PlayerView localPlayerView,
|
|
||||||
PlayerControlView castControlView,
|
|
||||||
Context context,
|
|
||||||
CastContext castContext) {
|
|
||||||
this.queuePositionListener = queuePositionListener;
|
|
||||||
this.localPlayerView = localPlayerView;
|
|
||||||
this.castControlView = castControlView;
|
|
||||||
mediaQueue = new ArrayList<>();
|
|
||||||
currentItemIndex = C.INDEX_UNSET;
|
|
||||||
concatenatingMediaSource = new ConcatenatingMediaSource();
|
|
||||||
|
|
||||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
|
/** Moves the item at position {@code from} to position {@code to}. */
|
||||||
RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
|
boolean moveItem(MediaItem item, int to);
|
||||||
exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector);
|
|
||||||
exoPlayer.addListener(this);
|
|
||||||
localPlayerView.setPlayer(exoPlayer);
|
|
||||||
|
|
||||||
castPlayer = new CastPlayer(castContext);
|
/** Removes the item at position {@code index}. */
|
||||||
castPlayer.addListener(this);
|
boolean removeItem(MediaItem item);
|
||||||
castPlayer.setSessionAvailabilityListener(this);
|
|
||||||
castControlView.setPlayer(castPlayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Queue manipulation methods.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plays a specified queue item in the current player.
|
|
||||||
*
|
|
||||||
* @param itemIndex The index of the item to play.
|
|
||||||
*/
|
|
||||||
public void selectQueueItem(int itemIndex) {
|
|
||||||
setCurrentItem(itemIndex, C.TIME_UNSET, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the index of the currently played item.
|
|
||||||
*/
|
|
||||||
public int getCurrentItemIndex() {
|
|
||||||
return currentItemIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Appends {@code sample} to the media queue.
|
|
||||||
*
|
|
||||||
* @param sample The {@link Sample} to append.
|
|
||||||
*/
|
|
||||||
public void addItem(Sample sample) {
|
|
||||||
mediaQueue.add(sample);
|
|
||||||
concatenatingMediaSource.addMediaSource(buildMediaSource(sample));
|
|
||||||
if (currentPlayer == castPlayer) {
|
|
||||||
castPlayer.addItems(buildMediaQueueItem(sample));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the size of the media queue.
|
|
||||||
*/
|
|
||||||
public int getMediaQueueSize() {
|
|
||||||
return mediaQueue.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the item at the given index in the media queue.
|
|
||||||
*
|
|
||||||
* @param position The index of the item.
|
|
||||||
* @return The item at the given index in the media queue.
|
|
||||||
*/
|
|
||||||
public Sample getItem(int position) {
|
|
||||||
return mediaQueue.get(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the item at the given index from the media queue.
|
|
||||||
*
|
|
||||||
* @param itemIndex The index of the item to remove.
|
|
||||||
* @return Whether the removal was successful.
|
|
||||||
*/
|
|
||||||
public boolean removeItem(int itemIndex) {
|
|
||||||
concatenatingMediaSource.removeMediaSource(itemIndex);
|
|
||||||
if (currentPlayer == castPlayer) {
|
|
||||||
if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
|
||||||
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
|
||||||
if (castTimeline.getPeriodCount() <= itemIndex) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mediaQueue.remove(itemIndex);
|
|
||||||
if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) {
|
|
||||||
maybeSetCurrentItemAndNotify(C.INDEX_UNSET);
|
|
||||||
} else if (itemIndex < currentItemIndex) {
|
|
||||||
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Moves an item within the queue.
|
|
||||||
*
|
|
||||||
* @param fromIndex The index of the item to move.
|
|
||||||
* @param toIndex The target index of the item in the queue.
|
|
||||||
* @return Whether the item move was successful.
|
|
||||||
*/
|
|
||||||
public boolean moveItem(int fromIndex, int toIndex) {
|
|
||||||
// Player update.
|
|
||||||
concatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
|
|
||||||
if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
|
||||||
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
|
||||||
int periodCount = castTimeline.getPeriodCount();
|
|
||||||
if (periodCount <= fromIndex || periodCount <= toIndex) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id;
|
|
||||||
castPlayer.moveItem(elementId, toIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaQueue.add(toIndex, mediaQueue.remove(fromIndex));
|
|
||||||
|
|
||||||
// Index update.
|
|
||||||
if (fromIndex == currentItemIndex) {
|
|
||||||
maybeSetCurrentItemAndNotify(toIndex);
|
|
||||||
} else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) {
|
|
||||||
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
|
|
||||||
} else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) {
|
|
||||||
maybeSetCurrentItemAndNotify(currentItemIndex + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Miscellaneous methods.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatches a given {@link KeyEvent} to the corresponding view of the current player.
|
|
||||||
*
|
|
||||||
* @param event The {@link KeyEvent}.
|
|
||||||
* @return Whether the event was handled by the target view.
|
|
||||||
*/
|
|
||||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
|
||||||
if (currentPlayer == exoPlayer) {
|
|
||||||
return localPlayerView.dispatchKeyEvent(event);
|
|
||||||
} else /* currentPlayer == castPlayer */ {
|
|
||||||
return castControlView.dispatchKeyEvent(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Releases the manager and the players that it holds.
|
|
||||||
*/
|
|
||||||
public void release() {
|
|
||||||
currentItemIndex = C.INDEX_UNSET;
|
|
||||||
mediaQueue.clear();
|
|
||||||
concatenatingMediaSource.clear();
|
|
||||||
castPlayer.setSessionAvailabilityListener(null);
|
|
||||||
castPlayer.release();
|
|
||||||
localPlayerView.setPlayer(null);
|
|
||||||
exoPlayer.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Player.EventListener implementation.
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
|
||||||
updateCurrentItemIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
|
|
||||||
updateCurrentItemIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTimelineChanged(
|
|
||||||
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
|
|
||||||
updateCurrentItemIndex();
|
|
||||||
if (timeline.isEmpty()) {
|
|
||||||
castMediaQueueCreationPending = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CastPlayer.SessionAvailabilityListener implementation.
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCastSessionAvailable() {
|
|
||||||
setCurrentPlayer(castPlayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCastSessionUnavailable() {
|
|
||||||
setCurrentPlayer(exoPlayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal methods.
|
|
||||||
|
|
||||||
private void init() {
|
|
||||||
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateCurrentItemIndex() {
|
|
||||||
int playbackState = currentPlayer.getPlaybackState();
|
|
||||||
maybeSetCurrentItemAndNotify(
|
|
||||||
playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED
|
|
||||||
? currentPlayer.getCurrentWindowIndex() : C.INDEX_UNSET);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setCurrentPlayer(Player currentPlayer) {
|
|
||||||
if (this.currentPlayer == currentPlayer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// View management.
|
|
||||||
if (currentPlayer == exoPlayer) {
|
|
||||||
localPlayerView.setVisibility(View.VISIBLE);
|
|
||||||
castControlView.hide();
|
|
||||||
} else /* currentPlayer == castPlayer */ {
|
|
||||||
localPlayerView.setVisibility(View.GONE);
|
|
||||||
castControlView.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Player state management.
|
|
||||||
long playbackPositionMs = C.TIME_UNSET;
|
|
||||||
int windowIndex = C.INDEX_UNSET;
|
|
||||||
boolean playWhenReady = false;
|
|
||||||
if (this.currentPlayer != null) {
|
|
||||||
int playbackState = this.currentPlayer.getPlaybackState();
|
|
||||||
if (playbackState != Player.STATE_ENDED) {
|
|
||||||
playbackPositionMs = this.currentPlayer.getCurrentPosition();
|
|
||||||
playWhenReady = this.currentPlayer.getPlayWhenReady();
|
|
||||||
windowIndex = this.currentPlayer.getCurrentWindowIndex();
|
|
||||||
if (windowIndex != currentItemIndex) {
|
|
||||||
playbackPositionMs = C.TIME_UNSET;
|
|
||||||
windowIndex = currentItemIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.currentPlayer.stop(true);
|
|
||||||
} else {
|
|
||||||
// This is the initial setup. No need to save any state.
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentPlayer = currentPlayer;
|
|
||||||
|
|
||||||
// Media queue management.
|
|
||||||
castMediaQueueCreationPending = currentPlayer == castPlayer;
|
|
||||||
if (currentPlayer == exoPlayer) {
|
|
||||||
exoPlayer.prepare(concatenatingMediaSource);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Playback transition.
|
|
||||||
if (windowIndex != C.INDEX_UNSET) {
|
|
||||||
setCurrentItem(windowIndex, playbackPositionMs, playWhenReady);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts playback of the item at the given position.
|
|
||||||
*
|
|
||||||
* @param itemIndex The index of the item to play.
|
|
||||||
* @param positionMs The position at which playback should start.
|
|
||||||
* @param playWhenReady Whether the player should proceed when ready to do so.
|
|
||||||
*/
|
|
||||||
private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
|
|
||||||
maybeSetCurrentItemAndNotify(itemIndex);
|
|
||||||
if (castMediaQueueCreationPending) {
|
|
||||||
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
|
|
||||||
for (int i = 0; i < items.length; i++) {
|
|
||||||
items[i] = buildMediaQueueItem(mediaQueue.get(i));
|
|
||||||
}
|
|
||||||
castMediaQueueCreationPending = false;
|
|
||||||
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
|
|
||||||
} else {
|
|
||||||
currentPlayer.seekTo(itemIndex, positionMs);
|
|
||||||
currentPlayer.setPlayWhenReady(playWhenReady);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void maybeSetCurrentItemAndNotify(int currentItemIndex) {
|
|
||||||
if (this.currentItemIndex != currentItemIndex) {
|
|
||||||
int oldIndex = this.currentItemIndex;
|
|
||||||
this.currentItemIndex = currentItemIndex;
|
|
||||||
queuePositionListener.onQueuePositionChanged(oldIndex, currentItemIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MediaSource buildMediaSource(DemoUtil.Sample sample) {
|
|
||||||
Uri uri = Uri.parse(sample.uri);
|
|
||||||
switch (sample.mimeType) {
|
|
||||||
case DemoUtil.MIME_TYPE_SS:
|
|
||||||
return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
|
||||||
case DemoUtil.MIME_TYPE_DASH:
|
|
||||||
return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
|
||||||
case DemoUtil.MIME_TYPE_HLS:
|
|
||||||
return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
|
||||||
case DemoUtil.MIME_TYPE_VIDEO_MP4:
|
|
||||||
return new ExtractorMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
|
||||||
default: {
|
|
||||||
throw new IllegalStateException("Unsupported type: " + sample.mimeType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MediaQueueItem buildMediaQueueItem(DemoUtil.Sample sample) {
|
|
||||||
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
|
|
||||||
movieMetadata.putString(MediaMetadata.KEY_TITLE, sample.name);
|
|
||||||
MediaInfo mediaInfo = new MediaInfo.Builder(sample.uri)
|
|
||||||
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED).setContentType(sample.mimeType)
|
|
||||||
.setMetadata(movieMetadata).build();
|
|
||||||
return new MediaQueueItem.Builder(mediaInfo).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/** Releases any acquired resources. */
|
||||||
|
void release();
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,12 @@
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
<vector android:alpha="0.8" android:height="24dp" android:viewportHeight="24.0"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:viewportWidth="24.0" android:width="24dp"
|
android:height="24.0dp"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
android:viewportHeight="24.0"
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z"/>
|
android:viewportWidth="24.0"
|
||||||
|
android:width="24.0dp" >
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M18,13h-5v5c0,0.55 -0.45,1 -1,1h0c-0.55,0 -1,-0.45 -1,-1v-5H6c-0.55,0 -1,-0.45 -1,-1v0c0,-0.55 0.45,-1 1,-1h5V6c0,-0.55 0.45,-1 1,-1h0c0.55,0 1,0.45 1,1v5h5c0.55,0 1,0.45 1,1v0C19,12.55 18.55,13 18,13z"/>
|
||||||
</vector>
|
</vector>
|
@ -13,17 +13,10 @@
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/textView"
|
android:id="@+id/textView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_weight="1"
|
|
||||||
android:textSize="20sp"
|
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
|
android:textSize="20sp"
|
||||||
android:text="@string/cast_context_error"/>
|
android:text="@string/cast_context_error"/>
|
||||||
</LinearLayout>
|
|
@ -19,34 +19,42 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:keepScreenOn="true">
|
android:keepScreenOn="true">
|
||||||
|
|
||||||
<com.google.android.exoplayer2.ui.PlayerView android:id="@+id/local_player_view"
|
<com.google.android.exoplayer2.ui.PlayerView android:id="@+id/local_player_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="12"
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/black"
|
||||||
app:repeat_toggle_modes="all|one"/>
|
app:repeat_toggle_modes="all|one"/>
|
||||||
|
|
||||||
<RelativeLayout android:layout_width="match_parent"
|
<RelativeLayout android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="12">
|
android:layout_weight="1">
|
||||||
<android.support.v7.widget.RecyclerView android:id="@+id/sample_list"
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView android:id="@+id/sample_list"
|
||||||
android:choiceMode="singleChoice"
|
android:choiceMode="singleChoice"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:scrollbars="vertical"
|
android:scrollbars="vertical"
|
||||||
android:fadeScrollbars="false"/>
|
android:fadeScrollbars="false"/>
|
||||||
<ImageButton android:id="@+id/add_sample_button"
|
|
||||||
android:background="@drawable/ic_add_circle_white_24dp"
|
<com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/add_sample_button"
|
||||||
|
android:src="@drawable/ic_plus"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentEnd="true"
|
android:layout_alignParentEnd="true"
|
||||||
android:layout_alignParentRight="true"
|
android:layout_alignParentRight="true"
|
||||||
android:layout_alignParentBottom="true"
|
android:layout_alignParentBottom="true"
|
||||||
android:padding="30dp"/>
|
android:layout_margin="16dp"
|
||||||
|
android:contentDescription="@string/add_samples"/>
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<com.google.android.exoplayer2.ui.PlayerControlView android:id="@+id/cast_control_view"
|
<com.google.android.exoplayer2.ui.PlayerControlView android:id="@+id/cast_control_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="2"
|
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:repeat_toggle_modes="all|one"
|
app:repeat_toggle_modes="all|one"
|
||||||
app:show_timeout="-1"/>
|
app:show_timeout="-1"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<ListView android:id="@+id/sample_list"
|
<ListView android:id="@+id/sample_list"
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
<item
|
<item
|
||||||
android:id="@+id/media_route_menu_item"
|
android:id="@+id/media_route_menu_item"
|
||||||
android:title="@string/media_route_menu_title"
|
android:title="@string/media_route_menu_title"
|
||||||
app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"
|
app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
|
||||||
app:showAsAction="always" />
|
app:showAsAction="always" />
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
|
@ -20,8 +20,10 @@
|
|||||||
|
|
||||||
<string name="media_route_menu_title">Cast</string>
|
<string name="media_route_menu_title">Cast</string>
|
||||||
|
|
||||||
<string name="sample_list_dialog_title">Add samples</string>
|
<string name="add_samples">Add samples</string>
|
||||||
|
|
||||||
<string name="cast_context_error">Failed to get Cast context. Try updating Google Play Services and restart the app.</string>
|
<string name="cast_context_error">Failed to get Cast context. Try updating Google Play Services and restart the app.</string>
|
||||||
|
|
||||||
|
<string name="player_error_msg">Player error encountered. Select a queue item to reprepare. Check the logcat and receiver app\'s console for more info.</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -16,7 +16,6 @@ apply plugin: 'com.android.application'
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
@ -26,7 +25,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
versionName project.ext.releaseVersion
|
versionName project.ext.releaseVersion
|
||||||
versionCode project.ext.releaseVersionCode
|
versionCode project.ext.releaseVersionCode
|
||||||
minSdkVersion 16
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,8 +41,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
// The demo app does not have translations.
|
// The demo app isn't indexed and doesn't have translations.
|
||||||
disable 'MissingTranslation'
|
disable 'GoogleAppIndexingWarning','MissingTranslation'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +53,7 @@ dependencies {
|
|||||||
implementation project(modulePrefix + 'library-hls')
|
implementation project(modulePrefix + 'library-hls')
|
||||||
implementation project(modulePrefix + 'library-smoothstreaming')
|
implementation project(modulePrefix + 'library-smoothstreaming')
|
||||||
implementation project(modulePrefix + 'extension-ima')
|
implementation project(modulePrefix + 'extension-ima')
|
||||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
implementation 'androidx.annotation:annotation:1.0.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
||||||
|
@ -23,8 +23,8 @@ import com.google.android.exoplayer2.ExoPlayer;
|
|||||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
|
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||||
@ -114,7 +114,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
case C.TYPE_HLS:
|
case C.TYPE_HLS:
|
||||||
return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||||
case C.TYPE_OTHER:
|
case C.TYPE_OTHER:
|
||||||
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("Unsupported type: " + type);
|
throw new IllegalStateException("Unsupported type: " + type);
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@ apply plugin: 'com.android.application'
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
@ -26,7 +25,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
versionName project.ext.releaseVersion
|
versionName project.ext.releaseVersion
|
||||||
versionCode project.ext.releaseVersionCode
|
versionCode project.ext.releaseVersionCode
|
||||||
minSdkVersion 16
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,8 +44,9 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
// The demo app does not have translations.
|
// The demo app isn't indexed, doesn't have translations, and has a
|
||||||
disable 'MissingTranslation'
|
// banner for AndroidTV that's only in xhdpi density.
|
||||||
|
disable 'GoogleAppIndexingWarning','MissingTranslation','IconDensities'
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions "extensions"
|
flavorDimensions "extensions"
|
||||||
@ -62,7 +62,10 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
implementation 'androidx.annotation:annotation:1.0.2'
|
||||||
|
implementation 'androidx.legacy:legacy-support-core-ui:1.0.0'
|
||||||
|
implementation 'androidx.fragment:fragment:1.0.0'
|
||||||
|
implementation 'com.google.android.material:material:1.0.0'
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation project(modulePrefix + 'library-dash')
|
implementation project(modulePrefix + 'library-dash')
|
||||||
implementation project(modulePrefix + 'library-hls')
|
implementation project(modulePrefix + 'library-hls')
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.google.android.exoplayer2.demo">
|
package="com.google.android.exoplayer2.demo">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
@ -33,11 +34,13 @@
|
|||||||
android:banner="@drawable/ic_banner"
|
android:banner="@drawable/ic_banner"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:name="com.google.android.exoplayer2.demo.DemoApplication">
|
android:name="com.google.android.exoplayer2.demo.DemoApplication"
|
||||||
|
tools:ignore="UnusedAttribute">
|
||||||
|
|
||||||
<activity android:name="com.google.android.exoplayer2.demo.SampleChooserActivity"
|
<activity android:name="com.google.android.exoplayer2.demo.SampleChooserActivity"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:label="@string/application_name">
|
android:label="@string/application_name"
|
||||||
|
android:theme="@style/Theme.AppCompat">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
@ -330,11 +330,11 @@
|
|||||||
"samples": [
|
"samples": [
|
||||||
{
|
{
|
||||||
"name": "Super speed",
|
"name": "Super speed",
|
||||||
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism"
|
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Super speed (PlayReady)",
|
"name": "Super speed (PlayReady)",
|
||||||
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism",
|
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest",
|
||||||
"drm_scheme": "playready"
|
"drm_scheme": "playready"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -16,6 +16,13 @@
|
|||||||
package com.google.android.exoplayer2.demo;
|
package com.google.android.exoplayer2.demo;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||||
|
import com.google.android.exoplayer2.RenderersFactory;
|
||||||
|
import com.google.android.exoplayer2.database.DatabaseProvider;
|
||||||
|
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
|
||||||
|
import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil;
|
||||||
|
import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
|
||||||
|
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||||
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
@ -28,21 +35,24 @@ import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
|
|||||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
|
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
|
||||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||||
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Placeholder application to facilitate overriding Application methods for debugging and testing.
|
* Placeholder application to facilitate overriding Application methods for debugging and testing.
|
||||||
*/
|
*/
|
||||||
public class DemoApplication extends Application {
|
public class DemoApplication extends Application {
|
||||||
|
|
||||||
|
private static final String TAG = "DemoApplication";
|
||||||
private static final String DOWNLOAD_ACTION_FILE = "actions";
|
private static final String DOWNLOAD_ACTION_FILE = "actions";
|
||||||
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
|
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
|
||||||
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
|
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
|
||||||
private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2;
|
|
||||||
|
|
||||||
protected String userAgent;
|
protected String userAgent;
|
||||||
|
|
||||||
|
private DatabaseProvider databaseProvider;
|
||||||
private File downloadDirectory;
|
private File downloadDirectory;
|
||||||
private Cache downloadCache;
|
private Cache downloadCache;
|
||||||
private DownloadManager downloadManager;
|
private DownloadManager downloadManager;
|
||||||
@ -71,6 +81,18 @@ public class DemoApplication extends Application {
|
|||||||
return "withExtensions".equals(BuildConfig.FLAVOR);
|
return "withExtensions".equals(BuildConfig.FLAVOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RenderersFactory buildRenderersFactory(boolean preferExtensionRenderer) {
|
||||||
|
@DefaultRenderersFactory.ExtensionRendererMode
|
||||||
|
int extensionRendererMode =
|
||||||
|
useExtensionRenderers()
|
||||||
|
? (preferExtensionRenderer
|
||||||
|
? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
||||||
|
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
|
||||||
|
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
|
||||||
|
return new DefaultRenderersFactory(/* context= */ this)
|
||||||
|
.setExtensionRendererMode(extensionRendererMode);
|
||||||
|
}
|
||||||
|
|
||||||
public DownloadManager getDownloadManager() {
|
public DownloadManager getDownloadManager() {
|
||||||
initDownloadManager();
|
initDownloadManager();
|
||||||
return downloadManager;
|
return downloadManager;
|
||||||
@ -81,31 +103,51 @@ public class DemoApplication extends Application {
|
|||||||
return downloadTracker;
|
return downloadTracker;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected synchronized Cache getDownloadCache() {
|
||||||
|
if (downloadCache == null) {
|
||||||
|
File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY);
|
||||||
|
downloadCache =
|
||||||
|
new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider());
|
||||||
|
}
|
||||||
|
return downloadCache;
|
||||||
|
}
|
||||||
|
|
||||||
private synchronized void initDownloadManager() {
|
private synchronized void initDownloadManager() {
|
||||||
if (downloadManager == null) {
|
if (downloadManager == null) {
|
||||||
|
DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider());
|
||||||
|
upgradeActionFile(
|
||||||
|
DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false);
|
||||||
|
upgradeActionFile(
|
||||||
|
DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true);
|
||||||
DownloaderConstructorHelper downloaderConstructorHelper =
|
DownloaderConstructorHelper downloaderConstructorHelper =
|
||||||
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
|
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
|
||||||
downloadManager =
|
downloadManager =
|
||||||
new DownloadManager(
|
new DownloadManager(
|
||||||
downloaderConstructorHelper,
|
this, downloadIndex, new DefaultDownloaderFactory(downloaderConstructorHelper));
|
||||||
MAX_SIMULTANEOUS_DOWNLOADS,
|
|
||||||
DownloadManager.DEFAULT_MIN_RETRY_COUNT,
|
|
||||||
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE));
|
|
||||||
downloadTracker =
|
downloadTracker =
|
||||||
new DownloadTracker(
|
new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadManager);
|
||||||
/* context= */ this,
|
|
||||||
buildDataSourceFactory(),
|
|
||||||
new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE));
|
|
||||||
downloadManager.addListener(downloadTracker);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized Cache getDownloadCache() {
|
private void upgradeActionFile(
|
||||||
if (downloadCache == null) {
|
String fileName, DefaultDownloadIndex downloadIndex, boolean addNewDownloadsAsCompleted) {
|
||||||
File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY);
|
try {
|
||||||
downloadCache = new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor());
|
ActionFileUpgradeUtil.upgradeAndDelete(
|
||||||
|
new File(getDownloadDirectory(), fileName),
|
||||||
|
/* downloadIdProvider= */ null,
|
||||||
|
downloadIndex,
|
||||||
|
/* deleteOnFailure= */ true,
|
||||||
|
addNewDownloadsAsCompleted);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "Failed to upgrade action file: " + fileName, e);
|
||||||
}
|
}
|
||||||
return downloadCache;
|
}
|
||||||
|
|
||||||
|
private DatabaseProvider getDatabaseProvider() {
|
||||||
|
if (databaseProvider == null) {
|
||||||
|
databaseProvider = new ExoDatabaseProvider(this);
|
||||||
|
}
|
||||||
|
return databaseProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
private File getDownloadDirectory() {
|
private File getDownloadDirectory() {
|
||||||
@ -118,8 +160,8 @@ public class DemoApplication extends Application {
|
|||||||
return downloadDirectory;
|
return downloadDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CacheDataSourceFactory buildReadOnlyCacheDataSource(
|
protected static CacheDataSourceFactory buildReadOnlyCacheDataSource(
|
||||||
DefaultDataSourceFactory upstreamFactory, Cache cache) {
|
DataSource.Factory upstreamFactory, Cache cache) {
|
||||||
return new CacheDataSourceFactory(
|
return new CacheDataSourceFactory(
|
||||||
cache,
|
cache,
|
||||||
upstreamFactory,
|
upstreamFactory,
|
||||||
|
@ -16,13 +16,14 @@
|
|||||||
package com.google.android.exoplayer2.demo;
|
package com.google.android.exoplayer2.demo;
|
||||||
|
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
|
import com.google.android.exoplayer2.offline.Download;
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager.TaskState;
|
|
||||||
import com.google.android.exoplayer2.offline.DownloadService;
|
import com.google.android.exoplayer2.offline.DownloadService;
|
||||||
import com.google.android.exoplayer2.scheduler.PlatformScheduler;
|
import com.google.android.exoplayer2.scheduler.PlatformScheduler;
|
||||||
import com.google.android.exoplayer2.ui.DownloadNotificationUtil;
|
import com.google.android.exoplayer2.ui.DownloadNotificationHelper;
|
||||||
import com.google.android.exoplayer2.util.NotificationUtil;
|
import com.google.android.exoplayer2.util.NotificationUtil;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/** A service for downloading media. */
|
/** A service for downloading media. */
|
||||||
public class DemoDownloadService extends DownloadService {
|
public class DemoDownloadService extends DownloadService {
|
||||||
@ -31,12 +32,23 @@ public class DemoDownloadService extends DownloadService {
|
|||||||
private static final int JOB_ID = 1;
|
private static final int JOB_ID = 1;
|
||||||
private static final int FOREGROUND_NOTIFICATION_ID = 1;
|
private static final int FOREGROUND_NOTIFICATION_ID = 1;
|
||||||
|
|
||||||
|
private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
|
||||||
|
|
||||||
|
private DownloadNotificationHelper notificationHelper;
|
||||||
|
|
||||||
public DemoDownloadService() {
|
public DemoDownloadService() {
|
||||||
super(
|
super(
|
||||||
FOREGROUND_NOTIFICATION_ID,
|
FOREGROUND_NOTIFICATION_ID,
|
||||||
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
|
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
|
||||||
CHANNEL_ID,
|
CHANNEL_ID,
|
||||||
R.string.exo_download_notification_channel_name);
|
R.string.exo_download_notification_channel_name);
|
||||||
|
nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
notificationHelper = new DownloadNotificationHelper(this, CHANNEL_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -50,40 +62,29 @@ public class DemoDownloadService extends DownloadService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Notification getForegroundNotification(TaskState[] taskStates) {
|
protected Notification getForegroundNotification(List<Download> downloads) {
|
||||||
return DownloadNotificationUtil.buildProgressNotification(
|
return notificationHelper.buildProgressNotification(
|
||||||
/* context= */ this,
|
R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads);
|
||||||
R.drawable.exo_controls_play,
|
|
||||||
CHANNEL_ID,
|
|
||||||
/* contentIntent= */ null,
|
|
||||||
/* message= */ null,
|
|
||||||
taskStates);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onTaskStateChanged(TaskState taskState) {
|
protected void onDownloadChanged(Download download) {
|
||||||
if (taskState.action.isRemoveAction) {
|
Notification notification;
|
||||||
|
if (download.state == Download.STATE_COMPLETED) {
|
||||||
|
notification =
|
||||||
|
notificationHelper.buildDownloadCompletedNotification(
|
||||||
|
R.drawable.ic_download_done,
|
||||||
|
/* contentIntent= */ null,
|
||||||
|
Util.fromUtf8Bytes(download.request.data));
|
||||||
|
} else if (download.state == Download.STATE_FAILED) {
|
||||||
|
notification =
|
||||||
|
notificationHelper.buildDownloadFailedNotification(
|
||||||
|
R.drawable.ic_download_done,
|
||||||
|
/* contentIntent= */ null,
|
||||||
|
Util.fromUtf8Bytes(download.request.data));
|
||||||
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Notification notification = null;
|
NotificationUtil.setNotification(this, nextNotificationId++, notification);
|
||||||
if (taskState.state == TaskState.STATE_COMPLETED) {
|
|
||||||
notification =
|
|
||||||
DownloadNotificationUtil.buildDownloadCompletedNotification(
|
|
||||||
/* context= */ this,
|
|
||||||
R.drawable.exo_controls_play,
|
|
||||||
CHANNEL_ID,
|
|
||||||
/* contentIntent= */ null,
|
|
||||||
Util.fromUtf8Bytes(taskState.action.data));
|
|
||||||
} else if (taskState.state == TaskState.STATE_FAILED) {
|
|
||||||
notification =
|
|
||||||
DownloadNotificationUtil.buildDownloadFailedNotification(
|
|
||||||
/* context= */ this,
|
|
||||||
R.drawable.exo_controls_play,
|
|
||||||
CHANNEL_ID,
|
|
||||||
/* contentIntent= */ null,
|
|
||||||
Util.fromUtf8Bytes(taskState.action.data));
|
|
||||||
}
|
|
||||||
int notificationId = FOREGROUND_NOTIFICATION_ID + 1 + taskState.taskId;
|
|
||||||
NotificationUtil.setNotification(this, notificationId, notification);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,54 +15,34 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.demo;
|
package com.google.android.exoplayer2.demo;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Handler;
|
import androidx.annotation.Nullable;
|
||||||
import android.os.HandlerThread;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.offline.ActionFile;
|
import com.google.android.exoplayer2.RenderersFactory;
|
||||||
import com.google.android.exoplayer2.offline.DownloadAction;
|
import com.google.android.exoplayer2.offline.Download;
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadCursor;
|
||||||
import com.google.android.exoplayer2.offline.DownloadHelper;
|
import com.google.android.exoplayer2.offline.DownloadHelper;
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadIndex;
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager.TaskState;
|
import com.google.android.exoplayer2.offline.DownloadRequest;
|
||||||
import com.google.android.exoplayer2.offline.DownloadService;
|
import com.google.android.exoplayer2.offline.DownloadService;
|
||||||
import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper;
|
|
||||||
import com.google.android.exoplayer2.offline.StreamKey;
|
import com.google.android.exoplayer2.offline.StreamKey;
|
||||||
import com.google.android.exoplayer2.offline.TrackKey;
|
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||||
import com.google.android.exoplayer2.source.TrackGroup;
|
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
|
||||||
import com.google.android.exoplayer2.source.dash.offline.DashDownloadHelper;
|
|
||||||
import com.google.android.exoplayer2.source.hls.offline.HlsDownloadHelper;
|
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadHelper;
|
|
||||||
import com.google.android.exoplayer2.ui.DefaultTrackNameProvider;
|
|
||||||
import com.google.android.exoplayer2.ui.TrackNameProvider;
|
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.util.Log;
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CopyOnWriteArraySet;
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
|
|
||||||
/**
|
/** Tracks media that has been downloaded. */
|
||||||
* Tracks media that has been downloaded.
|
public class DownloadTracker {
|
||||||
*
|
|
||||||
* <p>Tracked downloads are persisted using an {@link ActionFile}, however in a real application
|
|
||||||
* it's expected that state will be stored directly in the application's media database, so that it
|
|
||||||
* can be queried efficiently together with other information about the media.
|
|
||||||
*/
|
|
||||||
public class DownloadTracker implements DownloadManager.Listener {
|
|
||||||
|
|
||||||
/** Listens for changes in the tracked downloads. */
|
/** Listens for changes in the tracked downloads. */
|
||||||
public interface Listener {
|
public interface Listener {
|
||||||
@ -75,28 +55,21 @@ public class DownloadTracker implements DownloadManager.Listener {
|
|||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final DataSource.Factory dataSourceFactory;
|
private final DataSource.Factory dataSourceFactory;
|
||||||
private final TrackNameProvider trackNameProvider;
|
|
||||||
private final CopyOnWriteArraySet<Listener> listeners;
|
private final CopyOnWriteArraySet<Listener> listeners;
|
||||||
private final HashMap<Uri, DownloadAction> trackedDownloadStates;
|
private final HashMap<Uri, Download> downloads;
|
||||||
private final ActionFile actionFile;
|
private final DownloadIndex downloadIndex;
|
||||||
private final Handler actionFileWriteHandler;
|
|
||||||
|
@Nullable private StartDownloadDialogHelper startDownloadDialogHelper;
|
||||||
|
|
||||||
public DownloadTracker(
|
public DownloadTracker(
|
||||||
Context context,
|
Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) {
|
||||||
DataSource.Factory dataSourceFactory,
|
|
||||||
File actionFile,
|
|
||||||
DownloadAction.Deserializer... deserializers) {
|
|
||||||
this.context = context.getApplicationContext();
|
this.context = context.getApplicationContext();
|
||||||
this.dataSourceFactory = dataSourceFactory;
|
this.dataSourceFactory = dataSourceFactory;
|
||||||
this.actionFile = new ActionFile(actionFile);
|
|
||||||
trackNameProvider = new DefaultTrackNameProvider(context.getResources());
|
|
||||||
listeners = new CopyOnWriteArraySet<>();
|
listeners = new CopyOnWriteArraySet<>();
|
||||||
trackedDownloadStates = new HashMap<>();
|
downloads = new HashMap<>();
|
||||||
HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker");
|
downloadIndex = downloadManager.getDownloadIndex();
|
||||||
actionFileWriteThread.start();
|
downloadManager.addListener(new DownloadManagerListener());
|
||||||
actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper());
|
loadDownloads();
|
||||||
loadTrackedActions(
|
|
||||||
deserializers.length > 0 ? deserializers : DownloadAction.getDefaultDeserializers());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addListener(Listener listener) {
|
public void addListener(Listener listener) {
|
||||||
@ -108,167 +81,139 @@ public class DownloadTracker implements DownloadManager.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isDownloaded(Uri uri) {
|
public boolean isDownloaded(Uri uri) {
|
||||||
return trackedDownloadStates.containsKey(uri);
|
Download download = downloads.get(uri);
|
||||||
|
return download != null && download.state != Download.STATE_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public List<StreamKey> getOfflineStreamKeys(Uri uri) {
|
public List<StreamKey> getOfflineStreamKeys(Uri uri) {
|
||||||
if (!trackedDownloadStates.containsKey(uri)) {
|
Download download = downloads.get(uri);
|
||||||
return Collections.emptyList();
|
return download != null && download.state != Download.STATE_FAILED
|
||||||
}
|
? download.request.streamKeys
|
||||||
return trackedDownloadStates.get(uri).getKeys();
|
: Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void toggleDownload(Activity activity, String name, Uri uri, String extension) {
|
public void toggleDownload(
|
||||||
if (isDownloaded(uri)) {
|
FragmentManager fragmentManager,
|
||||||
DownloadAction removeAction =
|
String name,
|
||||||
getDownloadHelper(uri, extension).getRemoveAction(Util.getUtf8Bytes(name));
|
Uri uri,
|
||||||
startServiceWithAction(removeAction);
|
String extension,
|
||||||
|
RenderersFactory renderersFactory) {
|
||||||
|
Download download = downloads.get(uri);
|
||||||
|
if (download != null) {
|
||||||
|
DownloadService.sendRemoveDownload(
|
||||||
|
context, DemoDownloadService.class, download.request.id, /* foreground= */ false);
|
||||||
} else {
|
} else {
|
||||||
StartDownloadDialogHelper helper =
|
if (startDownloadDialogHelper != null) {
|
||||||
new StartDownloadDialogHelper(activity, getDownloadHelper(uri, extension), name);
|
startDownloadDialogHelper.release();
|
||||||
helper.prepare();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DownloadManager.Listener
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onInitialized(DownloadManager downloadManager) {
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) {
|
|
||||||
DownloadAction action = taskState.action;
|
|
||||||
Uri uri = action.uri;
|
|
||||||
if ((action.isRemoveAction && taskState.state == TaskState.STATE_COMPLETED)
|
|
||||||
|| (!action.isRemoveAction && taskState.state == TaskState.STATE_FAILED)) {
|
|
||||||
// A download has been removed, or has failed. Stop tracking it.
|
|
||||||
if (trackedDownloadStates.remove(uri) != null) {
|
|
||||||
handleTrackedDownloadStatesChanged();
|
|
||||||
}
|
}
|
||||||
|
startDownloadDialogHelper =
|
||||||
|
new StartDownloadDialogHelper(
|
||||||
|
fragmentManager, getDownloadHelper(uri, extension, renderersFactory), name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void loadDownloads() {
|
||||||
public void onIdle(DownloadManager downloadManager) {
|
try (DownloadCursor loadedDownloads = downloadIndex.getDownloads()) {
|
||||||
// Do nothing.
|
while (loadedDownloads.moveToNext()) {
|
||||||
}
|
Download download = loadedDownloads.getDownload();
|
||||||
|
downloads.put(download.request.uri, download);
|
||||||
// Internal methods
|
|
||||||
|
|
||||||
private void loadTrackedActions(DownloadAction.Deserializer[] deserializers) {
|
|
||||||
try {
|
|
||||||
DownloadAction[] allActions = actionFile.load(deserializers);
|
|
||||||
for (DownloadAction action : allActions) {
|
|
||||||
trackedDownloadStates.put(action.uri, action);
|
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(TAG, "Failed to load tracked actions", e);
|
Log.w(TAG, "Failed to query downloads", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleTrackedDownloadStatesChanged() {
|
private DownloadHelper getDownloadHelper(
|
||||||
for (Listener listener : listeners) {
|
Uri uri, String extension, RenderersFactory renderersFactory) {
|
||||||
listener.onDownloadsChanged();
|
|
||||||
}
|
|
||||||
final DownloadAction[] actions = trackedDownloadStates.values().toArray(new DownloadAction[0]);
|
|
||||||
actionFileWriteHandler.post(
|
|
||||||
() -> {
|
|
||||||
try {
|
|
||||||
actionFile.store(actions);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(TAG, "Failed to store tracked actions", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startDownload(DownloadAction action) {
|
|
||||||
if (trackedDownloadStates.containsKey(action.uri)) {
|
|
||||||
// This content is already being downloaded. Do nothing.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
trackedDownloadStates.put(action.uri, action);
|
|
||||||
handleTrackedDownloadStatesChanged();
|
|
||||||
startServiceWithAction(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startServiceWithAction(DownloadAction action) {
|
|
||||||
DownloadService.startWithAction(context, DemoDownloadService.class, action, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DownloadHelper getDownloadHelper(Uri uri, String extension) {
|
|
||||||
int type = Util.inferContentType(uri, extension);
|
int type = Util.inferContentType(uri, extension);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case C.TYPE_DASH:
|
case C.TYPE_DASH:
|
||||||
return new DashDownloadHelper(uri, dataSourceFactory);
|
return DownloadHelper.forDash(uri, dataSourceFactory, renderersFactory);
|
||||||
case C.TYPE_SS:
|
case C.TYPE_SS:
|
||||||
return new SsDownloadHelper(uri, dataSourceFactory);
|
return DownloadHelper.forSmoothStreaming(uri, dataSourceFactory, renderersFactory);
|
||||||
case C.TYPE_HLS:
|
case C.TYPE_HLS:
|
||||||
return new HlsDownloadHelper(uri, dataSourceFactory);
|
return DownloadHelper.forHls(uri, dataSourceFactory, renderersFactory);
|
||||||
case C.TYPE_OTHER:
|
case C.TYPE_OTHER:
|
||||||
return new ProgressiveDownloadHelper(uri);
|
return DownloadHelper.forProgressive(uri);
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("Unsupported type: " + type);
|
throw new IllegalStateException("Unsupported type: " + type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class StartDownloadDialogHelper
|
private class DownloadManagerListener implements DownloadManager.Listener {
|
||||||
implements DownloadHelper.Callback, DialogInterface.OnClickListener {
|
|
||||||
|
|
||||||
private final DownloadHelper downloadHelper;
|
@Override
|
||||||
private final String name;
|
public void onDownloadChanged(DownloadManager downloadManager, Download download) {
|
||||||
|
downloads.put(download.request.uri, download);
|
||||||
private final AlertDialog.Builder builder;
|
for (Listener listener : listeners) {
|
||||||
private final View dialogView;
|
listener.onDownloadsChanged();
|
||||||
private final List<TrackKey> trackKeys;
|
}
|
||||||
private final ArrayAdapter<String> trackTitles;
|
|
||||||
private final ListView representationList;
|
|
||||||
|
|
||||||
public StartDownloadDialogHelper(
|
|
||||||
Activity activity, DownloadHelper downloadHelper, String name) {
|
|
||||||
this.downloadHelper = downloadHelper;
|
|
||||||
this.name = name;
|
|
||||||
builder =
|
|
||||||
new AlertDialog.Builder(activity)
|
|
||||||
.setTitle(R.string.exo_download_description)
|
|
||||||
.setPositiveButton(android.R.string.ok, this)
|
|
||||||
.setNegativeButton(android.R.string.cancel, null);
|
|
||||||
|
|
||||||
// Inflate with the builder's context to ensure the correct style is used.
|
|
||||||
LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
|
|
||||||
dialogView = dialogInflater.inflate(R.layout.start_download_dialog, null);
|
|
||||||
|
|
||||||
trackKeys = new ArrayList<>();
|
|
||||||
trackTitles =
|
|
||||||
new ArrayAdapter<>(
|
|
||||||
builder.getContext(), android.R.layout.simple_list_item_multiple_choice);
|
|
||||||
representationList = dialogView.findViewById(R.id.representation_list);
|
|
||||||
representationList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
|
|
||||||
representationList.setAdapter(trackTitles);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void prepare() {
|
|
||||||
downloadHelper.prepare(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDownloadRemoved(DownloadManager downloadManager, Download download) {
|
||||||
|
downloads.remove(download.request.uri);
|
||||||
|
for (Listener listener : listeners) {
|
||||||
|
listener.onDownloadsChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class StartDownloadDialogHelper
|
||||||
|
implements DownloadHelper.Callback,
|
||||||
|
DialogInterface.OnClickListener,
|
||||||
|
DialogInterface.OnDismissListener {
|
||||||
|
|
||||||
|
private final FragmentManager fragmentManager;
|
||||||
|
private final DownloadHelper downloadHelper;
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private TrackSelectionDialog trackSelectionDialog;
|
||||||
|
private MappedTrackInfo mappedTrackInfo;
|
||||||
|
|
||||||
|
public StartDownloadDialogHelper(
|
||||||
|
FragmentManager fragmentManager, DownloadHelper downloadHelper, String name) {
|
||||||
|
this.fragmentManager = fragmentManager;
|
||||||
|
this.downloadHelper = downloadHelper;
|
||||||
|
this.name = name;
|
||||||
|
downloadHelper.prepare(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void release() {
|
||||||
|
downloadHelper.release();
|
||||||
|
if (trackSelectionDialog != null) {
|
||||||
|
trackSelectionDialog.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadHelper.Callback implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPrepared(DownloadHelper helper) {
|
public void onPrepared(DownloadHelper helper) {
|
||||||
for (int i = 0; i < downloadHelper.getPeriodCount(); i++) {
|
if (helper.getPeriodCount() == 0) {
|
||||||
TrackGroupArray trackGroups = downloadHelper.getTrackGroups(i);
|
Log.d(TAG, "No periods found. Downloading entire stream.");
|
||||||
for (int j = 0; j < trackGroups.length; j++) {
|
startDownload();
|
||||||
TrackGroup trackGroup = trackGroups.get(j);
|
downloadHelper.release();
|
||||||
for (int k = 0; k < trackGroup.length; k++) {
|
return;
|
||||||
trackKeys.add(new TrackKey(i, j, k));
|
|
||||||
trackTitles.add(trackNameProvider.getTrackName(trackGroup.getFormat(k)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!trackKeys.isEmpty()) {
|
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
|
||||||
builder.setView(dialogView);
|
if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) {
|
||||||
|
Log.d(TAG, "No dialog content. Downloading entire stream.");
|
||||||
|
startDownload();
|
||||||
|
downloadHelper.release();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
builder.create().show();
|
trackSelectionDialog =
|
||||||
|
TrackSelectionDialog.createForMappedTrackInfoAndParameters(
|
||||||
|
/* titleId= */ R.string.exo_download_description,
|
||||||
|
mappedTrackInfo,
|
||||||
|
/* initialParameters= */ DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
|
||||||
|
/* allowAdaptiveSelections =*/ false,
|
||||||
|
/* allowMultipleOverrides= */ true,
|
||||||
|
/* onClickListener= */ this,
|
||||||
|
/* onDismissListener= */ this);
|
||||||
|
trackSelectionDialog.show(fragmentManager, /* tag= */ null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -279,20 +224,51 @@ public class DownloadTracker implements DownloadManager.Listener {
|
|||||||
Log.e(TAG, "Failed to start download", e);
|
Log.e(TAG, "Failed to start download", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialogInterface.OnClickListener implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
ArrayList<TrackKey> selectedTrackKeys = new ArrayList<>();
|
for (int periodIndex = 0; periodIndex < downloadHelper.getPeriodCount(); periodIndex++) {
|
||||||
for (int i = 0; i < representationList.getChildCount(); i++) {
|
downloadHelper.clearTrackSelections(periodIndex);
|
||||||
if (representationList.isItemChecked(i)) {
|
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
|
||||||
selectedTrackKeys.add(trackKeys.get(i));
|
if (!trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i)) {
|
||||||
|
downloadHelper.addTrackSelectionForSingleRenderer(
|
||||||
|
periodIndex,
|
||||||
|
/* rendererIndex= */ i,
|
||||||
|
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
|
||||||
|
trackSelectionDialog.getOverrides(/* rendererIndex= */ i));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!selectedTrackKeys.isEmpty() || trackKeys.isEmpty()) {
|
DownloadRequest downloadRequest = buildDownloadRequest();
|
||||||
// We have selected keys, or we're dealing with single stream content.
|
if (downloadRequest.streamKeys.isEmpty()) {
|
||||||
DownloadAction downloadAction =
|
// All tracks were deselected in the dialog. Don't start the download.
|
||||||
downloadHelper.getDownloadAction(Util.getUtf8Bytes(name), selectedTrackKeys);
|
return;
|
||||||
startDownload(downloadAction);
|
|
||||||
}
|
}
|
||||||
|
startDownload(downloadRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialogInterface.OnDismissListener implementation.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDismiss(DialogInterface dialogInterface) {
|
||||||
|
trackSelectionDialog = null;
|
||||||
|
downloadHelper.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal methods.
|
||||||
|
|
||||||
|
private void startDownload() {
|
||||||
|
startDownload(buildDownloadRequest());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startDownload(DownloadRequest downloadRequest) {
|
||||||
|
DownloadService.sendAddDownload(
|
||||||
|
context, DemoDownloadService.class, downloadRequest, /* foreground= */ false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DownloadRequest buildDownloadRequest() {
|
||||||
|
return downloadHelper.getDownloadRequest(Util.getUtf8Bytes(name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,14 +15,13 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.demo;
|
package com.google.android.exoplayer2.demo;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@ -33,11 +32,11 @@ import android.widget.TextView;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.C.ContentType;
|
import com.google.android.exoplayer2.C.ContentType;
|
||||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||||
import com.google.android.exoplayer2.PlaybackPreparer;
|
import com.google.android.exoplayer2.PlaybackPreparer;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import com.google.android.exoplayer2.RenderersFactory;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||||
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
||||||
@ -46,21 +45,17 @@ import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
|||||||
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
|
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
|
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
||||||
import com.google.android.exoplayer2.offline.FilteringManifestParser;
|
|
||||||
import com.google.android.exoplayer2.offline.StreamKey;
|
import com.google.android.exoplayer2.offline.StreamKey;
|
||||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
|
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistParserFactory;
|
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
|
|
||||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||||
@ -70,7 +65,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
|||||||
import com.google.android.exoplayer2.ui.DebugTextViewHelper;
|
import com.google.android.exoplayer2.ui.DebugTextViewHelper;
|
||||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||||
import com.google.android.exoplayer2.ui.PlayerView;
|
import com.google.android.exoplayer2.ui.PlayerView;
|
||||||
import com.google.android.exoplayer2.ui.TrackSelectionView;
|
|
||||||
import com.google.android.exoplayer2.ui.spherical.SphericalSurfaceView;
|
import com.google.android.exoplayer2.ui.spherical.SphericalSurfaceView;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
@ -85,7 +79,7 @@ import java.util.List;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/** An activity that plays media using {@link SimpleExoPlayer}. */
|
/** An activity that plays media using {@link SimpleExoPlayer}. */
|
||||||
public class PlayerActivity extends Activity
|
public class PlayerActivity extends AppCompatActivity
|
||||||
implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener {
|
implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener {
|
||||||
|
|
||||||
public static final String DRM_SCHEME_EXTRA = "drm_scheme";
|
public static final String DRM_SCHEME_EXTRA = "drm_scheme";
|
||||||
@ -130,7 +124,9 @@ public class PlayerActivity extends Activity
|
|||||||
|
|
||||||
private PlayerView playerView;
|
private PlayerView playerView;
|
||||||
private LinearLayout debugRootView;
|
private LinearLayout debugRootView;
|
||||||
|
private Button selectTracksButton;
|
||||||
private TextView debugTextView;
|
private TextView debugTextView;
|
||||||
|
private boolean isShowingTrackSelectionDialog;
|
||||||
|
|
||||||
private DataSource.Factory dataSourceFactory;
|
private DataSource.Factory dataSourceFactory;
|
||||||
private SimpleExoPlayer player;
|
private SimpleExoPlayer player;
|
||||||
@ -165,10 +161,10 @@ public class PlayerActivity extends Activity
|
|||||||
}
|
}
|
||||||
|
|
||||||
setContentView(R.layout.player_activity);
|
setContentView(R.layout.player_activity);
|
||||||
View rootView = findViewById(R.id.root);
|
|
||||||
rootView.setOnClickListener(this);
|
|
||||||
debugRootView = findViewById(R.id.controls_root);
|
debugRootView = findViewById(R.id.controls_root);
|
||||||
debugTextView = findViewById(R.id.debug_text_view);
|
debugTextView = findViewById(R.id.debug_text_view);
|
||||||
|
selectTracksButton = findViewById(R.id.select_tracks_button);
|
||||||
|
selectTracksButton.setOnClickListener(this);
|
||||||
|
|
||||||
playerView = findViewById(R.id.player_view);
|
playerView = findViewById(R.id.player_view);
|
||||||
playerView.setControllerVisibilityListener(this);
|
playerView.setControllerVisibilityListener(this);
|
||||||
@ -203,6 +199,7 @@ public class PlayerActivity extends Activity
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNewIntent(Intent intent) {
|
public void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
releasePlayer();
|
releasePlayer();
|
||||||
releaseAdsLoader();
|
releaseAdsLoader();
|
||||||
clearStartPosition();
|
clearStartPosition();
|
||||||
@ -277,6 +274,7 @@ public class PlayerActivity extends Activity
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
public void onSaveInstanceState(Bundle outState) {
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
updateTrackSelectorParameters();
|
updateTrackSelectorParameters();
|
||||||
updateStartPosition();
|
updateStartPosition();
|
||||||
outState.putParcelable(KEY_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters);
|
outState.putParcelable(KEY_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters);
|
||||||
@ -297,23 +295,15 @@ public class PlayerActivity extends Activity
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
if (view.getParent() == debugRootView) {
|
if (view == selectTracksButton
|
||||||
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
&& !isShowingTrackSelectionDialog
|
||||||
if (mappedTrackInfo != null) {
|
&& TrackSelectionDialog.willHaveContent(trackSelector)) {
|
||||||
CharSequence title = ((Button) view).getText();
|
isShowingTrackSelectionDialog = true;
|
||||||
int rendererIndex = (int) view.getTag();
|
TrackSelectionDialog trackSelectionDialog =
|
||||||
int rendererType = mappedTrackInfo.getRendererType(rendererIndex);
|
TrackSelectionDialog.createForTrackSelector(
|
||||||
boolean allowAdaptiveSelections =
|
trackSelector,
|
||||||
rendererType == C.TRACK_TYPE_VIDEO
|
/* onDismissListener= */ dismissedDialog -> isShowingTrackSelectionDialog = false);
|
||||||
|| (rendererType == C.TRACK_TYPE_AUDIO
|
trackSelectionDialog.show(getSupportFragmentManager(), /* tag= */ null);
|
||||||
&& mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
|
|
||||||
== MappedTrackInfo.RENDERER_SUPPORT_NO_TRACKS);
|
|
||||||
Pair<AlertDialog, TrackSelectionView> dialogPair =
|
|
||||||
TrackSelectionView.getDialog(this, title, trackSelector, rendererIndex);
|
|
||||||
dialogPair.second.setShowDisableOption(true);
|
|
||||||
dialogPair.second.setAllowAdaptiveSelections(allowAdaptiveSelections);
|
|
||||||
dialogPair.first.show();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,7 +311,7 @@ public class PlayerActivity extends Activity
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void preparePlayback() {
|
public void preparePlayback() {
|
||||||
initializePlayer();
|
player.retry();
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlaybackControlView.VisibilityListener implementation
|
// PlaybackControlView.VisibilityListener implementation
|
||||||
@ -413,13 +403,8 @@ public class PlayerActivity extends Activity
|
|||||||
|
|
||||||
boolean preferExtensionDecoders =
|
boolean preferExtensionDecoders =
|
||||||
intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false);
|
intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false);
|
||||||
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode =
|
RenderersFactory renderersFactory =
|
||||||
((DemoApplication) getApplication()).useExtensionRenderers()
|
((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders);
|
||||||
? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
|
||||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
|
|
||||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
|
|
||||||
DefaultRenderersFactory renderersFactory =
|
|
||||||
new DefaultRenderersFactory(this, extensionRendererMode);
|
|
||||||
|
|
||||||
trackSelector = new DefaultTrackSelector(trackSelectionFactory);
|
trackSelector = new DefaultTrackSelector(trackSelectionFactory);
|
||||||
trackSelector.setParameters(trackSelectorParameters);
|
trackSelector.setParameters(trackSelectorParameters);
|
||||||
@ -464,7 +449,7 @@ public class PlayerActivity extends Activity
|
|||||||
player.seekTo(startWindow, startPosition);
|
player.seekTo(startWindow, startPosition);
|
||||||
}
|
}
|
||||||
player.prepare(mediaSource, !haveStartPosition, false);
|
player.prepare(mediaSource, !haveStartPosition, false);
|
||||||
updateButtonVisibilities();
|
updateButtonVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
private MediaSource buildMediaSource(Uri uri) {
|
private MediaSource buildMediaSource(Uri uri) {
|
||||||
@ -473,24 +458,22 @@ public class PlayerActivity extends Activity
|
|||||||
|
|
||||||
private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
|
private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
|
||||||
@ContentType int type = Util.inferContentType(uri, overrideExtension);
|
@ContentType int type = Util.inferContentType(uri, overrideExtension);
|
||||||
|
List<StreamKey> offlineStreamKeys = getOfflineStreamKeys(uri);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case C.TYPE_DASH:
|
case C.TYPE_DASH:
|
||||||
return new DashMediaSource.Factory(dataSourceFactory)
|
return new DashMediaSource.Factory(dataSourceFactory)
|
||||||
.setManifestParser(
|
.setStreamKeys(offlineStreamKeys)
|
||||||
new FilteringManifestParser<>(new DashManifestParser(), getOfflineStreamKeys(uri)))
|
|
||||||
.createMediaSource(uri);
|
.createMediaSource(uri);
|
||||||
case C.TYPE_SS:
|
case C.TYPE_SS:
|
||||||
return new SsMediaSource.Factory(dataSourceFactory)
|
return new SsMediaSource.Factory(dataSourceFactory)
|
||||||
.setManifestParser(
|
.setStreamKeys(offlineStreamKeys)
|
||||||
new FilteringManifestParser<>(new SsManifestParser(), getOfflineStreamKeys(uri)))
|
|
||||||
.createMediaSource(uri);
|
.createMediaSource(uri);
|
||||||
case C.TYPE_HLS:
|
case C.TYPE_HLS:
|
||||||
return new HlsMediaSource.Factory(dataSourceFactory)
|
return new HlsMediaSource.Factory(dataSourceFactory)
|
||||||
.setPlaylistParserFactory(
|
.setStreamKeys(offlineStreamKeys)
|
||||||
new DefaultHlsPlaylistParserFactory(getOfflineStreamKeys(uri)))
|
|
||||||
.createMediaSource(uri);
|
.createMediaSource(uri);
|
||||||
case C.TYPE_OTHER:
|
case C.TYPE_OTHER:
|
||||||
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||||
default: {
|
default: {
|
||||||
throw new IllegalStateException("Unsupported type: " + type);
|
throw new IllegalStateException("Unsupported type: " + type);
|
||||||
}
|
}
|
||||||
@ -617,41 +600,9 @@ public class PlayerActivity extends Activity
|
|||||||
|
|
||||||
// User controls
|
// User controls
|
||||||
|
|
||||||
private void updateButtonVisibilities() {
|
private void updateButtonVisibility() {
|
||||||
debugRootView.removeAllViews();
|
selectTracksButton.setEnabled(
|
||||||
if (player == null) {
|
player != null && TrackSelectionDialog.willHaveContent(trackSelector));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
|
||||||
if (mappedTrackInfo == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
|
|
||||||
TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(i);
|
|
||||||
if (trackGroups.length != 0) {
|
|
||||||
Button button = new Button(this);
|
|
||||||
int label;
|
|
||||||
switch (player.getRendererType(i)) {
|
|
||||||
case C.TRACK_TYPE_AUDIO:
|
|
||||||
label = R.string.exo_track_selection_title_audio;
|
|
||||||
break;
|
|
||||||
case C.TRACK_TYPE_VIDEO:
|
|
||||||
label = R.string.exo_track_selection_title_video;
|
|
||||||
break;
|
|
||||||
case C.TRACK_TYPE_TEXT:
|
|
||||||
label = R.string.exo_track_selection_title_text;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
button.setText(label);
|
|
||||||
button.setTag(i);
|
|
||||||
button.setOnClickListener(this);
|
|
||||||
debugRootView.addView(button);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showControls() {
|
private void showControls() {
|
||||||
@ -687,16 +638,7 @@ public class PlayerActivity extends Activity
|
|||||||
if (playbackState == Player.STATE_ENDED) {
|
if (playbackState == Player.STATE_ENDED) {
|
||||||
showControls();
|
showControls();
|
||||||
}
|
}
|
||||||
updateButtonVisibilities();
|
updateButtonVisibility();
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
|
|
||||||
if (player.getPlaybackError() != null) {
|
|
||||||
// The user has performed a seek whilst in the error state. Update the resume position so
|
|
||||||
// that if the user then retries, playback resumes from the position to which they seeked.
|
|
||||||
updateStartPosition();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -705,8 +647,7 @@ public class PlayerActivity extends Activity
|
|||||||
clearStartPosition();
|
clearStartPosition();
|
||||||
initializePlayer();
|
initializePlayer();
|
||||||
} else {
|
} else {
|
||||||
updateStartPosition();
|
updateButtonVisibility();
|
||||||
updateButtonVisibilities();
|
|
||||||
showControls();
|
showControls();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -714,7 +655,7 @@ public class PlayerActivity extends Activity
|
|||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("ReferenceEquality")
|
@SuppressWarnings("ReferenceEquality")
|
||||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||||
updateButtonVisibilities();
|
updateButtonVisibility();
|
||||||
if (trackGroups != lastSeenTrackGroupArray) {
|
if (trackGroups != lastSeenTrackGroupArray) {
|
||||||
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
||||||
if (mappedTrackInfo != null) {
|
if (mappedTrackInfo != null) {
|
||||||
|
@ -15,14 +15,14 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.demo;
|
package com.google.android.exoplayer2.demo;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.AssetManager;
|
import android.content.res.AssetManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import android.util.JsonReader;
|
import android.util.JsonReader;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
@ -37,6 +37,7 @@ import android.widget.ImageButton;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import com.google.android.exoplayer2.ParserException;
|
import com.google.android.exoplayer2.ParserException;
|
||||||
|
import com.google.android.exoplayer2.RenderersFactory;
|
||||||
import com.google.android.exoplayer2.offline.DownloadService;
|
import com.google.android.exoplayer2.offline.DownloadService;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
|
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
|
||||||
@ -54,7 +55,7 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** An activity for selecting from a list of media samples. */
|
/** An activity for selecting from a list of media samples. */
|
||||||
public class SampleChooserActivity extends Activity
|
public class SampleChooserActivity extends AppCompatActivity
|
||||||
implements DownloadTracker.Listener, OnChildClickListener {
|
implements DownloadTracker.Listener, OnChildClickListener {
|
||||||
|
|
||||||
private static final String TAG = "SampleChooserActivity";
|
private static final String TAG = "SampleChooserActivity";
|
||||||
@ -177,7 +178,15 @@ public class SampleChooserActivity extends Activity
|
|||||||
.show();
|
.show();
|
||||||
} else {
|
} else {
|
||||||
UriSample uriSample = (UriSample) sample;
|
UriSample uriSample = (UriSample) sample;
|
||||||
downloadTracker.toggleDownload(this, sample.name, uriSample.uri, uriSample.extension);
|
RenderersFactory renderersFactory =
|
||||||
|
((DemoApplication) getApplication())
|
||||||
|
.buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem));
|
||||||
|
downloadTracker.toggleDownload(
|
||||||
|
getSupportFragmentManager(),
|
||||||
|
sample.name,
|
||||||
|
uriSample.uri,
|
||||||
|
uriSample.extension,
|
||||||
|
renderersFactory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -350,8 +359,7 @@ public class SampleChooserActivity extends Activity
|
|||||||
? null
|
? null
|
||||||
: new DrmInfo(drmScheme, drmLicenseUrl, drmKeyRequestProperties, drmMultiSession);
|
: new DrmInfo(drmScheme, drmLicenseUrl, drmKeyRequestProperties, drmMultiSession);
|
||||||
if (playlistSamples != null) {
|
if (playlistSamples != null) {
|
||||||
UriSample[] playlistSamplesArray = playlistSamples.toArray(
|
UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]);
|
||||||
new UriSample[playlistSamples.size()]);
|
|
||||||
return new PlaylistSample(sampleName, drmInfo, playlistSamplesArray);
|
return new PlaylistSample(sampleName, drmInfo, playlistSamplesArray);
|
||||||
} else {
|
} else {
|
||||||
return new UriSample(
|
return new UriSample(
|
||||||
|
@ -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_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:visibility="gone"/>
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<Button android:id="@+id/select_tracks_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/track_selection_title"
|
||||||
|
android:enabled="false"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
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
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
<item android:id="@+id/prefer_extension_decoders"
|
<item android:id="@+id/prefer_extension_decoders"
|
||||||
android:title="@string/prefer_extension_decoders"
|
android:title="@string/prefer_extension_decoders"
|
||||||
android:showAsAction="never"
|
android:checkable="true"
|
||||||
android:checkable="true"/>
|
app:showAsAction="never"/>
|
||||||
<item android:id="@+id/random_abr"
|
<item android:id="@+id/random_abr"
|
||||||
android:title="@string/random_abr"
|
android:title="@string/random_abr"
|
||||||
android:showAsAction="never"
|
android:checkable="true"
|
||||||
android:checkable="true"/>
|
app:showAsAction="never"/>
|
||||||
</menu>
|
</menu>
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
|
|
||||||
<string name="application_name">ExoPlayer</string>
|
<string name="application_name">ExoPlayer</string>
|
||||||
|
|
||||||
|
<string name="track_selection_title">Select tracks</string>
|
||||||
|
|
||||||
<string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string>
|
<string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string>
|
||||||
|
|
||||||
<string name="error_cleartext_not_permitted">Cleartext traffic not permitted</string>
|
<string name="error_cleartext_not_permitted">Cleartext traffic not permitted</string>
|
||||||
|
@ -15,8 +15,11 @@
|
|||||||
-->
|
-->
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<style name="PlayerTheme" parent="android:Theme.Holo">
|
<style name="TrackSelectionDialogThemeOverlay" parent="ThemeOverlay.AppCompat.Dialog.Alert">
|
||||||
<item name="android:windowNoTitle">true</item>
|
<item name="windowNoTitle">false</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="PlayerTheme" parent="Theme.AppCompat.NoActionBar">
|
||||||
<item name="android:windowBackground">@android:color/black</item>
|
<item name="android:windowBackground">@android:color/black</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
The cast extension is a [Player][] implementation that controls playback on a
|
The cast extension is a [Player][] implementation that controls playback on a
|
||||||
Cast receiver app.
|
Cast receiver app.
|
||||||
|
|
||||||
[Player]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/Player.html
|
[Player]: https://exoplayer.dev/doc/reference/index.html?com/google/android/exoplayer2/Player.html
|
||||||
|
|
||||||
## Getting the extension ##
|
## Getting the extension ##
|
||||||
|
|
||||||
|
@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
@ -24,32 +23,21 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 14
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
consumerProguardFiles 'proguard-rules.txt'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions.unitTests.includeAndroidResources = true
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api 'com.google.android.gms:play-services-cast-framework:16.1.2'
|
api 'com.google.android.gms:play-services-cast-framework:16.1.2'
|
||||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
implementation 'androidx.annotation:annotation:1.0.2'
|
||||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
|
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation project(modulePrefix + 'library-ui')
|
implementation project(modulePrefix + 'library-ui')
|
||||||
testImplementation project(modulePrefix + 'testutils')
|
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
testImplementation 'junit:junit:' + junitVersion
|
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
|
||||||
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
|
|
||||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
|
||||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||||
// These dependencies are necessary to force the supportLibraryVersion of
|
|
||||||
// com.android.support:support-v4, com.android.support:appcompat-v7 and
|
|
||||||
// com.android.support:mediarouter-v7 to be used. Else older versions are
|
|
||||||
// used, for example via:
|
|
||||||
// com.google.android.gms:play-services-cast-framework:15.0.1
|
|
||||||
// |-- com.android.support:mediarouter-v7:26.1.0
|
|
||||||
api 'com.android.support:support-v4:' + supportLibraryVersion
|
|
||||||
api 'com.android.support:mediarouter-v7:' + supportLibraryVersion
|
|
||||||
api 'com.android.support:recyclerview-v7:' + supportLibraryVersion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
@ -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;
|
package com.google.android.exoplayer2.ext.cast;
|
||||||
|
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.support.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.BasePlayer;
|
import com.google.android.exoplayer2.BasePlayer;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
@ -52,35 +52,18 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||||||
* {@link Player} implementation that communicates with a Cast receiver app.
|
* {@link Player} implementation that communicates with a Cast receiver app.
|
||||||
*
|
*
|
||||||
* <p>The behavior of this class depends on the underlying Cast session, which is obtained from the
|
* <p>The behavior of this class depends on the underlying Cast session, which is obtained from the
|
||||||
* Cast context passed to {@link #CastPlayer}. To keep track of the session,
|
* Cast context passed to {@link #CastPlayer}. To keep track of the session, {@link
|
||||||
* {@link #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be
|
* #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be
|
||||||
* implemented and attached to the player.</p>
|
* implemented and attached to the player.
|
||||||
*
|
*
|
||||||
* <p> If no session is available, the player state will remain unchanged and calls to methods that
|
* <p>If no session is available, the player state will remain unchanged and calls to methods that
|
||||||
* alter it will be ignored. Querying the player state is possible even when no session is
|
* alter it will be ignored. Querying the player state is possible even when no session is
|
||||||
* available, in which case, the last observed receiver app state is reported.</p>
|
* available, in which case, the last observed receiver app state is reported.
|
||||||
*
|
*
|
||||||
* <p>Methods should be called on the application's main thread.</p>
|
* <p>Methods should be called on the application's main thread.
|
||||||
*/
|
*/
|
||||||
public final class CastPlayer extends BasePlayer {
|
public final class CastPlayer extends BasePlayer {
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener of changes in the cast session availability.
|
|
||||||
*/
|
|
||||||
public interface SessionAvailabilityListener {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a cast session becomes available to the player.
|
|
||||||
*/
|
|
||||||
void onCastSessionAvailable();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the cast session becomes unavailable.
|
|
||||||
*/
|
|
||||||
void onCastSessionUnavailable();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final String TAG = "CastPlayer";
|
private static final String TAG = "CastPlayer";
|
||||||
|
|
||||||
private static final int RENDERER_COUNT = 3;
|
private static final int RENDERER_COUNT = 3;
|
||||||
@ -591,7 +574,9 @@ public final class CastPlayer extends BasePlayer {
|
|||||||
CastTimeline oldTimeline = currentTimeline;
|
CastTimeline oldTimeline = currentTimeline;
|
||||||
MediaStatus status = getMediaStatus();
|
MediaStatus status = getMediaStatus();
|
||||||
currentTimeline =
|
currentTimeline =
|
||||||
status != null ? timelineTracker.getCastTimeline(status) : CastTimeline.EMPTY_CAST_TIMELINE;
|
status != null
|
||||||
|
? timelineTracker.getCastTimeline(remoteMediaClient)
|
||||||
|
: CastTimeline.EMPTY_CAST_TIMELINE;
|
||||||
return !oldTimeline.equals(currentTimeline);
|
return !oldTimeline.equals(currentTimeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,24 +15,66 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.cast;
|
package com.google.android.exoplayer2.ext.cast;
|
||||||
|
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import android.util.SparseArray;
|
||||||
import android.util.SparseIntArray;
|
import android.util.SparseIntArray;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.gms.cast.MediaInfo;
|
|
||||||
import com.google.android.gms.cast.MediaQueueItem;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link Timeline} for Cast media queues.
|
* A {@link Timeline} for Cast media queues.
|
||||||
*/
|
*/
|
||||||
/* package */ final class CastTimeline extends Timeline {
|
/* package */ final class CastTimeline extends Timeline {
|
||||||
|
|
||||||
|
/** Holds {@link Timeline} related data for a Cast media item. */
|
||||||
|
public static final class ItemData {
|
||||||
|
|
||||||
|
/** Holds no media information. */
|
||||||
|
public static final ItemData EMPTY = new ItemData();
|
||||||
|
|
||||||
|
/** The duration of the item in microseconds, or {@link C#TIME_UNSET} if unknown. */
|
||||||
|
public final long durationUs;
|
||||||
|
/**
|
||||||
|
* The default start position of the item in microseconds, or {@link C#TIME_UNSET} if unknown.
|
||||||
|
*/
|
||||||
|
public final long defaultPositionUs;
|
||||||
|
|
||||||
|
private ItemData() {
|
||||||
|
this(/* durationUs= */ C.TIME_UNSET, /* defaultPositionUs */ C.TIME_UNSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param durationUs See {@link #durationsUs}.
|
||||||
|
* @param defaultPositionUs See {@link #defaultPositionUs}.
|
||||||
|
*/
|
||||||
|
public ItemData(long durationUs, long defaultPositionUs) {
|
||||||
|
this.durationUs = durationUs;
|
||||||
|
this.defaultPositionUs = defaultPositionUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns an instance with the given {@link #durationsUs}. */
|
||||||
|
public ItemData copyWithDurationUs(long durationUs) {
|
||||||
|
if (durationUs == this.durationUs) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return new ItemData(durationUs, defaultPositionUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns an instance with the given {@link #defaultPositionsUs}. */
|
||||||
|
public ItemData copyWithDefaultPositionUs(long defaultPositionUs) {
|
||||||
|
if (defaultPositionUs == this.defaultPositionUs) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return new ItemData(durationUs, defaultPositionUs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@link Timeline} for a cast queue that has no items. */
|
||||||
public static final CastTimeline EMPTY_CAST_TIMELINE =
|
public static final CastTimeline EMPTY_CAST_TIMELINE =
|
||||||
new CastTimeline(Collections.emptyList(), Collections.emptyMap());
|
new CastTimeline(new int[0], new SparseArray<>());
|
||||||
|
|
||||||
private final SparseIntArray idsToIndex;
|
private final SparseIntArray idsToIndex;
|
||||||
private final int[] ids;
|
private final int[] ids;
|
||||||
@ -40,28 +82,23 @@ import java.util.Map;
|
|||||||
private final long[] defaultPositionsUs;
|
private final long[] defaultPositionsUs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param items A list of cast media queue items to represent.
|
* Creates a Cast timeline from the given data.
|
||||||
* @param contentIdToDurationUsMap A map of content id to duration in microseconds.
|
*
|
||||||
|
* @param itemIds The ids of the items in the timeline.
|
||||||
|
* @param itemIdToData Maps item ids to {@link ItemData}.
|
||||||
*/
|
*/
|
||||||
public CastTimeline(List<MediaQueueItem> items, Map<String, Long> contentIdToDurationUsMap) {
|
public CastTimeline(int[] itemIds, SparseArray<ItemData> itemIdToData) {
|
||||||
int itemCount = items.size();
|
int itemCount = itemIds.length;
|
||||||
int index = 0;
|
|
||||||
idsToIndex = new SparseIntArray(itemCount);
|
idsToIndex = new SparseIntArray(itemCount);
|
||||||
ids = new int[itemCount];
|
ids = Arrays.copyOf(itemIds, itemCount);
|
||||||
durationsUs = new long[itemCount];
|
durationsUs = new long[itemCount];
|
||||||
defaultPositionsUs = new long[itemCount];
|
defaultPositionsUs = new long[itemCount];
|
||||||
for (MediaQueueItem item : items) {
|
for (int i = 0; i < ids.length; i++) {
|
||||||
int itemId = item.getItemId();
|
int id = ids[i];
|
||||||
ids[index] = itemId;
|
idsToIndex.put(id, i);
|
||||||
idsToIndex.put(itemId, index);
|
ItemData data = itemIdToData.get(id, ItemData.EMPTY);
|
||||||
MediaInfo mediaInfo = item.getMedia();
|
durationsUs[i] = data.durationUs;
|
||||||
String contentId = mediaInfo.getContentId();
|
defaultPositionsUs[i] = data.defaultPositionUs;
|
||||||
durationsUs[index] =
|
|
||||||
contentIdToDurationUsMap.containsKey(contentId)
|
|
||||||
? contentIdToDurationUsMap.get(contentId)
|
|
||||||
: CastUtils.getStreamDurationUs(mediaInfo);
|
|
||||||
defaultPositionsUs[index] = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
|
|
||||||
index++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +145,7 @@ import java.util.Map;
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object getUidOfPeriod(int periodIndex) {
|
public Integer getUidOfPeriod(int periodIndex) {
|
||||||
return ids[periodIndex];
|
return ids[periodIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,53 +15,84 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.cast;
|
package com.google.android.exoplayer2.ext.cast;
|
||||||
|
|
||||||
import com.google.android.gms.cast.MediaInfo;
|
import android.util.SparseArray;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.gms.cast.MediaQueueItem;
|
import com.google.android.gms.cast.MediaQueueItem;
|
||||||
import com.google.android.gms.cast.MediaStatus;
|
import com.google.android.gms.cast.MediaStatus;
|
||||||
import java.util.HashMap;
|
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates {@link CastTimeline}s from cast receiver app media status.
|
* Creates {@link CastTimeline CastTimelines} from cast receiver app status updates.
|
||||||
*
|
*
|
||||||
* <p>This class keeps track of the duration reported by the current item to fill any missing
|
* <p>This class keeps track of the duration reported by the current item to fill any missing
|
||||||
* durations in the media queue items [See internal: b/65152553].
|
* durations in the media queue items [See internal: b/65152553].
|
||||||
*/
|
*/
|
||||||
/* package */ final class CastTimelineTracker {
|
/* package */ final class CastTimelineTracker {
|
||||||
|
|
||||||
private final HashMap<String, Long> contentIdToDurationUsMap;
|
private final SparseArray<CastTimeline.ItemData> itemIdToData;
|
||||||
private final HashSet<String> scratchContentIdSet;
|
|
||||||
|
|
||||||
public CastTimelineTracker() {
|
public CastTimelineTracker() {
|
||||||
contentIdToDurationUsMap = new HashMap<>();
|
itemIdToData = new SparseArray<>();
|
||||||
scratchContentIdSet = new HashSet<>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a {@link CastTimeline} that represent the given {@code status}.
|
* Returns a {@link CastTimeline} that represents the state of the given {@code
|
||||||
|
* remoteMediaClient}.
|
||||||
*
|
*
|
||||||
* @param status The Cast media status.
|
* <p>Returned timelines may contain values obtained from {@code remoteMediaClient} in previous
|
||||||
* @return A {@link CastTimeline} that represent the given {@code status}.
|
* invocations of this method.
|
||||||
|
*
|
||||||
|
* @param remoteMediaClient The Cast media client.
|
||||||
|
* @return A {@link CastTimeline} that represents the given {@code remoteMediaClient} status.
|
||||||
*/
|
*/
|
||||||
public CastTimeline getCastTimeline(MediaStatus status) {
|
public CastTimeline getCastTimeline(RemoteMediaClient remoteMediaClient) {
|
||||||
MediaInfo mediaInfo = status.getMediaInfo();
|
int[] itemIds = remoteMediaClient.getMediaQueue().getItemIds();
|
||||||
List<MediaQueueItem> items = status.getQueueItems();
|
if (itemIds.length > 0) {
|
||||||
removeUnusedDurationEntries(items);
|
// Only remove unused items when there is something in the queue to avoid removing all entries
|
||||||
|
// if the remote media client clears the queue temporarily. See [Internal ref: b/128825216].
|
||||||
if (mediaInfo != null) {
|
removeUnusedItemDataEntries(itemIds);
|
||||||
String contentId = mediaInfo.getContentId();
|
|
||||||
long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
|
|
||||||
contentIdToDurationUsMap.put(contentId, durationUs);
|
|
||||||
}
|
}
|
||||||
return new CastTimeline(items, contentIdToDurationUsMap);
|
|
||||||
|
// TODO: Reset state when the app instance changes [Internal ref: b/129672468].
|
||||||
|
MediaStatus mediaStatus = remoteMediaClient.getMediaStatus();
|
||||||
|
if (mediaStatus == null) {
|
||||||
|
return CastTimeline.EMPTY_CAST_TIMELINE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int currentItemId = mediaStatus.getCurrentItemId();
|
||||||
|
long durationUs = CastUtils.getStreamDurationUs(mediaStatus.getMediaInfo());
|
||||||
|
itemIdToData.put(
|
||||||
|
currentItemId,
|
||||||
|
itemIdToData
|
||||||
|
.get(currentItemId, CastTimeline.ItemData.EMPTY)
|
||||||
|
.copyWithDurationUs(durationUs));
|
||||||
|
|
||||||
|
for (MediaQueueItem item : mediaStatus.getQueueItems()) {
|
||||||
|
int itemId = item.getItemId();
|
||||||
|
itemIdToData.put(
|
||||||
|
itemId,
|
||||||
|
itemIdToData
|
||||||
|
.get(itemId, CastTimeline.ItemData.EMPTY)
|
||||||
|
.copyWithDefaultPositionUs((long) (item.getStartTime() * C.MICROS_PER_SECOND)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CastTimeline(itemIds, itemIdToData);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeUnusedDurationEntries(List<MediaQueueItem> items) {
|
private void removeUnusedItemDataEntries(int[] itemIds) {
|
||||||
scratchContentIdSet.clear();
|
HashSet<Integer> scratchItemIds = new HashSet<>(/* initialCapacity= */ itemIds.length * 2);
|
||||||
for (MediaQueueItem item : items) {
|
for (int id : itemIds) {
|
||||||
scratchContentIdSet.add(item.getMedia().getContentId());
|
scratchItemIds.add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
while (index < itemIdToData.size()) {
|
||||||
|
if (!scratchItemIds.contains(itemIdToData.keyAt(index))) {
|
||||||
|
itemIdToData.removeAt(index);
|
||||||
|
} else {
|
||||||
|
index++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
contentIdToDurationUsMap.keySet().retainAll(scratchContentIdSet);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,11 +31,13 @@ import com.google.android.gms.cast.MediaTrack;
|
|||||||
* unknown or not applicable.
|
* unknown or not applicable.
|
||||||
*
|
*
|
||||||
* @param mediaInfo The media info to get the duration from.
|
* @param mediaInfo The media info to get the duration from.
|
||||||
* @return The duration in microseconds.
|
* @return The duration in microseconds, or {@link C#TIME_UNSET} if unknown or not applicable.
|
||||||
*/
|
*/
|
||||||
public static long getStreamDurationUs(MediaInfo mediaInfo) {
|
public static long getStreamDurationUs(MediaInfo mediaInfo) {
|
||||||
long durationMs =
|
if (mediaInfo == null) {
|
||||||
mediaInfo != null ? mediaInfo.getStreamDuration() : MediaInfo.UNKNOWN_DURATION;
|
return C.TIME_UNSET;
|
||||||
|
}
|
||||||
|
long durationMs = mediaInfo.getStreamDuration();
|
||||||
return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET;
|
return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,6 +111,7 @@ import com.google.android.gms.cast.MediaTrack;
|
|||||||
/* codecs= */ null,
|
/* codecs= */ null,
|
||||||
/* bitrate= */ Format.NO_VALUE,
|
/* bitrate= */ Format.NO_VALUE,
|
||||||
/* selectionFlags= */ 0,
|
/* selectionFlags= */ 0,
|
||||||
|
/* roleFlags= */ 0,
|
||||||
mediaTrack.getLanguage());
|
mediaTrack.getLanguage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
package com.google.android.exoplayer2.ext.cast;
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.testutil.TimelineAsserts;
|
import com.google.android.exoplayer2.testutil.TimelineAsserts;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import com.google.android.gms.cast.MediaInfo;
|
import com.google.android.gms.cast.MediaInfo;
|
||||||
import com.google.android.gms.cast.MediaQueueItem;
|
|
||||||
import com.google.android.gms.cast.MediaStatus;
|
import com.google.android.gms.cast.MediaStatus;
|
||||||
import java.util.ArrayList;
|
import com.google.android.gms.cast.framework.media.MediaQueue;
|
||||||
|
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
|
||||||
|
import java.util.Collections;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
import org.robolectric.RobolectricTestRunner;
|
|
||||||
|
|
||||||
/** Tests for {@link CastTimelineTracker}. */
|
/** Tests for {@link CastTimelineTracker}. */
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class CastTimelineTrackerTest {
|
public class CastTimelineTrackerTest {
|
||||||
|
|
||||||
private static final long DURATION_1_MS = 1000;
|
|
||||||
private static final long DURATION_2_MS = 2000;
|
private static final long DURATION_2_MS = 2000;
|
||||||
private static final long DURATION_3_MS = 3000;
|
private static final long DURATION_3_MS = 3000;
|
||||||
private static final long DURATION_4_MS = 4000;
|
private static final long DURATION_4_MS = 4000;
|
||||||
@ -39,91 +39,89 @@ public class CastTimelineTrackerTest {
|
|||||||
|
|
||||||
/** Tests that duration of the current media info is correctly propagated to the timeline. */
|
/** Tests that duration of the current media info is correctly propagated to the timeline. */
|
||||||
@Test
|
@Test
|
||||||
public void testGetCastTimeline() {
|
public void testGetCastTimelinePersistsDuration() {
|
||||||
MediaInfo mediaInfo;
|
|
||||||
MediaStatus status =
|
|
||||||
mockMediaStatus(
|
|
||||||
new int[] {1, 2, 3},
|
|
||||||
new String[] {"contentId1", "contentId2", "contentId3"},
|
|
||||||
new long[] {DURATION_1_MS, MediaInfo.UNKNOWN_DURATION, MediaInfo.UNKNOWN_DURATION});
|
|
||||||
|
|
||||||
CastTimelineTracker tracker = new CastTimelineTracker();
|
CastTimelineTracker tracker = new CastTimelineTracker();
|
||||||
mediaInfo = getMediaInfo("contentId1", DURATION_1_MS);
|
|
||||||
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
|
|
||||||
TimelineAsserts.assertPeriodDurations(
|
|
||||||
tracker.getCastTimeline(status), C.msToUs(DURATION_1_MS), C.TIME_UNSET, C.TIME_UNSET);
|
|
||||||
|
|
||||||
mediaInfo = getMediaInfo("contentId3", DURATION_3_MS);
|
RemoteMediaClient remoteMediaClient =
|
||||||
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
|
mockRemoteMediaClient(
|
||||||
|
/* itemIds= */ new int[] {1, 2, 3, 4, 5},
|
||||||
|
/* currentItemId= */ 2,
|
||||||
|
/* currentDurationMs= */ DURATION_2_MS);
|
||||||
TimelineAsserts.assertPeriodDurations(
|
TimelineAsserts.assertPeriodDurations(
|
||||||
tracker.getCastTimeline(status),
|
tracker.getCastTimeline(remoteMediaClient),
|
||||||
C.msToUs(DURATION_1_MS),
|
|
||||||
C.TIME_UNSET,
|
C.TIME_UNSET,
|
||||||
C.msToUs(DURATION_3_MS));
|
C.msToUs(DURATION_2_MS),
|
||||||
|
C.TIME_UNSET,
|
||||||
|
C.TIME_UNSET,
|
||||||
|
C.TIME_UNSET);
|
||||||
|
|
||||||
mediaInfo = getMediaInfo("contentId2", DURATION_2_MS);
|
remoteMediaClient =
|
||||||
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
|
mockRemoteMediaClient(
|
||||||
|
/* itemIds= */ new int[] {1, 2, 3},
|
||||||
|
/* currentItemId= */ 3,
|
||||||
|
/* currentDurationMs= */ DURATION_3_MS);
|
||||||
TimelineAsserts.assertPeriodDurations(
|
TimelineAsserts.assertPeriodDurations(
|
||||||
tracker.getCastTimeline(status),
|
tracker.getCastTimeline(remoteMediaClient),
|
||||||
C.msToUs(DURATION_1_MS),
|
C.TIME_UNSET,
|
||||||
C.msToUs(DURATION_2_MS),
|
C.msToUs(DURATION_2_MS),
|
||||||
C.msToUs(DURATION_3_MS));
|
C.msToUs(DURATION_3_MS));
|
||||||
|
|
||||||
MediaStatus newStatus =
|
remoteMediaClient =
|
||||||
mockMediaStatus(
|
mockRemoteMediaClient(
|
||||||
new int[] {4, 1, 5, 3},
|
/* itemIds= */ new int[] {1, 3},
|
||||||
new String[] {"contentId4", "contentId1", "contentId5", "contentId3"},
|
/* currentItemId= */ 3,
|
||||||
new long[] {
|
/* currentDurationMs= */ DURATION_3_MS);
|
||||||
MediaInfo.UNKNOWN_DURATION,
|
|
||||||
MediaInfo.UNKNOWN_DURATION,
|
|
||||||
DURATION_5_MS,
|
|
||||||
MediaInfo.UNKNOWN_DURATION
|
|
||||||
});
|
|
||||||
mediaInfo = getMediaInfo("contentId5", DURATION_5_MS);
|
|
||||||
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
|
|
||||||
TimelineAsserts.assertPeriodDurations(
|
TimelineAsserts.assertPeriodDurations(
|
||||||
tracker.getCastTimeline(newStatus),
|
tracker.getCastTimeline(remoteMediaClient), C.TIME_UNSET, C.msToUs(DURATION_3_MS));
|
||||||
C.TIME_UNSET,
|
|
||||||
C.msToUs(DURATION_1_MS),
|
|
||||||
C.msToUs(DURATION_5_MS),
|
|
||||||
C.msToUs(DURATION_3_MS));
|
|
||||||
|
|
||||||
mediaInfo = getMediaInfo("contentId3", DURATION_3_MS);
|
remoteMediaClient =
|
||||||
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
|
mockRemoteMediaClient(
|
||||||
|
/* itemIds= */ new int[] {1, 2, 3, 4, 5},
|
||||||
|
/* currentItemId= */ 4,
|
||||||
|
/* currentDurationMs= */ DURATION_4_MS);
|
||||||
TimelineAsserts.assertPeriodDurations(
|
TimelineAsserts.assertPeriodDurations(
|
||||||
tracker.getCastTimeline(newStatus),
|
tracker.getCastTimeline(remoteMediaClient),
|
||||||
C.TIME_UNSET,
|
C.TIME_UNSET,
|
||||||
C.msToUs(DURATION_1_MS),
|
C.TIME_UNSET,
|
||||||
C.msToUs(DURATION_5_MS),
|
C.msToUs(DURATION_3_MS),
|
||||||
C.msToUs(DURATION_3_MS));
|
|
||||||
|
|
||||||
mediaInfo = getMediaInfo("contentId4", DURATION_4_MS);
|
|
||||||
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
|
|
||||||
TimelineAsserts.assertPeriodDurations(
|
|
||||||
tracker.getCastTimeline(newStatus),
|
|
||||||
C.msToUs(DURATION_4_MS),
|
C.msToUs(DURATION_4_MS),
|
||||||
C.msToUs(DURATION_1_MS),
|
C.TIME_UNSET);
|
||||||
C.msToUs(DURATION_5_MS),
|
|
||||||
C.msToUs(DURATION_3_MS));
|
remoteMediaClient =
|
||||||
|
mockRemoteMediaClient(
|
||||||
|
/* itemIds= */ new int[] {1, 2, 3, 4, 5},
|
||||||
|
/* currentItemId= */ 5,
|
||||||
|
/* currentDurationMs= */ DURATION_5_MS);
|
||||||
|
TimelineAsserts.assertPeriodDurations(
|
||||||
|
tracker.getCastTimeline(remoteMediaClient),
|
||||||
|
C.TIME_UNSET,
|
||||||
|
C.TIME_UNSET,
|
||||||
|
C.msToUs(DURATION_3_MS),
|
||||||
|
C.msToUs(DURATION_4_MS),
|
||||||
|
C.msToUs(DURATION_5_MS));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaStatus mockMediaStatus(
|
private static RemoteMediaClient mockRemoteMediaClient(
|
||||||
int[] itemIds, String[] contentIds, long[] durationsMs) {
|
int[] itemIds, int currentItemId, long currentDurationMs) {
|
||||||
ArrayList<MediaQueueItem> items = new ArrayList<>();
|
RemoteMediaClient remoteMediaClient = Mockito.mock(RemoteMediaClient.class);
|
||||||
for (int i = 0; i < contentIds.length; i++) {
|
|
||||||
MediaInfo mediaInfo = getMediaInfo(contentIds[i], durationsMs[i]);
|
|
||||||
MediaQueueItem item = Mockito.mock(MediaQueueItem.class);
|
|
||||||
Mockito.when(item.getMedia()).thenReturn(mediaInfo);
|
|
||||||
Mockito.when(item.getItemId()).thenReturn(itemIds[i]);
|
|
||||||
items.add(item);
|
|
||||||
}
|
|
||||||
MediaStatus status = Mockito.mock(MediaStatus.class);
|
MediaStatus status = Mockito.mock(MediaStatus.class);
|
||||||
Mockito.when(status.getQueueItems()).thenReturn(items);
|
Mockito.when(status.getQueueItems()).thenReturn(Collections.emptyList());
|
||||||
return status;
|
Mockito.when(remoteMediaClient.getMediaStatus()).thenReturn(status);
|
||||||
|
Mockito.when(status.getMediaInfo()).thenReturn(getMediaInfo(currentDurationMs));
|
||||||
|
Mockito.when(status.getCurrentItemId()).thenReturn(currentItemId);
|
||||||
|
MediaQueue mediaQueue = mockMediaQueue(itemIds);
|
||||||
|
Mockito.when(remoteMediaClient.getMediaQueue()).thenReturn(mediaQueue);
|
||||||
|
return remoteMediaClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaInfo getMediaInfo(String contentId, long durationMs) {
|
private static MediaQueue mockMediaQueue(int[] itemIds) {
|
||||||
return new MediaInfo.Builder(contentId)
|
MediaQueue mediaQueue = Mockito.mock(MediaQueue.class);
|
||||||
|
Mockito.when(mediaQueue.getItemIds()).thenReturn(itemIds);
|
||||||
|
return mediaQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MediaInfo getMediaInfo(long durationMs) {
|
||||||
|
return new MediaInfo.Builder(/*contentId= */ "")
|
||||||
.setStreamDuration(durationMs)
|
.setStreamDuration(durationMs)
|
||||||
.setContentType(MimeTypes.APPLICATION_MP4)
|
.setContentType(MimeTypes.APPLICATION_MP4)
|
||||||
.setStreamType(MediaInfo.STREAM_TYPE_NONE)
|
.setStreamType(MediaInfo.STREAM_TYPE_NONE)
|
||||||
|
@ -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][].
|
The Cronet extension is an [HttpDataSource][] implementation using [Cronet][].
|
||||||
|
|
||||||
[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
|
[HttpDataSource]: https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
|
||||||
[Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F
|
[Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F
|
||||||
|
|
||||||
## Getting the extension ##
|
## Getting the extension ##
|
||||||
@ -52,4 +52,4 @@ respectively.
|
|||||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.cronet.*`
|
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.cronet.*`
|
||||||
belong to this module.
|
belong to this module.
|
||||||
|
|
||||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
|
||||||
|
@ -16,10 +16,9 @@ apply plugin: 'com.android.library'
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,12 +26,14 @@ android {
|
|||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions.unitTests.includeAndroidResources = true
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api 'org.chromium.net:cronet-embedded:71.3578.98'
|
api 'org.chromium.net:cronet-embedded:73.3683.76'
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
implementation 'androidx.annotation:annotation:1.0.2'
|
||||||
testImplementation project(modulePrefix + 'library')
|
testImplementation project(modulePrefix + 'library')
|
||||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||||
}
|
}
|
||||||
|
@ -16,10 +16,11 @@
|
|||||||
package com.google.android.exoplayer2.ext.cronet;
|
package com.google.android.exoplayer2.ext.cronet;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||||
|
import com.google.android.exoplayer2.metadata.icy.IcyHeaders;
|
||||||
import com.google.android.exoplayer2.upstream.BaseDataSource;
|
import com.google.android.exoplayer2.upstream.BaseDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DataSourceException;
|
import com.google.android.exoplayer2.upstream.DataSourceException;
|
||||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
@ -493,6 +494,11 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||||||
if (dataSpec.httpBody != null && !isContentTypeHeaderSet) {
|
if (dataSpec.httpBody != null && !isContentTypeHeaderSet) {
|
||||||
throw new IOException("HTTP request with non-empty body must set Content-Type");
|
throw new IOException("HTTP request with non-empty body must set Content-Type");
|
||||||
}
|
}
|
||||||
|
if (dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA)) {
|
||||||
|
requestBuilder.addHeader(
|
||||||
|
IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME,
|
||||||
|
IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE);
|
||||||
|
}
|
||||||
// Set the Range header.
|
// Set the Range header.
|
||||||
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
|
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
|
||||||
StringBuilder rangeValue = new StringBuilder();
|
StringBuilder rangeValue = new StringBuilder();
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.cronet;
|
package com.google.android.exoplayer2.ext.cronet;
|
||||||
|
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
|
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
package com.google.android.exoplayer2.ext.cronet;
|
package com.google.android.exoplayer2.ext.cronet;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.support.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import com.google.android.exoplayer2.util.Log;
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.lang.annotation.Documented;
|
import java.lang.annotation.Documented;
|
||||||
|
@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat;
|
|||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -28,10 +29,9 @@ import org.junit.Test;
|
|||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
import org.robolectric.RobolectricTestRunner;
|
|
||||||
|
|
||||||
/** Tests for {@link ByteArrayUploadDataProvider}. */
|
/** Tests for {@link ByteArrayUploadDataProvider}. */
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public final class ByteArrayUploadDataProviderTest {
|
public final class ByteArrayUploadDataProviderTest {
|
||||||
|
|
||||||
private static final byte[] TEST_DATA = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
|
private static final byte[] TEST_DATA = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
|
||||||
|
@ -31,6 +31,7 @@ import static org.mockito.Mockito.when;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.ConditionVariable;
|
import android.os.ConditionVariable;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
@ -62,10 +63,9 @@ import org.junit.Test;
|
|||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
import org.robolectric.RobolectricTestRunner;
|
|
||||||
|
|
||||||
/** Tests for {@link CronetDataSource}. */
|
/** Tests for {@link CronetDataSource}. */
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public final class CronetDataSourceTest {
|
public final class CronetDataSourceTest {
|
||||||
|
|
||||||
private static final int TEST_CONNECT_TIMEOUT_MS = 100;
|
private static final int TEST_CONNECT_TIMEOUT_MS = 100;
|
||||||
|
@ -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
|
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||||
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
||||||
[#2781]: https://github.com/google/ExoPlayer/issues/2781
|
[#2781]: https://github.com/google/ExoPlayer/issues/2781
|
||||||
[Supported formats]: https://google.github.io/ExoPlayer/supported-formats.html#ffmpeg-extension
|
[Supported formats]: https://exoplayer.dev/supported-formats.html#ffmpeg-extension
|
||||||
|
|
||||||
## Links ##
|
## Links ##
|
||||||
|
|
||||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ffmpeg.*`
|
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ffmpeg.*`
|
||||||
belong to this module.
|
belong to this module.
|
||||||
|
|
||||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
|
||||||
|
@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
@ -33,12 +32,15 @@ android {
|
|||||||
jniLibs.srcDir 'src/main/libs'
|
jniLibs.srcDir 'src/main/libs'
|
||||||
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
|
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions.unitTests.includeAndroidResources = true
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
implementation 'androidx.annotation:annotation:1.0.2'
|
||||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
|
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
package com.google.android.exoplayer2.ext.ffmpeg;
|
package com.google.android.exoplayer2.ext.ffmpeg;
|
||||||
|
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.ffmpeg;
|
package com.google.android.exoplayer2.ext.ffmpeg;
|
||||||
|
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||||
|
@ -15,10 +15,11 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.ffmpeg;
|
package com.google.android.exoplayer2.ext.ffmpeg;
|
||||||
|
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||||
import com.google.android.exoplayer2.util.LibraryLoader;
|
import com.google.android.exoplayer2.util.LibraryLoader;
|
||||||
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -30,6 +31,8 @@ public final class FfmpegLibrary {
|
|||||||
ExoPlayerLibraryInfo.registerModule("goog.exo.ffmpeg");
|
ExoPlayerLibraryInfo.registerModule("goog.exo.ffmpeg");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final String TAG = "FfmpegLibrary";
|
||||||
|
|
||||||
private static final LibraryLoader LOADER =
|
private static final LibraryLoader LOADER =
|
||||||
new LibraryLoader("avutil", "avresample", "avcodec", "ffmpeg");
|
new LibraryLoader("avutil", "avresample", "avcodec", "ffmpeg");
|
||||||
|
|
||||||
@ -69,7 +72,14 @@ public final class FfmpegLibrary {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
String codecName = getCodecName(mimeType, encoding);
|
String codecName = getCodecName(mimeType, encoding);
|
||||||
return codecName != null && ffmpegHasDecoder(codecName);
|
if (codecName == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!ffmpegHasDecoder(codecName)) {
|
||||||
|
Log.w(TAG, "No " + codecName + " decoder available. Check the FFmpeg build configuration.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
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.*`
|
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.flac.*`
|
||||||
belong to this module.
|
belong to this module.
|
||||||
|
|
||||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
|
||||||
|
@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
@ -34,13 +33,15 @@ android {
|
|||||||
jniLibs.srcDir 'src/main/libs'
|
jniLibs.srcDir 'src/main/libs'
|
||||||
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
|
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions.unitTests.includeAndroidResources = true
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
androidTestImplementation 'androidx.test:runner:' + testRunnerVersion
|
implementation 'androidx.annotation:annotation:1.0.2'
|
||||||
androidTestImplementation project(modulePrefix + 'testutils')
|
androidTestImplementation project(modulePrefix + 'testutils')
|
||||||
|
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
|
||||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,9 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.google.android.exoplayer2.ext.flac.test">
|
package="com.google.android.exoplayer2.ext.flac.test">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
|
<uses-sdk/>
|
||||||
|
|
||||||
<application android:debuggable="true"
|
<application android:debuggable="true"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
|
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
|
||||||
|
@ -16,22 +16,26 @@
|
|||||||
package com.google.android.exoplayer2.ext.flac;
|
package com.google.android.exoplayer2.ext.flac;
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
import android.test.InstrumentationTestCase;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
/** Unit test for {@link FlacBinarySearchSeeker}. */
|
/** Unit test for {@link FlacBinarySearchSeeker}. */
|
||||||
public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public final class FlacBinarySearchSeekerTest {
|
||||||
|
|
||||||
private static final String NOSEEKTABLE_FLAC = "bear_no_seek.flac";
|
private static final String NOSEEKTABLE_FLAC = "bear_no_seek.flac";
|
||||||
private static final int DURATION_US = 2_741_000;
|
private static final int DURATION_US = 2_741_000;
|
||||||
|
|
||||||
@Override
|
@Before
|
||||||
protected void setUp() throws Exception {
|
public void setUp() {
|
||||||
super.setUp();
|
|
||||||
if (!FlacLibrary.isAvailable()) {
|
if (!FlacLibrary.isAvailable()) {
|
||||||
fail("Flac library not available.");
|
fail("Flac library not available.");
|
||||||
}
|
}
|
||||||
@ -39,7 +43,8 @@ public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
|
|||||||
|
|
||||||
public void testGetSeekMap_returnsSeekMapWithCorrectDuration()
|
public void testGetSeekMap_returnsSeekMapWithCorrectDuration()
|
||||||
throws IOException, FlacDecoderException, InterruptedException {
|
throws IOException, FlacDecoderException, InterruptedException {
|
||||||
byte[] data = TestUtil.getByteArray(getInstrumentation().getContext(), NOSEEKTABLE_FLAC);
|
byte[] data =
|
||||||
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NOSEEKTABLE_FLAC);
|
||||||
|
|
||||||
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
|
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
|
||||||
FlacDecoderJni decoderJni = new FlacDecoderJni();
|
FlacDecoderJni decoderJni = new FlacDecoderJni();
|
||||||
@ -57,7 +62,8 @@ public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
|
|||||||
|
|
||||||
public void testSetSeekTargetUs_returnsSeekPending()
|
public void testSetSeekTargetUs_returnsSeekPending()
|
||||||
throws IOException, FlacDecoderException, InterruptedException {
|
throws IOException, FlacDecoderException, InterruptedException {
|
||||||
byte[] data = TestUtil.getByteArray(getInstrumentation().getContext(), NOSEEKTABLE_FLAC);
|
byte[] data =
|
||||||
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NOSEEKTABLE_FLAC);
|
||||||
|
|
||||||
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
|
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
|
||||||
FlacDecoderJni decoderJni = new FlacDecoderJni();
|
FlacDecoderJni decoderJni = new FlacDecoderJni();
|
||||||
|
@ -16,11 +16,13 @@
|
|||||||
package com.google.android.exoplayer2.ext.flac;
|
package com.google.android.exoplayer2.ext.flac;
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import android.test.InstrumentationTestCase;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
|
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
@ -38,9 +40,12 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
/** Seeking tests for {@link FlacExtractor} when the FLAC stream does not have a SEEKTABLE. */
|
/** Seeking tests for {@link FlacExtractor} when the FLAC stream does not have a SEEKTABLE. */
|
||||||
public final class FlacExtractorSeekTest extends InstrumentationTestCase {
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public final class FlacExtractorSeekTest {
|
||||||
|
|
||||||
private static final String NO_SEEKTABLE_FLAC = "bear_no_seek.flac";
|
private static final String NO_SEEKTABLE_FLAC = "bear_no_seek.flac";
|
||||||
private static final int DURATION_US = 2_741_000;
|
private static final int DURATION_US = 2_741_000;
|
||||||
@ -54,18 +59,18 @@ public final class FlacExtractorSeekTest extends InstrumentationTestCase {
|
|||||||
private PositionHolder positionHolder;
|
private PositionHolder positionHolder;
|
||||||
private long totalInputLength;
|
private long totalInputLength;
|
||||||
|
|
||||||
@Override
|
@Before
|
||||||
protected void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
super.setUp();
|
|
||||||
if (!FlacLibrary.isAvailable()) {
|
if (!FlacLibrary.isAvailable()) {
|
||||||
fail("Flac library not available.");
|
fail("Flac library not available.");
|
||||||
}
|
}
|
||||||
expectedOutput = new FakeExtractorOutput();
|
expectedOutput = new FakeExtractorOutput();
|
||||||
extractAllSamplesFromFileToExpectedOutput(getInstrumentation().getContext(), NO_SEEKTABLE_FLAC);
|
extractAllSamplesFromFileToExpectedOutput(
|
||||||
|
ApplicationProvider.getApplicationContext(), NO_SEEKTABLE_FLAC);
|
||||||
expectedTrackOutput = expectedOutput.trackOutputs.get(0);
|
expectedTrackOutput = expectedOutput.trackOutputs.get(0);
|
||||||
|
|
||||||
dataSource =
|
dataSource =
|
||||||
new DefaultDataSourceFactory(getInstrumentation().getContext(), "UserAgent")
|
new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), "UserAgent")
|
||||||
.createDataSource();
|
.createDataSource();
|
||||||
totalInputLength = readInputLength();
|
totalInputLength = readInputLength();
|
||||||
positionHolder = new PositionHolder();
|
positionHolder = new PositionHolder();
|
||||||
|
@ -15,17 +15,20 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.flac;
|
package com.google.android.exoplayer2.ext.flac;
|
||||||
|
|
||||||
import android.test.InstrumentationTestCase;
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
/**
|
/** Unit test for {@link FlacExtractor}. */
|
||||||
* Unit test for {@link FlacExtractor}.
|
@RunWith(AndroidJUnit4.class)
|
||||||
*/
|
public class FlacExtractorTest {
|
||||||
public class FlacExtractorTest extends InstrumentationTestCase {
|
|
||||||
|
|
||||||
@Override
|
@Before
|
||||||
protected void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
super.setUp();
|
|
||||||
if (!FlacLibrary.isAvailable()) {
|
if (!FlacLibrary.isAvailable()) {
|
||||||
fail("Flac library not available.");
|
fail("Flac library not available.");
|
||||||
}
|
}
|
||||||
@ -33,11 +36,11 @@ public class FlacExtractorTest extends InstrumentationTestCase {
|
|||||||
|
|
||||||
public void testExtractFlacSample() throws Exception {
|
public void testExtractFlacSample() throws Exception {
|
||||||
ExtractorAsserts.assertBehavior(
|
ExtractorAsserts.assertBehavior(
|
||||||
FlacExtractor::new, "bear.flac", getInstrumentation().getContext());
|
FlacExtractor::new, "bear.flac", ApplicationProvider.getApplicationContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testExtractFlacSampleWithId3Header() throws Exception {
|
public void testExtractFlacSampleWithId3Header() throws Exception {
|
||||||
ExtractorAsserts.assertBehavior(
|
ExtractorAsserts.assertBehavior(
|
||||||
FlacExtractor::new, "bear_with_id3.flac", getInstrumentation().getContext());
|
FlacExtractor::new, "bear_with_id3.flac", ApplicationProvider.getApplicationContext());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,21 +15,21 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.flac;
|
package com.google.android.exoplayer2.ext.flac;
|
||||||
|
|
||||||
import static androidx.test.InstrumentationRegistry.getContext;
|
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import androidx.test.runner.AndroidJUnit4;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.ExoPlayer;
|
import com.google.android.exoplayer2.ExoPlayer;
|
||||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.Renderer;
|
import com.google.android.exoplayer2.Renderer;
|
||||||
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
@ -56,7 +56,7 @@ public class FlacPlaybackTest {
|
|||||||
|
|
||||||
private void playUri(String uri) throws Exception {
|
private void playUri(String uri) throws Exception {
|
||||||
TestPlaybackRunnable testPlaybackRunnable =
|
TestPlaybackRunnable testPlaybackRunnable =
|
||||||
new TestPlaybackRunnable(Uri.parse(uri), getContext());
|
new TestPlaybackRunnable(Uri.parse(uri), ApplicationProvider.getApplicationContext());
|
||||||
Thread thread = new Thread(testPlaybackRunnable);
|
Thread thread = new Thread(testPlaybackRunnable);
|
||||||
thread.start();
|
thread.start();
|
||||||
thread.join();
|
thread.join();
|
||||||
@ -83,12 +83,12 @@ public class FlacPlaybackTest {
|
|||||||
Looper.prepare();
|
Looper.prepare();
|
||||||
LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer();
|
LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer();
|
||||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
|
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
|
||||||
player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector);
|
player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
|
||||||
player.addListener(this);
|
player.addListener(this);
|
||||||
MediaSource mediaSource =
|
MediaSource mediaSource =
|
||||||
new ExtractorMediaSource.Factory(
|
new ProgressiveMediaSource.Factory(
|
||||||
new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"))
|
new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"),
|
||||||
.setExtractorsFactory(MatroskaExtractor.FACTORY)
|
MatroskaExtractor.FACTORY)
|
||||||
.createMediaSource(uri);
|
.createMediaSource(uri);
|
||||||
player.prepare(mediaSource);
|
player.prepare(mediaSource);
|
||||||
player.setPlayWhenReady(true);
|
player.setPlayWhenReady(true);
|
||||||
|
@ -17,8 +17,8 @@ package com.google.android.exoplayer2.ext.flac;
|
|||||||
|
|
||||||
import static com.google.android.exoplayer2.util.Util.getPcmEncoding;
|
import static com.google.android.exoplayer2.util.Util.getPcmEncoding;
|
||||||
|
|
||||||
import android.support.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
|
import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
|
||||||
@ -94,7 +94,7 @@ public final class FlacExtractor implements Extractor {
|
|||||||
|
|
||||||
/** Constructs an instance with flags = 0. */
|
/** Constructs an instance with flags = 0. */
|
||||||
public FlacExtractor() {
|
public FlacExtractor() {
|
||||||
this(0);
|
this(/* flags= */ 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,7 +42,9 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||||
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
|
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
|
||||||
*/
|
*/
|
||||||
public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
|
public LibflacAudioRenderer(
|
||||||
|
Handler eventHandler,
|
||||||
|
AudioRendererEventListener eventListener,
|
||||||
AudioProcessor... audioProcessors) {
|
AudioProcessor... audioProcessors) {
|
||||||
super(eventHandler, eventListener, audioProcessors);
|
super(eventHandler, eventListener, audioProcessors);
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.flac;
|
|||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
|
import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
|
||||||
@ -27,6 +28,7 @@ import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
|
|||||||
import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
|
import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
|
||||||
import com.google.android.exoplayer2.extractor.ogg.OggExtractor;
|
import com.google.android.exoplayer2.extractor.ogg.OggExtractor;
|
||||||
import com.google.android.exoplayer2.extractor.ts.Ac3Extractor;
|
import com.google.android.exoplayer2.extractor.ts.Ac3Extractor;
|
||||||
|
import com.google.android.exoplayer2.extractor.ts.Ac4Extractor;
|
||||||
import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
|
import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
|
||||||
import com.google.android.exoplayer2.extractor.ts.PsExtractor;
|
import com.google.android.exoplayer2.extractor.ts.PsExtractor;
|
||||||
import com.google.android.exoplayer2.extractor.ts.TsExtractor;
|
import com.google.android.exoplayer2.extractor.ts.TsExtractor;
|
||||||
@ -35,10 +37,9 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.robolectric.RobolectricTestRunner;
|
|
||||||
|
|
||||||
/** Unit test for {@link DefaultExtractorsFactory}. */
|
/** Unit test for {@link DefaultExtractorsFactory}. */
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public final class DefaultExtractorsFactoryTest {
|
public final class DefaultExtractorsFactoryTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -59,6 +60,7 @@ public final class DefaultExtractorsFactoryTest {
|
|||||||
Mp3Extractor.class,
|
Mp3Extractor.class,
|
||||||
AdtsExtractor.class,
|
AdtsExtractor.class,
|
||||||
Ac3Extractor.class,
|
Ac3Extractor.class,
|
||||||
|
Ac4Extractor.class,
|
||||||
TsExtractor.class,
|
TsExtractor.class,
|
||||||
FlvExtractor.class,
|
FlvExtractor.class,
|
||||||
OggExtractor.class,
|
OggExtractor.class,
|
||||||
|
@ -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.*`
|
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.gvr.*`
|
||||||
belong to this module.
|
belong to this module.
|
||||||
|
|
||||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
|
||||||
|
@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
@ -27,11 +26,14 @@ android {
|
|||||||
minSdkVersion 19
|
minSdkVersion 19
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions.unitTests.includeAndroidResources = true
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
implementation project(modulePrefix + 'library-ui')
|
||||||
|
implementation 'androidx.annotation:annotation:1.0.2'
|
||||||
api 'com.google.vr:sdk-base:1.190.0'
|
api 'com.google.vr:sdk-base:1.190.0'
|
||||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.gvr;
|
package com.google.android.exoplayer2.ext.gvr;
|
||||||
|
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
@ -38,9 +38,11 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
|||||||
private static final int FRAMES_PER_OUTPUT_BUFFER = 1024;
|
private static final int FRAMES_PER_OUTPUT_BUFFER = 1024;
|
||||||
private static final int OUTPUT_CHANNEL_COUNT = 2;
|
private static final int OUTPUT_CHANNEL_COUNT = 2;
|
||||||
private static final int OUTPUT_FRAME_SIZE = OUTPUT_CHANNEL_COUNT * 2; // 16-bit stereo output.
|
private static final int OUTPUT_FRAME_SIZE = OUTPUT_CHANNEL_COUNT * 2; // 16-bit stereo output.
|
||||||
|
private static final int NO_SURROUND_FORMAT = GvrAudioSurround.SurroundFormat.INVALID;
|
||||||
|
|
||||||
private int sampleRateHz;
|
private int sampleRateHz;
|
||||||
private int channelCount;
|
private int channelCount;
|
||||||
|
private int pendingGvrAudioSurroundFormat;
|
||||||
@Nullable private GvrAudioSurround gvrAudioSurround;
|
@Nullable private GvrAudioSurround gvrAudioSurround;
|
||||||
private ByteBuffer buffer;
|
private ByteBuffer buffer;
|
||||||
private boolean inputEnded;
|
private boolean inputEnded;
|
||||||
@ -57,6 +59,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
|||||||
sampleRateHz = Format.NO_VALUE;
|
sampleRateHz = Format.NO_VALUE;
|
||||||
channelCount = Format.NO_VALUE;
|
channelCount = Format.NO_VALUE;
|
||||||
buffer = EMPTY_BUFFER;
|
buffer = EMPTY_BUFFER;
|
||||||
|
pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -92,33 +95,28 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
|||||||
}
|
}
|
||||||
this.sampleRateHz = sampleRateHz;
|
this.sampleRateHz = sampleRateHz;
|
||||||
this.channelCount = channelCount;
|
this.channelCount = channelCount;
|
||||||
maybeReleaseGvrAudioSurround();
|
|
||||||
int surroundFormat;
|
|
||||||
switch (channelCount) {
|
switch (channelCount) {
|
||||||
case 1:
|
case 1:
|
||||||
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_MONO;
|
pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_MONO;
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO;
|
pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO;
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
surroundFormat = GvrAudioSurround.SurroundFormat.FIRST_ORDER_AMBISONICS;
|
pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.FIRST_ORDER_AMBISONICS;
|
||||||
break;
|
break;
|
||||||
case 6:
|
case 6:
|
||||||
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_FIVE_DOT_ONE;
|
pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_FIVE_DOT_ONE;
|
||||||
break;
|
break;
|
||||||
case 9:
|
case 9:
|
||||||
surroundFormat = GvrAudioSurround.SurroundFormat.SECOND_ORDER_AMBISONICS;
|
pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SECOND_ORDER_AMBISONICS;
|
||||||
break;
|
break;
|
||||||
case 16:
|
case 16:
|
||||||
surroundFormat = GvrAudioSurround.SurroundFormat.THIRD_ORDER_AMBISONICS;
|
pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.THIRD_ORDER_AMBISONICS;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
|
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
|
||||||
}
|
}
|
||||||
gvrAudioSurround = new GvrAudioSurround(surroundFormat, sampleRateHz, channelCount,
|
|
||||||
FRAMES_PER_OUTPUT_BUFFER);
|
|
||||||
gvrAudioSurround.updateNativeOrientation(w, x, y, z);
|
|
||||||
if (buffer == EMPTY_BUFFER) {
|
if (buffer == EMPTY_BUFFER) {
|
||||||
buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE)
|
buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE)
|
||||||
.order(ByteOrder.nativeOrder());
|
.order(ByteOrder.nativeOrder());
|
||||||
@ -128,7 +126,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isActive() {
|
public boolean isActive() {
|
||||||
return gvrAudioSurround != null;
|
return pendingGvrAudioSurroundFormat != NO_SURROUND_FORMAT || gvrAudioSurround != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -156,14 +154,17 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void queueEndOfStream() {
|
public void queueEndOfStream() {
|
||||||
Assertions.checkNotNull(gvrAudioSurround);
|
if (gvrAudioSurround != null) {
|
||||||
|
gvrAudioSurround.triggerProcessing();
|
||||||
|
}
|
||||||
inputEnded = true;
|
inputEnded = true;
|
||||||
gvrAudioSurround.triggerProcessing();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ByteBuffer getOutput() {
|
public ByteBuffer getOutput() {
|
||||||
Assertions.checkNotNull(gvrAudioSurround);
|
if (gvrAudioSurround == null) {
|
||||||
|
return EMPTY_BUFFER;
|
||||||
|
}
|
||||||
int writtenBytes = gvrAudioSurround.getOutput(buffer, 0, buffer.capacity());
|
int writtenBytes = gvrAudioSurround.getOutput(buffer, 0, buffer.capacity());
|
||||||
buffer.position(0).limit(writtenBytes);
|
buffer.position(0).limit(writtenBytes);
|
||||||
return buffer;
|
return buffer;
|
||||||
@ -171,13 +172,20 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnded() {
|
public boolean isEnded() {
|
||||||
Assertions.checkNotNull(gvrAudioSurround);
|
return inputEnded
|
||||||
return inputEnded && gvrAudioSurround.getAvailableOutputSize() == 0;
|
&& (gvrAudioSurround == null || gvrAudioSurround.getAvailableOutputSize() == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void flush() {
|
public void flush() {
|
||||||
if (gvrAudioSurround != null) {
|
if (pendingGvrAudioSurroundFormat != NO_SURROUND_FORMAT) {
|
||||||
|
maybeReleaseGvrAudioSurround();
|
||||||
|
gvrAudioSurround =
|
||||||
|
new GvrAudioSurround(
|
||||||
|
pendingGvrAudioSurroundFormat, sampleRateHz, channelCount, FRAMES_PER_OUTPUT_BUFFER);
|
||||||
|
gvrAudioSurround.updateNativeOrientation(w, x, y, z);
|
||||||
|
pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;
|
||||||
|
} else if (gvrAudioSurround != null) {
|
||||||
gvrAudioSurround.flush();
|
gvrAudioSurround.flush();
|
||||||
}
|
}
|
||||||
inputEnded = false;
|
inputEnded = false;
|
||||||
@ -191,13 +199,13 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
|||||||
sampleRateHz = Format.NO_VALUE;
|
sampleRateHz = Format.NO_VALUE;
|
||||||
channelCount = Format.NO_VALUE;
|
channelCount = Format.NO_VALUE;
|
||||||
buffer = EMPTY_BUFFER;
|
buffer = EMPTY_BUFFER;
|
||||||
|
pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeReleaseGvrAudioSurround() {
|
private void maybeReleaseGvrAudioSurround() {
|
||||||
if (this.gvrAudioSurround != null) {
|
if (gvrAudioSurround != null) {
|
||||||
GvrAudioSurround gvrAudioSurround = this.gvrAudioSurround;
|
|
||||||
this.gvrAudioSurround = null;
|
|
||||||
gvrAudioSurround.release();
|
gvrAudioSurround.release();
|
||||||
|
gvrAudioSurround = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
|
<resources>
|
||||||
android:id="@+id/representation_list"
|
<style name="VrTheme" parent="android:Theme.Material"/>
|
||||||
android:layout_width="match_parent"
|
</resources>
|
||||||
android:layout_height="match_parent"/>
|
|
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.
|
alongside content.
|
||||||
|
|
||||||
[IMA]: https://developers.google.com/interactive-media-ads/docs/sdks/android/
|
[IMA]: https://developers.google.com/interactive-media-ads/docs/sdks/android/
|
||||||
[AdsLoader]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/source/ads/AdsLoader.html
|
[AdsLoader]: https://exoplayer.dev/doc/reference/index.html?com/google/android/exoplayer2/source/ads/AdsLoader.html
|
||||||
|
|
||||||
## Getting the extension ##
|
## Getting the extension ##
|
||||||
|
|
||||||
@ -61,4 +61,4 @@ playback.
|
|||||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ima.*`
|
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ima.*`
|
||||||
belong to this module.
|
belong to this module.
|
||||||
|
|
||||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
|
||||||
|
@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
@ -28,23 +27,14 @@ android {
|
|||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
consumerProguardFiles 'proguard-rules.txt'
|
consumerProguardFiles 'proguard-rules.txt'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions.unitTests.includeAndroidResources = true
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.10.6'
|
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2'
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'com.google.android.gms:play-services-ads:17.1.2'
|
implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0'
|
||||||
// These dependencies are necessary to force the supportLibraryVersion of
|
|
||||||
// com.android.support:support-v4 and com.android.support:customtabs to be
|
|
||||||
// used. Else older versions are used, for example via:
|
|
||||||
// com.google.android.gms:play-services-ads:17.1.2
|
|
||||||
// |-- com.android.support:customtabs:26.1.0
|
|
||||||
implementation 'com.android.support:support-v4:' + supportLibraryVersion
|
|
||||||
implementation 'com.android.support:customtabs:' + supportLibraryVersion
|
|
||||||
testImplementation 'com.google.truth:truth:' + truthVersion
|
|
||||||
testImplementation 'junit:junit:' + junitVersion
|
|
||||||
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
|
|
||||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
|
||||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,8 +19,9 @@ import android.content.Context;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.support.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import com.google.ads.interactivemedia.v3.api.Ad;
|
import com.google.ads.interactivemedia.v3.api.Ad;
|
||||||
@ -216,7 +217,7 @@ public final class ImaAdsLoader
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @VisibleForTesting
|
@VisibleForTesting
|
||||||
/* package */ Builder setImaFactory(ImaFactory imaFactory) {
|
/* package */ Builder setImaFactory(ImaFactory imaFactory) {
|
||||||
this.imaFactory = Assertions.checkNotNull(imaFactory);
|
this.imaFactory = Assertions.checkNotNull(imaFactory);
|
||||||
return this;
|
return this;
|
||||||
@ -755,7 +756,8 @@ public final class ImaAdsLoader
|
|||||||
// until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered
|
// until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered
|
||||||
// just after an ad group isn't incorrectly attributed to the next ad group.
|
// just after an ad group isn't incorrectly attributed to the next ad group.
|
||||||
int nextAdGroupIndex =
|
int nextAdGroupIndex =
|
||||||
adPlaybackState.getAdGroupIndexAfterPositionUs(C.msToUs(contentPositionMs));
|
adPlaybackState.getAdGroupIndexAfterPositionUs(
|
||||||
|
C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));
|
||||||
if (nextAdGroupIndex != expectedAdGroupIndex && nextAdGroupIndex != C.INDEX_UNSET) {
|
if (nextAdGroupIndex != expectedAdGroupIndex && nextAdGroupIndex != C.INDEX_UNSET) {
|
||||||
long nextAdGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[nextAdGroupIndex]);
|
long nextAdGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[nextAdGroupIndex]);
|
||||||
if (nextAdGroupTimeMs == C.TIME_END_OF_SOURCE) {
|
if (nextAdGroupTimeMs == C.TIME_END_OF_SOURCE) {
|
||||||
@ -1389,7 +1391,7 @@ public final class ImaAdsLoader
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Factory for objects provided by the IMA SDK. */
|
/** Factory for objects provided by the IMA SDK. */
|
||||||
// @VisibleForTesting
|
@VisibleForTesting
|
||||||
/* package */ interface ImaFactory {
|
/* package */ interface ImaFactory {
|
||||||
/** @see ImaSdkSettings */
|
/** @see ImaSdkSettings */
|
||||||
ImaSdkSettings createImaSdkSettings();
|
ImaSdkSettings createImaSdkSettings();
|
||||||
|
@ -22,10 +22,12 @@ import static org.mockito.Mockito.verify;
|
|||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.ads.interactivemedia.v3.api.Ad;
|
import com.google.ads.interactivemedia.v3.api.Ad;
|
||||||
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
|
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
|
||||||
import com.google.ads.interactivemedia.v3.api.AdEvent;
|
import com.google.ads.interactivemedia.v3.api.AdEvent;
|
||||||
@ -54,11 +56,9 @@ import org.junit.runner.RunWith;
|
|||||||
import org.mockito.InOrder;
|
import org.mockito.InOrder;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
import org.robolectric.RobolectricTestRunner;
|
|
||||||
import org.robolectric.RuntimeEnvironment;
|
|
||||||
|
|
||||||
/** Test for {@link ImaAdsLoader}. */
|
/** Test for {@link ImaAdsLoader}. */
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class ImaAdsLoaderTest {
|
public class ImaAdsLoaderTest {
|
||||||
|
|
||||||
private static final long CONTENT_DURATION_US = 10 * C.MICROS_PER_SECOND;
|
private static final long CONTENT_DURATION_US = 10 * C.MICROS_PER_SECOND;
|
||||||
@ -95,8 +95,8 @@ public class ImaAdsLoaderTest {
|
|||||||
adDisplayContainer,
|
adDisplayContainer,
|
||||||
fakeAdsRequest,
|
fakeAdsRequest,
|
||||||
fakeAdsLoader);
|
fakeAdsLoader);
|
||||||
adViewGroup = new FrameLayout(RuntimeEnvironment.application);
|
adViewGroup = new FrameLayout(ApplicationProvider.getApplicationContext());
|
||||||
adOverlayView = new View(RuntimeEnvironment.application);
|
adOverlayView = new View(ApplicationProvider.getApplicationContext());
|
||||||
adViewProvider =
|
adViewProvider =
|
||||||
new AdsLoader.AdViewProvider() {
|
new AdsLoader.AdViewProvider() {
|
||||||
@Override
|
@Override
|
||||||
@ -237,7 +237,7 @@ public class ImaAdsLoaderTest {
|
|||||||
adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline, adDurationsUs);
|
adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline, adDurationsUs);
|
||||||
when(adsManager.getAdCuePoints()).thenReturn(Arrays.asList(cuePoints));
|
when(adsManager.getAdCuePoints()).thenReturn(Arrays.asList(cuePoints));
|
||||||
imaAdsLoader =
|
imaAdsLoader =
|
||||||
new ImaAdsLoader.Builder(RuntimeEnvironment.application)
|
new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
|
||||||
.setImaFactory(testImaFactory)
|
.setImaFactory(testImaFactory)
|
||||||
.setImaSdkSettings(imaSdkSettings)
|
.setImaSdkSettings(imaSdkSettings)
|
||||||
.buildForAdTag(TEST_URI);
|
.buildForAdTag(TEST_URI);
|
||||||
|
@ -1 +0,0 @@
|
|||||||
manifest=src/test/AndroidManifest.xml
|
|
@ -18,7 +18,6 @@ apply plugin: 'com.android.library'
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
@ -29,6 +28,8 @@ android {
|
|||||||
minSdkVersion project.ext.minSdkVersion
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions.unitTests.includeAndroidResources = true
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
@ -57,6 +57,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
*/
|
*/
|
||||||
public final class JobDispatcherScheduler implements Scheduler {
|
public final class JobDispatcherScheduler implements Scheduler {
|
||||||
|
|
||||||
|
private static final boolean DEBUG = false;
|
||||||
private static final String TAG = "JobDispatcherScheduler";
|
private static final String TAG = "JobDispatcherScheduler";
|
||||||
private static final String KEY_SERVICE_ACTION = "service_action";
|
private static final String KEY_SERVICE_ACTION = "service_action";
|
||||||
private static final String KEY_SERVICE_PACKAGE = "service_package";
|
private static final String KEY_SERVICE_PACKAGE = "service_package";
|
||||||
@ -78,8 +79,8 @@ public final class JobDispatcherScheduler implements Scheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean schedule(Requirements requirements, String serviceAction, String servicePackage) {
|
public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) {
|
||||||
Job job = buildJob(jobDispatcher, requirements, jobTag, serviceAction, servicePackage);
|
Job job = buildJob(jobDispatcher, requirements, jobTag, servicePackage, serviceAction);
|
||||||
int result = jobDispatcher.schedule(job);
|
int result = jobDispatcher.schedule(job);
|
||||||
logd("Scheduling job: " + jobTag + " result: " + result);
|
logd("Scheduling job: " + jobTag + " result: " + result);
|
||||||
return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS;
|
return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS;
|
||||||
@ -96,26 +97,18 @@ public final class JobDispatcherScheduler implements Scheduler {
|
|||||||
FirebaseJobDispatcher dispatcher,
|
FirebaseJobDispatcher dispatcher,
|
||||||
Requirements requirements,
|
Requirements requirements,
|
||||||
String tag,
|
String tag,
|
||||||
String serviceAction,
|
String servicePackage,
|
||||||
String servicePackage) {
|
String serviceAction) {
|
||||||
Job.Builder builder =
|
Job.Builder builder =
|
||||||
dispatcher
|
dispatcher
|
||||||
.newJobBuilder()
|
.newJobBuilder()
|
||||||
.setService(JobDispatcherSchedulerService.class) // the JobService that will be called
|
.setService(JobDispatcherSchedulerService.class) // the JobService that will be called
|
||||||
.setTag(tag);
|
.setTag(tag);
|
||||||
|
|
||||||
switch (requirements.getRequiredNetworkType()) {
|
if (requirements.isUnmeteredNetworkRequired()) {
|
||||||
case Requirements.NETWORK_TYPE_NONE:
|
builder.addConstraint(Constraint.ON_UNMETERED_NETWORK);
|
||||||
// do nothing.
|
} else if (requirements.isNetworkRequired()) {
|
||||||
break;
|
builder.addConstraint(Constraint.ON_ANY_NETWORK);
|
||||||
case Requirements.NETWORK_TYPE_ANY:
|
|
||||||
builder.addConstraint(Constraint.ON_ANY_NETWORK);
|
|
||||||
break;
|
|
||||||
case Requirements.NETWORK_TYPE_UNMETERED:
|
|
||||||
builder.addConstraint(Constraint.ON_UNMETERED_NETWORK);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requirements.isIdleRequired()) {
|
if (requirements.isIdleRequired()) {
|
||||||
@ -129,7 +122,7 @@ public final class JobDispatcherScheduler implements Scheduler {
|
|||||||
Bundle extras = new Bundle();
|
Bundle extras = new Bundle();
|
||||||
extras.putString(KEY_SERVICE_ACTION, serviceAction);
|
extras.putString(KEY_SERVICE_ACTION, serviceAction);
|
||||||
extras.putString(KEY_SERVICE_PACKAGE, servicePackage);
|
extras.putString(KEY_SERVICE_PACKAGE, servicePackage);
|
||||||
extras.putInt(KEY_REQUIREMENTS, requirements.getRequirementsData());
|
extras.putInt(KEY_REQUIREMENTS, requirements.getRequirements());
|
||||||
builder.setExtras(extras);
|
builder.setExtras(extras);
|
||||||
|
|
||||||
return builder.build();
|
return builder.build();
|
||||||
|
@ -28,4 +28,4 @@ locally. Instructions for doing this can be found in ExoPlayer's
|
|||||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.leanback.*`
|
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.leanback.*`
|
||||||
belong to this module.
|
belong to this module.
|
||||||
|
|
||||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
|
||||||
|
@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
@ -27,11 +26,14 @@ android {
|
|||||||
minSdkVersion 17
|
minSdkVersion 17
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions.unitTests.includeAndroidResources = true
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation('com.android.support:leanback-v17:' + supportLibraryVersion)
|
implementation 'androidx.annotation:annotation:1.0.2'
|
||||||
|
implementation 'androidx.leanback:leanback:1.0.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
@ -17,11 +17,11 @@ package com.google.android.exoplayer2.ext.leanback;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import android.support.v17.leanback.R;
|
import androidx.leanback.R;
|
||||||
import android.support.v17.leanback.media.PlaybackGlueHost;
|
import androidx.leanback.media.PlaybackGlueHost;
|
||||||
import android.support.v17.leanback.media.PlayerAdapter;
|
import androidx.leanback.media.PlayerAdapter;
|
||||||
import android.support.v17.leanback.media.SurfaceHolderGlueHost;
|
import androidx.leanback.media.SurfaceHolderGlueHost;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
|
@ -29,4 +29,4 @@ locally. Instructions for doing this can be found in ExoPlayer's
|
|||||||
* [Javadoc][]: Classes matching
|
* [Javadoc][]: Classes matching
|
||||||
`com.google.android.exoplayer2.ext.mediasession.*` belong to this module.
|
`com.google.android.exoplayer2.ext.mediasession.*` belong to this module.
|
||||||
|
|
||||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
|
||||||
|
@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
@ -27,11 +26,13 @@ android {
|
|||||||
minSdkVersion project.ext.minSdkVersion
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions.unitTests.includeAndroidResources = true
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
api 'com.android.support:support-media-compat:' + supportLibraryVersion
|
api 'androidx.media:media:1.0.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
@ -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.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.media.session.PlaybackStateCompat;
|
import android.support.v4.media.session.PlaybackStateCompat;
|
||||||
|
import com.google.android.exoplayer2.ControlDispatcher;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.util.RepeatModeUtil;
|
import com.google.android.exoplayer2.util.RepeatModeUtil;
|
||||||
|
|
||||||
/**
|
/** Provides a custom action for toggling repeat modes. */
|
||||||
* Provides a custom action for toggling repeat modes.
|
|
||||||
*/
|
|
||||||
public final class RepeatModeActionProvider implements MediaSessionConnector.CustomActionProvider {
|
public final class RepeatModeActionProvider implements MediaSessionConnector.CustomActionProvider {
|
||||||
|
|
||||||
|
/** The default repeat toggle modes. */
|
||||||
|
@RepeatModeUtil.RepeatToggleModes
|
||||||
|
public static final int DEFAULT_REPEAT_TOGGLE_MODES =
|
||||||
|
RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL;
|
||||||
|
|
||||||
private static final String ACTION_REPEAT_MODE = "ACTION_EXO_REPEAT_MODE";
|
private static final String ACTION_REPEAT_MODE = "ACTION_EXO_REPEAT_MODE";
|
||||||
|
|
||||||
private final Player player;
|
|
||||||
@RepeatModeUtil.RepeatToggleModes
|
@RepeatModeUtil.RepeatToggleModes
|
||||||
private final int repeatToggleModes;
|
private final int repeatToggleModes;
|
||||||
private final CharSequence repeatAllDescription;
|
private final CharSequence repeatAllDescription;
|
||||||
@ -37,27 +40,23 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance.
|
* Creates a new instance.
|
||||||
* <p>
|
*
|
||||||
* Equivalent to {@code RepeatModeActionProvider(context, player,
|
* <p>Equivalent to {@code RepeatModeActionProvider(context, DEFAULT_REPEAT_TOGGLE_MODES)}.
|
||||||
* MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES)}.
|
|
||||||
*
|
*
|
||||||
* @param context The context.
|
* @param context The context.
|
||||||
* @param player The player on which to toggle the repeat mode.
|
|
||||||
*/
|
*/
|
||||||
public RepeatModeActionProvider(Context context, Player player) {
|
public RepeatModeActionProvider(Context context) {
|
||||||
this(context, player, MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES);
|
this(context, DEFAULT_REPEAT_TOGGLE_MODES);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance enabling the given repeat toggle modes.
|
* Creates a new instance enabling the given repeat toggle modes.
|
||||||
*
|
*
|
||||||
* @param context The context.
|
* @param context The context.
|
||||||
* @param player The player on which to toggle the repeat mode.
|
|
||||||
* @param repeatToggleModes The toggle modes to enable.
|
* @param repeatToggleModes The toggle modes to enable.
|
||||||
*/
|
*/
|
||||||
public RepeatModeActionProvider(Context context, Player player,
|
public RepeatModeActionProvider(
|
||||||
@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
|
Context context, @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
|
||||||
this.player = player;
|
|
||||||
this.repeatToggleModes = repeatToggleModes;
|
this.repeatToggleModes = repeatToggleModes;
|
||||||
repeatAllDescription = context.getString(R.string.exo_media_action_repeat_all_description);
|
repeatAllDescription = context.getString(R.string.exo_media_action_repeat_all_description);
|
||||||
repeatOneDescription = context.getString(R.string.exo_media_action_repeat_one_description);
|
repeatOneDescription = context.getString(R.string.exo_media_action_repeat_one_description);
|
||||||
@ -65,16 +64,17 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCustomAction(String action, Bundle extras) {
|
public void onCustomAction(
|
||||||
|
Player player, ControlDispatcher controlDispatcher, String action, Bundle extras) {
|
||||||
int mode = player.getRepeatMode();
|
int mode = player.getRepeatMode();
|
||||||
int proposedMode = RepeatModeUtil.getNextRepeatMode(mode, repeatToggleModes);
|
int proposedMode = RepeatModeUtil.getNextRepeatMode(mode, repeatToggleModes);
|
||||||
if (mode != proposedMode) {
|
if (mode != proposedMode) {
|
||||||
player.setRepeatMode(proposedMode);
|
controlDispatcher.dispatchSetRepeatMode(player, proposedMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PlaybackStateCompat.CustomAction getCustomAction() {
|
public PlaybackStateCompat.CustomAction getCustomAction(Player player) {
|
||||||
CharSequence actionLabel;
|
CharSequence actionLabel;
|
||||||
int iconResourceId;
|
int iconResourceId;
|
||||||
switch (player.getRepeatMode()) {
|
switch (player.getRepeatMode()) {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user