安卓Jetpack Compose+Kotlin, 使用ExoPlayer播放多个【本地】音频,播放完随机播放下一首,遇到播放错误,也自动播放下一首

简介: 使用Kotlin和Jetpack Compose开发的安卓应用中,实现了两个EvoPlayer同时播放res/raw目录下的音频。一个音轨播放人声(顺序播放),另一个播放背景音乐(随机播放)。每个音轨都有独立的播放和停止控制,且在播放结束或遇到错误时会自动切换到下一首。MediaPlayer置于ViewModel中,UI界面包含播放和停止按钮,控制两个音轨。每次切换音频前,还会随机调整播放速度在0.9到1.2之间。代码示例展示了如何创建ViewModel和UI以实现这一功能。



需求描述:

kotlin开发安卓app, Jetpack Compose框架,使用2个EvoPlayer播放res/raw中的音乐,实现2个音轨同时播放,一个是人声音轨,有多个人声音频,另一个是背景音乐音轨,有多个背景音乐, 不断循环播放列表中的音乐, 人声轨道为顺序播放,背景音乐轨道为随机播放, MediaPlayer在viewModel中, UI层有2个按钮,分别控制播放和停止,按钮同时控制2个音轨的播放和停止 。 遇到报错,则继续播放。人声轨道和背景音乐轨道,每次播放下一首音频之前,随机设置播放速度在0.9到1.2之间。






dependencies {
    // Jetpack Compose
    implementation "androidx.compose.ui:ui:1.1.1"
    implementation "androidx.compose.material:material:1.1.1"
    implementation "androidx.compose.ui:ui-tooling-preview:1.1.1"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1"
    implementation "androidx.activity:activity-compose:1.4.0"
    implementation "androidx.activity:activity-ktx:1.4.0"

    // ExoPlayer
    implementation 'com.google.android.exoplayer:exoplayer:2.14.1'
}


接下来,我们创建ViewModel来管理ExoPlayer:


import android.app.Application
import android.content.Context
import android.net.Uri
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.PlaybackException
import com.google.android.exoplayer2.PlaybackParameters
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.random.Random

class AudioPlayerViewModel(application: Application) : AndroidViewModel(application) {

    private val context: Context = application.applicationContext

    private val voicePlayer: ExoPlayer = ExoPlayer.Builder(context).build().apply {
        addListener(object: Player.Listener {
            override fun onPlaybackStateChanged(playbackState: Int) {
                if (playbackState == Player.STATE_ENDED) {
                    playNextVoiceTrack()
                }
            }

            override fun onPlayerError(error: PlaybackException) {
                playNextVoiceTrack()
            }
        })
    }

    private val bgmPlayer: ExoPlayer = ExoPlayer.Builder(context).build().apply {
        addListener(object: Player.Listener {
            override fun onPlaybackStateChanged(playbackState: Int) {
                if (playbackState == Player.STATE_ENDED) {
                    playNextBgmTrack()
                }
            }

            override fun onPlayerError(error: PlaybackException) {
                playNextBgmTrack()
            }
        })
    }

    private val voiceTracks = listOf(R.raw.voice1, R.raw.voice2) // Add your voice tracks here
    private val bgmTracks = listOf(R.raw.bgm1, R.raw.bgm2) // Add your BGM tracks here

    private var currentVoiceTrackIndex = 0

    init {
        playNextVoiceTrack()
        playNextBgmTrack()
    }

    private fun playAudio(player: ExoPlayer, uri: Uri, playbackSpeed: Float) {
        player.apply {
            val mediaItem = MediaItem.fromUri(uri)
            setMediaItem(mediaItem)
            playbackParameters = PlaybackParameters(playbackSpeed)
            prepare()
            playWhenReady = true
        }
    }

    private fun playNextVoiceTrack() {
        val uri = getUriForRawFile(context, voiceTracks[currentVoiceTrackIndex])
        val playbackSpeed = Random.nextFloat() * (1.2f - 0.9f) + 0.9f // Random speed between 0.9 and 1.2
        playAudio(voicePlayer, uri, playbackSpeed)
        currentVoiceTrackIndex = (currentVoiceTrackIndex + 1) % voiceTracks.size
    }

    private fun playNextBgmTrack() {
        val randomIndex = Random.nextInt(bgmTracks.size)
        val uri = getUriForRawFile(context, bgmTracks[randomIndex])
        val playbackSpeed = Random.nextFloat() * (1.2f - 0.9f) + 0.9f // Random speed between 0.9 and 1.2
        playAudio(bgmPlayer, uri, playbackSpeed)
    }

    private fun getUriForRawFile(context: Context, resId: Int): Uri {
        return Uri.parse("android.resource://${context.packageName}/$resId")
    }

    fun play() {
        voicePlayer.playWhenReady = true
        bgmPlayer.playWhenReady = true
    }

    fun stop() {
        voicePlayer.stop()
        bgmPlayer.stop()
    }

    override fun onCleared() {
        super.onCleared()
        voicePlayer.release()
        bgmPlayer.release()
    }
}



接下来,我们在 UI 层创建 Compose 组件,使用按钮来控制两个音轨的播放和停止。




import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp()
        }
    }
}

@Composable
fun MyApp(audioPlayerViewModel: AudioPlayerViewModel = viewModel()) {
    Scaffold(
        topBar = {
            TopAppBar(title = { Text("Audio Player") })
        },
        content = {
            Column(
                Modifier
                    .fillMaxSize()
                    .padding(16.dp),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally
            ) {
                Button(
                    onClick = { audioPlayerViewModel.play() },
                    modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)
                ) {
                    Text("Play Audio")
                }
                Button(
                    onClick = { audioPlayerViewModel.stop() },
                    modifier = Modifier.fillMaxWidth()
                ) {
                    Text("Stop Audio")
                }
            }
        }
    )
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MyApp()
}


解释

  1. ViewModel:
  • AudioPlayerViewModel 中初始化两个 ExoPlayer 实例: voicePlayer 和 bgmPlayer。
  • 添加了两个 Player.Listener 来监听播放状态和错误,分别为人声音轨和背景音乐音轨处理播放结束和错误事件。
  • 提供 playNextVoiceTrack 和 playNextBgmTrack 方法来实现分别播放人声音轨和背景音乐音轨,并在每次播放时随机设置播放速度。
  • playAudio 方法用来播放指定 URI 的音轨,并设置播放速度。
  • play 方法和 stop 方法分别控制两个音轨的播放和停止。
  1. UI层:
  • MyApp 组件使用了两个按钮,通过点击按钮来控制 viewModel 中的播放和停止操作。

通过这种方式,应用程序可以同时播放两个音轨,并分别控制播放和停止。人声音轨按照列表顺序播放,背景音乐音轨随机播放。每次播放下一首音轨之前,都会随机设置播放速度。错误处理逻辑保证了在出现错误时可以继续播放下一首音轨。




相关文章
|
安全 Java Android开发
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
505 0
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
|
Android开发 Kotlin
Android经典面试题之Kotlin的==和===有什么区别?
本文介绍了 Kotlin 中 `==` 和 `===` 操作符的区别:`==` 用于比较值是否相等,而 `===` 用于检查对象身份。对于基本类型,两者行为相似;对于对象引用,`==` 比较值相等性,`===` 检查引用是否指向同一实例。此外,还列举了其他常用比较操作符及其应用场景。
545 94
|
Android开发 开发者 Kotlin
Android实战经验之Kotlin中快速实现MVI架构
MVI架构通过单向数据流和不可变状态,提供了一种清晰、可预测的状态管理方式。在Kotlin中实现MVI架构,不仅提高了代码的可维护性和可测试性,还能更好地应对复杂的UI交互和状态管理。通过本文的介绍,希望开发者能够掌握MVI架构的核心思想,并在实际项目中灵活应用。
681 8
|
编译器 Android开发 开发者
Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
Lambda表达式和匿名函数都是Kotlin中强大的特性,帮助开发者编写简洁而高效的代码。理解它们的区别和适用场景,有助于选择最合适的方式来解决问题。希望本文的详细讲解和示例能够帮助你在Kotlin开发中更好地运用这些特性。
457 9
|
编译器 Android开发 开发者
带你了解Android Jetpack库中的依赖注入框架:Hilt
本文介绍了Hilt,这是Google为Android开发的依赖注入框架,基于Dagger构建,旨在简化依赖注入过程。Hilt通过自动化的组件和注解减少了DI的样板代码,提高了应用的可测试性和可维护性。文章详细讲解了Hilt的主要概念、基本用法及原理,帮助开发者更好地理解和应用Hilt。
1097 18
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
325 4
|
存储 前端开发 测试技术
Android kotlin MVVM 架构简单示例入门
Android kotlin MVVM 架构简单示例入门
623 1
|
存储 前端开发 测试技术
Kotlin教程笔记-使用Kotlin + JetPack 对旧项目进行MVVM改造
Kotlin教程笔记-使用Kotlin + JetPack 对旧项目进行MVVM改造
|
测试技术 数据库 Android开发
深入解析Android架构组件——Jetpack的使用与实践
本文旨在探讨谷歌推出的Android架构组件——Jetpack,在现代Android开发中的应用。Jetpack作为一系列库和工具的集合,旨在帮助开发者更轻松地编写出健壮、可维护且性能优异的应用。通过详细解析各个组件如Lifecycle、ViewModel、LiveData等,我们将了解其原理和使用场景,并结合实例展示如何在实际项目中应用这些组件,提升开发效率和应用质量。
644 6
|
JSON 调度 数据库
Android面试之5个Kotlin深度面试题:协程、密封类和高阶函数
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点。文章详细解析了Kotlin中的协程、扩展函数、高阶函数、密封类及`inline`和`reified`关键字在Android开发中的应用,帮助读者更好地理解和使用这些特性。
627 1

热门文章

最新文章