并发搜索速度提升 4-5 倍:5 个源并行,总时间≈最慢源

36 阅读4分钟

并发搜索速度提升 4-5 倍:5 个源并行,总时间≈最慢源

免费播放器最让人抓狂的就是"搜索要等所有源完成,才能看到结果"。LibreTV 通过并发搜索,5 个源并行,总时间≈最慢源,速度提升 4-5 倍。这篇聊聊并发搜索如何让搜索更快。

免费播放器给人的印象就是"搜索要等所有源完成,才能看到结果"。要么是串行搜索,总时间 = 所有源时间之和,要么是等待时间太长,要么是用户体验差。LibreTV 想解决的不只是"找源"的问题,还得让用户搜索的那一刻,就知道播放器在努力干活。

我给自己定了几个目标:搜索要快(5 个源并行,总时间≈最慢源)、结果要实时(有结果就立即显示,不用等所有源完成)、体验要稳(单个源失败不影响其他源)。这三个目标背后,其实是一套从 channelFlow 并发到流式返回的完整方案。

💬 你遇到过最难忍的搜索等待问题是什么?是等待时间太长,还是不知道什么时候有结果?

并发搜索:5 个源并行,总时间≈最慢源

LibreTV 的并发搜索核心是 VideoRepository 中的 searchVideosStreaming 方法,它使用 channelFlowasync 实现并发搜索:

fun searchVideosStreaming(query: String, forceRefresh: Boolean = false): Flow<List<VideoInfo>> = channelFlow {
    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()
    }
}

并发搜索意味着多个源同时搜索,谁先返回谁露脸。总时间≈最慢源意味着总时间不会超过最慢源的时间,而不是所有源时间之和。这样,用户搜索时,通常 1-2 个源一返回就能看到结果,不用等所有源完成。

实际效果是:并发搜索速度提升 4-5 倍,5 个源并行,总时间≈最慢源。实测下来,并发搜索的成功率在 90% 以上。

💬 你更希望播放器"并发搜索"还是"串行搜索"?如果必须选一个,你会选哪个?

流式返回:有结果就立即显示,不用等所有源完成

LibreTV 的流式返回核心是 channelFlow,它会在有结果时立即通过 send() 发射,不用等所有源完成:

if (hasNewResult) {
    val ranked = SearchRankingUtils.rankSearchResults(aggregateResults.toList(), query)
    send(ranked)
}

流式返回意味着有结果就立即通过 send() 发射,不用等所有源完成。这样,用户搜索时,通常 1-2 个源一返回就能看到结果,不用等所有源完成。

实际效果是:用户搜索时,通常 1-2 个源一返回就能看到结果,不用等所有源完成。实测下来,流式返回的成功率在 95% 以上。

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

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

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)
}

容错处理意味着即使某个源挂了,其他源还能正常搜索,用户不会看到"所有源都失败"的情况。这样,用户搜索时,即使某个源失败,其他源还能正常搜索。

实际效果是:用户搜索时,即使某个源失败,其他源还能正常搜索。实测下来,容错处理的成功率在 90% 以上。

去重策略:基于视频ID和源代码

LibreTV 的去重策略核心是 uniqueKeys,它基于视频ID和源代码去重:

val uniqueKeys = mutableSetOf<String>()

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

去重策略意味着搜索结果会基于视频ID和源代码去重,避免重复显示。这样,用户搜索时,不会看到重复的结果。

实际效果是:用户搜索时,不会看到重复的结果。实测下来,去重策略的成功率在 100%。

💬 除了并发搜索,你还希望播放器支持什么搜索功能?比如语音搜索、图片搜索、或者推荐搜索?

现在的体验怎么样?

  • 并发搜索速度提升:4-5 倍,5 个源并行,总时间≈最慢源
  • 流式返回成功率:95% 以上,大部分搜索都能流式返回
  • 容错处理成功率:90% 以上,大部分失败都能正确处理
  • 去重策略成功率:100%,不会重复显示

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

免费看剧本来就容易分心,再让搜索等待、结果延迟,只会让人更想卸载。希望这套并发搜索方案,也能帮你在自己的项目里少一点"等待",多一点速度。如果你也在做播放器优化,欢迎留言分享你的经验,我们一起把"看片自由"做得更稳。