安卓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 中的播放和停止操作。

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




相关文章
|
3月前
|
设计模式 Android开发 Kotlin
Android经典实战之Kotlin委托模式和by关键字
本文介绍了Kotlin中`by`关键字在类及属性委托中的运用,通过实例展示了如何利用类委托简化接口实现,以及如何借助标准与自定义属性委托管理属性的读写操作。通过`by`关键字的支持,Kotlin使得委托模式的实现更为直观且高效。
81 4
|
3月前
|
缓存 安全 Android开发
Android经典实战之用Kotlin泛型实现键值对缓存
本文介绍了Kotlin中泛型的基础知识与实际应用。泛型能提升代码的重用性、类型安全及可读性。文中详细解释了泛型的基本语法、泛型函数、泛型约束以及协变和逆变的概念,并通过一个数据缓存系统的实例展示了泛型的强大功能。
41 2
|
2月前
|
Android开发 Kotlin
Android经典面试题之Kotlin的==和===有什么区别?
本文介绍了 Kotlin 中 `==` 和 `===` 操作符的区别:`==` 用于比较值是否相等,而 `===` 用于检查对象身份。对于基本类型,两者行为相似;对于对象引用,`==` 比较值相等性,`===` 检查引用是否指向同一实例。此外,还列举了其他常用比较操作符及其应用场景。
186 93
|
23天前
|
存储 前端开发 测试技术
Android kotlin MVVM 架构简单示例入门
Android kotlin MVVM 架构简单示例入门
27 1
|
1月前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
43 4
|
1月前
|
JSON 调度 数据库
Android面试之5个Kotlin深度面试题:协程、密封类和高阶函数
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点。文章详细解析了Kotlin中的协程、扩展函数、高阶函数、密封类及`inline`和`reified`关键字在Android开发中的应用,帮助读者更好地理解和使用这些特性。
20 1
|
1月前
|
Android开发 Kotlin
Android面试题之Kotlin中如何实现串行和并行任务?
本文介绍了 Kotlin 中 `async` 和 `await` 在并发编程中的应用,包括并行与串行任务的处理方法。并通过示例代码展示了如何启动并收集异步任务的结果。
23 0
|
1月前
|
Java 调度 Android开发
Android面试题之Kotlin中async 和 await实现并发的原理和面试总结
本文首发于公众号“AntDream”,详细解析了Kotlin协程中`async`与`await`的原理及其非阻塞特性,并提供了相关面试题及答案。协程作为轻量级线程,由Kotlin运行时库管理,`async`用于启动协程并返回`Deferred`对象,`await`则用于等待该对象完成并获取结果。文章还探讨了协程与传统线程的区别,并展示了如何取消协程任务及正确释放资源。
23 0
|
2月前
|
安全 Android开发 开发者
探索安卓开发的未来:Kotlin的崛起与Flutter的挑战
在移动开发的广阔天地中,安卓平台始终占据着举足轻重的地位。随着技术的不断进步和开发者需求的多样化,Kotlin和Flutter成为了改变游戏规则的新玩家。本文将深入探讨Kotlin如何以其现代化的特性赢得开发者的青睐,以及Flutter凭借跨平台的能力如何挑战传统的安卓开发模式。通过实际案例分析,我们将揭示这两种技术如何塑造未来的安卓应用开发。
73 6
|
2月前
|
安全 Java Android开发
探索安卓应用开发的新趋势:Kotlin和Jetpack Compose
在安卓应用开发领域,随着技术的不断进步,新的编程语言和框架层出不穷。Kotlin作为一种现代的编程语言,因其简洁性和高效性正逐渐取代Java成为安卓开发的首选语言。同时,Jetpack Compose作为一个新的UI工具包,提供了一种声明式的UI设计方法,使得界面编写更加直观和灵活。本文将深入探讨Kotlin和Jetpack Compose的特点、优势以及如何结合使用它们来构建现代化的安卓应用。
59 4