サービスに関する最新情報

メディア再生の向上: Media3 の PreloadManager の特集 - パート 2

所要時間: 9 分
Mayuri Khinvasara Khabya さんのプロフィールを表示
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 のアクティビティが、アクティブなプレーヤーに必要な帯域幅を消費し、現在の動画が停止する可能性があります。再生中のストールは、ユーザー エクスペリエンスの重大な障害です。

単一の 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 のトラックを取得するという無駄なシナリオを回避できます。< br /> プリロード マネージャーは、TrackSelector の同じインスタンスをプレーヤーと共有しないでください。代わりに、異なる TrackSelector インスタンスを使用する必要がありますが、実装は同じです。そのため、DefaultPreloadManager.Builder では TrackSelector ではなく TrackSelectorFactory を設定します。

preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)

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

preloadManagerBuilder.setRenderersFactory(customRenderersFactory)

Exoplayer コンポーネントの詳細を確認する。

基本ルール: 共通の Playback 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()

ヒント: 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 を使用して、分類されたプリロード戦略を微調整する

プリロードする対象(ウィンドウ内のアイテム)を定義したので、各アイテムをどの程度プリロードするかについて、明確に定義された戦略を適用できます。パート 1 で説明した TargetPreloadStatusControl の設定により、この粒度を実現する方法はすでに確認しました。

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

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

まとめと次のステップ

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

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

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

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

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

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

動画の再生速度を上げて、快適な視聴をお楽しみください。🚀

作成者:
続きを読む