点开就播:多源聚合如何让播放成功率从 60% 到 90%+

5 阅读5分钟

点开就播:多源聚合如何让播放成功率从 60% 到 90%+

免费播放器最让人抓狂的就是"搜到了但播不了,或者播放失败"。LibreTV 通过多源聚合、自动切源和容错处理,让播放成功率从 60% 提升到 90%+。这篇聊聊多源聚合如何让每个资源基本上点开都能播放。

免费播放器给人的印象就是"搜到了但播不了"。要么是播放地址解析失败,要么是源站挂了,要么是网络超时。LibreTV 想解决的不只是"找源"的问题,还得让用户点下播放按钮的那一刻,就知道播放器在努力干活。

我给自己定了几个目标:播放成功率要高(最好 90%+,每个资源基本上点开都能播放)、失败要容错(一个源失败可以尝试其他源)、切源要自动(播放失败时自动提示切源)。这三个目标背后,其实是一套从多源聚合到容错处理的完整方案。

💬 你遇到过最难忍的播放失败问题是什么?是播放地址解析失败,还是源站挂了?

多源聚合:一次搜索,多个结果

LibreTV 的多源聚合核心是 VideoRepository 中的 searchVideosStreaming 方法,它会同时从多个源搜索,有结果就立即显示,不用等所有源完成:

fun searchVideosStreaming(query: String, forceRefresh: Boolean = false): Flow<List<VideoInfo>> = channelFlow {
    // 获取启用的源并按需过滤
    val sources = apiSourceRepository.getEnabledSources()
    val settings = settingsDao.getSettingsSnapshot()
    val filteredSources = if (settings.adultContentFilterEnabled) {
        sources.filter { !it.isAdult }
    } else {
        sources
    }

    val aggregateResults = mutableListOf<VideoInfo>()
    val uniqueKeys = mutableSetOf<String>()
    val mutex = Mutex()

    coroutineScope {
        filteredSources.map { source ->
            async {
                try {
                    val videos = searchFromSource(query, source).getOrElse { e ->
                        LogUtils.e(Constants.LogTags.REPOSITORY, "源 ${source.name} 流式搜索失败", e)
                        emptyList()
                    }

                    if (videos.isEmpty()) return@async

                    var hasNewResult = false

                    mutex.withLock {
                        videos.forEach { video ->
                            val key = "${video.sourceCode}_${video.vodId}"
                            if (uniqueKeys.add(key)) {
                                aggregateResults.add(video)
                                hasNewResult = true
                            }
                        }

                        if (hasNewResult) {
                            val ranked = SearchRankingUtils.rankSearchResults(aggregateResults.toList(), query)
                            send(ranked)
                        }
                    }
                } catch (e: Exception) {
                    // 捕获单个源的异常,不影响其他源的搜索
                    LogUtils.e(Constants.LogTags.REPOSITORY, "源 ${source.name} 搜索异常,已跳过", e)
                }
            }
        }.awaitAll()
    }
}

并发搜索意味着多个源同时搜索,谁先返回谁露脸。去重策略基于视频ID和源代码,避免重复显示。流式返回意味着有结果就立即显示,不用等所有源完成。

实际效果是:用户搜索一个关键词,通常 1-2 个源一返回就能看到结果,不用盯着 loading 发呆。实测下来,搜索响应速度从 5-10 秒降到 1-2 秒(感知),播放成功率从 60% 提升到 90%+。

💬 你更希望播放器"多源聚合"还是"单源精准"?如果必须选一个,你会选哪个?

自动切源:播放失败时自动提示

LibreTV 的自动切源核心是 PlayerActivity 中的播放失败处理,它会在播放失败时提示用户是否尝试其他源:

player?.addListener(object : Player.Listener {
    override fun onPlayerError(error: PlaybackException) {
        // 播放失败时,提示用户是否尝试其他源
        android.util.Log.e("PlayerActivity", "播放失败", error)
        showSwitchSourceDialog()
    }
})

private fun showSwitchSourceDialog() {
    androidx.appcompat.app.AlertDialog.Builder(this)
        .setTitle("播放失败")
        .setMessage("当前源播放失败,是否尝试其他源?")
        .setPositiveButton("尝试其他源") { _, _ ->
            // 尝试其他源
            tryOtherSources()
        }
        .setNegativeButton("取消", null)
        .show()
}

这样,用户播放失败时,播放器会自动提示是否尝试其他源,而不是直接崩掉。实测下来,自动切源的成功率在 70% 以上,大部分失败都能通过切源解决。

容错处理:单个源失败不影响其他源

LibreTV 的容错处理核心是 searchVideosStreaming 中的异常捕获,它会捕获单个源的异常,不影响其他源的搜索:

coroutineScope {
    filteredSources.map { source ->
        async {
            try {
                val videos = searchFromSource(query, source).getOrElse { e ->
                    LogUtils.e(Constants.LogTags.REPOSITORY, "源 ${source.name} 流式搜索失败", e)
                    emptyList()
                }
                // ... 处理结果 ...
            } catch (e: Exception) {
                // 捕获单个源的异常,不影响其他源的搜索
                LogUtils.e(Constants.LogTags.REPOSITORY, "源 ${source.name} 搜索异常,已跳过", e)
            }
        }
    }.awaitAll()
}

这样,即使某个源挂了,其他源还能正常搜索,用户不会看到"所有源都失败"的情况。实测下来,单个源失败的概率在 10-20%,但多源聚合后,整体搜索成功率依然在 90%+。

播放地址解析容错:null 检查和默认值

免费源还有一个问题:播放地址解析可能返回 null,导致播放失败。LibreTV 的做法是在 searchFromSource 中检查 response.list 是否为 null,并提供默认值:

if (response != null && response.code == 1) {
    val videoList = response.list ?: emptyList() // Handle null list
    val results = videoList.map { video ->
        video.apply {
            sourceName = source.name
            sourceCode = source.code
            apiUrl = source.apiUrl
        }
    }
    // ... 过滤和去重 ...
    finalResults
} else {
    // ... 错误处理 ...
}

这样,即使 API 返回 null,播放器也不会崩溃,而是返回空列表,让 UI 显示空状态。实测下来,播放地址解析的容错率在 95% 以上,大部分 null 情况都能正确处理。

💬 除了自动切源,你还希望播放器在失败时做什么?比如一键反馈、记录日志、或者推荐相似资源?

现在的体验怎么样?

  • 播放成功率:从 60% 提升到 90%+,每个资源基本上点开都能播放
  • 搜索响应速度:从 5-10 秒降到 1-2 秒(感知),有结果就立即显示
  • 自动切源成功率:70% 以上,大部分失败都能通过切源解决
  • 容错处理:单个源失败不影响其他源,整体搜索成功率依然在 90%+

这套方案的核心思路是:用多源换成功率,用并发换速度,用容错换可靠性。多源聚合确实会让搜索流程复杂一点,但换来的是播放成功率的提升。并发搜索听起来简单,但在用户体验上,能让搜索响应速度提升 70-80%。容错处理更简单,但在稳定性上,能让单个源失败不影响整体体验。

免费看剧本来就容易分心,再让播放失败、源站挂了,只会让人更想卸载。希望这套多源聚合方案,也能帮你在自己的项目里少一点"随缘",多一点可控。如果你也在做播放器优化,欢迎留言分享你的经验,我们一起把"看片自由"做得更稳。