You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ExoPlayer TimelineQueueNavigator.getMediaDescription索引错误致崩溃排查

问题:TimelineQueueNavigator的windowIndex越界触发IndexOutOfBoundsException崩溃

开发音乐播放器时,TimelineQueueNavigator的getMediaDescription方法中传入的windowIndex参数会出现超出本地歌曲列表长度的情况(比如列表仅3个条目却返回索引3),直接导致数组越界崩溃。

错误日志

2022-12-31 14:12:40.844 15041-15041 AndroidRuntime          com.example.spotifyclone             D  Shutting down VM
2022-12-31 14:12:40.861 15041-15041 AndroidRuntime          com.example.spotifyclone             E  FATAL EXCEPTION: main
                                                                                                    Process: com.example.spotifyclone, PID: 15041
                                                                                                    java.lang.IndexOutOfBoundsException: Index: 3, Size: 3
                                                                                                        at java.util.ArrayList.get(ArrayList.java:437)
                                                                                                        at com.example.spotifyclone.exoplayer.MusicService$MusicQueueNavigator.getMediaDescription(MusicService.kt:120)
                                                                                                        at com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator.publishFloatingQueueWindow(TimelineQueueNavigator.java:207)
                                                                                                        at com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator.onTimelineChanged(TimelineQueueNavigator.java:127)
                                                                                                        at com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector$ComponentListener.onEvents(MediaSessionConnector.java:1146)
                                                                                                        at com.google.android.exoplayer2.ExoPlayerImpl.lambda$new$0$com-google-android-exoplayer2-ExoPlayerImpl(ExoPlayerImpl.java:265)
                                                                                                        at com.google.android.exoplayer2.ExoPlayerImpl$$ExternalSyntheticLambda11.invoke(Unknown Source:4)
                                                                                                        at com.google.android.exoplayer2.util.ListenerSet$ListenerHolder.iterationFinished(ListenerSet.java:293)
                                                                                                        at com.google.android.exoplayer2.util.ListenerSet.handleMessage(ListenerSet.java:244)
                                                                                                        at com.google.android.exoplayer2.util.ListenerSet.$r8$lambda$eEvjP-IE0x3J2lRvKfFbbjRFRvc(Unknown Source:0)
                                                                                                        at com.google.android.exoplayer2.util.ListenerSet$$ExternalSyntheticLambda0.handleMessage(Unknown Source:2)
                                                                                                        at android.os.Handler.dispatchMessage(Handler.java:102)
                                                                                                        at android.os.Looper.loopOnce(Looper.java:233)
                                                                                                        at android.os.Looper.loop(Looper.java:344)
                                                                                                        at android.app.ActivityThread.main(ActivityThread.java:8212)
                                                                                                        at java.lang.reflect.Method.invoke(Native Method)
                                                                                                        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:584)
                                                                                                        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1034)

关键代码片段

TimelineQueueNavigator实现

private inner class MusicQueueNavigator: TimelineQueueNavigator(mediaSession) {
    override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
        return firebaseMusicSource.songs[windowIndex].description
    }
}

MusicService核心逻辑

private fun preparePlayer(
    songs:List<MediaMetadataCompat>,
    itemToPlay:MediaMetadataCompat?,
    playNow: Boolean
){
    val curSongIndex = if(curPlayingSong == null) 0 else songs.indexOf(itemToPlay)

    exoPlayer.prepare()
    exoPlayer.addMediaSource(firebaseMusicSource.asMediaSource(dataSourceFactory))
    exoPlayer.seekTo(curSongIndex,0L)
    exoPlayer.playWhenReady =  playNow
    Log.d("MUSICSERVICE", "Player prepared ")
}

override fun onLoadChildren(
    parentId: String,
    result: Result<MutableList<MediaBrowserCompat.MediaItem>>
) {
    when(parentId){
        MEDIA_ROOT_ID -> {
            val resultSent:Boolean = firebaseMusicSource.whenReady { isInitialized ->
                if(isInitialized){
                    result.sendResult(firebaseMusicSource.asMediaItem())
                    if(!isPlayerPrepared && firebaseMusicSource.songs.isEmpty()){
                        preparePlayer( firebaseMusicSource.songs, firebaseMusicSource.songs[0], false )
                        isPlayerPrepared = true
                        Log.d("MUSICSERVICE", "Player prepared during loadChildren ")
                    }
                }else{
                    mediaSession.sendSessionEvent(NETWORK_ERROR, null)
                    result.sendResult(null)
                }
            }
            if(!resultSent){
                result.detach()
            }
        }
    }
}

问题根源分析

  1. 媒体源重复添加:preparePlayer方法每次调用都会执行addMediaSource但未清空现有媒体源,导致ExoPlayer的Timeline窗口数量远超过本地firebaseMusicSource.songs列表长度,最终windowIndex超出范围。
  2. 索引校验缺失:getMediaDescription直接使用传入的windowIndex访问列表,未做合法性校验,一旦索引越界就崩溃。
  3. 初始化时序问题:onCreate中异步加载数据的同时直接初始化播放器,可能导致播放器初始化时歌曲列表未加载完成,后续数据更新后索引不匹配。
  4. onLoadChildren逻辑错误:判断条件firebaseMusicSource.songs.isEmpty()写反,空列表时调用songs[0]会直接触发崩溃,逻辑上应该在列表非空时初始化播放器。

修复方案

1. 修复preparePlayer方法,避免重复添加媒体源

private fun preparePlayer(
    songs:List<MediaMetadataCompat>,
    itemToPlay:MediaMetadataCompat?,
    playNow: Boolean
){
    // 空列表直接返回,避免后续操作越界
    if (songs.isEmpty()) return
    
    // 计算合法的播放索引,避免indexOf返回-1的情况
    val curSongIndex = if(curPlayingSong == null) 0 else songs.indexOf(itemToPlay).takeIf { it != -1 } ?: 0

    // 先清空现有媒体源,保证Timeline与本地列表长度一致
    exoPlayer.clearMediaItems()
    exoPlayer.addMediaSource(firebaseMusicSource.asMediaSource(dataSourceFactory))
    exoPlayer.seekTo(curSongIndex, 0L)
    exoPlayer.playWhenReady = playNow
    // 最后执行prepare,确保媒体源加载完成
    exoPlayer.prepare()
    Log.d("MUSICSERVICE", "Player prepared ")
}

2. 为getMediaDescription添加索引安全校验

private inner class MusicQueueNavigator: TimelineQueueNavigator(mediaSession) {
    override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
        // 将索引限制在合法范围内
        val validIndex = windowIndex.coerceIn(0, firebaseMusicSource.songs.lastIndex)
        // 安全获取元素,避免空列表情况
        return firebaseMusicSource.songs.getOrElse(validIndex) { 
            firebaseMusicSource.songs.firstOrNull()?.description ?: MediaDescriptionCompat.Builder().build()
        }
    }
}

3. 修复onLoadChildren中的逻辑错误

override fun onLoadChildren(
    parentId: String,
    result: Result<MutableList<MediaBrowserCompat.MediaItem>>
) {
    when(parentId){
        MEDIA_ROOT_ID -> {
            val resultSent:Boolean = firebaseMusicSource.whenReady { isInitialized ->
                if(isInitialized){
                    result.sendResult(firebaseMusicSource.asMediaItem())
                    // 修改判断条件,仅在列表非空且未初始化播放器时执行
                    if(!isPlayerPrepared && firebaseMusicSource.songs.isNotEmpty()){
                        preparePlayer( firebaseMusicSource.songs, firebaseMusicSource.songs[0], false )
                        isPlayerPrepared = true
                        Log.d("MUSICSERVICE", "Player prepared during loadChildren ")
                    }
                }else{
                    mediaSession.sendSessionEvent(NETWORK_ERROR, null)
                    result.sendResult(null)
                }
            }
            if(!resultSent){
                result.detach()
            }
        }
    }
}

4. 调整初始化时序,确保数据加载完成后再初始化播放器

将媒体会话和播放器的初始化逻辑移到数据加载完成后:

override fun onCreate() {
    super.onCreate()

    serviceScope.launch {
        // 等待数据加载完成
        firebaseMusicSource.fetchMediaData()
        // 数据加载完成后再初始化媒体会话和播放器
        initMediaSessionAndPlayer()
    }
}

private fun initMediaSessionAndPlayer() {
    val activityIntent = packageManager?.getLaunchIntentForPackage(packageName)?.let {
        if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ) PendingIntent.getActivity(this, 0, it, PendingIntent.FLAG_MUTABLE)
        else PendingIntent.getActivity(this, 0, it, 0 )
    }

    mediaSession = MediaSessionCompat(this, SERVICE_TAG).apply {
        setSessionActivity(activityIntent)
        isActive = true
    }

    sessionToken = mediaSession.sessionToken

    musicNotificationManager = MusicNotificationManager(
        context= this,
        sessionToken = mediaSession.sessionToken,
        notificationListener= MusicPlayerNotificationListener(this),
        newSongCallbacks= {
            curSongDuration = exoPlayer.duration
        }
    )

    val musicPlaybackPreparer:MusicPlaybackPreparer = MusicPlaybackPreparer(firebaseMusicSource){
        curPlayingSong = it
        preparePlayer(
            firebaseMusicSource.songs,
            it,
            true
        )
        Log.d("MUSICSERVICE", "Player Playback prepared and also player prepared ")
    }

    mediaSessionConnector = MediaSessionConnector(mediaSession)
    mediaSessionConnector.setPlaybackPreparer(musicPlaybackPreparer)
    mediaSessionConnector.setPlayer(exoPlayer)
    mediaSessionConnector.setQueueNavigator(MusicQueueNavigator())

    musicPlayerEventListener = MusicPlayerEventListener(this)
    exoPlayer.addListener(musicPlayerEventListener)
    musicNotificationManager.showNotification(exoPlayer)
}

内容的提问来源于stack exchange,提问作者SmartphonE_Codes

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 05:56:04