Download To Go
The player library supports download of both clear and DRM protected content.
Custom license request handling
Similar to playing DRM protected content, you can handle license requests while downloading DRM protected content as follows:
dwldManager.drmDelegate = dwldManager.drmDelegate = object : DRMDelegate {
override fun onKeyResponseRequired(
assetURL: String,
request: DRMKeyRequest,
callback: Callback<ByteArray, Error>
) {
// Add custom logic
}
}
- The license has a specific validity and can be renewed in the background using the
renewLicenseAPIs in theDownloadManager. - Downloads expiry handling is based on both the license duration and remaining playback duration. For example, the downloaded asset license can be valid
for 30 days, but when the first attempt of playback is made, the remaining playback duration value is checked and this could be, for example, the next 48
hours when the asset can be played. If one of the license expiry conditions is met, the appropriate error is reported.
DownloadManager.Listenernotifies the application client about the change in download's expiry date by invoking theonDownloadExpiryChangedcallback. - To ensure that there are no disruptions in offline playback, we recommend you configure a
DRMDelegatewith the player created for offline playback and handle the license request on demand. (This works only if the device is connected to the internet).
The platform player library provides aDefaultDRMDelegatewhich can be created as shown below:
val drmDelegate = DefaultDRMDelegate(coroutineScope)
Download manager
Download requirements
DownloadRequirements is an enum of all possible requirements available to configure downloads. The only default requirement is DeviceRequirements.NETWORK.
| Name | Description |
|---|---|
| NETWORK | Represents the requirement that the device should have a network connection. |
| NETWORK_UNMETERED | Represents the requirement that the device should have an unmetered network connection. |
| DEVICE_IDLE | Represents the requirement that the device should be idle. |
| DEVICE_CHARGING | Represents the requirement that the device should be charging. |
| DEVICE_STORAGE_NOT_LOW | Represents the requirement that the device's available storage should not be low. This is generally the point where the user is given a "low storage" warning by the Android OS. |
val downloadProperties = listOf(
DownloadRequirements.NETWORK //Pass all preferred requirements
)
When the DEVICE_STORAGE_NOT_LOW requirement isn't fulfilled (that is, the device doesn't have enough storage), ongoing and new downloads are put in DownloadState.QUEUED (refer to Download states) and resume only when the available storage isn't low.
Download properties
DownloadProperties is a collection of configurations that dictate how downloads should be queued and processed.
| Name | Type | Default value | Description |
|---|---|---|---|
| maxDownloadTasks | Int | 50 | The maximum number of tasks queued for download. (Must be greater than 0) |
| maxParallelDownloads | Int | 3 | The maximum number of tasks parallel downloads. (Must be greater than 0) |
| minRetryCount | Int | 5 | The minimum number of times that a download will be retried. A download will fail if the specified number of retries is exceeded without any progress being made. |
| requirements | List<DownloadRequirements> | DownloadRequirements.NETWORK | The list of DownloadRequirements to be fulfilled for every download task. |
| downloadsAutoPurgeOnExpiry | Boolean | false | Enables automatic deletion of all the downloads which expired. |
All the above-mentioned properties are optional, with the defaults being the best suited for most cases. You can override the defaults by passing the preferred values to the constructor.
You can also enable or disable automatic deletion of all the downloads that expired by calling the DownloadManager.updateDownloadsAutoPurgeOnExpiry API.
In Android, the auto purge on expiry functionality requires also adding the WorkManager dependency in your application's build.gradle:
dependencies {
// MANDATORY DEPENDENCY FOR enabling automatic deletion of all the downloads which expired
implementation 'androidx.work:work-runtime-ktx:2.10.3`
}
Creating a download manager
You can initialize the DownloadManager by passing app context on the Application's onCreate() lifecycle method.
For ExoPlayer-based legacy implementation:
DownloadManager.create(
this,
logger,
drmDelegate, // Refer to Custom License Request handling section
downloadProperties
).resumeDownloads()
For Media3-based legacy implementation:
DownloadManager.create(
this,
logger,
drmDelegate, // Refer to Custom License Request handling section
downloadProperties,
useDownloadService = false
).resumeDownloads()
For Media3-based service implementation:
DownloadManager.create(
this,
logger,
drmDelegate, // Refer to Custom License Request handling section
downloadProperties,
useDownloadService = true
)
Benefits of using the Media3 service-based implementation:
- Download tasks are handled in a separate process, ensuring that downloads continue even if the main application is killed.
- System optimizations for foreground services are leveraged, improving download reliability and performance.
From SDK v7.1.58 onwards, the Media3-based service is recommended for better reliability of downloads and
it's enabled as default for offline download handling. For applications that wish to continue using
the legacy download handling logic, set the useDownloadService parameter as false.
Manifest changes for Media3 service-based implementation
When using the Media3 service-based implementation, make the following changes in the AndroidManifest.xml file.
- Add the
FOREGROUND_SERVICE_DATA_SYNCpermission:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
- Add the
DownloadService:
<service android:name="com.quickplay.vstb7.player.offline.service.DownloadService"
android:exported="false"
android:foregroundServiceType="dataSync">
<intent-filter>
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</service>
When your apps target Android 14 and above, you’ll need to declare for foreground service types that you use in a declaration on the App content page (Monitor and improve > App content) in Play Console.
You also require to provide a video demonstrating foreground service feature. The video should demonstrate the steps the user needs to take in your app in order to trigger the feature.
Creating a Download Request
// The preferred video bitrate (quality) to be downloaded
val preferredVideoBitrate = 100_000_000
// Creating a Clear Content Download Request
val clearContentDownloadRequest = DownloadRequest.Builder()
.mediaURL(contentURL)
.mediaType(MediaType.DASH)
.drmLicenseURL("")
.drmScheme(DRMScheme.NONE)
.preferredVideoBitrate(preferredVideoBitrate)
.addPreferredAudioLanguage("en")
.addPreferredTextLanguage("en")
.metadata(contentName)
.id(contentURL)
.build()
// Creating a DRM Content Download Request
val drmContentDownloadRequest = DownloadRequest.Builder()
.mediaURL(contentURL)
.mediaType(MediaType.DASH)
.drmLicenseURL(license)
.drmScheme(DRMScheme.WIDEVINE)
.preferredVideoBitrate(preferredVideoBitrate)
.addPreferredAudioLanguage("en")
.addPreferredTextLanguage("en")
.id(contentURL)
.metadata(contentName)
.build()
- Application developers must ensure that the download ID passed during
DownloadRequestcreation is unique. - Application developers may also pass any metadata that they want to preserve as part of downloads.
- Application developers may pass any desired
preferredVideoBitrateto download. The matching algorithm will automatically choose the closest available variant.
Enqueueing a download request
Before enqueueing a download, it's a good practice to verify if there's already an existing download corresponding to the passed ID.
//It's assumed that DownloadManager.create is called beforehand
val downloadManager = DownloadManager.instance
val downloads = downloadManager.getAllDownloads()
val existingDownload = downloads.find { request.id == it.id }
if (existingDownload == null) {
val result = downloadManager.enqueueDownload(request)
}
The enqueueDownload API returns a result which can be either a success or failure. Application developers can obtain the result and update the UI accordingly.
Pausing and resuming downloads
You can pause and resume any ongoing download:
fun toggleDownload(downloadID: String) {
val downloads = downloadManager.getAllDownloads()
val chosenDownload = downloads.find { it.id == downloadID } ?: return
if (chosenDownload.state == DownloadState.DOWNLOADING) {
downloadManager.pauseDownload(chosenDownload)
} else {
downloadManager.resumeDownload(chosenDownload)
}
}
Purging downloads
You can purge any ongoing or completed download:
val downloads = downloadManager.getAllDownloads()
val chosenDownload = downloads.find { it.id == downloadID }
downloadManager.purgeDownload(chosenDownload)
Listening to download events
You can implement and attach a DownloadManager.Listener object to the DownloadManager to get notified about the different changes in download tasks.
val downloadListener = object : DownloadManager.Listener {
override fun onDownloadStateChanged(download: Download) {
logger.info { "download state changed to :${download.state}" }
}
override fun onDownloadProgress(download: Download) {
logger.info { "download progressed percent :${download.progressPercent}" }
}
override fun onDownloadRemoved(download: Download) {
logger.info { "download removed :${download.id}" }
}
override fun onWaitingForRequirementsChanged() {
logger.error { "Waiting for requirements to be met." }
}
}
downloadManager.addListener(downloadListener)
Download states
The DownloadState represents the state of a particular download:
- QUEUED - Download request is enqueued but hasn't started yet due to policy restrictions such as maximum parallel download limit, specified requirements not met.
- DOWNLOADING - Corresponding download task is active and the corresponding media resources are downloaded.
- PAUSED - Corresponding download task is suspended and won't be made active until explicitly resumed.
- FAILED - Download has failed because of chunk unavailability, device storage unavailability, or other server failures.
- COMPLETED - Download is complete and ready for offline playback.
- REMOVING - A transient state which indicates that the download is getting removed from the system.
- STALE - Indicates the download went into a stale state. This currently has no significance in Android and is present only to be inline with iOS states.
Offline playback
To play a downloaded content, configure the download ID in the PlayerRequest builder:
val downloads = downloadManager.getAllDownloads()
val completedDownload = downloads.find {
it.id == downloadID && it.state == DownloadState.COMPLETED
} ?: return
val player = PlayerBuilder()
.mediaURL(completedDownload.mediaURL)
.mediaType(MediaType.DASH)
.drmScheme(DRMScheme.WIDEVINE)
.drmLicenseURL("") //can be empty
.downloadID(completedDownload.id)
.build(applicationContext)
player.drmDelegate = DefaultDRMDelegate(coroutineScope)