美文网首页iOS图形处理相关ios进阶视频篇
iOS-AVFoundation自定义相机详解

iOS-AVFoundation自定义相机详解

作者: cdcyd | 来源:发表于2016-08-30 15:18 被阅读14171次
    AVFoundation 中关于视频主要的类
    • 目录

      • 相机基本实现步骤
      • 捕捉会话——AVCaptureSession
      • 捕捉输入——AVCaptureDeviceInput
      • 捕捉预览——AVCaptureVideoPreviewLayer/OpenGL ES
      • 捕捉连接——AVCaptureConnection
      • 拍照——AVCaptureStillImageOutput
      • 音频——AVCaptureAudioDataOutput
      • 视频——AVCaptureVideoDataOutput
      • 生成视频文件——AVAssetWriter、AVAssetWriterInput
      • 写入相册——ALAssetsLibrary、PHPhotoLibrary
      • 操作相机
        • 转换摄像头
        • 补光
        • 闪光灯
        • 聚焦
        • 曝光
        • 自动聚焦曝光
      • 视频重力——Video gravity
      • 方向问题——Orientation
      • 项目地址
    • 相机实现步骤,下面对每一会对每一步需要做的事情详解

      1.创建session(捕捉会话)
      2.创建device input(捕捉设备输入)
      3.预览view
      4.创建capture output(捕捉的输出)
      5.拍照、录视频(元数据转成图片或文件)

    • 捕捉会话——AVCaptureSession
      AVCaptureSession(捕捉会话管理):它从物理设备得到数据流(比如摄像头和麦克风),输出到一个或多个目的地,它可以通过会话预设值(session preset),来控制捕捉数据的格式和质量
      下面是创建一个 session 的代码:

      AVCaptureSession *captureSession = [[AVCaptureSession alloc]init];
      [captureSession setSessionPreset:AVCaptureSessionPresetPhoto];
      

      SessionPreset在iOS中大概有11个

      NSString *const AVCaptureSessionPresetPhoto;
      NSString *const AVCaptureSessionPresetHigh;
      NSString *const AVCaptureSessionPresetMedium;
      NSString *const AVCaptureSessionPresetLow;
      NSString *const AVCaptureSessionPreset352x288;
      NSString *const AVCaptureSessionPreset640x480;
      NSString *const AVCaptureSessionPreset1280x720;
      NSString *const AVCaptureSessionPreset1920x1080;
      NSString *const AVCaptureSessionPresetiFrame960x540;
      NSString *const AVCaptureSessionPresetiFrame1280x720;
      NSString *const AVCaptureSessionPresetInputPriority;

      第一个代表高像素图片输出;接下来三种为相对预设(low, medium, high),这些预设的编码配置会因设备不同而不同,如果选择high,那么你选定的相机会提供给你该设备所能支持的最高画质;再后面就是特定分辨率的预设(352x288 VGA, 1920x1080 VGA, 1280x720 VGA, 640x480 VGA, 960x540 iFrame, 1280x720 iFrame);最后一个代表 capture session 不去控制音频与视频输出设置,而是通过已连接的捕获设备的 activeFormat 来反过来控制 capture session 的输出质量等级

      注意:所有对 capture session 的调用都是阻塞的,因此建议将它们分配到后台串行队列中,不过这里为了简单,不考虑性能,所以省略了dispatch queue

    • 捕捉输入——AVCaptureDeviceInput
      AVCaptureDeviceInput(捕捉设备):它实际上是为摄像头和麦克风等物理设备定义的接口,我们可以通过它来访问或控制这些硬件设备。比如控制摄像头的对焦、曝光等。

        /**
          该方法会返回当前能够输入视频的全部设备,包括前后摄像头和外接设备
          NSArray *videoDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
      
          该方法会返回当前能够输入音频的全部设备
          NSArray *videoDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
          */
      
        // 获取视频输入设备,该方法默认返回iPhone的后置摄像头
        AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
        // 将捕捉设备加入到捕捉会话中
        AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:error];
        if (videoInput) {
            if ([_captureSession canAddInput:videoInput]){
                [_captureSession addInput:videoInput];
            }
        }
      
        // 音频输入
        AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
        AVCaptureDeviceInput *audioIn = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:error];
        if ([_captureSession canAddInput:audioIn]){
            [_captureSession addInput:audioIn];
        }
      
    • 捕捉预览——AVCaptureVideoPreviewLayer/OpenGL ES
      AVCaptureVideoPreviewLayer(捕捉预览):它是CALayer的子类,可被用于自动显示相机产生的实时图像。previewLayer支持视频重力概念,可以控制视频内容渲染的缩放和拉效果(关于视频重力,将在后面进行详解)

      // 创建一个previewLayer
      AVCaptureVideoPreviewLayer  *previewLayer = [[AVCaptureVideoPreviewLayer alloc] initpWithFrame:self.view.bounds]
      [previewLayer.layer setVideoGravity:AVLayerVideoGravityResizeAspect];
      [previewLayer.layer setSession:session];
      
      // 将屏幕坐标系的点转换为previewLayer坐标系的点
      - (CGPoint)captureDevicePointForPoint:(CGPoint)point {                      
        return [previewLayer.layer captureDevicePointOfInterestForPoint:point];
       }
      

      注意:

      1. 它看起来有点像输出,但其实不是,它仅用来预览摄像头捕捉的画面。真正用于输出的是AVCaptureSession(previewLayer拥有session,session拥有outputs);
      2. 它的坐标系和屏幕的坐标系不同,如果点击某区域实现对焦时,我们需要将设备的坐标系转换为实时预览图的坐标;
      3. 它的坐标原点永远都在右上角,这和我们手机的坐标系不同,手机坐标系的原点是不变的。因此拍照或录制视频时,要先得到设备方向(关于方向问题,后面会详解),计算输出的旋转角度。

      捕捉预览除了用AVCaptureVideoPreviewLayer外,还可以用OpenGL ES绘制,我们可以从输出数据流捕捉单一的图像帧,并使用 OpenGL ES手动地把它们显示在 view 上。如果我们想对预览视图进行操作,如使用滤镜,我们就必须这样做。这里不做深入研究,下面给出一段简单的实现代码:

       // 创建glview
       EAGLContext *context = [[EAGLContext alloc]initWithAPI:kEAGLRenderingAPIOpenGLES2];
       GLKView *glView = [[GLKView alloc]initWithFrame:self.view.bounds context:context];
       [EAGLContext setCurrentContext:context];
       [self.view addSubview:glView];
       glView.transform = CGAffineTransformMakeRotation(M_PI_2);
       glView.frame = [UIApplication sharedApplication].keyWindow.bounds;
      
       // 在视频输出函数中绘制出来
      -(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection{
       if (_glview.context != [EAGLContext currentContext]) {
           [EAGLContext setCurrentContext:_glview.context];
       }
       CVImageBufferRef imageRef = CMSampleBufferGetImageBuffer(sampleBuffer);
       CIImage *image = [CIImage imageWithCVImageBuffer:imageRef];
       [_glview bindDrawable];
       [_cicontext drawImage:image inRect:image.extent fromRect:image.extent];
       [_glview display];
      }
      
    • 捕捉连接——AVCaptureConnection
      捕捉连接负责将捕捉会话接收的媒体类型和输出连接起来,比如AVCaptureAudioDataOutput可以接受音频数据,AVCaptureVideoDataOutput可以接受视频数据。会话通过捕捉连接,确定哪些输入视频,那些输入音频。通过对捕捉连接的访问,可以对信号流进行底层控制,比如禁用某些特定的连接。

        // 设置视频捕捉连接
        _videoConnection = [videoOutput connectionWithMediaType:AVMediaTypeVideo];
        _videoConnection.videoOrientation = self.referenceOrientation;
        // 在视频元数据的输出函数中,如果捕捉连接是视频连接,则写入视频数据
        if (connection == _videoConnection){
            if ([self inputsReadyToRecord]){
                [self writeSampleBuffer:sampleBuffer ofType:AVMediaTypeVideo];
            }
        }
      
        // 设置音频捕捉连接
        _audioConnection = [audioOut connectionWithMediaType:AVMediaTypeAudio];
        // 在视频元数据的输出函数中,如果捕捉连接是音频连接,则写入音频数据
        if (connection == _audioConnection){
            if (_readyToRecordVideo && _readyToRecordAudio){
                [self writeSampleBuffer:sampleBuffer ofType:AVMediaTypeAudio];
            }
        }
      
    • 拍照——AVCaptureStillImageOutput
      AVCaptureStillImageOutput会为我们捕捉高分辨率的图像,起设置如下:

        // 创建image output 代码
        AVCaptureStillImageOutput *imageOutput = [[AVCaptureStillImageOutput alloc] init];            
        imageOutput.outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG};
        if ([_captureSession canAddOutput:imageOutput]) {
            [_captureSession addOutput:imageOutput];
            _imageOutput = imageOutput;
        }
      
        // 输出图片
        AVCaptureConnection *connection = [_imageOutput connectionWithMediaType:AVMediaTypeVideo];
        if (connection.isVideoOrientationSupported) {
            connection.videoOrientation = [self currentVideoOrientation];
        }
        id takePictureSuccess = ^(CMSampleBufferRef sampleBuffer,NSError *error){
            if (sampleBuffer == NULL) {
                [self showError:error];
                return ;
            }
            NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:sampleBuffer];
            UIImage *image = [[UIImage alloc]initWithData:imageData];
        };
        [_imageOutput captureStillImageAsynchronouslyFromConnection:connection completionHandler:takePictureSuccess];
      
      
    • 音频——AVCaptureAudioDataOutput
      AVCaptureAudioDataOutput(音频数据输出):它输出硬件实时捕捉的音频数字样本,还有一个音频输出类是AVCaptureAudioFileOutput,不过它只能在录制完成后输出完整的音频文件。

        // 音频输出
        AVCaptureAudioDataOutput *audioOut = [[AVCaptureAudioDataOutput alloc] init];
        [audioOut setSampleBufferDelegate:self queue:captureQueue];
        if ([_captureSession canAddOutput:audioOut]){
            [_captureSession addOutput:audioOut];
        }
      
    • 视频——AVCaptureVideoDataOutput
      AVCaptureVideoDataOutput(视频数据输出):它输出硬件实时捕捉的视频数字样本,还有一个音频和视频输出类是AVCaptureMovieFileOutput,不过它只能在录制完成后输出完整的视频和音频文件。

        // 视频输出
        AVCaptureVideoDataOutput *videoOut = [[AVCaptureVideoDataOutput alloc] init];
        [videoOut setAlwaysDiscardsLateVideoFrames:YES];
        [videoOut setVideoSettings:@{(id)kCVPixelBufferPixelFormatTypeKey : [NSNumber numberWithInt:kCVPixelFormatType_32BGRA]}];
        [videoOut setSampleBufferDelegate:self queue:captureQueue];
        if ([_captureSession canAddOutput:videoOut]){
            [_captureSession addOutput:videoOut];
            _videoOutput = videoOut;
        }
      
    • 生成视频文件——AVAssetWriter、AVAssetWriterInput
      AVAssetWriter:用于对媒体资源进行编码并讲其写入到容器文件中,比如一个QuickTime文件。
      AVAssetWriterInput:用于处理指定的媒体类型,比如音频和视频。
      AVAssetWriterInputPixelBufferAdaptor:这个类在生成视频文件时提供最优性能,不过Demo没有使用该类,有兴趣的可以去研究一下

      // 初始化一个assetWriter
      NSError *error;
      _assetWriter = [[AVAssetWriter alloc] initWithURL:_movieURL fileType:AVFileTypeQuickTimeMovie error:&error];
      if (error){
         [self showError:error];
      }
      
      // 配置视频源数据输入
      - (BOOL)setupAssetWriterVideoInput:(CMFormatDescriptionRef)currentFormatDescription
      {
        CGFloat bitsPerPixel;
        CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(currentFormatDescription);
        NSUInteger numPixels = dimensions.width * dimensions.height;
        NSUInteger bitsPerSecond;
        
        if (numPixels < (640 * 480)){
            bitsPerPixel = 4.05;
        }
        else{
            bitsPerPixel = 11.4;
        }
        
        bitsPerSecond = numPixels * bitsPerPixel;
        NSDictionary *videoCompressionSettings = @{AVVideoCodecKey  : AVVideoCodecH264,
                                                   AVVideoWidthKey  : [NSNumber numberWithInteger:dimensions.width],
                                                   AVVideoHeightKey : [NSNumber numberWithInteger:dimensions.height],
                                                   AVVideoCompressionPropertiesKey:@{AVVideoAverageBitRateKey:[NSNumber numberWithInteger:bitsPerSecond],
                                                                                     AVVideoMaxKeyFrameIntervalKey:[NSNumber numberWithInteger:30]}
                                                   };
        if ([_assetWriter canApplyOutputSettings:videoCompressionSettings forMediaType:AVMediaTypeVideo])
        {
            _assetVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoCompressionSettings];
            _assetVideoInput.expectsMediaDataInRealTime = YES;
            _assetVideoInput.transform = [self transformFromCurrentVideoOrientationToOrientation:self.referenceOrientation];
            if ([_assetWriter canAddInput:_assetVideoInput]){
                [_assetWriter addInput:_assetVideoInput];
            }
            else{
                [self showError:_assetWriter.error];
                return NO;
            }
        }
        else{
            [self showError:_assetWriter.error];
            return NO;
        }
        return YES;
      }
      
      // 配置音频源数据输入
      - (BOOL)setupAssetWriterAudioInput:(CMFormatDescriptionRef)currentFormatDescription
      {
        size_t aclSize = 0;
        const AudioStreamBasicDescription *currentASBD = CMAudioFormatDescriptionGetStreamBasicDescription(currentFormatDescription);
        const AudioChannelLayout *currentChannelLayout = CMAudioFormatDescriptionGetChannelLayout(currentFormatDescription, &aclSize);
        
        NSData *currentChannelLayoutData = nil;
        if (currentChannelLayout && aclSize > 0 ){
            currentChannelLayoutData = [NSData dataWithBytes:currentChannelLayout length:aclSize];
        }
        else{
            currentChannelLayoutData = [NSData data];
        }
            
        NSDictionary *audioCompressionSettings = @{AVFormatIDKey : [NSNumber numberWithInteger:kAudioFormatMPEG4AAC],
                                                   AVSampleRateKey : [NSNumber numberWithFloat:currentASBD->mSampleRate],
                                                   AVEncoderBitRatePerChannelKey : [NSNumber numberWithInt:64000],
                                                   AVNumberOfChannelsKey : [NSNumber numberWithInteger:currentASBD->mChannelsPerFrame],
                                                   AVChannelLayoutKey : currentChannelLayoutData};
        
        if ([_assetWriter canApplyOutputSettings:audioCompressionSettings forMediaType:AVMediaTypeAudio])
        {
            _assetAudioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:audioCompressionSettings];
            _assetAudioInput.expectsMediaDataInRealTime = YES;
            
            if ([_assetWriter canAddInput:_assetAudioInput]){
                [_assetWriter addInput:_assetAudioInput];
            }
            else{
                [self showError:_assetWriter.error];
                return NO;
            }
        }
        else{
            [self showError:_assetWriter.error];
            return NO;
        }
        
        return YES;
      }
      

      通过上面的代码,我们就准备好了一个AVAssetWriter了,就可以用它来生产视频文件,我们可以在视频源数据输出函数中写入

      - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection{
      if (_recording) {
           CFRetain(sampleBuffer);
           dispatch_async(_movieWritingQueue, ^{
               if (_assetWriter)
               {
                   if (connection == _videoConnection)
                   {
                       if (!_readyToRecordVideo){
                           _readyToRecordVideo = [self setupAssetWriterVideoInput:CMSampleBufferGetFormatDescription(sampleBuffer)];
                       }
                       if ([self inputsReadyToRecord]){
                           [self writeSampleBuffer:sampleBuffer ofType:AVMediaTypeVideo];
                       }
                   }
                   else if (connection == _audioConnection){
                       if (!_readyToRecordAudio){
                           _readyToRecordAudio = [self setupAssetWriterAudioInput:CMSampleBufferGetFormatDescription(sampleBuffer)];
                       }
                       if ([self inputsReadyToRecord]){
                           [self writeSampleBuffer:sampleBuffer ofType:AVMediaTypeAudio];
                       }
                   }
               }
               CFRelease(sampleBuffer);
           });
       }
      }
      
    • 写入相册——ALAssetsLibrary、PHPhotoLibrary
      iOS9.0以前:

        ALAssetsLibrary *lab = [[ALAssetsLibrary alloc]init];
        // 保存视频
        [lab writeVideoAtPathToSavedPhotosAlbum:_movieURL completionBlock:^(NSURL *assetURL, NSError *error) {
             if (error) {
                 [self showError:error];
             }
         }];
      

      iOS9.0以后

      [PHPhotoLibrary requestAuthorization:^( PHAuthorizationStatus status ) {
           if (status == PHAuthorizationStatusAuthorized) {
               [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
                   // 保存视频
                   PHAssetCreationRequest *videoRequest = [PHAssetCreationRequest creationRequestForAsset];
                   [videoRequest addResourceWithType:PHAssetResourceTypeVideo fileURL:_movieURL options:nil];           
                } completionHandler:^( BOOL success, NSError * _Nullable error ) {
                   if (!success) {
                       [self showError:error];
                   }
               }];
           }
       }];
      
    • 操作相机

      相机的操作都是一些固定的代码,我就不多讲了,我们只需要注意以下几点:
      1.闪光灯和手电筒不能同时开启
      2.在前置摄像头时不能开启手电筒,所有在转换时,会被强制关闭
      3.前后摄像头需要分别设置闪光灯的开关,所以我们必须记录当前闪光灯的设置状态,在转换完成之后,还需要重新设置一次
      4.在转换摄像头时,你之前设置的视频输出就无效了,你需要删除原来的视频输出,再重新添加一个新的视频输出(我也不知道为什么会有这种情况,但是音频源数据是一直都有的,视频源数据每次转换摄像头都需要重新设置视频输出)

      • 转换摄像头
      - (BOOL)switchCameras{
        NSError *error;
        AVCaptureDevice *videoDevice = [self inactiveCamera];
        AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
        if (videoInput) {
            AVCaptureFlashMode flashMode = [self flashMode];
      
            // 转换摄像头
            [_session beginConfiguration];
            [_session removeInput:_deviceInput];
            if ([_session canAddInput:videoInput]) {
                CATransition *animation = [CATransition animation];
                animation.type = @"oglFlip";
                animation.subtype = kCATransitionFromLeft;
                animation.duration = 0.5;
                [self.cameraView.previewView.layer addAnimation:animation forKey:@"flip"];
                [_session addInput:videoInput];
                _deviceInput = videoInput;
            } else {
                [_session addInput:_deviceInput];
            }
            [_session commitConfiguration];
      
            // 完成后需要重新设置视频输出链接
            _videoConnection = [_videoOutput connectionWithMediaType:AVMediaTypeVideo];
      
            // 如果后置转前置,系统会自动关闭手电筒,如果之前打开的,需要更新UI
            if (videoDevice.position == AVCaptureDevicePositionFront) {
                [self.cameraView changeTorch:NO];
            }
      
            // 前后摄像头的闪光灯不是同步的,所以在转换摄像头后需要重新设置闪光灯
            [self changeFlash:flashMode];
      
            return nil;
        }
        return error;
      }
      
      • 补光
        AVCaptureDevice *device = [self activeCamera];
        if (device.torchMode != torchMode && [device isTorchModeSupported:torchMode]) {
            NSError *error;
            if ([device lockForConfiguration:&error]) {
                device.torchMode = torchMode;
                [device unlockForConfiguration];
            } 
            else{
                [self showError:error];
            }
        }
      
      • 闪光灯
        AVCaptureDevice *device = [self activeCamera];
        if (device.flashMode != flashMode && [device isFlashModeSupported:flashMode]) {
            NSError *error;
            if ([device lockForConfiguration:&error]) {
                device.flashMode = flashMode;
                [device unlockForConfiguration];
            } 
            else{
                [self showError:error];
            }
        }
      
      • 聚焦
      - (void)focusAtPoint:(CGPoint)point {                                       
        AVCaptureDevice *device = [self activeCamera];
        if ([self cameraSupportsTapToFocus] && [device isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {
            NSError *error;
            if ([device lockForConfiguration:&error]) {                         
                device.focusPointOfInterest = point;
                device.focusMode = AVCaptureFocusModeAutoFocus;
                [device unlockForConfiguration];
            } 
            else{
                [self showError:error];
            }
        }
      }
      
      • 曝光
      static const NSString *CameraAdjustingExposureContext;
      - (void)exposeAtPoint:(CGPoint)point{
        AVCaptureDevice *device = [self activeCamera];
        if ([self cameraSupportsTapToExpose] && [device isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) {
            NSError *error;
            if ([device lockForConfiguration:&error]) {                         
                device.exposurePointOfInterest = point;
                device.exposureMode = AVCaptureExposureModeContinuousAutoExposure;
                if ([device isExposureModeSupported:AVCaptureExposureModeLocked]) {
                    [device addObserver:self                                    
                             forKeyPath:@"adjustingExposure"
                                options:NSKeyValueObservingOptionNew
                                context:&CameraAdjustingExposureContext];
                }
                [device unlockForConfiguration];
            } 
            else{
                [self showError:error];
            }
        }
      }
      
      - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
        if (context == &CameraAdjustingExposureContext) {                     
            AVCaptureDevice *device = (AVCaptureDevice *)object;
            if (!device.isAdjustingExposure && [device isExposureModeSupported:AVCaptureExposureModeLocked]) {
                [object removeObserver:self                                     
                            forKeyPath:@"adjustingExposure"
                               context:&CameraAdjustingExposureContext];
                dispatch_async(dispatch_get_main_queue(), ^{                    
                    NSError *error;
                    if ([device lockForConfiguration:&error]) {
                        device.exposureMode = AVCaptureExposureModeLocked;
                        [device unlockForConfiguration];
                    } 
                    else{
                        [self showError:error];
                    }
                });
            }
        } 
        else{
            [super observeValueForKeyPath:keyPath
                                 ofObject:object
                                   change:change
                                  context:context];
        }
      }
      
      • 自动聚焦/曝光
      - (BOOL)resetFocusAndExposureModes{
        AVCaptureDevice *device = [self activeCamera];
        AVCaptureExposureMode exposureMode = AVCaptureExposureModeContinuousAutoExposure;
        AVCaptureFocusMode focusMode = AVCaptureFocusModeContinuousAutoFocus;
        BOOL canResetFocus = [device isFocusPointOfInterestSupported] && [device isFocusModeSupported:focusMode];
        BOOL canResetExposure = [device isExposurePointOfInterestSupported] && [device isExposureModeSupported:exposureMode];
        CGPoint centerPoint = CGPointMake(0.5f, 0.5f);                          
        NSError *error;
        if ([device lockForConfiguration:&error]) {
            if (canResetFocus) {                                                
                device.focusMode = focusMode;
                device.focusPointOfInterest = centerPoint;
            }
            if (canResetExposure) {                                             
                device.exposureMode = exposureMode;
                device.exposurePointOfInterest = centerPoint;
            }
            [device unlockForConfiguration];
            return YES;
        } 
        else{
            [self showError:error];
            return NO;
        }
      }
      
    • 视频重力——Video gravity

      视频重力:控制视频内容渲染的缩放和拉伸效果。
      举个例子,在我们设置会话时有一个参数session preset,它是用来控制捕捉数据格式和质量了。我的测试机是6s,当我选择参数AVCaptureSessionPresetPhoto时,输出图片大小如下:

      Printing description of image:
      <CIImage: 0x12c7bdad0 extent [0 0 750 1000]>
      affine [1 0 0 -1 0 1000] extent=[0 0 750 1000]
      colormatch "QuickTime 'nclc' Video (1,1,6)"-to-workingspace extent=[0 0 750 1000]
      IOSurface 0x12da00008 BGRA8 extent=[0 0 750 1000]
      

      当我选择参数AVCaptureSessionPresetHigh时,输出图片大小如下:

       Printing description of image:
       <CIImage: 0x15f851680 extent [0 0 1080 1920]>
       affine [1 0 0 -1 0 1920] extent=[0 0 1080 1920]
       colormatch "QuickTime 'nclc' Video (1,1,6)"-to-workingspace extent=[0 0 1080 1920]
       IOSurface 0x15f900008 BGRA8 extent=[0 0 1080 1920]
      

      可以看出选择不同的session preset,会输出不同大小的图片,但是这些图片都是很大的,这么大的图片要显示在手机预览层,必须要缩放,而视频重力其实就是缩放参数。
      AVLayerVideoGravityResizeAspect:在预览层区域内缩放视频,保持视频原始宽高比。这是默认值,同时适用大多数情况。使用该参数预览时,有可能不能铺满整个预览视图
      AVLayerVideoGravityResizeAspectFill:按照视频的宽高比将视频拉伸填满整个图层。使用该参数时,很可能造成视频预览图片被裁剪,而拍摄输出没有被裁剪,这样就会使预览图和最终拍摄的图不一致。
      AVLayerVideoGravityResize:拉伸视频内容以匹配预览层大小,这个是最不常用的,可能造成视频扭曲。

    • 方向问题——Orientation

      设备方向device orientation

      // 设备方向
      UIDevice *device = [UIDevice currentDevice] ;   
      switch (device.orientation) {  
          case UIDeviceOrientationFaceUp:  
          NSLog(@"屏幕朝上平躺");  
              break;  
          case UIDeviceOrientationFaceDown:  
          NSLog(@"屏幕朝下平躺");  
              break;     
          case UIDeviceOrientationUnknown:  
          NSLog(@"未知方向");  
              break;  
          case UIDeviceOrientationLandscapeLeft:  
          NSLog(@"屏幕向左橫置");  
              break;  
          case UIDeviceOrientationLandscapeRight:  
          NSLog(@"屏幕向右橫置");  
              break;  
          case UIDeviceOrientationPortrait:  
          NSLog(@"屏幕直立");  
              break;  
          case UIDeviceOrientationPortraitUpsideDown:  
          NSLog(@"屏幕直立,上下顛倒");  
              break;    
      

      从上面可以看到所有的设备方向,而视频方向videoOrientation没有那么多分类,它分为:
      AVCaptureVideoOrientationPortrait home健在下
      AVCaptureVideoOrientationPortraitUpsideDown home健在上
      AVCaptureVideoOrientationLandscapeRight home健在右
      AVCaptureVideoOrientationLandscapeLeft home健在左
      这些视频方向,是视频或拍照时的输入方向,而我们的数据输出时会跟具这些输入方向自动对图片或视频进行矩阵变换,以达到最佳的用户体验。
      这里以拍照举个例子(视频同理):
      假如你横着手机拍了一张照片,第一次你在拍照前不传入视频方向,它默认为AVCaptureVideoOrientationPortrait,这是正常手机拿着的姿势,所以到输出时不会对图片进行矩阵变换,当你把图片存入相册时,你会发现,你要正确查看这张图,你也需要横着手机看。如果你是倒着手机拍的,就需要倒着手机看。但是如果你在拍照前传入视频方向,比如你横着手机拍,并且home健在右,就传入参数AVCaptureVideoOrientationLandscapeRight,这时你存入相册的照片就可以以正常拿手机的姿势查看它了。

      // 在拍照前通过会话连接,传入当前输入视频方向(视频同理也可以这样做)
      AVCaptureConnection *connection = [_imageOutput connectionWithMediaType:AVMediaTypeVideo];
      if (connection.isVideoOrientationSupported) {
          connection.videoOrientation = [self currentVideoOrientation];
      }
      

      苹果给出的类处理后都是默认正常拿手机的姿势观看,不管是图片还是视频,如果我们想拍出的所有图片或视频都需要横着手机看,我们这时可以不传入视频方向,这样视频到输出时就不会被变换,我们在视频输入类中,手动对视频进行transform变换,这样就可以实现我们想要的查看方式,在本例中,视频就是用的这种处理方式。

      // 视频的播放方向,后面计算视频旋转角度使用
      _referenceOrientation = AVCaptureVideoOrientationPortrait;
      
       // 这行代码在设置视频输入方向为默认输入方向
       _videoConnection.videoOrientation =   AVCaptureVideoOrientationPortrait;
      
       // 视频输入类中手动旋转视频方向
       _assetVideoInput.transform = [self transformFromCurrentVideoOrientationToOrientation:self.referenceOrientation];
      
       // 旋转视频方向函数实现
       -  (CGAffineTransform)transformFromCurrentVideoOrientationToOrientation:(AVCaptureVideoOrientation)orientation
       {
           CGFloat orientationAngleOffset = [self angleOffsetFromPortraitOrientationToOrientation:orientation];
           CGFloat videoOrientationAngleOffset = [self angleOffsetFromPortraitOrientationToOrientation:self.motionManager.videoOrientation];
           CGFloat angleOffset;
           if ([self activeCamera].position == AVCaptureDevicePositionBack) {
               angleOffset = orientationAngleOffset - videoOrientationAngleOffset;
           }
           else{
               angleOffset = videoOrientationAngleOffset - orientationAngleOffset + M_PI_2;
           }
           CGAffineTransform transform = CGAffineTransformMakeRotation(angleOffset);
           return transform;
       }
      
       - (CGFloat)angleOffsetFromPortraitOrientationToOrientation:(AVCaptureVideoOrientation)orientation
       {
       CGFloat angle = 0.0;
       switch (orientation)
       {
           case AVCaptureVideoOrientationPortrait:
               angle = 0.0;
               break;
           case AVCaptureVideoOrientationPortraitUpsideDown:
               angle = M_PI;
               break;
           case AVCaptureVideoOrientationLandscapeRight:
               angle = -M_PI_2;
               break;
           case AVCaptureVideoOrientationLandscapeLeft:
               angle = M_PI_2;
               break;
           default:
               break;
       }
       return angle;
       }
      
    • 项目地址
      https://github.com/cdcyd/CCCamera

    相关文章

      网友评论

      • Sky109:大神,请教你一个问题,原生的扫码如何做到像微信那样曝光补偿的效果呢?也就是暗增亮减,防止扫码反光,我已经试了很多方法都达不到那种效果,麻烦看到后给点建议,谢谢🙏🙏
        cdcyd:可以设置曝光补偿或自定义曝光,可以看下这篇文章 https://www.objccn.io/issue-21-3/
      • koreadragon:兄弟可以的,解决了几个痛点
      • 6d201d703e76:楼主!这里cc 的视频能不能让我看看,,,我也是vip
        cdcyd:@勇敢的新111 什么视频?
      • YY程序猿:在您的基础上改了个相机,代码风格很优雅!!学习了!!
      • _码奴:自定义相机不能直接设置某个属性设置成正方形吗,只能获取到照片后截取吗
        _码奴:@cdcyd 你回复的和我问的好像不是一个问题啊,我想知道能不能通过设置某个属性就获取到指定宽高比例的图片,320*240也不是正方形啊,这只是一个像素清晰度的表示,也不是指图片就是快高320*240啊,我现在是获取到高清图片之后又截取的图片,不知道有没有像录制视频时那样直接设置输出的一个配置属性有可以实现录制的图像是指定大小的
        cdcyd:对的,摄像头获取的照片大小是用这个函数设置的[session setSessionPreset:AVCaptureSessionPreset320x240],这句话意思就是设置获取320X240像素的图片,AVCaptureSessionPreset320x240这个是个枚举类型SessionPreset,在文章的中对11个枚举类型都有解释
      • 问问问你:横屏有问题。
      • yadong:写的很棒,好文章
      • 拉风的胖鱼:请问下,我是这边要求16:9 ,4:3,1:1图片,这个我都是通过剪切实现的,然后其他部分用View挡住,但是我发现最后生成的图片都有点模糊,而且成像也有点模糊,请问是什么原因?
      • 聪zero:预览的图片大小比实际的小很多,为什么?
      • 杰克道长:前置摄像拍出的照片 左右翻转了。
        杰克道长:@cdcyd 可以用AVCaptureConnection中的 .videoMirrored 设置
        cdcyd:@杰克道长 是镜像效果,系统默认就是这样,你可以试一下苹果的相机应用也是这样的
      • 西叶lv:- (void)lightSensitive {

        // 1.获取硬件设备
        AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

        // 2.创建输入流
        AVCaptureDeviceInput *input = [[AVCaptureDeviceInput alloc]initWithDevice:device error:nil];

        // 3.创建设备输出流
        AVCaptureVideoDataOutput *output = [[AVCaptureVideoDataOutput alloc] init];
        [output setSampleBufferDelegate:self queue:dispatch_get_main_queue()];


        // AVCaptureSession属性
        AVCaptureSession *session = [[AVCaptureSession alloc]init];
        // 设置为高质量采集率
        [session setSessionPreset:AVCaptureSessionPresetHigh];
        // 添加会话输入和输出
        if ([session canAddInput:input]) {
        [session addInput:input];
        }
        if ([session canAddOutput:output]) {
        [session addOutput:output];
        }

        // 9.启动会话
        [session startRunning];

        }
        我这样写,AVCaptureSession对象被提前释放了,[session startRunning];无效,只能把AVCaptureSession对象定义成属性,才行.为啥AVCaptureSession对象会被提前释放呢??求解答
        西叶lv:@聪zero 哦哦,必须保证在这个界面下,session回话一直在,是吧
        聪zero:局部变量,方法走完了就释放
      • cjy027:很详细,很需要,谢谢,打赏支持
      • 何康老鬼:厉害 学习了 写的真详细,理解的很深涉及广
      • 819e6e93a666:你好,我设置点击屏幕的时候手动聚焦和手动曝光,那应该什么时候重置为自动曝光和聚焦呢?
      • 90edb4b73d9d:在captureOuput函数中的sampleBuffer为空
      • PPFSaber:受教了 至今看到的最详细的自定义相机分析:smile:
      • 小小小小小小米:苹果7的手机,10.2的系统,会在第一次进入gl渲染之后退出来,再进gl渲染,会崩溃在这一句 [_glview display]; 是什么原因呢 - -
        小小小小小小米:@cdcyd 昨天真机跑得时候没有报错信息就蹦在那句了,回家路上测试又不闪退了 - - 奇了怪了,不好意思。
        cdcyd:我用10.2的系统,没有出现问题,你看它具体报的什么错
      • 4b4d42e9a1bb:题外的iOS9.o应该是写错了吧,很喜欢楼主这种文章,对相机各类函数调用理解深入了
      • 做一个有爱的伸手党:你好 为什么 我用了以后 在我自己的项目里 可以调用方法 但是显示界面一片空白 不会出现图像呢
      • 吴不省:请问下,Demo在切换视频时候为什么会有明显黑边,切换视频输入为什么一定要同时切换输出呢?
      • Sam_Zhou:不错 充实
      • 街角没有咖啡店:这类知识国内网站上非常少,期待LZ更深入的分析、应用文章,加油。
      • brownfeng:非常详细.学习

      本文标题:iOS-AVFoundation自定义相机详解

      本文链接:https://www.haomeiwen.com/subject/yxszsttx.html