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

Android中ExoPlayer通过WebSocket播放直播流卡顿/无法播放求助

问题分析与修复方案

核心问题定位

你的直播流播放卡在缓冲状态,主要问题集中在数据源读取逻辑的非阻塞设计、视频分片时序混乱、ExoPlayer直播模式适配缺失这几个方面,以下是具体修复步骤:


一、修复DataSource读取阻塞逻辑

当前read方法无数据时直接返回0,ExoPlayer会误判为流已结束或数据不足,需改为阻塞等待数据(ExoPlayer的DataSource读取在后台线程执行,阻塞不会影响主线程)。

1. 替换数据收集容器为阻塞队列

用LinkedBlockingQueue替代ConcurrentSkipListSet,保证分片时序(FIFO)且支持阻塞获取:

class WssDataStreamCollector : WebSocketListener() {
    companion object{
        private const val TAG="WssDataStreamCollector"
    }
    // 保证分片顺序,支持阻塞获取
    private val wssData = LinkedBlockingQueue<ByteString>()

    override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
        Log.e(TAG, "onMessage: byteString")
        wssData.offer(bytes)
    }

    override fun onMessage(webSocket: WebSocket, text: String) {
        val response: StreamResponse?=Gson().fromJson<StreamResponse>(text, StreamResponse::class.java)
        response?.let {
            val videoChunk: ByteString= UtilHelper.base64StringToByteString(it.dataChunk)!!
            wssData.offer(videoChunk)
            Log.e(TAG, "video_chunk receiver: ${it.timestamp}")
        }
    }

    override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
        super.onClosing(webSocket, code, reason)
        wssData.clear()
        Log.e("WssDataStreamCollector","connection Closed")
    }

    override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
        Log.e("onFailure","reason ${response?.toString() ?: "null"}")
        Log.e("onFailure","errMsg ${t.message ?: "unknown"}")
    }

    // 阻塞获取下一个分片,直到有数据或线程中断
    @Throws(InterruptedException::class)
    fun getNextStream(): ByteString {
        return wssData.take()
    }
}

2. 修改DataSource的read方法逻辑

override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
    try {
        // 阻塞等待下一个分片
        if (currentByteStream == null) {
            currentByteStream = dataStreamCollector.getNextStream().toByteArray()
            currentPosition = 0
            remainingBytes = currentByteStream?.size ?: 0
            Log.d("WssStreamDataSource", "New chunk received, size: ${currentByteStream?.size}")
        }

        val readSize = min(length, remainingBytes)
        currentByteStream?.copyInto(buffer, offset, currentPosition, currentPosition + readSize)
        currentPosition += readSize
        remainingBytes -= readSize

        Log.d("WssStreamDataSource", "Read $readSize bytes, remaining $remainingBytes in chunk")

        if (remainingBytes == 0) {
            currentByteStream = null
        }

        return readSize
    } catch (e: InterruptedException) {
        // 线程中断时返回结束标记
        return C.RESULT_END_OF_INPUT
    }
}

二、修正视频分片时序与格式适配

  1. 时序问题:ConcurrentSkipListSet会自动排序,打乱服务器发送的分片顺序,改用LinkedBlockingQueue严格保证FIFO顺序。
  2. Extractor配置:允许无moov头的分片(直播流通常第一个分片携带moov,后续分片仅含moof+mdat):
@UnstableApi
internal class CustomMediaExtractor : ExtractorsFactory {
    override fun createExtractors(): Array<Extractor> {
        return arrayOf(FragmentedMp4Extractor(FragmentedMp4Extractor.FLAG_ALLOW_FRAGMENTS_WITHOUT_MOOV))
    }
}

三、适配ExoPlayer直播模式

给ProgressiveMediaSource标记为直播流,让ExoPlayer采用直播专用的缓存和加载逻辑:

val factory = ProgressiveMediaSource.Factory(
    WssDSFactory(wsDataSource),
    CustomMediaExtractor()
).setIsLive(true) // 启用直播模式

四、兼容性与细节修复

  1. Base64解码兼容低版本:替换java.util.Base64为Android系统API,避免API26以下崩溃:
class UtilHelper {
    companion object {
        const val wsUrl = "wss://some_webstream_url"
        val ddMMYYYhhmmssa = SimpleDateFormat("dd-MM-yyyy hh:mm:ss a", Locale.US)

        fun base64StringToByteString(base64String: String): ByteString {
            val decodedBytes = android.util.Base64.decode(base64String, android.util.Base64.DEFAULT)
            return ByteString.of(*decodedBytes)
        }
    }
}
  1. WebSocket正常关闭:用标准关闭方法替代cancel(),避免服务器端异常:
private fun closeWebsocketConnection() {
    webSocketClient?.close(1000, "Closing connection")
}

五、调试建议

  • 在read方法中添加日志,确认分片是否正常读取;
  • 用ffmpeg工具解析单个视频分片,确认格式为有效的Fragmented MP4(含moof+mdat结构);
  • 检查服务器发送的Base64编码是否无截断、编码格式正确。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 19:33:09