Skip to content

Commit 68bf43a

Browse files
committed
fix(live-session): fg 暂停期维持媒体检测轻量轮询
P2-4:轻量轮询门控由 media 条件扩为 !manual && (media||fg)(抽纯函数 light_poll_enabled 并单测)——fg 暂停期视频暂停/结束即锁存 media,回位 fg 解除后不伪恢复采集,等视频恢复播放自动真恢复;watchdog 探针随之覆盖 fg 期;5s OCR 恢复判定门控在 media_held 上,防 fg 期把播放中误当恢复沿 每 5s 落伪 Play。测试:+2 门控真值表用例;全量 app_lib_tests 2327 通过。
1 parent 4f8a710 commit 68bf43a

2 files changed

Lines changed: 59 additions & 8 deletions

File tree

‎app/src-tauri/src/live_session_frame.rs‎

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,24 @@ fn media_sound_recent(slot: &Arc<Mutex<Option<Instant>>>) -> bool {
136136
last.is_some_and(|t| t.elapsed().as_millis() as u64 <= crate::media_state::SOUND_RECENT_MS)
137137
}
138138

139+
/// 暂停分支轻量轮询门控(纯函数;P2-4 审查修复抽离以便单测)。
140+
///
141+
/// @ai-context(Why):manual 锁存期语义=用户冻结,worker 不跟随任何自动源
142+
/// (全冻结不轮询);media **或 fg** 任一 auto 条件持有时维持
143+
/// 1s 取帧 + 媒体检测拍——fg 暂停期媒体条件照常锁存:用户离开
144+
/// 期间视频暂停/结束即锁存 media(否则回位 fg 解除即"恢复采集",
145+
/// 主路径需 ~3-4s 滞回才重锁存 media,期间环境声混入/UI 闪采集
146+
/// 中/时间轴伪运行段);回位链条 = fg 解除 → media 仍 held →
147+
/// 不真恢复 → 视频恢复播放 → 自动真恢复。
148+
fn light_poll_enabled(cond: crate::pause_state::PauseConditions) -> bool {
149+
!cond.manual && (cond.media || cond.foreground)
150+
}
151+
139152
/// 审查 F3:自动暂停期 watchdog 探针(无提示无心跳——暂停语义下停更提示无
140153
/// 意义;仅做观测 + WGC 自愈)。防复合卡死:暂停期间 WGC 会话失活(REQ-281
141154
/// 原场景)→ 恢复检测读不到新帧 → 永久卡自动暂停;此处探针周期性复活会话。
155+
/// P2-4:轻量轮询扩到 fg 暂停期后探针随之覆盖 fg 期(视频暂停/画面停更期间
156+
/// 同样需要 WGC 自愈;探针只观测 + 复活,任意暂停期无提示副作用)。
142157
fn watchdog_paused_probe(
143158
mut screen: Option<&mut ScreenCaptureSampler>,
144159
liveness: &mut crate::frame_liveness::FrameLiveness,
@@ -311,8 +326,9 @@ pub fn run_screen_worker(
311326
// 2026-08 A1:暂停边沿跟踪(暂停期画面链整体冻结:采样/前台监控/播放器
312327
// 检测全部跳过——"会话时间"在暂停期间不前进)
313328
// 批 2a:auto_paused 局部 bool 删除——暂停来源收敛在 pause_state 单状态机
314-
// (reason/条件锁存);本 worker 只按自身来源行动:media 条件持有时轻量
315-
// 轮询找恢复信号、manual 锁存期全冻结(不跟随任何自动源)
329+
// (reason/条件锁存);本 worker 只按自身来源行动:auto 条件(media/fg,
330+
// P2-4 起 fg 期同样)持有时轻量轮询找恢复信号、manual 锁存期全冻结
331+
// (不跟随任何自动源)
316332
let mut worker_paused = pause.paused.load(Ordering::SeqCst);
317333
// 前台自动暂停门控(批 2a):250ms 节拍独立于采样拍;锚定资格=有目标
318334
// 窗口(本 worker 存在 ⇔ 画面链开启,anchor_eligible 第二参装配侧已隐含)
@@ -365,9 +381,11 @@ pub fn run_screen_worker(
365381
worker_paused = true;
366382
eprintln!("[ScreenWorker] 会话暂停,画面链冻结(等恢复/来源解除)");
367383
}
368-
// 非 manual 锁存:media 条件持有时轻量轮询找恢复信号
369-
// (fg 条件的回位检测由上方门控节拍负责——互不解除只解自己)
370-
if pause.media_held() && !pause.manual_held() {
384+
// 非 manual 锁存:auto 条件(media/fg)持有时轻量轮询找恢复信号。
385+
// P2-4:门控扩到 fg 期——fg 暂停期也维持媒体检测(视频暂停/结束即
386+
// 锁存 media;否则回位 fg 解除即伪恢复采集,见 light_poll_enabled
387+
// 注释);fg 条件的解除仍由上方门控节拍负责(互不解除只解自己)
388+
if light_poll_enabled(pause.conditions()) {
371389
// P2 自动暂停:轻量轮询——仅取帧刷新 latest_frame + 播放检测。
372390
// 检测读的就是 latest_frame,不刷新则永远看到暂停帧 → 无法发现
373391
// 恢复;1s 一拍仅取帧(零分析),5s 一拍检测(沿用 REQ-125 节流)
@@ -384,7 +402,10 @@ pub fn run_screen_worker(
384402
&mut got_frame,
385403
);
386404
// 审查 F3:暂停期 watchdog 探针(无提示)——WGC 会话失活时
387-
// 恢复检测永远读不到新帧 → 自动暂停永久卡死;此处定期自愈
405+
// 恢复检测永远读不到新帧 → 自动暂停永久卡死;此处定期自愈。
406+
// P2-4:轻量轮询扩到 fg 期后探针随之覆盖 fg 期(fg 期视频
407+
// 暂停/画面停更同样会饿死媒体恢复检测,需同款 WGC 自愈;
408+
// 探针只观测 + 复活,任意暂停期无提示副作用)
388409
watchdog_paused_probe(
389410
screen.as_mut(),
390411
&mut liveness,
@@ -418,7 +439,13 @@ pub fn run_screen_worker(
418439
eprintln!("[ScreenWorker] 随播随停:声画恢复 → 自动解除暂停");
419440
}
420441
}
421-
if last_player_check_at.elapsed() >= Duration::from_secs(5) {
442+
// P2-4:OCR 恢复判定只在 media 条件持有时有意义——fg-only
443+
// 暂停期视频正常播放(帧无暂停图标),裸跑会把"播放中"误当
444+
// 恢复沿,每 5s 落一次伪 Play/伪 live:media-resumed(时间轴
445+
// 伪运行段);media 持有时仍是 REQ-125 恢复兜底(5s 判定)
446+
if pause.media_held()
447+
&& last_player_check_at.elapsed() >= Duration::from_secs(5)
448+
{
422449
last_player_check_at = Instant::now();
423450
let check_now_ms = comp_epoch.elapsed().as_millis() as u64;
424451
if let Some(f) = latest_frame.lock().ok().and_then(|g| g.clone()) {
@@ -473,7 +500,8 @@ pub fn run_screen_worker(
473500
+ Duration::from_millis(pause.total_paused_ms.load(Ordering::SeqCst));
474501
// REQ-291(v0.19.7):随播随停 1s 拍(独立于采样——idle 静默期仍判暂停;
475502
// 手动暂停不判:manual 锁存期语义是用户冻结,不跟随视频——批 2a 起
476-
// 主路径只在未暂停时运行,暂停期恢复检测在暂停分支按媒体条件轮询)
503+
// 主路径只在未暂停时运行,暂停期恢复检测在暂停分支按 auto 条件
504+
// (media/fg)轮询)
477505
if !paused_now && last_media_tick.elapsed() >= Duration::from_secs(1) {
478506
last_media_tick = Instant::now();
479507
let sound_recent = media_sound_recent(&media_sound);

‎app/src-tauri/src/live_session_frame_tests.rs‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
//! @ai-context: bgra_to_rgb_image 已随 M4 编排函数移入 region_ocr.rs
66
//! (保持本文件内联测试的引用更新)。
77
8+
use super::*;
9+
use crate::pause_state::PauseConditions;
810
use crate::region_ocr::bgra_to_rgb_image;
911

1012
#[test]
@@ -24,3 +26,24 @@ fn bgra_rejects_mismatched_size() {
2426
assert!(bgra_to_rgb_image(&[0u8; 3], 2, 1).is_none());
2527
assert!(bgra_to_rgb_image(&[], 0, 0).is_none());
2628
}
29+
30+
#[test]
31+
fn light_poll_runs_for_media_or_foreground_pause_but_not_manual() {
32+
// Arrange & Act & Assert(P2-4 门控真值表):
33+
assert!(!light_poll_enabled(PauseConditions { manual: true, foreground: false, media: false }),
34+
"manual 锁存期全冻结(用户冻结语义,不跟随任何自动源)");
35+
assert!(light_poll_enabled(PauseConditions { manual: false, foreground: false, media: true }),
36+
"media 暂停期维持轻量轮询(既有语义不回归)");
37+
assert!(light_poll_enabled(PauseConditions { manual: false, foreground: true, media: false }),
38+
"P2-4:fg 暂停期也维持媒体检测——离开期间视频暂停/结束即锁存 media");
39+
assert!(light_poll_enabled(PauseConditions { manual: false, foreground: true, media: true }));
40+
assert!(!light_poll_enabled(PauseConditions::default()), "防御:无持因不轮询");
41+
}
42+
43+
#[test]
44+
fn light_poll_manual_latch_overrides_every_auto_condition() {
45+
// Arrange & Act & Assert:manual 与任意 auto 条件叠加时门控仍关闭
46+
assert!(!light_poll_enabled(PauseConditions { manual: true, foreground: true, media: false }));
47+
assert!(!light_poll_enabled(PauseConditions { manual: true, foreground: false, media: true }));
48+
assert!(!light_poll_enabled(PauseConditions { manual: true, foreground: true, media: true }));
49+
}

0 commit comments

Comments
 (0)