2026-07-23 14:44:41 +08:00
|
|
|
|
#import <AVFoundation/AVFoundation.h>
|
|
|
|
|
|
#import <UIKit/UIKit.h>
|
|
|
|
|
|
|
2026-07-23 15:04:51 +08:00
|
|
|
|
@interface NativeCameraController : NSObject <AVCaptureMetadataOutputObjectsDelegate>
|
|
|
|
|
|
@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
|
2026-07-23 14:56:53 +08:00
|
|
|
|
{
|
2026-07-23 15:04:51 +08:00
|
|
|
|
self.gameObjectName = goName;
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
NSLog(@"[NativeCamera] 使用摄像头: %@", device.localizedName);
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
2026-07-23 14:56:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 15:04:51 +08:00
|
|
|
|
// 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"];
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-07-23 14:56:53 +08:00
|
|
|
|
|
2026-07-23 15:04:51 +08:00
|
|
|
|
/// 选择后置主摄(排除超广角/长焦)
|
|
|
|
|
|
- (AVCaptureDevice *)findRearCamera
|
|
|
|
|
|
{
|
2026-07-23 14:56:53 +08:00
|
|
|
|
if (@available(iOS 10.0, *)) {
|
|
|
|
|
|
AVCaptureDeviceDiscoverySession *session = [AVCaptureDeviceDiscoverySession
|
|
|
|
|
|
discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInWideAngleCamera,
|
|
|
|
|
|
AVCaptureDeviceTypeBuiltInTelephotoCamera,
|
|
|
|
|
|
AVCaptureDeviceTypeBuiltInUltraWideCamera]
|
|
|
|
|
|
mediaType:AVMediaTypeVideo
|
2026-07-23 15:04:51 +08:00
|
|
|
|
position:AVCaptureDevicePositionBack];
|
|
|
|
|
|
// 优先宽角(主摄)
|
|
|
|
|
|
for (AVCaptureDevice *d in session.devices) {
|
|
|
|
|
|
if (d.deviceType == AVCaptureDeviceTypeBuiltInWideAngleCamera) {
|
|
|
|
|
|
return d;
|
2026-07-23 14:56:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-23 15:04:51 +08:00
|
|
|
|
// 兜底:返回第一个
|
|
|
|
|
|
return session.devices.firstObject;
|
2026-07-23 14:56:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
return [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 15:04:51 +08:00
|
|
|
|
- (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;
|
|
|
|
|
|
}
|
2026-07-23 14:44:41 +08:00
|
|
|
|
|
2026-07-23 15:04:51 +08:00
|
|
|
|
- (void)sendToUnity:(NSString *)msg
|
|
|
|
|
|
{
|
|
|
|
|
|
if (self.gameObjectName) {
|
|
|
|
|
|
UnitySendMessage([self.gameObjectName UTF8String], "OnNativeScanResult", [msg UTF8String]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-23 14:44:41 +08:00
|
|
|
|
|
2026-07-23 15:04:51 +08:00
|
|
|
|
#pragma mark - AVCaptureMetadataOutputObjectsDelegate
|
2026-07-23 14:44:41 +08:00
|
|
|
|
|
2026-07-23 15:04:51 +08:00
|
|
|
|
- (void)captureOutput:(AVCaptureOutput *)output
|
|
|
|
|
|
didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects
|
|
|
|
|
|
fromConnection:(AVCaptureConnection *)connection
|
|
|
|
|
|
{
|
|
|
|
|
|
if (self.hasScanned) return;
|
|
|
|
|
|
if (metadataObjects.count == 0) return;
|
2026-07-23 14:44:41 +08:00
|
|
|
|
|
2026-07-23 15:04:51 +08:00
|
|
|
|
AVMetadataMachineReadableCodeObject *code = metadataObjects.firstObject;
|
|
|
|
|
|
if (![code isKindOfClass:[AVMetadataMachineReadableCodeObject class]]) return;
|
|
|
|
|
|
if (!code.stringValue) return;
|
2026-07-23 14:44:41 +08:00
|
|
|
|
|
2026-07-23 15:04:51 +08:00
|
|
|
|
self.hasScanned = YES;
|
|
|
|
|
|
NSLog(@"[NativeCamera] 扫描到二维码: %@", code.stringValue);
|
|
|
|
|
|
|
|
|
|
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
|
|
|
|
[self.session stopRunning];
|
|
|
|
|
|
});
|
2026-07-23 14:44:41 +08:00
|
|
|
|
|
2026-07-23 15:04:51 +08:00
|
|
|
|
[self sendToUnity:code.stringValue];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
|
|
// ──── 单例管理 ────
|
|
|
|
|
|
static NativeCameraController *g_cameraController = nil;
|
|
|
|
|
|
|
|
|
|
|
|
extern "C" {
|
|
|
|
|
|
|
|
|
|
|
|
void _StartNativeCamera(const char *gameObjectName)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (g_cameraController) {
|
|
|
|
|
|
[g_cameraController stop];
|
|
|
|
|
|
g_cameraController = nil;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 权限请求
|
|
|
|
|
|
AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
|
|
|
|
|
|
if (status == AVAuthorizationStatusNotDetermined) {
|
|
|
|
|
|
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
|
|
|
|
|
|
if (granted) {
|
|
|
|
|
|
g_cameraController = [[NativeCameraController alloc] init];
|
|
|
|
|
|
NSString *goName = [NSString stringWithUTF8String:gameObjectName];
|
|
|
|
|
|
[g_cameraController startWithName:goName];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
UnitySendMessage((char *)gameObjectName, "OnNativeScanResult", "ERROR:PERMISSION_DENIED");
|
2026-07-23 14:44:41 +08:00
|
|
|
|
}
|
2026-07-23 15:04:51 +08:00
|
|
|
|
}];
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-07-23 14:44:41 +08:00
|
|
|
|
|
2026-07-23 15:04:51 +08:00
|
|
|
|
if (status != AVAuthorizationStatusAuthorized) {
|
|
|
|
|
|
UnitySendMessage((char *)gameObjectName, "OnNativeScanResult", "ERROR:PERMISSION_DENIED");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
g_cameraController = [[NativeCameraController alloc] init];
|
|
|
|
|
|
NSString *goName = [NSString stringWithUTF8String:gameObjectName];
|
|
|
|
|
|
[g_cameraController startWithName:goName];
|
2026-07-23 14:44:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 15:04:51 +08:00
|
|
|
|
void _StopNativeCamera()
|
2026-07-23 14:44:41 +08:00
|
|
|
|
{
|
2026-07-23 15:04:51 +08:00
|
|
|
|
if (g_cameraController) {
|
|
|
|
|
|
[g_cameraController stop];
|
|
|
|
|
|
g_cameraController = nil;
|
|
|
|
|
|
}
|
2026-07-23 14:44:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|