fix: 修复MediaProjection误判回退到无障碍截图问题
- 移除采集循环中所有自动回退到无障碍截图的逻辑 - 黑屏帧不再触发模式切换,改为发送缓存帧保持画面连续 - Surface失效时尝试重建VirtualDisplay而非直接回退 - MediaProjection为null时从Holder重新获取而非回退 - 禁止服务端captureMode指令触发switchToAccessibilityMode - 无障碍截图模式统一固定3秒间隔(成功/失败/异常均等3秒)
This commit is contained in:
@@ -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秒再重试
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user