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