mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Offline DRM in main demo app
PiperOrigin-RevId: 325413035
This commit is contained in:
parent
c7a1151c2b
commit
a5e6e3054d
8 changed files with 130 additions and 34 deletions
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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 {
|
||||||
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;
|
||||||
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;
|
||||||
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 {
|
||||||
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 {
|
||||||
@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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
|
|
||||||
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 {
|
||||||
|
|
||||||
@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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
}
|
}
|
||||||
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 @@
|
||||||
|
|
||||||
<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 @@
|
||||||
|
|
||||||
<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 {
|
||||||
* @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 {
|
||||||
}
|
}
|
||||||
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.
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,16 @@ public final class DownloadRequest implements Parcelable {
|
||||||
return new DownloadRequest(id, uri, mimeType, streamKeys, keySetId, customCacheKey, data);
|
return new DownloadRequest(id, uri, mimeType, streamKeys, keySetId, customCacheKey, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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}.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue