Context

几天前把扫码登录的服务器总结了一下,由于时间太晚,篇幅太长,而且涉及的内容是两个板块的,所以就没有在那一篇文章中写app端的一些实现。

事实上,app这里并没有简单多少,主要的难点大概就是扫码这里吧~之前虽然是在Alinone中做过扫码的功能,但是当时使用了cocoapods里面的一个库,所以没有怎么深入的理解AVFoundation。这次,我就偏偏不要用开源库,读读官方文档,把AVFoundation的一些功能搞搞明白。

How does it work

好好研究了官方文档一番,我大概明白了一件事儿,整个视频流的工作过程是由一个会话(AVCaptureSession)来控制的,这个会话有输入(AVCaptureInput)和输出(AVCaptureOutput)

当session开始工作的时候,数据就从输入经过会话传到了输出,当然这里说的全都是抽象类,具体到这个app里面的话,输出就是AVCaptureOutput的一个子类AVCaptureMetadataOutput

这玩意儿就是专门用来解析视频流信息,提取其中的metadata的,其中包括BarCode、QRCode等其他code~牛逼的很

Make it

这次没有用什么传说中的设计模式来写这个demo,因为这样会有点儿小题大做的感觉,直接在VC中定义了一切。

@interface MainViewController ()<AVCaptureMetadataOutputObjectsDelegate>
{
    AVCaptureVideoPreviewLayer *previewLayer;
    dispatch_queue_t avProcessQueue;
    IBOutlet UIView *scan_view;
    AVCaptureSession *avCaptureSession;
    AVCaptureDevice *avDevice;
    AVCaptureDeviceInput *avInput;
    AVCaptureMetadataOutput *metaOutput;
    NSMutableArray *capturedList;
}
@property (nonatomic, weak) IBOutlet UIButton *scanCodeButton;

@end

注意这里有个avProcessQueue(dispatch_queue_t),因为视频处理是比较消耗资源和时间的,所以如果让他们在主线程中运行的话,必死,所以要单独搞一个线程出来让它们运行。

AVCaptureVideoPreviewLayer是CALayer的一个子类,可以方方便便的把AVSession里面的内容显示出来,如果没有这个东西的话,就很难想象如何把摄像头捕捉到的东西显示在界面上了~

capturedList是一个用来扫描结果的纪录列表,后面会解释他存在的意义。

Configure AVFoundation Elements

- (void)viewDidLoad {
    [super viewDidLoad];
    // 建立一个新的队列(线程)
    avProcessQueue = dispatch_queue_create( "session queue", DISPATCH_QUEUE_SERIAL );

    // 初始化设备
    AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

    // 初始化Session
    avCaptureSession=[[AVCaptureSession alloc]init];
    [avCaptureSession setSessionPreset:AVCaptureSessionPresetHigh];

    // 初始化Input
    NSError *error;
    avInput=[[AVCaptureDeviceInput alloc]initWithDevice:device error:&error];
    [avCaptureSession addInput:avInput];

    // 初始化Output
    metaOutput =[[AVCaptureMetadataOutput alloc]init];
    [metaOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
    [avCaptureSession addOutput:metaOutput];
    metaOutput.metadataObjectTypes = @[AVMetadataObjectTypeQRCode];

    // 初始化预览layer
    previewLayer=[AVCaptureVideoPreviewLayer layerWithSession:avCaptureSession];
    previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
    [scan_view.layer addSublayer:previewLayer];
    previewLayer.frame=CGRectMake(0, 0, 250, 250);

}

这里有一个超级超级超级大的坑,我找了大半天才知道原因

metaOutput.metadataObjectTypes = @[AVMetadataObjectTypeQRCode];

就是这个玩意儿,无论我怎么写,都会直接crash,说什么output不支持这种识别方式(我都删的只剩下一种了诶,同学!),结果原来是这个扫描类型的设置必须在该output被加入了session才可以设置,不然就会直接挂掉……也就是

[avCaptureSession addOutput:metaOutput];

这个之后才可以有效的设置识别类型

Please Allow Me To Control Your Camera

不过,摄像头是一个比较敏感的东西,涉及到了隐私,所以在某一代系统之后,AVFoundation要获取视频流是需要权限才行的~

switch ( [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo] )
{
    case AVAuthorizationStatusAuthorized:
    {
        // 以前就获取到权限了!
        break;
    }
    case AVAuthorizationStatusNotDetermined:
    {
        // 还不知道有没有获取到权限,我们来问一问
        [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^( BOOL granted ) {
            // 索取权限完毕,处理结果
        }];
        break;
    }
    default:
    {
        // 以前就没有获取到权限!
        break;
    }
}

这里已经注释的很清楚了,直接在适当的位置复制粘贴进去就好啦!

I got it

在前面也看到了

[metaOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];

这一行设置,意思是AVCaptureMetadataOutput扫描到有效的信息之后,会通过代理的形式调用一个回调函数(方法),顺便把扫描结果给返回出来,所以这里我就让我的MainViewController遵从了AVCaptureMetadataOutputObjectsDelegate协议,并且实现了接口需要的方法

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection{
    if (metadataObjects.count>0) {
        for (AVMetadataObject *data in metadataObjects){
            if ([data isKindOfClass:[AVMetadataMachineReadableCodeObject class]]) {
                AVMetadataMachineReadableCodeObject *result=(AVMetadataMachineReadableCodeObject*)data;
                if (![capturedList containsObject:result.stringValue]) {
                    // 这里出现了capturedList!
                    [capturedList addObject:result.stringValue];
                    // 扫描的Raw内容就是这么简单
                    NSLog(@"scanned code is %@",result.stringValue);
                    [NetworkHelper permit:result.stringValue andSucceed:nil andFail:nil];
                }
            }
        }
    }
}

好了,现在可以说明一下,为什么要用capturedList啦~

因为,这个- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection方法是不会中断的,是会在每个周期内都执行的(只要摄像头下有符合要求的Code),

换句话说,就是如果你的摄像头指着一个QRCode,系统就会使劲的调用这个代理方法,假设没有做任何的判断,那么很有可能对一个QRCode的处理会在一瞬间做N多次,在例子中我用到了一个网络请求,如果没有加前面的那个判断的话,服务器可能会哭~(哈哈)

End

好像只要这么多就可以把扫码功能搞定了呢!我当年为啥要偷懒去用开源库呢~

(要不然我也把这个东西整理一下,开个源,骗一些Star?)