iOS-AVFoundation自定义相机详解

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];
      // 在视频元数据的输出函数中,如果捕捉连接是视频连接,则写入视频数据
      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;
      }
    }
    
    • 缩放(factor:缩放因子,这里我们需要使他成指数型增长,rate:缩放速率)
    - (id)zoom:(AVCaptureDevice *)device factor:(CGFloat)factor {
      if (device.activeFormat.videoMaxZoomFactor > factor && factor >= 1.0) {
          NSError *error;
          if ([device lockForConfiguration:&error]) {
              [device rampToVideoZoomFactor:factor withRate:4.0];
              [device unlockForConfiguration];
          }
          return error;
      }
      return [self error:@"不支持的缩放倍数" code:200];
    }
    
  • 视频重力——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 = videoOrientationAngleOffset - orientationAngleOffset + M_PI_2;
         } else {
             angleOffset = orientationAngleOffset - videoOrientationAngleOffset + 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;
     }
     return angle;
     }
    
  • 项目地址
    https://github.com/cdcyd/CCCamera

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容