519 lines
17 KiB
C#
Raw Normal View History

2026-04-28 16:35:51 +08:00
using System;
using System.Collections;
using System.Collections.Generic;
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>
/// 二维码扫描器 - 调用摄像头扫描二维码
/// 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-04-28 16:35:51 +08:00
[Header("UI组件")]
public RawImage cameraPreview; // 摄像头预览
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-06-08 08:55:10 +08:00
[Header("自动对焦")]
[Tooltip("启用对焦")]
2026-06-08 08:55:10 +08:00
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
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;
void OnEnable()
{
StartScan();
}
void OnDisable()
{
StopScan();
}
/// <summary>
/// 开始扫描
/// </summary>
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
2026-04-28 16:35:51 +08:00
StartCoroutine(InitializeCamera());
#endif
2026-04-28 16:35:51 +08:00
}
/// <summary>
/// 停止扫描
/// </summary>
public void StopScan()
{
isScanning = false;
StopAllCoroutines();
#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-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);
}
// ──── 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>
/// 初始化摄像头WebCamTexture 方案)
2026-04-28 16:35:51 +08:00
/// </summary>
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));
}
2026-04-28 16:35:51 +08:00
if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Camera))
{
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))
{
ToastUI.Show("100085");
2026-04-28 16:35:51 +08:00
yield break;
}
#endif
WebCamDevice[] devices = WebCamTexture.devices;
if (devices.Length == 0)
{
ToastUI.Show("100086");
2026-04-28 16:35:51 +08:00
yield break;
}
// 优先后置主摄
2026-04-28 16:35:51 +08:00
string deviceName = devices[0].name;
currentDevice = devices[0];
2026-04-28 16:35:51 +08:00
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];
}
2026-04-28 16:35:51 +08:00
}
}
Debug.Log($"[ScanQRcode] 选中摄像头: {deviceName}");
webCamTexture = new WebCamTexture(deviceName, 2160, 30);
2026-04-28 16:35:51 +08:00
webCamTexture.Play();
yield return new WaitUntil(() => webCamTexture.width > 100);
2026-04-28 16:35:51 +08:00
AdjustPreviewAspect();
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());
if (enableAutoFocus)
{
StartCoroutine(AutoFocusCoroutine());
}
2026-04-28 16:35:51 +08:00
}
/// <summary>
/// 自动对焦维护协程
/// </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;
int cameraWidth = webCamTexture.width;
int cameraHeight = webCamTexture.height;
2026-06-10 15:04:14 +08:00
RectTransform previewRect = cameraPreview.GetComponent<RectTransform>();
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-06-10 15:04:14 +08:00
CutAndRotateCameraFrame(cameraWidth, cameraHeight, previewWidth, previewHeight);
2026-06-10 15:04:14 +08:00
cameraPreview.texture = previewTexture;
cameraPreview.uvRect = new Rect(0, 0, 1, 1);
previewRect.localEulerAngles = Vector3.zero;
}
2026-06-10 15:04:14 +08:00
private Texture2D previewTexture;
2026-06-10 15:04:14 +08:00
/// <summary>
/// 从相机画面裁切并旋转,生成预览画面
/// </summary>
private void CutAndRotateCameraFrame(int camW, int camH, int targetW, int targetH)
{
int cropY = (camW - targetH) / 2;
2026-06-10 15:04:14 +08:00
Color32[] cameraPixels = webCamTexture.GetPixels32();
Color32[] targetPixels = new Color32[targetW * targetH];
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++)
{
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-06-10 15:04:14 +08:00
previewTexture.SetPixels32(targetPixels);
previewTexture.Apply();
}
[SerializeField] private float previewUpdateInterval = 0.03f;
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>
/// 扫描协程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
{
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
{
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
string result = null;
bool scanComplete = false;
2026-06-08 08:55:10 +08:00
System.Threading.ThreadPool.QueueUserWorkItem(_ =>
{
try
2026-06-08 08:55:10 +08:00
{
result = DecodeQRCodeFromBytes(rgbBytes, texWidth, texHeight);
}
catch (Exception e)
2026-04-28 16:35:51 +08:00
{
Debug.LogError($"[ScanQRcode] 解码异常: {e.Message}");
2026-04-28 16:35:51 +08:00
}
finally
2026-04-28 16:35:51 +08:00
{
scanComplete = true;
2026-04-28 16:35: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-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}");
}
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-06-08 08:55:10 +08:00
var reader = new ZXing.QrCode.QRCodeReader();
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-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-06-08 08:55:10 +08:00
return null;
2026-04-28 16:35:51 +08:00
}
#endif // !UNITY_IOS || UNITY_EDITOR
// ──── 公共处理方法 ────
2026-06-10 15:04:14 +08:00
/// <summary>
/// 处理扫描结果
2026-06-10 15:04:14 +08:00
/// </summary>
private void ProcessScanResult(string result)
2026-06-10 15:04:14 +08:00
{
Debug.Log($"[ScanQRcode] 扫描结果: {result}");
if (scanType == ScanType.ConnectDevice)
2026-06-10 15:04:14 +08:00
{
string macAddress = ExtractMacAddress(result);
if (!string.IsNullOrEmpty(macAddress))
2026-06-10 15:04:14 +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;
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
var kvMatch = macKeyValueRegex.Match(input);
2026-06-08 08:55:10 +08:00
if (kvMatch.Success)
return FormatMacAddress(kvMatch.Groups[1].Value);
2026-06-08 08:55:10 +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>
/// 格式化 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}",
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);
}
}
}