fix: Web端投屏画面缩小闪烁问题

- canvas尺寸锁定策略:首次有效帧锁定canvas尺寸,后续帧统一drawImage缩放绘制
- 消除不同采集模式(MediaProjection/无障碍截图)帧尺寸不一致导致的canvas反复resize
- 设备切换或断开时重置锁定尺寸,下次连接重新锁定
- 移除renderLatestFrame对screenDisplay.fitMode的无效依赖
This commit is contained in:
wdvipa
2026-02-15 18:41:47 +08:00
parent e3c5767024
commit 5ce99d8708

View File

@@ -38,10 +38,9 @@ const DeviceScreen: React.FC<DeviceScreenProps> = ({ deviceId, onScreenSizeChang
const isRenderingRef = useRef(false) const isRenderingRef = useRef(false)
const imageSizeRef = useRef<{ width: number, height: number } | null>(null) const imageSizeRef = useRef<{ width: number, height: number } | null>(null)
// ✅ 尺寸稳定性防止偶发异常帧导致canvas闪烁 // canvas锁定尺寸首次有效帧确定canvas尺寸后不再变化
const pendingSizeRef = useRef<{ width: number, height: number } | null>(null) // 避免不同采集模式(MediaProjection/无障碍截图)帧尺寸不一致导致闪烁
const pendingSizeCountRef = useRef(0) const lockedCanvasSizeRef = useRef<{ width: number, height: number } | null>(null)
const SIZE_STABLE_THRESHOLD = 3 // 连续3帧相同尺寸才更新
// ✅ 添加控制权状态跟踪,避免重复申请 // ✅ 添加控制权状态跟踪,避免重复申请
const [isControlRequested, setIsControlRequested] = useState(false) const [isControlRequested, setIsControlRequested] = useState(false)
@@ -172,6 +171,8 @@ const DeviceScreen: React.FC<DeviceScreenProps> = ({ deviceId, onScreenSizeChang
rafIdRef.current = 0 rafIdRef.current = 0
} }
isRenderingRef.current = false isRenderingRef.current = false
// 设备切换或断开时重置锁定尺寸,下次连接重新锁定
lockedCanvasSizeRef.current = null
} }
}, [webSocket, deviceId]) }, [webSocket, deviceId])
@@ -303,52 +304,33 @@ const DeviceScreen: React.FC<DeviceScreenProps> = ({ deviceId, onScreenSizeChang
return return
} }
// canvas尺寸匹配bitmap只在尺寸真正变化时才设置 // canvas尺寸锁定策略:
// 注意设置canvas.width/height会清空画布内容所以必须紧接着绘制新帧 // 首次有效帧确定canvas尺寸后锁定后续帧直接绘制到固定尺寸canvas上
const needsResize = canvas.width !== bitmap.width || canvas.height !== bitmap.height // 避免不同采集模式帧尺寸不一致导致canvas反复resize闪烁
if (needsResize) { const locked = lockedCanvasSizeRef.current
if (!locked) {
// 首次帧锁定canvas尺寸
lockedCanvasSizeRef.current = { width: bitmap.width, height: bitmap.height }
canvas.width = bitmap.width canvas.width = bitmap.width
canvas.height = bitmap.height canvas.height = bitmap.height
} else if (canvas.width !== locked.width || canvas.height !== locked.height) {
// canvas被外部重置了恢复锁定尺寸
canvas.width = locked.width
canvas.height = locked.height
} }
switch (screenDisplay.fitMode) { // 始终将bitmap绘制到整个canvas区域浏览器自动缩放
case 'fit': // 这样不同尺寸的帧都能正确显示,不会闪烁
drawFitMode(ctx, bitmap, canvas) ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height)
break
case 'fill':
drawFillMode(ctx, bitmap, canvas)
break
case 'stretch':
drawStretchMode(ctx, bitmap, canvas)
break
case 'original':
drawOriginalMode(ctx, bitmap, canvas)
break
}
const bw = bitmap.width // 使用锁定的canvas尺寸上报保持稳定
const bh = bitmap.height const reportSize = lockedCanvasSizeRef.current
if (reportSize) {
const prevSize = imageSizeRef.current const prevSize = imageSizeRef.current
if (!prevSize || prevSize.width !== bw || prevSize.height !== bh) { if (!prevSize || prevSize.width !== reportSize.width || prevSize.height !== reportSize.height) {
// 尺寸稳定性检查:只有连续多帧相同尺寸才更新,防止偶发异常帧闪烁 imageSizeRef.current = { width: reportSize.width, height: reportSize.height }
const pending = pendingSizeRef.current setImageSize({ width: reportSize.width, height: reportSize.height })
if (pending && pending.width === bw && pending.height === bh) {
pendingSizeCountRef.current++
} else {
pendingSizeRef.current = { width: bw, height: bh }
pendingSizeCountRef.current = 1
} }
if (pendingSizeCountRef.current >= SIZE_STABLE_THRESHOLD || !prevSize) {
imageSizeRef.current = { width: bw, height: bh }
setImageSize({ width: bw, height: bh })
pendingSizeRef.current = null
pendingSizeCountRef.current = 0
}
} else {
// 尺寸未变重置pending
pendingSizeRef.current = null
pendingSizeCountRef.current = 0
} }
const now = Date.now() const now = Date.now()
@@ -373,7 +355,7 @@ const DeviceScreen: React.FC<DeviceScreenProps> = ({ deviceId, onScreenSizeChang
} }
doRender() doRender()
}, [screenDisplay.fitMode]) }, [])
const drawFitMode = (ctx: CanvasRenderingContext2D, img: HTMLImageElement | ImageBitmap, canvas: HTMLCanvasElement) => { const drawFitMode = (ctx: CanvasRenderingContext2D, img: HTMLImageElement | ImageBitmap, canvas: HTMLCanvasElement) => {
const scale = Math.min(canvas.width / img.width, canvas.height / img.height) const scale = Math.min(canvas.width / img.width, canvas.height / img.height)