Manage Offline Content For Playback
One of the main challenges of an implementation to enable offline playback is to provide the operations to work with assets, such as starting content downloads, pausing resuming downloads, canceling downloads and deleting already downloaded content. The Bitmovin SDK provides a class called OfflineManager
that encapsulates all these operations and seamlessly controls the state, taking into account events like application going into background and application getting killed by the user or getting crashed.
Initialize an OfflineManager
In your AppDelegate
class, add the following to initialize the singleton OfflineManager
which is used to manage the whole lifecycle of offline assets.
1OfflineManager.initializeOfflineManager()
Download protected or unprotected content
Before you can actually download the content, first we need to create an instance of a SourceItem that represents the asset to be downloaded.
1let sourceItem = SourceItem(url: URL(string: "https://www.example.com/stream.m3u8")!)2sourceItem.itemTitle = "Sample Content"3sourceItem.itemDescription = "Description of this content"
In case the source is DRM-protected, you just need to provide its DRM configuration like you would normally do for an online playback use case:
1let fpsConfig = FairplayConfiguration(license: licenseUrl, certificateURL: certificateUrl)2sourceItem.add(drmConfiguration: fpsConfig)
You will also need a reference to the OfflineManager
singleton instance:
1let offlineManager = OfflineManager.sharedInstance()
Now that you have both the offline manager and source item, you can already kick off the download operation, calling the download(sourceItem:) method.
1offlineManager.download(sourceItem: sourceItem)
You can check the state of the download operation with the offlineState
method:
1offlineManager.offlineState(for: sourceItem) == .downloading2offlineManager.offlineState(for: sourceItem) == .downloaded3offlineManager.offlineState(for: sourceItem) == .suspended4offlineManager.offlineState(for: sourceItem) == .notDownloaded5offlineManager.offlineState(for: sourceItem) == .canceling
The offlineState(for:)
method is very useful especially to perform pre-validations, for example, before allowing the user to cancel a download. However, the SDK also allows you to add event listeners where you can track all the states mentioned above.
1offlineManager.add(listener: self, for: sourceItem)
And then:
1extension MyViewController: OfflineManagerListener {23 func offlineManager(_ offlineManager: OfflineManager, didFailWithError error: Error?) {4 let errorMessage = error?.localizedDescription ?? "unknown"5 print("[MyViewController] Download resulted in error: \(errorMessage)")6 setViewState(.notDownloaded)7 }89 func offlineManagerDidFinishDownload(_ offlineManager: OfflineManager) {10 print("[MyViewController] Download Finished")11 setViewState(.downloaded)12 }1314 func offlineManager(_ offlineManager: OfflineManager, didProgressTo progress: Double) {15 print("[MyViewController] Progress")16 // update ui with current progress17 setViewState(.downloading, withProgress: progress)18 }1920 func offlineManagerDidSuspendDownload(_ offlineManager: OfflineManager) {21 print("[MyViewController] Suspended")22 setViewState(.suspended)23 }2425 func offlineManager(_ offlineManager: OfflineManager, didResumeDownloadWithProgress progress: Double) {26 print("[MyViewController] Resumed")27 setViewState(.downloading, withProgress: progress)28 }2930 func offlineManagerDidCancelDownload(_ offlineManager: OfflineManager) {31 print("[MyViewController] Cancelled")32 setViewState(.notDownloaded)33 }34}
Cancel an ongoing download
The OfflineManager offers the cancelDownload(for:)
method that cancels a running or suspended download operation. When a download was canceled successfully, also all partially downloaded data is deleted automatically.
1guard offlineManager.offlineState(for: sourceItem) == .downloading ||2 offlineManager.offlineState(for: sourceItem) == .suspended else {3 return4}5offlineManager.cancelDownload(for: sourceItem)
Suspend a ongoing download
The suspendDownload(for:)
method can be used to suspend (pause) an ongoing download operation.
1guard offlineManager.offlineState(for: sourceItem) == .downloading else {2 return3}4offlineManager.suspendDownload(for: sourceItem)
Resume an suspended download
If the download of a given source item is .suspended
state it can be resumed at any time with the following instructions:
1guard offlineManager.offlineState(for: sourceItem) == .suspended else {2 return3}4offlineManager.resumeDownload(for: sourceItem)
Delete all downloaded data
You can force to delete all cached data for a given source item with the following:
1guard offlineManager.offlineState(for: sourceItem) == .downloaded else {2 return3}4offlineManager.deleteOfflineData(for: sourceItem)
Overview of states and allowed method calls
The following table outlines all allowed actions in all possible offline states a source item can have. It also shows which listener methods are called on the OfflineManagerListener
:
Current State | Method call triggering transition | OfflineManagerListener Event | Following State |
---|---|---|---|
NotDownloaded | download(sourceItem:) | - | Downloading |
Downloading | cancelDownload(for:) | - | Canceling |
Downloading | suspendDownload(for:) | offlineManagerDidSuspendDownload(:) | Suspended |
Downloading | - | offlineManagerDidFinishDownload(:) | Downloaded |
Downloading | - | offlineManager(:didProgressTo:) | Downloading |
Downloaded | deleteOfflineData(for:) | - | NotDownloaded |
Suspended | resumeDownload(for:) | offlineManager(:didResumeDownloadWithProgress:) | Downloading |
Suspended | cancelDownload(for:) | - | Canceling |
Canceling | - | offlineManagerDidCancelDownload(:) | NotDownloaded |
Play online/offline content seamlessly
Now that you know how to manage assets for offline playback, it's time to focus on the playback side of things. Assuming the state of the source item is .downloaded
, .downloading
or .suspended
, before passing the source item to the player configuration object you should check whether the device has access to the internet or not. If it doesn't, you can make sure the asset is playable offline by calling the isPlayableOffline
method:
1guard offlineManager.isPlayableOffline(sourceItem: sourceItem),2 let offlineSourceItem = offlineManager.createOfflineSourceItem(for: sourceItem, restrictedToAssetCache: true) else {3 finishWithError(title: "Error", message: "The device seems to be offline, but no offline content for the selected source available.")4 return5}6sourceItem = offlineSourceItem
The code snippet above also shows off how to create an OfflineSourceItem
for the given SourceItem
. This method gives you the option to restrictToAssetCache
, which will basically restrict the playback to the audio and subtitle tracks which are cached on disk. As a result, tracks which are not cached, does not show up as selectable in the player UI. If the device has access to the internet, you should probably set false to this parameter as it would allow the user to select tracks that are not cached yet. In case the state of the source item is .notDownloaded
or .canceling
and the device does not have access to the internet, then the playback won't be possible and you should notify the user. Now you have the source item to pass to the player configuration:
1let config = PlayerConfiguration()2config.sourceItem = sourceItem // this can be a SourceItem or a OfflineSourceItem34let player = BitmovinPlayer(configuration: config)5let playerView = BMPBitmovinPlayerView(player: player, frame: CGRect.zero)67player.add(listener: self)8playerView.add(listener: self)910playerView.autoresizingMask = [.flexibleHeight, .flexibleWidth]11playerView.frame = playerViewContainer.bounds1213playerViewContainer.addSubview(playerView)14playerViewContainer.bringSubview(toFront: playerView)1516self.playerView = playerView17self.player = player
Sample App
Here you will find a set of iOS sample applications and we have a dedicated app to show off the offline playback capabilities. Here is a short description of the important files of that project:
Models/SampleViewModel.swift: The source items are declared here. As a sample application the source items are hardcoded, but you could build that list fetching from a remote server for example.
Controllers/SamplesTableViewController.swift: shows a table with all the assets declared in the SampleViewModel class.
Controllers/SampleDetailViewController.swift: It receives a sourceItem and attaches the click handlers for the buttons: download, pause, resume, cancel, delete and play. This class invokes most of the methods of the OfflineManager.
Controllers/PlaybackViewController.swift: it also takes a source item and based on the offline state of it combined with the internet connectivity status it decides whether to use the same sourceItem that was received or to create an OfflineSourceItem out of the original sourceItem.
Utils/Reachability.h: useful class to check the connectivity with the internet.