AVFoundation实现小视频录制

模仿微信小视频功能

  • 思路

利用系统AVCaptureSession来实现,添加video和audio的AVCaptureConnection

我们先来先看一下AVCaptureSession的官方文档 有一段important的description,一定要单独起一个线程


AVFoundation实现小视频录制_第1张图片
官方文档是个好东西

初始化AVCaptureSession

self.session = [AVCaptureSession new];
if ([self.session canSetSessionPreset:AVCaptureSessionPresetHigh]) {
    [self.session setSessionPreset:AVCaptureSessionPresetHigh];
}

添加两个摄像头

//摄像头输入
- (AVCaptureDeviceInput *)backCameraInput {
    if (_backCameraInput == nil) {
        NSError *error;
        _backCameraInput = [[AVCaptureDeviceInput alloc] initWithDevice:[self cameroWithPosition:AVCaptureDevicePositionBack] error:&error];
        if (error) {
            NSLog(@"后置摄像头获取失败");
        }
    }
    return _backCameraInput;
}

- (AVCaptureDeviceInput *)frontCameraInput {
    if (_frontCameraInput == nil) {
        NSError *error;
        _frontCameraInput = [[AVCaptureDeviceInput alloc] initWithDevice:[self cameroWithPosition:AVCaptureDevicePositionFront] error:&error];
        if (error) {
            NSLog(@"前置摄像头获取失败");
        }
    }
    return _frontCameraInput;
}

- (void)setupInputs {
NSError *error = nil;
if ([self.session canAddInput:self.backCameraInput]) {
    [self.session addInput:self.backCameraInput];
}
else {
    [CCHUD showWhiteBackText:kGetLocalizedString(@"添加视频输入失败")];
    return;
}
AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio] error:&error];
if (!audioInput) {
    [CCHUD showWhiteBackText:kGetLocalizedString(@"初始化音频输入设备失败")];
    return;
}
if ([self.session canAddInput:audioInput]) {
    [self.session addInput:audioInput];
}
else {
    [CCHUD showWhiteBackText:kGetLocalizedString(@"添加音频输入失败")];
    return;
}
}

添加两个输出源

- (void)setupVideoOutput {
self.videoOutput = [[AVCaptureVideoDataOutput alloc] init];
[self.videoOutput setSampleBufferDelegate:self queue:self.videoQueue];
if ([self.session canAddOutput:self.videoOutput]) {
    [self.session addOutput:self.videoOutput];
    self.videoConnection = [self.videoOutput connectionWithMediaType:AVMediaTypeVideo];
    [self.videoConnection setVideoOrientation:AVCaptureVideoOrientationPortrait];
    self.videoConnection.videoMirrored = self.isFront;
}
else {
    [CCHUD showWhiteBackText:kGetLocalizedString(@"添加视频输出失败")];
    return;
}
self.audioOutput = [[AVCaptureAudioDataOutput alloc] init];
[self.audioOutput setSampleBufferDelegate:self queue:self.audioQueue];

if ([self.session canAddOutput:self.audioOutput]) {
    [self.session addOutput:self.audioOutput];
    self.audioConnection = [self.audioOutput connectionWithMediaType:AVMediaTypeAudio];
}
else {
    [CCHUD showWhiteBackText:kGetLocalizedString(@"添加视频输出失败")];
    return;
}
}

想要可见,还需要添加一个AVCaptureVideoPreviewLayer,layer负责绘制output的buffer逐帧绘制

- (void)setupPreviewLayer {
self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.session];
self.previewLayer.frame = CGRectMake(0, WindCS_TOP_HEIGHT, CCScreenWidth, CCScreenHeight - WindCS_IPHONE_X_BOTTOM_HEIGHT - WindCS_TOP_HEIGHT);
self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
[self.previewView.layer addSublayer:self.previewLayer];
self.previewView.layer.masksToBounds = YES;
}

写出文件 这里还需要一个重要的东西 AVAssetWriter

AVFoundation实现小视频录制_第2张图片
官方文档是个好东西

开始录制

self.avPath = [[WindCSFilePathManager windCS_CoreVideoPath] stringByAppendingPathComponent:[NSString stringWithFormat:@"%.4f.mp4",[[NSDate new] timeIntervalSince1970]]];
self.writer =  [AVAssetWriter assetWriterWithURL:[NSURL fileURLWithPath:self.avPath] fileType:AVFileTypeMPEG4 error:nil];
self.writerVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:[self videoCompressSettings]];
self.writerVideoInput.expectsMediaDataInRealTime = YES;
if ([self.writer canAddInput:self.writerVideoInput]) {
    [self.writer addInput:self.writerVideoInput];
}
self.writerAudioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:[self audioCompressSettings]];
self.writerAudioInput.expectsMediaDataInRealTime = YES;
if ([self.writer canAddInput:self.writerAudioInput]) {
    [self.writer addInput:self.writerAudioInput];
}
  WindCS_WEAK_SELF
 [self adjustVideoOrientationComplete:^{
    WindCS_STRONG_SELF
    dispatch_async(dispatch_get_main_queue(), ^{
        
        strongSelf.isRecording = YES;
    });
    
}];
self.isRecording = YES;

output的配置

- (NSDictionary *)videoCompressSettings {
NSDictionary *compressionProperties = @{ AVVideoAverageBitRateKey : @(200 * 8 * 1024),
                                         AVVideoExpectedSourceFrameRateKey: @25,
                                         AVVideoProfileLevelKey : AVVideoProfileLevelH264HighAutoLevel };
CGFloat height = (CCScreenHeight - WindCS_TOP_HEIGHT - WindCS_IPHONE_X_BOTTOM_HEIGHT);
return @{ AVVideoCodecKey : AVVideoCodecH264,
          AVVideoWidthKey : @(CCScreenWidth * 2),
          AVVideoHeightKey : @(height * 2),
          AVVideoCompressionPropertiesKey : compressionProperties,
          AVVideoScalingModeKey : AVVideoScalingModeResizeAspectFill };
}

- (NSDictionary *)audioCompressSettings {
AudioChannelLayout layout = { .mChannelLayoutTag = kAudioChannelLayoutTag_Stereo,
    .mChannelBitmap = 0,
    .mNumberChannelDescriptions = 0 };
NSData *channelLayoutData = [NSData dataWithBytes:&layout length:offsetof(AudioChannelLayout, mChannelDescriptions)];
return @{ AVFormatIDKey: @(kAudioFormatMPEG4AAC),
          AVEncoderBitRateKey : @96000,
          AVSampleRateKey : @44100,
          AVChannelLayoutKey : channelLayoutData,
          AVNumberOfChannelsKey : @2 };
}

视频方向调整

  /// 调整视频方向
- (void)adjustVideoOrientationComplete:(void(^)(void))finished {
UIDeviceOrientation orientation = [UIDevice currentDevice].orientation;
if (orientation == UIDeviceOrientationLandscapeRight) {
    self.writerVideoInput.transform = CGAffineTransformMakeRotation(M_PI / 2.0);
    finished();
   } else if (orientation == UIDeviceOrientationLandscapeLeft) {
       self.writerVideoInput.transform = CGAffineTransformMakeRotation(M_PI * 1.5);
       finished();
   } else if (orientation == UIDeviceOrientationPortraitUpsideDown) {
       self.writerVideoInput.transform = CGAffineTransformMakeRotation(M_PI);
       finished();
   } else if (orientation == UIDeviceOrientationUnknown || orientation == UIDeviceOrientationPortrait) {
       if ([self.cmmotionManager isDeviceMotionAvailable]) {
           self.cmmotionManager.deviceMotionUpdateInterval = 0;
           WindCS_WEAK_SELF
           [self.cmmotionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMDeviceMotion * _Nullable motion, NSError * _Nullable error) {
               double x = motion.gravity.x;
               double y = motion.gravity.y;
               double xAngle = atan2(x,y)/M_PI*180.0;
               WindCS_STRONG_SELF
               if (xAngle < -45 && xAngle > -135) {
                   strongSelf.writerVideoInput.transform = CGAffineTransformMakeRotation(M_PI * 1.5);
               } else if (xAngle > 45 && xAngle < 135) {
                   strongSelf.writerVideoInput.transform = CGAffineTransformMakeRotation(M_PI / 2.0);
               } else if (xAngle >= -45 && xAngle <= 45) {
                    self.writerVideoInput.transform = CGAffineTransformMakeRotation(M_PI);
                }
               [strongSelf.cmmotionManager stopDeviceMotionUpdates];
               finished();
           }];
       }
   }
}

AVCaptureVideoDataOutputSampleBufferDelegate

- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
// 录视频
if (self.isRecording) {
    @synchronized (self) {
        if (self.writer.status == AVAssetWriterStatusUnknown) {
            [self.writer startWriting];
            NSLog(@"self.writer startWriting");
            [self.writer startSessionAtSourceTime:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)];
        }
        [self write:sampleBuffer fromConnection:connection];
        
    }
}
}

- (void)write:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
if (connection == self.videoConnection) {
    if (self.writerVideoInput.readyForMoreMediaData) {
        [self.writerVideoInput appendSampleBuffer:sampleBuffer];
    }
}
else if (connection == self.audioConnection) {
    if (self.writerAudioInput.readyForMoreMediaData) {
        [self.writerAudioInput appendSampleBuffer:sampleBuffer];
    }
}
}

停止录制

dispatch_async(dispatch_get_main_queue(), ^{
    if ([self.writer.inputs containsObject:self.writerVideoInput]) {
        [self.writerVideoInput markAsFinished];
    }
    if ([self.writer.inputs containsObject:self.writerAudioInput]) {
        [self.writerAudioInput markAsFinished];
    }
    [self.writer finishWritingWithCompletionHandler:^{
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"file size : %fM", [self showSize:self.avPath]);
            };
    }];
});
AVFoundation实现小视频录制_第3张图片
官方文档是个好东西

至此,一个小视频录制的功能其实就简单搭建完成了,当然 这里有很多细节,比如:

  • 参数的配置 可以根据自己的需要去配置
  • 写出来的视频如何压缩合适?
  • 既然可以逐帧写入,name是否可以逐帧的编辑呢?

接下来有时间可以一起研究~
你的喜欢或者关注是我继续的动力哦~

你可能感兴趣的:(AVFoundation实现小视频录制)