どうもこんばんわ。
2014年が10年前ってやばくない?そんなには経ってないやろ・・・って思ってたときにふと、
その頃に飯のテレビCMを逆再生にしたやつを見せてくれたことを思い出したので、今回は逆再生動画を作るアプリを作ってみる
逆再生動画を作る Android アプリを探してるんだけど検索妨害するのやめない?
はい。
審査中なので、通っていれば以下のURL
で開けるようになるはずです。
https://play.google.com/store/apps/details?id=io.github.takusan23.dougaundroid
ソースコードあるので、もしビルドしたい方がいれば
https://github.com/takusan23/DougaUnDroid
本題
というわけで、今回は選択した動画を逆から再生するような動画を作るアプリを作ろうと思います。
逆再生動画作成アプリです。
もちろんAndroid
のMediaCodec
やOpenGL ES
等のみを利用します。
ffmpeg
?バイナリサイズとライセンスの面から今回は無しです!、使えるならそっち使うのが良いと思う。
先に作品例を
こんなのが作れます
逆から再生する動画を作るのは難しい
逆再生の動画ってよく見かけると思いますが、実は作るのが難しいんですよね。
世の動画編集アプリはよくやっていると思います。
動画というのは、写真1枚1枚がパラパラ漫画のように動いて見えるので、それぞれ1枚ずつ保存しているかのように見えるかもしれません。
しかし、写真1枚1枚をファイルに保存している割には、動画ファイルのサイズがそんなに大きくないんですよね。これがGIF
ならバカでかくなるのですが、動画はそんなに大きくないですね。
単純に1枚ずつ保存した場合、1秒間に30fps
なら30枚
、60fps
なら60枚
あるはずなので膨大なファイルサイズになってしまうような気がするのですが、
現実のカメラアプリや動画編集アプリはそうではありませんね。一体写真データは何処へ・・?
その答えがコーデックと、エンコーダーですね。
コーデックというのが、動画を圧縮するアルゴリズムでのことで、そのアルゴリズムを動かすのがエンコーダーですね。
じゃあ一体コーデックさんはどのようにして動画を圧縮しているのか。ですが普通に難しいことをしているので、
逆再生の動画を作る上で障壁になっている部分を話すと、
一個前の写真と今の写真を見比べて、変わっている部分のみをファイルに保存します。
変わっていない部分は一個前の写真を引き続き参照するようにしたわけですね。
↑ 雑な絵ですが、、、
こんな感じに2枚目には猫が増えた場合、増えた分だけを保存するようなことをしているらしい。
この、今の写真と比較している一個前の写真のことをキーフレームといいます。
実際は一個前の写真と比較するわけではなく、一定間隔でこのキーフレームが生成され、間はすべてキーフレームからの差分のみが保存されるってわけです。
これにより、1秒間に30枚写真が来た(30fps
)としても、変わっている部分のみを記録することでファイルサイズを小さくすることに成功するわけですね。
しかし、これには欠点があり、時間が増える方向にしか再生できないということです。
ほとんどのフレームがキーフレームからの差分なので、キーフレームの間にあるフレーム(写真)へを表示したいために動画をシークした場合、キーフレームまで遡る必要があります。
という感じで、時間が増える方向にしか再生出来ないという前提があるおかげで、ストレージや通信料を節約しつつ高画質な動画をお届け出来ているわけですね((?))。
詳しくは
- Iフレーム / キーフレーム
- フレーム間予測 / フレーム間圧縮
- 動画コーデック
とかで調べてみてください。
まあこの辺今回意識しなくても作れるので
あ、あと音声に関しては PCM を2バイトずつ後ろから取り出して入れ直すだけなのでそこまで大変じゃないです
二番煎じ
はい。
https://www.sisik.eu/blog/android/media/reverse-video
ぱっと読んだ感じ、普通に難しそう。
OpenGL
無しでやったみたい。あのInputSurface.java
とかいうやつがいらなくなる(別に必要でもAOSP
からコピーするだけだけど)みたいです。
(でも体感OpenGL
をMediaCodec
に噛ませておいたほうが良さそう感はあるんだよなあ、リサイズとか出来るし)
あと記事読んで気付いた、ByteBuffer
(写真(映像フレーム)のバイト配列)を直接扱う方法もあるか、、、
いやでもByteBuffer
をMediaCodec
で扱うとか絶対やだ。
今回の作戦
とにかく、今日使われている動画は増える方向にしか再生できなくて、減る方向に再生する場合は難しいよってことがわかったところで今回の作戦です。
先駆け者さんは、キーフレームとその間をすべてキーフレームに変換したそうです。どこへシークしても完全な状態で持つことを選択したそう。
つまり写真1枚1枚持つのと同じ方法を取ったみたいです。
ただ、これやるとファイルサイズがかなり大きくなりそうなのと、すべてキーフレームにするためのMediaCodec
周りを書くのがやだかなあ。
というわけで今回の作戦はこちら、前作ったCanvasから動画を作る
やつを使います!
(ちなみにこれも若干間違ってることにこれ作っているときに気付きました。。。)
https://takusan.negitoro.dev/posts/android_canvas_to_video/
それから、動画からBitmap
を貰えるMediaMetadataRetriever
も使う
どうやらMediaMetadataRetriever#getFrameAtTime
メソッドで、指定した時間の動画フレーム(写真)が取れるらしい。
https://developer.android.com/reference/android/media/MediaMetadataRetriever
これらを組み合わせて、今回は、1枚1枚動画ファイルから動画フレーム(写真)を後ろから取り出し、Canvas
に描画し、エンコーダーに突っ込むことにします。
どうやらMediaMetadataRetriever#getFrameAtTime
は時間が増える方向じゃなくて、減るような方向にも対応しているみたい。これで行こう。
先述の説明の通り、すべてがキーフレームではないので、まずキーフレームまで移動して、その後差分を見る・・・って事をするはずなので普通に高コストだとは思う。
ただ全部をキーフレームになるような動画を作るよりはマシな気がしなくもない。いやgetFrameAtTime
が多分重たいので、全部キーフレームのほうが早いのかなあ、、、
ながれ
映像は↑の感じで、逆から取り出してCanvas
に書く方法で。
音声は、PCM
にして配列を反転させればいいので映像ほぞ難しくないです。
動画を支える技術
MediaCodec
とかが何なのかは他の記事で書いたので、そっちを見て。
ざっくりいうと
MediaCodec
MediaExtractor
mp4 / webm
等のコンテナからメタデータ、エンコードされたデータを取り出す
MediaMuxer
- エンコーダーから出てきたデータを
mp4 / webm
コンテナに書き込む
OpenGL ES
MediaCodec
と組み合わせると、映像を加工したり出来る
つくる
ながかった
環境
なまえ | あたい |
---|
端末 | Pixel 8 Pro / Xperia 1 V |
Android Studio | Android Studio Hedgehog 2023.1.1 Patch 2 |
つくる
Jetpack Compose
使うけど、別にView
でもいいです。
どうせ主役はMediaCodec
周りなのだから
適当にレイアウトを用意
動画を選ぶボタンと、処理を開始するボタンをMainActivity
におきます。
Canvas から動画を作る処理
前記事で書いたので、あんまり深入りはしないけど(てか覚えてない)
https://takusan.negitoro.dev/posts/android_canvas_to_video/
とりあえずこの2つをコピペします。AOSP
にちょっと手を加えただけなので私も何やってるのかよくわからない。
これらを組み合わせて、Canvas
で動画を作る処理を書きます。
まずコード全文です。解説はこの後します。(といってもMediaCodec
周りは複雑すぎて私もわからん)
これで、写真1枚1枚Canvas
で書いて動画を作る処理ができました。30fps
なら一秒間に30回Canvas
で書く漢字ですね!。
(毎フレームCanvas
で書いてエンコードする。)
解説ですが、
Kotlin coroutine
でOpenGL
をうまく使うために、新しい単一スレッドのDispatcher
を作ります。
これの何が嬉しいかと言うと詳しくは前回の記事で、ざっくりいうとこれから作るDispatcher
だと常に同じスレッドが使われます。同じスレッドでOpenGL
を操作する必要があるので
(makeCurrent
したスレッド以外ではOpenGL
関連できない?)
https://takusan.negitoro.dev/posts/android_14_media_projection_partial/#録画部分に組み込む話と-kotlin-coroutine-の話
それから、MediaCodec
とOpenGL
周りを用意します。
OpenGL
はスレッド注意です!
あとは、保存先のMediaMuxer
を用意して、エンコーダーとOpenGL
のメインループ?を開始します。
メインループ?内でCanvas
を使って描画をする感じですね。
映像を逆にする処理
MediaMetadataRetriever
を作って後ろから動画の写真(フレーム)を取り出して、Canvas
に書くので、ここだけに高レベルAPI
で完結します。
Canvas
なので、自由に書くことが出来ます。
MediaExtractor
よりもMediaMetadataRetriever
の方が、fps
とかビットレート
とか取れるんですね。
そういえば、縦動画の場合、Android
だと縦と横が入れ替わった状態で返ってくるんですよね。
これだと縦動画を入れても、エンコーダーの動画の縦横が横動画のときの値になってしまいます。ので、回転情報を見て、height / width
を入れ替えて取り出すようにする必要があります。
https://stackoverflow.com/questions/45879813/
↓ このへんね
音声のエンコーダー・デコーダー
まずはAAC (mp4 の中に入ってる音声データ)
を未圧縮状態、PCM
のバイト配列に変換します。
デコーダーを使ってデコードすることで、PCM
に戻すことが出来ます。
PCM
にすればバイト配列をいじることが出来るようになり、音声データに手を入れることが出来ます。
それから、PCM
のままだとmp4
に入らないので、エンコーダーも用意します。
というわけでMediaCodec
を使ったエンコーダー・デコーダーがこちらです。
なんで動いてるかはよくわからない、適当にif
を消したらなんか動かなくなったのでもう知らない...
音声と逆にする処理
次に、音声データを逆に並べ替える処理を書きます。。。
が、ここで音声データの保存方法と言うか、PCM
がどの用にバイナリを保存しているのか。という話が必要だった。
サンプリングレート・チャンネル数・量子化ビット数
知っていれば飛ばしていいです。というか前話した気がする、まあいいや。
PCM
のバイト配列を並び替えて、逆再生動画の音声を作るわけですが、ただ反転させれば逆再生になるかというと微妙。
なので話をします
- チャンネル数
- これは簡単
- 左右同じ音を出したい場合は 1
- 左右違う音を出したい場合は 2
- 大体 2 なはず
- サンプリングレート
- 1秒間に何回音を記録するか。です。
- 大体、44,100 回か、 48,000 回のどちらかだと思います
- 音声コーデックが
AAC
の場合は 44,100 回が多そう
- 音声コーデックが
WebM
の場合は 48,000 回が多そう
- 今回は
AAC
なので44,100
でいきます
- 量子化ビット数
- 英語だと
bitDepth
?
- サンプリングレートの回数分記録するわけですが、何バイトで表現するかです
- 多分
16bit
が多い?
16bit
の場合、一回の記録で2バイト
使うことになります
- 2チャンネルの場合は左右それぞれ
2バイト
使う事になります
なので、最後の量子化ビット数を考えながら、PCM
のバイト配列を反転させる必要があります。
そのまま反転させたら8bit (1バイト)
の場合以外は動かなそう。2バイト
ずつ操作しないといけないので。。。
だと
1チャンネル目(16bit なので2バイト) | 2チャンネル目(16bit なので2バイト) | 1秒間にサンプリングレートの数だけ記録... |
---|
0x00 0x00 | 0x00 0x00 | ... |
そういえば最初が右か左かは忘れました、どっちだっけ
上記を考慮して、多分これでPCM
のバイト配列の反転ができると思います。
量子化ビット数
はメタデータから取る方法がなさそうだったので、音声データ量を求める公式を入れ替えて量子化ビット数
を求める公式を作って計算するようにしてみました。
デコードするやつ
、PCMを逆に並び替えるやつ
、PCMをエンコードするやつ
をそれぞれ作って、Uri
を渡せば動くようにします。
これらはJetpack Compose
で作ったUI
側で呼び出して使います。
解説ですが、decode()
はAAC(mp4)
からPCM
へ、encode()
はPCM
からAAC(mp4)
にする処理です。
さっき作ったAudioEncoder / AudioDecoder
クラスはここで使うわけですね
PCM
はデカくなるのでメモリにのせるのもアレかなと思い、アプリが使えるストレージgetExternalFilesDir
に一旦ファイルを置いています。
reversePcmAudioData
は、上記の説明通りにPCM
を反転に並び替えるやつです。
ところで、InputStream
系のread
は逆方向に読み取ることが出来ないらしく、
逆から取り出すためには、ファイルのデータをすべてバイト配列の変数に入れるか(File#readBytes()
)、InputStream
ではなく、RandomAccessFile
にして指定位置からデータを取り出すのどちらかが必要?
どっちがいいんだろう、詳しくないや
MediaExtractor
を作るユーティリティ関数があります。
↑で書いたコードで使うのでこれも持ってきてね。MediaExtractor#selectTrack
の呼び忘れには注意
音声と映像のトラックを保存する処理
AudioReverseProcessor
とVideoReverseProcessor
を書いたあたりで気付いたかもしれませんが、
これ音声と映像がそれぞれのmp4
に保存されちゃうんですよね。
.mp4
一つのファイルに、映像トラックと音声トラックをそれぞれ入れたいわけですが、それをするにはMediaMuxer
を使えばよいです。
(あくまでもトラックを合わせているだけなので、すでにある音声トラックに音を重ねたいとかはまた別のことをする必要があります)
この辺で音を重ねてます: https://takusan.negitoro.dev/posts/summer_vacation_music_vocal_only/
コードです。
端末の動画フォルダへ保存する処理
getExternalFilesDir
にあるファイルを端末の動画フォルダへコピーする処理です。
公式はこの辺で説明しています。 https://developer.android.com/training/data-storage/shared/media#add-item
MediaStore
とかいうメディア系の所在を記録してるデータベース
みたいなやつが居て、そいつに対してレコードを追加すると、
一意の値(Uri
)が貰えるので、それでJava
のInputStream
、OutputStream
を開けばよいです。
これまでに作った処理を合体
上で作ったreverseAudio
とreverseVideoFrame
を組み合わせて、逆再生動画を作る処理がこちらです!
Kotlin coroutine
のtry-finally
でリソース開放できるやつすき
launch { }
じゃなくてasync { }
でもいいですが、今回はlaunch { }
で返り値返していないので、これでいいはず。
返り値があるならasync { }
のが良さそう。
UI から呼び出す
↑で作った処理を呼び出します。
が、結構時間がかかるので、現在の状態を表示させておくと良いでしょう。
遅すぎてprintln
も書きました。logcat
に出るはず
動作確認
動画を選んで開始を押せばいいはず。
終わると終わりって出ます。
検証動画ですが、ニコ動で逆再生タグの付いた動画を動画撮影してみて、このアプリで変換して、逆再生が元に戻っていれば成功じゃないでしょうか?
ちゃんと逆再生が元の再生に戻ってますでしょうか・・?
ソースコード
https://github.com/takusan23/AndroidReverseVideoMaker
おわりに
めちゃめちゃ時間がかかる
この方法はあんまり良くないかもしれない。
あ、あと、Android
のMediaMuxer
(コンテナに書き込むマルチプレクサ)は、ストリーミング出来ないmp4
を吐き出すので、
ffmpeg
を使ってmoov atom
を先頭にしてからこのブログに貼ってます。
以上です、お疲れ様でした 888888