たくさんの自由帳
Androidのお話
たくさんの自由帳
投稿日 : | 0 日前
文字数(だいたい) : 8263
目次
本題
環境
media3
どうにかして OpenGL ES を回避できませんか
SurfaceView は多分ぼかしが出来ない
映像の出力先は1つだけ
Bitmap を取り出して背景をぼかす
適当にプロジェクトを作る
SurfaceView を置いて、最低限動画が再生できるように
media3-effect を使い始める
アスペクト比
自前のフラグメントシェーダーで映像に手をいれる
これから何をやるか
ざっくりフラグメントシェーダー
フラグメントシェーダーを ExoPlayer に適用する
エフェクトを追加
完成品
番外編 動画ファイルにして欲しい
media3-transformer を入れる
組み込む
動画書き出し完成品
そーすこーど
おわりに
おわりに2
どうもこんにちは。
きらかの 攻略しました。"草なんだが"すき
まさかの接点でびっくり、
事件が解決しておわってしまった、もうちょっと見たかった
あと仮想世界でのシーンがあってよかった!!!

自作動画編集アプリ チュートリアル 動画の両端をぼかした動画の作り方 - たくさんの自由帳
https://takusan.negitoro.dev/posts/akari_droid_tutorial_video_side_blur/
今回は開発としてmedia3ライブラリのプレイヤーで、同じように両端をぼかすやつをやってみようと思います。
OpenGL ESを触りますが、OpenGL ESセットアップはmedia3 effectライブラリが、OpenGL ESでぼかし処理をするGLSL コード(フラグメントシェーダー)はGitHubからお借りすることにします。
| なまえ | あたい |
|---|---|
| 端末 | Pixel 8 Pro / Xperia 1 V |
| Android Studio | Android Studio Ladybug Feature Drop 2024.2.2 |
| 言語 | Kotlin / GLSL(ぼかし処理) |
| minSdk | 21 |
この記事では、Jetpack Composeを使いますがxmlでUIを作ってもいいです。SurfaceViewを画面内に設置できれば何でも良いです、この記事の本題はプレイヤー周りですから。
プレイヤーのmedia3-exoplayerを始めとして、プレイヤーのUIを提供するmedia3-ui、
動画のフレーム加工するmedia3-effect、簡単な動画編集ができるmedia3-transformerなんかがあります。ExoPlayerと呼ばれていたものはmedia3-exoplayerにあたりますね。
media3-effectでぼかしを適用するフラグメントシェーダーを書いて、動画の両端をぼかそうと目論んでいます。effectが登場するまではOpenGL ESのセットアップまで自分で書かないといけなくて大変だった、、今ならフラグメントシェーダーと少しのKotlin コードでいいはず(Uniform変数とかの)。ExoPlayer/demos/gl at release-v2 · google/ExoPlayer
This project is deprecated and stale. The latest ExoPlayer code is available in https://github.com/androidx/media - google/ExoPlayer
https://github.com/google/ExoPlayer/tree/release-v2/demos/gl
GitHubからお借りすることにします。。。ありざいす!GitHub - GameMakerDiscord/blur-shaders: A collection of blur shader examples, with a written tutorial.
A collection of blur shader examples, with a written tutorial. - GameMakerDiscord/blur-shaders
https://github.com/GameMakerDiscord/blur-shaders
多分厳しいと思う
やったことないけど、ぼかせないはず。
SurfaceViewは他のViewと作りが違う(別スレッドで描画できたりする特殊なやつ)なので試せてないけど多分無理な気がする。
せいぜい上に半透明のViewを重ねるのが限界で、ぼかしは出来ないんじゃないかなあ、、
話がそれるけどWindowsがクラッシュした時、ブルースクリーンに何故かそれまで見ていたYouTubeの動画が一緒に残る事があるけど、
それと同じ雰囲気を感じる、一部の描画処理を回避してるからブルースクリーンを貫通するみたいな。
ExoPlayerが使っているデコーダー(MediaCodec)の制限上、映像の出力先を1つしか指定できないのでこれは出来ない。Is it possible to play one ExoPlayer instance at two PlayerView at the same time? · Issue #4880 · google/ExoPlayer
Task description Show two views that play the same stream and are synchronized with each other What I have currently achieved so far Create 2 instances of ExoPlayer with the same stream Uri and for...
https://github.com/google/ExoPlayer/issues/4880
TextureViewは普通にgetBitmap()が、SurfaceViewもPixelCopyを使うことでBitmapが取得できますが、
間に合わなくてフレームドロップしてしまいそう。30fpsなら33ms、60fpsなら16ms以内にBitmapを取り出す必要があるがそんなに高速じゃない。
ぼかす背景用のBitmapならちょっと遅れてもそんなに気にならんかも。要検証。
適当に作ったら、media3-exoplayer、media3-effectライブラリを入れてください。hlsやmpeg-dashで配信された動画を再生する場合はそれらも入れてください。今回は端末の中にある動画を再生するだけなのでこれだけ。
app/build.gradle.kts
dependencies {
// media3
implementation("androidx.media3:media3-exoplayer:1.5.1")
implementation("androidx.media3:media3-effect:1.5.1")
// 以下省略..
}お好みでバージョンカタログにしてください。
AndroidView()でSurfaceViewを置けば良いです。いつの間にかAndroidExternalSurface()っていうAndroidView() + SurfaceViewを既に用意したものがあるのでそれを使っても良いかもしれません。
動画は端末内にあるものを選んで再生することにします。別にインターネットから取ってきてもいいですが本題からずれるので、、、
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
Media3VideoSideBlurTheme {
MainScreen()
}
}
}
}
@Composable
private fun MainScreen() {
val context = LocalContext.current
// ExoPlayer
val exoPlayer = remember { ExoPlayer.Builder(context).build() }
// PhotoPicker
val videoPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri ->
// 動画を選んだらセットして再生
exoPlayer.setMediaItem(MediaItem.fromUri(uri ?: return@rememberLauncherForActivityResult))
exoPlayer.prepare()
exoPlayer.playWhenReady = true
}
)
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = { videoPicker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.VideoOnly)) }) {
Text(text = "動画を選ぶ")
}
// アスペクト比を縦動画に
AndroidView(
modifier = Modifier
.fillMaxHeight(0.8f)
.aspectRatio(9 / 16f),
factory = { SurfaceView(it) },
update = { surfaceView ->
// 出力先にする
exoPlayer.setVideoSurfaceView(surfaceView)
}
)
}
}
}結果はこんな感じで、まあアスペクト比が縦なので引き伸ばされてます。
これから端をぼかしていこうと思います。
effect自体のチュートリアルは存在しない。本来はmedia3-transformerっていうシンプルな動画編集ライブラリの、映像加工のためのライブラリなんですよねこれ。
ただ、effectを動画編集ではなくて通常再生でも使うことができるので大丈夫。
もちろん、ぼかしを入れた動画を動画ファイルにするためにmedia3-transformerを使うことが出来ます。せっかくなので最後に試してみましょう。
まずは出力サイズを決めます。
縦動画なのでアスペクト比を16:9ではなく9:16にします。ExoPlayerのインスタンス生成後にExoPlayer#setVideoEffectsを呼び出すことで、media3-effectの各エフェクトが適用できます。
// ExoPlayer
val exoPlayer = remember {
ExoPlayer.Builder(context).build().apply {
setVideoEffects(
listOf(
// 縦動画に
Presentation.createForAspectRatio(9 / 16f, Presentation.LAYOUT_SCALE_TO_FIT)
)
)
}
}これだけでまずはアスペクト比が自動的に修正されます。便利。
media3-effectには既にぼかしとか、上にテキストを重ねるとか、回転行列を適用するとか、色々あるのですが、自分でフラグメントシェーダーを書いて加工することも出来ます。
はい。2回に分けて描画します。
まずは行列を使って動画のスケールを大きくし、縦方向を目一杯埋めます。(回転やスケールや位置変更だけなら最初から出来ます)
そのあと、フラグメントシェーダーに書いたぼかし処理を動かすためglDrawArrays()します。これで目いっぱいにぼかした動画が表示されるはず。
その次に、今度は行列をもとに戻し、フラグメントシェーダーもぼかさずそのまま出力するようにして、もう一回glDrawArrays()を呼んで描画します。
これでぼかし動画がつくれた!
WebGLの登場により知見が増えてるので調べたりAIに聞けばいいと思う。android.opengl.Matrix クラス(package 違い多数存在)がユーティリティ関数を公開しているので、自分で計算する必要はないです。GLSLは2つ書く必要がありバーテックスシェーダーとフラグメントシェーダーですね。
この中でも今回は色を確定するフラグメントシェーダーを触ります。
何もしないフラグメントシェーダーはこうなります。(GLES 3.0)
#version 300 es
precision highp float;
in vec2 vTexSamplingCoord;
uniform sampler2D uTexSampler;
// 出力する色
out vec4 fragColor;
void main()
{
vec4 Color = texture( uTexSampler, vTexSamplingCoord);
fragColor = Color;
}上2行はおまじないです(調べてください)。main()関数が画面のピクセルごとに呼ばれて、texture()関数で画像の色を取り出し、fragColor変数に代入しています。out vec4 fragColorが実際に画面に表示される色になります。
uniform sampler2D uTexSamplerのuniformは、CPUから(ここではKotlinで)値をフラグメントシェーダー(GPU)に渡したい時に使います。intやfloat、ベクトルだって渡せます。
vec4は、小数点の数字が4つ入れられる型で、グラフィックスの世界では行列やベクトルとか呼んでいるものです。まぁ配列です。
普通にfloatやintもあります。
texture()はvec4型を返し、それぞれvec4(red, green, blue, alpha )の順番で並んでます。各値は0.0から1.0です。255ではないです。
そのため、赤色だけ出力したい場合は帰ってきたvec4から、r以外のgとbを0すればいいですね。
fragColor = texture(uTexSampler, vTexSamplingCoord);
fragColor.g = 0.;
fragColor.b = 0.;.gや.bの添字はvec4とかのベクトルに生えてて、スウィズル演算子とか言う名前がついてます。
これ以外にも.rgbとすればvec3(red, green, blue)が帰ってきます。.xyzとかもあります。
また、vec系のコンストラクタは、すでにあるvec系を引数に取ることもできるため、上記の赤色だけ出力はこの用に置き換えることも出来ます。
fragColor = texture(uTexSampler, vTexSamplingCoord);
fragColor = vec4(fragColor.r, vec3(0));OpenGLまでとは言わなくてもAGSLとかいうAndroid版のフラグメントシェーダーみたいなのがあります。GLSLのフラグメントシェーダーと同じなので移植も簡単そう。
Cómo usar AGSL en tu app para Android | Views | Android Developers
El lenguaje de sombreado de gráficos (AGSL) de Android funciona dentro del sistema de renderización de gráficos de Android para personalizar los dibujos.
https://developer.android.com/develop/ui/views/graphics/agsl/using-agsl?hl=es-419

Made in Compose - Dynamic Island
Recreating the dynamic island animation from the new iPhone 14 Pro in Jetpack Compose.
https://www.sinasamaki.com/dynamic-island/
iPhoneのDynamic Islandを再現するやつ
Making Jellyfish move in Compose: Animating ImageVectors and applying AGSL RenderEffects 🐠
Learn how to use ImageVectors in Compose
https://medium.com/androiddevelopers/making-jellyfish-move-in-compose-animating-imagevectors-and-applying-agsl-rendereffects-3666596a8888
でも多分OpenGL ESやWebGL、あとはさっきのAndroid AGSLにそのまま貼り付けても動かないと思います。でも動くように直すのもそんなに難しくないはずで、shadertoyが最初から提供している変数を自分で追加すれば動くようになる、、、はず。
例えばテクスチャはiChannelになってるので、自分で定義するように直せばいいはず。
他にも解像度とかも渡すようにしないといけないかも。
まずは答えを。どーん。GlEffectを継承したクラスを作ります。継承する関数が一つあるので、それでGlShaderProgramを返します。GlShaderProgramがフラグメントシェーダー(やバーテックスシェーダー)で映像を加工するためのクラスで、ここの関数が動画のフレームのたびに呼ばれるということですね。
先述の通り、2回に分けて描画します。2回目は目一杯広げないため、描画してない両端の部分は透明であってほしいです。せっかくぼかしたのに、、、
というわけでまずはアルファチャンネルを有効にします。GLES20.glEnable(GLES20.GL_BLEND)の部分ですね。
あとは動画のフレームのたびに(映像が切り替わるたびに)drawFrameが呼ばれるので、OpenGL ESで描画しようって。iDrawModeというUniform変数を切り替えることで、ぼかすかぼかさないかを選べるようにしてあります。
1回目はぼかすのでsetIntUniform("iDrawMode", 1)を、あと、行列を操作し、目一杯広がるようにしています。Matrix.scaleM(identityTransformationMatrix, 0, 3.5f, 3.5f, 1f)ですね。
おわったら描画。glDrawArrays()します。
つぎに、ぼかさずに描画したいのでsetIntUniform("iDrawMode", 2)します。
行列も大きくなったままなので、setIdentityM(identityTransformationMatrix, 0)で戻します。
これでもう一度glDrawArrays()することで、背景をぼかした動画が作れるわけです。
ぼかし処理はフラグメントシェーダーでやっているので、ぼかし具合はフラグメントシェーダー内の定数を操作することで変更できます!!
/** ExoPlayer で両端をぼかすやつ */
@UnstableApi
class Media3VideoSideBlurEffect : GlEffect {
override fun toGlShaderProgram(context: Context, useHdr: Boolean): GlShaderProgram {
return BlurEffectGlShaderProgram(useHdr, 1)
}
private class BlurEffectGlShaderProgram(
useHighPrecisionColorComponents: Boolean,
texturePoolCapacity: Int
) : BaseGlShaderProgram(useHighPrecisionColorComponents, texturePoolCapacity) {
private val glProgram = GlProgram(VERTEX_SHADER, FRAGMENT_SHADER)
private val identityTransformationMatrix = GlUtil.create4x4IdentityMatrix()
private val identityTexTransformationMatrix = GlUtil.create4x4IdentityMatrix()
private var size: Size? = null
init {
glProgram.setBufferAttribute(
"aFramePosition",
GlUtil.getNormalizedCoordinateBounds(),
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE
)
// Uniform 変数を更新
glProgram.setFloatsUniform("uTransformationMatrix", identityTransformationMatrix)
glProgram.setFloatsUniform("uTexTransformationMatrix", identityTexTransformationMatrix)
// アルファブレンディングを有効
// 2回 glDrawArrays してブラーの背景の上に動画を重ねるため
GLES20.glEnable(GLES20.GL_BLEND)
GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA)
}
override fun configure(inputWidth: Int, inputHeight: Int): Size {
val size = Size(inputWidth, inputHeight)
// Uniform 変数でも使いたい
this.size = size
return size
}
override fun drawFrame(inputTexId: Int, presentationTimeUs: Long) {
glProgram.use()
// テクスチャID(映像フレーム)をセット
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0)
// サイズを Uniform 変数に入れる
glProgram.setFloatsUniform("vResolution", floatArrayOf(size!!.width.toFloat(), size!!.height.toFloat()))
// 描画する。まず背景(ブラー)
glProgram.setIntUniform("iDrawMode", 1)
// 3倍くらいに拡大してはみ出させる
Matrix.setIdentityM(identityTransformationMatrix, 0)
Matrix.scaleM(identityTransformationMatrix, 0, 3.5f, 3.5f, 1f)
glProgram.setFloatsUniform("uTransformationMatrix", identityTransformationMatrix)
glProgram.bindAttributesAndUniforms()
// The four-vertex triangle strip forms a quad.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4)
// 次に最前面の動画を
glProgram.setIntUniform("iDrawMode", 2)
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0)
// はみ出しは戻す
Matrix.setIdentityM(identityTransformationMatrix, 0)
glProgram.setFloatsUniform("uTransformationMatrix", identityTransformationMatrix)
glProgram.bindAttributesAndUniforms()
// The four-vertex triangle strip forms a quad.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4)
}
companion object {
/** バーテックスシェーダー */
private const val VERTEX_SHADER = """#version 300 es
in vec4 aFramePosition;
uniform mat4 uTransformationMatrix;
uniform mat4 uTexTransformationMatrix;
out vec2 vTexSamplingCoord;
void main() {
gl_Position = uTransformationMatrix * aFramePosition;
vec4 texturePosition = vec4(aFramePosition.x * 0.5 + 0.5,
aFramePosition.y * 0.5 + 0.5, 0.0, 1.0);
vTexSamplingCoord = (uTexTransformationMatrix * texturePosition).xy;
}
"""
/**
* フラグメントシェーダー
* thx!!!!
* https://github.com/GameMakerDiscord/blur-shaders
*/
private const val FRAGMENT_SHADER = """#version 300 es
precision highp float;
in vec2 vTexSamplingCoord;
uniform sampler2D uTexSampler;
// どっちを描画するか。1 = 背景(ブラー) / 2 = 最前面(ブラーしない)
uniform int iDrawMode;
// 動画のサイズ
uniform vec2 vResolution;
// ぼかし
const int Quality = 3;
const int Directions = 16;
const float Pi = 6.28318530718; //pi * 2
const float Radius = 16.0; // ぼかし具合
// 出力する色
out vec4 fragColor;
void main()
{
vec2 radius = Radius / vResolution.xy;
vec4 Color = texture( uTexSampler, vTexSamplingCoord);
// 背景を描画するモード
if (iDrawMode == 1) {
for( float d=0.0;d<Pi;d+=Pi/float(Directions) )
{
for( float i=1.0/float(Quality);i<=1.0;i+=1.0/float(Quality) )
{
Color += texture( uTexSampler, vTexSamplingCoord+vec2(cos(d),sin(d))*radius*i);
}
}
Color /= float(Quality)*float(Directions)+1.0;
fragColor = Color;
}
// 最前面の動画を描画するモード
if (iDrawMode == 2) {
fragColor = Color;
}
}
"""
}
}
}これを、ExoPlayer.setVideoEffectsの配列に追加すれば完成です。見てみましょう。
// ExoPlayer
val exoPlayer = remember {
ExoPlayer.Builder(context).build().apply {
setVideoEffects(
listOf(
// 縦動画に
Presentation.createForAspectRatio(9 / 16f, Presentation.LAYOUT_SCALE_TO_FIT),
// ぼかす
Media3VideoSideBlurEffect()
)
)
}
}setVideoEffectsのエフェクトは先述の通りmedia3-transformerの動画編集で使うことができるので、動画ファイルを作る機能も作ってみましょう。
app/build.gradle.ktsにライブラリを追加します。
dependencies {
// media3
implementation("androidx.media3:media3-exoplayer:1.5.1")
implementation("androidx.media3:media3-effect:1.5.1")
// transformer
implementation("androidx.media3:media3-transformer:1.5.1")
implementation("androidx.media3:media3-common:1.5.1")
// 以下省略...まとめてどーーーん。EditedMediaItem.BuilderにsetEffectsってのがあるので、それをExoPlayerのときと同じく渡せばいいです。
あとはTransformerのサンプル通りでいいはず。
動画の保存先にUriが選べないので、一旦getExternalFilesDir()などのJava File APIが使える領域に保存したあと、
動画フォルダに追加しuriを返してもらい、InputStream / OutputStreamでコピーすればいいです。KotlinだとcopyTo拡張関数のお陰ではい、一発
// media3-transformer 用
val scope = rememberCoroutineScope()
val transformVideoPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri ->
uri ?: return@rememberLauncherForActivityResult
// 動画の一時保存先
val tempVideoFile = context.getExternalFilesDir(null)!!.resolve("VideoSideBlur_${System.currentTimeMillis()}.mp4")
val inputMediaItem = MediaItem.fromUri(uri)
val editedMediaItem = EditedMediaItem.Builder(inputMediaItem).apply {
setEffects(
Effects(
/* audioProcessors = */ emptyList(),
/* videoEffects = */ listOf(
// 縦動画に
Presentation.createForAspectRatio(9 / 16f, Presentation.LAYOUT_SCALE_TO_FIT),
// ぼかす
Media3VideoSideBlurEffect()
)
)
)
}.build()
val transformer = Transformer.Builder(context).apply {
setVideoMimeType(MimeTypes.VIDEO_H264)
addListener(object : Transformer.Listener {
// 完了した
override fun onCompleted(composition: Composition, exportResult: ExportResult) {
super.onCompleted(composition, exportResult)
// 端末の動画フォルダに移動させる
val contentValues = contentValuesOf(
MediaStore.Video.Media.DISPLAY_NAME to tempVideoFile.name,
MediaStore.Video.Media.RELATIVE_PATH to "${Environment.DIRECTORY_MOVIES}/VideoSideBlur"
)
val copyToUri = context.contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues)!!
context.contentResolver.openOutputStream(copyToUri)?.use { outputStream ->
tempVideoFile.inputStream().use { inputStream ->
inputStream.copyTo(outputStream)
}
}
tempVideoFile.delete()
Toast.makeText(context, "おわり", Toast.LENGTH_SHORT).show()
}
})
}.build()
transformer.start(editedMediaItem, tempVideoFile.path)
}
)
Button(onClick = { transformVideoPicker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.VideoOnly)) }) {
Text(text = "ぼかした動画を動画ファイルにする")
}動画フォルダのVideoSideBlurフォルダ内にあるはず。
media3-effect、多分media3-transformer(動画編集)が主な利用目的でmedia3-exoplayerはおまけというか、media3-transformerのプレビューのためみたいな側面があるのか、
いくつかIssueがあります。。。
どうしてもこれをやる場合は十分に動作確認をしたほうが良さそうです。
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
ちなみに10 ビット HDR 動画もを入れても動きます。もちろんmedia3-transformerもね。