たくさんの自由帳
Androidのお話
たくさんの自由帳
投稿日 : | 0 日前
文字数(だいたい) : 20022
目次
本題
つくった
番外編 UltraHDR から 10-bit HDR 動画を作る
UltraHDR を古い端末で撮影するからくり
10-bit HDR 動画から UltraHDR を作成するからくり
Android の API にはない
Android 16 に先を越された
UltraHDR 画像のために 10-bit HDR 動画からフレームを取り出す流れ
OpenGL を回避できないのですか?
環境
作っていく
libultrahdr をビルドする
必要なライブラリを入れる
libultrahdr ソースコードを持ってくる
ヘッダーファイルをコピーしておく
おわり
Android Studio でプロジェクトを作る
libultrahdr ライブラリをアプリに入れる
libultrahdr ライブラリを使って UltraHDR を作成する C++
OpenGL ES 周りを用意していく
付録 Media3
InputSurface.kt
TextureRendererSurfaceTexture.kt
TextureRenderer.kt
OpenGlRenderer.kt
MediaCodec デコーダー
レイアウトを作る
動画を選ぶ
動画情報を取得
動画のシークバーを用意する
描画を準備
完成!!
Xperia 1 V でも動く!
作品集
そーすこーど
おわりに
おわりに2
おわりに3
おわりに4
!?!?!??!!!!?!?!
妙にリアルな数字わらった
歌ってるのかわいい、無印のグランドエンディング曲!!
かこちゃん、他のヒロインの時でも面白い!!やろう!!、ほかルートでも安心や
うなぎ...
!!!
ついにどこのオシャレなのかが、、、!、
全年齢やったから、あかりちゃんルート2回目だけど、さすが、めにょあ、わかってた
うお~~~~~
私だけかもしれないけど、あかりちゃん、声が良くなったような気がする。無印やってしばらく経ってるせいかもしれないけど!!!
?!?!?!?
ここすき
ここもすき
これがサクラの国だから!?!?
えちえちシーンのせいでより一層お別れが辛くなってる説ある!!
わたしもこれ、です、もっと学園生活を見たかった、、、
あいかちゃん!!!かわいすぎ。とにかくこれだけは見てほしい、
!??!?!!!
読むんだ、、、さすがえちえち版(?)
(この後のお話が神)
!!!!!!!!!!!!!
ぴ、ぴとーかわいい
抵抗しちゃだめ!
!!!ここ新規シナリオな気がする、わくわく
む~~ん
あとここの、パタパタの声がかわいすぎる!!!!!のでやろう!
え?!?!?!!!
あいかちゃん!!!のえちえちシーンが良かったので神ゲーです、やろう
めちゃめちゃよかった、全年齢からここまで来てくれた甲斐がある。。
あと、えちえちシーン、早々に済ませる気だな~って思ってたけど、最後に一回あった。うれc
UltraHDR
って既存の写真に明るさのデータ(?)を持たせてより明るく表示できる技術なのですが、皆さん使ってますか?10-bit HDR
動画の静止画版みたいな技術ですけど、、
え、Google Pixel じゃないから撮影できないって?
端末が古いから無理?
10-bit HDR
動画からUltraHDR
を撮影するアプリを作りました。UltraHDR
撮影はに対応していなくても、これを使えば10-bit HDR 動画撮影に対応していれば、その動画のフレームを取り出してUltraHDR
画像を生成します。
例えばXperia
だとXperia 1 VII
しかUltraHDR
撮影に対応していませんが、HDR 動画撮影
機能は少なくともXperia 1 V
から搭載されているので、このアプリを使えば、1 V
でもUltraHDR
が作れるって寸法。
ただ、もうすこし古くなると、必要になるOpenGL ES
の機能が実装されて無くて使えないような。。。
このアプリのソースコードもあります。
この逆の例は普通にあります。しかも2通りの例があります、!!
私がやるのはこの逆!!!
Media3 1.4.0 — what’s new?
Media3 1.4.0 is released with new preload utilities, better HDR support in Transformer and images in PlayerView
https://medium.com/google-exoplayer/media3-1-4-0-whats-new-ba1c9c17ee1a
media/libraries/effect/src/main/assets/shaders/insert_ultra_hdr.glsl at bfe5930f7f29c6492d60e3d01a90abd3c138b615 · androidx/media
Jetpack Media3 support libraries for media use cases, including ExoPlayer, an extensible media player for Android - androidx/media
https://github.com/androidx/media/blob/bfe5930f7f29c6492d60e3d01a90abd3c138b615/libraries/effect/src/main/assets/shaders/insert_ultra_hdr.glsl
[Sample] [Media] - Implementation of "UltraHDR to HDR Video" by madebymozart · Pull Request #83 · android/platform-samples
Description This CL contains sample code for converting a series of UltraHDR images into an HDR video using GPU hardware acceleration. When rendering an UltraHDR image into a 10-bit frame, the gain...
https://github.com/android/platform-samples/pull/83
UltraHDR -> 10-bit HDR
のサンプルが出ているまさかの二通り出揃うとはびっくりした。これ誰もやらないのがセオリーだろこれ、、、
10-bit HDR 動画
っていう、明るさと鮮やかさが特徴の動画があります。
この機能はiPhone 12
あたりからスマホでも10-bit HDR
撮影ができるようになってきました。
この動画撮影をした後に、動画のフレーム(写真)をUltraHDR
の画像を作る処理に渡すことでできます。UltraHDR
に必要な眩しさは10-bit HDR 動画
ですでに撮影しているので、10-bit HDR 動画撮影に対応していればUltraHDR
保存が使えるわけです。
10-bit HDR 動画
の詳細とAndroid API
に関しては前に書いた記事を見てもらうとして、
ざっくり言うとこんな感じで、
使える色が増えて、それに合わせてガンマカーブ(取り込んだ色をカラーコードにする関数)も進化?している
既存 | 10-bit HDR | |
---|---|---|
色空間 | BT.709 | BT.2020 |
ガンマカーブ | 標準(?) | HLG / PQ / Dolby Vision (スマホは HLG) |
RGBA バイト列 | RGBA すべてで 8ビット (RGBA8888) | RGB で 10 ビット、残り 2 ビットが A (RGBA1010102) |
話を戻して、なら、なんかいい感じに、動画のフレーム(写真)をUltraHDR
画像に出来るのではないかと。
というわけでUltraHDR
を作成するライブラリを見てみます。
抜粋、
エンコードのシナリオ1番
、必要なものは、、RGBA1010102
の画像ファイルのみ!!!!!!!!!!!
おおお!10-bit HDR 動画
からどうにかしてフレームをゲットして、libultrahdr
に渡せば良さそうですね、、、、
えっlibultrahdr
、ビルドしないとダメなの?
libultrahdr
のライブラリ自体はP010 or rgba1010102 or rgbaf16
のどれか1つさえあれば、UltraHDR
画像が作れるのですが、Android
のAPI
は一味違います(不穏な)
たしかに、Kotlin/Java から UltraHDR を作る関数は存在するのですが、
それがこれ、JPEG_R
がそうなのですが、
コンストラクタでrgba1010102
の画像バイト配列を渡した後、SDR画像のrgba8888
画像が必要!!
なんでシナリオ1
がないの!!!!
え、C++
書く必要ありますか。そうですか、、
一応触れておくか...
Android 16
の新機能を探している皆さんに朗報。
このバージョンからHDR 動画・HDR 写真が含まれたスクリーンショット
を撮影した時、UltraHDR
のような眩しいままでスクリーンショットが撮影できます。
ということは、今回作ったアプリを使わなくても、HDR
動画を全画面にしてスクリーンショットを取れば、静止画でも眩しいんですけどね。。。。
価値を絞り出すとすると、スクリーンショットはpng
画像なので、UltraHDR
とはまた別の謎の技術を使っています。UltraHDR
のサポートを謳っていても、そのHDR スクリーンショット PNG
までサポートしているかは不明なので、今のところ眩しい静止画はUltraHDR
がいいのかな、
試した限り、HDR スクリーンショット PNG 画像
もAndroid
のBitmap#getGainMap
では取得できてそう、、、
これのAPI
を使わない他のアプリとかはしらない、
あと、画面録画のMediaProjection API
はまだSDR
っぽい?iOS
はHDR
の画面録画に対応したらしいのでAndroid
も来てほしい
UltraHDR
のC++
ライブラリ、libultrahdr
を呼び出す処理を書くOpenGL ES
を用意する(EGL
)際に、RGB
それぞれが10ビット
使えるようにするMediaCodec (ビデオデコーダー)
で動画のデコーダーを作る、出力先は↑で作ったOpenGL ES
OpenGL ES
に描画した動画フレームをglReadPixels
で取得glReadPixels
の結果をlibultrahdr
のC++
で書かれた関数に渡すMediaStore
に保存MediaCodec
の出力先に、画像キャプチャ担当のImageReader
クラスが存在します。HDR
のピクセルのバイト配列を取得できて、libultrahdr
ライブラリ自体が要求する方式を考慮すると、P010
でImageReader
を設定する必要があるのですが、
これが Snapdragon 端末ではエラーになってしまいます。
Fatal signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x76a4d3d000 in tid 8806 (aframeextractor), pid 8806 (aframeextractor)
Cmdline: io.github.takusan23.androidhdrp010orrgbaframeextractor
pid: 8806, tid: 8806, name: aframeextractor >>> io.github.takusan23.androidhdrp010orrgbaframeextractor <<<
#09 pc 0000000000004f88 /data/app/~~Fk18EqBBBYz0MJ_HrpBaHA==/io.github.takusan23.androidhdrp010orrgbaframeextractor-Ipx8f0RO4XwTcwrlBpAWYQ==/base.apk (io.github.takusan23.androidhdrp010orrgbaframeextractor.MainActivityKt.MainScreen$getDataFromImage+0)
#14 pc 0000000000005068 /data/app/~~Fk18EqBBBYz0MJ_HrpBaHA==/io.github.takusan23.androidhdrp010orrgbaframeextractor-Ipx8f0RO4XwTcwrlBpAWYQ==/base.apk (io.github.takusan23.androidhdrp010orrgbaframeextractor.MainActivityKt.access$MainScreen$getDataFromImage+0)
#19 pc 0000000000004c38 /data/app/~~Fk18EqBBBYz0MJ_HrpBaHA==/io.github.takusan23.androidhdrp010orrgbaframeextractor-Ipx8f0RO4XwTcwrlBpAWYQ==/base.apk (io.github.takusan23.androidhdrp010orrgbaframeextractor.MainActivityKt$MainScreen$startYuvP010$1.invokeSuspend+0)
なにこれ、もうAndroid
開発のユーザーランドからは直せない雰囲気なんだけど。Pixel
はなんかImageReader + P010
を実装してたっぽい?Snapdragon
で使えないなら採用しません、、
で、一方、なぜかOpenGL ES
はRGB
を10ビット
で問題なく動き(眩しい!!)、glReadPixels
もRGB
が10ビット
になるように指定すると、大体の端末で動くし、
それをlibultrahdr
に入れるとUltraHDR
画像ができてしまった、、
というわけで、今回はこれで行きます。
パソコン | Windows 10 Pro |
Android Studio | Narwhal Feature Drop 2025.1.2 |
端末 | Pixel 8 Pro / Xperia 1 V |
言語 | Kotlin / C++ (ちょっとだけ!!!) |
今回、libultrahdr
ライブラリをAndroid
向けにビルドするのにWSL2
を利用します。
理由はC++
の環境作っても今後使う機会ないはず、、消したい、、よく使うAndroid Studio
とはわけが違って、、
この都合上、Windows
でWSL2
を使うか、Linux
マシンを用意しないとこの記事通りでは作れないと思います。
もちろん他のOS
、それこそもちろんWindows
でもビルドできる方法があります。
ビルドが済んだらさっさと消したい。サンドボックス的な使い方にWSL2
がドンピシャ。
今回はビルドにLinux
マシンを使います。他のOS
でもビルドできるので、以下参照。
今回は先述の通りWSL2
のUbuntu
を使います。ので用意してください。
もう私のWindows
マシンにはインストールされているので、どういう手順だったか忘れてしまったのですが、、
一回も使ったことない場合はwsl --install
を叩けばいいっぽい?
Ubuntu
がインストールされると、username
とpassword
を決めるように言われるので、よしなに入力してください。
おわったら、いつものコマンドを叩きます。以下2つ。
sudo apt update
sudo apt upgrade
初sudo
なので、さっきのパスワードを入力するように言われるはず。入れてください。
2番目はY
を答える。時間かかるのでしばらく待つ!!
で、本当はC 言語
のコンパイラーを入れれば良いのですが、なんか面倒なので、
全部入りパッケージをapt install
しようと思います。多分gcc
やclang
を手動で入れるで良い。。
というわけで、全部入りのbuild-essential
を入れます。
sudo apt install build-essential
これもY/n
聞かれるのでY
。
これでgcc
が一緒に入ってくるはず。
次にlibultrahdr
に必要なライブラリを用意します。
叩くコマンドはこれです。
sudo apt install cmake pkg-config libjpeg-dev ninja-build
最後に、Android NDK
を入れます。
お作法なのか、/opt
に配置するのが良いみたいなので、先に/opt
に移動してから取得します。sudo
がいるけど、、
cd /opt
sudo wget https://dl.google.com/android/repository/android-ndk-r26d-linux.zip
sudo unzip android-ndk-r26d-linux.zip
もしunzip
が存在しないコマンドだよって帰ってきたらapt
から入れて、その後にもう一回unzip
コマンドを叩いて。
sudo apt install unzip
これでビルドに必要な素材が揃いました。
このコマンドを叩くことで、ホームディレクトリのフォルダにソースコードが出来る
cd ~
git clone https://github.com/google/libultrahdr.git
cd libultrahdr
mkdir build_directory
cd build_directory
そしたら、以下のコマンドを叩きます。
引数ですが、
DCMAKE_TOOLCHAIN_FILE
はそのままでよさそう、DUHDR_BUILD_DEPS
もそのまま、DANDROID_PLATFORM
もそのままでいいはず。DUHDR_ANDROID_NDK_PATH
はさっき展開したNDK
の位置(/opt
に入れたならそのままで)、DANDROID_ABI
は、armeabi-v7a
かarm64-v8a
かx86
かx86_64
のどれかです。
ABI (CPU アーキテクチャ)
は多分全パターンでやらないと、エミュレータだとライブラリが存在しない!!ってエラーになってしまう。。
とりあえずARM 64Bit の arm64-v8a
をビルドしてみましょう。
cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/android.cmake -DUHDR_ANDROID_NDK_PATH=/opt/android-ndk-r26d/ -DUHDR_BUILD_DEPS=1 -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-23 ../
これで叩くと、ビルドの準備がされます。
ここでC言語
のコンパイラーが存在しないと失敗します、がbuild-essential
の中に入ってるので問題なく動くはず、
ちなみに、失敗してうまくいかない場合は、build_directory
を一回消して、作り直して、さっきのcmake
のコマンドを叩くとよいです。
画像のようにcmake
がうまくいったら、以下のコマンドを叩きビルドします。
ninja
これが成功すると、今いるbuild_directory
にlibuhdr.so
って名前のファイルができているはずです!!!
おめでとう!
これをどこかWindows
マシンにコピーして、残りのCPU
アーキテクチャの分だけ繰り返します。WSL
はエクスプローラーから見れます。仮想マシンと違ってすごい便利です
C++
を書くときにinclude
するためのファイル!!!(#include
なんか懐かしくて草)
が、別にこのファイルは適当にgit clone
して取得してもいいです。が、せっかくWSL2
の環境にclone済み
のがあるので、ここから拝借します。
ultrahdr_api.h
ってファイルです。これをコピーしておいてください。
もうWSL2
は使わないのでWindows
から消しちゃっていいです。PowerShell
でwsl --unregister Ubuntu
でエンターすると消えます。Cドライブ
返してくれ
Native C++
ではなくJetpack Compose
のやつを選んでいいです。
というのも、前書いた通りC++
コードを後から追加するほうがJetpack Compose
よりも楽だからです。
プロジェクトができたら、File
→Add C++ to Module
を選んで、C++
が書けるように準備します。
ダイアログでなにか聞かれてもOK
を選んで。
画像は前回の記事の再利用です、、
app/src/main
にjniLibs
フォルダを作り、その中にそれぞれのABI
のフォルダを作り、その中にそのABI
でビルドした共有ライブラリを配置します。
それとは別に、jniLibs
にinclude
フォルダを作り、その中にはヘッダーファイルを入れます、
何言ってるか分からんと思うので、スクショを貼るとこうです。
次に、cpp
フォルダの中にあるCMakeLists.txt
を開いて、以下を足します。
これするとC++
コードから#include
できて、ライブラリもリンクされるようになってるはず。
add_library(libuhdr SHARED IMPORTED)
set_target_properties(
libuhdr
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libuhdr.so
)
include_directories(${CMAKE_SOURCE_DIR}/../jniLibs/include)
target_link_libraries(${CMAKE_PROJECT_NAME} libuhdr)
まず、Kotlin
側でUltraHDR
画像を作る関数を置くためのクラスを作りました。
関数はexternal fun
で宣言してください。
文字列の引数は、入力ファイルパスと、出力先ファイルパスで、あと縦と横は画像の縦と横です。
object LibUltraHdr {
// 実装は C++ へ
external fun createUltraHdr(width: Int, height: Int, rgba1010102PixelPath: String, resultPath: String)
// C++ コードをロードする
// 実装が書かれている C++ ファイル名
init {
System.loadLibrary("hdrvideotoultrahdr")
}
}
関数の部分が真っ赤になってると思うので、マウスオーバーしてCreate JNI function ...
を押します。
System.loadLibrary
に渡した名前と同じファイル名に生成されればOKです!
ここにC++
コードを書いていきます。が、ぶっちゃけサンプルのパクリなので詳しい文法とかは理解できてません、、
とりあえず全部張ります。
関数名Java_io_github_takusan23_hdrvideotoultrahdr_LibUltraHdr_createUltraHdr
は各自違うはず。
#include <jni.h>
#include <malloc.h>
#include <iostream>
#include <fstream>
#include <android/log.h>
#include "ultrahdr_api.h"
#define READ_BYTES(DESC, ADDR, LEN) \
DESC.read(static_cast<char*>(ADDR), (LEN)); \
if (DESC.gcount() != (LEN)) { \
std::cerr << "failed to read : " << (LEN) << " bytes, read : " << DESC.gcount() << " bytes" \
<< std::endl; \
return false; \
}
static bool loadFile(const char *filename, uhdr_raw_image_t *handle) {
std::ifstream ifd(filename, std::ios::binary);
if (ifd.good()) {
if (handle->fmt == UHDR_IMG_FMT_24bppYCbCrP010) {
const size_t bpp = 2;
READ_BYTES(ifd, handle->planes[UHDR_PLANE_Y], bpp * handle->w * handle->h)
READ_BYTES(ifd, handle->planes[UHDR_PLANE_UV],
bpp * (handle->w / 2) * (handle->h / 2) * 2)
return true;
} else if (handle->fmt == UHDR_IMG_FMT_32bppRGBA1010102 ||
handle->fmt == UHDR_IMG_FMT_32bppRGBA8888) {
const size_t bpp = 4;
READ_BYTES(ifd, handle->planes[UHDR_PLANE_PACKED], bpp * handle->w * handle->h)
return true;
} else if (handle->fmt == UHDR_IMG_FMT_64bppRGBAHalfFloat) {
const size_t bpp = 8;
READ_BYTES(ifd, handle->planes[UHDR_PLANE_PACKED], bpp * handle->w * handle->h)
return true;
} else if (handle->fmt == UHDR_IMG_FMT_12bppYCbCr420) {
READ_BYTES(ifd, handle->planes[UHDR_PLANE_Y], (size_t) handle->w * handle->h)
READ_BYTES(ifd, handle->planes[UHDR_PLANE_U],
(size_t) (handle->w / 2) * (handle->h / 2))
READ_BYTES(ifd, handle->planes[UHDR_PLANE_V],
(size_t) (handle->w / 2) * (handle->h / 2))
return true;
}
return false;
}
return false;
}
static bool writeFile(const char *filename, void *&result, size_t length) {
std::ofstream ofd(filename, std::ios::binary);
if (ofd.is_open()) {
ofd.write(static_cast<char *>(result), length);
return true;
}
std::cerr << "unable to write to file : " << filename << std::endl;
return false;
}
extern "C"
JNIEXPORT void JNICALL
Java_io_github_takusan23_hdrvideotoultrahdr_LibUltraHdr_createUltraHdr(JNIEnv *env, jobject thiz,
jint width, jint height,
jstring rgba1010102_pixel_path,
jstring result_path) {
// pointer
const char *native_rgba1010102_path = env->GetStringUTFChars(rgba1010102_pixel_path, 0);
const char *native_ultra_hdr_path = env->GetStringUTFChars(result_path, 0);
// Load rgba1010102
const size_t bpp = 4;
uhdr_raw_image_t mRawRgba1010102Image{};
mRawRgba1010102Image.fmt = UHDR_IMG_FMT_32bppRGBA1010102;
mRawRgba1010102Image.cg = UHDR_CG_DISPLAY_P3;
mRawRgba1010102Image.ct = UHDR_CT_HLG;
mRawRgba1010102Image.range = UHDR_CR_FULL_RANGE;
mRawRgba1010102Image.w = width;
mRawRgba1010102Image.h = height;
mRawRgba1010102Image.planes[UHDR_PLANE_PACKED] = malloc(bpp * width * height);
mRawRgba1010102Image.planes[UHDR_PLANE_UV] = nullptr;
mRawRgba1010102Image.planes[UHDR_PLANE_V] = nullptr;
mRawRgba1010102Image.stride[UHDR_PLANE_PACKED] = width;
mRawRgba1010102Image.stride[UHDR_PLANE_UV] = 0;
mRawRgba1010102Image.stride[UHDR_PLANE_V] = 0;
loadFile(native_rgba1010102_path, &mRawRgba1010102Image);
// https://github.com/google/libultrahdr/blob/6db3a83ee2b1f79850f3f597172289808dc6a331/examples/ultrahdr_app.cpp#L776-L781
uhdr_codec_private_t *handle = uhdr_create_encoder();
uhdr_enc_set_raw_image(handle, &mRawRgba1010102Image, UHDR_HDR_IMG);
uhdr_enc_set_quality(handle, 95, UHDR_BASE_IMG);
uhdr_enc_set_quality(handle, 95, UHDR_GAIN_MAP_IMG);
uhdr_enc_set_using_multi_channel_gainmap(handle, true);
uhdr_enc_set_gainmap_scale_factor(handle, 1);
uhdr_enc_set_gainmap_gamma(handle, 1.f);
uhdr_enc_set_preset(handle, UHDR_USAGE_BEST_QUALITY);
uhdr_encode(handle);
auto output = uhdr_get_encoded_stream(handle);
// for decoding
uhdr_compressed_image_t mUhdrImage{};
mUhdrImage.data = malloc(output->data_sz);
memcpy(mUhdrImage.data, output->data, output->data_sz);
mUhdrImage.capacity = mUhdrImage.data_sz = output->data_sz;
mUhdrImage.cg = output->cg;
mUhdrImage.ct = output->ct;
mUhdrImage.range = output->range;
uhdr_release_encoder(handle);
writeFile(native_ultra_hdr_path, mUhdrImage.data, mUhdrImage.data_sz);
free(mUhdrImage.data);
}
多分無駄があるような気もしますが、、とりあえずこれで動くので!!
はい!!C++
編終わり!!
Camera2API HDR 動画撮影
の記事で触れたそれです。今回もこれを使います、
その記事もAOSP
が使ってるInputSurface
をお借りしているんですが。。
要はOpenGL ES
のセットアップ(EGL)で、GLES 3
を使うようにして、RGB
を10ビット
にして、EGL_GL_COLORSPACE_BT2020_HLG_EXT
を渡せば良さそう。
とりあえず以下のコードをひらすらコピペしていってください、、
先述の記事との違いは、HDR
のみにして、Canvas
をOpenGL ES
に転写する処理も今回は使わないので消して、glReadPixels
出来るようにしました。
そういえば、Android
にもOpenGLES
とSurfaceView
を合体させたクラスがすでにあるんですが、HDR
行けない気がする、、、
書いたあとに気づいた、Media3 (ExoPlayer)
にHDR
動画からフレームをRGBA1010102
形式でフレームを取り出す機能があるみたいです!!(使ったことなく不明)OpenGL
もMediaCodec
もどうしてもやりたくない場合は調べても良いかも。この記事ではコピペで動くようにしてますが、が、が、まあやりたくはないでしょうね。
先述のCamera2 API HDR動画撮影
の記事から拝借。
これがEGL
まわりで、
/**
* [OpenGlRenderer] で描画する際に OpenGL ES の設定が必要で、引数の Surface に対して、EGL 周りの設定をしてくれるやつ。
* HDR 有効時は EGL 1.4 、GLES 3.0 でセットアップする。[OpenGlRenderer] は GL スレッドから呼び出すこと。
*
* @param outputSurface 出力先 [Surface]
* @param isEnableTenBitHdr 10-bit HDR を利用する場合は true
*/
class InputSurface(private val outputSurface: Surface) {
private var mEGLDisplay = EGL14.EGL_NO_DISPLAY
private var mEGLContext = EGL14.EGL_NO_CONTEXT
private var mEGLSurface = EGL14.EGL_NO_SURFACE
init {
// 10-bit HDR のためには HLG の表示が必要。
// それには OpenGL ES 3.0 でセットアップし、10Bit に設定する必要がある。
eglSetupForTenBitHdr()
}
/** 10-bit HDR version. Prepares EGL. We want a GLES 3.0 context and a surface that supports recording. */
private fun eglSetupForTenBitHdr() {
mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
throw RuntimeException("unable to get EGL14 display")
}
val version = IntArray(2)
if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
throw RuntimeException("unable to initialize EGL14")
}
// Configure EGL for recording and OpenGL ES 3.0.
val attribList = intArrayOf(
EGL14.EGL_RENDERABLE_TYPE, EGLExt.EGL_OPENGL_ES3_BIT_KHR,
EGL14.EGL_RED_SIZE, 10,
EGL14.EGL_GREEN_SIZE, 10,
EGL14.EGL_BLUE_SIZE, 10,
EGL14.EGL_ALPHA_SIZE, 2,
EGL14.EGL_SURFACE_TYPE, (EGL14.EGL_WINDOW_BIT or EGL14.EGL_PBUFFER_BIT),
// EGL_RECORDABLE_ANDROID, 1, // RGBA1010102 だと使えないし多分いらない
EGL14.EGL_NONE
)
val configs = arrayOfNulls<EGLConfig>(1)
val numConfigs = IntArray(1)
EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.size, numConfigs, 0)
checkEglError("eglCreateContext RGBA1010102 ES3")
// Configure context for OpenGL ES 3.0.
val attrib_list = intArrayOf(
EGL14.EGL_CONTEXT_CLIENT_VERSION, 3,
EGL14.EGL_NONE
)
mEGLContext = EGL14.eglCreateContext(
mEGLDisplay, configs[0], EGL14.EGL_NO_CONTEXT,
attrib_list, 0
)
checkEglError("eglCreateContext")
// Create a window surface, and attach it to the Surface we received.
// EGL_GL_COLORSPACE_BT2020_HLG_EXT を使うことで OpenGL ES で HDR 表示が可能になる(HLG 形式)
// TODO 10-bit HDR(BT2020 / HLG)に対応していない端末で有効にした場合にエラーになる。とりあえず対応していない場合は何もしない
val surfaceAttribs = if (isAvailableExtension("EGL_EXT_gl_colorspace_bt2020_hlg")) {
intArrayOf(
EGL_GL_COLORSPACE_KHR, EGL_GL_COLORSPACE_BT2020_HLG_EXT,
EGL14.EGL_NONE
)
} else {
intArrayOf(
EGL14.EGL_NONE
)
}
mEGLSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, configs[0], outputSurface, surfaceAttribs, 0)
checkEglError("eglCreateWindowSurface")
}
/** Prepares EGL. We want a GLES 3.0 context and a surface that supports recording. */
private fun eglSetupForSdr() {
mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
throw RuntimeException("unable to get EGL14 display")
}
val version = IntArray(2)
if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
throw RuntimeException("unable to initialize EGL14")
}
// Configure EGL for recording and OpenGL ES 3.0.
val attribList = intArrayOf(
EGL14.EGL_RED_SIZE, 8,
EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8,
EGL14.EGL_ALPHA_SIZE, 8,
EGL14.EGL_RENDERABLE_TYPE, EGLExt.EGL_OPENGL_ES3_BIT_KHR,
EGL_RECORDABLE_ANDROID, 1,
EGL14.EGL_NONE
)
val configs = arrayOfNulls<EGLConfig>(1)
val numConfigs = IntArray(1)
EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.size, numConfigs, 0)
checkEglError("eglCreateContext RGB888 ES3")
// Configure context for OpenGL ES 3.0.
val attrib_list = intArrayOf(
EGL14.EGL_CONTEXT_CLIENT_VERSION, 3,
EGL14.EGL_NONE
)
mEGLContext = EGL14.eglCreateContext(
mEGLDisplay, configs[0], EGL14.EGL_NO_CONTEXT,
attrib_list, 0
)
checkEglError("eglCreateContext")
// Create a window surface, and attach it to the Surface we received.
val surfaceAttribs = intArrayOf(
EGL14.EGL_NONE
)
mEGLSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, configs[0], outputSurface, surfaceAttribs, 0)
checkEglError("eglCreateWindowSurface")
}
/** Discards all resources held by this class, notably the EGL context. */
fun destroy() {
if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT)
EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface)
EGL14.eglDestroyContext(mEGLDisplay, mEGLContext)
EGL14.eglReleaseThread()
EGL14.eglTerminate(mEGLDisplay)
}
mEGLDisplay = EGL14.EGL_NO_DISPLAY
mEGLContext = EGL14.EGL_NO_CONTEXT
mEGLSurface = EGL14.EGL_NO_SURFACE
}
/**
* Makes our EGL context and surface current.
*/
fun makeCurrent() {
EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)
checkEglError("eglMakeCurrent")
}
/**
* Calls eglSwapBuffers. Use this to "publish" the current frame.
*/
fun swapBuffers(): Boolean {
val result = EGL14.eglSwapBuffers(mEGLDisplay, mEGLSurface)
checkEglError("eglSwapBuffers")
return result
}
/**
* Sends the presentation time stamp to EGL. Time is expressed in nanoseconds.
*/
fun setPresentationTime(nsecs: Long) {
EGLExt.eglPresentationTimeANDROID(mEGLDisplay, mEGLSurface, nsecs)
checkEglError("eglPresentationTimeANDROID")
}
/**
* Checks for EGL errors. Throws an exception if one is found.
*/
private fun checkEglError(msg: String) {
val error = EGL14.eglGetError()
if (error != EGL14.EGL_SUCCESS) {
throw RuntimeException("$msg: EGL error: 0x${Integer.toHexString(error)}")
}
}
/**
* OpenGL ES の拡張機能をサポートしているか。
* 例えば 10-bit HDR を描画する機能は新し目の Android にしか無いため
*
* @param extensionName "EGL_EXT_gl_colorspace_bt2020_hlg" など
* @return 拡張機能をサポートしている場合は true
*/
private fun isAvailableExtension(extensionName: String): Boolean {
val display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
val eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS)
return eglExtensions != null && eglExtensions.contains(extensionName)
}
companion object {
private const val EGL_RECORDABLE_ANDROID = 0x3142
// HDR 表示に必要
private const val EGL_GL_COLORSPACE_KHR = 0x309D
private const val EGL_GL_COLORSPACE_BT2020_HLG_EXT = 0x3540
}
}
これが、OpenGL ES
のテクスチャとして、カメラ映像や動画デコーダーの出力結果が使えるようになるやつ。詳しくは前述の記事で、、、!
/**
* [SurfaceTexture]をラップしたもの、ちょっと使いにくいので
*
* @param initTexName [OpenGlRenderer.generateTextureId]
*/
class TextureRendererSurfaceTexture(private val initTexName: Int) {
private val surfaceTexture = SurfaceTexture(initTexName)
private val _isAvailableFrameFlow = MutableStateFlow(false)
/** [SurfaceTexture.detachFromGLContext]したら false */
private var isAttach = true
/** [SurfaceTexture]へ映像を渡す[Surface] */
val surface = Surface(surfaceTexture) // Surface に SurfaceTexture を渡すというよくわからない API 設計
init {
surfaceTexture.setOnFrameAvailableListener {
// StateFlow はスレッドセーフが約束されているので
_isAvailableFrameFlow.value = true
}
}
/**
* [SurfaceTexture.setDefaultBufferSize] を呼び出す
* Camera2 API の解像度、SurfaceTexture の場合はここで決定する
*/
fun setTextureSize(width: Int, height: Int) {
surfaceTexture.setDefaultBufferSize(width, height)
}
/**
* GL コンテキストを切り替え、テクスチャ ID の変更を行う。GL スレッドから呼び出すこと。
* [OpenGlRenderer]を作り直しする場合など。
*
* @param texName テクスチャ
*/
fun attachGl(texName: Int) {
// 余計に呼び出さないようにする
if (!isAttach) {
surfaceTexture.attachToGLContext(texName)
isAttach = true
}
}
/**
* GL コンテキストから切り離す。GL スレッドから呼び出すこと。
* [OpenGlRenderer]を作り直しする場合など。
*/
fun detachGl() {
if (isAttach) {
surfaceTexture.detachFromGLContext()
isAttach = false
}
}
/** 新しいフレームが来るまで待って、[SurfaceTexture.updateTexImage]を呼び出す */
suspend fun awaitUpdateTexImage() {
// フラグが来たら折る
_isAvailableFrameFlow.first { it /* == true */ }
_isAvailableFrameFlow.value = false
surfaceTexture.updateTexImage()
}
/** [SurfaceTexture.getTransformMatrix]を呼ぶ */
fun getTransformMatrix(mtx: FloatArray) {
surfaceTexture.getTransformMatrix(mtx)
}
/**
* 破棄する
* GL スレッドから呼び出すこと(テクスチャを破棄したい)
* TODO テクスチャを明示的に破棄すべきか
*/
fun destroy() {
val textures = intArrayOf(initTexName)
GLES20.glDeleteTextures(1, textures, 0)
surface.release()
surfaceTexture.release()
}
}
で、これが実際にフラグメントシェーダー等を使って、ビデオデコーダーからでてくる映像をOpenGL ES
で描画している箇所になります。
詳しくは先述の(ry
awaitUpdateTexImage()
は新しいフレームが来るまで待つ関数で、ただそもそもフレームが変化しない事があるので、(30fpsの動画を60fpsのペースで取ったら変化しないので)
それはタイムアウト付きで、永遠に待ったりはしないように。
MediaCodec
周りをいじってたときに、フレームが更新されたときのみテクスチャを更新したら、なんか、気付かないくらいだけど、カクカクしてる気がする。エンコードするとフレーム落ちする事がある問題を修正 · takusan23/AkariDroid@e3d08ef
https://github.com/takusan23/AkariDroid/commit/e3d08ef7e46c8cc36b55740c30d12bf55eef8078
/**
* [OpenGlRenderer]から実際の描画処理を持ってきたもの。OpenGL ES のセットアップは[InputSurface]でやる。
*/
class TextureRenderer() {
private val mTriangleVertices = ByteBuffer.allocateDirect(mTriangleVerticesData.size * FLOAT_SIZE_BYTES).order(ByteOrder.nativeOrder()).asFloatBuffer()
private val mMVPMatrix = FloatArray(16)
private val mSTMatrix = FloatArray(16)
private var mProgram = 0
private var muMVPMatrixHandle = 0
private var muSTMatrixHandle = 0
private var maPositionHandle = 0
private var maTextureHandle = 0
// Uniform 変数のハンドル
private var sSurfaceTextureHandle = 0
// テクスチャ ID
private var surfaceTextureTextureId = 0
init {
mTriangleVertices.put(mTriangleVerticesData).position(0)
}
/**
* SurfaceTexture を描画する。
* GL スレッドから呼び出すこと。
*
* @param surfaceTexture 描画する[SurfaceTexture]
* @param onTransform 位置や回転を適用するための行列を作るための関数
*/
suspend fun drawSurfaceTexture(
surfaceTexture: TextureRendererSurfaceTexture,
onTransform: ((mvpMatrix: FloatArray) -> Unit)? = null
) {
// attachGlContext の前に呼ぶ必要あり。多分
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, surfaceTextureTextureId)
// 映像を OpenGL ES で使う準備
surfaceTexture.detachGl()
surfaceTexture.attachGl(surfaceTextureTextureId)
// 映像が来るまで待つ、500ms に来なかったら更新しない
withTimeoutOrNull(500) {
surfaceTexture.awaitUpdateTexImage()
}
surfaceTexture.getTransformMatrix(mSTMatrix)
// glError 1282 の原因とかになる
GLES20.glUseProgram(mProgram)
checkGlError("glUseProgram")
// テクスチャの ID をわたす
GLES20.glUniform1i(sSurfaceTextureHandle, 0) // GLES20.GL_TEXTURE0
// そのほかの値を渡す
mTriangleVertices.position(TRIANGLE_VERTICES_DATA_POS_OFFSET)
GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices)
checkGlError("glVertexAttribPointer maPosition")
GLES20.glEnableVertexAttribArray(maPositionHandle)
checkGlError("glEnableVertexAttribArray maPositionHandle")
mTriangleVertices.position(TRIANGLE_VERTICES_DATA_UV_OFFSET)
GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices)
checkGlError("glVertexAttribPointer maTextureHandle")
GLES20.glEnableVertexAttribArray(maTextureHandle)
checkGlError("glEnableVertexAttribArray maTextureHandle")
// 行列を適用したい場合
Matrix.setIdentityM(mMVPMatrix, 0)
if (onTransform != null) {
onTransform(mMVPMatrix)
}
// 描画する
GLES20.glUniformMatrix4fv(muSTMatrixHandle, 1, false, mSTMatrix, 0)
GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mMVPMatrix, 0)
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
checkGlError("glDrawArrays")
GLES20.glFinish()
}
/**
* バーテックスシェーダ、フラグメントシェーダーをコンパイルする。
* GL スレッドから呼び出すこと。
*/
fun prepareShader() {
mProgram = createProgram(
vertexSource = VERTEX_SHADER,
// TODO HLG だろうと samplerExternalOES から HDR のフレームが取れてそう
fragmentSource = FRAGMENT_SHADER_10BIT_HDR
)
if (mProgram == 0) {
throw RuntimeException("failed creating program")
}
maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition")
checkGlError("glGetAttribLocation aPosition")
if (maPositionHandle == -1) {
throw RuntimeException("Could not get attrib location for aPosition")
}
maTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTextureCoord")
checkGlError("glGetAttribLocation aTextureCoord")
if (maTextureHandle == -1) {
throw RuntimeException("Could not get attrib location for aTextureCoord")
}
muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix")
checkGlError("glGetUniformLocation uMVPMatrix")
if (muMVPMatrixHandle == -1) {
throw RuntimeException("Could not get attrib location for uMVPMatrix")
}
muSTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uSTMatrix")
checkGlError("glGetUniformLocation uSTMatrix")
if (muSTMatrixHandle == -1) {
throw RuntimeException("Could not get attrib location for uSTMatrix")
}
sSurfaceTextureHandle = GLES20.glGetUniformLocation(mProgram, "sSurfaceTexture")
checkGlError("glGetUniformLocation sSurfaceTexture")
if (sSurfaceTextureHandle == -1) {
throw RuntimeException("Could not get attrib location for sSurfaceTexture")
}
// テクスチャ ID を払い出してもらう
val textures = IntArray(1)
GLES20.glGenTextures(1, textures, 0)
surfaceTextureTextureId = textures[0]
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, surfaceTextureTextureId)
checkGlError("glBindTexture cameraTextureId")
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST.toFloat())
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR.toFloat())
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
checkGlError("glTexParameter")
}
/** テクスチャ ID を払い出す。[SurfaceTexture]を作成するのに必要なので。 */
fun generateTextureId(): Int {
val textures = IntArray(1)
GLES20.glGenTextures(1, textures, 0)
return textures.first()
}
/**
* 描画前に呼び出す。
* GL スレッドから呼び出すこと。
*/
fun prepareDraw() {
// クリア?多分必要
GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT or GLES20.GL_COLOR_BUFFER_BIT)
GLES20.glUseProgram(mProgram)
checkGlError("glUseProgram")
}
/**
* glReadPixels を呼び出す。
* [drawEnd]の後、[AkariGraphicsInputSurface.swapBuffers]の前に呼び出す必要がありそう?
* (swapBuffers の後だと真っ暗だった)
*
* @return [android.opengl.GLES30.glReadPixels] の結果
*/
fun glReadPixels(width: Int, height: Int): ByteArray {
// RGBA で 4バイト使う
val byteArray = ByteArray(4 * height * width)
val byteBuffer = ByteBuffer.wrap(byteArray)
// OpenGL ES の EGL で RGB 10 ビット、Alpha 2 ビット使っているので GL_UNSIGNED_INT_2_10_10_10_REV
GLES30.glReadPixels(0, 0, width, height, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_INT_2_10_10_10_REV, byteBuffer)
return byteArray
}
private fun checkGlError(op: String) {
val error = GLES20.glGetError()
if (error != GLES20.GL_NO_ERROR) {
throw RuntimeException("$op: glError $error")
}
}
/**
* GLSL(フラグメントシェーダー・バーテックスシェーダー)をコンパイルして、OpenGL ES とリンクする
*
* @throws RuntimeException 構文エラーの場合に投げる
* @throws RuntimeException それ以外
* @return 0 以外で成功
*/
private fun createProgram(vertexSource: String, fragmentSource: String): Int {
val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource)
if (vertexShader == 0) {
return 0
}
val pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource)
if (pixelShader == 0) {
return 0
}
var program = GLES20.glCreateProgram()
checkGlError("glCreateProgram")
if (program == 0) {
return 0
}
GLES20.glAttachShader(program, vertexShader)
checkGlError("glAttachShader")
GLES20.glAttachShader(program, pixelShader)
checkGlError("glAttachShader")
GLES20.glLinkProgram(program)
val linkStatus = IntArray(1)
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0)
if (linkStatus[0] != GLES20.GL_TRUE) {
GLES20.glDeleteProgram(program)
program = 0
}
return program
}
/**
* GLSL(フラグメントシェーダー・バーテックスシェーダー)のコンパイルをする
*
* @throws RuntimeException 構文エラーの場合に投げる
* @throws RuntimeException それ以外
* @return 0 以外で成功
*/
private fun loadShader(shaderType: Int, source: String): Int {
val shader = GLES20.glCreateShader(shaderType)
checkGlError("glCreateShader type=$shaderType")
GLES20.glShaderSource(shader, source)
GLES20.glCompileShader(shader)
val compiled = IntArray(1)
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0)
if (compiled[0] == 0) {
// 失敗したら例外を投げる。その際に構文エラーのメッセージを取得する
val syntaxErrorMessage = GLES20.glGetShaderInfoLog(shader)
GLES20.glDeleteShader(shader)
throw RuntimeException(syntaxErrorMessage)
// ここで return 0 しても例外を投げるので意味がない
// shader = 0
}
return shader
}
companion object {
private const val FLOAT_SIZE_BYTES = 4
private const val TRIANGLE_VERTICES_DATA_STRIDE_BYTES = 5 * FLOAT_SIZE_BYTES
private const val TRIANGLE_VERTICES_DATA_POS_OFFSET = 0
private const val TRIANGLE_VERTICES_DATA_UV_OFFSET = 3
private val mTriangleVerticesData = floatArrayOf(
-1.0f, -1.0f, 0f, 0f, 0f,
1.0f, -1.0f, 0f, 1f, 0f,
-1.0f, 1.0f, 0f, 0f, 1f,
1.0f, 1.0f, 0f, 1f, 1f
)
private const val VERTEX_SHADER = """#version 300 es
in vec4 aPosition;
in vec4 aTextureCoord;
uniform mat4 uMVPMatrix;
uniform mat4 uSTMatrix;
out vec2 vTextureCoord;
void main() {
gl_Position = uMVPMatrix * aPosition;
vTextureCoord = (uSTMatrix * aTextureCoord).xy;
}
"""
/** 10-bit HDR の時に使うフラグメントシェーダー */
private const val FRAGMENT_SHADER_10BIT_HDR = """#version 300 es
#extension GL_EXT_YUV_target : require
precision mediump float;
in vec2 vTextureCoord;
uniform __samplerExternal2DY2YEXT sSurfaceTexture;
// 出力色
out vec4 FragColor;
// https://github.com/android/camera-samples/blob/a07d5f1667b1c022dac2538d1f553df20016d89c/Camera2Video/app/src/main/java/com/example/android/camera2/video/HardwarePipeline.kt#L107
vec3 yuvToRgb(vec3 yuv) {
const vec3 yuvOffset = vec3(0.0625, 0.5, 0.5);
const mat3 yuvToRgbColorTransform = mat3(
1.1689f, 1.1689f, 1.1689f,
0.0000f, -0.1881f, 2.1502f,
1.6853f, -0.6530f, 0.0000f
);
return clamp(yuvToRgbColorTransform * (yuv - yuvOffset), 0.0, 1.0);
}
void main() {
vec4 outColor = vec4(0.0, 0.0, 0.0, 1.0);
outColor.rgb = yuvToRgb(texture(sSurfaceTexture, vTextureCoord).rgb);
FragColor = outColor;
}
"""
}
}
さて、今まで作ったInputSurface
、TextureRendererSurfaceTexture
、TextureRenderer
をつなぎ合わせて使えるようにするクラスを作ります。OpenGL ES
はコンテキストをスレッドに紐付けている(自分かどうかの識別にスレッドを使ってる)。ので、マルチスレッドとかはないです。Kotlin Coroutines
のwithContext
を使うことで、スレッドを切り替えるのが容易になったため、このようなスレッドに依存するような処理が便利だった。
詳しい話は先述の記事(ry
/**
* OpenGL ES を利用して[TextureRendererSurfaceTexture]を描画するクラス。
*
* @param width 動画の横
* @param height 動画の縦
* @param outputSurface 描画した内容の出力先 Surface。SurfaceView、MediaRecorder、MediaCodec など
*/
class OpenGlRenderer(
private val width: Int,
private val height: Int,
outputSurface: Surface
) {
/** OpenGL 描画用スレッドの Kotlin Coroutine Dispatcher */
@OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class)
private val openGlRelatedThreadDispatcher = newSingleThreadContext("openGlRelatedThreadDispatcher")
private val inputSurface = InputSurface(outputSurface)
private val textureRenderer = TextureRenderer()
/** OpenGL ES の用意をし、フラグメントシェーダー等をコンパイルする */
suspend fun prepare() {
withContext(openGlRelatedThreadDispatcher) {
inputSurface.makeCurrent()
textureRenderer.prepareShader()
GLES20.glViewport(0, 0, width, height)
}
}
/** テクスチャ ID を払い出す。SurfaceTexture 作成に使うので */
suspend fun generateTextureId(): Int {
return withContext(openGlRelatedThreadDispatcher) {
textureRenderer.generateTextureId()
}
}
/**
* 一回だけ描画し、glReadPixels の結果を返す。
* 返り値の ByteArray は、[Bitmap.createBitmap]して、[Bitmap.copyPixelsFromBuffer] に渡すと Bitmap にできます。
*
* @param width 幅
* @param height 高さ
* @param draw このブロックは GL スレッドから呼び出されます
* @return [draw]した内容を Bitmap にしたもの
*/
suspend fun drawOneshotAndGlReadPixels(width: Int, height: Int, draw: suspend TextureRenderer.() -> Unit): ByteArray {
return withContext(openGlRelatedThreadDispatcher) {
textureRenderer.prepareDraw()
draw(textureRenderer)
val byteArray = textureRenderer.glReadPixels(width, height)
inputSurface.swapBuffers()
byteArray
}
}
/** 破棄する */
suspend fun destroy() {
// try-finally で呼び出されるため NonCancellable 必須
withContext(openGlRelatedThreadDispatcher + NonCancellable) {
inputSurface.destroy()
}
openGlRelatedThreadDispatcher.close()
}
}
mp4
からHEVC
の映像データをデコーダーに渡すことで、出力結果をOpenGL ES
で描画することができます。
AkariDroid/akari-core/src/main/java/io/github/takusan23/akaricore/graphics/mediacodec/AkariVideoDecoder.kt at master · takusan23/AkariDroid
お正月なので動画の上にCanvasで落書きしてエンコードする. Contribute to takusan23/AkariDroid development by creating an account on GitHub.
https://github.com/takusan23/AkariDroid/blob/master/akari-core/src/main/java/io/github/takusan23/akaricore/graphics/mediacodec/AkariVideoDecoder.kt
指定した時間のデータをMediaExtractor
で取り出してMediaCodec
に渡しているだけ、
ちなみにOpenGL ES
なしの、ImageReader
を使う場合でも結局ここのコードは書かないといけない、
余計なコードが入ってるんだけどもう消す気もなく、、
/** MediaCodec を使って動画をデコードする */
class VideoDecoder {
private var decodeMediaCodec: MediaCodec? = null
private var mediaExtractor: MediaExtractor? = null
/** 最後の[seekTo]で取得したフレームの位置 */
private var latestDecodePositionMs = 0L
/** 前回のシーク位置 */
private var prevSeekToMs = -1L
/**
* デコードの準備をする
*
* @param context [Context]
* @param uri PhotoPicker 等で選んだやつ
* @param outputSurface デコードした映像の出力先
*/
fun prepare(context: Context, uri: Uri, outputSurface: Surface) {
val mediaExtractor = MediaExtractor().apply {
context.contentResolver.openFileDescriptor(uri, "r")?.use {
setDataSource(it.fileDescriptor)
}
}
this.mediaExtractor = mediaExtractor
// 動画トラックを探す
val (trackIndex, mediaFormat) = (0 until mediaExtractor.trackCount)
.map { mediaExtractor.getTrackFormat(it) }
.withIndex()
.first { it.value.getString(MediaFormat.KEY_MIME)?.startsWith("video/") == true }
mediaExtractor.selectTrack(trackIndex)
// MediaCodec を作る
val codecName = mediaFormat.getString(MediaFormat.KEY_MIME)!!
decodeMediaCodec = MediaCodec.createDecoderByType(codecName).apply {
configure(mediaFormat, outputSurface, null, 0)
}
decodeMediaCodec?.start()
}
/**
* シークする。
* これを連続で呼び出しフレームを連続で取り出し再生させる。
*
* @param seekToMs 動画フレームの時間
* @return [SeekResult]
*/
suspend fun seekTo(seekToMs: Long): SeekResult {
val isSuccessDecodeFrame = when {
// 現在の再生位置よりも戻る方向に(巻き戻し)した場合
seekToMs < prevSeekToMs -> {
latestDecodePositionMs = prevSeekTo(seekToMs)
SeekResult(
isSuccessful = true,
isNewFrame = true
)
}
// シーク不要
// 例えば 30fps なら 33ms 毎なら新しい Bitmap を返す必要があるが、 16ms 毎に要求されたら Bitmap 変化しないので
// つまり映像のフレームレートよりも高頻度で Bitmap が要求されたら、前回取得した Bitmap がそのまま使い回せる
seekToMs < latestDecodePositionMs -> {
// do nothing
SeekResult(
isSuccessful = true,
isNewFrame = false
)
}
else -> {
// 巻き戻しでも無く、フレームを取り出す必要がある
val framePositionMsOrNull = nextSeekTo(seekToMs)
if (framePositionMsOrNull != null) {
latestDecodePositionMs = framePositionMsOrNull
}
SeekResult(
isSuccessful = framePositionMsOrNull != null,
isNewFrame = true
)
}
}
prevSeekToMs = seekToMs
return isSuccessDecodeFrame
}
/** 破棄する */
fun destroy() {
decodeMediaCodec?.stop()
decodeMediaCodec?.release()
mediaExtractor?.release()
}
/**
* 前回の時間よりも次のフレームを取り出す。
* シークするとキーフレームまで戻ってしまうので、極力シークを避けるようにしています。
*
* @param seekToMs 欲しいフレームの時間
* @return 次のフレームがない場合は null。そうじゃない場合は動画フレームの時間
*/
private suspend fun nextSeekTo(seekToMs: Long): Long? {
val decodeMediaCodec = decodeMediaCodec!!
val mediaExtractor = mediaExtractor!!
var isRunning = true
val bufferInfo = MediaCodec.BufferInfo()
var returnValue: Long? = null
while (isRunning) {
// キャンセル時
yield()
// コンテナフォーマットからサンプルを取り出し、デコーダーに渡す
// シークしないことで、連続してフレームを取得する場合にキーフレームまで戻る必要がなくなり、早くなる
val inputBufferIndex = decodeMediaCodec.dequeueInputBuffer(TIMEOUT_US)
if (0 <= inputBufferIndex) {
// デコーダーへ流す
val inputBuffer = decodeMediaCodec.getInputBuffer(inputBufferIndex)!!
val size = mediaExtractor.readSampleData(inputBuffer, 0)
// データが有ればデコーダーへ、もうデータがなければ終了シグナルを送る
if (0 <= size) {
decodeMediaCodec.queueInputBuffer(inputBufferIndex, 0, size, mediaExtractor.sampleTime, 0)
mediaExtractor.advance()
// シーク先が、果てしなく遠い場所になっているときの対応
// 極力シークしないことで、高速にフレームを取り出せるようにしている(シークすると戻る必要が出てくるので)
// が、動画編集で、動画素材の最後をプレビューする際に、一切シークがないと遅くなってしまう
// なので、果てしなく遠い、つまり、連続で取り出して先にキーフレームが来た場合、その場合は、近場のキーフレームにシークしたほうが速いので
val isAvailable = mediaExtractor.sampleTime != -1L
val isKeyframe = mediaExtractor.sampleFlags and MediaExtractor.SAMPLE_FLAG_SYNC != 0
val extractTimeMs = mediaExtractor.sampleTime / 1000L
if (isAvailable && isKeyframe && extractTimeMs < seekToMs) {
mediaExtractor.seekTo(seekToMs * 1000, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
decodeMediaCodec.flush()
}
} else {
decodeMediaCodec.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
}
}
// デコーダーから映像を受け取る部分
var isDecoderOutputAvailable = true
while (isDecoderOutputAvailable) {
// キャンセル時
yield()
// デコード結果が来ているか
val outputBufferIndex = decodeMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US)
when {
outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER -> {
// リトライが必要
isDecoderOutputAvailable = false
}
0 <= outputBufferIndex -> {
// ImageReader ( Surface ) に描画する
val doRender = bufferInfo.size != 0
decodeMediaCodec.releaseOutputBuffer(outputBufferIndex, doRender)
// もうデコーダーからデータが来ない場合はループを抜ける
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
isRunning = false
isDecoderOutputAvailable = false
returnValue = null
}
if (doRender) {
// 欲しいフレームの時間に到達した場合、ループを抜ける
val presentationTimeMs = bufferInfo.presentationTimeUs / 1000
if (seekToMs <= presentationTimeMs) {
isRunning = false
isDecoderOutputAvailable = false
returnValue = presentationTimeMs
}
}
}
}
}
}
return returnValue
}
/**
* 前回の時間よりも前のフレームを取り出す。
* キーフレームまで戻るため[nextSeekTo]より時間がかかります。
*
* @param seekToMs 欲しいフレームの時間
* @return フレームの時間
*/
private suspend fun prevSeekTo(seekToMs: Long): Long {
val decodeMediaCodec = decodeMediaCodec!!
val mediaExtractor = mediaExtractor!!
// シークする。SEEK_TO_PREVIOUS_SYNC なので、シーク位置にキーフレームがない場合はキーフレームがある場所まで戻る
// エンコードサれたデータを順番通りに送るわけではない(隣接したデータじゃない)ので flush する
mediaExtractor.seekTo(seekToMs * 1000, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
decodeMediaCodec.flush()
// デコーダーに渡す
var isRunning = true
val bufferInfo = MediaCodec.BufferInfo()
var returnValue = 0L
while (isRunning) {
// キャンセル時
yield()
// コンテナフォーマットからサンプルを取り出し、デコーダーに渡す
// while で繰り返しているのは、シーク位置がキーフレームのため戻った場合に、狙った時間のフレームが表示されるまで繰り返しデコーダーに渡すため
val inputBufferIndex = decodeMediaCodec.dequeueInputBuffer(TIMEOUT_US)
if (0 <= inputBufferIndex) {
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
}
0 <= outputBufferIndex -> {
// Surface へ描画
val doRender = bufferInfo.size != 0
decodeMediaCodec.releaseOutputBuffer(outputBufferIndex, doRender)
// 欲しいフレームの時間に到達した場合、ループを抜ける
val presentationTimeMs = bufferInfo.presentationTimeUs / 1000
if (doRender) {
if (seekToMs <= presentationTimeMs) {
isRunning = false
isDecoderOutputAvailable = false
returnValue = presentationTimeMs
}
}
}
}
}
// もうない場合
if (mediaExtractor.sampleTime == -1L) break
}
return returnValue
}
/**
* [seekTo]の結果
*
* @param isSuccessful フレームが取得できた場合は true。もう無い場合などは false。
* @param isNewFrame フレームが更新された場合は true。つまり動画の fps よりも早くフレームを取り出した場合、前回のフレームが使われることがあるため、その場合は false。
*/
data class SeekResult internal constructor(
val isSuccessful: Boolean,
val isNewFrame: Boolean
)
companion object {
/** MediaCodec タイムアウト */
private const val TIMEOUT_US = 0L
}
}
あとは、Activity
に動画を選ぶボタンを付けて、位置を決めてもらって、プレビューを用意して、保存ボタンを押したらUltraHDR
画像が作成されるようにしましょう。
まずはレイアウト。ドーン。
SurfaceView
をCompose
で作る時、今まではAndroidView
を使っていたのですが、
いつの間にかAndroidExternalSurface
ってコンポーネントができていました。しかもコールバックがKotlin Coroutines
のsuspend fun
になっててモダン!!
とりあえず押した時のイベントとかは後回しで、レイアウトだけだとこうです
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
HDRVideoToUltraHDRTheme {
MainScreen()
}
}
}
}
@Composable
private fun MainScreen() {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column(
modifier = Modifier.padding(innerPadding),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Button(onClick = { }) {
Text("動画選択")
}
AndroidExternalSurface(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1.7f)
) {
onSurface { surface, _, _ ->
}
}
Slider(
value = 0f,
onValueChange = {}
)
Button(onClick = { }) {
Text("UltraHDR にして保存")
}
}
}
}
PhotoPicker
を開くように。コールバックの中身はこのあとすぐ!
@Composable
private fun MainScreen() {
// 動画
val videoPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { }
)
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column(
modifier = Modifier.padding(innerPadding),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Button(onClick = {
videoPicker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.VideoOnly))
}) {
Text("動画選択")
}
// 以下省略 ...
動画の時間、縦横サイズをMediaMetadataRetriever
から取り出します。シークできるように。と、縦横はglReadPixels
のときに必要になるので。MediaCodec
とか使っておきながら最後は高レベルAPI
で草とかなしで。
ちなみに、MediaMetadataRetriever#use { }
はAndroid 10
以降のみで、それ以前は手動でクローズする必要があります。
// 動画
val videoDurationMs = remember { mutableLongStateOf(0L) }
val currentPositionMs = remember { mutableLongStateOf(0L) }
val videoWidth = remember { mutableIntStateOf(0) }
val videoHeight = remember { mutableIntStateOf(0) }
val videoPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri ->
uri ?: return@rememberLauncherForActivityResult
// 動画情報
MediaMetadataRetriever().use { retriever ->
context.contentResolver.openFileDescriptor(uri, "r")?.use {
retriever.setDataSource(it.fileDescriptor)
}
videoDurationMs.longValue = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L
videoWidth.intValue = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: 0
videoHeight.intValue = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: 0
}
}
)
雑にLong
をFloat
にしてます、Compose
のスライダー、FloatRange
なんだよな、、
Slider(
value = currentPositionMs.longValue.toFloat(),
valueRange = 0f..videoDurationMs.longValue.toFloat(),
onValueChange = { currentPositionMs.longValue = it.toLong() }
)
まず値を用意します。別に値変化を追跡しなくていいのでmutableStateOf()
は使ってません。再コンポジションに耐えられればいいのでremember { }
だけ。OpenGlRenderer
はインスタンス生成を待ってから、デコーダー等の準備をしないとなので、それだけはState<T>
です。
あと今更ですがContext
とCoroutineScope
を。
val context = LocalContext.current
val scope = rememberCoroutineScope()
// OpenGL ES とか
val renderer = remember { mutableStateOf<OpenGlRenderer?>(null) }
var surfaceTexture = remember<TextureRendererSurfaceTexture?> { null }
var videoDecoder = remember<VideoDecoder?> { null }
まずはOpenGL ES
の描画。これはAndroidExternalSurface
のブロック内でSurface
と、Surface
破棄コールバックが取得できます。
さっき作ったクラスたちを呼び出していきます。
そうだ、SurfaceSize
は指定しないとglReadPixels
で黒色の余白ができてしまいます。
それから、OpenGlRenderer
は内部でglViewport
を呼び出すため、動画の縦横サイズが必要になります。
(なんかglViewport
無くても動きそうな感あるんだけど、、、、一応)
なので、もう面倒なので、videoWidth
とvideoHeight
が0
以外になるのを待ってからAndroidExternalSurface
を描画しようと思います。2箇所で必要になるならもうこれだ。
if (videoWidth.intValue != 0 && videoHeight.intValue != 0) {
AndroidExternalSurface(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1.7f),
surfaceSize = IntSize(videoWidth.intValue, videoHeight.intValue)
) {
onSurface { surface, _, _ ->
// 多分 glViewport を呼ばないといけないので、そのためのサイズ
val viewportWidth = snapshotFlow { videoWidth.intValue }.first { it != 0 }
val viewportHeight = snapshotFlow { videoHeight.intValue }.first { it != 0 }
// OpenGL ES 用意
renderer.value = OpenGlRenderer(viewportWidth, viewportHeight, surface).apply { prepare() }
// 破棄されたら破棄
surface.onDestroyed {
scope.launch {
renderer.value?.destroy()
renderer.value = null
}
}
}
}
}
次にVideoDecoder
とSurfaceTexture
。
これはPhotoPicker
の取得コールバックで作ることにします。これは↑のOpenGL
を待った後に呼び出す必要があるため、
こっちもState<T>
をFlow
にしてインスタンスができるのを待ってます。
val videoPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri ->
uri ?: return@rememberLauncherForActivityResult
// 動画情報
MediaMetadataRetriever().use { retriever ->
context.contentResolver.openFileDescriptor(uri, "r")?.use {
retriever.setDataSource(it.fileDescriptor)
}
videoDurationMs.longValue = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L
videoWidth.intValue = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: 0
videoHeight.intValue = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: 0
}
scope.launch {
// OpenGL を待つ
val processor = snapshotFlow { renderer.value }.filterNotNull().first()
// デコーダーを用意する
surfaceTexture = TextureRendererSurfaceTexture(processor.generateTextureId())
videoDecoder = VideoDecoder().apply {
prepare(context, uri, surfaceTexture.surface)
}
}
}
)
最後に描画する関数を書きましょう。
シークした時なんかはここを呼び出す感じです。JetpackCompose
だとfun draw
よりもval draw = { }
派がが多そう。
isSave
をtrue
にすると写真フォルダにUltraHDR
画像が保存されます。が、今思った、描画のたびにglReadPixels
してるのはいまいちな気がしてきた。
やってることは、
SurfaceView
に描画+glReadPixels
するlibultrahdr
で作ったC++
関数を呼び出してrgba1010102
からUltraHDR
画像を作成そういえば、glReadPixels
は上下逆さまの画像になります。
最後にUltraHDR
のBitmap
を逆さまにする方法でもよいですが、onTransform = { }
の関数で回転行列が渡されて加工できるように作ったので、(作れるようにしたので!!!)OpenGL
に描画する段階で逆さまにして、glReadPixels
すれば、正しい向きになるようになります。
欠点はプレビューも逆さまになる点です。。
それと、なぜかGoogle フォト
で開いたときにUltraHDR
表示になりません。
色々試した限り、Bitmap
のインスタンスを経由してMediaStore
に保存すると、ちゃんとUltraHDR
表示になります(よく分からない)
1行足すだけです。
fun draw(isSave: Boolean = false) {
val processor = renderer.value ?: return
val surfaceTexture = surfaceTexture ?: return
val videoDecoder = videoDecoder ?: return
scope.launch(Dispatchers.Default) {
// 時間を進める
videoDecoder.seekTo(seekToMs = currentPositionMs.longValue)
// 描画する
val glReadPixelsResult = processor.drawOneshotAndGlReadPixels(
width = videoWidth.intValue,
height = videoHeight.intValue
) {
drawSurfaceTexture(
surfaceTexture = surfaceTexture,
onTransform = { mvpMatrix ->
// glReadPixels すると上下逆さまになるので、OpenGL で描画する時点で反転させておく
if (isSave) {
Matrix.scaleM(mvpMatrix, 0, 1f, -1f, 1f)
}
}
)
}
// 保存する場合
if (isSave) {
// 一時的にファイルに保存
val rgba1010102File = context.getExternalFilesDir(null)!!
.resolve("rgba1010102")
.apply { writeBytes(glReadPixelsResult) }
// 完成品パスも
val resultFile = context.getExternalFilesDir(null)!!
.resolve("ultrahdr_${System.currentTimeMillis()}.jpg")
// C++ で書いた libultrahdr を呼び出す
// resultPath に UltraHDR 画像ができる
LibUltraHdr.createUltraHdr(
width = videoWidth.intValue,
height = videoHeight.intValue,
rgba1010102PixelPath = rgba1010102File.path,
resultPath = resultFile.path
)
// UltraHDR として Google フォトで認識されない問題を修正
// 一回 Bitmap にして保存すると治る
val libUltraHdrBitmap = BitmapFactory.decodeFile(resultFile.path)
// Pictures/HDRVideoToUltraHdr フォルダに保存
val fileContentValues = contentValuesOf(
MediaStore.Images.ImageColumns.DISPLAY_NAME to resultFile.name,
MediaStore.Images.ImageColumns.RELATIVE_PATH to "${Environment.DIRECTORY_PICTURES}/HDRVideoToUltraHdr"
)
val uri = context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, fileContentValues)!!
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
resultFile.inputStream().use { inputStream ->
inputStream.copyTo(outputStream)
}
}
// 消す
rgba1010102File.delete()
resultFile.delete()
}
}
}
これを、シークしたときと
Slider(
value = currentPositionMs.longValue.toFloat(),
valueRange = 0f..videoDurationMs.longValue.toFloat(),
onValueChange = {
currentPositionMs.longValue = it.toLong()
// プレビュー更新
draw()
}
)
保存ボタンを押したときはisSave=true
で呼び出して
Button(onClick = { draw(isSave = true) }) {
Text("UltraHDR にして保存")
}
あと、PhotoPicker
で動画を選んで、動画デコーダーを起動したあとも描画するようにしました。このままだとシークバーを動かすまでSurfaceView
にプレビューされないので。。
なおGoogle Pixel
が使ってるExsynos
のGPU
だと何故かここで描画されません。Snapdragon
なら動くのに謎です(???)
// デコーダーを用意する
scope.launch {
// 多分 OpenGL ES が初期化済みなはずなので、!!
surfaceTexture = TextureRendererSurfaceTexture(processor!!.generateTextureId())
videoDecoder = VideoDecoder().apply {
prepare(context, uri, surfaceTexture.surface)
}
// まず一回
draw()
}
ボタンを押して動画を選んで、シークバーを動かすと時間がプレビューに反映されて、保存を押せばUltraHDR
になっているはず!
ちゃんとUltraHDR
表記です。
1 VII
から見て2世代前ですが、ちゃんと動きました!!!
この子も10-bit HDR
動画撮影が可能(しかも最大4K 120fps
でデータヤバそう)
まぶしすぎる
ちゃーはん
ここ福山雅治のMV
のあそこじゃね?!?!(先に曲名言えよ)
どうぞ!
Xperia 1 V
使ってて、新型でたら機種変しよ~~って思ったときにこのアプリを作ろうと思いました。
どーせ新型にUltraHDR
撮影機能なんて搭載されないと思い込んでたので、、
思わぬ収穫だった(標準カメラで撮れるに越したことはない)
glReadPixels
がうまくいかないときに試す
Android
のSurfaceView
のSurfaceHolder.setFixedSize
を呼び出してない
AndroidExternalSurface
の場合はsurfaceSize
引数EGL
を直接扱っている場合、swapBuffer()
よりも前に呼び出す私の作りが怪しい気がするけど!!!、Google Pixel
が採用してるExynos
のGPU
だけ初回時に更新されないんだけど!?!??!
リークだからあれだけど、Pixel 10
からSoC
のベンダーが変わるらしい!よ?試してみたいな
それはそれとして、Snapdragon
のAdreno GPU
が優秀すぎると毎回思う。