たくさんの自由帳
Androidのお話
たくさんの自由帳
投稿日 : | 0 日前
文字数(だいたい) : 18593
目次
本題
欲しい要件
既にあるやつじゃだめなの?
MediaMetadataRetriever
getFrameAtTime
getFrameAtIndex
MediaMetadataRetriever は並列処理できない可能性
MediaPlayer + ImageReader
そもそも連続したフレームだったら高速に取り出せるんですか?
動画のフレーム(画像)の話
なぜ既知の解決策が遅いのか
なぜ高速に取り出せると思っているのか
つくる
環境
今回の作戦
MediaCodec とゆかいな仲間たち
OpenGL 周りを AOSP から借りてくる
VideoFrameBitmapExtractor.kt
初期化する処理
映像からフレームを取り出す処理
前回より前の位置にあるフレームを取り出す
前回より後の位置にあるフレームを取り出す
追記
ImageReader から Bitmap を取り出す処理
組み合わせる
Jetpack Compose で作った UI 側で呼び出して使う
使ってみた
ベンチマーク
3枚フレーム取り出してみる
0から3秒まで連続してフレームを取り出してみる
動画からフレームを連続して取り出して保存してみる
連続して取り出すのは得意
苦手なのもある
ソースコードです
おまけ
MediaCodec の出力先を OpenGL 無し ImageReader にするのは辞めておいたほうがいい
Google Pixel 以外で落ちる
取り出したフレームが壊れている
追記:2024/05/30 ImageReader の width と height は決まっている説
追記:2024/06/04
追記:2024/09/16 もうちょっと速くできる
おわりに
どうもこんばんわ。
FLIP*FLOP 〜INNOCENCE OVERCLOCK〜 攻略しました。
めっちゃ あまあまなシナリオ + かわいいヒロイン がいる神ゲーです。ほんとにあまあまでした
おじいちゃん何者なの((?))続編で明かされるんかな
ボクっ娘だ!
元気いっぱいイオちゃんかわいい!!
サブヒロインも可愛いけど攻略できない、そんな・・・
というわけでOPのCD、開け、、ます!OP曲めっちゃいい!
Androidで動画の1コマ1コマを画像として取り出す処理をAndroidでやりたい。
30 fps なら 1秒間に 30 枚画像が切り替わるわけですが、その1枚1枚を切り出したい。
特に、30fps なら 30枚、連続したフレームを取り出すのに特化した処理を自作したい。
理由は後述しますが遅い!
Androidの解決策では遅い動画のサムネイル画像が一回ぽっきりで欲しいとかで、動画のフレームを取得するなら別に遅くてもなんともならないと思うのですが、、、
時間が増加する方向に向かって映像のフレームを取り出す分には、高速にフレームを取り出せる気がするんですよね。以下擬似コード
// 時間が増加する分には速く取れるように作れそう。
val bitmap1 = getVideoFrameBitmap(ms = 1_000)
val bitmap2 = getVideoFrameBitmap(ms = 1_100)
val bitmap3 = getVideoFrameBitmap(ms = 1_200)
val bitmap4 = getVideoFrameBitmap(ms = 1_300)
val bitmap5 = getVideoFrameBitmap(ms = 1_400)ライブラリを使わずに、Androidで完結させたい場合は多分以下のパターンがある
高レベルAPIですね。ffprobe的な使い方から、動画のフレームを取ったりも出来ます。
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen() {
val context = LocalContext.current
val bitmap = remember { mutableStateOf<ImageBitmap?>(null) }
val videoPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri ->
// MediaMetadataRetriever は File からも作れます
uri ?: return@rememberLauncherForActivityResult
val mediaMetadataRetriever = MediaMetadataRetriever().apply {
setDataSource(context, uri)
}
// Bitmap を取り出す
// 引数の単位は Ms ではなく Us です
bitmap.value = mediaMetadataRetriever.getFrameAtTime(13_000_000, MediaMetadataRetriever.OPTION_CLOSEST)?.asImageBitmap()
// もう使わないなら
mediaMetadataRetriever.release()
}
)
Scaffold(
topBar = {
TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) })
}
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
if (bitmap.value != null) {
Image(
modifier = Modifier.fillMaxWidth(),
bitmap = bitmap.value!!,
contentDescription = null
)
}
Button(onClick = { videoPicker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.VideoOnly)) }) {
Text(text = "取り出す")
}
}
}
}すごく簡単に使える。
動画を渡して時間とオプションを指定するだけで、Bitmapとして取れる。
オプションですが
フレームを正確に欲しい場合は、MediaMetadataRetriever.OPTION_CLOSESTを使うしか無いと思うんですが、結構遅いのでちょっと使えないかな。
// 時間が巻き戻らなければ速くフレーム返して欲しい
// これだけでも 1 秒くらいはかかっちゃう
mediaMetadataRetriever.getFrameAtTime(1_000_000L, MediaMetadataRetriever.OPTION_CLOSEST)
mediaMetadataRetriever.getFrameAtTime(1_100_000L, MediaMetadataRetriever.OPTION_CLOSEST)
mediaMetadataRetriever.getFrameAtTime(1_200_000L, MediaMetadataRetriever.OPTION_CLOSEST)こっちのほうがgetFrameAtTimeよりは高速らしいですが、あんまり速くない気がする。
引数には時間ではなく、フレームの番号を渡す必要があります。30fpsなら、1秒間に30枚あるので、、、
全部で何フレームあるかは、METADATA_KEY_VIDEO_FRAME_COUNTで取れるらしいので、欲しい時間のフレーム番号を計算で出せば良さそう。
が、これも私が試した限りあんまり速くないので別に・・・
MediaMetadataRetrieverが何やってるのかあんまりわからないのですが、
なんだかgetFrameAtIndex / getFrameAtTimeが遅いからって並列にしても対して速くないです。
まず直列パターン。
// 3枚取り出す
val time = measureTimeMillis {
repeat(3) {
val timeUs = it * 1_000_000L
val mediaMetadataRetriever = MediaMetadataRetriever().apply {
setDataSource(context, uri)
}
// Bitmap を取り出す
// 引数の単位は Ms ではなく Us です
mediaMetadataRetriever.getFrameAtTime(timeUs, MediaMetadataRetriever.OPTION_CLOSEST)?.asImageBitmap()
// もう使わないなら
mediaMetadataRetriever.release()
}
}
println("time = $time")次に並列のパターン。
scope.launch {
// 3枚取り出す
val time = measureTimeMillis {
(0 until 3).map {
launch {
val timeUs = it * 1_000_000L
val mediaMetadataRetriever = MediaMetadataRetriever().apply {
setDataSource(context, uri)
}
// Bitmap を取り出す
// 引数の単位は Ms ではなく Us です
mediaMetadataRetriever.getFrameAtTime(timeUs, MediaMetadataRetriever.OPTION_CLOSEST)?.asImageBitmap()
// もう使わないなら
mediaMetadataRetriever.release()
}
}.joinAll()
}
println("time = $time ms")
}まず直列パターンの結果ですが、
Pixel 6 Protime = 1090 mstime = 1082 msPixel 8 Protime = 744 mstime = 702 msそしてコレが並列パターンの結果。
Pixel 6 Protime = 1087 mstime = 983 msPixel 8 Protime = 691 mstime = 653 ms若干早いけど誤差なのでは・・・?MediaMetadataRetriever、もしかして内部でインスタンスを共通にしてて切り替えて使ってる?
うーん、おそい!
ネタバレすると、これは自作した後に気付いたんですが、これも結局あんまり早くない
動画プレイヤーのMediaPlayerの出力先にImageReaderを使う方法。MediaPlayerの出力先に普通は、SurfaceViewとか、TextureViewとかを渡しますが、ImageReaderを渡すと画像データとして取ることが出来ます。SurfaceView / TextureViewが画面に表示する物だとしたら、ImageReaderは画像データにしてくれるものでしょうか。MediaRecorderは動画にしてくれるやつです。
(まあTextureViewをキャプチャするのと対して変わらんと思うけど、、、ImageReaderとか言う適役がいるので)
で、で、で、、、MediaPlayer#seekToして、ImageReaderで動画のフレームを画像にすれば高速に取れるのではないかと。
でもだめだったよ。これもあんまり早くない。
並列はこれで達成できるかもしれない。
ただ、MediaPlayer#seekToがMediaMetadataRetrieverのときと同じく、正確性を求めるなら速度が遅くなるみたい。
次のフレームをseekToで指定しても速くなかった。うん。高レベルAPIだし、そりゃそうなるか。
そういえば、MediaPlayer、これ動画を再生するものなので、連続してフレームをBitmapにするとかなら得意なんじゃないだろうか。
動くか分からんけどこんなの。
// TODO 動くかわからない。多分動かない
val imageReader = ImageReader.newInstance(videoWidth, videoHeight, ImageFormat.YUV_420_888, 2)
imageReader.setOnImageAvailableListener({
// 毎フレームごとに呼ばれるんじゃないかな
// 連続して取り出す分には出来そう
val currentPosition = mediaPlayer.currentPosition
val image = imageReader.acquireLatestImage()
val bitmap = image.toBitmap() // toBitmap は自分で書いて...
}, null)
val mediaPlayer = MediaPlayer().apply {
setSurface(imageReader.surface)
setDataSource(videoFile.path)
prepare()
seekTo(1_000)
start() // 再生
}ただ、連続してフレームがとれるというか、毎フレームBitmapを生成することになるので、Bitmapをどっかに置いておかないといけない。
一旦画像にして保存してもいいけど、、、できれば指定した時間のフレームだけ欲しいし、その次のフレームが欲しかったらすぐ返して欲しい。

キーフレームとは?Iフレーム・Pフレーム・Bフレームの違い【GOP】
動画のおける「キーフレーム」の意味について紹介します。動画の仕組みキーフレームについて説明する前に、「動画の仕組み」について簡単に説明します。動画の仕組みは、ザックリ言うと「パラパラ漫画」です。1枚1枚の画像(フレームと呼ぶ)を高速でめくっ
https://aviutl.info/keyframe/
それには動画がどうやって動画を圧縮しているかの話と、キーフレームの話が必要で、しますね。
前も話した気がするけどこのサイトGoogleで見つからない事が多いのでまた書きますね。
(SSGでも動く全文検索検討するかあ~)
動画というのは、画像が切り替わっている用に見えますが、考えてみてください。30fpsだと1秒間に30枚の画像を保存しているのかと。
してないですね。仮に作ったとしても動画ファイルはそんなに大きくなりません。でも30fpsなら30枚分あるはずの画像はどこに行ってしまったのか・・・
小さく出来る理由ですが、前回のフレーム(画像)からの差分のみを動画ファイルへ保存するんですね。
動画というのはほとんど変わらない部分も含まれているわけで、それらは前のフレームを参照してねとすれば、動画ファイルは小さく出来ます。
前のフレームに依存する代わりにファイルを小さく出来ました。
ただ、すべてのフレームを前のフレームに依存させてしまうと、今度は巻き戻しができなくなってしまいます。
ドロイドくん3つのフレームを表示させたい場合、フレーム単体では表示できないので、それよりも前(上の絵では最初)に戻る必要があります。
でも毎回最初に戻っていてはシークがとんでもなく遅くなってしまうので、定期的にキーフレームという、前のフレームに依存しない完全な状態の画像を差し込んでいます。
1秒に一回くらいとかですかね。これなら、大幅に戻ったりする必要がなくなるのでシークも早くなります。
もちろん、動画のコーデックはこれ以外の技術を使って動画のファイルサイズを縮小していますが、今回の高速でフレームを取り出す話しには多分関係ないので飛ばします。
シークしているからでしょう。MediaMetadataRetrieverには4つのオプションがあるといいました。
↑のフレームの話を聞いたら、OPTION_CLOSESTがなんで遅くて、それ以外がなんで早いか。分かる気がしませんか?OPTION_PREVIOUS_SYNC / OPTION_NEXT_SYNC / OPTION_CLOSEST_SYNCはキーフレームを探すのに対して(フレーム単体で画像になっている)、OPTION_CLOSESTはキーフレームからの差分までも見る必要があるため、キーフレームまで移動した後指定時間になるまで進める必要があり、時間がかかるわけです。
そして、OPTION_CLOSESTの場合、おそらく毎回キーフレームまで戻っている?ために遅くなっている?MediaMetadataRetrieverもMediaPlayerも多分そう。
キーフレームまで戻るから遅いのでは。巻き戻すわけじゃないから戻らないように時前で書けばいいのでは???
絶対戻らないという前提があれば、連続したフレームを取り出すのも早いんじゃないかという話です。
// 毎フレーム、巻き戻ししなければ速く取得できる処理が作れるのではないか。
getVideoFrameBitmap(ms = 16)
getVideoFrameBitmap(ms = 33)
getVideoFrameBitmap(ms = 66)
getVideoFrameBitmap(ms = 99)というわけで、今回は動画のフレームをBitmapとして取り出す処理。(MediaMetadataRetriever#getFrameAtTimeの代替)、
かつキーフレームまで戻らない仕様を込めて自前で作ってみようと思います。
(ちなみに)
(MediaMetadataRetrieverは指定した時間が、前回のフレームの次のフレームだったとしても、OPTION_CLOSEST指定している限りキーフレームまで戻っているのが悪いと言われると微妙。)
(次のフレームなら効率が悪いと思いますが、前回のフレームよりも前に戻る場合は、キーフレームまで戻るこの方法が必要なのでまあ仕方ないところがある。)
前置きが長過ぎる
| Android Studio | Android Studio Hedgehog 2023.1.1 Patch 2 |
| 端末 | Pixel 8 Pro / Xperia 1 V |
| 言語 | Koltin / OpenGL |
一応MediaCodecの出力先をImageReaderにするだけで動くので、MediaCodec系といっしょに使われるOpenGLとかは要らないはずですが(嘘です。なんか間違えたのかGoogle Pixel以外で落ちました。OpenGLを噛ませないと動きません。落ちた話は後半でします。)OpenGLを一枚噛ませるとさせておくとより安心です
→ 2024/05/30 追記もあります。ImageReader何もわからない。
前回の位置から、巻き戻っていない場合は、コンテナから次のデータを取り出してデコーダーに渡すようにします。
これをするため、フレームが取得し終わってもMediaCodec / MediaExtractorはそのままにしておく必要があります(待機状態というのでしょうか・・)
AVCを生データにAACをPCMにmp4 / webm等のコンテナフォーマットから、パラメーターや実際のデータを取り出すやつSurfaceViewが画面に表示するやつなら、これは静止画に変換するやつMediaCodecで出てきたフレームを加工したりできるMediaCodecの出力先SurfaceはOpenGLを使ったInputSurfaceを経由させるのがお作法らしい
何やってるか私もわからないのでAOSPから借りてくることにします。
私がやったのはKotlin化くらいです。
AndroidVideoFrameFastNextExtractor/app/src/main/java/io/github/takusan23/androidvideoframefastnextextractor/gl/TextureRenderer.kt at master · takusan23/AndroidVideoFrameFastNextExtractor
動画からフレームを取得する処理。巻き戻ししなければ高速でフレームが取得できます。. Contribute to takusan23/AndroidVideoFrameFastNextExtractor development by creating an account on GitHub.
https://github.com/takusan23/AndroidVideoFrameFastNextExtractor/blob/master/app/src/main/java/io/github/takusan23/androidvideoframefastnextextractor/gl/TextureRenderer.kt
AndroidVideoFrameFastNextExtractor/app/src/main/java/io/github/takusan23/androidvideoframefastnextextractor/gl/InputSurface.kt at master · takusan23/AndroidVideoFrameFastNextExtractor
動画からフレームを取得する処理。巻き戻ししなければ高速でフレームが取得できます。. Contribute to takusan23/AndroidVideoFrameFastNextExtractor development by creating an account on GitHub.
https://github.com/takusan23/AndroidVideoFrameFastNextExtractor/blob/master/app/src/main/java/io/github/takusan23/androidvideoframefastnextextractor/gl/InputSurface.kt
適当にクラスを作って、以下の関数を用意します。
それぞれの中身はこれから書きます。
newSingleThreadContextがなんで必要かは前書いたのでそっち見てまあ言うとnewSingleThreadContextってやつを使うことで、常に同じスレッドで処理してくれるDispatcherを作れます。これをwithContextとかで使えばいい。
あ、でも複数のVideoFrameBitmapExtractor()のインスタンスを作って使う場合は、openGlRenderDispatcherをそれぞれ作らないといけないので、companion objectに置いたらダメですね。
/**
* [MediaCodec]と[MediaExtractor]、[ImageReader]を使って高速に動画からフレームを取り出す
*/
class VideoFrameBitmapExtractor {
/** MediaCodec デコーダー */
private var decodeMediaCodec: MediaCodec? = null
/** Extractor */
private var mediaExtractor: MediaExtractor? = null
/** 映像デコーダーから Bitmap として取り出すための ImageReader */
private var imageReader: ImageReader? = null
/** 最後の[getVideoFrameBitmap]で取得したフレームの位置 */
private var latestDecodePositionMs = 0L
/** 前回のシーク位置 */
private var prevSeekToMs = -1L
/** 前回[getImageReaderBitmap]で作成した Bitmap */
private var prevBitmap: Bitmap? = null
/**
* デコーダーを初期化する
*
* @param uri 動画ファイル
*/
suspend fun prepareDecoder(
context: Context,
uri: Uri,
) = withContext(Dispatchers.IO) {
// todo
}
/**
* 指定位置の動画のフレームを取得して、[Bitmap]で返す
*
* @param seekToMs シーク位置
* @return Bitmap
*/
suspend fun getVideoFrameBitmap(
seekToMs: Long
): Bitmap = withContext(Dispatchers.Default) {
// TODO
}
/** 破棄する */
fun destroy() {
decodeMediaCodec?.release()
mediaExtractor?.release()
imageReader?.close()
inputSurface?.release()
}
companion object {
/** MediaCodec タイムアウト */
private const val TIMEOUT_US = 10_000L
/** OpenGL 用に用意した描画用スレッド。Kotlin coroutines では Dispatcher を切り替えて使う */
@OptIn(DelicateCoroutinesApi::class)
private val openGlRenderDispatcher = newSingleThreadContext("openGlRenderDispatcher")
}
}prepareDecoder関数の中身です。ContextとUriはJetpack Composeで作るUI側で貰えるので
trackIndex = ...の部分は、mp4 / webmから映像トラックを探してselectTrackします。
音声トラックと映像トラックで2つしか無いと思いますが。
ImageReaderですが、MediaCodecで使う場合はImageFormat.YUV_420_888じゃないとだめっぽいです。
/**
* デコーダーを初期化する
*
* @param uri 動画ファイル
*/
suspend fun prepareDecoder(
context: Context,
uri: Uri,
) = withContext(Dispatchers.IO) {
// コンテナからメタデータを取り出す
val mediaExtractor = MediaExtractor().apply {
context.contentResolver.openFileDescriptor(uri, "r")?.use {
setDataSource(it.fileDescriptor)
}
}
this@VideoFrameBitmapExtractor.mediaExtractor = mediaExtractor
// 映像トラックを探して指定する。音声と映像で2️個入ってるので
val trackIndex = (0 until mediaExtractor.trackCount)
.map { index -> mediaExtractor.getTrackFormat(index) }
.indexOfFirst { mediaFormat -> mediaFormat.getString(MediaFormat.KEY_MIME)?.startsWith("video/") == true }
mediaExtractor.selectTrack(trackIndex)
// デコーダーの用意
val mediaFormat = mediaExtractor.getTrackFormat(trackIndex)
val codecName = mediaFormat.getString(MediaFormat.KEY_MIME)!!
val videoHeight = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT)
val videoWidth = mediaFormat.getInteger(MediaFormat.KEY_WIDTH)
// Surface 経由で Bitmap が取れる ImageReader つくる
imageReader = ImageReader.newInstance(videoWidth, videoHeight, PixelFormat.RGBA_8888, 2)
// 描画用スレッドに切り替える
withContext(openGlRenderDispatcher) {
// MediaCodec と ImageReader の間に OpenGL を経由させる
// 経由させないと、Google Pixel 以外(Snapdragon 端末とか)で動かなかった
this@VideoFrameBitmapExtractor.inputSurface = InputSurface(
surface = imageReader!!.surface,
textureRenderer = TextureRenderer()
)
inputSurface!!.makeCurrent()
inputSurface!!.createRender()
}
// 映像デコーダー起動
// デコード結果を ImageReader に流す
decodeMediaCodec = MediaCodec.createDecoderByType(codecName).apply {
configure(mediaFormat, inputSurface!!.drawSurface, null, 0)
}
decodeMediaCodec!!.start()
}前回フレームを取り出した再生位置よりも前の位置のを取り出す処理と、後の位置のを取り出す処理で2つ処理を分けたほうが良さそう。
|-------◯----|
↑前回取り出したフレームの位置よりも前にある場合は、もうこれは仕方ないので、一旦キーフレームまで戻って、指定時間になるまでコンテナから取り出してデコードを続けます。
とりあえず前のキーフレームまでシークして待てばいいので、後の位置よりも簡単ですね。
一点、現時点の再生位置よりも巻き戻すシークの場合MediaCodec#flushしないとだめっぽい?
試した感じ、flush()呼ばないと巻き戻らないんだよね。flush()を呼ぶ場合、MediaCodec#dequeueInputBufferで取ったバッファのインデックスを、MediaCodec#queueInputBufferに渡してMediaCodecに返却してからflushを呼ぶようにしましょうね。
(MediaCodec#dequeueInputBufferを呼びっぱなしにしてflushすると怒られます)
MediaCodecのドキュメントついに役に立つのか!)MediaCodec | API reference | Android Developers
https://developer.android.com/reference/android/media/MediaCodec
あとはwhileで欲しい時間のフレームが来るまで繰り返すだけです。readSampleDataで取り出してqueueInputBufferでデコーダーに詰める。デコードできたかどうかはdequeueOutputBufferを呼び出して、データが来ていればSurfaceに描画です。
単位がMsじゃなくてUsなので注意。
/**
* 今の再生位置よりも前の位置にシークして、指定した時間のフレームまでデコードする。
* 指定した時間のフレームがキーフレームじゃない場合は、キーフレームまでさらに巻き戻すので、ちょっと時間がかかります。
*
* @param seekToMs シーク位置
*/
private suspend fun awaitSeekToPrevDecode(
seekToMs: Long
) = withContext(Dispatchers.Default) {
val decodeMediaCodec = decodeMediaCodec!!
val mediaExtractor = mediaExtractor!!
val inputSurface = inputSurface!!
// シークする。SEEK_TO_PREVIOUS_SYNC なので、シーク位置にキーフレームがない場合はキーフレームがある場所まで戻る
mediaExtractor.seekTo(seekToMs * 1000, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
// エンコードサれたデータを順番通りに送るわけではない(隣接したデータじゃない)ので flush する
decodeMediaCodec.flush()
// デコーダーに渡す
var isRunning = true
val bufferInfo = MediaCodec.BufferInfo()
while (isRunning) {
// キャンセル時
if (!isActive) break
// コンテナフォーマットからサンプルを取り出し、デコーダーに渡す
// while で繰り返しているのは、シーク位置がキーフレームのため戻った場合に、狙った時間のフレームが表示されるまで繰り返しデコーダーに渡すため
val inputBufferIndex = decodeMediaCodec.dequeueInputBuffer(TIMEOUT_US)
if (inputBufferIndex >= 0) {
val inputBuffer = decodeMediaCodec.getInputBuffer(inputBufferIndex)!!
// デコーダーへ流す
val size = mediaExtractor.readSampleData(inputBuffer, 0)
decodeMediaCodec.queueInputBuffer(inputBufferIndex, 0, size, mediaExtractor.sampleTime, 0)
// 狙ったフレームになるまでデータを進める
mediaExtractor.advance()
}
// デコーダーから映像を受け取る部分
var isDecoderOutputAvailable = true
while (isDecoderOutputAvailable) {
// デコード結果が来ているか
val outputBufferIndex = decodeMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US)
when {
outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER -> {
// もう無い時
isDecoderOutputAvailable = false
}
outputBufferIndex >= 0 -> {
// ImageReader ( Surface ) に描画する
val doRender = bufferInfo.size != 0
decodeMediaCodec.releaseOutputBuffer(outputBufferIndex, doRender)
// OpenGL で描画して、ImageReader で撮影する
// OpenGL 描画用スレッドに切り替えてから、swapBuffers とかやる
withContext(openGlRenderDispatcher) {
if (doRender) {
var errorWait = false
try {
inputSurface.awaitNewImage()
} catch (e: Exception) {
errorWait = true
}
if (!errorWait) {
inputSurface.drawImage()
inputSurface.setPresentationTime(bufferInfo.presentationTimeUs * 1000)
inputSurface.swapBuffers()
}
}
}
// 欲しいフレームの時間に到達した場合、ループを抜ける
val presentationTimeMs = bufferInfo.presentationTimeUs / 1000
if (seekToMs <= presentationTimeMs) {
isRunning = false
latestDecodePositionMs = presentationTimeMs
}
}
}
}
}
}2024/03/28 追記。コード間違えてた、対応しないと無限ループに陥ります。詳しくは後述
|-------◯----|
↑さて、MediaMetadataRetriever#getFrameAtTimeにはない、巻き戻さなければキーフレームまで戻らないを実装していきます。
が、が、が、巻き戻さなければなんですが、これだと前回よりもかけ離れた先にある場所へシークするのが遅くなってしまいます。連続したフレームの取得なら早くなりますが、
遠い場所へシークする場合は近くのキーフレームまでシークしたほうが早いです。(これがないと前回からの差分を全部取り出すので効率が悪い)
というわけで、欲しい位置のフレームの取得よりも先に、キーフレームが出現した場合は一気に近い位置までシークするような処理を書きました。
(前回よりも数フレーム先のフレームなら、キーフレームまでシークせずに取り出せるので高速ですが、次次...あれ先にキーフレームが来ちゃうの?ってくらい離れていると逆に一気にシークした方が良い)
なんか手こずったけどなんとかなりました(MediaExtractor#getSampleTimeとBufferInfo#getPresentationTimeUs()って微妙に違うのか・・)。
それ以外は↑のコードと大体一緒なので説明は省略で。
/**
* 今の再生位置よりも後の位置にシークして、指定した時間のフレームまでデコードする。
*
* また高速化のため、まず[seekToMs]へシークするのではなく、次のキーフレームまでデータをデコーダーへ渡します。
* この間に[seekToMs]のフレームがあればシークしません。
* これにより、キーフレームまで戻る必要がなくなり、連続してフレームを取得する場合は高速に取得できます。
*
* @param seekToMs シーク位置
*/
private suspend fun awaitSeekToNextDecode(
seekToMs: Long
) = withContext(Dispatchers.Default) {
val decodeMediaCodec = decodeMediaCodec!!
val mediaExtractor = mediaExtractor!!
val inputSurface = inputSurface!!
var isRunning = isActive
val bufferInfo = MediaCodec.BufferInfo()
while (isRunning) {
// キャンセル時
if (!isActive) break
// コンテナフォーマットからサンプルを取り出し、デコーダーに渡す
// シークしないことで、連続してフレームを取得する場合にキーフレームまで戻る必要がなくなり、早くなる
val inputBufferIndex = decodeMediaCodec.dequeueInputBuffer(TIMEOUT_US)
if (inputBufferIndex >= 0) {
// デコーダーへ流す
val inputBuffer = decodeMediaCodec.getInputBuffer(inputBufferIndex)!!
val size = mediaExtractor.readSampleData(inputBuffer, 0)
decodeMediaCodec.queueInputBuffer(inputBufferIndex, 0, size, mediaExtractor.sampleTime, 0)
}
// デコーダーから映像を受け取る部分
var isDecoderOutputAvailable = true
while (isDecoderOutputAvailable) {
// デコード結果が来ているか
val outputBufferIndex = decodeMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US)
when {
outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER -> {
// もう無い時
isDecoderOutputAvailable = false
}
outputBufferIndex >= 0 -> {
// ImageReader ( Surface ) に描画する
val doRender = bufferInfo.size != 0
decodeMediaCodec.releaseOutputBuffer(outputBufferIndex, doRender)
// OpenGL で描画して、ImageReader で撮影する
// OpenGL 描画用スレッドに切り替えてから、swapBuffers とかやる
withContext(openGlRenderDispatcher) {
if (doRender) {
var errorWait = false
try {
inputSurface.awaitNewImage()
} catch (e: Exception) {
errorWait = true
}
if (!errorWait) {
inputSurface.drawImage()
inputSurface.setPresentationTime(bufferInfo.presentationTimeUs * 1000)
inputSurface.swapBuffers()
}
}
}
// 欲しいフレームの時間に到達した場合、ループを抜ける
val presentationTimeMs = bufferInfo.presentationTimeUs / 1000
if (seekToMs <= presentationTimeMs) {
isRunning = false
latestDecodePositionMs = presentationTimeMs
}
}
}
}
// 次に進める
mediaExtractor.advance()
// 欲しいフレームが前回の呼び出しと連続していないときの処理
// 例えば、前回の取得位置よりもさらに数秒以上先にシークした場合、指定位置になるまで待ってたら遅くなるので、数秒先にあるキーフレームまでシークする
// で、このシークが必要かどうかの判定がこれ。数秒先をリクエストした結果、欲しいフレームが来るよりも先にキーフレームが来てしまった
// この場合は一気にシーク位置に一番近いキーフレームまで進める
// ただし、キーフレームが来ているサンプルの時間を比べて、欲しいフレームの位置の方が大きくなっていることを確認してから。
// デコーダーの時間 presentationTimeUs と、MediaExtractor の sampleTime は同じじゃない?らしく、sampleTime の方がデコーダーの時間より早くなるので注意
val isKeyFrame = mediaExtractor.sampleFlags and MediaExtractor.SAMPLE_FLAG_SYNC != 0
val currentSampleTimeMs = mediaExtractor.sampleTime / 1000
if (isKeyFrame && currentSampleTimeMs < seekToMs) {
mediaExtractor.seekTo(seekToMs * 1000, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
}
}
}MediaExtractorからもうデータが取れないときの対応が必要です。
これしないと、最後まで取得しようとした時に多分無限ループになります。
もうフレームがないときの対応 · takusan23/AndroidVideoFrameFastNextExtractor@bceea63
https://github.com/takusan23/AndroidVideoFrameFastNextExtractor/commit/bceea63d9bb12616eea65a261d8c309900c9c0ff
説明すると、
getVideoFrameBitmapのBitmapをnullableにする。
suspend fun getVideoFrameBitmap(
seekToMs: Long
- ): Bitmap = withContext(Dispatchers.Default) {
+ ): Bitmap? = withContext(Dispatchers.Default) {getVideoFrameBitmapのelseでフレームがないならgetImageReaderBitmapを呼ばないように。
else -> {
// 巻き戻しでも無く、フレームを取り出す必要がある
awaitSeekToNextDecode(seekToMs)
- getImageReaderBitmap()
+ // 巻き戻しでも無く、フレームを取り出す必要がある
+ val hasData = awaitSeekToNextDecode(seekToMs)
+ if (hasData) getImageReaderBitmap() else null
}awaitSeekToNextDecodeがBooleanを返せるようにします。trueならフレームがある(getImageReaderBitmapが呼び出せる)、falseなら無いです。
private suspend fun awaitSeekToNextDecode(
seekToMs: Long
- ) = withContext(Dispatchers.Default) {
+ ): Boolean = withContext(Dispatchers.Default) {awaitSeekToNextDecodeのループ直前で、MediaExtractor#getSampleTime()を呼び出して、-1(もうデータがない)場合は何もせず、false を返します。
val decodeMediaCodec = decodeMediaCodec!!
val mediaExtractor = mediaExtractor!!
val inputSurface = inputSurface!!
+ // advance() で false を返したことがある場合、もうデータがない。getSampleTime も -1 になる。
+ if (mediaExtractor.sampleTime == -1L) {
+ return@withContext false
+ }
+
var isRunning = isActive
val bufferInfo = MediaCodec.BufferInfo()MediaExtractor#advance()の返り値を見て、もうデータがない場合(false)は、ループを抜けるようにします。
- // 次に進める
- mediaExtractor.advance()
+ // 次に進める。advance() が false の場合はもうデータがないので、break する。
+ val isEndOfFile = !mediaExtractor.advance()
+ if (isEndOfFile) {
+ break
+ }これで、無限ループは回避出来るはず。nullableになった関係で、呼び出し箇所も修正が必要かもです。
- bitmap.value = videoFrameBitmapExtractor.getVideoFrameBitmap(currentPositionMs.value).asImageBitmap()
+ bitmap.value = videoFrameBitmapExtractor.getVideoFrameBitmap(currentPositionMs.value)?.asImageBitmap()acquireLatestImageして、Bufferとって、Bitmapにしています。
/** [imageReader]から[Bitmap]を取り出す */
private suspend fun getImageReaderBitmap(): Bitmap = withContext(Dispatchers.Default) {
val image = imageReader!!.acquireLatestImage()
val width = image.width
val height = image.height
val buffer = image.planes.first().buffer
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
bitmap.copyPixelsFromBuffer(buffer)
prevBitmap = bitmap
// Image を close する
image.close()
return@withContext bitmap
}awaitSeekToNextDecodeとかawaitSeekToPrevDecodeとかgetImageReaderBitmapを組み合わせて、動画のフレームを取り出す関数を完成させます。
あ~
シーク不要の部分で、なんで前回の Bitmapを返しているかなんですが、動画フレームの枚数よりも多くのフレームをリクエストしてきた時に前回のフレームを返すためのものです。
どういうことかと言うと、30fpsなら1秒間に30枚までならフレームを取り出せますが、1秒間に60枚取ろうとするとフレームの枚数よりも多くのフレームを要求することになり、余計にデコードが進んでいってしまい、ズレていってしまいます。
(30fpsなら33ミリ秒毎に取り出す処理なら問題ないけど、16ミリ秒毎に取り出すと、フレームの枚数よりも多くのフレームを取るから壊れちゃう。)
フレームの枚数よりも多くのフレームを要求してきても壊れないように、最後取得したフレームの位置を見て、もし最後取得したフレームよりも前の位置だったら前回のBitmapを返すようにしました。
/**
* 指定位置の動画のフレームを取得して、[Bitmap]で返す
*
* @param seekToMs シーク位置
* @return Bitmap
*/
suspend fun getVideoFrameBitmap(
seekToMs: Long
): Bitmap = withContext(Dispatchers.Default) {
val videoFrameBitmap = when {
// 現在の再生位置よりも戻る方向に(巻き戻し)した場合
seekToMs < prevSeekToMs -> {
awaitSeekToPrevDecode(seekToMs)
getImageReaderBitmap()
}
// シーク不要
// 例えば 30fps なら 33ms 毎なら新しい Bitmap を返す必要があるが、 16ms 毎に要求されたら Bitmap 変化しないので
// つまり映像のフレームレートよりも高頻度で Bitmap が要求されたら、前回取得した Bitmap がそのまま使い回せる
seekToMs < latestDecodePositionMs && prevBitmap != null -> {
prevBitmap!!
}
else -> {
// 巻き戻しでも無く、フレームを取り出す必要がある
awaitSeekToNextDecode(seekToMs)
getImageReaderBitmap()
}
}
prevSeekToMs = seekToMs
return@withContext videoFrameBitmap
}ボタンと画像を表示するやつをおいて、ボタンを押したら動画を選ぶやつを開いて、選んだら↑の処理を呼び出す。
これで一通り出来たかな。ボタンを押して動画を選べば出てきます。
fun VideoFrameBitmapExtractorScreen() {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val bitmap = remember { mutableStateOf<ImageBitmap?>(null) }
// フレームを取り出すやつと取り出した位置
val currentPositionMs = remember { mutableStateOf(0L) }
val videoFrameBitmapExtractor = remember { VideoFrameBitmapExtractor() }
val videoPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri ->
uri ?: return@rememberLauncherForActivityResult
scope.launch {
videoFrameBitmapExtractor.prepareDecoder(context, uri)
currentPositionMs.value = 1000
bitmap.value = videoFrameBitmapExtractor.getVideoFrameBitmap(currentPositionMs.value).asImageBitmap()
}
}
)
// 破棄時
DisposableEffect(key1 = Unit) {
onDispose { videoFrameBitmapExtractor.destroy() }
}
Scaffold(
topBar = {
TopAppBar(title = { Text(text = "VideoFrameBitmapExtractorScreen") })
}
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
if (bitmap.value != null) {
Image(
modifier = Modifier.fillMaxWidth(),
bitmap = bitmap.value!!,
contentDescription = null
)
}
Text(text = "currentPositionMs = ${currentPositionMs.value}")
Button(onClick = {
videoPicker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.VideoOnly))
}) { Text(text = "取り出す") }
Button(onClick = {
scope.launch {
currentPositionMs.value += 16
bitmap.value = videoFrameBitmapExtractor.getVideoFrameBitmap(currentPositionMs.value).asImageBitmap()
}
}) { Text(text = "16ms 進める") }
}
}
}これを好きなところで表示してください。
好きな場所でいいので、今回は検証のためにMainActivity.ktとかで。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AndroidVideoFrameFastNextExtractorTheme {
VideoFrameBitmapExtractorScreen()
}
}
}
}ちゃんとImage()に映像のフレームが写っています。
ちょっとずつだけど進めるを押せば動画も進んでいそう。
頑張って作ったので、MediaMetadataRetriever#getFrameAtTimeよりも早くないと困るぞ・・・!
今回は正確なフレームが欲しいので、MediaMetadataRetriever#getFrameAtTimeの第2引数には遅いですがMediaMetadataRetriever.OPTION_CLOSESTを指定します。
意地悪ですね・・
とりあえず連続して3回取り出してみる。
val totalTimeVideoFrameBitmapExtractor = remember { mutableStateOf(0L) }
val totalTimeMetadataRetriever = remember { mutableStateOf(0L) }
fun startVideoFrameBitmapExtractorBenchMark(uri: Uri?) {
uri ?: return
scope.launch(Dispatchers.Default) {
totalTimeVideoFrameBitmapExtractor.value = 0
totalTimeVideoFrameBitmapExtractor.value = measureTimeMillis {
VideoFrameBitmapExtractor().apply {
prepareDecoder(context, uri)
getVideoFrameBitmap(1_000)
getVideoFrameBitmap(1_100)
getVideoFrameBitmap(1_200)
}
}
}
}
fun startMediaMetadataRetrieverBenchMark(uri: Uri?) {
uri ?: return
scope.launch(Dispatchers.Default) {
totalTimeMetadataRetriever.value = 0
totalTimeMetadataRetriever.value = measureTimeMillis {
MediaMetadataRetriever().apply {
setDataSource(context, uri)
getFrameAtTime(1_000_000, MediaMetadataRetriever.OPTION_CLOSEST)
getFrameAtTime(1_100_000, MediaMetadataRetriever.OPTION_CLOSEST)
getFrameAtTime(1_200_000, MediaMetadataRetriever.OPTION_CLOSEST)
}
}
}
}うーん?Xperiaに関しては自作しないほうが速いぞ・・?
なんならGoogle Pixelの方も若干速いくらいで誤差っちゃ誤差かもしれない
VideoFrameBitmapExtractorMediaMetadataRetriever#getFrameAtTimeVideoFrameBitmapExtractorMediaMetadataRetriever#getFrameAtTimeVideoFrameBitmapExtractorMediaMetadataRetriever#getFrameAtTimeい、、いや、連続してフレームを取る際に早くなっていればええんや。
こっちが早くなっていれば万々歳
// 0 から 3 秒まで、33 ずつ増やした数字の配列(30fps = 33ms なので)
val BenchMarkFramePositionMsList = (0 until 3_000L step 33)
val totalTimeVideoFrameBitmapExtractor = remember { mutableStateOf(0L) }
val totalTimeMetadataRetriever = remember { mutableStateOf(0L) }
fun startVideoFrameBitmapExtractorBenchMark(uri: Uri?) {
uri ?: return
scope.launch(Dispatchers.Default) {
totalTimeVideoFrameBitmapExtractor.value = 0
totalTimeVideoFrameBitmapExtractor.value = measureTimeMillis {
VideoFrameBitmapExtractor().apply {
prepareDecoder(context, uri)
BenchMarkFramePositionMsList.forEach { framePositionMs ->
println("framePositionMs = $framePositionMs")
getVideoFrameBitmap(framePositionMs)
}
}
}
}
}
fun startMediaMetadataRetrieverBenchMark(uri: Uri?) {
uri ?: return
scope.launch(Dispatchers.Default) {
totalTimeMetadataRetriever.value = 0
totalTimeMetadataRetriever.value = measureTimeMillis {
MediaMetadataRetriever().apply {
setDataSource(context, uri)
BenchMarkFramePositionMsList.forEach { framePositionMs ->
println("framePositionMs = $framePositionMs")
getFrameAtTime(framePositionMs * 1000, MediaMetadataRetriever.OPTION_CLOSEST)
}
}
}
}
}結果はこちらです。
連続して取得する方はかなり速いです。まあ巻き戻ししなければ速く取れるように作っているのでそれはそうなのですが。
うれしい!ハッピーハッピーハッピー(猫ぴょんぴょん)
VideoFrameBitmapExtractorMediaMetadataRetriever#getFrameAtTimeVideoFrameBitmapExtractorMediaMetadataRetriever#getFrameAtTimeVideoFrameBitmapExtractorMediaMetadataRetriever#getFrameAtTime連続して取り出して保存する処理を書きました。
↑で書いたBitmap取り出しした後MediaStoreを使って写真フォルダに保存する処理が入ってます。多分保存処理があんまり速度でないんですけど、、、
巻き戻ししなければキーフレームまで戻らないので、次のフレームの取得は早くなります。(コンテナから次のデータ取ってデコーダーに入れてでてくるのを待てば良い)
試した感じかなりいい成績ですよ。
連続して取り出さない場合はMediaMetadataRetrieverの方が早くなることがあります。(1秒で1フレームずつ取り出すとか)
あと巻き戻す場合は完敗だとおもいます。
こっから先は知見の共有なので、本編とは関係ないです。
私が試した限り動かなかった。
あと動画によってはGoogle Pixelでもぶっ壊れているフレームを吐き出してた。。。
Google Pixel で動いていれば他でも動くと思ってた時期が私にもありました
私の名前は ImageReader です。Google Pixel を使ってます(それ以外では動かないので :( )
素直にMediaCodecの出力先をImageReaderにしたら、Google Pixel以外で落ちた。
動かないの書いても無駄ですが一応。MediaCodecで使うImageReaderはYUV_420_888にする必要があります。
// Surface 経由で Bitmap が取れる ImageReader つくる
imageReader = ImageReader.newInstance(videoWidth, videoHeight, ImageFormat.YUV_420_888, 2)
// 映像デコーダー起動
// デコード結果を ImageReader に流す
decodeMediaCodec = MediaCodec.createDecoderByType(codecName).apply {
configure(mediaFormat, imageReader!!.surface, null, 0)
}
decodeMediaCodec!!.start()ただ、Google Pixel以外の端末(Qualcomm Snapdragon 搭載端末?)だとなんか落ちて、
しかもネイティブの部分(C++ か何かで書かれてる部分)で落ちているのでかなり厳しい雰囲気。
java_vm_ext.cc:591] JNI DETECTED ERROR IN APPLICATION: non-zero capacity for nullptr pointer: 1
java_vm_ext.cc:591] in call to NewDirectByteBuffer
java_vm_ext.cc:591] from android.media.ImageReader$SurfaceImage$SurfacePlane[] android.media.ImageReader$SurfaceImage.nativeCreatePlanes(int, int, long)
runtime.cc:691] Runtime aborting...
runtime.cc:691] Dumping all threads without mutator lock held
runtime.cc:691] All threads:
runtime.cc:691] DALVIK THREADS (26):
runtime.cc:691] "main" prio=10 tid=1 Nativeというわけで調べたら、デコーダーに渡すMediaFormatで、mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible)を指定すれば直るって!
Failed to initialize decoder when using ImageReader, whereas same video can be successfully played back · Issue #8920 · google/ExoPlayer
I'm trying to extract a single frame from a video by using ImageReader. However, my target device keeps failing to initialize its decoder: this is strange, because when I'm trying to playback the s...
https://github.com/google/ExoPlayer/issues/8920
// Surface 経由で Bitmap が取れる ImageReader つくる
imageReader = ImageReader.newInstance(videoWidth, videoHeight, ImageFormat.YUV_420_888, 2)
// 映像デコーダー起動
// デコード結果を ImageReader に流す
decodeMediaCodec = MediaCodec.createDecoderByType(codecName).apply {
// Google Pixel 以外(Snapdragon 搭載機?)でおちるので
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible)
configure(mediaFormat, imageReader!!.surface, null, 0)
}
decodeMediaCodec!!.start()でもこれもだめで、クラッシュこそしなくなりましたが、
画像がぶっ壊れている。ちゃんと出てくる画像もあるけど大体失敗してる。
ちなみにPixelでも失敗したのでMediaCodecとかImageReader何もわからない。
解決策があるのか不明ですが、、、結局OpenGLを一枚噛ませたら直ったのでもうそれで。MediaCodec周りはSurface直指定よりOpenGLを噛ませておくと安牌。なんだろう、SoC (というか GPU ?)の違いなのかな。
あ、ちなみにもしこれがうまく行っても、YUV_420_888をBitmapにするのが面倒そう。
MediaCodecの映像入出力にはSurface以外にByteBuffer(ByteArray)も使えますが、ByteBufferだと色の面倒も自前で調整しないといけない???
スマホのSoC違ったら動かないとか嫌すぎる・・・
それに比べるとOpenGL周りの用意は面倒なものの、AndroidのSurfaceTexture、デコーダーから出てくるこの色?カラーフォーマット?の扱い(YUVとかRGBとか)を勝手に吸収してくれている可能性。stackoverflowだとその事に関して言及してるんだけど、公式だとどこでSurfaceのカラーフォーマットの話書いてあるんだろうか。
Android MediaCodec output format: GLES External Texture (YUV / NV12) to GLES Texture (RGB)
I am currently trying to develop a video player on Android, but am struggling with color formats. Context: I extract and decode a video through the standard combinaison of MediaExtractor/MediaCodec.
https://stackoverflow.com/questions/46244179/android-mediacodec-output-format-gles-external-texture-yuv-nv12-to-gles-tex
Can the Media Codec decoders output RGB-like formats?
I am trying to convert a VP9 video using the Android Media Codec. When I set the KEY_COLOR_FORMAT of the format to something other than YUV formats, I get the following error: "[OMX.qcom.video.dec...
https://stackoverflow.com/questions/60748942/can-the-media-codec-decoders-output-rgb-like-formats
OpenGL ES周りは厳しいけど(AOSPコピペで何がなんだかわからない)、けど、それなりのメリットはありそうです。
あとフラグメントシェーダーで加工できるのもメリットだけど難しそう。
ImageReaderのドキュメントには乗ってませんが、width / heightの値は決まった数字以外で動かないっぽい?
変な解像度videoWidth = 1104 / videoHeight = 2560の場合に出力された映像がぐちゃぐちゃになっちゃった。
調べてもよくわからないので、色々試した感じ、1280x720とかの解像度は動く。けどメジャーじゃない、中途半端な数字では動かない。MediaCodecの噂では、16の倍数じゃないといけないとかで、16で割れるかのチェックを入れてみたんですけどそれもダメそうで。結局動く値に丸めることにした。
面倒なので縦も横も同じ正方形に、ImageReaderから取り出したBitmapをBitmap#scaleで元のサイズに戻すのが、今のところ安定している。。。
// しかし、一部の縦動画(画面録画)を入れるとどうしても乱れてしまう。
// Google Pixel の場合は、縦と横にを 16 で割り切れる数字にすることで修正できたが、Snapdragon は直らなかった。
// ・・・・
// Snapdragon がどうやっても直んないので、別の方法を取る。
// 色々いじってみた結果、Snapdragon も 320 / 480 / 720 / 1280 / 1920 / 2560 / 3840 とかのキリがいい数字は何故か動くので、もうこの値を縦と横に適用する。
// その後元あった Bitmap のサイズに戻す。もう何もわからない。なんだよこれ・・
val originWidth = videoWidth
val originHeight = videoHeight
val maxSize = maxOf(videoWidth, videoHeight)
val imageReaderSize = when {
maxSize < 320 -> 320
maxSize < 480 -> 480
maxSize < 720 -> 720
maxSize < 1280 -> 1280
maxSize < 1920 -> 1920
maxSize < 2560 -> 2560
maxSize < 3840 -> 3840
else -> 1920 // 何もなければ適当に Full HD
}
imageReader = ImageReader.newInstance(imageReaderSize, imageReaderSize, PixelFormat.RGBA_8888, 2)戻すのがこの辺。
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
bitmap.copyPixelsFromBuffer(buffer)
// アスペクト比を戻す
val resultBitmap = bitmap.scale(originWidth, originHeight)やっぱり16の倍数にするだけでいい気がする。
もう少し速く出来るのでその話をします。TIMEOUT_USの時間を0にすればさらに速くなるはずです。これを書いた時点の私は10_000Lよりも小さくすると映像が崩れてしまうので、最低でもこれくらいは必要だろうと適当に入れてたのですが、そんなことはなく単に私の書いたコードが間違えてました。
映像が崩れてしまう原因ですが、デコーダーに入れて無いのにコンテナ(mp4)のデータを次に読み進めていたのが原因でした。
デコーダーに入れた後にデータを次に読み進める分には正解なのですが、デコーダーに入れてもないのにデータを読み進めたのが悪かったようです。はい私のせい。
タイムアウトが長かったため、タイムアウトよりもデコーダーの用意が先に来ることがほとんどになるためうまく動いていた。しかし、タイムアウトを短くしたことにより、デコーダーが間に合わずリトライが必要になる場合が出てきた。
しかし、現状のコードではデコーダーに入れたかどうか確認せずにコンテナのデータを読み進めていた。確認してないのでデータだけが先に読み進んでしまった。
明らかにキーフレームが欠落した動画フレームが出てきて疑ったらビンゴ。
というわけで修正箇所がこんな感じで、MediaCodec#queueInputBufferをしていることを確認してからMediaExtractor#advanceするように直しました。
あとタイムアウトの定数を0にした。
companion object {
/** MediaCodec タイムアウト */
- private const val TIMEOUT_US = 10_000L
+ private const val TIMEOUT_US = 0L- // 次に進める。advance() が false の場合はもうデータがないので、break する。
- val isEndOfFile = !mediaExtractor.advance()
- if (isEndOfFile) {
- break
+ // 次に進める。デコーダーにデータを入れた事を確認してから。
+ // advance() が false の場合はもうデータがないので、break する。
+ if (0 <= inputBufferIndex) {
+ val isEndOfFile = !mediaExtractor.advance()
+ if (isEndOfFile) {
+ break
+ }
}あとここも。
タイムアウトを短くしたことによりlatestDecodePositionMs = presentationTimeMsの部分が2回以上通過しちゃうことがあった。
別プロジェクトでおかしくなってしまったので調査したらこの部分が2回以上呼ばれてたからだった。
val presentationTimeMs = bufferInfo.presentationTimeUs / 1000
if (seekToMs <= presentationTimeMs) {
isRunning = false
+ isDecoderOutputAvailable = false
latestDecodePositionMs = presentationTimeMs
}
}これが乱れた動画フレーム。
デコーダーに入れるべきデータがずれてしまったのが多分原因。
TIMEOUT_USが0になったので、さらに速くなった?リトライが多くなってるとは思うけどコード間違って無ければ問題ないはず。
ちなみにこれがTIMEOUT_USが10_000Lだった頃。↑が改善後なので速くなった。
修正コミットはこちらです。
TIMEOUT_US を 0 にしてもう少し速く動かせるように · takusan23/AndroidVideoFrameFastNextExtractor@0965aa2
https://github.com/takusan23/AndroidVideoFrameFastNextExtractor/commit/0965aa2504377c18b9faafd7b1a18d41f04d73c5
latestDecodePositionMs = presentationTimeMs が複数回呼ばれるようになったのを修正 · takusan23/AndroidVideoFrameFastNextExtractor@6a29497
https://github.com/takusan23/AndroidVideoFrameFastNextExtractor/commit/6a294974d6202fc9a9d359c8ab0cfe0d23c0a24a
こんな長々と書く予定はありませんでした。
ぜひ試す際はいろんな動画を入れてみるといいと思います、たまに変に動くやついる↑もそれで見つけた