プロダクト ニュース

メディア再生の強化: 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 エラー): このコールバックは、デバッグとモニタリングに役立ちます。プリロードが失敗すると、関連する例外が提供されて呼び出されます。

次のコード例に示すように、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 のプリロード アクティビティがアクティブなプレーヤーに必要な帯域幅を消費するため、現在の動画が停止します。再生中の停止は、ユーザー エクスペリエンスの重大な失敗です。

単一の 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 が異なるスレッドで動作している場合、競合状態が発生する可能性があります。プレーヤーのスレッドが MediaSource から読み取ろうとしているまさにその瞬間に、PreloadManager のスレッドが 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()

ヒント: DefaultLoadControl などの ExoPlayer のデフォルト コンポーネントを使用している場合は、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 を使用してマネージャーとプレーヤーの両方のインスタンスを作成します。
  • スライディング ウィンドウ パターンを実装し、add() と remove() の呼び出しを積極的に管理して OutOfMemoryError を防ぎます。
  • TargetPreloadStatusControl を使用して、パフォーマンスとリソース消費のバランスを取るスマートな階層型プリロード戦略を作成します。

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

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

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

ぜひお試しください。🚀

作成者:

続きを読む