diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 06c1ed7f80..deda085f40 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,7 @@ ### dev-v2 (not yet released) ### +* Add basic DRM support to the Cast demo app. * Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s ([#5779](https://github.com/google/ExoPlayer/issues/5779)). * Assume that encrypted content requires secure decoders in renderer support diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java index fcee88ec49..a837bd77e5 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java @@ -44,11 +44,14 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaMetadata; import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.framework.CastContext; import java.util.ArrayList; +import org.json.JSONException; +import org.json.JSONObject; /** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */ /* package */ class DefaultReceiverPlayerManager @@ -394,12 +397,47 @@ import java.util.ArrayList; private static MediaQueueItem buildMediaQueueItem(MediaItem item) { MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title); - MediaInfo mediaInfo = + MediaInfo.Builder mediaInfoBuilder = new MediaInfo.Builder(item.media.uri.toString()) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) .setContentType(item.mimeType) - .setMetadata(movieMetadata) - .build(); - return new MediaQueueItem.Builder(mediaInfo).build(); + .setMetadata(movieMetadata); + if (!item.drmSchemes.isEmpty()) { + MediaItem.DrmScheme scheme = item.drmSchemes.get(0); + try { + // This configuration is only intended for testing and should *not* be used in production + // environments. See comment in the Cast Demo app's options provider. + JSONObject drmConfiguration = getDrmConfigurationJson(scheme); + if (drmConfiguration != null) { + mediaInfoBuilder.setCustomData(drmConfiguration); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + return new MediaQueueItem.Builder(mediaInfoBuilder.build()).build(); + } + + @Nullable + private static JSONObject getDrmConfigurationJson(MediaItem.DrmScheme scheme) + throws JSONException { + String drmScheme; + if (C.WIDEVINE_UUID.equals(scheme.uuid)) { + drmScheme = "widevine"; + } else if (C.PLAYREADY_UUID.equals(scheme.uuid)) { + drmScheme = "playready"; + } else { + return null; + } + MediaItem.UriBundle licenseServer = Assertions.checkNotNull(scheme.licenseServer); + JSONObject exoplayerConfig = + new JSONObject().put("withCredentials", false).put("protectionSystem", drmScheme); + if (!licenseServer.uri.equals(Uri.EMPTY)) { + exoplayerConfig.put("licenseUrl", licenseServer.uri.toString()); + } + if (!licenseServer.requestHeaders.isEmpty()) { + exoplayerConfig.put("headers", new JSONObject(licenseServer.requestHeaders)); + } + return new JSONObject().put("exoPlayerConfig", exoplayerConfig); } } diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java index 9625304252..9599da15cb 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java @@ -15,13 +15,13 @@ */ package com.google.android.exoplayer2.castdemo; -import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.MimeTypes; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.UUID; /** Utility methods and constants for the Cast demo application. */ @@ -30,44 +30,25 @@ import java.util.UUID; /** Represents a media sample. */ public static final class Sample { - /** The uri of the media content. */ + /** The URI of the media content. */ public final String uri; /** The name of the sample. */ public final String name; /** The mime type of the sample media content. */ public final String mimeType; - /** - * The {@link UUID} of the DRM scheme that protects the content, or null if the content is not - * DRM-protected. - */ - @Nullable public final UUID drmSchemeUuid; - /** - * The url from which players should obtain DRM licenses, or null if the content is not - * DRM-protected. - */ - @Nullable public final Uri licenseServerUri; + /** Data to configure DRM license acquisition. May be null if content is not DRM-protected. */ + @Nullable public final DrmConfiguration drmConfiguration; - /** - * @param uri See {@link #uri}. - * @param name See {@link #name}. - * @param mimeType See {@link #mimeType}. - */ public Sample(String uri, String name, String mimeType) { - this(uri, name, mimeType, /* drmSchemeUuid= */ null, /* licenseServerUriString= */ null); + this(uri, name, mimeType, /* drmConfiguration= */ null); } public Sample( - String uri, - String name, - String mimeType, - @Nullable UUID drmSchemeUuid, - @Nullable String licenseServerUriString) { + String uri, String name, String mimeType, @Nullable DrmConfiguration drmConfiguration) { this.uri = uri; this.name = name; this.mimeType = mimeType; - this.drmSchemeUuid = drmSchemeUuid; - this.licenseServerUri = - licenseServerUriString != null ? Uri.parse(licenseServerUriString) : null; + this.drmConfiguration = drmConfiguration; } @Override @@ -76,6 +57,29 @@ import java.util.UUID; } } + /** Holds information required to play DRM-protected content. */ + public static final class DrmConfiguration { + + /** The {@link UUID} of the DRM scheme that protects the content. */ + public final UUID drmSchemeUuid; + /** + * The URI from which players should obtain DRM licenses. May be null if the license server URI + * is provided as part of the media. + */ + @Nullable public final String licenseServerUri; + /** HTTP request headers to include the in DRM license requests. */ + public final Map httpRequestHeaders; + + public DrmConfiguration( + UUID drmSchemeUuid, + @Nullable String licenseServerUri, + Map httpRequestHeaders) { + this.drmSchemeUuid = drmSchemeUuid; + this.licenseServerUri = licenseServerUri; + this.httpRequestHeaders = httpRequestHeaders; + } + } + public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD; public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8; public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS; diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 17eeed2da7..5ed434eed6 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.castdemo; import android.content.Context; +import android.net.Uri; import android.os.Bundle; import androidx.core.graphics.ColorUtils; import androidx.appcompat.app.AlertDialog; @@ -36,6 +37,7 @@ import android.widget.TextView; import android.widget.Toast; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider; import com.google.android.exoplayer2.ext.cast.MediaItem; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; @@ -121,6 +123,7 @@ public class MainActivity extends AppCompatActivity String applicationId = castContext.getCastOptions().getReceiverApplicationId(); switch (applicationId) { case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID: + case DefaultCastOptionsProvider.APP_ID_DEFAULT_RECEIVER_WITH_DRM: playerManager = new DefaultReceiverPlayerManager( /* listener= */ this, @@ -202,11 +205,17 @@ public class MainActivity extends AppCompatActivity .setMedia(sample.uri) .setTitle(sample.name) .setMimeType(sample.mimeType); - if (sample.drmSchemeUuid != null) { + DemoUtil.DrmConfiguration drmConfiguration = sample.drmConfiguration; + if (drmConfiguration != null) { mediaItemBuilder.setDrmSchemes( Collections.singletonList( new MediaItem.DrmScheme( - sample.drmSchemeUuid, new MediaItem.UriBundle(sample.licenseServerUri)))); + drmConfiguration.drmSchemeUuid, + new MediaItem.UriBundle( + drmConfiguration.licenseServerUri != null + ? Uri.parse(drmConfiguration.licenseServerUri) + : Uri.EMPTY, + drmConfiguration.httpRequestHeaders)))); } playerManager.addItem(mediaItemBuilder.build()); mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java index 06f0bec971..5aed1373e5 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java @@ -27,11 +27,33 @@ import java.util.List; */ public final class DefaultCastOptionsProvider implements OptionsProvider { + /** + * App id of the Default Media Receiver app. Apps that do not require DRM support may use this + * receiver receiver app ID. + * + *

See https://developers.google.com/cast/docs/caf_receiver/#default_media_receiver. + */ + public static final String APP_ID_DEFAULT_RECEIVER = + CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID; + + /** + * App id for receiver app with rudimentary support for DRM. + * + *

This app id is only suitable for ExoPlayer's Cast Demo app, and it is not intended for + * production use. In order to use DRM, custom receiver apps should be used. For environments that + * do not require DRM, the default receiver app should be used (see {@link + * #APP_ID_DEFAULT_RECEIVER}). + */ + // TODO: Add a documentation resource link for DRM support in the receiver app [Internal ref: + // b/128603245]. + public static final String APP_ID_DEFAULT_RECEIVER_WITH_DRM = "A12D4273"; + @Override public CastOptions getCastOptions(Context context) { return new CastOptions.Builder() - .setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID) - .setStopReceiverApplicationWhenEndingSession(true).build(); + .setReceiverApplicationId(APP_ID_DEFAULT_RECEIVER_WITH_DRM) + .setStopReceiverApplicationWhenEndingSession(true) + .build(); } @Override