fix(studio): 回放视图加一个无条件「停止」(Esc 同效),并让「文件一帧都不出」直接报错而不是一直等

- What: `ToolbarWidget` 新增 `set_reset_role("reset"/"stop")`,回放视图把轨道第二个按钮变成「停止」;
  `StudioMainWindow._stop_replay_activity()` 一次停掉回放(play_timer)与「从视频重算」通道(worker),
  并清掉三维面板上的重算画面,状态栏说明"已停止了什么";`_on_reset`/`_on_stop_shortcut`(Esc) 按角色路由;
  `_start_playback`/`_pause_playback` 结束时调用 `_update_run_enabled()`,让停止按钮在有东西可停时才可用;
  两个采集 worker 新增 `FILE_FIRST_FRAME_TIMEOUT_S=10s`:文件源一帧都没出来就报错结束会话
  (原先 `saw_frame` 为假时 EOF 分支永不执行,循环空转到天荒地老、窗口一直停在"连接中")。
- Why: 用户报告"视频播放完之后无法再次按回放,卡住了",并建议加手动停止。复现得到两种状态:
  ① 普通回放播到末尾后再按回放**是可以**重新从头播的(探针实测 0.967→0→0.371,计时器在跑),
  不是这条路径的问题;② 「从视频重算」通道跑文件时,工作线程可能既不出帧也不结束
  (实测:1.5 s 的合成视频跑 25 s,packets=0 且 is_running=True),于是界面永远停在运行/连接状态,
  操作者看到"卡住"。所以既给一个无论什么状态都能按的停止,也让"读不出帧"变成一条明确的错误。
- Verification: 新增/更新用例:`test_worker_frame_rate.py` 的"文件一帧都不出→error+消息"(缩短时限跑,
  真实 worker);`test_ui_smoke.py` 三项:回放视图的停止按钮(播放中可用、按下停回放、重算中停下重算、
  无事可停时说明)、Esc 同样能停、实时视图仍是「重置」;apps/studio 全量通过;shared 全量通过;
  ui_check 583 项 0 失败(新增回放停止的 5 项检查,并给"场景已加载却仍在显示消息"那条加了诊断细节,
  它此前偶发失败过一次、这次跑通)。
- Not in this change: 普通回放"播到末尾→再按回放"的路径(实测正常,未改);让回放直接播放"非记录"的视频
  (仍走「从视频重算」);`_recompute_running` 与 worker 真实状态的进一步合并(现在停止按钮同时看两者)。
This commit is contained in:
ykcheungab
2026-09-14 17:40:49 +08:00
parent a26f77bb67
commit eb1dfbce8a
9 changed files with 302 additions and 4 deletions
+42 -1
View File
@@ -2882,7 +2882,9 @@ def main() -> int:
)
check(
not pane.showing_message,
"the pane is still showing a message although a scene loaded",
"the pane is still showing a message although a scene loaded "
f"(loaded_path={pane._loaded_path}, source={getattr(pane._source, 'path', None)}, "
f"loaded={pane._loaded}, message={pane.message.text()[:40]!r})",
)
if view_for_load is not None and view_for_load.panel is not None:
source_text = view_for_load.panel.source_label.text()
@@ -3076,6 +3078,45 @@ def main() -> int:
"the rail's primary button is not the replay's transport",
)
check(replay_window.toolbar.btn_run.isEnabled(), "the replay has no transport")
# The replay view offers an unconditional stop (the rail's second button), because
# the transport is a play/pause toggle and "which one is stuck?" is not a question
# to ask an operator in front of a frozen picture.
check(
replay_window.toolbar.reset_role() == "stop",
"the replay view has no stop button",
)
check(
replay_window.toolbar.btn_reset.text() == t("toolbar.stop_playback"),
f"the stop button is not labelled as one: "
f"{replay_window.toolbar.btn_reset.text()!r}",
)
# Start playing so there is something to stop, then stop it with that button.
replay_window.toolbar.btn_run.setChecked(True)
app.processEvents()
check(
replay_window._play_timer.isActive(),
"the playback did not start, so the stop check would prove nothing",
)
check(
replay_window.toolbar.btn_reset.isEnabled(),
"the stop is not offered while the transport is playing",
)
replay_window.toolbar.btn_reset.click()
app.processEvents()
check(
not replay_window.toolbar.btn_run.isChecked(),
"the stop did not stop the transport",
)
check(
not replay_window._play_timer.isActive(),
"the stop left the playback timer running",
)
check(
t("toolbar.stopped_playback") in replay_window.statusBar().currentMessage(),
f"the stop did not say what it stopped: "
f"{replay_window.statusBar().currentMessage()!r}",
)
check(
not replay_window.toolbar.btn_run.isChecked(),
"the replay starts while playing",