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

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




相关文章
|
5天前
|
安全 Java 编译器
Android面试题之Java 泛型和Kotlin泛型
**Java泛型是JDK5引入的特性,用于编译时类型检查和安全。泛型擦除会在运行时移除类型参数,用Object或边界类型替换。这导致几个限制:不能直接创建泛型实例,不能使用instanceof,泛型数组与协变冲突,以及在静态上下文中的限制。通配符如<?>用于增强灵活性,<? extends T>只读,<? super T>只写。面试题涉及泛型原理和擦除机制。
15 3
Android面试题之Java 泛型和Kotlin泛型
|
2天前
|
安全 Java Android开发
安卓开发中的新趋势:Kotlin与Jetpack的完美结合
【6月更文挑战第20天】在不断进化的移动应用开发领域,Android平台以其开放性和灵活性赢得了全球开发者的青睐。然而,随着技术的迭代,传统Java语言在Android开发中逐渐显露出局限性。Kotlin,一种现代的静态类型编程语言,以其简洁、安全和高效的特性成为了Android开发中的新宠。同时,Jetpack作为一套支持库、工具和指南,旨在帮助开发者更快地打造优秀的Android应用。本文将探讨Kotlin与Jetpack如何共同推动Android开发进入一个新的时代,以及这对开发者意味着什么。
|
6天前
|
安全 JavaScript 前端开发
kotlin开发安卓app,JetPack Compose框架,给webview新增一个按钮,点击刷新网页
在Kotlin中开发Android应用,使用Jetpack Compose框架时,可以通过添加一个按钮到TopAppBar来实现WebView页面的刷新功能。按钮位于右上角,点击后调用`webViewState?.reload()`来刷新网页内容。以下是代码摘要:
|
7天前
|
JavaScript Java Android开发
kotlin安卓在Jetpack Compose 框架下跨组件通讯EventBus
**EventBus** 是一个Android事件总线库,简化组件间通信。要使用它,首先在Gradle中添加依赖`implementation &#39;org.greenrobot:eventbus:3.3.1&#39;`。然后,可选地定义事件类如`MessageEvent`。在活动或Fragment的`onCreate`中注册订阅者,在`onDestroy`中反注册。通过`@Subscribe`注解方法处理事件,如`onMessageEvent`。发送事件使用`EventBus.getDefault().post()`。
|
7天前
|
Android开发 Kotlin
Android面试题 之 Kotlin DataBinding 图片加载和绑定RecyclerView
本文介绍了如何在Android中使用DataBinding和BindingAdapter。示例展示了如何创建`MyBindingAdapter`,包含一个`setImage`方法来设置ImageView的图片。布局文件使用`&lt;data&gt;`标签定义变量,并通过`app:image`调用BindingAdapter。在Activity中设置变量值传递给Adapter处理。此外,还展示了如何在RecyclerView的Adapter中使用DataBinding,如`MyAdapter`,在子布局`item.xml`中绑定User对象到视图。关注公众号AntDream阅读更多内容。
16 1
|
7天前
|
JavaScript 前端开发 Android开发
kotlin安卓在Jetpack Compose 框架下使用webview , 网页中的JavaScript代码如何与native交互
在Jetpack Compose中使用Kotlin创建Webview组件,设置JavaScript交互:`@Composable`函数`ComposableWebView`加载网页并启用JavaScript。通过`addJavascriptInterface`添加`WebAppInterface`类,允许JavaScript调用Android方法如播放音频。当页面加载完成时,执行`onWebViewReady`回调。
|
1天前
|
Java Android开发 Kotlin
Android面试题:App性能优化之Java和Kotlin常见的数据结构
Java数据结构摘要:ArrayList基于数组,适合查找和修改;LinkedList适合插入删除;HashMap1.8后用数组+链表/红黑树,初始化时预估容量可避免扩容。SparseArray优化查找,ArrayMap减少冲突。 Kotlin优化摘要:Kotlin的List用`listOf/mutableListOf`,Map用`mapOf/mutableMapOf`,支持操作符重载和扩展函数。序列提供懒加载,解构用于遍历Map,扩展函数默认参数增强灵活性。
9 0
|
Android开发 数据格式 XML
|
4天前
|
存储 Java 数据库连接
Android Java开发异步
【6月更文挑战第15天】
|
1天前
|
Java 开发工具 Android开发
安卓与iOS开发差异解析
【6月更文挑战第21天】本文旨在深入探讨安卓和iOS两大移动操作系统在应用开发过程中的主要差异。通过对比分析,揭示各自的设计哲学、编程语言选择、用户界面构建、性能优化策略以及发布流程的异同。文章将提供开发者视角下的实用信息,帮助他们更好地理解各自平台的特点和挑战,从而做出更明智的开发决策。