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 } }
二、修正视频分片时序与格式适配
- 时序问题:
ConcurrentSkipListSet会自动排序,打乱服务器发送的分片顺序,改用LinkedBlockingQueue严格保证FIFO顺序。 - 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) // 启用直播模式
四、兼容性与细节修复
- 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) } } }
- WebSocket正常关闭:用标准关闭方法替代
cancel(),避免服务器端异常:
private fun closeWebsocketConnection() { webSocketClient?.close(1000, "Closing connection") }
五、调试建议
- 在
read方法中添加日志,确认分片是否正常读取; - 用ffmpeg工具解析单个视频分片,确认格式为有效的Fragmented MP4(含moof+mdat结构);
- 检查服务器发送的Base64编码是否无截断、编码格式正确。
内容的提问来源于stack exchange,提问作者Anish Vahora
相关产品推荐
相关产品推荐

