Post

Exploring iOS HLS Cache Implementation Methods

How to achieve caching functionality while playing m3u8 streaming media files using AVPlayer

Exploring iOS HLS Cache Implementation Methods

ℹ️ℹ️ℹ️ The following content is translated by OpenAI.

Click here to view the original Chinese version. | 點此查看本文中文版


Exploring iOS HLS Cache Implementation Methods

How to achieve caching functionality while playing m3u8 streaming media files using AVPlayer

photo by [Mihis Alex](https://www.pexels.com/zh-tw/@mcraftpix?utm_content=attributionCopyText&utm_medium=referral&utm_source=pexels){:target="_blank"}

photo by Mihis Alex

[2023/03/12] Update

I have open-sourced my previous implementation, so friends in need can use it directly.

  • Customizable cache strategies, can use PINCache or others…
  • Externally, just call the make AVAsset factory with the URL, and the AVAsset will support caching.
  • Implemented data flow strategies using Combine.
  • Wrote some tests.

About

HTTP Live Streaming (HLS) is a streaming media network transport protocol based on HTTP proposed by Apple.

For music playback, in non-streaming situations, we use mp3 as the music file, and the time it takes to download the entire file depends on its size; HLS splits a file into multiple smaller files, allowing playback to start as soon as the first segment is received, without needing to download the entire file first!

The .m3u8 file records the bitrate, playback order, duration, and overall audio information of these segmented .ts files. It can also provide encryption protection, low-latency live streaming, etc.

Example of a .m3u8 file (aviciiwakemeup.m3u8):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:9.900411,
aviciiwakemeup–00001.ts
#EXTINF:9.900400,
aviciiwakemeup–00002.ts
#EXTINF:9.900411,
aviciiwakemeup–00003.ts
#EXTINF:9.900411,
.
.
.
#EXTINF:6.269389,
aviciiwakemeup-00028.ts
#EXT-X-ENDLIST

*EXT-X-ALLOW-CACHE has been deprecated in iOS≥ 8/Protocol Ver.7 and is no longer meaningful whether this line is present or not.

Goal

For a streaming media service, caching is extremely important; because each audio file can range from MB to several GB, if every replay requires fetching the file from the server again, it puts a heavy load on the server, and data costs are significant. Having a caching layer can save a lot of money for the service and prevent users from wasting bandwidth and time re-downloading; it’s a win-win mechanism (but remember to set limits/timely clean-up to avoid overwhelming users’ devices).

Issues

In the past, with non-streaming mp3/mp4 files, there wasn’t much to handle; you would just download the file to the device before playback, starting playback only after the download was complete. Regardless, you had to download the entire file before playback, so why not just use URLSession to download the file and then feed the local file path to AVPlayer for playback? Alternatively, you could use the proper method of implementing caching through AVAssetResourceLoaderDelegate in the delegate methods.

When it comes to streaming, the idea is straightforward: read the .m3u8 file, parse the information inside, and cache each .ts file. However, I found that the implementation was not as simple as I expected, and the complexity exceeded my imagination, which is why this article exists!

For playback, we still use AVPlayer from iOS AVFoundation directly, as there is no difference in handling streaming and non-streaming files.

Example:

1
2
3
let url:URL = URL(string:"https://zhgchg.li/aviciiwakemeup.m3u8")
var player: AVPlayer = AVPlayer(url: url)
player.play()

2021–01–05 Update:

We have reverted to using mp3 files, which allows us to directly implement using AVAssetResourceLoaderDelegate. For detailed implementation, refer to “AVPlayer Caching in Action.”

Implementation Solutions

Several solutions we can achieve for our goal and the issues encountered during implementation.

Solution 1. AVAssetResourceLoaderDelegate ❌

The first idea was to do it the same way as with mp3/mp4! Use AVAssetResourceLoaderDelegate to cache the .ts files in the delegate methods.

However, I’m sorry, this approach doesn’t work because we cannot intercept the download request information for the .ts files in the delegate. You can find this confirmed in this Q&A and the official documentation.

For implementation of AVAssetResourceLoaderDelegate, refer to “AVPlayer Caching in Action.”

Solution 2.1 URLProtocol Intercept Requests ❌

URLProtocol is a method I recently learned, which allows us to intercept all requests based on the URL Loading System (URLSession, API calls, image downloads, etc.) to modify requests and responses and return them as if nothing happened, stealthily. For more about URLProtocol, refer to this article.

Applying this method, we intended to intercept AVFoundation AVPlayer’s requests for .m3u8 and .ts files, returning cached data if available locally; otherwise, we would send a real request. This would also achieve our goal.

Again, I’m sorry, this approach doesn’t work either; AVFoundation AVPlayer’s requests are not part of the URL Loading System, so we cannot intercept them. *There is a claim that it works on the simulator but not on actual devices.

Solution 2.2 Force it into URLProtocol ❌

Based on Solution 2.1, a brute-force method is to change the request URL to a custom scheme (e.g., streetVoiceCache://). Since AVFoundation cannot handle this request, it will throw it out, allowing our URLProtocol to intercept it and perform our desired actions.

1
2
3
let url:URL = URL(string:"streetVoiceCache://zhgchg.li/aviciiwakemeup.m3u8?originSchme=https")
var player: AVPlayer = AVPlayer(url: url)
player.play()

URLProtocol will intercept streetVoiceCache://zhgchg.li/aviciiwakemeup.m3u8?originSchme=https, and we just need to revert it to the original URL, then send a URLSession request to fetch the data and perform caching ourselves; the requests for .ts files in the m3u8 will also be intercepted by URLProtocol, allowing us to cache them as well.

Everything seemed perfect, but when I excitedly built and ran the app, Apple slapped me with:

Error: 12881 “CoreMediaErrorDomain custom url not redirect”

It doesn’t accept the response data I provide for the .ts file requests; I can only use the urlProtocol:wasRedirectedTo method to redirect to the original HTTPS request for it to play normally. Even if I download the .ts files locally and redirect to that file:// file, it still doesn’t accept it. According to the official forum, the answer is that this cannot be done; .m3u8 must originate from HTTP/HTTPS (so even if you place the entire .m3u8 and all the segmented .ts files locally, you cannot use file:// to play them with AVPlayer), and .ts files cannot be provided with data via URLProtocol.

fxxk…

Solution 2.2–2 Same as Solution 2.2 but with AVAssetResourceLoaderDelegate ❌

The implementation is similar to Solution 2.2, feeding AVPlayer a custom scheme to enter AVAssetResourceLoaderDelegate; then we handle it ourselves.

Same result as 2.2:

Error: 12881 “CoreMediaErrorDomain custom url not redirect”

The official forum provides the same answer.

This can be used for decryption processing (refer to this article or this example), but it still cannot achieve caching functionality.

Solution 3. Reverse Proxy Server ⍻ (Feasible but not perfect)

This method is the most commonly suggested solution when looking for how to handle HLS caching; it involves setting up an HTTP server in the app to serve as a reverse proxy server.

The principle is simple: run an HTTP server on the app, say on port 8080, so the URL becomes http://127.0.0.1:8080/; then we can handle incoming requests and provide responses.

Applied to our case, we change the request URL to: http://127.0.0.1:8080/aviciiwakemeup.m3u8?origin=http://zhgchg.li/

In the HTTP server’s handler, we intercept *.m3u8 requests; when a request comes in, it enters our handler, and we can do whatever we want, controlling what data to respond with. The .ts files will also come in; here we can implement our desired caching mechanism.

To AVPlayer, it appears as a standard streaming audio file at http://*.m3u8, so there won’t be any issues.

For a complete implementation example, refer to:

Since I also referred to this example, I used GCDWebServer for the local HTTP server part, and there’s also the newer Telegraph available. ( CocoaHttpServer is not recommended due to lack of updates)

It looks good! But there’s a problem:

Our service is a music streaming service rather than a video playback platform, and many times users are switching music while the app is running in the background; will the local HTTP server still be active then?

GCDWebServer’s documentation states that it will automatically disconnect when entering the background and reconnect when returning to the foreground, but you can set the parameter GCDWebServerOption_AutomaticallySuspendInBackground:false to prevent this mechanism.

However, in practice, if there are no requests sent for a while, the server will still disconnect (and the status will be incorrect, still showing as isRunning); it feels like the system has killed it; after digging into the HTTP server approach, I found that the underlying implementation is based on sockets, and according to the official documentation on socket services, this limitation cannot be resolved; the system will suspend it when there are no new connections in the background.

*I found a convoluted method online… just send a long request or continuously send empty requests to ensure the server doesn’t get suspended by the system in the background.

All of the above pertains to the app’s background state; when in the foreground, the server is stable and won’t be suspended due to idleness, so there’s no issue!

That said, since it relies on other services, while testing in the development environment is fine, it’s also advisable to implement a rollback handling (AVPlayer.AVPlayerItemFailedToPlayToEndTimeErrorKey notification); otherwise, if the service goes down, users will be stuck.

So it's not perfect...

Solution 4. Use the HTTP Client’s own caching mechanism ❌

Our .m3u8/.ts file response headers provide Cache-Control, Age, eTag, and other HTTP client cache information; our website’s caching mechanism works perfectly in Chrome, and I also saw mentions of cache-control headers for caching in the official new specification document for Protocol Extension for Low-Latency HLS.

However, in practice, AVFoundation AVPlayer does not exhibit any HTTP client caching effects, so this approach doesn’t work! It’s just wishful thinking.

Solution 5. Do not use AVFoundation AVPlayer to play audio files ✔

Implement your own audio file parsing, caching, encoding, and playback functionality.

This is too hardcore, requiring deep technical skills and a lot of time; I haven’t researched it.

Here’s an open-source player for reference: FreeStreamer, and if you choose this solution, it would be better to stand on the shoulders of giants and use a third-party library directly.

Solution 5–1. Do not use HLS

Same as Solution 5, this is too hardcore, requiring deep technical skills and a lot of time; I haven’t researched it.

Solution 6. Convert .ts segmented files to .mp3/.mp4 files ✔

I haven’t researched this, but it is indeed feasible; however, it seems complicated to handle the downloaded .ts files, converting each to .mp3 or .mp4 files and playing them in order, or compressing them into a single file, which sounds challenging.

For those interested, refer to this article.

Solution 7. Download the complete file before playback ⍻

This method cannot be accurately called caching while playing; it actually downloads the entire audio file content before starting playback. If it’s .m3u8, as mentioned in Solution 2.2, it cannot be directly downloaded and played locally.

To implement this, you would need to use the iOS ≥ 10 API AVAssetDownloadTask.makeAssetDownloadTask, which will package the .m3u8 into a .movpkg file stored locally for user playback.

This is more akin to offline playback rather than caching functionality.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Additionally, users can view and manage downloaded audio files from the "Settings" -> "General" -> "iPhone Storage" -> APP.

![Downloaded Videos Section Below](/assets/d796bf8e661e/1*_YNIdy8NRkhVdeDTNvXzxA.jpeg)

Downloaded Videos Section Below

**For detailed implementation, please refer to this example:**

[![](https://opengraph.githubassets.com/a2ceae202336428494e5cd51b78cfbba3d139c135eaf232b4d2dffd2a7673eba/zhonglaoban/HLS-Stream)](https://github.com/zhonglaoban/HLS-Stream){:target="_blank"}

### Conclusion

The exploration process took almost a whole week, going around in circles and nearly driving me crazy; currently, there is no reliable and easy-to-deploy method.

I will update if I have new ideas!
#### References
- [iOS Audio Playback (Nine): Streaming and Caching](http://msching.github.io/blog/2016/05/24/audio-in-ios-9/){:target="_blank"}
- [StyleShare/HLSCachingReverseProxyServer](https://github.com/StyleShare/HLSCachingReverseProxyServer){:target="_blank"}

If you have any questions or suggestions, feel free to [contact me](https://www.zhgchg.li/contact){:target="_blank"}.



This article was first published on Medium ➡️ Click Here

Automatically converted and synchronized using ZMediumToMarkdown and Medium-to-jekyll-starter.

Improve this page on Github.

Buy me a beer

5,231 Total Views
Last Statistics Date: 2025-03-25 | 4,950 Views on Medium.
This post is licensed under CC BY 4.0 by the author.