7

I'm currently implementing offline streaming with FairPlay streaming. Therefor I'm downloading streams using an AVAssetDownloadTask.

I want to give the users feedback about the size of the download which starts to begin:

Are you sure you want to download this stream? It will take 2.4GB to download and you currently have 14GB of space left

I've checking properties like countOfBytesReceived and countOfBytesExpectedToReceive but these wont give back correct values.

let headRequest = NSMutableURLRequest(URL: asset.streamURL)
headRequest.HTTPMethod = "HEAD"
let sizeTask = NSURLSession.sharedSession().dataTaskWithRequest(headRequest) { (data, response, error) in
    print("Expected size is \(response?.expectedContentLength)")
}.resume()

prints a size of 2464, where at the end the size is 3GB.

During the download I logged above properties:

func URLSession(session: NSURLSession, assetDownloadTask: AVAssetDownloadTask, didLoadTimeRange timeRange: CMTimeRange, totalTimeRangesLoaded loadedTimeRanges: [NSValue], timeRangeExpectedToLoad: CMTimeRange) {
    print("Downloaded \( convertFileSizeToMegabyte(Float(assetDownloadTask.countOfBytesReceived)))/\(convertFileSizeToMegabyte(Float(assetDownloadTask.countOfBytesExpectedToReceive))) MB")
}

But these stay at zero:

Downloaded 0.0/0.0 MB

Antoine
  • 21,544
  • 11
  • 81
  • 91

3 Answers3

2

HLS streams are actually a collection of files known as manifests and transport streams. Manifests usually contain a text listing of sub-manifests (each one corresponding to a different bitrate), and these sub-manifests contains a list of transport streams that contain the actual movie data.

In your code, when you download the HLS URL, you're actually downloading just the master manifest, and that's typically a few thousand bytes. If you want to copy the entire stream, you'll need to parse all the manifests, replicate the folder structure of the original stream, and grab the transport segments too (these are usually in 10-second segments, so there can be hundreds of these). You may need to rewrite URLs if the manifests are specified with absolute URLs as well.

To compute the size of each stream, you could multiply the bitrate (listed in the master manifest) by the duration of the stream; that might be a good enough estimate for download purposes.

A better answer here, since you're using the AVAssetDownloadTask in the context of offline FairPlay, is to implement the AVAssetDownloadDelegate. One of the methods in that protocol gives you the progress you're looking for:

URLSession:assetDownloadTask:didLoadTimeRange:totalTimeRangesLoaded:timeRangeExpectedToLoad:

Here's WWDC 2016 Session 504 showing this delegate in action.

There are a lot of details related to offline playback with FairPlay, so it's a good idea to go through that video very carefully.

pixbug
  • 404
  • 3
  • 6
0

I haven't worked with this API personally, but I am at least somewhat familiar with HTTP Live Streaming. With that knowledge, I think I know why you're not able to get the info you're looking for.

The HLS protocol is designed for handling live streaming as well as streaming of fixed-length assets. It does this by dicing up the media into in what are typically about ten-second chunks, IIRC, and listing the URLs for those chunks in a playlist file at a specific URL.

If the playlist does not change, then you can download the playlist, calculate the number of files, get the length of the first file, and multiply that by the number of files, and you'll get a crude approximation, which you can replace with an exact value when you start retrieving the last chunk.

However there is no guarantee that the playlist will not change. With HLS, the playlist can potentially change every ten seconds, by removing the oldest segments (or not) and adding new segments at the end. In this way, HLS supports streaming of live broadcasts that have no end at all. In that context, the notion of the download having a size is nonsensical.

To make matters worse, 2464 is probably the size of the playlist file, not the size of the first asset in it, which is to say that it tells you nothing unless that subclass's didReceiveResponse: method works, in which case you might be able to obtain the length of each segment by reading the Content-Length header as it fetches it. And even if it does work normally, you probably still can't obtain the number of segments from this API (and there's also no guarantee that all the segments will be precisely the same length, though they should be pretty close).

I suspect that to obtain the information you want, even for a non-live asset, you would probably have to fetch the playlist, parse it yourself, and perform a series of HEAD requests for each of the media segment URLs listed in it.

Fortunately, the HLS specification is a publicly available standard, so if you want to go down that path, there are RFCs you can read to learn about the structure of the playlist file. And AFAIK, the playlist itself isn't encrypted with any DRM or anything, so it should be possible to do so even though the actual decryption portion of the API isn't public (AFAIK).

dgatwood
  • 9,519
  • 1
  • 24
  • 48
0

This is my C#/Xamarin code to compute the final download size. It is most likely imperfect, especially with the new codecs supported with iOS11, but you should get the idea.

private static async Task<long> GetFullVideoBitrate(string manifestUrl)
{
    string bandwidthPattern = "#EXT-X-STREAM-INF:.*(BANDWIDTH=(?<bitrate>\\d+)).*";
    string videoPattern = "^" + bandwidthPattern + "(RESOLUTION=(?<width>\\d+)x(?<height>\\d+)).*CODECS=\".*avc1.*\".*$";
    string audioPattern = "^(?!.*RESOLUTION)" + bandwidthPattern + "CODECS=\".*mp4a.*\".*$";

    HttpClient manifestClient = new HttpClient();
    Regex videoInfoRegex = new Regex(videoPattern, RegexOptions.Multiline);
    Regex audioInfoRegex = new Regex(audioPattern, RegexOptions.Multiline);
    string manifestData = await manifestClient.GetStringAsync(manifestUrl);
    MatchCollection videoMatches = videoInfoRegex.Matches(manifestData);
    MatchCollection audioMatches = audioInfoRegex.Matches(manifestData);
    List<long> videoBitrates = new List<long>();
    List<long> audioBitrates = new List<long>();

    foreach (Match match in videoMatches)
    {
        long bitrate;

        if (long.TryParse(match.Groups["bitrate"]
                               .Value,
                          out bitrate))
        {
            videoBitrates.Add(bitrate);
        }
    }

    foreach (Match match in audioMatches)
    {
        long bitrate;

        if (long.TryParse(match.Groups["bitrate"]
                               .Value,
                          out bitrate))
        {
            audioBitrates.Add(bitrate);
        }
    }

    if (videoBitrates.Any() && audioBitrates.Any())
    {
        IEnumerable<long> availableBitrate = videoBitrates.Where(b => b >= Settings.VideoQuality.ToBitRate());
        long videoBitrateSelected = availableBitrate.Any() ? availableBitrate.First() : videoBitrates.Max();
        long totalAudioBitrate = audioBitrates.Sum();

        return videoBitrateSelected + totalAudioBitrate;
    }

    return 0;
}
Sylvain Gravel
  • 131
  • 2
  • 5