ExoPlayer TimelineQueueNavigator.getMediaDescription索引错误致崩溃排查
开发音乐播放器时,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() } } } }
问题根源分析
- 媒体源重复添加:
preparePlayer方法每次调用都会执行addMediaSource但未清空现有媒体源,导致ExoPlayer的Timeline窗口数量远超过本地firebaseMusicSource.songs列表长度,最终windowIndex超出范围。 - 索引校验缺失:
getMediaDescription直接使用传入的windowIndex访问列表,未做合法性校验,一旦索引越界就崩溃。 - 初始化时序问题:
onCreate中异步加载数据的同时直接初始化播放器,可能导致播放器初始化时歌曲列表未加载完成,后续数据更新后索引不匹配。 - 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
相关产品推荐
相关产品推荐

