記事

iOS HLS Cache|AVPlayerでm3u8を再生しながら効率的にキャッシュする方法

iOSでAVPlayerを使いm3u8ストリーミング再生中にキャッシュを実装したい開発者向けに、再生と同時にデータを保存し通信負荷を軽減する具体的手法を解説。スムーズな視聴体験と高速読み込みを両立します。

iOS HLS Cache|AVPlayerでm3u8を再生しながら効率的にキャッシュする方法

本記事は AI による翻訳をもとに作成されています。表現が不自然な箇所がありましたら、ぜひコメントでお知らせください。

記事一覧


iOS HLS Cache 実践方法探求の旅

AVPlayer で m3u8 ストリーム再生中に再生しながらキャッシュする方法

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] アップデート

私は以前の実装をオープンソース化しましたので、必要な方は直接ご利用ください。

  • カスタマイズしたキャッシュ戦略は、PINCacheやその他の方法を使用できます…

  • 外部は make AVAsset ファクトリーを呼び出し、URL を渡すだけで、AVAsset がキャッシュをサポートします。

  • Combine を使ったデータフロー戦略の実装

  • いくつかテストを書きました

について

HTTP Live Streaming (略してHLS) は、Appleが提唱した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 で非推奨 となっており、あっても意味がありません。

目標

動画ストリーミングサービスにおいて、キャッシュは非常に重要です。音声ファイルは小さいものでも数MB、大きいものでは数GBにもなります。毎回再生するたびにサーバーからファイルを再取得すると、サーバーの負荷が非常に高くなり、通信量も莫大なコストがかかります。キャッシュ層があれば、サービスのコストを大幅に削減でき、ユーザーも無駄な通信や再ダウンロードの時間を節約できます。これは双方にとってメリットのある仕組みですが、ユーザーのデバイスが容量不足にならないよう、容量上限の設定や定期的なクリアが必要です。

問題

従来のストリーミングでない場合、mp3/mp4 は特に処理することはなく、再生前にデバイスにダウンロードし、ダウンロード完了後に再生を開始していました。いずれにせよ最後までダウンロードしなければ再生できないので、自分で URLSession を使ってファイルをダウンロードし、ローカルに保存したファイルの file:// パスを AVPlayer に渡して再生すればよいです。または正式な方法として、AVAssetResourceLoaderDelegate の Delegate メソッド内でダウンロードしたデータをキャッシュする方法もあります。

ストリーミングの考え方は実にシンプルで、まず .m3u8 ファイルを読み込み、その中の情報を解析して、各 .ts ファイルをキャッシュすれば良いだけです。しかし、実装してみるとそんなに簡単ではなく、想像以上に難しかったため、本記事を書くことになりました!

再生部分は引き続き iOS の AVFoundation の AVPlayer を直接使用し、操作上はストリーミングファイルと非ストリーミングファイルで差異はありません。

例:

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 辺り再生とキャッシュの実践 」を参照してください。

方案 2.1 URLProtocol リクエストのインターセプト ❌

URLProtocol は最近学んだ方法で、URL Loading System に基づくすべてのリクエスト(URLSession、APIコール、画像ダウンロードなど)を私たちが傍受して、Request や Response を修正し、何事もなかったかのように戻すことができます。こっそり行う感じです。URLProtocol についてはこちらの記事を参照してください。

この方法を適用して、AVFoundation の AVPlayer が .m3u8.ts のリクエストを行う際にそれを intercept し、ローカルにキャッシュがあればキャッシュデータを直接返し、なければ実際にリクエストを送るようにします。これにより目的を達成できます。

同様に申し訳ありませんが、この方法も通用しません。なぜなら、AVFoundationのAVPlayerのリクエストは URL Loading System 上で行われておらず、インターセプトできないからです。
シミュレーターでは可能だが実機ではできないと言われています

方案 2.2 URLProtocol に無理やり通す方法 ❌

方案 2.1 の奇抜な方法によると、リクエストの URL をカスタムスキーム(例: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 をインターセプトします。このとき、元のURLに復元してから URLSession でデータを取得すれば、ここでキャッシュ処理が可能です。m3u8 内の .ts ファイルのリクエストも同様に URLProtocol によってインターセプトされ、同じくキャッシュができます。

すべてが完璧に見えましたが、ワクワクしながらアプリをビルドして実行したところ、Appleに思い切り一撃を食らいました:

Error: 12881 “CoreMediaErrorDomain カスタムURLがリダイレクトされていません”

彼は私が与えた .ts ファイルのリクエストのレスポンスデータを受け付けません。urlProtocol:wasRedirectedTo メソッドで元の HTTPS リクエストにリダイレクトしないと正常に再生できません。たとえ .ts ファイルをローカルにダウンロードしてからその file:// ファイルにリダイレクトしても、受け付けません。公式フォーラム(リンク)での回答は「そういう使い方はできない」ということです。.m3u8 は HTTP/Https からのものでなければならず(つまり、.m3u8 とすべての分割ファイル .ts をローカルに置いても、file:// を使って AVPlayer で再生することはできません)、さらに .ts ファイルも URLProtocol で独自にデータを渡すことはできません。

fxxk…

方案 2.2–2 同じく方案 2.2 ですが、方案 1 の AVAssetResourceLoaderDelegate を組み合わせて実装 ❌

実装方法は方案 2.2 のように、AVPlayer にカスタムスキームを渡して AVAssetResourceLoaderDelegate に入らせ、そこで自分で処理します。

2.2 と同じ結果:

Error: 12881 “CoreMediaErrorDomain カスタムURLがリダイレクトされていません”

公式フォーラム 同じ回答です。

復号処理に使えます(参考:こちらの記事 または このサンプル )が、キャッシュ機能は実現できません。

方案 3. リバースプロキシサーバー ⍻(可能だが完璧ではない)

この方法は、HLSキャッシュの処理方法を探す際に最も多く挙げられる回答であり、APP上でHTTPサーバーを立ててリバースプロキシサーバーとして動作させるというものです。

原理は非常に簡単で、APP内で HTTP サーバーを起動し、例えば 8080 ポートを使うと、URL は http://127.0.0.1:8080/ になります。そこに来るリクエストを処理して、レスポンスを返すことができます。

私たちのケースに適用すると、リクエストURLを次のように変更します: http://127.0.0.1:8080/aviciiwakemeup.m3u8?origin=http://zhgchg.li/

HTTPサーバーのハンドラーで *.m3u8 をインターセプトして処理します。このときリクエストが来るとハンドラーに入り、何をするかは自由に決められ、どんなデータをレスポンスするかも自分で制御できます。.ts ファイルも同様に入ってくるので、ここでキャッシュ機構を実装できます。

AVPlayer にとっては標準的な http://.m3u8 のストリーミング音声ファイルなので、問題はありません。

完全な実装例はこちらを参照してください:

私もこのサンプルを参考にしているため、Local HTTP Server の部分は GCDWebServer を使用しています。その他に、より新しい Telegraph も使えます。(CocoaHttpServer は長期間更新されていないため、おすすめしません)

良さそうです!しかし、ひとつ問題があります:

私たちのサービスは音楽ストリーミングであり、映像再生プラットフォームではありません。音楽ストリーミングでは多くの場合、ユーザーがバックグラウンドで音楽を切り替えています;その時にローカルHTTPサーバーはまだ動作していますか?

GCDWebServer の説明では、バックグラウンドに入ると自動的に切断し、フォアグラウンドに戻ると自動的に復帰しますが、パラメータ GCDWebServerOption_AutomaticallySuspendInBackground:false を設定することで、この機能を無効にできます。

しかし、実際にテストすると一定時間リクエストを送らないとサーバーは切断され(状態は誤っていて、まだ isRunning のまま)、システムに強制終了されているように感じます。HTTPサーバーの方法 を詳しく調べたところ、内部はすべてソケットベースであることが分かり、公式のソケットサービスのドキュメント を確認した結果、この問題は解決できず、バックグラウンドで新しい接続がない場合はシステムによって停止される仕様でした。

ネット上で見つけた複雑な方法…長いリクエストを送るか、空のリクエストを繰り返し送って、サーバーがバックグラウンドでシステムに停止されないようにする。

以上はすべてアプリがバックグラウンドにある場合の話であり、フォアグラウンドではサーバーは非常に安定しており、アイドル状態で一時停止されることもなく、この問題はありません!

結局は他のサービスに依存しているため、開発環境で問題なくても、実際の運用ではロールバック処理(AVPlayer.AVPlayerItemFailedToPlayToEndTimeErrorKey 通知)を実装することをおすすめします。さもないと、万が一サービスが落ちた場合にユーザーが固まってしまいます。

だから完璧ではないんです…

方案 4. HTTP クライアント自体のキャッシュ機能を使用 ❌

私たちの .m3u8/.ts ファイルのレスポンスヘッダーには、Cache-ControlAgeeTag などの HTTP クライアントキャッシュ情報が含まれています。私たちのウェブサイトのキャッシュ機構は Chrome 上で問題なく動作しています。また、公式の新しい Protocol Extension for Low-Latency HLS (低遅延HLS) 初期仕様書でも、キャッシュに関して cache-control ヘッダーを設定してキャッシュを行うことが可能と記載されています。

しかし実際には AVFoundation の AVPlayer には HTTP クライアントのキャッシュ機能はなく、この方法は通用しません。単なる夢物語です。

方案 5. AVFoundation AVPlayer を使わずに音声ファイルを再生 ✔

音声ファイルの解析、キャッシュ、エンコード、再生機能を自分で実装する。

かなりハードコアで、高度な技術力と多くの時間が必要;研究していません。

参考用のオープンソースプレーヤーを紹介します: FreeStreamer 。もしこの方法を選ぶなら、巨人の肩に乗って、直接サードパーティのライブラリを使うほうが良いでしょう。

方案 5–1. HLS を使わない場合

同じ方法5、非常にハードで、高度な技術力と多大な時間が必要;研究していません。

方案 6. .ts 分割ファイルを .mp3/.mp4 ファイルに変換する ✔

研究はしていませんが、確かに可能です。ただ、ダウンロード済みの .ts ファイルを個別に .mp3 や .mp4 に変換して順番に再生したり、一つのファイルに圧縮したりするのは、やや複雑で難しそうです。

興味があれば、こちらの記事 をご参照ください。

方案 7. ファイルを完全にダウンロードしてから再生 ⍻

この方法は正確にはストリーミング再生と同時にキャッシュすることではなく、音声ファイル全体をダウンロードしてから再生を開始します。.m3u8の場合は、2.2の方法で述べたように、直接ダウンロードしてローカルで再生することはできません。

実装する場合は iOS 10 以上の API AVAssetDownloadTask.makeAssetDownloadTask を使用し、実際には .m3u8 をパッケージ化して .movpkg としてローカルに保存し、ユーザーが再生できるようにします。

こちらはキャッシュ機能というより、オフライン再生に近いです。

また、ユーザーは「設定」->「一般」->「iPhoneストレージ」-> アプリ内で、ダウンロード済みのパッケージ化された音声ファイルを確認・管理できます。

下方 已下載的影片 部分

下記は ダウンロード済みの動画 部分です

詳細な実装は以下のサンプルを参照してください:

結論

以上の探索にはほぼ一週間かかり、あちこち試行錯誤してほとんど気が狂いそうでした。現時点では信頼できて簡単に導入できる方法はまだありません。

新しいアイデアがあればまた更新します!

参考資料

ご質問やご意見がございましたら、こちらからご連絡ください

Post Mediumから変換 by ZMediumToMarkdown.


🍺 Buy me a beer on PayPal

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

本記事は Medium にて初公開されました(こちらからオリジナル版を確認)。ZMediumToMarkdown による自動変換・同期技術を使用しています。

Improve this page on Github.

本記事は著者により CC BY 4.0 に基づき公開されています。