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);
}
}
}