From aba15b69522f66fe2d1e8ead1735f811fc6e81a3 Mon Sep 17 00:00:00 2001 From: Shanuj Shekhar Date: Mon, 25 Sep 2023 11:34:30 -0400 Subject: [PATCH 1/3] Add new APIs to ExoMediaDrm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes --- - Added `removeOfflineLicense(byte[])` and `getOfflineLicenseKeySetIds` and consumed them in their implementations Background --- - These APIs will help in addressing an increasing amount of `java.lang.IllegalArgumentException: Failed to restore keys: BAD_VALUE` which is our top playback error in our app - Based on our discussion with Widevine team and [this exoplayer issue](https://github.com/google/ExoPlayer/issues/11202#issuecomment-1708792594) - TL;DR: The failure occurs on startup if the user has 200+ offline licenses, we would like to add the functionality to remove offline licenses **Note: Why we want these APIs in ExoMediaDrm and not in OfflineLicenseHelper** - As per the issue above, we would like to access these 2 public APIs in MediaDrm that don’t exist in `OfflineLicenseHelper` or `ExoMediaDrm` - APIs interested in: - [MediaDrm#removeOfflineLicense()](https://developer.android.com/reference/android/media/MediaDrm#removeOfflineLicense(byte%5B%5D)): To remove offline license - [MediaDrm#getOfflineLicenseKeySetIds()](https://developer.android.com/reference/android/media/MediaDrm#getOfflineLicenseKeySetIds()): To see number of offline licenses on startup - We use `OfflineLicenseHelper` to download license for L1 and we don't interact with `ExoMediaDrm` directly. But for the alternate Widevine integration, we directly depend on `ExoMediaDrm` APIs to override and call CDM Native APIs. - We would like to have the functionality of removing offline licenses for both integration which would need access to above APIs in `ExoMediaDrm`. Links --- - https://github.com/androidx/media/issues/659 --- .../exoplayer/drm/DummyExoMediaDrm.java | 12 ++++++++++ .../media3/exoplayer/drm/ExoMediaDrm.java | 15 +++++++++++++ .../exoplayer/drm/FrameworkMediaDrm.java | 22 +++++++++++++++++++ .../media3/test/utils/FakeExoMediaDrm.java | 11 ++++++++++ 4 files changed, 60 insertions(+) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DummyExoMediaDrm.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DummyExoMediaDrm.java index 58efd2f082..24b0086c80 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DummyExoMediaDrm.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DummyExoMediaDrm.java @@ -24,6 +24,7 @@ import androidx.media3.common.DrmInitData; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.decoder.CryptoConfig; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -156,4 +157,15 @@ public final class DummyExoMediaDrm implements ExoMediaDrm { public @C.CryptoType int getCryptoType() { return C.CRYPTO_TYPE_UNSUPPORTED; } + + @Override + public void removeOfflineLicense(byte[] keySetId) { + // Should not be invoked. No session should exist. + throw new IllegalStateException(); + } + + @Override + public List getOfflineLicenseKeySetIds() { + return Collections.emptyList(); + } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/ExoMediaDrm.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/ExoMediaDrm.java index 9bd4205a7d..6a94bdfe03 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/ExoMediaDrm.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/ExoMediaDrm.java @@ -583,4 +583,19 @@ public interface ExoMediaDrm { */ @C.CryptoType int getCryptoType(); + + /** + * Removes an offline license. + * This method is generally not needed, and should only be used if the preferred approach of passing {@link MediaDrm#KEY_TYPE_RELEASE} to {@link #getKeyRequest} is not possible. + * + * See {@link MediaDrm#removeOfflineLicense(byte[])} for more details. + */ + void removeOfflineLicense(byte[] keySetId); + + /** + * This method returns a list of the keySetIds for all offline licenses. + * + * See {@link MediaDrm#getOfflineLicenseKeySetIds()} for more details. + */ + List getOfflineLicenseKeySetIds(); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/FrameworkMediaDrm.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/FrameworkMediaDrm.java index 9ed07382a9..21047593b9 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/FrameworkMediaDrm.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/FrameworkMediaDrm.java @@ -26,6 +26,7 @@ import android.media.MediaDrmException; import android.media.NotProvisionedException; import android.media.UnsupportedSchemeException; import android.media.metrics.LogSessionId; +import android.os.Build; import android.os.PersistableBundle; import android.text.TextUtils; import androidx.annotation.DoNotInline; @@ -46,6 +47,7 @@ import com.google.common.base.Charsets; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -380,6 +382,26 @@ public final class FrameworkMediaDrm implements ExoMediaDrm { return C.CRYPTO_TYPE_FRAMEWORK; } + @Override + @RequiresApi(Build.VERSION_CODES.Q) + public void removeOfflineLicense(byte[] keySetId) { + if (Util.SDK_INT < Build.VERSION_CODES.Q) { + throw new UnsupportedOperationException(); + } + + mediaDrm.removeOfflineLicense(keySetId); + } + + @Override + @RequiresApi(Build.VERSION_CODES.Q) + public List getOfflineLicenseKeySetIds() { + if (Util.SDK_INT < Build.VERSION_CODES.Q) { + return Collections.emptyList(); + } + + return mediaDrm.getOfflineLicenseKeySetIds(); + } + private static SchemeData getSchemeData(UUID uuid, List schemeDatas) { if (!C.WIDEVINE_UUID.equals(uuid)) { // For non-Widevine CDMs always use the first scheme data. diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeExoMediaDrm.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeExoMediaDrm.java index 4f802f073a..ce0339c889 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeExoMediaDrm.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeExoMediaDrm.java @@ -43,6 +43,7 @@ import com.google.common.primitives.Bytes; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -413,6 +414,16 @@ public final class FakeExoMediaDrm implements ExoMediaDrm { return FakeCryptoConfig.TYPE; } + @Override + public void removeOfflineLicense(byte[] keySetId) { + throw new UnsupportedOperationException(); + } + + @Override + public List getOfflineLicenseKeySetIds() { + return Collections.emptyList(); + } + // Methods to facilitate testing public int getReferenceCount() { From 8e6d3e7541fc43609b8ab8af5d589f2141d31328 Mon Sep 17 00:00:00 2001 From: Ian Baker Date: Tue, 26 Sep 2023 15:46:08 +0100 Subject: [PATCH 2/3] Reformat some javadoc and use Guava empty list --- .../exoplayer/drm/DummyExoMediaDrm.java | 24 +++++------ .../media3/exoplayer/drm/ExoMediaDrm.java | 39 ++++++++++++------ .../exoplayer/drm/FrameworkMediaDrm.java | 40 +++++++++---------- .../media3/test/utils/FakeExoMediaDrm.java | 21 +++++----- 4 files changed, 67 insertions(+), 57 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DummyExoMediaDrm.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DummyExoMediaDrm.java index 24b0086c80..634b9bd92a 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DummyExoMediaDrm.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DummyExoMediaDrm.java @@ -24,7 +24,7 @@ import androidx.media3.common.DrmInitData; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.decoder.CryptoConfig; -import java.util.Collections; +import com.google.common.collect.ImmutableList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -121,6 +121,17 @@ public final class DummyExoMediaDrm implements ExoMediaDrm { throw new IllegalStateException(); } + @Override + public void removeOfflineLicense(byte[] keySetId) { + // Should not be invoked. No session should exist. + throw new IllegalStateException(); + } + + @Override + public ImmutableList getOfflineLicenseKeySetIds() { + return ImmutableList.of(); + } + @Override @Nullable public PersistableBundle getMetrics() { @@ -157,15 +168,4 @@ public final class DummyExoMediaDrm implements ExoMediaDrm { public @C.CryptoType int getCryptoType() { return C.CRYPTO_TYPE_UNSUPPORTED; } - - @Override - public void removeOfflineLicense(byte[] keySetId) { - // Should not be invoked. No session should exist. - throw new IllegalStateException(); - } - - @Override - public List getOfflineLicenseKeySetIds() { - return Collections.emptyList(); - } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/ExoMediaDrm.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/ExoMediaDrm.java index 6a94bdfe03..711bcf665f 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/ExoMediaDrm.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/ExoMediaDrm.java @@ -523,6 +523,32 @@ public interface ExoMediaDrm { */ void restoreKeys(byte[] sessionId, byte[] keySetId); + /** + * Removes an offline license. + * + *

This method is generally not needed, and should only be used if the preferred approach of + * generating a license release request by passing {@link #KEY_TYPE_RELEASE} to {@link + * #getKeyRequest} is not possible. + * + *

This is an optional method, and some implementations may only support it on certain Android + * API levels. + * + *

See {@link MediaDrm#removeOfflineLicense(byte[])} for more details. + * + * @param keySetId The {@code keySetId} of the license to remove. + */ + void removeOfflineLicense(byte[] keySetId); + + /** + * Returns a list of the {@code keySetIds} for all offline licenses. + * + *

This is an optional method, and some implementations may only support it on certain Android + * API levels. If the method is unsupported an empty list is returned. + * + *

See {@link MediaDrm#getOfflineLicenseKeySetIds()} for more details. + */ + List getOfflineLicenseKeySetIds(); + /** * Returns metrics data for this ExoMediaDrm instance, or {@code null} if metrics are unavailable. */ @@ -584,18 +610,5 @@ public interface ExoMediaDrm { @C.CryptoType int getCryptoType(); - /** - * Removes an offline license. - * This method is generally not needed, and should only be used if the preferred approach of passing {@link MediaDrm#KEY_TYPE_RELEASE} to {@link #getKeyRequest} is not possible. - * - * See {@link MediaDrm#removeOfflineLicense(byte[])} for more details. - */ - void removeOfflineLicense(byte[] keySetId); - /** - * This method returns a list of the keySetIds for all offline licenses. - * - * See {@link MediaDrm#getOfflineLicenseKeySetIds()} for more details. - */ - List getOfflineLicenseKeySetIds(); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/FrameworkMediaDrm.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/FrameworkMediaDrm.java index 21047593b9..d8d348dbd5 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/FrameworkMediaDrm.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/FrameworkMediaDrm.java @@ -44,10 +44,10 @@ import androidx.media3.common.util.Util; import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.extractor.mp4.PsshAtomUtil; import com.google.common.base.Charsets; +import com.google.common.collect.ImmutableList; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -329,6 +329,24 @@ public final class FrameworkMediaDrm implements ExoMediaDrm { mediaDrm.restoreKeys(sessionId, keySetId); } + @Override + @RequiresApi(Build.VERSION_CODES.Q) + public void removeOfflineLicense(byte[] keySetId) { + if (Util.SDK_INT < Build.VERSION_CODES.Q) { + throw new UnsupportedOperationException(); + } + mediaDrm.removeOfflineLicense(keySetId); + } + + @Override + @RequiresApi(Build.VERSION_CODES.Q) + public List getOfflineLicenseKeySetIds() { + if (Util.SDK_INT < Build.VERSION_CODES.Q) { + return ImmutableList.of(); + } + return mediaDrm.getOfflineLicenseKeySetIds(); + } + @UnstableApi @Override @Nullable @@ -382,26 +400,6 @@ public final class FrameworkMediaDrm implements ExoMediaDrm { return C.CRYPTO_TYPE_FRAMEWORK; } - @Override - @RequiresApi(Build.VERSION_CODES.Q) - public void removeOfflineLicense(byte[] keySetId) { - if (Util.SDK_INT < Build.VERSION_CODES.Q) { - throw new UnsupportedOperationException(); - } - - mediaDrm.removeOfflineLicense(keySetId); - } - - @Override - @RequiresApi(Build.VERSION_CODES.Q) - public List getOfflineLicenseKeySetIds() { - if (Util.SDK_INT < Build.VERSION_CODES.Q) { - return Collections.emptyList(); - } - - return mediaDrm.getOfflineLicenseKeySetIds(); - } - private static SchemeData getSchemeData(UUID uuid, List schemeDatas) { if (!C.WIDEVINE_UUID.equals(uuid)) { // For non-Widevine CDMs always use the first scheme data. diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeExoMediaDrm.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeExoMediaDrm.java index ce0339c889..63e05fc411 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeExoMediaDrm.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeExoMediaDrm.java @@ -43,7 +43,6 @@ import com.google.common.primitives.Bytes; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -362,6 +361,16 @@ public final class FakeExoMediaDrm implements ExoMediaDrm { throw new UnsupportedOperationException(); } + @Override + public void removeOfflineLicense(byte[] keySetId) { + throw new UnsupportedOperationException(); + } + + @Override + public ImmutableList getOfflineLicenseKeySetIds() { + return ImmutableList.of(); + } + @Nullable @Override public PersistableBundle getMetrics() { @@ -414,16 +423,6 @@ public final class FakeExoMediaDrm implements ExoMediaDrm { return FakeCryptoConfig.TYPE; } - @Override - public void removeOfflineLicense(byte[] keySetId) { - throw new UnsupportedOperationException(); - } - - @Override - public List getOfflineLicenseKeySetIds() { - return Collections.emptyList(); - } - // Methods to facilitate testing public int getReferenceCount() { From a1767d349ad538bb3203ed63190f43138eef7802 Mon Sep 17 00:00:00 2001 From: Ian Baker Date: Thu, 28 Sep 2023 15:54:10 +0100 Subject: [PATCH 3/3] Fix review comments --- .../media3/exoplayer/drm/DummyExoMediaDrm.java | 12 ------------ .../media3/exoplayer/drm/ExoMediaDrm.java | 16 +++++++++++----- .../media3/exoplayer/drm/FrameworkMediaDrm.java | 14 +++++++------- .../media3/test/utils/FakeExoMediaDrm.java | 10 ---------- 4 files changed, 18 insertions(+), 34 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DummyExoMediaDrm.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DummyExoMediaDrm.java index 634b9bd92a..58efd2f082 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DummyExoMediaDrm.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DummyExoMediaDrm.java @@ -24,7 +24,6 @@ import androidx.media3.common.DrmInitData; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.decoder.CryptoConfig; -import com.google.common.collect.ImmutableList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -121,17 +120,6 @@ public final class DummyExoMediaDrm implements ExoMediaDrm { throw new IllegalStateException(); } - @Override - public void removeOfflineLicense(byte[] keySetId) { - // Should not be invoked. No session should exist. - throw new IllegalStateException(); - } - - @Override - public ImmutableList getOfflineLicenseKeySetIds() { - return ImmutableList.of(); - } - @Override @Nullable public PersistableBundle getMetrics() { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/ExoMediaDrm.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/ExoMediaDrm.java index 711bcf665f..3d41aa667f 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/ExoMediaDrm.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/ExoMediaDrm.java @@ -536,18 +536,26 @@ public interface ExoMediaDrm { *

See {@link MediaDrm#removeOfflineLicense(byte[])} for more details. * * @param keySetId The {@code keySetId} of the license to remove. + * @throws UnsupportedOperationException if the implementation doesn't support this method. */ - void removeOfflineLicense(byte[] keySetId); + default void removeOfflineLicense(byte[] keySetId) { + throw new UnsupportedOperationException(); + } /** * Returns a list of the {@code keySetIds} for all offline licenses. * *

This is an optional method, and some implementations may only support it on certain Android - * API levels. If the method is unsupported an empty list is returned. + * API levels. * *

See {@link MediaDrm#getOfflineLicenseKeySetIds()} for more details. + * + * @return The list of {@code keySetIds} for all offline licenses. + * @throws UnsupportedOperationException if the implementation doesn't support this method. */ - List getOfflineLicenseKeySetIds(); + default List getOfflineLicenseKeySetIds() { + throw new UnsupportedOperationException(); + } /** * Returns metrics data for this ExoMediaDrm instance, or {@code null} if metrics are unavailable. @@ -609,6 +617,4 @@ public interface ExoMediaDrm { */ @C.CryptoType int getCryptoType(); - - } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/FrameworkMediaDrm.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/FrameworkMediaDrm.java index d8d348dbd5..1cfb5d8dea 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/FrameworkMediaDrm.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/FrameworkMediaDrm.java @@ -26,7 +26,6 @@ import android.media.MediaDrmException; import android.media.NotProvisionedException; import android.media.UnsupportedSchemeException; import android.media.metrics.LogSessionId; -import android.os.Build; import android.os.PersistableBundle; import android.text.TextUtils; import androidx.annotation.DoNotInline; @@ -44,7 +43,6 @@ import androidx.media3.common.util.Util; import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.extractor.mp4.PsshAtomUtil; import com.google.common.base.Charsets; -import com.google.common.collect.ImmutableList; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; @@ -330,19 +328,21 @@ public final class FrameworkMediaDrm implements ExoMediaDrm { } @Override - @RequiresApi(Build.VERSION_CODES.Q) + @UnstableApi + @RequiresApi(29) public void removeOfflineLicense(byte[] keySetId) { - if (Util.SDK_INT < Build.VERSION_CODES.Q) { + if (Util.SDK_INT < 29) { throw new UnsupportedOperationException(); } mediaDrm.removeOfflineLicense(keySetId); } @Override - @RequiresApi(Build.VERSION_CODES.Q) + @UnstableApi + @RequiresApi(29) public List getOfflineLicenseKeySetIds() { - if (Util.SDK_INT < Build.VERSION_CODES.Q) { - return ImmutableList.of(); + if (Util.SDK_INT < 29) { + throw new UnsupportedOperationException(); } return mediaDrm.getOfflineLicenseKeySetIds(); } diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeExoMediaDrm.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeExoMediaDrm.java index 63e05fc411..4f802f073a 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeExoMediaDrm.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeExoMediaDrm.java @@ -361,16 +361,6 @@ public final class FakeExoMediaDrm implements ExoMediaDrm { throw new UnsupportedOperationException(); } - @Override - public void removeOfflineLicense(byte[] keySetId) { - throw new UnsupportedOperationException(); - } - - @Override - public ImmutableList getOfflineLicenseKeySetIds() { - return ImmutableList.of(); - } - @Nullable @Override public PersistableBundle getMetrics() {