zoukankan      html  css  js  c++  java
  • [iOS]手把手教你实现微信小视频

      本文个人原创,转载请注明出处,谢谢。

      前段时间项目要求需要在聊天模块中加入类似微信的小视频功能,这边博客主要是为了总结遇到的问题和解决方法,希望能够对有同样需求的朋友有所帮助。

      效果预览:

      这里先罗列遇到的主要问题:  

    1. 视频剪裁  微信的小视频只是取了摄像头获取的一部分画面
    2. 滚动预览的卡顿问题  AVPlayer播放视频在滚动中会出现很卡的问题

          接下来让我们一步步来实现。

    Part 1 实现视频录制

      1.录制类WKMovieRecorder实现

      创建一个录制类WKMovieRecorder,负责视频录制。

    @interface WKMovieRecorder : NSObject
    
    + (WKMovieRecorder*) sharedRecorder;

      - (instancetype)initWithMaxDuration:(NSTimeInterval)duration;

    @end

      定义回调block

    /**
     *  录制结束
     *
     *  @param info     回调信息
     *  @param isCancle YES:取消 NO:正常结束
     */
    typedef void(^FinishRecordingBlock)(NSDictionary *info, WKRecorderFinishedReason finishReason);
    /**
     *  焦点改变
     */
    typedef void(^FocusAreaDidChanged)();
    /**
     *  权限验证
     *
     *  @param success 是否成功
     */
    typedef void(^AuthorizationResult)(BOOL success);
    
    @interface WKMovieRecorder : NSObject
    //回调
    @property (nonatomic, copy) FinishRecordingBlock finishBlock;//录制结束回调
    @property (nonatomic, copy) FocusAreaDidChanged focusAreaDidChangedBlock;
    @property (nonatomic, copy) AuthorizationResult authorizationResultBlock;
    @end

      定义一个cropSize用于视频裁剪

    @property (nonatomic, assign) CGSize cropSize;

      接下来就是capture的实现了,这里代码有点长,懒得看的可以直接看后面的视频剪裁部分

      录制配置:

      

    @interface WKMovieRecorder ()
    <
    AVCaptureVideoDataOutputSampleBufferDelegate,
    AVCaptureAudioDataOutputSampleBufferDelegate,
    WKMovieWriterDelegate
    >
    
    {
        AVCaptureSession* _session;
        AVCaptureVideoPreviewLayer* _preview;
        WKMovieWriter* _writer;
        //暂停录制
        BOOL _isCapturing;
        BOOL _isPaused;
        BOOL _discont;
        int _currentFile;
        CMTime _timeOffset;
        CMTime _lastVideo;
        CMTime _lastAudio;
        
        NSTimeInterval _maxDuration;
    }
    
    // Session management.
    @property (nonatomic, strong) dispatch_queue_t sessionQueue;
    @property (nonatomic, strong) dispatch_queue_t videoDataOutputQueue;
    @property (nonatomic, strong) AVCaptureSession *session;
    @property (nonatomic, strong) AVCaptureDevice *captureDevice;
    @property (nonatomic, strong) AVCaptureDeviceInput *videoDeviceInput;
    @property (nonatomic, strong) AVCaptureStillImageOutput *stillImageOutput;
    @property (nonatomic, strong) AVCaptureConnection *videoConnection;
    @property (nonatomic, strong) AVCaptureConnection *audioConnection;
    @property (nonatomic, strong) NSDictionary *videoCompressionSettings;
    @property (nonatomic, strong) NSDictionary *audioCompressionSettings;
    @property (nonatomic, strong) AVAssetWriterInputPixelBufferAdaptor *adaptor;
    @property (nonatomic, strong) AVCaptureVideoDataOutput *videoDataOutput;
    
    
    
    //Utilities
    @property (nonatomic, strong) NSMutableArray *frames;//存储录制帧
    @property (nonatomic, assign) CaptureAVSetupResult result;
    @property (atomic, readwrite) BOOL isCapturing;
    @property (atomic, readwrite) BOOL isPaused;
    @property (nonatomic, strong) NSTimer *durationTimer;
    
    @property (nonatomic, assign) WKRecorderFinishedReason finishReason;
    
    @end

      实例化方法:

    + (WKMovieRecorder *)sharedRecorder
    {
        static WKMovieRecorder *recorder;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            recorder = [[WKMovieRecorder alloc] initWithMaxDuration:CGFLOAT_MAX];
        });
        
        return recorder;
    }
    
    - (instancetype)initWithMaxDuration:(NSTimeInterval)duration
    {
        if(self = [self init]){
            _maxDuration = duration;
            _duration = 0.f;
        }
        
        return self;
    }
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            _maxDuration = CGFLOAT_MAX;
            _duration = 0.f;
            _sessionQueue = dispatch_queue_create("wukong.movieRecorder.queue", DISPATCH_QUEUE_SERIAL );
            _videoDataOutputQueue = dispatch_queue_create( "wukong.movieRecorder.video", DISPATCH_QUEUE_SERIAL );
            dispatch_set_target_queue( _videoDataOutputQueue, dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_HIGH, 0 ) );
        }
        return self;
    }

      2.初始化设置

      初始化设置分别为session创建、权限检查以及session配置

      1.session创建

      

    self.session = [[AVCaptureSession alloc] init];
    self.result = CaptureAVSetupResultSuccess;

      2.权限检查

      

    //权限检查
            switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
                case AVAuthorizationStatusNotDetermined: {
                    [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
                        if (granted) {
                            self.result = CaptureAVSetupResultSuccess;
                        }
                    }];
                    break;
                }
                case AVAuthorizationStatusAuthorized: {
                    
                    break;
                }
                default:{
                    self.result = CaptureAVSetupResultCameraNotAuthorized;
                }
            }
            
            if ( self.result != CaptureAVSetupResultSuccess) {
                
                if (self.authorizationResultBlock) {
                    self.authorizationResultBlock(NO);
                }
                return;
            }
            

      3.session配置

      session配置是需要注意的是AVCaptureSession的配置不能在主线程, 需要自行创建串行线程。

      3.1.1 获取输入设备与输入流

      

     AVCaptureDevice *captureDevice = [[self class] deviceWithMediaType:AVMediaTypeVideo preferringPosition:AVCaptureDevicePositionBack];
                
      _captureDevice = captureDevice;
                
      NSError *error = nil;
      _videoDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:captureDevice error:&error];
                
      if (!_videoDeviceInput) {
       NSLog(@"未找到设备");
      }

      3.1.2 录制帧数设置

      帧数设置的主要目的是适配iPhone4,毕竟是应该淘汰的机器了

      

           int frameRate;
                if ( [NSProcessInfo processInfo].processorCount == 1 )
                {
                    if ([self.session canSetSessionPreset:AVCaptureSessionPresetLow]) {
                        [self.session setSessionPreset:AVCaptureSessionPresetLow];
                    }
                    frameRate = 10;
                }else{
                    if ([self.session canSetSessionPreset:AVCaptureSessionPreset640x480]) {
                        [self.session setSessionPreset:AVCaptureSessionPreset640x480];
                    }
                    frameRate = 30;
                }
                
                CMTime frameDuration = CMTimeMake( 1, frameRate );
                
                if ( [_captureDevice lockForConfiguration:&error] ) {
                    _captureDevice.activeVideoMaxFrameDuration = frameDuration;
                    _captureDevice.activeVideoMinFrameDuration = frameDuration;
                    [_captureDevice unlockForConfiguration];
                }
                else {
                    NSLog( @"videoDevice lockForConfiguration returned error %@", error );
                }

      3.1.3 视频输出设置

      视频输出设置需要注意的问题是:要设置videoConnection的方向,这样才能保证设备旋转时的显示正常。

            //Video
                if ([self.session canAddInput:_videoDeviceInput]) {
                    
                    [self.session addInput:_videoDeviceInput];
                    self.videoDeviceInput = _videoDeviceInput;
                    [self.session removeOutput:_videoDataOutput];
                    
                    AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init];
                    _videoDataOutput = videoOutput;
                    videoOutput.videoSettings = @{ (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA) };
                    
                    [videoOutput setSampleBufferDelegate:self queue:_videoDataOutputQueue];
                    
                    videoOutput.alwaysDiscardsLateVideoFrames = NO;
                    
                    if ( [_session canAddOutput:videoOutput] ) {
                        [_session addOutput:videoOutput];
                        
                        [_captureDevice addObserver:self forKeyPath:@"adjustingFocus" options:NSKeyValueObservingOptionNew context:FocusAreaChangedContext];
                        
                        _videoConnection = [videoOutput connectionWithMediaType:AVMediaTypeVideo];
                        
                        if(_videoConnection.isVideoStabilizationSupported){
                            _videoConnection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
                        }
    
                        
                        UIInterfaceOrientation statusBarOrientation = [UIApplication sharedApplication].statusBarOrientation;
                        AVCaptureVideoOrientation initialVideoOrientation = AVCaptureVideoOrientationPortrait;
                        if ( statusBarOrientation != UIInterfaceOrientationUnknown ) {
                            initialVideoOrientation = (AVCaptureVideoOrientation)statusBarOrientation;
                        }
                        
                        _videoConnection.videoOrientation = initialVideoOrientation;
                    }
    
                }
                else{
                    NSLog(@"无法添加视频输入到会话");
                }

      3.1.4 音频设置

      需要注意的是为了不丢帧,需要把音频输出的回调队列放在串行队列中

           //audio
                AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
                AVCaptureDeviceInput *audioDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:&error];
                
                
                if ( ! audioDeviceInput ) {
                    NSLog( @"Could not create audio device input: %@", error );
                }
                
                if ( [self.session canAddInput:audioDeviceInput] ) {
                    [self.session addInput:audioDeviceInput];
                    
                }
                else {
                    NSLog( @"Could not add audio device input to the session" );
                }
                
                AVCaptureAudioDataOutput *audioOut = [[AVCaptureAudioDataOutput alloc] init];
                // Put audio on its own queue to ensure that our video processing doesn't cause us to drop audio
                dispatch_queue_t audioCaptureQueue = dispatch_queue_create( "wukong.movieRecorder.audio", DISPATCH_QUEUE_SERIAL );
                [audioOut setSampleBufferDelegate:self queue:audioCaptureQueue];
                
                if ( [self.session canAddOutput:audioOut] ) {
                    [self.session addOutput:audioOut];
                }
                _audioConnection = [audioOut connectionWithMediaType:AVMediaTypeAudio];

      还需要注意一个问题就是对于session的配置代码应该是这样的

    [self.session beginConfiguration];
    
    ...配置代码
    
    [self.session commitConfiguration];

      由于篇幅问题,后面的录制代码我就挑重点的讲了。

      3.2  视频存储

      现在我们需要在AVCaptureVideoDataOutputSampleBufferDelegate与AVCaptureAudioDataOutputSampleBufferDelegate的回调中,将音频和视频写入沙盒。在这个过程中需要注意的,在启动session后获取到的第一帧黑色的,需要放弃。

      3.2.1 创建WKMovieWriter类来封装视频存储操作

      WKMovieWriter的主要作用是利用AVAssetWriter拿到CMSampleBufferRef,剪裁后再写入到沙盒中。

      这是剪裁配置的代码,AVAssetWriter会根据cropSize来剪裁视频,这里需要注意的一个问题是cropSize的width必须是320的整数倍,不然的话剪裁出来的视频右侧会出现一条绿色的线

      NSDictionary *videoSettings;
    if (_cropSize.height == 0 || _cropSize.width == 0) { _cropSize = [UIScreen mainScreen].bounds.size; } videoSettings = [NSDictionary dictionaryWithObjectsAndKeys: AVVideoCodecH264, AVVideoCodecKey, [NSNumber numberWithInt:_cropSize.width], AVVideoWidthKey, [NSNumber numberWithInt:_cropSize.height], AVVideoHeightKey, AVVideoScalingModeResizeAspectFill,AVVideoScalingModeKey, nil];

      至此,视频录制就完成了。

      接下来需要解决的预览的问题了

    Part 2 卡顿问题解决

      1.1 gif图生成

      通过查资料发现了这篇blog 介绍说微信团队解决预览卡顿的问题使用的是播放图片gif,但是博客中的示例代码有问题,通过CoreAnimation来播放图片导致内存暴涨而crash。但是,还是给了我一些灵感,因为之前项目的启动页用到了gif图片的播放,所以我就想能不能把视频转成图片,然后再转成gif图进行播放,这样不就解决了问题了吗。于是我开始google功夫不负有心人找到了,图片数组转gif图片的方法

      gif图转换代码

    static void makeAnimatedGif(NSArray *images, NSURL *gifURL, NSTimeInterval duration) {
        NSTimeInterval perSecond = duration /images.count;
        
        NSDictionary *fileProperties = @{
                                         (__bridge id)kCGImagePropertyGIFDictionary: @{
                                                 (__bridge id)kCGImagePropertyGIFLoopCount: @0, // 0 means loop forever
                                                 }
                                         };
        
        NSDictionary *frameProperties = @{
                                          (__bridge id)kCGImagePropertyGIFDictionary: @{
                                                  (__bridge id)kCGImagePropertyGIFDelayTime: @(perSecond), // a float (not double!) in seconds, rounded to centiseconds in the GIF data
                                                  }
                                          };
       
        CGImageDestinationRef destination = CGImageDestinationCreateWithURL((__bridge CFURLRef)gifURL, kUTTypeGIF, images.count, NULL);
        CGImageDestinationSetProperties(destination, (__bridge CFDictionaryRef)fileProperties);
        
        for (UIImage *image in images) {
            @autoreleasepool {
                
                CGImageDestinationAddImage(destination, image.CGImage, (__bridge CFDictionaryRef)frameProperties);
            }
        }
        
        if (!CGImageDestinationFinalize(destination)) {
            NSLog(@"failed to finalize image destination");
        }else{
            
            
        }
        CFRelease(destination);
    }

      转换是转换成功了,但是出现了新的问题,使用ImageIO生成gif图片时会导致内存暴涨,瞬间涨到100M以上,如果多个gif图同时生成的话一样会crash掉,为了解决这个问题需要用一个串行队列来进行gif图的生成  

      1.2 视频转换为UIImages

      主要是通过AVAssetReader、AVAssetTrack、AVAssetReaderTrackOutput 来进行转换

    //转成UIImage
    - (void)convertVideoUIImagesWithURL:(NSURL *)url finishBlock:(void (^)(id images, NSTimeInterval duration))finishBlock
    {
            AVAsset *asset = [AVAsset assetWithURL:url];
            NSError *error = nil;
            self.reader = [[AVAssetReader alloc] initWithAsset:asset error:&error];
            
            NSTimeInterval duration = CMTimeGetSeconds(asset.duration);
            __weak typeof(self)weakSelf = self;
            dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
            dispatch_async(backgroundQueue, ^{
                __strong typeof(weakSelf) strongSelf = weakSelf;
                NSLog(@"");
                
                
                if (error) {
                    NSLog(@"%@", [error localizedDescription]);
                    
                }
                
                NSArray *videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];
                
                AVAssetTrack *videoTrack =[videoTracks firstObject];
                if (!videoTrack) {
                    return ;
                }
                int m_pixelFormatType;
                //     视频播放时,
                m_pixelFormatType = kCVPixelFormatType_32BGRA;
                // 其他用途,如视频压缩
                //    m_pixelFormatType = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
                
                NSMutableDictionary *options = [NSMutableDictionary dictionary];
                [options setObject:@(m_pixelFormatType) forKey:(id)kCVPixelBufferPixelFormatTypeKey];
                AVAssetReaderTrackOutput *videoReaderOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:videoTrack outputSettings:options];
                
                if ([strongSelf.reader canAddOutput:videoReaderOutput]) {
                    
                    [strongSelf.reader addOutput:videoReaderOutput];
                }
                [strongSelf.reader startReading];
                
                
                NSMutableArray *images = [NSMutableArray array];
                // 要确保nominalFrameRate>0,之前出现过android拍的0帧视频
                while ([strongSelf.reader status] == AVAssetReaderStatusReading && videoTrack.nominalFrameRate > 0) {
                     @autoreleasepool {
                    // 读取 video sample
                    CMSampleBufferRef videoBuffer = [videoReaderOutput copyNextSampleBuffer];
                    
                    if (!videoBuffer) {
                        break;
                    }
                    
                    [images addObject:[WKVideoConverter convertSampleBufferRefToUIImage:videoBuffer]];
                    
                    CFRelease(videoBuffer);
                }
                
                
             }
                if (finishBlock) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        finishBlock(images, duration);
                    });
                }
            });
       
    
    }

      在这里有一个值得注意的问题,在视频转image的过程中,由于转换时间很短,在短时间内videoBuffer不能够及时得到释放,在多个视频同时转换时任然会出现内存问题,这个时候就需要用autoreleasepool来实现及时释放

    @autoreleasepool {
      // 读取 video sample
      CMSampleBufferRef videoBuffer = [videoReaderOutput copyNextSampleBuffer];
        if (!videoBuffer) {
          break;
         }
                        
        [images addObject:[WKVideoConverter convertSampleBufferRefToUIImage:videoBuffer]];
         CFRelease(videoBuffer);
    }

      至此,微信小视频的难点(我认为的)就解决了,至于其他的实现代码请看demo就基本实现了,demo可以从这里下载,如果觉得对你有帮助的话,帮我star一下哟,感激不尽  

    资料:

      UIImages转gif图  https://gist.github.com/sahara108/6772691

      视频暂停录制  http://www.gdcl.co.uk/2013/02/20/iPhone-Pause.html

      视频crop绿边解决 http://stackoverflow.com/questions/22883525/avassetexportsession-giving-me-a-green-border-on-right-and-bottom-of-output-vide

      视频裁剪:http://stackoverflow.com/questions/15737781/video-capture-with-11-aspect-ratio-in-ios/16910263#16910263

      CMSampleBufferRef转image https://developer.apple.com/library/ios/qa/qa1702/_index.html

      微信小视频分析 http://www.jianshu.com/p/3d5ccbde0de1

    感谢以上文章的作者

  • 相关阅读:
    NSDate 与NSString的互相转换
    NSArray 基础
    IOS 使用自定义字体
    页面跳转添加动画
    Builder 模式
    树的子结构
    Singleton 模式
    合并两个排序的链表(递归算法)
    合并两个排序的链表(非递归)
    反转链表
  • 原文地址:https://www.cnblogs.com/pretty-guy/p/5811290.html
Copyright © 2011-2022 走看看