2026-04-28 16:35:51 +08:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections;
|
|
|
|
|
|
using System.Collections.Generic;
|
2026-07-23 14:44:41 +08:00
|
|
|
|
using System.Runtime.InteropServices;
|
2026-04-28 16:35:51 +08:00
|
|
|
|
using Kill.UI.Components;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.UI;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Kill.UI.Pages
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 二维码扫描器 - 调用摄像头扫描二维码
|
2026-07-23 15:04:51 +08:00
|
|
|
|
/// Android: 使用 WebCamTexture + ZXing 解码
|
|
|
|
|
|
/// iOS: 使用原生 AVCaptureSession(连续对焦 + 原生 QR 检测)
|
2026-04-28 16:35:51 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class ScanQRcode : MonoBehaviour
|
|
|
|
|
|
{
|
2026-06-08 08:55:10 +08:00
|
|
|
|
[Serializable]
|
|
|
|
|
|
public enum ScanType
|
|
|
|
|
|
{
|
|
|
|
|
|
ConnectDevice, // 连接设备
|
|
|
|
|
|
Other // 其他用途
|
|
|
|
|
|
}
|
2026-07-23 15:04:51 +08:00
|
|
|
|
|
2026-04-28 16:35:51 +08:00
|
|
|
|
[Header("UI组件")]
|
|
|
|
|
|
public RawImage cameraPreview; // 摄像头预览
|
2026-07-23 15:04:51 +08:00
|
|
|
|
|
2026-04-28 16:35:51 +08:00
|
|
|
|
[Header("扫描设置")]
|
2026-06-10 15:04:14 +08:00
|
|
|
|
float scanInterval = 0.2f; // 扫描间隔(秒)
|
2026-06-08 08:55:10 +08:00
|
|
|
|
public ScanType scanType = ScanType.ConnectDevice; // 扫描类型
|
2026-07-23 15:04:51 +08:00
|
|
|
|
|
2026-06-08 08:55:10 +08:00
|
|
|
|
[Header("自动对焦")]
|
2026-07-23 15:04:51 +08:00
|
|
|
|
[Tooltip("启用对焦")]
|
2026-06-08 08:55:10 +08:00
|
|
|
|
public bool enableAutoFocus = true;
|
2026-07-23 14:44:41 +08:00
|
|
|
|
|
|
|
|
|
|
#if UNITY_IOS && !UNITY_EDITOR
|
|
|
|
|
|
[DllImport("__Internal")]
|
2026-07-23 15:04:51 +08:00
|
|
|
|
private static extern void _StartNativeCamera(string gameObjectName);
|
2026-07-23 14:44:41 +08:00
|
|
|
|
|
|
|
|
|
|
[DllImport("__Internal")]
|
2026-07-23 15:04:51 +08:00
|
|
|
|
private static extern void _StopNativeCamera();
|
2026-07-23 14:44:41 +08:00
|
|
|
|
#endif
|
2026-07-23 15:04:51 +08:00
|
|
|
|
|
2026-04-28 16:35:51 +08:00
|
|
|
|
// 摄像头相关
|
|
|
|
|
|
private WebCamTexture webCamTexture;
|
2026-06-08 08:55:10 +08:00
|
|
|
|
private WebCamDevice currentDevice;
|
2026-04-28 16:35:51 +08:00
|
|
|
|
private bool isScanning = false;
|
2026-06-08 08:55:10 +08:00
|
|
|
|
private bool isProcessing = false;
|
2026-04-28 16:35:51 +08:00
|
|
|
|
|
|
|
|
|
|
// 扫描结果回调
|
|
|
|
|
|
public event Action<string> OnQRCodeScanned;
|
|
|
|
|
|
|
2026-07-23 15:08:11 +08:00
|
|
|
|
// 预览纹理(所有平台共用清理逻辑)
|
|
|
|
|
|
private Texture2D previewTexture;
|
|
|
|
|
|
|
2026-04-28 16:35:51 +08:00
|
|
|
|
void OnEnable()
|
|
|
|
|
|
{
|
|
|
|
|
|
StartScan();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void OnDisable()
|
|
|
|
|
|
{
|
|
|
|
|
|
StopScan();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 开始扫描
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void StartScan()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (isScanning) return;
|
|
|
|
|
|
|
2026-07-23 15:04:51 +08:00
|
|
|
|
#if UNITY_IOS && !UNITY_EDITOR
|
|
|
|
|
|
// iOS: 使用原生 AVCaptureSession(连续对焦 + 原生 QR 检测)
|
|
|
|
|
|
if (cameraPreview != null)
|
|
|
|
|
|
cameraPreview.gameObject.SetActive(false);
|
|
|
|
|
|
_StartNativeCamera(gameObject.name);
|
|
|
|
|
|
isScanning = true;
|
|
|
|
|
|
#else
|
|
|
|
|
|
// Android/其他: 使用 WebCamTexture + ZXing
|
2026-04-28 16:35:51 +08:00
|
|
|
|
StartCoroutine(InitializeCamera());
|
2026-07-23 15:04:51 +08:00
|
|
|
|
#endif
|
2026-04-28 16:35:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 停止扫描
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void StopScan()
|
|
|
|
|
|
{
|
|
|
|
|
|
isScanning = false;
|
|
|
|
|
|
StopAllCoroutines();
|
|
|
|
|
|
|
2026-07-23 15:04:51 +08:00
|
|
|
|
#if UNITY_IOS && !UNITY_EDITOR
|
|
|
|
|
|
_StopNativeCamera();
|
|
|
|
|
|
if (cameraPreview != null)
|
|
|
|
|
|
cameraPreview.gameObject.SetActive(true);
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
2026-04-28 16:35:51 +08:00
|
|
|
|
if (webCamTexture != null && webCamTexture.isPlaying)
|
|
|
|
|
|
{
|
|
|
|
|
|
webCamTexture.Stop();
|
|
|
|
|
|
webCamTexture = null;
|
|
|
|
|
|
}
|
2026-07-23 15:04:51 +08:00
|
|
|
|
|
2026-06-10 15:04:14 +08:00
|
|
|
|
if (previewTexture != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Destroy(previewTexture);
|
|
|
|
|
|
previewTexture = null;
|
|
|
|
|
|
}
|
2026-04-28 16:35:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 关闭扫码界面
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void Close()
|
|
|
|
|
|
{
|
|
|
|
|
|
StopScan();
|
|
|
|
|
|
gameObject.SetActive(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 15:04:51 +08:00
|
|
|
|
// ──── iOS 原生回调 ────
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 由 iOS 原生插件调用(通过 UnitySendMessage)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
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
|
|
|
|
|
|
|
2026-04-28 16:35:51 +08:00
|
|
|
|
/// <summary>
|
2026-07-23 15:04:51 +08:00
|
|
|
|
/// 初始化摄像头(WebCamTexture 方案)
|
2026-04-28 16:35:51 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
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));
|
|
|
|
|
|
}
|
2026-07-23 15:04:51 +08:00
|
|
|
|
|
2026-04-28 16:35:51 +08:00
|
|
|
|
if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Camera))
|
|
|
|
|
|
{
|
2026-07-23 15:04:51 +08:00
|
|
|
|
ToastUI.Show("100085");
|
2026-04-28 16:35:51 +08:00
|
|
|
|
yield break;
|
|
|
|
|
|
}
|
|
|
|
|
|
#else
|
|
|
|
|
|
yield return Application.RequestUserAuthorization(UserAuthorization.WebCam);
|
|
|
|
|
|
|
|
|
|
|
|
if (!Application.HasUserAuthorization(UserAuthorization.WebCam))
|
|
|
|
|
|
{
|
2026-07-23 15:04:51 +08:00
|
|
|
|
ToastUI.Show("100085");
|
2026-04-28 16:35:51 +08:00
|
|
|
|
yield break;
|
|
|
|
|
|
}
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
WebCamDevice[] devices = WebCamTexture.devices;
|
|
|
|
|
|
if (devices.Length == 0)
|
|
|
|
|
|
{
|
2026-07-23 15:04:51 +08:00
|
|
|
|
ToastUI.Show("100086");
|
2026-04-28 16:35:51 +08:00
|
|
|
|
yield break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 15:04:51 +08:00
|
|
|
|
// 优先后置主摄
|
2026-04-28 16:35:51 +08:00
|
|
|
|
string deviceName = devices[0].name;
|
2026-07-23 14:56:53 +08:00
|
|
|
|
currentDevice = devices[0];
|
2026-04-28 16:35:51 +08:00
|
|
|
|
for (int i = 0; i < devices.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!devices[i].isFrontFacing)
|
|
|
|
|
|
{
|
2026-07-23 14:56:53 +08:00
|
|
|
|
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];
|
|
|
|
|
|
}
|
2026-04-28 16:35:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-23 14:56:53 +08:00
|
|
|
|
Debug.Log($"[ScanQRcode] 选中摄像头: {deviceName}");
|
2026-07-23 15:04:51 +08:00
|
|
|
|
|
|
|
|
|
|
webCamTexture = new WebCamTexture(deviceName, 2160, 30);
|
2026-04-28 16:35:51 +08:00
|
|
|
|
webCamTexture.Play();
|
|
|
|
|
|
|
|
|
|
|
|
yield return new WaitUntil(() => webCamTexture.width > 100);
|
2026-07-23 14:44:41 +08:00
|
|
|
|
|
2026-04-28 16:35:51 +08:00
|
|
|
|
AdjustPreviewAspect();
|
2026-07-23 15:04:51 +08:00
|
|
|
|
|
2026-04-28 16:35:51 +08:00
|
|
|
|
isScanning = true;
|
2026-06-10 15:04:14 +08:00
|
|
|
|
StartCoroutine(PreviewUpdateCoroutine());
|
2026-04-28 16:35:51 +08:00
|
|
|
|
StartCoroutine(ScanCoroutine());
|
2026-07-23 15:04:51 +08:00
|
|
|
|
|
2026-07-23 14:44:41 +08:00
|
|
|
|
if (enableAutoFocus)
|
|
|
|
|
|
{
|
|
|
|
|
|
StartCoroutine(AutoFocusCoroutine());
|
|
|
|
|
|
}
|
2026-04-28 16:35:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-07-23 15:04:51 +08:00
|
|
|
|
/// 自动对焦维护协程
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private IEnumerator AutoFocusCoroutine()
|
|
|
|
|
|
{
|
|
|
|
|
|
while (isScanning)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (webCamTexture != null && webCamTexture.isPlaying)
|
|
|
|
|
|
{
|
|
|
|
|
|
webCamTexture.autoFocusPoint = new Vector2(0.5f, 0.5f);
|
|
|
|
|
|
}
|
|
|
|
|
|
yield return new WaitForSeconds(1f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 调整预览画面比例
|
2026-04-28 16:35:51 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void AdjustPreviewAspect()
|
|
|
|
|
|
{
|
2026-06-10 15:04:14 +08:00
|
|
|
|
if (webCamTexture == null || cameraPreview == null) return;
|
2026-07-23 15:04:51 +08:00
|
|
|
|
|
|
|
|
|
|
int cameraWidth = webCamTexture.width;
|
|
|
|
|
|
int cameraHeight = webCamTexture.height;
|
|
|
|
|
|
|
2026-06-10 15:04:14 +08:00
|
|
|
|
RectTransform previewRect = cameraPreview.GetComponent<RectTransform>();
|
2026-07-23 15:04:51 +08:00
|
|
|
|
int previewWidth = (int)previewRect.rect.width;
|
|
|
|
|
|
int previewHeight = (int)previewRect.rect.height;
|
|
|
|
|
|
|
2026-06-10 15:04:14 +08:00
|
|
|
|
if (previewTexture == null || previewTexture.width != previewWidth || previewTexture.height != previewHeight)
|
2026-04-28 16:35:51 +08:00
|
|
|
|
{
|
2026-06-10 15:04:14 +08:00
|
|
|
|
if (previewTexture != null)
|
|
|
|
|
|
Destroy(previewTexture);
|
|
|
|
|
|
previewTexture = new Texture2D(previewWidth, previewHeight, TextureFormat.RGB24, false);
|
2026-04-28 16:35:51 +08:00
|
|
|
|
}
|
2026-07-23 15:04:51 +08:00
|
|
|
|
|
2026-06-10 15:04:14 +08:00
|
|
|
|
CutAndRotateCameraFrame(cameraWidth, cameraHeight, previewWidth, previewHeight);
|
2026-07-23 15:04:51 +08:00
|
|
|
|
|
2026-06-10 15:04:14 +08:00
|
|
|
|
cameraPreview.texture = previewTexture;
|
|
|
|
|
|
cameraPreview.uvRect = new Rect(0, 0, 1, 1);
|
|
|
|
|
|
previewRect.localEulerAngles = Vector3.zero;
|
|
|
|
|
|
}
|
2026-07-23 15:04:51 +08:00
|
|
|
|
|
2026-06-10 15:04:14 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 从相机画面裁切并旋转,生成预览画面
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void CutAndRotateCameraFrame(int camW, int camH, int targetW, int targetH)
|
|
|
|
|
|
{
|
2026-07-23 15:04:51 +08:00
|
|
|
|
int cropY = (camW - targetH) / 2;
|
2026-06-10 15:04:14 +08:00
|
|
|
|
Color32[] cameraPixels = webCamTexture.GetPixels32();
|
|
|
|
|
|
Color32[] targetPixels = new Color32[targetW * targetH];
|
2026-07-23 15:04:51 +08:00
|
|
|
|
|
2026-06-10 15:04:14 +08:00
|
|
|
|
for (int y = 0; y < targetH; y++)
|
2026-04-28 16:35:51 +08:00
|
|
|
|
{
|
2026-06-10 15:04:14 +08:00
|
|
|
|
for (int x = 0; x < targetW; x++)
|
|
|
|
|
|
{
|
2026-07-23 15:04:51 +08:00
|
|
|
|
int srcX = cropY + (targetH - 1 - y);
|
|
|
|
|
|
int srcY = x;
|
|
|
|
|
|
|
2026-06-10 15:04:14 +08:00
|
|
|
|
if (srcX >= 0 && srcX < camW && srcY >= 0 && srcY < camH)
|
|
|
|
|
|
{
|
|
|
|
|
|
int srcIndex = srcY * camW + srcX;
|
|
|
|
|
|
int dstIndex = y * targetW + x;
|
|
|
|
|
|
targetPixels[dstIndex] = cameraPixels[srcIndex];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-28 16:35:51 +08:00
|
|
|
|
}
|
2026-07-23 15:04:51 +08:00
|
|
|
|
|
2026-06-10 15:04:14 +08:00
|
|
|
|
previewTexture.SetPixels32(targetPixels);
|
|
|
|
|
|
previewTexture.Apply();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 15:04:51 +08:00
|
|
|
|
[SerializeField] private float previewUpdateInterval = 0.03f;
|
2026-07-23 14:44:41 +08:00
|
|
|
|
|
2026-06-10 15:04:14 +08:00
|
|
|
|
private IEnumerator PreviewUpdateCoroutine()
|
|
|
|
|
|
{
|
|
|
|
|
|
while (isScanning && webCamTexture != null && webCamTexture.isPlaying)
|
2026-04-28 16:35:51 +08:00
|
|
|
|
{
|
2026-06-10 15:04:14 +08:00
|
|
|
|
if (previewTexture != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
CutAndRotateCameraFrame(webCamTexture.width, webCamTexture.height, previewTexture.width, previewTexture.height);
|
|
|
|
|
|
}
|
|
|
|
|
|
yield return new WaitForSeconds(previewUpdateInterval);
|
2026-04-28 16:35:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-07-23 15:04:51 +08:00
|
|
|
|
/// 扫描协程(ZXing 解码)
|
2026-04-28 16:35:51 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
private IEnumerator ScanCoroutine()
|
|
|
|
|
|
{
|
|
|
|
|
|
yield return null;
|
|
|
|
|
|
|
|
|
|
|
|
while (isScanning && webCamTexture != null && webCamTexture.isPlaying)
|
|
|
|
|
|
{
|
2026-06-08 08:55:10 +08:00
|
|
|
|
if (isProcessing)
|
|
|
|
|
|
{
|
|
|
|
|
|
yield return null;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
isProcessing = true;
|
|
|
|
|
|
|
2026-06-10 15:04:14 +08:00
|
|
|
|
if (previewTexture != null)
|
2026-04-28 16:35:51 +08:00
|
|
|
|
{
|
2026-07-23 15:04:51 +08:00
|
|
|
|
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++)
|
2026-04-28 16:35:51 +08:00
|
|
|
|
{
|
2026-07-23 15:04:51 +08:00
|
|
|
|
rgbBytes[i * 3] = pixels[i].r;
|
|
|
|
|
|
rgbBytes[i * 3 + 1] = pixels[i].g;
|
|
|
|
|
|
rgbBytes[i * 3 + 2] = pixels[i].b;
|
|
|
|
|
|
}
|
2026-06-23 11:27:51 +08:00
|
|
|
|
|
2026-07-23 15:04:51 +08:00
|
|
|
|
string result = null;
|
|
|
|
|
|
bool scanComplete = false;
|
2026-06-08 08:55:10 +08:00
|
|
|
|
|
2026-07-23 15:04:51 +08:00
|
|
|
|
System.Threading.ThreadPool.QueueUserWorkItem(_ =>
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
2026-06-08 08:55:10 +08:00
|
|
|
|
{
|
2026-07-23 15:04:51 +08:00
|
|
|
|
result = DecodeQRCodeFromBytes(rgbBytes, texWidth, texHeight);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception e)
|
2026-04-28 16:35:51 +08:00
|
|
|
|
{
|
2026-07-23 15:04:51 +08:00
|
|
|
|
Debug.LogError($"[ScanQRcode] 解码异常: {e.Message}");
|
2026-04-28 16:35:51 +08:00
|
|
|
|
}
|
2026-07-23 15:04:51 +08:00
|
|
|
|
finally
|
2026-04-28 16:35:51 +08:00
|
|
|
|
{
|
2026-07-23 15:04:51 +08:00
|
|
|
|
scanComplete = true;
|
2026-04-28 16:35:51 +08:00
|
|
|
|
}
|
2026-07-23 15:04:51 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
yield return new WaitUntil(() => scanComplete);
|
|
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(result))
|
|
|
|
|
|
{
|
|
|
|
|
|
ProcessScanResult(result);
|
2026-04-28 16:35:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 08:55:10 +08:00
|
|
|
|
isProcessing = false;
|
2026-04-28 16:35:51 +08:00
|
|
|
|
yield return new WaitForSeconds(scanInterval);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 08:55:10 +08:00
|
|
|
|
/// <summary>
|
2026-07-23 15:04:51 +08:00
|
|
|
|
/// 从原始字节数据解析二维码(后台线程安全)
|
2026-06-23 11:27:51 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
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;
|
2026-06-08 08:55:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError($"二维码解析失败: {ex.Message}");
|
|
|
|
|
|
}
|
2026-07-23 15:04:51 +08:00
|
|
|
|
return null;
|
2026-06-08 08:55:10 +08:00
|
|
|
|
}
|
2026-06-10 15:04:14 +08:00
|
|
|
|
|
2026-06-08 08:55:10 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 尝试使用不同的二值化算法解码
|
|
|
|
|
|
/// </summary>
|
2026-06-10 15:04:14 +08:00
|
|
|
|
private string TryDecodeWithBinarizer(ZXing.LuminanceSource luminanceSource)
|
2026-06-08 08:55:10 +08:00
|
|
|
|
{
|
|
|
|
|
|
var hints = new Dictionary<ZXing.DecodeHintType, object>
|
|
|
|
|
|
{
|
|
|
|
|
|
{ ZXing.DecodeHintType.TRY_HARDER, true },
|
|
|
|
|
|
{ ZXing.DecodeHintType.POSSIBLE_FORMATS, new List<ZXing.BarcodeFormat> { ZXing.BarcodeFormat.QR_CODE } }
|
|
|
|
|
|
};
|
2026-07-23 15:04:51 +08:00
|
|
|
|
|
2026-06-08 08:55:10 +08:00
|
|
|
|
var reader = new ZXing.QrCode.QRCodeReader();
|
2026-07-23 15:04:51 +08:00
|
|
|
|
|
2026-06-08 08:55:10 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-04-28 16:35:51 +08:00
|
|
|
|
var binarizer = new ZXing.Common.HybridBinarizer(luminanceSource);
|
|
|
|
|
|
var binaryBitmap = new ZXing.BinaryBitmap(binarizer);
|
|
|
|
|
|
var result = reader.decode(binaryBitmap, hints);
|
2026-06-08 08:55:10 +08:00
|
|
|
|
if (result != null)
|
|
|
|
|
|
return result.Text;
|
2026-04-28 16:35:51 +08:00
|
|
|
|
}
|
2026-06-08 08:55:10 +08:00
|
|
|
|
catch { }
|
2026-07-23 15:04:51 +08:00
|
|
|
|
|
2026-06-08 08:55:10 +08:00
|
|
|
|
try
|
2026-04-28 16:35:51 +08:00
|
|
|
|
{
|
2026-06-08 08:55:10 +08:00
|
|
|
|
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;
|
2026-04-28 16:35:51 +08:00
|
|
|
|
}
|
2026-06-08 08:55:10 +08:00
|
|
|
|
catch { }
|
2026-07-23 15:04:51 +08:00
|
|
|
|
|
2026-06-08 08:55:10 +08:00
|
|
|
|
return null;
|
2026-04-28 16:35:51 +08:00
|
|
|
|
}
|
2026-07-23 15:04:51 +08:00
|
|
|
|
#endif // !UNITY_IOS || UNITY_EDITOR
|
|
|
|
|
|
|
|
|
|
|
|
// ──── 公共处理方法 ────
|
|
|
|
|
|
|
2026-06-10 15:04:14 +08:00
|
|
|
|
/// <summary>
|
2026-07-23 15:04:51 +08:00
|
|
|
|
/// 处理扫描结果
|
2026-06-10 15:04:14 +08:00
|
|
|
|
/// </summary>
|
2026-07-23 15:04:51 +08:00
|
|
|
|
private void ProcessScanResult(string result)
|
2026-06-10 15:04:14 +08:00
|
|
|
|
{
|
2026-07-23 15:04:51 +08:00
|
|
|
|
Debug.Log($"[ScanQRcode] 扫描结果: {result}");
|
|
|
|
|
|
|
|
|
|
|
|
if (scanType == ScanType.ConnectDevice)
|
2026-06-10 15:04:14 +08:00
|
|
|
|
{
|
2026-07-23 15:04:51 +08:00
|
|
|
|
string macAddress = ExtractMacAddress(result);
|
|
|
|
|
|
if (!string.IsNullOrEmpty(macAddress))
|
2026-06-10 15:04:14 +08:00
|
|
|
|
{
|
2026-07-23 15:04:51 +08:00
|
|
|
|
OnQRCodeScanned?.Invoke(macAddress);
|
|
|
|
|
|
OnQRCodeScanned = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
ToastUI.Show("100084");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!string.IsNullOrEmpty(result))
|
|
|
|
|
|
{
|
|
|
|
|
|
OnQRCodeScanned?.Invoke(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
ToastUI.Show("100084");
|
2026-06-10 15:04:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-28 16:35:51 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-06-08 08:55:10 +08:00
|
|
|
|
/// 从字符串中提取 MAC 地址
|
2026-04-28 16:35:51 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
private string ExtractMacAddress(string input)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrEmpty(input))
|
|
|
|
|
|
return null;
|
|
|
|
|
|
|
2026-07-23 15:04:51 +08:00
|
|
|
|
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})");
|
2026-06-08 08:55:10 +08:00
|
|
|
|
|
2026-07-23 15:04:51 +08:00
|
|
|
|
var kvMatch = macKeyValueRegex.Match(input);
|
2026-06-08 08:55:10 +08:00
|
|
|
|
if (kvMatch.Success)
|
2026-07-23 15:04:51 +08:00
|
|
|
|
return FormatMacAddress(kvMatch.Groups[1].Value);
|
2026-06-08 08:55:10 +08:00
|
|
|
|
|
2026-07-23 15:04:51 +08:00
|
|
|
|
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);
|
2026-04-28 16:35:51 +08:00
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 08:55:10 +08:00
|
|
|
|
/// <summary>
|
2026-07-23 15:04:51 +08:00
|
|
|
|
/// 格式化 MAC 地址为 XX:XX:XX:XX:XX:XX
|
2026-06-08 08:55:10 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
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}",
|
2026-07-23 15:04:51 +08:00
|
|
|
|
cleanMac.Substring(0, 2), cleanMac.Substring(2, 2),
|
|
|
|
|
|
cleanMac.Substring(4, 2), cleanMac.Substring(6, 2),
|
|
|
|
|
|
cleanMac.Substring(8, 2), cleanMac.Substring(10, 2));
|
2026-04-28 16:35:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void OnDestroy()
|
|
|
|
|
|
{
|
|
|
|
|
|
StopScan();
|
|
|
|
|
|
gameObject.SetActive(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|