Player makes license request in the same session after a short pause & seek to live edge for live drm content
Version
Media3 1.6.1
More version details
For a live content with drm if we pause the playback for couple of minutes & resume playback with seeking to live edge player is making request to the license server again within the same session.
We want to understand why player would make this request again within the same session, also we have setMultiSession to false in drm configuration & there is key rotation.
Devices that reproduce the issue
All devices
Devices that do not reproduce the issue
None
Reproducible in the demo app?
Yes
Reproduction steps
Play this live content. Pause it for like 4-5 minutes. Resume playback with Go Live. Player makes request to license server. Playback fails with DrmSessionException.
Expected result
Playback should continue was expected as behind live window is handled within app.
Actual result
Playback error with androidx.media3.exoplayer.drm.DrmSession$DrmSessionException: java.lang.IllegalArgumentException: {}: BAD_VALUE
Media
We will share the media details with asset url & license url since the token expiry is long issue won't be reproduced.
Bug Report
- [x] You will email the zip file produced by
adb bugreportto [email protected] after filing this issue.
I played the provided stream in the demo app. I paused it for ~5 mins. Then I resumed playback and pressed the "skip to next" button to seek back to the live edge.
With a debug breakpoint in DefaultDrmSession.release, I see it get completely released (ref count goes to zero) due to handling BehindLiveWindowException here resulting in calling stopInternal:
https://github.com/androidx/media/blob/4423af424b3ba492f5a62d851ed6f5453ae0c3aa/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java#L899-L900
Debug stack trace (line numbers relate to the tip of main branch):
release:339, DefaultDrmSession (androidx.media3.exoplayer.drm)
release:427, DefaultDrmSessionManager (androidx.media3.exoplayer.drm)
releaseSourceInternal:627, DashMediaSource (androidx.media3.exoplayer.dash)
releaseSource:292, BaseMediaSource (androidx.media3.exoplayer.source)
releaseSourceInternal:86, CompositeMediaSource (androidx.media3.exoplayer.source)
releaseSourceInternal:138, MaskingMediaSource (androidx.media3.exoplayer.source)
releaseSource:292, BaseMediaSource (androidx.media3.exoplayer.source)
release:363, MediaSourceList (androidx.media3.exoplayer)
resetInternal:1970, ExoPlayerImplInternal (androidx.media3.exoplayer)
stopInternal:1847, ExoPlayerImplInternal (androidx.media3.exoplayer)
handleIoException:929, ExoPlayerImplInternal (androidx.media3.exoplayer)
handleMessage:900, ExoPlayerImplInternal (androidx.media3.exoplayer)
dispatchMessage:105, Handler (android.os)
loopOnce:232, Looper (android.os)
loop:317, Looper (android.os)
run:85, HandlerThread (android.os)
This results in releasing the MediaSource, which releases the DefaultDrmSessionManager, which releases all its DefaultDrmSession references.
You can try avoiding this loss of state by keeping the DefaultDrmSessionManager alive by holding an 'external' reference to it. You will need to make sure you release it when you are done with playback. See a more detailed explanation in https://github.com/androidx/media/issues/2048#issuecomment-2682662780.
Oh okay understood thanks @icbaker we will work on this.
class CachingDrmSessionManagerProvider implements DrmSessionManagerProvider {
private final DrmSessionManagerProvider delegate;
@Nullable private DrmSessionManager cachedManager;
private CachingDrmSessionManagerProvider(DrmSessionManagerProvider delegate) {
this.delegate = delegate;
}
@Override
public DrmSessionManager get(MediaItem mediaItem) {
DrmSessionManager drmSessionManager = delegate.get(mediaItem);
if (drmSessionManager != cachedManager) {
if (cachedManager != null) {
cachedManager.release();
}
drmSessionManager.prepare();
cachedManager = drmSessionManager;
}
return drmSessionManager;
}
public final void releaseCachedManager() {
if (cachedManager != null) {
cachedManager.release();
}
}
}
@icbaker calling drmSessionManager.prepare(); showing the below warning
That is just a warning (because we don't know what the "right" (playback) thread is yet), but looking at the stack trace it seems the call will be from the application thread (incorrect).
That will result in the MediaDrm.setOnEventListener being registered with a handler on the wrong thread - but this is actually fine because the implementaion of the listener immediately posts the result to the playback thread anyway.
It's possible (but seems unlikely) that this will cause other race conditions inside DefaultDrmSessionManager.
An alternative proposal that avoids this would be to move the 'reference holding' inside a custom DrmSessionManager impl using the decorator pattern around DefaultDrmSessionManager. You would then construct an instance of this in your DrmSessionManagerProvider impl, and hold onto a reference to it so you can 'fully release' (using the new releaseExtraReference() method defined below) when releasing the player. I haven't tested this.
public class ReferenceHoldingDrmSessionManager implements DrmSessionManager {
private final DrmSessionManager delegate;
private boolean holdingExtraReference;
public ReferenceHoldingDrmSessionManager(DrmSessionManager delegate) {
this.delegate = delegate;
}
@Override
public void prepare() {
delegate.prepare();
if (!holdingExtraReference) {
delegate.prepare();
}
}
@Override
public void release() {
delegate.release();
}
public void releaseExtraReference() {
if (holdingExtraReference) {
delegate.release();
holdingExtraReference = false;
}
}
@Override
public void setPlayer(Looper playbackLooper, PlayerId playerId) {
delegate.setPlayer(playbackLooper, playerId);
}
@Override
public DrmSessionReference preacquireSession(
@Nullable EventDispatcher eventDispatcher, Format format) {
return delegate.preacquireSession(eventDispatcher, format);
}
@Nullable
@Override
public DrmSession acquireSession(@Nullable EventDispatcher eventDispatcher, Format format) {
return delegate.acquireSession(eventDispatcher, format);
}
@Override
public @CryptoType int getCryptoType(Format format) {
return delegate.getCryptoType(format);
}
}
Closing because I think the question has been answered.