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
This commit is contained in:
ibaker 2021-09-08 17:29:05 +01:00 committed by Oliver Woodman
parent b6e0ade939
commit ee8df7afcb
4 changed files with 124 additions and 8 deletions

View File

@ -20,6 +20,10 @@
* Fix a bug when [depending on ExoPlayer locally](README.md#locally) with * Fix a bug when [depending on ExoPlayer locally](README.md#locally) with
a relative path a relative path
([#9403](https://github.com/google/ExoPlayer/issues/9403)). ([#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: * Extractors:
* Support TS packets without PTS flag * Support TS packets without PTS flag
([#9294](https://github.com/google/ExoPlayer/issues/9294)). ([#9294](https://github.com/google/ExoPlayer/issues/9294)).

View File

@ -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<SimpleExoPlayer> 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> 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();
}
}

View File

@ -53,7 +53,6 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. * A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}.
@ -298,8 +297,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
@Nullable private ExoMediaDrm exoMediaDrm; @Nullable private ExoMediaDrm exoMediaDrm;
@Nullable private DefaultDrmSession placeholderDrmSession; @Nullable private DefaultDrmSession placeholderDrmSession;
@Nullable private DefaultDrmSession noMultiSessionDrmSession; @Nullable private DefaultDrmSession noMultiSessionDrmSession;
private @MonotonicNonNull Looper playbackLooper; @Nullable private Looper playbackLooper;
private @MonotonicNonNull Handler playbackHandler; @Nullable private Handler playbackHandler;
private int mode; private int mode;
@Nullable private byte[] offlineLicenseKeySetId; @Nullable private byte[] offlineLicenseKeySetId;
@ -484,7 +483,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
} }
releaseAllPreacquiredSessions(); releaseAllPreacquiredSessions();
maybeReleaseMediaDrm(); maybeFullyReleaseManager();
} }
@Override @Override
@ -663,6 +662,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
this.playbackLooper = playbackLooper; this.playbackLooper = playbackLooper;
this.playbackHandler = new Handler(playbackLooper); this.playbackHandler = new Handler(playbackLooper);
} else { } else {
// Check this manager is only being used by a single player at a time.
checkState(this.playbackLooper == playbackLooper); checkState(this.playbackLooper == playbackLooper);
checkNotNull(playbackHandler); checkNotNull(playbackHandler);
} }
@ -779,12 +779,18 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
return session; return session;
} }
private void maybeReleaseMediaDrm() { private void maybeFullyReleaseManager() {
if (exoMediaDrm != null if (exoMediaDrm != null
&& prepareCallsCount == 0 && prepareCallsCount == 0
&& sessions.isEmpty() && sessions.isEmpty()
&& preacquiredSessionReferences.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(); checkNotNull(exoMediaDrm).release();
exoMediaDrm = null; exoMediaDrm = null;
} }
@ -935,7 +941,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
keepaliveSessions.remove(session); keepaliveSessions.remove(session);
} }
} }
maybeReleaseMediaDrm(); maybeFullyReleaseManager();
} }
} }

View File

@ -19,6 +19,7 @@ import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem; 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.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManagerProvider; import com.google.android.exoplayer2.drm.DefaultDrmSessionManagerProvider;
import com.google.android.exoplayer2.drm.DrmSessionManager; 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 com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import java.util.List; import java.util.List;
/** Factory for creating {@link MediaSource MediaSources} from {@link MediaItem MediaItems}. */ /**
* Factory for creating {@link MediaSource MediaSources} from {@link MediaItem MediaItems}.
*
* <p>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 { public interface MediaSourceFactory {
/** @deprecated Use {@link MediaItem.PlaybackProperties#streamKeys} instead. */ /** @deprecated Use {@link MediaItem.PlaybackProperties#streamKeys} instead. */