import React, { useRef, useEffect, useState, useCallback } from 'react'
import { Card, Spin } from 'antd'
import { useSelector } from 'react-redux'
import type { RootState } from '../../store/store'

/** 画质档位（参�?billd-desk 的参数化控制�?*/
const QUALITY_PROFILES = [
  { key: 'low', label: 'Low', fps: 5, quality: 30, resolution: '360P' },
  { key: 'medium', label: 'Medium', fps: 10, quality: 45, resolution: '480P' },
  { key: 'high', label: 'High', fps: 15, quality: 60, resolution: '720P' },
  { key: 'ultra', label: 'Ultra', fps: 20, quality: 75, resolution: '1080P' },
]

interface DeviceScreenProps {
  deviceId: string
  onScreenSizeChange?: (size: { width: number, height: number }) => void
}

const MIN_VALID_BASE64_FRAME_LENGTH = 4000
const MIN_VALID_BINARY_FRAME_BYTES = 1200
const WEBRTC_CONNECT_TIMEOUT_MS = 12000
const WEBRTC_DISCONNECT_GRACE_MS = 2800
const WEBRTC_STATS_CHECK_INTERVAL_MS = 2000
const WEBRTC_MAX_RECONNECT_ATTEMPTS = 2
const WEBRTC_RESTART_COOLDOWN_MS = 1500
const WEBRTC_ADAPTIVE_RETRY_DELAY_MS = 900
const FORCE_WS_VIDEO_ONLY = false
const WS_VIDEO_FALLBACK_ENABLED = true
type VideoTransportMode = 'ws_binary' | 'webrtc'
type VideoSessionMode = 'ws_default' | 'webrtc_boost' | 'fallback_ws'
const DEFAULT_VIDEO_TRANSPORT: VideoTransportMode = 'ws_binary'
const DEFAULT_VIDEO_SESSION_MODE: VideoSessionMode = 'ws_default'
const WEBRTC_AUTO_RECONNECT_ENABLED = false
const WEBRTC_AUTO_PROJECTION_RECOVERY_ENABLED = false
const VIDEO_SESSION_MODE_LABEL: Record<VideoSessionMode, string> = {
  ws_default: 'WS DEFAULT',
  webrtc_boost: 'BOOST',
  fallback_ws: 'WS FALLBACK'
}
type WebRtcProfile = {
  label: string
  fps: number
  maxLongEdge: number
  bitrateKbps: number
  priority: 'smooth' | 'balanced' | 'quality'
}
const WEBRTC_PROFILES: WebRtcProfile[] = [
  { label: 'smooth-h', fps: 40, maxLongEdge: 960, bitrateKbps: 2200, priority: 'smooth' },
  { label: 'smooth-m', fps: 32, maxLongEdge: 840, bitrateKbps: 1600, priority: 'smooth' },
  { label: 'smooth-l', fps: 24, maxLongEdge: 720, bitrateKbps: 1100, priority: 'smooth' }
]
const WEBRTC_DEBUG_EVENT_LIMIT = 50
const MEDIA_PROJECTION_REFRESH_COOLDOWN_MS = 12000
const MEDIA_PROJECTION_AUTORETRY_DELAY_MS = 7000

type WebRtcDebugState = {
  transport: VideoTransportMode
  profileLabel: string
  reconnectAttempts: number
  weakNetworkStrike: number
  stableNetworkStrike: number
  queuedRemoteIce: number
  localIceSent: number
  remoteIceReceived: number
  remoteIceApplied: number
  connectionState: string
  iceConnectionState: string
  signalingState: string
  inboundFps: number
  lossRate: number
  lastReason: string
  lastError: string
  lastDeviceState: string
  lastDeviceDetail: string
  updatedAt: number
  events: string[]
}

const createWebRtcDebugState = (transport: VideoTransportMode): WebRtcDebugState => ({
  transport,
  profileLabel: '-',
  reconnectAttempts: 0,
  weakNetworkStrike: 0,
  stableNetworkStrike: 0,
  queuedRemoteIce: 0,
  localIceSent: 0,
  remoteIceReceived: 0,
  remoteIceApplied: 0,
  connectionState: '-',
  iceConnectionState: '-',
  signalingState: '-',
  inboundFps: 0,
  lossRate: 0,
  lastReason: '',
  lastError: '',
  lastDeviceState: '',
  lastDeviceDetail: '',
  updatedAt: Date.now(),
  events: []
})

const formatWebRtcDeviceDetail = (detail: any): string => {
  if (!detail || typeof detail !== 'object') return ''
  const detailObj = detail as Record<string, any>
  const entries: string[] = []
  if (typeof detailObj.phase === 'string' && detailObj.phase) entries.push(`phase=${detailObj.phase}`)
  if (typeof detailObj.exceptionClass === 'string' && detailObj.exceptionClass) entries.push(`ex=${detailObj.exceptionClass}`)
  if (typeof detailObj.exceptionMessage === 'string' && detailObj.exceptionMessage) entries.push(`msg=${detailObj.exceptionMessage}`)
  if (typeof detailObj.permissionLike === 'boolean') entries.push(`permissionLike=${detailObj.permissionLike}`)
  if (typeof detailObj.hasPermissionData === 'boolean') entries.push(`hasPermissionData=${detailObj.hasPermissionData}`)
  if (typeof detailObj.hasProjectionObject === 'boolean') entries.push(`hasProjectionObject=${detailObj.hasProjectionObject}`)

  if (entries.length > 0) {
    return entries.join(' | ')
  }

  try {
    return JSON.stringify(detailObj)
  } catch {
    return ''
  }
}

const shouldUseWsScreenshotFallback = (reason: string): boolean => {
  const normalized = reason.trim().toLowerCase()
  if (!normalized) return false

  // No MediaProjection permission on Android SRT uploader.
  if (normalized.includes('media_projection_not_ready')) return true
  if (normalized.includes('projection')) return true
  if (normalized.includes('permission')) return true
  return false
}

const normalizeScreenDataBinArgs = (args: any[]): { deviceId: string, meta: any, frame: any } | null => {
  if (!args.length) return null

  let emittedDeviceId = ''
  let meta: any = {}
  let frame: any = null

  if (typeof args[0] === 'string' && args.length >= 3) {
    emittedDeviceId = args[0]
    meta = args[1] ?? {}
    frame = args[2]
  } else if (
    args.length >= 2 &&
    args[0] &&
    typeof args[0] === 'object' &&
    !(args[0] instanceof Blob) &&
    !(args[0] instanceof ArrayBuffer) &&
    !ArrayBuffer.isView(args[0])
  ) {
    meta = args[0] ?? {}
    frame = args[1]
  } else if (args.length === 1 && args[0] && typeof args[0] === 'object') {
    const payload = args[0]
    emittedDeviceId = typeof payload.deviceId === 'string' ? payload.deviceId : ''
    meta = payload.meta ?? payload
    frame = payload.frame ?? payload.data
  } else {
    return null
  }

  const metaDeviceId = typeof meta?.deviceId === 'string' ? meta.deviceId : ''
  const resolvedDeviceId = metaDeviceId || emittedDeviceId
  if (!resolvedDeviceId) return null

  return {
    deviceId: resolvedDeviceId,
    meta: meta ?? {},
    frame
  }
}

const toBinaryFrameBlob = (frame: any, mimeType: string): Blob | null => {
  if (!frame) return null

  if (frame instanceof Blob) return frame
  if (frame instanceof ArrayBuffer) return new Blob([frame], { type: mimeType })
  if (ArrayBuffer.isView(frame)) return new Blob([frame], { type: mimeType })
  if (frame?.type === 'Buffer' && Array.isArray(frame.data)) return new Blob([new Uint8Array(frame.data)], { type: mimeType })
  if (Array.isArray(frame)) return new Blob([new Uint8Array(frame)], { type: mimeType })

  return null
}

const normalizeSdpLineEndings = (sdp: string): string => {
  if (!sdp) return ''
  const lines = sdp
    .replace(/\r\n/g, '\n')
    .replace(/\r/g, '\n')
    .split('\n')
    .map((line) => line.trimEnd())
    .filter((line) => line.length > 0)
  if (!lines.length) return ''
  return `${lines.join('\r\n')}\r\n`
}

const buildAndroidCompatOfferSdp = (sdp: string): { sdp: string, changed: boolean } => {
  const normalized = normalizeSdpLineEndings(sdp)
  if (!normalized) return { sdp: normalized, changed: false }
  const lines = normalized.split('\r\n').filter(Boolean)
  const rewritten: string[] = []
  let changed = false

  for (const line of lines) {
    const lower = line.toLowerCase()
    if (lower === 'a=extmap-allow-mixed' || lower === 'a=rtcp-rsize') {
      changed = true
      continue
    }
    if (lower.startsWith('a=ice-options:')) {
      const options = line
        .slice('a=ice-options:'.length)
        .trim()
        .split(/\s+/)
        .filter((item) => item && item.toLowerCase() !== 'renomination')
      const rewrittenLine = options.length ? `a=ice-options:${options.join(' ')}` : ''
      if (rewrittenLine !== line) changed = true
      if (rewrittenLine) rewritten.push(rewrittenLine)
      continue
    }
    rewritten.push(line)
  }

  const compat = rewritten.length ? `${rewritten.join('\r\n')}\r\n` : normalized
  return {
    sdp: compat,
    changed: changed || compat !== normalized
  }
}

/**
 * 璁惧灞忓箷鏄剧ず缁勪欢
 */
const DeviceScreen: React.FC<DeviceScreenProps> = ({ deviceId, onScreenSizeChange }) => {
  // const dispatch = useDispatch<AppDispatch>()
  const canvasRef = useRef<HTMLCanvasElement>(null)
  const srtVideoRef = useRef<HTMLVideoElement>(null)
  const fullscreenContainerRef = useRef<HTMLDivElement>(null)
  const [isLoading, setIsLoading] = useState(true)
  const [imageSize, setImageSize] = useState<{ width: number, height: number } | null>(null)
  const [isFullscreen, setIsFullscreen] = useState(false)
  const [videoTransport, setVideoTransport] = useState<VideoTransportMode>(
    FORCE_WS_VIDEO_ONLY ? 'ws_binary' : DEFAULT_VIDEO_TRANSPORT
  )
  const videoTransportRef = useRef<VideoTransportMode>(
    FORCE_WS_VIDEO_ONLY ? 'ws_binary' : DEFAULT_VIDEO_TRANSPORT
  )
  const [videoSessionMode, setVideoSessionMode] = useState<VideoSessionMode>(DEFAULT_VIDEO_SESSION_MODE)
  const videoSessionModeRef = useRef<VideoSessionMode>(DEFAULT_VIDEO_SESSION_MODE)
  const [srtStatusText, setSrtStatusText] = useState('')
  const srtProfileLabel = 'WebRTC'
  const webrtcPeerRef = useRef<RTCPeerConnection | null>(null)
  const webrtcRemoteStreamRef = useRef<MediaStream | null>(null)
  const pendingRemoteCandidatesRef = useRef<RTCIceCandidateInit[]>([])
  const webrtcConnectedRef = useRef(false)
  const webrtcLowFpsStrikeRef = useRef(0)
  
  // FPS 璁＄畻锛氫娇鐢ㄦ粦鍔ㄧ獥鍙ｇ粺璁＄湡瀹炲抚鐜?
  const fpsFrameTimesRef = useRef<number[]>([])
  const [displayFps, setDisplayFps] = useState(0)
  const lastFpsUpdateRef = useRef(0)
  
  // 甯ф覆鏌撴帶鍒讹細鍙繚鐣欐渶鏂板抚锛岀�?rAF 椹卞姩娓叉煋
  const latestFrameRef = useRef<any>(null)
  const rafIdRef = useRef<number>(0)
  const isRenderingRef = useRef(false)
  const imageSizeRef = useRef<{ width: number, height: number } | null>(null)
  
  // canvas閿佸畾灏哄锛氬彇鍘嗗彶鏈€澶у抚灏哄閿佸畾�?
  // 閬垮厤涓嶅悓閲囬泦妯″紡(MediaProjection/鏃犻殰纰嶆埅�?甯у昂瀵镐笉涓€鑷村鑷撮棯鐑?
  const lockedCanvasSizeRef = useRef<{ width: number, height: number } | null>(null)
  
  // Base64棰勮В鐮佺紦瀛橈細灏哹ase64瀛楃涓叉彁鍓嶈浆涓築lob锛岄伩鍏嶆覆鏌撴椂闃诲涓荤嚎绋?
  const pendingBlobRef = useRef<Blob | null>(null)
  
  // 娣诲姞鎺у埗鏉冪姸鎬佽窡韪紝閬垮厤閲嶅鐢宠�?
  const [isControlRequested, setIsControlRequested] = useState(false)
  const [currentWebSocket, setCurrentWebSocket] = useState<any>(null)
  const [lastControlRequestTime, setLastControlRequestTime] = useState<number>(0)
  const [webrtcTurnUrls, setWebrtcTurnUrls] = useState('')
  const [webrtcTurnUsername, setWebrtcTurnUsername] = useState('')
  const [webrtcTurnPassword, setWebrtcTurnPassword] = useState('')
  
  const { webSocket } = useSelector((state: RootState) => state.connection)
  const { connectedDevices } = useSelector((state: RootState) => state.devices)
  const { screenDisplay, operationEnabled } = useSelector((state: RootState) => state.ui)
  const exposurePercent = Math.max(30, Math.min(200, Math.round(Number(screenDisplay?.exposure ?? 100) || 100)))
  const exposureFilter = `brightness(${(exposurePercent / 100).toFixed(2)})`

  const device = connectedDevices.find(d => d.id === deviceId)
  const deviceTurnUrls = String((device as any)?.webrtcTurnUrls || '').trim()
  const deviceTurnUsername = String((device as any)?.webrtcTurnUsername || '').trim()
  const deviceTurnPassword = String((device as any)?.webrtcTurnPassword || '').trim()
  const deviceSupportsWebRtc = (() => {
    const transports = Array.isArray((device as any)?.supportedVideoTransports)
      ? ((device as any).supportedVideoTransports as unknown[])
      : []
    if (!transports.length) return true
    return transports.some((item) => typeof item === 'string' && item.toLowerCase() === 'webrtc')
  })()

  // 鐢昏川鎺у埗鐘舵€?
  const [currentProfile, setCurrentProfile] = useState('medium')
  const [showQualityPanel, setShowQualityPanel] = useState(false)
  const [showWebRtcDebugPanel, setShowWebRtcDebugPanel] = useState(false)
  const [networkStats, setNetworkStats] = useState({ fps: 0, dropRate: 0, avgFrameSize: 0 })
  const [webrtcDebug, setWebrtcDebug] = useState<WebRtcDebugState>(() =>
    createWebRtcDebugState(FORCE_WS_VIDEO_ONLY ? 'ws_binary' : DEFAULT_VIDEO_TRANSPORT)
  )
  const [webrtcCopyStatus, setWebrtcCopyStatus] = useState('')
  const frameCountRef = useRef(0)
  const droppedFrameCountRef = useRef(0)
  const feedbackTimerRef = useRef<number>(0)
  const lastFeedbackTimeRef = useRef(0)
  const copyStatusTimerRef = useRef<number>(0)
  const projectionPermissionRetryTimerRef = useRef<number>(0)
  const lastProjectionRefreshRequestTimeRef = useRef(0)

  const updateWebRtcDebug = useCallback((patch: Partial<WebRtcDebugState>, event?: string) => {
    setWebrtcDebug((prev) => {
      const nextUpdatedAt = Date.now()
      const next: WebRtcDebugState = {
        ...prev,
        ...patch,
        updatedAt: nextUpdatedAt
      }
      if (event) {
        const time = new Date(nextUpdatedAt).toISOString().slice(11, 19)
        next.events = [`${time} ${event}`, ...prev.events].slice(0, WEBRTC_DEBUG_EVENT_LIMIT)
      }
      return next
    })
  }, [])

  const pushWebRtcDebugEvent = useCallback((event: string) => {
    updateWebRtcDebug({}, event)
  }, [updateWebRtcDebug])

  const buildWebRtcDiagnosisHints = useCallback((debug: WebRtcDebugState): string[] => {
    const hints: string[] = []
    const lowerReason = debug.lastReason.toLowerCase()
    const lowerError = debug.lastError.toLowerCase()
    const hasProjectionIssue =
      lowerReason.includes('projection') ||
      lowerReason.includes('permission') ||
      lowerError.includes('projection') ||
      lowerError.includes('permission')

    if (debug.transport !== 'webrtc') {
      hints.push('当前视频通道已回退到 WS 截图流，WebRTC 未在工作。')
    }

    if (debug.connectionState === 'connecting' && debug.signalingState === 'have-local-offer') {
      hints.push('连接卡在 have-local-offer，优先检查服务端是否收到 offer 并回传 answer。')
    }

    if (!hasProjectionIssue && debug.localIceSent > 0 && debug.remoteIceReceived === 0) {
      hints.push('本地 ICE 已发送但远端 ICE 未收到，优先检查 signaling 转发和 device 端候选上报。')
    }

    if (debug.remoteIceReceived > 0 && debug.remoteIceApplied === 0) {
      hints.push('远端 ICE 已收到但未应用，可能是 remoteDescription 未设置成功。')
    }

    if (debug.connectionState === 'failed' || debug.iceConnectionState === 'failed') {
      hints.push('ICE/PeerConnection 已 failed，建议先在同局域网只保留 host candidate 验证链路。')
    }

    if (debug.reconnectAttempts >= WEBRTC_MAX_RECONNECT_ATTEMPTS) {
      hints.push('重试次数已到上限，建议先看最后一次 error/reason 再决定是修 signaling 还是修权限。')
    }

    if (debug.lossRate > 0.15) {
      hints.push('实时丢包超过 15%，弱网明显，建议优先降低码率和帧率。')
    }

    if (debug.inboundFps > 0 && debug.inboundFps < 10) {
      hints.push('inbound FPS 偏低，当前更像“可连通但传输质量差”。')
    }

    if (hasProjectionIssue) {
      hints.push('日志命中投屏/权限关键字，先确认 Android 端 MediaProjection 授权链路。')
    }

    if (!hints.length) {
      hints.push('未检测到明显单点故障，请结合事件时间线和 server/device 日志继续定位。')
    }

    return hints
  }, [])

  const setCopyStatusWithAutoClear = useCallback((statusText: string) => {
    setWebrtcCopyStatus(statusText)
    if (copyStatusTimerRef.current) {
      clearTimeout(copyStatusTimerRef.current)
      copyStatusTimerRef.current = 0
    }
    copyStatusTimerRef.current = window.setTimeout(() => {
      setWebrtcCopyStatus('')
      copyStatusTimerRef.current = 0
    }, 3500)
  }, [])

  const copyTextToClipboard = useCallback(async (text: string): Promise<boolean> => {
    if (!text) return false
    try {
      if (navigator?.clipboard?.writeText) {
        await navigator.clipboard.writeText(text)
        return true
      }
    } catch {
      // fallback
    }

    try {
      const textArea = document.createElement('textarea')
      textArea.value = text
      textArea.style.position = 'fixed'
      textArea.style.left = '-9999px'
      textArea.style.top = '0'
      textArea.setAttribute('readonly', 'true')
      document.body.appendChild(textArea)
      textArea.focus()
      textArea.select()
      const copied = document.execCommand('copy')
      document.body.removeChild(textArea)
      return copied
    } catch {
      return false
    }
  }, [])

  const buildWebRtcDiagnosticReport = useCallback(() => {
    const nowIso = new Date().toISOString()
    const hints = buildWebRtcDiagnosisHints(webrtcDebug)
    const eventsText = webrtcDebug.events.length > 0 ? webrtcDebug.events.join('\n') : '(empty)'
    const lines: string[] = [
      '=== WebRTC Diagnostic Report ===',
      `time: ${nowIso}`,
      `deviceId: ${deviceId}`,
      `page: ${typeof window !== 'undefined' ? window.location.href : '-'}`,
      `userAgent: ${typeof navigator !== 'undefined' ? navigator.userAgent : '-'}`,
      '',
      '[Transport]',
      `sessionMode: ${videoSessionMode}`,
      `videoTransport: ${videoTransport}`,
      `statusText: ${srtStatusText || '-'}`,
      '',
      '[WebRTC Runtime]',
      `profileLabel: ${webrtcDebug.profileLabel}`,
      `connectionState: ${webrtcDebug.connectionState}`,
      `iceConnectionState: ${webrtcDebug.iceConnectionState}`,
      `signalingState: ${webrtcDebug.signalingState}`,
      `reconnectAttempts: ${webrtcDebug.reconnectAttempts}/${WEBRTC_MAX_RECONNECT_ATTEMPTS}`,
      `weakNetworkStrike: ${webrtcDebug.weakNetworkStrike}`,
      `stableNetworkStrike: ${webrtcDebug.stableNetworkStrike}`,
      `localIceSent: ${webrtcDebug.localIceSent}`,
      `remoteIceReceived: ${webrtcDebug.remoteIceReceived}`,
      `remoteIceApplied: ${webrtcDebug.remoteIceApplied}`,
      `queuedRemoteIce: ${webrtcDebug.queuedRemoteIce}`,
      `inboundFps: ${webrtcDebug.inboundFps.toFixed(1)}`,
      `lossRate: ${(webrtcDebug.lossRate * 100).toFixed(2)}%`,
      `displayFps: ${displayFps}`,
      `wsDropRate: ${(networkStats.dropRate * 100).toFixed(2)}%`,
      `lastReason: ${webrtcDebug.lastReason || '-'}`,
      `lastError: ${webrtcDebug.lastError || '-'}`,
      `lastDeviceState: ${webrtcDebug.lastDeviceState || '-'}`,
      `lastDeviceDetail: ${webrtcDebug.lastDeviceDetail || '-'}`,
      `updatedAt: ${Number.isFinite(webrtcDebug.updatedAt) ? new Date(webrtcDebug.updatedAt).toISOString() : '-'}`,
      '',
      '[Auto Diagnosis]',
      ...hints.map((hint, index) => `${index + 1}. ${hint}`),
      '',
      '[Events]',
      eventsText
    ]
    return lines.join('\n')
  }, [
    buildWebRtcDiagnosisHints,
    webrtcDebug,
    deviceId,
    videoSessionMode,
    videoTransport,
    srtStatusText,
    displayFps,
    networkStats.dropRate
  ])

  const handleCopyWebRtcReport = useCallback(async () => {
    const report = buildWebRtcDiagnosticReport()
    const copied = await copyTextToClipboard(report)
    if (copied) {
      setCopyStatusWithAutoClear('诊断报告已复制')
      pushWebRtcDebugEvent('report_copied')
      return
    }
    setCopyStatusWithAutoClear('复制失败，请手动复制下方文本')
    pushWebRtcDebugEvent('report_copy_failed')
  }, [buildWebRtcDiagnosticReport, copyTextToClipboard, setCopyStatusWithAutoClear, pushWebRtcDebugEvent])

  const handleCopyWebRtcEvents = useCallback(async () => {
    const eventsText = webrtcDebug.events.length > 0 ? webrtcDebug.events.join('\n') : '(empty)'
    const copied = await copyTextToClipboard(eventsText)
    if (copied) {
      setCopyStatusWithAutoClear('事件日志已复制')
      pushWebRtcDebugEvent('events_copied')
      return
    }
    setCopyStatusWithAutoClear('复制失败，请手动复制下方文本')
    pushWebRtcDebugEvent('events_copy_failed')
  }, [webrtcDebug.events, copyTextToClipboard, setCopyStatusWithAutoClear, pushWebRtcDebugEvent])

  const handleClearWebRtcEvents = useCallback(() => {
    setWebrtcDebug((prev) => ({
      ...prev,
      events: [],
      updatedAt: Date.now()
    }))
    setCopyStatusWithAutoClear('事件日志已清空')
  }, [setCopyStatusWithAutoClear])

  const clearProjectionPermissionRetryTimer = useCallback(() => {
    if (projectionPermissionRetryTimerRef.current) {
      clearTimeout(projectionPermissionRetryTimerRef.current)
      projectionPermissionRetryTimerRef.current = 0
    }
  }, [])

  const requestMediaProjectionRefreshFromWeb = useCallback((reason: string) => {
    if (!WEBRTC_AUTO_PROJECTION_RECOVERY_ENABLED) return
    if (!webSocket || !deviceId) return
    const now = Date.now()
    if (now - lastProjectionRefreshRequestTimeRef.current < MEDIA_PROJECTION_REFRESH_COOLDOWN_MS) {
      return
    }
    lastProjectionRefreshRequestTimeRef.current = now
    webSocket.emit('control_message', {
      type: 'REFRESH_MEDIA_PROJECTION_MANUAL',
      deviceId,
      data: {
        source: 'webrtc_auto_recover',
        reason
      },
      timestamp: now
    })
    updateWebRtcDebug({
      lastReason: `projection_refresh_requested:${reason}`
    }, `projection_refresh_requested:${reason}`)
  }, [webSocket, deviceId, updateWebRtcDebug])

  const scheduleMediaProjectionAutoRetry = useCallback((reason: string) => {
    if (!WEBRTC_AUTO_PROJECTION_RECOVERY_ENABLED || !WEBRTC_AUTO_RECONNECT_ENABLED) return
    clearProjectionPermissionRetryTimer()
    projectionPermissionRetryTimerRef.current = window.setTimeout(() => {
      projectionPermissionRetryTimerRef.current = 0
      if (videoTransportRef.current !== 'ws_binary') return
      setSrtStatusText('WebRTC retrying after projection permission request...')
      setIsLoading(true)
      setVideoTransport('webrtc')
      updateWebRtcDebug({
        transport: 'webrtc',
        lastReason: `projection_retry:${reason}`
      }, `projection_retry:${reason}`)
    }, MEDIA_PROJECTION_AUTORETRY_DELAY_MS)
  }, [clearProjectionPermissionRetryTimer, updateWebRtcDebug])

  useEffect(() => {
    videoTransportRef.current = videoTransport
    updateWebRtcDebug({ transport: videoTransport })
  }, [videoTransport, updateWebRtcDebug])

  useEffect(() => {
    videoSessionModeRef.current = videoSessionMode
    if (typeof window === 'undefined') return
    window.dispatchEvent(new CustomEvent('remote-control:video-mode-changed', {
      detail: {
        deviceId,
        mode: videoSessionMode,
        transport: videoTransportRef.current,
        timestamp: Date.now()
      }
    }))
  }, [deviceId, videoSessionMode])

  useEffect(() => {
    const initialTransport: VideoTransportMode = FORCE_WS_VIDEO_ONLY ? 'ws_binary' : DEFAULT_VIDEO_TRANSPORT
    setWebrtcDebug(createWebRtcDebugState(initialTransport))
  }, [deviceId, webSocket])

  useEffect(() => {
    return () => {
      if (copyStatusTimerRef.current) {
        clearTimeout(copyStatusTimerRef.current)
        copyStatusTimerRef.current = 0
      }
      if (projectionPermissionRetryTimerRef.current) {
        clearTimeout(projectionPermissionRetryTimerRef.current)
        projectionPermissionRetryTimerRef.current = 0
      }
    }
  }, [])

  useEffect(() => {
    if (!webSocket) return

    const handleCapabilities = (payload: any) => {
      if (typeof payload?.webrtcTurnUrls === 'string') {
        setWebrtcTurnUrls(payload.webrtcTurnUrls.trim())
      }
      if (typeof payload?.webrtcTurnUsername === 'string') {
        setWebrtcTurnUsername(payload.webrtcTurnUsername.trim())
      }
      if (typeof payload?.webrtcTurnPassword === 'string') {
        setWebrtcTurnPassword(payload.webrtcTurnPassword.trim())
      }
    }

    webSocket.on('server_capabilities', handleCapabilities)
    return () => {
      webSocket.off('server_capabilities', handleCapabilities)
    }
  }, [webSocket])

  const buildIceServers = useCallback((): RTCIceServer[] => {
    const iceServers: RTCIceServer[] = [{ urls: ['stun:stun.l.google.com:19302'] }]
    const turnUrlsRaw = (webrtcTurnUrls || deviceTurnUrls).trim()
    if (!turnUrlsRaw) return iceServers

    const urls = turnUrlsRaw
      .split(/[,\s;]+/)
      .map((item) => item.trim())
      .filter(Boolean)
    if (!urls.length) return iceServers

    const username = (webrtcTurnUsername || deviceTurnUsername).trim()
    const credential = (webrtcTurnPassword || deviceTurnPassword).trim()

    const turnServer: RTCIceServer = { urls }
    if (username) turnServer.username = username
    if (credential) turnServer.credential = credential
    iceServers.push(turnServer)
    return iceServers
  }, [webrtcTurnUrls, webrtcTurnUsername, webrtcTurnPassword, deviceTurnUrls, deviceTurnUsername, deviceTurnPassword])

  const closeWebRtcSession = useCallback((opts?: { emitStop?: boolean, reason?: string }) => {
    const emitStop = opts?.emitStop === true
    const reason = opts?.reason || 'webrtc_cleanup'

    if (emitStop && webSocket && deviceId) {
      webSocket.emit('webrtc_stop', {
        deviceId,
        reason,
        timestamp: Date.now()
      })
    }

    const pc = webrtcPeerRef.current
    webrtcPeerRef.current = null
    if (pc) {
      try {
        pc.ontrack = null
        pc.onicecandidate = null
        pc.oniceconnectionstatechange = null
        pc.onconnectionstatechange = null
        pc.getSenders().forEach((sender) => {
          try {
            sender.track?.stop()
          } catch {
            // ignore
          }
        })
        pc.close()
      } catch (error) {
        console.warn('Close WebRTC peer failed', error)
      }
    }

    if (webrtcRemoteStreamRef.current) {
      try {
        webrtcRemoteStreamRef.current.getTracks().forEach((track) => track.stop())
      } catch {
        // ignore
      }
    }
    webrtcRemoteStreamRef.current = null
    pendingRemoteCandidatesRef.current = []
    webrtcConnectedRef.current = false
    updateWebRtcDebug({
      queuedRemoteIce: 0,
      connectionState: 'closed',
      iceConnectionState: 'closed',
      signalingState: 'closed',
      lastReason: reason
    }, `session_close:${reason}`)

    const videoElement = srtVideoRef.current
    if (videoElement && videoElement.srcObject) {
      videoElement.srcObject = null
    }
  }, [webSocket, deviceId, updateWebRtcDebug])

  const fallbackToWsTransport = useCallback((reason: string, emitStop: boolean = false) => {
    if (videoTransportRef.current === 'webrtc') {
      closeWebRtcSession({ emitStop, reason })
    }

    const allowWsFallback = FORCE_WS_VIDEO_ONLY || WS_VIDEO_FALLBACK_ENABLED || shouldUseWsScreenshotFallback(reason)
    if (allowWsFallback) {
      const normalizedReason = reason.trim().toLowerCase()
      const manualWsReason = normalizedReason.startsWith('manual_') || normalizedReason.includes('device_or_socket_changed')
      setVideoTransport('ws_binary')
      setVideoSessionMode(manualWsReason ? 'ws_default' : 'fallback_ws')
      setIsLoading(false)
      updateWebRtcDebug({
        transport: 'ws_binary',
        lastReason: reason
      }, `fallback_ws:${reason}`)
      const fallbackHint = shouldUseWsScreenshotFallback(reason)
        ? 'WebRTC unavailable, switched to screenshot fallback'
        : 'WebRTC fallback'
      setSrtStatusText(reason ? `${fallbackHint}: ${reason}` : fallbackHint)
      return
    }

    setVideoTransport('webrtc')
    setVideoSessionMode('webrtc_boost')
    setIsLoading(false)
    updateWebRtcDebug({
      transport: 'webrtc',
      lastReason: reason
    }, `stay_webrtc:${reason}`)
    setSrtStatusText(reason ? `WebRTC unavailable: ${reason}` : 'WebRTC unavailable')
  }, [closeWebRtcSession, updateWebRtcDebug])

  useEffect(() => {
    closeWebRtcSession({ emitStop: false, reason: 'device_or_socket_changed' })
    clearProjectionPermissionRetryTimer()
    setVideoTransport(FORCE_WS_VIDEO_ONLY ? 'ws_binary' : DEFAULT_VIDEO_TRANSPORT)
    setVideoSessionMode(DEFAULT_VIDEO_SESSION_MODE)
    setSrtStatusText(FORCE_WS_VIDEO_ONLY ? 'WS-only video mode' : 'WS screenshot mode (default)')
    setIsLoading(true)
    fpsFrameTimesRef.current = []
    setDisplayFps(0)
  }, [deviceId, webSocket, closeWebRtcSession, clearProjectionPermissionRetryTimer])

  useEffect(() => {
    if (typeof window === 'undefined') return
    const handleOverclockMode = (event: Event) => {
      const customEvent = event as CustomEvent<{ deviceId?: string }>
      if (!customEvent.detail?.deviceId || customEvent.detail.deviceId !== deviceId) return
      if (FORCE_WS_VIDEO_ONLY) return
      if (!deviceSupportsWebRtc) {
        setSrtStatusText('Device does not support WebRTC overclock mode')
        return
      }
      if (webSocket) {
        webSocket.emit('control_message', {
          type: 'REFRESH_MEDIA_PROJECTION_MANUAL',
          deviceId,
          data: {
            source: 'manual_overclock_mode',
            reason: 'manual_overclock_mode'
          },
          timestamp: Date.now()
        })
      }
      clearProjectionPermissionRetryTimer()
      setVideoTransport('webrtc')
      setVideoSessionMode('webrtc_boost')
      setIsLoading(true)
      setSrtStatusText('Overclock mode negotiating WebRTC...')
      updateWebRtcDebug({
        transport: 'webrtc',
        lastReason: 'manual_overclock_mode'
      }, 'manual_overclock_mode')
    }

    const handleWsDefaultMode = (event: Event) => {
      const customEvent = event as CustomEvent<{ deviceId?: string }>
      if (!customEvent.detail?.deviceId || customEvent.detail.deviceId !== deviceId) return
      fallbackToWsTransport('manual_exit_boost', true)
      setVideoSessionMode('ws_default')
      setSrtStatusText('WS screenshot mode (manual)')
    }

    window.addEventListener('remote-control:overclock-mode', handleOverclockMode as EventListener)
    window.addEventListener('remote-control:ws-default-mode', handleWsDefaultMode as EventListener)
    return () => {
      window.removeEventListener('remote-control:overclock-mode', handleOverclockMode as EventListener)
      window.removeEventListener('remote-control:ws-default-mode', handleWsDefaultMode as EventListener)
    }
  }, [deviceId, deviceSupportsWebRtc, webSocket, clearProjectionPermissionRetryTimer, fallbackToWsTransport, updateWebRtcDebug])

  // 瀹夊叏鍦伴€氱煡鐖剁粍浠跺睆骞曞昂瀵稿彉鍖栵紙�?useEffect 涓€岄潪娓叉煋鏈熼棿锛?
  useEffect(() => {
    if (imageSize && onScreenSizeChange) {
      onScreenSizeChange(imageSize)
    }
  }, [imageSize, onScreenSizeChange])

  // 鐢昏川鍙嶉锛氬畾鏈熷悜鏈嶅姟绔姤鍛婄綉缁滆川�?
  useEffect(() => {
    if (!webSocket || !deviceId || videoTransport !== 'ws_binary') return

    const sendFeedback = () => {
      const now = Date.now()
      if (now - lastFeedbackTimeRef.current < 3000) return // 3绉掑彂涓€娆?
      lastFeedbackTimeRef.current = now

      const totalFrames = frameCountRef.current
      const droppedFrames = droppedFrameCountRef.current
      const dropRate = totalFrames > 0 ? droppedFrames / totalFrames : 0
      const fps = displayFps

      webSocket.emit('quality_feedback', {
        deviceId,
        fps,
        dropRate,
      })

      setNetworkStats({ fps, dropRate, avgFrameSize: 0 })

      // 閲嶇疆璁℃暟
      frameCountRef.current = 0
      droppedFrameCountRef.current = 0
    }

    feedbackTimerRef.current = window.setInterval(sendFeedback, 3000)

    // 鐩戝惉鏈嶅姟绔敾璐ㄥ彉鏇撮€氱�?
    const handleQualityChanged = (data: any) => {
      if (data.deviceId === deviceId) {
        if (data.profile) setCurrentProfile(data.profile)
      }
    }
    webSocket.on('quality_changed', handleQualityChanged)

    return () => {
      if (feedbackTimerRef.current) clearInterval(feedbackTimerRef.current)
      webSocket.off('quality_changed', handleQualityChanged)
    }
  }, [webSocket, deviceId, displayFps, videoTransport])

  useEffect(() => {
    if (!webSocket || !deviceId) return
    if (videoTransport !== 'webrtc') return
    if (FORCE_WS_VIDEO_ONLY) {
      setVideoTransport('ws_binary')
      setVideoSessionMode('ws_default')
      setIsLoading(false)
      setSrtStatusText('WS binary video mode (WebRTC disabled)')
      updateWebRtcDebug({
        transport: 'ws_binary',
        lastReason: 'force_ws_video_only'
      }, 'force_ws_video_only')
      return
    }
    if (!deviceSupportsWebRtc) {
      setVideoTransport('ws_binary')
      setVideoSessionMode('ws_default')
      setIsLoading(false)
      setSrtStatusText(
        'WebRTC unavailable on this device, using WS screenshot fallback'
      )
      updateWebRtcDebug({
        transport: 'ws_binary',
        lastReason: 'device_not_support_webrtc'
      }, 'device_not_support_webrtc')
      return
    }
    if (typeof window === 'undefined' || typeof window.RTCPeerConnection !== 'function') {
      updateWebRtcDebug({
        lastReason: 'browser_no_rtc_support'
      }, 'browser_no_rtc_support')
      fallbackToWsTransport('browser_no_rtc_support', false)
      return
    }

    let disposed = false
    let connectTimeoutId = 0
    let statsTimerId = 0
    let disconnectGraceTimerId = 0
    let initialStartTimerId = 0
    let restartCooldownTimerId = 0
    let startInProgress = false
    let adaptiveInProgress = false
    let reconnectAttempts = 0
    let profileIndex = 0
    let weakNetworkStrike = 0
    let stableNetworkStrike = 0
    let localIceSent = 0
    let remoteIceReceived = 0
    let remoteIceApplied = 0
    let lastRestartAt = 0
    updateWebRtcDebug({
      transport: 'webrtc',
      profileLabel: WEBRTC_PROFILES[profileIndex]?.label || '-',
      reconnectAttempts,
      weakNetworkStrike,
      stableNetworkStrike,
      queuedRemoteIce: 0,
      localIceSent,
      remoteIceReceived,
      remoteIceApplied,
      lastReason: 'webrtc_effect_started',
      lastError: ''
    }, 'webrtc_effect_started')

    const clearRuntimeTimers = () => {
      if (connectTimeoutId) {
        clearTimeout(connectTimeoutId)
        connectTimeoutId = 0
      }
      if (statsTimerId) {
        clearInterval(statsTimerId)
        statsTimerId = 0
      }
      if (disconnectGraceTimerId) {
        clearTimeout(disconnectGraceTimerId)
        disconnectGraceTimerId = 0
      }
      if (initialStartTimerId) {
        clearTimeout(initialStartTimerId)
        initialStartTimerId = 0
      }
      if (restartCooldownTimerId) {
        clearTimeout(restartCooldownTimerId)
        restartCooldownTimerId = 0
      }
    }

    const armConnectTimeout = () => {
      if (connectTimeoutId) clearTimeout(connectTimeoutId)
      connectTimeoutId = window.setTimeout(() => {
        if (disposed) return
        if (videoTransportRef.current !== 'webrtc') return
        if (webrtcConnectedRef.current) return
        pushWebRtcDebugEvent('connect_timeout')
        void maybeDowngradeOrReconnect('connect_timeout')
      }, WEBRTC_CONNECT_TIMEOUT_MS)
    }

    const startStatsLoop = () => {
      if (statsTimerId) clearInterval(statsTimerId)
      statsTimerId = window.setInterval(async () => {
        if (disposed) return
        if (videoTransportRef.current !== 'webrtc') return
        const pc = webrtcPeerRef.current
        if (!pc || pc.connectionState !== 'connected') return

        try {
          const report = await pc.getStats()
          let inboundFps = 0
          let packetsLost = 0
          let packetsReceived = 0

          report.forEach((item: any) => {
            if (item?.type !== 'inbound-rtp') return
            if (item?.kind !== 'video' && item?.mediaType !== 'video') return

            const fps = Number(item?.framesPerSecond || 0)
            if (Number.isFinite(fps) && fps > 0) inboundFps = Math.max(inboundFps, fps)

            const lost = Number(item?.packetsLost || 0)
            if (Number.isFinite(lost) && lost >= 0) packetsLost += lost

            const received = Number(item?.packetsReceived || 0)
            if (Number.isFinite(received) && received >= 0) packetsReceived += received
          })

          const totalPackets = packetsLost + packetsReceived
          const lossRate = totalPackets > 0 ? packetsLost / totalPackets : 0

          if (lossRate > 0.12 || (inboundFps > 0 && inboundFps < 12)) {
            weakNetworkStrike += 1
            stableNetworkStrike = 0
          } else {
            weakNetworkStrike = Math.max(0, weakNetworkStrike - 1)
            if (lossRate < 0.03 && inboundFps >= 20) {
              stableNetworkStrike += 1
            } else {
              stableNetworkStrike = Math.max(0, stableNetworkStrike - 1)
            }
          }
          updateWebRtcDebug({
            inboundFps,
            lossRate,
            weakNetworkStrike,
            stableNetworkStrike
          })

          if (weakNetworkStrike >= 3) {
            weakNetworkStrike = 0
            stableNetworkStrike = 0
            updateWebRtcDebug({
              weakNetworkStrike,
              stableNetworkStrike
            }, 'weak_network_trigger')
            void maybeDowngradeOrReconnect('weak_network')
            return
          }

          // Network recovered for a while: try one-level quality upgrade.
          if (WEBRTC_AUTO_RECONNECT_ENABLED && stableNetworkStrike >= 8 && profileIndex > 0 && reconnectAttempts === 0) {
            stableNetworkStrike = 0
            profileIndex -= 1
            updateWebRtcDebug({
              profileLabel: WEBRTC_PROFILES[profileIndex]?.label || '-',
              stableNetworkStrike
            }, 'network_recovered_upgrade')
            void createAndStartWebRtc(profileIndex, 'network_recovered_upgrade')
          }
        } catch (error) {
          console.debug('WebRTC stats read failed', error)
          updateWebRtcDebug({
            lastError: `stats_read_failed:${error instanceof Error ? error.message : String(error)}`
          })
        }
      }, WEBRTC_STATS_CHECK_INTERVAL_MS)
    }

    const createAndStartWebRtc = async (requestedProfileIndex: number, reason: string) => {
      if (disposed || startInProgress) return
      startInProgress = true
      try {
        const now = Date.now()
        const elapsed = now - lastRestartAt
        if (elapsed < WEBRTC_RESTART_COOLDOWN_MS) {
          const waitMs = WEBRTC_RESTART_COOLDOWN_MS - elapsed
          await new Promise<void>((resolve) => {
            restartCooldownTimerId = window.setTimeout(() => {
              restartCooldownTimerId = 0
              resolve()
            }, waitMs)
          })
          if (disposed || videoTransportRef.current !== 'webrtc') return
        }

        const nextIndex = Math.max(0, Math.min(WEBRTC_PROFILES.length - 1, requestedProfileIndex))
        const profile = WEBRTC_PROFILES[nextIndex]
        profileIndex = nextIndex
        updateWebRtcDebug({
          profileLabel: profile.label,
          reconnectAttempts,
          weakNetworkStrike,
          stableNetworkStrike,
          lastReason: reason
        }, `start:${reason} profile=${profile.label}`)

        lastRestartAt = Date.now()
        closeWebRtcSession({ emitStop: false, reason: `local_restart_${reason}` })

        const peerConnection = new RTCPeerConnection({
          iceServers: buildIceServers(),
          bundlePolicy: 'max-bundle',
          iceCandidatePoolSize: 6
        })
        webrtcPeerRef.current = peerConnection
        pendingRemoteCandidatesRef.current = []
        setVideoTransport('webrtc')
        setSrtStatusText(`WebRTC negotiating (${profile.label})...`)
        setIsLoading(true)
        webrtcConnectedRef.current = false
        updateWebRtcDebug({
          transport: 'webrtc',
          profileLabel: profile.label,
          reconnectAttempts,
          weakNetworkStrike,
          stableNetworkStrike,
          queuedRemoteIce: 0,
          connectionState: peerConnection.connectionState,
          iceConnectionState: peerConnection.iceConnectionState,
          signalingState: peerConnection.signalingState,
          lastReason: `negotiating:${reason}`,
          lastError: ''
        }, `pc_created:${profile.label}`)
        armConnectTimeout()

        peerConnection.ontrack = (event: RTCTrackEvent) => {
          if (disposed) return
          const stream = event.streams[0] || new MediaStream([event.track])
          webrtcRemoteStreamRef.current = stream

          const videoElement = srtVideoRef.current
          if (videoElement && videoElement.srcObject !== stream) {
            videoElement.srcObject = stream
            const playResult = videoElement.play()
            if (playResult && typeof playResult.catch === 'function') {
              void playResult.catch(() => {
                // autoplay may be blocked
              })
            }
          }

          reconnectAttempts = 0
          weakNetworkStrike = 0
          stableNetworkStrike = 0
          if (disconnectGraceTimerId) {
            clearTimeout(disconnectGraceTimerId)
            disconnectGraceTimerId = 0
          }
          webrtcConnectedRef.current = true
          setIsLoading(false)
          setSrtStatusText(`WebRTC streaming (${profile.label})`)
          updateWebRtcDebug({
            profileLabel: profile.label,
            reconnectAttempts,
            weakNetworkStrike,
            stableNetworkStrike,
            connectionState: peerConnection.connectionState,
            iceConnectionState: peerConnection.iceConnectionState,
            signalingState: peerConnection.signalingState,
            lastReason: 'ontrack'
          }, `ontrack:${event.track.kind}`)
          startStatsLoop()
          if (connectTimeoutId) {
            clearTimeout(connectTimeoutId)
            connectTimeoutId = 0
          }
        }

        peerConnection.onicecandidate = (event: RTCPeerConnectionIceEvent) => {
          if (disposed) return
          if (!event.candidate) return
          localIceSent += 1
          updateWebRtcDebug({
            localIceSent,
            connectionState: peerConnection.connectionState,
            iceConnectionState: peerConnection.iceConnectionState,
            signalingState: peerConnection.signalingState
          })
          webSocket.emit('webrtc_ice_candidate', {
            deviceId,
            candidate: event.candidate.candidate,
            sdpMid: event.candidate.sdpMid,
            sdpMLineIndex: event.candidate.sdpMLineIndex,
            timestamp: Date.now()
          })
        }

        peerConnection.onconnectionstatechange = () => {
          if (disposed) return
          const state = peerConnection.connectionState
          updateWebRtcDebug({
            connectionState: state,
            iceConnectionState: peerConnection.iceConnectionState,
            signalingState: peerConnection.signalingState,
            reconnectAttempts
          }, `pc_state:${state}`)
          if (state === 'connected') {
            reconnectAttempts = 0
            if (disconnectGraceTimerId) {
              clearTimeout(disconnectGraceTimerId)
              disconnectGraceTimerId = 0
            }
            webrtcConnectedRef.current = true
            setIsLoading(false)
            setSrtStatusText(`WebRTC connected (${profile.label})`)
            updateWebRtcDebug({
              reconnectAttempts
            })
            return
          }
          if (state === 'connecting') {
            setSrtStatusText(`WebRTC connecting (${profile.label})...`)
            return
          }
          if (state === 'disconnected') {
            setSrtStatusText('WebRTC disconnected, reconnecting...')
            if (disconnectGraceTimerId) clearTimeout(disconnectGraceTimerId)
            disconnectGraceTimerId = window.setTimeout(() => {
              if (disposed) return
              void maybeDowngradeOrReconnect('disconnected')
            }, WEBRTC_DISCONNECT_GRACE_MS)
            return
          }
          if (state === 'failed') {
            void maybeDowngradeOrReconnect('connection_failed')
            return
          }
          if (state === 'closed') {
            if (!disposed && !startInProgress) {
              void maybeDowngradeOrReconnect('connection_closed')
            }
          }
        }

        peerConnection.oniceconnectionstatechange = () => {
          if (disposed) return
          updateWebRtcDebug({
            iceConnectionState: peerConnection.iceConnectionState,
            connectionState: peerConnection.connectionState,
            signalingState: peerConnection.signalingState
          }, `ice_state:${peerConnection.iceConnectionState}`)
          if (peerConnection.iceConnectionState === 'failed') {
            void maybeDowngradeOrReconnect('ice_failed')
          }
        }

        peerConnection.addTransceiver('video', { direction: 'recvonly' })

        const offer = await peerConnection.createOffer({
          offerToReceiveVideo: true
        })
        await peerConnection.setLocalDescription(offer)
        const localOfferSdp = String(offer.sdp || peerConnection.localDescription?.sdp || '')
        const compatOffer = buildAndroidCompatOfferSdp(localOfferSdp)
        if (!compatOffer.sdp) {
          throw new Error('offer_sdp_empty_after_set_local')
        }
        updateWebRtcDebug({
          signalingState: peerConnection.signalingState
        }, 'offer_created')

        webSocket.emit('webrtc_offer', {
          deviceId,
          type: offer.type,
          sdp: compatOffer.sdp,
          fps: profile.fps,
          maxLongEdge: profile.maxLongEdge,
          bitrateKbps: profile.bitrateKbps,
          priority: profile.priority,
          timestamp: Date.now()
        })
        if (compatOffer.changed) {
          pushWebRtcDebugEvent('offer_sdp_compat_applied')
        } else {
          pushWebRtcDebugEvent('offer_sdp_compat_not_needed')
        }
        updateWebRtcDebug({
          profileLabel: profile.label,
          lastReason: `offer_sent:${profile.label}`
        }, `offer_sent:${profile.label}`)
      } catch (error) {
        console.error('Create WebRTC offer failed', error)
        updateWebRtcDebug({
          lastError: `offer_create_failed:${error instanceof Error ? error.message : String(error)}`,
          lastReason: 'offer_create_failed'
        }, 'offer_create_failed')
        void maybeDowngradeOrReconnect('offer_create_failed')
      } finally {
        startInProgress = false
      }
    }

    const maybeDowngradeOrReconnect = async (reason: string) => {
      if (disposed || videoTransportRef.current !== 'webrtc') return
      if (!WEBRTC_AUTO_RECONNECT_ENABLED) {
        updateWebRtcDebug({
          reconnectAttempts,
          lastReason: `adaptive_disabled:${reason}`
        }, `adaptive_disabled:${reason}`)
        fallbackToWsTransport(`webrtc_${reason}`, true)
        return
      }
      if (adaptiveInProgress) {
        updateWebRtcDebug({
          lastReason: `adaptive_skip_busy:${reason}`,
          reconnectAttempts
        }, `adaptive_skip_busy:${reason}`)
        return
      }
      adaptiveInProgress = true
      updateWebRtcDebug({
        lastReason: reason,
        reconnectAttempts,
        profileLabel: WEBRTC_PROFILES[profileIndex]?.label || '-'
      }, `adaptive:${reason}`)
      try {
        await new Promise<void>((resolve) => {
          restartCooldownTimerId = window.setTimeout(() => {
            restartCooldownTimerId = 0
            resolve()
          }, WEBRTC_ADAPTIVE_RETRY_DELAY_MS)
        })
        if (disposed || videoTransportRef.current !== 'webrtc') return

        if (profileIndex < WEBRTC_PROFILES.length - 1) {
          profileIndex += 1
          reconnectAttempts = 0
          updateWebRtcDebug({
            reconnectAttempts,
            profileLabel: WEBRTC_PROFILES[profileIndex]?.label || '-'
          }, `adaptive_downgrade:${WEBRTC_PROFILES[profileIndex]?.label || '-'}`)
          await createAndStartWebRtc(profileIndex, `${reason}_downgrade`)
          return
        }

        if (reconnectAttempts < WEBRTC_MAX_RECONNECT_ATTEMPTS) {
          reconnectAttempts += 1
          updateWebRtcDebug({
            reconnectAttempts
          }, `adaptive_retry:${reconnectAttempts}`)
          await createAndStartWebRtc(profileIndex, `${reason}_retry_${reconnectAttempts}`)
          return
        }

        updateWebRtcDebug({
          reconnectAttempts,
          lastReason: `fallback_ws:${reason}`
        }, `adaptive_fallback_ws:${reason}`)
        fallbackToWsTransport(`webrtc_${reason}`, true)
      } finally {
        adaptiveInProgress = false
      }
    }

    const handleWebRtcState = (payload: any) => {
      if (payload?.deviceId !== deviceId) return
      const state = typeof payload?.state === 'string' ? payload.state.toLowerCase() : ''
      const message = typeof payload?.message === 'string' ? payload.message : 'unknown'
      const detailText = formatWebRtcDeviceDetail(payload?.extra?.detail)
      const reasonWithDetail = detailText ? `${message} | ${detailText}` : message
      setSrtStatusText(`WebRTC device: ${state || 'unknown'} (${message})`)
      updateWebRtcDebug({
        lastDeviceState: `${state || 'unknown'}:${message}`,
        lastDeviceDetail: detailText,
        lastReason: `device_state:${state || 'unknown'}`,
        reconnectAttempts
      }, detailText
        ? `device_state:${state || 'unknown'}:${message} | ${detailText}`
        : `device_state:${state || 'unknown'}:${message}`
      )

      const normalizedMessage = reasonWithDetail.toLowerCase()
      if (
        normalizedMessage.includes('component_cleanup') ||
        normalizedMessage.includes('effect_cleanup')
      ) {
        return
      }

      if (normalizedMessage.includes('media_projection_not_ready') && WEBRTC_AUTO_PROJECTION_RECOVERY_ENABLED) {
        requestMediaProjectionRefreshFromWeb(message)
        scheduleMediaProjectionAutoRetry(message)
      }

      if (shouldUseWsScreenshotFallback(message)) {
        fallbackToWsTransport(reasonWithDetail, false)
        return
      }

      if (state === 'running') {
        reconnectAttempts = 0
        updateWebRtcDebug({
          reconnectAttempts
        })
        return
      }

      if (state === 'starting') {
        // Device acknowledged offer; extend connection timeout window while waiting answer/candidates.
        armConnectTimeout()
        return
      }

      if (state === 'failed') {
        void maybeDowngradeOrReconnect(reasonWithDetail || 'device_failed')
        return
      }

      if (state === 'stopped') {
        // Explicit remote stop should still fallback to screenshot mode.
        if (normalizedMessage.includes('remote_stop') || normalizedMessage.includes('controller_release')) {
          fallbackToWsTransport(reasonWithDetail, false)
          return
        }
        void maybeDowngradeOrReconnect(reasonWithDetail || 'device_stopped')
      }
    }

    const handleWebRtcAnswer = async (payload: any) => {
      if (payload?.deviceId !== deviceId) return
      const sdp = typeof payload?.sdp === 'string' ? payload.sdp : ''
      const type = typeof payload?.type === 'string' ? payload.type : 'answer'
      if (!sdp) return

      const pc = webrtcPeerRef.current
      if (!pc) return

      try {
        updateWebRtcDebug({
          lastReason: 'answer_received'
        }, 'answer_received')
        await pc.setRemoteDescription({
          type: type as RTCSdpType,
          sdp
        })
        updateWebRtcDebug({
          signalingState: pc.signalingState,
          queuedRemoteIce: pendingRemoteCandidatesRef.current.length
        }, 'answer_applied')

        if (pendingRemoteCandidatesRef.current.length > 0) {
          const queued = [...pendingRemoteCandidatesRef.current]
          pendingRemoteCandidatesRef.current = []
          for (const candidate of queued) {
            try {
              await pc.addIceCandidate(candidate)
              remoteIceApplied += 1
            } catch (error) {
              console.warn('Apply queued remote candidate failed', error)
              updateWebRtcDebug({
                lastError: `apply_queued_candidate_failed:${error instanceof Error ? error.message : String(error)}`
              })
            }
          }
          updateWebRtcDebug({
            remoteIceApplied,
            queuedRemoteIce: pendingRemoteCandidatesRef.current.length
          }, `queued_candidates_applied:${queued.length}`)
        }
      } catch (error) {
        console.error('Set remote WebRTC answer failed', error)
        updateWebRtcDebug({
          lastError: `set_remote_answer_failed:${error instanceof Error ? error.message : String(error)}`
        }, 'set_remote_answer_failed')
        void maybeDowngradeOrReconnect('set_remote_answer_failed')
      }
    }

    const handleWebRtcIceCandidate = async (payload: any) => {
      if (payload?.deviceId !== deviceId) return
      const candidate = typeof payload?.candidate === 'string' ? payload.candidate : ''
      if (!candidate) return
      remoteIceReceived += 1

      const iceCandidate: RTCIceCandidateInit = {
        candidate,
        sdpMid: typeof payload?.sdpMid === 'string' ? payload.sdpMid : undefined,
        sdpMLineIndex: Number.isInteger(payload?.sdpMLineIndex) ? payload.sdpMLineIndex : undefined
      }

      const pc = webrtcPeerRef.current
      if (!pc) return

      if (!pc.remoteDescription) {
        pendingRemoteCandidatesRef.current.push(iceCandidate)
        updateWebRtcDebug({
          remoteIceReceived,
          queuedRemoteIce: pendingRemoteCandidatesRef.current.length
        })
        return
      }

      try {
        await pc.addIceCandidate(iceCandidate)
        remoteIceApplied += 1
        updateWebRtcDebug({
          remoteIceReceived,
          remoteIceApplied,
          queuedRemoteIce: pendingRemoteCandidatesRef.current.length
        })
      } catch (error) {
        console.warn('Add remote ICE candidate failed', error)
        updateWebRtcDebug({
          lastError: `add_remote_candidate_failed:${error instanceof Error ? error.message : String(error)}`
        })
      }
    }

    const handleWebRtcStop = (payload: any) => {
      if (payload?.deviceId !== deviceId) return
      const reason = typeof payload?.reason === 'string' ? payload.reason : 'webrtc_remote_stopped'
      updateWebRtcDebug({
        lastReason: `remote_stop:${reason}`
      }, `remote_stop:${reason}`)
      const normalizedReason = reason.toLowerCase()
      if (
        normalizedReason.includes('component_cleanup') ||
        normalizedReason.includes('effect_cleanup')
      ) {
        return
      }
      if (normalizedReason.includes('media_projection_not_ready') && WEBRTC_AUTO_PROJECTION_RECOVERY_ENABLED) {
        requestMediaProjectionRefreshFromWeb(reason)
        scheduleMediaProjectionAutoRetry(reason)
      }
      if (shouldUseWsScreenshotFallback(reason)) {
        fallbackToWsTransport(reason, false)
        return
      }
      void maybeDowngradeOrReconnect(reason)
    }

    const handleWebRtcError = (payload: any) => {
      if (payload?.deviceId !== deviceId) return
      const message = typeof payload?.message === 'string' ? payload.message : 'webrtc_error'
      updateWebRtcDebug({
        lastError: message,
        lastReason: 'remote_error'
      }, `remote_error:${message}`)
      const normalizedMessage = message.toLowerCase()
      if (normalizedMessage.includes('media_projection_not_ready') && WEBRTC_AUTO_PROJECTION_RECOVERY_ENABLED) {
        requestMediaProjectionRefreshFromWeb(message)
        scheduleMediaProjectionAutoRetry(message)
      }
      if (shouldUseWsScreenshotFallback(message)) {
        fallbackToWsTransport(message, false)
        return
      }
      if (message === 'no_device_control' || message === 'device_offline') {
        fallbackToWsTransport(message, false)
        return
      }
      void maybeDowngradeOrReconnect(message)
    }

    webSocket.on('webrtc_state', handleWebRtcState)
    webSocket.on('webrtc_answer', handleWebRtcAnswer)
    webSocket.on('webrtc_ice_candidate', handleWebRtcIceCandidate)
    webSocket.on('webrtc_stop', handleWebRtcStop)
    webSocket.on('webrtc_error', handleWebRtcError)

    // Delay initial start slightly to avoid React dev strict-mode double effect causing duplicate offers.
    initialStartTimerId = window.setTimeout(() => {
      if (disposed) return
      if (videoTransportRef.current !== 'webrtc') return
      void createAndStartWebRtc(profileIndex, 'initial')
    }, 80)

    return () => {
      disposed = true
      clearRuntimeTimers()
      webSocket.off('webrtc_state', handleWebRtcState)
      webSocket.off('webrtc_answer', handleWebRtcAnswer)
      webSocket.off('webrtc_ice_candidate', handleWebRtcIceCandidate)
      webSocket.off('webrtc_stop', handleWebRtcStop)
      webSocket.off('webrtc_error', handleWebRtcError)
      closeWebRtcSession({ emitStop: false, reason: 'effect_cleanup' })
    }
  }, [
    webSocket,
    deviceId,
    deviceSupportsWebRtc,
    videoTransport,
  ])

  useEffect(() => {
    if (videoTransport !== 'webrtc') return
    const canvas = canvasRef.current
    if (!canvas) return
    const ctx = canvas.getContext('2d')
    if (!ctx) return
    ctx.clearRect(0, 0, canvas.width, canvas.height)
  }, [videoTransport])

  useEffect(() => {
    if (videoTransport !== 'webrtc') return
    const video = srtVideoRef.current as (HTMLVideoElement & {
      requestVideoFrameCallback?: (callback: (now: number, metadata: unknown) => void) => number
      cancelVideoFrameCallback?: (handle: number) => void
    }) | null

    if (!video || typeof video.requestVideoFrameCallback !== 'function') {
      return
    }

    let callbackHandle = 0
    const onFrame = () => {
      const now = Date.now()
      fpsFrameTimesRef.current.push(now)
      const cutoff = now - 2000
      fpsFrameTimesRef.current = fpsFrameTimesRef.current.filter(t => t > cutoff)

      if (now - lastFpsUpdateRef.current > 500) {
        lastFpsUpdateRef.current = now
        setDisplayFps(Math.round(fpsFrameTimesRef.current.length / 2))
      }

      callbackHandle = video.requestVideoFrameCallback!(onFrame)
    }

    callbackHandle = video.requestVideoFrameCallback(onFrame)

    return () => {
      if (callbackHandle && typeof video.cancelVideoFrameCallback === 'function') {
        video.cancelVideoFrameCallback(callbackHandle)
      }
    }
  }, [videoTransport])

  const handleSrtVideoLoadedMetadata = useCallback(() => {
    const video = srtVideoRef.current
    if (!video) return
    if (video.videoWidth > 0 && video.videoHeight > 0) {
      const size = { width: video.videoWidth, height: video.videoHeight }
      imageSizeRef.current = size
      setImageSize(size)
    }
    setIsLoading(false)
    setSrtStatusText('WebRTC playing')
    pushWebRtcDebugEvent(`video_metadata:${video.videoWidth}x${video.videoHeight}`)
  }, [pushWebRtcDebugEvent])

  const handleSrtVideoWaiting = useCallback(() => {
    if (videoTransportRef.current !== 'webrtc') return
    setSrtStatusText('WebRTC buffering')
    pushWebRtcDebugEvent('video_waiting')
  }, [pushWebRtcDebugEvent])

  const handleSrtVideoPlaying = useCallback(() => {
    if (videoTransportRef.current === 'webrtc') {
      setIsLoading(false)
      setSrtStatusText('WebRTC playing')
      pushWebRtcDebugEvent('video_playing')
    }
  }, [pushWebRtcDebugEvent])

  useEffect(() => {
    if (videoTransport !== 'webrtc') {
      webrtcLowFpsStrikeRef.current = 0
      return
    }
    if (isLoading || displayFps <= 0) return

    if (displayFps < 8) {
      webrtcLowFpsStrikeRef.current += 1
    } else {
      webrtcLowFpsStrikeRef.current = 0
    }

    if (webrtcLowFpsStrikeRef.current >= 14) {
      webrtcLowFpsStrikeRef.current = 0
      setSrtStatusText('WebRTC low FPS detected, adaptive reconnecting...')
    }
  }, [videoTransport, isLoading, displayFps])

  // 鍒囨崲鐢昏川妗ｄ�?
  const handleSetProfile = useCallback((profileKey: string) => {
    if (!webSocket) return
    webSocket.emit('set_quality_profile', { deviceId, profile: profileKey })
    setCurrentProfile(profileKey)
  }, [webSocket, deviceId])

  // 鐩戝惉灞忓箷鏁版嵁鐨勭嫭绔媢seEffect锛岄伩鍏嶄笌鎺у埗鏉冮€昏緫娣峰悎
  useEffect(() => {
    if (!webSocket) return

    const handleScreenData = (data: any) => {
      if (videoTransportRef.current === 'webrtc') return
      if (data.deviceId === deviceId) {
        // 甯ц鏁扮敤浜庤川閲忓弽棣?
        frameCountRef.current++
        
        // 杩囨护榛戝睆甯э細Base64闀垮害<4000瀛楃�?�?KB JPEG)鍑犱箮鑲畾鏄粦灞?绌虹櫧甯?
        // 姝ｅ�?80�?54 JPEG鍗充娇鏈€浣庤川閲忎�?8000瀛楃�?
        const dataLen = typeof data.data === 'string' ? data.data.length : 0
        const MIN_VALID_FRAME_LENGTH = MIN_VALID_BASE64_FRAME_LENGTH
        
        if (dataLen > 0 && dataLen < MIN_VALID_FRAME_LENGTH) {
          // 榛戝睆甯э細涓㈠純锛屼繚鎸乧anvas涓婁竴甯у唴瀹逛笉�?
          droppedFrameCountRef.current++
          if (frameCountRef.current % 30 === 0) {
            console.log(`[ScreenData] frame=${frameCountRef.current} dropped_black_frame size=${dataLen} < ${MIN_VALID_FRAME_LENGTH}, totalDropped=${droppedFrameCountRef.current}`)
          }
          return
        }
        
        // 璇婃柇锛氳褰曟暟鎹埌杈鹃鐜?
        if (frameCountRef.current % 30 === 0) {
          console.log(`[ScreenData] frame=${frameCountRef.current} size=${dataLen}, rendering=${isRenderingRef.current}, decoding=${decodingRef.current}`)
        }
        
        // 鎻愬墠灏咮ase64杞负Blob锛岄伩鍏嶆覆鏌撳惊鐜腑闃诲涓荤嚎�?
        // fetch+data URI鏂瑰紡姣攁tob+閫愬瓧鑺傚鍒跺揩寰楀锛屼笖涓嶉樆濉炰富绾跨�?
        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()
          }
        }
        
        // 鍙湪棣栧抚鏃舵洿鏂發oading鐘舵€侊紝閬垮厤姣忓抚瑙﹀彂閲嶆覆�?
        if (isLoading) setIsLoading(false)
      }
    }

    const handleScreenDataBin = (...args: any[]) => {
      if (videoTransportRef.current === 'webrtc') return
      const normalized = normalizeScreenDataBinArgs(args)
      if (!normalized || normalized.deviceId !== deviceId) return

      const format = normalized.meta?.format ?? 'JPEG'
      const mimeType = `image/${String(format).toLowerCase()}`
      const frameBlob = toBinaryFrameBlob(normalized.frame, mimeType)
      if (!frameBlob) return

      frameCountRef.current++
      if (frameBlob.size > 0 && frameBlob.size < MIN_VALID_BINARY_FRAME_BYTES) {
        droppedFrameCountRef.current++
        return
      }

      pendingBlobRef.current = frameBlob
      latestFrameRef.current = {
        ...(normalized.meta && typeof normalized.meta === 'object' ? normalized.meta : {}),
        deviceId: normalized.deviceId,
        format,
        timestamp: normalized.meta?.timestamp || Date.now(),
        data: '__binary_frame__'
      }

      if (!isRenderingRef.current) {
        isRenderingRef.current = true
        renderLatestFrame()
      }

      if (isLoading) setIsLoading(false)
    }

    webSocket.on('screen_data', handleScreenData)
    webSocket.on('screen_data_bin', handleScreenDataBin)

    return () => {
      webSocket.off('screen_data', handleScreenData)
      webSocket.off('screen_data_bin', handleScreenDataBin)
      // 娓呯�?rAF
      if (rafIdRef.current) {
        cancelAnimationFrame(rafIdRef.current)
        rafIdRef.current = 0
      }
      isRenderingRef.current = false
      // 璁惧鍒囨崲鎴栨柇寮€鏃堕噸缃攣瀹氬昂瀵革紝涓嬫杩炴帴閲嶆柊閿佸�?
      lockedCanvasSizeRef.current = null
    }
  }, [webSocket, deviceId])

  // Auto-refresh: detect no-frame timeout and request screen refresh from device
  const lastFrameTimeRef = useRef<number>(Date.now())
  const refreshRequestedRef = useRef<boolean>(false)
  const NO_FRAME_TIMEOUT = 8000 // 8 seconds without frames triggers refresh
  const REFRESH_COOLDOWN = 15000 // minimum 15 seconds between refresh requests

  // Update last frame time when screen_data arrives
  useEffect(() => {
    if (!webSocket || !deviceId) return

    const trackFrameTime = (data: any) => {
      if (data.deviceId === deviceId) {
        lastFrameTimeRef.current = Date.now()
        refreshRequestedRef.current = false
      }
    }

    const trackBinaryFrameTime = (...args: any[]) => {
      const normalized = normalizeScreenDataBinArgs(args)
      if (!normalized || normalized.deviceId !== deviceId) return
      lastFrameTimeRef.current = Date.now()
      refreshRequestedRef.current = false
    }

    webSocket.on('screen_data', trackFrameTime)
    webSocket.on('screen_data_bin', trackBinaryFrameTime)
    return () => {
      webSocket.off('screen_data', trackFrameTime)
      webSocket.off('screen_data_bin', trackBinaryFrameTime)
    }
  }, [webSocket, deviceId])

  // Periodic check for frame timeout
  useEffect(() => {
    if (!webSocket || !deviceId || videoTransport !== 'ws_binary') return

    const checkInterval = window.setInterval(() => {
      const elapsed = Date.now() - lastFrameTimeRef.current
      if (elapsed < NO_FRAME_TIMEOUT) return
      if (refreshRequestedRef.current) return

      console.log(`[AutoRefresh] No frames for ${Math.round(elapsed / 1000)}s, requesting screen refresh`)
      refreshRequestedRef.current = true
      lastFrameTimeRef.current = Date.now() - NO_FRAME_TIMEOUT + REFRESH_COOLDOWN

      webSocket.emit('client_event', {
        type: 'REFRESH_SCREEN',
        data: { deviceId }
      })
    }, 3000) // check every 3 seconds

    return () => { clearInterval(checkInterval) }
  }, [webSocket, deviceId, videoTransport])

  // 鎺у埗鏉冪鐞嗙殑鐙珛useEffect锛屽彧鍦ㄥ繀瑕佹椂鐢宠/閲婃�?
  useEffect(() => {
    if (!webSocket || !deviceId) return

    // 濡傛灉WebSocket瀹炰緥鍙戠敓鍙樺寲锛岄噸缃帶鍒舵潈鐘舵�?
    if (currentWebSocket !== webSocket) {
      setIsControlRequested(false)
      setCurrentWebSocket(webSocket)
    }

    // 鍙湪鏈敵璇疯繃鎺у埗鏉冩椂鐢宠锛屼笖闃叉閲嶅鍙戦�?
    if (!isControlRequested) {
      const now = Date.now()
      // 闃叉鐭椂闂村唴閲嶅鍙戦€佽姹傦紙2绉掑唴鍙兘鍙戦€佷竴娆★級
      if (now - lastControlRequestTime < 2000) {
        console.log('Control request skipped due cooldown')
        return
      }
      
      console.log('[Control] requesting device control', deviceId)
      setLastControlRequestTime(now)
      webSocket.emit('client_event', {
        type: 'REQUEST_DEVICE_CONTROL',
        data: { deviceId }
      })
      setIsControlRequested(true)
    }

    // 鐩戝惉鎺у埗鏉冨搷�?
    const handleControlResponse = (data: any) => {
      if (data.deviceId === deviceId) {
        if (data.success) {
          console.log('[Control] device control granted', deviceId)
        } else {
          console.warn('[Control] device control denied', data.message)
          // 濡傛灉澶辫触锛屽厑璁搁噸鏂扮敵璇?
          setIsControlRequested(false)
        }
      }
    }

    // 鐩戝惉鎺у埗閿欒锛岃嚜鍔ㄩ噸鏂扮敵璇锋帶鍒舵�?
    const handleControlError = (data: any) => {
      if (data.deviceId === deviceId && data.error === 'NO_PERMISSION') {
        console.warn('[Control] permission lost, requesting control again', deviceId)
        setIsControlRequested(false)
      }
    }

    webSocket.on('device_control_response', handleControlResponse)
    webSocket.on('control_error', handleControlError)

    // 娓呯悊鍑芥暟锛氬彧鍦ㄧ粍浠跺嵏杞芥垨deviceId鍙樺寲鏃堕噴鏀炬帶鍒舵�?
    return () => {
      webSocket.off('device_control_response', handleControlResponse)
      webSocket.off('control_error', handleControlError)
      
      // 鍙湁鍦ㄥ凡鐢宠杩囨帶鍒舵潈鏃舵墠閲婃�?
      if (isControlRequested) {
        console.log('[Control] releasing device control', deviceId)
        webSocket.emit('client_event', {
          type: 'RELEASE_DEVICE_CONTROL',
          data: { deviceId }
        })
      }
    }
  }, [webSocket, deviceId, isControlRequested, currentWebSocket]) // 涓嶅寘鍚玪astControlRequestTime閬垮厤閲嶅鎵ц

  // 楂樻€ц兘娓叉煋锛歝reateImageBitmap 绂诲睆瑙ｇ爜 + 鎸佺�?rAF 寰�?
  const decodingRef = useRef(false)

  const renderLatestFrame = useCallback(() => {
    const doRender = () => {
      const frameData = latestFrameRef.current
      const preDecodedBlob = pendingBlobRef.current
      if (!frameData || !preDecodedBlob) {
        // 娌℃湁鏂板抚锛屽仠姝㈠惊鐜紝绛?handleScreenData 閲嶆柊鍚姩
        isRenderingRef.current = false
        return
      }

      // 鍙栬蛋甯ф暟鎹拰棰勮В鐮丅lob
      latestFrameRef.current = null
      pendingBlobRef.current = null

      const canvas = canvasRef.current
      if (!canvas) {
        isRenderingRef.current = false
        return
      }

      if (!frameData?.data || !frameData?.format) {
        rafIdRef.current = requestAnimationFrame(doRender)
        return
      }

      // 涓婁竴甯ц繕鍦ㄨВ鐮侊紝鎶婃暟鎹斁鍥炲幓锛屼笅�?rAF 鍐嶈�?
      if (decodingRef.current) {
        latestFrameRef.current = frameData
        pendingBlobRef.current = preDecodedBlob
        rafIdRef.current = requestAnimationFrame(doRender)
        return
      }

      decodingRef.current = true

      // 鐩存帴浣跨敤棰勮В鐮佺殑Blob锛岃烦杩嘊ase64瑙ｇ爜姝ラ�?
      createImageBitmap(preDecodedBlob)
        .then(bitmap => {
          decodingRef.current = false

          const ctx = canvas.getContext('2d')
          if (!ctx) {
            bitmap.close()
            rafIdRef.current = requestAnimationFrame(doRender)
            return
          }

          // 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 {
            // 鍚庣画甯э細鍙湪鏂板抚鏇村ぇ鏃舵洿鏂伴攣瀹氬昂瀵革紝闃叉缂╁皬闂儊
            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
            }
          }

          // 濮嬬粓灏哹itmap缁樺埗鍒版暣涓猚anvas鍖哄煙锛屾祻瑙堝櫒鑷姩缂╂�?
          ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height)

          // 浣跨敤閿佸畾鐨刢anvas灏哄涓婃姤锛屼繚鎸佺ǔ�?
          const reportSize = lockedCanvasSizeRef.current
          if (reportSize) {
            const prevSize = imageSizeRef.current
            if (!prevSize || prevSize.width !== reportSize.width || prevSize.height !== reportSize.height) {
              imageSizeRef.current = { width: reportSize.width, height: reportSize.height }
              setImageSize({ width: reportSize.width, height: reportSize.height })
            }
          }

          const now = Date.now()
          fpsFrameTimesRef.current.push(now)
          const cutoff = now - 2000
          fpsFrameTimesRef.current = fpsFrameTimesRef.current.filter(t => t > cutoff)
          if (now - lastFpsUpdateRef.current > 500) {
            lastFpsUpdateRef.current = now
            setDisplayFps(Math.round(fpsFrameTimesRef.current.length / 2))
          }

          bitmap.close()

          // 瑙ｇ爜瀹屾垚鍚庡缁堣皟搴︿笅涓€甯э紝淇濇寔寰幆娲昏�?
          rafIdRef.current = requestAnimationFrame(doRender)
        })
        .catch(err => {
          decodingRef.current = false
          console.error('Image decode failed:', err)
          rafIdRef.current = requestAnimationFrame(doRender)
        })
    }

    doRender()
  }, [])

  // 鍧愭爣杞崲鍑芥暟锛氬皢canvas鍧愭爣杞崲涓鸿澶囧潗�?
  const convertCanvasToDeviceCoords = useCallback((canvasX: number, canvasY: number, canvas: HTMLCanvasElement, device: any) => {
    if (!device) return null
    
    const deviceWidth = device.screenWidth
    const deviceHeight = device.screenHeight
    
    // 浣跨敤canvas鐨勫疄闄呮樉绀哄昂瀵革紝鑰屼笉鏄唴閮ㄥ垎杈ㄧ�?
    const rect = canvas.getBoundingClientRect()
    const canvasWidth = rect.width
    const canvasHeight = rect.height
    
    let imageX: number, imageY: number, imageWidth: number, imageHeight: number
    
    // 鐢变簬canvas浣跨敤浜唎bjectFit: 'contain'锛屾祻瑙堝櫒浼氳嚜鍔ㄤ繚鎸佸楂樻瘮
    // 鎴戜滑闇€瑕佽绠楀浘鍍忓湪canvas涓殑瀹為檯鏄剧ず浣嶇疆鍜屽昂�?
    const scale = Math.min(canvasWidth / deviceWidth, canvasHeight / deviceHeight)
    imageWidth = deviceWidth * scale
    imageHeight = deviceHeight * scale
    imageX = (canvasWidth - imageWidth) / 2
    imageY = (canvasHeight - imageHeight) / 2
    

    
    // 妫€鏌ョ偣鍑绘槸鍚﹀湪鍥惧儚鍖哄煙鍐?
    if (canvasX < imageX || canvasX > imageX + imageWidth || 
        canvasY < imageY || canvasY > imageY + imageHeight) {
      console.warn('鐐瑰嚮鍦ㄥ浘鍍忓尯鍩熷')
      return null // 鐐瑰嚮鍦ㄥ浘鍍忓尯鍩熷
    }
    
    // 灏哻anvas鍧愭爣杞崲涓哄浘鍍忓唴鐨勭浉瀵瑰潗鏍?
    const relativeX = canvasX - imageX
    const relativeY = canvasY - imageY
    
    // 杞崲涓鸿澶囧潗鏍?
    const deviceX = (relativeX / imageWidth) * deviceWidth
    const deviceY = (relativeY / imageHeight) * deviceHeight
    
    console.log('deviceX', deviceX)
    console.log('deviceY', deviceY)
    // 纭繚鍧愭爣鍦ㄨ澶囪寖鍥村�?
    const clampedX = Math.max(0, Math.min(deviceWidth - 1, deviceX))
    const clampedY = Math.max(0, Math.min(deviceHeight - 1, deviceY))
    

    
    return { x: clampedX, y: clampedY }
  }, [])

  // const handleMouseEvent = useCallback((event: React.MouseEvent, action: string) => {
  //   if (!webSocket || !device) return

  //   const canvas = canvasRef.current
  //   if (!canvas) return

  //   const rect = canvas.getBoundingClientRect()
  //   const canvasX = event.clientX - rect.left
  //   const canvasY = event.clientY - rect.top
    
  //   // 鏍规嵁鏄剧ず妯″紡璁＄畻姝ｇ‘鐨勮澶囧潗鏍?
  //   const deviceCoords = convertCanvasToDeviceCoords(canvasX, canvasY, canvas, device)
    
  //   if (!deviceCoords) {
  //   console.warn('Coordinate conversion failed, click may be outside image bounds')
  //     return
  //   }

  //   console.log(`鐐瑰嚮浣嶇疆杞�? Canvas(${canvasX.toFixed(1)}, ${canvasY.toFixed(1)}) -> Device(${deviceCoords.x.toFixed(1)}, ${deviceCoords.y.toFixed(1)})`)

  //   webSocket.emit('control_message', {
  //     type: action,
  //     deviceId,
  //     data: { x: deviceCoords.x, y: deviceCoords.y },
  //     timestamp: Date.now()
  //   })

  //   // 鏄剧ず瑙︽懜鎸囩ず鍣?
  //   if (screenDisplay.showTouchIndicator) {
  //     showTouchIndicator(canvasX, canvasY)
  //   }
  // }, [webSocket, device, deviceId, screenDisplay.showTouchIndicator, convertCanvasToDeviceCoords])

  const showTouchIndicator = (x: number, y: number) => {
    const indicator = document.createElement('div')
    indicator.style.position = 'absolute'
    indicator.style.left = `${x - 10}px`
    indicator.style.top = `${y - 10}px`
    indicator.style.width = '20px'
    indicator.style.height = '20px'
    indicator.style.borderRadius = '50%'
    indicator.style.backgroundColor = 'rgba(24, 144, 255, 0.6)'
    indicator.style.border = '2px solid #1890ff'
    indicator.style.pointerEvents = 'none'
    indicator.style.zIndex = '1000'

    const container = canvasRef.current?.parentElement
    if (container) {
      container.style.position = 'relative'
      container.appendChild(indicator)

      setTimeout(() => {
        container.removeChild(indicator)
      }, 500)
    }
  }

  const showSwipeIndicator = (startX: number, startY: number, endX: number, endY: number) => {
    const container = canvasRef.current?.parentElement
    if (!container) return

    // 鍒涘缓婊戝姩杞ㄨ抗绾?
    const line = document.createElement('div')
    const length = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2))
    const angle = Math.atan2(endY - startY, endX - startX) * 180 / Math.PI

    line.style.position = 'absolute'
    line.style.left = `${startX}px`
    line.style.top = `${startY}px`
    line.style.width = `${length}px`
    line.style.height = '2px'
    line.style.backgroundColor = '#ff4d4f'
    line.style.transformOrigin = '0 50%'
    line.style.transform = `rotate(${angle}deg)`
    line.style.pointerEvents = 'none'
    line.style.zIndex = '1000'

    // 鍒涘缓绠ご
    const arrow = document.createElement('div')
    arrow.style.position = 'absolute'
    arrow.style.left = `${endX - 5}px`
    arrow.style.top = `${endY - 5}px`
    arrow.style.width = '10px'
    arrow.style.height = '10px'
    arrow.style.backgroundColor = '#ff4d4f'
    arrow.style.transform = 'rotate(45deg)'
    arrow.style.pointerEvents = 'none'
    arrow.style.zIndex = '1000'

    container.style.position = 'relative'
    container.appendChild(line)
    container.appendChild(arrow)

    setTimeout(() => {
      if (container.contains(line)) container.removeChild(line)
      if (container.contains(arrow)) container.removeChild(arrow)
    }, 800)
  }

  const showLongPressIndicator = (x: number, y: number) => {
    const indicator = document.createElement('div')
    indicator.style.position = 'absolute'
    indicator.style.left = `${x - 15}px`
    indicator.style.top = `${y - 15}px`
    indicator.style.width = '30px'
    indicator.style.height = '30px'
    indicator.style.borderRadius = '50%'
    indicator.style.backgroundColor = 'rgba(255, 77, 79, 0.6)'
    indicator.style.border = '3px solid #ff4d4f'
    indicator.style.pointerEvents = 'none'
    indicator.style.zIndex = '1000'
    indicator.style.animation = 'pulse 1s infinite'

    const container = canvasRef.current?.parentElement
    if (container) {
      container.style.position = 'relative'
      container.appendChild(indicator)

      setTimeout(() => {
        if (container.contains(indicator)) {
          container.removeChild(indicator)
        }
      }, 1000)
    }
  }

  // 鏄剧ず闀挎寜鎷栨嫿寮€濮嬫寚绀哄櫒
  const showLongPressDragStartIndicator = (x: number, y: number) => {
    const container = canvasRef.current?.parentElement
    if (!container) return

    // 鍒涘缓闀挎寜鎷栨嫿寮€濮嬫寚绀哄櫒锛堣緝澶х殑鍦嗗湀锛屾鑹诧級
    const indicator = document.createElement('div')
    indicator.style.position = 'absolute'
    indicator.style.left = `${x - 20}px`
    indicator.style.top = `${y - 20}px`
    indicator.style.width = '40px'
    indicator.style.height = '40px'
    indicator.style.borderRadius = '50%'
    indicator.style.backgroundColor = 'rgba(255, 165, 0, 0.7)'
    indicator.style.border = '4px solid #ff8c00'
    indicator.style.pointerEvents = 'none'
    indicator.style.zIndex = '1000'
    indicator.style.animation = 'pulse 0.8s infinite'
    indicator.className = 'long-press-drag-start'

    // 娣诲姞鏂囧瓧鏍囪�?
    const label = document.createElement('div')
    label.style.position = 'absolute'
    label.style.left = '50%'
    label.style.top = '50%'
    label.style.transform = 'translate(-50%, -50%)'
    label.style.fontSize = '10px'
    label.style.fontWeight = 'bold'
    label.style.color = '#fff'
    label.style.textShadow = '1px 1px 2px rgba(0,0,0,0.8)'
    label.textContent = 'Hold'
    indicator.appendChild(label)

    container.style.position = 'relative'
    container.appendChild(indicator)

    return indicator
  }

  // 鏄剧ず闀挎寜鎷栨嫿璺緞鎸囩ず鍣?
  const showLongPressDragPath = (startX: number, startY: number, endX: number, endY: number) => {
    const container = canvasRef.current?.parentElement
    if (!container) return

    // 娓呴櫎涔嬪墠鐨勬嫋鎷藉紑濮嬫寚绀哄櫒
    const existingIndicator = container.querySelector('.long-press-drag-start')
    if (existingIndicator) {
      container.removeChild(existingIndicator)
    }

    // 鍒涘缓鎷栨嫿璺緞绾匡紙绮椾竴浜涳紝姗欒壊娓愬彉�?
    const line = document.createElement('div')
    const length = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2))
    const angle = Math.atan2(endY - startY, endX - startX) * 180 / Math.PI

    line.style.position = 'absolute'
    line.style.left = `${startX}px`
    line.style.top = `${startY}px`
    line.style.width = `${length}px`
    line.style.height = '4px'
    line.style.background = 'linear-gradient(to right, #ff8c00, #ff6347)'
    line.style.transformOrigin = '0 50%'
    line.style.transform = `rotate(${angle}deg)`
    line.style.pointerEvents = 'none'
    line.style.zIndex = '999'
    line.style.boxShadow = '0 0 6px rgba(255, 140, 0, 0.6)'

    // 鍒涘缓璧峰鐐规寚绀哄櫒
    const startIndicator = document.createElement('div')
    startIndicator.style.position = 'absolute'
    startIndicator.style.left = `${startX - 8}px`
    startIndicator.style.top = `${startY - 8}px`
    startIndicator.style.width = '16px'
    startIndicator.style.height = '16px'
    startIndicator.style.borderRadius = '50%'
    startIndicator.style.backgroundColor = '#ff8c00'
    startIndicator.style.border = '2px solid #fff'
    startIndicator.style.pointerEvents = 'none'
    startIndicator.style.zIndex = '1001'

    // 鍒涘缓缁撴潫鐐规寚绀哄櫒锛堝甫绠ご�?
    const endIndicator = document.createElement('div')
    endIndicator.style.position = 'absolute'
    endIndicator.style.left = `${endX - 12}px`
    endIndicator.style.top = `${endY - 12}px`
    endIndicator.style.width = '24px'
    endIndicator.style.height = '24px'
    endIndicator.style.borderRadius = '50%'
    endIndicator.style.backgroundColor = '#ff6347'
    endIndicator.style.border = '3px solid #fff'
    endIndicator.style.pointerEvents = 'none'
    endIndicator.style.zIndex = '1001'
    endIndicator.style.boxShadow = '0 0 8px rgba(255, 99, 71, 0.8)'

    // 娣诲姞绠ご鍥炬�?
    const arrow = document.createElement('div')
    arrow.style.position = 'absolute'
    arrow.style.left = '50%'
    arrow.style.top = '50%'
    arrow.style.transform = 'translate(-50%, -50%)'
    arrow.style.fontSize = '12px'
    arrow.style.color = '#fff'
    arrow.style.fontWeight = 'bold'
    arrow.textContent = '->'
    endIndicator.appendChild(arrow)

    container.style.position = 'relative'
    container.appendChild(line)
    container.appendChild(startIndicator)
    container.appendChild(endIndicator)

    // 1.5绉掑悗娓呴櫎鎸囩ず鍣?
    setTimeout(() => {
      if (container.contains(line)) container.removeChild(line)
      if (container.contains(startIndicator)) container.removeChild(startIndicator)
      if (container.contains(endIndicator)) container.removeChild(endIndicator)
    }, 1500)
  }

  // 娣诲姞婊戝姩澶勭�?
  const [isDragging, setIsDragging] = useState(false)
  const [dragStart, setDragStart] = useState<{x: number, y: number} | null>(null)
  
  // 娣诲姞闀挎寜澶勭�?
  const [isLongPressTriggered, setIsLongPressTriggered] = useState(false)
  const longPressTimerRef = useRef<number | null>(null)
  
  // 娣诲姞闀挎寜鎷栨嫿澶勭悊鐘舵�?
  const [isLongPressDragging, setIsLongPressDragging] = useState(false)
  const [longPressDragStartPos, setLongPressDragStartPos] = useState<{x: number, y: number} | null>(null)
  
  // 娣诲姞鎷栨嫿璺緞鏀堕泦鐘舵�?
  const [dragPath, setDragPath] = useState<Array<{x: number, y: number}>>([])
  const lastMoveTimeRef = useRef<number>(0)



  // 澶勭悊鐪熸鐨勭偣鍑伙紙浠巑ouseUp涓皟鐢級
  const performClick = useCallback((canvasX: number, canvasY: number) => {
    if (!webSocket || !device) return
    
    // 妫€鏌ユ搷浣滄槸鍚﹁鍏佽
    if (!operationEnabled) {
      console.warn('Click blocked: operation disabled')
      return
    }

    const canvas = canvasRef.current
    if (!canvas) return

    const deviceCoords = convertCanvasToDeviceCoords(canvasX, canvasY, canvas, device)
    
    if (!deviceCoords) {
      console.warn('Coordinate conversion failed, click may be outside image bounds')
      return
    }



    webSocket.emit('control_message', {
      type: 'CLICK',
      deviceId,
      data: { x: deviceCoords.x, y: deviceCoords.y },
      timestamp: Date.now()
    })

    // 鏄剧ず瑙︽懜鎸囩ず鍣?
    if (screenDisplay.showTouchIndicator) {
      showTouchIndicator(canvasX, canvasY)
    }
    

  }, [webSocket, device, deviceId, screenDisplay.showTouchIndicator, convertCanvasToDeviceCoords, operationEnabled])

  // 澶勭悊闀挎寜鎿嶄�?
  const performLongPress = useCallback((canvasX: number, canvasY: number) => {
    if (!webSocket || !device) return
    
    // 妫€鏌ユ搷浣滄槸鍚﹁鍏佽
    if (!operationEnabled) {
      console.warn('Long press blocked: operation disabled')
      return
    }

    const canvas = canvasRef.current
    if (!canvas) return

    const deviceCoords = convertCanvasToDeviceCoords(canvasX, canvasY, canvas, device)
    
    if (!deviceCoords) {
      console.warn('Coordinate conversion failed, long press may be outside image bounds')
      return
    }

    console.log(`Long press: Canvas(${canvasX.toFixed(1)}, ${canvasY.toFixed(1)}) -> Device(${deviceCoords.x.toFixed(1)}, ${deviceCoords.y.toFixed(1)})`)

    webSocket.emit('control_message', {
      type: 'LONG_PRESS',
      deviceId,
      data: { x: deviceCoords.x, y: deviceCoords.y },
      timestamp: Date.now()
    })

    // 鏄剧ず闀挎寜鎸囩ず鍣紙浣跨敤涓嶅悓棰滆壊鍖哄垎�?
    if (screenDisplay.showTouchIndicator) {
      showLongPressIndicator(canvasX, canvasY)
    }

  }, [webSocket, device, deviceId, screenDisplay.showTouchIndicator, convertCanvasToDeviceCoords, operationEnabled])
  
  const handleMouseDown = useCallback((event: React.MouseEvent) => {
    event.preventDefault()
    setIsDragging(true)
    
    const canvas = canvasRef.current
    if (!canvas) return
    
    const rect = canvas.getBoundingClientRect()
    const startX = event.clientX - rect.left
    const startY = event.clientY - rect.top
    
    setDragStart({ x: startX, y: startY })
    
    // 鍚姩闀挎寜璁℃椂鍣?
    setIsLongPressTriggered(false)
    setIsLongPressDragging(false)
    setLongPressDragStartPos(null)
    setDragPath([]) // 娓呯悊鎷栨嫿璺�?
    longPressTimerRef.current = window.setTimeout(() => {
      setIsLongPressTriggered(true)
      // 闀挎寜瑙﹀彂鍚庝笉绔嬪嵆鎵ц鎿嶄綔锛岃€屾槸鍑嗗杩涘叆鎷栨嫿妯″紡
      console.log('Long press triggered, preparing drag mode')
    }, 500) // 500ms 鍚庤Е鍙戦暱鎸?
  }, [performLongPress])

  const handleMouseMove = useCallback((event: React.MouseEvent) => {
    if (!isDragging || !dragStart) return
    
    event.preventDefault()
    
    // 澶勭悊闀挎寜鎷栨嫿閫昏緫 - 浼樺寲涓鸿繛缁墜鍔?
    if (isLongPressTriggered && !isLongPressDragging) {
      // 闀挎寜宸茶Е鍙戯紝鐢ㄦ埛寮€濮嬫嫋鎷斤紝杩涘叆闀挎寜鎷栨嫿妯″紡
      if (!webSocket || !device || !operationEnabled) return
      
      const canvas = canvasRef.current
      if (!canvas) return
      
      // 杞崲寮€濮嬩綅缃负璁惧鍧愭�?
      const startCoords = convertCanvasToDeviceCoords(dragStart.x, dragStart.y, canvas, device)
      if (!startCoords) return
      
      setIsLongPressDragging(true)
      setLongPressDragStartPos(startCoords)
      
      // 浼樺寲锛氬垵濮嬪寲鎷栨嫿璺緞锛屽寘鍚捣濮嬬偣
      setDragPath([startCoords])
      
      // 鏄剧ず闀挎寜鎷栨嫿寮€濮嬫寚绀哄櫒
      if (screenDisplay.showTouchIndicator) {
        showLongPressDragStartIndicator(dragStart.x, dragStart.y)
      }
      
      console.log(`Long press drag started: Device(${startCoords.x.toFixed(1)}, ${startCoords.y.toFixed(1)})`)
    } else if (isLongPressDragging) {
      // 浼樺寲锛氭敹闆嗘嫋鎷借矾寰勭偣锛岃€屼笉鏄珛鍗冲彂閫佹秷鎭?
      const now = Date.now()
      
      // 棰戠巼鎺у埗锛氭瘡50ms鏈€澶氳褰曚竴涓偣锛岄伩鍏嶈矾寰勮繃�?
      if (now - lastMoveTimeRef.current < 50) return
      
      const canvas = canvasRef.current
      if (!canvas) return
      
      const rect = canvas.getBoundingClientRect()
      const currentX = event.clientX - rect.left
      const currentY = event.clientY - rect.top
      
      const currentCoords = convertCanvasToDeviceCoords(currentX, currentY, canvas, device)
      if (!currentCoords) return
      
      // 娣诲姞褰撳墠鐐瑰埌璺緞�?
      setDragPath(prevPath => {
        const newPath = [...prevPath, currentCoords]
        // 闄愬埗璺緞鐐规暟閲忥紝閬垮厤鍐呭瓨鍗犵敤杩囧ぇ
        return newPath.length > 100 ? [newPath[0], ...newPath.slice(-99)] : newPath
      })
      
      lastMoveTimeRef.current = now
      console.debug(`Long press drag collecting path: Device(${currentCoords.x.toFixed(1)}, ${currentCoords.y.toFixed(1)})`)
    }
  }, [isDragging, dragStart, isLongPressTriggered, isLongPressDragging, webSocket, device, deviceId, operationEnabled, convertCanvasToDeviceCoords, screenDisplay.showTouchIndicator, dragPath])

  const handleMouseUp = useCallback((event: React.MouseEvent) => {
    if (!isDragging || !dragStart) return
    
    event.preventDefault()
    setIsDragging(false)
    
    // 娓呯悊闀挎寜璁℃椂鍣?
    if (longPressTimerRef.current) {
      clearTimeout(longPressTimerRef.current)
      longPressTimerRef.current = null
    }
    
    // 澶勭悊闀挎寜鐩稿叧鎿嶄綔
    if (isLongPressTriggered) {
      if (isLongPressDragging) {
        // 浼樺寲锛氭墽琛屽畬鏁寸殑闀挎寜鎷栨嫿鎵嬪娍
        if (webSocket && device && operationEnabled && longPressDragStartPos && dragPath.length > 0) {
          const canvas = canvasRef.current
          if (canvas) {
            const rect = canvas.getBoundingClientRect()
            const endX = event.clientX - rect.left
            const endY = event.clientY - rect.top
            
            const endCoords = convertCanvasToDeviceCoords(endX, endY, canvas, device)
            if (endCoords) {
              // 灏嗙粨鏉熺偣娣诲姞鍒拌矾寰勪�?
              const completePath = [...dragPath, endCoords]
              
              // 鍙戦€佸寘鍚畬鏁磋矾寰勭殑闀挎寜鎷栨嫿娑堟伅
              webSocket.emit('control_message', {
                type: 'LONG_PRESS_DRAG',
                deviceId,
                data: { 
                  path: completePath,
                  startX: completePath[0].x,
                  startY: completePath[0].y,
                  endX: endCoords.x, 
                  endY: endCoords.y,
                  duration: Math.max(2000, 1500 + completePath.length * 20) // 浼樺寲锛氫负寰Щ鍔ㄩ暱鎸夐鐣欐洿澶氭椂�?
                },
                timestamp: Date.now()
              })
              
              console.log(`Long press drag completed: Device(${completePath[0].x.toFixed(1)}, ${completePath[0].y.toFixed(1)}) -> Device(${endCoords.x.toFixed(1)}, ${endCoords.y.toFixed(1)}), pathPoints=${completePath.length}`)
              
              // 鏄剧ず闀挎寜鎷栨嫿璺緞鎸囩ず鍣?
              if (screenDisplay.showTouchIndicator) {
                showLongPressDragPath(dragStart.x, dragStart.y, endX, endY)
              }
            }
          }
        }
      } else {
        // 闀挎寜宸茶Е鍙戜絾娌℃湁鎷栨嫿锛屾墽琛屾櫘閫氶暱鎸夋搷浣?
        const canvas = canvasRef.current
        const rect = canvas?.getBoundingClientRect()
        if (rect) {
          const endX = event.clientX - rect.left
          const endY = event.clientY - rect.top
          performLongPress(endX, endY)
        }
      }
      
      // 娓呯悊闀挎寜鎷栨嫿鐩稿叧鐘舵�?
      setDragStart(null)
      setIsLongPressTriggered(false)
      setIsLongPressDragging(false)
      setLongPressDragStartPos(null)
      setDragPath([]) // 娓呯悊鎷栨嫿璺�?
      return
    }
    
    const canvas = canvasRef.current
    if (!canvas || !webSocket || !device) return
    
    const rect = canvas.getBoundingClientRect()
    const endX = event.clientX - rect.left
    const endY = event.clientY - rect.top
    
    // 璁＄畻绉诲姩璺濈�?
    const deltaX = Math.abs(endX - dragStart.x)
    const deltaY = Math.abs(endY - dragStart.y)
    const threshold = 10 // 澧炲姞闃堝€煎埌10鍍忕礌锛岄伩鍏嶈瑙?
    
    if (deltaX < threshold && deltaY < threshold) {
      // 灏忚窛绂荤Щ鍔紝褰撲綔鐐瑰嚮澶勭�?
      performClick(endX, endY)
    } else {
      // 澶ц窛绂荤Щ鍔紝褰撲綔婊戝姩澶勭悊
      
      // 妫€鏌ユ搷浣滄槸鍚﹁鍏佽
      if (!operationEnabled) {
        console.warn('Swipe blocked: operation disabled')
        return
      }
      
      // 浣跨敤姝ｇ‘鐨勫潗鏍囪浆鎹㈠嚱�?
      const startCoords = convertCanvasToDeviceCoords(dragStart.x, dragStart.y, canvas, device)
      const endCoords = convertCanvasToDeviceCoords(endX, endY, canvas, device)
      
      if (!startCoords || !endCoords) {
        console.warn('Coordinate conversion failed, swipe may be outside image bounds')
        return
      }
      

      
      webSocket.emit('control_message', {
        type: 'SWIPE',
        deviceId,
        data: { 
          startX: startCoords.x,
          startY: startCoords.y,
          endX: endCoords.x,
          endY: endCoords.y,
          duration: 300
        },
        timestamp: Date.now()
      })

      // 鏄剧ず婊戝姩鎸囩ず鍣?
      if (screenDisplay.showTouchIndicator) {
        showSwipeIndicator(dragStart.x, dragStart.y, endX, endY)
      }
    }
    
    setDragStart(null)
  }, [isDragging, dragStart, webSocket, device, deviceId, screenDisplay.showTouchIndicator, convertCanvasToDeviceCoords, performClick, operationEnabled, isLongPressTriggered, isLongPressDragging, longPressDragStartPos, performLongPress, dragPath])
  
  // 澶勭悊榧犳爣绂诲紑鐢诲竷
  const handleMouseLeave = useCallback(() => {
    setIsDragging(false)
    setDragStart(null)
    
    // 娓呯悊闀挎寜璁℃椂鍣ㄥ拰鐘舵�?
    if (longPressTimerRef.current) {
      clearTimeout(longPressTimerRef.current)
      longPressTimerRef.current = null
    }
    setIsLongPressTriggered(false)
    
    // 娓呯悊闀挎寜鎷栨嫿鐩稿叧鐘舵�?
    setIsLongPressDragging(false)
    setLongPressDragStartPos(null)
    setDragPath([]) // 娓呯悊鎷栨嫿璺�?
  }, [])

  // 鍏ㄥ睆鍒囨崲
  const toggleFullscreen = useCallback(() => {
    const container = fullscreenContainerRef.current
    if (!container) return

    if (!document.fullscreenElement) {
      container.requestFullscreen().catch(err => {
        console.warn('杩涘叆鍏ㄥ睆澶辫�?', err)
      })
    } else {
      document.exitFullscreen()
    }
  }, [])

  // 鐩戝�?fullscreenchange 浜嬩欢鍚屾鐘舵�?
  useEffect(() => {
    const onFullscreenChange = () => {
      setIsFullscreen(!!document.fullscreenElement)
    }
    document.addEventListener('fullscreenchange', onFullscreenChange)
    return () => {
      document.removeEventListener('fullscreenchange', onFullscreenChange)
    }
  }, [])

  // 鍏ㄥ睆妯″紡涓嬭绠?canvas �?CSS 灏哄锛堜繚鎸佸楂樻瘮锛岄€傞厤灞忓箷�?
  // 浣跨�?state 瀛樺偍鍏ㄥ睆瀹瑰櫒灏哄锛岀‘淇濆叏灞忓垏鎹㈠拰绐楀彛resize鏃惰Е鍙戦噸娓叉煋
  const [containerSize, setContainerSize] = useState<{ w: number; h: number }>({ w: 0, h: 0 })

  useEffect(() => {
    if (!isFullscreen) return
    const updateSize = () => {
      setContainerSize({ w: window.innerWidth, h: window.innerHeight })
    }
    // 鍏ㄥ睆鍚庣珛鍗虫洿鏂?+ 寤惰繜鏇存柊锛堥儴鍒嗘祻瑙堝櫒鍏ㄥ睆鍔ㄧ敾瀹屾垚鍚庡昂瀵告墠绋冲畾�?
    updateSize()
    const timer = setTimeout(updateSize, 100)
    window.addEventListener('resize', updateSize)
    return () => {
      clearTimeout(timer)
      window.removeEventListener('resize', updateSize)
    }
  }, [isFullscreen])

  const getCanvasStyle = useCallback((): React.CSSProperties => {
    if (!isFullscreen || !imageSize || containerSize.w === 0) {
      return {
        width: imageSize ? `${imageSize.width}px` : '100%',
        height: imageSize ? `${imageSize.height}px` : 'auto',
      }
    }
    // 鍏ㄥ睆鏃讹細canvas 鎸夊楂樻瘮缂╂斁濉弧灞忓�?
    const scale = Math.min(containerSize.w / imageSize.width, containerSize.h / imageSize.height)
    return {
      width: `${Math.round(imageSize.width * scale)}px`,
      height: `${Math.round(imageSize.height * scale)}px`,
    }
  }, [isFullscreen, imageSize, containerSize])

  const webrtcDebugUpdatedAt = Number.isFinite(webrtcDebug.updatedAt)
    ? new Date(webrtcDebug.updatedAt).toISOString().slice(11, 19)
    : '--:--:--'
  const webrtcDiagnosisHints = buildWebRtcDiagnosisHints(webrtcDebug)
  const webrtcDiagnosticReport = buildWebRtcDiagnosticReport()



  if (!device) {
    return (
      <Card title="设备屏幕">
        <div style={{ textAlign: 'center', padding: '50px' }}>
          设备未找到
        </div>
      </Card>
    )
  }

  return (
    <>
      {/* 娣诲姞CSS鍔ㄧ�?*/}
      <style>
        {`
          @keyframes pulse {
            0% { opacity: 1; }
            50% { opacity: 0.6; }
            100% { opacity: 1; }
          }
        `}
      </style>
      
      <div
        ref={fullscreenContainerRef}
        style={{ 
          position: 'relative', 
          width: '100%', 
          height: '100%',
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          overflow: 'hidden',
          backgroundColor: isFullscreen ? '#000' : undefined,
        }}
      >
      {/* 椤堕儴淇℃伅�?- 灞忓箷闃呰鍣ㄦā寮忎笅闅愯棌 */}

      {/* 鎿嶄綔鐘舵€佹寚绀哄櫒 */}
      {!operationEnabled && (
        <div style={{
          position: 'absolute',
          top: '20px',
          right: '20px',
          zIndex: 20,
          color: '#fff',
          backgroundColor: 'rgba(255, 77, 79, 0.9)',
          padding: '8px 16px',
          borderRadius: '4px',
          fontSize: '14px',
          fontWeight: 'bold',
          border: '2px solid #ff4d4f',
          animation: 'pulse 2s infinite'
        }}>
          [LOCKED] Input blocked
        </div>
      )}



      {isLoading && (
        <div style={{
          position: 'absolute',
          top: '50%',
          left: '50%',
          transform: 'translate(-50%, -50%)',
          zIndex: 10
        }}>
          <Spin size="large" />
          <div style={{ color: '#fff', marginTop: '16px' }}>
            {videoTransport === 'webrtc' ? 'Connecting WebRTC stream...' : 'Connecting screenshot stream...'}
          </div>
        </div>
      )}
      
      {srtStatusText && (
        <div style={{
          position: 'absolute',
          top: '8px',
          left: '8px',
          zIndex: 20,
          color: '#fff',
          background: 'rgba(0,0,0,0.6)',
          borderRadius: '4px',
          padding: '4px 8px',
          fontSize: '11px',
          maxWidth: '70%',
          wordBreak: 'break-word'
        }}>
          {srtStatusText}
        </div>
      )}
      
      {/* Canvas wrapper - position:relative so touch/swipe indicators are positioned relative to canvas */}
      <div style={{ position: 'relative' }}>
      {videoTransport === 'webrtc' && (
        <video
          ref={srtVideoRef}
          autoPlay
          muted
          playsInline
          onLoadedMetadata={handleSrtVideoLoadedMetadata}
          onWaiting={handleSrtVideoWaiting}
          onPlaying={handleSrtVideoPlaying}
          style={{
            ...getCanvasStyle(),
            display: 'block',
            objectFit: 'contain',
            backgroundColor: '#000',
            pointerEvents: 'none',
            filter: exposureFilter
          }}
        />
      )}
      <canvas
        ref={canvasRef}
        style={{
          ...getCanvasStyle(),
          cursor: 'pointer',
          display: 'block',
            ...(videoTransport === 'webrtc'
              ? {
                position: 'absolute',
                top: 0,
                left: 0,
                backgroundColor: 'transparent'
              }
              : {
                filter: exposureFilter
              })
        }}
        onMouseDown={handleMouseDown}
        onMouseUp={handleMouseUp}
        onMouseMove={handleMouseMove}
        onMouseLeave={handleMouseLeave}
        onContextMenu={(e) => e.preventDefault()}
      />
      </div>

      {showWebRtcDebugPanel && (
        <div style={{
          position: 'absolute',
          right: '8px',
          bottom: '40px',
          zIndex: 22,
          width: 'min(460px, calc(100vw - 16px))',
          maxHeight: '55vh',
          overflowY: 'auto',
          background: 'rgba(0,0,0,0.86)',
          color: '#d9f7ff',
          border: '1px solid rgba(24, 144, 255, 0.45)',
          borderRadius: '6px',
          padding: '8px',
          fontFamily: 'monospace',
          fontSize: '11px',
          lineHeight: 1.45,
          wordBreak: 'break-word'
        }}>
          <div style={{ color: '#69c0ff', marginBottom: '6px' }}>
            WebRTC Debug Panel
          </div>
          <div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap', marginBottom: '6px' }}>
            <button
              onClick={handleCopyWebRtcReport}
              style={{
                background: 'rgba(24,144,255,0.75)',
                color: '#fff',
                border: 'none',
                padding: '2px 8px',
                borderRadius: '3px',
                fontSize: '11px',
                cursor: 'pointer'
              }}
            >
              Copy Report
            </button>
            <button
              onClick={handleCopyWebRtcEvents}
              style={{
                background: 'rgba(0,0,0,0.55)',
                color: '#fff',
                border: '1px solid rgba(255,255,255,0.18)',
                padding: '2px 8px',
                borderRadius: '3px',
                fontSize: '11px',
                cursor: 'pointer'
              }}
            >
              Copy Events
            </button>
            <button
              onClick={handleClearWebRtcEvents}
              style={{
                background: 'rgba(0,0,0,0.55)',
                color: '#fff',
                border: '1px solid rgba(255,255,255,0.18)',
                padding: '2px 8px',
                borderRadius: '3px',
                fontSize: '11px',
                cursor: 'pointer'
              }}
            >
              Clear Events
            </button>
          </div>
          {webrtcCopyStatus && (
            <div style={{ color: '#95de64', marginBottom: '6px' }}>{webrtcCopyStatus}</div>
          )}
          <div>transport: {webrtcDebug.transport}</div>
          <div>session mode: {videoSessionMode}</div>
          <div>profile: {webrtcDebug.profileLabel}</div>
          <div>pc/ice/sdp: {webrtcDebug.connectionState} / {webrtcDebug.iceConnectionState} / {webrtcDebug.signalingState}</div>
          <div>reconnect: {webrtcDebug.reconnectAttempts} / {WEBRTC_MAX_RECONNECT_ATTEMPTS}</div>
          <div>strike weak/stable: {webrtcDebug.weakNetworkStrike} / {webrtcDebug.stableNetworkStrike}</div>
          <div>ice local sent: {webrtcDebug.localIceSent}</div>
          <div>ice remote recv/applied/queued: {webrtcDebug.remoteIceReceived} / {webrtcDebug.remoteIceApplied} / {webrtcDebug.queuedRemoteIce}</div>
          <div>inbound fps: {webrtcDebug.inboundFps.toFixed(1)}</div>
          <div>loss rate: {(webrtcDebug.lossRate * 100).toFixed(2)}%</div>
          <div>last reason: {webrtcDebug.lastReason || '-'}</div>
          <div>last error: {webrtcDebug.lastError || '-'}</div>
          <div>device state: {webrtcDebug.lastDeviceState || '-'}</div>
          <div>device detail: {webrtcDebug.lastDeviceDetail || '-'}</div>
          <div>updated at: {webrtcDebugUpdatedAt}</div>
          <div style={{ color: '#69c0ff', marginTop: '6px' }}>auto diagnosis:</div>
          {webrtcDiagnosisHints.map((hint, index) => (
            <div key={`${hint}-${index}`}>[{index + 1}] {hint}</div>
          ))}
          <div style={{ color: '#69c0ff', marginTop: '6px' }}>events:</div>
          {webrtcDebug.events.length === 0 && <div>-</div>}
          {webrtcDebug.events.map((event, index) => (
            <div key={`${event}-${index}`}>{event}</div>
          ))}
          <div style={{ color: '#69c0ff', marginTop: '6px' }}>report text:</div>
          <textarea
            readOnly
            value={webrtcDiagnosticReport}
            onFocus={(event) => event.currentTarget.select()}
            style={{
              width: '100%',
              minHeight: '120px',
              marginTop: '4px',
              background: 'rgba(0,0,0,0.55)',
              color: '#fff',
              border: '1px solid rgba(255,255,255,0.2)',
              borderRadius: '4px',
              padding: '6px',
              fontFamily: 'monospace',
              fontSize: '11px',
              lineHeight: 1.35,
              resize: 'vertical'
            }}
          />
        </div>
      )}

      {/* 鐢昏川鎺у埗闈㈡澘 + 鍏ㄥ睆鎸夐挳 */}
      <div style={{
        position: 'absolute',
        bottom: '8px',
        right: '8px',
        zIndex: 20,
        display: 'flex',
        alignItems: 'center',
        gap: '6px',
      }}>
        <span style={{
          color: '#fff',
          fontSize: '11px',
          background: 'rgba(0,0,0,0.5)',
          padding: '2px 6px',
          borderRadius: '3px',
          fontFamily: 'monospace',
        }}>
          [{VIDEO_SESSION_MODE_LABEL[videoSessionMode]}|{videoTransport === 'webrtc' ? srtProfileLabel : 'WS'}] {displayFps} FPS{videoTransport === 'ws_binary' && networkStats.dropRate > 0.05 ? ` [!]${(networkStats.dropRate * 100).toFixed(0)}% drop` : ''}
        </span>
        <div style={{ position: 'relative' }}>
          <button
            onClick={() => setShowQualityPanel(!showQualityPanel)}
            style={{
              background: 'rgba(0,0,0,0.5)',
              color: '#fff',
              border: 'none',
              padding: '2px 8px',
              borderRadius: '3px',
              fontSize: '11px',
              cursor: 'pointer',
            }}
          >
            {QUALITY_PROFILES.find(p => p.key === currentProfile)?.label || 'Custom'}
          </button>
          {showQualityPanel && (
            <div style={{
              position: 'absolute',
              bottom: '100%',
              right: 0,
              marginBottom: '4px',
              background: 'rgba(0,0,0,0.85)',
              borderRadius: '6px',
              padding: '8px',
              minWidth: '120px',
            }}>
              {QUALITY_PROFILES.map(p => (
                <div
                  key={p.key}
                  onClick={() => { handleSetProfile(p.key); setShowQualityPanel(false) }}
                  style={{
                    color: currentProfile === p.key ? '#1890ff' : '#fff',
                    fontSize: '12px',
                    padding: '4px 8px',
                    cursor: 'pointer',
                    borderRadius: '3px',
                    background: currentProfile === p.key ? 'rgba(24,144,255,0.15)' : 'transparent',
                  }}
                >
                  {p.label} ({p.fps}fps / {p.resolution})
                </div>
              ))}
            </div>
          )}
        </div>
        <button
          onClick={() => setShowWebRtcDebugPanel((prev) => !prev)}
          style={{
            background: showWebRtcDebugPanel ? 'rgba(24,144,255,0.6)' : 'rgba(0,0,0,0.5)',
            color: '#fff',
            border: 'none',
            padding: '2px 8px',
            borderRadius: '3px',
            fontSize: '11px',
            cursor: 'pointer'
          }}
        >
          {showWebRtcDebugPanel ? 'Debug On' : 'Debug'}
        </button>
        {/* 鍏ㄥ�?閫€鍑哄叏灞忔寜�?*/}
        <button
          onClick={toggleFullscreen}
          title={isFullscreen ? '退出全屏 (ESC)' : '全屏显示'}
          style={{
            background: 'rgba(0,0,0,0.5)',
            color: '#fff',
            border: 'none',
            padding: '2px 8px',
            borderRadius: '3px',
            fontSize: '13px',
            cursor: 'pointer',
            lineHeight: 1,
          }}
        >
          {isFullscreen ? 'X' : '[ ]'}
        </button>
      </div>
    </div>
    </>
  )
}

export default DeviceScreen




