Release media2 extension
PiperOrigin-RevId: 320351394
This commit is contained in:
parent
9a42141e66
commit
02f8cdf1d9
@ -231,6 +231,9 @@
|
|||||||
([#7357](https://github.com/google/ExoPlayer/issues/7357)).
|
([#7357](https://github.com/google/ExoPlayer/issues/7357)).
|
||||||
* Metadata: Add minimal DVB Application Information Table (AIT) support
|
* Metadata: Add minimal DVB Application Information Table (AIT) support
|
||||||
([#6922](https://github.com/google/ExoPlayer/pull/6922)).
|
([#6922](https://github.com/google/ExoPlayer/pull/6922)).
|
||||||
|
* Media2 extension: Publish media2 extension for integrating ExoPlayer with
|
||||||
|
`androidx.media2.common.SessionPlayer` and
|
||||||
|
`androidx.media2.session.MediaSession`.
|
||||||
* Cast extension: Implement playlist API and deprecate the old queue
|
* Cast extension: Implement playlist API and deprecate the old queue
|
||||||
manipulation API.
|
manipulation API.
|
||||||
* Demo app: Retain previous position in list of samples.
|
* Demo app: Retain previous position in list of samples.
|
||||||
|
@ -39,6 +39,7 @@ include modulePrefix + 'extension-ima'
|
|||||||
include modulePrefix + 'extension-cast'
|
include modulePrefix + 'extension-cast'
|
||||||
include modulePrefix + 'extension-cronet'
|
include modulePrefix + 'extension-cronet'
|
||||||
include modulePrefix + 'extension-mediasession'
|
include modulePrefix + 'extension-mediasession'
|
||||||
|
include modulePrefix + 'extension-media2'
|
||||||
include modulePrefix + 'extension-okhttp'
|
include modulePrefix + 'extension-okhttp'
|
||||||
include modulePrefix + 'extension-opus'
|
include modulePrefix + 'extension-opus'
|
||||||
include modulePrefix + 'extension-vp9'
|
include modulePrefix + 'extension-vp9'
|
||||||
@ -65,6 +66,7 @@ project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensio
|
|||||||
project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast')
|
project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast')
|
||||||
project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet')
|
project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet')
|
||||||
project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession')
|
project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession')
|
||||||
|
project(modulePrefix + 'extension-media2').projectDir = new File(rootDir, 'extensions/media2')
|
||||||
project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp')
|
project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp')
|
||||||
project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus')
|
project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus')
|
||||||
project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')
|
project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')
|
||||||
|
53
extensions/media2/README.md
Normal file
53
extensions/media2/README.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# ExoPlayer Media2 extension #
|
||||||
|
|
||||||
|
The Media2 extension provides builders for [SessionPlayer][] and [MediaSession.SessionCallback][] in
|
||||||
|
the [Media2 library][].
|
||||||
|
|
||||||
|
Compared to [MediaSessionConnector][] that uses [MediaSessionCompat][], this provides finer grained
|
||||||
|
control for incoming calls, so you can selectively allow/reject commands per controller.
|
||||||
|
|
||||||
|
## Getting the extension ##
|
||||||
|
|
||||||
|
The easiest way to use the extension is to add it as a gradle dependency:
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
implementation 'com.google.android.exoplayer:extension-media2:2.X.X'
|
||||||
|
```
|
||||||
|
|
||||||
|
where `2.X.X` is the version, which must match the version of the ExoPlayer
|
||||||
|
library being used.
|
||||||
|
|
||||||
|
Alternatively, you can clone the ExoPlayer repository and depend on the module
|
||||||
|
locally. Instructions for doing this can be found in ExoPlayer's
|
||||||
|
[top level README][].
|
||||||
|
|
||||||
|
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||||
|
|
||||||
|
## Using the extension ##
|
||||||
|
|
||||||
|
### Using `SessionPlayerConnector` ###
|
||||||
|
|
||||||
|
`SessionPlayerConnector` is a [SessionPlayer][] implementation wrapping a given `Player`.
|
||||||
|
You can use a [SessionPlayer][] instance to build a [MediaSession][], or to set the player
|
||||||
|
associated with a [VideoView][] or [MediaControlView][]
|
||||||
|
|
||||||
|
### Using `SessionCallbackBuilder` ###
|
||||||
|
|
||||||
|
`SessionCallbackBuilder` lets you build a [MediaSession.SessionCallback][] instance given its
|
||||||
|
collaborators. You can use a [MediaSession.SessionCallback][] to build a [MediaSession][].
|
||||||
|
|
||||||
|
## Links ##
|
||||||
|
|
||||||
|
* [Javadoc][]: Classes matching
|
||||||
|
`com.google.android.exoplayer2.ext.media2.*` belong to this module.
|
||||||
|
|
||||||
|
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
|
||||||
|
|
||||||
|
[SessionPlayer]: https://developer.android.com/reference/androidx/media2/common/SessionPlayer
|
||||||
|
[MediaSession]: https://developer.android.com/reference/androidx/media2/session/MediaSession
|
||||||
|
[MediaSession.SessionCallback]: https://developer.android.com/reference/androidx/media2/session/MediaSession.SessionCallback
|
||||||
|
[Media2 library]: https://developer.android.com/jetpack/androidx/releases/media2
|
||||||
|
[MediaSessionCompat]: https://developer.android.com/reference/android/support/v4/media/session/MediaSessionCompat
|
||||||
|
[MediaSessionConnector]: https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.html
|
||||||
|
[VideoView]: https://developer.android.com/reference/androidx/media2/widget/VideoView
|
||||||
|
[MediaControlView]: https://developer.android.com/reference/androidx/media2/widget/MediaControlView
|
43
extensions/media2/build.gradle
Normal file
43
extensions/media2/build.gradle
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// Copyright 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.
|
||||||
|
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
||||||
|
|
||||||
|
android.defaultConfig.minSdkVersion 19
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(modulePrefix + 'library-core')
|
||||||
|
implementation 'androidx.collection:collection:' + androidxCollectionVersion
|
||||||
|
implementation 'androidx.concurrent:concurrent-futures:1.0.0'
|
||||||
|
implementation 'com.google.guava:guava:' + guavaVersion
|
||||||
|
api 'androidx.media2:media2-session:1.0.3'
|
||||||
|
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
||||||
|
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
|
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||||
|
androidTestImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion
|
||||||
|
androidTestImplementation 'androidx.test:core:' + androidxTestCoreVersion
|
||||||
|
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
|
||||||
|
androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion
|
||||||
|
androidTestImplementation 'com.google.truth:truth:' + truthVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
ext {
|
||||||
|
javadocTitle = 'Media2 extension'
|
||||||
|
}
|
||||||
|
apply from: '../../javadoc_library.gradle'
|
||||||
|
|
||||||
|
ext {
|
||||||
|
releaseArtifact = 'extension-media2'
|
||||||
|
releaseDescription = 'Media2 extension for ExoPlayer.'
|
||||||
|
}
|
||||||
|
apply from: '../../publish.gradle'
|
28
extensions/media2/proguard-rules.txt
Normal file
28
extensions/media2/proguard-rules.txt
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Proguard rules specific to the media2 extension.
|
||||||
|
|
||||||
|
# Constructors and methods accessed via reflection in ExoPlayerUtils.
|
||||||
|
-dontnote com.google.android.exoplayer2.source.dash.DashMediaSource$Factory
|
||||||
|
-keepclasseswithmembers class com.google.android.exoplayer2.source.dash.DashMediaSource$Factory {
|
||||||
|
public <init>(com.google.android.exoplayer2.upstream.DataSource$Factory);
|
||||||
|
public com.google.android.exoplayer2.source.dash.DashMediaSource$Factory setTag(java.lang.Object);
|
||||||
|
}
|
||||||
|
-dontnote com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory
|
||||||
|
-keepclasseswithmembers class com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory {
|
||||||
|
public <init>(com.google.android.exoplayer2.upstream.DataSource$Factory);
|
||||||
|
public com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory setTag(java.lang.Object);
|
||||||
|
}
|
||||||
|
-dontnote com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory
|
||||||
|
-keepclasseswithmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory {
|
||||||
|
public <init>(com.google.android.exoplayer2.upstream.DataSource$Factory);
|
||||||
|
public com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory setTag(java.lang.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Don't warn about checkerframework and Kotlin annotations
|
||||||
|
-dontwarn org.checkerframework.**
|
||||||
|
-dontwarn kotlin.annotations.jvm.**
|
||||||
|
-dontwarn javax.annotation.**
|
||||||
|
|
||||||
|
# Work around [internal: b/151134701]: keep non-public versionedparcelable
|
||||||
|
# classes.
|
||||||
|
-keep class * implements androidx.versionedparcelable.VersionedParcelable
|
||||||
|
-keep class androidx.media2.common.MediaParcelUtils$MediaItemParcelImpl
|
38
extensions/media2/src/androidTest/AndroidManifest.xml
Normal file
38
extensions/media2/src/androidTest/AndroidManifest.xml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright (C) 2016 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 xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="com.google.android.exoplayer2.ext.media2.test">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-sdk/>
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="false"
|
||||||
|
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
|
||||||
|
<activity android:name="com.google.android.exoplayer2.ext.media2.MediaStubActivity"
|
||||||
|
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="MediaStubActivity"/>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
<instrumentation
|
||||||
|
android:targetPackage="com.google.android.exoplayer2.ext.media2.test"
|
||||||
|
android:name="androidx.test.runner.AndroidJUnitRunner"/>
|
||||||
|
|
||||||
|
</manifest>
|
@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 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.media2;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.support.v4.media.session.MediaControllerCompat;
|
||||||
|
import android.support.v4.media.session.MediaSessionCompat;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.media2.common.SessionPlayer;
|
||||||
|
import androidx.media2.common.SessionPlayer.PlayerResult;
|
||||||
|
import androidx.media2.session.MediaSession;
|
||||||
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry;
|
||||||
|
import com.google.android.exoplayer2.ext.media2.test.R;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** Unit test for {@link MediaSessionUtil} */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class MediaSessionUtilTest {
|
||||||
|
private static final int PLAYER_STATE_CHANGE_WAIT_TIME_MS = 5_000;
|
||||||
|
|
||||||
|
@Rule public final PlayerTestRule playerTestRule = new PlayerTestRule();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getSessionCompatToken_withMediaControllerCompat_returnsValidToken() throws Exception {
|
||||||
|
// Workaround to instantiate MediaSession with public androidx.media dependency.
|
||||||
|
// TODO(b/146536708): Remove this workaround when the relevant change is released via
|
||||||
|
// androidx.media 1.2.0.
|
||||||
|
if (Looper.myLooper() == null) {
|
||||||
|
Looper.prepare();
|
||||||
|
}
|
||||||
|
|
||||||
|
Context context = ApplicationProvider.getApplicationContext();
|
||||||
|
|
||||||
|
SessionPlayerConnector sessionPlayerConnector = playerTestRule.getSessionPlayerConnector();
|
||||||
|
MediaSession.SessionCallback sessionCallback =
|
||||||
|
new SessionCallbackBuilder(context, sessionPlayerConnector).build();
|
||||||
|
TestUtils.loadResource(context, R.raw.testmp3_2, sessionPlayerConnector);
|
||||||
|
ListenableFuture<PlayerResult> prepareResult = sessionPlayerConnector.prepare();
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
sessionPlayerConnector.registerPlayerCallback(
|
||||||
|
playerTestRule.getExecutor(),
|
||||||
|
new SessionPlayer.PlayerCallback() {
|
||||||
|
@Override
|
||||||
|
public void onPlayerStateChanged(@NonNull SessionPlayer player, int playerState) {
|
||||||
|
if (playerState == SessionPlayer.PLAYER_STATE_PLAYING) {
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
MediaSession session2 =
|
||||||
|
new MediaSession.Builder(context, sessionPlayerConnector)
|
||||||
|
.setSessionCallback(playerTestRule.getExecutor(), sessionCallback)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
InstrumentationRegistry.getInstrumentation()
|
||||||
|
.runOnMainSync(
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
MediaSessionCompat.Token token =
|
||||||
|
Assertions.checkNotNull(MediaSessionUtil.getSessionCompatToken(session2));
|
||||||
|
MediaControllerCompat controllerCompat = new MediaControllerCompat(context, token);
|
||||||
|
controllerCompat.getTransportControls().play();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assertThat(
|
||||||
|
prepareResult
|
||||||
|
.get(PLAYER_STATE_CHANGE_WAIT_TIME_MS, TimeUnit.MILLISECONDS)
|
||||||
|
.getResultCode())
|
||||||
|
.isEqualTo(PlayerResult.RESULT_SUCCESS);
|
||||||
|
assertThat(latch.await(PLAYER_STATE_CHANGE_WAIT_TIME_MS, TimeUnit.MILLISECONDS)).isTrue();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.media2;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.KeyguardManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.SurfaceHolder;
|
||||||
|
import android.view.SurfaceView;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import com.google.android.exoplayer2.ext.media2.test.R;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
|
/** Stub activity to play media contents on. */
|
||||||
|
public class MediaStubActivity extends Activity {
|
||||||
|
private static final String TAG = "MediaStubActivity";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.mediaplayer);
|
||||||
|
|
||||||
|
// disable enter animation.
|
||||||
|
overridePendingTransition(0, 0);
|
||||||
|
|
||||||
|
if (Util.SDK_INT >= 27) {
|
||||||
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
setTurnScreenOn(true);
|
||||||
|
setShowWhenLocked(true);
|
||||||
|
KeyguardManager keyguardManager =
|
||||||
|
(KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
|
||||||
|
keyguardManager.requestDismissKeyguard(this, null);
|
||||||
|
} else {
|
||||||
|
getWindow()
|
||||||
|
.addFlags(
|
||||||
|
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||||
|
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
|
||||||
|
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
|
||||||
|
| WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void finish() {
|
||||||
|
super.finish();
|
||||||
|
|
||||||
|
// disable exit animation.
|
||||||
|
overridePendingTransition(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
Log.i(TAG, "onResume");
|
||||||
|
super.onResume();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
Log.i(TAG, "onPause");
|
||||||
|
super.onPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SurfaceHolder getSurfaceHolder() {
|
||||||
|
SurfaceView surface = findViewById(R.id.surface);
|
||||||
|
return surface.getHolder();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.media2;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.media.AudioManager;
|
||||||
|
import android.os.Looper;
|
||||||
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry;
|
||||||
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
|
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import org.junit.rules.ExternalResource;
|
||||||
|
|
||||||
|
/** Rule for tests that use {@link SessionPlayerConnector}. */
|
||||||
|
public class PlayerTestRule extends ExternalResource {
|
||||||
|
private Context context;
|
||||||
|
private ExecutorService executor;
|
||||||
|
|
||||||
|
private SessionPlayerConnector sessionPlayerConnector;
|
||||||
|
private SimpleExoPlayer exoPlayer;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void before() {
|
||||||
|
context = ApplicationProvider.getApplicationContext();
|
||||||
|
executor = Executors.newFixedThreadPool(1);
|
||||||
|
|
||||||
|
InstrumentationRegistry.getInstrumentation()
|
||||||
|
.runOnMainSync(
|
||||||
|
() -> {
|
||||||
|
// Initialize AudioManager on the main thread to workaround b/78617702 that
|
||||||
|
// audio focus listener is called on the thread where the AudioManager was
|
||||||
|
// originally initialized.
|
||||||
|
// Without posting this, audio focus listeners wouldn't be called because the
|
||||||
|
// listeners would be posted to the test thread (here) where it waits until the
|
||||||
|
// tests are finished.
|
||||||
|
AudioManager audioManager =
|
||||||
|
(AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||||
|
|
||||||
|
exoPlayer = new SimpleExoPlayer.Builder(context).setLooper(Looper.myLooper()).build();
|
||||||
|
ConcatenatingMediaSource concatenatingMediaSource = new ConcatenatingMediaSource();
|
||||||
|
TimelinePlaylistManager manager =
|
||||||
|
new TimelinePlaylistManager(context, concatenatingMediaSource);
|
||||||
|
ConcatenatingMediaSourcePlaybackPreparer playbackPreparer =
|
||||||
|
new ConcatenatingMediaSourcePlaybackPreparer(exoPlayer, concatenatingMediaSource);
|
||||||
|
sessionPlayerConnector =
|
||||||
|
new SessionPlayerConnector(exoPlayer, manager, playbackPreparer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void after() {
|
||||||
|
if (sessionPlayerConnector != null) {
|
||||||
|
sessionPlayerConnector.close();
|
||||||
|
sessionPlayerConnector = null;
|
||||||
|
}
|
||||||
|
if (exoPlayer != null) {
|
||||||
|
InstrumentationRegistry.getInstrumentation()
|
||||||
|
.runOnMainSync(
|
||||||
|
() -> {
|
||||||
|
exoPlayer.release();
|
||||||
|
exoPlayer = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (executor != null) {
|
||||||
|
executor.shutdown();
|
||||||
|
executor = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExecutorService getExecutor() {
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SessionPlayerConnector getSessionPlayerConnector() {
|
||||||
|
return sessionPlayerConnector;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SimpleExoPlayer getSimpleExoPlayer() {
|
||||||
|
return exoPlayer;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,710 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.media2;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.ext.media2.TestUtils.assertPlayerResultSuccess;
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static com.google.common.truth.Truth.assertWithMessage;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.media2.common.CallbackMediaItem;
|
||||||
|
import androidx.media2.common.DataSourceCallback;
|
||||||
|
import androidx.media2.common.MediaItem;
|
||||||
|
import androidx.media2.common.Rating;
|
||||||
|
import androidx.media2.common.SessionPlayer;
|
||||||
|
import androidx.media2.common.UriMediaItem;
|
||||||
|
import androidx.media2.session.HeartRating;
|
||||||
|
import androidx.media2.session.MediaController;
|
||||||
|
import androidx.media2.session.MediaSession;
|
||||||
|
import androidx.media2.session.SessionCommand;
|
||||||
|
import androidx.media2.session.SessionCommandGroup;
|
||||||
|
import androidx.media2.session.SessionResult;
|
||||||
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import androidx.test.filters.LargeTest;
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry;
|
||||||
|
import androidx.test.rule.ActivityTestRule;
|
||||||
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
|
import com.google.android.exoplayer2.ext.media2.test.R;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** Tests {@link SessionCallbackBuilder}. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class SessionCallbackBuilderTest {
|
||||||
|
@Rule
|
||||||
|
public final ActivityTestRule<MediaStubActivity> activityRule =
|
||||||
|
new ActivityTestRule<>(MediaStubActivity.class);
|
||||||
|
|
||||||
|
@Rule public final PlayerTestRule playerTestRule = new PlayerTestRule();
|
||||||
|
|
||||||
|
private static final String MEDIA_SESSION_ID = SessionCallbackBuilderTest.class.getSimpleName();
|
||||||
|
private static final long CONTROLLER_COMMAND_WAIT_TIME_MS = 3_000;
|
||||||
|
private static final long PLAYER_STATE_CHANGE_OVER_SESSION_WAIT_TIME_MS = 10_000;
|
||||||
|
private static final long PLAYER_STATE_CHANGE_WAIT_TIME_MS = 5_000;
|
||||||
|
|
||||||
|
private Context context;
|
||||||
|
private Executor executor;
|
||||||
|
private SessionPlayerConnector sessionPlayerConnector;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
// Workaround to instantiate MediaSession with public androidx.media dependency.
|
||||||
|
// TODO(jaewan): Remove this workaround when androidx.media 1.2.0 is released.
|
||||||
|
if (Looper.myLooper() == null) {
|
||||||
|
Looper.prepare();
|
||||||
|
}
|
||||||
|
context = ApplicationProvider.getApplicationContext();
|
||||||
|
executor = playerTestRule.getExecutor();
|
||||||
|
sessionPlayerConnector = playerTestRule.getSessionPlayerConnector();
|
||||||
|
|
||||||
|
// Sets the surface to the player for manual check.
|
||||||
|
InstrumentationRegistry.getInstrumentation()
|
||||||
|
.runOnMainSync(
|
||||||
|
() -> {
|
||||||
|
SimpleExoPlayer exoPlayer = playerTestRule.getSimpleExoPlayer();
|
||||||
|
exoPlayer
|
||||||
|
.getVideoComponent()
|
||||||
|
.setVideoSurfaceHolder(activityRule.getActivity().getSurfaceHolder());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void constructor() throws Exception {
|
||||||
|
try (MediaSession session =
|
||||||
|
createMediaSession(
|
||||||
|
sessionPlayerConnector,
|
||||||
|
new SessionCallbackBuilder(context, sessionPlayerConnector).build())) {
|
||||||
|
assertPlayerResultSuccess(
|
||||||
|
sessionPlayerConnector.setMediaItem(TestUtils.createMediaItem(context)));
|
||||||
|
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
|
||||||
|
|
||||||
|
OnConnectedListener listener =
|
||||||
|
(controller, allowedCommands) -> {
|
||||||
|
List<Integer> disallowedCommandCodes =
|
||||||
|
Arrays.asList(
|
||||||
|
SessionCommand.COMMAND_CODE_SESSION_SET_RATING, // no rating callback
|
||||||
|
SessionCommand.COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM, // no media item provider
|
||||||
|
SessionCommand
|
||||||
|
.COMMAND_CODE_PLAYER_REPLACE_PLAYLIST_ITEM, // no media item provider
|
||||||
|
SessionCommand.COMMAND_CODE_PLAYER_SET_MEDIA_ITEM, // no media item provider
|
||||||
|
SessionCommand.COMMAND_CODE_PLAYER_SET_PLAYLIST, // no media item provider
|
||||||
|
SessionCommand.COMMAND_CODE_SESSION_REWIND, // no current media item
|
||||||
|
SessionCommand.COMMAND_CODE_SESSION_FAST_FORWARD // no current media item
|
||||||
|
);
|
||||||
|
assertDisallowedCommands(disallowedCommandCodes, allowedCommands);
|
||||||
|
};
|
||||||
|
try (MediaController controller = createConnectedController(session, listener, null)) {
|
||||||
|
assertThat(controller.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_PAUSED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allowedCommand_withoutPlaylist_disallowsSkipTo() throws Exception {
|
||||||
|
int testRewindIncrementMs = 100;
|
||||||
|
int testFastForwardIncrementMs = 100;
|
||||||
|
|
||||||
|
try (MediaSession session =
|
||||||
|
createMediaSession(
|
||||||
|
sessionPlayerConnector,
|
||||||
|
new SessionCallbackBuilder(context, sessionPlayerConnector)
|
||||||
|
.setRatingCallback(
|
||||||
|
(mediaSession, controller, mediaId, rating) ->
|
||||||
|
SessionResult.RESULT_ERROR_BAD_VALUE)
|
||||||
|
.setRewindIncrementMs(testRewindIncrementMs)
|
||||||
|
.setFastForwardIncrementMs(testFastForwardIncrementMs)
|
||||||
|
.setMediaItemProvider(new SessionCallbackBuilder.DefaultMediaItemProvider())
|
||||||
|
.build())) {
|
||||||
|
assertPlayerResultSuccess(
|
||||||
|
sessionPlayerConnector.setMediaItem(TestUtils.createMediaItem(context)));
|
||||||
|
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
|
||||||
|
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
OnConnectedListener listener =
|
||||||
|
(controller, allowedCommands) -> {
|
||||||
|
List<Integer> disallowedCommandCodes =
|
||||||
|
Arrays.asList(
|
||||||
|
SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_PLAYLIST_ITEM,
|
||||||
|
SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM,
|
||||||
|
SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM);
|
||||||
|
assertDisallowedCommands(disallowedCommandCodes, allowedCommands);
|
||||||
|
latch.countDown();
|
||||||
|
};
|
||||||
|
try (MediaController controller = createConnectedController(session, listener, null)) {
|
||||||
|
assertThat(latch.await(CONTROLLER_COMMAND_WAIT_TIME_MS, TimeUnit.MILLISECONDS)).isTrue();
|
||||||
|
|
||||||
|
assertSessionResultFailure(controller.skipToNextPlaylistItem());
|
||||||
|
assertSessionResultFailure(controller.skipToPreviousPlaylistItem());
|
||||||
|
assertSessionResultFailure(controller.skipToPlaylistItem(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allowedCommand_whenPlaylistSet_allowsSkipTo() throws Exception {
|
||||||
|
List<MediaItem> testPlaylist = new ArrayList<>();
|
||||||
|
testPlaylist.add(TestUtils.createMediaItem(context, R.raw.testvideo));
|
||||||
|
testPlaylist.add(TestUtils.createMediaItem(context, R.raw.sample_not_seekable));
|
||||||
|
int testRewindIncrementMs = 100;
|
||||||
|
int testFastForwardIncrementMs = 100;
|
||||||
|
|
||||||
|
try (MediaSession session =
|
||||||
|
createMediaSession(
|
||||||
|
sessionPlayerConnector,
|
||||||
|
new SessionCallbackBuilder(context, sessionPlayerConnector)
|
||||||
|
.setRatingCallback(
|
||||||
|
(mediaSession, controller, mediaId, rating) ->
|
||||||
|
SessionResult.RESULT_ERROR_BAD_VALUE)
|
||||||
|
.setRewindIncrementMs(testRewindIncrementMs)
|
||||||
|
.setFastForwardIncrementMs(testFastForwardIncrementMs)
|
||||||
|
.setMediaItemProvider(new SessionCallbackBuilder.DefaultMediaItemProvider())
|
||||||
|
.build())) {
|
||||||
|
|
||||||
|
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(testPlaylist, null));
|
||||||
|
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
|
||||||
|
|
||||||
|
OnConnectedListener connectedListener =
|
||||||
|
(controller, allowedCommands) -> {
|
||||||
|
List<Integer> allowedCommandCodes =
|
||||||
|
Arrays.asList(
|
||||||
|
SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM,
|
||||||
|
SessionCommand.COMMAND_CODE_PLAYER_SEEK_TO,
|
||||||
|
SessionCommand.COMMAND_CODE_SESSION_REWIND,
|
||||||
|
SessionCommand.COMMAND_CODE_SESSION_FAST_FORWARD);
|
||||||
|
assertAllowedCommands(allowedCommandCodes, allowedCommands);
|
||||||
|
|
||||||
|
List<Integer> disallowedCommandCodes =
|
||||||
|
Arrays.asList(SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM);
|
||||||
|
assertDisallowedCommands(disallowedCommandCodes, allowedCommands);
|
||||||
|
};
|
||||||
|
|
||||||
|
CountDownLatch allowedCommandChangedLatch = new CountDownLatch(1);
|
||||||
|
OnAllowedCommandsChangedListener allowedCommandChangedListener =
|
||||||
|
(controller, allowedCommands) -> {
|
||||||
|
List<Integer> allowedCommandCodes =
|
||||||
|
Arrays.asList(SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM);
|
||||||
|
assertAllowedCommands(allowedCommandCodes, allowedCommands);
|
||||||
|
|
||||||
|
List<Integer> disallowedCommandCodes =
|
||||||
|
Arrays.asList(
|
||||||
|
SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM,
|
||||||
|
SessionCommand.COMMAND_CODE_PLAYER_SEEK_TO,
|
||||||
|
SessionCommand.COMMAND_CODE_SESSION_REWIND,
|
||||||
|
SessionCommand.COMMAND_CODE_SESSION_FAST_FORWARD);
|
||||||
|
assertDisallowedCommands(disallowedCommandCodes, allowedCommands);
|
||||||
|
allowedCommandChangedLatch.countDown();
|
||||||
|
};
|
||||||
|
try (MediaController controller =
|
||||||
|
createConnectedController(session, connectedListener, allowedCommandChangedListener)) {
|
||||||
|
assertPlayerResultSuccess(sessionPlayerConnector.skipToNextPlaylistItem());
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
allowedCommandChangedLatch.await(
|
||||||
|
CONTROLLER_COMMAND_WAIT_TIME_MS, TimeUnit.MILLISECONDS))
|
||||||
|
.isTrue();
|
||||||
|
|
||||||
|
// Also test whether the rewind fails as expected.
|
||||||
|
assertSessionResultFailure(controller.rewind());
|
||||||
|
assertThat(sessionPlayerConnector.getCurrentPosition()).isEqualTo(0);
|
||||||
|
assertThat(controller.getCurrentPosition()).isEqualTo(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allowedCommand_afterCurrentMediaItemPrepared_notifiesSeekToAvailable()
|
||||||
|
throws Exception {
|
||||||
|
List<MediaItem> testPlaylist = new ArrayList<>();
|
||||||
|
testPlaylist.add(TestUtils.createMediaItem(context, R.raw.testvideo));
|
||||||
|
|
||||||
|
int resid = R.raw.video_480x360_mp4_h264_1350kbps_30fps_aac_stereo_192kbps_44100hz;
|
||||||
|
TestDataSourceCallback source =
|
||||||
|
TestDataSourceCallback.fromAssetFd(context.getResources().openRawResourceFd(resid));
|
||||||
|
CountDownLatch readAllowedLatch = new CountDownLatch(1);
|
||||||
|
DataSourceCallback dataSource =
|
||||||
|
new DataSourceCallback() {
|
||||||
|
@Override
|
||||||
|
public int readAt(long position, byte[] buffer, int offset, int size) {
|
||||||
|
try {
|
||||||
|
assertThat(
|
||||||
|
readAllowedLatch.await(
|
||||||
|
PLAYER_STATE_CHANGE_WAIT_TIME_MS, TimeUnit.MILLISECONDS))
|
||||||
|
.isTrue();
|
||||||
|
} catch (Exception e) {
|
||||||
|
assertWithMessage("Unexpected exception %s", e).fail();
|
||||||
|
}
|
||||||
|
return source.readAt(position, buffer, offset, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getSize() {
|
||||||
|
return source.getSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
source.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
testPlaylist.add(new CallbackMediaItem.Builder(dataSource).build());
|
||||||
|
|
||||||
|
try (MediaSession session =
|
||||||
|
createMediaSession(
|
||||||
|
sessionPlayerConnector,
|
||||||
|
new SessionCallbackBuilder(context, sessionPlayerConnector).build())) {
|
||||||
|
|
||||||
|
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(testPlaylist, null));
|
||||||
|
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
|
||||||
|
|
||||||
|
CountDownLatch seekToAllowedForSecondMediaItem = new CountDownLatch(1);
|
||||||
|
OnAllowedCommandsChangedListener allowedCommandsChangedListener =
|
||||||
|
(controller, allowedCommands) -> {
|
||||||
|
if (allowedCommands.hasCommand(SessionCommand.COMMAND_CODE_PLAYER_SEEK_TO)
|
||||||
|
&& controller.getCurrentMediaItemIndex() == 1) {
|
||||||
|
seekToAllowedForSecondMediaItem.countDown();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
try (MediaController controller =
|
||||||
|
createConnectedController(
|
||||||
|
session, /* onConnectedListener= */ null, allowedCommandsChangedListener)) {
|
||||||
|
assertPlayerResultSuccess(sessionPlayerConnector.skipToNextPlaylistItem());
|
||||||
|
|
||||||
|
readAllowedLatch.countDown();
|
||||||
|
assertThat(
|
||||||
|
seekToAllowedForSecondMediaItem.await(
|
||||||
|
CONTROLLER_COMMAND_WAIT_TIME_MS, TimeUnit.MILLISECONDS))
|
||||||
|
.isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setRatingCallback_withRatingCallback_receivesRatingCallback() throws Exception {
|
||||||
|
String testMediaId = "testRating";
|
||||||
|
Rating testRating = new HeartRating(true);
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
SessionCallbackBuilder.RatingCallback ratingCallback =
|
||||||
|
(session, controller, mediaId, rating) -> {
|
||||||
|
assertThat(mediaId).isEqualTo(testMediaId);
|
||||||
|
assertThat(rating).isEqualTo(testRating);
|
||||||
|
latch.countDown();
|
||||||
|
return SessionResult.RESULT_SUCCESS;
|
||||||
|
};
|
||||||
|
|
||||||
|
try (MediaSession session =
|
||||||
|
createMediaSession(
|
||||||
|
sessionPlayerConnector,
|
||||||
|
new SessionCallbackBuilder(context, sessionPlayerConnector)
|
||||||
|
.setRatingCallback(ratingCallback)
|
||||||
|
.build())) {
|
||||||
|
try (MediaController controller = createConnectedController(session)) {
|
||||||
|
assertSessionResultSuccess(
|
||||||
|
controller.setRating(testMediaId, testRating), CONTROLLER_COMMAND_WAIT_TIME_MS);
|
||||||
|
assertThat(latch.await(0, TimeUnit.MILLISECONDS)).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setCustomCommandProvider_withCustomCommandProvider_receivesCustomCommand()
|
||||||
|
throws Exception {
|
||||||
|
SessionCommand testCommand = new SessionCommand("exo.ext.media2.COMMAND", null);
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
SessionCallbackBuilder.CustomCommandProvider provider =
|
||||||
|
new SessionCallbackBuilder.CustomCommandProvider() {
|
||||||
|
@Override
|
||||||
|
public SessionResult onCustomCommand(
|
||||||
|
MediaSession session,
|
||||||
|
MediaSession.ControllerInfo controllerInfo,
|
||||||
|
SessionCommand customCommand,
|
||||||
|
@Nullable Bundle args) {
|
||||||
|
assertThat(customCommand.getCustomAction()).isEqualTo(testCommand.getCustomAction());
|
||||||
|
assertThat(args).isNull();
|
||||||
|
latch.countDown();
|
||||||
|
return new SessionResult(SessionResult.RESULT_SUCCESS, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SessionCommandGroup getCustomCommands(
|
||||||
|
MediaSession session, MediaSession.ControllerInfo controllerInfo) {
|
||||||
|
return new SessionCommandGroup.Builder().addCommand(testCommand).build();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try (MediaSession session =
|
||||||
|
createMediaSession(
|
||||||
|
sessionPlayerConnector,
|
||||||
|
new SessionCallbackBuilder(context, sessionPlayerConnector)
|
||||||
|
.setCustomCommandProvider(provider)
|
||||||
|
.build())) {
|
||||||
|
OnAllowedCommandsChangedListener listener =
|
||||||
|
(controller, allowedCommands) -> {
|
||||||
|
boolean foundCustomCommand = false;
|
||||||
|
for (SessionCommand command : allowedCommands.getCommands()) {
|
||||||
|
if (TextUtils.equals(testCommand.getCustomAction(), command.getCustomAction())) {
|
||||||
|
foundCustomCommand = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(foundCustomCommand).isTrue();
|
||||||
|
};
|
||||||
|
try (MediaController controller = createConnectedController(session, null, listener)) {
|
||||||
|
assertSessionResultSuccess(
|
||||||
|
controller.sendCustomCommand(testCommand, null), CONTROLLER_COMMAND_WAIT_TIME_MS);
|
||||||
|
assertThat(latch.await(0, TimeUnit.MILLISECONDS)).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@LargeTest
|
||||||
|
@Test
|
||||||
|
public void setRewindIncrementMs_withPositiveRewindIncrement_rewinds() throws Exception {
|
||||||
|
int testResId = R.raw.video_480x360_mp4_h264_1350kbps_30fps_aac_stereo_192kbps_44100hz;
|
||||||
|
int testDuration = 10_000;
|
||||||
|
int tolerance = 100;
|
||||||
|
int testSeekPosition = 2_000;
|
||||||
|
int testRewindIncrementMs = 500;
|
||||||
|
|
||||||
|
TestUtils.loadResource(context, testResId, sessionPlayerConnector);
|
||||||
|
|
||||||
|
// seekTo() sometimes takes couple of seconds. Disable default timeout behavior.
|
||||||
|
try (MediaSession session =
|
||||||
|
createMediaSession(
|
||||||
|
sessionPlayerConnector,
|
||||||
|
new SessionCallbackBuilder(context, sessionPlayerConnector)
|
||||||
|
.setRewindIncrementMs(testRewindIncrementMs)
|
||||||
|
.setSeekTimeoutMs(0)
|
||||||
|
.build())) {
|
||||||
|
try (MediaController controller = createConnectedController(session)) {
|
||||||
|
// Prepare first to ensure that seek() works.
|
||||||
|
assertSessionResultSuccess(
|
||||||
|
controller.prepare(), PLAYER_STATE_CHANGE_OVER_SESSION_WAIT_TIME_MS);
|
||||||
|
|
||||||
|
assertThat((float) sessionPlayerConnector.getDuration())
|
||||||
|
.isWithin(tolerance)
|
||||||
|
.of(testDuration);
|
||||||
|
assertSessionResultSuccess(
|
||||||
|
controller.seekTo(testSeekPosition), PLAYER_STATE_CHANGE_OVER_SESSION_WAIT_TIME_MS);
|
||||||
|
assertThat((float) sessionPlayerConnector.getCurrentPosition())
|
||||||
|
.isWithin(tolerance)
|
||||||
|
.of(testSeekPosition);
|
||||||
|
|
||||||
|
// Test rewind
|
||||||
|
assertSessionResultSuccess(
|
||||||
|
controller.rewind(), PLAYER_STATE_CHANGE_OVER_SESSION_WAIT_TIME_MS);
|
||||||
|
assertThat((float) sessionPlayerConnector.getCurrentPosition())
|
||||||
|
.isWithin(tolerance)
|
||||||
|
.of(testSeekPosition - testRewindIncrementMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@LargeTest
|
||||||
|
@Test
|
||||||
|
public void setFastForwardIncrementMs_withPositiveFastForwardIncrement_fastsForward()
|
||||||
|
throws Exception {
|
||||||
|
int testResId = R.raw.video_480x360_mp4_h264_1350kbps_30fps_aac_stereo_192kbps_44100hz;
|
||||||
|
int testDuration = 10_000;
|
||||||
|
int tolerance = 100;
|
||||||
|
int testSeekPosition = 2_000;
|
||||||
|
int testFastForwardIncrementMs = 300;
|
||||||
|
|
||||||
|
TestUtils.loadResource(context, testResId, sessionPlayerConnector);
|
||||||
|
|
||||||
|
// seekTo() sometimes takes couple of seconds. Disable default timeout behavior.
|
||||||
|
try (MediaSession session =
|
||||||
|
createMediaSession(
|
||||||
|
sessionPlayerConnector,
|
||||||
|
new SessionCallbackBuilder(context, sessionPlayerConnector)
|
||||||
|
.setFastForwardIncrementMs(testFastForwardIncrementMs)
|
||||||
|
.setSeekTimeoutMs(0)
|
||||||
|
.build())) {
|
||||||
|
try (MediaController controller = createConnectedController(session)) {
|
||||||
|
// Prepare first to ensure that seek() works.
|
||||||
|
assertSessionResultSuccess(
|
||||||
|
controller.prepare(), PLAYER_STATE_CHANGE_OVER_SESSION_WAIT_TIME_MS);
|
||||||
|
|
||||||
|
assertThat((float) sessionPlayerConnector.getDuration())
|
||||||
|
.isWithin(tolerance)
|
||||||
|
.of(testDuration);
|
||||||
|
assertSessionResultSuccess(
|
||||||
|
controller.seekTo(testSeekPosition), PLAYER_STATE_CHANGE_OVER_SESSION_WAIT_TIME_MS);
|
||||||
|
assertThat((float) sessionPlayerConnector.getCurrentPosition())
|
||||||
|
.isWithin(tolerance)
|
||||||
|
.of(testSeekPosition);
|
||||||
|
|
||||||
|
// Test fast-forward
|
||||||
|
assertSessionResultSuccess(
|
||||||
|
controller.fastForward(), PLAYER_STATE_CHANGE_OVER_SESSION_WAIT_TIME_MS);
|
||||||
|
assertThat((float) sessionPlayerConnector.getCurrentPosition())
|
||||||
|
.isWithin(tolerance)
|
||||||
|
.of(testSeekPosition + testFastForwardIncrementMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setMediaItemProvider_withMediaItemProvider_receivesOnCreateMediaItem()
|
||||||
|
throws Exception {
|
||||||
|
int testResId = R.raw.testmp3_2;
|
||||||
|
Uri testMediaIdUri = TestUtils.createResourceUri(context, testResId);
|
||||||
|
|
||||||
|
CountDownLatch providerLatch = new CountDownLatch(1);
|
||||||
|
SessionCallbackBuilder.DefaultMediaItemProvider defaultMediaItemProvider =
|
||||||
|
new SessionCallbackBuilder.DefaultMediaItemProvider();
|
||||||
|
SessionCallbackBuilder.MediaItemProvider provider =
|
||||||
|
(session, controllerInfo, mediaId) -> {
|
||||||
|
assertThat(mediaId).isEqualTo(testMediaIdUri.toString());
|
||||||
|
providerLatch.countDown();
|
||||||
|
return defaultMediaItemProvider.onCreateMediaItem(session, controllerInfo, mediaId);
|
||||||
|
};
|
||||||
|
|
||||||
|
CountDownLatch currentMediaItemChangedLatch = new CountDownLatch(1);
|
||||||
|
sessionPlayerConnector.registerPlayerCallback(
|
||||||
|
executor,
|
||||||
|
new SessionPlayer.PlayerCallback() {
|
||||||
|
@Override
|
||||||
|
public void onCurrentMediaItemChanged(
|
||||||
|
@NonNull SessionPlayer player, @NonNull MediaItem item) {
|
||||||
|
assertThat(((UriMediaItem) item).getUri()).isEqualTo(testMediaIdUri);
|
||||||
|
currentMediaItemChangedLatch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try (MediaSession session =
|
||||||
|
createMediaSession(
|
||||||
|
sessionPlayerConnector,
|
||||||
|
new SessionCallbackBuilder(context, sessionPlayerConnector)
|
||||||
|
.setMediaItemProvider(provider)
|
||||||
|
.build())) {
|
||||||
|
try (MediaController controller = createConnectedController(session)) {
|
||||||
|
assertSessionResultSuccess(
|
||||||
|
controller.setMediaItem(testMediaIdUri.toString()),
|
||||||
|
PLAYER_STATE_CHANGE_OVER_SESSION_WAIT_TIME_MS);
|
||||||
|
assertThat(providerLatch.await(0, TimeUnit.MILLISECONDS)).isTrue();
|
||||||
|
assertThat(
|
||||||
|
currentMediaItemChangedLatch.await(
|
||||||
|
CONTROLLER_COMMAND_WAIT_TIME_MS, TimeUnit.MILLISECONDS))
|
||||||
|
.isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setSkipCallback_withSkipBackward_receivesOnSkipBackward() throws Exception {
|
||||||
|
CountDownLatch skipBackwardCalledLatch = new CountDownLatch(1);
|
||||||
|
SessionCallbackBuilder.SkipCallback skipCallback =
|
||||||
|
new SessionCallbackBuilder.SkipCallback() {
|
||||||
|
@Override
|
||||||
|
public int onSkipBackward(
|
||||||
|
MediaSession session, MediaSession.ControllerInfo controllerInfo) {
|
||||||
|
skipBackwardCalledLatch.countDown();
|
||||||
|
return SessionResult.RESULT_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onSkipForward(
|
||||||
|
MediaSession session, MediaSession.ControllerInfo controllerInfo) {
|
||||||
|
return SessionResult.RESULT_ERROR_NOT_SUPPORTED;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
try (MediaSession session =
|
||||||
|
createMediaSession(
|
||||||
|
sessionPlayerConnector,
|
||||||
|
new SessionCallbackBuilder(context, sessionPlayerConnector)
|
||||||
|
.setSkipCallback(skipCallback)
|
||||||
|
.build())) {
|
||||||
|
try (MediaController controller = createConnectedController(session)) {
|
||||||
|
assertSessionResultSuccess(controller.skipBackward(), CONTROLLER_COMMAND_WAIT_TIME_MS);
|
||||||
|
assertThat(skipBackwardCalledLatch.await(0, TimeUnit.MILLISECONDS)).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setSkipCallback_withSkipForward_receivesOnSkipForward() throws Exception {
|
||||||
|
CountDownLatch skipForwardCalledLatch = new CountDownLatch(1);
|
||||||
|
SessionCallbackBuilder.SkipCallback skipCallback =
|
||||||
|
new SessionCallbackBuilder.SkipCallback() {
|
||||||
|
@Override
|
||||||
|
public int onSkipBackward(
|
||||||
|
MediaSession session, MediaSession.ControllerInfo controllerInfo) {
|
||||||
|
return SessionResult.RESULT_ERROR_NOT_SUPPORTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onSkipForward(
|
||||||
|
MediaSession session, MediaSession.ControllerInfo controllerInfo) {
|
||||||
|
skipForwardCalledLatch.countDown();
|
||||||
|
return SessionResult.RESULT_SUCCESS;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
try (MediaSession session =
|
||||||
|
createMediaSession(
|
||||||
|
sessionPlayerConnector,
|
||||||
|
new SessionCallbackBuilder(context, sessionPlayerConnector)
|
||||||
|
.setSkipCallback(skipCallback)
|
||||||
|
.build())) {
|
||||||
|
try (MediaController controller = createConnectedController(session)) {
|
||||||
|
assertSessionResultSuccess(controller.skipForward(), CONTROLLER_COMMAND_WAIT_TIME_MS);
|
||||||
|
assertThat(skipForwardCalledLatch.await(0, TimeUnit.MILLISECONDS)).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setPostConnectCallback_afterConnect_receivesOnPostConnect() throws Exception {
|
||||||
|
CountDownLatch postConnectLatch = new CountDownLatch(1);
|
||||||
|
SessionCallbackBuilder.PostConnectCallback postConnectCallback =
|
||||||
|
(session, controllerInfo) -> postConnectLatch.countDown();
|
||||||
|
try (MediaSession session =
|
||||||
|
createMediaSession(
|
||||||
|
sessionPlayerConnector,
|
||||||
|
new SessionCallbackBuilder(context, sessionPlayerConnector)
|
||||||
|
.setPostConnectCallback(postConnectCallback)
|
||||||
|
.build())) {
|
||||||
|
try (MediaController controller = createConnectedController(session)) {
|
||||||
|
assertThat(postConnectLatch.await(CONTROLLER_COMMAND_WAIT_TIME_MS, TimeUnit.MILLISECONDS))
|
||||||
|
.isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setDisconnectedCallback_afterDisconnect_receivesOnDisconnected() throws Exception {
|
||||||
|
CountDownLatch disconnectedLatch = new CountDownLatch(1);
|
||||||
|
SessionCallbackBuilder.DisconnectedCallback disconnectCallback =
|
||||||
|
(session, controllerInfo) -> disconnectedLatch.countDown();
|
||||||
|
try (MediaSession session =
|
||||||
|
createMediaSession(
|
||||||
|
sessionPlayerConnector,
|
||||||
|
new SessionCallbackBuilder(context, sessionPlayerConnector)
|
||||||
|
.setDisconnectedCallback(disconnectCallback)
|
||||||
|
.build())) {
|
||||||
|
try (MediaController controller = createConnectedController(session)) {}
|
||||||
|
assertThat(disconnectedLatch.await(CONTROLLER_COMMAND_WAIT_TIME_MS, TimeUnit.MILLISECONDS))
|
||||||
|
.isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MediaSession createMediaSession(
|
||||||
|
SessionPlayer sessionPlayer, MediaSession.SessionCallback callback) {
|
||||||
|
return new MediaSession.Builder(context, sessionPlayer)
|
||||||
|
.setSessionCallback(executor, callback)
|
||||||
|
.setId(MEDIA_SESSION_ID)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private MediaController createConnectedController(MediaSession session) throws Exception {
|
||||||
|
return createConnectedController(session, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MediaController createConnectedController(
|
||||||
|
MediaSession session,
|
||||||
|
OnConnectedListener onConnectedListener,
|
||||||
|
OnAllowedCommandsChangedListener onAllowedCommandsChangedListener)
|
||||||
|
throws Exception {
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
MediaController.ControllerCallback callback =
|
||||||
|
new MediaController.ControllerCallback() {
|
||||||
|
@Override
|
||||||
|
public void onAllowedCommandsChanged(
|
||||||
|
@NonNull MediaController controller, @NonNull SessionCommandGroup commands) {
|
||||||
|
if (onAllowedCommandsChangedListener != null) {
|
||||||
|
onAllowedCommandsChangedListener.onAllowedCommandsChanged(controller, commands);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConnected(
|
||||||
|
@NonNull MediaController controller, @NonNull SessionCommandGroup allowedCommands) {
|
||||||
|
if (onConnectedListener != null) {
|
||||||
|
onConnectedListener.onConnected(controller, allowedCommands);
|
||||||
|
}
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
MediaController controller =
|
||||||
|
new MediaController.Builder(context)
|
||||||
|
.setSessionToken(session.getToken())
|
||||||
|
.setControllerCallback(ContextCompat.getMainExecutor(context), callback)
|
||||||
|
.build();
|
||||||
|
latch.await();
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertSessionResultSuccess(Future<SessionResult> future) throws Exception {
|
||||||
|
assertSessionResultSuccess(future, CONTROLLER_COMMAND_WAIT_TIME_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertSessionResultSuccess(Future<SessionResult> future, long timeoutMs)
|
||||||
|
throws Exception {
|
||||||
|
SessionResult result = future.get(timeoutMs, TimeUnit.MILLISECONDS);
|
||||||
|
assertThat(result.getResultCode()).isEqualTo(SessionResult.RESULT_SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertSessionResultFailure(Future<SessionResult> future) throws Exception {
|
||||||
|
SessionResult result =
|
||||||
|
future.get(PLAYER_STATE_CHANGE_OVER_SESSION_WAIT_TIME_MS, TimeUnit.MILLISECONDS);
|
||||||
|
assertThat(result.getResultCode()).isNotEqualTo(SessionResult.RESULT_SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertAllowedCommands(
|
||||||
|
List<Integer> expectedAllowedCommandsCode, SessionCommandGroup allowedCommands) {
|
||||||
|
for (int commandCode : expectedAllowedCommandsCode) {
|
||||||
|
assertWithMessage("Command should be allowed, code=" + commandCode)
|
||||||
|
.that(allowedCommands.hasCommand(commandCode))
|
||||||
|
.isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertDisallowedCommands(
|
||||||
|
List<Integer> expectedDisallowedCommandsCode, SessionCommandGroup allowedCommands) {
|
||||||
|
for (int commandCode : expectedDisallowedCommandsCode) {
|
||||||
|
assertWithMessage("Command shouldn't be allowed, code=" + commandCode)
|
||||||
|
.that(allowedCommands.hasCommand(commandCode))
|
||||||
|
.isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface OnAllowedCommandsChangedListener {
|
||||||
|
void onAllowedCommandsChanged(MediaController controller, SessionCommandGroup allowedCommands);
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface OnConnectedListener {
|
||||||
|
void onConnected(MediaController controller, SessionCommandGroup allowedCommands);
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.media2;
|
||||||
|
|
||||||
|
import android.content.res.AssetFileDescriptor;
|
||||||
|
import android.util.Log;
|
||||||
|
import androidx.media2.common.DataSourceCallback;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/** A DataSourceCallback that reads from a byte array for use in tests. */
|
||||||
|
public class TestDataSourceCallback extends DataSourceCallback {
|
||||||
|
private static final String TAG = "TestDataSourceCallback";
|
||||||
|
|
||||||
|
private byte[] data;
|
||||||
|
|
||||||
|
// Read an asset fd into a new byte array media item. Closes afd.
|
||||||
|
public static TestDataSourceCallback fromAssetFd(AssetFileDescriptor afd) throws IOException {
|
||||||
|
try {
|
||||||
|
InputStream in = afd.createInputStream();
|
||||||
|
int size = (int) afd.getDeclaredLength();
|
||||||
|
byte[] data = new byte[size];
|
||||||
|
int writeIndex = 0;
|
||||||
|
int numRead;
|
||||||
|
do {
|
||||||
|
numRead = in.read(data, writeIndex, size - writeIndex);
|
||||||
|
writeIndex += numRead;
|
||||||
|
} while (numRead >= 0);
|
||||||
|
return new TestDataSourceCallback(data);
|
||||||
|
} finally {
|
||||||
|
afd.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestDataSourceCallback(byte[] data) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized int readAt(long position, byte[] buffer, int offset, int size) {
|
||||||
|
// Clamp reads past the end of the source.
|
||||||
|
if (position >= data.length) {
|
||||||
|
return -1; // -1 indicates EOF
|
||||||
|
}
|
||||||
|
if (position + size > data.length) {
|
||||||
|
size -= (position + size) - data.length;
|
||||||
|
}
|
||||||
|
System.arraycopy(data, (int) position, buffer, offset, size);
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized long getSize() {
|
||||||
|
Log.v(TAG, "getSize: " + data.length);
|
||||||
|
return data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: it's fine to keep using this media item after closing it.
|
||||||
|
@Override
|
||||||
|
public synchronized void close() {
|
||||||
|
Log.v(TAG, "close()");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.media2;
|
||||||
|
|
||||||
|
import static androidx.media2.common.SessionPlayer.PlayerResult.RESULT_SUCCESS;
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.net.Uri;
|
||||||
|
import androidx.media2.common.MediaItem;
|
||||||
|
import androidx.media2.common.MediaMetadata;
|
||||||
|
import androidx.media2.common.SessionPlayer;
|
||||||
|
import androidx.media2.common.SessionPlayer.PlayerResult;
|
||||||
|
import androidx.media2.common.UriMediaItem;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/** Utilities for tests. */
|
||||||
|
public final class TestUtils {
|
||||||
|
private static final long PLAYER_STATE_CHANGE_WAIT_TIME_MS = 5_000;
|
||||||
|
|
||||||
|
public static Uri createResourceUri(Context context, int resId) {
|
||||||
|
Resources resources = context.getResources();
|
||||||
|
return new Uri.Builder()
|
||||||
|
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
|
||||||
|
.authority(resources.getResourcePackageName(resId))
|
||||||
|
.appendPath(resources.getResourceTypeName(resId))
|
||||||
|
.appendPath(resources.getResourceEntryName(resId))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MediaItem createMediaItem(Context context) {
|
||||||
|
return createMediaItem(context, com.google.android.exoplayer2.ext.media2.test.R.raw.testvideo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MediaItem createMediaItem(Context context, int resId) {
|
||||||
|
Uri testVideoUri = createResourceUri(context, resId);
|
||||||
|
String resourceName = context.getResources().getResourceName(resId);
|
||||||
|
MediaMetadata metadata =
|
||||||
|
new MediaMetadata.Builder()
|
||||||
|
.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, resourceName)
|
||||||
|
.build();
|
||||||
|
return new UriMediaItem.Builder(testVideoUri).setMetadata(metadata).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<MediaItem> createPlaylist(Context context, int size) {
|
||||||
|
List<MediaItem> items = new ArrayList<>();
|
||||||
|
for (int i = 0; i < size; ++i) {
|
||||||
|
items.add(createMediaItem(context));
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void loadResource(Context context, int resId, SessionPlayer sessionPlayer)
|
||||||
|
throws Exception {
|
||||||
|
Uri testUri = TestUtils.createResourceUri(context, resId);
|
||||||
|
MediaItem mediaItem = createMediaItem(context, resId);
|
||||||
|
assertPlayerResultSuccess(sessionPlayer.setMediaItem(mediaItem));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void assertPlayerResultSuccess(Future<PlayerResult> future) throws Exception {
|
||||||
|
assertPlayerResult(future, RESULT_SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void assertPlayerResult(
|
||||||
|
Future<PlayerResult> future, /* @PlayerResult.ResultCode */ int playerResult)
|
||||||
|
throws Exception {
|
||||||
|
assertThat(future).isNotNull();
|
||||||
|
PlayerResult result = future.get(PLAYER_STATE_CHANGE_WAIT_TIME_MS, TimeUnit.MILLISECONDS);
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.getResultCode()).isEqualTo(playerResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TestUtils() {
|
||||||
|
// Prevent from instantiation.
|
||||||
|
}
|
||||||
|
}
|
41
extensions/media2/src/androidTest/res/layout/mediaplayer.xml
Normal file
41
extensions/media2/src/androidTest/res/layout/mediaplayer.xml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 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.
|
||||||
|
-->
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:keepScreenOn="true"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<SurfaceView android:id="@+id/surface"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1">
|
||||||
|
</SurfaceView>
|
||||||
|
|
||||||
|
<SurfaceView android:id="@+id/surface2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1">
|
||||||
|
</SurfaceView>
|
||||||
|
|
||||||
|
<SurfaceView android:id="@+id/surface3"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1">
|
||||||
|
</SurfaceView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
BIN
extensions/media2/src/androidTest/res/raw/number1.mp4
Normal file
BIN
extensions/media2/src/androidTest/res/raw/number1.mp4
Normal file
Binary file not shown.
BIN
extensions/media2/src/androidTest/res/raw/number2.mp4
Normal file
BIN
extensions/media2/src/androidTest/res/raw/number2.mp4
Normal file
Binary file not shown.
BIN
extensions/media2/src/androidTest/res/raw/number3.mp4
Normal file
BIN
extensions/media2/src/androidTest/res/raw/number3.mp4
Normal file
Binary file not shown.
BIN
extensions/media2/src/androidTest/res/raw/sample_not_seekable.ts
Normal file
BIN
extensions/media2/src/androidTest/res/raw/sample_not_seekable.ts
Normal file
Binary file not shown.
BIN
extensions/media2/src/androidTest/res/raw/testmp3.mp3
Executable file
BIN
extensions/media2/src/androidTest/res/raw/testmp3.mp3
Executable file
Binary file not shown.
BIN
extensions/media2/src/androidTest/res/raw/testmp3_2.mp3
Normal file
BIN
extensions/media2/src/androidTest/res/raw/testmp3_2.mp3
Normal file
Binary file not shown.
BIN
extensions/media2/src/androidTest/res/raw/testvideo.3gp
Normal file
BIN
extensions/media2/src/androidTest/res/raw/testvideo.3gp
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
16
extensions/media2/src/main/AndroidManifest.xml
Normal file
16
extensions/media2/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright 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.media2"/>
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.media2;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.ExoPlayer;
|
||||||
|
import com.google.android.exoplayer2.PlaybackPreparer;
|
||||||
|
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
|
||||||
|
/** Prepares an {@link ExoPlayer} instance with a {@link ConcatenatingMediaSource}. */
|
||||||
|
public final class ConcatenatingMediaSourcePlaybackPreparer implements PlaybackPreparer {
|
||||||
|
|
||||||
|
private final ExoPlayer exoPlayer;
|
||||||
|
private final ConcatenatingMediaSource concatenatingMediaSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a concatenating media source playback preparer.
|
||||||
|
*
|
||||||
|
* @param exoPlayer The player to prepare.
|
||||||
|
* @param concatenatingMediaSource The concatenating media source with which to prepare the
|
||||||
|
* player.
|
||||||
|
*/
|
||||||
|
public ConcatenatingMediaSourcePlaybackPreparer(
|
||||||
|
ExoPlayer exoPlayer, ConcatenatingMediaSource concatenatingMediaSource) {
|
||||||
|
this.exoPlayer = exoPlayer;
|
||||||
|
this.concatenatingMediaSource = Assertions.checkNotNull(concatenatingMediaSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preparePlayback() {
|
||||||
|
exoPlayer.prepare(concatenatingMediaSource);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.media2;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media2.common.DataSourceCallback;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.upstream.BaseDataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/** An ExoPlayer {@link DataSource} for reading from a {@link DataSourceCallback}. */
|
||||||
|
/* package */ final class DataSourceCallbackDataSource extends BaseDataSource {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a factory for {@link DataSourceCallbackDataSource}s.
|
||||||
|
*
|
||||||
|
* @return A factory for data sources that read from the data source callback.
|
||||||
|
*/
|
||||||
|
public static DataSource.Factory getFactory(DataSourceCallback dataSourceCallback) {
|
||||||
|
Assertions.checkNotNull(dataSourceCallback);
|
||||||
|
return () -> new DataSourceCallbackDataSource(dataSourceCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final DataSourceCallback dataSourceCallback;
|
||||||
|
|
||||||
|
@Nullable private Uri uri;
|
||||||
|
private long position;
|
||||||
|
private long bytesRemaining;
|
||||||
|
private boolean opened;
|
||||||
|
|
||||||
|
public DataSourceCallbackDataSource(DataSourceCallback dataSourceCallback) {
|
||||||
|
super(/* isNetwork= */ false);
|
||||||
|
this.dataSourceCallback = Assertions.checkNotNull(dataSourceCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long open(DataSpec dataSpec) throws IOException {
|
||||||
|
uri = dataSpec.uri;
|
||||||
|
position = dataSpec.position;
|
||||||
|
transferInitializing(dataSpec);
|
||||||
|
long dataSourceCallbackSize = dataSourceCallback.getSize();
|
||||||
|
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||||
|
bytesRemaining = dataSpec.length;
|
||||||
|
} else if (dataSourceCallbackSize != -1) {
|
||||||
|
bytesRemaining = dataSourceCallbackSize - position;
|
||||||
|
} else {
|
||||||
|
bytesRemaining = C.LENGTH_UNSET;
|
||||||
|
}
|
||||||
|
opened = true;
|
||||||
|
transferStarted(dataSpec);
|
||||||
|
return bytesRemaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] buffer, int offset, int readLength) throws IOException {
|
||||||
|
if (readLength == 0) {
|
||||||
|
return 0;
|
||||||
|
} else if (bytesRemaining == 0) {
|
||||||
|
return C.RESULT_END_OF_INPUT;
|
||||||
|
}
|
||||||
|
int bytesToRead =
|
||||||
|
bytesRemaining == C.LENGTH_UNSET ? readLength : (int) Math.min(bytesRemaining, readLength);
|
||||||
|
int bytesRead = dataSourceCallback.readAt(position, buffer, offset, bytesToRead);
|
||||||
|
if (bytesRead == -1) {
|
||||||
|
if (bytesRemaining != C.LENGTH_UNSET) {
|
||||||
|
throw new EOFException();
|
||||||
|
}
|
||||||
|
return C.RESULT_END_OF_INPUT;
|
||||||
|
}
|
||||||
|
position += bytesRead;
|
||||||
|
if (bytesRemaining != C.LENGTH_UNSET) {
|
||||||
|
bytesRemaining -= bytesRead;
|
||||||
|
}
|
||||||
|
bytesTransferred(bytesRead);
|
||||||
|
return bytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public Uri getUri() {
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
uri = null;
|
||||||
|
if (opened) {
|
||||||
|
opened = false;
|
||||||
|
transferEnded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 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.media2;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.support.v4.media.session.MediaSessionCompat;
|
||||||
|
import androidx.media2.session.MediaSession;
|
||||||
|
|
||||||
|
/** Utility methods to use {@link MediaSession} with other existing Exo modules. */
|
||||||
|
public final class MediaSessionUtil {
|
||||||
|
|
||||||
|
/** Gets the {@link MediaSessionCompat.Token} from the {@link MediaSession}. */
|
||||||
|
// TODO(b/152764014): Deprecate this API when MediaSession#getSessionCompatToken() is released.
|
||||||
|
public static MediaSessionCompat.Token getSessionCompatToken(MediaSession session2) {
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
@SuppressWarnings("RestrictTo")
|
||||||
|
MediaSessionCompat sessionCompat = session2.getSessionCompat();
|
||||||
|
return sessionCompat.getSessionToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
private MediaSessionUtil() {
|
||||||
|
// Prevent from instantiation.
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,452 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.media2;
|
||||||
|
|
||||||
|
import androidx.annotation.GuardedBy;
|
||||||
|
import androidx.annotation.IntDef;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media2.common.MediaItem;
|
||||||
|
import androidx.media2.common.MediaMetadata;
|
||||||
|
import androidx.media2.common.SessionPlayer;
|
||||||
|
import androidx.media2.common.SessionPlayer.PlayerResult;
|
||||||
|
import com.google.android.exoplayer2.util.Log;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/** Manages the queue of player actions and handles running them one by one. */
|
||||||
|
/* package */ class PlayerCommandQueue implements AutoCloseable {
|
||||||
|
|
||||||
|
private static final String TAG = "PlayerCommandQueue";
|
||||||
|
private static final boolean DEBUG = false;
|
||||||
|
|
||||||
|
// Redefine command codes rather than using constants from SessionCommand here, because command
|
||||||
|
// code for setAudioAttribute() is missing in SessionCommand.
|
||||||
|
/** Command code for {@link SessionPlayer#setAudioAttributes}. */
|
||||||
|
public static final int COMMAND_CODE_PLAYER_SET_AUDIO_ATTRIBUTES = 0;
|
||||||
|
|
||||||
|
/** Command code for {@link SessionPlayer#play} */
|
||||||
|
public static final int COMMAND_CODE_PLAYER_PLAY = 1;
|
||||||
|
|
||||||
|
/** Command code for {@link SessionPlayer#replacePlaylistItem(int, MediaItem)} */
|
||||||
|
public static final int COMMAND_CODE_PLAYER_REPLACE_PLAYLIST_ITEM = 2;
|
||||||
|
|
||||||
|
/** Command code for {@link SessionPlayer#skipToPreviousPlaylistItem()} */
|
||||||
|
public static final int COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM = 3;
|
||||||
|
|
||||||
|
/** Command code for {@link SessionPlayer#skipToNextPlaylistItem()} */
|
||||||
|
public static final int COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM = 4;
|
||||||
|
|
||||||
|
/** Command code for {@link SessionPlayer#skipToPlaylistItem(int)} */
|
||||||
|
public static final int COMMAND_CODE_PLAYER_SKIP_TO_PLAYLIST_ITEM = 5;
|
||||||
|
|
||||||
|
/** Command code for {@link SessionPlayer#updatePlaylistMetadata(MediaMetadata)} */
|
||||||
|
public static final int COMMAND_CODE_PLAYER_UPDATE_LIST_METADATA = 6;
|
||||||
|
|
||||||
|
/** Command code for {@link SessionPlayer#setRepeatMode(int)} */
|
||||||
|
public static final int COMMAND_CODE_PLAYER_SET_REPEAT_MODE = 7;
|
||||||
|
|
||||||
|
/** Command code for {@link SessionPlayer#setShuffleMode(int)} */
|
||||||
|
public static final int COMMAND_CODE_PLAYER_SET_SHUFFLE_MODE = 8;
|
||||||
|
|
||||||
|
/** Command code for {@link SessionPlayer#setMediaItem(MediaItem)} */
|
||||||
|
public static final int COMMAND_CODE_PLAYER_SET_MEDIA_ITEM = 9;
|
||||||
|
|
||||||
|
/** Command code for {@link SessionPlayer#seekTo(long)} */
|
||||||
|
public static final int COMMAND_CODE_PLAYER_SEEK_TO = 10;
|
||||||
|
|
||||||
|
/** Command code for {@link SessionPlayer#prepare()} */
|
||||||
|
public static final int COMMAND_CODE_PLAYER_PREPARE = 11;
|
||||||
|
|
||||||
|
/** Command code for {@link SessionPlayer#setPlaybackSpeed(float)} */
|
||||||
|
public static final int COMMAND_CODE_PLAYER_SET_SPEED = 12;
|
||||||
|
|
||||||
|
/** Command code for {@link SessionPlayer#pause()} */
|
||||||
|
public static final int COMMAND_CODE_PLAYER_PAUSE = 13;
|
||||||
|
|
||||||
|
/** Command code for {@link SessionPlayer#setPlaylist(List, MediaMetadata)} */
|
||||||
|
public static final int COMMAND_CODE_PLAYER_SET_PLAYLIST = 14;
|
||||||
|
|
||||||
|
/** Command code for {@link SessionPlayer#addPlaylistItem(int, MediaItem)} */
|
||||||
|
public static final int COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM = 15;
|
||||||
|
|
||||||
|
/** Command code for {@link SessionPlayer#removePlaylistItem(int)} */
|
||||||
|
public static final int COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM = 16;
|
||||||
|
|
||||||
|
/** List of session commands whose result would be set after the command is finished. */
|
||||||
|
@Documented
|
||||||
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
|
@IntDef(
|
||||||
|
value = {
|
||||||
|
COMMAND_CODE_PLAYER_SET_AUDIO_ATTRIBUTES,
|
||||||
|
COMMAND_CODE_PLAYER_PLAY,
|
||||||
|
COMMAND_CODE_PLAYER_REPLACE_PLAYLIST_ITEM,
|
||||||
|
COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM,
|
||||||
|
COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM,
|
||||||
|
COMMAND_CODE_PLAYER_SKIP_TO_PLAYLIST_ITEM,
|
||||||
|
COMMAND_CODE_PLAYER_UPDATE_LIST_METADATA,
|
||||||
|
COMMAND_CODE_PLAYER_SET_REPEAT_MODE,
|
||||||
|
COMMAND_CODE_PLAYER_SET_SHUFFLE_MODE,
|
||||||
|
COMMAND_CODE_PLAYER_SET_MEDIA_ITEM,
|
||||||
|
COMMAND_CODE_PLAYER_SEEK_TO,
|
||||||
|
COMMAND_CODE_PLAYER_PREPARE,
|
||||||
|
COMMAND_CODE_PLAYER_SET_SPEED,
|
||||||
|
COMMAND_CODE_PLAYER_PAUSE,
|
||||||
|
COMMAND_CODE_PLAYER_SET_PLAYLIST,
|
||||||
|
COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM,
|
||||||
|
COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM,
|
||||||
|
})
|
||||||
|
public @interface CommandCode {}
|
||||||
|
|
||||||
|
/** Command whose result would be set later via listener after the command is finished. */
|
||||||
|
@Documented
|
||||||
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
|
@IntDef(
|
||||||
|
value = {COMMAND_CODE_PLAYER_PREPARE, COMMAND_CODE_PLAYER_PLAY, COMMAND_CODE_PLAYER_PAUSE})
|
||||||
|
public @interface AsyncCommandCode {}
|
||||||
|
|
||||||
|
// Should be only used on the handler.
|
||||||
|
private final PlayerWrapper player;
|
||||||
|
private final PlayerHandler handler;
|
||||||
|
private final Object lock;
|
||||||
|
|
||||||
|
@GuardedBy("lock")
|
||||||
|
private final Deque<PlayerCommand> pendingPlayerCommandQueue;
|
||||||
|
|
||||||
|
@GuardedBy("lock")
|
||||||
|
private boolean closed;
|
||||||
|
|
||||||
|
// Should be only used on the handler.
|
||||||
|
@Nullable private AsyncPlayerCommandResult pendingAsyncPlayerCommandResult;
|
||||||
|
|
||||||
|
public PlayerCommandQueue(PlayerWrapper player, PlayerHandler handler) {
|
||||||
|
this.player = player;
|
||||||
|
this.handler = handler;
|
||||||
|
lock = new Object();
|
||||||
|
pendingPlayerCommandQueue = new ArrayDeque<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
synchronized (lock) {
|
||||||
|
if (closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closed = true;
|
||||||
|
}
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
handler.removeCallbacksAndMessages(/* token= */ null);
|
||||||
|
List<PlayerCommand> queue;
|
||||||
|
synchronized (lock) {
|
||||||
|
queue = new ArrayList<>(pendingPlayerCommandQueue);
|
||||||
|
pendingPlayerCommandQueue.clear();
|
||||||
|
}
|
||||||
|
for (PlayerCommand playerCommand : queue) {
|
||||||
|
playerCommand.result.set(
|
||||||
|
new PlayerResult(PlayerResult.RESULT_INFO_SKIPPED, /* item= */ null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListenableFuture<PlayerResult> addCommand(
|
||||||
|
@CommandCode int commandCode, Callable<Boolean> command) {
|
||||||
|
return addCommand(commandCode, command, /* tag= */ null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListenableFuture<PlayerResult> addCommand(
|
||||||
|
@CommandCode int commandCode, Callable<Boolean> command, @Nullable Object tag) {
|
||||||
|
SettableFuture<PlayerResult> result = SettableFuture.create();
|
||||||
|
synchronized (lock) {
|
||||||
|
if (closed) {
|
||||||
|
// OK to set result with lock hold because developers cannot add listener here.
|
||||||
|
result.set(new PlayerResult(PlayerResult.RESULT_ERROR_INVALID_STATE, /* item= */ null));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
PlayerCommand playerCommand = new PlayerCommand(commandCode, command, result, tag);
|
||||||
|
result.addListener(
|
||||||
|
() -> {
|
||||||
|
if (result.isCancelled()) {
|
||||||
|
boolean isCommandPending;
|
||||||
|
synchronized (lock) {
|
||||||
|
isCommandPending = pendingPlayerCommandQueue.remove(playerCommand);
|
||||||
|
}
|
||||||
|
if (isCommandPending) {
|
||||||
|
result.set(
|
||||||
|
new PlayerResult(
|
||||||
|
PlayerResult.RESULT_INFO_SKIPPED, player.getCurrentMediaItem()));
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "canceled " + playerCommand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pendingAsyncPlayerCommandResult != null
|
||||||
|
&& pendingAsyncPlayerCommandResult.result == result) {
|
||||||
|
pendingAsyncPlayerCommandResult = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processPendingCommandOnHandler();
|
||||||
|
},
|
||||||
|
handler::postOrRun);
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "adding " + playerCommand);
|
||||||
|
}
|
||||||
|
pendingPlayerCommandQueue.add(playerCommand);
|
||||||
|
}
|
||||||
|
processPendingCommand();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void notifyCommandError() {
|
||||||
|
handler.postOrRun(
|
||||||
|
() -> {
|
||||||
|
@Nullable AsyncPlayerCommandResult pendingResult = pendingAsyncPlayerCommandResult;
|
||||||
|
if (pendingResult == null) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "Ignoring notifyCommandError(). No pending async command.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingResult.result.set(
|
||||||
|
new PlayerResult(PlayerResult.RESULT_ERROR_UNKNOWN, player.getCurrentMediaItem()));
|
||||||
|
pendingAsyncPlayerCommandResult = null;
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "error on " + pendingResult);
|
||||||
|
}
|
||||||
|
processPendingCommandOnHandler();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void notifyCommandCompleted(@AsyncCommandCode int completedCommandCode) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "notifyCommandCompleted, completedCommandCode=" + completedCommandCode);
|
||||||
|
}
|
||||||
|
handler.postOrRun(
|
||||||
|
() -> {
|
||||||
|
@Nullable AsyncPlayerCommandResult pendingResult = pendingAsyncPlayerCommandResult;
|
||||||
|
if (pendingResult == null || pendingResult.commandCode != completedCommandCode) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"Unexpected Listener is notified from the Player. Player may be used"
|
||||||
|
+ " directly rather than "
|
||||||
|
+ toLogFriendlyString(completedCommandCode));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingResult.result.set(
|
||||||
|
new PlayerResult(PlayerResult.RESULT_SUCCESS, player.getCurrentMediaItem()));
|
||||||
|
pendingAsyncPlayerCommandResult = null;
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "completed " + pendingResult);
|
||||||
|
}
|
||||||
|
processPendingCommandOnHandler();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processPendingCommand() {
|
||||||
|
handler.postOrRun(this::processPendingCommandOnHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processPendingCommandOnHandler() {
|
||||||
|
while (pendingAsyncPlayerCommandResult == null) {
|
||||||
|
@Nullable PlayerCommand playerCommand;
|
||||||
|
synchronized (lock) {
|
||||||
|
playerCommand = pendingPlayerCommandQueue.poll();
|
||||||
|
}
|
||||||
|
if (playerCommand == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int commandCode = playerCommand.commandCode;
|
||||||
|
// Check if it's @AsyncCommandCode
|
||||||
|
boolean asyncCommand = isAsyncCommand(playerCommand.commandCode);
|
||||||
|
|
||||||
|
// Continuous COMMAND_CODE_PLAYER_SEEK_TO can be skipped.
|
||||||
|
if (commandCode == COMMAND_CODE_PLAYER_SEEK_TO) {
|
||||||
|
@Nullable List<PlayerCommand> skippingCommands = null;
|
||||||
|
while (true) {
|
||||||
|
synchronized (lock) {
|
||||||
|
@Nullable PlayerCommand pendingCommand = pendingPlayerCommandQueue.peek();
|
||||||
|
if (pendingCommand == null || pendingCommand.commandCode != commandCode) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pendingPlayerCommandQueue.poll();
|
||||||
|
if (skippingCommands == null) {
|
||||||
|
skippingCommands = new ArrayList<>();
|
||||||
|
}
|
||||||
|
skippingCommands.add(playerCommand);
|
||||||
|
playerCommand = pendingCommand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (skippingCommands != null) {
|
||||||
|
for (PlayerCommand skippingCommand : skippingCommands) {
|
||||||
|
skippingCommand.result.set(
|
||||||
|
new PlayerResult(PlayerResult.RESULT_INFO_SKIPPED, player.getCurrentMediaItem()));
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "skipping pending command, " + skippingCommand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asyncCommand) {
|
||||||
|
// Result would come later, via #notifyCommandCompleted().
|
||||||
|
// Set pending player result first because it may be notified while the command is running.
|
||||||
|
pendingAsyncPlayerCommandResult =
|
||||||
|
new AsyncPlayerCommandResult(commandCode, playerCommand.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "start processing command, " + playerCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
int resultCode;
|
||||||
|
if (player.hasError()) {
|
||||||
|
resultCode = PlayerResult.RESULT_ERROR_INVALID_STATE;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
boolean handled = playerCommand.command.call();
|
||||||
|
resultCode = handled ? PlayerResult.RESULT_SUCCESS : PlayerResult.RESULT_INFO_SKIPPED;
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
resultCode = PlayerResult.RESULT_ERROR_INVALID_STATE;
|
||||||
|
} catch (IllegalArgumentException | IndexOutOfBoundsException e) {
|
||||||
|
resultCode = PlayerResult.RESULT_ERROR_BAD_VALUE;
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
resultCode = PlayerResult.RESULT_ERROR_PERMISSION_DENIED;
|
||||||
|
} catch (Exception e) {
|
||||||
|
resultCode = PlayerResult.RESULT_ERROR_UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "command processed, " + playerCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asyncCommand) {
|
||||||
|
if (resultCode != PlayerResult.RESULT_SUCCESS
|
||||||
|
&& pendingAsyncPlayerCommandResult != null
|
||||||
|
&& playerCommand.result == pendingAsyncPlayerCommandResult.result) {
|
||||||
|
pendingAsyncPlayerCommandResult = null;
|
||||||
|
playerCommand.result.set(new PlayerResult(resultCode, player.getCurrentMediaItem()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
playerCommand.result.set(new PlayerResult(resultCode, player.getCurrentMediaItem()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String toLogFriendlyString(@AsyncCommandCode int commandCode) {
|
||||||
|
switch (commandCode) {
|
||||||
|
case COMMAND_CODE_PLAYER_PLAY:
|
||||||
|
return "SessionPlayerConnector#play()";
|
||||||
|
case COMMAND_CODE_PLAYER_PAUSE:
|
||||||
|
return "SessionPlayerConnector#pause()";
|
||||||
|
case COMMAND_CODE_PLAYER_PREPARE:
|
||||||
|
return "SessionPlayerConnector#prepare()";
|
||||||
|
default:
|
||||||
|
// Never happens.
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isAsyncCommand(@CommandCode int commandCode) {
|
||||||
|
switch (commandCode) {
|
||||||
|
case COMMAND_CODE_PLAYER_PLAY:
|
||||||
|
case COMMAND_CODE_PLAYER_PAUSE:
|
||||||
|
case COMMAND_CODE_PLAYER_PREPARE:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class AsyncPlayerCommandResult {
|
||||||
|
@AsyncCommandCode public final int commandCode;
|
||||||
|
public final SettableFuture<PlayerResult> result;
|
||||||
|
|
||||||
|
public AsyncPlayerCommandResult(
|
||||||
|
@AsyncCommandCode int commandCode, SettableFuture<PlayerResult> result) {
|
||||||
|
this.commandCode = commandCode;
|
||||||
|
this.result = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder stringBuilder =
|
||||||
|
new StringBuilder("AsyncPlayerCommandResult {commandCode=")
|
||||||
|
.append(commandCode)
|
||||||
|
.append(", result=")
|
||||||
|
.append(result.hashCode());
|
||||||
|
if (result.isDone()) {
|
||||||
|
try {
|
||||||
|
int resultCode = result.get(/* timeout= */ 0, TimeUnit.MILLISECONDS).getResultCode();
|
||||||
|
stringBuilder.append(", resultCode=").append(resultCode);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// pass-through.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stringBuilder.append("}");
|
||||||
|
return stringBuilder.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class PlayerCommand {
|
||||||
|
public final int commandCode;
|
||||||
|
public final Callable<Boolean> command;
|
||||||
|
// Result shouldn't be set with lock held, because it may trigger listener set by developers.
|
||||||
|
public final SettableFuture<PlayerResult> result;
|
||||||
|
@Nullable private final Object tag;
|
||||||
|
|
||||||
|
public PlayerCommand(
|
||||||
|
int commandCode,
|
||||||
|
Callable<Boolean> command,
|
||||||
|
SettableFuture<PlayerResult> result,
|
||||||
|
@Nullable Object tag) {
|
||||||
|
this.commandCode = commandCode;
|
||||||
|
this.command = command;
|
||||||
|
this.result = result;
|
||||||
|
this.tag = tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder stringBuilder =
|
||||||
|
new StringBuilder("PlayerCommand {commandCode=")
|
||||||
|
.append(commandCode)
|
||||||
|
.append(", result=")
|
||||||
|
.append(result.hashCode());
|
||||||
|
if (result.isDone()) {
|
||||||
|
try {
|
||||||
|
int resultCode = result.get(/* timeout= */ 0, TimeUnit.MILLISECONDS).getResultCode();
|
||||||
|
stringBuilder.append(", resultCode=").append(resultCode);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// pass-through.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tag != null) {
|
||||||
|
stringBuilder.append(", tag=").append(tag);
|
||||||
|
}
|
||||||
|
stringBuilder.append("}");
|
||||||
|
return stringBuilder.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.media2;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
|
||||||
|
/** A {@link Handler} that provides {@link #postOrRun(Runnable)}. */
|
||||||
|
/* package */ final class PlayerHandler extends Handler {
|
||||||
|
public PlayerHandler(Looper looper) {
|
||||||
|
super(looper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Posts the {@link Runnable} if the calling thread differs with the {@link Looper} of this
|
||||||
|
* handler. Otherwise, runs the runnable directly.
|
||||||
|
*
|
||||||
|
* @param r A runnable to either post or run.
|
||||||
|
* @return {@code true} if it's successfully run. {@code false} otherwise.
|
||||||
|
*/
|
||||||
|
public boolean postOrRun(Runnable r) {
|
||||||
|
if (Thread.currentThread() != getLooper().getThread()) {
|
||||||
|
return post(r);
|
||||||
|
}
|
||||||
|
r.run();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,533 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.media2;
|
||||||
|
|
||||||
|
import androidx.annotation.IntRange;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media.AudioAttributesCompat;
|
||||||
|
import androidx.media2.common.MediaItem;
|
||||||
|
import androidx.media2.common.MediaMetadata;
|
||||||
|
import androidx.media2.common.SessionPlayer;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.ControlDispatcher;
|
||||||
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
|
import com.google.android.exoplayer2.PlaybackPreparer;
|
||||||
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import com.google.android.exoplayer2.Timeline;
|
||||||
|
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||||
|
import com.google.android.exoplayer2.audio.AudioListener;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps an ExoPlayer {@link Player} instance and provides methods and notifies events like those in
|
||||||
|
* the {@link SessionPlayer} API.
|
||||||
|
*/
|
||||||
|
/* package */ final class PlayerWrapper {
|
||||||
|
|
||||||
|
/** Listener for player wrapper events. */
|
||||||
|
public interface Listener {
|
||||||
|
/**
|
||||||
|
* Called when the player state is changed.
|
||||||
|
*
|
||||||
|
* <p>This method will be called at first if multiple events should be notified at once.
|
||||||
|
*/
|
||||||
|
void onPlayerStateChanged(/* @SessionPlayer.PlayerState */ int playerState);
|
||||||
|
|
||||||
|
/** Called when the player is prepared. */
|
||||||
|
void onPrepared(MediaItem mediaItem, int bufferingPercentage);
|
||||||
|
|
||||||
|
/** Called when a seek request has completed. */
|
||||||
|
void onSeekCompleted();
|
||||||
|
|
||||||
|
/** Called when the player rebuffers. */
|
||||||
|
void onBufferingStarted(MediaItem mediaItem);
|
||||||
|
|
||||||
|
/** Called when the player becomes ready again after rebuffering. */
|
||||||
|
void onBufferingEnded(MediaItem mediaItem, int bufferingPercentage);
|
||||||
|
|
||||||
|
/** Called periodically with the player's buffered position as a percentage. */
|
||||||
|
void onBufferingUpdate(MediaItem mediaItem, int bufferingPercentage);
|
||||||
|
|
||||||
|
/** Called when current media item is changed. */
|
||||||
|
void onCurrentMediaItemChanged(MediaItem mediaItem);
|
||||||
|
|
||||||
|
/** Called when playback of the item list has ended. */
|
||||||
|
void onPlaybackEnded();
|
||||||
|
|
||||||
|
/** Called when the player encounters an error. */
|
||||||
|
void onError(@Nullable MediaItem mediaItem);
|
||||||
|
|
||||||
|
/** Called when the playlist is changed */
|
||||||
|
void onPlaylistChanged();
|
||||||
|
|
||||||
|
/** Called when the shuffle mode is changed */
|
||||||
|
void onShuffleModeChanged(int shuffleMode);
|
||||||
|
|
||||||
|
/** Called when the repeat mode is changed */
|
||||||
|
void onRepeatModeChanged(int repeatMode);
|
||||||
|
|
||||||
|
/** Called when the audio attributes is changed */
|
||||||
|
void onAudioAttributesChanged(AudioAttributesCompat audioAttributes);
|
||||||
|
|
||||||
|
/** Called when the playback speed is changed */
|
||||||
|
void onPlaybackSpeedChanged(float playbackSpeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int POLL_BUFFER_INTERVAL_MS = 1000;
|
||||||
|
|
||||||
|
private final Listener listener;
|
||||||
|
private final PlayerHandler handler;
|
||||||
|
private final Runnable pollBufferRunnable;
|
||||||
|
|
||||||
|
private final Player player;
|
||||||
|
private final PlaylistManager playlistManager;
|
||||||
|
private final PlaybackPreparer playbackPreparer;
|
||||||
|
private final ControlDispatcher controlDispatcher;
|
||||||
|
private final ComponentListener componentListener;
|
||||||
|
|
||||||
|
private boolean prepared;
|
||||||
|
private boolean rebuffering;
|
||||||
|
private int currentWindowIndex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new ExoPlayer wrapper.
|
||||||
|
*
|
||||||
|
* @param listener A listener for player wrapper events.
|
||||||
|
* @param player The player to handle commands
|
||||||
|
* @param playlistManager The playlist manager to handle playlist commands
|
||||||
|
* @param playbackPreparer The playback preparer to prepare
|
||||||
|
* @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
|
||||||
|
* changes to the player.
|
||||||
|
*/
|
||||||
|
PlayerWrapper(
|
||||||
|
Listener listener,
|
||||||
|
Player player,
|
||||||
|
PlaylistManager playlistManager,
|
||||||
|
PlaybackPreparer playbackPreparer,
|
||||||
|
ControlDispatcher controlDispatcher) {
|
||||||
|
this.listener = listener;
|
||||||
|
this.player = player;
|
||||||
|
this.playlistManager = playlistManager;
|
||||||
|
this.playbackPreparer = playbackPreparer;
|
||||||
|
this.controlDispatcher = controlDispatcher;
|
||||||
|
|
||||||
|
componentListener = new ComponentListener();
|
||||||
|
player.addListener(componentListener);
|
||||||
|
@Nullable Player.AudioComponent audioComponent = player.getAudioComponent();
|
||||||
|
if (audioComponent != null) {
|
||||||
|
audioComponent.addAudioListener(componentListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
handler = new PlayerHandler(player.getApplicationLooper());
|
||||||
|
pollBufferRunnable = new PollBufferRunnable();
|
||||||
|
|
||||||
|
currentWindowIndex = C.INDEX_UNSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean setMediaItem(MediaItem mediaItem) {
|
||||||
|
boolean handled = playlistManager.setMediaItem(player, mediaItem);
|
||||||
|
if (handled) {
|
||||||
|
currentWindowIndex = playlistManager.getCurrentMediaItemIndex(player);
|
||||||
|
}
|
||||||
|
return handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean setPlaylist(List<MediaItem> playlist, @Nullable MediaMetadata metadata) {
|
||||||
|
boolean handled = playlistManager.setPlaylist(player, playlist, metadata);
|
||||||
|
if (handled) {
|
||||||
|
currentWindowIndex = playlistManager.getCurrentMediaItemIndex(player);
|
||||||
|
}
|
||||||
|
return handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean addPlaylistItem(int index, MediaItem item) {
|
||||||
|
return playlistManager.addPlaylistItem(player, index, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean removePlaylistItem(@IntRange(from = 0) int index) {
|
||||||
|
return playlistManager.removePlaylistItem(player, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean replacePlaylistItem(int index, MediaItem item) {
|
||||||
|
return playlistManager.replacePlaylistItem(player, index, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean skipToPreviousPlaylistItem() {
|
||||||
|
return playlistManager.skipToPreviousPlaylistItem(player, controlDispatcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean skipToNextPlaylistItem() {
|
||||||
|
return playlistManager.skipToNextPlaylistItem(player, controlDispatcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean skipToPlaylistItem(@IntRange(from = 0) int index) {
|
||||||
|
return playlistManager.skipToPlaylistItem(player, controlDispatcher, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean updatePlaylistMetadata(@Nullable MediaMetadata metadata) {
|
||||||
|
return playlistManager.updatePlaylistMetadata(player, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean setRepeatMode(int repeatMode) {
|
||||||
|
return controlDispatcher.dispatchSetRepeatMode(
|
||||||
|
player, Utils.getExoPlayerRepeatMode(repeatMode));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean setShuffleMode(int shuffleMode) {
|
||||||
|
return controlDispatcher.dispatchSetShuffleModeEnabled(
|
||||||
|
player, Utils.getExoPlayerShuffleMode(shuffleMode));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public List<MediaItem> getPlaylist() {
|
||||||
|
return playlistManager.getPlaylist(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public MediaMetadata getPlaylistMetadata() {
|
||||||
|
return playlistManager.getPlaylistMetadata(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRepeatMode() {
|
||||||
|
return Utils.getRepeatMode(player.getRepeatMode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getShuffleMode() {
|
||||||
|
return Utils.getShuffleMode(player.getShuffleModeEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCurrentMediaItemIndex() {
|
||||||
|
return playlistManager.getCurrentMediaItemIndex(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPreviousMediaItemIndex() {
|
||||||
|
return player.getPreviousWindowIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNextMediaItemIndex() {
|
||||||
|
return player.getNextWindowIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public MediaItem getCurrentMediaItem() {
|
||||||
|
return playlistManager.getCurrentMediaItem(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean prepare() {
|
||||||
|
if (prepared) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
playbackPreparer.preparePlayback();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean play() {
|
||||||
|
if (player.getPlaybackState() == Player.STATE_ENDED) {
|
||||||
|
int currentWindowIndex = playlistManager.getCurrentMediaItemIndex(player);
|
||||||
|
boolean seekHandled =
|
||||||
|
controlDispatcher.dispatchSeekTo(player, currentWindowIndex, /* positionMs= */ 0);
|
||||||
|
if (!seekHandled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolean playWhenReady = player.getPlayWhenReady();
|
||||||
|
int suppressReason = player.getPlaybackSuppressionReason();
|
||||||
|
if (playWhenReady && suppressReason == Player.PLAYBACK_SUPPRESSION_REASON_NONE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean pause() {
|
||||||
|
boolean playWhenReady = player.getPlayWhenReady();
|
||||||
|
int suppressReason = player.getPlaybackSuppressionReason();
|
||||||
|
if (!playWhenReady && suppressReason == Player.PLAYBACK_SUPPRESSION_REASON_NONE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean seekTo(long position) {
|
||||||
|
int currentWindowIndex = playlistManager.getCurrentMediaItemIndex(player);
|
||||||
|
return controlDispatcher.dispatchSeekTo(player, currentWindowIndex, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCurrentPosition() {
|
||||||
|
Assertions.checkState(getState() != SessionPlayer.PLAYER_STATE_IDLE);
|
||||||
|
return Math.max(0, player.getCurrentPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getDuration() {
|
||||||
|
Assertions.checkState(getState() != SessionPlayer.PLAYER_STATE_IDLE);
|
||||||
|
long duration = player.getDuration();
|
||||||
|
return duration == C.TIME_UNSET ? -1 : duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getBufferedPosition() {
|
||||||
|
Assertions.checkState(getState() != SessionPlayer.PLAYER_STATE_IDLE);
|
||||||
|
return player.getBufferedPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* @SessionPlayer.PlayerState */
|
||||||
|
private int getState() {
|
||||||
|
if (hasError()) {
|
||||||
|
return SessionPlayer.PLAYER_STATE_ERROR;
|
||||||
|
}
|
||||||
|
int state = player.getPlaybackState();
|
||||||
|
boolean playWhenReady = player.getPlayWhenReady();
|
||||||
|
switch (state) {
|
||||||
|
case Player.STATE_IDLE:
|
||||||
|
return SessionPlayer.PLAYER_STATE_IDLE;
|
||||||
|
case Player.STATE_ENDED:
|
||||||
|
return SessionPlayer.PLAYER_STATE_PAUSED;
|
||||||
|
case Player.STATE_BUFFERING:
|
||||||
|
case Player.STATE_READY:
|
||||||
|
return playWhenReady
|
||||||
|
? SessionPlayer.PLAYER_STATE_PLAYING
|
||||||
|
: SessionPlayer.PLAYER_STATE_PAUSED;
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean setAudioAttributes(AudioAttributesCompat audioAttributes) {
|
||||||
|
Player.AudioComponent audioComponent = Assertions.checkStateNotNull(player.getAudioComponent());
|
||||||
|
audioComponent.setAudioAttributes(
|
||||||
|
Utils.getAudioAttributes(audioAttributes), /* handleAudioFocus= */ true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AudioAttributesCompat getAudioAttributes() {
|
||||||
|
@Nullable Player.AudioComponent audioComponent = player.getAudioComponent();
|
||||||
|
return Utils.getAudioAttributesCompat(
|
||||||
|
audioComponent != null ? audioComponent.getAudioAttributes() : AudioAttributes.DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean setPlaybackSpeed(float playbackSpeed) {
|
||||||
|
player.setPlaybackParameters(new PlaybackParameters(playbackSpeed));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getPlaybackSpeed() {
|
||||||
|
return player.getPlaybackParameters().speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
controlDispatcher.dispatchStop(player, /* reset= */ true);
|
||||||
|
prepared = false;
|
||||||
|
rebuffering = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
handler.removeCallbacks(pollBufferRunnable);
|
||||||
|
player.removeListener(componentListener);
|
||||||
|
|
||||||
|
@Nullable Player.AudioComponent audioComponent = player.getAudioComponent();
|
||||||
|
if (audioComponent != null) {
|
||||||
|
audioComponent.removeAudioListener(componentListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCurrentMediaItemSeekable() {
|
||||||
|
return getCurrentMediaItem() != null
|
||||||
|
&& !player.isPlayingAd()
|
||||||
|
&& player.isCurrentWindowSeekable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canSkipToPlaylistItem() {
|
||||||
|
@Nullable List<MediaItem> playlist = getPlaylist();
|
||||||
|
return playlist != null && playlist.size() > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canSkipToPreviousPlaylistItem() {
|
||||||
|
return player.hasPrevious();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canSkipToNextPlaylistItem() {
|
||||||
|
return player.hasNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasError() {
|
||||||
|
return player.getPlayerError() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handlePlayWhenReadyChanged() {
|
||||||
|
listener.onPlayerStateChanged(getState());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handlePlayerStateChanged(@Player.State int state) {
|
||||||
|
if (state == Player.STATE_READY || state == Player.STATE_BUFFERING) {
|
||||||
|
handler.postOrRun(pollBufferRunnable);
|
||||||
|
} else {
|
||||||
|
handler.removeCallbacks(pollBufferRunnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case Player.STATE_BUFFERING:
|
||||||
|
maybeNotifyBufferingEvents();
|
||||||
|
break;
|
||||||
|
case Player.STATE_READY:
|
||||||
|
maybeNotifyReadyEvents();
|
||||||
|
break;
|
||||||
|
case Player.STATE_ENDED:
|
||||||
|
maybeNotifyEndedEvents();
|
||||||
|
break;
|
||||||
|
case Player.STATE_IDLE:
|
||||||
|
// Do nothing.
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handlePositionDiscontinuity(@Player.DiscontinuityReason int reason) {
|
||||||
|
int currentWindowIndex = playlistManager.getCurrentMediaItemIndex(player);
|
||||||
|
if (this.currentWindowIndex != currentWindowIndex) {
|
||||||
|
this.currentWindowIndex = currentWindowIndex;
|
||||||
|
MediaItem currentMediaItem =
|
||||||
|
Assertions.checkNotNull(playlistManager.getCurrentMediaItem(player));
|
||||||
|
listener.onCurrentMediaItemChanged(currentMediaItem);
|
||||||
|
} else {
|
||||||
|
listener.onSeekCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handlePlayerError() {
|
||||||
|
listener.onPlayerStateChanged(SessionPlayer.PLAYER_STATE_ERROR);
|
||||||
|
listener.onError(getCurrentMediaItem());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleRepeatModeChanged(@Player.RepeatMode int repeatMode) {
|
||||||
|
listener.onRepeatModeChanged(Utils.getRepeatMode(repeatMode));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleShuffleMode(boolean shuffleModeEnabled) {
|
||||||
|
listener.onShuffleModeChanged(Utils.getShuffleMode(shuffleModeEnabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handlePlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||||
|
listener.onPlaybackSpeedChanged(playbackParameters.speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleTimelineChanged() {
|
||||||
|
playlistManager.onTimelineChanged(player);
|
||||||
|
listener.onPlaylistChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleAudioAttributesChanged(AudioAttributes audioAttributes) {
|
||||||
|
listener.onAudioAttributesChanged(Utils.getAudioAttributesCompat(audioAttributes));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateBufferingAndScheduleNextPollBuffer() {
|
||||||
|
MediaItem mediaItem = Assertions.checkNotNull(getCurrentMediaItem());
|
||||||
|
listener.onBufferingUpdate(mediaItem, player.getBufferedPercentage());
|
||||||
|
handler.removeCallbacks(pollBufferRunnable);
|
||||||
|
handler.postDelayed(pollBufferRunnable, POLL_BUFFER_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeNotifyBufferingEvents() {
|
||||||
|
MediaItem mediaItem = Assertions.checkNotNull(getCurrentMediaItem());
|
||||||
|
if (prepared && !rebuffering) {
|
||||||
|
rebuffering = true;
|
||||||
|
listener.onBufferingStarted(mediaItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeNotifyReadyEvents() {
|
||||||
|
MediaItem mediaItem = Assertions.checkNotNull(getCurrentMediaItem());
|
||||||
|
boolean prepareComplete = !prepared;
|
||||||
|
if (prepareComplete) {
|
||||||
|
prepared = true;
|
||||||
|
handlePositionDiscontinuity(Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);
|
||||||
|
listener.onPlayerStateChanged(SessionPlayer.PLAYER_STATE_PAUSED);
|
||||||
|
listener.onPrepared(mediaItem, player.getBufferedPercentage());
|
||||||
|
}
|
||||||
|
if (rebuffering) {
|
||||||
|
rebuffering = false;
|
||||||
|
listener.onBufferingEnded(mediaItem, player.getBufferedPercentage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeNotifyEndedEvents() {
|
||||||
|
if (player.getPlayWhenReady()) {
|
||||||
|
listener.onPlayerStateChanged(SessionPlayer.PLAYER_STATE_PAUSED);
|
||||||
|
listener.onPlaybackEnded();
|
||||||
|
player.setPlayWhenReady(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ComponentListener implements Player.EventListener, AudioListener {
|
||||||
|
|
||||||
|
// Player.EventListener implementation.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
|
||||||
|
handlePlayWhenReadyChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlaybackStateChanged(@Player.State int state) {
|
||||||
|
handlePlayerStateChanged(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
|
||||||
|
handlePositionDiscontinuity(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayerError(ExoPlaybackException error) {
|
||||||
|
handlePlayerError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
|
||||||
|
handleRepeatModeChanged(repeatMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||||
|
handleShuffleMode(shuffleModeEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||||
|
handlePlaybackParametersChanged(playbackParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTimelineChanged(Timeline timeline, int reason) {
|
||||||
|
handleTimelineChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
// AudioListener implementation.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAudioAttributesChanged(AudioAttributes audioAttributes) {
|
||||||
|
handleAudioAttributesChanged(audioAttributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class PollBufferRunnable implements Runnable {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
updateBufferingAndScheduleNextPollBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,156 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.media2;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media2.common.MediaItem;
|
||||||
|
import androidx.media2.common.MediaMetadata;
|
||||||
|
import androidx.media2.common.SessionPlayer;
|
||||||
|
import com.google.android.exoplayer2.ControlDispatcher;
|
||||||
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Interface that handles playlist edit and navigation operations. */
|
||||||
|
public interface PlaylistManager {
|
||||||
|
/**
|
||||||
|
* See {@link SessionPlayer#setPlaylist(List, MediaMetadata)}.
|
||||||
|
*
|
||||||
|
* @param player The player used to build SessionPlayer together.
|
||||||
|
* @param playlist A list of {@link MediaItem} objects to set as a play list.
|
||||||
|
* @param metadata The metadata of the playlist.
|
||||||
|
* @return true if the operation was dispatched. False if suppressed.
|
||||||
|
*/
|
||||||
|
boolean setPlaylist(Player player, List<MediaItem> playlist, @Nullable MediaMetadata metadata);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link SessionPlayer#addPlaylistItem(int, MediaItem)}.
|
||||||
|
*
|
||||||
|
* @param player The player used to build SessionPlayer together.
|
||||||
|
* @param index The index of the item you want to add in the playlist.
|
||||||
|
* @param mediaItem The media item you want to add.
|
||||||
|
* @return true if the operation was dispatched. False if suppressed.
|
||||||
|
*/
|
||||||
|
boolean addPlaylistItem(Player player, int index, MediaItem mediaItem);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link SessionPlayer#removePlaylistItem(int)}.
|
||||||
|
*
|
||||||
|
* @param player The player used to build SessionPlayer together.
|
||||||
|
* @param index The index of the item you want to remove in the playlist.
|
||||||
|
* @return true if the operation was dispatched. False if suppressed.
|
||||||
|
*/
|
||||||
|
boolean removePlaylistItem(Player player, int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link SessionPlayer#replacePlaylistItem(int, MediaItem)}.
|
||||||
|
*
|
||||||
|
* @param player The player used to build SessionPlayer together.
|
||||||
|
* @param mediaItem The media item you want to replace with.
|
||||||
|
* @return true if the operation was dispatched. False if suppressed.
|
||||||
|
*/
|
||||||
|
boolean replacePlaylistItem(Player player, int index, MediaItem mediaItem);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link SessionPlayer#setMediaItem(MediaItem)}.
|
||||||
|
*
|
||||||
|
* @param player The player used to build SessionPlayer together.
|
||||||
|
* @param mediaItem The media item you want to set.
|
||||||
|
* @return true if the operation was dispatched. False if suppressed.
|
||||||
|
*/
|
||||||
|
boolean setMediaItem(Player player, MediaItem mediaItem);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link SessionPlayer#updatePlaylistMetadata(MediaMetadata)}.
|
||||||
|
*
|
||||||
|
* @param player The player used to build SessionPlayer together.
|
||||||
|
* @param metadata The metadata of the playlist.
|
||||||
|
* @return true if the operation was dispatched. False if suppressed.
|
||||||
|
*/
|
||||||
|
boolean updatePlaylistMetadata(Player player, @Nullable MediaMetadata metadata);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link SessionPlayer#skipToNextPlaylistItem()}.
|
||||||
|
*
|
||||||
|
* @param player The player used to build SessionPlayer together.
|
||||||
|
* @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
|
||||||
|
* changes to the player.
|
||||||
|
* @return true if the operation was dispatched. False if suppressed.
|
||||||
|
*/
|
||||||
|
boolean skipToNextPlaylistItem(Player player, ControlDispatcher controlDispatcher);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link SessionPlayer#skipToPreviousPlaylistItem()}.
|
||||||
|
*
|
||||||
|
* @param player The player used to build SessionPlayer together.
|
||||||
|
* @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
|
||||||
|
* changes to the player.
|
||||||
|
* @return true if the operation was dispatched. False if suppressed.
|
||||||
|
*/
|
||||||
|
boolean skipToPreviousPlaylistItem(Player player, ControlDispatcher controlDispatcher);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link SessionPlayer#skipToPlaylistItem(int)}.
|
||||||
|
*
|
||||||
|
* @param player The player used to build SessionPlayer together.
|
||||||
|
* @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
|
||||||
|
* changes to the player.
|
||||||
|
* @return true if the operation was dispatched. False if suppressed.
|
||||||
|
*/
|
||||||
|
boolean skipToPlaylistItem(Player player, ControlDispatcher controlDispatcher, int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link SessionPlayer#getCurrentMediaItemIndex()}.
|
||||||
|
*
|
||||||
|
* @param player The player used to build SessionPlayer together.
|
||||||
|
* @return The current media item index
|
||||||
|
*/
|
||||||
|
int getCurrentMediaItemIndex(Player player);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link SessionPlayer#getCurrentMediaItem()}.
|
||||||
|
*
|
||||||
|
* @param player The player used to build SessionPlayer together.
|
||||||
|
* @return The current media item index
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
MediaItem getCurrentMediaItem(Player player);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link SessionPlayer#setPlaylist(List, MediaMetadata)}.
|
||||||
|
*
|
||||||
|
* @param player The player used to build SessionPlayer together.
|
||||||
|
* @return The playlist.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
List<MediaItem> getPlaylist(Player player);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link SessionPlayer#getPlaylistMetadata()}.
|
||||||
|
*
|
||||||
|
* @param player The player used to build SessionPlayer together.
|
||||||
|
* @return The metadata of the playlist.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
MediaMetadata getPlaylistMetadata(Player player);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the player's timeline is changed.
|
||||||
|
*
|
||||||
|
* @param player The player used to build SessionPlayer together.
|
||||||
|
*/
|
||||||
|
void onTimelineChanged(Player player);
|
||||||
|
}
|
@ -0,0 +1,384 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.media2;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media2.common.MediaItem;
|
||||||
|
import androidx.media2.common.MediaMetadata;
|
||||||
|
import androidx.media2.common.Rating;
|
||||||
|
import androidx.media2.common.SessionPlayer;
|
||||||
|
import androidx.media2.session.MediaSession;
|
||||||
|
import androidx.media2.session.SessionCommand;
|
||||||
|
import androidx.media2.session.SessionCommandGroup;
|
||||||
|
import androidx.media2.session.SessionResult;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.ext.media2.SessionCallbackBuilder.AllowedCommandProvider;
|
||||||
|
import com.google.android.exoplayer2.ext.media2.SessionCallbackBuilder.CustomCommandProvider;
|
||||||
|
import com.google.android.exoplayer2.ext.media2.SessionCallbackBuilder.DisconnectedCallback;
|
||||||
|
import com.google.android.exoplayer2.ext.media2.SessionCallbackBuilder.MediaItemProvider;
|
||||||
|
import com.google.android.exoplayer2.ext.media2.SessionCallbackBuilder.PostConnectCallback;
|
||||||
|
import com.google.android.exoplayer2.ext.media2.SessionCallbackBuilder.RatingCallback;
|
||||||
|
import com.google.android.exoplayer2.ext.media2.SessionCallbackBuilder.SkipCallback;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.android.exoplayer2.util.Log;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
/* package */ class SessionCallback extends MediaSession.SessionCallback {
|
||||||
|
private static final String TAG = "SessionCallback";
|
||||||
|
|
||||||
|
private final SessionPlayer sessionPlayer;
|
||||||
|
private final int fastForwardMs;
|
||||||
|
private final int rewindMs;
|
||||||
|
private final int seekTimeoutMs;
|
||||||
|
private final Set<MediaSession> sessions;
|
||||||
|
private final AllowedCommandProvider allowedCommandProvider;
|
||||||
|
@Nullable private final RatingCallback ratingCallback;
|
||||||
|
@Nullable private final CustomCommandProvider customCommandProvider;
|
||||||
|
@Nullable private final MediaItemProvider mediaItemProvider;
|
||||||
|
@Nullable private final SkipCallback skipCallback;
|
||||||
|
@Nullable private final PostConnectCallback postConnectCallback;
|
||||||
|
@Nullable private final DisconnectedCallback disconnectedCallback;
|
||||||
|
private boolean loggedUnexpectedSessionPlayerWarning;
|
||||||
|
|
||||||
|
public SessionCallback(
|
||||||
|
SessionPlayerConnector sessionPlayerConnector,
|
||||||
|
int fastForwardMs,
|
||||||
|
int rewindMs,
|
||||||
|
int seekTimeoutMs,
|
||||||
|
AllowedCommandProvider allowedCommandProvider,
|
||||||
|
@Nullable RatingCallback ratingCallback,
|
||||||
|
@Nullable CustomCommandProvider customCommandProvider,
|
||||||
|
@Nullable MediaItemProvider mediaItemProvider,
|
||||||
|
@Nullable SkipCallback skipCallback,
|
||||||
|
@Nullable PostConnectCallback postConnectCallback,
|
||||||
|
@Nullable DisconnectedCallback disconnectedCallback) {
|
||||||
|
this.sessionPlayer = sessionPlayerConnector;
|
||||||
|
this.allowedCommandProvider = allowedCommandProvider;
|
||||||
|
this.ratingCallback = ratingCallback;
|
||||||
|
this.customCommandProvider = customCommandProvider;
|
||||||
|
this.mediaItemProvider = mediaItemProvider;
|
||||||
|
this.skipCallback = skipCallback;
|
||||||
|
this.postConnectCallback = postConnectCallback;
|
||||||
|
this.disconnectedCallback = disconnectedCallback;
|
||||||
|
this.fastForwardMs = fastForwardMs;
|
||||||
|
this.rewindMs = rewindMs;
|
||||||
|
this.seekTimeoutMs = seekTimeoutMs;
|
||||||
|
this.sessions = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||||
|
|
||||||
|
// Register PlayerCallback and make it to be called before the ListenableFuture set the result.
|
||||||
|
// It help the PlayerCallback to update allowed commands before pended Player APIs are executed.
|
||||||
|
sessionPlayerConnector.registerPlayerCallback(Runnable::run, new PlayerCallback());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public SessionCommandGroup onConnect(
|
||||||
|
MediaSession session, MediaSession.ControllerInfo controllerInfo) {
|
||||||
|
sessions.add(session);
|
||||||
|
if (!allowedCommandProvider.acceptConnection(session, controllerInfo)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
SessionCommandGroup baseAllowedCommands = buildAllowedCommands(session, controllerInfo);
|
||||||
|
return allowedCommandProvider.getAllowedCommands(session, controllerInfo, baseAllowedCommands);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPostConnect(
|
||||||
|
@NonNull MediaSession session, @NonNull MediaSession.ControllerInfo controller) {
|
||||||
|
if (postConnectCallback != null) {
|
||||||
|
postConnectCallback.onPostConnect(session, controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisconnected(MediaSession session, MediaSession.ControllerInfo controller) {
|
||||||
|
if (session.getConnectedControllers().isEmpty()) {
|
||||||
|
sessions.remove(session);
|
||||||
|
}
|
||||||
|
if (disconnectedCallback != null) {
|
||||||
|
disconnectedCallback.onDisconnected(session, controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onCommandRequest(
|
||||||
|
MediaSession session, MediaSession.ControllerInfo controller, SessionCommand command) {
|
||||||
|
return allowedCommandProvider.onCommandRequest(session, controller, command);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public MediaItem onCreateMediaItem(
|
||||||
|
MediaSession session, MediaSession.ControllerInfo controller, String mediaId) {
|
||||||
|
Assertions.checkNotNull(mediaItemProvider);
|
||||||
|
return mediaItemProvider.onCreateMediaItem(session, controller, mediaId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onSetRating(
|
||||||
|
MediaSession session, MediaSession.ControllerInfo controller, String mediaId, Rating rating) {
|
||||||
|
if (ratingCallback != null) {
|
||||||
|
return ratingCallback.onSetRating(session, controller, mediaId, rating);
|
||||||
|
}
|
||||||
|
return SessionResult.RESULT_ERROR_NOT_SUPPORTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SessionResult onCustomCommand(
|
||||||
|
MediaSession session,
|
||||||
|
MediaSession.ControllerInfo controller,
|
||||||
|
SessionCommand customCommand,
|
||||||
|
@Nullable Bundle args) {
|
||||||
|
if (customCommandProvider != null) {
|
||||||
|
return customCommandProvider.onCustomCommand(session, controller, customCommand, args);
|
||||||
|
}
|
||||||
|
return new SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onFastForward(MediaSession session, MediaSession.ControllerInfo controller) {
|
||||||
|
if (fastForwardMs > 0) {
|
||||||
|
return seekToOffset(fastForwardMs);
|
||||||
|
}
|
||||||
|
return SessionResult.RESULT_ERROR_NOT_SUPPORTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onRewind(MediaSession session, MediaSession.ControllerInfo controller) {
|
||||||
|
if (rewindMs > 0) {
|
||||||
|
return seekToOffset(-rewindMs);
|
||||||
|
}
|
||||||
|
return SessionResult.RESULT_ERROR_NOT_SUPPORTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onSkipBackward(
|
||||||
|
@NonNull MediaSession session, @NonNull MediaSession.ControllerInfo controller) {
|
||||||
|
if (skipCallback != null) {
|
||||||
|
return skipCallback.onSkipBackward(session, controller);
|
||||||
|
}
|
||||||
|
return SessionResult.RESULT_ERROR_NOT_SUPPORTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onSkipForward(
|
||||||
|
@NonNull MediaSession session, @NonNull MediaSession.ControllerInfo controller) {
|
||||||
|
if (skipCallback != null) {
|
||||||
|
return skipCallback.onSkipForward(session, controller);
|
||||||
|
}
|
||||||
|
return SessionResult.RESULT_ERROR_NOT_SUPPORTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int seekToOffset(long offsetMs) {
|
||||||
|
long positionMs = sessionPlayer.getCurrentPosition() + offsetMs;
|
||||||
|
long durationMs = sessionPlayer.getDuration();
|
||||||
|
if (durationMs != C.TIME_UNSET) {
|
||||||
|
positionMs = Math.min(positionMs, durationMs);
|
||||||
|
}
|
||||||
|
positionMs = Math.max(positionMs, 0);
|
||||||
|
|
||||||
|
ListenableFuture<SessionPlayer.PlayerResult> result = sessionPlayer.seekTo(positionMs);
|
||||||
|
try {
|
||||||
|
if (seekTimeoutMs <= 0) {
|
||||||
|
return result.get().getResultCode();
|
||||||
|
}
|
||||||
|
return result.get(seekTimeoutMs, TimeUnit.MILLISECONDS).getResultCode();
|
||||||
|
} catch (ExecutionException | InterruptedException | TimeoutException e) {
|
||||||
|
Log.w(TAG, "Failed to get the seeking result", e);
|
||||||
|
return SessionResult.RESULT_ERROR_UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SessionCommandGroup buildAllowedCommands(
|
||||||
|
MediaSession session, MediaSession.ControllerInfo controllerInfo) {
|
||||||
|
SessionCommandGroup.Builder build;
|
||||||
|
@Nullable
|
||||||
|
SessionCommandGroup commands =
|
||||||
|
(customCommandProvider != null)
|
||||||
|
? customCommandProvider.getCustomCommands(session, controllerInfo)
|
||||||
|
: null;
|
||||||
|
if (commands != null) {
|
||||||
|
build = new SessionCommandGroup.Builder(commands);
|
||||||
|
} else {
|
||||||
|
build = new SessionCommandGroup.Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
build.addAllPredefinedCommands(SessionCommand.COMMAND_VERSION_1);
|
||||||
|
// TODO: Use removeCommand(int) when it's added [Internal: b/142848015].
|
||||||
|
if (mediaItemProvider == null) {
|
||||||
|
build.removeCommand(new SessionCommand(SessionCommand.COMMAND_CODE_PLAYER_SET_MEDIA_ITEM));
|
||||||
|
build.removeCommand(new SessionCommand(SessionCommand.COMMAND_CODE_PLAYER_SET_PLAYLIST));
|
||||||
|
build.removeCommand(new SessionCommand(SessionCommand.COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM));
|
||||||
|
build.removeCommand(
|
||||||
|
new SessionCommand(SessionCommand.COMMAND_CODE_PLAYER_REPLACE_PLAYLIST_ITEM));
|
||||||
|
}
|
||||||
|
if (ratingCallback == null) {
|
||||||
|
build.removeCommand(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING));
|
||||||
|
}
|
||||||
|
if (skipCallback == null) {
|
||||||
|
build.removeCommand(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SKIP_BACKWARD));
|
||||||
|
build.removeCommand(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SKIP_FORWARD));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply player's capability.
|
||||||
|
// Check whether the session has unexpectedly changed the player.
|
||||||
|
if (session.getPlayer() instanceof SessionPlayerConnector) {
|
||||||
|
SessionPlayerConnector sessionPlayerConnector = (SessionPlayerConnector) session.getPlayer();
|
||||||
|
|
||||||
|
// Check whether skipTo* works.
|
||||||
|
if (!sessionPlayerConnector.canSkipToPlaylistItem()) {
|
||||||
|
build.removeCommand(
|
||||||
|
new SessionCommand(SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_PLAYLIST_ITEM));
|
||||||
|
}
|
||||||
|
if (!sessionPlayerConnector.canSkipToPreviousPlaylistItem()) {
|
||||||
|
build.removeCommand(
|
||||||
|
new SessionCommand(SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM));
|
||||||
|
}
|
||||||
|
if (!sessionPlayerConnector.canSkipToNextPlaylistItem()) {
|
||||||
|
build.removeCommand(
|
||||||
|
new SessionCommand(SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether seekTo/rewind/fastForward works.
|
||||||
|
if (!sessionPlayerConnector.isCurrentMediaItemSeekable()) {
|
||||||
|
build.removeCommand(new SessionCommand(SessionCommand.COMMAND_CODE_PLAYER_SEEK_TO));
|
||||||
|
build.removeCommand(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_FAST_FORWARD));
|
||||||
|
build.removeCommand(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_REWIND));
|
||||||
|
} else {
|
||||||
|
if (fastForwardMs <= 0) {
|
||||||
|
build.removeCommand(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_FAST_FORWARD));
|
||||||
|
}
|
||||||
|
if (rewindMs <= 0) {
|
||||||
|
build.removeCommand(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_REWIND));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!loggedUnexpectedSessionPlayerWarning) {
|
||||||
|
// This can happen if MediaSession#updatePlayer() is called.
|
||||||
|
Log.e(TAG, "SessionPlayer isn't a SessionPlayerConnector. Guess the allowed command.");
|
||||||
|
loggedUnexpectedSessionPlayerWarning = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fastForwardMs <= 0) {
|
||||||
|
build.removeCommand(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_FAST_FORWARD));
|
||||||
|
}
|
||||||
|
if (rewindMs <= 0) {
|
||||||
|
build.removeCommand(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_REWIND));
|
||||||
|
}
|
||||||
|
@Nullable List<MediaItem> playlist = sessionPlayer.getPlaylist();
|
||||||
|
if (playlist == null) {
|
||||||
|
build.removeCommand(
|
||||||
|
new SessionCommand(SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM));
|
||||||
|
build.removeCommand(
|
||||||
|
new SessionCommand(SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM));
|
||||||
|
build.removeCommand(
|
||||||
|
new SessionCommand(SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_PLAYLIST_ITEM));
|
||||||
|
} else {
|
||||||
|
if (playlist.isEmpty()
|
||||||
|
&& (sessionPlayer.getRepeatMode() == SessionPlayer.REPEAT_MODE_NONE
|
||||||
|
|| sessionPlayer.getRepeatMode() == SessionPlayer.REPEAT_MODE_ONE)) {
|
||||||
|
build.removeCommand(
|
||||||
|
new SessionCommand(
|
||||||
|
SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM));
|
||||||
|
}
|
||||||
|
if (playlist.size() == sessionPlayer.getCurrentMediaItemIndex() + 1
|
||||||
|
&& (sessionPlayer.getRepeatMode() == SessionPlayer.REPEAT_MODE_NONE
|
||||||
|
|| sessionPlayer.getRepeatMode() == SessionPlayer.REPEAT_MODE_ONE)) {
|
||||||
|
build.removeCommand(
|
||||||
|
new SessionCommand(SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM));
|
||||||
|
}
|
||||||
|
if (playlist.size() <= 1) {
|
||||||
|
build.removeCommand(
|
||||||
|
new SessionCommand(SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_PLAYLIST_ITEM));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return build.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isBufferedState(/* @SessionPlayer.BuffState */ int buffState) {
|
||||||
|
return buffState == SessionPlayer.BUFFERING_STATE_BUFFERING_AND_PLAYABLE
|
||||||
|
|| buffState == SessionPlayer.BUFFERING_STATE_COMPLETE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class PlayerCallback extends SessionPlayer.PlayerCallback {
|
||||||
|
private boolean currentMediaItemBuffered;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlaylistChanged(
|
||||||
|
SessionPlayer player, @Nullable List<MediaItem> list, @Nullable MediaMetadata metadata) {
|
||||||
|
updateAllowedCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayerStateChanged(SessionPlayer player, int playerState) {
|
||||||
|
updateAllowedCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRepeatModeChanged(SessionPlayer player, int repeatMode) {
|
||||||
|
updateAllowedCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onShuffleModeChanged(SessionPlayer player, int shuffleMode) {
|
||||||
|
updateAllowedCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCurrentMediaItemChanged(SessionPlayer player, MediaItem item) {
|
||||||
|
currentMediaItemBuffered = isBufferedState(player.getBufferingState());
|
||||||
|
updateAllowedCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBufferingStateChanged(
|
||||||
|
SessionPlayer player, @Nullable MediaItem item, int buffState) {
|
||||||
|
if (currentMediaItemBuffered || player.getCurrentMediaItem() != item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isBufferedState(buffState)) {
|
||||||
|
currentMediaItemBuffered = true;
|
||||||
|
updateAllowedCommands();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAllowedCommands() {
|
||||||
|
for (MediaSession session : sessions) {
|
||||||
|
List<MediaSession.ControllerInfo> connectedControllers = session.getConnectedControllers();
|
||||||
|
for (MediaSession.ControllerInfo controller : connectedControllers) {
|
||||||
|
SessionCommandGroup baseAllowedCommands = buildAllowedCommands(session, controller);
|
||||||
|
SessionCommandGroup allowedCommands =
|
||||||
|
allowedCommandProvider.getAllowedCommands(session, controller, baseAllowedCommands);
|
||||||
|
if (allowedCommands == null) {
|
||||||
|
allowedCommands = new SessionCommandGroup.Builder().build();
|
||||||
|
}
|
||||||
|
session.setAllowedCommands(controller, allowedCommands);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,563 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.media2;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.provider.Settings;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media.MediaSessionManager;
|
||||||
|
import androidx.media.MediaSessionManager.RemoteUserInfo;
|
||||||
|
import androidx.media2.common.MediaItem;
|
||||||
|
import androidx.media2.common.MediaMetadata;
|
||||||
|
import androidx.media2.common.Rating;
|
||||||
|
import androidx.media2.common.SessionPlayer;
|
||||||
|
import androidx.media2.common.UriMediaItem;
|
||||||
|
import androidx.media2.session.MediaController;
|
||||||
|
import androidx.media2.session.MediaSession;
|
||||||
|
import androidx.media2.session.MediaSession.ControllerInfo;
|
||||||
|
import androidx.media2.session.SessionCommand;
|
||||||
|
import androidx.media2.session.SessionCommandGroup;
|
||||||
|
import androidx.media2.session.SessionResult;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds {@link MediaSession.SessionCallback} with various collaborators.
|
||||||
|
*
|
||||||
|
* @see MediaSession.SessionCallback
|
||||||
|
*/
|
||||||
|
public final class SessionCallbackBuilder {
|
||||||
|
/** Default timeout value for {@link #setSeekTimeoutMs}. */
|
||||||
|
public static final int DEFAULT_SEEK_TIMEOUT_MS = 1_000;
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final SessionPlayerConnector sessionPlayerConnector;
|
||||||
|
private int fastForwardMs;
|
||||||
|
private int rewindMs;
|
||||||
|
private int seekTimeoutMs;
|
||||||
|
@Nullable private RatingCallback ratingCallback;
|
||||||
|
@Nullable private CustomCommandProvider customCommandProvider;
|
||||||
|
@Nullable private MediaItemProvider mediaItemProvider;
|
||||||
|
@Nullable private AllowedCommandProvider allowedCommandProvider;
|
||||||
|
@Nullable private SkipCallback skipCallback;
|
||||||
|
@Nullable private PostConnectCallback postConnectCallback;
|
||||||
|
@Nullable private DisconnectedCallback disconnectedCallback;
|
||||||
|
|
||||||
|
/** Provides allowed commands for {@link MediaController}. */
|
||||||
|
public interface AllowedCommandProvider {
|
||||||
|
/**
|
||||||
|
* Called to query whether to allow connection from the controller.
|
||||||
|
*
|
||||||
|
* <p>If it returns {@code true} to accept connection, then {@link #getAllowedCommands} will be
|
||||||
|
* immediately followed to return initial allowed command.
|
||||||
|
*
|
||||||
|
* <p>Prefer use {@link PostConnectCallback} for any extra initialization about controller,
|
||||||
|
* where controller is connected and session can send commands to the controller.
|
||||||
|
*
|
||||||
|
* @param session The media session.
|
||||||
|
* @param controllerInfo The {@link ControllerInfo} for the controller that is requesting
|
||||||
|
* connect.
|
||||||
|
* @return {@code true} to accept connection. {@code false} otherwise.
|
||||||
|
*/
|
||||||
|
boolean acceptConnection(MediaSession session, ControllerInfo controllerInfo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to query allowed commands in following cases:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>A {@link MediaController} requests to connect, and allowed commands is required to tell
|
||||||
|
* initial allowed commands.
|
||||||
|
* <li>Underlying {@link SessionPlayer} state changes, and allowed commands may be updated via
|
||||||
|
* {@link MediaSession#setAllowedCommands}.
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>The provided {@code baseAllowedSessionCommand} is built automatically based on the state
|
||||||
|
* of the {@link SessionPlayer}, {@link RatingCallback}, {@link MediaItemProvider}, {@link
|
||||||
|
* CustomCommandProvider}, and {@link SkipCallback} so may be a useful starting point for any
|
||||||
|
* required customizations.
|
||||||
|
*
|
||||||
|
* @param session The media session.
|
||||||
|
* @param controllerInfo The {@link ControllerInfo} for the controller for which allowed
|
||||||
|
* commands are being queried.
|
||||||
|
* @param baseAllowedSessionCommand Base allowed session commands for customization.
|
||||||
|
* @return The allowed commands for the controller.
|
||||||
|
* @see MediaSession.SessionCallback#onConnect(MediaSession, ControllerInfo)
|
||||||
|
*/
|
||||||
|
SessionCommandGroup getAllowedCommands(
|
||||||
|
MediaSession session,
|
||||||
|
ControllerInfo controllerInfo,
|
||||||
|
SessionCommandGroup baseAllowedSessionCommand);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a {@link MediaController} has called an API that controls {@link SessionPlayer}
|
||||||
|
* set to the {@link MediaSession}.
|
||||||
|
*
|
||||||
|
* @param session The media session.
|
||||||
|
* @param controllerInfo A {@link ControllerInfo} that needs allowed command update.
|
||||||
|
* @param command A {@link SessionCommand} from the controller.
|
||||||
|
* @return A session result code defined in {@link SessionResult}.
|
||||||
|
* @see MediaSession.SessionCallback#onCommandRequest
|
||||||
|
*/
|
||||||
|
int onCommandRequest(
|
||||||
|
MediaSession session, ControllerInfo controllerInfo, SessionCommand command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Callback receiving a user rating for a specified media id. */
|
||||||
|
public interface RatingCallback {
|
||||||
|
/**
|
||||||
|
* Called when the specified controller has set a rating for the specified media id.
|
||||||
|
*
|
||||||
|
* @see MediaSession.SessionCallback#onSetRating(MediaSession, MediaSession.ControllerInfo,
|
||||||
|
* String, Rating)
|
||||||
|
* @see androidx.media2.session.MediaController#setRating(String, Rating)
|
||||||
|
* @return One of the {@link SessionResult} {@code RESULT_*} constants describing the success or
|
||||||
|
* failure of the operation, for example, {@link SessionResult#RESULT_SUCCESS} if the
|
||||||
|
* operation succeeded.
|
||||||
|
*/
|
||||||
|
int onSetRating(MediaSession session, ControllerInfo controller, String mediaId, Rating rating);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callbacks for querying what custom commands are supported, and for handling a custom command
|
||||||
|
* when a controller sends it.
|
||||||
|
*/
|
||||||
|
public interface CustomCommandProvider {
|
||||||
|
/**
|
||||||
|
* Called when a controller has sent a custom command.
|
||||||
|
*
|
||||||
|
* @param session The media session.
|
||||||
|
* @param controllerInfo The {@link ControllerInfo} for the controller that sent the custom
|
||||||
|
* command.
|
||||||
|
* @param customCommand A {@link SessionCommand} from the controller.
|
||||||
|
* @param args A {@link Bundle} with the extra argument.
|
||||||
|
* @see MediaSession.SessionCallback#onCustomCommand(MediaSession, MediaSession.ControllerInfo,
|
||||||
|
* SessionCommand, Bundle)
|
||||||
|
* @see androidx.media2.session.MediaController#sendCustomCommand(SessionCommand, Bundle)
|
||||||
|
*/
|
||||||
|
SessionResult onCustomCommand(
|
||||||
|
MediaSession session,
|
||||||
|
ControllerInfo controllerInfo,
|
||||||
|
SessionCommand customCommand,
|
||||||
|
@Nullable Bundle args);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@link SessionCommandGroup} with custom commands to publish to the controller, or
|
||||||
|
* {@code null} if no custom commands should be published.
|
||||||
|
*
|
||||||
|
* @param session The media session.
|
||||||
|
* @param controllerInfo The {@link ControllerInfo} for the controller that is requesting custom
|
||||||
|
* commands.
|
||||||
|
* @return The custom commands to publish, or {@code null} if no custom commands should be
|
||||||
|
* published.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
SessionCommandGroup getCustomCommands(MediaSession session, ControllerInfo controllerInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Provides the {@link MediaItem}. */
|
||||||
|
public interface MediaItemProvider {
|
||||||
|
/**
|
||||||
|
* Called when {@link MediaSession.SessionCallback#onCreateMediaItem(MediaSession,
|
||||||
|
* ControllerInfo, String)} is called.
|
||||||
|
*
|
||||||
|
* @param session The media session.
|
||||||
|
* @param controllerInfo The {@link ControllerInfo} for the controller that has requested to
|
||||||
|
* create the item.
|
||||||
|
* @return A new {@link MediaItem} that {@link SessionPlayerConnector} can play.
|
||||||
|
* @see MediaSession.SessionCallback#onCreateMediaItem(MediaSession, ControllerInfo, String)
|
||||||
|
* @see androidx.media2.session.MediaController#addPlaylistItem(int, String)
|
||||||
|
* @see androidx.media2.session.MediaController#replacePlaylistItem(int, String)
|
||||||
|
* @see androidx.media2.session.MediaController#setMediaItem(String)
|
||||||
|
* @see androidx.media2.session.MediaController#setPlaylist(List, MediaMetadata)
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
MediaItem onCreateMediaItem(
|
||||||
|
MediaSession session, ControllerInfo controllerInfo, String mediaId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Callback receiving skip backward and skip forward. */
|
||||||
|
public interface SkipCallback {
|
||||||
|
/**
|
||||||
|
* Called when the specified controller has sent skip backward.
|
||||||
|
*
|
||||||
|
* @param session The media session.
|
||||||
|
* @param controllerInfo The {@link ControllerInfo} for the controller that has requested to
|
||||||
|
* skip backward.
|
||||||
|
* @see MediaSession.SessionCallback#onSkipBackward(MediaSession, MediaSession.ControllerInfo)
|
||||||
|
* @see MediaController#skipBackward()
|
||||||
|
* @return One of the {@link SessionResult} {@code RESULT_*} constants describing the success or
|
||||||
|
* failure of the operation, for example, {@link SessionResult#RESULT_SUCCESS} if the
|
||||||
|
* operation succeeded.
|
||||||
|
*/
|
||||||
|
int onSkipBackward(MediaSession session, ControllerInfo controllerInfo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the specified controller has sent skip forward.
|
||||||
|
*
|
||||||
|
* @param session The media session.
|
||||||
|
* @param controllerInfo The {@link ControllerInfo} for the controller that has requested to
|
||||||
|
* skip forward.
|
||||||
|
* @see MediaSession.SessionCallback#onSkipForward(MediaSession, MediaSession.ControllerInfo)
|
||||||
|
* @see MediaController#skipForward()
|
||||||
|
* @return One of the {@link SessionResult} {@code RESULT_*} constants describing the success or
|
||||||
|
* failure of the operation, for example, {@link SessionResult#RESULT_SUCCESS} if the
|
||||||
|
* operation succeeded.
|
||||||
|
*/
|
||||||
|
int onSkipForward(MediaSession session, ControllerInfo controllerInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Callback for handling extra initialization after the connection. */
|
||||||
|
public interface PostConnectCallback {
|
||||||
|
/**
|
||||||
|
* Called after the specified controller is connected, and you need extra initialization.
|
||||||
|
*
|
||||||
|
* @param session The media session.
|
||||||
|
* @param controllerInfo The {@link ControllerInfo} for the controller that just connected.
|
||||||
|
* @see MediaSession.SessionCallback#onPostConnect(MediaSession, ControllerInfo)
|
||||||
|
*/
|
||||||
|
void onPostConnect(MediaSession session, MediaSession.ControllerInfo controllerInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Callback for handling controller disconnection. */
|
||||||
|
public interface DisconnectedCallback {
|
||||||
|
/**
|
||||||
|
* Called when the specified controller is disconnected.
|
||||||
|
*
|
||||||
|
* @param session The media session.
|
||||||
|
* @param controllerInfo The {@link ControllerInfo} for the disconnected controller.
|
||||||
|
* @see MediaSession.SessionCallback#onDisconnected(MediaSession, ControllerInfo)
|
||||||
|
*/
|
||||||
|
void onDisconnected(MediaSession session, MediaSession.ControllerInfo controllerInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of {@link AllowedCommandProvider} that behaves as follows:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Accepts connection requests from controller if any of the following conditions are met:
|
||||||
|
* <ul>
|
||||||
|
* <li>Controller is in the same package as the session.
|
||||||
|
* <li>Controller is allowed via {@link #setTrustedPackageNames(List)}.
|
||||||
|
* <li>Controller has package name {@link RemoteUserInfo#LEGACY_CONTROLLER}. See {@link
|
||||||
|
* ControllerInfo#getPackageName() package name limitation} for details.
|
||||||
|
* <li>Controller is trusted (i.e. has MEDIA_CONTENT_CONTROL permission or has enabled
|
||||||
|
* notification manager).
|
||||||
|
* </ul>
|
||||||
|
* <li>Allows all commands that the current player can handle.
|
||||||
|
* <li>Accepts all command requests for allowed commands.
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Note: this implementation matches the behavior of the ExoPlayer MediaSession extension and
|
||||||
|
* {@link android.support.v4.media.session.MediaSessionCompat}.
|
||||||
|
*/
|
||||||
|
public static final class DefaultAllowedCommandProvider implements AllowedCommandProvider {
|
||||||
|
private final Context context;
|
||||||
|
private final List<String> trustedPackageNames;
|
||||||
|
|
||||||
|
public DefaultAllowedCommandProvider(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
trustedPackageNames = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean acceptConnection(MediaSession session, ControllerInfo controllerInfo) {
|
||||||
|
return TextUtils.equals(controllerInfo.getPackageName(), context.getPackageName())
|
||||||
|
|| TextUtils.equals(controllerInfo.getPackageName(), RemoteUserInfo.LEGACY_CONTROLLER)
|
||||||
|
|| trustedPackageNames.contains(controllerInfo.getPackageName())
|
||||||
|
|| isTrusted(controllerInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SessionCommandGroup getAllowedCommands(
|
||||||
|
MediaSession session,
|
||||||
|
ControllerInfo controllerInfo,
|
||||||
|
SessionCommandGroup baseAllowedSessionCommands) {
|
||||||
|
return baseAllowedSessionCommands;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onCommandRequest(
|
||||||
|
MediaSession session, ControllerInfo controllerInfo, SessionCommand command) {
|
||||||
|
return SessionResult.RESULT_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the package names from which the session will accept incoming connections.
|
||||||
|
*
|
||||||
|
* <p>Apps that have {@code android.Manifest.permission.MEDIA_CONTENT_CONTROL}, packages listed
|
||||||
|
* in enabled_notification_listeners and the current package are always trusted, even if they
|
||||||
|
* are not specified here.
|
||||||
|
*
|
||||||
|
* @param packageNames Package names from which the session will accept incoming connections.
|
||||||
|
* @see MediaSession.SessionCallback#onConnect(MediaSession, MediaSession.ControllerInfo)
|
||||||
|
* @see MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo)
|
||||||
|
*/
|
||||||
|
public void setTrustedPackageNames(@Nullable List<String> packageNames) {
|
||||||
|
trustedPackageNames.clear();
|
||||||
|
if (packageNames != null && !packageNames.isEmpty()) {
|
||||||
|
trustedPackageNames.addAll(packageNames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Replace with ControllerInfo#isTrusted() when it's unhidden [Internal: b/142835448].
|
||||||
|
private boolean isTrusted(MediaSession.ControllerInfo controllerInfo) {
|
||||||
|
// Check whether the controller has granted MEDIA_CONTENT_CONTROL.
|
||||||
|
if (context
|
||||||
|
.getPackageManager()
|
||||||
|
.checkPermission(
|
||||||
|
Manifest.permission.MEDIA_CONTENT_CONTROL, controllerInfo.getPackageName())
|
||||||
|
== PackageManager.PERMISSION_GRANTED) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether the app has an enabled notification listener.
|
||||||
|
String enabledNotificationListeners =
|
||||||
|
Settings.Secure.getString(context.getContentResolver(), "enabled_notification_listeners");
|
||||||
|
if (!TextUtils.isEmpty(enabledNotificationListeners)) {
|
||||||
|
String[] components = enabledNotificationListeners.split(":");
|
||||||
|
for (String componentString : components) {
|
||||||
|
@Nullable ComponentName component = ComponentName.unflattenFromString(componentString);
|
||||||
|
if (component != null) {
|
||||||
|
if (component.getPackageName().equals(controllerInfo.getPackageName())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of {@link MediaItemProvider} that assumes the media id is a URI string.
|
||||||
|
*/
|
||||||
|
public static final class DefaultMediaItemProvider implements MediaItemProvider {
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public MediaItem onCreateMediaItem(
|
||||||
|
MediaSession session, ControllerInfo controllerInfo, String mediaId) {
|
||||||
|
if (TextUtils.isEmpty(mediaId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
new URI(mediaId);
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
// Ignore if mediaId isn't a URI.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
MediaMetadata metadata =
|
||||||
|
new MediaMetadata.Builder()
|
||||||
|
.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, mediaId)
|
||||||
|
.build();
|
||||||
|
return new UriMediaItem.Builder(Uri.parse(mediaId)).setMetadata(metadata).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new builder.
|
||||||
|
*
|
||||||
|
* <p>The builder uses the following default values:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link AllowedCommandProvider}: {@link DefaultAllowedCommandProvider}
|
||||||
|
* <li>Seek timeout: {@link #DEFAULT_SEEK_TIMEOUT_MS}
|
||||||
|
* <li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* Unless stated above, {@code null} or {@code 0} would be used to disallow relevant features.
|
||||||
|
*
|
||||||
|
* @param context A context.
|
||||||
|
* @param sessionPlayerConnector A session player connector to handle incoming calls from the
|
||||||
|
* controller.
|
||||||
|
*/
|
||||||
|
public SessionCallbackBuilder(Context context, SessionPlayerConnector sessionPlayerConnector) {
|
||||||
|
this.context = Assertions.checkNotNull(context);
|
||||||
|
this.sessionPlayerConnector = Assertions.checkNotNull(sessionPlayerConnector);
|
||||||
|
this.seekTimeoutMs = DEFAULT_SEEK_TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link RatingCallback} to handle user ratings.
|
||||||
|
*
|
||||||
|
* @param ratingCallback A rating callback.
|
||||||
|
* @return This builder.
|
||||||
|
* @see MediaSession.SessionCallback#onSetRating(MediaSession, ControllerInfo, String, Rating)
|
||||||
|
* @see androidx.media2.session.MediaController#setRating(String, Rating)
|
||||||
|
*/
|
||||||
|
public SessionCallbackBuilder setRatingCallback(@Nullable RatingCallback ratingCallback) {
|
||||||
|
this.ratingCallback = ratingCallback;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link CustomCommandProvider} to handle incoming custom commands.
|
||||||
|
*
|
||||||
|
* @param customCommandProvider A custom command provider.
|
||||||
|
* @return This builder.
|
||||||
|
* @see MediaSession.SessionCallback#onCustomCommand(MediaSession, ControllerInfo, SessionCommand,
|
||||||
|
* Bundle)
|
||||||
|
* @see androidx.media2.session.MediaController#sendCustomCommand(SessionCommand, Bundle)
|
||||||
|
*/
|
||||||
|
public SessionCallbackBuilder setCustomCommandProvider(
|
||||||
|
@Nullable CustomCommandProvider customCommandProvider) {
|
||||||
|
this.customCommandProvider = customCommandProvider;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link MediaItemProvider} that will convert media ids to {@link MediaItem MediaItems}.
|
||||||
|
*
|
||||||
|
* @param mediaItemProvider The media item provider.
|
||||||
|
* @return This builder.
|
||||||
|
* @see MediaSession.SessionCallback#onCreateMediaItem(MediaSession, ControllerInfo, String)
|
||||||
|
* @see androidx.media2.session.MediaController#addPlaylistItem(int, String)
|
||||||
|
* @see androidx.media2.session.MediaController#replacePlaylistItem(int, String)
|
||||||
|
* @see androidx.media2.session.MediaController#setMediaItem(String)
|
||||||
|
* @see androidx.media2.session.MediaController#setPlaylist(List, MediaMetadata)
|
||||||
|
*/
|
||||||
|
public SessionCallbackBuilder setMediaItemProvider(
|
||||||
|
@Nullable MediaItemProvider mediaItemProvider) {
|
||||||
|
this.mediaItemProvider = mediaItemProvider;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link AllowedCommandProvider} to provide allowed commands for controllers.
|
||||||
|
*
|
||||||
|
* @param allowedCommandProvider A allowed command provider.
|
||||||
|
* @return This builder.
|
||||||
|
*/
|
||||||
|
public SessionCallbackBuilder setAllowedCommandProvider(
|
||||||
|
@Nullable AllowedCommandProvider allowedCommandProvider) {
|
||||||
|
this.allowedCommandProvider = allowedCommandProvider;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link SkipCallback} to handle skip backward and skip forward.
|
||||||
|
*
|
||||||
|
* @param skipCallback The skip callback.
|
||||||
|
* @return This builder.
|
||||||
|
* @see MediaSession.SessionCallback#onSkipBackward(MediaSession, ControllerInfo)
|
||||||
|
* @see MediaSession.SessionCallback#onSkipForward(MediaSession, ControllerInfo)
|
||||||
|
* @see MediaController#skipBackward()
|
||||||
|
* @see MediaController#skipForward()
|
||||||
|
*/
|
||||||
|
public SessionCallbackBuilder setSkipCallback(@Nullable SkipCallback skipCallback) {
|
||||||
|
this.skipCallback = skipCallback;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link PostConnectCallback} to handle extra initialization after the connection.
|
||||||
|
*
|
||||||
|
* @param postConnectCallback The post connect callback.
|
||||||
|
* @return This builder.
|
||||||
|
* @see MediaSession.SessionCallback#onPostConnect(MediaSession, ControllerInfo)
|
||||||
|
*/
|
||||||
|
public SessionCallbackBuilder setPostConnectCallback(
|
||||||
|
@Nullable PostConnectCallback postConnectCallback) {
|
||||||
|
this.postConnectCallback = postConnectCallback;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link DisconnectedCallback} to handle cleaning up controller.
|
||||||
|
*
|
||||||
|
* @param disconnectedCallback The disconnected callback.
|
||||||
|
* @return This builder.
|
||||||
|
* @see MediaSession.SessionCallback#onDisconnected(MediaSession, ControllerInfo)
|
||||||
|
*/
|
||||||
|
public SessionCallbackBuilder setDisconnectedCallback(
|
||||||
|
@Nullable DisconnectedCallback disconnectedCallback) {
|
||||||
|
this.disconnectedCallback = disconnectedCallback;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the rewind increment in milliseconds.
|
||||||
|
*
|
||||||
|
* @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the
|
||||||
|
* rewind to be disabled.
|
||||||
|
* @return This builder.
|
||||||
|
* @see MediaSession.SessionCallback#onRewind(MediaSession, MediaSession.ControllerInfo)
|
||||||
|
* @see #setSeekTimeoutMs(int)
|
||||||
|
*/
|
||||||
|
public SessionCallbackBuilder setRewindIncrementMs(int rewindMs) {
|
||||||
|
this.rewindMs = rewindMs;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the fast forward increment in milliseconds.
|
||||||
|
*
|
||||||
|
* @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will
|
||||||
|
* cause the fast forward to be disabled.
|
||||||
|
* @return This builder.
|
||||||
|
* @see MediaSession.SessionCallback#onFastForward(MediaSession, MediaSession.ControllerInfo)
|
||||||
|
* @see #setSeekTimeoutMs(int)
|
||||||
|
*/
|
||||||
|
public SessionCallbackBuilder setFastForwardIncrementMs(int fastForwardMs) {
|
||||||
|
this.fastForwardMs = fastForwardMs;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the timeout in milliseconds for fast forward and rewind operations, or {@code 0} for no
|
||||||
|
* timeout. If a timeout is set, controllers will receive an error if the session's call to {@link
|
||||||
|
* SessionPlayer#seekTo} takes longer than this amount of time.
|
||||||
|
*
|
||||||
|
* @param seekTimeoutMs A timeout for {@link SessionPlayer#seekTo}. A non-positive value will wait
|
||||||
|
* forever.
|
||||||
|
* @return This builder.
|
||||||
|
*/
|
||||||
|
public SessionCallbackBuilder setSeekTimeoutMs(int seekTimeoutMs) {
|
||||||
|
this.seekTimeoutMs = seekTimeoutMs;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds {@link MediaSession.SessionCallback}.
|
||||||
|
*
|
||||||
|
* @return A new callback for a media session.
|
||||||
|
*/
|
||||||
|
public MediaSession.SessionCallback build() {
|
||||||
|
return new SessionCallback(
|
||||||
|
sessionPlayerConnector,
|
||||||
|
fastForwardMs,
|
||||||
|
rewindMs,
|
||||||
|
seekTimeoutMs,
|
||||||
|
allowedCommandProvider == null
|
||||||
|
? new DefaultAllowedCommandProvider(context)
|
||||||
|
: allowedCommandProvider,
|
||||||
|
ratingCallback,
|
||||||
|
customCommandProvider,
|
||||||
|
mediaItemProvider,
|
||||||
|
skipCallback,
|
||||||
|
postConnectCallback,
|
||||||
|
disconnectedCallback);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,764 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.media2;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
import androidx.annotation.FloatRange;
|
||||||
|
import androidx.annotation.GuardedBy;
|
||||||
|
import androidx.annotation.IntRange;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.util.ObjectsCompat;
|
||||||
|
import androidx.core.util.Pair;
|
||||||
|
import androidx.media.AudioAttributesCompat;
|
||||||
|
import androidx.media2.common.FileMediaItem;
|
||||||
|
import androidx.media2.common.MediaItem;
|
||||||
|
import androidx.media2.common.MediaMetadata;
|
||||||
|
import androidx.media2.common.SessionPlayer;
|
||||||
|
import com.google.android.exoplayer2.ControlDispatcher;
|
||||||
|
import com.google.android.exoplayer2.DefaultControlDispatcher;
|
||||||
|
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||||
|
import com.google.android.exoplayer2.PlaybackPreparer;
|
||||||
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import org.checkerframework.checker.initialization.qual.Initialized;
|
||||||
|
import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of {@link SessionPlayer} that wraps a given ExoPlayer {@link Player} instance.
|
||||||
|
*
|
||||||
|
* <h3>Ownership</h3>
|
||||||
|
*
|
||||||
|
* <p>{@code SessionPlayerConnector} takes ownership of the provided ExoPlayer {@link Player}
|
||||||
|
* instance between when it's constructed and when it's {@link #close() closed}. No other components
|
||||||
|
* should interact with the wrapped player (otherwise, unexpected event callbacks from the wrapped
|
||||||
|
* player may put the session player in an inconsistent state).
|
||||||
|
*
|
||||||
|
* <p>Call {@link SessionPlayer#close()} when the {@code SessionPlayerConnector} is no longer needed
|
||||||
|
* to regain ownership of the wrapped player. It is the caller's responsibility to release the
|
||||||
|
* wrapped player via {@link Player#release()}.
|
||||||
|
*
|
||||||
|
* <h3>Threading model</h3>
|
||||||
|
*
|
||||||
|
* <p>Internally this implementation posts operations to and receives callbacks on the thread
|
||||||
|
* associated with {@link Player#getApplicationLooper()}, so it is important not to block this
|
||||||
|
* thread. In particular, when awaiting the result of an asynchronous session player operation, apps
|
||||||
|
* should generally use {@link ListenableFuture#addListener(Runnable, Executor)} to be notified of
|
||||||
|
* completion, rather than calling the blocking {@link ListenableFuture#get()} method.
|
||||||
|
*/
|
||||||
|
public final class SessionPlayerConnector extends SessionPlayer {
|
||||||
|
|
||||||
|
static {
|
||||||
|
ExoPlayerLibraryInfo.registerModule("goog.exo.media2");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String TAG = "SessionPlayerConnector";
|
||||||
|
|
||||||
|
private static final int END_OF_PLAYLIST = -1;
|
||||||
|
private final Object stateLock = new Object();
|
||||||
|
|
||||||
|
private final PlayerHandler taskHandler;
|
||||||
|
private final Executor taskHandlerExecutor;
|
||||||
|
private final PlayerWrapper player;
|
||||||
|
private final PlayerCommandQueue playerCommandQueue;
|
||||||
|
|
||||||
|
@GuardedBy("stateLock")
|
||||||
|
private final Map<MediaItem, Integer> mediaItemToBuffState = new HashMap<>();
|
||||||
|
|
||||||
|
@GuardedBy("stateLock")
|
||||||
|
/* @PlayerState */
|
||||||
|
private int state;
|
||||||
|
|
||||||
|
@GuardedBy("stateLock")
|
||||||
|
private boolean closed;
|
||||||
|
|
||||||
|
// Should be only accessed on the executor, which is currently single-threaded.
|
||||||
|
@Nullable private MediaItem currentMediaItem;
|
||||||
|
@Nullable private List<MediaItem> currentPlaylist;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance using {@link DefaultControlDispatcher} to dispatch player commands.
|
||||||
|
*
|
||||||
|
* @param player The player to wrap.
|
||||||
|
* @param playlistManager The {@link PlaylistManager}.
|
||||||
|
* @param playbackPreparer The {@link PlaybackPreparer}.
|
||||||
|
*/
|
||||||
|
public SessionPlayerConnector(
|
||||||
|
Player player, PlaylistManager playlistManager, PlaybackPreparer playbackPreparer) {
|
||||||
|
this(player, playlistManager, playbackPreparer, new DefaultControlDispatcher());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance using the provided {@link ControlDispatcher} to dispatch player commands.
|
||||||
|
*
|
||||||
|
* @param player The player to wrap.
|
||||||
|
* @param playlistManager The {@link PlaylistManager}.
|
||||||
|
* @param playbackPreparer The {@link PlaybackPreparer}.
|
||||||
|
* @param controlDispatcher The {@link ControlDispatcher}.
|
||||||
|
*/
|
||||||
|
public SessionPlayerConnector(
|
||||||
|
Player player,
|
||||||
|
PlaylistManager playlistManager,
|
||||||
|
PlaybackPreparer playbackPreparer,
|
||||||
|
ControlDispatcher controlDispatcher) {
|
||||||
|
Assertions.checkNotNull(player);
|
||||||
|
Assertions.checkNotNull(playlistManager);
|
||||||
|
Assertions.checkNotNull(playbackPreparer);
|
||||||
|
Assertions.checkNotNull(controlDispatcher);
|
||||||
|
|
||||||
|
state = PLAYER_STATE_IDLE;
|
||||||
|
taskHandler = new PlayerHandler(player.getApplicationLooper());
|
||||||
|
taskHandlerExecutor = taskHandler::postOrRun;
|
||||||
|
ExoPlayerWrapperListener playerListener = new ExoPlayerWrapperListener();
|
||||||
|
PlayerWrapper playerWrapper =
|
||||||
|
new PlayerWrapper(
|
||||||
|
playerListener, player, playlistManager, playbackPreparer, controlDispatcher);
|
||||||
|
this.player = playerWrapper;
|
||||||
|
playerCommandQueue = new PlayerCommandQueue(this.player, taskHandler);
|
||||||
|
|
||||||
|
@SuppressWarnings("assignment.type.incompatible")
|
||||||
|
@Initialized
|
||||||
|
SessionPlayerConnector initializedThis = this;
|
||||||
|
initializedThis.<Void>runPlayerCallableBlocking(
|
||||||
|
/* callable= */ () -> {
|
||||||
|
playerWrapper.reset();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<PlayerResult> play() {
|
||||||
|
return playerCommandQueue.addCommand(
|
||||||
|
PlayerCommandQueue.COMMAND_CODE_PLAYER_PLAY, /* command= */ () -> player.play());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<PlayerResult> pause() {
|
||||||
|
return playerCommandQueue.addCommand(
|
||||||
|
PlayerCommandQueue.COMMAND_CODE_PLAYER_PAUSE, /* command= */ () -> player.pause());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<PlayerResult> prepare() {
|
||||||
|
return playerCommandQueue.addCommand(
|
||||||
|
PlayerCommandQueue.COMMAND_CODE_PLAYER_PREPARE, /* command= */ () -> player.prepare());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<PlayerResult> seekTo(long position) {
|
||||||
|
return playerCommandQueue.addCommand(
|
||||||
|
PlayerCommandQueue.COMMAND_CODE_PLAYER_SEEK_TO,
|
||||||
|
/* command= */ () -> player.seekTo(position),
|
||||||
|
/* tag= */ position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<PlayerResult> setPlaybackSpeed(
|
||||||
|
@FloatRange(from = 0.0f, to = Float.MAX_VALUE, fromInclusive = false) float playbackSpeed) {
|
||||||
|
Assertions.checkArgument(playbackSpeed > 0f);
|
||||||
|
return playerCommandQueue.addCommand(
|
||||||
|
PlayerCommandQueue.COMMAND_CODE_PLAYER_SET_SPEED,
|
||||||
|
/* command= */ () -> player.setPlaybackSpeed(playbackSpeed));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<PlayerResult> setAudioAttributes(AudioAttributesCompat attr) {
|
||||||
|
return playerCommandQueue.addCommand(
|
||||||
|
PlayerCommandQueue.COMMAND_CODE_PLAYER_SET_AUDIO_ATTRIBUTES,
|
||||||
|
/* command= */ () -> player.setAudioAttributes(Assertions.checkNotNull(attr)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
/* @PlayerState */
|
||||||
|
public int getPlayerState() {
|
||||||
|
synchronized (stateLock) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getCurrentPosition() {
|
||||||
|
long position =
|
||||||
|
runPlayerCallableBlocking(
|
||||||
|
/* callable= */ player::getCurrentPosition,
|
||||||
|
/* defaultValueWhenException= */ UNKNOWN_TIME);
|
||||||
|
return position >= 0 ? position : UNKNOWN_TIME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getDuration() {
|
||||||
|
long position =
|
||||||
|
runPlayerCallableBlocking(
|
||||||
|
/* callable= */ player::getDuration, /* defaultValueWhenException= */ UNKNOWN_TIME);
|
||||||
|
return position >= 0 ? position : UNKNOWN_TIME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getBufferedPosition() {
|
||||||
|
long position =
|
||||||
|
runPlayerCallableBlocking(
|
||||||
|
/* callable= */ player::getBufferedPosition,
|
||||||
|
/* defaultValueWhenException= */ UNKNOWN_TIME);
|
||||||
|
return position >= 0 ? position : UNKNOWN_TIME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
/* @BuffState */
|
||||||
|
public int getBufferingState() {
|
||||||
|
@Nullable
|
||||||
|
MediaItem mediaItem =
|
||||||
|
this.<@NullableType MediaItem>runPlayerCallableBlocking(
|
||||||
|
/* callable= */ player::getCurrentMediaItem, /* defaultValueWhenException= */ null);
|
||||||
|
if (mediaItem == null) {
|
||||||
|
return BUFFERING_STATE_UNKNOWN;
|
||||||
|
}
|
||||||
|
@Nullable Integer buffState;
|
||||||
|
synchronized (stateLock) {
|
||||||
|
buffState = mediaItemToBuffState.get(mediaItem);
|
||||||
|
}
|
||||||
|
return buffState == null ? BUFFERING_STATE_UNKNOWN : buffState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@FloatRange(from = 0.0f, to = Float.MAX_VALUE, fromInclusive = false)
|
||||||
|
public float getPlaybackSpeed() {
|
||||||
|
return runPlayerCallableBlocking(
|
||||||
|
/* callable= */ player::getPlaybackSpeed, /* defaultValueWhenException= */ 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public AudioAttributesCompat getAudioAttributes() {
|
||||||
|
return runPlayerCallableBlockingWithNullOnException(/* callable= */ player::getAudioAttributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<PlayerResult> setMediaItem(MediaItem item) {
|
||||||
|
Assertions.checkNotNull(item);
|
||||||
|
Assertions.checkArgument(!(item instanceof FileMediaItem));
|
||||||
|
ListenableFuture<PlayerResult> result =
|
||||||
|
playerCommandQueue.addCommand(
|
||||||
|
PlayerCommandQueue.COMMAND_CODE_PLAYER_SET_MEDIA_ITEM, () -> player.setMediaItem(item));
|
||||||
|
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<PlayerResult> setPlaylist(
|
||||||
|
final List<MediaItem> playlist, @Nullable MediaMetadata metadata) {
|
||||||
|
Assertions.checkNotNull(playlist);
|
||||||
|
Assertions.checkArgument(!playlist.isEmpty());
|
||||||
|
for (int i = 0; i < playlist.size(); i++) {
|
||||||
|
MediaItem item = playlist.get(i);
|
||||||
|
Assertions.checkNotNull(item);
|
||||||
|
Assertions.checkArgument(!(item instanceof FileMediaItem));
|
||||||
|
for (int j = 0; j < i; j++) {
|
||||||
|
Assertions.checkArgument(
|
||||||
|
item != playlist.get(j),
|
||||||
|
"playlist shouldn't contain duplicated item, index=" + i + " vs index=" + j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ListenableFuture<PlayerResult> result =
|
||||||
|
playerCommandQueue.addCommand(
|
||||||
|
PlayerCommandQueue.COMMAND_CODE_PLAYER_SET_PLAYLIST,
|
||||||
|
/* command= */ () -> player.setPlaylist(playlist, metadata));
|
||||||
|
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<PlayerResult> addPlaylistItem(int index, MediaItem item) {
|
||||||
|
Assertions.checkArgument(index >= 0);
|
||||||
|
Assertions.checkNotNull(item);
|
||||||
|
Assertions.checkArgument(!(item instanceof FileMediaItem));
|
||||||
|
ListenableFuture<PlayerResult> result =
|
||||||
|
playerCommandQueue.addCommand(
|
||||||
|
PlayerCommandQueue.COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM,
|
||||||
|
/* command= */ () -> player.addPlaylistItem(index, item));
|
||||||
|
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<PlayerResult> removePlaylistItem(@IntRange(from = 0) int index) {
|
||||||
|
Assertions.checkArgument(index >= 0);
|
||||||
|
ListenableFuture<PlayerResult> result =
|
||||||
|
playerCommandQueue.addCommand(
|
||||||
|
PlayerCommandQueue.COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM,
|
||||||
|
/* command= */ () -> player.removePlaylistItem(index));
|
||||||
|
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<PlayerResult> replacePlaylistItem(int index, MediaItem item) {
|
||||||
|
Assertions.checkArgument(index >= 0);
|
||||||
|
Assertions.checkNotNull(item);
|
||||||
|
Assertions.checkArgument(!(item instanceof FileMediaItem));
|
||||||
|
ListenableFuture<PlayerResult> result =
|
||||||
|
playerCommandQueue.addCommand(
|
||||||
|
PlayerCommandQueue.COMMAND_CODE_PLAYER_REPLACE_PLAYLIST_ITEM,
|
||||||
|
/* command= */ () -> player.replacePlaylistItem(index, item));
|
||||||
|
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<PlayerResult> skipToPreviousPlaylistItem() {
|
||||||
|
ListenableFuture<PlayerResult> result =
|
||||||
|
playerCommandQueue.addCommand(
|
||||||
|
PlayerCommandQueue.COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM,
|
||||||
|
/* command= */ player::skipToPreviousPlaylistItem);
|
||||||
|
result.addListener(this::notifySkipToCompletedOnHandler, taskHandlerExecutor);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<PlayerResult> skipToNextPlaylistItem() {
|
||||||
|
ListenableFuture<PlayerResult> result =
|
||||||
|
playerCommandQueue.addCommand(
|
||||||
|
PlayerCommandQueue.COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM,
|
||||||
|
/* command= */ player::skipToNextPlaylistItem);
|
||||||
|
result.addListener(this::notifySkipToCompletedOnHandler, taskHandlerExecutor);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<PlayerResult> skipToPlaylistItem(@IntRange(from = 0) int index) {
|
||||||
|
Assertions.checkArgument(index >= 0);
|
||||||
|
ListenableFuture<PlayerResult> result =
|
||||||
|
playerCommandQueue.addCommand(
|
||||||
|
PlayerCommandQueue.COMMAND_CODE_PLAYER_SKIP_TO_PLAYLIST_ITEM,
|
||||||
|
/* command= */ () -> player.skipToPlaylistItem(index));
|
||||||
|
result.addListener(this::notifySkipToCompletedOnHandler, taskHandlerExecutor);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<PlayerResult> updatePlaylistMetadata(@Nullable MediaMetadata metadata) {
|
||||||
|
return playerCommandQueue.addCommand(
|
||||||
|
PlayerCommandQueue.COMMAND_CODE_PLAYER_UPDATE_LIST_METADATA,
|
||||||
|
/* command= */ () -> {
|
||||||
|
boolean handled = player.updatePlaylistMetadata(metadata);
|
||||||
|
if (handled) {
|
||||||
|
notifySessionPlayerCallback(
|
||||||
|
callback ->
|
||||||
|
callback.onPlaylistMetadataChanged(SessionPlayerConnector.this, metadata));
|
||||||
|
}
|
||||||
|
return handled;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<PlayerResult> setRepeatMode(int repeatMode) {
|
||||||
|
return playerCommandQueue.addCommand(
|
||||||
|
PlayerCommandQueue.COMMAND_CODE_PLAYER_SET_REPEAT_MODE,
|
||||||
|
/* command= */ () -> player.setRepeatMode(repeatMode));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<PlayerResult> setShuffleMode(int shuffleMode) {
|
||||||
|
return playerCommandQueue.addCommand(
|
||||||
|
PlayerCommandQueue.COMMAND_CODE_PLAYER_SET_SHUFFLE_MODE,
|
||||||
|
/* command= */ () -> player.setShuffleMode(shuffleMode));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public List<MediaItem> getPlaylist() {
|
||||||
|
return runPlayerCallableBlockingWithNullOnException(/* callable= */ player::getPlaylist);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public MediaMetadata getPlaylistMetadata() {
|
||||||
|
return runPlayerCallableBlockingWithNullOnException(
|
||||||
|
/* callable= */ player::getPlaylistMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getRepeatMode() {
|
||||||
|
return runPlayerCallableBlocking(
|
||||||
|
/* callable= */ player::getRepeatMode, /* defaultValueWhenException= */ REPEAT_MODE_NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getShuffleMode() {
|
||||||
|
return runPlayerCallableBlocking(
|
||||||
|
/* callable= */ player::getShuffleMode, /* defaultValueWhenException= */ SHUFFLE_MODE_NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public MediaItem getCurrentMediaItem() {
|
||||||
|
return runPlayerCallableBlockingWithNullOnException(
|
||||||
|
/* callable= */ player::getCurrentMediaItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCurrentMediaItemIndex() {
|
||||||
|
return runPlayerCallableBlocking(
|
||||||
|
/* callable= */ player::getCurrentMediaItemIndex,
|
||||||
|
/* defaultValueWhenException= */ END_OF_PLAYLIST);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPreviousMediaItemIndex() {
|
||||||
|
return runPlayerCallableBlocking(
|
||||||
|
/* callable= */ player::getPreviousMediaItemIndex,
|
||||||
|
/* defaultValueWhenException= */ END_OF_PLAYLIST);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getNextMediaItemIndex() {
|
||||||
|
return runPlayerCallableBlocking(
|
||||||
|
/* callable= */ player::getNextMediaItemIndex,
|
||||||
|
/* defaultValueWhenException= */ END_OF_PLAYLIST);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(b/147706139): Call super.close() after updating media2-common to 1.1.0
|
||||||
|
@SuppressWarnings("MissingSuperCall")
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
synchronized (stateLock) {
|
||||||
|
if (closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closed = true;
|
||||||
|
}
|
||||||
|
reset();
|
||||||
|
|
||||||
|
this.<Void>runPlayerCallableBlockingInternal(
|
||||||
|
/* callable= */ () -> {
|
||||||
|
player.close();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionPlayerConnector-specific functions.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the current media item is seekable.
|
||||||
|
*
|
||||||
|
* @return {@code true} if supported. {@code false} otherwise.
|
||||||
|
*/
|
||||||
|
/* package */ boolean isCurrentMediaItemSeekable() {
|
||||||
|
return runPlayerCallableBlocking(
|
||||||
|
/* callable= */ player::isCurrentMediaItemSeekable, /* defaultValueWhenException= */ false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether {@link #skipToPlaylistItem(int)} is supported.
|
||||||
|
*
|
||||||
|
* @return {@code true} if supported. {@code false} otherwise.
|
||||||
|
*/
|
||||||
|
/* package */ boolean canSkipToPlaylistItem() {
|
||||||
|
return runPlayerCallableBlocking(
|
||||||
|
/* callable= */ player::canSkipToPlaylistItem, /* defaultValueWhenException= */ false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether {@link #skipToPreviousPlaylistItem()} is supported.
|
||||||
|
*
|
||||||
|
* @return {@code true} if supported. {@code false} otherwise.
|
||||||
|
*/
|
||||||
|
/* package */ boolean canSkipToPreviousPlaylistItem() {
|
||||||
|
return runPlayerCallableBlocking(
|
||||||
|
/* callable= */ player::canSkipToPreviousPlaylistItem,
|
||||||
|
/* defaultValueWhenException= */ false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether {@link #skipToNextPlaylistItem()} is supported.
|
||||||
|
*
|
||||||
|
* @return {@code true} if supported. {@code false} otherwise.
|
||||||
|
*/
|
||||||
|
/* package */ boolean canSkipToNextPlaylistItem() {
|
||||||
|
return runPlayerCallableBlocking(
|
||||||
|
/* callable= */ player::canSkipToNextPlaylistItem, /* defaultValueWhenException= */ false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets {@link SessionPlayerConnector} to its uninitialized state if not closed. After calling
|
||||||
|
* this method, you will have to initialize it again by setting the media item and calling {@link
|
||||||
|
* #prepare()}.
|
||||||
|
*
|
||||||
|
* <p>Note that if the player is closed, there is no way to reuse the instance.
|
||||||
|
*/
|
||||||
|
private void reset() {
|
||||||
|
// Cancel the pending commands.
|
||||||
|
playerCommandQueue.reset();
|
||||||
|
synchronized (stateLock) {
|
||||||
|
state = PLAYER_STATE_IDLE;
|
||||||
|
mediaItemToBuffState.clear();
|
||||||
|
}
|
||||||
|
this.<Void>runPlayerCallableBlockingInternal(
|
||||||
|
/* callable= */ () -> {
|
||||||
|
player.reset();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setState(/* @PlayerState */ int state) {
|
||||||
|
boolean needToNotify = false;
|
||||||
|
synchronized (stateLock) {
|
||||||
|
if (this.state != state) {
|
||||||
|
this.state = state;
|
||||||
|
needToNotify = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (needToNotify) {
|
||||||
|
notifySessionPlayerCallback(
|
||||||
|
callback -> callback.onPlayerStateChanged(SessionPlayerConnector.this, state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setBufferingState(MediaItem item, /* @BuffState */ int state) {
|
||||||
|
@Nullable Integer previousState;
|
||||||
|
synchronized (stateLock) {
|
||||||
|
previousState = mediaItemToBuffState.put(item, state);
|
||||||
|
}
|
||||||
|
if (previousState == null || previousState != state) {
|
||||||
|
notifySessionPlayerCallback(
|
||||||
|
callback -> callback.onBufferingStateChanged(SessionPlayerConnector.this, item, state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifySessionPlayerCallback(SessionPlayerCallbackNotifier notifier) {
|
||||||
|
synchronized (stateLock) {
|
||||||
|
if (closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<Pair<SessionPlayer.PlayerCallback, Executor>> callbacks = getCallbacks();
|
||||||
|
for (Pair<SessionPlayer.PlayerCallback, Executor> pair : callbacks) {
|
||||||
|
SessionPlayer.PlayerCallback callback = Assertions.checkNotNull(pair.first);
|
||||||
|
Executor executor = Assertions.checkNotNull(pair.second);
|
||||||
|
executor.execute(() -> notifier.callCallback(callback));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handlePlaylistChangedOnHandler() {
|
||||||
|
List<MediaItem> currentPlaylist = player.getPlaylist();
|
||||||
|
boolean notifyCurrentPlaylist = !ObjectsCompat.equals(this.currentPlaylist, currentPlaylist);
|
||||||
|
this.currentPlaylist = currentPlaylist;
|
||||||
|
MediaMetadata playlistMetadata = player.getPlaylistMetadata();
|
||||||
|
|
||||||
|
MediaItem currentMediaItem = player.getCurrentMediaItem();
|
||||||
|
boolean notifyCurrentMediaItem = !ObjectsCompat.equals(this.currentMediaItem, currentMediaItem);
|
||||||
|
this.currentMediaItem = currentMediaItem;
|
||||||
|
|
||||||
|
if (!notifyCurrentMediaItem && !notifyCurrentPlaylist) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notifySessionPlayerCallback(
|
||||||
|
callback -> {
|
||||||
|
if (notifyCurrentPlaylist) {
|
||||||
|
callback.onPlaylistChanged(
|
||||||
|
SessionPlayerConnector.this, currentPlaylist, playlistMetadata);
|
||||||
|
}
|
||||||
|
if (notifyCurrentMediaItem) {
|
||||||
|
Assertions.checkNotNull(
|
||||||
|
currentMediaItem, "PlaylistManager#currentMediaItem() cannot be changed to null");
|
||||||
|
callback.onCurrentMediaItemChanged(SessionPlayerConnector.this, currentMediaItem);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifySkipToCompletedOnHandler() {
|
||||||
|
MediaItem currentMediaItem = Assertions.checkNotNull(player.getCurrentMediaItem());
|
||||||
|
if (ObjectsCompat.equals(this.currentMediaItem, currentMediaItem)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.currentMediaItem = currentMediaItem;
|
||||||
|
notifySessionPlayerCallback(
|
||||||
|
callback ->
|
||||||
|
callback.onCurrentMediaItemChanged(SessionPlayerConnector.this, currentMediaItem));
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T runPlayerCallableBlocking(Callable<T> callable) {
|
||||||
|
synchronized (stateLock) {
|
||||||
|
Assertions.checkState(!closed);
|
||||||
|
}
|
||||||
|
return runPlayerCallableBlockingInternal(callable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T runPlayerCallableBlockingInternal(Callable<T> callable) {
|
||||||
|
SettableFuture<T> future = SettableFuture.create();
|
||||||
|
boolean success =
|
||||||
|
taskHandler.postOrRun(
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
future.set(callable.call());
|
||||||
|
} catch (Throwable e) {
|
||||||
|
future.setException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Assertions.checkState(success);
|
||||||
|
boolean wasInterrupted = false;
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
return future.get();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// We always wait for player calls to return.
|
||||||
|
wasInterrupted = true;
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
@Nullable Throwable cause = e.getCause();
|
||||||
|
Log.e(TAG, "Internal player error", new RuntimeException(cause));
|
||||||
|
throw new IllegalStateException(cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (wasInterrupted) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private <T> T runPlayerCallableBlockingWithNullOnException(Callable<@NullableType T> callable) {
|
||||||
|
try {
|
||||||
|
return runPlayerCallableBlocking(callable);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T runPlayerCallableBlocking(Callable<T> callable, T defaultValueWhenException) {
|
||||||
|
try {
|
||||||
|
return runPlayerCallableBlocking(callable);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return defaultValueWhenException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface SessionPlayerCallbackNotifier {
|
||||||
|
void callCallback(SessionPlayer.PlayerCallback callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ExoPlayerWrapperListener implements PlayerWrapper.Listener {
|
||||||
|
@Override
|
||||||
|
public void onPlayerStateChanged(int playerState) {
|
||||||
|
setState(playerState);
|
||||||
|
if (playerState == PLAYER_STATE_PLAYING) {
|
||||||
|
playerCommandQueue.notifyCommandCompleted(PlayerCommandQueue.COMMAND_CODE_PLAYER_PLAY);
|
||||||
|
} else if (playerState == PLAYER_STATE_PAUSED) {
|
||||||
|
playerCommandQueue.notifyCommandCompleted(PlayerCommandQueue.COMMAND_CODE_PLAYER_PAUSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPrepared(MediaItem mediaItem, int bufferingPercentage) {
|
||||||
|
Assertions.checkNotNull(mediaItem);
|
||||||
|
|
||||||
|
if (bufferingPercentage >= 100) {
|
||||||
|
setBufferingState(mediaItem, BUFFERING_STATE_COMPLETE);
|
||||||
|
} else {
|
||||||
|
setBufferingState(mediaItem, BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
|
||||||
|
}
|
||||||
|
playerCommandQueue.notifyCommandCompleted(PlayerCommandQueue.COMMAND_CODE_PLAYER_PREPARE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSeekCompleted() {
|
||||||
|
notifySessionPlayerCallback(
|
||||||
|
callback -> callback.onSeekCompleted(SessionPlayerConnector.this, getCurrentPosition()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBufferingStarted(MediaItem mediaItem) {
|
||||||
|
setBufferingState(mediaItem, BUFFERING_STATE_BUFFERING_AND_STARVED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBufferingUpdate(MediaItem mediaItem, int bufferingPercentage) {
|
||||||
|
if (bufferingPercentage >= 100) {
|
||||||
|
setBufferingState(mediaItem, BUFFERING_STATE_COMPLETE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBufferingEnded(MediaItem mediaItem, int bufferingPercentage) {
|
||||||
|
if (bufferingPercentage >= 100) {
|
||||||
|
setBufferingState(mediaItem, BUFFERING_STATE_COMPLETE);
|
||||||
|
} else {
|
||||||
|
setBufferingState(mediaItem, BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCurrentMediaItemChanged(MediaItem mediaItem) {
|
||||||
|
if (ObjectsCompat.equals(currentMediaItem, mediaItem)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentMediaItem = mediaItem;
|
||||||
|
notifySessionPlayerCallback(
|
||||||
|
callback -> callback.onCurrentMediaItemChanged(SessionPlayerConnector.this, mediaItem));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlaybackEnded() {
|
||||||
|
notifySessionPlayerCallback(
|
||||||
|
callback -> callback.onPlaybackCompleted(SessionPlayerConnector.this));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(@Nullable MediaItem mediaItem) {
|
||||||
|
playerCommandQueue.notifyCommandError();
|
||||||
|
if (mediaItem != null) {
|
||||||
|
setBufferingState(mediaItem, BUFFERING_STATE_UNKNOWN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlaylistChanged() {
|
||||||
|
handlePlaylistChangedOnHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onShuffleModeChanged(int shuffleMode) {
|
||||||
|
notifySessionPlayerCallback(
|
||||||
|
callback -> callback.onShuffleModeChanged(SessionPlayerConnector.this, shuffleMode));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRepeatModeChanged(int repeatMode) {
|
||||||
|
notifySessionPlayerCallback(
|
||||||
|
callback -> callback.onRepeatModeChanged(SessionPlayerConnector.this, repeatMode));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlaybackSpeedChanged(float playbackSpeed) {
|
||||||
|
notifySessionPlayerCallback(
|
||||||
|
callback -> callback.onPlaybackSpeedChanged(SessionPlayerConnector.this, playbackSpeed));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAudioAttributesChanged(AudioAttributesCompat audioAttributes) {
|
||||||
|
notifySessionPlayerCallback(
|
||||||
|
callback ->
|
||||||
|
callback.onAudioAttributesChanged(SessionPlayerConnector.this, audioAttributes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.media2;
|
||||||
|
|
||||||
|
import androidx.concurrent.futures.CallbackToFutureAdapter;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A replacement of com.google.common.util.concurrent.SettableFuture with CallbackToFutureAdapter to
|
||||||
|
* avoid the dependency on Guava.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("ShouldNotSubclass")
|
||||||
|
/* package */ class SettableFuture<V> implements ListenableFuture<V> {
|
||||||
|
static <V> SettableFuture<V> create() {
|
||||||
|
return new SettableFuture<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ListenableFuture<V> future;
|
||||||
|
private final CallbackToFutureAdapter.Completer<V> completer;
|
||||||
|
|
||||||
|
SettableFuture() {
|
||||||
|
AtomicReference<CallbackToFutureAdapter.Completer<V>> completerRef = new AtomicReference<>();
|
||||||
|
future =
|
||||||
|
CallbackToFutureAdapter.getFuture(
|
||||||
|
completer -> {
|
||||||
|
completerRef.set(completer);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
completer = Assertions.checkNotNull(completerRef.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addListener(Runnable listener, Executor executor) {
|
||||||
|
future.addListener(listener, executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean cancel(boolean mayInterruptIfRunning) {
|
||||||
|
return future.cancel(mayInterruptIfRunning);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCancelled() {
|
||||||
|
return future.isCancelled();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDone() {
|
||||||
|
return future.isDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public V get() throws ExecutionException, InterruptedException {
|
||||||
|
return future.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public V get(long timeout, TimeUnit unit)
|
||||||
|
throws ExecutionException, InterruptedException, TimeoutException {
|
||||||
|
return future.get(timeout, unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
void set(V value) {
|
||||||
|
completer.set(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setException(Throwable throwable) {
|
||||||
|
completer.setException(throwable);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,332 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.media2;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media2.common.CallbackMediaItem;
|
||||||
|
import androidx.media2.common.MediaItem;
|
||||||
|
import androidx.media2.common.MediaMetadata;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.ControlDispatcher;
|
||||||
|
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||||
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import com.google.android.exoplayer2.Timeline;
|
||||||
|
import com.google.android.exoplayer2.source.ClippingMediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A default {@link PlaylistManager} implementation based on {@link ConcatenatingMediaSource} that
|
||||||
|
* maps a {@link MediaItem} in the playlist to a {@link MediaSource}.
|
||||||
|
*
|
||||||
|
* <p>When it's used, {@link Player}'s Timeline shouldn't be changed directly, and should be only
|
||||||
|
* changed via {@link TimelinePlaylistManager}. If it's not, internal playlist would be out of sync
|
||||||
|
* with the actual {@link Timeline}. If you need to change Timeline directly, build your own {@link
|
||||||
|
* PlaylistManager} instead.
|
||||||
|
*/
|
||||||
|
public class TimelinePlaylistManager implements PlaylistManager {
|
||||||
|
private static final String TAG = "TimelinePlaylistManager";
|
||||||
|
|
||||||
|
private final MediaSourceFactory sourceFactory;
|
||||||
|
private final ConcatenatingMediaSource concatenatingMediaSource;
|
||||||
|
private final List<MediaItem> playlist;
|
||||||
|
@Nullable private MediaMetadata playlistMetadata;
|
||||||
|
private boolean loggedUnexpectedTimelineChanges;
|
||||||
|
|
||||||
|
/** Factory to create {@link MediaSource}s. */
|
||||||
|
public interface MediaSourceFactory {
|
||||||
|
/**
|
||||||
|
* Creates a {@link MediaSource} for the given {@link MediaItem}.
|
||||||
|
*
|
||||||
|
* @param mediaItem The {@link MediaItem} to create a media source for.
|
||||||
|
* @return A {@link MediaSource} or {@code null} if no source can be created for the given
|
||||||
|
* description.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
MediaSource createMediaSource(MediaItem mediaItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of the {@link MediaSourceFactory}.
|
||||||
|
*
|
||||||
|
* <p>This doesn't support the {@link androidx.media2.common.FileMediaItem}.
|
||||||
|
*/
|
||||||
|
public static final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
||||||
|
private final Context context;
|
||||||
|
private final DataSource.Factory dataSourceFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default constructor with {@link DefaultDataSourceFactory}.
|
||||||
|
*
|
||||||
|
* @param context The context.
|
||||||
|
*/
|
||||||
|
public DefaultMediaSourceFactory(Context context) {
|
||||||
|
this(
|
||||||
|
context,
|
||||||
|
new DefaultDataSourceFactory(
|
||||||
|
context, Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default constructor with {@link DataSource.Factory}.
|
||||||
|
*
|
||||||
|
* @param context The context.
|
||||||
|
* @param dataSourceFactory The dataSourceFactory to create {@link MediaSource} from {@link
|
||||||
|
* MediaItem}.
|
||||||
|
*/
|
||||||
|
public DefaultMediaSourceFactory(Context context, DataSource.Factory dataSourceFactory) {
|
||||||
|
this.context = Assertions.checkNotNull(context);
|
||||||
|
this.dataSourceFactory = Assertions.checkNotNull(dataSourceFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MediaSource createMediaSource(MediaItem mediaItem) {
|
||||||
|
// Create a source for the item.
|
||||||
|
MediaSource mediaSource =
|
||||||
|
Utils.createUnclippedMediaSource(context, dataSourceFactory, mediaItem);
|
||||||
|
|
||||||
|
// Apply clipping if needed.
|
||||||
|
long startPosition = mediaItem.getStartPosition();
|
||||||
|
long endPosition = mediaItem.getEndPosition();
|
||||||
|
if (startPosition != 0L || endPosition != MediaItem.POSITION_UNKNOWN) {
|
||||||
|
if (endPosition == MediaItem.POSITION_UNKNOWN) {
|
||||||
|
endPosition = C.TIME_END_OF_SOURCE;
|
||||||
|
}
|
||||||
|
// Disable the initial discontinuity to give seamless transitions to clips.
|
||||||
|
mediaSource =
|
||||||
|
new ClippingMediaSource(
|
||||||
|
mediaSource,
|
||||||
|
C.msToUs(startPosition),
|
||||||
|
C.msToUs(endPosition),
|
||||||
|
/* enableInitialDiscontinuity= */ false,
|
||||||
|
/* allowDynamicClippingUpdates= */ false,
|
||||||
|
/* relativeToDefaultPosition= */ true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mediaSource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link TimelinePlaylistManager} with the {@link DefaultMediaSourceFactory}.
|
||||||
|
*
|
||||||
|
* @param context The context.
|
||||||
|
* @param concatenatingMediaSource The {@link ConcatenatingMediaSource} to manipulate.
|
||||||
|
*/
|
||||||
|
public TimelinePlaylistManager(
|
||||||
|
Context context, ConcatenatingMediaSource concatenatingMediaSource) {
|
||||||
|
this(concatenatingMediaSource, new DefaultMediaSourceFactory(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link TimelinePlaylistManager} with a given mediaSourceFactory.
|
||||||
|
*
|
||||||
|
* @param concatenatingMediaSource The {@link ConcatenatingMediaSource} to manipulate.
|
||||||
|
* @param sourceFactory The {@link MediaSourceFactory} to build media sources.
|
||||||
|
*/
|
||||||
|
public TimelinePlaylistManager(
|
||||||
|
ConcatenatingMediaSource concatenatingMediaSource, MediaSourceFactory sourceFactory) {
|
||||||
|
this.concatenatingMediaSource = concatenatingMediaSource;
|
||||||
|
this.sourceFactory = sourceFactory;
|
||||||
|
this.playlist = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setPlaylist(
|
||||||
|
Player player, List<MediaItem> playlist, @Nullable MediaMetadata metadata) {
|
||||||
|
// Check for duplication.
|
||||||
|
for (int i = 0; i < playlist.size(); i++) {
|
||||||
|
MediaItem mediaItem = playlist.get(i);
|
||||||
|
Assertions.checkArgument(playlist.indexOf(mediaItem) == i);
|
||||||
|
}
|
||||||
|
for (MediaItem mediaItem : this.playlist) {
|
||||||
|
if (!playlist.contains(mediaItem)) {
|
||||||
|
releaseMediaItem(mediaItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.playlist.clear();
|
||||||
|
this.playlist.addAll(playlist);
|
||||||
|
this.playlistMetadata = metadata;
|
||||||
|
|
||||||
|
concatenatingMediaSource.clear();
|
||||||
|
|
||||||
|
List<MediaSource> mediaSources = new ArrayList<>();
|
||||||
|
for (int i = 0; i < playlist.size(); i++) {
|
||||||
|
MediaItem mediaItem = playlist.get(i);
|
||||||
|
MediaSource mediaSource = createMediaSource(mediaItem);
|
||||||
|
mediaSources.add(mediaSource);
|
||||||
|
}
|
||||||
|
concatenatingMediaSource.addMediaSources(mediaSources);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean addPlaylistItem(Player player, int index, MediaItem mediaItem) {
|
||||||
|
Assertions.checkArgument(!playlist.contains(mediaItem));
|
||||||
|
index = Util.constrainValue(index, 0, playlist.size());
|
||||||
|
|
||||||
|
playlist.add(index, mediaItem);
|
||||||
|
MediaSource mediaSource = createMediaSource(mediaItem);
|
||||||
|
concatenatingMediaSource.addMediaSource(index, mediaSource);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean removePlaylistItem(Player player, int index) {
|
||||||
|
MediaItem mediaItemToRemove = playlist.remove(index);
|
||||||
|
releaseMediaItem(mediaItemToRemove);
|
||||||
|
concatenatingMediaSource.removeMediaSource(index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean replacePlaylistItem(Player player, int index, MediaItem mediaItem) {
|
||||||
|
Assertions.checkArgument(!playlist.contains(mediaItem));
|
||||||
|
index = Util.constrainValue(index, 0, playlist.size());
|
||||||
|
|
||||||
|
MediaItem mediaItemToRemove = playlist.get(index);
|
||||||
|
playlist.set(index, mediaItem);
|
||||||
|
releaseMediaItem(mediaItemToRemove);
|
||||||
|
|
||||||
|
MediaSource mediaSourceToAdd = createMediaSource(mediaItem);
|
||||||
|
concatenatingMediaSource.removeMediaSource(index);
|
||||||
|
concatenatingMediaSource.addMediaSource(index, mediaSourceToAdd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setMediaItem(Player player, MediaItem mediaItem) {
|
||||||
|
// TODO(jaewan): Distinguish setMediaItem(item) and setPlaylist({item})
|
||||||
|
List<MediaItem> playlist = new ArrayList<>();
|
||||||
|
playlist.add(mediaItem);
|
||||||
|
return setPlaylist(player, playlist, /* metadata */ null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean updatePlaylistMetadata(Player player, @Nullable MediaMetadata metadata) {
|
||||||
|
this.playlistMetadata = metadata;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean skipToNextPlaylistItem(Player player, ControlDispatcher controlDispatcher) {
|
||||||
|
Timeline timeline = player.getCurrentTimeline();
|
||||||
|
Assertions.checkState(!timeline.isEmpty());
|
||||||
|
int nextWindowIndex = player.getNextWindowIndex();
|
||||||
|
if (nextWindowIndex != C.INDEX_UNSET) {
|
||||||
|
return controlDispatcher.dispatchSeekTo(player, nextWindowIndex, C.TIME_UNSET);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean skipToPreviousPlaylistItem(Player player, ControlDispatcher controlDispatcher) {
|
||||||
|
Timeline timeline = player.getCurrentTimeline();
|
||||||
|
Assertions.checkState(!timeline.isEmpty());
|
||||||
|
int previousWindowIndex = player.getPreviousWindowIndex();
|
||||||
|
if (previousWindowIndex != C.INDEX_UNSET) {
|
||||||
|
return controlDispatcher.dispatchSeekTo(player, previousWindowIndex, C.TIME_UNSET);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean skipToPlaylistItem(Player player, ControlDispatcher controlDispatcher, int index) {
|
||||||
|
Timeline timeline = player.getCurrentTimeline();
|
||||||
|
Assertions.checkState(!timeline.isEmpty());
|
||||||
|
// Use checkState() instead of checkIndex() for throwing IllegalStateException.
|
||||||
|
// checkIndex() throws IndexOutOfBoundsException which maps the RESULT_ERROR_BAD_VALUE
|
||||||
|
// but RESULT_ERROR_INVALID_STATE with IllegalStateException is expected here.
|
||||||
|
Assertions.checkState(0 <= index && index < timeline.getWindowCount());
|
||||||
|
int windowIndex = player.getCurrentWindowIndex();
|
||||||
|
if (windowIndex != index) {
|
||||||
|
return controlDispatcher.dispatchSeekTo(player, index, C.TIME_UNSET);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCurrentMediaItemIndex(Player player) {
|
||||||
|
return playlist.isEmpty() ? C.INDEX_UNSET : player.getCurrentWindowIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public MediaItem getCurrentMediaItem(Player player) {
|
||||||
|
int index = getCurrentMediaItemIndex(player);
|
||||||
|
return (index != C.INDEX_UNSET) ? playlist.get(index) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MediaItem> getPlaylist(Player player) {
|
||||||
|
return new ArrayList<>(playlist);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public MediaMetadata getPlaylistMetadata(Player player) {
|
||||||
|
return playlistMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTimelineChanged(Player player) {
|
||||||
|
checkTimelineWindowCountEqualsToPlaylistSize(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseMediaItem(MediaItem mediaItem) {
|
||||||
|
try {
|
||||||
|
if (mediaItem instanceof CallbackMediaItem) {
|
||||||
|
((CallbackMediaItem) mediaItem).getDataSourceCallback().close();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.w(TAG, "Error releasing media item " + mediaItem, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MediaSource createMediaSource(MediaItem mediaItem) {
|
||||||
|
return Assertions.checkNotNull(
|
||||||
|
sourceFactory.createMediaSource(mediaItem),
|
||||||
|
"createMediaSource() failed, mediaItem=" + mediaItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether Timeline's window count matches with the playlist size, and leave log for
|
||||||
|
// mismatch. It's to check whether the Timeline and playlist are out of sync or not at the best
|
||||||
|
// effort.
|
||||||
|
private void checkTimelineWindowCountEqualsToPlaylistSize(Player player) {
|
||||||
|
if (player.getPlaybackState() == Player.STATE_IDLE) {
|
||||||
|
// Cannot do check in STATE_IDLE, because Timeline isn't available.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Timeline timeline = player.getCurrentTimeline();
|
||||||
|
if ((playlist == null && timeline.getWindowCount() == 1)
|
||||||
|
|| (playlist != null && playlist.size() == timeline.getWindowCount())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!loggedUnexpectedTimelineChanges) {
|
||||||
|
Log.w(TAG, "Timeline is unexpectedly changed. Playlist can be out of sync.");
|
||||||
|
loggedUnexpectedTimelineChanges = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,200 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.media2;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media.AudioAttributesCompat;
|
||||||
|
import androidx.media2.common.CallbackMediaItem;
|
||||||
|
import androidx.media2.common.MediaItem;
|
||||||
|
import androidx.media2.common.SessionPlayer;
|
||||||
|
import androidx.media2.common.UriMediaItem;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||||
|
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||||
|
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||||
|
import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||||
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.RawResourceDataSource;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility methods for the media2 extension (primarily for translating between the media2 and
|
||||||
|
* ExoPlayer {@link Player} APIs).
|
||||||
|
*/
|
||||||
|
/* package */ final class Utils {
|
||||||
|
|
||||||
|
private static final ExtractorsFactory sExtractorsFactory =
|
||||||
|
new DefaultExtractorsFactory()
|
||||||
|
.setAdtsExtractorFlags(AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an ExoPlayer media source for the given media item. The given {@link MediaItem} is set
|
||||||
|
* as the tag of the source.
|
||||||
|
*/
|
||||||
|
public static MediaSource createUnclippedMediaSource(
|
||||||
|
Context context, DataSource.Factory dataSourceFactory, MediaItem mediaItem) {
|
||||||
|
if (mediaItem instanceof UriMediaItem) {
|
||||||
|
Uri uri = ((UriMediaItem) mediaItem).getUri();
|
||||||
|
if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme())) {
|
||||||
|
String path = Assertions.checkNotNull(uri.getPath());
|
||||||
|
int resourceIdentifier;
|
||||||
|
if (uri.getPathSegments().size() == 1 && uri.getPathSegments().get(0).matches("\\d+")) {
|
||||||
|
resourceIdentifier = Integer.parseInt(uri.getPathSegments().get(0));
|
||||||
|
} else {
|
||||||
|
if (path.startsWith("/")) {
|
||||||
|
path = path.substring(1);
|
||||||
|
}
|
||||||
|
@Nullable String host = uri.getHost();
|
||||||
|
String resourceName = (TextUtils.isEmpty(host) ? "" : (host + ":")) + path;
|
||||||
|
resourceIdentifier =
|
||||||
|
context.getResources().getIdentifier(resourceName, "raw", context.getPackageName());
|
||||||
|
}
|
||||||
|
Assertions.checkState(resourceIdentifier != 0);
|
||||||
|
uri = RawResourceDataSource.buildRawResourceUri(resourceIdentifier);
|
||||||
|
}
|
||||||
|
return createMediaSource(uri, dataSourceFactory, /* tag= */ mediaItem);
|
||||||
|
} else if (mediaItem instanceof CallbackMediaItem) {
|
||||||
|
CallbackMediaItem callbackMediaItem = (CallbackMediaItem) mediaItem;
|
||||||
|
dataSourceFactory =
|
||||||
|
DataSourceCallbackDataSource.getFactory(callbackMediaItem.getDataSourceCallback());
|
||||||
|
return new ProgressiveMediaSource.Factory(dataSourceFactory, sExtractorsFactory)
|
||||||
|
.setTag(mediaItem)
|
||||||
|
.createMediaSource(Uri.EMPTY);
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns ExoPlayer audio attributes for the given audio attributes. */
|
||||||
|
public static AudioAttributes getAudioAttributes(AudioAttributesCompat audioAttributesCompat) {
|
||||||
|
return new AudioAttributes.Builder()
|
||||||
|
.setContentType(audioAttributesCompat.getContentType())
|
||||||
|
.setFlags(audioAttributesCompat.getFlags())
|
||||||
|
.setUsage(audioAttributesCompat.getUsage())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns audio attributes for the given ExoPlayer audio attributes. */
|
||||||
|
public static AudioAttributesCompat getAudioAttributesCompat(AudioAttributes audioAttributes) {
|
||||||
|
return new AudioAttributesCompat.Builder()
|
||||||
|
.setContentType(audioAttributes.contentType)
|
||||||
|
.setFlags(audioAttributes.flags)
|
||||||
|
.setUsage(audioAttributes.usage)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the SimpleExoPlayer's shuffle mode for the given shuffle mode. */
|
||||||
|
public static boolean getExoPlayerShuffleMode(int shuffleMode) {
|
||||||
|
switch (shuffleMode) {
|
||||||
|
case SessionPlayer.SHUFFLE_MODE_ALL:
|
||||||
|
case SessionPlayer.SHUFFLE_MODE_GROUP:
|
||||||
|
return true;
|
||||||
|
case SessionPlayer.SHUFFLE_MODE_NONE:
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the shuffle mode for the given ExoPlayer's shuffle mode */
|
||||||
|
public static int getShuffleMode(boolean exoPlayerShuffleMode) {
|
||||||
|
return exoPlayerShuffleMode ? SessionPlayer.SHUFFLE_MODE_ALL : SessionPlayer.SHUFFLE_MODE_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the ExoPlayer's repeat mode for the given repeat mode. */
|
||||||
|
@Player.RepeatMode
|
||||||
|
public static int getExoPlayerRepeatMode(int repeatMode) {
|
||||||
|
switch (repeatMode) {
|
||||||
|
case SessionPlayer.REPEAT_MODE_ALL:
|
||||||
|
case SessionPlayer.REPEAT_MODE_GROUP:
|
||||||
|
return Player.REPEAT_MODE_ALL;
|
||||||
|
case SessionPlayer.REPEAT_MODE_ONE:
|
||||||
|
return Player.REPEAT_MODE_ONE;
|
||||||
|
case SessionPlayer.REPEAT_MODE_NONE:
|
||||||
|
return Player.REPEAT_MODE_OFF;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the repeat mode for the given SimpleExoPlayer's repeat mode. */
|
||||||
|
public static int getRepeatMode(@Player.RepeatMode int exoPlayerRepeatMode) {
|
||||||
|
switch (exoPlayerRepeatMode) {
|
||||||
|
case Player.REPEAT_MODE_ALL:
|
||||||
|
return SessionPlayer.REPEAT_MODE_ALL;
|
||||||
|
case Player.REPEAT_MODE_ONE:
|
||||||
|
return SessionPlayer.REPEAT_MODE_ONE;
|
||||||
|
case Player.REPEAT_MODE_OFF:
|
||||||
|
return SessionPlayer.REPEAT_MODE_NONE;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MediaSource createMediaSource(
|
||||||
|
Uri uri, DataSource.Factory dataSourceFactory, Object tag) {
|
||||||
|
// TODO: Deduplicate with DefaultMediaSource once MediaItem support in ExoPlayer has been
|
||||||
|
// released. See [Internal: b/150857202].
|
||||||
|
@Nullable Class<? extends MediaSourceFactory> factoryClazz = null;
|
||||||
|
try {
|
||||||
|
// LINT.IfChange
|
||||||
|
switch (Util.inferContentType(uri)) {
|
||||||
|
case C.TYPE_DASH:
|
||||||
|
factoryClazz =
|
||||||
|
Class.forName("com.google.android.exoplayer2.source.dash.DashMediaSource$Factory")
|
||||||
|
.asSubclass(MediaSourceFactory.class);
|
||||||
|
break;
|
||||||
|
case C.TYPE_HLS:
|
||||||
|
factoryClazz =
|
||||||
|
Class.forName("com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory")
|
||||||
|
.asSubclass(MediaSourceFactory.class);
|
||||||
|
break;
|
||||||
|
case C.TYPE_SS:
|
||||||
|
factoryClazz =
|
||||||
|
Class.forName(
|
||||||
|
"com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory")
|
||||||
|
.asSubclass(MediaSourceFactory.class);
|
||||||
|
break;
|
||||||
|
case C.TYPE_OTHER:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (factoryClazz != null) {
|
||||||
|
MediaSourceFactory mediaSourceFactory =
|
||||||
|
factoryClazz.getConstructor(DataSource.Factory.class).newInstance(dataSourceFactory);
|
||||||
|
factoryClazz.getMethod("setTag", Object.class).invoke(mediaSourceFactory, tag);
|
||||||
|
return mediaSourceFactory.createMediaSource(uri);
|
||||||
|
}
|
||||||
|
// LINT.ThenChange(../../../../../../../../../proguard-rules.txt)
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Expected if the app was built without the corresponding module.
|
||||||
|
}
|
||||||
|
return new ProgressiveMediaSource.Factory(dataSourceFactory).setTag(tag).createMediaSource(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Utils() {
|
||||||
|
// Prevent instantiation.
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.
|
||||||
|
*/
|
||||||
|
@NonNullApi
|
||||||
|
package com.google.android.exoplayer2.ext.media2;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.util.NonNullApi;
|
1
extensions/media2/src/main/proguard-rules.txt
Symbolic link
1
extensions/media2/src/main/proguard-rules.txt
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../proguard-rules.txt
|
Loading…
x
Reference in New Issue
Block a user