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:
@@ -1994,6 +1994,98 @@ def test_the_recompute_mode_reruns_the_pipeline_over_the_video(
|
||||
assert not window.toolbar.btn_run.isChecked(), "the transport stayed checked"
|
||||
|
||||
|
||||
def test_the_replay_view_offers_a_stop_that_always_works(make_window, replay_recording):
|
||||
"""The operator must not have to work out *what* is stuck before stopping it.
|
||||
|
||||
The transport is a play/pause toggle, which is ambiguous exactly when something is
|
||||
wrong. So the rail's second button becomes an unconditional stop in this view, and
|
||||
it covers both things that can run there: plain playback and a recompute pass.
|
||||
"""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
window = _show(make_window(), (1440, 880))
|
||||
window.brand_bar.btn_playback.click()
|
||||
QApplication.processEvents()
|
||||
assert window._load_recording(replay_recording)
|
||||
QApplication.processEvents()
|
||||
|
||||
assert window.toolbar.reset_role() == "stop", (
|
||||
"the replay view kept the tracker reset button instead of a stop"
|
||||
)
|
||||
assert window.toolbar.btn_reset.text() == t("toolbar.stop_playback")
|
||||
assert not window.toolbar.btn_reset.isEnabled(), (
|
||||
"a stop is offered while nothing is running"
|
||||
)
|
||||
|
||||
# Plain playback: the stop button appears while it plays and pressing it stops.
|
||||
window.toolbar.btn_run.setChecked(True)
|
||||
QApplication.processEvents()
|
||||
assert window._play_timer.isActive()
|
||||
assert window.toolbar.btn_reset.isEnabled(), "no stop offered while playing"
|
||||
window.toolbar.btn_reset.click()
|
||||
QApplication.processEvents()
|
||||
assert not window._play_timer.isActive(), "the stop did not stop the playback"
|
||||
assert not window.toolbar.btn_run.isChecked(), "the transport still shows playing"
|
||||
assert t("toolbar.stopped_playback") in window.statusBar().currentMessage(), (
|
||||
window.statusBar().currentMessage()
|
||||
)
|
||||
|
||||
# A recompute pass: the same button stops it, without stopping the recording.
|
||||
stopped: list = []
|
||||
window._stop_run = lambda **kwargs: stopped.append(kwargs.get("reason"))
|
||||
window._recompute_running = True
|
||||
window._update_run_enabled()
|
||||
assert window.toolbar.btn_reset.isEnabled(), "no stop offered while a pass runs"
|
||||
window.toolbar.btn_reset.click()
|
||||
QApplication.processEvents()
|
||||
assert stopped == ["user_stop"], stopped
|
||||
assert t("toolbar.stopped_pass") in window.statusBar().currentMessage(), (
|
||||
window.statusBar().currentMessage()
|
||||
)
|
||||
|
||||
# Nothing running: it says so instead of doing nothing at all.
|
||||
# (The stubbed ``_stop_run`` above records the reason but does not clear the flag
|
||||
# the real one clears, so the state is completed by hand here.)
|
||||
window._recompute_running = False
|
||||
window.toolbar.btn_reset.click()
|
||||
QApplication.processEvents()
|
||||
assert t("toolbar.nothing_to_stop") in window.statusBar().currentMessage(), (
|
||||
window.statusBar().currentMessage()
|
||||
)
|
||||
window.close()
|
||||
|
||||
|
||||
def test_escape_stops_the_replay_too(make_window, replay_recording):
|
||||
"""Esc is the documented way out; in the replay view it stops the replay."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
window = _show(make_window(), (1440, 880))
|
||||
window.brand_bar.btn_playback.click()
|
||||
QApplication.processEvents()
|
||||
assert window._load_recording(replay_recording)
|
||||
QApplication.processEvents()
|
||||
window.toolbar.btn_run.setChecked(True)
|
||||
QApplication.processEvents()
|
||||
assert window._play_timer.isActive()
|
||||
window.act_stop.trigger()
|
||||
QApplication.processEvents()
|
||||
assert not window._play_timer.isActive(), "Esc did not stop the playback"
|
||||
assert not window.toolbar.btn_run.isChecked()
|
||||
window.close()
|
||||
|
||||
|
||||
def test_the_live_view_keeps_the_tracker_reset(make_window):
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
window = _show(make_window(), (1440, 880))
|
||||
window.brand_bar.btn_live.click()
|
||||
QApplication.processEvents()
|
||||
assert window.toolbar.reset_role() == "reset"
|
||||
window.close()
|
||||
|
||||
|
||||
def test_a_video_that_is_not_a_recording_is_refused_with_a_reason(
|
||||
make_window, record_root
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user