どうもこんばんわ。
FLIP*FLOP 〜INNOCENCE OVERCLOCK〜 攻略しました。
めっちゃ あまあまなシナリオ + かわいいヒロイン がいる神ゲーです。ほんとにあまあまでした
おじいちゃん何者なの((?))続編で明かされるんかな
ボクっ娘だ!
元気いっぱいイオちゃんかわいい!!
サブヒロインも可愛いけど攻略できない、そんな・・・
というわけでOP
のCD
、開け、、ます!
OP
曲めっちゃいい!
本題
Android
で動画の1コマ1コマを画像として取り出す処理をAndroid
でやりたい。
30 fps なら 1秒間に 30 枚画像が切り替わるわけですが、その1枚1枚を切り出したい。
特に、30fps なら 30枚
、連続したフレームを取り出すのに特化した処理を自作したい。
理由は後述しますが遅い!
欲しい要件
- 指定位置のフレーム(動画の指定位置を画像にする)
- 動画の再生時間が増加する方向に対して連続でフレームを取り出す際には、高速であってほしい
- 巻き戻しは遅くなって仕方がない
- 高速で取れるハズな理由も後述します
- 後述しますが既知の
Android
の解決策では遅い
動画のサムネイル画像が一回ぽっきりで欲しいとかで、動画のフレームを取得するなら別に遅くてもなんともならないと思うのですが、、、
時間が増加する方向に向かって映像のフレームを取り出す分には、高速にフレームを取り出せる気がするんですよね。以下擬似コード
既にあるやつじゃだめなの?
ライブラリを使わずに、Android
で完結させたい場合は多分以下のパターンがある
高レベルAPI
ですね。
ffprobe
的な使い方から、動画のフレームを取ったりも出来ます。
getFrameAtTime
https://developer.android.com/reference/android/media/MediaMetadataRetriever#getFrameAtTime(long,%20int)
すごく簡単に使える。
動画を渡して時間とオプションを指定するだけで、Bitmap
として取れる。
オプションですが
- 動画の時間に近いフレームを取り出す(高速)
- MediaMetadataRetriever.OPTION_PREVIOUS_SYNC
- 高速な代わりに、指定の時間よりも前のフレームになる
- MediaMetadataRetriever.OPTION_NEXT_SYNC
- 高速な代わりに、指定の時間よりも前か後のフレームになる
- MediaMetadataRetriever.OPTION_CLOSEST_SYNC
- 高速な代わりに、指定の時間よりも後のフレームになる
- 動画の時間を厳密にしたフレームを取り出す(めちゃ遅い)
- MediaMetadataRetriever.OPTION_CLOSEST
フレームを正確に欲しい場合は、MediaMetadataRetriever.OPTION_CLOSEST
を使うしか無いと思うんですが、結構遅いのでちょっと使えないかな。
getFrameAtIndex
https://developer.android.com/reference/android/media/MediaMetadataRetriever#getFrameAtIndex(int,%20android.media.MediaMetadataRetriever.BitmapParams)
こっちのほうがgetFrameAtTime
よりは高速らしいですが、あんまり速くない気がする。
引数には時間ではなく、フレームの番号を渡す必要があります。30fps
なら、1秒間に30枚
あるので、、、
全部で何フレームあるかは、METADATA_KEY_VIDEO_FRAME_COUNT
で取れるらしいので、欲しい時間のフレーム番号を計算で出せば良さそう。
が、これも私が試した限りあんまり速くないので別に・・・
MediaMetadataRetriever
が何やってるのかあんまりわからないのですが、
なんだかgetFrameAtIndex / getFrameAtTime
が遅いからって並列にしても対して速くないです。
まず直列パターン。
次に並列のパターン。
まず直列パターンの結果ですが、
Pixel 6 Pro
time = 1090 ms
time = 1082 ms
Pixel 8 Pro
time = 744 ms
time = 702 ms
そしてコレが並列パターンの結果。
Pixel 6 Pro
time = 1087 ms
time = 983 ms
Pixel 8 Pro
time = 691 ms
time = 653 ms
若干早いけど誤差なのでは・・・?
MediaMetadataRetriever
、もしかして内部でインスタンスを共通にしてて切り替えて使ってる?
うーん、おそい!
ネタバレすると、これは自作した後に気付いたんですが、これも結局あんまり早くない
動画プレイヤーのMediaPlayer
の出力先にImageReader
を使う方法。
MediaPlayer
の出力先に普通は、SurfaceView
とか、TextureView
とかを渡しますが、ImageReader
を渡すと画像データ
として取ることが出来ます。
SurfaceView / TextureView
が画面に表示する物だとしたら、ImageReader
は画像データにしてくれるものでしょうか。MediaRecorder
は動画にしてくれるやつです。
(まあTextureView
をキャプチャするのと対して変わらんと思うけど、、、ImageReader
とか言う適役がいるので)
で、で、で、、、MediaPlayer#seekTo
して、ImageReader
で動画のフレームを画像にすれば高速に取れるのではないかと。
でもだめだったよ。これもあんまり早くない。
https://developer.android.com/reference/android/media/MediaPlayer#seekTo(long,%20int)
並列はこれで達成できるかもしれない。
ただ、MediaPlayer#seekTo
がMediaMetadataRetriever
のときと同じく、正確性を求めるなら速度が遅くなるみたい。
次のフレームをseekTo
で指定しても速くなかった。うん。高レベルAPI
だし、そりゃそうなるか。
そういえば、MediaPlayer
、これ動画を再生するものなので、連続してフレームをBitmap
にするとかなら得意なんじゃないだろうか。
動くか分からんけどこんなの。
ただ、連続してフレームがとれるというか、毎フレームBitmap
を生成することになるので、Bitmap
をどっかに置いておかないといけない。
一旦画像にして保存してもいいけど、、、できれば指定した時間のフレームだけ欲しいし、その次のフレームが欲しかったらすぐ返して欲しい。
そもそも連続したフレームだったら高速に取り出せるんですか?
動画のフレーム(画像)の話
それには動画がどうやって動画を圧縮しているかの話と、キーフレームの話が必要で、しますね。
前も話した気がするけどこのサイトGoogle
で見つからない事が多いのでまた書きますね。
(SSG
でも動く全文検索検討するかあ~)
動画というのは、画像が切り替わっている用に見えますが、考えてみてください。30fps
だと1秒間に30枚の画像
を保存しているのかと。
してないですね。仮に作ったとしても動画ファイルはそんなに大きくなりません。でも30fps
なら30枚分
あるはずの画像はどこに行ってしまったのか・・・
小さく出来る理由ですが、前回のフレーム(画像)からの差分のみを動画ファイルへ保存するんですね。
動画というのはほとんど変わらない部分も含まれているわけで、それらは前のフレームを参照してねとすれば、動画ファイルは小さく出来ます。
前のフレームに依存する代わりにファイルを小さく出来ました。
ただ、すべてのフレームを前のフレームに依存させてしまうと、今度は巻き戻しができなくなってしまいます。
ドロイドくん3つのフレームを表示させたい場合、フレーム単体では表示できないので、それよりも前(上の絵では最初)に戻る必要があります。
でも毎回最初に戻っていてはシークがとんでもなく遅くなってしまうので、定期的にキーフレームという、前のフレームに依存しない完全な状態の画像を差し込んでいます。
1秒に一回くらいとかですかね。これなら、大幅に戻ったりする必要がなくなるのでシークも早くなります。
もちろん、動画のコーデックはこれ以外の技術を使って動画のファイルサイズを縮小していますが、今回の高速でフレームを取り出す話しには多分関係ないので飛ばします。
なぜ既知の解決策が遅いのか
シークしているからでしょう。
MediaMetadataRetriever
には4つのオプションがあるといいました。
- OPTION_PREVIOUS_SYNC (高速)
- OPTION_NEXT_SYNC (高速)
- OPTION_CLOSEST_SYNC (高速)
- OPTION_CLOSEST (低速)
↑のフレームの話を聞いたら、OPTION_CLOSEST
がなんで遅くて、それ以外がなんで早いか。分かる気がしませんか?
OPTION_PREVIOUS_SYNC / OPTION_NEXT_SYNC / OPTION_CLOSEST_SYNC
はキーフレームを探すのに対して(フレーム単体で画像になっている)、
OPTION_CLOSEST
はキーフレームからの差分までも見る必要があるため、キーフレームまで移動した後指定時間になるまで進める必要があり、時間がかかるわけです。
そして、OPTION_CLOSEST
の場合、おそらく毎回キーフレームまで戻っている?ために遅くなっている?
MediaMetadataRetriever
もMediaPlayer
も多分そう。
なぜ高速に取り出せると思っているのか
キーフレームまで戻るから遅いのでは。巻き戻すわけじゃないから戻らないように時前で書けばいいのでは???
絶対戻らないという前提があれば、連続したフレームを取り出すのも早いんじゃないかという話です。
というわけで、今回は動画のフレームを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
とかは要らないはずですが
OpenGL
を一枚噛ませるとさせておくとより安心です(嘘です。なんか間違えたのかGoogle Pixel以外で落ちました。OpenGLを噛ませないと動きません。落ちた話は後半でします。)
→ 2024/05/30 追記もあります。ImageReader
何もわからない。
今回の作戦
前回の位置から、巻き戻っていない場合は、コンテナから次のデータを取り出してデコーダーに渡すようにします。
これをするため、フレームが取得し終わってもMediaCodec / MediaExtractor
はそのままにしておく必要があります(待機状態というのでしょうか・・)
- MediaCodec
- MediaExtractor
mp4 / webm
等のコンテナフォーマットから、パラメーターや実際のデータを取り出すやつ
- デコーダーに渡すときに使う
- ImageReader
SurfaceView
が画面に表示するやつなら、これは静止画に変換するやつ
- Surface
- 映像データを運ぶパイプみたいなやつです
- このパイプみたいなやつがいるおかげで、私たちは映像データをバイト配列でやり取りする必要がなくなります
- OpenGL
MediaCodec
で出てきたフレームを加工したりできる
- そのほか、
MediaCodec
の出力先Surface
はOpenGL
を使ったInputSurface
を経由させるのがお作法らしい
InputSurface.java
- よくわからないけど
OpenGL
を経由させるのが安牌
OpenGL 周りを AOSP から借りてくる
何やってるか私もわからないのでAOSP
から借りてくることにします。
私がやったのはKotlin
化くらいです。
適当にクラスを作って、以下の関数を用意します。
それぞれの中身はこれから書きます。
newSingleThreadContext
がなんで必要かは前書いたのでそっち見て
→ https://takusan.negitoro.dev/posts/android_14_media_projection_partial/#録画部分に組み込む話と-kotlin-coroutine-の話
まあ言うと
newSingleThreadContext
ってやつを使うことで、常に同じスレッドで処理してくれるDispatcher
を作れます。これをwithContext
とかで使えばいい。
あ、でも複数のVideoFrameBitmapExtractor()
のインスタンスを作って使う場合は、openGlRenderDispatcher
をそれぞれ作らないといけないので、companion object
に置いたらダメですね。
初期化する処理
prepareDecoder
関数の中身です。
Context
とUri
はJetpack Compose
で作るUI
側で貰えるので
trackIndex = ...
の部分は、mp4 / webm
から映像トラックを探してselectTrack
します。
音声トラックと映像トラックで2つしか無いと思いますが。
ImageReader
ですが、MediaCodec
で使う場合はImageFormat.YUV_420_888
じゃないとだめっぽいです。
映像からフレームを取り出す処理
前回フレームを取り出した再生位置よりも前の位置のを取り出す処理と、後の位置のを取り出す処理で2つ処理を分けたほうが良さそう。
前回より前の位置にあるフレームを取り出す
前回取り出したフレームの位置よりも前にある場合は、もうこれは仕方ないので、一旦キーフレームまで戻って、指定時間になるまでコンテナから取り出してデコードを続けます。
とりあえず前のキーフレームまでシークして待てばいいので、後の位置よりも簡単ですね。
一点、現時点の再生位置よりも巻き戻すシークの場合MediaCodec#flush
しないとだめっぽい?
試した感じ、flush()
呼ばないと巻き戻らないんだよね。
flush()
を呼ぶ場合、MediaCodec#dequeueInputBuffer
で取ったバッファのインデックスを、MediaCodec#queueInputBuffer
に渡してMediaCodec
に返却してからflush
を呼ぶようにしましょうね。
(MediaCodec#dequeueInputBuffer
を呼びっぱなしにしてflush
すると怒られます)
(クソながMediaCodec
のドキュメントついに役に立つのか!)
https://developer.android.com/reference/android/media/MediaCodec#for-decoders-that-do-not-support-adaptive-playback-including-when-not-decoding-onto-a-surface
あとはwhile
で欲しい時間のフレームが来るまで繰り返すだけです。
readSampleData
で取り出してqueueInputBuffer
でデコーダーに詰める。デコードできたかどうかはdequeueOutputBuffer
を呼び出して、データが来ていればSurface
に描画です。
単位がMs
じゃなくてUs
なので注意。
前回より後の位置にあるフレームを取り出す
2024/03/28 追記。コード間違えてた、対応しないと無限ループに陥ります。詳しくは後述
さて、MediaMetadataRetriever#getFrameAtTime
にはない、巻き戻さなければキーフレームまで戻らないを実装していきます。
が、が、が、巻き戻さなければなんですが、これだと前回よりもかけ離れた先にある場所へシークするのが遅くなってしまいます。連続したフレームの取得なら早くなりますが、
遠い場所へシークする場合は近くのキーフレームまでシークしたほうが早いです。(これがないと前回からの差分を全部取り出すので効率が悪い)
というわけで、欲しい位置のフレームの取得よりも先に、キーフレームが出現した場合は一気に近い位置までシークするような処理を書きました。
(前回よりも数フレーム先のフレームなら、キーフレームまでシークせずに取り出せるので高速ですが、次次...あれ先にキーフレームが来ちゃうの?ってくらい離れていると逆に一気にシークした方が良い)
なんか手こずったけどなんとかなりました(MediaExtractor#getSampleTime
とBufferInfo#getPresentationTimeUs()
って微妙に違うのか・・)。
それ以外は↑のコードと大体一緒なので説明は省略で。
追記
MediaExtractor
からもうデータが取れないときの対応が必要です。
これしないと、最後まで取得しようとした時に多分無限ループになります。
なので、もうデータがない場合は null を返すように修正する必要があります。
このコミット参照。
https://github.com/takusan23/AndroidVideoFrameFastNextExtractor/commit/bceea63d9bb12616eea65a261d8c309900c9c0ff#diff-c7c817c9f4104122158d59c5d4aa6a97ba894f79f99e73d15eee88b892502ebe
説明すると、
getVideoFrameBitmap
のBitmap
をnullable
にする。
getVideoFrameBitmap
のelse
でフレームがないならgetImageReaderBitmap
を呼ばないように。
awaitSeekToNextDecode
がBoolean
を返せるようにします。true
ならフレームがある(getImageReaderBitmap
が呼び出せる)、false
なら無いです。
awaitSeekToNextDecode
のループ直前で、MediaExtractor#getSampleTime()
を呼び出して、-1
(もうデータがない)場合は何もせず、false を返します。
MediaExtractor#advance()
の返り値を見て、もうデータがない場合(false
)は、ループを抜けるようにします。
これで、無限ループは回避出来るはず。
nullable
になった関係で、呼び出し箇所も修正が必要かもです。
ImageReader から Bitmap を取り出す処理
acquireLatestImage
して、Buffer
とって、Bitmap
にしています。
組み合わせる
awaitSeekToNextDecode
とかawaitSeekToPrevDecode
とかgetImageReaderBitmap
を組み合わせて、動画のフレームを取り出す関数を完成させます。
あ~
シーク不要の部分で、なんで前回の Bitmap
を返しているかなんですが、動画フレームの枚数よりも多くのフレームをリクエストしてきた時に前回のフレームを返すためのものです。
どういうことかと言うと、30fps
なら1秒間に30枚
までならフレームを取り出せますが、1秒間に60枚
取ろうとするとフレームの枚数よりも多くのフレームを要求することになり、余計にデコードが進んでいってしまい、ズレていってしまいます。
(30fps
なら33ミリ秒
毎に取り出す処理なら問題ないけど、16ミリ秒
毎に取り出すと、フレームの枚数よりも多くのフレームを取るから壊れちゃう。)
フレームの枚数よりも多くのフレームを要求してきても壊れないように、最後取得したフレームの位置を見て、もし最後取得したフレームよりも前の位置だったら前回のBitmap
を返すようにしました。
Jetpack Compose で作った UI 側で呼び出して使う
ボタンと画像を表示するやつをおいて、ボタンを押したら動画を選ぶやつを開いて、選んだら↑の処理を呼び出す。
これで一通り出来たかな。ボタンを押して動画を選べば出てきます。
これを好きなところで表示してください。
好きな場所でいいので、今回は検証のためにMainActivity.kt
とかで。
使ってみた
ちゃんとImage()
に映像のフレームが写っています。
ちょっとずつだけど進める
を押せば動画も進んでいそう。
ベンチマーク
頑張って作ったので、MediaMetadataRetriever#getFrameAtTime
よりも早くないと困るぞ・・・!
今回は正確なフレームが欲しいので、MediaMetadataRetriever#getFrameAtTime
の第2引数には遅いですがMediaMetadataRetriever.OPTION_CLOSEST
を指定します。
意地悪ですね・・
3枚フレーム取り出してみる
とりあえず連続して3回取り出してみる。
うーん?
Xperia
に関しては自作しないほうが速いぞ・・?
なんならGoogle Pixel
の方も若干速いくらいで誤差っちゃ誤差かもしれない
- Xperia 1 V
- 自前の
VideoFrameBitmapExtractor
MediaMetadataRetriever#getFrameAtTime
- Pixel 8 Pro
- 自前の
VideoFrameBitmapExtractor
MediaMetadataRetriever#getFrameAtTime
- Pixel 6 Pro
- 自前の
VideoFrameBitmapExtractor
MediaMetadataRetriever#getFrameAtTime
0から3秒まで連続してフレームを取り出してみる
い、、いや、連続してフレームを取る際に早くなっていればええんや。
こっちが早くなっていれば万々歳
結果はこちらです。
連続して取得する方はかなり速いです。まあ巻き戻ししなければ速く取れるように作っているのでそれはそうなのですが。
うれしい!ハッピーハッピーハッピー(猫ぴょんぴょん)
- Xperia 1 V
- 自前の
VideoFrameBitmapExtractor
MediaMetadataRetriever#getFrameAtTime
- Pixel 8 Pro
- 自前の
VideoFrameBitmapExtractor
MediaMetadataRetriever#getFrameAtTime
- Pixel 6 Pro
- 自前の
VideoFrameBitmapExtractor
MediaMetadataRetriever#getFrameAtTime
動画からフレームを連続して取り出して保存してみる
連続して取り出して保存する処理を書きました。
↑で書いたBitmap
取り出しした後MediaStore
を使って写真フォルダに保存する処理が入ってます。多分保存処理があんまり速度でないんですけど、、、
https://github.com/takusan23/AndroidVideoFrameFastNextExtractor/blob/875cf02a003a6d186f5b0f695d5ee08e9d895360/app/src/main/java/io/github/takusan23/androidvideoframefastnextextractor/ui/screen/VideoFrameExtractAndSaveScreen.kt#L121
連続して取り出すのは得意
巻き戻ししなければキーフレームまで戻らないので、次のフレームの取得は早くなります。(コンテナから次のデータ取ってデコーダーに入れてでてくるのを待てば良い)
試した感じかなりいい成績ですよ。
自前↓
MediaMetadataRetriever↓
自前↓
MediaMetadataRetriever↓
苦手なのもある
連続して取り出さない場合はMediaMetadataRetriever
の方が早くなることがあります。(1秒で1フレームずつ取り出すとか)
あと巻き戻す場合は完敗だとおもいます。
自前↓
MediaMetadataRetriever↓
ソースコードです
https://github.com/takusan23/AndroidVideoFrameFastNextExtractor
おまけ
こっから先は知見の共有なので、本編とは関係ないです。
私が試した限り動かなかった。
あと動画によってはGoogle Pixel
でもぶっ壊れているフレームを吐き出してた。。。
Google Pixel 以外で落ちる
Google Pixel で動いていれば他でも動くと思ってた時期が私にもありました
私の名前は ImageReader です。Google Pixel を使ってます(それ以外では動かないので :( )
素直にMediaCodec
の出力先をImageReader
にしたら、Google Pixel
以外で落ちた。
動かないの書いても無駄ですが一応。MediaCodec
で使うImageReader
はYUV_420_888
にする必要があります。
ただ、Google Pixel
以外の端末(Qualcomm Snapdragon
搭載端末?)だとなんか落ちて、
しかもネイティブの部分(C++ か何かで書かれてる部分
)で落ちているのでかなり厳しい雰囲気。
というわけで調べたら、デコーダーに渡すMediaFormat
で、mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible)
を指定すれば直るって!
取り出したフレームが壊れている
でもこれもだめで、クラッシュこそしなくなりましたが、
画像がぶっ壊れている。ちゃんと出てくる画像もあるけど大体失敗してる。
ちなみにPixel
でも失敗したのでMediaCodec
とかImageReader
何もわからない。
↑ 謎の緑の線
↑ 砂嵐のように何も見えない
↑ Pixel でもだめだった
↑ なぜかこれだけ動いた
解決策があるのか不明ですが、、、結局OpenGL
を一枚噛ませたら直ったのでもうそれで。
MediaCodec
周りはSurface
直指定よりOpenGL
を噛ませておくと安牌。なんだろう、SoC (というか GPU ?)
の違いなのかな。
あ、ちなみにもしこれがうまく行っても、YUV_420_888
をBitmap
にするのが面倒そう。
MediaCodec
の映像入出力にはSurface
以外にByteBuffer
(ByteArray
)も使えますが、ByteBuffer
だと色の面倒も自前で調整しないといけない???
スマホのSoC
違ったら動かないとか嫌すぎる・・・
それに比べるとOpenGL
周りの用意は面倒なものの、AndroidのSurfaceTexture
、デコーダーから出てくるこの色?カラーフォーマット?の扱い(YUV
とかRGB
とか)を勝手に吸収してくれている可能性。
stackoverflow
だとその事に関して言及してるんだけど、公式だとどこでSurface
のカラーフォーマットの話書いてあるんだろうか。
OpenGL ES
周りは厳しいけど(AOSP
コピペで何がなんだかわからない)、けど、それなりのメリットはありそうです。
あとフラグメントシェーダーで加工できるのもメリットだけど難しそう。
追記:2024/05/30 ImageReader の width と height は決まっている説
ImageReader
のドキュメントには乗ってませんが、width / height
の値は決まった数字以外で動かないっぽい?
https://developer.android.com/reference/android/media/ImageReader#newInstance(int,%20int,%20int,%20int,%20long)
変な解像度videoWidth = 1104 / videoHeight = 2560
の場合に出力された映像がぐちゃぐちゃになっちゃった。
調べてもよくわからないので、色々試した感じ、1280x720
とかの解像度は動く。けどメジャーじゃない、中途半端な数字では動かない。
MediaCodec
の噂では、16
の倍数じゃないといけないとかで、16
で割れるかのチェックを入れてみたんですけどそれもダメそうで。結局動く値に丸めることにした。
面倒なので縦も横も同じ正方形に、ImageReader
から取り出したBitmap
をBitmap#scale
で元のサイズに戻すのが、今のところ安定している。。。
戻すのがこの辺。
追記:2024/06/04
やっぱり16
の倍数にするだけでいい気がする。
追記:2024/09/16 もうちょっと速くできる
もう少し速く出来るのでその話をします。
TIMEOUT_US
の時間を0
にすればさらに速くなるはずです。これを書いた時点の私は10_000L
よりも小さくすると映像が崩れてしまうので、最低でもこれくらいは必要だろうと適当に入れてたのですが、そんなことはなく単に私の書いたコードが間違えてました。
映像が崩れてしまう原因ですが、デコーダーに入れて無いのにコンテナ(mp4
)のデータを次に読み進めていたのが原因でした。
デコーダーに入れた後にデータを次に読み進める分には正解なのですが、デコーダーに入れてもないのにデータを読み進めたのが悪かったようです。はい私のせい。
タイムアウトが長かったため、タイムアウトよりもデコーダーの用意が先に来ることがほとんどになるためうまく動いていた。しかし、タイムアウトを短くしたことにより、デコーダーが間に合わずリトライが必要になる場合が出てきた。
しかし、現状のコードではデコーダーに入れたかどうか確認せずにコンテナのデータを読み進めていた。確認してないのでデータだけが先に読み進んでしまった。
明らかにキーフレームが欠落した動画フレームが出てきて疑ったらビンゴ。
というわけで修正箇所がこんな感じで、MediaCodec#queueInputBuffer
をしていることを確認してからMediaExtractor#advance
するように直しました。
あとタイムアウトの定数を0
にした。
あとここも。
タイムアウトを短くしたことによりlatestDecodePositionMs = presentationTimeMs
の部分が2回以上通過しちゃうことがあった。
別プロジェクトでおかしくなってしまったので調査したらこの部分が2回以上呼ばれてたからだった。
これが乱れた動画フレーム。
デコーダーに入れるべきデータがずれてしまったのが多分原因。
TIMEOUT_US
が0
になったので、さらに速くなった?リトライが多くなってるとは思うけどコード間違って無ければ問題ないはず。
ちなみにこれがTIMEOUT_US
が10_000L
だった頃。↑が改善後なので速くなった。
修正コミットはこちらです。
おわりに
こんな長々と書く予定はありませんでした。
ぜひ試す際はいろんな動画を入れてみるといいと思います、たまに変に動くやついる↑もそれで見つけた