需求描述:
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 false } val request = Request.Builder().url(url).head().build() val response = okHttpClient.newCall(request).execute() if (!response.isSuccessful) { return false } val remoteFileSize = response.header("Content-Length")?.toLongOrNull() ?: return 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() } } } 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") } } } ) } showBackground = true) ( fun DefaultPreview() { MyApp() }
解释
ViewModel
- 依赖注入:通过构造函数注入 Application 以获取 context。
- ExoPlayer 实例:在 AudioPlayerViewModel 中初始化 ExoPlayer 实例。
- OkHttp 客户端:在 AudioPlayerViewModel 中初始化 OkHttpClient 实例。
- 播放逻辑:
- play 方法:调用 playRandomAudio 随机播放一首音频。
- playRandomAudio 方法:随机选择一个 URL,检查缓存文件是否已存在且有效(文件大小一致)。如果缓存有效,则直接播放本地文件;否则下载并缓存,然后播放。如果播放出现错误,则随机播放下一首。
- isCacheValid 方法:发送 HEAD 请求获取远程文件大小,并与本地文件大小进行比较,以验证缓存是否有效。
- downloadAndPlay 方法:如果缓存无效,下载音频文件并缓存到指定文件夹中,然后播放音频。如果下载出现错误,则随机播放下一首。
- playAudio 方法:通过 ExoPlayer 播放本地文件,并设置随机播放速度(0.9 到 1.2 之间)。
- ExoPlayer 事件监听:
- onPlaybackStateChanged 方法:监听播放状态,当播放结束时,随机播放下一首音频。
- onPlayerError 方法:监听播放错误,当发生错误时,随机播放下一首音频。
UI层
- Compose 布局:通过 Column 布局组织各个 UI 组件,包括播放和停止按钮。
- 按钮操作:
- Play Audio 按钮:调用 audioPlayerViewModel.play() 开始播放音频。
- Stop Audio 按钮:调用 audioPlayerViewModel.stop() 停止音频播放。
通过以上代码,你可以随机播放远程 URL 列表中的音频文件,并使用 OkHttp 下载和缓存音频文件。如果文件已经下载且大小一致,则直接从缓存中播放,避免重复下载。用户界面使用 Jetpack Compose 构建,并提供播放和停止按钮以控制音频播放,同时处理播放完成或出现错误时自动随机播放下一首音频,并设置随机播放速度。