From d88b7ecbb6ed89601b448c73bd55b5de5a56017d Mon Sep 17 00:00:00 2001 From: SylvainMorel <62572068+SylvainMorel@users.noreply.github.com> Date: Thu, 24 Oct 2024 09:26:45 -0400 Subject: [PATCH] Add support for multiple offline DRM keys (#28) We need to support multiple DRM keys for different time periods on offline playbacks. This wasn't supported by ExoPlayer. This PR adds support for multiple keys. - Added a new setMode function in DefaultDrmSessionManager to be able to pass the needed data (lists of drmKeys and list of hash codes to find the right key for a given format) - Replaced the single keySetId in DefaultDrmSessionManager by the above 2 lists - When we acquire a session with a new format, use the hash codes list to find the index of the right key to use, instead of using the single offline key. Notes: - I had to add a field in DefaultDrmSession to be able to reuse the session. The existing schemeDatas is used in multiple ways, so changing it to keep the scheme datas even for offline playback would have required more changes (we try to limit the scope of changes to facilitate merges) - I initially also made the similar changes in MediaItem (which is given the offline key). But after investigation, turns out this code path is useless for us, we override or handle ourselves all the related object creation. So for the same reason as the previous point, I decided to revert those changes. --- .../androidx/media3/common/DrmInitData.java | 12 ++++ .../media3/common/PlaybackException.java | 5 ++ .../exoplayer/drm/DefaultDrmSession.java | 6 ++ .../drm/DefaultDrmSessionManager.java | 61 ++++++++++++++++--- 4 files changed, 76 insertions(+), 8 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/DrmInitData.java b/libraries/common/src/main/java/androidx/media3/common/DrmInitData.java index 32a5d234f73..44bf08aa570 100644 --- a/libraries/common/src/main/java/androidx/media3/common/DrmInitData.java +++ b/libraries/common/src/main/java/androidx/media3/common/DrmInitData.java @@ -370,6 +370,18 @@ public int hashCode() { return hashCode; } + // MIREGO: added toString + @Override + public String toString() { + return "SchemeData{" + + "hashCode=" + hashCode + + ", uuid=" + uuid + + ", licenseServerUrl='" + licenseServerUrl + '\'' + + ", mimeType='" + mimeType + '\'' + + ", data=" + Arrays.toString(data) + + '}'; + } + // Parcelable implementation. @Override diff --git a/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java b/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java index 2cbe9ed94fb..e37dca25808 100644 --- a/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java +++ b/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java @@ -310,6 +310,9 @@ public class PlaybackException extends Exception { /** MIREGO: Caused by the video codec being stalled (issue under investigation) */ public static final int ERROR_CODE_VIDEO_CODEC_STALLED = 5906; + /** MIREGO: Caused by a Drm offline key hashcode not found in the hash codes array */ + public static final int ERROR_CODE_OFFLINE_DRM_HASH_CODE_NOT_FOUND = 5907; + // DRM errors (6xxx). /** Caused by an unspecified error related to DRM protection. */ @@ -483,6 +486,8 @@ public static String getErrorCodeName(@ErrorCode int errorCode) { return "ERROR_CODE_NTP"; case ERROR_CODE_VIDEO_CODEC_STALLED: return "ERROR_CODE_VIDEO_CODEC_STALLED"; + case ERROR_CODE_OFFLINE_DRM_HASH_CODE_NOT_FOUND: + return "ERROR_CODE_OFFLINE_DRM_HASH_CODE_NOT_FOUND"; default: if (errorCode >= CUSTOM_ERROR_CODE_BASE) { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSession.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSession.java index 5f06b76f54c..a371886a04d 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSession.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSession.java @@ -123,6 +123,9 @@ public interface ReferenceCountListener { /** The DRM scheme datas, or null if this session uses offline keys. */ @Nullable public final List schemeDatas; + // MIREGO: added to be able to reuse offline sessions with compatible data without messing with the field schemeDatas, that is used to determine if it's an offline key when getting the key request + @Nullable public final List schemeDatasEvenOffline; + private final ExoMediaDrm mediaDrm; private final ProvisioningManager provisioningManager; private final ReferenceCountListener referenceCountListener; @@ -202,6 +205,9 @@ public DefaultDrmSession( } else { this.schemeDatas = Collections.unmodifiableList(Assertions.checkNotNull(schemeDatas)); } + // MIREGO: added. + this.schemeDatasEvenOffline = Collections.unmodifiableList(Assertions.checkNotNull(schemeDatas)); + this.keyRequestParameters = keyRequestParameters; this.callback = callback; this.eventDispatchers = new CopyOnWriteMultiset<>(); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSessionManager.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSessionManager.java index 12199a88b42..42cf497b877 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSessionManager.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSessionManager.java @@ -52,6 +52,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -324,7 +326,11 @@ private MissingSchemeDataException(UUID uuid) { private @MonotonicNonNull Looper playbackLooper; private @MonotonicNonNull Handler playbackHandler; private int mode; - @Nullable private byte[] offlineLicenseKeySetId; + + // MIREGO: multiple offline DRM keys. + private List offlineLicenseKeySetIdList; + private List drmInitDataHashList; + private @MonotonicNonNull PlayerId playerId; /* package */ @Nullable volatile MediaDrmHandler mediaDrmHandler; @@ -388,7 +394,21 @@ public void setMode(@Mode int mode, @Nullable byte[] offlineLicenseKeySetId) { checkNotNull(offlineLicenseKeySetId); } this.mode = mode; - this.offlineLicenseKeySetId = offlineLicenseKeySetId; + // MIREGO: multiple offline DRM keys. + this.offlineLicenseKeySetIdList = (offlineLicenseKeySetId != null) ? ImmutableList.of(offlineLicenseKeySetId) : Collections.emptyList(); + this.drmInitDataHashList = Collections.emptyList(); + } + + // MIREGO: multiple offline DRM keys. Added function + public void setMode(@Mode int mode, List offlineLicenseKeySetIdList, List drmInitDataHashList) { + Log.d(TAG, "setMode %d offlineLicenseKeySetId size=%s", mode, offlineLicenseKeySetIdList.size()); + checkState(sessions.isEmpty()); + if (mode == MODE_QUERY || mode == MODE_RELEASE) { + checkArgument(!offlineLicenseKeySetIdList.isEmpty()); + } + this.mode = mode; + this.offlineLicenseKeySetIdList = offlineLicenseKeySetIdList; + this.drmInitDataHashList = drmInitDataHashList; } // DrmSessionManager implementation. @@ -479,7 +499,7 @@ private DrmSession acquireSession( } @Nullable List schemeDatas = null; - if (offlineLicenseKeySetId == null) { + if (offlineLicenseKeySetIdList.isEmpty()) { // MIREGO: multiple offline DRM keys schemeDatas = getSchemeDatas(checkNotNull(format.drmInitData), uuid, false); if (schemeDatas.isEmpty()) { final MissingSchemeDataException error = new MissingSchemeDataException(uuid); @@ -490,6 +510,10 @@ private DrmSession acquireSession( return new ErrorStateDrmSession( new DrmSessionException(error, PlaybackException.ERROR_CODE_DRM_CONTENT_ERROR)); } + } else if (offlineLicenseKeySetIdList.size() > 1) { // MIREGO: multiple offline DRM keys. + // we have to select the right session or create a new one with the right drmInitData/key + // if we only have 1 key set, just fallback to legacy behavior (use a null schemeData so we always use the same session) + schemeDatas = getSchemeDatas(checkNotNull(format.drmInitData), uuid, false); } @Nullable DefaultDrmSession session; @@ -499,7 +523,7 @@ private DrmSession acquireSession( // Only use an existing session if it has matching init data. session = null; for (DefaultDrmSession existingSession : sessions) { - if (Util.areEqual(existingSession.schemeDatas, schemeDatas)) { + if (Util.areEqual(existingSession.schemeDatasEvenOffline, schemeDatas)) { // MIREGO: multiple offline DRM keys. session = existingSession; break; } @@ -511,6 +535,7 @@ private DrmSession acquireSession( session = createAndAcquireSessionWithRetry( schemeDatas, + format.drmInitData.hashCode(), // MIREGO: multiple offline DRM keys /* isPlaceholderSession= */ false, eventDispatcher, shouldReleasePreacquiredSessionsBeforeRetrying); @@ -558,6 +583,7 @@ private DrmSession maybeAcquirePlaceholderSession( DefaultDrmSession placeholderDrmSession = createAndAcquireSessionWithRetry( /* schemeDatas= */ ImmutableList.of(), + 0, // MIREGO: multiple offline DRM keys /* isPlaceholderSession= */ true, /* eventDispatcher= */ null, shouldReleasePreacquiredSessionsBeforeRetrying); @@ -570,7 +596,7 @@ private DrmSession maybeAcquirePlaceholderSession( } private boolean canAcquireSession(DrmInitData drmInitData) { - if (offlineLicenseKeySetId != null) { + if (!offlineLicenseKeySetIdList.isEmpty()) { // MIREGO: multiple offline DRM keys // An offline license can be restored so a session can always be acquired. return true; } @@ -623,17 +649,18 @@ private void maybeCreateMediaDrmHandler(Looper playbackLooper) { private DefaultDrmSession createAndAcquireSessionWithRetry( @Nullable List schemeDatas, + int drmInitDataHash, // MIREGO: multiple offline DRM keys boolean isPlaceholderSession, @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, boolean shouldReleasePreacquiredSessionsBeforeRetrying) { DefaultDrmSession session = - createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher); + createAndAcquireSession(schemeDatas, drmInitDataHash, isPlaceholderSession, eventDispatcher); // MIREGO: multiple offline DRM keys // If we're short on DRM session resources, first try eagerly releasing all our keepalive // sessions and then retry the acquisition. if (acquisitionFailedIndicatingResourceShortage(session) && !keepaliveSessions.isEmpty()) { releaseAllKeepaliveSessions(); undoAcquisition(session, eventDispatcher); - session = createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher); + session = createAndAcquireSession(schemeDatas, drmInitDataHash, isPlaceholderSession, eventDispatcher); // MIREGO: multiple offline DRM keys } // If the acquisition failed again due to continued resource shortage, and @@ -649,7 +676,7 @@ private DefaultDrmSession createAndAcquireSessionWithRetry( releaseAllKeepaliveSessions(); } undoAcquisition(session, eventDispatcher); - session = createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher); + session = createAndAcquireSession(schemeDatas, drmInitDataHash, isPlaceholderSession, eventDispatcher); // MIREGO: multiple offline DRM keys } return session; } @@ -703,11 +730,29 @@ private void releaseAllPreacquiredSessions() { */ private DefaultDrmSession createAndAcquireSession( @Nullable List schemeDatas, + int drmInitDataHash, // MIREGO: multiple offline DRM keys boolean isPlaceholderSession, @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) { checkNotNull(exoMediaDrm); // Placeholder sessions should always play clear samples without keys. boolean playClearSamplesWithoutKeys = this.playClearSamplesWithoutKeys | isPlaceholderSession; + + // MIREGO: multiple offline DRM keys. Added block to get the right keySetId + byte[] offlineLicenseKeySetId = null; + if (offlineLicenseKeySetIdList.size() == 1) { // Keep legacy behavior with single offline key (just use it) + offlineLicenseKeySetId = offlineLicenseKeySetIdList.get(0); + } else if (!offlineLicenseKeySetIdList.isEmpty()){ //multiple offline DRM keys. Find the right keyId from the drmInitData hash + int index = drmInitDataHashList.indexOf(drmInitDataHash); + if (index >= 0) { + offlineLicenseKeySetId = offlineLicenseKeySetIdList.get(index); + } else { // oops, we haven't found the drmInitData hash. We might as well fallback to the first key. + Log.e(TAG, + new PlaybackException("Offline Drm hash code not found. Trying first available key", new RuntimeException(), + PlaybackException.ERROR_CODE_OFFLINE_DRM_HASH_CODE_NOT_FOUND)); + offlineLicenseKeySetId = offlineLicenseKeySetIdList.get(0); + } + } + DefaultDrmSession session = new DefaultDrmSession( uuid,