Skip to content

Latest commit

 

History

History
126 lines (89 loc) · 7.91 KB

File metadata and controls

126 lines (89 loc) · 7.91 KB

ADR-007: 采集会话生命周期与窗口解耦(持续不间断运行)

状态

已接受(Accepted,2026-08-18;四项机制已实施,真机验收待执行)

日期

2026-08-18

背景

课堂助手实时采集链路(v0.2.0)存在三类生命周期缺口,破坏"持续不间断运行"承诺:

  1. 关闭即杀:应用无 CloseRequested 拦截,用户点关闭直接退出进程——采集线程被杀,会话停留在 recording 状态,融合与 finish 不执行(仅 mark_interrupted_sessions 兜底标 failed,丢失停止流程产物)
  2. 音频中断即死:WASAPI 捕获在设备失效/默认设备切换(插拔耳机、设备被占用)时,run_capture_inner 返回 Err → 捕获线程退出 → channel Disconnected → 会话线程 break → 整个会话死亡,无重连机制
  3. 降级单向:DXGI 失效(锁屏/远程桌面/设备丢失)后永久降级 GDI,不重建 DXGI 主路径;window_rect 为启动时快照,窗口移动/缩放/分辨率变化后画面裁剪错位

已验证满足的行为(无需改动,需固化为测试):前端页面切换(display:none 不卸载,TD-004)与窗口最小化均不影响 Rust 后台采集线程。

约束:

  • 本地优先:恢复机制必须纯本地(无云端心跳)
  • 数据连续性:恢复后时间戳必须连续(会话时间轴基准不因重连重置)
  • 可观测:恢复/降级事件须推送前端(用户可感知"采集中/恢复中")
  • 关闭保护:仅在采集进行时拦截关闭;空闲时关闭行为与现在一致

决策

我们将实施以下四项机制:

1. 关闭保护(CloseRequested 拦截)

  • tauri::Builder::on_window_event 注册 CloseRequested 处理:AppState.live_session 有活动会话时 event.prevent_close() + emit app:close-requested
  • 前端监听该事件 → tauri-plugin-dialog(已注册)弹确认框:"当前正在进行采集,确定要停止并退出吗?"
    • 确认 → stop_live_session(含 5s 有界等待)→ 成功后再 window.close()(此时采集已停,CloseRequested 放行)
    • 取消 → 无操作,采集继续
  • 竞态(停止与关闭同时发生):关闭检查时 active 已取走则放行,残留由 mark_interrupted_sessions 兜底

2. 音频自动重连(指数退避 + 时间戳连续)

  • capture_loop 外层包重试循环:run_capture 返回 Err 且未收到 stop_flag → 指数退避(0.5s/1s/2s/4s…上限 10s)→ 重新走完整初始化(COM/设备/格式/环回)
  • 时间戳基准(started Instant)提升到重试循环外——重连不重置基准,时间轴连续("无缝续接"的关键)
  • 重连期间 channel 保持打开(会话线程 recv_timeout 超时分支继续轮询,不会 Disconnected)→ 会话不死亡
  • 重连触发时 emit live:recovering,成功后 emit live:recovered(前端状态徽标)
  • 触发条件:初始化失败(无设备/设备被占用)与运行期错误(AUDCLNT_E_DEVICE_INVALIDATED 等)统一走重连

3. 屏幕捕获自愈(DXGI 周期重建 + 窗口矩形刷新)

  • DXGI 周期重建:GDI 降级期间,每隔固定间隔(30s 或 N 次捕获)尝试 DxgiState::create 重建 DXGI 主路径;成功即切回(锁屏/远程桌面恢复后自动回 DXGI)
  • 窗口矩形每次捕获前刷新:GetWindowRect 不再只取启动快照——窗口移动/缩放/分辨率变化后裁剪自动跟随
  • 目标窗口失效回退:GetWindowRect 失败(窗口已关闭)→ 自动回退全屏捕获(window_rect 清零)+ emit live:window-lost 通知前端;画面链路不中断

4. 全局采集状态徽标(UX 固化)

  • 前端 App 顶层监听 live:status / live:recovering / live:recovered,导航栏常驻采集状态徽标("🎙 采集中"),页面切换/最小化后用户仍可感知采集在跑

备选方案

方案 A:完整生命周期管理(关闭保护 + 重连 + 自愈)(选择)

  • 优点:三类缺口一次补齐;恢复机制全部本地;时间戳连续保证数据可用性
  • 缺点:改动面较大(audio_loopback / dxgi_capture / lib / 前端);重连行为需实测调参(退避上限)
  • 适用场景:当前需求

方案 B:仅关闭保护(最小改动)

  • 优点:改动最小(仅 lib.rs + 前端确认框)
  • 缺点:音频中断即死问题仍在(插拔耳机即毁会话);DXGI 单向降级仍在
  • 适用场景:不满足"持续不间断"承诺;否决

方案 C:捕获常驻服务化(采集与会话解耦为独立常驻进程/服务)

  • 优点:隔离最强,UI 崩溃不影响采集
  • 缺点:进程间通信/生命周期管理复杂度高(跨进程 DB 竞争、模型加载翻倍);MVP 阶段过度设计
  • 适用场景:V1.0 后若出现"UI 频繁重启"需求再评估;当前否决

方案 D:WASAPI IMMNotificationClient 设备变更事件驱动重连

  • 优点:重连响应最快(事件驱动而非错误触发)
  • 缺点:COM 回调实现复杂度高(跨线程封送);与错误触发重连可叠加
  • 适用场景:作为 2 的后续增强(P2);当前用错误触发 + 退避

选择理由

  • 三类缺口(关闭即杀/音频即死/降级单向)都是真实破坏"持续不间断"的行为,须一次补齐(方案 A)
  • 音频错误触发 + 指数退避(方案 D 的简化版)已覆盖设备插拔/占用/失效三类场景,实现成本低且与现有错误路径天然结合(Err 已全路径返回)
  • 时间戳基准外提是"无缝"的关键——不这样做,重连后数据时间轴断裂,恢复无意义
  • 常驻服务化(方案 C)是 V1.0 之后的架构选项,MVP 阶段不做

影响

正面影响

  • 关闭保护:采集进行时误关不丢数据;停止流程(融合/finish)保证执行
  • 音频重连:插拔耳机/设备切换不毁会话;恢复后时间轴连续
  • 屏幕自愈:锁屏/远程桌面/分辨率变化后自动回 DXGI 主路径;窗口移动裁剪不错位
  • 可观测:恢复事件 + 全局徽标,用户对"后台仍在采集"有明确感知

负面影响 / 代价

  • audio_loopback.rs / dxgi_capture.rs 结构改动(重试循环/周期重建)
  • lib.rs 增加窗口事件处理;前端增加确认框与状态徽标
  • 重连参数(退避上限、重建间隔)需实测校准

风险

  • 重连风暴:设备长期不可用时退避循环持续打印错误——需日志节流(仅首次 + 每次退避升级)
  • 关闭竞态:确认后 stop 与用户再次点关闭的竞态窗口——以 active_session_id 为唯一判定源
  • 前端确认框依赖 dialog 插件权限(capabilities 已含?需核查 default.json)

合规性验证

  • 采集进行中关闭应用 → 弹出确认框;取消后采集继续;确认后会话正常 finish(非 failed)
  • 空闲时关闭应用 → 直接退出,无确认框
  • 采集中拔掉音频输出设备再插回 → 会话不死亡,恢复后时间戳连续(无跳变)
  • 锁屏/远程桌面往返 → DXGI 恢复后切回主路径(日志验证 dxgi 后端名)
  • 拖动窗口/改分辨率 → 画面裁剪跟随窗口新位置
  • 关闭目标窗口(网课窗口)→ 回退全屏 + 前端收到 window-lost 通知,会话继续
  • 前端页面切换/窗口最小化 → 采集持续(已有行为,测试固化)
  • cargo test 新增:重连退避状态机、时间戳连续性(纯逻辑部分)

相关决策

  • ADR-001: WASAPI 端点环回音频捕获方案(重连机制在其基础上扩展)
  • ADR-002: DXGI 屏幕捕获与关键帧变化检测方案(自愈机制在其基础上扩展)
  • ADR-004: 会话管理数据模型方案(failed 兜底语义不变)
  • REQ-031: 融合停止异步化(关闭确认后的停止流程复用其异步化产物)

参考