diff --git a/src/components/Device/DeviceScreen.tsx b/src/components/Device/DeviceScreen.tsx index e2bf319..8cad483 100644 --- a/src/components/Device/DeviceScreen.tsx +++ b/src/components/Device/DeviceScreen.tsx @@ -38,10 +38,13 @@ const DeviceScreen: React.FC = ({ deviceId, onScreenSizeChang const isRenderingRef = useRef(false) const imageSizeRef = useRef<{ width: number, height: number } | null>(null) - // canvas锁定尺寸:首次有效帧确定canvas尺寸后不再变化, + // canvas锁定尺寸:取历史最大帧尺寸锁定, // 避免不同采集模式(MediaProjection/无障碍截图)帧尺寸不一致导致闪烁 const lockedCanvasSizeRef = useRef<{ width: number, height: number } | null>(null) + // Base64预解码缓存:将base64字符串提前转为Blob,避免渲染时阻塞主线程 + const pendingBlobRef = useRef(null) + // 添加控制权状态跟踪,避免重复申请 const [isControlRequested, setIsControlRequested] = useState(false) const [currentWebSocket, setCurrentWebSocket] = useState(null) @@ -147,17 +150,43 @@ const DeviceScreen: React.FC = ({ deviceId, onScreenSizeChang console.log(`[屏幕数据] 已收到 ${frameCountRef.current} 帧, 数据大小: ${dataLen}, 渲染中: ${isRenderingRef.current}, 解码中: ${decodingRef.current}`) } - // 只保存最新帧引用,不立即解码 - latestFrameRef.current = data + // 提前将Base64转为Blob,避免渲染循环中阻塞主线程 + // fetch+data URI方式比atob+逐字节复制快得多,且不阻塞主线程 + const format = data?.format ?? 'JPEG' + const mimeType = `image/${format.toLowerCase()}` + + if (typeof data.data === 'string') { + try { + const dataUri = `data:${mimeType};base64,${data.data}` + fetch(dataUri) + .then(res => res.blob()) + .then(blob => { + pendingBlobRef.current = blob + latestFrameRef.current = data + + if (!isRenderingRef.current) { + isRenderingRef.current = true + renderLatestFrame() + } + }) + .catch(err => { + console.error('Base64 pre-decode failed:', err) + }) + } catch (err) { + console.error('Frame pre-decode error:', err) + } + } else { + pendingBlobRef.current = new Blob([data.data], { type: mimeType }) + latestFrameRef.current = data + + if (!isRenderingRef.current) { + isRenderingRef.current = true + renderLatestFrame() + } + } // 只在首帧时更新loading状态,避免每帧触发重渲染 if (isLoading) setIsLoading(false) - - // 如果没有正在进行的渲染循环,启动一个 - if (!isRenderingRef.current) { - isRenderingRef.current = true - renderLatestFrame() - } } } @@ -293,14 +322,16 @@ const DeviceScreen: React.FC = ({ deviceId, onScreenSizeChang const renderLatestFrame = useCallback(() => { const doRender = () => { const frameData = latestFrameRef.current - if (!frameData) { + const preDecodedBlob = pendingBlobRef.current + if (!frameData || !preDecodedBlob) { // 没有新帧,停止循环,等 handleScreenData 重新启动 isRenderingRef.current = false return } - // 取走帧数据 + // 取走帧数据和预解码Blob latestFrameRef.current = null + pendingBlobRef.current = null const canvas = canvasRef.current if (!canvas) { @@ -316,27 +347,15 @@ const DeviceScreen: React.FC = ({ deviceId, onScreenSizeChang // 上一帧还在解码,把数据放回去,下个 rAF 再试 if (decodingRef.current) { latestFrameRef.current = frameData + pendingBlobRef.current = preDecodedBlob rafIdRef.current = requestAnimationFrame(doRender) return } decodingRef.current = true - let blobPromise: Promise - if (typeof frameData.data === 'string') { - const binaryStr = atob(frameData.data) - const len = binaryStr.length - const bytes = new Uint8Array(len) - for (let i = 0; i < len; i++) { - bytes[i] = binaryStr.charCodeAt(i) - } - blobPromise = Promise.resolve(new Blob([bytes], { type: `image/${frameData.format.toLowerCase()}` })) - } else { - blobPromise = Promise.resolve(new Blob([frameData.data], { type: `image/${frameData.format.toLowerCase()}` })) - } - - blobPromise - .then(blob => createImageBitmap(blob)) + // 直接使用预解码的Blob,跳过Base64解码步骤 + createImageBitmap(preDecodedBlob) .then(bitmap => { decodingRef.current = false @@ -347,23 +366,32 @@ const DeviceScreen: React.FC = ({ deviceId, onScreenSizeChang return } - // canvas尺寸锁定策略: - // 首次有效帧确定canvas尺寸后锁定,后续帧直接绘制到固定尺寸canvas上 - // 避免不同采集模式帧尺寸不一致导致canvas反复resize闪烁 + // canvas尺寸锁定策略(增强版): + // 取历史最大帧尺寸锁定canvas,避免小帧导致画面缩小闪烁 + // 当新帧比锁定尺寸大时,更新锁定尺寸(设备旋转等场景) const locked = lockedCanvasSizeRef.current if (!locked) { // 首次帧:锁定canvas尺寸 lockedCanvasSizeRef.current = { width: bitmap.width, height: bitmap.height } canvas.width = bitmap.width canvas.height = bitmap.height - } else if (canvas.width !== locked.width || canvas.height !== locked.height) { - // canvas被外部重置了,恢复锁定尺寸 - canvas.width = locked.width - canvas.height = locked.height + } else { + // 后续帧:只在新帧更大时更新锁定尺寸,防止缩小闪烁 + const needUpdate = bitmap.width > locked.width || bitmap.height > locked.height + if (needUpdate) { + const newW = Math.max(locked.width, bitmap.width) + const newH = Math.max(locked.height, bitmap.height) + lockedCanvasSizeRef.current = { width: newW, height: newH } + canvas.width = newW + canvas.height = newH + } else if (canvas.width !== locked.width || canvas.height !== locked.height) { + // canvas被外部重置了,恢复锁定尺寸 + canvas.width = locked.width + canvas.height = locked.height + } } // 始终将bitmap绘制到整个canvas区域,浏览器自动缩放 - // 这样不同尺寸的帧都能正确显示,不会闪烁 ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height) // 使用锁定的canvas尺寸上报,保持稳定 @@ -392,7 +420,7 @@ const DeviceScreen: React.FC = ({ deviceId, onScreenSizeChang }) .catch(err => { decodingRef.current = false - console.error('图像解码失败:', err) + console.error('Image decode failed:', err) rafIdRef.current = requestAnimationFrame(doRender) }) }