From ee8df7afcb7982519b9375b590f8bdb086f0b08c Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 8 Sep 2021 17:29:05 +0100 Subject: [PATCH] Ensure MediaSourceFactory instances can be re-used This fixes DefaultDrmSessionManager so it can be used by a new Player instance (by nulling out its reference to the playback thread, which is unique per-Player instance). This only works if the DefaultDrmSessionManager is 'fully released' before being used by the second Player instance, meaning that the reference count of the manager and all its sessions is zero. #exofixit #minor-release Issue: #9099 PiperOrigin-RevId: 395490506 --- RELEASENOTES.md | 4 + ...MediaSourceFactoryInstrumentationTest.java | 99 +++++++++++++++++++ .../drm/DefaultDrmSessionManager.java | 20 ++-- .../exoplayer2/source/MediaSourceFactory.java | 9 +- 4 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryInstrumentationTest.java 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. */