-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add DRM support for download content #31
Conversation
…rage to it can store the DRM key id Update SecureStorage to add new methods to save and fetch DRM key id. This ID will later be used to fetch the DRM key needed to decrypt a DRM-protected content. The ID is saved per content (per audio URL) and per DRM technology (we currently only support Widevine). Also make SecureStorage injectable with Dagger.
…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
…PEG-Dash audio playback 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.
…wnloaded audio, release and remove the downloaded license and its download info
Armadillo/src/main/java/com/scribd/armadillo/download/drm/DashDrmLicenseDownloader.kt
Show resolved
Hide resolved
} | ||
|
||
private fun DefaultHttpDataSource.Factory.addCustomHeaders(customHeaders: Map<String, String>) { | ||
customHeaders.takeIf { it.isNotEmpty() }?.let { headers -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the reason for this? Don't want to clear old ones if an empty map is sent in?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, at some point in my head I thought this was needed, but now I'm thinking again about it, it's not actually necessary. More than that, it's not great, since we might actually want to reset the headers by using an empty map.
I'm removing this logic
Armadillo/src/main/java/com/scribd/armadillo/download/drm/DrmLicenseDownloader.kt
Show resolved
Hide resolved
override fun init() = downloadTracker.init() | ||
|
||
override fun download(audiobook: AudioPlayable) { | ||
// Download DRM license for offline use | ||
offlineDrmManager.downloadDrmLicenseForOffline(audiobook) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need to block until the offline download is finished, or can they happen in parallel?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea they can happen in parallel. Updating this
} | ||
|
||
override fun saveDrmDownload(context: Context, audioUrl: String, drmDownload: DrmDownload) { | ||
context.getSharedPreferences(DOWNLOAD_FILENAME, Context.MODE_PRIVATE).also { sharedPrefs -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make more sense to inject the shared prefs into this class?
|
||
override fun getAllDrmDownloads(context: Context): Map<String, DrmDownload> = | ||
context.getSharedPreferences(DOWNLOAD_FILENAME, Context.MODE_PRIVATE).let { sharedPrefs -> | ||
sharedPrefs.all.keys.mapNotNull { key -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need to skip DOWNLOAD_KEY
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've put all the DRM download prefs into its own shared prefs file, so we don't need to exclude anything
|
||
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)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inject the provider maybe?
|
||
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need to get bypassed for non-DRM content?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In DrmMediaSourceHelper
, we check if the DRM object is non-null before adding anything DRM-related, so we're okay with not checking it here
override fun equals(other: Any?): Boolean { | ||
if (this === other) return true | ||
if (javaClass != other?.javaClass) return false | ||
|
||
other as DrmDownload | ||
|
||
if (!drmKeyId.contentEquals(other.drmKeyId)) return false | ||
if (drmType != other.drmType) return false | ||
if (licenseServer != other.licenseServer) return false | ||
if (audioType != other.audioType) return false | ||
|
||
return true | ||
} | ||
|
||
override fun hashCode(): Int { | ||
var result = drmKeyId.contentHashCode() | ||
result = 31 * result + drmType.hashCode() | ||
result = 31 * result + licenseServer.hashCode() | ||
result = 31 * result + audioType | ||
return result | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should get this by default with data class, why do we need to do it by hand?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's because this class contains a field of type ByteArray. Not generating equals and hashCode results in a warning
private const val TAG = "OfflineDrmManager" | ||
} | ||
|
||
fun downloadDrmLicenseForOffline(audiobook: AudioPlayable) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NP: Should the param name be playable rather than audiobook? Since this is open source, it's possible users could have other DRM'd audio ¯\_(ツ)_/¯
Totally valid to keep it specific to our use case for clarity, though
… : accept an empty map of custom headers for the HTTP data source, as there's no reason to exclude that case
… : update and add some missing kdocs
…oad/removal to execute in parallel to audio download/removal
…s : use injection for SharedPreferences and DrmSessionManagerProvider
…s : rename various params "audiobook" to "audioPlayable"
…pening in the download coroutines and dispatching the corresponding ErrorAction to notify the client app of the error
When an audio content is downloaded and this content is protected by DRM, the DRM license needs to be downloaded and persisted alongside with the audio.
The license downloading is handled automatically by ExoPlayer's
OfflineLicenseHelper
. This tools downloads and persists the license, and provides us with a key ID which can be used to fetch the downloaded license.During playback, instead of requesting the license from the server like in streaming mode, we provide the downloaded DRM license key ID to ExoPlayer so the local license can be used to decrypt the content.
When an audio download is removed, we also remove the key ID to the license, as well as remove the downloaded license locally and notify the server that the license should be released.