using System; using System.Collections; using System.Collections.Generic; using System.Runtime.InteropServices; using Kill.UI.Components; using UnityEngine; using UnityEngine.UI; namespace Kill.UI.Pages { /// /// 二维码扫描器 - 调用摄像头扫描二维码 /// Android: 使用 WebCamTexture + ZXing 解码 /// iOS: 使用原生 AVCaptureSession(连续对焦 + 原生 QR 检测) /// public class ScanQRcode : MonoBehaviour { [Serializable] public enum ScanType { ConnectDevice, // 连接设备 Other // 其他用途 } [Header("UI组件")] public RawImage cameraPreview; // 摄像头预览 [Header("扫描设置")] float scanInterval = 0.2f; // 扫描间隔(秒) public ScanType scanType = ScanType.ConnectDevice; // 扫描类型 [Header("自动对焦")] [Tooltip("启用对焦")] public bool enableAutoFocus = true; #if UNITY_IOS && !UNITY_EDITOR [DllImport("__Internal")] private static extern void _StartNativeCamera(string gameObjectName); [DllImport("__Internal")] private static extern void _StopNativeCamera(); #endif // 摄像头相关 private WebCamTexture webCamTexture; private WebCamDevice currentDevice; private bool isScanning = false; private bool isProcessing = false; // 扫描结果回调 public event Action OnQRCodeScanned; // 预览纹理(所有平台共用清理逻辑) private Texture2D previewTexture; void OnEnable() { StartScan(); } void OnDisable() { StopScan(); } /// /// 开始扫描 /// public void StartScan() { if (isScanning) return; #if UNITY_IOS && !UNITY_EDITOR // iOS: 使用原生 AVCaptureSession(连续对焦 + 原生 QR 检测) if (cameraPreview != null) cameraPreview.gameObject.SetActive(false); _StartNativeCamera(gameObject.name); isScanning = true; #else // Android/其他: 使用 WebCamTexture + ZXing StartCoroutine(InitializeCamera()); #endif } /// /// 停止扫描 /// public void StopScan() { isScanning = false; StopAllCoroutines(); #if UNITY_IOS && !UNITY_EDITOR _StopNativeCamera(); if (cameraPreview != null) cameraPreview.gameObject.SetActive(true); #endif if (webCamTexture != null && webCamTexture.isPlaying) { webCamTexture.Stop(); webCamTexture = null; } if (previewTexture != null) { Destroy(previewTexture); previewTexture = null; } } /// /// 关闭扫码界面 /// public void Close() { StopScan(); gameObject.SetActive(false); } // ──── iOS 原生回调 ──── /// /// 由 iOS 原生插件调用(通过 UnitySendMessage) /// public void OnNativeScanResult(string result) { if (string.IsNullOrEmpty(result)) return; if (result == "CAMERA_READY") { Debug.Log("[ScanQRcode] 原生摄像头已就绪"); return; } if (result.StartsWith("ERROR:")) { Debug.LogError($"[ScanQRcode] 原生摄像头错误: {result}"); ToastUI.Show("100086"); return; } Debug.Log($"[ScanQRcode] 原生扫描结果: {result}"); ProcessScanResult(result); } // ──── Android / 编辑器的 WebCamTexture 方案 ──── #if !UNITY_IOS || UNITY_EDITOR /// /// 初始化摄像头(WebCamTexture 方案) /// private IEnumerator InitializeCamera() { #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)) { ToastUI.Show("100085"); yield break; } #else yield return Application.RequestUserAuthorization(UserAuthorization.WebCam); if (!Application.HasUserAuthorization(UserAuthorization.WebCam)) { ToastUI.Show("100085"); yield break; } #endif WebCamDevice[] devices = WebCamTexture.devices; if (devices.Length == 0) { ToastUI.Show("100086"); yield break; } // 优先后置主摄 string deviceName = devices[0].name; currentDevice = devices[0]; for (int i = 0; i < devices.Length; i++) { if (!devices[i].isFrontFacing) { string lowerName = devices[i].name.ToLower(); if (!lowerName.Contains("telephoto") && !lowerName.Contains("ultra wide")) { deviceName = devices[i].name; currentDevice = devices[i]; break; } if (deviceName == devices[0].name) { deviceName = devices[i].name; currentDevice = devices[i]; } } } Debug.Log($"[ScanQRcode] 选中摄像头: {deviceName}"); // 使用 1080p 原生分辨率,iOS 会触发系统连续自动对焦 webCamTexture = new WebCamTexture(deviceName, 1920, 1080, 30); webCamTexture.Play(); yield return new WaitUntil(() => webCamTexture.width > 100); Debug.Log($"[ScanQRcode] 实际摄像头分辨率: {webCamTexture.width}x{webCamTexture.height} FPS:{webCamTexture.requestedFPS}"); AdjustPreviewAspect(); isScanning = true; StartCoroutine(PreviewUpdateCoroutine()); StartCoroutine(ScanCoroutine()); if (enableAutoFocus) { #if UNITY_IOS && !UNITY_EDITOR // iOS 通过高分辨率触发系统连续自动对焦,无需额外操作 #else StartCoroutine(AutoFocusCoroutine()); #endif } } /// /// 自动对焦维护协程 /// private IEnumerator AutoFocusCoroutine() { while (isScanning) { if (webCamTexture != null && webCamTexture.isPlaying) { webCamTexture.autoFocusPoint = new Vector2(0.5f, 0.5f); } yield return new WaitForSeconds(1f); } } /// /// 调整预览画面比例 /// private void AdjustPreviewAspect() { if (webCamTexture == null || cameraPreview == null) return; int cameraWidth = webCamTexture.width; int cameraHeight = webCamTexture.height; RectTransform previewRect = cameraPreview.GetComponent(); int previewWidth = (int)previewRect.rect.width; int previewHeight = (int)previewRect.rect.height; if (previewTexture == null || previewTexture.width != previewWidth || previewTexture.height != previewHeight) { if (previewTexture != null) Destroy(previewTexture); previewTexture = new Texture2D(previewWidth, previewHeight, TextureFormat.RGB24, false); } CutAndRotateCameraFrame(cameraWidth, cameraHeight, previewWidth, previewHeight); cameraPreview.texture = previewTexture; cameraPreview.uvRect = new Rect(0, 0, 1, 1); previewRect.localEulerAngles = Vector3.zero; } /// /// 从相机画面裁切并旋转,生成预览画面 /// private void CutAndRotateCameraFrame(int camW, int camH, int targetW, int targetH) { int cropY = (camW - targetH) / 2; Color32[] cameraPixels = webCamTexture.GetPixels32(); Color32[] targetPixels = new Color32[targetW * targetH]; for (int y = 0; y < targetH; y++) { for (int x = 0; x < targetW; x++) { int srcX = cropY + (targetH - 1 - y); int srcY = x; if (srcX >= 0 && srcX < camW && srcY >= 0 && srcY < camH) { int srcIndex = srcY * camW + srcX; int dstIndex = y * targetW + x; targetPixels[dstIndex] = cameraPixels[srcIndex]; } } } previewTexture.SetPixels32(targetPixels); previewTexture.Apply(); } [SerializeField] private float previewUpdateInterval = 0.03f; private IEnumerator PreviewUpdateCoroutine() { while (isScanning && webCamTexture != null && webCamTexture.isPlaying) { if (previewTexture != null) { CutAndRotateCameraFrame(webCamTexture.width, webCamTexture.height, previewTexture.width, previewTexture.height); } yield return new WaitForSeconds(previewUpdateInterval); } } /// /// 扫描协程(ZXing 解码) /// private IEnumerator ScanCoroutine() { yield return null; while (isScanning && webCamTexture != null && webCamTexture.isPlaying) { if (isProcessing) { yield return null; continue; } isProcessing = true; if (previewTexture != null) { Color32[] pixels = previewTexture.GetPixels32(); int texWidth = previewTexture.width; int texHeight = previewTexture.height; byte[] rgbBytes = new byte[pixels.Length * 3]; for (int i = 0; i < pixels.Length; i++) { rgbBytes[i * 3] = pixels[i].r; rgbBytes[i * 3 + 1] = pixels[i].g; rgbBytes[i * 3 + 2] = pixels[i].b; } string result = null; bool scanComplete = false; System.Threading.ThreadPool.QueueUserWorkItem(_ => { try { result = DecodeQRCodeFromBytes(rgbBytes, texWidth, texHeight); } catch (Exception e) { Debug.LogError($"[ScanQRcode] 解码异常: {e.Message}"); } finally { scanComplete = true; } }); yield return new WaitUntil(() => scanComplete); if (!string.IsNullOrEmpty(result)) { ProcessScanResult(result); } } isProcessing = false; yield return new WaitForSeconds(scanInterval); } } /// /// 从原始字节数据解析二维码(后台线程安全) /// private string DecodeQRCodeFromBytes(byte[] rgbBytes, int width, int height) { try { var luminanceSource = new ZXing.RGBLuminanceSource(rgbBytes, width, height); string result = TryDecodeWithBinarizer(luminanceSource); if (!string.IsNullOrEmpty(result)) return result; } catch (Exception ex) { Debug.LogError($"二维码解析失败: {ex.Message}"); } return null; } /// /// 尝试使用不同的二值化算法解码 /// private string TryDecodeWithBinarizer(ZXing.LuminanceSource 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(); 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 { } 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; } #endif // !UNITY_IOS || UNITY_EDITOR // ──── 公共处理方法 ──── /// /// 处理扫描结果 /// private void ProcessScanResult(string result) { Debug.Log($"[ScanQRcode] 扫描结果: {result}"); if (scanType == ScanType.ConnectDevice) { string macAddress = ExtractMacAddress(result); if (!string.IsNullOrEmpty(macAddress)) { OnQRCodeScanned?.Invoke(macAddress); OnQRCodeScanned = null; } else { ToastUI.Show("100084"); } } else { if (!string.IsNullOrEmpty(result)) { OnQRCodeScanned?.Invoke(result); } else { ToastUI.Show("100084"); } } } /// /// 从字符串中提取 MAC 地址 /// private string ExtractMacAddress(string input) { if (string.IsNullOrEmpty(input)) return null; var 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})"); var kvMatch = macKeyValueRegex.Match(input); if (kvMatch.Success) return FormatMacAddress(kvMatch.Groups[1].Value); var 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}"); var colonMatch = macColonRegex.Match(input); if (colonMatch.Success) return FormatMacAddress(colonMatch.Value); var macNoColonRegex = new System.Text.RegularExpressions.Regex(@"[0-9A-Fa-f]{12}"); var noColonMatch = macNoColonRegex.Match(input); if (noColonMatch.Success) return FormatMacAddress(noColonMatch.Value); return null; } /// /// 格式化 MAC 地址为 XX:XX:XX:XX:XX:XX /// private string FormatMacAddress(string mac) { if (string.IsNullOrEmpty(mac)) return null; string cleanMac = mac.Replace(":", "").Replace("-", "").ToUpper(); if (cleanMac.Length != 12) return null; 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)); } void OnDestroy() { StopScan(); gameObject.SetActive(false); } } }