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

最近想用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)
    }
}

终究效果:

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