fix: 修复MediaProjection误判回退到无障碍截图问题

- 移除采集循环中所有自动回退到无障碍截图的逻辑
- 黑屏帧不再触发模式切换,改为发送缓存帧保持画面连续
- Surface失效时尝试重建VirtualDisplay而非直接回退
- MediaProjection为null时从Holder重新获取而非回退
- 禁止服务端captureMode指令触发switchToAccessibilityMode
- 无障碍截图模式统一固定3秒间隔(成功/失败/异常均等3秒)
This commit is contained in:
wdvipa
2026-02-15 19:21:04 +08:00
parent 92cfc10150
commit 56648697bb

View File

@@ -317,21 +317,10 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
/**
* 切换到无障碍截图模式(由服务端指令触发)
* 停止当前采集,切换到 AccessibilityService.takeScreenshot
* 已禁用:不再允许服务端黑帧检测触发模式切换,避免误判导致权限回退
*/
fun switchToAccessibilityMode() {
if (useAccessibilityScreenshot) {
Log.d(TAG, "已经在无障碍截图模式,跳过切换")
return
}
Log.i(TAG, "切换到无障碍截图模式")
// 停止当前采集
stopCapture()
// 清理 MediaProjection 资源
cleanupVirtualDisplayOnly()
// 启用无障碍截图并重新开始
enableAccessibilityScreenshotMode()
startCapture()
Log.i(TAG, "收到切换无障碍截图模式指令,已忽略(禁止服务端触发模式切换)")
}
/**
@@ -483,33 +472,47 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
var consecutiveFailures = 0
virtualDisplayRebuildCount = 0
// 最小有效帧大小阈值正常480×854 JPEG即使最低质量也>5KB
// 最小有效帧大小阈值正常480x854 JPEG即使最低质量也>5KB
// 低于此值的帧几乎肯定是黑屏/空白帧VirtualDisplay未刷新
val MIN_VALID_FRAME_SIZE = 5 * 1024 // 5KB
// 连续黑屏帧计数,用于判断是否应该切换到无障碍截图
// 连续黑屏帧计数 - 不再用于触发回退,仅用于日志统计
var consecutiveBlackFrames = 0
val MAX_BLACK_FRAMES_BEFORE_FALLBACK = 30 // 连续30个黑屏帧后切换到无障碍截图
while (isCapturing) {
try {
// MediaProjection对象有效性检测
// onStop回调会将Holder中的对象清理为null
// 此时继续采集毫无意义,应立即回退到无障碍截图
// 如果本地引用为null尝试从Holder重新获取
if (mediaProjection == null) {
Log.w(TAG, "MediaProjection对象已失效(被系统回收或onStop清理),立即回退到无障碍截图")
cleanupVirtualDisplayOnly()
fallbackToAccessibilityCapture()
return@launch
Log.w(TAG, "MediaProjection本地引用为null尝试从Holder重新获取")
mediaProjection = MediaProjectionHolder.getMediaProjection()
if (mediaProjection == null) {
Log.w(TAG, "Holder中也无有效MediaProjection等待权限恢复发送缓存帧")
val cachedJpeg = compressCachedFrame(30000)
if (cachedJpeg != null) {
sendFrameToServer(cachedJpeg)
}
delay(3000)
continue
}
Log.i(TAG, "从Holder重新获取到MediaProjection继续采集")
}
// Surface有效性检测BufferQueue被系统abandoned后立即回退
// Surface有效性检测BufferQueue被系统abandoned后尝试重建
val currentSurface = imageReader?.surface
if (currentSurface == null || !currentSurface.isValid) {
Log.w(TAG, "ImageReader Surface已失效(null=${currentSurface == null}, isValid=${currentSurface?.isValid}), 回退到无障碍截图")
Log.w(TAG, "ImageReader Surface已失效, 尝试重建VirtualDisplay")
cleanupVirtualDisplayOnly()
fallbackToAccessibilityCapture()
return@launch
delay(500)
if (ensureMediaProjection()) {
setupMediaProjectionResources()
}
val rebuiltSurface = imageReader?.surface
if (rebuiltSurface == null || !rebuiltSurface.isValid) {
Log.w(TAG, "Surface重建失败等待下次循环重试")
}
delay(1000)
continue
}
// 安全获取Image防止maxImages溢出
@@ -541,25 +544,17 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
consecutiveBlackFrames = 0
Log.d(TAG, "MediaProjection 有效帧: ${jpegData.size} bytes")
} else {
// 疑似黑屏帧
// 疑似黑屏帧,仅记录日志,不触发回退
// MediaProjection仍在工作黑屏帧可能是短暂的屏幕状态变化
consecutiveBlackFrames++
consecutiveFailures++
Log.w(TAG, "黑屏帧(${jpegData.size}B < ${MIN_VALID_FRAME_SIZE}B)连续${consecutiveBlackFrames}")
// 连续黑屏帧过多MediaProjection在此设备上不可靠切换到无障碍截图
if (consecutiveBlackFrames >= MAX_BLACK_FRAMES_BEFORE_FALLBACK) {
Log.w(TAG, "MediaProjection连续${consecutiveBlackFrames}个黑屏帧,切换到无障碍截图模式")
safeRecycleBitmap(bitmap)
cleanupVirtualDisplayOnly()
fallbackToAccessibilityCapture()
return@launch
if (consecutiveBlackFrames % 50 == 1) {
Log.w(TAG, "黑屏帧(${jpegData.size}B < ${MIN_VALID_FRAME_SIZE}B), 连续${consecutiveBlackFrames}, 发送缓存帧")
}
// Send cached valid frame to maintain continuity (if available)
// 发送缓存的有效帧保持画面连续性
val cachedJpeg = compressCachedFrame(10000)
if (cachedJpeg != null && cachedJpeg.size >= MIN_VALID_FRAME_SIZE) {
sendFrameToServer(cachedJpeg)
Log.d(TAG, "Used cached frame instead of black frame")
}
}
@@ -583,26 +578,24 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
virtualDisplayRebuildCount++
Log.w(TAG, "连续 ${consecutiveFailures} 次无法获取有效帧,重建 VirtualDisplay (${virtualDisplayRebuildCount}/${MAX_VIRTUAL_DISPLAY_REBUILD_ATTEMPTS})")
// 重建次数超限,回退到无障碍截图
if (virtualDisplayRebuildCount > MAX_VIRTUAL_DISPLAY_REBUILD_ATTEMPTS) {
Log.w(TAG, "重建次数已达上限,回退到无障碍截图")
cleanupVirtualDisplayOnly()
fallbackToAccessibilityCapture()
return@launch
}
cleanupVirtualDisplayOnly()
delay(500)
// 重新检查MediaProjection是否可用
if (!ensureMediaProjection()) {
Log.w(TAG, "MediaProjection不可用等待恢复继续发送缓存帧")
consecutiveFailures = 0
continue
}
setupMediaProjectionResources()
consecutiveFailures = 0
// 重建后验证VirtualDisplay和Surface有效性
val rebuiltSurface = imageReader?.surface
if (virtualDisplay == null || rebuiltSurface == null || !rebuiltSurface.isValid) {
Log.w(TAG, "VirtualDisplay 重建后无效(vd=${virtualDisplay != null}, surface=${rebuiltSurface != null}, valid=${rebuiltSurface?.isValid}), 回退到无障碍截图")
cleanupVirtualDisplayOnly()
fallbackToAccessibilityCapture()
return@launch
Log.w(TAG, "VirtualDisplay 重建后无效,等待下次重试")
// 不回退,继续循环,下次会再尝试重建
}
}
}
@@ -719,19 +712,14 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
}
}
// 截图成功时按帧率延迟,失败时等待最小截图间隔后重试
if (consecutiveFailures == 0) {
delay(1000 / dynamicFps.toLong())
// 无障碍截图统一使用固定3秒间隔不区分成功/失败
// 系统对无障碍截图有约3秒的最小间隔限制低于此值会报错
val elapsed = System.currentTimeMillis() - lastScreenshotTime
val remaining = MIN_CAPTURE_INTERVAL - elapsed
if (remaining > 0) {
delay(remaining)
} else {
// 无障碍截图有系统级最小间隔限制(约3秒)
// 失败时等待剩余间隔时间,避免"截图间隔太短"错误
val elapsed = System.currentTimeMillis() - lastScreenshotTime
val remaining = MIN_CAPTURE_INTERVAL - elapsed
if (remaining > 0) {
delay(remaining)
} else {
delay(100)
}
delay(MIN_CAPTURE_INTERVAL)
}
} catch (e: CancellationException) {
@@ -739,7 +727,7 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
} catch (e: Exception) {
Log.w(TAG, "屏幕捕获失败: ${e.message}")
consecutiveFailures++
delay(100) // 出错时进一步缩短间隔保持流畅度从200ms改为100ms
delay(MIN_CAPTURE_INTERVAL) // 异常时也等待3秒再重试
}
}
}