using System; using System.Collections; using System.Collections.Generic; using Kill.UI.Components; using UnityEngine; using UnityEngine.UI; namespace Kill.UI.Pages { /// /// 二维码扫描器 - 调用摄像头扫描二维码 /// public class ScanQRcode : MonoBehaviour { [Serializable] public enum ScanType { ConnectDevice, // 连接设备 Other // 其他用途 } [Header("UI组件")] public RawImage cameraPreview; // 摄像头预览 [Header("扫描设置")] public float scanInterval = 0.15f; // 扫描间隔(秒) public ScanType scanType = ScanType.ConnectDevice; // 扫描类型 [Header("小二维码优化")] [Tooltip("启用图像放大(推荐用于小二维码)")] public bool enableUpscale = true; [Tooltip("放大倍数")] public float upscaleFactor = 2f; [Tooltip("启用多区域扫描(从画面中心向四周扫描)")] bool enableMultiRegionScan = false; [Tooltip("扫描区域数量")] public int scanRegionCount = 3; [Header("性能优化")] [Tooltip("降低扫描分辨率(推荐值:640或480)")] public int scanResolutionWidth = 640; [Tooltip("扫描处理线程数")] public bool useAsyncScan = true; [Header("自动对焦")] [Tooltip("启用自动对焦")] public bool enableAutoFocus = true; [Tooltip("对焦间隔(秒)")] public float focusInterval = 2f; // 摄像头相关 private WebCamTexture webCamTexture; private WebCamDevice currentDevice; private bool isScanning = false; private bool isProcessing = false; // 扫描结果回调 public event Action OnQRCodeScanned; void OnEnable() { StartScan(); } void OnDisable() { StopScan(); } /// /// 开始扫描 /// public void StartScan() { if (isScanning) return; StartCoroutine(InitializeCamera()); } /// /// 停止扫描 /// public void StopScan() { isScanning = false; StopAllCoroutines(); if (webCamTexture != null && webCamTexture.isPlaying) { webCamTexture.Stop(); webCamTexture = null; } } /// /// 关闭扫码界面 /// public void Close() { StopScan(); gameObject.SetActive(false); } /// /// 初始化摄像头 /// private IEnumerator InitializeCamera() { // Android 使用 Permission API 请求权限 #if UNITY_ANDROID && !UNITY_EDITOR if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Camera)) { UnityEngine.Android.Permission.RequestUserPermission(UnityEngine.Android.Permission.Camera); // 等待权限请求结果 yield return new WaitUntil(() => UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Camera)); } if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Camera)) { UpdateStatus("100085"); yield break; } #else // 其他平台使用传统方式 yield return Application.RequestUserAuthorization(UserAuthorization.WebCam); if (!Application.HasUserAuthorization(UserAuthorization.WebCam)) { UpdateStatus("100085"); yield break; } #endif // 获取后置摄像头 WebCamDevice[] devices = WebCamTexture.devices; if (devices.Length == 0) { UpdateStatus("100086"); yield break; } // 优先使用后置摄像头,并选择支持自动对焦的摄像头 string deviceName = devices[0].name; for (int i = 0; i < devices.Length; i++) { if (!devices[i].isFrontFacing) { deviceName = devices[i].name; currentDevice = devices[i]; break; } } // 使用较低的扫描分辨率以提高性能 int targetWidth = scanResolutionWidth; int targetHeight = Mathf.RoundToInt(targetWidth * Screen.height / Screen.width); // 创建摄像头纹理 webCamTexture = new WebCamTexture(deviceName, targetWidth, targetHeight, 30); cameraPreview.texture = webCamTexture; // 开始播放 webCamTexture.Play(); // 等待摄像头启动 yield return new WaitUntil(() => webCamTexture.width > 100); // 调整预览画面比例 AdjustPreviewAspect(); // 启动自动对焦 if (enableAutoFocus) { StartCoroutine(AutoFocusCoroutine()); } isScanning = true; // 开始扫描 StartCoroutine(ScanCoroutine()); } /// /// 自动对焦协程 /// private IEnumerator AutoFocusCoroutine() { while (isScanning && webCamTexture != null && webCamTexture.isPlaying) { TriggerAutoFocus(); yield return new WaitForSeconds(focusInterval); } } /// /// 触发自动对焦 /// private void TriggerAutoFocus() { #if UNITY_ANDROID && !UNITY_EDITOR try { using (AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) { using (AndroidJavaObject activity = unityPlayer.GetStatic("currentActivity")) { using (AndroidJavaObject camera = new AndroidJavaObject("android.hardware.Camera")) { // 尝试使用 Camera API 触发对焦 var cameraClass = new AndroidJavaClass("android.hardware.Camera"); var cameraInstance = cameraClass.CallStatic("open"); if (cameraInstance != null) { var parameters = cameraInstance.Call("getParameters"); if (parameters != null) { // 设置对焦模式 var supportedModes = parameters.Call("getSupportedFocusModes"); if (supportedModes != null) { // 优先使用连续自动对焦 if (supportedModes.Call("contains", "continuous-video")) { parameters.Call("setFocusMode", "continuous-video"); } else if (supportedModes.Call("contains", "auto")) { parameters.Call("setFocusMode", "auto"); } cameraInstance.Call("setParameters", parameters); } } cameraInstance.Call("release"); } } } } } catch (Exception e) { Debug.LogWarning($"[ScanQRcode] 自动对焦设置失败: {e.Message}"); } #elif UNITY_IOS && !UNITY_EDITOR // iOS 自动对焦通常由系统自动处理 // 如果需要手动控制,需要使用原生插件 Debug.Log("[ScanQRcode] iOS 自动对焦由系统管理"); #endif } /// /// 调整预览画面比例 /// private void AdjustPreviewAspect() { if (webCamTexture == null) return; float videoRatio = (float)webCamTexture.width / webCamTexture.height; float screenRatio = (float)Screen.width / Screen.height; // 根据摄像头方向调整 int rotation = webCamTexture.videoRotationAngle; cameraPreview.rectTransform.localEulerAngles = new Vector3(0, 0, -rotation); // 调整缩放以适应屏幕 if (rotation == 90 || rotation == 270) { videoRatio = 1f / videoRatio; } // 保持比例填充 if (videoRatio > screenRatio) { cameraPreview.rectTransform.localScale = new Vector3(screenRatio / videoRatio, 1, 1); } else { cameraPreview.rectTransform.localScale = new Vector3(1, videoRatio / screenRatio, 1); } } /// /// 扫描协程 /// private IEnumerator ScanCoroutine() { // 等待一帧确保摄像头已准备好 yield return null; while (isScanning && webCamTexture != null && webCamTexture.isPlaying) { // 避免同时处理多帧 if (isProcessing) { yield return null; continue; } isProcessing = true; // 获取当前帧 Texture2D snapshot = GetSnapshot(); if (snapshot != null) { // 异步解析二维码以避免卡顿 if (useAsyncScan) { string result = null; bool scanComplete = false; // 在后台线程执行解码 System.Threading.ThreadPool.QueueUserWorkItem(_ => { try { result = DecodeQRCode(snapshot); } catch (Exception e) { Debug.LogError($"[ScanQRcode] 解码异常: {e.Message}"); } finally { scanComplete = true; } }); // 等待解码完成 yield return new WaitUntil(() => scanComplete); // 在主线程销毁纹理 Destroy(snapshot); // 处理结果 if (!string.IsNullOrEmpty(result)) { ProcessScanResult(result); yield break; } } else { // 同步解码 string result = DecodeQRCode(snapshot); Destroy(snapshot); if (!string.IsNullOrEmpty(result)) { ProcessScanResult(result); yield break; } } } isProcessing = false; yield return new WaitForSeconds(scanInterval); } } /// /// 处理扫描结果 /// private void ProcessScanResult(string result) { // 根据扫描类型处理结果 if (scanType == ScanType.ConnectDevice) { // 从结果中提取 MAC 地址 string macAddress = ExtractMacAddress(result); if (!string.IsNullOrEmpty(macAddress)) { // 扫描成功,仅回传 MAC 地址 OnQRCodeScanned?.Invoke(macAddress); } else { // 二维码不符合要求,显示提示 UpdateStatus("100084"); } } else { if (!string.IsNullOrEmpty(result)) { // 其他用途,直接回传结果 OnQRCodeScanned?.Invoke(result); } else { // 二维码不符合要求,显示提示 UpdateStatus("100084"); } } } /// /// 获取摄像头当前帧 /// private Texture2D GetSnapshot() { if (webCamTexture == null || !webCamTexture.isPlaying) return null; try { Texture2D snapshot = new Texture2D(webCamTexture.width, webCamTexture.height); snapshot.SetPixels(webCamTexture.GetPixels()); snapshot.Apply(); // 如果启用放大,对图像进行放大处理 if (enableUpscale && upscaleFactor > 1f) { snapshot = UpscaleTexture(snapshot, upscaleFactor); } return snapshot; } catch { return null; } } /// /// 放大纹理(使用双线性插值)- 优化版本 /// private Texture2D UpscaleTexture(Texture2D source, float factor) { // 限制放大倍数,避免过度消耗性能 factor = Mathf.Clamp(factor, 1f, 3f); int newWidth = Mathf.RoundToInt(source.width * factor); int newHeight = Mathf.RoundToInt(source.height * factor); // 限制最大尺寸,避免内存问题 newWidth = Mathf.Min(newWidth, 1280); newHeight = Mathf.Min(newHeight, 720); // 如果尺寸没有变化,直接返回原图 if (newWidth <= source.width || newHeight <= source.height) { return source; } Texture2D result = new Texture2D(newWidth, newHeight, TextureFormat.RGB24, false); // 使用 RenderTexture 进行高质量缩放 RenderTexture rt = RenderTexture.GetTemporary(newWidth, newHeight, 0, RenderTextureFormat.RGB565); RenderTexture.active = rt; Graphics.Blit(source, rt); result.ReadPixels(new Rect(0, 0, newWidth, newHeight), 0, 0); result.Apply(); RenderTexture.active = null; RenderTexture.ReleaseTemporary(rt); // 销毁原始纹理 Destroy(source); return result; } /// /// 解析二维码(支持多区域扫描) /// private string DecodeQRCode(Texture2D texture) { try { // 首先尝试全图扫描 string result = TryDecodeRegion(texture, 0, 0, texture.width, texture.height); if (!string.IsNullOrEmpty(result)) return result; // 如果启用了多区域扫描,尝试从中心向外扫描不同区域 if (enableMultiRegionScan) { result = TryDecodeMultipleRegions(texture); if (!string.IsNullOrEmpty(result)) return result; } return null; } catch (Exception ex) { Debug.LogError($"二维码解析失败: {ex.Message}"); return null; } } /// /// 尝试从多个区域扫描(中心优先) /// private string TryDecodeMultipleRegions(Texture2D texture) { int width = texture.width; int height = texture.height; // 定义扫描区域(从中心向外) float[] regionScales = { 0.6f, 0.8f, 1.0f }; foreach (float scale in regionScales) { int regionW = Mathf.RoundToInt(width * scale); int regionH = Mathf.RoundToInt(height * scale); int startX = (width - regionW) / 2; int startY = (height - regionH) / 2; string result = TryDecodeRegion(texture, startX, startY, regionW, regionH); if (!string.IsNullOrEmpty(result)) return result; } return null; } /// /// 尝试解码指定区域 /// private string TryDecodeRegion(Texture2D texture, int startX, int startY, int width, int height) { try { // 确保区域在有效范围内 startX = Mathf.Max(0, startX); startY = Mathf.Max(0, startY); width = Mathf.Min(width, texture.width - startX); height = Mathf.Min(height, texture.height - startY); // 获取像素数据 Color32[] pixels = texture.GetPixels32(); // 转换为灰度图 byte[] grayscale = new byte[width * height]; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int srcIndex = (startY + y) * texture.width + (startX + x); int dstIndex = y * width + x; if (srcIndex < pixels.Length) { // 使用标准亮度公式: Y = 0.299R + 0.587G + 0.114B grayscale[dstIndex] = (byte)(0.299f * pixels[srcIndex].r + 0.587f * pixels[srcIndex].g + 0.114f * pixels[srcIndex].b); } } } // 创建 LuminanceSource var luminanceSource = new ZXing.PlanarYUVLuminanceSource( grayscale, width, height, 0, 0, width, height, false ); // 尝试多种二值化算法 string result = TryDecodeWithBinarizer(luminanceSource); if (!string.IsNullOrEmpty(result)) return result; return null; } catch { return null; } } /// /// 尝试使用不同的二值化算法解码 /// private string TryDecodeWithBinarizer(ZXing.PlanarYUVLuminanceSource luminanceSource) { var hints = new Dictionary { { ZXing.DecodeHintType.TRY_HARDER, true }, { ZXing.DecodeHintType.POSSIBLE_FORMATS, new List { ZXing.BarcodeFormat.QR_CODE } } }; var reader = new ZXing.QrCode.QRCodeReader(); // 尝试 HybridBinarizer try { var binarizer = new ZXing.Common.HybridBinarizer(luminanceSource); var binaryBitmap = new ZXing.BinaryBitmap(binarizer); var result = reader.decode(binaryBitmap, hints); if (result != null) return result.Text; } catch { } // 尝试 GlobalHistogramBinarizer try { var binarizer = new ZXing.Common.GlobalHistogramBinarizer(luminanceSource); var binaryBitmap = new ZXing.BinaryBitmap(binarizer); var result = reader.decode(binaryBitmap, hints); if (result != null) return result.Text; } catch { } return null; } /// /// 从字符串中提取 MAC 地址 /// 支持格式: /// 1. 带冒号格式:98:EA:A0:02:4E:06 /// 2. 不带冒号格式:98eaa002658e /// 3. 从键值对格式提取:Name:xxx;MAC:98eaa002658e /// private string ExtractMacAddress(string input) { if (string.IsNullOrEmpty(input)) return null; string macAddress = null; // 尝试从键值对格式提取 MAC:xxx // 匹配 MAC: 后面跟着 12 位十六进制字符(可能带冒号或不带) System.Text.RegularExpressions.Regex macKeyValueRegex = new System.Text.RegularExpressions.Regex(@"MAC:([0-9A-Fa-f]{2}:?[0-9A-Fa-f]{2}:?[0-9A-Fa-f]{2}:?[0-9A-Fa-f]{2}:?[0-9A-Fa-f]{2}:?[0-9A-Fa-f]{2})"); System.Text.RegularExpressions.Match kvMatch = macKeyValueRegex.Match(input); if (kvMatch.Success) { macAddress = kvMatch.Groups[1].Value; } else { // 尝试匹配带冒号的标准 MAC 格式 System.Text.RegularExpressions.Regex macColonRegex = new System.Text.RegularExpressions.Regex(@"[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}"); System.Text.RegularExpressions.Match colonMatch = macColonRegex.Match(input); if (colonMatch.Success) { macAddress = colonMatch.Value; } else { // 尝试匹配不带冒号的 12 位十六进制字符 System.Text.RegularExpressions.Regex macNoColonRegex = new System.Text.RegularExpressions.Regex(@"[0-9A-Fa-f]{12}"); System.Text.RegularExpressions.Match noColonMatch = macNoColonRegex.Match(input); if (noColonMatch.Success) { macAddress = noColonMatch.Value; } } } if (!string.IsNullOrEmpty(macAddress)) { // 统一转换为带冒号的大写格式 return FormatMacAddress(macAddress); } return null; } /// /// 将 MAC 地址统一格式化为带冒号的大写格式 /// 输入: 98eaa002658e 或 98:EA:A0:02:65:8E /// 输出: 98:EA:A0:02:65:8E /// private string FormatMacAddress(string mac) { if (string.IsNullOrEmpty(mac)) return null; // 移除所有冒号 string cleanMac = mac.Replace(":", "").Replace("-", "").ToUpper(); // 检查长度是否为 12 if (cleanMac.Length != 12) return null; // 格式化为 XX:XX:XX:XX:XX:XX return string.Format("{0}:{1}:{2}:{3}:{4}:{5}", cleanMac.Substring(0, 2), cleanMac.Substring(2, 2), cleanMac.Substring(4, 2), cleanMac.Substring(6, 2), cleanMac.Substring(8, 2), cleanMac.Substring(10, 2)); } /// /// 更新状态文字 /// private void UpdateStatus(string code) { ToastUI.Show(code); } void OnDestroy() { StopScan(); gameObject.SetActive(false); } } }