mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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)).
|
||||
* Metadata: Add minimal DVB Application Information Table (AIT) support
|
||||
([#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
|
||||
manipulation API.
|
||||
* Demo app: Retain previous position in list of samples.
|
||||
|
@ -39,6 +39,7 @@ include modulePrefix + 'extension-ima'
|
||||
include modulePrefix + 'extension-cast'
|
||||
include modulePrefix + 'extension-cronet'
|
||||
include modulePrefix + 'extension-mediasession'
|
||||
include modulePrefix + 'extension-media2'
|
||||
include modulePrefix + 'extension-okhttp'
|
||||
include modulePrefix + 'extension-opus'
|
||||
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-cronet').projectDir = new File(rootDir, 'extensions/cronet')
|
||||
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-opus').projectDir = new File(rootDir, 'extensions/opus')
|
||||
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