Commit a5e6e305 by christosts Committed by kim-vde

Offline DRM in main demo app

PiperOrigin-RevId: 325413035
parent c7a1151c
...@@ -168,6 +168,7 @@ ...@@ -168,6 +168,7 @@
[#6725](https://github.com/google/ExoPlayer/issues/6725), [#6725](https://github.com/google/ExoPlayer/issues/6725),
[#7066](https://github.com/google/ExoPlayer/issues/7066)). [#7066](https://github.com/google/ExoPlayer/issues/7066)).
* Downloads and caching: * Downloads and caching:
* Add support for offline DRM playbacks.
* Add builder in `DownloadRequest`. * Add builder in `DownloadRequest`.
* Support passing an `Executor` to `DefaultDownloaderFactory` on which * Support passing an `Executor` to `DefaultDownloaderFactory` on which
data downloads are performed. data downloads are performed.
......
...@@ -93,7 +93,7 @@ public final class DemoUtil { ...@@ -93,7 +93,7 @@ public final class DemoUtil {
} }
/** Returns a {@link DataSource.Factory}. */ /** Returns a {@link DataSource.Factory}. */
public static synchronized DataSource.Factory buildDataSourceFactory(Context context) { public static synchronized DataSource.Factory getDataSourceFactory(Context context) {
if (dataSourceFactory == null) { if (dataSourceFactory == null) {
context = context.getApplicationContext(); context = context.getApplicationContext();
DefaultDataSourceFactory upstreamFactory = DefaultDataSourceFactory upstreamFactory =
...@@ -151,7 +151,7 @@ public final class DemoUtil { ...@@ -151,7 +151,7 @@ public final class DemoUtil {
getHttpDataSourceFactory(context), getHttpDataSourceFactory(context),
Executors.newFixedThreadPool(/* nThreads= */ 6)); Executors.newFixedThreadPool(/* nThreads= */ 6));
downloadTracker = downloadTracker =
new DownloadTracker(context, buildDataSourceFactory(context), downloadManager); new DownloadTracker(context, getHttpDataSourceFactory(context), downloadManager);
} }
} }
......
...@@ -23,9 +23,15 @@ import android.net.Uri; ...@@ -23,9 +23,15 @@ import android.net.Uri;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.drm.OfflineLicenseHelper;
import com.google.android.exoplayer2.offline.Download; import com.google.android.exoplayer2.offline.Download;
import com.google.android.exoplayer2.offline.DownloadCursor; import com.google.android.exoplayer2.offline.DownloadCursor;
import com.google.android.exoplayer2.offline.DownloadHelper; import com.google.android.exoplayer2.offline.DownloadHelper;
...@@ -33,9 +39,11 @@ import com.google.android.exoplayer2.offline.DownloadIndex; ...@@ -33,9 +39,11 @@ import com.google.android.exoplayer2.offline.DownloadIndex;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.DownloadRequest;
import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
...@@ -55,7 +63,7 @@ public class DownloadTracker { ...@@ -55,7 +63,7 @@ public class DownloadTracker {
private static final String TAG = "DownloadTracker"; private static final String TAG = "DownloadTracker";
private final Context context; private final Context context;
private final DataSource.Factory dataSourceFactory; private final HttpDataSource.Factory httpDataSourceFactory;
private final CopyOnWriteArraySet<Listener> listeners; private final CopyOnWriteArraySet<Listener> listeners;
private final HashMap<Uri, Download> downloads; private final HashMap<Uri, Download> downloads;
private final DownloadIndex downloadIndex; private final DownloadIndex downloadIndex;
...@@ -64,9 +72,11 @@ public class DownloadTracker { ...@@ -64,9 +72,11 @@ public class DownloadTracker {
@Nullable private StartDownloadDialogHelper startDownloadDialogHelper; @Nullable private StartDownloadDialogHelper startDownloadDialogHelper;
public DownloadTracker( public DownloadTracker(
Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) { Context context,
HttpDataSource.Factory httpDataSourceFactory,
DownloadManager downloadManager) {
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
this.dataSourceFactory = dataSourceFactory; this.httpDataSourceFactory = httpDataSourceFactory;
listeners = new CopyOnWriteArraySet<>(); listeners = new CopyOnWriteArraySet<>();
downloads = new HashMap<>(); downloads = new HashMap<>();
downloadIndex = downloadManager.getDownloadIndex(); downloadIndex = downloadManager.getDownloadIndex();
...@@ -89,6 +99,7 @@ public class DownloadTracker { ...@@ -89,6 +99,7 @@ public class DownloadTracker {
return download != null && download.state != Download.STATE_FAILED; return download != null && download.state != Download.STATE_FAILED;
} }
@Nullable
public DownloadRequest getDownloadRequest(Uri uri) { public DownloadRequest getDownloadRequest(Uri uri) {
Download download = downloads.get(uri); Download download = downloads.get(uri);
return download != null && download.state != Download.STATE_FAILED ? download.request : null; return download != null && download.state != Download.STATE_FAILED ? download.request : null;
...@@ -107,7 +118,8 @@ public class DownloadTracker { ...@@ -107,7 +118,8 @@ public class DownloadTracker {
startDownloadDialogHelper = startDownloadDialogHelper =
new StartDownloadDialogHelper( new StartDownloadDialogHelper(
fragmentManager, fragmentManager,
DownloadHelper.forMediaItem(context, mediaItem, renderersFactory, dataSourceFactory), DownloadHelper.forMediaItem(
context, mediaItem, renderersFactory, httpDataSourceFactory),
mediaItem); mediaItem);
} }
} }
...@@ -157,6 +169,7 @@ public class DownloadTracker { ...@@ -157,6 +169,7 @@ public class DownloadTracker {
private TrackSelectionDialog trackSelectionDialog; private TrackSelectionDialog trackSelectionDialog;
private MappedTrackInfo mappedTrackInfo; private MappedTrackInfo mappedTrackInfo;
@Nullable private byte[] keySetId;
public StartDownloadDialogHelper( public StartDownloadDialogHelper(
FragmentManager fragmentManager, DownloadHelper downloadHelper, MediaItem mediaItem) { FragmentManager fragmentManager, DownloadHelper downloadHelper, MediaItem mediaItem) {
...@@ -177,12 +190,43 @@ public class DownloadTracker { ...@@ -177,12 +190,43 @@ public class DownloadTracker {
@Override @Override
public void onPrepared(@NonNull DownloadHelper helper) { public void onPrepared(@NonNull DownloadHelper helper) {
@Nullable DrmInitData drmInitData = findDrmInitData(helper);
if (drmInitData != null) {
if (Util.SDK_INT < 18) {
Toast.makeText(context, R.string.error_drm_unsupported_before_api_18, Toast.LENGTH_LONG)
.show();
Log.e(TAG, "Downloading DRM protected content is not supported on API versions below 18");
return;
}
// TODO(internal b/163107948): Support cases where DrmInitData are not in the manifest.
if (!hasSchemaData(drmInitData)) {
Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG)
.show();
Log.e(
TAG,
"Downloading content where DRM scheme data is not located in the manifest is not"
+ " supported");
return;
}
try {
// TODO(internal b/163107948): Download the license on another thread to keep the UI
// thread unblocked.
fetchOfflineLicense(drmInitData);
} catch (DrmSession.DrmSessionException e) {
Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG)
.show();
Log.e(TAG, "Failed to fetch offline DRM license", e);
return;
}
}
if (helper.getPeriodCount() == 0) { if (helper.getPeriodCount() == 0) {
Log.d(TAG, "No periods found. Downloading entire stream."); Log.d(TAG, "No periods found. Downloading entire stream.");
startDownload(); startDownload();
downloadHelper.release(); downloadHelper.release();
return; return;
} }
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0); mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) { if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) {
Log.d(TAG, "No dialog content. Downloading entire stream."); Log.d(TAG, "No dialog content. Downloading entire stream.");
...@@ -257,8 +301,59 @@ public class DownloadTracker { ...@@ -257,8 +301,59 @@ public class DownloadTracker {
} }
private DownloadRequest buildDownloadRequest() { private DownloadRequest buildDownloadRequest() {
return downloadHelper.getDownloadRequest( return downloadHelper
Util.getUtf8Bytes(checkNotNull(mediaItem.mediaMetadata.title))); .getDownloadRequest(Util.getUtf8Bytes(checkNotNull(mediaItem.mediaMetadata.title)))
.copyWithKeySetId(keySetId);
}
@RequiresApi(18)
private void fetchOfflineLicense(DrmInitData drmInitData)
throws DrmSession.DrmSessionException {
OfflineLicenseHelper offlineLicenseHelper =
OfflineLicenseHelper.newWidevineInstance(
mediaItem.playbackProperties.drmConfiguration.licenseUri.toString(),
httpDataSourceFactory,
new DrmSessionEventListener.EventDispatcher());
keySetId = offlineLicenseHelper.downloadLicense(drmInitData);
}
}
/**
* Returns whether any the {@link DrmInitData.SchemeData} contained in {@code drmInitData} has
* non-null {@link DrmInitData.SchemeData#data}.
*/
private static boolean hasSchemaData(DrmInitData drmInitData) {
for (int i = 0; i < drmInitData.schemeDataCount; i++) {
if (drmInitData.get(i).hasData()) {
return true;
}
}
return false;
}
/**
* Returns the first non-null {@link DrmInitData} found in the content's tracks, or null if no
* {@link DrmInitData} are found.
*/
@Nullable
private DrmInitData findDrmInitData(DownloadHelper helper) {
for (int periodIndex = 0; periodIndex < helper.getPeriodCount(); periodIndex++) {
MappedTrackInfo mappedTrackInfo = helper.getMappedTrackInfo(periodIndex);
for (int rendererIndex = 0;
rendererIndex < mappedTrackInfo.getRendererCount();
rendererIndex++) {
TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(rendererIndex);
for (int trackGroupIndex = 0; trackGroupIndex < trackGroups.length; trackGroupIndex++) {
TrackGroup trackGroup = trackGroups.get(trackGroupIndex);
for (int formatIndex = 0; formatIndex < trackGroup.length; formatIndex++) {
Format format = trackGroup.getFormat(formatIndex);
if (format.drmInitData != null) {
return format.drmInitData;
}
}
}
}
} }
return null;
} }
} }
...@@ -112,7 +112,7 @@ public class PlayerActivity extends AppCompatActivity ...@@ -112,7 +112,7 @@ public class PlayerActivity extends AppCompatActivity
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
Intent intent = getIntent(); Intent intent = getIntent();
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
dataSourceFactory = buildDataSourceFactory(); dataSourceFactory = DemoUtil.getDataSourceFactory(/* context= */ this);
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) { if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER); CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
} }
...@@ -405,11 +405,6 @@ public class PlayerActivity extends AppCompatActivity ...@@ -405,11 +405,6 @@ public class PlayerActivity extends AppCompatActivity
startPosition = C.TIME_UNSET; startPosition = C.TIME_UNSET;
} }
/** Returns a new DataSource factory. */
protected DataSource.Factory buildDataSourceFactory() {
return DemoUtil.buildDataSourceFactory(/* context= */ this);
}
// User controls // User controls
private void updateButtonVisibility() { private void updateButtonVisibility() {
......
...@@ -253,9 +253,6 @@ public class SampleChooserActivity extends AppCompatActivity ...@@ -253,9 +253,6 @@ public class SampleChooserActivity extends AppCompatActivity
} }
MediaItem.PlaybackProperties playbackProperties = MediaItem.PlaybackProperties playbackProperties =
checkNotNull(playlistHolder.mediaItems.get(0).playbackProperties); checkNotNull(playlistHolder.mediaItems.get(0).playbackProperties);
if (playbackProperties.drmConfiguration != null) {
return R.string.download_drm_unsupported;
}
if (((IntentUtil.Tag) checkNotNull(playbackProperties.tag)).isLive) { if (((IntentUtil.Tag) checkNotNull(playbackProperties.tag)).isLive) {
return R.string.download_live_unsupported; return R.string.download_live_unsupported;
} }
......
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
<string name="error_unrecognized_stereo_mode">Unrecognized stereo mode</string> <string name="error_unrecognized_stereo_mode">Unrecognized stereo mode</string>
<string name="error_drm_unsupported_before_api_18">Protected content not supported on API levels below 18</string> <string name="error_drm_unsupported_before_api_18">DRM content not supported on API levels below 18</string>
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string> <string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
...@@ -55,9 +55,9 @@ ...@@ -55,9 +55,9 @@
<string name="download_start_error">Failed to start download</string> <string name="download_start_error">Failed to start download</string>
<string name="download_playlist_unsupported">This demo app does not support downloading playlists</string> <string name="download_start_error_offline_license">Failed to obtain offline license</string>
<string name="download_drm_unsupported">This demo app does not support downloading protected content</string> <string name="download_playlist_unsupported">This demo app does not support downloading playlists</string>
<string name="download_scheme_unsupported">This demo app only supports downloading http streams</string> <string name="download_scheme_unsupported">This demo app only supports downloading http streams</string>
......
...@@ -743,13 +743,17 @@ public final class DownloadHelper { ...@@ -743,13 +743,17 @@ public final class DownloadHelper {
* @return The built {@link DownloadRequest}. * @return The built {@link DownloadRequest}.
*/ */
public DownloadRequest getDownloadRequest(String id, @Nullable byte[] data) { public DownloadRequest getDownloadRequest(String id, @Nullable byte[] data) {
DownloadRequest.Builder requestBuilder =
new DownloadRequest.Builder(id, playbackProperties.uri)
.setMimeType(playbackProperties.mimeType)
.setKeySetId(
playbackProperties.drmConfiguration != null
? playbackProperties.drmConfiguration.getKeySetId()
: null)
.setCustomCacheKey(playbackProperties.customCacheKey)
.setData(data);
if (mediaSource == null) { if (mediaSource == null) {
// TODO: add support for DRM (keySetId) [Internal ref: b/158980798] return requestBuilder.build();
return new DownloadRequest.Builder(id, playbackProperties.uri)
.setMimeType(playbackProperties.mimeType)
.setCustomCacheKey(playbackProperties.customCacheKey)
.setData(data)
.build();
} }
assertPreparedWithMedia(); assertPreparedWithMedia();
List<StreamKey> streamKeys = new ArrayList<>(); List<StreamKey> streamKeys = new ArrayList<>();
...@@ -763,13 +767,7 @@ public final class DownloadHelper { ...@@ -763,13 +767,7 @@ public final class DownloadHelper {
} }
streamKeys.addAll(mediaPreparer.mediaPeriods[periodIndex].getStreamKeys(allSelections)); streamKeys.addAll(mediaPreparer.mediaPeriods[periodIndex].getStreamKeys(allSelections));
} }
// TODO: add support for DRM (keySetId) [Internal ref: b/158980798] return requestBuilder.setStreamKeys(streamKeys).build();
return new DownloadRequest.Builder(id, playbackProperties.uri)
.setMimeType(playbackProperties.mimeType)
.setStreamKeys(streamKeys)
.setCustomCacheKey(playbackProperties.customCacheKey)
.setData(data)
.build();
} }
// Initialization of array of Lists. // Initialization of array of Lists.
......
...@@ -175,6 +175,16 @@ public final class DownloadRequest implements Parcelable { ...@@ -175,6 +175,16 @@ public final class DownloadRequest implements Parcelable {
} }
/** /**
* Returns a copy with the specified key set ID.
*
* @param keySetId The key set ID of the copy.
* @return The copy with the specified key set ID.
*/
public DownloadRequest copyWithKeySetId(@Nullable byte[] keySetId) {
return new DownloadRequest(id, uri, mimeType, streamKeys, keySetId, customCacheKey, data);
}
/**
* Returns the result of merging {@code newRequest} into this request. The requests must have the * Returns the result of merging {@code newRequest} into this request. The requests must have the
* same {@link #id}. * same {@link #id}.
* *
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment