概要
動画編集アプリのプレビューは、普通の動画再生より難しくなりがちです。
理由はシンプルで、1本の動画をそのまま流せばよいわけではないからです。編集画面では、ユーザーは頻繁にシークしますし、クリップ境界をまたいで再生します。さらに、プレビュー映像だけでなく、タイムラインの再生ヘッドや現在時刻表示も自然に追従していてほしくなります。
このあたりをMedia3で実装していると、Playerを複数持ちたくなる場面が出てきます。ただ、複数のPlayerを導入すると柔軟になる一方で、滑らかな再生を維持するために考えることも増えます。
この記事では、動画編集プレビューがなぜ難しいのかと、複雑にしすぎない設計の考え方を整理します。
1つのPlayerでも編集プレビューは難しい
通常の動画プレイヤーであれば、ExoPlayerにMediaItemを渡して再生するだけでも、最小構成としては成立します。
一方で、編集プレビューではタイムライン上の時刻から「今どのクリップのどの位置を表示するか」を求める必要があります。
たとえば、次のようなタイムラインを考えます。
- Clip A: 0s - 5s
- Clip B: 5s - 8s
- Clip C: 8s - 12s
このとき、タイムライン上の6.2秒を表示したい場合、実際に再生対象になるのはClip Bの1.2秒地点です。
つまり、編集プレビューは単なる連続再生ではなく、「タイムライン時刻を各クリップのローカル時刻に変換する処理」を前提にした仕組みになります。
複数Playerを使いたくなる場面
Media3で実装していると、複数のPlayerを持ちたくなる場面があります。
典型的なのは、現在再生中のクリップとは別に、次のクリップを先読みしておきたいケースです。たとえばmainPlayerでClip Aを再生しながら、別の手段でClip Bの準備を進めておけば、クリップ境界での切り替えをより自然にできそうに見えます。
この発想自体は自然ですし、Media3にもプリロードの仕組みがあります。ただし、複数のPlayerをまたいで滑らかに切り替える構成は、見た目ほど単純ではありません。
各Playerは独立した状態を持つため、seekTo()後の状態遷移やバッファ状況を揃える必要があります。そのため、クリップ境界の切り替えでは違和感が出やすく、実装上の調整ポイントになりやすいです。
難しいのはPlayerの数ではなく「時刻の同期」
複数Playerが難しい理由は、単に制御対象が増えるからではありません。実際に難しいのは、同期すべき対象が増えることです。
編集プレビューでは、少なくとも次のような要素が同じ時刻に追従している必要があります。
- プレビュー映像
- 音声
- タイムラインの再生ヘッド
- 現在時刻表示
- 必要に応じた自動スクロール
ここで、UIがmainPlayer.currentPositionを直接参照し、別の処理が別のPlayerの位置を参照しているような構成にすると、微妙なズレが出やすくなります。
たとえば、映像は進んでいるのに再生ヘッドの更新だけ少し遅れる、といった現象です。数十ミリ秒程度でも、編集画面では意外と気になります。
そのため、実装上はPlayerそのものを基準にするより、「アプリ全体で共有する再生時刻」を1つ持つ構成のほうが扱いやすくなります。
再生時刻は1つに寄せる
設計としては、UIやタイムラインが直接Playerを参照しないほうが整理しやすくなります。
まず、タイムライン基準の再生位置を持つPlaybackStateのような状態を1つ定義し、プレビューもタイムラインもその状態を参照する形にします。
data class PlaybackState(
val timelinePositionMs: Long,
val isPlaying: Boolean
)
そして、UI側はこの時刻を基準に更新します。
fun render(state: PlaybackState) {
timelineView.updatePlayhead(state.timelinePositionMs)
timeLabel.text = formatTime(state.timelinePositionMs)
}
このようにしておくと、内部でどのPlayerが再生を担当しているかが切り替わっても、UI側の基準はぶれにくくなります。
複数Playerは主役にしすぎない
複数のPlayer自体が悪いわけではありません。ただし、すべてを表に出る再生主体として扱うと、設計は複雑になりやすいです。
比較的扱いやすいのは、主役となるPlayerを1つにして、他を補助用途に分ける構成です。
たとえば、次のような役割分担です。
-
mainPlayer: ユーザーに見せるプレビュー再生 - プリロード用の仕組み: 次クリップの準備
- サムネイル生成用の処理: タイムライン表示の補助
このように責務を分けておくと、どこが再生の主体で、どこが補助なのかが明確になります。
一方で、クリップごとに表示担当のPlayerをどんどん切り替え、そのたびにUIの参照先まで変える構成は、境界条件が増えやすく、全体の見通しが悪くなりがちです。
タイムラインビューは全部を毎フレーム更新しない
タイムラインビューを含めると、再生中にすべてを更新したくなります。ただ、そこを頑張りすぎると、今度はUI側が重くなります。
再生中に本当に滑らかであってほしいのは、主に次の2つです。
- 再生ヘッドの移動
- 必要なときの自動スクロール
一方で、クリップ配置やサムネイル、波形などは毎フレーム更新する必要がないことが多いです。ここを分けるだけで、タイムラインビューはかなり軽くなります。
fun onPlaybackTick(positionMs: Long) {
timelineView.updatePlayhead(positionMs)
timelineView.scrollIfNeeded(positionMs)
}
このtickごとにレイアウト全体を作り直さないことが重要です。再生中に全体の再レイアウトまで始めると、滑らかさは崩れやすくなります。
まとめ
Media3で動画編集プレビューを作るときに難しいのは、複数Playerを扱うことそのものより、「プレビュー映像、音声、タイムラインを同じ時刻で揃えること」です。
そのためには、まずタイムライン基準の再生時刻を1つに寄せることが重要です。そのうえで、ユーザーに見える再生主体はできるだけ1つに保ち、他の仕組みはプリロードやサムネイル生成といった補助用途に分けます。さらに、タイムラインビューは必要な部分だけを更新するようにします。
編集プレビューは見た目以上に複雑な機能ですが、Playerを増やす前に「誰が時刻の基準を持つのか」を決めておくと、設計はかなり安定します。ここが曖昧なまま進めると、あとで調整コストが大きくなりやすいです。
参考文献