首页 > 代码库 > [iOS]手把手教你实现微信小视频

[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

感谢以上文章的作者

[iOS]手把手教你实现微信小视频