这里先罗列遇到的主要问题:
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用于视频裁剪
@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.初始化设置
//权限检查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配置
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 录制帧数设置
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 视频输出设置
//Videoif ([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 音频设置
//audioAVCaptureDevice *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 audiodispatch_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的配置代码应该是这样的
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];至此,视频录制就完成了。
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图的生成
//转成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 sampleCMSampleBufferRef 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可以从这里下载。