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?)