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.
1. Add the FOREGROUND_SERVICE_DATA_SYNC permission
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
2. 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.
Download size estimation and track discovery
- Before creating a download request, inspect the manifest asynchronously with DownloadSizeEstimator. This API discovers embedded audio and subtitle tracks and estimates the download size for each available video quality tier.
1. Initialize the estimator
Create an instance with the stream manifest URL, any optional headers, and the storage buffer you want to reserve.
val estimator = DownloadSizeEstimator(
contentUrl = contentURL,
customHeaders = headers, // Optional; also sent when loading child manifests.
multiBitratePreference = MultiBitratePreference.HIGH,
reserveStorageBytes = 500L * 1024 * 1024
)
2. Discover available audio and subtitle tracks
Retrieve the embedded audio and subtitle tracks from the manifest using getAvailableTracks().
val audioTracks = when (val result = estimator.getAvailableTracks(TrackVariantInfo.Type.AUDIO)) {
is Result.Success -> result.value
is Result.Failure -> {
// Handle the manifest or track discovery error.
return
}
}
val textTracks = when (val result = estimator.getAvailableTracks(TrackVariantInfo.Type.TEXT)) {
is Result.Success -> result.value
is Result.Failure -> {
// Handle the manifest or track discovery error.
return
}
}
3. Select the user's preferred audio tracks
Pick the preferred language from the discovered audio tracks. Pass an empty list to let the estimator use the manifest default track.
val selectedAudioTracks = audioTracks.filter { it.languageCode == "en" }
// Or use an empty list to keep the manifest default track:
// val selectedAudioTracks = emptyList<TrackVariantInfo>()
4. Estimate sizes for the selected track
Estimate the download sizes using the preferred audio track or tracks.
when (val result = estimator.estimateSizes(selectedAudioTracks)) {
is Result.Success -> {
// Use result.value.videoVariants to show size options in the UI.
}
is Result.Failure -> {
// Handle the size estimation error.
}
}
5. Inspect variants and storage status
Review the calculated video variants, format the estimated size for display, and verify whether the device has enough free space before enabling a download option.
when (val result = estimator.estimateSizes(selectedAudioTracks)) {
is Result.Success -> {
result.value.videoVariants.forEach { variant ->
val sizeText = variant.formattedSize ?: "Unknown size"
val resolutionText = variant.resolution ?: "Unknown resolution"
if (variant.storageStatus == StorageAvailability.SUFFICIENT) {
// Enable the download option and display sizeText.
} else {
// Disable the option or show an insufficient-storage warning.
}
// Pass variant.bitRate to DownloadRequest.Builder.preferredVideoBitrate().
}
}
is Result.Failure -> {
// Handle the size estimation error.
}
}
6. Recalculate after user selection changes
When the user changes the selected audio tracks in the UI, call estimateSizes() again to refresh the size estimates.
when (val updatedResult = estimator.estimateSizes(userSelectedAudioTracks)) {
is Result.Success -> {
updatedResult.value.videoVariants.forEach { variant ->
// Update the size text in the UI using variant.estimatedBytes or variant.formattedSize.
// Re-check variant.storageStatus to enable or disable the download button.
}
}
is Result.Failure -> {
// Handle the updated size estimation error.
}
}
Note:
- Storage status: Each variant returns
storageStatusasSUFFICIENTorINSUFFICIENT, based on the estimated size and the currently available device storage.- Subtitles: Subtitles are omitted from the size calculation, but you can pass them directly to
DownloadRequest.Builder.addPreferredTextLanguage()or other text settings.- Text tracks: Text tracks can be discovered for the UI, but subtitles, manifests, DRM data, and container overhead are not included in the size estimate.
Download estimation models
DownloadMediaOptions
The root model returned by a successful size estimate.
| Property | Type | Description |
|---|---|---|
videoVariants | List<VideoVariantInfo> | Available video quality tiers with their bitrate and estimated size. |
selectedAudioTracks | List<TrackVariantInfo> | Audio renditions selected for the estimate. |
VideoVariantInfo
Represents a single video quality tier in DownloadMediaOptions.videoVariants.
| Property | Type | Description |
|---|---|---|
id | String | Variant identifier. |
bitRate | Int | Video bitrate in bits per second. Pass this to DownloadRequest.Builder.preferredVideoBitrate(). |
resolution | String? | Resolution such as "1920x1080", or null if unavailable. |
width | Int? | Video width in pixels, or null if unavailable. |
height | Int? | Video height in pixels, or null if unavailable. |
estimatedBytes | Long? | Estimated video and selected audio size in bytes, or null if it cannot be calculated. |
formattedSize | String? | Formatted MB/GB size for display, or null if unknown. |
durationMs | Long? | Manifest duration used for the estimate, in milliseconds. |
storageStatus | StorageAvailability | Advisory status: SUFFICIENT, INSUFFICIENT, or UNKNOWN. |
StorageAvailability
| Case | Description |
|---|---|
SUFFICIENT | The device has enough available space for the estimated download size. |
INSUFFICIENT | The device does not have enough available space. |
UNKNOWN | The estimator could not determine whether the size fits the available storage. |
MultiBitratePreference
| Case | Description |
|---|---|
LOW | Selects the lowest bitrate variant when multiple variants exist for the same resolution. |
MEDIUM | Selects the median bitrate variant when multiple variants exist for the same resolution. |
HIGH | Default option. Selects the highest bitrate variant when multiple variants exist for the same resolution. |
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.
Configure Sidecar subtitles
For single-content download playback, you can configure sidecar WebVTT subtitle tracks using the QP Player's DownloadRequest.Builder. Each track requires a subtitle URL and a language code.
SidecarSubtitleTrack
Each sidecar subtitle track is defined by a SidecarSubtitleTrack instance.
| Name | Type | Required? | Description | Default Value |
|---|---|---|---|---|
| url | String | Yes | The fully formed URL of the WebVTT subtitle file received via PlayAuth. | N/A |
| language | String | Yes | IETF BCP 47 language code for the subtitle track. | N/A |
| mimeType | String | No | Mime Type of the option (text/ttml, text/vtt) | text/vtt |
| type | SidecarSubtitleType | No | SUBTITLE for translated dialogue or CAPTION for closed captions. | SUBTITLE |
DownloadRequest.Builder()
.mediaURL(url)
.sidecarSubtitleTracks(
listOf(
SidecarSubtitleTrack(
"https://example.com/en.vtt", //sample subtitle url
"en",
type = SidecarSubtitleType.SUBTITLE
),
SidecarSubtitleTrack(
"https://example.com/en-cc.vtt", //sample caption url
"en",
type = SidecarSubtitleType.CAPTION
)
)
)
.build()
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)
Custom headers for downloads
Use custom headers when the media manifest, license endpoint, or segment requests require authentication or custom request metadata. You can attach headers to a download request.
val customHeaders = mapOf(
"Key" to "value",
)
val request = DownloadRequest.Builder()
.mediaURL(contentURL)
.mediaType(MediaType.DASH)
.drmScheme(DRMScheme.NONE)
.id(contentURL)
.metadata(contentName)
.setCustomHeaders(customHeaders) // Add custom headers to the download request
.build()
val result = downloadManager.enqueueDownload(request)
You can also pass custom headers when checking track variants before downloading:
val variants = downloadManager.getAvailableTrackVariants(
contentUrl = contentURL,
customHeaders = customHeaders
)
- Custom headers are supported only by the Media3 service-based download implementation.
- The legacy download implementation ignores custom headers.
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)