プロダクト ニュース

メディア再生の強化: 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 の再生とどのように連携するかを視覚的に確認してください。

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 を共有すると、そのアロケータを PreloadManager とプレーヤーで正しく共有できます。LoadControl を使用してプリロードを効果的に制御する機能は、今後の Media3 1.9 リリースで利用できるようになります。

preloadManagerBuilder.setLoadControl(customLoadControl)

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

preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)

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

preloadManagerBuilder.setRenderersFactory(customRenderersFactory)

その他の Exoplayer コンポーネントについて詳しくは、こちらをご覧ください。

黄金律: すべてを管理する共通の再生 Looper

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

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

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

PreloadManager と ExoPlayer が異なるスレッドで動作している場合、競合状態が発生する可能性があります。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()

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

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

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

フィードバックをお寄せください。皆様からのご意見をお待ちしております。

今後の情報にご期待ください。動画再生を高速化しましょう。🚀

作成者:

続きを読む