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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 03:10:16