Skip to content

Commit

Permalink
Add support for multiple offline DRM keys (#28)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
SylvainMorel authored Oct 24, 2024
1 parent a7c7de2 commit d88b7ec
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ public interface ReferenceCountListener {
/** The DRM scheme datas, or null if this session uses offline keys. */
@Nullable public final List<SchemeData> 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<SchemeData> schemeDatasEvenOffline;

private final ExoMediaDrm mediaDrm;
private final ProvisioningManager provisioningManager;
private final ReferenceCountListener referenceCountListener;
Expand Down Expand Up @@ -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<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<byte[]> offlineLicenseKeySetIdList;
private List<Integer> drmInitDataHashList;

private @MonotonicNonNull PlayerId playerId;

/* package */ @Nullable volatile MediaDrmHandler mediaDrmHandler;
Expand Down Expand Up @@ -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<byte[]> offlineLicenseKeySetIdList, List<Integer> 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.
Expand Down Expand Up @@ -479,7 +499,7 @@ private DrmSession acquireSession(
}

@Nullable List<SchemeData> 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);
Expand All @@ -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;
Expand All @@ -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;
}
Expand All @@ -511,6 +535,7 @@ private DrmSession acquireSession(
session =
createAndAcquireSessionWithRetry(
schemeDatas,
format.drmInitData.hashCode(), // MIREGO: multiple offline DRM keys
/* isPlaceholderSession= */ false,
eventDispatcher,
shouldReleasePreacquiredSessionsBeforeRetrying);
Expand Down Expand Up @@ -558,6 +583,7 @@ private DrmSession maybeAcquirePlaceholderSession(
DefaultDrmSession placeholderDrmSession =
createAndAcquireSessionWithRetry(
/* schemeDatas= */ ImmutableList.of(),
0, // MIREGO: multiple offline DRM keys
/* isPlaceholderSession= */ true,
/* eventDispatcher= */ null,
shouldReleasePreacquiredSessionsBeforeRetrying);
Expand All @@ -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;
}
Expand Down Expand Up @@ -623,17 +649,18 @@ private void maybeCreateMediaDrmHandler(Looper playbackLooper) {

private DefaultDrmSession createAndAcquireSessionWithRetry(
@Nullable List<SchemeData> 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
Expand All @@ -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;
}
Expand Down Expand Up @@ -703,11 +730,29 @@ private void releaseAllPreacquiredSessions() {
*/
private DefaultDrmSession createAndAcquireSession(
@Nullable List<SchemeData> 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,
Expand Down

0 comments on commit d88b7ec

Please sign in to comment.