安卓Jetpack Compose+Kotlin, 使用ExoPlayer播放多个【远程url】音频,搭配Okhttp库进行下载和缓存,播放完随机播放下一首

简介: 这是一个Kotlin项目,使用Jetpack Compose和ExoPlayer框架开发Android应用,功能是播放远程URL音频列表。应用会检查本地缓存,如果文件存在且大小与远程文件一致则使用缓存,否则下载文件并播放。播放完成后或遇到异常,会随机播放下一首音频,并在播放前随机设置播放速度(0.9到1.2倍速)。代码包括ViewModel,负责音频管理和播放逻辑,以及UI层,包含播放和停止按钮。

需求描述:


kotlin 开发安卓app, 使用Jetpack Compose框架。 功能描述: 要播放一个列表中的音频,里面存储的是远程url音频。 使用ExoPlayer进行播放,  搭配OKhttp库,如果已经下载过该文件且文件大小与url返回的文件大小一致,则使用本地缓存的文件, 如果没有下载过,或者文件大小不一致,则先下载覆盖本地的文件, 下载完再播放, 如果本地文件不存在,直接下载然后播放。 帮我写一段代码示例。  分为viewModel和UI层, UI层 控制播放和停止。 音频播放完成时,随机播放下一首。 如果网络请求异常,也随机播放下一首。 如果播放出现异常, 也随机播放下一首。 播放下一首之前,随机设置播放速度为0.9到1.2之间




下面是一整套 Kotlin 代码示例,用于实现你描述的功能。这个示例使用 Jetpack Compose 和 ExoPlayer 来播放音频列表,并使用 OkHttp 库来处理音频文件的下载和缓存验证。音频播放完成或出现异常时,将自动随机播放下一首音频,并设置随机播放速度(0.9 到 1.2 之间)。

依赖项

首先,确保在 build.gradle 文件中添加必要的依赖项:



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'

    // OkHttp for downloading files
    implementation 'com.squareup.okhttp3:okhttp:4.9.3'

    // Coroutines for background tasks
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
}



ViewModel

首先,我们创建 AudioPlayerViewModel 来管理 ExoPlayer 实例、音频文件下载和缓存验证逻辑。

AudioPlayerViewModel.kt


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.PlaybackParameters
import com.google.android.exoplayer2.Player
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
import java.io.FileOutputStream
import kotlin.random.Random

class AudioPlayerViewModel(application: Application) : AndroidViewModel(application) {
    private val context: Context = application.applicationContext
    private val exoPlayer: ExoPlayer = ExoPlayer.Builder(context).build()
    private val okHttpClient = OkHttpClient()

    private val audioUrls = listOf(
        "https://example.com/path/to/audio1.mp3",
        "https://example.com/path/to/audio2.mp3",
        "https://example.com/path/to/audio3.mp3"
    )

    init {
        exoPlayer.addListener(object : Player.Listener {
            override fun onPlaybackStateChanged(playbackState: Int) {
                if (playbackState == Player.STATE_ENDED) {
                    playRandomAudio()
                }
            }

            override fun onPlayerError(error: com.google.android.exoplayer2.PlaybackException) {
                playRandomAudio()
            }
        })
    }

    fun play() {
        playRandomAudio()
    }

    fun stop() {
        exoPlayer.stop()
    }

    private fun playRandomAudio() {
        val url = audioUrls.random()
        viewModelScope.launch {
            try {
                val file = getCachedFile(url)

                if (isCacheValid(url, file)) {
                    playAudio(file)
                } else {
                    downloadAndPlay(url, file)
                }
            } catch (e: Exception) {
                e.printStackTrace()
                playRandomAudio() // Play next audio on error
            }
        }
    }

    private suspend fun getCachedFile(url: String): File {
        return withContext(Dispatchers.IO) {
            val fileName = url.substringAfterLast("/")
            File(context.cacheDir, fileName)
        }
    }

    private suspend fun isCacheValid(url: String, file: File): Boolean {
        return withContext(Dispatchers.IO) {
            if (!file.exists()) {
                return@withContext false
            }

            val request = Request.Builder().url(url).head().build()
            val response = okHttpClient.newCall(request).execute()

            if (!response.isSuccessful) {
                return@withContext false
            }

            val remoteFileSize = response.header("Content-Length")?.toLongOrNull() ?: return@withContext false
            val localFileSize = file.length()

            remoteFileSize == localFileSize
        }
    }

    private suspend fun downloadAndPlay(url: String, file: File) {
        withContext(Dispatchers.IO) {
            try {
                val request = Request.Builder().url(url).build()
                val response = okHttpClient.newCall(request).execute()

                if (response.isSuccessful) {
                    response.body?.let { body ->
                        val inputStream = body.byteStream()
                        val outputStream = FileOutputStream(file)

                        inputStream.use { input ->
                            outputStream.use { output ->
                                input.copyTo(output)
                            }
                        }

                        playAudio(file)
                    }
                } else {
                    playRandomAudio() // Play next audio on error
                }
            } catch (e: Exception) {
                e.printStackTrace()
                playRandomAudio() // Play next audio on error
            }
        }
    }

    private fun playAudio(file: File) {
        val uri = Uri.fromFile(file)
        val mediaItem = MediaItem.fromUri(uri)
        val playbackSpeed = Random.nextFloat() * (1.2f - 0.9f) + 0.9f // Random speed between 0.9 and 1.2

        exoPlayer.setMediaItem(mediaItem)
        exoPlayer.prepare()
        exoPlayer.playWhenReady = true
        exoPlayer.playbackParameters = PlaybackParameters(playbackSpeed)
    }

    override fun onCleared() {
        super.onCleared()
        exoPlayer.release()
    }
}




UI层

接下来,为 UI 层创建 Compose 组件,以控制播放和停止。

MainActivity.kt



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()
}




解释

ViewModel

  1. 依赖注入:通过构造函数注入 Application 以获取 context。
  2. ExoPlayer 实例:在 AudioPlayerViewModel 中初始化 ExoPlayer 实例。
  3. OkHttp 客户端:在 AudioPlayerViewModel 中初始化 OkHttpClient 实例。
  4. 播放逻辑:
  • play 方法:调用 playRandomAudio 随机播放一首音频。
  • playRandomAudio 方法:随机选择一个 URL,检查缓存文件是否已存在且有效(文件大小一致)。如果缓存有效,则直接播放本地文件;否则下载并缓存,然后播放。如果播放出现错误,则随机播放下一首。
  • isCacheValid 方法:发送 HEAD 请求获取远程文件大小,并与本地文件大小进行比较,以验证缓存是否有效。
  • downloadAndPlay 方法:如果缓存无效,下载音频文件并缓存到指定文件夹中,然后播放音频。如果下载出现错误,则随机播放下一首。
  • playAudio 方法:通过 ExoPlayer 播放本地文件,并设置随机播放速度(0.9 到 1.2 之间)。
  1. ExoPlayer 事件监听:
  • onPlaybackStateChanged 方法:监听播放状态,当播放结束时,随机播放下一首音频。
  • onPlayerError 方法:监听播放错误,当发生错误时,随机播放下一首音频。

UI层

  1. Compose 布局:通过 Column 布局组织各个 UI 组件,包括播放和停止按钮。
  2. 按钮操作:
  • Play Audio 按钮:调用 audioPlayerViewModel.play() 开始播放音频。
  • Stop Audio 按钮:调用 audioPlayerViewModel.stop() 停止音频播放。

通过以上代码,你可以随机播放远程 URL 列表中的音频文件,并使用 OkHttp 下载和缓存音频文件。如果文件已经下载且大小一致,则直接从缓存中播放,避免重复下载。用户界面使用 Jetpack Compose 构建,并提供播放和停止按钮以控制音频播放,同时处理播放完成或出现错误时自动随机播放下一首音频,并设置随机播放速度。





相关文章
|
11月前
|
缓存 Android开发 iOS开发
Kotlin跨平台Compose Multiplatform实战指南
Kotlin Multiplatform (KMP) 结合 Compose Multiplatform,助力开发者用一套代码构建跨平台应用(Android、iOS、桌面和 Web)。本文提供实战指南,涵盖环境搭建、项目结构、共享 UI 编写、平台适配、状态管理及资源处理等内容。通过 expect/actual 处理差异,借助官方文档与示例项目学习,减少重复代码,优化多平台开发体验。
2596 18
|
10月前
|
安全 Java Android开发
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
435 0
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
|
存储 前端开发 测试技术
Kotlin教程笔记-使用Kotlin + JetPack 对旧项目进行MVVM改造
Kotlin教程笔记-使用Kotlin + JetPack 对旧项目进行MVVM改造
|
测试技术 数据库 Android开发
深入解析Android架构组件——Jetpack的使用与实践
本文旨在探讨谷歌推出的Android架构组件——Jetpack,在现代Android开发中的应用。Jetpack作为一系列库和工具的集合,旨在帮助开发者更轻松地编写出健壮、可维护且性能优异的应用。通过详细解析各个组件如Lifecycle、ViewModel、LiveData等,我们将了解其原理和使用场景,并结合实例展示如何在实际项目中应用这些组件,提升开发效率和应用质量。
517 6
|
安全 Java Android开发
探索安卓应用开发的新趋势:Kotlin和Jetpack Compose
在安卓应用开发领域,随着技术的不断进步,新的编程语言和框架层出不穷。Kotlin作为一种现代的编程语言,因其简洁性和高效性正逐渐取代Java成为安卓开发的首选语言。同时,Jetpack Compose作为一个新的UI工具包,提供了一种声明式的UI设计方法,使得界面编写更加直观和灵活。本文将深入探讨Kotlin和Jetpack Compose的特点、优势以及如何结合使用它们来构建现代化的安卓应用。
572 11
|
编译器 Android开发 开发者
带你了解Android Jetpack库中的依赖注入框架:Hilt
本文介绍了Hilt,这是Google为Android开发的依赖注入框架,基于Dagger构建,旨在简化依赖注入过程。Hilt通过自动化的组件和注解减少了DI的样板代码,提高了应用的可测试性和可维护性。文章详细讲解了Hilt的主要概念、基本用法及原理,帮助开发者更好地理解和应用Hilt。
888 8
|
XML Android开发 UED
"掌握安卓开发新境界:深度解析AndroidManifest.xml中的Intent-filter配置,让你的App轻松响应scheme_url,开启无限交互可能!"
【8月更文挑战第2天】在安卓开发中,scheme_url 通过在`AndroidManifest.xml`中配置`Intent-filter`,使应用能响应特定URL启动或执行操作。基本配置下,应用可通过定义特定URL模式的`Intent-filter`响应相应链接。
480 12
|
存储 数据库 Android开发
🔥Android Jetpack全解析!拥抱Google官方库,让你的开发之旅更加顺畅无阻!🚀
【7月更文挑战第28天】在Android开发中追求高效稳定的路径?Android Jetpack作为Google官方库集合,是你的理想选择。它包含多个独立又协同工作的库,覆盖UI到安全性等多个领域,旨在减少样板代码,提高开发效率与应用质量。Jetpack核心组件如LiveData、ViewModel、Room等简化了数据绑定、状态保存及数据库操作。引入Jetpack只需在`build.gradle`中添加依赖。例如,使用Room进行数据库操作变得异常简单,从定义实体到实现CRUD操作,一切尽在掌握之中。拥抱Jetpack,提升开发效率,构建高质量应用!
611 4
|
Shell Android开发
安卓scheme_url调端:在AndroidManifest.xml 中如何配置 Intent-filter?
为了使Android应用响应vivo和oppo浏览器的Deep Link或自定义scheme调用,需在`AndroidManifest.xml`中配置`intent-filter`。定义启动的Activity及其支持的scheme和host,并确保Activity可由外部应用启动。示例展示了如何配置HTTP/HTTPS及自定义scheme,以及如何通过浏览器和adb命令进行测试,确保配置正确无误。
|
Java Android开发 UED
安卓scheme_url调端:如果手机上多个app都注册了 http或者https 的 intent。 调端的时候,调起哪个app呢?
当多个Android应用注册了相同的URL Scheme(如http或https)时,系统会在尝试打开这类链接时展示一个选择对话框,让用户挑选偏好应用。若用户选择“始终”使用某个应用,则后续相同链接将直接由该应用处理,无需再次选择。本文以App A与App B为例,展示了如何在`AndroidManifest.xml`中配置对http与https的支持,并提供了从其他应用发起调用的示例代码。此外,还讨论了如何在系统设置中管理这些默认应用选择,以及建议开发者为避免冲突应注册更独特的Scheme。