用途
本技能面向 Kotlin 共享会话(shared session)场景,解决跟随者(follower)如何构建可播放条目的问题。核心结论是:应当直接从共享会话携带的 payload 构建条目,而不是通过本地目录(catalogue)重新解析。payload 是契约,目录解析只是猜测。
主要能力与原则
从 payload 直接构建
共享会话携带条目 id 以及足够的展示元数据用于渲染一行。不要丢弃这些数据再用应用通用的代码路径重新解析。示例代码(名称已泛化):
private fun SharedTrack.toSessionItem(): GenericMediaItem =
GenericMediaItem(
mediaId = id,
uri = id,
metadata = GenericMediaMetadata(
title = title,
artist = artist.ifBlank { null }, // blank on the wire means absent
artworkUri = thumbnail.ifBlank { null },
// Forced, not inferred: every client in a session must be on the same rendition.
description = MEDIA_KIND.AUDIO,
),
customCacheKey = id,
)
流仍由组件在本地解析——会话从不在客户端之间传输媒体,这正是不同平台的客户端能够共享同一会话的原因。只有“播放什么”的描述来自网络。
陷阱
本地解析器会错误判断 rendition。 常见形态是类似“方形封面且不是宽文件名 ⇒ 音频”的布尔判断。当条目来自携带真实尺寸的搜索结果时它有效;但跟随者从裸 id 构建时没有尺寸,会回退到构造的 URL,其文件名恰好是宽标记之一,于是几乎所有条目都被解析为视频。rendition 并非装饰性字段:它选择不同的源构建方式。标记为视频的条目会被路由到合并的音视频源,导致跟随者看到无声画面,而源端听到纯音频。两个症状,一个原因,且都不指向解析器。
apply collector 内的网络往返是单元素队列。 应用共享命令的 collector 是单个挂起循环。其中任何逐条目查找——尤其是等待流产生非空数据的查找——不仅阻塞该条目,还阻塞其后所有命令。若确实需要查找,应在 collector 外启动,让 collector 保持纯 apply。
共享 id 同时是条目 id、源 uri 和缓存键。 组件从该 id 解析流,因此 payload 从不携带已解析的 URL,也不应携带:已解析 URL 是每客户端独立的、会过期、可能携带凭据。三个槽位保持同一 id 也是后续所有 id 比较(最后应用检查、队列 diff、屏障应答)能正常工作的前提,因为它们比较的是来自网络的 id。
空字符串不等于缺失。 网络格式通常无法表达“无艺术家”,未设置字段会以空字符串到达。直接传递会渲染空元数据行而非省略;应在边界处、在 builder 中一次性将空字符串映射为 null。
过滤空 id 并去重,当前条目放最前。 共享队列由其他客户端组装,可能同时包含两者。当前条目领先,使跟随者的队列位置与其他人一致;重复项否则会让同一条目出现两次,并使后续每个索引失同步。
每次添加前先清空。 从 payload 构建的队列是跟随者的整个队列,而非追加。若追加,每次重建(当共享队列在条目未变时发生变化就会重建)都会留下上一份副本,跟随者累积重复项,索引不再与其他人匹配。
头尾用不同调用添加。 只有头部携带开始决策。重新断言播放的追加会撤销该决策,把头尾写成一次调用会让这个错误不可避免。
发布端有同样问题的另一版本,且是单位 bug。 出站 payload 根据你拥有的数据从两个不同来源构建:目录行(时长通常以秒计)和实时组件(时长以毫秒计)。两个 builder、两种单位、一个字段——发送端不可见,因为那里没有读回。应在 builder 处转换,并把单位放进字段名或注释。
两侧选取封面列表的同一端。 payload builder 会从缩略图列表中取一个;一个 builder 取最大、另一个取第一个,会导致两个客户端为同一条目渲染不同封面,看起来像缓存 bug 但并非如此。
不要日后与正常路径“统一”。 它看起来像重复但并非如此:正常路径服务的是用户主动选择、可以承担查找的场景;此路径服务的是不能阻塞、不能猜测的 collector。在 builder 处留一行原因,否则一两个版本内就会被合并回去。
验证方式
读取每一处推断而非声明 rendition 的地方:
grep -rn -A8 -E "val is(Song|Video|Audio|Music)\b *=" --include="*.kt" . | grep -v "/build/"
从图像尺寸、宽高比比较或 URL 子串派生的布尔值是启发式——对携带真实尺寸的数据没问题,对裸 id 则错误。然后列出条目 builder,检查哪些直接声明了类型:
grep -rnE "fun [A-Za-z.]*to[A-Za-z]*MediaItem\(" --include="*.kt" . | grep -v "/build/"
grep -rnE "description = [A-Z_]+\.[A-Z]+" --include="*.kt" . | grep -v "/build/"
apply 路径使用的 builder 必须出现在第二个列表中;若没有,说明它继承了启发式。最后证明 apply collector 不会在网络处挂起:
for f in $(grep -rlE "toRoomMediaItem|toSessionItem|SharedTrack|RoomTrack" --include="*.kt" . \
| grep -v "/build/"); do
echo "== $f"; grep -nE "\.first *\{|\.firstOrNull *\{|await\(|\.collect *\{" "$f"
done
逐条查看每个命中等待什么:对内存集合的 first/firstOrNull 无害;对数据流的同样调用就是卡点。collect 块本身就是 collector——其中不能出现任何等待远程应答的内容。
行为上,用两个客户端:播放一个跟随者从未打开过的条目,确认它得到音频而非无声画面——只检查“有东西在播放”是看不出来的。然后把跟随者置于慢网络并连续发出多条命令:它们必须全部按序到达,而不是在第一条解析后成批到达。
适用场景
适用于 Kotlin 实现的共享会话/同步播放系统,尤其是跟随者端从共享 payload 构建播放队列的场景。
限制
资料未提供该技能的具体安装方式、依赖版本、支持的平台范围或收费信息。