Offline Playback

Offline, or persistent, storage, allows the storage of content and licenses for future playback. This is achieved through the use of two classes:

  • OTVPersistenceManager - the download manager for persistent assets
  • OTVPersistenceAsset - the asset we want to download

This guide follows the offline-storage example code project. If you are unfamiliar with any of steps, please refer back to the initial Integration Guide and basic-playback example code project.

Procedure

  1. Register the event listeners for the download stage changes and download progress.

    NotificationCenter.default.addObserver(
      self,
      selector: #selector(self.stateChanged),
      name: .OTVAssetDownloadStateChanged,
      object: nil)
    
    NotificationCenter.default.addObserver(
      self,
      selector: #selector(self.progressChanged),
      name: .OTVAssetDownloadProgress,
      object: nil)
    
  2. Add methods to handle the stateChanged and progressChanged notifications. This is just an example and uses controls (progressLabel, statusLabel, playButton and downloadButton) already created in the storyboard in the offline-playback example project.

    @objc func stateChanged(notification: NSNotification) {
        // These fields can be always be obtained from a state change notification
        let title = notification.userInfo![OTVPersistenceAsset.Keys.name] as! String
        let state = notification.userInfo![OTVPersistenceAsset.Keys.downloadState] as! String
    
        switch(state) {
        case OTVDownloadState.downloaded.rawValue:
        playButton.isEnabled = true
        statusLabel.text = "Downloaded '\(title)'"
        downloadButton.isEnabled = false
    
        case OTVDownloadState.error.rawValue:
        statusLabel.text = "Download error"
        playButton.isEnabled = false
        downloadButton.isEnabled = true
    
        case OTVDownloadState.downloading.rawValue:
        statusLabel.text = "Starting download..."
        downloadButton.isEnabled = false
    
        case OTVDownloadState.notDownloaded.rawValue:
        progressLabel.text = "Preparing..."
        playButton.isEnabled = false
    
        default:
        progressLabel.text = "Unknown State"
        playButton.isEnabled = false
        }
    }
    
    @objc func progressChanged(notification: NSNotification) {
        // These fields can be always be obtained from a progress change notification
        let title = notification.userInfo![OTVPersistenceAsset.Keys.name] as! String
        let downloadProgress = notification.userInfo![OTVPersistenceAsset.Keys.percentDownloaded] as! Double
    
        statusLabel.text = "Downloading '\(title)'..."
        progressLabel.text = String(format:"%.0f%%", downloadProgress)
    }
    
  3. To acquire a downloadable asset, register the content URL to the downloader. The downloader provides information about the asset to download during this step.

    The registration step fetches the metadata that is related to the asset. As soon as an application registers a URL, the manager will fetch the playlists. When information contained in the playlist is available it will be scanned for any license requirements to determine if the stream is clear or encrypted. If a license is required to playback the content, it will be requested. If a license is required and has been successfully acquired (or is not required) the state of the download will change to downloading.

    If the playlist or required license cannot be acquired, the state of the download will change to error.

    var OTVPersistenceAsset: OTVPersistenceAsset?
    let assetName = "Big Buck Bunny"
    let assetURL = URL(string:
        "https://d3bqrzf9w11pn3.cloudfront.net/dl2g_hls_bbb_clear/index.m3u8")!
    
        OTVPersistenceAsset = OTVPersistenceManager.sharedManager.startDownload(
        url: assetURL, asset: assetName, artwork: nil)
    

    startDownload() also provides an optional options parameter, that is used to provide extra information on minimum bitrate to use, and if a license should be pre-fetched or not.

    • OTVPrefetchLicense : Bool
    • AVAssetDownloadTaskMinimumRequiredMediaBitrateKey : Integer
    // for example...
    var yourOptions: [String: Any] = [AVAssetDownloadTaskMinimumRequiredMediaBitrateKey : 600000, OTVPrefetchLicense: True]
    
    OTVPersistenceManager.sharedManager.startDownload(
        url: assetURL, asset: assetName, artwork: nil, options: yourOptions )
    
  4. If required a download may be paused and resumed.

    OTVPersistenceManager.sharedManager.pauseDownload(asset: OTVPersistenceAsset)
    // and 
    OTVPersistenceManager.sharedManager.resumeDownload(asset: OTVPersistenceAsset)
    

    Note: The asset can only be paused once it is in the downloading state.

  5. Once downloaded, content can be viewed in a similar manner to streamed content.

    guard let url = OTVPersistenceAsset?.offlineURL() else {
        // Asset is not accessible, handle appropriately
        return
    }
    
    // In this example the media player is initialised without any media item
    // Here we use an existing media player and change the item to play
    let OTVItem = OTVAVPlayerItem(url: url)
    OTVPlayer.replaceCurrentItem(with: OTVItem)
    OTVPlayer.play()
    
  6. Finally, it is up to the application to purge the licence and asset when the user no longer needs it.

    private func deleteDownloads() {
        guard let downloads = OTVPersistenceManager.sharedManager.getDownloads() else {
        return
        }
    
        for asset in downloads {
        // The OTVPersistenceManager keeps a record of downloaded and downloading assets
            if OTVPersistenceManager.sharedManager.deleteDownload(asset: asset) {
                print("Item successfully deleted")
            } else {
                print("Item could not be deleted")
            }
        }
    }
    
    

Limitations

Recovery of terminated in-progress downloads

The SDK cannot recover downloads that are in progress when:

  • Running with Xcode (when Xcode terminates the process)

  • Termination via the App Switcher (e.g., on devices with a home button, a double-home-press and slide up)

  • Termination while the app is suspended and the device reboots

License expiry

  • License expiration is not yet supported.

iOS Version support

  • Only iOS 11.2 and later is supported.