基于Media3的本地音频播放器初体验

header-Android - Media3 is ready to play.png

最近想用Media3+Exoplayer写一个音频播放器练练手,网上翻了翻资料,相关内容比较少,有也基本是播放个音频没有后台服务的,不过最后还是整出来了,目前只实现了播放本地音乐

第一步就是添加依赖了

// Media3
implementation("androidx.media3:media3-exoplayer:1.0.1")
implementation("androidx.media3:media3-ui:1.0.1")
implementation("androidx.media3:media3-session:1.0.1")

第二步,创建一个服务对象,在这里实现player的初始化

class MusicPlayerService: MediaLibraryService() {

    lateinit var player: Player
    lateinit var session: MediaLibrarySession
    private val PLAYBACK_CHANNEL_ID = "playback_channel"
    private val PLAYBACK_NOTIFICATION_ID = 1
    private lateinit var playerNotificationManager: PlayerNotificationManager

    override fun  onCreate() {
        super.onCreate()

        player = ExoPlayer.Builder(applicationContext)
            .setAudioAttributes(AudioAttributes.DEFAULT, true) // 自动处理音频焦点
            .setHandleAudioBecomingNoisy(true) // 自动暂停播放
            .setRenderersFactory(
                DefaultRenderersFactory(this).setExtensionRendererMode(
                    DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER /* We prefer extensions, such as FFmpeg */
                )
            )
            .build()

        session = MediaLibrarySession.Builder(this, player,
            object: MediaLibrarySession.Callback {
                override fun onAddMediaItems(
                    mediaSession: MediaSession,
                    controller: MediaSession.ControllerInfo,
                    mediaItems: MutableList<MediaItem>
                ): ListenableFuture<MutableList<MediaItem>> {
                    val updatedMediaItems = mediaItems.map { it.buildUpon().setUri(it.mediaId).build() }.toMutableList()
                    return Futures.immediateFuture(updatedMediaItems)
                }
            }).build()

        initNotification()
    }

    private fun initNotification() {
        playerNotificationManager = PlayerNotificationManager.Builder(applicationContext,
            PLAYBACK_NOTIFICATION_ID, PLAYBACK_CHANNEL_ID)
            .build()
        // 这里可以对通知栏进行更多设置
        playerNotificationManager.setPlayer(player)
    }

    override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? {
        return session
    }

    override fun onDestroy() {
        session.release()
        playerNotificationManager.setPlayer(null)
        player.release()
        super.onDestroy()
    }
}

注:需要在Manifest里配置<service>

<service
    android:name=".MusicPlayerService"
    android:enabled="true"
    android:exported="true"
    android:foregroundServiceType="mediaPlayback">
    <intent-filter>
        <action android:name="androidx.media3.session.MediaSessionService" />
    </intent-filter>
</service>

第三步,在主界面获取这个player对象

val sessionToken = SessionToken(applicationContext, ComponentName(this, MusicPlayerService::class.java))
val mediaControllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
mediaControllerFuture.addListener({
    player = mediaControllerFuture.get()
}, MoreExecutors.directExecutor())

第四步写一个工具类,用于获取本地的音频内容 先申明权限:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

注:权限需要自己去动态申请的哈

工具类MetadataReaderUtils

data class MusicData(
    val id: Int = 0,
    val name: String? = null,
    val singer: String? = null,
    val album: Bitmap? = null,
    val path: String? = null,
    val duration: Long = 0,
    val size: Long = 0,
    val uri: Uri
)

object MetadataReaderUtils {

    fun getMusicDataList(context: Context): List<MusicData> {
        val list = mutableListOf<MusicData>()
        val data = context.contentResolver.query(
            MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
            null, null, null,
            MediaStore.Audio.Media.IS_MUSIC
        )?.use { cursor ->
            while (cursor.moveToNext()) {
                val id = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID))
                val name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME))
                val singer = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST))
                val albumId = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID))
                val path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA))
                val duration = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION))
                val size = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE))
                val uri = Uri.withAppendedPath(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id)

                val musicData = MusicData(
                    id.toInt(), name, singer, getAlbumArt(context, albumId), path, duration.toLong(), size.toLong(), uri
                )
                list.add(musicData)
            }
            cursor.close()
        }
        return list
    }

    fun getAlbumArt(context: Context, albumId: Long): Bitmap? {
        val contentUri = ContentUris.withAppendedId(
            MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
            albumId
        )
        return try {
            context.contentResolver.loadThumbnail(
                contentUri, Size(640, 480), null)
        } catch (e: Exception) {
            return null
        }
    }
}

最后在主界面写个列表加载就完事了,这里列表的布局和适配器代码就不贴了,下面是主界面完整代码:

class PlayerActivity : AppCompatActivity() {

    private lateinit var binding: ActivityPlayerBinding
    lateinit var player: Player

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityPlayerBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val musicDataList = MetadataReaderUtils.getMusicDataList(this)

        val sessionToken =
            SessionToken(applicationContext, ComponentName(this, MusicPlayerService::class.java))
        val mediaControllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
        mediaControllerFuture.addListener({
            // 注意这个返回player对象是需要一定时间的
            player = mediaControllerFuture.get()
            musicDataList.forEach {
                addMediaItem(it.uri)
            }
            val dataAdapter = MusicDataAdapter(musicDataList)
            binding.recyclerView.adapter = dataAdapter
            dataAdapter.setOnItemClickListener(object : MusicDataAdapter.OnItemClickListener {
                override fun onItemCLick(musicData: MusicData, position: Int) {
                    // 播放列表指定歌曲
                    player.seekTo(position, 0)
                    player.prepare()
                    player.play()
                }
            })
        }, MoreExecutors.directExecutor())
    }

    fun addMediaItem(uri: Uri) {
        val newItem = MediaItem.Builder()
            .setMediaId("$uri")
            .build()
        player.addMediaItem(newItem)
    }
}

最终效果:

S30620-16375830.gif

其实对这个Media还是一知半解,在线音乐不知道怎么处理,直接用player播放是可以的,但是加了这个后台服务就播放不了了,路过的大佬有懂的评论评论,大家多多交流交流[旺柴]

全部评论

相关推荐

05-11 11:48
河南大学 Java
程序员牛肉:我是26届的双非。目前有两段实习经历,大三上去的美团,现在来字节了,做的是国际电商的营销业务。希望我的经历对你有用。 1.好好做你的CSDN,最好是直接转微信公众号。因为这本质上是一个很好的展示自己技术热情的证据。我当时也是烂大街项目(网盘+鱼皮的一个项目)+零实习去面试美团,但是当时我的CSDN阅读量超百万,微信公众号阅读量40万。面试的时候面试官就告诉我说觉得我对技术挺有激情的。可以看看我主页的美团面试面经。 因此花点时间好好做这个知识分享,最好是单拉出来搞一个板块。各大公司都极其看中知识落地的能力。 可以看看我的简历对于博客的描述。这个帖子里面有:https://www.nowcoder.com/discuss/745348200596324352?sourceSSR=users 2.实习经历有一些东西删除了,目前看来你的产出其实很少。有些内容其实很扯淡,最好不要保留。有一些点你可能觉得很牛逼,但是面试官眼里是减分的。 你还能负责数据库表的设计?这个公司得垃圾成啥样子,才能让一个实习生介入数据库表的设计,不要写这种东西。 一个公司的财务审批系统应该是很稳定的吧?为什么你去了才有RBAC权限设计?那这个公司之前是怎么处理权限分离的?这些东西看着都有点扯淡了。 还有就是使用Redis实现轻量级的消息队列?那为什么这一块不使用专业的MQ呢?为什么要使用redis,这些一定要清楚, 就目前看来,其实你的这个实习技术还不错。不要太焦虑。就是有一些内容有点虚了。可以考虑从PR中再投一点产出
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务