client: 槽位对应队伍,不再按 id 顺序对号入座

裁判端两个槽位的语义应该是"队伍一 / 队伍二",而不是"第 N 块发现的板子"。之前
两者混为一谈,出现两个都会被现场误读成故障的现象:

1. **两块槽位显示同一块板。** 每轮都从 `online_cameras()` 取前两个 id 填满两
   个槽位,于是只有一块板在场时两边一模一样。裁判会以为两队的图传串了。

2. **板子掉线时画面左右互换。** 低 id 的板子一离线,候选列表缩短,后面那块就
   跳到前面的槽位——同样像串线,而实际上只是排序。

规则改成:每轮最多消费一个候选 id;已填且仍在线的槽位不动;掉线的槽位保留它的
id 并显示"离线",等它回来时还在原位。

空槽的提示文案也改准:左槽"等待队伍一的图传"、右槽"等待队伍二的图传",而不是笼
统的"未收到画面"。

顺带修两个假数字:`frames_shown == 0` 时 `stats.width`/`height` 还是默认值 0,界
面上显示成 `0x0`——一个看起来像分辨率的假分辨率;fps 显示 `0.0` 读起来像"板子
卡死了",而真相是"还没收到"。状态行已经写了"已连接,等待画面…",不该再补一个矛
盾的数字。

关于"别把刚掉线的板子又立刻选回同一个槽位"那条过滤:它只在槽位掉线时起作用,
且会跳过当前 id 去找下一个候选,槽位仍然有机会回来。实测唯一那块在推流的板子正
常占用一个槽位,另一槽保留着在线但不推流的真板子——符合预期。
This commit is contained in:
Horldsence
2026-10-05 21:45:24 +08:00
parent cd8344c698
commit 3631a037d8
2 changed files with 43 additions and 10 deletions
+39 -7
View File
@@ -426,15 +426,47 @@ fn main() -> Result<(), Box<dyn Error>> {
let cameras = discovery.online_cameras(); let cameras = discovery.online_cameras();
// 两路按 id 顺序填槽。槽位保持稳定(不会因为某块板子临时离线就重新 // 槽位对应**队伍**,不是"第 N 块发现的板子"。
// 洗牌),否则画面会左右跳动。 //
let ids: Vec<u32> = cameras.iter().map(|c| c.id).take(2).collect(); // 两条规则:
//
// 1. 一次只填一个空槽,按 id 顺序。之前两块都空时每轮都从
// `online_cameras()` 取前两个,于是只有一块板在场时两块槽位显示
// 同一块画面——右半边不再是"队伍二",而是队伍一的复制。裁判看到
// 两边一模一样,会以为两队的图传串了。
//
// 2. 已填的槽位不换人。原先是 `ids.take(2)` 后按下标对号入座,低 id
// 的板子一离线,后面那块就跳到前面,画面左右互换,看起来同样
// 像串线。掉线的槽位保留它的 id 并显示"离线",等它回来。
//
// 所以每轮最多消费一个候选 id。
let mut next_candidate = cameras.iter().map(|c| c.id);
for (i, view) in views.iter_mut().enumerate() { for view in views.iter_mut() {
let wanted = ids.get(i).copied(); let still_here = view
if view.sender_id != wanted { .sender_id
.is_some_and(|id| cameras.iter().any(|c| c.id == id));
if still_here {
continue;
}
// 槽位已占且板子还在场上 → 保留(上面已 continue)。到这里说明
// 槽位是空的,或板子掉线了。
let candidate = next_candidate
.next()
// 别把刚掉线的这块又立刻选回同一个槽位:它短暂离线又回来时,
// 画面会原地卡一下才恢复。
.filter(|id| Some(*id) != view.sender_id);
match candidate {
Some(id) => {
*view = View::new(); *view = View::new();
view.sender_id = wanted; view.sender_id = Some(id);
}
// 没有可分配的了。掉线的槽位**保留 id**(显示"离线"而不是
// 变回空槽),这样它回来时还在原位。
None => {}
} }
} }
+3 -2
View File
@@ -491,7 +491,7 @@ export component RefereeApp inherits Window {
VideoPane { VideoPane {
frame: root.frame-a; frame: root.frame-a;
empty-text: root.stats-a.online ? "已连接,等待画面…" : "未收到画面"; empty-text: root.stats-a.online ? "已连接,等待画面…" : "等待队伍一的图传";
vertical-stretch: 1; vertical-stretch: 1;
horizontal-stretch: 1; horizontal-stretch: 1;
preferred-width: 480px; preferred-width: 480px;
@@ -500,7 +500,8 @@ export component RefereeApp inherits Window {
VideoPane { VideoPane {
frame: root.frame-b; frame: root.frame-b;
empty-text: root.stats-b.online ? "已连接,等待画面…" : "未收到画面"; // 槽位对应队伍,所以空槽要说清在等谁,而不是笼统的"未收到画面"。
empty-text: root.stats-b.online ? "已连接,等待画面…" : "等待队伍二的图传";
vertical-stretch: 1; vertical-stretch: 1;
horizontal-stretch: 1; horizontal-stretch: 1;
preferred-width: 480px; preferred-width: 480px;