SoundPool
効果音に主に使います。
オーディオ属性のビルダーでサウンド用途やコンテンツタイプを指定します。
SoundPoolのビルダーでは上記で作成したオーディオ属性や、同時に流せる効果音の最大数などを指定します。
予めrawなどのディレクトリに入れておいた音声データのリソースをSoundPoolのload()で読み込みます。
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()関数を使っています。
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
BGM等音楽再生に主に使います。
MediaPlayer.create()でContextと音楽リソースを指定してインスタンスを作ります。
こちらでも画面破棄時にはrelease()でメモリを解放しています。
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()でまた始めから再生できるようにリソースを準備します。
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
動画再生に主に使います。
こちらを参考にさせていただきました。
動画のURLやビルダーを準備します。
seekTo()で初期の再生位置を指定できます。
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という変数を作っておきました。
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を使用してシークバーも作成しています。
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()
)
}
}