Skip to content

Commit

Permalink
[APT-10345] Renew a Downloaded DRM License
Browse files Browse the repository at this point in the history
Attempts to treat downloaded content license renewals similarly to how they are treated when streamed: attempt at renewing the license when playback starts.

Also moves the main thread observer to the point in ArmadilloPlayerChoreographer where it is returned, rather than using it in the Reducer. This addresses two problems: the state being inconsistent about which thread it was on, and preventing main thread ANRs.
  • Loading branch information
kabliz committed Sep 4, 2024
1 parent 266baa3 commit 6ef5275
Show file tree
Hide file tree
Showing 7 changed files with 46 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,8 @@ internal class ArmadilloPlayerChoreographer : ArmadilloPlayer {
/**
* emits the most recently emitted state and all the subsequent states when an observer subscribes to it.
*/
override val armadilloStateObservable
get() = stateProvider.stateSubject
override val armadilloStateObservable: Observable<ArmadilloState>
get() = stateProvider.stateSubject.observeOn(AndroidSchedulers.mainThread())

private val pollingInterval =
Observable.interval(observerPollIntervalMillis.longValue, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
Expand Down
13 changes: 5 additions & 8 deletions Armadillo/src/main/java/com/scribd/armadillo/StateStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,11 @@ internal class ArmadilloStateStore(private val reducer: Reducer, private val han
override fun init(state: ArmadilloState) = armadilloStateObservable.onNext(state)

override fun dispatch(action: Action) {
//run on consistent thread
handler.post {
val newAppState = reducer.reduce(currentState, action)
armadilloStateObservable.onNext(newAppState)

if (currentState.error != null) {
dispatch(ClearErrorAction)
}
val newAppState = reducer.reduce(currentState, action)
armadilloStateObservable.onNext(newAppState)

if (currentState.error != null) {
dispatch(ClearErrorAction)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.scribd.armadillo.download.drm.OfflineDrmManager
import com.scribd.armadillo.error.ArmadilloException
import com.scribd.armadillo.error.ArmadilloIOException
import com.scribd.armadillo.error.DownloadServiceLaunchedInBackground
import com.scribd.armadillo.error.DrmDownloadException
import com.scribd.armadillo.error.UnexpectedDownloadException
import com.scribd.armadillo.extensions.encodeInByteArray
import com.scribd.armadillo.extensions.toUri
Expand All @@ -39,6 +40,7 @@ internal interface DownloadEngine {
fun removeDownload(audioPlayable: AudioPlayable)
fun removeAllDownloads()
fun updateProgress()
fun redownloadDrmLicense(request: AudioPlayable.MediaRequest)
}

/**
Expand All @@ -55,7 +57,7 @@ internal class ExoplayerDownloadEngine @Inject constructor(
private val downloadTracker: DownloadTracker,
private val stateModifier: StateStore.Modifier,
private val offlineDrmManager: OfflineDrmManager,
@Named(Constants.DI.GLOBAL_SCOPE) private val globalScope: CoroutineScope,
@Named(Constants.DI.GLOBAL_SCOPE) private val scope: CoroutineScope,
) : DownloadEngine {
private val errorHandler = CoroutineExceptionHandler { _, e ->
stateModifier.dispatch(ErrorAction(
Expand All @@ -65,10 +67,10 @@ internal class ExoplayerDownloadEngine @Inject constructor(

override fun init() = downloadTracker.init()
override fun download(audioPlayable: AudioPlayable) {
globalScope.launch(errorHandler) {
scope.launch(errorHandler) {
launch {
// Download DRM license for offline use
offlineDrmManager.downloadDrmLicenseForOffline(audioPlayable)
offlineDrmManager.downloadDrmLicenseForOffline(audioPlayable.request)
}

launch {
Expand Down Expand Up @@ -96,21 +98,31 @@ internal class ExoplayerDownloadEngine @Inject constructor(
}

override fun removeDownload(audioPlayable: AudioPlayable) {
globalScope.launch(errorHandler) {
scope.launch(errorHandler) {
launch { downloadManager.removeDownload(audioPlayable.request.url) }
launch { offlineDrmManager.removeDownloadedDrmLicense(audioPlayable) }
launch { offlineDrmManager.removeDownloadedDrmLicense(audioPlayable.request) }
}
}

override fun removeAllDownloads() {
globalScope.launch(errorHandler) {
scope.launch(errorHandler) {
launch { downloadManager.removeAllDownloads() }
launch { offlineDrmManager.removeAllDownloadedDrmLicenses() }
}
}

override fun updateProgress() = downloadTracker.updateProgress()

override fun redownloadDrmLicense(request: AudioPlayable.MediaRequest) {
scope.launch(errorHandler) {
try {
offlineDrmManager.downloadDrmLicenseForOffline(request)
} catch (ex: DrmDownloadException){
//continue to try and use old license - a playback error appears elsewhere
}
}
}

private fun startDownload(context: Context, downloadRequest: DownloadRequest) =
DownloadService.sendAddDownload(context, downloadService, downloadRequest, true)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,31 +28,31 @@ internal class OfflineDrmManager @Inject constructor(
private const val TAG = "OfflineDrmManager"
}

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

// Persist DRM result, which includes the key ID that can be used to retrieve the offline license
secureStorage.saveDrmDownload(context, audioPlayable.request.url, drmResult)
secureStorage.saveDrmDownload(context, request.url, drmResult)
Log.i(TAG, "DRM license ready for offline usage")
}
}
}

suspend fun removeDownloadedDrmLicense(audioPlayable: AudioPlayable) {
suspend fun removeDownloadedDrmLicense(request: AudioPlayable.MediaRequest) {
withContext(Dispatchers.IO) {
audioPlayable.request.drmInfo?.let { drmInfo ->
secureStorage.getDrmDownload(context, audioPlayable.request.url, drmInfo.drmType)?.let { drmDownload ->
request.drmInfo?.let { drmInfo ->
secureStorage.getDrmDownload(context, request.url, drmInfo.drmType)?.let { drmDownload ->
// Remove the persisted download info immediately so audio playback would stop using the offline license
secureStorage.removeDrmDownload(context, audioPlayable.request.url, drmInfo.drmType)
secureStorage.removeDrmDownload(context, request.url, drmInfo.drmType)

// Release the DRM license
when (val type = drmDownload.audioType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.dash.DashMediaSource
import com.scribd.armadillo.StateStore
import com.scribd.armadillo.actions.OpeningLicenseAction
import com.scribd.armadillo.download.DownloadEngine
import com.scribd.armadillo.download.DownloadTracker
import com.scribd.armadillo.download.drm.events.WidevineSessionEventListener
import com.scribd.armadillo.extensions.toUri
Expand All @@ -23,6 +24,7 @@ internal class DashMediaSourceGenerator @Inject constructor(
private val downloadTracker: DownloadTracker,
private val drmMediaSourceHelper: DrmMediaSourceHelper,
private val drmSessionManagerProvider: DrmSessionManagerProvider,
private val downloadEngine: DownloadEngine,
private val stateStore: StateStore.Modifier,
) : MediaSourceGenerator {

Expand All @@ -40,6 +42,9 @@ internal class DashMediaSourceGenerator @Inject constructor(

return if (isDownloaded) {
val drmManager = drmSessionManagerProvider.get(mediaItem)
if(request.drmInfo?.drmType == DrmType.WIDEVINE) {
downloadEngine.redownloadDrmLicense(request)
}
DownloadHelper.createMediaSource(download!!.request, dataSourceFactory, drmManager)
} else {
DashMediaSource.Factory(dataSourceFactory)
Expand Down
6 changes: 5 additions & 1 deletion RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# Project Armadillo Release Notes

## 1.5.3
- Attempts to renew the widevine license of downloaded DRM content when playback begins, similarly to how streaming does it.
- Fixes ANR issue in the Reducer.

## 1.5.2
Fixes "Failed to Bind Service" issue introduced in 1.4, affecting MediaBrowser services.
- Fixes "Failed to Bind Service" issue introduced in 1.4, affecting MediaBrowser services.

## 1.5.1
- Adds DrmState to ArmadilloState, giving full visibility into DRM status to the client, including the expiration date for content.
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ org.gradle.jvmargs=-Xmx1536m
# org.gradle.parallel=true
PACKAGE_NAME=com.scribd.armadillo
GRADLE_PLUGIN_VERSION=7.2.0
LIBRARY_VERSION=1.5.2
LIBRARY_VERSION=1.5.3
EXOPLAYER_VERSION=2.19.1
RXJAVA_VERSION=2.2.4
RXANDROID_VERSION=2.0.1
Expand Down

0 comments on commit 6ef5275

Please sign in to comment.