安卓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 构建,并提供播放和停止按钮以控制音频播放,同时处理播放完成或出现错误时自动随机播放下一首音频,并设置随机播放速度。





相关文章
|
缓存 Java Maven
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
772 1
|
安全 Java Android开发
探索安卓应用开发的新趋势:Kotlin和Jetpack Compose
在安卓应用开发领域,随着技术的不断进步,新的编程语言和框架层出不穷。Kotlin作为一种现代的编程语言,因其简洁性和高效性正逐渐取代Java成为安卓开发的首选语言。同时,Jetpack Compose作为一个新的UI工具包,提供了一种声明式的UI设计方法,使得界面编写更加直观和灵活。本文将深入探讨Kotlin和Jetpack Compose的特点、优势以及如何结合使用它们来构建现代化的安卓应用。
574 11
|
XML Android开发 UED
"掌握安卓开发新境界:深度解析AndroidManifest.xml中的Intent-filter配置,让你的App轻松响应scheme_url,开启无限交互可能!"
【8月更文挑战第2天】在安卓开发中,scheme_url 通过在`AndroidManifest.xml`中配置`Intent-filter`,使应用能响应特定URL启动或执行操作。基本配置下,应用可通过定义特定URL模式的`Intent-filter`响应相应链接。
482 12
|
Android开发
Android 利用MediaPlayer实现音乐播放
本文提供了一个简单的Android MediaPlayer音乐播放示例,包括创建PlayerActivity、配置AndroidManifest.xml和activity_player.xml布局,以及实现播放和暂停功能的代码。
407 0
Android 利用MediaPlayer实现音乐播放
|
编解码 网络协议 开发工具
Android平台如何实现多路低延迟RTSP|RTMP播放?
本文档详细介绍了大牛直播SDK在Android平台上实现RTSP与RTMP流媒体播放及录像功能的技术细节。早在2015年,SDK的第一版就已经支持了多实例播放,并且通过简单的实例封装就能轻松实现。文档中提供了代码示例,展示了如何开启播放、停止播放以及开始和停止录像等功能。此外,SDK还提供了丰富的配置选项,例如设置录像目录、文件大小限制、转码选项等。总结部分列出了该SDK的关键特性,包括但不限于高稳定性和低延迟的播放能力、多实例支持、事件回调、硬解码支持、网络状态监控以及复杂的网络环境处理等。这些功能使得SDK能够应对各种应用场景,特别是在对延迟和稳定性有极高要求的情况下表现优异。
522 5
|
编解码 网络协议 vr&ar
Android平台下VR头显如何低延迟播放4K以上超高分辨率RTSP|RTMP流
这段内容讲述了VR头显中实现高分辨率视频播放的技术背景与实现方法,并强调了其重要性。高分辨率对于提升VR体验至关重要,它能提供更清晰的画面、增强沉浸感、补偿透镜放大效应,并维持宽广视场角下的图像质量。文中提到的大牛直播SDK具备极低的延迟(200-400ms),支持多种协议与格式,并具有丰富的功能特性,如多实例播放、事件回调、视频及音频格式支持等。此外,提供了基于Unity的播放器示例代码,展示了如何配置播放参数并开始播放。最后,作者指出此类技术在远程控制、虚拟仿真等应用场景中的重要意义。
323 3
|
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。
|
存储 移动开发 Android开发
使用kotlin Jetpack Compose框架开发安卓app, webview中h5如何访问手机存储上传文件
在Kotlin和Jetpack Compose中,集成WebView以支持HTML5页面访问手机存储及上传音频文件涉及关键步骤:1) 添加`READ_EXTERNAL_STORAGE`和`WRITE_EXTERNAL_STORAGE`权限,考虑Android 11的分区存储;2) 配置WebView允许JavaScript和文件访问,启用`javaScriptEnabled`、`allowFileAccess`等设置;3) HTML5页面使用`<input type="file">`让用户选择文件,利用File API;
|
域名解析 存储 缓存
HTTP请求流程概览:浏览器构建请求行含方法、URL和版本;检查缓存;解析IP与端口
【6月更文挑战第23天】 HTTP请求流程概览:浏览器构建请求行含方法、URL和版本;检查缓存;解析IP与端口;TCP连接(HTTP/1.1可能需排队);三次握手;发送请求头与体;服务器处理并返回响应;TCP连接可能关闭或保持;浏览器接收并显示响应,更新缓存。HTTP版本间有差异。
492 5