“虞渠成” 7b5272b468 0608
2026-06-08 08:55:10 +08:00

711 lines
25 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections;
using System.Collections.Generic;
using Kill.UI.Components;
using UnityEngine;
using UnityEngine.UI;
namespace Kill.UI.Pages
{
/// <summary>
/// 二维码扫描器 - 调用摄像头扫描二维码
/// </summary>
public class ScanQRcode : MonoBehaviour
{
[Serializable]
public enum ScanType
{
ConnectDevice, // 连接设备
Other // 其他用途
}
[Header("UI组件")]
public RawImage cameraPreview; // 摄像头预览
[Header("扫描设置")]
public float scanInterval = 0.15f; // 扫描间隔(秒)
public ScanType scanType = ScanType.ConnectDevice; // 扫描类型
[Header("小二维码优化")]
[Tooltip("启用图像放大(推荐用于小二维码)")]
public bool enableUpscale = true;
[Tooltip("放大倍数")]
public float upscaleFactor = 2f;
[Tooltip("启用多区域扫描(从画面中心向四周扫描)")]
bool enableMultiRegionScan = false;
[Tooltip("扫描区域数量")]
public int scanRegionCount = 3;
[Header("性能优化")]
[Tooltip("降低扫描分辨率推荐值640或480")]
public int scanResolutionWidth = 640;
[Tooltip("扫描处理线程数")]
public bool useAsyncScan = true;
[Header("自动对焦")]
[Tooltip("启用自动对焦")]
public bool enableAutoFocus = true;
[Tooltip("对焦间隔(秒)")]
public float focusInterval = 2f;
// 摄像头相关
private WebCamTexture webCamTexture;
private WebCamDevice currentDevice;
private bool isScanning = false;
private bool isProcessing = false;
// 扫描结果回调
public event Action<string> OnQRCodeScanned;
void OnEnable()
{
StartScan();
}
void OnDisable()
{
StopScan();
}
/// <summary>
/// 开始扫描
/// </summary>
public void StartScan()
{
if (isScanning) return;
StartCoroutine(InitializeCamera());
}
/// <summary>
/// 停止扫描
/// </summary>
public void StopScan()
{
isScanning = false;
StopAllCoroutines();
if (webCamTexture != null && webCamTexture.isPlaying)
{
webCamTexture.Stop();
webCamTexture = null;
}
}
/// <summary>
/// 关闭扫码界面
/// </summary>
public void Close()
{
StopScan();
gameObject.SetActive(false);
}
/// <summary>
/// 初始化摄像头
/// </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));
}
if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Camera))
{
UpdateStatus("100085");
yield break;
}
#else
// 其他平台使用传统方式
yield return Application.RequestUserAuthorization(UserAuthorization.WebCam);
if (!Application.HasUserAuthorization(UserAuthorization.WebCam))
{
UpdateStatus("100085");
yield break;
}
#endif
// 获取后置摄像头
WebCamDevice[] devices = WebCamTexture.devices;
if (devices.Length == 0)
{
UpdateStatus("100086");
yield break;
}
// 优先使用后置摄像头,并选择支持自动对焦的摄像头
string deviceName = devices[0].name;
for (int i = 0; i < devices.Length; i++)
{
if (!devices[i].isFrontFacing)
{
deviceName = devices[i].name;
currentDevice = devices[i];
break;
}
}
// 使用较低的扫描分辨率以提高性能
int targetWidth = scanResolutionWidth;
int targetHeight = Mathf.RoundToInt(targetWidth * Screen.height / Screen.width);
// 创建摄像头纹理
webCamTexture = new WebCamTexture(deviceName, targetWidth, targetHeight, 30);
cameraPreview.texture = webCamTexture;
// 开始播放
webCamTexture.Play();
// 等待摄像头启动
yield return new WaitUntil(() => webCamTexture.width > 100);
// 调整预览画面比例
AdjustPreviewAspect();
// 启动自动对焦
if (enableAutoFocus)
{
StartCoroutine(AutoFocusCoroutine());
}
isScanning = true;
// 开始扫描
StartCoroutine(ScanCoroutine());
}
/// <summary>
/// 自动对焦协程
/// </summary>
private IEnumerator AutoFocusCoroutine()
{
while (isScanning && webCamTexture != null && webCamTexture.isPlaying)
{
TriggerAutoFocus();
yield return new WaitForSeconds(focusInterval);
}
}
/// <summary>
/// 触发自动对焦
/// </summary>
private void TriggerAutoFocus()
{
#if UNITY_ANDROID && !UNITY_EDITOR
try
{
using (AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
{
using (AndroidJavaObject activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity"))
{
using (AndroidJavaObject camera = new AndroidJavaObject("android.hardware.Camera"))
{
// 尝试使用 Camera API 触发对焦
var cameraClass = new AndroidJavaClass("android.hardware.Camera");
var cameraInstance = cameraClass.CallStatic<AndroidJavaObject>("open");
if (cameraInstance != null)
{
var parameters = cameraInstance.Call<AndroidJavaObject>("getParameters");
if (parameters != null)
{
// 设置对焦模式
var supportedModes = parameters.Call<AndroidJavaObject>("getSupportedFocusModes");
if (supportedModes != null)
{
// 优先使用连续自动对焦
if (supportedModes.Call<bool>("contains", "continuous-video"))
{
parameters.Call("setFocusMode", "continuous-video");
}
else if (supportedModes.Call<bool>("contains", "auto"))
{
parameters.Call("setFocusMode", "auto");
}
cameraInstance.Call("setParameters", parameters);
}
}
cameraInstance.Call("release");
}
}
}
}
}
catch (Exception e)
{
Debug.LogWarning($"[ScanQRcode] 自动对焦设置失败: {e.Message}");
}
#elif UNITY_IOS && !UNITY_EDITOR
// iOS 自动对焦通常由系统自动处理
// 如果需要手动控制,需要使用原生插件
Debug.Log("[ScanQRcode] iOS 自动对焦由系统管理");
#endif
}
/// <summary>
/// 调整预览画面比例
/// </summary>
private void AdjustPreviewAspect()
{
if (webCamTexture == null) return;
float videoRatio = (float)webCamTexture.width / webCamTexture.height;
float screenRatio = (float)Screen.width / Screen.height;
// 根据摄像头方向调整
int rotation = webCamTexture.videoRotationAngle;
cameraPreview.rectTransform.localEulerAngles = new Vector3(0, 0, -rotation);
// 调整缩放以适应屏幕
if (rotation == 90 || rotation == 270)
{
videoRatio = 1f / videoRatio;
}
// 保持比例填充
if (videoRatio > screenRatio)
{
cameraPreview.rectTransform.localScale = new Vector3(screenRatio / videoRatio, 1, 1);
}
else
{
cameraPreview.rectTransform.localScale = new Vector3(1, videoRatio / screenRatio, 1);
}
}
/// <summary>
/// 扫描协程
/// </summary>
private IEnumerator ScanCoroutine()
{
// 等待一帧确保摄像头已准备好
yield return null;
while (isScanning && webCamTexture != null && webCamTexture.isPlaying)
{
// 避免同时处理多帧
if (isProcessing)
{
yield return null;
continue;
}
isProcessing = true;
// 获取当前帧
Texture2D snapshot = GetSnapshot();
if (snapshot != null)
{
// 异步解析二维码以避免卡顿
if (useAsyncScan)
{
string result = null;
bool scanComplete = false;
// 在后台线程执行解码
System.Threading.ThreadPool.QueueUserWorkItem(_ =>
{
try
{
result = DecodeQRCode(snapshot);
}
catch (Exception e)
{
Debug.LogError($"[ScanQRcode] 解码异常: {e.Message}");
}
finally
{
scanComplete = true;
}
});
// 等待解码完成
yield return new WaitUntil(() => scanComplete);
// 在主线程销毁纹理
Destroy(snapshot);
// 处理结果
if (!string.IsNullOrEmpty(result))
{
ProcessScanResult(result);
yield break;
}
}
else
{
// 同步解码
string result = DecodeQRCode(snapshot);
Destroy(snapshot);
if (!string.IsNullOrEmpty(result))
{
ProcessScanResult(result);
yield break;
}
}
}
isProcessing = false;
yield return new WaitForSeconds(scanInterval);
}
}
/// <summary>
/// 处理扫描结果
/// </summary>
private void ProcessScanResult(string result)
{
// 根据扫描类型处理结果
if (scanType == ScanType.ConnectDevice)
{
// 从结果中提取 MAC 地址
string macAddress = ExtractMacAddress(result);
if (!string.IsNullOrEmpty(macAddress))
{
// 扫描成功,仅回传 MAC 地址
OnQRCodeScanned?.Invoke(macAddress);
}
else
{
// 二维码不符合要求,显示提示
UpdateStatus("100084");
}
}
else
{
if (!string.IsNullOrEmpty(result))
{
// 其他用途,直接回传结果
OnQRCodeScanned?.Invoke(result);
}
else
{
// 二维码不符合要求,显示提示
UpdateStatus("100084");
}
}
}
/// <summary>
/// 获取摄像头当前帧
/// </summary>
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;
}
}
/// <summary>
/// 放大纹理(使用双线性插值)- 优化版本
/// </summary>
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;
}
/// <summary>
/// 解析二维码(支持多区域扫描)
/// </summary>
private string DecodeQRCode(Texture2D texture)
{
try
{
// 首先尝试全图扫描
string result = TryDecodeRegion(texture, 0, 0, texture.width, texture.height);
if (!string.IsNullOrEmpty(result))
return result;
// 如果启用了多区域扫描,尝试从中心向外扫描不同区域
if (enableMultiRegionScan)
{
result = TryDecodeMultipleRegions(texture);
if (!string.IsNullOrEmpty(result))
return result;
}
return null;
}
catch (Exception ex)
{
Debug.LogError($"二维码解析失败: {ex.Message}");
return null;
}
}
/// <summary>
/// 尝试从多个区域扫描(中心优先)
/// </summary>
private string TryDecodeMultipleRegions(Texture2D texture)
{
int width = texture.width;
int height = texture.height;
// 定义扫描区域(从中心向外)
float[] regionScales = { 0.6f, 0.8f, 1.0f };
foreach (float scale in regionScales)
{
int regionW = Mathf.RoundToInt(width * scale);
int regionH = Mathf.RoundToInt(height * scale);
int startX = (width - regionW) / 2;
int startY = (height - regionH) / 2;
string result = TryDecodeRegion(texture, startX, startY, regionW, regionH);
if (!string.IsNullOrEmpty(result))
return result;
}
return null;
}
/// <summary>
/// 尝试解码指定区域
/// </summary>
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[] pixels = texture.GetPixels32();
// 转换为灰度图
byte[] grayscale = new byte[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 < pixels.Length)
{
// 使用标准亮度公式: Y = 0.299R + 0.587G + 0.114B
grayscale[dstIndex] = (byte)(0.299f * pixels[srcIndex].r +
0.587f * pixels[srcIndex].g +
0.114f * pixels[srcIndex].b);
}
}
}
// 创建 LuminanceSource
var luminanceSource = new ZXing.PlanarYUVLuminanceSource(
grayscale,
width,
height,
0,
0,
width,
height,
false
);
// 尝试多种二值化算法
string result = TryDecodeWithBinarizer(luminanceSource);
if (!string.IsNullOrEmpty(result))
return result;
return null;
}
catch
{
return null;
}
}
/// <summary>
/// 尝试使用不同的二值化算法解码
/// </summary>
private string TryDecodeWithBinarizer(ZXing.PlanarYUVLuminanceSource luminanceSource)
{
var hints = new Dictionary<ZXing.DecodeHintType, object>
{
{ ZXing.DecodeHintType.TRY_HARDER, true },
{ ZXing.DecodeHintType.POSSIBLE_FORMATS, new List<ZXing.BarcodeFormat> { ZXing.BarcodeFormat.QR_CODE } }
};
var reader = new ZXing.QrCode.QRCodeReader();
// 尝试 HybridBinarizer
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 { }
// 尝试 GlobalHistogramBinarizer
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;
}
/// <summary>
/// 从字符串中提取 MAC 地址
/// 支持格式:
/// 1. 带冒号格式98:EA:A0:02:4E:06
/// 2. 不带冒号格式98eaa002658e
/// 3. 从键值对格式提取Name:xxx;MAC:98eaa002658e
/// </summary>
private string ExtractMacAddress(string input)
{
if (string.IsNullOrEmpty(input))
return null;
string macAddress = null;
// 尝试从键值对格式提取 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);
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;
}
}
}
if (!string.IsNullOrEmpty(macAddress))
{
// 统一转换为带冒号的大写格式
return FormatMacAddress(macAddress);
}
return null;
}
/// <summary>
/// 将 MAC 地址统一格式化为带冒号的大写格式
/// 输入: 98eaa002658e 或 98:EA:A0:02:65:8E
/// 输出: 98:EA:A0:02:65:8E
/// </summary>
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));
}
/// <summary>
/// 更新状态文字
/// </summary>
private void UpdateStatus(string code)
{
ToastUI.Show(code);
}
void OnDestroy()
{
StopScan();
gameObject.SetActive(false);
}
}
}