プロダクト ニュース

メディア再生の強化: Media3 の PreloadManager の詳細 - パート 2

所要時間: 9 分
Mayuri Khinvasara Khabya
デベロッパー リレーション エンジニア

Media3 を使用したメディアのプリロードに関する 3 部構成のシリーズの第 2 回です。このシリーズは、Android アプリで応答性が高く、レイテンシの低いメディア エクスペリエンスを構築するプロセスを説明することを目的としています。

  • パート 1: Media3 でのプリローディングの概要では、基本的な事項について説明しました。シンプルなプレイリスト用の PreloadConfiguration と、動的なユーザー インターフェース用のより強力な DefaultPreloadManager の違いについて説明しました。基本的な API ライフサイクル(add() でメディアを追加する、getMediaSource() で準備済みの MediaSource を取得する、setCurrentPlayingIndex() と invalidate() で優先度を管理する、remove() と release() でリソースを解放する)を実装する方法を学びました。
  • パート 2(この投稿): このブログでは、DefaultPreloadManager の高度な機能について説明します。PreloadManagerListener を使用して分析情報を取得する方法、ExoPlayer とコア コンポーネントを共有するなどのプロダクション レディなベスト プラクティスを実装する方法、スライディング ウィンドウ パターンをマスターしてメモリを効果的に管理する方法について説明します。
  • パート 3: このシリーズの最終パートでは、PreloadManager を永続ディスク キャッシュに統合する方法について詳しく説明します。これにより、リソース管理でデータ消費量を削減し、シームレスなエクスペリエンスを提供できます。

Media3 でのプリロードを初めて使用する場合は、先にパート 1を読むことを強くおすすめします。基本を理解したうえで、メディア再生の実装をレベルアップする方法を見ていきましょう。

リスニング: PreloadManagerListener で分析情報を取得する

アプリ デベロッパーは、本番環境で機能をリリースする際に、その背後にある分析情報を把握して取得したいと考えています。プリロード戦略が実際の環境で有効であることを確認するにはどうすればよいですか?この質問に答えるには、成功率、失敗、パフォーマンスに関するデータが必要です。このデータを収集する主なメカニズムは PreloadManagerListener インターフェースです。

PreloadManagerListener は、プリロードのプロセスとステータスに関する重要な分析情報を提供する 2 つの重要なコールバックを提供します。

  • onCompleted(MediaItem mediaItem): このコールバックは、TargetPreloadStatusControl で定義されているプリロード リクエストが正常に完了したときに呼び出されます。
  • onError(PreloadException error): このコールバックは、デバッグとモニタリングに役立ちます。プリロードが失敗したときに呼び出され、関連する例外が提供されます。

次のコード例に示すように、1 回のメソッド呼び出しでリスナーを登録できます。

  val preloadManagerListener = object : PreloadManagerListener {
    override fun onCompleted(mediaItem: MediaItem) {
        // Log success for analytics. 
        Log.d("PreloadAnalytics", "Preload completed for $mediaItem")
    }

    override fun onError( preloadError: PreloadException) {
        // Log the specific error for debugging and monitoring.
        Log.e("PreloadAnalytics", "Preload error ", preloadError)
    }
}

preloadManager.addListener(preloadManagerListener)

リスナーから分析情報を抽出する

これらのリスナー コールバックは、分析パイプラインにフックできます。これらのイベントを分析エンジンに転送することで、次のような重要な質問に答えることができます。

  • プリロードの成功率はどのくらいですか?(onCompleted イベントの数とプリロード試行の合計数の比率)
  • エラー率が最も高い CDN または動画形式はどれですか?(onError から例外を解析することで)
  • プリロードのエラー率はどれくらいですか?(onError イベントの数とプリロード試行回数の比率)

このデータから、プリローディング戦略に関する定量的なフィードバックを得て、A/B テストを実施し、データに基づいたユーザー エクスペリエンスの改善を行うことができます。このデータは、プリロードの期間やプリロードする動画の数、割り当てるバッファをインテリジェントに微調整するうえで役立ちます。

デバッグを超えて: onError を使用して UI を正常にフォールバックする

プリロードの失敗は、ユーザーにバッファリング イベントが発生する可能性が高いことを示す重要な指標です。onError コールバックを使用すると、リアクティブに対応できます。エラーを記録するだけでなく、UI を適応させることができます。たとえば、次の動画のプリロードに失敗した場合、アプリは次のスワイプで自動再生を無効にし、ユーザーがタップして再生を開始するように要求できます。

また、PreloadException 型を検査することで、よりインテリジェントな再試行戦略を定義できます。アプリは、エラー メッセージまたは HTTP ステータス コードに基づいて、失敗したソースをマネージャーから直ちに削除することを選択できます。読み込みの問題がユーザー エクスペリエンスに影響しないように、UI ストリームからアイテムを削除する必要があります。また、HttpDataSourceException などの PreloadException からより詳細なデータを取得して、エラーをさらに詳しく調べることができます。詳しくは、ExoPlayer のトラブルシューティングをご覧ください。

バディシステム: ExoPlayer とのコンポーネントの共有が必要な理由

DefaultPreloadManager と ExoPlayer は連携して動作するように設計されています。安定性と効率性を確保するため、複数のコアコンポーネントを共有する必要があります。別々のコンポーネントが連携せずに動作する場合、プリロードされたトラックが正しいプレーヤーで再生されるようにする必要があるため、プレーヤーのプリロードされたトラックのスレッドの安全性とユーザビリティに影響を与える可能性があります。個別のコンポーネントがネットワーク帯域幅やメモリなどの限られたリソースを競合し、パフォーマンスの低下につながる可能性もあります。ライフサイクルの重要な部分は、適切な破棄を処理することです。推奨される破棄順序は、まず PreloadManager をリリースし、次に ExoPlayer をリリースすることです。

DefaultPreloadManager.Builder は、この共有を容易にするように設計されており、PreloadManager とリンクされたプレーヤー インスタンスの両方をインスタンス化する API を備えています。BandwidthMeter、LoadControl、TrackSelector、Looper などのコンポーネントを共有する必要がある理由を見てみましょう。これらのコンポーネントが ExoPlayer Playback とどのように連携するかについては、視覚的な表現をご覧ください。

preloadManager2.png

共有 BandwidthMeter による帯域幅の競合の防止

BandwidthMeter は、過去の転送速度に基づいて利用可能なネットワーク帯域幅の推定値を提供します。PreloadManager とプレーヤーが別々のインスタンスを使用している場合、互いのネットワーク アクティビティを認識できないため、失敗シナリオにつながる可能性があります。たとえば、ユーザーが動画を視聴しているときにネットワーク接続が低下し、プリロードされた MediaSource が同時に今後の動画の積極的なダウンロードを開始するシナリオを考えてみましょう。MediaSource のプリロード アクティビティがアクティブなプレーヤーに必要な帯域幅を消費し、現在の動画が停止します。再生中の停止は、ユーザー エクスペリエンスの重大な失敗です。

1 つの BandwidthMeter を共有することで、TrackSelector は、プリロードまたは再生中に、現在のネットワーク状況とバッファの状態に基づいて、最高品質のトラックを選択できます。これにより、アクティブな再生セッションを保護し、スムーズなエクスペリエンスを確保するためのインテリジェントな判断が可能になります。

preloadManagerBuilder.setBandwidthMeter(customBandwidthMeter)

ExoPlayer の共有 LoadControl、TrackSelector、Renderer コンポーネントとの整合性を確保する

  • LoadControl: このコンポーネントは、再生を開始する前にバッファリングするデータの量や、追加データの読み込みを開始または停止するタイミングなど、バッファリング ポリシーを決定します。LoadControl を共有することで、プリロードされたメディアとアクティブに再生中のメディアの両方で、プレーヤーと PreloadManager のメモリ使用量が単一の調整されたバッファリング戦略によって制御され、リソース競合を防ぐことができます。一貫性を確保するには、プリロードするアイテムの数と期間を調整して、バッファサイズを適切に割り当てる必要があります。競合が発生した場合、プレーヤーは画面に表示されている現在のアイテムの再生を優先します。共有 LoadControl を使用すると、プリロード用に割り当てられたターゲット バッファ バイトが上限に達していない限り、プリロード マネージャーはプリロードを続行します。再生用の読み込みが完了するまで待機しません。

注: Media3(1.8)の最新バージョンで LoadControl を共有すると、その Allocator を PreloadManager およびプレーヤーと正しく共有できます。LoadControl を使用してプリロードを効果的に制御する機能は、今後の Media3 1.9 リリースで利用可能になる予定です。

preloadManagerBuilder.setLoadControl(customLoadControl)

  • TrackSelector: どのトラック(特定の解像度の動画、特定の言語の音声など)を読み込んで再生するかを選択するコンポーネントです。共有により、プリロード時に選択されたトラックが、プレーヤーが使用するトラックと同じになります。これにより、480p の動画トラックがプリロードされた後、再生時にプレーヤーがすぐにそれを破棄して 720p のトラックを取得するという無駄なシナリオを回避できます。プリロード マネージャーは、プレーヤーと 同じインスタンスの TrackSelector を共有すべきではありません。代わりに、異なる TrackSelector インスタンス(ただし同じ実装)を使用する必要があります。そのため、DefaultPreloadManager.Builder で TrackSelector ではなく TrackSelectorFactory を設定します。

preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)

  • レンダラ: このコンポーネントは、完全なレンダラを作成せずにプレーヤーの機能を理解する役割を担います。このブループリントをチェックして、最終的なプレーヤーがサポートする動画、音声、テキストの形式を確認します。これにより、互換性のあるメディア トラックのみをインテリジェントに選択してダウンロードし、プレーヤーが実際に再生できないコンテンツに帯域幅を無駄にすることがなくなります。

preloadManagerBuilder.setRenderersFactory(customRenderersFactory)

ExoPlayer コンポーネントの詳細をご確認ください。

黄金律: すべてを支配する共通の再生ルーパー

ExoPlayer インスタンスにアクセスできるスレッドは、プレーヤーの作成時に Looper を渡すことで明示的に指定できます。プレーヤーにアクセスする必要があるスレッドの Looper は、Player.getApplicationLooper を使用してクエリできます。プレーヤーと PreloadManager の間で共有 Looper を維持することで、これらの共有メディア オブジェクトに対するすべてのオペレーションが単一スレッドのメッセージ キューにシリアル化されることが保証されます。これにより、同時実行バグを減らすことができます。

読み込むかプリロードするメディアソースを含む PreloadManager とプレーヤーの間のインタラクションはすべて、同じ再生スレッドで行う必要があります。スレッドの安全性を確保するには Looper を共有する必要があるため、PreloadManager とプレーヤーの間で PlaybackLooper を共有する必要があります。

PreloadManager は、ステートフルな MediaSource オブジェクトをバックグラウンドで準備します。UI コードで player.setMediaSource(mediaSource) を呼び出すと、この複雑なステートフル オブジェクトがプリロードの MediaSource からプレーヤーに引き渡されます。このシナリオでは、PreloadMediaSource 全体がマネージャーからプレーヤーに移動します。これらのやり取りとハンドオフはすべて、同じ PlaybackLooper で行われる必要があります。

PreloadManager と ExoPlayer が異なるスレッドで動作している場合、競合状態が発生する可能性があります。PreloadManager のスレッドが MediaSource の内部状態(バッファへの新しいデータの書き込みなど)を変更しているまさにその瞬間に、プレーヤーのスレッドが MediaSource から読み取ろうとしている可能性があります。これにより、予測不可能な動作やデバッグが難しい IllegalStateException が発生します。

preloadManagerBuilder.setPreloadLooper(playbackLooper)

セットアップ自体で、上記のコンポーネントを ExoPlayer と DefaultPreloadManager の間で共有する方法を見てみましょう。

  val preloadManagerBuilder =
DefaultPreloadManager.Builder(context, targetPreloadStatusControl)

// Optional - Share components between ExoPlayer and DefaultPreloadManager
preloadManagerBuilder
     .setBandwidthMeter(customBandwidthMeter)
     .setLoadControl(customLoadControl)
     .setMediaSourceFactory(customMediaSourceFactory)
     .setTrackSelectorFactory(customTrackSelectorFactory)
     .setRenderersFactory(customRenderersFactory)
     .setPreloadLooper(playbackLooper)

val preloadManager = val preloadManagerBuilder.build()

ヒント: ExoPlayer のデフォルト コンポーネント(DefaultLoadControl など)を使用している場合は、それらを DefaultPreloadManager と明示的に共有する必要はありません。DefaultPreloadManager.Builder の buildExoPlayer を介して ExoPlayer インスタンスをビルドすると、デフォルトの実装とデフォルトの構成を使用している場合、これらのコンポーネントは自動的に相互参照されます。ただし、カスタム コンポーネントやカスタム構成を使用する場合は、上記の API を介して DefaultPreloadManager にそれらを明示的に通知する必要があります。

プロダクション レディなプリロード: スライディング ウィンドウ パターン

ダイナミック フィードでは、ユーザーは事実上無限の量のコンテンツをスクロールできます。対応する削除戦略なしで DefaultPreloadManager に動画を追加し続けると、必ず OutOfMemoryError が発生します。プリロードされた各 MediaSource は、メモリバッファを割り当てる SampleQueue を保持します。これらが蓄積されると、アプリケーションのヒープ領域が不足する可能性があります。この解決策は、スライディング ウィンドウと呼ばれるアルゴリズムです。スライディング ウィンドウ パターンでは、フィード内のユーザーの現在位置に論理的に隣接するアイテムの小さな管理可能なセットがメモリに保持されます。ユーザーがスクロールすると、管理対象アイテムのこの「ウィンドウ」も一緒にスライドし、表示された新しいアイテムが追加され、遠く離れたアイテムが削除されます。

slidingwindow.png

スライディング ウィンドウ パターンの実装

PreloadManager には組み込みの setWindowSize() メソッドがないことを理解しておくことが重要です。スライディング ウィンドウは、開発者がプリミティブな add() メソッドと remove() メソッドを使用して実装する設計パターンです。アプリケーション ロジックで、スクロールやページ変更などの UI イベントをこれらの API 呼び出しに接続する必要があります。このコード リファレンスが必要な場合は、socialite サンプルに実装されているスライディング ウィンドウ パターンをご覧ください。このサンプルには、スライディング ウィンドウを模倣した PreloadManagerWrapper も含まれています。

ユーザーの視聴でアイテムがすぐに表示される可能性がなくなった場合は、実装に preloadManager.remove(mediaItem) を追加することを忘れないでください。ユーザーの近くにないアイテムを削除できないことが、プリロード実装におけるメモリの問題の主な原因です。remove() 呼び出しにより、アプリのメモリ使用量を制限し、安定させるのに役立つリソースが確実に解放されます。

TargetPreloadStatusControl を使用してカテゴリ化されたプリロード戦略を微調整する

プリロードする対象(ウィンドウ内のアイテム)を定義したので、各アイテムのプリロード量を定義した戦略を適用できます。パート 1TargetPreloadStatusControl の設定で、この粒度を実現する方法はすでに説明しました。

位置 +/- 1 のアイテムは、位置 +/- 4 のアイテムよりも再生される可能性が高くなります。ユーザーが次に閲覧する可能性が最も高いアイテムに、より多くのリソース(ネットワーク、CPU、メモリ)を割り当てることができます。これにより、近接性に基づく「プリロード」戦略が作成されます。これは、即時再生と効率的なリソース使用のバランスを取るための鍵となります。

前のセクションで説明したように、PreloadManagerListener を介して分析データを使用し、プリロード期間の戦略を決定できます。

まとめと次のステップ

これで、Media3 の DefaultPreloadManager を使用して、高速で安定したリソース効率の高いメディア フィードを構築するための高度な知識を身につけました。

重要なポイントをまとめましょう。

  • PreloadManagerListener を使用して分析情報を収集し、堅牢なエラー処理を実装します。
  • 重要なコンポーネントが共有されるように、常に 1 つの DefaultPreloadManager.Builder を使用してマネージャーとプレーヤーの両方のインスタンスを作成します。
  • sliding window パターンを実装し、add() と remove() の呼び出しを積極的に管理して OutOfMemoryError を防ぎます。
  • TargetPreloadStatusControl を使用して、パフォーマンスとリソース消費のバランスを取るスマートな階層型プリロード戦略を作成します。

パート 3: プリロードされたメディアのキャッシュ保存

データをメモリにプリロードすると、パフォーマンスがすぐに向上しますが、トレードオフが生じる可能性があります。アプリを閉じたり、プリロードされたメディアをマネージャーから削除したりすると、データは消去されます。最適化をより持続的に行うには、プリロードとディスク キャッシュを組み合わせます。この機能は現在開発中で、今後数か月以内にリリースされる予定です。

共有するフィードバックはありますか?皆様からのご意見をお待ちしております。

ぜひお試しいただき、動画の再生速度を向上させてください。🚀

作成者:

続きを読む