Most people want to save time, money, and other resources in regards to all aspects of life. However, in this article, we will talk about software development for the iOS platform. So all these moments are right for Users of mobile applications too. An app should look nice, does something useful, and provide feedback as fast as possible. But usually, a lot of in App contents are somewhere in a Cloud. So we need to load them first, at least some part... maybe just a few bits at the beginning. There're many types of services that provide access to online video, different podcasts, and music. Or maybe chatting App with an ability to send short video messages. All should work fast or at least looks like so.
Let's look at playing audio/video files from remote storage. A user can play the same track ten times a day. And he can fell in sleep while looking at loading activity if it appears every time. Or not, usually he deletes the App from the device. But we want to have the best App in the world. So let's make our users happy! And avoid loading state as possible. Load data just once and cache it locally. You always have a chance to delete outdated and not actual data later. Let's begin with a simple case and research on how we can deal with such tasks. For example, we have a short video record that can be viewed many times. In any case, we'll need to download it. At least once explicitly. Alternatively, it will do for you the 'black box' under the hood. We will use the AVPlayer to play this video file in this example.
The fastest and easiest way to save the video file to the disk is to use AVAssetExportSession. You can create it with the AVURLAsset and quality preset. Then assign the outputURL and outputFileType. After call exportAsynchronously method to know when the exporter finished the task. That's all. It can be a good solution when you have just a few seconds in a record. With this approach, we can achieve the goal and save some development time.
However, what if we need a similar feature for a movie or podcast? A user can pause it and continue watching the next day. It can be not enough time to download the whole file with AVAssetExportSession. So here we have AVAssetResourceLoaderDelegate. It can help us to implement similar logic as provides exporter and even more.
Let's dive in it to understand how does it work, and how we can use it. Now we will talk about basics and a simple clone of the exporter. This example is not the optimal way. In the next article, we implement more interesting logic with AVAssetResourceLoaderDelegate.
Intercepting original AVAssetResourceLoadingRequests
At the beginning we have:
Some videoURL to the remote file.
AVPlayer to play content in the App. We use it with the wrapper MediaPlayer to make life a bit easier.
And SimpleResourceLoaderDelegate which we need to implement. It will do all the magic for us.
View controller has a separate method to create and setup MediaPlayer.
First, we create a custom loader delegate. Then we can create AVURLAsset with our videoURL.
Attention, we should not pass the original URL like https://www.some.com/short.mp4, but a bit modified https-demoloader://www.some.com/short.mp4 if we want to our SimpleResourceLoaderDelegate be invoked. After we can assign loaderDelegate to videoAsset. And finally, create AVPlayer (in our case wrapper MediaPlayer) with the videoAsset.
Let's go to the AVAssetResourceLoaderDelegate implementation and look what's going on in it. Right now we are interested in just two methods:
First one invokes when some data should be loaded
Second invokes when some previous request was canceled
When the App starts playing the video first method invokes when the player asks about a new batch of data. With our delegate, we intercept requests and load all data with ourselves and provide back to the origin AVAssetResourceLoadingRequest data which we have. All provided data is our responsibility from now. Here's a diagram of AVAssetResourceLoadingRequests:
As we can see AVAssetResourceLoader sends few requests to get the info about the file. And junks of data with requestedOffset and requestedLength.
We need to save all valid AVAssetResourceLoadingRequest somewhere. Let's use for simple Array for that
Also, we need to handle request cancelation. In this case, we will just remove the saved origin request from the array.
Downloading real data
Now we need to download all the required data for the video. We can create URLSession with the single URLSessionDataTask to keep everything simple in this example. Also, we need to implement a few methods from URLSessionTaskDelegate and URLSessionDataDelegate. We need these methods:
To process the response with file info and fill with it info request from origin AVAssetResourceLoadingRequest
To process data from our video file. We will store all the data in memory for now. It's not the best solution, but it is enough for a short video from our example.
Now we can create our URLSession with code
All received data we will save in the variable
And then we need to create our data task when we will receive the first AVAssetResourceLoadingRequest
So we have intercepted all origin requests from the resource loader and have created our data task to download video data. So now we can send chunks of downloaded data to the saved AVAssetResourceLoadingDataRequest.
But don't forget about AVAssetResourceLoadingContentInformationRequest too.
When we received the URLResponse from the URLSessionDataTask we should fill with it the AVAssetResourceLoadingContentInformationRequest.
It looks like:
And every time when we receive new data from data task we can check our saved origin loadingRequests and fill them with available data. And maybe finish some of them. For example, we have this case. Some data already downloaded. AVAssetResourceLoadingDataRequest has some requestedOffset and requestedLength. Also, we already sent some data to it, so currentOffset is not 0 too.
Looks like on the diagram
Now we need to calculate which pease of data available for the origin request. Logic looks like
Check with the length of downloaded data we can pass to the request
Respond with a chunk of data
Check if we sent all required data to the request, and finish it if so
When we finish with downloading the data, we can save it to a local file for example.
We are done!
That's all with the basics of the AVAssetResourceLoaderDelegate. We understand how does it work and how to deal with it. You can try the Demo project.