From 668dad0e6711ba4de92f933f93547da37f4894c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9C=E8=99=9E=E6=B8=A0=E6=88=90=E2=80=9D?= <“yuqucheng2006@qq.com”> Date: Thu, 23 Jul 2026 15:04:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(iOS):=20=E5=AE=9E=E7=8E=B0=E5=8E=9F?= =?UTF-8?q?=E7=94=9F=E4=BA=8C=E7=BB=B4=E7=A0=81=E6=89=AB=E6=8F=8F=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构iOS平台摄像头扫描逻辑,使用AVCaptureSession原生实现二维码扫描,替换原有的WebCamTexture方案: 1. 新增NativeCameraController封装摄像头会话、预览层和二维码检测逻辑 2. 实现摄像头权限申请、自动对焦配置和原生QR码识别 3. 适配C#层调用,通过UnitySendMessage回调扫描结果 4. 移除原iOS平台下冗余的自动对焦维护逻辑 --- Assets/Plugins/iOS/CameraFocusHelper.mm | 228 ++++-- .../UI/Pages/ConnectDevice/ScanQRcode.cs | 666 +++++------------- 2 files changed, 339 insertions(+), 555 deletions(-) diff --git a/Assets/Plugins/iOS/CameraFocusHelper.mm b/Assets/Plugins/iOS/CameraFocusHelper.mm index 49138e0..7fed5b3 100644 --- a/Assets/Plugins/iOS/CameraFocusHelper.mm +++ b/Assets/Plugins/iOS/CameraFocusHelper.mm @@ -1,103 +1,191 @@ #import #import -/// 通过名称查找摄像头设备 -static AVCaptureDevice *FindCameraByName(const char *cameraName) +@interface NativeCameraController : NSObject +@property (nonatomic, strong) AVCaptureSession *session; +@property (nonatomic, strong) AVCaptureVideoPreviewLayer *previewLayer; +@property (nonatomic, copy) NSString *gameObjectName; +@property (nonatomic, assign) BOOL hasScanned; +@end + +@implementation NativeCameraController + +- (void)startWithName:(NSString *)goName { - if (cameraName == NULL || strlen(cameraName) == 0) { - return [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; + self.gameObjectName = goName; + self.hasScanned = NO; + + // 1. 创建 session + self.session = [[AVCaptureSession alloc] init]; + self.session.sessionPreset = AVCaptureSessionPresetHigh; + + // 2. 获取后置摄像头 + AVCaptureDevice *device = [self findRearCamera]; + if (!device) { + [self sendToUnity:@"ERROR:NO_CAMERA"]; + return; } - NSString *name = [NSString stringWithUTF8String:cameraName]; + NSLog(@"[NativeCamera] 使用摄像头: %@", device.localizedName); - // iOS 10+ DiscoverySession + // 3. 配置连续自动对焦 + NSError *error = nil; + if ([device lockForConfiguration:&error]) { + if ([device isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) { + device.focusMode = AVCaptureFocusModeContinuousAutoFocus; + } + if ([device isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) { + device.exposureMode = AVCaptureExposureModeContinuousAutoExposure; + } + if ([device respondsToSelector:@selector(isAutoFocusRangeRestrictionSupported)] && + [device isAutoFocusRangeRestrictionSupported]) { + device.autoFocusRangeRestriction = AVCaptureAutoFocusRangeRestrictionNear; + } + [device unlockForConfiguration]; + NSLog(@"[NativeCamera] 已启用连续自动对焦"); + } else { + NSLog(@"[NativeCamera] 配置摄像头失败: %@", error.localizedDescription); + } + + // 4. 输入 + AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error]; + if (!input) { + [self sendToUnity:[NSString stringWithFormat:@"ERROR:INPUT:%@", error.localizedDescription]]; + return; + } + [self.session addInput:input]; + + // 5. 元数据输出(二维码检测) + AVCaptureMetadataOutput *metadataOutput = [[AVCaptureMetadataOutput alloc] init]; + [self.session addOutput:metadataOutput]; + [metadataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()]; + metadataOutput.metadataObjectTypes = @[AVMetadataObjectTypeQRCode]; + + // 6. 预览层 — 插入到 Unity 视图底层 + UIViewController *rootVC = UnityGetGLViewController(); + self.previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.session]; + self.previewLayer.frame = rootVC.view.bounds; + self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; + // 插入到最底层,Unity UI 正常渲染在上层 + [rootVC.view.layer insertSublayer:self.previewLayer atIndex:0]; + + // 7. 启动 session + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self.session startRunning]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self sendToUnity:@"CAMERA_READY"]; + }); + }); +} + +/// 选择后置主摄(排除超广角/长焦) +- (AVCaptureDevice *)findRearCamera +{ if (@available(iOS 10.0, *)) { AVCaptureDeviceDiscoverySession *session = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInWideAngleCamera, AVCaptureDeviceTypeBuiltInTelephotoCamera, AVCaptureDeviceTypeBuiltInUltraWideCamera] mediaType:AVMediaTypeVideo - position:AVCaptureDevicePositionUnspecified]; - for (AVCaptureDevice *device in session.devices) { - if ([device.localizedName containsString:name] || - [device.modelID containsString:name]) { - return device; - } - } - // 没找到精确匹配,返回第一个活跃的 - for (AVCaptureDevice *device in session.devices) { - if (device.isConnected && !device.isSuspended) { - return device; + position:AVCaptureDevicePositionBack]; + // 优先宽角(主摄) + for (AVCaptureDevice *d in session.devices) { + if (d.deviceType == AVCaptureDeviceTypeBuiltInWideAngleCamera) { + return d; } } + // 兜底:返回第一个 + return session.devices.firstObject; } - return [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; } +- (void)stop +{ + self.hasScanned = YES; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self.session stopRunning]; + }); + [self.previewLayer removeFromSuperlayer]; + self.previewLayer = nil; + self.session = nil; +} + +- (void)sendToUnity:(NSString *)msg +{ + if (self.gameObjectName) { + UnitySendMessage([self.gameObjectName UTF8String], "OnNativeScanResult", [msg UTF8String]); + } +} + +#pragma mark - AVCaptureMetadataOutputObjectsDelegate + +- (void)captureOutput:(AVCaptureOutput *)output +didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects + fromConnection:(AVCaptureConnection *)connection +{ + if (self.hasScanned) return; + if (metadataObjects.count == 0) return; + + AVMetadataMachineReadableCodeObject *code = metadataObjects.firstObject; + if (![code isKindOfClass:[AVMetadataMachineReadableCodeObject class]]) return; + if (!code.stringValue) return; + + self.hasScanned = YES; + NSLog(@"[NativeCamera] 扫描到二维码: %@", code.stringValue); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self.session stopRunning]; + }); + + [self sendToUnity:code.stringValue]; +} + +@end + +// ──── 单例管理 ──── +static NativeCameraController *g_cameraController = nil; + extern "C" { - /// 启用连续自动对焦和自动曝光(传入 C# 选中的摄像头名称) - void _EnableContinuousAutoFocus(const char *cameraName) + void _StartNativeCamera(const char *gameObjectName) { - dispatch_async(dispatch_get_main_queue(), ^{ - AVCaptureDevice *device = FindCameraByName(cameraName); - if (!device) { - NSLog(@"[CameraFocusHelper] 未找到摄像头设备"); - return; - } + if (g_cameraController) { + [g_cameraController stop]; + g_cameraController = nil; + } - NSLog(@"[CameraFocusHelper] 配置设备: %@ (name=%@, model=%@)", - device.localizedName, device.localizedName, device.modelID); - - NSError *error = nil; - if ([device lockForConfiguration:&error]) { - - // 连续自动对焦 - if ([device isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) { - device.focusMode = AVCaptureFocusModeContinuousAutoFocus; - NSLog(@"[CameraFocusHelper] 已设为连续自动对焦"); + // 权限请求 + AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; + if (status == AVAuthorizationStatusNotDetermined) { + [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { + if (granted) { + g_cameraController = [[NativeCameraController alloc] init]; + NSString *goName = [NSString stringWithUTF8String:gameObjectName]; + [g_cameraController startWithName:goName]; } else { - NSLog(@"[CameraFocusHelper] 设备不支持连续自动对焦"); + UnitySendMessage((char *)gameObjectName, "OnNativeScanResult", "ERROR:PERMISSION_DENIED"); } + }]; + return; + } - // 连续自动曝光 - if ([device isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) { - device.exposureMode = AVCaptureExposureModeContinuousAutoExposure; - } + if (status != AVAuthorizationStatusAuthorized) { + UnitySendMessage((char *)gameObjectName, "OnNativeScanResult", "ERROR:PERMISSION_DENIED"); + return; + } - // 对焦范围限制为近距 - if ([device respondsToSelector:@selector(isAutoFocusRangeRestrictionSupported)] && - [device isAutoFocusRangeRestrictionSupported]) { - device.autoFocusRangeRestriction = AVCaptureAutoFocusRangeRestrictionNear; - NSLog(@"[CameraFocusHelper] 已设对焦范围为近距"); - } - - [device unlockForConfiguration]; - NSLog(@"[CameraFocusHelper] 摄像头配置完成"); - } else { - NSLog(@"[CameraFocusHelper] lockForConfiguration 失败: %@", error.localizedDescription); - } - }); + g_cameraController = [[NativeCameraController alloc] init]; + NSString *goName = [NSString stringWithUTF8String:gameObjectName]; + [g_cameraController startWithName:goName]; } - /// 确保连续自动对焦仍在生效(传入 C# 选中的摄像头名称) - void _EnsureContinuousAutoFocus(const char *cameraName) + void _StopNativeCamera() { - dispatch_async(dispatch_get_main_queue(), ^{ - AVCaptureDevice *device = FindCameraByName(cameraName); - if (!device) return; - - NSError *error = nil; - if ([device lockForConfiguration:&error]) { - if (device.focusMode != AVCaptureFocusModeContinuousAutoFocus && - [device isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) { - device.focusMode = AVCaptureFocusModeContinuousAutoFocus; - NSLog(@"[CameraFocusHelper] 重新设回连续自动对焦"); - } - [device unlockForConfiguration]; - } - }); + if (g_cameraController) { + [g_cameraController stop]; + g_cameraController = nil; + } } } diff --git a/Assets/Scripts/UI/Pages/ConnectDevice/ScanQRcode.cs b/Assets/Scripts/UI/Pages/ConnectDevice/ScanQRcode.cs index b2e9489..c1cb115 100644 --- a/Assets/Scripts/UI/Pages/ConnectDevice/ScanQRcode.cs +++ b/Assets/Scripts/UI/Pages/ConnectDevice/ScanQRcode.cs @@ -10,6 +10,8 @@ namespace Kill.UI.Pages { /// /// 二维码扫描器 - 调用摄像头扫描二维码 + /// Android: 使用 WebCamTexture + ZXing 解码 + /// iOS: 使用原生 AVCaptureSession(连续对焦 + 原生 QR 检测) /// public class ScanQRcode : MonoBehaviour { @@ -19,37 +21,26 @@ namespace Kill.UI.Pages ConnectDevice, // 连接设备 Other // 其他用途 } + [Header("UI组件")] public RawImage cameraPreview; // 摄像头预览 + [Header("扫描设置")] float scanInterval = 0.2f; // 扫描间隔(秒) public ScanType scanType = ScanType.ConnectDevice; // 扫描类型 - - [Header("小二维码优化")] - [Tooltip("启用图像放大(推荐用于小二维码)")] - public bool enableUpscale = true; - [Tooltip("放大倍数")] - public float upscaleFactor = 2f; - - [Header("性能优化")] - [Tooltip("降低扫描分辨率(推荐值:640或480)")] - public int scanResolutionWidth = 1080; - [Tooltip("扫描处理线程数")] - public bool useAsyncScan = true; - + [Header("自动对焦")] - [Tooltip("启用自动对焦")] + [Tooltip("启用对焦")] public bool enableAutoFocus = true; - [Tooltip("对焦间隔(秒)")] - public float focusInterval = 1f; #if UNITY_IOS && !UNITY_EDITOR [DllImport("__Internal")] - private static extern void _EnableContinuousAutoFocus(string cameraName); + private static extern void _StartNativeCamera(string gameObjectName); [DllImport("__Internal")] - private static extern void _EnsureContinuousAutoFocus(string cameraName); + private static extern void _StopNativeCamera(); #endif + // 摄像头相关 private WebCamTexture webCamTexture; private WebCamDevice currentDevice; @@ -59,9 +50,6 @@ namespace Kill.UI.Pages // 扫描结果回调 public event Action OnQRCodeScanned; - // iOS 摄像头名称(供原生对焦插件使用) - private string selectedCameraName; - void OnEnable() { StartScan(); @@ -79,7 +67,16 @@ namespace Kill.UI.Pages { 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 } /// @@ -90,12 +87,18 @@ namespace Kill.UI.Pages 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) { @@ -113,8 +116,38 @@ namespace Kill.UI.Pages 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() { @@ -123,35 +156,32 @@ namespace Kill.UI.Pages 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"); + ToastUI.Show("100085"); yield break; } #else - // 其他平台使用传统方式 yield return Application.RequestUserAuthorization(UserAuthorization.WebCam); if (!Application.HasUserAuthorization(UserAuthorization.WebCam)) { - UpdateStatus("100085"); + ToastUI.Show("100085"); yield break; } #endif - // 获取后置摄像头 WebCamDevice[] devices = WebCamTexture.devices; if (devices.Length == 0) { - UpdateStatus("100086"); + ToastUI.Show("100086"); yield break; } - // 优先使用后置主摄(广角),iOS 多摄设备排除超广角/长焦 + // 优先后置主摄 string deviceName = devices[0].name; currentDevice = devices[0]; for (int i = 0; i < devices.Length; i++) @@ -159,15 +189,12 @@ namespace Kill.UI.Pages if (!devices[i].isFrontFacing) { string lowerName = devices[i].name.ToLower(); - // iOS 多摄命名: "Back Camera", "Telephoto Camera", "Ultra Wide Camera" - // 优先选不带 telephoto/ultra wide 的后置摄像头(即主摄) 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; @@ -176,121 +203,85 @@ namespace Kill.UI.Pages } } Debug.Log($"[ScanQRcode] 选中摄像头: {deviceName}"); - selectedCameraName = deviceName; - RectTransform cameraPreviewRect=cameraPreview.GetComponent(); - // 使用较低的扫描分辨率以提高性能 - int targetWidth = 2160; - - // 创建摄像头纹理 - webCamTexture = new WebCamTexture(deviceName, targetWidth, 30); - // 开始播放 + webCamTexture = new WebCamTexture(deviceName, 2160, 30); webCamTexture.Play(); - // 等待摄像头启动 yield return new WaitUntil(() => webCamTexture.width > 100); - // iOS: 延迟后调用原生插件强制开启连续自动对焦 - // (等待 Unity 内部 AVCaptureSession 完全就绪) -#if UNITY_IOS && !UNITY_EDITOR - if (enableAutoFocus) - { - yield return new WaitForSeconds(1.5f); - _EnableContinuousAutoFocus(deviceName); - } -#endif - // 调整预览画面比例 AdjustPreviewAspect(); - + isScanning = true; - // 开始预览更新和扫描 StartCoroutine(PreviewUpdateCoroutine()); StartCoroutine(ScanCoroutine()); - // 启动自动对焦维护协程(仅非 iOS 平台使用 autoFocusPoint,iOS 走原生插件) + if (enableAutoFocus) { StartCoroutine(AutoFocusCoroutine()); } } - /// - /// 调整预览画面比例 - 从相机画面中心截取最大适合预览画面比例的画面 - /// 手机竖着拿,摄像头默认横向输出,需要旋转并截取中间区域 - /// 预览框是横向的(1080x680),显示旋转后的竖向画面 + /// 自动对焦维护协程 + /// + 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; - - // 获取相机分辨率(摄像头默认横向,如 1920x1080) - int cameraWidth = webCamTexture.width; // 1920 - int cameraHeight = webCamTexture.height; // 1080 - - // 获取预览区域尺寸(横向预览框,比如 1080x680) + + int cameraWidth = webCamTexture.width; + int cameraHeight = webCamTexture.height; + RectTransform previewRect = cameraPreview.GetComponent(); - int previewWidth = (int)previewRect.rect.width; // 1080 - int previewHeight = (int)previewRect.rect.height; // 680 - - // 创建预览纹理(与预览框相同尺寸) + 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); } - - // 从相机画面中裁切并旋转 - // 手机竖着拿,相机横向输出,需要顺时针旋转90度 - // 从相机画面中间截取适合预览比例的竖向区域 + CutAndRotateCameraFrame(cameraWidth, cameraHeight, previewWidth, previewHeight); - - // 设置预览纹理 + cameraPreview.texture = previewTexture; cameraPreview.uvRect = new Rect(0, 0, 1, 1); - - // 重置旋转(因为已经在像素层面旋转了) previewRect.localEulerAngles = Vector3.zero; - - Debug.Log($"[ScanQRcode] 预览调整 - 相机: {cameraWidth}x{cameraHeight}, 预览: {previewWidth}x{previewHeight}"); } - + private Texture2D previewTexture; - + /// /// 从相机画面裁切并旋转,生成预览画面 /// private void CutAndRotateCameraFrame(int camW, int camH, int targetW, int targetH) { - // 相机横向 1920x1080,需要顺时针旋转90度变成 1080x1920 - // 预览框横向 1080x680 - // 从旋转后的 1080x1920 中截取中间 1080x680 区域 - - // 计算裁切区域(在原始相机画面上的坐标) - // 旋转后高度是 camW(1920),需要截取 targetH(680) 高度 - // 从中间截取:起始Y = (1920 - 680) / 2 = 620 - int cropY = (camW - targetH) / 2; // 620 - - // 获取相机像素数据 + int cropY = (camW - targetH) / 2; Color32[] cameraPixels = webCamTexture.GetPixels32(); Color32[] targetPixels = new Color32[targetW * targetH]; - - // 顺时针旋转90度并裁切 - // 原始坐标 (x, y) -> 顺时针旋转90度后 (y, camW-1-x) - // 目标 (x, y) 对应源: - // srcX = cropY + y (在裁切区域内纵向移动) - // srcY = x (横向直接对应,不翻转) + for (int y = 0; y < targetH; y++) { for (int x = 0; x < targetW; x++) { - // 目标像素在预览图中的位置 (x, y) - // 对应原始相机中的位置 - int srcX = cropY + (targetH - 1 - y); // 从裁切区域底部开始,向上取像素(修复上下颠倒) - int srcY = x; // 横向直接对应 - + int srcX = cropY + (targetH - 1 - y); + int srcY = x; + if (srcX >= 0 && srcX < camW && srcY >= 0 && srcY < camH) { int srcIndex = srcY * camW + srcX; @@ -299,48 +290,17 @@ namespace Kill.UI.Pages } } } - - // 设置像素到预览纹理 + previewTexture.SetPixels32(targetPixels); previewTexture.Apply(); } - /// - /// 自动对焦维护协程 - /// iOS: 走原生插件周期确保连续对焦模式不被重置 - /// 其他平台: 使用 WebCamTexture.autoFocusPoint 定期设置对焦点 - /// - private IEnumerator AutoFocusCoroutine() - { - while (isScanning) - { -#if UNITY_IOS && !UNITY_EDITOR - // iOS 通过原生插件确保连续自动对焦仍在生效 - if (!string.IsNullOrEmpty(selectedCameraName)) - { - _EnsureContinuousAutoFocus(selectedCameraName); - } -#else - // Android/其他: 定期重置对焦点到画面中心 - if (webCamTexture != null && webCamTexture.isPlaying) - { - webCamTexture.autoFocusPoint = new Vector2(0.5f, 0.5f); - } -#endif - yield return new WaitForSeconds(focusInterval); - } - } + [SerializeField] private float previewUpdateInterval = 0.03f; - [SerializeField] private float previewUpdateInterval = 0.03f; // 预览更新间隔(20fps) - - /// - /// 预览更新协程 - 降低更新频率避免卡顿 - /// private IEnumerator PreviewUpdateCoroutine() { while (isScanning && webCamTexture != null && webCamTexture.isPlaying) { - // 更新预览画面(限制频率) if (previewTexture != null) { CutAndRotateCameraFrame(webCamTexture.width, webCamTexture.height, previewTexture.width, previewTexture.height); @@ -350,16 +310,14 @@ namespace Kill.UI.Pages } /// - /// 扫描协程 + /// 扫描协程(ZXing 解码) /// private IEnumerator ScanCoroutine() { - // 等待一帧确保摄像头已准备好 yield return null; while (isScanning && webCamTexture != null && webCamTexture.isPlaying) { - // 避免同时处理多帧 if (isProcessing) { yield return null; @@ -368,62 +326,43 @@ namespace Kill.UI.Pages isProcessing = true; - // 使用预览纹理进行扫描 if (previewTexture != null) { - // 异步解析二维码以避免卡顿 - if (useAsyncScan) + 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++) { - // 在主线程提取像素数据(Unity API 必须在主线程调用) - 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; - - // 在后台线程执行 ZXing 解码(纯计算,不涉及 Unity API) - 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); - } + rgbBytes[i * 3] = pixels[i].r; + rgbBytes[i * 3 + 1] = pixels[i].g; + rgbBytes[i * 3 + 2] = pixels[i].b; } - else - { - // 同步解码 - string result = DecodeQRCode(previewTexture); - if (!string.IsNullOrEmpty(result)) + string result = null; + bool scanComplete = false; + + System.Threading.ThreadPool.QueueUserWorkItem(_ => + { + try { - ProcessScanResult(result); + 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); } } @@ -433,133 +372,7 @@ namespace Kill.UI.Pages } /// - /// 处理扫描结果 - /// - private void ProcessScanResult(string result) - { - Debug.Log($"扫描结果: {result}"); - // 根据扫描类型处理结果 - if (scanType == ScanType.ConnectDevice) - { - // 从结果中提取 MAC 地址 - string macAddress = ExtractMacAddress(result); - if (!string.IsNullOrEmpty(macAddress)) - { - // 扫描成功,仅回传 MAC 地址 - OnQRCodeScanned?.Invoke(macAddress); - OnQRCodeScanned-=OnQRCodeScanned; - } - 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; - - return null; - } - catch (Exception ex) - { - Debug.LogError($"二维码解析失败: {ex.Message}"); - return null; - } - } - - /// - /// 从原始字节数据解析二维码(不依赖 Unity API,可在后台线程调用) + /// 从原始字节数据解析二维码(后台线程安全) /// private string DecodeQRCodeFromBytes(byte[] rgbBytes, int width, int height) { @@ -569,74 +382,14 @@ namespace Kill.UI.Pages string result = TryDecodeWithBinarizer(luminanceSource); if (!string.IsNullOrEmpty(result)) return result; - - return null; } catch (Exception ex) { Debug.LogError($"二维码解析失败: {ex.Message}"); - return null; } + 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[] allPixels = texture.GetPixels32(); - - // 提取指定区域的像素 - Color32[] regionPixels = new Color32[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 < allPixels.Length) - { - regionPixels[dstIndex] = allPixels[srcIndex]; - } - } - } - - // 转换为 RGB 字节数组(ZXing 内部会自动处理灰度转换) - byte[] rgbBytes = new byte[regionPixels.Length * 3]; - for (int i = 0; i < regionPixels.Length; i++) - { - rgbBytes[i * 3] = regionPixels[i].r; - rgbBytes[i * 3 + 1] = regionPixels[i].g; - rgbBytes[i * 3 + 2] = regionPixels[i].b; - } - - // 使用 RGBLuminanceSource,让 ZXing 内部处理灰度转换 - var luminanceSource = new ZXing.RGBLuminanceSource(rgbBytes, width, height); - - // 尝试解码 - string result = TryDecodeWithBinarizer(luminanceSource); - if (!string.IsNullOrEmpty(result)) - return result; - - return null; - } - catch - { - return null; - } - } - /// /// 尝试使用不同的二值化算法解码 /// @@ -647,10 +400,9 @@ namespace Kill.UI.Pages { 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); @@ -660,8 +412,7 @@ namespace Kill.UI.Pages return result.Text; } catch { } - - // 尝试 GlobalHistogramBinarizer + try { var binarizer = new ZXing.Common.GlobalHistogramBinarizer(luminanceSource); @@ -671,146 +422,91 @@ namespace Kill.UI.Pages return result.Text; } catch { } - + return null; } - +#endif // !UNITY_IOS || UNITY_EDITOR + + // ──── 公共处理方法 ──── + /// - /// 应用锐化滤镜(拉普拉斯算子) - /// 增强图像边缘,提高模糊二维码识别率 + /// 处理扫描结果 /// - private byte[] ApplySharpenFilter(byte[] input, int width, int height) + private void ProcessScanResult(string result) { - byte[] output = new byte[input.Length]; - - // 拉普拉斯锐化核 - // 0 -1 0 - // -1 5 -1 - // 0 -1 0 - int[] kernel = { 0, -1, 0, -1, 5, -1, 0, -1, 0 }; - int kernelSize = 3; - int halfKernel = kernelSize / 2; - - for (int y = 0; y < height; y++) + Debug.Log($"[ScanQRcode] 扫描结果: {result}"); + + if (scanType == ScanType.ConnectDevice) { - for (int x = 0; x < width; x++) + string macAddress = ExtractMacAddress(result); + if (!string.IsNullOrEmpty(macAddress)) { - int sum = 0; - - // 应用卷积核 - for (int ky = -halfKernel; ky <= halfKernel; ky++) - { - for (int kx = -halfKernel; kx <= halfKernel; kx++) - { - int py = Mathf.Clamp(y + ky, 0, height - 1); - int px = Mathf.Clamp(x + kx, 0, width - 1); - - int pixelIndex = py * width + px; - int kernelIndex = (ky + halfKernel) * kernelSize + (kx + halfKernel); - - sum += input[pixelIndex] * kernel[kernelIndex]; - } - } - - // 限制在0-255范围内 - output[y * width + x] = (byte)Mathf.Clamp(sum, 0, 255); + OnQRCodeScanned?.Invoke(macAddress); + OnQRCodeScanned = null; + } + else + { + ToastUI.Show("100084"); + } + } + else + { + if (!string.IsNullOrEmpty(result)) + { + OnQRCodeScanned?.Invoke(result); + } + else + { + ToastUI.Show("100084"); } } - - return output; } /// /// 从字符串中提取 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; + 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})"); - // 尝试从键值对格式提取 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); + var 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; - } - } - } + return FormatMacAddress(kvMatch.Groups[1].Value); - if (!string.IsNullOrEmpty(macAddress)) - { - // 统一转换为带冒号的大写格式 - return FormatMacAddress(macAddress); - } + 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 地址统一格式化为带冒号的大写格式 - /// 输入: 98eaa002658e 或 98:EA:A0:02:65:8E - /// 输出: 98:EA:A0:02:65:8E + /// 格式化 MAC 地址为 XX:XX:XX:XX:XX:XX /// 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); + 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()