fix: MediaProjection权限丢失后无法回退无障碍截图和Web画面闪烁

- 采集循环每帧检测mediaProjection对象有效性,null时立即回退无障碍截图
- 统一fallbackToAccessibilityCapture方法,消除7处重复回退代码
- 无障碍截图失败后等待MIN_CAPTURE_INTERVAL剩余时间,避免截图间隔太短错误
- Web端canvas尺寸锁定策略:首帧锁定尺寸,后续帧统一缩放绘制,消除闪烁
This commit is contained in:
wdvipa
2026-02-15 18:41:38 +08:00
parent 4ac6078ca3
commit c63fbbd90f

View File

@@ -435,8 +435,7 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
} else { } else {
// MediaProjection 不可用,回退到无障碍截图 // MediaProjection 不可用,回退到无障碍截图
Log.i(TAG, "Android 11+MediaProjection 不可用,回退到无障碍截图模式") Log.i(TAG, "Android 11+MediaProjection 不可用,回退到无障碍截图模式")
enableAccessibilityScreenshotMode() fallbackToAccessibilityCapture()
startAccessibilityScreenCapture()
} }
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Log.i(TAG, "Android 5-10使用 MediaProjection VirtualDisplay 连续流式捕获") Log.i(TAG, "Android 5-10使用 MediaProjection VirtualDisplay 连续流式捕获")
@@ -464,13 +463,7 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
// 先确保 MediaProjection 可用 // 先确保 MediaProjection 可用
if (!ensureMediaProjection()) { if (!ensureMediaProjection()) {
Log.e(TAG, "MediaProjection 不可用,回退到无障碍截图") Log.e(TAG, "MediaProjection 不可用,回退到无障碍截图")
// 回退到无障碍截图作为兜底 fallbackToAccessibilityCapture()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
enableAccessibilityScreenshotMode()
startAccessibilityScreenCapture()
} else {
startFallbackCapture()
}
return@launch return@launch
} }
@@ -481,12 +474,7 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
if (virtualDisplay == null) { if (virtualDisplay == null) {
Log.e(TAG, "VirtualDisplay 创建失败,回退到无障碍截图") Log.e(TAG, "VirtualDisplay 创建失败,回退到无障碍截图")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { fallbackToAccessibilityCapture()
enableAccessibilityScreenshotMode()
startAccessibilityScreenCapture()
} else {
startFallbackCapture()
}
return@launch return@launch
} }
@@ -505,18 +493,22 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
while (isCapturing) { while (isCapturing) {
try { try {
// MediaProjection对象有效性检测
// onStop回调会将Holder中的对象清理为null
// 此时继续采集毫无意义,应立即回退到无障碍截图
if (mediaProjection == null) {
Log.w(TAG, "MediaProjection对象已失效(被系统回收或onStop清理),立即回退到无障碍截图")
cleanupVirtualDisplayOnly()
fallbackToAccessibilityCapture()
return@launch
}
// Surface有效性检测BufferQueue被系统abandoned后立即回退 // Surface有效性检测BufferQueue被系统abandoned后立即回退
// 防止acquireLatestImage()持续返回null触发无限重建循环
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已失效(null=${currentSurface == null}, isValid=${currentSurface?.isValid}), 回退到无障碍截图")
cleanupVirtualDisplayOnly() cleanupVirtualDisplayOnly()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { fallbackToAccessibilityCapture()
enableAccessibilityScreenshotMode()
startAccessibilityScreenCapture()
} else {
startFallbackCapture()
}
return@launch return@launch
} }
@@ -559,12 +551,7 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
Log.w(TAG, "MediaProjection连续${consecutiveBlackFrames}个黑屏帧,切换到无障碍截图模式") Log.w(TAG, "MediaProjection连续${consecutiveBlackFrames}个黑屏帧,切换到无障碍截图模式")
safeRecycleBitmap(bitmap) safeRecycleBitmap(bitmap)
cleanupVirtualDisplayOnly() cleanupVirtualDisplayOnly()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { fallbackToAccessibilityCapture()
enableAccessibilityScreenshotMode()
startAccessibilityScreenCapture()
} else {
startFallbackCapture()
}
return@launch return@launch
} }
@@ -600,12 +587,7 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
if (virtualDisplayRebuildCount > MAX_VIRTUAL_DISPLAY_REBUILD_ATTEMPTS) { if (virtualDisplayRebuildCount > MAX_VIRTUAL_DISPLAY_REBUILD_ATTEMPTS) {
Log.w(TAG, "重建次数已达上限,回退到无障碍截图") Log.w(TAG, "重建次数已达上限,回退到无障碍截图")
cleanupVirtualDisplayOnly() cleanupVirtualDisplayOnly()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { fallbackToAccessibilityCapture()
enableAccessibilityScreenshotMode()
startAccessibilityScreenCapture()
} else {
startFallbackCapture()
}
return@launch return@launch
} }
@@ -619,10 +601,7 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
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 重建后无效(vd=${virtualDisplay != null}, surface=${rebuiltSurface != null}, valid=${rebuiltSurface?.isValid}), 回退到无障碍截图")
cleanupVirtualDisplayOnly() cleanupVirtualDisplayOnly()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { fallbackToAccessibilityCapture()
enableAccessibilityScreenshotMode()
startAccessibilityScreenCapture()
}
return@launch return@launch
} }
} }
@@ -642,12 +621,25 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
} }
} }
/**
* 统一回退到无障碍截图的方法
* Android 11+ 使用无障碍截图,低版本使用测试图像兜底
*/
private fun fallbackToAccessibilityCapture() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
enableAccessibilityScreenshotMode()
startAccessibilityScreenCapture()
} else {
startFallbackCapture()
}
}
/** /**
* 确保 MediaProjection 可用 * 确保 MediaProjection 可用
* *
* 核心修复:禁止重复调用 getMediaProjection(resultCode, resultData) * 核心修复:禁止重复调用 getMediaProjection(resultCode, resultData)
* 每次调用都会创建新实例,系统会自动 stop 旧实例,触发 onStop 回调, * 每次调用都会创建新实例,系统会自动 stop 旧实例,触发 onStop 回调,
* 形成"权限丢失→恢复→再丢失"的死循环,这是权限频繁掉落的根因。 * 形成"权限丢失->恢复->再丢失"的死循环,这是权限频繁掉落的根因。
* *
* 只从 MediaProjectionHolder 获取已有对象,不重新创建。 * 只从 MediaProjectionHolder 获取已有对象,不重新创建。
*/ */
@@ -727,11 +719,19 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
} }
} }
// 截图成功时按帧率延迟,失败时短延迟后立即重试 // 截图成功时按帧率延迟,失败时等待最小截图间隔后重试
if (consecutiveFailures == 0) { if (consecutiveFailures == 0) {
delay(1000 / dynamicFps.toLong()) delay(1000 / dynamicFps.toLong())
} else { } else {
delay(50) // 失败时50ms后重试让系统截图间隔限制自己控制节奏 // 无障碍截图有系统级最小间隔限制(约3秒)
// 失败时等待剩余间隔时间,避免"截图间隔太短"错误
val elapsed = System.currentTimeMillis() - lastScreenshotTime
val remaining = MIN_CAPTURE_INTERVAL - elapsed
if (remaining > 0) {
delay(remaining)
} else {
delay(100)
}
} }
} catch (e: CancellationException) { } catch (e: CancellationException) {