@@ -18,9 +18,10 @@ use crate::capture::frame_diff::SampleRegion;
1818use crate :: db:: Db ;
1919use crate :: engine:: EnginePool ;
2020use crate :: fusion:: SubtitleSegment ;
21- use crate :: live_frame_process:: { capture_latest_only , persist_voted_subtitle, process_frame} ;
21+ use crate :: live_frame_process:: { persist_voted_subtitle, process_frame} ;
2222
23- use live_session_liveness:: { liveness_check, watchdog_paused_probe} ;
23+ use live_session_liveness:: liveness_check;
24+ use live_session_pause_poll:: media_sound_recent;
2425use live_session_worker_state:: FrameWorkerState ;
2526
2627/// 采样节拍(ms):与音频消费解耦,固定 1s 一拍(审查 M5 修复)。
@@ -48,25 +49,10 @@ pub(super) mod live_session_liveness;
4849#[ path = "live_session_worker_state.rs" ]
4950pub ( super ) mod live_session_worker_state;
5051
51- /// REQ-291:媒体级"最近有声"判定(窗口=SOUND_RECENT_MS ≥ 采样拍 1s——
52- /// 保证一拍内出现的声音必被读到;锁中毒按无声处理,零阻断)。
53- fn media_sound_recent ( slot : & Arc < Mutex < Option < Instant > > > ) -> bool {
54- let last = slot. lock ( ) . ok ( ) . and_then ( |g| * g) ;
55- last. is_some_and ( |t| t. elapsed ( ) . as_millis ( ) as u64 <= crate :: media_state:: SOUND_RECENT_MS )
56- }
52+ /// 暂停隔离与轻量轮询(前台门控拍/暂停期恢复检测;§3)。
53+ #[ path = "live_session_pause_poll.rs" ]
54+ pub ( super ) mod live_session_pause_poll;
5755
58- /// 暂停分支轻量轮询门控(纯函数;P2-4 审查修复抽离以便单测)。
59- ///
60- /// @ai-context(Why):manual 锁存期语义=用户冻结,worker 不跟随任何自动源
61- /// (全冻结不轮询);media **或 fg** 任一 auto 条件持有时维持
62- /// 1s 取帧 + 媒体检测拍——fg 暂停期媒体条件照常锁存:用户离开
63- /// 期间视频暂停/结束即锁存 media(否则回位 fg 解除即"恢复采集",
64- /// 主路径需 ~3-4s 滞回才重锁存 media,期间环境声混入/UI 闪采集
65- /// 中/时间轴伪运行段);回位链条 = fg 解除 → media 仍 held →
66- /// 不真恢复 → 视频恢复播放 → 自动真恢复。
67- fn light_poll_enabled ( cond : crate :: pause_state:: PauseConditions ) -> bool {
68- !cond. manual && ( cond. media || cond. foreground )
69- }
7056
7157/// 屏幕采样线程入口(TD-026 修复:OCR 从会话线程移出,音频消费不再被阻塞)。
7258///
@@ -127,151 +113,11 @@ pub fn run_screen_worker(
127113 ) ;
128114
129115 while !w. stop . load ( Ordering :: SeqCst ) {
130- // ── 前台门控采样(批 2a;250ms 节拍)──
131- // @ai-context: 非 manual 锁存期持续观察(含媒体/前台暂停期间——暂停期
132- // 也允许前台源锁存/解除);manual 锁存期冻结。观察分类:
133- // 前台=目标 → Target;前台=本进程自窗(浮窗/overlay/原生
134- // 对话框)→ Neutral(中性:不推进也不撤销);其余 Foreign;
135- // 全屏无锚点/探测失败 → Neutral(无证据不推断)
136- if w. fg_eligible {
137- let fg_now_ms = w. epoch . elapsed ( ) . as_millis ( ) as u64 ;
138- if fg_now_ms. saturating_sub ( w. last_fg_gate_ms ) >= crate :: foreground_pause:: FG_TICK_MS
139- && !w. pause . manual_held ( )
140- {
141- w. last_fg_gate_ms = fg_now_ms;
142- let obs = match ( w. hwnd , crate :: windows:: foreground_hwnd ( ) ) {
143- ( Some ( target) , Some ( fg) ) if fg == target => {
144- crate :: foreground_pause:: ForegroundObs :: Target
145- }
146- ( Some ( _) , Some ( fg) ) if crate :: windows:: is_self_hwnd ( fg) => {
147- crate :: foreground_pause:: ForegroundObs :: Neutral
148- }
149- ( Some ( _) , Some ( _) ) => crate :: foreground_pause:: ForegroundObs :: Foreign ,
150- _ => crate :: foreground_pause:: ForegroundObs :: Neutral ,
151- } ;
152- match w. fg_gate . tick ( obs) {
153- crate :: foreground_pause:: ForegroundDecision :: Suspend => {
154- let _ = w. pause
155- . request_pause ( crate :: pause_state:: PauseSource :: Foreground ) ;
156- eprintln ! ( "[ScreenWorker] 前台离开目标窗口(连续确认)→ 自动暂停捕获" ) ;
157- }
158- crate :: foreground_pause:: ForegroundDecision :: Resume => {
159- let _ = w. pause
160- . request_release ( crate :: pause_state:: PauseSource :: Foreground ) ;
161- eprintln ! ( "[ScreenWorker] 前台回到目标窗口 → 解除前台暂停" ) ;
162- }
163- crate :: foreground_pause:: ForegroundDecision :: None => { }
164- }
165- }
166- }
116+ w. foreground_gate_tick ( ) ;
167117 // ── 暂停检查(2026-08 A1 硬暂停;批 2a 来源感知扩展)──
168118 let paused_now = w. pause . paused . load ( Ordering :: SeqCst ) ;
169119 if paused_now {
170- if !w. worker_paused {
171- w. worker_paused = true ;
172- eprintln ! ( "[ScreenWorker] 会话暂停,画面链冻结(等恢复/来源解除)" ) ;
173- }
174- // 非 manual 锁存:auto 条件(media/fg)持有时轻量轮询找恢复信号。
175- // P2-4:门控扩到 fg 期——fg 暂停期也维持媒体检测(视频暂停/结束即
176- // 锁存 media;否则回位 fg 解除即伪恢复采集,见 light_poll_enabled
177- // 注释);fg 条件的解除仍由上方门控节拍负责(互不解除只解自己)
178- if light_poll_enabled ( w. pause . conditions ( ) ) {
179- // P2 自动暂停:轻量轮询——仅取帧刷新 latest_frame + 播放检测。
180- // 检测读的就是 latest_frame,不刷新则永远看到暂停帧 → 无法发现
181- // 恢复;1s 一拍仅取帧(零分析),5s 一拍检测(沿用 REQ-125 节流)
182- let comp_epoch = w. compensated_epoch ( ) ;
183- if w. last_sample_at . elapsed ( ) . as_millis ( ) as u64 >= SAMPLE_TICK_MS {
184- w. last_sample_at = Instant :: now ( ) ;
185- capture_latest_only (
186- w. screen . as_mut ( ) ,
187- & w. app ,
188- comp_epoch,
189- & w. latest_frame ,
190- & mut w. last_capture_error ,
191- & mut w. got_frame ,
192- ) ;
193- // 审查 F3:暂停期 watchdog 探针(无提示)——WGC 会话失活时
194- // 恢复检测永远读不到新帧 → 自动暂停永久卡死;此处定期自愈。
195- // P2-4:轻量轮询扩到 fg 期后探针随之覆盖 fg 期(fg 期视频
196- // 暂停/画面停更同样会饿死媒体恢复检测,需同款 WGC 自愈;
197- // 探针只观测 + 复活,任意暂停期无提示副作用)
198- watchdog_paused_probe (
199- w. screen . as_mut ( ) ,
200- & mut w. liveness ,
201- w. got_frame ,
202- Instant :: now ( ) ,
203- ) ;
204- // REQ-291 快恢复(主通道):声画任一恢复 ≤~1.5s 解除自动暂停
205- // (OCR 5s 判定降级为辅助——保留其后兜底)
206- if w. last_media_tick . elapsed ( ) >= Duration :: from_secs ( 1 ) {
207- w. last_media_tick = Instant :: now ( ) ;
208- let sound_recent = media_sound_recent ( & w. media_sound ) ;
209- if w. media_detector . tick ( sound_recent, w. got_frame )
210- == crate :: media_state:: MediaDecision :: Resume
211- {
212- let resume_ms = comp_epoch. elapsed ( ) . as_millis ( ) as u64 ;
213- crate :: player_behavior:: record_action (
214- & crate :: player_behavior:: PlayerAction {
215- kind : crate :: player_behavior:: PlayerActionKind :: Play ,
216- value : None ,
217- } ,
218- resume_ms,
219- w. session_id ,
220- & w. db ,
221- ) ;
222- // 批 2a:经 request API 解除媒体条件(只解自己——fg
223- // 仍锁存则暂停延续,本 worker 暂停分支继续等)
224- let _ = w. pause
225- . request_release ( crate :: pause_state:: PauseSource :: Media ) ;
226- w. last_player_paused = false ;
227- let _ = w. app . emit ( "live:media-resumed" , ( ) ) ;
228- eprintln ! ( "[ScreenWorker] 随播随停:声画恢复 → 自动解除暂停" ) ;
229- }
230- }
231- // P2-4:OCR 恢复判定只在 media 条件持有时有意义——fg-only
232- // 暂停期视频正常播放(帧无暂停图标),裸跑会把"播放中"误当
233- // 恢复沿,每 5s 落一次伪 Play/伪 live:media-resumed(时间轴
234- // 伪运行段);media 持有时仍是 REQ-125 恢复兜底(5s 判定)
235- if w. pause . media_held ( )
236- && w. last_player_check_at . elapsed ( ) >= Duration :: from_secs ( 5 )
237- {
238- w. last_player_check_at = Instant :: now ( ) ;
239- let check_now_ms = comp_epoch. elapsed ( ) . as_millis ( ) as u64 ;
240- if let Some ( f) = w. latest_frame . lock ( ) . ok ( ) . and_then ( |g| g. clone ( ) ) {
241- if let Some ( img) =
242- crate :: region_ocr:: bgra_to_rgb_image ( & f. bgraw , f. width , f. height )
243- {
244- let still_paused =
245- crate :: player_behavior:: detect_player_action ( & img) . is_some ( ) ;
246- if !still_paused {
247- // 恢复播放:落 Play 事件(REQ-125 语义一致)+
248- // 解除媒体条件(音频/捕获线程沿边沿自动恢复)
249- crate :: player_behavior:: record_action (
250- & crate :: player_behavior:: PlayerAction {
251- kind : crate :: player_behavior:: PlayerActionKind :: Play ,
252- value : None ,
253- } ,
254- check_now_ms,
255- w. session_id ,
256- & w. db ,
257- ) ;
258- let _ = w. pause
259- . request_release ( crate :: pause_state:: PauseSource :: Media ) ;
260- w. last_player_paused = false ;
261- let _ = w. app . emit ( "live:media-resumed" , ( ) ) ;
262- eprintln ! ( "[ScreenWorker] 视频恢复播放,自动解除暂停" ) ;
263- }
264- }
265- }
266- }
267- // v0.7.2(REQ-151):暂停态也探测播放器信息(时间文本仍在画面)——
268- // 会话开始时视频已暂停的场景,时长/集号识别不因此缺席
269- if w. last_info_probe_at . elapsed ( ) >= Duration :: from_secs ( 10 ) {
270- w. last_info_probe_at = Instant :: now ( ) ;
271- probe_player_info ( & w. app , & w. engines , & w. session_info , & w. roi_tracker , & w. latest_frame ) ;
272- }
273- }
274- }
120+ w. poll_while_paused ( ) ;
275121 std:: thread:: sleep ( Duration :: from_millis ( WORKER_POLL_MS ) ) ;
276122 continue ;
277123 }
0 commit comments