From a56975877983aeb66b807dac23e85e562f197384 Mon Sep 17 00:00:00 2001 From: Hong Ngoc Nguyen Date: Mon, 5 Feb 2024 18:28:12 -0500 Subject: [PATCH] [APT-9577] Add DRM for downloads for offline usage : add DRM support for MPEG-Dash audio playback in both online and offline mode Create new helper DrmMediaSourceHelper to generate the correct media item with DRM info depending on if the content is downloaded/being downloaded. If the content is streaming, we only need to include the general DRM info so the DRM license can be fetched from the server to decrypt the encrypted content. If the content is a download, we need to include all DRM info as well as the DRM key ID so the local DRM license can be used for decryption instead. --- .../com/scribd/armadillo/di/PlaybackModule.kt | 6 +++ .../armadillo/error/ArmadilloException.kt | 4 ++ .../mediasource/DashMediaSourceGenerator.kt | 25 +++------ .../mediasource/DrmMediaSourceHelper.kt | 52 +++++++++++++++++++ 4 files changed, 70 insertions(+), 17 deletions(-) create mode 100644 Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/DrmMediaSourceHelper.kt diff --git a/Armadillo/src/main/java/com/scribd/armadillo/di/PlaybackModule.kt b/Armadillo/src/main/java/com/scribd/armadillo/di/PlaybackModule.kt index 9ecaa41..0c2d7de 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/di/PlaybackModule.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/di/PlaybackModule.kt @@ -15,6 +15,8 @@ import com.scribd.armadillo.playback.MediaMetadataCompatBuilderImpl import com.scribd.armadillo.playback.PlaybackEngineFactoryHolder import com.scribd.armadillo.playback.PlaybackStateBuilderImpl import com.scribd.armadillo.playback.PlaybackStateCompatBuilder +import com.scribd.armadillo.playback.mediasource.DrmMediaSourceHelper +import com.scribd.armadillo.playback.mediasource.DrmMediaSourceHelperImpl import com.scribd.armadillo.playback.mediasource.HeadersMediaSourceHelper import com.scribd.armadillo.playback.mediasource.HeadersMediaSourceHelperImpl import com.scribd.armadillo.playback.mediasource.MediaSourceRetriever @@ -62,4 +64,8 @@ internal class PlaybackModule { @Provides @Singleton fun mediaSourceHelper(mediaSourceHelperImpl: HeadersMediaSourceHelperImpl): HeadersMediaSourceHelper = mediaSourceHelperImpl + + @Provides + @Singleton + fun drmMediaSourceHelper(drmMediaSourceHelperImpl: DrmMediaSourceHelperImpl): DrmMediaSourceHelper = drmMediaSourceHelperImpl } \ No newline at end of file diff --git a/Armadillo/src/main/java/com/scribd/armadillo/error/ArmadilloException.kt b/Armadillo/src/main/java/com/scribd/armadillo/error/ArmadilloException.kt index 78fb6d3..94511b9 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/error/ArmadilloException.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/error/ArmadilloException.kt @@ -139,3 +139,7 @@ data class DrmDownloadException(val exception: Exception) : ArmadilloException(e override val errorCode = 701 } +data class DrmPlaybackException(val exception: Exception) : ArmadilloException(exception) { + override val errorCode = 702 +} + diff --git a/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/DashMediaSourceGenerator.kt b/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/DashMediaSourceGenerator.kt index 2b17704..bef0a39 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/DashMediaSourceGenerator.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/DashMediaSourceGenerator.kt @@ -1,8 +1,7 @@ package com.scribd.armadillo.playback.mediasource import android.content.Context -import com.google.android.exoplayer2.MediaItem -import com.google.android.exoplayer2.MediaItem.DrmConfiguration +import com.google.android.exoplayer2.drm.DefaultDrmSessionManagerProvider import com.google.android.exoplayer2.offline.Download import com.google.android.exoplayer2.offline.DownloadHelper import com.google.android.exoplayer2.source.MediaSource @@ -14,31 +13,23 @@ import javax.inject.Inject internal class DashMediaSourceGenerator @Inject constructor( private val mediaSourceHelper: HeadersMediaSourceHelper, - private val downloadTracker: DownloadTracker) : MediaSourceGenerator { + private val downloadTracker: DownloadTracker, + private val drmMediaSourceHelper: DrmMediaSourceHelper, +) : MediaSourceGenerator { override fun generateMediaSource(context: Context, request: AudioPlayable.MediaRequest): MediaSource { val dataSourceFactory = mediaSourceHelper.createDataSourceFactory(context, request) downloadTracker.getDownload(request.url.toUri())?.let { if (it.state != Download.STATE_FAILED) { - return DownloadHelper.createMediaSource(it.request, dataSourceFactory) + val mediaItem = drmMediaSourceHelper.createMediaItem(context = context, request = request, isDownload = true) + return DownloadHelper.createMediaSource(it.request, dataSourceFactory, DefaultDrmSessionManagerProvider().get(mediaItem)) } } - val mediaItemBuilder = MediaItem.Builder() - .setUri(request.url) - - if (request.drmInfo != null) { - mediaItemBuilder.setDrmConfiguration( - DrmConfiguration.Builder(request.drmInfo.drmType.toExoplayerConstant()) - .setLicenseUri(request.drmInfo.licenseServer) - .setLicenseRequestHeaders(request.drmInfo.drmHeaders) - .build() - ) - } - + val mediaItem = drmMediaSourceHelper.createMediaItem(context = context, request = request, isDownload = false) return DashMediaSource.Factory(dataSourceFactory) - .createMediaSource(mediaItemBuilder.build()) + .createMediaSource(mediaItem) } override fun updateMediaSourceHeaders(request: AudioPlayable.MediaRequest) = mediaSourceHelper.updateMediaSourceHeaders(request) diff --git a/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/DrmMediaSourceHelper.kt b/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/DrmMediaSourceHelper.kt new file mode 100644 index 0000000..c906987 --- /dev/null +++ b/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/DrmMediaSourceHelper.kt @@ -0,0 +1,52 @@ +package com.scribd.armadillo.playback.mediasource + +import android.content.Context +import com.google.android.exoplayer2.MediaItem +import com.scribd.armadillo.encryption.SecureStorage +import com.scribd.armadillo.error.DrmPlaybackException +import com.scribd.armadillo.models.AudioPlayable +import javax.inject.Inject +import javax.inject.Singleton + +/** + * This is a helper responsible for generating the correct media source for an audio request. + * + * This will apply the correct DRM-related information needed for content decryption (if the content is DRM-protected). + * In case of a download media (the content is either downloaded or being downloaded), this includes the DRM key ID used for retrieving + * the local DRM license (instead of fetching DRM license from the server). + */ +internal interface DrmMediaSourceHelper { + fun createMediaItem( + context: Context, + request: AudioPlayable.MediaRequest, + isDownload: Boolean, + ): MediaItem +} + +@Singleton +internal class DrmMediaSourceHelperImpl @Inject constructor(private val secureStorage: SecureStorage) : DrmMediaSourceHelper { + + override fun createMediaItem(context: Context, request: AudioPlayable.MediaRequest, isDownload: Boolean): MediaItem = + MediaItem.Builder() + .setUri(request.url) + .apply { + // Apply DRM config if content is DRM-protected + val drmConfig = request.drmInfo?.let { drmInfo -> + MediaItem.DrmConfiguration.Builder(drmInfo.drmType.toExoplayerConstant()) + .setLicenseUri(drmInfo.licenseServer) + .setLicenseRequestHeaders(drmInfo.drmHeaders) + .apply { + // If the content is a download content, use the saved offline DRM key id. + // This ID is needed to retrieve the local DRM license for content decryption. + if (isDownload) { + secureStorage.getDrmKeyId(context, request.url, drmInfo.drmType.name)?.let { drmKeyId -> + setKeySetId(drmKeyId) + } ?: throw DrmPlaybackException(IllegalStateException("No DRM key id saved for download content")) + } + } + .build() + } + setDrmConfiguration(drmConfig) + } + .build() +}