feat(iOS): 实现原生二维码扫描功能
重构iOS平台摄像头扫描逻辑,使用AVCaptureSession原生实现二维码扫描,替换原有的WebCamTexture方案: 1. 新增NativeCameraController封装摄像头会话、预览层和二维码检测逻辑 2. 实现摄像头权限申请、自动对焦配置和原生QR码识别 3. 适配C#层调用,通过UnitySendMessage回调扫描结果 4. 移除原iOS平台下冗余的自动对焦维护逻辑
This commit is contained in:
parent
5c21080203
commit
668dad0e67
@ -1,103 +1,191 @@
|
|||||||
#import <AVFoundation/AVFoundation.h>
|
#import <AVFoundation/AVFoundation.h>
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
/// 通过名称查找摄像头设备
|
@interface NativeCameraController : NSObject <AVCaptureMetadataOutputObjectsDelegate>
|
||||||
static AVCaptureDevice *FindCameraByName(const char *cameraName)
|
@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) {
|
self.gameObjectName = goName;
|
||||||
return [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
|
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, *)) {
|
if (@available(iOS 10.0, *)) {
|
||||||
AVCaptureDeviceDiscoverySession *session = [AVCaptureDeviceDiscoverySession
|
AVCaptureDeviceDiscoverySession *session = [AVCaptureDeviceDiscoverySession
|
||||||
discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInWideAngleCamera,
|
discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInWideAngleCamera,
|
||||||
AVCaptureDeviceTypeBuiltInTelephotoCamera,
|
AVCaptureDeviceTypeBuiltInTelephotoCamera,
|
||||||
AVCaptureDeviceTypeBuiltInUltraWideCamera]
|
AVCaptureDeviceTypeBuiltInUltraWideCamera]
|
||||||
mediaType:AVMediaTypeVideo
|
mediaType:AVMediaTypeVideo
|
||||||
position:AVCaptureDevicePositionUnspecified];
|
position:AVCaptureDevicePositionBack];
|
||||||
for (AVCaptureDevice *device in session.devices) {
|
// 优先宽角(主摄)
|
||||||
if ([device.localizedName containsString:name] ||
|
for (AVCaptureDevice *d in session.devices) {
|
||||||
[device.modelID containsString:name]) {
|
if (d.deviceType == AVCaptureDeviceTypeBuiltInWideAngleCamera) {
|
||||||
return device;
|
return d;
|
||||||
}
|
|
||||||
}
|
|
||||||
// 没找到精确匹配,返回第一个活跃的
|
|
||||||
for (AVCaptureDevice *device in session.devices) {
|
|
||||||
if (device.isConnected && !device.isSuspended) {
|
|
||||||
return device;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 兜底:返回第一个
|
||||||
|
return session.devices.firstObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
|
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" {
|
extern "C" {
|
||||||
|
|
||||||
/// 启用连续自动对焦和自动曝光(传入 C# 选中的摄像头名称)
|
void _StartNativeCamera(const char *gameObjectName)
|
||||||
void _EnableContinuousAutoFocus(const char *cameraName)
|
|
||||||
{
|
{
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
if (g_cameraController) {
|
||||||
AVCaptureDevice *device = FindCameraByName(cameraName);
|
[g_cameraController stop];
|
||||||
if (!device) {
|
g_cameraController = nil;
|
||||||
NSLog(@"[CameraFocusHelper] 未找到摄像头设备");
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSLog(@"[CameraFocusHelper] 配置设备: %@ (name=%@, model=%@)",
|
// 权限请求
|
||||||
device.localizedName, device.localizedName, device.modelID);
|
AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
|
||||||
|
if (status == AVAuthorizationStatusNotDetermined) {
|
||||||
NSError *error = nil;
|
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
|
||||||
if ([device lockForConfiguration:&error]) {
|
if (granted) {
|
||||||
|
g_cameraController = [[NativeCameraController alloc] init];
|
||||||
// 连续自动对焦
|
NSString *goName = [NSString stringWithUTF8String:gameObjectName];
|
||||||
if ([device isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) {
|
[g_cameraController startWithName:goName];
|
||||||
device.focusMode = AVCaptureFocusModeContinuousAutoFocus;
|
|
||||||
NSLog(@"[CameraFocusHelper] 已设为连续自动对焦");
|
|
||||||
} else {
|
} else {
|
||||||
NSLog(@"[CameraFocusHelper] 设备不支持连续自动对焦");
|
UnitySendMessage((char *)gameObjectName, "OnNativeScanResult", "ERROR:PERMISSION_DENIED");
|
||||||
}
|
}
|
||||||
|
}];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 连续自动曝光
|
if (status != AVAuthorizationStatusAuthorized) {
|
||||||
if ([device isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) {
|
UnitySendMessage((char *)gameObjectName, "OnNativeScanResult", "ERROR:PERMISSION_DENIED");
|
||||||
device.exposureMode = AVCaptureExposureModeContinuousAutoExposure;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对焦范围限制为近距
|
g_cameraController = [[NativeCameraController alloc] init];
|
||||||
if ([device respondsToSelector:@selector(isAutoFocusRangeRestrictionSupported)] &&
|
NSString *goName = [NSString stringWithUTF8String:gameObjectName];
|
||||||
[device isAutoFocusRangeRestrictionSupported]) {
|
[g_cameraController startWithName:goName];
|
||||||
device.autoFocusRangeRestriction = AVCaptureAutoFocusRangeRestrictionNear;
|
|
||||||
NSLog(@"[CameraFocusHelper] 已设对焦范围为近距");
|
|
||||||
}
|
|
||||||
|
|
||||||
[device unlockForConfiguration];
|
|
||||||
NSLog(@"[CameraFocusHelper] 摄像头配置完成");
|
|
||||||
} else {
|
|
||||||
NSLog(@"[CameraFocusHelper] lockForConfiguration 失败: %@", error.localizedDescription);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 确保连续自动对焦仍在生效(传入 C# 选中的摄像头名称)
|
void _StopNativeCamera()
|
||||||
void _EnsureContinuousAutoFocus(const char *cameraName)
|
|
||||||
{
|
{
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
if (g_cameraController) {
|
||||||
AVCaptureDevice *device = FindCameraByName(cameraName);
|
[g_cameraController stop];
|
||||||
if (!device) return;
|
g_cameraController = nil;
|
||||||
|
}
|
||||||
NSError *error = nil;
|
|
||||||
if ([device lockForConfiguration:&error]) {
|
|
||||||
if (device.focusMode != AVCaptureFocusModeContinuousAutoFocus &&
|
|
||||||
[device isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) {
|
|
||||||
device.focusMode = AVCaptureFocusModeContinuousAutoFocus;
|
|
||||||
NSLog(@"[CameraFocusHelper] 重新设回连续自动对焦");
|
|
||||||
}
|
|
||||||
[device unlockForConfiguration];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,8 @@ namespace Kill.UI.Pages
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 二维码扫描器 - 调用摄像头扫描二维码
|
/// 二维码扫描器 - 调用摄像头扫描二维码
|
||||||
|
/// Android: 使用 WebCamTexture + ZXing 解码
|
||||||
|
/// iOS: 使用原生 AVCaptureSession(连续对焦 + 原生 QR 检测)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ScanQRcode : MonoBehaviour
|
public class ScanQRcode : MonoBehaviour
|
||||||
{
|
{
|
||||||
@ -19,37 +21,26 @@ namespace Kill.UI.Pages
|
|||||||
ConnectDevice, // 连接设备
|
ConnectDevice, // 连接设备
|
||||||
Other // 其他用途
|
Other // 其他用途
|
||||||
}
|
}
|
||||||
|
|
||||||
[Header("UI组件")]
|
[Header("UI组件")]
|
||||||
public RawImage cameraPreview; // 摄像头预览
|
public RawImage cameraPreview; // 摄像头预览
|
||||||
|
|
||||||
[Header("扫描设置")]
|
[Header("扫描设置")]
|
||||||
float scanInterval = 0.2f; // 扫描间隔(秒)
|
float scanInterval = 0.2f; // 扫描间隔(秒)
|
||||||
public ScanType scanType = ScanType.ConnectDevice; // 扫描类型
|
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("自动对焦")]
|
[Header("自动对焦")]
|
||||||
[Tooltip("启用自动对焦")]
|
[Tooltip("启用对焦")]
|
||||||
public bool enableAutoFocus = true;
|
public bool enableAutoFocus = true;
|
||||||
[Tooltip("对焦间隔(秒)")]
|
|
||||||
public float focusInterval = 1f;
|
|
||||||
|
|
||||||
#if UNITY_IOS && !UNITY_EDITOR
|
#if UNITY_IOS && !UNITY_EDITOR
|
||||||
[DllImport("__Internal")]
|
[DllImport("__Internal")]
|
||||||
private static extern void _EnableContinuousAutoFocus(string cameraName);
|
private static extern void _StartNativeCamera(string gameObjectName);
|
||||||
|
|
||||||
[DllImport("__Internal")]
|
[DllImport("__Internal")]
|
||||||
private static extern void _EnsureContinuousAutoFocus(string cameraName);
|
private static extern void _StopNativeCamera();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// 摄像头相关
|
// 摄像头相关
|
||||||
private WebCamTexture webCamTexture;
|
private WebCamTexture webCamTexture;
|
||||||
private WebCamDevice currentDevice;
|
private WebCamDevice currentDevice;
|
||||||
@ -59,9 +50,6 @@ namespace Kill.UI.Pages
|
|||||||
// 扫描结果回调
|
// 扫描结果回调
|
||||||
public event Action<string> OnQRCodeScanned;
|
public event Action<string> OnQRCodeScanned;
|
||||||
|
|
||||||
// iOS 摄像头名称(供原生对焦插件使用)
|
|
||||||
private string selectedCameraName;
|
|
||||||
|
|
||||||
void OnEnable()
|
void OnEnable()
|
||||||
{
|
{
|
||||||
StartScan();
|
StartScan();
|
||||||
@ -79,7 +67,16 @@ namespace Kill.UI.Pages
|
|||||||
{
|
{
|
||||||
if (isScanning) return;
|
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());
|
StartCoroutine(InitializeCamera());
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -90,12 +87,18 @@ namespace Kill.UI.Pages
|
|||||||
isScanning = false;
|
isScanning = false;
|
||||||
StopAllCoroutines();
|
StopAllCoroutines();
|
||||||
|
|
||||||
|
#if UNITY_IOS && !UNITY_EDITOR
|
||||||
|
_StopNativeCamera();
|
||||||
|
if (cameraPreview != null)
|
||||||
|
cameraPreview.gameObject.SetActive(true);
|
||||||
|
#endif
|
||||||
|
|
||||||
if (webCamTexture != null && webCamTexture.isPlaying)
|
if (webCamTexture != null && webCamTexture.isPlaying)
|
||||||
{
|
{
|
||||||
webCamTexture.Stop();
|
webCamTexture.Stop();
|
||||||
webCamTexture = null;
|
webCamTexture = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理预览纹理
|
// 清理预览纹理
|
||||||
if (previewTexture != null)
|
if (previewTexture != null)
|
||||||
{
|
{
|
||||||
@ -113,8 +116,38 @@ namespace Kill.UI.Pages
|
|||||||
gameObject.SetActive(false);
|
gameObject.SetActive(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ──── iOS 原生回调 ────
|
||||||
|
|
||||||
/// <summary>
|
/// <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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化摄像头(WebCamTexture 方案)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private IEnumerator InitializeCamera()
|
private IEnumerator InitializeCamera()
|
||||||
{
|
{
|
||||||
@ -123,35 +156,32 @@ namespace Kill.UI.Pages
|
|||||||
if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Camera))
|
if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Camera))
|
||||||
{
|
{
|
||||||
UnityEngine.Android.Permission.RequestUserPermission(UnityEngine.Android.Permission.Camera);
|
UnityEngine.Android.Permission.RequestUserPermission(UnityEngine.Android.Permission.Camera);
|
||||||
// 等待权限请求结果
|
|
||||||
yield return new WaitUntil(() => UnityEngine.Android.Permission.HasUserAuthorizedPermission(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))
|
if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Camera))
|
||||||
{
|
{
|
||||||
UpdateStatus("100085");
|
ToastUI.Show("100085");
|
||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
// 其他平台使用传统方式
|
|
||||||
yield return Application.RequestUserAuthorization(UserAuthorization.WebCam);
|
yield return Application.RequestUserAuthorization(UserAuthorization.WebCam);
|
||||||
|
|
||||||
if (!Application.HasUserAuthorization(UserAuthorization.WebCam))
|
if (!Application.HasUserAuthorization(UserAuthorization.WebCam))
|
||||||
{
|
{
|
||||||
UpdateStatus("100085");
|
ToastUI.Show("100085");
|
||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// 获取后置摄像头
|
|
||||||
WebCamDevice[] devices = WebCamTexture.devices;
|
WebCamDevice[] devices = WebCamTexture.devices;
|
||||||
if (devices.Length == 0)
|
if (devices.Length == 0)
|
||||||
{
|
{
|
||||||
UpdateStatus("100086");
|
ToastUI.Show("100086");
|
||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优先使用后置主摄(广角),iOS 多摄设备排除超广角/长焦
|
// 优先后置主摄
|
||||||
string deviceName = devices[0].name;
|
string deviceName = devices[0].name;
|
||||||
currentDevice = devices[0];
|
currentDevice = devices[0];
|
||||||
for (int i = 0; i < devices.Length; i++)
|
for (int i = 0; i < devices.Length; i++)
|
||||||
@ -159,15 +189,12 @@ namespace Kill.UI.Pages
|
|||||||
if (!devices[i].isFrontFacing)
|
if (!devices[i].isFrontFacing)
|
||||||
{
|
{
|
||||||
string lowerName = devices[i].name.ToLower();
|
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"))
|
if (!lowerName.Contains("telephoto") && !lowerName.Contains("ultra wide"))
|
||||||
{
|
{
|
||||||
deviceName = devices[i].name;
|
deviceName = devices[i].name;
|
||||||
currentDevice = devices[i];
|
currentDevice = devices[i];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// 兜底:第一个后置
|
|
||||||
if (deviceName == devices[0].name)
|
if (deviceName == devices[0].name)
|
||||||
{
|
{
|
||||||
deviceName = devices[i].name;
|
deviceName = devices[i].name;
|
||||||
@ -176,121 +203,85 @@ namespace Kill.UI.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Debug.Log($"[ScanQRcode] 选中摄像头: {deviceName}");
|
Debug.Log($"[ScanQRcode] 选中摄像头: {deviceName}");
|
||||||
selectedCameraName = deviceName;
|
|
||||||
RectTransform cameraPreviewRect=cameraPreview.GetComponent<RectTransform>();
|
|
||||||
// 使用较低的扫描分辨率以提高性能
|
|
||||||
int targetWidth = 2160;
|
|
||||||
|
|
||||||
// 创建摄像头纹理
|
|
||||||
webCamTexture = new WebCamTexture(deviceName, targetWidth, 30);
|
|
||||||
|
|
||||||
// 开始播放
|
webCamTexture = new WebCamTexture(deviceName, 2160, 30);
|
||||||
webCamTexture.Play();
|
webCamTexture.Play();
|
||||||
|
|
||||||
// 等待摄像头启动
|
|
||||||
yield return new WaitUntil(() => webCamTexture.width > 100);
|
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();
|
AdjustPreviewAspect();
|
||||||
|
|
||||||
isScanning = true;
|
isScanning = true;
|
||||||
// 开始预览更新和扫描
|
|
||||||
StartCoroutine(PreviewUpdateCoroutine());
|
StartCoroutine(PreviewUpdateCoroutine());
|
||||||
StartCoroutine(ScanCoroutine());
|
StartCoroutine(ScanCoroutine());
|
||||||
// 启动自动对焦维护协程(仅非 iOS 平台使用 autoFocusPoint,iOS 走原生插件)
|
|
||||||
if (enableAutoFocus)
|
if (enableAutoFocus)
|
||||||
{
|
{
|
||||||
StartCoroutine(AutoFocusCoroutine());
|
StartCoroutine(AutoFocusCoroutine());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 调整预览画面比例 - 从相机画面中心截取最大适合预览画面比例的画面
|
/// 自动对焦维护协程
|
||||||
/// 手机竖着拿,摄像头默认横向输出,需要旋转并截取中间区域
|
/// </summary>
|
||||||
/// 预览框是横向的(1080x680),显示旋转后的竖向画面
|
private IEnumerator AutoFocusCoroutine()
|
||||||
|
{
|
||||||
|
while (isScanning)
|
||||||
|
{
|
||||||
|
if (webCamTexture != null && webCamTexture.isPlaying)
|
||||||
|
{
|
||||||
|
webCamTexture.autoFocusPoint = new Vector2(0.5f, 0.5f);
|
||||||
|
}
|
||||||
|
yield return new WaitForSeconds(1f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 调整预览画面比例
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void AdjustPreviewAspect()
|
private void AdjustPreviewAspect()
|
||||||
{
|
{
|
||||||
if (webCamTexture == null || cameraPreview == null) return;
|
if (webCamTexture == null || cameraPreview == null) return;
|
||||||
|
|
||||||
// 获取相机分辨率(摄像头默认横向,如 1920x1080)
|
int cameraWidth = webCamTexture.width;
|
||||||
int cameraWidth = webCamTexture.width; // 1920
|
int cameraHeight = webCamTexture.height;
|
||||||
int cameraHeight = webCamTexture.height; // 1080
|
|
||||||
|
|
||||||
// 获取预览区域尺寸(横向预览框,比如 1080x680)
|
|
||||||
RectTransform previewRect = cameraPreview.GetComponent<RectTransform>();
|
RectTransform previewRect = cameraPreview.GetComponent<RectTransform>();
|
||||||
int previewWidth = (int)previewRect.rect.width; // 1080
|
int previewWidth = (int)previewRect.rect.width;
|
||||||
int previewHeight = (int)previewRect.rect.height; // 680
|
int previewHeight = (int)previewRect.rect.height;
|
||||||
|
|
||||||
// 创建预览纹理(与预览框相同尺寸)
|
|
||||||
if (previewTexture == null || previewTexture.width != previewWidth || previewTexture.height != previewHeight)
|
if (previewTexture == null || previewTexture.width != previewWidth || previewTexture.height != previewHeight)
|
||||||
{
|
{
|
||||||
if (previewTexture != null)
|
if (previewTexture != null)
|
||||||
{
|
|
||||||
Destroy(previewTexture);
|
Destroy(previewTexture);
|
||||||
}
|
|
||||||
previewTexture = new Texture2D(previewWidth, previewHeight, TextureFormat.RGB24, false);
|
previewTexture = new Texture2D(previewWidth, previewHeight, TextureFormat.RGB24, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从相机画面中裁切并旋转
|
|
||||||
// 手机竖着拿,相机横向输出,需要顺时针旋转90度
|
|
||||||
// 从相机画面中间截取适合预览比例的竖向区域
|
|
||||||
CutAndRotateCameraFrame(cameraWidth, cameraHeight, previewWidth, previewHeight);
|
CutAndRotateCameraFrame(cameraWidth, cameraHeight, previewWidth, previewHeight);
|
||||||
|
|
||||||
// 设置预览纹理
|
|
||||||
cameraPreview.texture = previewTexture;
|
cameraPreview.texture = previewTexture;
|
||||||
cameraPreview.uvRect = new Rect(0, 0, 1, 1);
|
cameraPreview.uvRect = new Rect(0, 0, 1, 1);
|
||||||
|
|
||||||
// 重置旋转(因为已经在像素层面旋转了)
|
|
||||||
previewRect.localEulerAngles = Vector3.zero;
|
previewRect.localEulerAngles = Vector3.zero;
|
||||||
|
|
||||||
Debug.Log($"[ScanQRcode] 预览调整 - 相机: {cameraWidth}x{cameraHeight}, 预览: {previewWidth}x{previewHeight}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Texture2D previewTexture;
|
private Texture2D previewTexture;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 从相机画面裁切并旋转,生成预览画面
|
/// 从相机画面裁切并旋转,生成预览画面
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void CutAndRotateCameraFrame(int camW, int camH, int targetW, int targetH)
|
private void CutAndRotateCameraFrame(int camW, int camH, int targetW, int targetH)
|
||||||
{
|
{
|
||||||
// 相机横向 1920x1080,需要顺时针旋转90度变成 1080x1920
|
int cropY = (camW - targetH) / 2;
|
||||||
// 预览框横向 1080x680
|
|
||||||
// 从旋转后的 1080x1920 中截取中间 1080x680 区域
|
|
||||||
|
|
||||||
// 计算裁切区域(在原始相机画面上的坐标)
|
|
||||||
// 旋转后高度是 camW(1920),需要截取 targetH(680) 高度
|
|
||||||
// 从中间截取:起始Y = (1920 - 680) / 2 = 620
|
|
||||||
int cropY = (camW - targetH) / 2; // 620
|
|
||||||
|
|
||||||
// 获取相机像素数据
|
|
||||||
Color32[] cameraPixels = webCamTexture.GetPixels32();
|
Color32[] cameraPixels = webCamTexture.GetPixels32();
|
||||||
Color32[] targetPixels = new Color32[targetW * targetH];
|
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 y = 0; y < targetH; y++)
|
||||||
{
|
{
|
||||||
for (int x = 0; x < targetW; x++)
|
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)
|
if (srcX >= 0 && srcX < camW && srcY >= 0 && srcY < camH)
|
||||||
{
|
{
|
||||||
int srcIndex = srcY * camW + srcX;
|
int srcIndex = srcY * camW + srcX;
|
||||||
@ -299,48 +290,17 @@ namespace Kill.UI.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置像素到预览纹理
|
|
||||||
previewTexture.SetPixels32(targetPixels);
|
previewTexture.SetPixels32(targetPixels);
|
||||||
previewTexture.Apply();
|
previewTexture.Apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
[SerializeField] private float previewUpdateInterval = 0.03f;
|
||||||
/// 自动对焦维护协程
|
|
||||||
/// iOS: 走原生插件周期确保连续对焦模式不被重置
|
|
||||||
/// 其他平台: 使用 WebCamTexture.autoFocusPoint 定期设置对焦点
|
|
||||||
/// </summary>
|
|
||||||
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; // 预览更新间隔(20fps)
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 预览更新协程 - 降低更新频率避免卡顿
|
|
||||||
/// </summary>
|
|
||||||
private IEnumerator PreviewUpdateCoroutine()
|
private IEnumerator PreviewUpdateCoroutine()
|
||||||
{
|
{
|
||||||
while (isScanning && webCamTexture != null && webCamTexture.isPlaying)
|
while (isScanning && webCamTexture != null && webCamTexture.isPlaying)
|
||||||
{
|
{
|
||||||
// 更新预览画面(限制频率)
|
|
||||||
if (previewTexture != null)
|
if (previewTexture != null)
|
||||||
{
|
{
|
||||||
CutAndRotateCameraFrame(webCamTexture.width, webCamTexture.height, previewTexture.width, previewTexture.height);
|
CutAndRotateCameraFrame(webCamTexture.width, webCamTexture.height, previewTexture.width, previewTexture.height);
|
||||||
@ -350,16 +310,14 @@ namespace Kill.UI.Pages
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 扫描协程
|
/// 扫描协程(ZXing 解码)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private IEnumerator ScanCoroutine()
|
private IEnumerator ScanCoroutine()
|
||||||
{
|
{
|
||||||
// 等待一帧确保摄像头已准备好
|
|
||||||
yield return null;
|
yield return null;
|
||||||
|
|
||||||
while (isScanning && webCamTexture != null && webCamTexture.isPlaying)
|
while (isScanning && webCamTexture != null && webCamTexture.isPlaying)
|
||||||
{
|
{
|
||||||
// 避免同时处理多帧
|
|
||||||
if (isProcessing)
|
if (isProcessing)
|
||||||
{
|
{
|
||||||
yield return null;
|
yield return null;
|
||||||
@ -368,62 +326,43 @@ namespace Kill.UI.Pages
|
|||||||
|
|
||||||
isProcessing = true;
|
isProcessing = true;
|
||||||
|
|
||||||
// 使用预览纹理进行扫描
|
|
||||||
if (previewTexture != null)
|
if (previewTexture != null)
|
||||||
{
|
{
|
||||||
// 异步解析二维码以避免卡顿
|
Color32[] pixels = previewTexture.GetPixels32();
|
||||||
if (useAsyncScan)
|
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 必须在主线程调用)
|
rgbBytes[i * 3] = pixels[i].r;
|
||||||
Color32[] pixels = previewTexture.GetPixels32();
|
rgbBytes[i * 3 + 1] = pixels[i].g;
|
||||||
int texWidth = previewTexture.width;
|
rgbBytes[i * 3 + 2] = pixels[i].b;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 处理扫描结果
|
/// 从原始字节数据解析二维码(后台线程安全)
|
||||||
/// </summary>
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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;
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogError($"二维码解析失败: {ex.Message}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 从原始字节数据解析二维码(不依赖 Unity API,可在后台线程调用)
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private string DecodeQRCodeFromBytes(byte[] rgbBytes, int width, int height)
|
private string DecodeQRCodeFromBytes(byte[] rgbBytes, int width, int height)
|
||||||
{
|
{
|
||||||
@ -569,74 +382,14 @@ namespace Kill.UI.Pages
|
|||||||
string result = TryDecodeWithBinarizer(luminanceSource);
|
string result = TryDecodeWithBinarizer(luminanceSource);
|
||||||
if (!string.IsNullOrEmpty(result))
|
if (!string.IsNullOrEmpty(result))
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Debug.LogError($"二维码解析失败: {ex.Message}");
|
Debug.LogError($"二维码解析失败: {ex.Message}");
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
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[] 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 尝试使用不同的二值化算法解码
|
/// 尝试使用不同的二值化算法解码
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -647,10 +400,9 @@ namespace Kill.UI.Pages
|
|||||||
{ ZXing.DecodeHintType.TRY_HARDER, true },
|
{ ZXing.DecodeHintType.TRY_HARDER, true },
|
||||||
{ ZXing.DecodeHintType.POSSIBLE_FORMATS, new List<ZXing.BarcodeFormat> { ZXing.BarcodeFormat.QR_CODE } }
|
{ ZXing.DecodeHintType.POSSIBLE_FORMATS, new List<ZXing.BarcodeFormat> { ZXing.BarcodeFormat.QR_CODE } }
|
||||||
};
|
};
|
||||||
|
|
||||||
var reader = new ZXing.QrCode.QRCodeReader();
|
var reader = new ZXing.QrCode.QRCodeReader();
|
||||||
|
|
||||||
// 尝试 HybridBinarizer
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var binarizer = new ZXing.Common.HybridBinarizer(luminanceSource);
|
var binarizer = new ZXing.Common.HybridBinarizer(luminanceSource);
|
||||||
@ -660,8 +412,7 @@ namespace Kill.UI.Pages
|
|||||||
return result.Text;
|
return result.Text;
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
|
|
||||||
// 尝试 GlobalHistogramBinarizer
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var binarizer = new ZXing.Common.GlobalHistogramBinarizer(luminanceSource);
|
var binarizer = new ZXing.Common.GlobalHistogramBinarizer(luminanceSource);
|
||||||
@ -671,146 +422,91 @@ namespace Kill.UI.Pages
|
|||||||
return result.Text;
|
return result.Text;
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
#endif // !UNITY_IOS || UNITY_EDITOR
|
||||||
|
|
||||||
|
// ──── 公共处理方法 ────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 应用锐化滤镜(拉普拉斯算子)
|
/// 处理扫描结果
|
||||||
/// 增强图像边缘,提高模糊二维码识别率
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private byte[] ApplySharpenFilter(byte[] input, int width, int height)
|
private void ProcessScanResult(string result)
|
||||||
{
|
{
|
||||||
byte[] output = new byte[input.Length];
|
Debug.Log($"[ScanQRcode] 扫描结果: {result}");
|
||||||
|
|
||||||
// 拉普拉斯锐化核
|
if (scanType == ScanType.ConnectDevice)
|
||||||
// 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++)
|
|
||||||
{
|
{
|
||||||
for (int x = 0; x < width; x++)
|
string macAddress = ExtractMacAddress(result);
|
||||||
|
if (!string.IsNullOrEmpty(macAddress))
|
||||||
{
|
{
|
||||||
int sum = 0;
|
OnQRCodeScanned?.Invoke(macAddress);
|
||||||
|
OnQRCodeScanned = null;
|
||||||
// 应用卷积核
|
}
|
||||||
for (int ky = -halfKernel; ky <= halfKernel; ky++)
|
else
|
||||||
{
|
{
|
||||||
for (int kx = -halfKernel; kx <= halfKernel; kx++)
|
ToastUI.Show("100084");
|
||||||
{
|
}
|
||||||
int py = Mathf.Clamp(y + ky, 0, height - 1);
|
}
|
||||||
int px = Mathf.Clamp(x + kx, 0, width - 1);
|
else
|
||||||
|
{
|
||||||
int pixelIndex = py * width + px;
|
if (!string.IsNullOrEmpty(result))
|
||||||
int kernelIndex = (ky + halfKernel) * kernelSize + (kx + halfKernel);
|
{
|
||||||
|
OnQRCodeScanned?.Invoke(result);
|
||||||
sum += input[pixelIndex] * kernel[kernelIndex];
|
}
|
||||||
}
|
else
|
||||||
}
|
{
|
||||||
|
ToastUI.Show("100084");
|
||||||
// 限制在0-255范围内
|
|
||||||
output[y * width + x] = (byte)Mathf.Clamp(sum, 0, 255);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 从字符串中提取 MAC 地址
|
/// 从字符串中提取 MAC 地址
|
||||||
/// 支持格式:
|
|
||||||
/// 1. 带冒号格式:98:EA:A0:02:4E:06
|
|
||||||
/// 2. 不带冒号格式:98eaa002658e
|
|
||||||
/// 3. 从键值对格式提取:Name:xxx;MAC:98eaa002658e
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private string ExtractMacAddress(string input)
|
private string ExtractMacAddress(string input)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(input))
|
if (string.IsNullOrEmpty(input))
|
||||||
return null;
|
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
|
var kvMatch = macKeyValueRegex.Match(input);
|
||||||
// 匹配 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)
|
if (kvMatch.Success)
|
||||||
{
|
return FormatMacAddress(kvMatch.Groups[1].Value);
|
||||||
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))
|
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);
|
||||||
return FormatMacAddress(macAddress);
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将 MAC 地址统一格式化为带冒号的大写格式
|
/// 格式化 MAC 地址为 XX:XX:XX:XX:XX:XX
|
||||||
/// 输入: 98eaa002658e 或 98:EA:A0:02:65:8E
|
|
||||||
/// 输出: 98:EA:A0:02:65:8E
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private string FormatMacAddress(string mac)
|
private string FormatMacAddress(string mac)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(mac))
|
if (string.IsNullOrEmpty(mac))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// 移除所有冒号
|
|
||||||
string cleanMac = mac.Replace(":", "").Replace("-", "").ToUpper();
|
string cleanMac = mac.Replace(":", "").Replace("-", "").ToUpper();
|
||||||
|
|
||||||
// 检查长度是否为 12
|
|
||||||
if (cleanMac.Length != 12)
|
if (cleanMac.Length != 12)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// 格式化为 XX:XX:XX:XX:XX:XX
|
|
||||||
return string.Format("{0}:{1}:{2}:{3}:{4}:{5}",
|
return string.Format("{0}:{1}:{2}:{3}:{4}:{5}",
|
||||||
cleanMac.Substring(0, 2),
|
cleanMac.Substring(0, 2), cleanMac.Substring(2, 2),
|
||||||
cleanMac.Substring(2, 2),
|
cleanMac.Substring(4, 2), cleanMac.Substring(6, 2),
|
||||||
cleanMac.Substring(4, 2),
|
cleanMac.Substring(8, 2), cleanMac.Substring(10, 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()
|
void OnDestroy()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user