@@ -24,6 +24,8 @@ use crate::live_frame_process::{
2424} ;
2525use crate :: subtitle_ocr:: SubtitleVoter ;
2626
27+ use live_session_liveness:: { liveness_check, watchdog_paused_probe} ;
28+
2729/// 采样节拍(ms):与音频消费解耦,固定 1s 一拍(审查 M5 修复)。
2830const SAMPLE_TICK_MS : u64 = 1000 ;
2931/// 采样线程轮询休眠(ms)——空转粒度,影响停止响应延迟。
@@ -41,93 +43,9 @@ pub struct LatestCapturedFrame {
4143 pub height : u32 ,
4244}
4345
44- // ── REQ-281(v0.19.6):帧心跳 / 停更提示事件载荷 ──
45-
46- /// live:frame-heartbeat 载荷(诊断观测:后端/帧到达/静默秒数/目标可见性)。
47- #[ derive( serde:: Serialize , Clone ) ]
48- #[ serde( rename_all = "camelCase" ) ]
49- struct FrameHeartbeatPayload {
50- backend : String ,
51- got_frame : bool ,
52- silent_secs : u64 ,
53- visible : bool ,
54- }
55-
56- /// live:frame-stalled 载荷(停更秒数——前端提示语)。
57- #[ derive( serde:: Serialize , Clone ) ]
58- #[ serde( rename_all = "camelCase" ) ]
59- struct FrameStalledPayload {
60- silent_secs : u64 ,
61- }
62-
63- /// REQ-281 停更监测副作用执行点(真实采样拍后调用一次):
64- ///
65- /// @ai-context: 判定语义(frame_liveness 纯状态机):仅 WGC 窗口捕获模式判停
66- /// (DXGI 无帧=桌面无变化属正常,不重建不提示);目标不可见时
67- /// 清提示不动作(最小化/遮挡另有既有 window-lost/用户感知路径)。
68- /// 心跳每 2s 一报(含后端名/帧到达/静默秒数/可见性——真机复现
69- /// 诊断与停更提示共用同一信号源)。
70- #[ allow( clippy:: too_many_arguments) ]
71- fn liveness_check (
72- app : & tauri:: AppHandle ,
73- mut screen : Option < & mut ScreenCaptureSampler > ,
74- liveness : & mut crate :: frame_liveness:: FrameLiveness ,
75- got_frame : bool ,
76- now : Instant ,
77- ) {
78- let backend = screen
79- . as_ref ( )
80- . map ( |s| s. backend_name ( ) . to_string ( ) )
81- . unwrap_or_else ( || "none" . to_string ( ) ) ;
82- let visible = screen. as_ref ( ) . is_none_or ( |s| s. target_visible ( ) ) ;
83- let silent_secs = liveness. stall_secs ( now) . unwrap_or ( 0 ) ;
84-
85- // 心跳(独立于停更判定——诊断观测恒可用)
86- if liveness. heartbeat_due ( now) {
87- liveness. mark_heartbeat ( now) ;
88- let _ = app. emit (
89- "live:frame-heartbeat" ,
90- FrameHeartbeatPayload {
91- backend : backend. clone ( ) ,
92- got_frame,
93- silent_secs,
94- visible,
95- } ,
96- ) ;
97- }
98-
99- // 非 WGC/无窗口:无"停更"语义(DXGI 超时=桌面无变化);残留提示清掉
100- if backend != "wgc" || !visible {
101- if liveness. stalled {
102- liveness. clear_stalled ( ) ;
103- let _ = app. emit ( "live:frame-recovered" , ( ) ) ;
104- }
105- return ;
106- }
107-
108- liveness. observe ( now, got_frame) ;
109- if liveness. recover_edge ( got_frame) {
110- liveness. clear_stalled ( ) ;
111- let _ = app. emit ( "live:frame-recovered" , ( ) ) ;
112- return ;
113- }
114- if liveness. stall_edge ( now) {
115- liveness. mark_stalled ( ) ;
116- let _ = app. emit (
117- "live:frame-stalled" ,
118- FrameStalledPayload {
119- silent_secs : liveness. stall_secs ( now) . unwrap_or ( 0 ) ,
120- } ,
121- ) ;
122- }
123- // 停更且到重建节流窗口 → WGC 会话自愈(用户复现=视频在动画面停更)
124- if !got_frame && liveness. recreate_due ( now) {
125- if let Some ( s) = screen. as_mut ( ) {
126- s. revive_wgc ( ) ;
127- }
128- liveness. mark_recreate ( now) ;
129- }
130- }
46+ /// REQ-281 停更监测(停更判定/心跳载荷/WGC 自愈;本文件 ≤300 行,AGENTS.md §3)。
47+ #[ path = "live_session_liveness.rs" ]
48+ pub ( super ) mod live_session_liveness;
13149
13250/// REQ-291:媒体级"最近有声"判定(窗口=SOUND_RECENT_MS ≥ 采样拍 1s——
13351/// 保证一拍内出现的声音必被读到;锁中毒按无声处理,零阻断)。
@@ -149,32 +67,6 @@ fn light_poll_enabled(cond: crate::pause_state::PauseConditions) -> bool {
14967 !cond. manual && ( cond. media || cond. foreground )
15068}
15169
152- /// 审查 F3:自动暂停期 watchdog 探针(无提示无心跳——暂停语义下停更提示无
153- /// 意义;仅做观测 + WGC 自愈)。防复合卡死:暂停期间 WGC 会话失活(REQ-281
154- /// 原场景)→ 恢复检测读不到新帧 → 永久卡自动暂停;此处探针周期性复活会话。
155- /// P2-4:轻量轮询扩到 fg 暂停期后探针随之覆盖 fg 期(视频暂停/画面停更期间
156- /// 同样需要 WGC 自愈;探针只观测 + 复活,任意暂停期无提示副作用)。
157- fn watchdog_paused_probe (
158- mut screen : Option < & mut ScreenCaptureSampler > ,
159- liveness : & mut crate :: frame_liveness:: FrameLiveness ,
160- got_frame : bool ,
161- now : Instant ,
162- ) {
163- let Some ( _) = screen. as_mut ( ) else { return } ;
164- liveness. observe ( now, got_frame) ;
165- let wgc = screen. as_ref ( ) . is_some_and ( |s| s. backend_name ( ) == "wgc" ) ;
166- let visible = screen. as_ref ( ) . is_none_or ( |s| s. target_visible ( ) ) ;
167- if !wgc || !visible || got_frame {
168- return ;
169- }
170- if liveness. recreate_due ( now) {
171- if let Some ( s) = screen. as_mut ( ) {
172- s. revive_wgc ( ) ;
173- }
174- liveness. mark_recreate ( now) ;
175- }
176- }
177-
17870/// 屏幕采样线程入口(TD-026 修复:OCR 从会话线程移出,音频消费不再被阻塞)。
17971///
18072/// @ai-context: ScreenCaptureSampler 持 COM 对象(非 Send),在本线程内创建与使用,
0 commit comments