Skip to content

Commit

Permalink
[APT-9577] Add DRM for downloads for offline usage : download DRM lic…
Browse files Browse the repository at this point in the history
…ense when download an MPEG-Dash audio

Create new helper DrmLicenseDownloader that is responsible for downloading the DRM license (and persist it on the device) for a DRM-protected content.
Once the DRM license is downloaded, we also persist separately its key ID, which is the ID used to retrieve the downloaded license from storage for playback.

When we start a new download, use this helper to download the DRM license to local storage. This currently only supports MPEG-Dash audio format
  • Loading branch information
rubeus90 committed Feb 12, 2024
1 parent abb2a0f commit 96ad754
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.scribd.armadillo.Constants
import com.scribd.armadillo.HeadersStore
import com.scribd.armadillo.StateStore
import com.scribd.armadillo.actions.ErrorAction
import com.scribd.armadillo.download.drm.OfflineDrmManager
import com.scribd.armadillo.error.DownloadServiceLaunchedInBackground
import com.scribd.armadillo.extensions.encodeInByteArray
import com.scribd.armadillo.extensions.toUri
Expand All @@ -39,15 +40,21 @@ internal interface DownloadEngine {
* Starts the [DownloadService] when necessary
*/
@Singleton
internal class ExoplayerDownloadEngine @Inject constructor(private val context: Context,
private val downloadHeadersStore: HeadersStore,
private val downloadService: Class<out DownloadService>,
private val downloadManager: DownloadManager,
private val downloadTracker: DownloadTracker,
private val stateModifier: StateStore.Modifier) : DownloadEngine {
internal class ExoplayerDownloadEngine @Inject constructor(
private val context: Context,
private val downloadHeadersStore: HeadersStore,
private val downloadService: Class<out DownloadService>,
private val downloadManager: DownloadManager,
private val downloadTracker: DownloadTracker,
private val stateModifier: StateStore.Modifier,
private val offlineDrmManager: OfflineDrmManager,
) : DownloadEngine {
override fun init() = downloadTracker.init()

override fun download(audiobook: AudioPlayable) {
// Download DRM license for offline use
offlineDrmManager.downloadDrmLicenseForOffline(audiobook)

val downloadHelper = downloadHelper(context, audiobook.request)

downloadHelper.prepare(object : DownloadHelper.Callback {
Expand Down Expand Up @@ -96,6 +103,7 @@ internal class ExoplayerDownloadEngine @Inject constructor(private val context:
C.TYPE_HLS,
C.TYPE_DASH ->
DownloadHelper.forMediaItem(context, mediaItem, renderersFactory, DefaultDataSource.Factory(context, dataSourceFactory))

C.TYPE_OTHER -> DownloadHelper.forMediaItem(context, mediaItem)
else -> throw IllegalStateException("Unsupported type: $type")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.scribd.armadillo.download.drm

import android.content.Context
import android.net.Uri
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.drm.DrmSessionEventListener
import com.google.android.exoplayer2.drm.OfflineLicenseHelper
import com.google.android.exoplayer2.source.dash.DashUtil
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import com.google.android.exoplayer2.util.Log
import com.scribd.armadillo.Constants
import com.scribd.armadillo.error.DrmDownloadException
import com.scribd.armadillo.models.DrmDownload
import com.scribd.armadillo.models.DrmInfo
import com.scribd.armadillo.models.DrmType
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
internal class DashDrmLicenseDownloader @Inject constructor(context: Context) : DrmLicenseDownloader {

private val drmDataSourceFactory = DefaultHttpDataSource.Factory().setUserAgent(Constants.getUserAgent(context))
private val audioDataSourceFactory = DefaultHttpDataSource.Factory().setUserAgent(Constants.getUserAgent(context))
private val drmEventDispatcher = DrmSessionEventListener.EventDispatcher()

override suspend fun downloadDrmLicense(
requestUrl: String,
customRequestHeaders: Map<String, String>,
drmInfo: DrmInfo,
): DrmDownload {
// Update data source for DRM license to add any DRM-specific request headers
drmDataSourceFactory.addCustomHeaders(drmInfo.drmHeaders)
// Update data source for audio to add custom headers
audioDataSourceFactory.addCustomHeaders(customRequestHeaders)

// Create helper to download DRM license
val offlineHelper = when (drmInfo.drmType) {
DrmType.WIDEVINE -> OfflineLicenseHelper.newWidevineInstance(drmInfo.licenseServer, drmDataSourceFactory, drmEventDispatcher)
}
return try {
val audioDataSource = audioDataSourceFactory.createDataSource()
val manifest = DashUtil.loadManifest(audioDataSource, Uri.parse(requestUrl))
val format = DashUtil.loadFormatWithDrmInitData(audioDataSource, manifest.getPeriod(0))
format?.let {
DrmDownload(
drmKeyId = offlineHelper.downloadLicense(format),
drmType = drmInfo.drmType,
licenseServer = drmInfo.licenseServer,
audioType = C.TYPE_DASH,
)
} ?: throw IllegalStateException("No media format retrieved for audio request")
} catch (e: Exception) {
Log.e(DrmLicenseDownloader.TAG, "Failure to download DRM license for offline usage", e)
throw DrmDownloadException(e)
}
}

private fun DefaultHttpDataSource.Factory.addCustomHeaders(customHeaders: Map<String, String>) {
customHeaders.takeIf { it.isNotEmpty() }?.let { headers ->
setDefaultRequestProperties(headers)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.scribd.armadillo.download.drm

import com.scribd.armadillo.models.DrmDownload
import com.scribd.armadillo.models.DrmInfo

/**
* This is a helper class responsible for downloading the DRM license to local storage for a DRM-protected content.
* This downloaded license can then be retrieved for offline usage using its key ID.
*/
internal interface DrmLicenseDownloader {
companion object {
const val TAG = "DrmLicenseDownloader"
}

/**
* Download and persist the DRM license
* @return the key ID of the DRM license. This key ID can be used to fetch the license from storage
*/
suspend fun downloadDrmLicense(
requestUrl: String,
customRequestHeaders: Map<String, String>,
drmInfo: DrmInfo,
): DrmDownload
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.scribd.armadillo.download.drm

import android.content.Context
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.util.Log
import com.google.android.exoplayer2.util.Util
import com.scribd.armadillo.Constants
import com.scribd.armadillo.encryption.SecureStorage
import com.scribd.armadillo.error.DrmContentTypeUnsupportedException
import com.scribd.armadillo.extensions.toUri
import com.scribd.armadillo.models.AudioPlayable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton

/**
* Manager class responsible for handling DRM downloading/persistence
*/
@Singleton
internal class OfflineDrmManager @Inject constructor(
private val context: Context,
@Named(Constants.DI.GLOBAL_SCOPE) private val globalScope: CoroutineScope,
private val secureStorage: SecureStorage,
private val dashDrmLicenseDownloader: DashDrmLicenseDownloader,
) {
companion object {
private const val TAG = "OfflineDrmManager"
}

fun downloadDrmLicenseForOffline(audiobook: AudioPlayable) {
globalScope.launch(Dispatchers.IO) {
audiobook.request.drmInfo?.let { drmInfo ->
val drmResult = when (@C.ContentType val type = Util.inferContentType(audiobook.request.url.toUri(), null)) {
C.TYPE_DASH -> dashDrmLicenseDownloader
else -> throw DrmContentTypeUnsupportedException(type)
}.downloadDrmLicense(
requestUrl = audiobook.request.url,
customRequestHeaders = audiobook.request.headers,
drmInfo = drmInfo,
)

// Persist DRM result, which includes the key ID that can be used to retrieve the offline license
secureStorage.saveDrmDownload(context, audiobook.request.url, drmResult)
Log.i(TAG, "DRM license ready for offline usage")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,15 @@ data class UnknownRendererException(val exception: Exception) : ArmadilloExcepti
override val errorCode: Int = 604
}

/**
* DRM errors
*/

data class DrmContentTypeUnsupportedException(val contentType: Int) : ArmadilloException(exception = Exception()) {
override val errorCode = 700
}

data class DrmDownloadException(val exception: Exception) : ArmadilloException(exception) {
override val errorCode = 701
}

0 comments on commit 96ad754

Please sign in to comment.