media icon indicating copy to clipboard operation
media copied to clipboard

Player makes license request in the same session after a short pause & seek to live edge for live drm content

Open riteshram opened this issue 7 months ago • 4 comments

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 bugreport to [email protected] after filing this issue.

riteshram avatar Jun 13 '25 04:06 riteshram

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.

icbaker avatar Jun 13 '25 09:06 icbaker

Oh okay understood thanks @icbaker we will work on this.

riteshram avatar Jun 16 '25 03:06 riteshram

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

Image

bharathnr21 avatar Jun 18 '25 09:06 bharathnr21

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);
  }
}

icbaker avatar Jun 20 '25 10:06 icbaker

Closing because I think the question has been answered.

icbaker avatar Jul 17 '25 14:07 icbaker