📜 ⬆️ ⬇️

How to cache AVURLAsset data downloaded by AVPLayer

iFunny app image


Hi, Habr. My name is Vlad. I work as an iOS developer at FunCorp. We make entertainment apps. You may have heard about our flagship iFunny and the popular in the CIS application IdAprikol. In this article I will talk about how to get the video data downloaded by the player for further work with them.


tl; dr


If you only need a solution, look here at this library .


Problem


In our application, iFunny content feed consists mainly of images and videos. For caching images, we use SDWebImage . For the video, we previously downloaded the file completely and only then began to play. It worked for short videos. On the long ones, too much time passed from the moment the screen was opened (the start of the download) to the start of playback, even on wifi.


Solutions


The first idea was to store AVAsset objects at the model level. This approach works within a session (AVPlayer will not download the same file several times), but will not work between application launches.


After that, I tried to transfer AVAsset to NSData using AVAssetExportSession . The export session worked well for AVAsset created from local files, but for remote assets I always got the error:


Error Domain=AVFoundationErrorDomain Code=-11800 “The operation could not be completed” UserInfo={NSLocalizedFailureReason=An unknown error occurred (-16974), NSLocalizedDescription=The operation could not be completed, NSUnderlyingError=0x60000025a940 {Error Domain=NSOSStatusErrorDomain Code=-16974 “(null)”}} 

The third solution was to use the resourceLoader field in AVURLAsset. This approach worked, but I ran into some problems during its implementation.


Implementation


According to Apple documentation :


 An AVAssetResourceLoader mediates requests to load resources required by an AVURLAsset by asking a delegate object that you provide for assistance. When a resource is required that cannot be loaded by the AVURLAsset itself, the resource loader makes a request of its delegate to load it and proceeds according to the delegate's response. 

First you need to make sure that AVURLAsset could not load the data itself and call the delegate methods of the resourceLoader for each request. To do this, it is enough to change the URL scheme of AVURLAsset from HTTP (S) to any other. Do not forget to keep the original, you still need it.


 NSURLComponents *components = [[NSURLComponents alloc] initWithURL:URL resolvingAgainstBaseURL:NO]; components.scheme = @“customscheme”; AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:[components URL] options:options]; 

When a resource loader cannot load a resource on its own, it calls the delegate method:


 - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest; 

By returning YES from this method, you tell the resource loader that you are now responsible for this request. The answer NO will result in an error inside AVURLAsset, since neither the resource loader nor the delegate can execute this request.


From this point on, work begins with the AVAssetResourceLoadingRequest object, which was passed to the delegate method by an argument. You can load data synchronously or asynchronously (remember to save the loadingRequest object somewhere if you load asynchronously). After the download is complete, you need to call finishLoading or finishLoadingWithError: depending on the result.




There are two types of download requests for AVAssetResourceLoadingRequest: a data request and a request for content information. You can determine the type by checking the fields:


 @property (nonatomic, readonly, nullable) AVAssetResourceLoadingContentInformationRequest *contentInformationRequest; @property (nonatomic, readonly, nullable) AVAssetResourceLoadingDataRequest *dataRequest; 

In response to the download request, you need to return the content type (UTI), its length and the flag "whether range requests are supported." For this, I used HTTP HEAD. When you receive the answer, you need to fill in the data in the fields of the contentInformationRequest object and call the finishLoading method.


In response to a data request, you must return the URL, indent, and length data contained in the AVAssetResourceLoadingDataRequest object. If you want to write your own implementation of the request, carefully read the AVAssetResourceLoadingDataRequest documentation , there are not quite obvious points.


I wrote an implementation with HTTP GET requests and a range header. While writing the data query, I noticed strange behavior. Requests and responses in NSURLSessionDataTas k might be different. The forum branch at developer.apple confirms that this is a bug inside NSURLCache. The range header is ignored and you may receive the wrong piece of data you requested. I managed to reproduce it only on iOS <= 10.


You must put the loaded data in the dataRequest using the method respondWithData:. You can save the same data to your cache. The next time you open this file, you can take data directly from the cache.


Thus, we were able to launch the video immediately after the download started and cache it without making unnecessary requests.




Implementation of all delegate methods can be found here . The library for caching AVURLAsset'a lies here , the readme describes how to work with it.


If you have any questions, welcome to the comments or in a personal. vdugnist


')

Source: https://habr.com/ru/post/346848/


All Articles