どうもこんばんわ。
D.C.5 Plus Happiness
を攻略しました、今年1楽しみゲーム(の割に数ヶ月やれてなかったのは許して)。すごく良かった。
前にやった D.C.5 は全年齢版、今回のはそれにえちえちシーンが追加された版です。
ついにこの先が、全年齢版とかたかだかキスで恋仲を確かめて終わりだし。とか言ってる私が救われた、先が見れて嬉しいよ
ちなみに5
ってついてるけど別に前作知らなくても遊べます!!!やろう。
面白かったらD.C.4
やると良いと思います。
心なしか色っぽいみずはちゃんのパッケのやつ!!!
(せっかくなので一番高いやつ買ってみたよ~~)
もうすでにD.C.5
の全年齢版をやってるのでえちえちシーンだけ見ても良いんだけど、、、見たいお話あるから2周目やります!!
2周目だから気付けるところがあって良い。あーこれかー的な。あ~~~~
!?!??!
まってえちえちシーン後付けじゃない!?、本編のシナリオに組み込まれてる。
なんなら本編のシナリオも影響ない範囲で少しだけ変わってそう?全年齢版で見たことないお話が!!?!?
かこちゃん!!
あとあんまり関係ないけど後ろ姿がD.C.
やってるな~って感じでいい
ここすき
かこちゃん、他の子のルートでいい味してるんだよな。
姉には辛辣なのおもろい
ゆきねえ!!
1周目だと見落としちゃいそうだけど、お互い心配してるのね、
自信満々な雪姉いい、それはそうと一回お話を知ってるので、要所要所にあるのが引っかかって辛いわね。
で一番最後は解決したからめにょあがいるのか。(2週目でわかった)
あと豪華版についてくる雪姉の歌うED曲のカバーが良い!。
めにょあ!!
かこちゃんがいい味、姉妹だ。
かわいい
タジタジめにょあ
あとすでにキャラ崩壊きてないか?
いまからキャラ崩壊めにょあが楽しみすぎる!!!!!
みずはちゃん!!!、キャラデザがよすぎる、かわいい。
それはそうとパン食い競争好きすぎるみずはちゃんかわいい。
そんな子がいかがわしい知識を得たら。。。最強。最強だった
グイグイ来るみずはちゃん、つよい
!?!?!?
じゃあ一番楽しみのあかりちゃんルート見ますか!!!。
まって声かわいいんだけど!?
ときたま引っかかるのも2週目でわかった、あーね。
それはそうとこの子のルートは唯一(!!)一緒に住んでるわけじゃないから丁寧に?書かれている感じがした。
いい!!!律儀だ
かわいい!かわいい
(この辺からsurface
が壊れてスクショに手間取ってる)
まあ何でも良いのでとにかくこの子のルートにある、何が好きですか?の部分と、チケットいらないですよ?のお話を見てきてください、
まじでこのシナリオいい!!!!!!!
!?!?!??
お話が良すぎる、キャラデザ、声全部いい!!、ぜひ!!!おすすめです
じゃあ私は全年齢版との差分探すから終わるね。
本題
終わりません。
この記事に貼り付けられている動画は、多分眩しいので暗闇で開いている方 がいたらは動画を見ないほうがいいかもしれません、、、
HDR (はいだいなみっくれんじ)
の動画って見たことありますか?あのディスプレイの明るさを貫通して眩しい動画
YouTube とかでHDR
って調べると見れると思います、あと周りのSDR
の部分が暗くなるあれ。
また、最近の端末だとスマホでもHDR
の動画が撮影できます。
とくにPixel
シリーズだと画面輝度がかなり明るいので結構わかりやすいと思う。てか眩しい。
これは東京タワーとスカイツリーが見えるってやつ。HDR
動画です。Chrome
だとデコードできるんかな。
(とっさに撮ったのでブレッブレ、音はないです)
サンプルとしてソースコードとともにGitHub
に上げておきました。(ファイル地味に大きくてごめんGitHub
)
https://github.com/takusan23/AndroidMediaCodecHdrVideoCanvasOverlay/blob/master/sample/10_bit_hdr_hevc.mp4
他にもあります、これはCamera2 API
を叩いて取ってみました。こちらです。
別に叩かずとも標準のカメラアプリをHDR
動画撮影有効で撮影すればいいだけです。
VIDEO
流石にデカすぎるのでYouTube
に上げたものを貼ります。
画質設定から HDR と書かれているものを選んでください。
眩しいのが思った以上に嫌われてて笑ったけど、今回はAndroid
でHDR 動画
の撮影と、動画編集の話をします。
HDR
の対になる単語がSDR (すたんだーどだいなみっくれんじ)
で、今までのMediaCodec
シリーズで扱ってきたものはすべてSDR
になります。
SDR
の時はあんまり意識しなくても使えましたが、HDR
の場合そうは行かないので、HDR
周りのAPI
を叩くために必要な知識みたいなのも書いていきたい。
自作アプリの対応状況
サンタさんです(大遅刻)
動画を扱う自作アプリでもHDR
の動画を扱えるようにアップデートしました。
この成果としてこの記事があるわけですね。
前後カメラを重ねて保存できるカメラアプリ
逆再生動画作成アプリ
再エンコードアプリ
動画編集アプリ(大した機能はない)
HDR
対応によりOpenGL ES
の力を借りないといけなくなってしまい、
前に記事で書いた逆再生の動画を作る 話と、動画編集アプリを作った 話、それぞれ書いたときとは大きく変わったソースコードになってしまいました、あの頃の記事で書いたコードではもう無い。。。
ま、まあOpenGL
とかいう複雑を取ってもなお速度向上が得れたので良かった。
2つの意味で使われる HDR
さて、まずはHDR
についてなんですが、まさかのここから話さないといけないという。
HDR
という単語、カメラの世界 では動画撮影 以外にも静止画撮影 時にも使われます。
しかし、この2つは大きく違うものなので注意しましょう。今回の記事は動画の HDR
の話です。
動画の HDR
https://www.eizo.co.jp/eizolibrary/color_management/hdr/
今回の話はこっちです。動画の方です。
従来のSDR
動画と比べて、HDR
で撮影するとより多くの明るさと色を使って撮影できる。
詳しくは後で話しますが、
SDR
で使っている色はBT.709 (Rec.709)
の範囲でしたが、HDR
ではBT.2020 (Rec.2020)
という更に広い色の範囲を使うため、より鮮やかになるわけです。
また、SDR
の時はRGB
をそれぞれ8 ビット(0xff)
で保存していたのに対して、HDR
だと10 ビット
で保存されます。
眩しく見えるのは8 ビット
以上を表現できる様になったからでしょうか(多分...)。
今回の記事では、写真のHDR
との差別化のため10 ビット HDR
と書き直すことにします。
写真の HDR
本題から外れてしまうので手短に。クソややこしい。
これは、複数回カメラの露出
を変えて撮影し、最終的に 1 枚の写真に合成することで、暗い部分が明るく取れるってやつです(逆光に強いとか)。
はい。。撮影方法が違うだけで、使っている色は SDR の頃から変わっていません。
なので、HDR
静止画撮影を有効にして撮影したとしても別にHDR
らしく明るく表示されたりするわけではありません。
これが動画のHDR
と、静止画撮影のHDR
は全く別と言われている理由です。
もしCamera2 API
でこの静止画撮影の HDR を有効にしたい場合は、Camera2 API 拡張機能
を使うのが多分正解です。
CameraExtensionCharacteristics.EXTENSION_HDR
https://developer.android.com/reference/android/hardware/camera2/CameraExtensionCharacteristics
Ultra HDR
これらのHDR
とか言うくせに蓋を開けたらSDR
のそれとは違い、動画のHDR
のように明るい写真を取れるような技術があります。Pixel 8
シリーズから追加されたUltra HDR
ですね。
SDR
で撮影した写真とともに、HDR
らしく明るさのためのゲインマップが保存されています。
HDR ざっくり入門
さて、いい加減コード書けやって話ですが、HDR
に関連する用語の整理を先にします。。
HLG / PQ / ST 2084 / BT.2020 / Dolby Vision
、このあたり何って?
https://developer.android.com/media/camera/camera2/hdr-video-capture
HDR の種類
HDR
といっても4つぐらい種類があります。
カメラアプリだとHDR
の ON/OFF くらいしかないのですが、例えば動画編集をするとなるとこの辺を意識する必要があります。
名前 HLG HDR10 HDR10+ ドルビービジョン (Dolby Vision profile 8.4) 特徴 SDR ディスプレイでも見れる 標準? HDR10 にメタデータを入れたもの profile 8.4 は HLG と互換性あり。本物はガンマカーブが PQ 映像コーデック H.265 (HEVC) H.265 (HEVC) H.265 (HEVC) HLG と互換性あり 色空間 (色域、カラースペース) BT.2020 (Rec.2020) BT.2020 (Rec.2020) BT.2020 (Rec.2020) HLG と互換性あり ガンマカーブ(伝達関数、EOTF) HLG PQ (ST 2084) PQ (ST 2084) HLG と互換性あり
ややこしいね。
また、HDR
形式の変換もそう簡単には出来ないようです(HLG
とPQ (ST 2084)
の変換)。
Android
では、HDR
動画撮影に対応している場合、少なくともHLG
形式のHDR
に対応している必要があるそうで、
今回の記事では HLG 形式の 10 ビット HDR 動画を扱います。 動画撮影も動画編集もHLG
形式を使います。
世の Android 端末が採用している HDR 形式調査
どうやら、Android 端末によってカメラアプリの HDR の形式が違うようです。
というわけでヨドバシで色々見てみました。
Pixel
とXperia
はHLG
。
Pixel
は記載ないですが、Google フォト
を見る限りHLG
です。Pixel
も60fps
で10 ビット HDR
撮影できるようにしてほしい。
Galaxy
、一部の Xiaomi
はHDR10+
。
Oppo
と一部の Xiaomi
、あとAQUOS R9 pro (!?)
はドルビービジョン
でした。
あと Android 関係ないけどiPhone
もこれ。(iPhone
はデフォルトでHDR
動画撮影が有効らしい、強気だ)
が、が、が、後述しますが、ドルビービジョンはドルビービジョンでもDolby Vision profile 8.4
ってやつな気がします。
Android
開発のお仕事の人は大変だろうね~、HDR
3つあって(笑)
仕事ですからね。仕方ないですね。
色空間
https://www.eizo.co.jp/eizolibrary/color_management/hdr/index2.html
これは表示できる色の範囲のことです。
10 ビット HDR
では、BT.709
よりも更に広い範囲を扱えるようにしたBT.2020
を利用します。
BT.709
はRec.709
、BT.2020
もRec.2020
って別名がついています。
開発時のドキュメントでRec.
の方で書かれてたとしても、同じですのでびっくりしないでください。
ガンマカーブ
HLG
やPQ (ST 2084)
のことですね。多分伝達関数とかのがあってそうな気がする。
、、、で、ガンマカーブって何?って話なんですが、説明できる気がしないので、ここでは2つのガンマカーブの違いだけを話します。
HLG
は、Wikipedia
曰く途中までSDR
のガンマカーブとほぼ同じだそうで、SDR
との互換性が高い。
PQ (ST 2084)
はHDR - SDR
のような互換性がないものの、Wikipedia
曰く人の視覚に合わせて作ったそうな。
映像コーデック
HDR
を扱える動画コーデックは、HEVC (H.265)
、VP9
、AV1
あたりがあります。
この中で使えるものを、手元のAndroid
端末で試したところ、HEVC
とAV1エンコーダー が10 ビット HDR
動画をエンコードすることが出来てそうでした。
ただ、AV1
はハードウェアエンコーダーがほぼ無いため、現状HEVC
を選ぶしか無さそう。VP9
もHDR
いけるってどっかで見たけどAndroid
だとダメそう?
覚えないとだめなの?
はい。
というのも、動画ファイル(コンテナフォーマット)には色空間とガンマカーブ の種類を保存しているらしく、HLG / HDR10 / HDR10+ / ドルビービジョン
かどうかは、
取得した色空間とガンマカーブから求める必要があります。
動画ファイルから一発でHLG / HDR10
等を取得できるわけじゃないので注意です。
これはMediaCodec
(エンコーダー)にも言えることで、同様にHLG / HDR10
を指定するAPI
ではなく、代わりに色空間
、ガンマカーブ
を指定するAPI
になっているので、
自分がエンコードしたいHDR
の種類の色空間
(まあこれはどれを選んでも BT.2020 ですが)、ガンマカーブ
を指定する必要があります。
Google フォト
がHLG
とかHDR10
とか表示していて、そういう一発で取得できるAPI
があるもんだと思ってたら普通に違った。
ドルビービジョン←これ本当に何
そもそもこれ、会社の名前がついているあたり自由に使えない可能性がある。
まあこれはH.265 (HEVC)
にも言えるんですが、、、
さて、これで説明を終わろうと思ったんですが、ガチで厄介なやつがいました。ドルビービジョンです。
iPhone
をはじめ、Xiaomi
、Oppo
、あとはAQUOS R9 pro
もこれでした。
Wikipedia
を読むあたり、ガンマカーブはPQ
を使ってるらしいのですが、撮影したデータを見るとなぜか HLG
なんですよね。
ん~~~~????
というわけでもう少し調べると、どうやらiPhone
とかXiaomi
とかが謳ってるドルビービジョン
はドルビービジョン
の中でもDolby Vision profile 8.4
とかいうやつらしく、
これはガンマカーブがPQ
ではなくHLG
らしい。それに加えて、ドルビービジョン
用のメタデータか何かが入っている。
ただ、そのドルビービジョン
のメタデータとやらを取っ払えばHLG
形式のHDR
として解釈していいらしい。。。
現にドルビービジョンが再生できない端末に対してはGoogle
フォト上ではHLG
と解釈されてそうですね。多分HEVC
のデコーダーに流せばデコードできるってことなのかな。
じゃあHLG
と互換があると思い込んでHEVC
のデコーダーを用意すると、多分今度は本物のドルビービジョン(言い方があれ、ガンマがPQ
の方)が無理な気がするんだけど。
まじでなにこれ?
あと開発者向けドキュメントあります。なぜかC++
だけど、、
https://professionalsupport.dolby.com/s/application-development-android?language=en_US
HDR に詳しくなりたいよ~~
Apple
の動画。冒頭はHDR
の概要なのでAndroid
関係なく動画に詳しくなれそう。
日本語の字幕があります。Firefox
なら字幕もPinP
できるはず。
https://developer.apple.com/jp/videos/play/wwdc2020/10010/
付録 HDR の動画に対応していないので、アプリ内で弾きたい
SDR
の動画しか対応していないアプリを作っている人向け。
取り急ぎフォトピッカー
でHDR
の動画を選んでしまった場合にエラーを出したいときに使ってください。
/**
* HDR かどうか判定する
*
* @param context [Context]
* @param uri フォトピッカー等で選んで
* @return true で HDR(HLG / HDR10 / HDR10+)
*/
private fun isHdrVideo (context: Context , uri: Uri ): Boolean {
return MediaMetadataRetriever (). use { mediaMetadataRetriever ->
// Uri を MediaMetadataRetriever に渡す
context.contentResolver. openFileDescriptor (uri, "r" )?. use {
mediaMetadataRetriever. setDataSource (it.fileDescriptor)
}
// 色空間、ガンマカーブを取得する
val colorSpace = mediaMetadataRetriever. extractMetadata (MediaMetadataRetriever.METADATA_KEY_COLOR_STANDARD)?. toInt ()
val colorTransfer = mediaMetadataRetriever. extractMetadata (MediaMetadataRetriever.METADATA_KEY_COLOR_TRANSFER)?. toInt ()
// HDR なら色空間が BT.2020、ガンマカーブが HLG か PQ であるはず
colorSpace == MediaFormat.COLOR_STANDARD_BT2020 && (colorTransfer == MediaFormat.COLOR_TRANSFER_HLG || colorTransfer == MediaFormat.COLOR_TRANSFER_ST2084)
}
}
@OptIn (ExperimentalMaterial3Api:: class )
@Composable
private fun MainScreen () {
val isHdr = remember { mutableStateOf ( false ) }
val context = LocalContext.current
val videoPicker = rememberLauncherForActivityResult (ActivityResultContracts. PickVisualMedia ()) { uri ->
uri ?: return @rememberLauncherForActivityResult
isHdr. value = isHdrVideo (context, uri)
}
Scaffold (
topBar = {
TopAppBar (
title = { Text (text = "HDR 判定くん" ) }
)
}
) { innerPadding ->
Column (modifier = Modifier. padding (innerPadding)) {
Button (onClick = { videoPicker. launch ( PickVisualMediaRequest (ActivityResultContracts.PickVisualMedia.VideoOnly)) }) {
Text (text = "動画を選ぶ" )
}
Text (text = if (isHdr. value ) "HDR 動画です" else "HDR 動画ではありません" )
}
}
}
もしくは、SDR
動画の場合は色空間がBT.709
なはずなので、SDR
かどうかを判定してもいいかもしれません。
/** SDR かどうか判定する */
private fun isSdrVideo (context: Context , uri: Uri ): Boolean {
return MediaMetadataRetriever (). use { mediaMetadataRetriever ->
// Uri を MediaMetadataRetriever に渡す
context.contentResolver. openFileDescriptor (uri, "r" )?. use {
mediaMetadataRetriever. setDataSource (it.fileDescriptor)
}
// 色空間を取得する
val colorSpace = mediaMetadataRetriever. extractMetadata (MediaMetadataRetriever.METADATA_KEY_COLOR_STANDARD)?. toInt ()
// SDR なら BT.709 のはず
colorSpace == MediaFormat.COLOR_STANDARD_BT709
}
}
環境
なまえ あたい Android Studio Android Studio Ladybug 2024.2.1 Patch 3 minSdk 33 たんまつ Pixel 8 Pro / Xperia 1 V (Camera2 API
でHDR撮影
のアプリは動きません)
Xperia 1 V
も最大4K 120fps
でHDR
動画が撮影できるのですが、Camera2 API には開放していない らしく標準カメラアプリ以外ではHDR
撮影ができません。Android
あるある。
後継機では直ってるといいなあこれ
Camera2 API で HDR 動画撮影をやってみる
動画撮影だけならOpenGL ES
はでてきません。安心!
CameraX は
CameraX
だとすぐ作れるらしいです!!!。
未だに使ったことがなくわからないですが、見た感じフラグを指定するだけなのかなって思ってたり。
https://android-developers.googleblog.com/2023/06/camerax-13-is-now-in-beta.html
付録 Camera2 API で対応している HDR の種類を取得する
getCameraCharacteristics
で、CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES
を取得すると、Camera2 API
側で対応しているHDR
の種類が取得できます。
https://developer.android.com/media/camera/camera2/hdr-video-capture?hl=ja#check_for_hdr_support
class MainActivity : ComponentActivity () {
override fun onCreate (savedInstanceState: Bundle ?) {
super . onCreate (savedInstanceState)
enableEdgeToEdge ()
val cameraManager = getSystemService (Context.CAMERA_SERVICE) as CameraManager
// バックカメラ
val backCameraId = cameraManager.cameraIdList. first { cameraId ->
cameraManager. getCameraCharacteristics (cameraId). get (CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK
}
val characteristic = cameraManager. getCameraCharacteristics (backCameraId)
val capabilities = characteristic[CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES]?.supportedProfiles ?: emptySet ()
val isHlg = DynamicRangeProfiles.HLG10 in capabilities
val isHdr10 = DynamicRangeProfiles.HDR10 in capabilities
val isHdr10plus = DynamicRangeProfiles.HDR10_PLUS in capabilities
println ( "isHlg = $isHlg / isHdr10 = $isHdr10 / isHdr10plus = $isHdr10plus " )
}
}
Pixel 8 Pro の場合はHLG
のみ対応でした。
isHlg = true / isHdr10 = false / isHdr10plus = false
Xperia 1 V はCamera2 API
でHDR
が利用できないので全部false
です。。。
AndroidManifest
適当なJetpack Compose
のプロジェクトを作ったら2つの権限を追加してください。カメラとマイクです。
< uses-permission android:name = "android.permission.CAMERA" />
< uses-permission android:name = "android.permission.RECORD_AUDIO" />
権限チェック画面とカメラ画面
動画撮影ということで、カメラと録音権限がない場合は先に要求する画面に遷移するようにしました。
本当はnavigation compose
あたりで画面切り替えするべきですが、まあサンプルなので、、、
class MainActivity : ComponentActivity () {
override fun onCreate (savedInstanceState: Bundle ?) {
super . onCreate (savedInstanceState)
enableEdgeToEdge ()
setContent {
val context = LocalContext.current
val isPermissionGranted = remember {
mutableStateOf (REQUIRED_PERMISSION. all { permission -> ContextCompat. checkSelfPermission (context, permission) == PackageManager.PERMISSION_GRANTED })
}
AndroidCamera2HdrTheme {
// 権限があればカメラ画面へ
if (isPermissionGranted. value ) {
MainScreen ()
} else {
PermissionScreen (onGranted = { isPermissionGranted. value = true })
}
}
}
}
}
/** 必要な権限 */
private val REQUIRED_PERMISSION = listOf (android.Manifest.permission.CAMERA, android.Manifest.permission.RECORD_AUDIO)
/** 権限ください画面 */
@Composable
private fun PermissionScreen (onGranted: () -> Unit) {
val permissionRequest = rememberLauncherForActivityResult (ActivityResultContracts. RequestMultiplePermissions ()) { it ->
if (it. all { it. value /* == true */ }) {
onGranted ()
}
}
Scaffold {
Column (Modifier. padding (it)) {
Button (onClick = { permissionRequest. launch (REQUIRED_PERMISSION. toTypedArray ()) }) {
Text (text = "権限を付与してください" )
}
}
}
}
/** カメラ画面 */
@Composable
private fun MainScreen () {
// このあとすぐ
}
カメラ操作用クラス
を作ります、Camera2 API
は複雑で、Jetpack Compose
のUI
といっしょに書くわけには行かないので、、、
まあこの辺を自力で書くくらいならCameraX
使えばいいのではと思った。
@SuppressLint ( "MissingPermission" )
class CameraController ( private val context: Context ) {
// このあとすぐ
}
プレビュー
先にコードを貼ります。どーーーん。
class CameraController ( private val context: Context ) {
private val cameraManager = context. getSystemService (Context.CAMERA_SERVICE) as CameraManager
private val cameraExecutor = Executors. newSingleThreadExecutor ()
private val scope = CoroutineScope (Dispatchers.Default + Job ())
private val _isRecording = MutableStateFlow ( false )
private var mediaRecorder: MediaRecorder ? = null
private var saveVideoFile: File ? = null
/** 今の処理キャンセル用 */
private var currentJob: Job ? = null
/** Surface の生成を通知する Flow */
private val _surfaceFlow = MutableStateFlow < Surface ?>( null )
/** カメラを開いて、状態を通知する Flow */
private val backCameraDeviceFlow = callbackFlow {
var _device: CameraDevice ? = null
// openCamera は複数回コールバック関数を呼び出すので flow にする必要がある
cameraManager. openCamera ( getBackCameraId (), cameraExecutor, object : StateCallback () {
override fun onOpened (camera: CameraDevice ) {
_device = camera
trySend (camera)
}
override fun onDisconnected (camera: CameraDevice ) {
// TODO エラー処理
_device?. close ()
_device = null
trySend ( null )
}
override fun onError (camera: CameraDevice , error: Int ) {
// TODO エラー処理
_device?. close ()
_device = null
trySend ( null )
}
})
// キャンセル時
awaitClose { _device?. close () }
}. stateIn (
scope = scope,
started = SharingStarted.Eagerly,
initialValue = null
)
/** 初回時、録画終了後に呼び出す */
fun prepare () {
scope. launch {
currentJob?. cancelAndJoin ()
// MediaRecorder を作る
initMediaRecorder ()
currentJob = launch {
// SurfaceView の生存に合わせる
// アプリ切り替えで SurfaceView は再生成されるので、常に監視する必要がある
_surfaceFlow. collectLatest { previewSurface ->
previewSurface ?: return @collectLatest
// 外カメラが開かれるのを待つ
val cameraDevice = backCameraDeviceFlow. filterNotNull (). first ()
// カメラ出力先。プレビューと MediaRecorder
val outputSurfaceList = listOfNotNull (previewSurface, mediaRecorder?.surface)
// CaptureRequest をつくる
val captureRequest = cameraDevice. createCaptureRequest (CameraDevice.TEMPLATE_RECORD). apply {
// HDR はここじゃない
outputSurfaceList. forEach { surface -> addTarget (surface) }
// FPS とかズームとか設定するなら
// set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(VIDEO_FPS, VIDEO_FPS))
}
// CaptureSession をつくる
val captureSession = cameraDevice. awaitCameraSessionConfiguration (outputSurfaceList = outputSurfaceList) ?: return @collectLatest
// プレビュー開始
captureSession. setRepeatingRequest (captureRequest. build (), null , null )
}
}
}
}
/** Surface の生成コールバックで呼び出す */
fun createSurface (surface: Surface ) {
_surfaceFlow. value = surface
}
/** Surface の破棄コールバックで呼び出す */
fun destroySurface () {
_surfaceFlow. value = null
}
/** MediaRecorder と仮のファイルを作る */
private fun initMediaRecorder () {
// 一時的に getExternalFilesDir に保存する
saveVideoFile = context. getExternalFilesDir ( null )!!. resolve ( " ${ System. currentTimeMillis () } .mp4" )
mediaRecorder = MediaRecorder (context). apply {
setAudioSource (MediaRecorder.AudioSource.MIC)
setVideoSource (MediaRecorder.VideoSource.SURFACE)
setOutputFormat (MediaRecorder.OutputFormat.MPEG_4)
setVideoEncoder (MediaRecorder.VideoEncoder.HEVC) // 10 ビット HDR 動画の場合は HEVC にする
setAudioEncoder (MediaRecorder.AudioEncoder.AAC)
setAudioChannels ( 2 )
setVideoEncodingBitRate ( 20_000_000 )
setVideoFrameRate ( 60 )
setVideoSize (VIDEO_WIDTH, VIDEO_HEIGHT)
setAudioEncodingBitRate ( 192_000 )
setAudioSamplingRate ( 48_000 )
setOutputFile (saveVideoFile)
prepare ()
}
}
/** CameraDevice#createCaptureSession をサスペンド関数にしたもの */
private suspend fun CameraDevice . awaitCameraSessionConfiguration (outputSurfaceList: List < Surface >): CameraCaptureSession ? = suspendCoroutine { continuation ->
// OutputConfiguration を作る
val outputConfigurationList = outputSurfaceList
. map { surface -> OutputConfiguration (surface) }
. onEach { outputConfig ->
// 10 ビット HDR 動画撮影を有効にする
// HDR 動画撮影に対応している場合、少なくとも HLG 形式の HDR に対応していることが保証されているので HLG
if ( isSupportedTenBitHdr ()) {
outputConfig.dynamicRangeProfile = DynamicRangeProfiles.HLG10
}
}
val sessionConfiguration = SessionConfiguration (SessionConfiguration.SESSION_REGULAR, outputConfigurationList, cameraExecutor, object : CameraCaptureSession . StateCallback () {
override fun onConfigured (captureSession: CameraCaptureSession ) {
continuation. resume (captureSession)
}
override fun onConfigureFailed (p0: CameraCaptureSession ) {
// TODO エラー処理
continuation. resume ( null )
}
})
createCaptureSession (sessionConfiguration)
}
/** 10 ビット HDR 動画撮影に対応している場合は true */
private fun isSupportedTenBitHdr (): Boolean {
val characteristic = cameraManager. getCameraCharacteristics ( getBackCameraId ())
val capabilities = characteristic[CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES]
return capabilities?. contains (CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT) == true
}
/** バックカメラの ID を返す */
private fun getBackCameraId (): String = cameraManager
.cameraIdList
. first { cameraId -> cameraManager. getCameraCharacteristics (cameraId). get (CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK }
companion object {
// フル HD
const val VIDEO_WIDTH = 1920
const val VIDEO_HEIGHT = 1080
const val VIDEO_FPS = 60
}
}
backCameraDeviceFlow
ってのがカメラを開いたあとの状態を通知するコールバックをFlow
にしたものになります。
コールバックが複数回呼ばれるのでFlow
にする必要があります。
stateIn()
でHotFlow
に変換していますが、これはアプリ利用中は常にCameraDevice
を使い回すためです。撮影が終わってプレビューに戻る際もCameraDevice
は同じものを使うため。
SurfaceView
はJetpack Compose
側で作ってもらうため、こちらは受け取り口を用意し、StateFlow
に入れています。
このFlow
を収集し、null
以外になったらプレビューを開始しています。
collectLatest { }
のなかでプレビューを始める処理をやっています。
本家のドキュメントではMediaCodec
で録画しろって書かれてますが、MediaRecorder
でもHDR
動画が録画できます。(HLG
しか見てないケド)
Surface
を2箇所渡す必要があるのを忘れないようにしてください。やらないとこれ : Each request must have at least one Surface target
Camera2 API
でHDR
動画撮影する場合はOutputConfiguration
でHDR
の形式をセットする形になります。
今回はHLG
で。また、端末の標準カメラアプリが対応していても、Camera2 API でサポートしているかはまた別の話なので、isSupportedTenBitHdr()
関数で確認しています。
10 ビット
に対応しているかは、getCameraCharacteristics()
でCameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES
を取得し、CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT
が中にあればいいはずです。
今回はHLG
を使うのでこれ以上は見ていないですが(先述の通り最低限HLG
形式はサポートされる保証がある)、HLG
ではなくHDR10
やドルビービジョン???
で撮影がしたい場合なんかは
CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES
を使うと対応しているHDR
の形式を取得できるそうです。
val availableProfiles = characteristic[CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES]?.supportedProfiles
val isHdr10Support = availableProfiles?. contains (DynamicRangeProfiles.HDR10) == true
録画部分
といってもあとはMediaRecorder
の開始と終了を呼ぶところを追加するだけなんですけどね。
MediaRecorder()
で撮影したデータはgetExternalFilesDir()
にあって、そのままでは写真アプリ
空見れないのでMediaStore
に登録して移動させています。
/** 録画中か */
val isRecording = _isRecording. asStateFlow ()
/** 撮影開始 */
fun startRecord () {
mediaRecorder?. start ()
_isRecording. value = true
}
/** 撮影終了 */
fun stopRecord () {
scope. launch {
// 処理を止める
currentJob?. cancelAndJoin ()
// 録画停止
mediaRecorder?. stop ()
mediaRecorder?. release ()
_isRecording. value = false
// 動画データを動画フォルダへ移動
val contentResolver = context.contentResolver
val contentValues = contentValuesOf (
MediaStore.Images.Media.DISPLAY_NAME to saveVideoFile!!.name,
MediaStore.Images.Media.RELATIVE_PATH to " ${ Environment.DIRECTORY_MOVIES } /AndroidCamera2HdrVideo"
)
val uri = contentResolver. insert (MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues)!!
saveVideoFile!!. inputStream (). use { inputStream ->
contentResolver. openOutputStream (uri)?. use { outputStream ->
inputStream. copyTo (outputStream)
}
}
saveVideoFile?. delete ()
// MediaRecorder は使い捨てなので、準備からやり直す
prepare ()
}
}
/** 終了時に呼ぶ */
fun destroy () {
scope. cancel ()
cameraExecutor. shutdown ()
}
UI 部分
こっちは特にないかな、SurfaceView
と録画開始、終了
ボタンをおいただけ。
注意点としては横画面しか対応できていない点です。縦動画は直してないので撮れません、、、
SurfaceHolder#setFixedSize
を呼ぶことでCamera2 API
のプレビューが歪む という初見殺しも対応できます。
https://developer.android.com/reference/android/hardware/camera2/CameraDevice#createCaptureSession(android.hardware.camera2.params.SessionConfiguration)
/** カメラ画面 */
@Composable
private fun MainScreen () {
val context = LocalContext.current
val cameraController = remember { CameraController (context) }
val isRecording = cameraController.isRecording. collectAsState ()
DisposableEffect (key1 = Unit) {
// プレビュー開始
cameraController. prepare ()
// 使わなくなったら破棄
onDispose { cameraController. destroy () }
}
// TODO 横画面しか対応できていない
Box (modifier = Modifier. fillMaxSize ()) {
AndroidView (
modifier = Modifier
. align (Alignment.Center)
. aspectRatio (CameraController.VIDEO_WIDTH / CameraController.VIDEO_HEIGHT. toFloat ()),
factory = {
SurfaceView (context). apply {
holder. addCallback ( object : SurfaceHolder . Callback {
override fun surfaceCreated (holder: SurfaceHolder ) {
// 解像度を合わせておく
holder. setFixedSize (CameraController.VIDEO_WIDTH, CameraController.VIDEO_HEIGHT)
cameraController. createSurface (holder.surface)
}
override fun surfaceChanged (holder: SurfaceHolder , format: Int , width: Int , height: Int ) {
// do nothing
}
override fun surfaceDestroyed (holder: SurfaceHolder ) {
cameraController. destroySurface ()
}
})
}
}
)
Box (
modifier = Modifier
. padding (bottom = 30 .dp)
. align (Alignment.BottomCenter)
. clip (CircleShape)
. size ( 80 .dp)
// 録画中は色を変える
. background ( if (isRecording. value ) Color.Red else Color.Gray)
. clickable { if (isRecording. value ) cameraController. stopRecord () else cameraController. startRecord () }
)
}
}
これで動くんじゃないでしょうか?
Camera2 API 製の HDR 動画
東京駅です、
VIDEO
撮影風景です。スマホでスマホ取ってる意味不明な人
ちゃんとCamera2 API
で10 ビット HDR
の撮影ができてそうです。HDR
表示です。
Camera2 API HDR のソースコード
はい。
camera2_hdr
ブランチがそうです、
https://github.com/takusan23/AndroidCamera2ApiHdrVideo
質問 プレビューが HDR じゃない
作って気付きました。撮影中は眩しくないのですが、実際に再生してみると眩しく表示されます。
なぜかプレビューのSurfaceView
にはHDR
が適用されてない?
camera2
のサンプルアプリを動かしてみると眩しいのですが 、私もこれやりたいなー思って読み進めていくと、
OpenGL ES
のフラグメントシェーダーとか書いてて あ~ って感じに。
https://github.com/android/camera-samples/blob/a07d5f1667b1c022dac2538d1f553df20016d89c/Camera2Video/app/src/main/java/com/example/android/camera2/video/HardwarePipeline.kt#L155
CameraX
がHDR
動画撮影に対応して随分たった後に、プレビューがHDR
に対応したのも、もしかしれこれ???
このあとすぐ!
質問 カメラ映像に文字を入れたりしたい
どうやら静止画撮影にはウォーターマークを付ける機能があるそうです、これ動画にも欲しくないですか?
ちなみにこれもOpenGL ES
で上からCanvas
で書いたBitmap
を描画するなどの方法が必要です。
このあとすぐ!
HDR と OpenGL ES
さて、おそらくプレビューもHDR
にしたい場合や(なんでプレビュー映し出すのにOpenGL ES
を経由する必要があるのかはマジで不明)、
そもそもカメラ映像を加工したいという場合は、OpenGL ES
を使う必要があります。
OpenGL ES で HDR(HLG) 表示をするためには
OpenGL ES
のセットアップ(EGL
とかいう)で、以下の点を直します(?)
GLES 3
を使うようにする
HDR(HLG)
の表示にはOpenGL ES 3
系を使う必要があるそうです。
GLES 2
から移行の場合は、シェーダーの書き直しが必要かもしれないし、バージョンディレクティブを書くという方法もあるはず。
今なら生成 AI
に書き直してもらえそう、WebGL
あたりで知見あるでしょAI
も
RGBA8888
をRGBA1010102
にする
EGL_RED_SIZE
、EGL_GREEN_SIZE
、EGL_BLUE_SIZE
を10
EGL_ALPHA_SIZE
を2
eglCreateWindowSurface
時のint 配列
の中に[EGL_GL_COLORSPACE_KHR, EGL_GL_COLORSPACE_BT2020_HLG_EXT]
を足す
が、そもそもBT2020_HLG_EXT
が使えるかを判定する必要がありそうです
なお上記の定数は自分で定義しないといけません、、
フラグメントシェーダーをちょっと直す(実は直さなくても動いてそう )
これでSurfaceView / MediaRecorder / MediaCodec
で10 ビット HDR
映像が扱えるようになります。うわ眩しい!
Camera2 API + OpenGL でカメラ映像のプレビューと動画撮影
OpenGL ES
にすれば多分プレビューもHDR
で表示されます、ついでにOpenGL ES
で描画するならってことで上に文字を重ねます。。
OpenGL ES 周りを揃えていく
いつものAOSP
から借りてきているInputSurface.java
を10 ビット HDR
に対応させます。
ありざいす
https://cs.android.com/android/platform/superproject/main/+/main:cts/tests/tests/media/common/src/android/media/cts/InputSurface.java
10 bit HDR
対応版です。
引数で10 ビット HDR
有効時は、GLES 3
にしてRGBA1010102
にして[EGL_GL_COLORSPACE_KHR, EGL_GL_COLORSPACE_BT2020_HLG_EXT]
します。
isAvailableExtension()
を使うことでOpenGL
側がHLG
に対応しているか見れます。camera2
のサンプルアプリがそうやってた。
https://github.com/android/camera-samples/blob/a07d5f1667b1c022dac2538d1f553df20016d89c/Camera2Video/app/src/main/java/com/example/android/camera2/video/HardwarePipeline.kt#L561-L568
OpenGlRenderer
は後で作ります。
その他はいつも通りです
/**
* [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 val isEnableTenBitHdr: Boolean
) {
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 に設定する必要がある。
if (isEnableTenBitHdr) {
eglSetupForTenBitHdr ()
} else {
eglSetupForSdr ()
}
}
/** 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
}
}
TextureRendererSurfaceTexture.kt
名前が終わってる。
SurfaceTexture
ってのがOpenGL ES
のテクスチャにカメラ映像とか動画のデコード結果を使えるようにするやつ。テクスチャの名の通り。
が、ちょっとそのままだと使いにくいので、ラップしています。Surface()
にSurfaceTexture
いれるのずっと不思議なAPI
設計したよなあって思ってる。
10-bit HDR
の対応としては特にはないです。SurfaceTexture
はSDR
でもHDR
でも特に設定することなく使えるそうです。
あと、他のプロジェクトから持ってきた都合上、なんか余計なもの(attachGl
、detachGl
)がありますが、このアプリではいらないですね、、、
コピーしてきた元がこうなってた都合上です。詳しくはTextureRenderer
で。
/**
* [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]を呼び出す */
fun checkAndUpdateTexImage () {
val isAvailable = _isAvailableFrameFlow. value
if (isAvailable) {
_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 ()
}
}
TextureRenderer.kt
これが実際にOpenGL ES
でフラグメントシェーダー
を書いてUniform 変数
を探して描画している部分になります。
SurfaceTexture
の映像を描画するのと、Canvas
で書いたものを描画する機能があります。
Canvas
でも描画できるようにしたので、これで上に文字を重ねることが出来ます!やった!
10-bit HDR
の対応箇所としては、フラグメントシェーダーかな。それ以外はSDR
のときと同じでいいはずです。
有効時はFRAGMENT_SHADER_10BIT_HDR
の方を使うようにしています。
が、これまで通りFRAGMENT_SHADER
の方を使ってもHDR
表示されていそうでよくわかりません 。今回はサンプル通りやろうと思います。
詳しい説明はコードの後に、それ以外はコード内のコメント見て頑張って・・・
/**
* [OpenGlRenderer]から実際の描画処理を持ってきたもの。OpenGL ES のセットアップは[InputSurface]でやる。
* カメラ映像や動画デコーダーの映像を描画したり、Canvas で書いたものを OpenGL ES へ転写したりする。
*
* @param width 映像の幅
* @param height 映像の高さ
* @param isEnableTenBitHdr 10-bit HDR を利用する場合は true
*/
class TextureRenderer (
private val width: Int ,
private val height: Int ,
private val isEnableTenBitHdr: Boolean
) {
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
private var sCanvasTextureHandle = 0
private var iDrawModeHandle = 0
// テクスチャ ID
private var surfaceTextureTextureId = 0
private var canvasTextureTextureId = 0
// Canvas 描画のため Bitmap
private val canvasBitmap = Bitmap. createBitmap (width, height, Bitmap.Config.ARGB_8888)
private val canvas = Canvas (canvasBitmap)
init {
mTriangleVertices. put (mTriangleVerticesData). position ( 0 )
}
/**
* Canvas に書く。
* GL スレッドから呼び出すこと。
*/
fun drawCanvas (draw: Canvas .() -> Unit) {
// 前回のを消す
canvas. drawColor ( 0 , PorterDuff.Mode.CLEAR)
// 書く
draw (canvas)
// 多分いる
GLES20. glActiveTexture (GLES20.GL_TEXTURE1)
GLES20. glBindTexture (GLES20.GL_TEXTURE_2D, canvasTextureTextureId)
// テクスチャを転送
// texImage2D、引数違いがいるので注意
GLUtils. texImage2D (GLES20.GL_TEXTURE_2D, 0 , canvasBitmap, 0 )
checkGlError ( "GLUtils.texImage2D" )
// glError 1282 の原因とかになる
GLES20. glUseProgram (mProgram)
checkGlError ( "glUseProgram" )
// テクスチャの ID をわたす
GLES20. glUniform1i (sSurfaceTextureHandle, 0 ) // GLES20.GL_TEXTURE0
GLES20. glUniform1i (sCanvasTextureHandle, 1 ) // GLES20.GL_TEXTURE1
// モード切替
GLES20. glUniform1i (iDrawModeHandle, FRAGMENT_SHADER_DRAW_MODE_CANVAS_BITMAP)
checkGlError ( "glUniform1i sSurfaceTextureHandle sCanvasTextureHandle iDrawModeHandle" )
// そのほかの値を渡す
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 (mSTMatrix, 0 )
Matrix. setIdentityM (mMVPMatrix, 0 )
// 描画する
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 ()
}
/**
* SurfaceTexture を描画する。
* GL スレッドから呼び出すこと。
*
* @param surfaceTexture 描画する[SurfaceTexture]
* @param onTransform 位置や回転を適用するための行列を作るための関数
*/
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)
// 映像が来ていればテクスチャ更新
surfaceTexture. checkAndUpdateTexImage ()
surfaceTexture. getTransformMatrix (mSTMatrix)
// glError 1282 の原因とかになる
GLES20. glUseProgram (mProgram)
checkGlError ( "glUseProgram" )
// テクスチャの ID をわたす
GLES20. glUniform1i (sSurfaceTextureHandle, 0 ) // GLES20.GL_TEXTURE0
GLES20. glUniform1i (sCanvasTextureHandle, 1 ) // GLES20.GL_TEXTURE1
// モード切替
GLES20. glUniform1i (iDrawModeHandle, FRAGMENT_SHADER_DRAW_MODE_SURFACE_TEXTURE)
checkGlError ( "glUniform1i sSurfaceTextureHandle sCanvasTextureHandle iDrawModeHandle" )
// そのほかの値を渡す
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 = if (isEnableTenBitHdr) FRAGMENT_SHADER_10BIT_HDR else FRAGMENT_SHADER
)
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" )
}
sCanvasTextureHandle = GLES20. glGetUniformLocation (mProgram, "sCanvasTexture" )
checkGlError ( "glGetUniformLocation sCanvasTexture" )
if (sCanvasTextureHandle == - 1 ) {
throw RuntimeException ( "Could not get attrib location for sCanvasTexture" )
}
iDrawModeHandle = GLES20. glGetUniformLocation (mProgram, "iDrawMode" )
checkGlError ( "glGetUniformLocation iDrawMode" )
if (iDrawModeHandle == - 1 ) {
throw RuntimeException ( "Could not get attrib location for iDrawMode" )
}
// テクスチャ ID を払い出してもらう
// SurfaceTexture / Canvas Bitmap 用
val textures = IntArray ( 2 )
GLES20. glGenTextures ( 2 , 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" )
canvasTextureTextureId = textures[ 1 ]
GLES20. glActiveTexture (GLES20.GL_TEXTURE1)
GLES20. glBindTexture (GLES20.GL_TEXTURE_2D, canvasTextureTextureId)
GLES20. glTexParameterf (GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST. toFloat ())
GLES20. glTexParameterf (GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR. toFloat ())
GLES20. glTexParameteri (GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
GLES20. glTexParameteri (GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
checkGlError ( "glTexParameter" )
// アルファブレンディング
// Canvas で書いた際に、透明な部分は透明になるように
GLES20. glEnable (GLES20.GL_BLEND)
GLES20. glBlendFunc (GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA)
checkGlError ( "glEnable GLES20.GL_BLEND" )
}
/** テクスチャ 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)
// drawCanvas / drawSurfaceTexture どっちも呼び出さない場合 glUseProgram 誰もしないので
GLES20. glUseProgram (mProgram)
checkGlError ( "glUseProgram" )
}
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;
}
"""
// iDrawMode に渡す定数
private const val FRAGMENT_SHADER_DRAW_MODE_SURFACE_TEXTURE = 1
private const val FRAGMENT_SHADER_DRAW_MODE_CANVAS_BITMAP = 2
/** 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 sampler2D sCanvasTexture;
uniform __samplerExternal2DY2YEXT sSurfaceTexture;
// 何を描画するか
// 1 SurfaceTexture(カメラや動画のデコード映像)
// 2 Bitmap(テキストや画像を描画した Canvas)
uniform int iDrawMode;
// 出力色
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);
if (iDrawMode == 1) {
outColor.rgb = yuvToRgb(texture(sSurfaceTexture, vTextureCoord).rgb);
} else if (iDrawMode == 2) {
// テクスチャ座標なので Y を反転
outColor = texture(sCanvasTexture, vec2(vTextureCoord.x, 1.0 - vTextureCoord.y));
}
FragColor = outColor;
}
"""
/** SDR のときに使うフラグメントシェーダー */
private const val FRAGMENT_SHADER = """#version 300 es
#extension GL_OES_EGL_image_external_essl3 : require
precision mediump float;
in vec2 vTextureCoord;
uniform sampler2D sCanvasTexture;
uniform samplerExternalOES sSurfaceTexture;
// 何を描画するか
// 1 SurfaceTexture(カメラや動画のデコード映像)
// 2 Bitmap(テキストや画像を描画した Canvas)
uniform int iDrawMode;
// 出力色
out vec4 FragColor;
void main() {
vec4 outColor = vec4(0.0, 0.0, 0.0, 1.0);
if (iDrawMode == 1) {
outColor = texture(sSurfaceTexture, vTextureCoord);
} else if (iDrawMode == 2) {
// テクスチャ座標なので Y を反転
outColor = texture(sCanvasTexture, vec2(vTextureCoord.x, 1.0 - vTextureCoord.y));
}
FragColor = outColor;
}
"""
}
}
10-bit HDR のフラグメントシェーダー
フラグメントシェーダーだけ抜粋しました。
GLSL 3.0
を使うぞというバージョンディレクティブと、GL_EXT_YUV_target
を使うそうです。
SurfaceTexture
のテクスチャの部分、SDR
の時はsamplerExternalOES
を使っていましたが、HDR
だと__samplerExternal2DY2YEXT
とかいうやつを使うようです。よくわからない。
texture()
で読み出したあと、yuvToRgb()
に入れていますが、これもよく分からず、Camera2 API
のサンプルコードからまんま借りてきたものになります。何この定数。。。
iDrawMode
のUniform 変数
にいれる数字によってSurfaceTexture / Canvas(Bitmap)
どっちを描画するかが決められるようにしました。
映像の上に文字を重ねる時は、SurfaceTexture
を描画した後にCanvas
で描画したBitmap
を描画させます。クリアされるまでは残るのでこれでいいはず。
あと、これも後でまた言及しますがそのままSDR
の画像を重ねると普通に眩しいです。
#version 300 es
#extension GL_EXT_YUV_target : require
precision mediump float ;
in vec2 vTextureCoord;
uniform sampler2D sCanvasTexture ;
uniform __samplerExternal2DY2YEXT sSurfaceTexture ;
// 何を描画するか
// 1 SurfaceTexture(カメラや動画のデコード映像)
// 2 Bitmap(テキストや画像を描画した Canvas)
uniform int iDrawMode;
// 出力色
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 );
if (iDrawMode == 1 ) {
outColor . rgb = yuvToRgb ( texture ( sSurfaceTexture , vTextureCoord). rgb );
} else if (iDrawMode == 2 ) {
// テクスチャ座標なので Y を反転
outColor = texture ( sCanvasTexture , vec2 ( vTextureCoord . x , 1.0 - vTextureCoord . y ));
}
FragColor = outColor;
}
SurfaceTexture のテクスチャ ID の話
そういえば、SurfaceTexture
の作成にはテクスチャ ID
が必要なのですが、private var surfaceTextureTextureId
といった感じで隠されちゃっています。
SurfaceTexture()
の作成のために公開するべきなのではという話ですが、これは描画時にSurfaceTexture()
を切り替えできるようにするためです。
が、これから作るアプリでは SurfaceTexture は一つで事足りるため(外カメラの映像一つだけなので)明らかにオーバースペックです。
コピー元のコードではSurfaceTexture
を複数持って、描画のたびにテクスチャID
だけ切り替えることで複数の映像を扱うアプリを作ってましたが、今回は使わないのでどうでもいいです。
https://github.com/takusan23/KomaDroid/blob/0bc67b6ad94774d696e5e472e0e5b9001255478a/app/src/main/java/io/github/takusan23/komadroid/KomaDroidCameraManager.kt#L723-L747
OpenGlRenderer.kt
さて、今まで作ったInputSurface
、TextureRendererSurfaceTexture
、TextureRenderer
をつなぎ合わせて使えるようにするクラスを作ります。
ばらばらで使いにくいってのもそうなんですが、OpenGL ES
はスレッドを意識する必要があって、
makeCurrent()
を呼び出したスレッドからしか描画(というかOpenGL ES
の関数呼び出し)が出来ないのでスレッドを気にせず使えるようにしたいですよね。
というわけでスレッドといえばKotlin Coroutines
ですね、新しく作ったスレッドでコルーチンが処理できるようにnewSingleThreadContext()
を使います。
これとwithContext()
すれば、このクラス利用側はスレッドの意識をしなくてもすみます。
それ以外はTextureRenderer
、InputSurface
で作った処理を呼び出している感じで特にはないです。。。
/**
* OpenGL ES を利用して[TextureRendererSurfaceTexture]や Canvas の中身を描画するクラス。
* カメラ映像に Canvas で書いた文字を重ねたり出来ます。
*
* @param outputSurface 描画した内容の出力先 Surface。SurfaceView、MediaRecorder、MediaCodec など
* @param width 幅
* @param height 高さ
* @param isEnableTenBitHdr 10-bit HDR を有効にする場合は true
*/
class OpenGlRenderer (
outputSurface: Surface ,
width: Int ,
height: Int ,
isEnableTenBitHdr: Boolean
) {
/** OpenGL 描画用スレッドの Kotlin Coroutine Dispatcher */
@OptIn (ExperimentalCoroutinesApi:: class , DelicateCoroutinesApi:: class )
private val openGlRelatedThreadDispatcher = newSingleThreadContext ( "openGlRelatedThreadDispatcher" )
private val inputSurface = InputSurface (outputSurface, isEnableTenBitHdr)
private val textureRenderer = TextureRenderer (width, height, isEnableTenBitHdr)
/** OpenGL ES の用意をし、フラグメントシェーダー等をコンパイルする */
suspend fun prepare () {
withContext (openGlRelatedThreadDispatcher) {
inputSurface. makeCurrent ()
textureRenderer. prepareShader ()
}
}
/** テクスチャ ID を払い出す。SurfaceTexture 作成に使うので */
suspend fun generateTextureId (): Int {
return withContext (openGlRelatedThreadDispatcher) {
textureRenderer. generateTextureId ()
}
}
/** 描画する */
suspend fun drawLoop (drawTexture: suspend TextureRenderer .() -> DrawContinuesData) {
withContext (openGlRelatedThreadDispatcher) {
while ( true ) {
yield ()
// 描画する
textureRenderer. prepareDraw ()
val continuesData = drawTexture (textureRenderer)
// presentationTime が多分必要。swapBuffers して Surface に流す
inputSurface. setPresentationTime (continuesData.currentTimeNanoSeconds)
inputSurface. swapBuffers ()
// 続行するか
if (!continuesData.isAvailableNext) break
}
}
}
/** 破棄する */
suspend fun destroy () {
// try-finally で呼び出されるため NonCancellable 必須
withContext (openGlRelatedThreadDispatcher + NonCancellable) {
inputSurface. destroy ()
}
openGlRelatedThreadDispatcher. close ()
}
/**
* 描画を続行するかのデータ
*
* @param isAvailableNext 次も描画できる場合は true
* @param currentTimeNanoSeconds プレビューのときは使われてない(?)、MediaRecorder の場合は[System.nanoTime]、MediaCodec の場合は今のフレームの時間を入れてください。
*/
data class DrawContinuesData (
var isAvailableNext: Boolean ,
var currentTimeNanoSeconds: Long = System. nanoTime ()
)
}
HDR の映像を OpenGL で見る
先程のカメラアプリに組み込んでみようと思います。
といっても、CameraController
のprepare()
を少し変えるだけなんですが。
(OpenGL
のセットアップ、映像出力先をSurfaceTexture
、描画のメインループを追加)
prepare()
関数の書き換えたあと全てと、追加したdrawFrame()
拡張関数がこちらです。
せっかくなのでCanvas
で文字を書いてカメラ映像に重ねてみます。
/** 初回時、録画終了後に呼び出す */
fun prepare () {
scope. launch {
currentJob?. cancelAndJoin ()
// MediaRecorder を作る
initMediaRecorder ()
// MediaRecorder 用 OpenGlRenderer と SurfaceTexture を作る
val (recordOpenGlRenderer, recordSurfaceTexture) = createOpenGlRendererAndSurfaceTexture (mediaRecorder!!.surface)
currentJob = launch {
// SurfaceView の生存に合わせる
// アプリ切り替えで SurfaceView は再生成されるので、常に監視する必要がある
_surfaceFlow. collectLatest { previewSurface ->
previewSurface ?: return @collectLatest
// プレビュー 用 OpenGlRenderer と SurfaceTexture を作る
val (previewOpenGlRenderer, previewSurfaceTexture) = createOpenGlRendererAndSurfaceTexture (previewSurface)
// 外カメラが開かれるのを待つ
val cameraDevice = backCameraDeviceFlow. filterNotNull (). first ()
// カメラ出力先。それぞれの SurfaceTexture
val outputSurfaceList = listOfNotNull (previewSurfaceTexture.surface, recordSurfaceTexture.surface)
// CaptureRequest をつくる
val captureRequest = cameraDevice. createCaptureRequest (CameraDevice.TEMPLATE_RECORD). apply {
// HDR はここじゃない
outputSurfaceList. forEach { surface -> addTarget (surface) }
// FPS とかズームとか設定するなら
set (CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range (VIDEO_FPS, VIDEO_FPS))
}
// CaptureSession をつくる
val captureSession = cameraDevice. awaitCameraSessionConfiguration (outputSurfaceList = outputSurfaceList) ?: return @collectLatest
// カメラ映像を流し始める
captureSession. setRepeatingRequest (captureRequest. build (), null , null )
coroutineScope {
// プレビューを描画する
launch {
try {
// メインループ
val drawContinuesData = OpenGlRenderer. DrawContinuesData ( true , 0 )
previewOpenGlRenderer. drawLoop {
drawFrame (previewSurfaceTexture)
drawContinuesData
}
} finally {
// コルーチンキャンセル時
previewSurfaceTexture. destroy ()
previewOpenGlRenderer. destroy ()
}
}
// MediaRecorder のを描画する
launch {
try {
// メインループ
val drawContinuesData = OpenGlRenderer. DrawContinuesData ( true , 0 )
recordOpenGlRenderer. drawLoop {
drawFrame (recordSurfaceTexture)
// MediaRecorder は setPresentationTime の指定が必要(そう)
drawContinuesData.currentTimeNanoSeconds = System. nanoTime ()
drawContinuesData
}
} finally {
// コルーチンキャンセル時
recordSurfaceTexture. destroy ()
recordOpenGlRenderer. destroy ()
}
}
}
}
}
}
}
private val textPaint = Paint (). apply {
color = Color.WHITE
textSize = 50f
isAntiAlias = true
}
/** プレビューと録画の描画を共通化するための関数 */
private fun TextureRenderer . drawFrame (surfaceTexture: TextureRendererSurfaceTexture ) {
drawSurfaceTexture (surfaceTexture) { mvpMatrix ->
// 回転する
// TODO 常に横画面で使う想定のため、条件分岐がありません。縦持ちでも使いたい場合は if (isLandscape) { } をやってください
Matrix. rotateM (mvpMatrix, 0 , 90f , 0f , 0f , 1f )
}
drawCanvas {
drawText ( "撮影: ${ Build.MODEL } コーデック:HEVC ダイナミックレンジ:HDR(BT.2020, HLG)" , 100f , 1000f , textPaint)
}
}
/** [OpenGlRenderer]と[TextureRendererSurfaceTexture]を作る */
@SuppressLint ( "Recycle" )
private suspend fun createOpenGlRendererAndSurfaceTexture (surface: Surface ): Pair < OpenGlRenderer , TextureRendererSurfaceTexture > {
// OpenGL ES で描画する OpenGlRenderer を作る
val openGlRenderer = OpenGlRenderer (surface, VIDEO_WIDTH, VIDEO_HEIGHT, isSupportedTenBitHdr ())
openGlRenderer. prepare ()
// カメラ映像を OpenGL ES へ渡す SurfaceTexture を作る
val surfaceTexture = TextureRendererSurfaceTexture (openGlRenderer. generateTextureId ())
// カメラ映像の解像度を設定
surfaceTexture. setTextureSize (VIDEO_WIDTH, VIDEO_HEIGHT)
return openGlRenderer to surfaceTexture
}
動かしてみる
どうでしょうか?プレビューもHDR
になりましたか?文字も出ているはず。。
(機種名によっては長くて見切れちゃってるかも。drawText()
の座標を直してみてください)
VIDEO
ドコモの5G SA
見てきました。このアンテナがそうなのかは知らない(えっ)
VIDEO
相変わらずスマホでスマホ取ってる意味不明な人
質問 文字がやけに眩しい
HDR
に無理やりSDR
の画像を入れたからですかね、何かしら明るさを調整してあげたほうがいいかもしれません。てかそうです。
Media3 Transformer
すでに対策済みらしく、若干暗く描画する対応をしているそうです。
https://medium.com/google-exoplayer/media3-1-4-0-whats-new-ba1c9c17ee1a
OpenGL で HDR と文字入れソースコード
ブランチはopengl
です。git checkout
してください。
https://github.com/takusan23/AndroidCamera2ApiHdrVideo
10 ビット HDR 動画編集
標準カメラアプリだと対応してるけど、Camera2 API
経由だとHDR
対応してないよ~~~って人向け。
ちなみに動画編集やるってなった場合でも、さっき作ったOpenGL ES
周りは転用できるので、あとはMediaCodec でデコーダー、エンコーダー を作ればほぼ終わり、、、、
いやそれが一番たいへん(今回はコピペできるよう貼っておきます)。
これもSDR
のときと同じようにMediaCodec + OpenGL
でやれば良いです。
OpenGL
の設定はHDR
カメラのそれと同じものを使えばいいです。動画編集だとカメラ映像ではなく、動画ファイルから動画トラックを取り出しデコーダーに入れて出てきた映像を使えばいいのですが、これも特に変えることなく動くと思います。
カメラの動画撮影と違い、こちらは確実にMediaRecorder
ではなくMediaCodec
の方を使わないといけないので若干面倒ですね(フレームごと取り出してやらないといけないので...)。
(でもコピペで動きます)
付録 エンコーダーが 10 ビット HDR に対応しているか見る
わかりにくいですが、対応しているプロファイルを見ると良さそうです。
HEVC
以外はmedia3
ライブラリを参考にしました、ありざいす
https://github.com/androidx/media/blob/76088cd6af7f263aba238b7a48d64bd4f060cb8b/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderUtil.java#L130
override fun onCreate (savedInstanceState: Bundle ?) {
super . onCreate (savedInstanceState)
enableEdgeToEdge ()
println ( "HEVC = ${ getSupportedHdrEncodingByHevc () } " )
println ( "VP9 = ${ getSupportedHdrEncodingByVp9 () } " )
println ( "AV1 = ${ getSupportedHdrEncodingByAv1 () } " )
}
/** HEVC エンコーダーが対応している HDR の種類を返す */
private fun getSupportedHdrEncodingByHevc (): SupportHdrEncoding {
val hevcEncoderList = MediaCodecList (MediaCodecList.REGULAR_CODECS)
.codecInfos
. filter { !it.isAlias }
. filter { it.isEncoder && MediaFormat.MIMETYPE_VIDEO_HEVC in it.supportedTypes }
// ハードウェアエンコーダーで、ない場合はソフトウェア
val hevcEncoder = hevcEncoderList. firstOrNull { it.isHardwareAccelerated } ?: hevcEncoderList. first ()
// 対応しているか見る
val profileList = hevcEncoder. getCapabilitiesForType (MediaFormat.MIMETYPE_VIDEO_HEVC).profileLevels. map { it.profile }
return SupportHdrEncoding (
hlg = MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10 in profileList,
hdr10 = MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10HDR10 in profileList,
hdr10plus = MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10HDR10Plus in profileList
)
}
/** AV1 エンコーダーが対応している HDR の種類を返す */
private fun getSupportedHdrEncodingByAv1 (): SupportHdrEncoding {
val hevcEncoderList = MediaCodecList (MediaCodecList.REGULAR_CODECS)
.codecInfos
. filter { !it.isAlias }
. filter { it.isEncoder && MediaFormat.MIMETYPE_VIDEO_AV1 in it.supportedTypes }
// ハードウェアエンコーダーで、ない場合はソフトウェア
val hevcEncoder = hevcEncoderList. firstOrNull { it.isHardwareAccelerated } ?: hevcEncoderList. first ()
// 対応しているか見る
val profileList = hevcEncoder. getCapabilitiesForType (MediaFormat.MIMETYPE_VIDEO_AV1).profileLevels. map { it.profile }
return SupportHdrEncoding (
hlg = MediaCodecInfo.CodecProfileLevel.AV1ProfileMain10 in profileList,
hdr10 = MediaCodecInfo.CodecProfileLevel.AV1ProfileMain10HDR10 in profileList,
hdr10plus = MediaCodecInfo.CodecProfileLevel.AV1ProfileMain10HDR10Plus in profileList
)
}
/** VP9 エンコーダーが対応している HDR の種類を返す */
private fun getSupportedHdrEncodingByVp9 (): SupportHdrEncoding {
val hevcEncoderList = MediaCodecList (MediaCodecList.REGULAR_CODECS)
.codecInfos
. filter { !it.isAlias }
. filter { it.isEncoder && MediaFormat.MIMETYPE_VIDEO_VP9 in it.supportedTypes }
// ハードウェアエンコーダーで、ない場合はソフトウェア
val hevcEncoder = hevcEncoderList. firstOrNull { it.isHardwareAccelerated } ?: hevcEncoderList. first ()
// 対応しているか見る
val profileList = hevcEncoder. getCapabilitiesForType (MediaFormat.MIMETYPE_VIDEO_VP9).profileLevels. map { it.profile }
// HLG / PQ 兼用らしい
val isHlgOrPqAvailable = MediaCodecInfo.CodecProfileLevel.VP9Profile2HDR in profileList || MediaCodecInfo.CodecProfileLevel.VP9Profile3HDR in profileList
val isHdr10plus = MediaCodecInfo.CodecProfileLevel.VP9Profile2HDR10Plus in profileList || MediaCodecInfo.CodecProfileLevel.VP9Profile3HDR10Plus in profileList
return SupportHdrEncoding (
hlg = isHlgOrPqAvailable,
hdr10 = isHlgOrPqAvailable,
hdr10plus = isHdr10plus
)
}
data class SupportHdrEncoding (
val hlg: Boolean ,
val hdr10: Boolean ,
val hdr10plus: Boolean
)
HDR エンコードのエンコーダー対応状況を見てみる
HLG
、HDR10
、HDR10+
それぞれで見てみます。
Pixel 8 Pro
みたところHEVC
、VP9
、AV1
すべてが対応している?
HEVC = SupportHdrEncoding(hlg=true, hdr10=true, hdr10plus=true)
VP9 = SupportHdrEncoding(hlg=true, hdr10=true, hdr10plus=true)
AV1 = SupportHdrEncoding(hlg=true, hdr10=true, hdr10plus=true)
Xperia 1 V
HEVC
がすべて行けそう、VP9
は全部だめ、AV1
がHLG
のみ。なおAV1
はソフトウェアエンコーダーなので遅い。
HEVC = SupportHdrEncoding(hlg=true, hdr10=true, hdr10plus=true)
VP9 = SupportHdrEncoding(hlg=false, hdr10=false, hdr10plus=false)
AV1 = SupportHdrEncoding(hlg=true, hdr10=false, hdr10plus=false)
ただ、冒頭の通りで、私がミスっているのか VP9 で HDR の映像をエンコードすることは出来ませんでした 。。。
ここで対応してるって返しているの、何?
HDR 動画撮影で使った OpenGL 周りのコードを持ってくる
この4つです。
動画ファイルから映像トラックを取り出し渡して1枚1枚フレームを取り出して、Surface
に描画するやつを作ります。コピペで動きます。
デコーダーを用意する関数、次の時間のフレームを取り出す関数、リソース開放する関数しか無いです。
次の時間 のフレームを取得する関数しか無いのは、動画編集で一方向(0→動画時間)へしか動画を再生しないためです。
真面目に動画のプレイヤーをMediaCodec
で作る場合は、これでは時間が減る方向にシークできないので、前の時間にシークできる関数も作る必要があります。が、別に動画プレイヤーを作りたいわけじゃないので、増える方向のみでいいわけです。
/** MediaCodec を使って動画をデコードする */
class VideoDecoder {
private var decodeMediaCodec: MediaCodec ? = null
private var mediaExtractor: MediaExtractor ? = null
/**
* デコードの準備をする
*
* @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 ()
}
/** 破棄する */
fun destroy () {
decodeMediaCodec?. stop ()
decodeMediaCodec?. release ()
mediaExtractor?. release ()
}
/**
* 次の時間のフレームを取得する
*
* @param seekToMs 欲しいフレームの時間
* @return 次のフレームがない場合は null。そうじゃない場合は動画フレームの時間
*/
suspend fun seekNextFrame (seekToMs: Long ): Long ? {
val decodeMediaCodec = decodeMediaCodec!!
val mediaExtractor = mediaExtractor!!
// advance() で false を返したことがある場合、もうデータがない。getSampleTime も -1 になる。
if (mediaExtractor.sampleTime == - 1L ) {
return null
}
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 )
decodeMediaCodec. queueInputBuffer (inputBufferIndex, 0 , size, mediaExtractor.sampleTime, 0 )
}
// デコーダーから映像を受け取る部分
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 (doRender) {
// 欲しいフレームの時間に到達した場合、ループを抜ける
val presentationTimeMs = bufferInfo.presentationTimeUs / 1000
if (seekToMs <= presentationTimeMs) {
isRunning = false
isDecoderOutputAvailable = false
returnValue = presentationTimeMs
}
}
}
}
}
// 次に進める。デコーダーにデータを入れた事を確認してから。
// advance() が false の場合はもうデータがないので、break
if ( 0 <= inputBufferIndex) {
val isEndOfFile = !mediaExtractor. advance ()
if (isEndOfFile) {
// return で false(フレームが取得できない旨)を返す
returnValue = null
break
}
}
}
return returnValue
}
companion object {
/** MediaCodec タイムアウト */
private const val TIMEOUT_US = 0L
}
}
デコードの逆。Surface
で入力されてきた映像をエンコーダーに渡してエンコードして動画ファイルに書き込む。
HDR
の情報(色空間、ガンマカーブ、プロファイル)が渡せるようになってます。MediaRecorder
の時はなんか勝手にHLG
が使われたのですが、MediaCodec
の場合は明示的に渡さないとだめだった気がします。多分。
/** MediaCodec を使って動画をエンコードする */
class VideoEncoder {
private var encodeMediaCodec: MediaCodec ? = null
private var mediaMuxer: MediaMuxer ? = null
/**
* MediaCodec エンコーダーの準備をする
*
* @param videoFilePath 保存先
* @param codecName コーデック名
* @param bitRate ビットレート
* @param frameRate フレームレート
* @param keyframeInterval キーフレームの間隔
* @param outputVideoWidth 動画の高さ
* @param outputVideoHeight 動画の幅
* @param tenBitHdrParametersOrNullSdr SDR 動画の場合は null。HDR でエンコードする場合は[TenBitHdrParameters]を埋めてください
*/
fun prepare (
videoFilePath: String ,
outputVideoWidth: Int = 1280 ,
outputVideoHeight: Int = 720 ,
frameRate: Int = 30 ,
bitRate: Int = 1_000_000 ,
keyframeInterval: Int = 1 ,
codecName: String = MediaFormat.MIMETYPE_VIDEO_HEVC,
tenBitHdrParametersOrNullSdr: TenBitHdrParameters ? = null
) {
// エンコーダーにセットするMediaFormat
// コーデックが指定されていればそっちを使う
val videoMediaFormat = MediaFormat. createVideoFormat (codecName, outputVideoWidth, outputVideoHeight). apply {
setInteger (MediaFormat.KEY_BIT_RATE, bitRate)
setInteger (MediaFormat.KEY_FRAME_RATE, frameRate)
setInteger (MediaFormat.KEY_I_FRAME_INTERVAL, keyframeInterval)
setInteger (MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
// 10-bit HDR のパラメーターをセット
if (tenBitHdrParametersOrNullSdr != null ) {
setInteger (MediaFormat.KEY_PROFILE, tenBitHdrParametersOrNullSdr.codecProfile)
setInteger (MediaFormat.KEY_COLOR_STANDARD, tenBitHdrParametersOrNullSdr.colorStandard)
setInteger (MediaFormat.KEY_COLOR_TRANSFER, tenBitHdrParametersOrNullSdr.colorTransfer)
setFeatureEnabled (MediaCodecInfo.CodecCapabilities.FEATURE_HdrEditing, true )
}
}
// マルチプレクサ
mediaMuxer = MediaMuxer (videoFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
encodeMediaCodec = MediaCodec. createEncoderByType (codecName). apply {
configure (videoMediaFormat, null , null , MediaCodec.CONFIGURE_FLAG_ENCODE)
}
}
/** エンコーダーの入力になる[Surface]を取得する */
fun getInputSurface (): Surface = encodeMediaCodec!!. createInputSurface ()
/** エンコーダーを開始する */
suspend fun start () {
val encodeMediaCodec = encodeMediaCodec ?: return
val mediaMuxer = mediaMuxer ?: return
var videoTrackIndex = - 1
val bufferInfo = MediaCodec. BufferInfo ()
encodeMediaCodec. start ()
try {
while ( true ) {
// yield() で 占有しないよう
yield ()
// Surface経由でデータを貰って保存する
val encoderStatus = encodeMediaCodec. dequeueOutputBuffer (bufferInfo, TIMEOUT_US)
if ( 0 <= encoderStatus) {
if (bufferInfo.size > 0 ) {
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG == 0 ) {
val encodedData = encodeMediaCodec. getOutputBuffer (encoderStatus)!!
mediaMuxer. writeSampleData (videoTrackIndex, encodedData, bufferInfo)
}
}
encodeMediaCodec. releaseOutputBuffer (encoderStatus, false )
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// MediaMuxerへ映像トラックを追加するのはこのタイミングで行う
// このタイミングでやると固有のパラメーターがセットされたMediaFormatが手に入る(csd-0 とか)
// 映像がぶっ壊れている場合(緑で塗りつぶされてるとか)は多分このあたりが怪しい
val newFormat = encodeMediaCodec.outputFormat
videoTrackIndex = mediaMuxer. addTrack (newFormat)
mediaMuxer. start ()
}
if (encoderStatus != MediaCodec.INFO_TRY_AGAIN_LATER) {
continue
}
}
} finally {
// エンコーダー終了
encodeMediaCodec. signalEndOfInputStream ()
encodeMediaCodec. stop ()
encodeMediaCodec. release ()
// コンテナフォーマットに書き込む処理も終了
mediaMuxer. stop ()
mediaMuxer. release ()
}
}
/**
* 10-bit HDR の動画を作成するためのパラメーター。
* 色空間とガンマカーブを指定してください。
*
* HLG 形式の HDR の場合は[MediaFormat.COLOR_STANDARD_BT2020]と[MediaFormat.COLOR_TRANSFER_HLG]と[MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10]。
* デフォルト引数は HLG。
*
* 定数自体は Android 7 からありますが、10-bit HDR の動画編集が(MediaCodec が?) 13 以上なので。
*
* @param colorStandard 色空間
* @param colorTransfer ガンマカーブ
* @param codecProfile コーデックのプロファイル
*/
data class TenBitHdrParameters (
val colorStandard: Int = MediaFormat.COLOR_STANDARD_BT2020,
val colorTransfer: Int = MediaFormat.COLOR_TRANSFER_HLG,
val codecProfile: Int = MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10
)
companion object {
/** タイムアウト */
private const val TIMEOUT_US = 0L
}
}
音声トラックを入れ直す処理を作る
動画編集すると、映像トラックと音声トラックは別々になっているので、一つの動画ファイル(コンテナフォーマット)に映像トラックと音声トラックを一つにまとめる処理を書きます。
OpenGL
を使って編集した動画には音声トラックが無いので、これを使い元の動画データから音声データだけ入れ直します。
object MediaMuxerTool {
/** それぞれ音声トラック、映像トラックを取り出して、2つのトラックを一つの mp4 にする */
@SuppressLint ( "WrongConstant" )
suspend fun mixAvTrack (
context: Context ,
audioTrackUri: Uri ,
videoTrackFilePath: String ,
resultFilePath: String
) {
// 出力先
val mediaMuxer = MediaMuxer (resultFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
// 音声は元の動画データから
// 映像は作った仮ファイルから
val (audioExtractor, audioFormat) = createMediaExtractor (context, audioTrackUri, "audio/" )
val (videoExtractor, videoFormat) = createMediaExtractor (videoTrackFilePath, "video/" )
// MediaMuxer に追加して開始
val audioTrackIndex = mediaMuxer. addTrack (audioFormat)
val videoTrackIndex = mediaMuxer. addTrack (videoFormat)
mediaMuxer. start ()
// MediaExtractor からトラックを取り出して、MediaMuxer に入れ直す処理
listOf (
audioExtractor to audioTrackIndex,
videoExtractor to videoTrackIndex
). forEach { (extractor, trackIndex) ->
val byteBuffer = ByteBuffer. allocate ( 1024 * 4096 )
val bufferInfo = MediaCodec. BufferInfo ()
// データが無くなるまで回す
while ( true ) {
yield ()
// データを読み出す
val offset = byteBuffer. arrayOffset ()
bufferInfo.size = extractor. readSampleData (byteBuffer, offset)
// もう無い場合
if (bufferInfo.size < 0 ) break
// 書き込む
bufferInfo.presentationTimeUs = extractor.sampleTime
bufferInfo.flags = extractor.sampleFlags // Lintがキレるけど黙らせる
mediaMuxer. writeSampleData (trackIndex, byteBuffer, bufferInfo)
// 次のデータに進める
extractor. advance ()
}
// あとしまつ
extractor. release ()
}
// 後始末
mediaMuxer. stop ()
mediaMuxer. release ()
}
/** [Uri]から[MediaExtractor]を作る */
private fun createMediaExtractor (context: Context , uri: Uri , mimeType: String ): Pair < MediaExtractor , MediaFormat > {
val mediaExtractor = MediaExtractor (). apply {
context.contentResolver. openFileDescriptor (uri, "r" )?. use {
setDataSource (it.fileDescriptor)
}
}
val (trackIndex, mediaFormat) = ( 0 until mediaExtractor.trackCount)
. map { index -> mediaExtractor. getTrackFormat (index) }
. withIndex ()
. first { it. value . getString (MediaFormat.KEY_MIME)?. startsWith (mimeType) == true }
mediaExtractor. selectTrack (trackIndex)
return mediaExtractor to mediaFormat
}
/** [filePath]から[MediaExtractor]を作る */
private fun createMediaExtractor (filePath: String , mimeType: String ): Pair < MediaExtractor , MediaFormat > {
val mediaExtractor = MediaExtractor (). apply {
setDataSource (filePath)
}
val (trackIndex, mediaFormat) = ( 0 until mediaExtractor.trackCount)
. map { index -> mediaExtractor. getTrackFormat (index) }
. withIndex ()
. first { it. value . getString (MediaFormat.KEY_MIME)?. startsWith (mimeType) == true }
mediaExtractor. selectTrack (trackIndex)
return mediaExtractor to mediaFormat
}
}
適当に UI を作る
動画を選ぶボタンと、開始ボタンを用意しました。
class MainActivity : ComponentActivity () {
override fun onCreate (savedInstanceState: Bundle ?) {
super . onCreate (savedInstanceState)
enableEdgeToEdge ()
setContent {
AndroidMediaCodecHdrVideoCanvasOverlayTheme {
HomeScreen ()
}
}
}
}
@OptIn (ExperimentalMaterial3Api:: class )
@Composable
private fun HomeScreen () {
val context = LocalContext.current
val scope = rememberCoroutineScope ()
val videoUri = remember { mutableStateOf < Uri ?>( null ) }
val overlayText = remember { mutableStateOf ( "10 ビット HDR 動画の編集テスト" ) }
val videoPicker = rememberLauncherForActivityResult (
contract = ActivityResultContracts. PickVisualMedia (),
onResult = { videoUri. value = it }
)
fun start () {
scope. launch {
// このあとすぐ
}
}
Scaffold (
topBar = { TopAppBar (title = { Text (text = stringResource (R.string.app_name)) }) },
modifier = Modifier. fillMaxSize ()
) { innerPadding ->
Column (modifier = Modifier. padding (innerPadding)) {
Button (onClick = { videoPicker. launch ( PickVisualMediaRequest (ActivityResultContracts.PickVisualMedia.VideoOnly)) }) {
Text (text = "動画を選ぶ" )
}
OutlinedTextField (
value = overlayText. value ,
onValueChange = { overlayText. value = it },
label = { Text (text = "映像の上に重ねる文字" ) }
)
if (videoUri. value != null ) {
Button (onClick = { start () }) {
Text (text = "動画編集を始める" )
}
}
}
}
}
組み合わせる
さっきのstart()
関数の中身です。
動画編集で使うOpenGL
のやつや、OpenGL
で描画したものをエンコードして動画ファイルに書き込むエンコーダーの初期設定が続きます。
それらの初期設定には動画の高さ等が必要なので、MediaMetadataRetriever
を使ってます。
のんきにuse { }
を使っていますが、AutoClosable インターフェース
を実装してるのはAndroid 10
以降なので古いバージョンでも使いたい場合はclose()
を呼んでください。
縦動画の場合は動画のwidth / height
が逆になるので、rotation
の値を見るようにしてください。
また、今回は10-bit HDR
動画を扱うので、色空間(colorStandart
)とガンマカーブ(colorTransfer
)を取っています。これらはMediaCodec
で渡せる形(MediaFormat
クラスの定数)で返してくれるので、
取り出したら特に変換とかはせず、そのまま渡せば良いです。
プロファイルですがHDR
の種類が、HLG
ならHEVCProfileMain10
、HDR10
ならHEVCProfileMain10HDR10
、HDR10+
ならHEVCProfileMain10HDR10Plus
を指定すればいいはずです。
OpenGL
で描画するやつを作り、OpenGL
で映像データをテクスチャとして使えるSurfaceTexture
も作ります。
そのSurfaceTexture
に映像を流すためのデコーダーも作り、一通り揃いました。
エンコーダーを起動し、OpenGL
で描画するためのメインループを呼びます。
ここで次の映像フレームがなくなるまで、デコーダーから映像フレームを取り出し、OpenGL
で描画しその上から、Canvas
を使って文字を書いて同じくOpenGL
で描画。
MediaCodec
に今のフレームの時間を伝える必要が多分あるのでsetPresentationTime()
でフレームの時間も渡しています。これを動画が終わるまで続けます。
描画ループが終了するまで(動画フレームを最後まで取り出した)、join()
で一時停止し、戻ってきたらエンコーダー側もcancel()
させ動画編集を終わらせます。
ただ、このままだと映像の上に文字が重なった映像しかなく、音がなくなってしまいました。音を元の動画データから入れ直す作業が最後に必要です。
元の動画データから音声トラックを入れ直してきたらMediaStore
に保存して完了です。
ちなみに必要な時間は動画時間と同じくらいになるんじゃないかなと思います。そんなに大きく時間がかかるわけじゃ無さそう。
fun start () {
scope. launch (Dispatchers.Default) {
val inputUri = videoUri. value ?: return @launch
val tempVideoTrackFile = context. getExternalFilesDir ( null )?. resolve ( "temp_video.mp4" ) ?: return @launch
// 動画のメタデータを取得
val videoWidth: Int
val videoHeight: Int
val fps: Int
val colorStandard: Int
val colorTransfer: Int
MediaMetadataRetriever (). use { mediaMetadataRetriever ->
context.contentResolver. openFileDescriptor (inputUri, "r" )?. use {
mediaMetadataRetriever. setDataSource (it.fileDescriptor)
}
// FPS / 色空間 / ガンマカーブ
fps = mediaMetadataRetriever. extractMetadata (MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE)?. toIntOrNull () ?: 30
colorStandard = mediaMetadataRetriever. extractMetadata (MediaMetadataRetriever.METADATA_KEY_COLOR_STANDARD)?. toIntOrNull () ?: MediaFormat.COLOR_STANDARD_BT2020
colorTransfer = mediaMetadataRetriever. extractMetadata (MediaMetadataRetriever.METADATA_KEY_COLOR_TRANSFER)?. toIntOrNull () ?: MediaFormat.COLOR_TRANSFER_HLG
// 動画の縦横サイズは ROTATION を見る必要あり
val __videoWidth = mediaMetadataRetriever. extractMetadata (MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?. toIntOrNull () ?: 1280
val __videoHeight = mediaMetadataRetriever. extractMetadata (MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?. toIntOrNull () ?: 720
val rotation = mediaMetadataRetriever. extractMetadata (MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?. toIntOrNull () ?: 0
// 縦動画の場合、縦横が入れ替わるので
val (_videoWidth, _videoHeight) = when (rotation) {
90 , 270 -> __videoHeight to __videoWidth
else -> __videoWidth to __videoHeight
}
videoWidth = _videoWidth
videoHeight = _videoHeight
}
// 動画のエンコーダー
val videoEncoder = VideoEncoder (). apply {
prepare (
videoFilePath = tempVideoTrackFile.path,
outputVideoWidth = videoWidth,
outputVideoHeight = videoHeight,
frameRate = fps,
bitRate = 20_000_000 ,
keyframeInterval = 1 ,
codecName = MediaFormat.MIMETYPE_VIDEO_HEVC,
tenBitHdrParametersOrNullSdr = VideoEncoder. TenBitHdrParameters (
colorStandard = colorStandard,
colorTransfer = colorTransfer,
codecProfile = when (colorTransfer) {
MediaFormat.COLOR_TRANSFER_HLG -> MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10
MediaFormat.COLOR_TRANSFER_ST2084 -> MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10HDR10
else -> - 1 // ココには来ないはず
}
)
)
}
// OpenGL で描画するクラス
val openGlRenderer = OpenGlRenderer (
outputSurface = videoEncoder. getInputSurface (),
width = videoWidth,
height = videoHeight,
// BT.2020 でかつ、HLG か ST2084
isEnableTenBitHdr = (colorStandard == MediaFormat.COLOR_STANDARD_BT2020 && (colorTransfer == MediaFormat.COLOR_TRANSFER_HLG || colorTransfer == MediaFormat.COLOR_TRANSFER_ST2084))
). apply { prepare () }
// 映像を流す SurfaceTexture を作る
val surfaceTexture = TextureRendererSurfaceTexture (openGlRenderer. generateTextureId ())
// 動画のデコーダーを作る
val videoDecoder = VideoDecoder (). apply {
prepare (
context = context,
uri = inputUri,
outputSurface = surfaceTexture.surface
)
}
try {
coroutineScope {
// エンコーダー
val encoderJob = launch { videoEncoder. start () }
// OpenGL のメインループ
val openGlJob = launch {
val continuesData = OpenGlRenderer. DrawContinuesData ( true , 0 )
val frameMs = 1_000 / fps
val textPaint = Paint (). apply {
color = Color.WHITE
textSize = 100f
isAntiAlias = true
}
var currentPositionMs = 0L
// isAvailableNext が false までループする
openGlRenderer. drawLoop {
// フレームを取得。次のフレームがない場合は null
val hasNextFrame = videoDecoder. seekNextFrame (currentPositionMs) != null
currentPositionMs += frameMs
// 描画する
drawSurfaceTexture (surfaceTexture)
drawCanvas { drawText (overlayText. value , 100f , 200f , textPaint) }
// ループ続行情報
continuesData.isAvailableNext = hasNextFrame
continuesData.currentTimeNanoSeconds = currentPositionMs * 1_000_000L // nanoseconds に変換する
continuesData
}
}
// 終わったらエンコーダーも終わり
openGlJob. join ()
encoderJob. cancelAndJoin ()
}
} finally {
surfaceTexture. destroy ()
openGlRenderer. destroy ()
videoDecoder. destroy ()
}
// 音声トラックがないので(音がないので)、元の動画データから入れ直す
val resultFile = context. getExternalFilesDir ( null )?. resolve ( "result_ ${ System. currentTimeMillis () } .mp4" )!!
MediaMuxerTool. mixAvTrack (
context = context,
audioTrackUri = inputUri,
videoTrackFilePath = tempVideoTrackFile.path,
resultFilePath = resultFile.path
)
// 端末の動画フォルダに入れる
val contentResolver = context.contentResolver
val contentValues = contentValuesOf (
MediaStore.Images.Media.DISPLAY_NAME to resultFile.name,
MediaStore.Images.Media.RELATIVE_PATH to " ${ Environment.DIRECTORY_MOVIES } /AndroidMediaCodecHdrVideoCanvasOverlay"
)
val uri = contentResolver. insert (MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues)!!
resultFile. inputStream (). use { inputStream ->
contentResolver. openOutputStream (uri)?. use { outputStream ->
inputStream. copyTo (outputStream)
}
}
// コピーしたので消す
tempVideoTrackFile. delete ()
resultFile. delete ()
// おわり
withContext (Dispatchers.Main) {
Toast. makeText (context, "おわり" , Toast.LENGTH_SHORT). show ()
}
}
}
10 ビット動画の編集
動かしてみます。
できた動画はこちらです
ちゃんと10 ビット HDR
の動画の上に文字が出ています。しかもHDR
を保持したまま動画編集したので眩しさや色の鮮やかさは残っています。
VIDEO
HDR 動画編集 ソースコード
どうぞ
https://github.com/takusan23/AndroidMediaCodecHdrVideoCanvasOverlay
10 ビット HDR の動画をできる限り SDR にしたい
https://developer.android.com/media/media3/transformer/tone-mapping
HDR
からSDR
の変換がしたい場合、多分自分で作るよりもMedia3 Transformer
ライブラリでやったほうがいいと思います・・・
このためだけにOpenGL
だったりMediaCodec
やりたくないでしょ。
このHDR
からSDR
にするやつ、トーンマッピング とかいう名前がついているそうで、Android 13
以降であれば、
MediaCodec
のデコーダー設定時のMediaFormat
にsetInteger(MediaFormat.KEY_COLOR_TRANSFER_REQUEST, MediaFormat.COLOR_TRANSFER_SDR_VIDEO)
するだけ。
あとは出力先Surface
からSDR
に変換された映像が流れてきます。
ちなみに上記を使わず何もしない状態でSDR
にするとかなり白っぽくなります。
それと、この標準トーンマッピング
はすべての端末で利用できるわけではない らしく、
手元のXperia 1 V
では対応してなかった、、、
Android 組み込みのトーンマッピングを使ってみる
これはさっき作った動画編集のプログラムを少し変えるだけで出来ます。
デコーダーのMediaFormat
に一つ追加する
setInteger(MediaFormat.KEY_COLOR_TRANSFER_REQUEST, MediaFormat.COLOR_TRANSFER_SDR_VIDEO)
エンコーダーにはcolorSpace
、colorTransform
の値をSDR(BT.709)
にする(か、そもそも指定しない )
OpenGL ES
の設定でHLG
を利用しない(SDR
のときと同じInputSurface
の処理をする)
まずはデコーダーのprepare 関数
に引数を一つ追加します。
/**
* デコードの準備をする
*
* @param context [Context]
* @param uri PhotoPicker 等で選んだやつ
* @param outputSurface デコードした映像の出力先
* @param toToneMapSdr SDR にトーンマッピングする場合は true
*/
fun prepare (
context: Context ,
uri: Uri ,
outputSurface: Surface ,
toToneMapSdr: Boolean = false
) {
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)
// SDR に変換する場合は、MediaFormat にオプション指定
if (toToneMapSdr) {
mediaFormat. setInteger (MediaFormat.KEY_COLOR_TRANSFER_REQUEST, MediaFormat.COLOR_TRANSFER_SDR_VIDEO)
}
// MediaCodec を作る
val codecName = mediaFormat. getString (MediaFormat.KEY_MIME)!!
decodeMediaCodec = MediaCodec. createDecoderByType (codecName). apply {
configure (mediaFormat, outputSurface, null , 0 )
}
decodeMediaCodec?. start ()
}
次にエンコーダーにはSDR
を、OpenGL
にはHLG
を使わないようにし、デコーダーにはtoToneMapSdr
をtrue
にしてあげます。
// 省略...
// 動画のエンコーダー
val videoEncoder = VideoEncoder (). apply {
prepare (
videoFilePath = tempVideoTrackFile.path,
outputVideoWidth = videoWidth,
outputVideoHeight = videoHeight,
frameRate = fps,
bitRate = 20_000_000 ,
keyframeInterval = 1 ,
codecName = MediaFormat.MIMETYPE_VIDEO_HEVC,
tenBitHdrParametersOrNullSdr = null // SDR にする
)
}
// OpenGL で描画するクラス
val openGlRenderer = OpenGlRenderer (
outputSurface = videoEncoder. getInputSurface (),
width = videoWidth,
height = videoHeight,
isEnableTenBitHdr = false // SDR にする
). apply { prepare () }
// 映像を流す SurfaceTexture を作る
val surfaceTexture = TextureRendererSurfaceTexture (openGlRenderer. generateTextureId ())
// 動画のデコーダーを作る
val videoDecoder = VideoDecoder (). apply {
prepare (
context = context,
uri = inputUri,
outputSurface = surfaceTexture.surface,
toToneMapSdr = true // SDR にする
)
}
// 省略...
あとは動画編集のプログラムそのままで使えます。
結果
結構近い色になってると思う
VIDEO
ちなみにこれをしないでSDR
にするとまじでかすれた色 になります。。。
VIDEO
HLG 互換のドルビービジョンをドルビービジョン非対応端末でデコードする
そもそもドルビービジョンの会社にライセンス料を払わないといけないのか、HLG
としてデコードする分にはお金がかからないのかとかその辺知らないので、、、
ここでは技術面のみ。
https://professionalsupport.dolby.com/s/article/Transcoding-Dolby-Vision-profile-8-4-to-HLG-on-Android?language=en_US
多分ドルビービジョンはドルビービジョン用のデコーダーが存在する気がして、対応端末であれば、これまでのコードで特段対応することなくデコードできるんじゃないかなと思います。
(すいません、手元にドルビービジョン
がデコードできる端末がなくこれで動くかは分からないです。)
一方、非対応端末でドルビービジョンの動画をいれると、ドルビービジョンのデコーダーなんてねえよ。うるせえよ。的な例外が投げられると思います。
ですが、冒頭の通りiPhone
やXiaomi
等スマホに入ってるドルビービジョン
はHLG
と互換性があるのでHEVC
デコーダーでデコードできるはず。
その修正を入れてみます。
VideoDecoder
クラスのprepare
関数で、ドルビービジョンのコーデックが要求されていて、かつ端末にドルビービジョンのデコーダーがない場合は、
HEVC
のデコーダーを作るようにしました。デコーダーの有無は上記のURL
から持ってきました。。
/**
* デコードの準備をする
*
* @param context [Context]
* @param uri PhotoPicker 等で選んだやつ
* @param outputSurface デコードした映像の出力先
* @param toToneMapSdr SDR にトーンマッピングする場合は true
*/
fun prepare (
context: Context ,
uri: Uri ,
outputSurface: Surface ,
toToneMapSdr: Boolean = false
) {
// ... 以下省略 ...
// MediaCodec を作る
var codecName = mediaFormat. getString (MediaFormat.KEY_MIME)!!
// ドルビービジョンの動画で、ドルビービジョンのデコーダーがない場合は HEVC としてデコードさせる
// ただ、本物のドルビービジョンは HLG と互換性がないので、HLG と互換性があるドルビービジョンのみ対応できる
if (codecName == MediaFormat.MIMETYPE_VIDEO_DOLBY_VISION && ! hasDolbyVisionDecoder ()) {
codecName = MediaFormat.MIMETYPE_VIDEO_HEVC
}
decodeMediaCodec = MediaCodec. createDecoderByType (codecName). apply {
configure (mediaFormat, outputSurface, null , 0 )
}
decodeMediaCodec?. start ()
}
/** ドルビービジョンのデコーダーが存在するか */
private fun hasDolbyVisionDecoder (): Boolean {
val format = MediaFormat ()
format. setString (MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_DOLBY_VISION)
val mediaCodecList = MediaCodecList (MediaCodecList.REGULAR_CODECS)
val decoder = mediaCodecList. findDecoderForFormat (format)
// true if a Dolby Vision decoder is found, false otherwise.
return decoder != null
}
でも、多分本物のドルビービジョン(PQ
の方、何て言うのか知らない)がこれだと多分動かない、、、
まー本物はそれ用のデコーダーが存在するんじゃないんかな(しらんけど)
ソースコード(再掲)
https://github.com/takusan23/AndroidMediaCodecHdrVideoCanvasOverlay
すぺしゃるさんくす
Camera2 API
サンプルアプリのHDR
動画撮影サンプル
おまけ
MediaRecorder
のstop()
が反応しなくなったり、MediaCodec
がいきなり失敗するようになったら、端末の再起動を試してみるとよいです。
おわりに
しばらくはこの10 ビット HDR
で撮影しようと思った。
(と思ったけどPixel
は30fps
でしか10 ビット HDR
撮れないんだよな。どっちを取るか。)
私が見ている分には、HDR
→SDR
にトーンマッピングしても気付ける自信あんまりないし(えっ)
以上です、おつかれさまでした。8888888
おわりに2
Google フォト
で10 ビット HDR
の動画のカットと回転をやろうとしたら対応してなくて、自作動画編集アプリでちょうど対応しておいて良かった。。。
(回転機能はないので大急ぎでつけた)
おわりに3
YouTube
にHDR
の動画アップしたのに画質一覧にはSDR
しかないんですけど?
って思って調べてみるとHDR
の画質が用意されるには結構時間がかかるそう 。
「YouTube HDR 時間かかる」とかいうふわっふわな検索ワードですら引っかかる。
そういえばHDR
のメタデータがなんとかってやつは多分、MediaRecorder
もしくはMediaCodec + MediaMuxer
あたりが勝手にmp4
に書き込んでおいてくれているはずなので、
メタデータが付いていないってことは無さそう。
数時間後に反映された、Android
のAPI
はちゃんとYouTube
側が分かるメタデータをつけてくれたようです。
ところで、Pixel 8 Pro
、カタログスペックでは10 ビット HDR
撮影だと60fps
無理なんだけど、なんかCamera2 API
を叩くと60fps
になってる?
YouTube
にアップして気付いた。ただフレームを水増ししただけの30fps
の可能性もある。