音乐播放对接状态栏MediaSession控制

本文首发地址 https://h89.cn/archives/364.html

MediaPlayer播放音源

MediaPlayer可以播放视频,也可以播放视频,下面是一个使用MediaPlayer播放音频的示例

fun playMp3FromUrl(url: String) {
    val mediaPlayer = MediaPlayer()

    try {
        // 1. 设置数据源
        mediaPlayer.setDataSource(url)

        // 2. 异步准备播放器
        mediaPlayer.prepareAsync()

        // 3. 监听准备完成事件
        mediaPlayer.setOnPreparedListener { mp ->
            // 4. 准备完成后开始播放
            mp.start()
        }

        // 5. 监听播放错误
        mediaPlayer.setOnErrorListener { mp, what, extra ->
            // 处理错误,例如网络问题、文件不存在等
            println("播放错误:what=$what, extra=$extra")
            false // 返回 false 表示你没有处理完错误,让系统继续处理
        }

        //6. 监听播放结束
        mediaPlayer.setOnCompletionListener { mp->
                // 播放结束
                println("播放完成")
        }
    } catch (e: IOException) {
        // 处理 IOException,例如无效的 URL
        println("IO 错误:${e.message}")
    }
}

如果要想实现在通知栏显示以及控制播放,如下图,我们得自己实现自定义通知栏,处理控制事件

如果我们想让下拉状态栏,可以控制音频播放,实现下图效果

我们就需要对接 MediaSession,可以参考如下代码实现

private lateinit var mediaSession: MediaSession

private fun setupMediaSession(context:Context) {
    mediaSession = MediaSession(this, "MusicService")
    mediaSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS or MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS)
    val audioAttributes = AudioAttributes.Builder()
        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        .setUsage(AudioAttributes.USAGE_MEDIA)
        .build()
    mediaSession.setAudioAttributes(audioAttributes)

    mediaSession.setMediaControllerCallback(object : MediaSession.Callback() {
        override fun onPlay() {
            // 处理播放逻辑
            Toast.makeText(context, "Play action", Toast.LENGTH_SHORT).show()
        }

        override fun onPause() {
            // 处理暂停逻辑
            Toast.makeText(context, "Pause action", Toast.LENGTH_SHORT).show()
        }

        override fun onSkipToNext() {
            // 处理下一首逻辑
            Toast.makeText(context, "Next song action", Toast.LENGTH_SHORT).show()
        }

        override fun onSkipToPrevious() {
            // 处理上一首逻辑
            Toast.makeText(context, "Previous song action", Toast.LENGTH_SHORT).show()
        }
    })

    val mediaMetadata = MediaMetadata.Builder()
        .putString(MediaMetadata.METADATA_KEY_TITLE, "Song Title")
        .putString(MediaMetadata.METADATA_KEY_ARTIST, "Artist Name")
        .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, null) // 替换为实际专辑封面
        .build()
    mediaSession.setMetadata(mediaMetadata)
    mediaSession.isActive = true
}

是否有其他简单的方案?
是的,使用 androidx.media3 库中的 ExoPlayerMediaSession 能够更方便地对接系统通知栏和状态栏的播放控制。

使用ExoPlayer播放音频

ExoPlayerGoogle 官方推荐的媒体播放库,它比 MediaPlayer 功能更强大且更灵活。
ExoPlayer 已经被集成到 androidx.media3 库中,方便开发者使用。下面是使用示例:
依赖仓库如下

implementation("androidx.media3:media3-exoplayer:1.6.0")
implementation("androidx.media3:media3-session:1.6.0")
implementation("androidx.media3:media3-ui:1.6.0")

播放代码

fun playMp3WithExoPlayer(context: Context, mp3Url: String) {
    // 1. 创建 ExoPlayer 实例
    val player = ExoPlayer.Builder(context).build()

    // 2. 创建 MediaItem
    val mediaItem = MediaItem.fromUri(Uri.parse(mp3Url))

    // 3. 将 MediaItem 添加到播放器
    player.setMediaItem(mediaItem)

    // 4. 准备播放器
    player.prepare()

    // 5. 开始播放
    player.play()

    // 在不需要时,释放播放器
    // player.release()
}

如何对接系统通知栏和状态栏的播放控制,需要如下修改,设置MediaMetadata,并使用PlayerNotificationManager

// 实现 MediaLibrarySessionCallback
private class MediaLibrarySessionCallback : MediaLibrarySession.Callback {
    override fun onAddMediaItems(
        mediaLibrarySession: MediaLibrarySession,
        controller: MediaSession.ControllerInfo,
        mediaItems: MutableList<MediaItem>
    ): ListenableFuture<List<MediaItem>> {
        // val updatedMediaItems = mediaItems.map { it.buildUpon().setUri(it.requestMetadata.mediaUri!!).build() }
        return Futures.immediateFuture(mediaItems)
    }
}

//初始化 mediaSession
mediaSession = MediaLibrarySession.Builder( this, player, MediaLibrarySessionCallback()).build()

private fun createPendingIntent(): PendingIntent {
    val notificationIntent = Intent(this, HomeActivity::class.java)
    val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)
    } else {
        PendingIntent.getActivity(
            this, 0, notificationIntent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
        )
    }
    return pendingIntent
}

// 设置通知栏 使用 ExoPlayer 播放
notificationManager = PlayerNotificationManager.Builder(this, NOTIFICATION_ID, CHANNEL_ID)
    .setMediaDescriptionAdapter(DefaultMediaDescriptionAdapter(createPendingIntent()))
    .setNotificationListener(object : PlayerNotificationManager.NotificationListener {
        override fun onNotificationPosted(
            notificationId: Int, notification: Notification, ongoing: Boolean
        ) {
            Log.d("onNotificationPosted:,ongoing:$ongoing,notification:$notification")
            startForeground(notificationId, notification)
        }

        override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) {
            Log.d("onNotificationCancelled: ")
        }
    }).build()
notificationManager?.setSmallIcon(R.mipmap.icon_default_logo)
notificationManager?.setMediaSessionToken(mediaSession.platformToken)
notificationManager?.setPlayer(player)

// 设置MediaMetadata,以便状态栏和通知栏播放控制器使用
// 你的媒体数据类,需要包含 name, content, coverImage, audioUrl, id 等属性
val metaData = MediaMetadata.Builder().setTitle(item.name).setArtist(item.content)
    .setArtworkUri(item.coverImage?.toUri()).build()
var mediaItem = MediaItem.Builder()
    .setMediaMetadata(metaData).setUri(item.audioUrl)
    .build()
player.addMediaItem(mediaItem)

可以通过如下方式播放列表指定的索引
player.seekTo(index, 0)

使用 ExoPlayer 相对 MediaPlayer 简单了许多,因为它提供了更高级别的 API 来处理媒体会话和通知栏控制。

Exopler 缓存配置

播放音频时,我们可以配置缓存,可以在一定程度减少等待时间,减少服务器带宽压力

@OptIn(UnstableApi::class)
private val cacheDataSourceFactory by lazy {
    val cacheFolder = File(MyApplication.getInstance().cacheDir, "media_cache")
    val databaseProvider = StandaloneDatabaseProvider(MyApplication.getInstance())
    val lru = LeastRecentlyUsedCacheEvictor(200 * 1024 * 1024)
    val simpleCache = SimpleCache(cacheFolder, lru, databaseProvider)

    val httpDataSourceFactory = DefaultHttpDataSource.Factory()
    CacheDataSource.Factory().setCache(simpleCache).setUpstreamDataSourceFactory(httpDataSourceFactory)
        .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
}

val mediaSource = ProgressiveMediaSource.Factory(cacheDataSourceFactory).createMediaSource(mediaItem)
// 列表需要替换 addMediaItem 使用 addMediaSource 
player.setMediaSource(mediaSource)

总结

本文介绍了在 Android 应用中对接音视频播放与系统状态栏 MediaSession 控制的两种主要方式:使用 MediaPlayer 和使用 ExoPlayer

  • MediaPlayer: 虽然 MediaPlayer 可以实现基本的音频播放,但要对接系统通知栏和状态栏的播放控制,需要开发者手动创建和管理 MediaSessionCompat 以及自定义通知栏,处理各种播放控制事件,实现较为复杂。

  • ExoPlayer: ExoPlayer 是 Google 官方推荐的更强大、更灵活的媒体播放库,并且与 androidx.media3 库中的 MediaSessionPlayerNotificationManager 集成得更好。使用 ExoPlayerPlayerNotificationManager 可以更简洁地实现状态栏和通知栏的播放控制,ExoPlayer 会自动处理大部分与系统媒体控制的交互。


参考文章
https://developer.android.com/media/media3/exoplayer


本文链接:音乐播放对接状态栏MediaSession控制 - https://h89.cn/archives/364.html

版权声明:原创文章 遵循 CC 4.0 BY-SA 版权协议,转载请附上原文链接和本声明。

标签: MediaSession, ExoPlayer, MediaPlayer, media3, MediaMetadata

添加新评论