メディアの利用は、ユーザーがモバイル デバイスに費やす時間の大部分を占めています。そのため、再生エクスペリエンスは、完璧で、効率性が高く、バッテリー駆動時間に配慮したものである必要があります。さらに、ユーザーは、スマートフォンからリビングルームのテレビなど、デバイス間でこれらのエクスペリエンスがシームレスに移行することを期待しています。
Media3 と ExoPlayer のアーキテクチャの更新により、さまざまなフォーム ファクタでシームレスな再生がどのように実現されるかをご覧ください。Media3 Compose UI ツールキットを使用したレスポンシブなインターフェースの作成、新しい PreloadManager を使用したフィードのバッファリングの排除、CastPlayer と空間オーディオを使用したリビングルームの画面と Android XR へのアプリの拡張について説明します。
文字起こし
違いますね。
皆さん、こんにちは。
トニと申します。
私は Android メディアチームのエンジニアです。
Kristina と Donovan と一緒に、Jetpack Media3 が Android のあらゆるメディアニーズにどのように役立つかについてお話しします。
Media3 は、メディア関連のあらゆる処理に対応する Android のサポート ライブラリです。ExoPlayer による再生、メディア セッションと Cast によるメディアの共有、Transformer によるメディアの編集、フレームやメタデータの抽出などのメディアの検査など、さまざまな処理に対応しています。
Media3 は、この領域全体を 1 つのライブラリ グループでカバーしています。
サポート ライブラリとして、アプリにバンドルされています。
つまり、一貫性を確保できるため、すべての OS バージョンとすべてのデバイスで同じコードを実行できます。
また、更新可能性にも役立ちます。
バグの修正はすべて API 24 まで反映され、OS のリリース サイクルには関連付けられません。
また、サポート ライブラリとして、Android フレームワークでは不可能な、非常に豊富なカスタマイズ オプションを提供できます。
また、GitHub のライブラリとして、オープンソースを第一に考えています。
つまり、Google が行った変更はすべて、すぐに GitHub で利用できるようになります。
アクティブな Issue Tracker があり、pull リクエストを受け付けています。
ここで強調したいのは、これらすべてのユースケースにおいて、Google はあらゆるレベルの詳細を重視しているということです。高レベルの UI 要素から、シンプルなすぐに使える API、詳細なカスタマイズ オプション、さまざまな形式や標準の幅広いサポート、そしてそれらすべてを支えるパフォーマンスと効率性まで、すべてを重視しています。
Media3 はこの領域全体をカバーしています。
今回は、これらのトピックの一部をさらに詳しく掘り下げていきます。
新しい Compose UI モジュール、メディア セッションとの共有をさらに容易にする新しい CastPlayer、新しい高度なエフェクトを備えた刷新された編集パイプライン、メタデータとフレームを取得するための新しいツール、いくつかの新しいストリーミング テクノロジー(特に Eclipsa の動画と音声)、プリロード、スクラブ、バッテリー節約のための多数の QoE ユーティリティについて説明します。
それでは、最初のトピックである、Media3 を使用してメディアの UI 要素を構築する方法について詳しく見ていきましょう。
最近、Media3 UI Compose と Media3 UI Compose Material 3 という 2 つの新しいモジュールをリリースしました。
ここでは、Media3 1.10 でリリースされた、それ以上の変更が加えられていない、高レベルのプレーヤー コンポーザブルを確認できます。
@Composable
fun videoScreen() {
Player(viewModel.exoPlayer)
}すぐに使用できます。
必要なのは Media3 プレーヤーのみです。
この例では ExoPlayer です。通常はビューモデルにあります。
ただし、CastPlayer、メディア コントローラ、アプリ内の他のプレーヤーなど、別のプレーヤーを使用することもできます。
ここで、ExoPlayer が信頼できる情報源であることに注目してください。
このコンポーザブルにはすべての情報が含まれており、プレーヤー コンポーザブルが UI のレンダリングとすべての接続を処理します。
しかし、この UI をカスタマイズして、新しいボタン、異なる位置、異なるスタイル、テキスト オーバーレイを追加したい場合はどうすればよいでしょうか。
プレーヤー コンポーザブルは、ビルディング ブロックで構成されています。
交換できます。
個々のブロックをカスタマイズすることも、アプリの他の場所でスタンドアロンで使用することもできます。
たとえば、上部、中央、bottomControls を個別にカスタマイズできます。
また、動画が再生されていないときに表示されるシャッターや、エラー オーバーレイの外観をカスタマイズすることもできます。
しかし、これは最初のレイヤにすぎません。
デフォルトのセンターと bottomControls には、カスタマイズ可能なより詳細なスロットもあります。
たとえば、中央のボタンを入れ替えて他の要素はそのままにしたり、スライダーの上または下に要素を追加したりできます。
Media3 Compose には、これを行うために必要なビルディング ブロックがすべて用意されています。
たとえば、再生 / 一時停止ボタン、リピートモード、シャッフル モード、ミュートボタンなど、さまざまなボタンがあります。
次に、画面に表示されるスライダーや位置テキストなどの進行状況インジケーターがあります。
また、多数のコンテナとオーバーレイもあります。
先ほど紹介した高レベルのプレーヤー コンポーザブルは一例ですが、他にもエラー オーバーレイや字幕など、さまざまな例があります。
さらに、このミニコントローラなど、他にも便利なコンポーネントがあります。
ユーザーがアプリ内を移動しているときに、ボトムシートに配置できます。
まだまだ終わりではありません。
Google は、このライブラリをさらに拡張することに積極的に取り組んでいます。たとえば、クリッピング スライダーなどの新しい編集コントロールが近日公開される予定です。
では、このカスタマイズを実際に見て、これらのビルディング ブロックがどのように使用されているかを確認しましょう。
Player(viewModel.exoPlayer)現在スライダーの横にある位置と再生時間のテキストを、スライダーの上に移動するとします。
Player(
player = viewModel.exoPlayer,
bottomControls = {
PlayerDefaults.BottomControls(
player,
left = {}, right = {},
above = {
PositionAndDurationText(
player, separator = " • ")
},
)
},
)そこで、これらの bottomControls をオーバーライドします。
デフォルトのコンポーネントを引き続き使用できますが、左と右のスロットを空に設定して非表示にします。
代わりに、位置と再生時間のテキストを上のスロットに配置し、カスタム セパレータを使用して、より興味深い外観にしています。
ここで、centerControls オーバーレイを完全に削除し、代わりに速度コントロールとミュートボタンを備えた再生ボタンをスライダーのすぐ上に配置するとします。
Player(
player = viewModel.exoPlayer,
centerControls = {},
bottomControls = { ...
above = {
Row(Modifier.fillMaxWidth()) {
PositionAndDurationText(...)
Spacer(Modifier.weight(1f))
PlaybackSpeedToggleButton(player)
MuteButton(player)
PlayPauseButton(player)
}
},
)
}これもまったく同じ考え方です。
centerControls を空に設定して非表示にし、スライダーの上のスロットを拡張して、追加のスペースと表示する 3 つのボタンを追加できます。
これらのボタンはライブラリ自体から提供されるため、追加のコードを記述する必要はありません。
これだけで十分です。
この方法で、これらの要素のスタイル設定を続けることができます。
たとえば、ミュートボタンのアイコンを変更し、機能はそのままにする場合も、同じように動作します。
Player(
...
bottomControls = { ...
above = {
...
MuteButton(player,
painter = { painterResource(
if (showMuted)
R.drawable.muted
else
R.drawable.unmuted )})
}
},
)ご覧のとおり、ボタンのカスタム ペインターを定義して、アイコンを自分で選択できます。
このコンポーネントはメタデータを提供するため、この例の showMuted のように状態を確認して適切なアイコンを選択するだけで済みます。
これが Media3 Compose の主なメリットです。
アプリのデザインに集中でき、残りの処理は Media3 が行います。
プレーヤーから状態を読み取り、プレーヤーとやり取りして、その間のすべての遷移とアニメーションを処理します。
次に、UI からメディアの共有に話を移しましょう。
再生状態は、同じデバイス上の他のアプリや、リモート デバイスに接続されている他のアプリと共有できます。
MediaSession を使用して、進行中の再生を公開するのが主な方法です。
これにより、再生の状態とメタデータが他のユーザーに公開され、他のユーザーが再生を制御できるようになります。
Gemini、システム UI メディア通知、キャスト、Wear OS、Android Auto のコンパニオン アプリは、この方法でアプリと通信します。
MediaSession は Media3 プレーヤーをラップします。プレーヤーがあれば、簡単に作成できます。
MediaSession.Builder(context, player).build()Compose UI の場合と同様に、これは ExoPlayer でも、アプリに組み込まれている他のプレーヤーでもかまいません。
バックグラウンドで再生する場合は、通常、MediaSessionService または MediaLibraryService 内でホストします。これにより、すべてのライフサイクルと通知などの処理が自動的に行われます。
class PlaybackService : MediaSessionService() {
private lateinit var mediaSession:
MediaSession
override fun onGetSession(
info: ControllerInfo) : MediaSession?
= mediaSession
}メディアアプリを開発している方なら、こうしたことはすでにご存じでしょう。
今回ご紹介するエキサイティングな新機能は、新しく書き直された CastPlayer です。
Google Cast を使用すると、ローカル再生を Android TV、Chromecast、スマート スピーカーなどのリモート デバイスに転送できます。
以前は、個別の Cast SDK を公開していましたが、現在はメンテナンス モードになっています。
代わりに、Media3 Cast が完全に置き換えます。Media3 Cast は、よりシンプルで合理化された統合を提供します。
ここで最も重要な要素は CastPlayer です。これは、ExoPlayer と同じ標準プレーヤー インターフェースを実装しています。
ExoPlayer はローカル再生用ですが、CastPlayer は、ExoPlayer を介してローカルで再生することも、Cast プロトコルを介してリモートで再生することもできる委任インスタンスです。
すべてを処理します。
出力デバイスの変更をリッスンし、接続を確立し、状態を同期して、必要に応じて再生を転送します。
そのため、CastPlayer をアプリに統合するのは非常に簡単です。
val player = ExoPlayer.Builder(context).build()
mediaSession = MediaSession.Builder(context,player)
.build()このように ExoPlayer を作成して MediaSession と共有する代わりに、CastPlayer を使用します。
Cast.getSingletonInstance(context).initialize()
val player = CastPlayer.Builder(context).build()
mediaSession = MediaSession.Builder(context,player)
.build()他に必要なことは、アプリのどこかで使用する前に Cast システムを初期化することだけです。
この CastPlayer は、ローカル再生用に ExoPlayer を内部でビルドします。ローカル プレーヤーをさらにカスタマイズするには、独自の ExoPlayer をビルドして、ローカル プレーヤーとしてこの CastPlayer に渡すこともできます。
Cast.getSingletonInstance(context).initialize()
val exoPlayer = ExoPlayer.Builder(context).build()
val player = CastPlayer.Builder(context)
.setLocalPlayer(exoPlayer).build()
mediaSession = MediaSession.Builder(context,player)
.build()これも Media3 の原則と同じです。
シンプルなすぐに使える API ビルディング ブロックだけでなく、さらにカスタマイズするために必要なすべてのカスタマイズ オプションも提供しています。
この設定で、ユーザーがローカル再生とリモート再生を実際に切り替える方法について疑問に思われるかもしれません。
メディア通知のシステム出力スイッチャー ダイアログですぐに使用できます。
隅にある小さなボタンです。
しかし、キャスト アイコンを再生画面に表示して、見つけやすくしたい場合はどうすればよいでしょうか。
数分前に作成した、このカスタマイズされた Compose UI を覚えていますか?
Player(
exoPlayer,
centerControls = {}
bottomControls = { /* customized */ },
)Media3 Cast には、そこに配置できる既製のボタンも用意されています。
たとえば、既製の MediaRouteButton を topControls に追加するだけで、それ以上のパラメータを設定する必要はありません。
Player(
castPlayer,
centerControls = {}
bottomControls = { /* customized */ },
topControls = {
PlayerDefaults.TopControls(castPlayer) {
MediaRouteButton(
Modifier.align(Alignment.TopEnd))
}
},
)このボタンをクリックすると、同じシステム出力スイッチャー ダイアログが開き、CastPlayer が再生を転送します。
また、ここで CastPlayer を UI に接続して、リモートで発生したことがローカル UI に反映されるようにしています。
それでは、編集のユースケースについて説明するために、Kristina にバトンタッチします。
ありがとうございました
こんにちは。今回は、編集と高度なエフェクトについて説明します。
Media3 API は複数のコンポーネントで構成されています。
まず、Transformer は編集 API のメインのエントリ ポイントです。
Transformer は、メディアを処理、コード変換、ファイルにエクスポートします。
次に、CompositionPlayer です。これは、メディア コンポジションのリアルタイム プレビューをレンダリングします。
これにより、最終的なエクスポートの前に編集内容をプレビューして微調整できます。
次にエフェクトです。
シンプルな視覚効果を適用したり、グラフィック オーバーレイを追加したりする際に、フレーム単位の処理を完全に制御できます。
最後に、Muxer があります。
これは、パイプラインの最終出力ステージとして機能し、処理された音声、動画、メタデータのトラックを受け取り、標準化された MP4 コンテナに書き込みます。
この 1 年間、Google は動画編集パイプラインをゼロから見直し、再構築してきました。本日は、その大幅な機能強化についてご紹介します。
パイプラインを再構築した理由
そのため、従来のアーキテクチャにはいくつかの課題がありました。
1 つ目は、すべての処理ステップにメタデータを伝播できなかったことです。
OpenGL と密接に結合していたため、最新のグラフィック ツールの柔軟性が制限されていました。
また、API が複雑なため、デベロッパーにとってカスタマイズとメンテナンスが大きな課題となっていました。
これらの課題に対処するため、Google は新しい処理パイプラインを構築しました。
まず、複雑なマルチアセット構成の組み立てがはるかに簡単になりました。
次に、OpenGL から切り離すことで、WebGPU などの最新のグラフィック フレームワークを自由に使用できるようになります。
最後に、Media3 がデコーダーとエンコーダーを直接管理するようになり、すべてのデバイスで一貫性と効率性が向上しました。
このパイプラインの仕組みを見てみましょう。
デコーダは、Media3 が管理するハードウェア バッファにフレームを直接出力します。
フレーム アグリゲータは、複数のシーケンスにわたってフレームを調整し、カスタム フレーム プロセッサがそれらのバッファを読み取って処理し、エンコーダに送信できるようにします。
FrameProcessor インターフェースは、アプリレベルの処理の主要な拡張ポイントとして機能します。
public interface FrameProcessor
extends AutoCloseable {
interface Listener {
void onWakeup();
void onError(
VideoFrameProcessingException exception);
void onFrameProcessed(
Frame frame,
@Nullable SyncFenceWrapper onCompleteFence);
}
interface Factory {
FrameProcessor create(
FrameWriter output,
Executor listenerExecutor,
Listener listener);
}
boolean queue(List<AsyncFrame> frames);
void signalEndOfStream();
}アプリケーションは、リスナー インターフェースを介して、フレームが処理されるたびに Media3 に通知し、同期フェンスを渡します。これにより、Media3 はバッファを安全に再利用できるタイミングを正確に把握できます。
統合メモリに関する注意事項: Media3 がハードウェア バッファの所有権を持つことで、アプリはデータを直接読み取り、変更できます。
このゼロコピー アプローチにより、メモリ オーバーヘッドが削減され、処理が超高速に維持されます。
新しい編集 API を活用しているデベロッパーの 1 社が Adobe です。
Adobe Premiere チームは、メディア編集インフラストラクチャの基盤として Media3 編集 API を使用することで、Android エコシステム全体で高い互換性を確保しながら、独自のカスタム パイプラインと WebGPU を使用するために必要な柔軟性を維持しました。
次に、Media3 1.12 リリースの一環として、新しいエフェクトをリリースすることをお知らせします。
まずは動画拡張機能から。
内部的には、このアーキテクチャはモジュラー セットアップに依存しています。
アプリは Media3 エフェクト Play 開発者サービスと統合され、Play 開発者サービス拡張モジュールとインターフェースします。
動画補正モジュールには、トーン マッピング、ノイズ除去、ブレ軽減、高画質化の機能が含まれています。
Video Enhance をパイプラインに統合するには、数行のコードを記述するだけです。
val frameProcessorFactory = EnhancementSessionDecorator.Builder(/* context = */ this, baseGlFactory)
.setTonemappingEnabled(true)
.setDeblurAndDenoiseEnabled(true)
.setUpscaleVideoEnabled(true)
.build()
val transformerBuilder = Transformer.Builder(/* context = */ this)
.setFrameProcessorFactory(frameProcessorFactory)EnhancementSessionDecorator.Builder を使用すると、トーンマッピング、ブレ除去、アップスケーリングなどの機能を選択的に有効にし、結果の frameProcessorFactory とハードウェア バッファ ヘルパーを Transformer.Builder に渡すことができます。
Media3 1.12 で追加されるもう一つのエフェクトは、スタジオ サウンドです。
スタジオ サウンドは、動画の音声トラックを自動的に分析し、音声、周囲のノイズ、音楽などの個別のステムに分離します。
クリエイターは、風や人混みによる雑音を簡単に低減し、クリアな会話や音楽を前面に押し出すことができます。
スタジオ サウンドを統合するには、エフェクト オブジェクト内でスタジオ サウンドの音声プロセッサを渡します。
val editedMediaItem =
EditedMediaItem.Builder(MediaItem.fromUri(media.uri))
.setEffects(
Effects(
listOf(
soundSeparationAudioProcessor
),
emptyList(), // video effects
)
)
.build()これにより、オーディオ プロセッサがメディア アイテムにアタッチされます。
Studio Sound 音声プロセッサを作成するときに、リスナー コールバックをアタッチして、onResult を介して分析された音声チャンネル マッピングを受信します。
val soundSeparationAudioProcessor =
SoundSeparationAudioProcessor(
getApplication(),
mainExecutor,
object : SoundSeparationAudioProcessor.Listener {
override fun onResult(channelToAudioType: SparseIntArray) {
for (i in 0 until channelToAudioType.size()) {
val channelIndex = channelToAudioType.keyAt(i)
val audioType = channelToAudioType.valueAt(i)
val label = toAudioTypeName(audioType)
detectedChannelVolumes.add(ChannelVolume(channelIndex, audioType, label))
}
}
},
)各チャンネルを反復処理して、チャンネル インデックス、識別された音声タイプ、読み取り可能なラベルを抽出します(オペレーションの結果が利用可能な場合)。
では、スタジオ サウンドの実際の効果を聴いてみましょう。
まず、1 つ目の効果です。泣かないで
夕食の準備をしています。
背景で赤ちゃんの泣き声が聞こえますが、エフェクトが適用されています。
お聞きいただいたとおり、スタジオ サウンド エフェクトのおかげで、バックグラウンドで赤ちゃんの泣き声は聞こえませんでした。
では、メタデータの取得について詳しく見ていきましょう。
Media3 を使用してコンテナ メタデータを読み取り、フレームを抽出する方法について説明します。
MetadataRetriever 以降、Kotlin コルーチンとシームレスに統合され、クリーンで非ブロッキングなコードを実現します。
メディア アイテムごとに 1 つの不変のレトリーバーを使用する、よりクリーンでエラーが発生しにくい設計になっています。
すべての API バージョンとデバイスで一貫して動作します。
また、幅広いメディア フォーマットをすぐにサポートできます。
MetadataRetriever を使用した実際の例を次に示します。
MetadataRetriever.Builder(context, mediaItem).build().use { retriever ->
// 1. Duration (in microseconds)
val durationUs = retriever.retrieveDurationUs().await()
// 2. Track formats & container metadata (reuses the same preparation pass)
val trackGroups = retriever.retrieveTrackGroups().await()
for (i in 0 until trackGroups.length) {
val format = trackGroups[i].getFormat(0)
// Video/Audio properties: format.width, format.height, format.colorInfo …
// Rich metadata: SlowMotionData, MotionPhotoMetadata, Mp4TimestampData …
val metadata = format.metadata
}
// 3. Stream structure & manifest info
val timeline = retriever.retrieveTimeline().await()
}Kotlin コルーチンを使用すると、再生時間の取得、トラック形式とメタデータの検査を、すべて 1 つのクリーンなブロック内で非同期に行うことができます。
次に、FrameExtractor について説明します。
これにより、メディア アイテムからデコードされた動画フレームを迅速かつ効率的に取得できます。
デコーダなどの基盤となるリソースを自動的に管理し、ビジュアル エフェクトや HDR トーン マッピングを適用できます。また、きめ細かいシーク制御により、必要なフレームを正確に取得できます。
コードでのフレーム抽出は次のようになります。
FrameExtractor.Builder(context, mediaItem)
.setSeekParameters(SeekParameters.CLOSEST_SYNC) // Fast keyframe seeking
.setEffects(listOf(Presentation.createForHeight(360))) // GPU downscaling
.setExtractHdrFrames(true) // Optional: HLG HDR or UltraHDR
.build()
.use { extractor ->
// Representative thumbnail
val thumbnail = extractor.getThumbnail().await()
// Frame at specific timestamp (bitmap + actual presentationTimeMs)
val frame = extractor.getFrame(positionMs = 5_000L).await()
}シーク パラメータ、GPU ダウンスケーリング、エフェクト、HDR サポートなどのオプションを使用してインスタンスを構築し、クリーンなサスペンド呼び出しを使用してサムネイルと特定のフレームを非同期で待機します。
では、ドノヴァンに代わります。
Kristina さん、ありがとうございます。
次に、Media3 に追加された、基盤となるストリーミング技術のサポートについて説明します。これには、Eclipsa Video や Eclipsa オーディオ などのフォーマットのサポートが含まれます。
そのため、Media3 の主な取り組みは、ストリーミング標準を先取りして、新しい仕様が導入されるたびにメディア パイプラインを書き直す必要がないようにすることです。
最近リリースされた機能の例をいくつかご紹介します。
コンテンツ ステアリングにより、ストリーミング中にバックエンドの CDN パスを動的に切り替えることができます。
サーバーガイド型広告挿入をサポートしており、パーソナライズド広告をシームレスに挿入できます。
また、完全な AV1 HDR サポートなど、複数のエントリ ポイントで AV1 のサポートを導入しました。
高忠実度のキャプチャと編集を可能にする Android プロフェッショナル動画をサポートしています。
また、JPEG 形式と HEIC 形式の両方でモーション フォトのサポートを追加しています。
もちろん、Eclipsa Video と Eclipsa オーディオ のサポートも追加しています。これらについては、後ほど詳しく説明します。
そして、これらすべてがデバイスのサポートの展開に伴って自動的にサポートされるようになります。
では、HDR の次の進化形である Eclipsa Video と、それが解決しようとしている根本的な問題について見ていきましょう。
以前の HDR 形式では、記述的なメタデータが提供され、シーンの見え方が決定されていましたが、ディスプレイにレンダリング方法を指示することはありませんでした。
まず、この点を明確にしましょう。
環境によって、コンテンツを鮮明かつ快適に表示するための平均画面輝度の推奨値は異なります。
薄暗い部屋では、この範囲は 100 ~ 300 ニトになります。
そしてもちろん、環境が明るくなるにつれて、画面も明るくしてコンテンツをはっきりと見られるようにする必要があります。直射日光の下でスマートフォンを見る場合は、コンテンツをはっきりと見られるように、画面の輝度を 1,000 ニト、2,000 ニト、あるいはそれ以上に上げる必要があります。
従来の SDR は 100 ~ 300 の範囲で動作します。
もちろん、HDR は画面全体を明るくするためのものではありません。
ダイナミック レンジを拡大して、火や太陽光などのハイライトをローカライズしたいと考えています。
しかし、スマートフォンは、暗い部屋や明るいビーチなど、どこにでも持ち歩くため、いくつかの複雑な要素が加わります。
さらに、多くのアプリでは、SDR UI 要素と SDR コンテンツ、HDR コンテンツが混在したフィードが使用されています。そのため、特に深夜にスクロールしているときなどに、不快感や眩しさを感じることがあります。
HDR の価値がよくわかりますね。
これにより、より多くの環境でコンテンツが美しく表示されるようになります。
HDR をオンにすると、ユーザー エンゲージメントが向上することも確認されています。
しかし、既存の HDR 形式は、このような環境を考慮していません。
そこで Eclipsa Video が真価を発揮します。
(笑い声)
ありがとうございました
ありがとうございました
そのため、Eclipsa Video は記述的なメタデータではなく、規範的なメタデータを提供します。
これは主に 3 つの柱に基づいています。
1 つ目の柱は、基準となる白のアンカーです。
これにより、標準の輝度がディスプレイの周囲光に合わせて調整された SDR ホワイト ポイントにロックされ、輝度が突然急上昇することがなくなります。
2 つ目の柱は、クリエイターの SDR ベースラインです。
これにより、クリエイターの正確なトーン マッピング カーブが SDR に戻されます。
3 つ目は、ヘッドルーム適応型ゲイン曲線です。これは、ディスプレイのリアルタイム HDR ヘッドルームに合わせてハイライトを決定論的にスケーリングし、コンテンツが視聴環境に適合するようにします。
この機能は、Android 17 以降を搭載したデバイスの Media3 1.11 で、ExoPlayer の標準機能として利用できます。
また、Eclipsa Video はオープン標準に基づいて構築されているため、業界をまたいだ互換性があります。
HDR をサポートするなら、今が絶好のタイミングです。
オーディオ面では、オープンソースでロイヤリティフリーの 3D 空間オーディオ形式である Eclipsa オーディオ があります。
これは、Alliance for Open Media の IAMF 仕様に基づいています。
シーンベースのアンビソニックとチャンネルベースのオーディオを組み合わせて、複数のミックス プレゼンテーションを含む単一のストリームにします。
これにより、会話の強調などのオブジェクトのような操作が可能になり、ステレオ ヘッドフォンからサラウンド音声設定まで、あらゆるものに対応します。
Media3 には、実際には 2 つの再生パスがあります。
Android 17 以降では、ExoPlayer と Android Audio Framework が Eclipsa オーディオを自動的にレンダリングします。
Android 16 以前でも、オプションは引き続き利用できます。
IAMF 拡張機能は、オプションの Media3 アドオン ライブラリを介して有効にできます。
作成側では、Media3 muxer を使用して IAMF 音声トラックを書き込むことができます。
しかし、高画質フォーマットをサポートすることは、全体像の半分にすぎません。
その品質を効率的に提供する必要があります。
ここからは、Media3 がパイプラインを高速化、効率化する方法について説明します。
本日お話しした多くのことと同様に、これらのパフォーマンス改善のほとんどは、Media3 のバージョンを最新に保つだけで、すぐに利用できます。
たとえば、PCM 音声オフロード機能を使用すると、音声再生を低電力処理ユニットにルーティングできます。
初期の分析では、画面オフ時のオーディオ再生で最大 20% の省電力が確認されています。
PCM オーディオ オフロードに加えて、ExoPlayer の内部レンダリング ループのウェイクアップを VSYNC およびデコーダ バッファの可用性と整合させる動的スケジューリングも導入されています。
また、レンダラーのプリウォーミングも可能です。これにより、セカンダリ動画レンダラーをプリウォーミングして、再生リストの切り替えを瞬時に行い、広告の挿入を容易にすることができます。
また、プレーヤーの停止検出とプリキャッシュ機能により、バッファとレンダラの停止を自動的に検出し、プリキャッシュされたメモリをクリーンにバインドします。
もう 1 つの最適なモードは、スクラブモードです。
これは、ユーザーがコンテンツを頻繁にスクラブするアプリに適しています。
スクラブモードを有効にすると、シークバーのドラッグ操作の応答性が向上し、スムーズになります。
なお、これはスクラブモードがオフの場合です。
右側は、スクラブモードがオンになっている状態です。
ありがとうございました
ありがとうございました
それでは
次に、Android システムの健全性について説明します。
共有 RAM、CPU、バッテリー リソースを尊重する必要があります。Android がオーディオの強化とフォアグラウンド サービスに関するルールを強化するにつれて、独自の未加工サービスとビットマップを管理するアプリでエラーが発生することがよくあります。
たとえば、フォアグラウンド サービスの開始が許可されていない例外や、バインダーのメモリ不足例外などが発生することがあります。
そこで、メディア サービス セッション、メディア通知マネージャー、ビットマップ パイプラインをさまざまな方法で強化しました。たとえば、古いフォアグラウンド サービス インテントを防止したり、アートワーク メモリをバインドしたりしています。
ただし、Media3 を使用して標準のガイダンスに従っている場合は、これらのシステム健全性制限はすべて自動的に処理され、影響はありません。
最後に、Media3 に近日公開予定の機能についてお話しします。
動画をエンコードまたはエクスポートする際、ハードウェアの多様性が最大の課題の一つになることがあります。
多くのアプリは、万能なコーデック構成を使用していますが、ご存じのとおり、これは品質とパフォーマンスの最適化につながりません。
そこで、CodecDB を構築しました。
CodecDB は、標準化された品質指標と Android デバイス全体のハードウェア ベンチマークを使用して、実際の動画測定を広範囲に実施して構築されています。
では、実際に見てみましょう。
左側は、スマートフォンで撮影した動画のフレームです。多くのデバイスで使用される、非常に一般的な動画構成を使用しています。
右側は、CodecDB から取得したデバイスレベルの最適な構成を使用した場合です。
では、違いはどこにあるのでしょうか。
高精細な領域を見ると、品質の違いが最もよくわかります。
木の枝や低木の鮮明さ、建物の端のシャープさを見てください。
つまり、CodecDB を活用することで、特定のデバイスの機能を活かした、より優れた視聴体験を提供できるようになります。
コード内で CodecDB を使用するのは非常に簡単です。
val config = CodecDb.onDevice(context)
.forEncodingFallingBackTo {
constraints -> createFallbackConfig(constraints)
}
.findOptimalSettings()
.forEditing()
.withResolution(1920, 1080, /* frameRate= */ 30.0)
.withAnyCodec()
.targetingQuality(VideoEncoder.QUALITY_LEVEL_VERY_GOOD)
.get()必要なすべてのパラメータをガイドする、Fluent ステージ ビルダー パターンを使用します。
ユースケースを指定します。
ここでは編集に使用しています。
これにより、Media3 のエクスポートまたはコード変換パイプラインを使用していることが CodecDB に通知されます。
他にも、たとえばカメラ キャプチャを使用している場合は、カメラ キャプチャを設定できます。
また、ターゲットの解像度とフレームレートも渡します。
そして、1080p の動画に 8 Mbps が必要なのか 15 Mbps が必要なのかを推測する代わりに、使用するコーデックを CodecDB に伝え、知覚品質の目標をリクエストします。
この例では、品質レベル「非常に良い」を使用しています。
内部的には、CodecDB がその SOC の実証的なベンチマーク曲線を検索し、理想的なコーデックとエンコーダーのパラメータを返します。
CodecDB は、まもなく Media3 でリリースされる予定です。リリースされると、ネイティブの Media3 タイプが使用されるようになります。
そのため、既存の Media3 コードとの統合は簡単です。
違いますね。
本日は、Media3 の優れた機能をたくさんご紹介しました。ここで、それらを振り返ってみましょう。
今回は、新しい Compose UI モジュール、新しい CastPlayer、刷新された編集パイプライン、Media3 で使用できる多くの新しいエフェクトについて説明しました。
また、メタデータとフレームを取得する新しいツールも確認されています。
また、新しいストリーミング技術のサポート、パフォーマンスの向上、ユーティリティについても説明しました。
Media3 はオープンソース ライブラリであるため、これらの機能がどのように実装されているかを GitHub リポジトリで確認できます。
現時点で、Media3 の最新の安定版リリースは 1.11.1 です。
現在アルファ版の 1.12.0 もご確認ください。
この短縮リンクから、Media3 に関するデベロッパー向けドキュメントにアクセスできます。
ありがとうございました。引き続き ADS をお楽しみください。
@Composable
fun videoScreen() {
Player(viewModel.exoPlayer)
}Player(viewModel.exoPlayer)Player(
player = viewModel.exoPlayer,
bottomControls = {
PlayerDefaults.BottomControls(
player,
left = {}, right = {},
above = {
PositionAndDurationText(
player, separator = " • ")
},
)
},
)Player(
player = viewModel.exoPlayer,
centerControls = {},
bottomControls = { ...
above = {
Row(Modifier.fillMaxWidth()) {
PositionAndDurationText(...)
Spacer(Modifier.weight(1f))
PlaybackSpeedToggleButton(player)
MuteButton(player)
PlayPauseButton(player)
}
},
)
}Player(
...
bottomControls = { ...
above = {
...
MuteButton(player,
painter = { painterResource(
if (showMuted)
R.drawable.muted
else
R.drawable.unmuted )})
}
},
)MediaSession.Builder(context, player).build()class PlaybackService : MediaSessionService() {
private lateinit var mediaSession:
MediaSession
override fun onGetSession(
info: ControllerInfo) : MediaSession?
= mediaSession
}val player = ExoPlayer.Builder(context).build()
mediaSession = MediaSession.Builder(context,player)
.build()Cast.getSingletonInstance(context).initialize()
val player = CastPlayer.Builder(context).build()
mediaSession = MediaSession.Builder(context,player)
.build()Cast.getSingletonInstance(context).initialize()
val exoPlayer = ExoPlayer.Builder(context).build()
val player = CastPlayer.Builder(context)
.setLocalPlayer(exoPlayer).build()
mediaSession = MediaSession.Builder(context,player)
.build()Player(
exoPlayer,
centerControls = {}
bottomControls = { /* customized */ },
)Player(
castPlayer,
centerControls = {}
bottomControls = { /* customized */ },
topControls = {
PlayerDefaults.TopControls(castPlayer) {
MediaRouteButton(
Modifier.align(Alignment.TopEnd))
}
},
)public interface FrameProcessor
extends AutoCloseable {
interface Listener {
void onWakeup();
void onError(
VideoFrameProcessingException exception);
void onFrameProcessed(
Frame frame,
@Nullable SyncFenceWrapper onCompleteFence);
}
interface Factory {
FrameProcessor create(
FrameWriter output,
Executor listenerExecutor,
Listener listener);
}
boolean queue(List<AsyncFrame> frames);
void signalEndOfStream();
}val frameProcessorFactory = EnhancementSessionDecorator.Builder(/* context = */ this, baseGlFactory)
.setTonemappingEnabled(true)
.setDeblurAndDenoiseEnabled(true)
.setUpscaleVideoEnabled(true)
.build()
val transformerBuilder = Transformer.Builder(/* context = */ this)
.setFrameProcessorFactory(frameProcessorFactory)val editedMediaItem =
EditedMediaItem.Builder(MediaItem.fromUri(media.uri))
.setEffects(
Effects(
listOf(
soundSeparationAudioProcessor
),
emptyList(), // video effects
)
)
.build()val soundSeparationAudioProcessor =
SoundSeparationAudioProcessor(
getApplication(),
mainExecutor,
object : SoundSeparationAudioProcessor.Listener {
override fun onResult(channelToAudioType: SparseIntArray) {
for (i in 0 until channelToAudioType.size()) {
val channelIndex = channelToAudioType.keyAt(i)
val audioType = channelToAudioType.valueAt(i)
val label = toAudioTypeName(audioType)
detectedChannelVolumes.add(ChannelVolume(channelIndex, audioType, label))
}
}
},
)MetadataRetriever.Builder(context, mediaItem).build().use { retriever ->
// 1. Duration (in microseconds)
val durationUs = retriever.retrieveDurationUs().await()
// 2. Track formats & container metadata (reuses the same preparation pass)
val trackGroups = retriever.retrieveTrackGroups().await()
for (i in 0 until trackGroups.length) {
val format = trackGroups[i].getFormat(0)
// Video/Audio properties: format.width, format.height, format.colorInfo …
// Rich metadata: SlowMotionData, MotionPhotoMetadata, Mp4TimestampData …
val metadata = format.metadata
}
// 3. Stream structure & manifest info
val timeline = retriever.retrieveTimeline().await()
}FrameExtractor.Builder(context, mediaItem)
.setSeekParameters(SeekParameters.CLOSEST_SYNC) // Fast keyframe seeking
.setEffects(listOf(Presentation.createForHeight(360))) // GPU downscaling
.setExtractHdrFrames(true) // Optional: HLG HDR or UltraHDR
.build()
.use { extractor ->
// Representative thumbnail
val thumbnail = extractor.getThumbnail().await()
// Frame at specific timestamp (bitmap + actual presentationTimeMs)
val frame = extractor.getFrame(positionMs = 5_000L).await()
}val config = CodecDb.onDevice(context)
.forEncodingFallingBackTo {
constraints -> createFallbackConfig(constraints)
}
.findOptimalSettings()
.forEditing()
.withResolution(1920, 1080, /* frameRate= */ 30.0)
.withAnyCodec()
.targetingQuality(VideoEncoder.QUALITY_LEVEL_VERY_GOOD)
.get()