# Downloaders

We developed different downloaders for handling HLS/FPS and MPEG-DASH content. They can all be used behind the common interface `DownloaderAPI`

## DownloaderAPI

The main data type in `DownloaderAPI` is a `Download`.

    let url = URL(string: "..")!
    let config = PlayerConfiguration(with: url, contentType: "...")

    let downloader = PRESTOplaySDK.shared.downloader()
    let download = downloader.createDownload(config, headers: [:])
    downloader.prepareDownload(download.uuid) { download, error in
        guard let download else { return }
        var res = false

        if let videoRendition = download.videoTracks.first?.renditions.first {
            res = download.selectVideoRendition(videoRendition)
        }
        if let audioRendition = download.audioTracks.first?.renditions.first {
            res = download.selectAudioRendition(audioRendition)
        }

        if let error = downloader.startDownload(download.uuid, delegate: nil) {
            print("Start error: \(error)")
        }
    }

To receive events related to a `Download` we use a delegate:

    class MyDownloadDelegate: DownloadDelegate {
        public let complete: () -> Void

        public init (_ complete: @escaping () -> Void) {
            self.complete = complete
        }

        func didStateChange(_ download: Download) {
            if download.state == .success {
                complete()
            }
        }

        func didProgressChange(_ download: Download) {
        }
    }

## License

If you want to prefetch the license without forcing the user to be online during the first playback, use the method PRESTOplaySDK.shared.prefetcher(for:)

    let prefetcher = PRESTOplaySDK.shared.prefetcher(for: config)
    prefetcher?.prefetchKeys() { error in
        if error != nil {
            // Handle error
        }
    }

Firstly, the prefetcher searches for keys in `Multivariant Playlist` in `EXT-X-SESSION-KEY`. If keys are found the prefetcher downloads the listed keys. If keys are not found the prefetcher search for `EXT-X-KEY` tags in:

- variant playlists (`EXT-X-STREAM-INF`)
- rendition tracks (`EXT-X-MEDIA`)
- iframe tracks (`EXT-X-I-FRAME-STREAM-INF`)

## HLS streams

For downloading HLS and FPS we created a wrapper around the native HLS downloader `AVAssetDownloadTask`

- enables querying and selection of stream bitrate
- enables querying of audio media streams and subtitle media tracks
- adds support for pre-selecting audio and subtitle tracks before download begins
- greatly simplifies API usage and the monitoring of downloading progress.
- supports preselection of the preferred media bitrate. If no suitable media bitrate is found, the highest bitrate will be selected. Only one media bitrate can be selected, meaning that only one video rendition will be downloaded
- background downloads are fully supported
- If the content is encrypted, you can prefetch the license before starting the download. You can also get the license after the download has completed (and without starting playback).

Background download tasks are handled by `NSURLSession` underneath and there are differences between the test environment and the production environment.

For background downloads an instance of HLSDownloader should be kept in AppDelegate. This is the only supported way from Apple to preserve completionHandlers of background sessions in-between Application Lifecycle changes (in our example we use static class field, but it is also possible to use object variables in AppDelegate).

When additional tracks are selected, the complete download size is not known in advance due to an API limitation. The download progress restarts from zero for each additional track. You can still differentiate between the main download (which includes the minimum set of tracks that can be played) and additional tracks by using the flag.

When using a master playlist, the CODECS attribute of the EXT-X-STREAM-INF tag may signal CODECs not supported for playback on the downloading device, but download of such streams will be possible. Check Apple FAQ for an updated list of recommended codecs.

### HLS download progress

Natively, Apple’s API measures download progress by stream duration cached. The PRESTOplay SDK uses average bandwidth provided in the main playlist to estimate download size and progress.

Since `AVERAGE-BANDWIDTH` is an optional part of the HLS playlist, the SDK falls back to the mandatory `BANDWIDTH` field. `BANDWIDTH` is a peak segment bit rate, so download sizes can be overestimated (see the HLS streaming standard for details).

We recommend using native time-based download progress measures for HLS content.

## MPEG-DASH streams

DASH streams are downloaded using `NSURLSession` with custom manifest parser library.

## Additional track selection

Additional track selection can be done on `Download` class instance. We encourage to store all possible selection in your `ViewController` class.

    var audioTracks = [AudioTrack]()
    var audioRenditions = [AudioRendition]()
    var textTracks = [TextTrack]()
    var videoRenditions = [VideoRendition]()

First you need to create the download.

    let download = downloader.createDownload(persistableConfiguration, headers: [:])

When `prepareDownload` step is finished you can get the list of all available to download tracks.

    downloader.prepareDownload(download.uuid) { download, error in

        ...

        download.audioTracks.forEach { track in
            track.renditions.forEach { rendition in
                self.audioRenditions.append(rendition)
            }
        }
        self.audioTracks = download.audioTracks
        self.textTracks = download.textTracks

        download.videoTracks.forEach { track in
            track.renditions.forEach { rendition in
                self.videoRenditions.append(rendition)
            }
        }

        ...

    }

Below you can find a universal method to select and unselect tracks for the download (for native HLS, only the default video track is selected).

    func selectTrack(_ indexPath: IndexPath , isSelected: Bool) {
        if indexPath.section == audioSectionIndex {
            let rendition = audioRenditions[indexPath.row]
            if isSelected {
                _ = download?.selectAudioRendition(rendition)
            } else {
                _ = download?.unselectAudioRendition(rendition)
            }
        } else if indexPath.section == textSectionIndex {
            let textTrack = textTracks[indexPath.row]
            if isSelected {
                _ = download?.selectTextTrack(textTrack)
            } else {
                _ = download?.unselectTextTrack(textTrack)
            }
        } else if indexPath.section == renditionSectionIndex {
            let rendition = videoRenditions[indexPath.row]
            if isSelected {
                _ = download?.selectVideoRendition(rendition)
            } else {
                _ = download?.unselectVideoRendition(rendition)
            }
        }
    }