diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d700482627..bbf9a87e30 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -20,6 +20,10 @@ * Fix a bug when [depending on ExoPlayer locally](README.md#locally) with a relative path ([#9403](https://github.com/google/ExoPlayer/issues/9403)). + * Fix bug in `DefaultDrmSessionManager` which prevented + `MediaSourceFactory` instances from being re-used by `ExoPlayer` + instances with non-overlapping lifecycles + ([#9099](https://github.com/google/ExoPlayer/issues/9099)). * Extractors: * Support TS packets without PTS flag ([#9294](https://github.com/google/ExoPlayer/issues/9294)). diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryInstrumentationTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryInstrumentationTest.java new file mode 100644 index 0000000000..921fd0f027 --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryInstrumentationTest.java @@ -0,0 +1,99 @@ +/* + * Copyright 2021 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.source; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.PlaybackException; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.util.ConditionVariable; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Instrumentation tests for {@link DefaultMediaSourceFactory}. */ +@RunWith(AndroidJUnit4.class) +public final class DefaultMediaSourceFactoryInstrumentationTest { + + // https://github.com/google/ExoPlayer/issues/9099 + @Test + public void reuseMediaSourceFactoryBetweenPlayerInstances() throws Exception { + MediaItem mediaItem = + new MediaItem.Builder() + .setUri("asset:///media/mp4/sample.mp4") + .setDrmUuid(C.WIDEVINE_UUID) + .setDrmSessionForClearPeriods(true) + .build(); + AtomicReference player = new AtomicReference<>(); + DefaultMediaSourceFactory defaultMediaSourceFactory = + new DefaultMediaSourceFactory(getInstrumentation().getContext()); + getInstrumentation() + .runOnMainSync( + () -> + player.set( + new ExoPlayer.Builder(getInstrumentation().getContext()) + .setMediaSourceFactory(defaultMediaSourceFactory) + .build())); + playUntilEndAndRelease(player.get(), mediaItem); + getInstrumentation() + .runOnMainSync( + () -> + player.set( + new ExoPlayer.Builder(getInstrumentation().getContext()) + .setMediaSourceFactory(defaultMediaSourceFactory) + .build())); + playUntilEndAndRelease(player.get(), mediaItem); + } + + private void playUntilEndAndRelease(Player player, MediaItem mediaItem) + throws InterruptedException { + ConditionVariable playbackComplete = new ConditionVariable(); + AtomicReference playbackException = new AtomicReference<>(); + getInstrumentation() + .runOnMainSync( + () -> { + player.addListener( + new Player.Listener() { + @Override + public void onPlaybackStateChanged(@Player.State int playbackState) { + if (playbackState == Player.STATE_ENDED) { + playbackComplete.open(); + } + } + + @Override + public void onPlayerError(PlaybackException error) { + playbackException.set(error); + playbackComplete.open(); + } + }); + player.setMediaItem(mediaItem); + player.prepare(); + player.play(); + }); + + playbackComplete.block(); + getInstrumentation().runOnMainSync(player::release); + getInstrumentation().waitForIdleSync(); + assertThat(playbackException.get()).isNull(); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 1ea5fed05e..ac02fb1aed 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -53,7 +53,6 @@ import java.util.Map; import java.util.Set; import java.util.UUID; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. @@ -298,8 +297,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager { @Nullable private ExoMediaDrm exoMediaDrm; @Nullable private DefaultDrmSession placeholderDrmSession; @Nullable private DefaultDrmSession noMultiSessionDrmSession; - private @MonotonicNonNull Looper playbackLooper; - private @MonotonicNonNull Handler playbackHandler; + @Nullable private Looper playbackLooper; + @Nullable private Handler playbackHandler; private int mode; @Nullable private byte[] offlineLicenseKeySetId; @@ -484,7 +483,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { } releaseAllPreacquiredSessions(); - maybeReleaseMediaDrm(); + maybeFullyReleaseManager(); } @Override @@ -663,6 +662,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { this.playbackLooper = playbackLooper; this.playbackHandler = new Handler(playbackLooper); } else { + // Check this manager is only being used by a single player at a time. checkState(this.playbackLooper == playbackLooper); checkNotNull(playbackHandler); } @@ -779,12 +779,18 @@ public class DefaultDrmSessionManager implements DrmSessionManager { return session; } - private void maybeReleaseMediaDrm() { + private void maybeFullyReleaseManager() { if (exoMediaDrm != null && prepareCallsCount == 0 && sessions.isEmpty() && preacquiredSessionReferences.isEmpty()) { - // This manager and all its sessions are fully released so we can release exoMediaDrm. + // This manager and all its sessions are fully released so we can null out the looper & + // handler references and release exoMediaDrm. + if (playbackLooper != null) { + checkNotNull(playbackHandler).removeCallbacksAndMessages(/* token= */ null); + playbackHandler = null; + playbackLooper = null; + } checkNotNull(exoMediaDrm).release(); exoMediaDrm = null; } @@ -935,7 +941,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { keepaliveSessions.remove(session); } } - maybeReleaseMediaDrm(); + maybeFullyReleaseManager(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java index 755096bbe9..cbdae8aa41 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java @@ -19,6 +19,7 @@ import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DefaultDrmSessionManagerProvider; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -31,7 +32,13 @@ import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import java.util.List; -/** Factory for creating {@link MediaSource MediaSources} from {@link MediaItem MediaItems}. */ +/** + * Factory for creating {@link MediaSource MediaSources} from {@link MediaItem MediaItems}. + * + *

A factory must only be used by a single {@link Player} at a time. A factory can only be + * re-used by a second {@link Player} if the previous {@link Player} and all associated resources + * are fully released. + */ public interface MediaSourceFactory { /** @deprecated Use {@link MediaItem.PlaybackProperties#streamKeys} instead. */