Offline downloads using the VdoCipher Android SDK
Overview
The VdoCipher Android SDK offers capability to download videos to local storage for offline playback on Android devices running Lollipop and above (API level 20+).
It includes APIs to :
- Fetch available download options for a video in your dashboard
- Download media assets to local storage
- Track download progress
- Manage downloads (query or delete downloads)
We'll explore a typical download workflow in this document. The Sample App on Github provides code examples for a typical use case.
Ensure android.permission.FOREGROUND_SERVICE_DATA_SYNC is declared in the manifest for download to work when target SDK version is set as 34 and higher.
Add the below permission in the AndroidManifest.xml -
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
Generating OTP for Offline Playback
To initialize video playback on website or app, your backend has to generate
an OTP
+playbackInfo
using the VdoCipher API. The regular OTP does
not have permission to be persisted offline. To enable offline playback you
will need to send additional parameters while making the API call for the OTP.
You can specify the time period for which the offline download will be available for playback. The time period of validity is called the rental duration. Beyond the rental duration the license will expire, and the downloaded file will no longer play.
You can find more details on the OTP-based playback mechanism at the API page.
Get the available options for a media
A video in your VdoCipher dashboard may be encoded in multiple bitrates (for adaptive streaming) or has multiple audio tracks for different languages. Hence, there are some options regarding what exactly needs to be downloaded. For offline playback, adaptive doesn't make sense, and also the user typically has a preferred language. So, you must specify exactly one video track and exactly one audio track for download.
The first step is to get the available options for the media. We'll use the OptionsDownloader
class for this. We'll need a playbackInfo
and otp
corresponding to the video to fetch the options.
- Java
- Kotlin
OptionsDownloader optionsDownloader = new OptionsDownloader(context);
// assuming we have otp and playbackInfo
optionsDownloader.downloadOptionsWithOtp(
otp,
playbackInfo,
new OptionsDownloader.Callback() {
@Override
public void onOptionsReceived(DownloadOptions options) {
// we have received the available download options
Log.i("Success", "onOptionsReceived");
// ...
}
@Override
public void onOptionsNotReceived(ErrorDescription errDesc) {
// there was an error downloading the available options
String errMsg = "onOptionsNotReceived : " + errDesc.toString();
Log.e("Error", errMsg);
}
}
);
val optionsDownloader = OptionsDownloader(context)
// assuming we have otp and playbackInfo
optionsDownloader.downloadOptionsWithOtp(
otp,
playbackInfo,
object : OptionsDownloader.Callback {
override fun onOptionsReceived(options: DownloadOptions) {
// we have received the available download options
Log.i("Success", "onOptionsReceived")
// ...
}
override fun onOptionsNotReceived(errDesc: ErrorDescription) {
// there was an error downloading the available options
val errMsg = "onOptionsNotReceived : " + errDesc.toString()
Log.e("Error", errMsg)
}
}
)
The available options are received in the OptionsDownloader.Callback#onOptionsReceived()
callback in the form of a DownloadOptions
instance. A DownloadOptions
object contains a MediaInfo
object (with general details of the media, such as title, description as set in your VdoCipher dashboard, etc.) and an array of Track
objects corresponding to the available audio and video track options. Each Track
object in the array corresponds to a audio or video track (specified by Track#type
) and contains relevant information such as bitrate, resolution, language, etc.
Once we've obtained the available options, the next step is to make a selection of which tracks to download.
Select the tracks to download
As mentioned earlier in this document, we need to select exactly one audio track and one video track.
Once we have the download options, we may make selections automatically based on user preferences etc. (e.g. select the highest quality/bitrate) or present the options to the user to choose.
Once a selection has been made (automatically or by the user), make a DownloadSelections
object using the options we received in the previous step and the indices of the tracks we want to download in the Track
array in the DownloadOptions
.
- Java
- Kotlin
// selections must include exactly one audio track (Track#TYPE_AUDIO) and one video track (Track#TYPE_VIDEO)
// track indices are the index of the track in the Track array in received DownloadOptions
int[] selectionIndices = new int[]{audioTrackIndex, videoTrackIndex};
DownloadSelections downloadSelections = new DownloadSelections(downloadOptions, selectionIndices);
// selections must include exactly one audio track (Track#TYPE_AUDIO) and one video track (Track#TYPE_VIDEO)
// track indices are the index of the track in the Track array in received DownloadOptions
val selectionIndices = intArrayOf(audioTrackIndex, videoTrackIndex)
val downloadSelections = DownloadSelections(downloadOptions, selectionIndices)
Now we have made the selctions specifying which tracks to download. Next step is to specify some more options such as download location on local storage and create a DownloadRequest
.
Specify more options
We need to specify a storage location for the media files to be downloaded. This location must be a folder on external storage. External storage does not necessarily mean a removable media such as a SD card. Specifically, the location should be inside the directory tree returned by Context#getExternalFilesDir() or one of the directory tree returned by Context#getExternalFilesDirs(). This will also ensure the media files are deleted when the app is uninstalled.
We'll store all downloads to a dedicated folder on the external storage.
- Java
- Kotlin
// ensure external storage is in read-write mode
if (!isExternalStorageWritable()) {
// External storage is not available; can't proceed
Log.e("error", "external storage not available");
Toast.makeText(this, "external storage not available", Toast.LENGTH_LONG).show();
return;
}
/*
* To download data on external storage -
* File[] directories = this.getExternalFilesDirs(null);
* For downloading on SD card use directories[1]
* For downloading on device's external space use directories[0] - It is same as using getExternalFilesDir(null)
*
* Refer - https://developer.android.com/about/versions/kitkat/android-4.4#ExternalStorage
* Note - check getExternalStorageState before saving on SD card
*
*/
// we'll download all videos to directory "offlineVideos" on the primary external storage
String downloadLocation;
try {
downloadLocation = getExternalFilesDir(null).getPath() + File.separator + "offlineVideos";
} catch (NullPointerException npe) {
Log.e("error", "external storage not available: " + Log.getStackTraceString(npe));
Toast.makeText(this, "external storage not available", Toast.LENGTH_LONG).show();
return;
}
// ensure download directory is created
File dlLocation = new File(downloadLocation);
if (!(dlLocation.exists() && dlLocation.isDirectory())) {
// directory not created yet; let's create it
if (!dlLocation.mkdir()) {
Log.e("error", "failed to create storage directory");
Toast.makeText(this, "failed to create storage directory", Toast.LENGTH_LONG).show();
return;
}
}
// ensure external storage is in read-write mode
if (!isExternalStorageWritable()) {
// External storage is not available; can't proceed
Log.e("error", "external storage not available")
Toast.makeText(this, "external storage not available", Toast.LENGTH_LONG).show()
return
}
/*
* To download data on external storage -
* val directories = this.getExternalFilesDirs(null)
* For downloading on SD card use directories[1]
* For downloading on device's external space use directories[0] - It is same as using getExternalFilesDir(null)
*
* Refer - https://developer.android.com/about/versions/kitkat/android-4.4#ExternalStorage
* Note - check getExternalStorageState before saving on SD card
*
*/
// we'll download all videos to directory "offlineVideos" on the primary external storage
val downloadLocation: String
try {
downloadLocation = "${getExternalFilesDir(null)?.path}${File.separator}offlineVideos"
} catch (npe: NullPointerException) {
Log.e("error", "external storage not available: " + Log.getStackTraceString(npe))
Toast.makeText(this, "external storage not available", Toast.LENGTH_LONG).show()
return
}
// ensure download directory is created
val dlLocation = File(downloadLocation)
if (!(dlLocation.exists() && dlLocation.isDirectory)) {
// directory not created yet; let's create it
if (!dlLocation.mkdir()) {
Log.e("error", "failed to create storage directory")
Toast.makeText(this, "failed to create storage directory", Toast.LENGTH_LONG).show()
return
}
}
Now we will pass this downloadLocation while getting the instance of VdoDownloadManager
- Java
- Kotlin
vdoDownloadManager = VdoDownloadManager.getInstance(this, downloadLocation);
vdoDownloadManager = VdoDownloadManager.getInstance(this, downloadLocation)
Now we have an instance of VdoDownloadManager
that can be used to enqueue download. We'll do this in the upcoming steps.
Custom Notification Helper
We can now set a custom Download Notification Helper to vdoDownloadManager if required.
- Java
- Kotlin
vdoDownloadManager.setDownloadNotificationHelper(CustomDownloadNotificationHelper.class);
vdoDownloadManager.setDownloadNotificationHelper(CustomDownloadNotificationHelper::class.java)
Enqueue request for download
The VdoDownloadManager
class handles enqueuing requests for download. This class is similar to the DownloadManager
in Android sdk.
Now we'll create a new DownloadRequest
.
- Java
- Kotlin
// build a DownloadRequest
DownloadRequest request = new DownloadRequest.Builder(downloadSelections).build();
// build a DownloadRequest
val request = DownloadRequest.Builder(downloadSelections).build()
Now we have a DownloadRequest
that can be enqueued for download right away.
- Java
- Kotlin
// enqueue request to VdoDownloadManager for download
try {
vdoDownloadManager.enqueueV2(request);
} catch (IllegalArgumentException | IllegalStateException e) {
Log.e("error", "error enqueuing download request");
Toast.makeText(this, "error enqueuing download request", Toast.LENGTH_LONG).show();
}
// enqueue request to VdoDownloadManager for download
try {
vdoDownloadManager.enqueueV2(request)
} catch (e: IllegalArgumentException) {
Log.e("error", "error enqueuing download request")
Toast.makeText(this, "error enqueuing download request", Toast.LENGTH_LONG).show()
} catch (e: IllegalStateException) {
Log.e("error", "error enqueuing download request")
Toast.makeText(this, "error enqueuing download request", Toast.LENGTH_LONG).show()
}
This will add the request to the download queue and start download when all requests enqueued before have completed.
Monitoring download progress
We can monitor the progress of the download queue by registering a EventListener
with the VdoDownloadManager
.
- Java
- Kotlin
// register a listener for download events; recommended to do this in Activity's onStart()
VdoDownloadManager vdoDownloadManager = VdoDownloadManager.getInstance(activityContext);
vdoDownloadManager.addEventListener(eventListener);
// don't forget to de-register the listener in Activity's onStop() to avoid memory leaks
vdoDownloadManager.removeEventListener(eventListener);
// register a listener for download events; recommended to do this in Activity's onStart()
val vdoDownloadManager = VdoDownloadManager.getInstance(activityContext)
vdoDownloadManager.addEventListener(eventListener)
// don't forget to de-register the listener in Activity's onStop() to avoid memory leaks
vdoDownloadManager.removeEventListener(eventListener)
Query for downloads
VdoDownloadManager
allows querying for all downloads managed by it or only specific downloads specified by filters.
Queries provide status of all downloads that are queued or downloding or completed. Make a Query
object and specify any filters you want.
- Java
- Kotlin
Query query = new Query();
// set mediaId filters if these are the only videos for which you want the status
query.setFilterByMediaId(mediaId1, mediaId2);
// set filters by status
// here we filter for downloads which are queued and downloading
query.setFilterByStatus(VdoDownloadManager.STATUS_PENDING, VdoDownloadManager.STATUS_DOWNLOADING);
val query = Query()
// set mediaId filters if these are the only videos for which you want the status
query.setFilterByMediaId(mediaId1, mediaId2)
// set filters by status
// here we filter for downloads which are queued and downloading
query.setFilterByStatus(VdoDownloadManager.STATUS_PENDING, VdoDownloadManager.STATUS_DOWNLOADING)
Now we can use this query object to query status for the specified downloads. A query result is provided as a list of DownloadStatus
objects which provides information such as the MediaInfo
, status, any errors if they occured while downloading, etc.
The query results are provided asynchronously to a QueryResultListener
provided as an argument to the query method.
- Java
- Kotlin
vdoDownloadManager.query(query, new VdoDownloadManager.QueryResultListener() {
@Override
public void onQueryResult(List statusList) {
int size = statusList.size();
Log.i(TAG, size + " results found");
}
});
vdoDownloadManager.query(query, object : VdoDownloadManager.QueryResultListener {
override fun onQueryResult(statusList: List<*>) {
val size = statusList.size
Log.i(TAG, "$size results found")
}
})
Offline Playback
To play downloaded videos, the initial step involves creating a VdoInitParams
for offline playback. This VdoInitParams
is then used in conjunction with the player.
- Java
- Kotlin
playerFragment.initialize(PlayerActivity.this);
...
...
@Override
public void onInitializationSuccess(PlayerHost playerHost, VdoPlayer player, boolean wasRestored) {
Log.i(TAG, "onInitializationSuccess");
this.player = player;
// Add a listener for playback events.
player.addPlaybackEventListener(playbackListener);
// ...
}
VdoInitParams vdoInitParams = VdoInitParams.createParamsForOffline(mediaId);
player.load(vdoInitParams);
playerFragment.initialize(this)
...
...
override fun onInitializationSuccess(playerHost: PlayerHost, player: VdoPlayer, wasRestored: Boolean) {
Log.i(TAG, "onInitializationSuccess")
this.player = player
// Add a listener for playback events.
player.addPlaybackEventListener(playbackListener)
// ...
}
val vdoInitParams = VdoInitParams.createParamsForOffline(mediaId)
player.load(vdoInitParams)
Note: Please ensure to replace mediaId
with the actual media ID of the downloaded video.
Delete downloads
To delete a media download use the VdoDownloadManager#remove()
method. This will cancel the download if it is still downloading or pending and remove any downloaded media files. This method can be used to delete a single media or multiple medias. You will also receive a EventListener#onDeleted()
callback if you have an EventListener
registered with the VdoDownloadManager
.
- Java
- Kotlin
VdoDownloadManager vdoDownloadManager = VdoDownloadManager.getInstance(activityContext);
vdoDownloadManager.remove(mediaIdstoDelete);
val vdoDownloadManager = VdoDownloadManager.getInstance(activityContext)
vdoDownloadManager.remove(mediaIdstoDelete)
Pause downloads
To pause a single media download use the VdoDownloadManager#stopDownload()
and to pause all downloads, use VdoDownloadManager#pauseAllDownloads()
method. You will also receive a EventListener#onChanged()
callback if you have an EventListener
registered with the VdoDownloadManager
.
- Java
- Kotlin
VdoDownloadManager vdoDownloadManager = VdoDownloadManager.getInstance(activityContext);
vdoDownloadManager.stopDownload(mediaIdtoPause);
vdoDownloadManager.pauseAllDownloads(mediaIdstoPause);
val vdoDownloadManager = VdoDownloadManager.getInstance(activityContext)
vdoDownloadManager.stopDownload(mediaIdtoPause)
vdoDownloadManager.pauseAllDownloads(mediaIdstoPause)
Resume downloads
To resume a single media download use the VdoDownloadManager#resumeDownload()
and to resume all downloads, use VdoDownloadManager#resumeDownloads()
method. You will also receive a EventListener#onChanged()
callback if you have an EventListener
registered with the VdoDownloadManager
.
- Java
- Kotlin
VdoDownloadManager vdoDownloadManager = VdoDownloadManager.getInstance(activityContext);
vdoDownloadManager.resumeDownload(mediaIdtoResume);
vdoDownloadManager.resumeDownloads(mediaIdstoResume);
val vdoDownloadManager = VdoDownloadManager.getInstance(activityContext)
vdoDownloadManager.resumeDownload(mediaIdtoResume)
vdoDownloadManager.resumeDownloads(mediaIdstoResume)
Check if video is expired
To check if a video is expired, use the DownloadStatus.isExpired()
method. This will return true or false based on whether the provided media id is expired or not.
- Java
- Kotlin
// Call isExpired() method on downloadStatus object to check for expiry.
DownloadStatus downloadStatus = downloadStatusList.get(position);
boolean isExpired = downloadStatus.isExpired(context);
if (isExpired) {
// Either re-download the video or remove the video.
}
// Call isExpired() method on downloadStatus object to check for expiry.
val downloadStatus = downloadStatusList[position]
val isExpired = downloadStatus.isExpired(context)
if (isExpired) {
// Either re-download the video or remove the video.
}
- If the video is expired, adjust the UI accordingly. Avoid playing the expired video to prevent displaying error code 6187 along with a message; the error message will provide viewers with context about the video expiration.
- For expired video, you can choose to either re-download the video or remove the video.