文章

iOS HLS Cache 实作全攻略|AVPlayer 串流边播边快取技术解析

针对 iOS AVPlayer 播放 HLS m3u8 串流时无法拦截快取的痛点,深入分析多种快取方案,从 AVAssetResourceLoaderDelegate 到 Reverse Proxy Server,揭示技术瓶颈与可行解法,助你优化串流体验并有效节省伺服器与流量成本。

iOS HLS Cache 实作全攻略|AVPlayer 串流边播边快取技术解析

基于 SEO 考量,本文标题与描述经 AI 调整,原始版本请参考内文。

文章目录


iOS HLS Cache 实践方法探究之旅

使用 AVPlayer 播放 m3u8 串流影音档时如何做到边播放边 Cache 的功能

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

我将之前的实作开源了,有需求的朋友可直接使用。

  • 客制化 Cache 策略,可以用 PINCache or 其他…

  • 外部只需呼叫 make AVAsset 工厂,带入 URL,则 AVAsset 就能支援 Caching

  • 使用 Combine 实现 Data Flow 策略

  • 写了一些测试

关于

HTTP Live Streaming (简称HLS) 是苹果提出基于HTTP的串流媒体网络传输协议。

以播放音乐来说,非串流情况下我们使用 mp3 作为音乐档,这个档案有多大就要花多久时间全部下载下来才能播放;而 HLS 就是把一个档案分割成多个小档案,读到哪播到哪,所以拿到第一个分割区块就能开始播放,不用整个都下载完!

.m3u8 档就是纪录这些分割的 .ts 小档案的码率、播放顺序、时间 还有整个音讯的资讯,另外也可以做加解密保护、低延迟直播…等等

.m3u8 档范例(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 已在 iOS≥ 8/Protocol Ver.7 deprecated ,有没有这行都没有用意义了。

目标

对于一个影音串流服务, Cache 非常之重要 ;因为每个音讯档案小则 MB 大则几 GB ,如果每次重播都要再从伺服器拉一次档案,对 Server 的 Loading 来说非常吃力,而且流量都是 \(\) ,如果有个 Cache 层能为服务节省许多金钱,对使用者来说也不用浪费网路、浪费时间重新下载;是一个双赢的机制 (但要记得设定上限/定时清除,避免把使用者的设备塞爆)。

问题

以往非串流时 mp3/mp4 没什么好处理的,就是在播放前先下载到设备上,下载完成才开始播放;反正不管怎样都要载完才能播,那不如我们自己用 URLSession 下载完档案后再喂 file:// 下载在本地的档案路径给 AVPlayer 做播放即可;或正规方式,使用 AVAssetResourceLoaderDelegate 在 Delegate 方法中对下载的资料进行 Cache 缓存。

遇到串流想法其实也很直白,就是先读 .m3u8 档,然后在解析里面的资讯,对每个 .ts 档做 Cache 即可;但实作发现事情没有这么简单,处理难度超乎我的想像,所以才会有此篇文章!

播放部分我们一样直接使用 iOS AVFoundation 的 AVPlayer,在操作上串流/非串流档案没有差异。

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 更新:

我们退而求其次退回去使用 mp3 档,这样就能直接使用 AVAssetResourceLoaderDelegate 进行实作,详细实作可参考「 AVPlayer 边播边 Cache 实战 」。

实践方案

针对我们的目标能达成的几个方案及实践时遇到的问题。

方案 1. AVAssetResourceLoaderDelegate ❌

第一个想法就是,那我们就照 mp3/mp4 时的做法就好啦!一样用 AVAssetResourceLoaderDelegate 在 Delegate 方法中缓存 .ts 档案。

不过很抱歉,此路不通,因为无法在 Delegate 中拦截到 .ts 档案的下载请求资讯,可以在这则 问答官方文件 上确切此事。

AVAssetResourceLoaderDelegate 实作可参考「 AVPlayer 边播边 Cache 实战 」。

方案 2.1 URLProtocol 拦截请求 ❌

URLProtocol 也是最近才学到的方法,所有基于 URL Loading System 的请求 (URLSession、Call API、下载图片…) 都可以被我们拦截下来修改 Request、Response 然后再返回,一切就像没发生一样,偷偷来;关于 URLProtocol 可以参考 此篇文章

应用此方法,我们打算拦截 AVFoundation AVPlayer 在要求 .m3u8.ts 的请求时,拦截下来然后如果本地有 Cache 就直接返回 Cache Data,没有则再真的再发 Request 出去;这样也能达到我们的目标。

一样,很抱歉,此路也不通;因为 AVFoundation AVPlayer 的请求不是在 URL Loading System 上,我们无从拦截。 *有一说是 模拟器上可以但实机上不行

方案 2.2 暴力让他能进 URLProtocol ❌

根据 方案 2.1 脑洞大开的暴力法,如果我把请求网址换成一个自订的 Scheme (EX: streetVoiceCache://),因 AVFoundation 无法处理这个请求,所以会丢出来,这样我们的 URLProtocol 就能拦截到,做我们想做的事。

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

URLProtocol 会拦截到 streetVoiceCache://zhgchg.li/aviciiwakemeup.m3u8?originSchme=https ,这时我们只要帮他还原成原来的网址,然后发个 URLSession 去要资料就能在这边自己做 Cache;m3u8 中的 .ts 档案请求一样也会被 URLProtocol 拦截到,一样我们能在这自己做 Cache。

一切看似都那么完美,但当我兴高采烈的 Build-Run 完 APP 后,苹果直接搧了我一巴掌:

Error: 12881 “CoreMediaErrorDomain custom url not redirect”

他不吃我给 .ts 档案 Request 的 Response Data,我只能用 urlProtocol:wasRedirectedTo 这个方法 redirectTo 原始 Https 请求才能正常播放,即使我把 .ts 档案下载到本地然后 redirectTo 那个 file:// 档案;他也不接受,查 官方论坛 得到答案就是不能这样做; .m3u8 只能是来源于 Http/Https (所以即使你把整个 .m3u8 还有所有分割档 .ts 都放在本地,有无法使用 file:// 给 AVPlayer播放),另外 .ts 也不能使用 URLProtocol 自行给予 Data。

fxxk…

方案 2.2–2 同方案 2.2 但是搭配 方案 1 AVAssetResourceLoaderDelegate 来实现 ❌

实作方式如方案 2.2 ,喂给 AVPlayer 自订的 Scheme 让他进 AVAssetResourceLoaderDelegate;然后我们在自己处理。

同 2.2 结果:

Error: 12881 “CoreMediaErrorDomain custom url not redirect”

官方论坛 同样的回答。

可以拿来做解密处理(可以参考 此篇文章此范例 )但还是无法实现 Cache 功能。

方案 3. Reverse Proxy Server ⍻ (可行,但非完美)

这个方法是在找如何处理 HLS Cache 时,最多人给的答案;就是在 APP 上起一个 HTTP Server 做 Reverse Proxy Server 服务。

原理也很简单,APP 上 On 一个 HTTP Server 假设是 8080 Port,网址就会是 http://127.0.0.1:8080/ ;然后我们可以对连进来的 Request 做处理,给出 Response。

套用到我们的案例就是,把请求网址换成: http://127.0.0.1:8080/aviciiwakemeup.m3u8?origin=http://zhgchg.li/

在 HTTP Server 的 Handler 上对 *.m3u8 拦截处理,这时有 Request 进来就会进到我们的 Handler 中,看我们想干嘛就干嘛,想 Response 什么 Data 都是我们自己控制, .ts 档同样会进来;这边就可以做我们想做的 Cache 机制。

对 AVPlayer 来说就是个 http://.m3u8 的标准串流音讯档,所以不会有任何问题。

完整实作范例可参考:

因为我也是参考此范例做的,所以 Local HTTP Server 的部分我也是使用 GCDWebServer ,另外还有更新的 Telegraph 可以使用。( CocoaHttpServer 太久没更新就不推荐用了)

看起来不错!但有个问题:

我们的服务是音乐串流而非影音播放平台,音乐串流很多时候使用者都是在背景执行音乐切换的;这时候 Local HTTP Server 还会在??

GCDWebServer 的说明是当进入背景时会自动断线、回前景自动恢复,但可以透过设置参数 GCDWebServerOption_AutomaticallySuspendInBackground:false 不让他有这个机制。

但是实测如果一段时间没有发送请求 Server 还是会断线 (且状态会是错的,还是 isRunning) 感觉就是被系统砍了;深掘了 HTTP Server 的做法 后发现底层都是基于 socket,查了 官方对 socket 服务的文件 后,此缺陷是无法解决的,本来在背景下没有新的连接时就会被系统暂停。

*网路上有找到很绕的方法…就是发个长请求、或不断发空的请求确保 Server 在背景不会被系统暂停掉。

以上都是针对 APP 在背景的状况,在前景时 Server 很稳,也不会因为闲置被暂停,没这问题!

是说毕竟是依赖在其他服务上,开发环境测试没问题,实际应用也建议要接个 rollback 处理(AVPlayer.AVPlayerItemFailedToPlayToEndTimeErrorKey 通知);否则有个万一服务挂掉,使用者会卡死。

所以说不完美啊…

方案 4. 使用 HTTP Client 本身的 caching 机制 ❌

我们的 .m3u8/.ts 档的 Response Headers 都有给予 Cache-ControlAgeeTag … 这些 HTTP Client Cache 资讯;我们的网站 Cache 机制在 Chrome 上使用也完全没问题,另外也在官方新的针对 Protocol Extension for Low-Latency HLS (低延迟HLS) 初步规格文件中提到 Cache 的地方也看到可以设定 cache-control headers 来做缓存。

但实际 AVFoundation AVPlayer 并没有任何 HTTP Client Caching 效果,此路也不通!单纯痴人说梦。

方案 5. 不使用 AVFoundation AVPlayer 播放音讯档 ✔

自己实现音讯档解析、缓存、编码、播放功能。

太硬核了,需要很深的技术能力及大量时间;没研究。

附上一个网路开源播放器做参考: FreeStreamer ,真要选择此方案不如站在巨人的肩膀上,直接用第三方套件了。

方案 5–1. 不使用 HLS

同方案 5 , 太硬核了,需要很深的技术能力及大量时间;没研究。

方案 6. 将 .ts 分割档转成 .mp3/.mp4 档案 ✔

没研究,但的确可行;不过想起来就觉得复杂,要处理已下载的 .ts 档案,个别转成 .mp3 或 .mp4 档案然后照顺序播放、或是压缩成一个档案什么的,想起来就不太好做。

有兴趣可参考 此篇文章

方案 7. 下载完整档案后再播放 ⍻

这个方法不能确切叫边播边 Cache,实际是载下整个音讯档案的内容,然后才开始播放;如果是 .m3u8 如同方案 2.2 提到的,不能直接载下来放在本地播放。

要实作的话要用到 iOS ≥ 10 的 API AVAssetDownloadTask.makeAssetDownloadTask ,实际会将 . m3u8 打包成 .movpkg 放在本地,供使用者播放。

这边比较像是做离线播放而非做 Cache 的功能。

另外使用者也能从「设定」->「一般」->「iPhone 储存空间」-> APP 中查看、管理已下载打包的音讯档案。

下方 已下载的影片 部分

下方 已下载的影片 部分

详细实作可参考此范例:

结语

以上的探索路程大概花了快一整周,绕来绕去、快要丧心病狂了;目前还没有一个可靠的、容易部署的方法。

如果有新的想法再来更新!

参考资料

有任何问题及指教欢迎 与我联络


🍺 Buy me a beer on PayPal

👉👉👉 Follow Me On Medium! (1,053+ Followers) 👈👈👈

本文首次发表于 Medium (点击查看原始版本),由 ZMediumToMarkdown 提供自动转换与同步技术。

Improve this page on Github.

本文由作者以 CC BY 4.0 授权。