【Android】各メディア系ライブラリ用途別まとめ

## SoundPool

https://developer.android.com/reference/android/media/SoundPool

効果音に主に使います。

[オーディオ属性](https://developer.android.com/reference/android/media/AudioAttributes)のビルダーでサウンド用途やコンテンツタイプを指定します。
SoundPoolのビルダーでは上記で作成したオーディオ属性や、同時に流せる効果音の最大数などを指定します。
予めrawなどのディレクトリに入れておいた音声データのリソースをSoundPoolのload()で読み込みます。

```SoundPoolScreen.kt
        val context = LocalContext.current
        // オーディオ属性の定義
        val audioAttributes = AudioAttributes.Builder()
            // 用途の指定
            .setUsage(AudioAttributes.USAGE_MEDIA)
            // コンテンツのタイプを指定
            .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
            .build()
        val soundPool = SoundPool.Builder()
            .setAudioAttributes(audioAttributes)
            // 同時に流せる最大数
            .setMaxStreams(3)
            .build()
        // 引数は順にコンテキスト、リソースID、優先度
        val pianoId = soundPool.load(context, R.raw.piano, 1)
```

下記は実際に流す画面サンプルです。
メモリ不足にならないように、DisposableEffectで画面破棄された時にSoundPoolインスタンスのrelease()をしています。
実際に流すにはplay()関数を使っています。

```SoundPoolScreen.kt
    Column {
        // メモリ不足にならないようにrelease()やunload()を行う
        DisposableEffect(LocalLifecycleOwner.current) {
            onDispose { soundPool.release() }
        }

        Button(onClick = {
            // 引数は順にID、左音量、右音量、優先度、ループ、再生速度
            soundPool.play(pianoId, 1.0f, 1.0f, 1, 0, 1.0f)
        }) {
            Text(text = "Piano")
        }
    }

```

## MediaPlayer

https://developer.android.com/media/platform/mediaplayer?hl=ja

BGM等音楽再生に主に使います。

MediaPlayer.create()でContextと音楽リソースを指定してインスタンスを作ります。
こちらでも画面破棄時にはrelease()でメモリを解放しています。

```MediaPlayerScreen.kt
        val context = LocalContext.current
        val lifecycleOwner = LocalLifecycleOwner.current
        val mediaPlayer = MediaPlayer.create(context, R.raw.acoustic)
        DisposableEffect(lifecycleOwner) {
            onDispose {
                mediaPlayer?.release()
            }
        }

```

以下は音楽再生のサンプルです。
mediaPlayer.start()で実際に音楽を鳴らします。
isPlayingで音楽再生中かどうかのフラグを持って、2番目のボタンに一時停止の役割を持たせています。
pause()は一時停止で、stop()は完全に停止です。
prepare()でまた始めから再生できるようにリソースを準備します。

```MediaPlayerScreen.kt
    Column {
        var isPlaying by remember {
            mutableStateOf(false)
        }
        Button(
            onClick = {
                mediaPlayer?.start()
                isPlaying = true
            }) {
            Text(text = "Acoustic")
        }
        Button(
            onClick = {
                if (isPlaying) {
                    mediaPlayer.pause()
                    isPlaying = false
                } else {
                    mediaPlayer?.start()
                    isPlaying = true
                }
            }) {
            Text(
                text = if (isPlaying) "Pause"
                else "Play"
            )
        }
        Button(
            onClick = {
                mediaPlayer?.stop()
                isPlaying = false
                mediaPlayer.prepare()
            }) {
            Text(text = "Stop")
        }
    }

```


## ExoPlayer

https://developer.android.com/media/media3/exoplayer?hl=ja

動画再生に主に使います。

こちらを参考にさせていただきました。

https://tech.gunosy.io/entry/android_compose_video_player

動画のURLやビルダーを準備します。
seekTo()で初期の再生位置を指定できます。

```ExoPlayerScreen.kt
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val url =
        "https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4"
    val exoPlayer = remember(context) {
        ExoPlayer.Builder(context).build().apply {
            setMediaItem(MediaItem.fromUri(Uri.parse(url)))
            // 再生準備させる
            prepare()
            // 再生位置の指定
            seekTo(0L)
        }
    }
```

諸々のフラグ管理のため動画の再生状態をenumで作っています。
シークバーの表示に使うため、currentTimeという変数を作っておきました。

```ExoPlayerScreen.kt
    enum class VideoState {
        LOADING,
        PAUSE,
        START,
        ENDED,
    }

    // 再生状態
    var videoState by remember {
        mutableStateOf(VideoState.LOADING)
    }
    // シークバー用動画の進捗度
    var currentTime by remember {
        mutableLongStateOf(0L)
    }
    // 動画の状態によってUIパーツの状態・表示を切り替える
    exoPlayer.addListener(object : Player.Listener {
        // 状態を通知する
        override fun onPlaybackStateChanged(playbackState: Int) {
            super.onPlaybackStateChanged(playbackState)
            // 再生準備完了時に自動的に再生開始するかどうか
            val playWhenReady = exoPlayer.playWhenReady
            val newState = when {
                playbackState == Player.STATE_ENDED && playWhenReady -> VideoState.ENDED
                playbackState == Player.STATE_BUFFERING || playbackState == Player.STATE_IDLE -> VideoState.LOADING
                playbackState == Player.STATE_READY && playWhenReady -> VideoState.START
                playbackState == Player.STATE_READY -> VideoState.PAUSE
                else -> null
            }
            if (newState != null && newState != videoState) {
                videoState = newState
            }
        }
    })

    // 動画再生時のシークバー連動
    if (videoState == VideoState.START) {
        LaunchedEffect(Unit) {
            repeat(Int.MAX_VALUE) {
                delay(1000)
                // 動画時間を取得してcurrentTimeに反映
                currentTime = exoPlayer.currentPosition
            }
        }
    }

    DisposableEffect(lifecycleOwner) {
        // バックグラウンドにいったら停止
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_PAUSE) {
                exoPlayer.pause()
                videoState = VideoState.PAUSE
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)

        // 画面の破棄時にリリース
        onDispose {
            exoPlayer.run {
                playWhenReady = false
                clearVideoSurface()
                release()
            }
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
```

下記が実際のUIになります。
実際の表示にはAndroidViewを使用しています。
exoPlayer.play()で再生、pause()で一時停止します。
Sliderを使用してシークバーも作成しています。

```ExoPlayerScreen.kt
    Box(
        modifier = Modifier.fillMaxSize()
    ) {
        AndroidView(
            modifier = Modifier
                .fillMaxWidth()
                .wrapContentHeight(),
            factory = { context ->
                PlayerView(context).apply {
                    // デフォルトのUIパーツを表示するかどうか
                    useController = false
                    player = exoPlayer
                }
            }
        )
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .height(42.dp)
                .background(Color.Black)
                .padding(start = 8.dp)
                .align(Alignment.BottomCenter),
            verticalAlignment = Alignment.CenterVertically
        ) {
            // 再生・停止ボタン
            if (videoState == VideoState.START) {
                Button(onClick = {
                    exoPlayer.pause()
                    videoState = VideoState.PAUSE
                }) {
                    Image(
                        painter = painterResource(R.drawable.pause),
                        contentDescription = null
                    )
                }
            } else {
                Button(onClick = {
                    exoPlayer.play()
                    videoState = VideoState.START
                }) {
                    Image(
                        painter = painterResource(R.drawable.play),
                        contentDescription = null
                    )
                }
            }

            // 動画の時間
            Text(
                text = SimpleDateFormat("mm:ss", Locale.JAPAN).format(Date(currentTime))
            )

            // シークバー
            Slider(
                value = currentTime.toFloat(),
                // シークバー操作を動画の進捗に反映させる
                onValueChange = { timeMs ->
                    val time = timeMs.toLong()
                    exoPlayer.seekTo(time)
                    currentTime = time
                },
                // 動画の長さまで
                valueRange = 0f..exoPlayer.duration.coerceAtLeast(0L).toFloat(),
                colors = SliderDefaults.colors()
            )
        }
    }
```