ExoPlayer版本升级后UI阻塞、倒计时失效问题求助
ExoPlayer 2.18.2升级后倒计时失效、UI阻塞问题排查
我开发的APP逻辑为:通过ExoPlayer先播放广告音频,广告结束后立即启动HLS直播流,同时屏幕显示10秒倒计时文本。在ExoPlayer 2.12.3及以下版本运行正常,但升级至2.18.2后出现两个问题:
- 倒计时失效,无法正常显示
- 直播流启动时UI被阻塞,屏幕空白,约10秒后才显示播放器UI
尝试过在主线程启动CoroutineScope,无效果:
CoroutineScope(Dispatchers.Main).launch { audioServiceViewModel.playOrPauseLiveStream() }
核心相关代码
倒计时实现
val countDownTimer = object : CountDownTimer(10000, 1000) { override fun onTick(ms: Long) { try { if (ms.toInt() / 1000 == 1) binding.skipText.text = context?.getString(R.string.continue_automatically_one_second, (ms / 1000).toString()) else if (ms / 1000 > 0) binding.skipText.text = context?.getString(R.string.continue_automatically, (ms / 1000).toString()) } catch (e: Exception) { e.printStackTrace() } } override fun onFinish() { try { closeAdView() } catch (e: Exception) { e.printStackTrace() } } }
播放状态监听与触发逻辑
audioServiceViewModel.playbackState.observe(viewLifecycleOwner) { val animation = resources.getInteger(android.R.integer.config_shortAnimTime) if (it.state == PlaybackStateCompat.STATE_STOPPED) audioServiceViewModel.playOrPauseLiveStream() if (it.state == PlaybackStateCompat.STATE_PLAYING && audioServiceViewModel.streamType == StreamType.LIVE_STREAM) { binding.skipText.apply { alpha = 0f visibility = VISIBLE animate().alpha(1f).setDuration(animation.toLong()) .setListener(null) } if (!countDownTimerStarted) { countDownTimer?.start() countDownTimerStarted = true } } } fun playOrPauseLiveStream() { if (isPlaying) pause() else playLiveStream() } fun playLiveStream() { if (shouldDisplayAd()) playAd(result) val livePrefs = LiveSharedPreferences(prefs) var shouldContinue = true nowPlaying.value?.let { if (isPlaying) { livePrefs.getString(GlobalConstants.SELECTED_STREAM, "").observeForever { id -> shouldContinue = if (id == it.getString(MetadataKey.STREAM_ID)) { transportControls.play() false } else true } } } if (shouldContinue) operationQueue.add { mediaController.sendCommand( MediaControllerCommand.CMD_INIT_LIVE_STREAM, Bundle(), ResultReceiver(operationsHandler) ) } } fun playAd(ad: VastAd?) { operationQueue.add { val gson = Gson() val args = Bundle().apply { putString(MediaControllerCommand.ARG_AD_OBJECT, gson.toJson(ad)) } mediaController.sendCommand( MediaControllerCommand.CMD_INIT_AD_STREAM, args, ResultReceiver(operationsHandler) ) } }
ExoPlayer命令处理与媒体源准备
override fun onCommand( player: Player, command: String, extras: Bundle?, cb: ResultReceiver? ): Boolean { when (command) { MediaControllerCommand.CMD_INIT_LIVE_STREAM -> { return prepareLiveStream(extras) } MediaControllerCommand.CMD_INIT_AD_STREAM -> { return prepareAdStream(extras) } } return false } fun prepareAdStream(extras: Bundle?): Boolean { val adUrl = "some_vast_ad_url" val dataSourceFactory = DefaultHttpDataSource.Factory().setUserAgent(userAgent) val mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory) .createMediaSource(MediaItem.fromUri(Uri.parse(ad))) exoPlayer.apply { playWhenReady = true setMediaSource(mediaSource) prepare() } StreamMetadata.apply { type = StreamType.AD_STREAM } return true } fun prepareLiveStream(extras: Bundle?): Boolean { var streamUrl = "some_stream_url" val dataSourceFactory = DefaultHttpDataSource.Factory().setUserAgent(userAgent) val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(streamUrl)) exoPlayer.apply { playWhenReady = true setMediaSource(mediaSource) prepare() } StreamMetadata.apply { type = StreamType.LIVE_STREAM } return true }
问题排查与解决方案
1. UI阻塞原因与修复
ExoPlayer从2.16版本开始,prepare()默认同步执行媒体源解析操作,若HLS流解析耗时较长,会阻塞调用线程(若调用线程为主线程则直接导致UI卡顿空白)。
修复方法:改用异步准备方法prepareAsync()
修改prepareAdStream和prepareLiveStream中的代码:
exoPlayer.apply { playWhenReady = true setMediaSource(mediaSource) prepareAsync() // 替换原有的prepare() }
2. 倒计时失效原因与修复
原逻辑依赖PlaybackStateCompat.STATE_PLAYING启动倒计时,但升级后prepareAsync()会让播放器先进入STATE_BUFFERING状态,HLS流缓冲耗时可能导致倒计时启动延迟;另外countDownTimerStarted的单次标记逻辑可能因状态触发时机变化失效。
优化方案:
- 直接监听ExoPlayer内部的
Player.Listener,精准捕获播放状态变化 - 广告播放结束后立即启动倒计时,无需等待直播流进入播放状态
- 确保倒计时UI操作在主线程执行
修改播放状态监听逻辑:
// 替换原有的playbackState.observe逻辑 exoPlayer.addListener(object : Player.Listener { override fun onPlaybackStateChanged(playbackState: Int) { when(playbackState) { Player.STATE_ENDED -> { // 广告播放结束,启动直播流并立即显示倒计时 audioServiceViewModel.playLiveStream() if (!countDownTimerStarted) { countDownTimer.start() countDownTimerStarted = true // 主线程更新UI binding.skipText.apply { alpha = 0f visibility = VISIBLE animate().alpha(1f) .setDuration(resources.getInteger(android.R.integer.config_shortAnimTime).toLong()) .setListener(null) } } } } } })
3. 强制UI操作在主线程
虽然CountDownTimer回调默认在主线程,但为避免上下文异常,可强制主线程更新UI:
override fun onTick(ms: Long) { try { Handler(Looper.getMainLooper()).post { val remainingSeconds = ms / 1000 binding.skipText.text = if (remainingSeconds == 1L) { context?.getString(R.string.continue_automatically_one_second, remainingSeconds.toString()) } else if (remainingSeconds > 0) { context?.getString(R.string.continue_automatically, remainingSeconds.toString()) } else { "" } } } catch (e: Exception) { e.printStackTrace() } }
内容的提问来源于stack exchange,提问作者Tenghan Jiang
相关产品推荐
相关产品推荐

