35

Root Problem​

Our video buffers a lot when seeking in iOS. It buffers quite a bit more than our web player which saves copies of the already watched segments in temp storage.

​Desired Solution​

Caching the video segments locally on the device's disk. We're fine with caching a single quality and always replaying it.

​Blocker​

We can't find a way to perform caching within AVFoundation/AVPlayer.

What We've Tried

2 ways to intercept networking requests with AVPlayer.

  1. Conforming to ​AVAssetResourceLoaderDelegate ​and handling the loading of the media manually

Doesn't work with HLS. You can load the m3u8 files by implementing AVAssetResourceLoaderDelegate​, which allows you to pass authentication or to decrypt the response, however the .ts files can't be loaded. Here's the code we tried: https://gist.github.com/nathanhillyer/84e46152d7c4c88183b6

  1. Implementing a NSURLProtocol ​to capture requests for .ts files.

AVURLAsset actually avoids being intercepted. Somehow the network requests just don't get captured. (No clue why)

Nathan Hillyer
  • 1,959
  • 1
  • 18
  • 22
  • 1
    I was just about to implement a NSURLProtocol to try and cache `.ts` files. @narohi - Did you ever figure this out? I also really want to cache HLS segments on disk. Glad to find out that it won't work before I waste my time. Right now I'm leaning towards dropping `AVPlayer`, and just writing my own HLS player by parsing the `m3u8` playlists and using `AVQueuePlayer`. JWPlayer SDK for iOS looks promising, but their "contact us for our annual contract pricing" sounds a bit scary. – ndbroadbent May 31 '15 at 17:17
  • Ahhh, it looks like we followed exactly the same tutorial, and ended up in the same place. I'm also getting `AVPlayerItemStatus.Failed` after I call `finishLoading()` on the request, and the error is just: `The operation could not be completed` – ndbroadbent May 31 '15 at 22:54
  • 1
    you can proxy the HLS stream - download the ts manually and host the m3u8 on the device (localhost/xxx.m3u8) – Itay Kinnrot Jun 02 '15 at 20:21
  • @nathan.f77 we are currently prototyping out proxying the stream with CocoaHTTPServer and the results are promising – Nathan Hillyer Jun 04 '15 at 21:19
  • @ItayKinnrot thanks! that's the solution we've had in mind and started prototyping – Nathan Hillyer Jun 04 '15 at 21:20
  • @narohi - Awesome, thanks for letting me know. I'm also planning to write a proxy server with caching. Another advantage is that your HLS implementation can actually share bandwidth measurements between different videos. (I think AVPlayer doesn't share any state between videos.) – ndbroadbent Jun 04 '15 at 21:37
  • @nahori Hi, I've tried that solution as well but all I can get cached is the .m3u8, .ts files won't cache no matter what. Have anyone got an idea of what I may do please? – Roux Jul 24 '15 at 07:45
  • @Roux we rewrite the HLS stream to point locally and then pass the remote URL as a parameter. once the local URL is intercepted by CocoaHTTPServer we start caching the HLS segment and serve it as an HTTP response. we've got the caching solution in prototype right now but plan on open sourcing it by the end of the year – Nathan Hillyer Jul 24 '15 at 15:55
  • @nahori Ok, thanks for the heads up, glad to hear it! ;) – Roux Jul 27 '15 at 08:42
  • @narohi is there any chance, that you will make your solution opensource? :) – Ponf Aug 31 '15 at 15:40
  • @Ponf yes, but it I wouldn't expect to see it until Q1 2016 – Nathan Hillyer Aug 31 '15 at 20:22
  • So your solution is available now as opensource, @narohi – Vikas Dadheech Jun 01 '17 at 08:33
  • The company for which I was working decided to not open source it. Sorry. – Nathan Hillyer Jun 01 '17 at 16:10
  • 3
    I wrote a reverse proxy server to cache HLS segments and it worked. The company I'm working for just made this solution open source: https://github.com/StyleShare/HLSCachingReverseProxyServer – devxoul Nov 20 '19 at 09:31

4 Answers4

13

Let's start with really good news - iOS 10 and up - gives this out of the box. No more need for hacks soon. More details can be found in the following WWDC16 session about whats new in HTTP Live Streaming: https://developer.apple.com/videos/play/wwdc2016/504/

Now back to the current state of things - iOS 9 and lower: With AVPlayer, no. But you can cache HLS segments via a local HTTP server and play the local stream with AVPlayer.

AVPlayer and AVAsset don't contain the necessary information when dealing with HLS playback (It behaves differently than a MP4 static file for example).

TL;DR - You need to use HTTP requests to get the segments and serve them using a local HTTP Server.

A few companies, including the one I'm working for, are using this strategy.

Use a connection to download the segments at the quality you want, rebuild the manifest and flatten it all into one directory and one quality and then use a local http server inside the app to serve it to AVPlayer (AVPlayer can only play HLS streams served over HTTP - not from file assets).

There are edge cases, such as, buffering if you want to play and download in one run, rebuilding the m3u8 manifest correctly, and different AVPlayer states with disk reading.

I've found this out from first hand knowledge, both having such a system in production for 5 years and other video products in the App Store that use the same solution - in total serving many users.

This is also the best solution we've found for android.

Liviu R
  • 679
  • 8
  • 16
  • 2
    I appreciate your perspective, and we did proxy to a local HLS stream. The original question "Is it possible to cache HLS segments with AVPlayer?" isn't answered correctly, however. The answer is **no**, but it is possible to cache HLS segments _with_ a local HTTP server. If you update your answer, I will accept it. – Nathan Hillyer May 21 '16 at 19:02
  • Revised the answer :) – Liviu R May 22 '16 at 21:57
  • BTW - make sure to use a non standard port for the HTTP proxy to avoid collisions with other apps / other apps you distribute with the same mechanism :) Especially if you have some background permission. – Liviu R May 22 '16 at 21:59
  • Great solution! This way, would music/video keep on running (endless) when the app is in the background? – Daan Jun 08 '16 at 16:25
  • @dubbelugh - One would need to add a background mode audio and it should. I have to say I haven't tried it though. We are not allowing background playback like that (like most video apps) so no insights there beyond that note. – Liviu R Jun 09 '16 at 18:37
  • @LiviuR - Just tested audio in background mode and indeed it works. I use GCDWebServer as local HTTP server and it works with background mode out of the box. – Daan Jun 16 '16 at 07:56
  • @dubbelugh ... now the sad part / happy part of all this - is iOS 10 introduces this feature out of the box :) – Liviu R Jun 16 '16 at 23:27
  • @LiviuR, do you mean that caching/buffering from ios 10? – user2526811 Oct 21 '16 at 12:15
  • @LiviuR can you please share an open source project or file demonstrating the same concept? – Vikas Dadheech Jun 21 '17 at 11:57
  • @VikasDadheech - the iOS 9 and lower version? unfortunately no. It's part of my company source code. I would highly suggest supporting iOS 10 and up and going with the built in solution as described in the WWDC16 session I linked to. It's much more reliable, supports DRM and with iOS 11 also adds the option for the user to manage that content from settings as well. – Liviu R Jun 22 '17 at 13:22
  • are you saying HLS **segments** get cached now any work needed and I can easily rewind to a downloaded segment using AVPlayer? Does that require any `cache-control` headers from the response? – mfaani May 07 '21 at 16:52
  • This answer is REALLY old. TL:DR - you need to download the asset and play it from the asset URL. it's not automatic. here's the relevant documentation page: https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/MediaPlaybackGuide/Contents/Resources/en.lproj/HTTPLiveStreaming/HTTPLiveStreaming.html – Liviu R May 10 '21 at 14:48
3

Actually, we can get AVPlayer to play a video from network but if you want to to cache the downloaded data to play it locally, with AVPlayer that seems impossible now.

Fortunately, there are a great API is the resourceLoader object in AVURLAsset, which you can provide controlled access to a remote audio file to AVPlayer. This works like a local HTTP proxy but without all the hassles.

You can find more detail on https://gist.github.com/anonymous/83a93746d1ea52e9d23f

HSG
  • 1,224
  • 11
  • 17
  • 1
    Thanks Hung. As stated in the original question the AVURLAsset's resource loader doesn't call its delegate for HLS video, so that solution will not work. – Nathan Hillyer Oct 06 '15 at 01:52
  • Thank you. I did not read your question carefully in term of using the AVURLAsset's resource loader because I did not think that its delegate does not call for HLS. Currently, do you have any alternative approach for this problem? – HSG Oct 07 '15 at 08:33
  • 1
    Yes, we are running CocoaHTTPServer locally and proxying the HLS stream. I'd like to be able to do this with just AVPlayer but we haven't found a solution for that yet. – Nathan Hillyer Oct 20 '15 at 19:22
1

Starting with iOS 10, you can use AVFoundation to download and store HLS movies on users' devices while they have access to a fast, reliable network, and watch them later without a network connection.

AVAssetDownloadURLSession

This wwdc2016/504/ session talks about Offline HLS. It is about downloading and persisting assets using AVAssetDownloadURLSession, which is a subclass of URLSession, and here is used to manage AVAssetDownloadTasks. The APIs mentioned in this session are available after iOS10.

AVAggregateAssetDownloadTask

wwdc2017/504 session introduced AVAggregateAssetDownloadTask in iOS11.

An AVAssetDownloadTask used for downloading multiple AVMediaSelections for a single AVAsset, under the umbrella of a single download task.

Apple provides an example project for using AVFoundation to Play and Persist HTTP Live Streams. Demo doc. The demo project uses AVAggregateAssetDownloadTask

AVAssetDownloadStorageManager

/wwdc2017/504 also introduced a new API, AVAssetDownloadStorageManager, to manage the policy for automatic purging of downloaded AVAssets.

  • Expiration date
  • Priority (important, default)
// Get the singleton
let storageManager = AVAssetDownloadStorageManager.shared()
// Set the policy
let newPolicy = AVMutableAssetDownloadStorageManagementPolicy() 
newPolicy.expirationDate = myExpiryDate
newPolicy.priority = .important 
storageManager.setStorageManagementPolicy(newPolicy, forURL: myDownloadStorageURL)
RY_ Zheng
  • 3,041
  • 29
  • 36
0

About NSURLProtocol: As I understood, it make own requests, so your custom tags/fields/marks will be removed.

I've made it other way: redirect segments requests to some custom url scheme and just check the scheme in protocol's canInitWithRequest method.

This way it works just fine. (spent a week to figure whole hls-processing thing out...)

norlin
  • 1,205
  • 11
  • 24