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

效果预览:

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

  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);
}

[iOS]手把手教你实现微信小视频相关推荐

  1. 手把手教你开发微信小程序中的插件

    继上次 手把手教你实现微信小程序中的自定义组件 已经有一段时间了(不了解的小伙伴建议去看看,因为插件很多内容跟组件相似),今年3月13日,小程序新增了 小程序**「插件」 功能,以及开发者工具新增 「 ...

  2. 手把手教你进行微信小程序开发案例1---计算器

    由于之前的文章中已经教会了大家如何注册自己的一个微信小程序,并且利用微信开发工具进行小程序的开发,所以这里不再介绍如何下载工具和注册账号,不懂的小伙伴们可以观看我之前发过的教程哦. #####下面我将 ...

  3. 手把手教你玩微信小程序跳一跳

    最近微信小程序火的半边天都红了,虽然不会写,但是至少也可以仿照网上大神开发外挂吧!下面手把手教妹纸们(汉纸们请自觉的眼观耳听)怎么愉快的在微信小游戏中获得高分. 废话不多说,咱们这就发车了!呸!咱们这 ...

  4. 手把手教你制作微信小程序,开源、免费、快速搞定

    最近做了个"罗孚传车"的小程序 一时兴起,做了一个小程序,将个人收集的同汽车相关的行业资讯和学习资料,分享到小程序中,既作为历史资料保存,又提供给更多的人学习和了解,还能装一下:) ...

  5. 另一个小程序 返回的支付结果如何得到_手把手教你测微信小程序

    WeTest 导读 在小程序持续大量爆发的形势下,现在已经成为了各平台竞争的战略布局重点.至今年2月,月活超500万的微信小程序已经达到237个,其中个人开发占比高达2成.因小程序的开发门槛低.传播快 ...

  6. 微信小程序轮播中的current_手把手教你美化微信小程序中的轮播效果

    微信小程序现在依然很火,相信大家有目共睹:所以作为前端开发者,掌握如何开发小程序,是一项必备技能了,你觉得呢? 相对于PC和H5开发,小程序坑很多,所以需要慢慢"踩",被坑多了,路 ...

  7. 手把手教您搭建微信小程序商城?(二)

    之前[文章]有很多人问我小程序商城上线的事情~所以我打算把文章完善下 一.注册小程序 进入微信公众平台,单击"立即注册",选择帐号注册类型:[小程序]. 2.填写小程序的帐户信息, ...

  8. 手把手教您搭建微信小程序商城?

    小程序的火热程度无需多言,相对于淘宝.天猫等平台,微信小程序的优点非常明显,下面简单介绍下-- 首先是费用低,无需昂贵的店铺年费. 自由自主可控,微信小程序商城是完全属于自己的店铺,不用受制于大平台苛 ...

  9. 手把手教你迁移微信小程序到 QQ 浏览器!

    继微信.QQ 之后,QQ 浏览器上也可以使用小程序了. 12 月 5 日,QQ浏览器小程序宣布,实现与微信小程序打通.QQ 浏览器 Android 版现已上线小程序,在搜索的场景下,小程序嵌入 QQ ...

  10. 手把手教你将微信小程序放到git上

    目录 背景 步骤 背景 首先,要创建一个自己的git仓库,这里默认大家都能够自己创建了git仓库了.如果不会创建仓库的话,百 度 一 下,很容易就能够创建了!(后续,如有不知道在哪里,怎么创建仓库的话 ...

最新文章

  1. 魔改Attention大集合
  2. 高等数学、线性代数、概率论数理统计书籍推荐
  3. linux实战应用案例: 如何在 Linux 安装 MySQL 8 数据库?(图文详细教程)
  4. Ansible roles角色实战案例:httpd nginx memcached mysql
  5. android模拟器EditText 不能用物理键盘输入,也不能用电脑键盘输入
  6. git add失效问题以及git status结果与github网页结果不一致(转载+自己整理)
  7. Python 循环删除指定文件夹下所有的.longtian类型文件
  8. cannot convert value of type ‘org.codehaus.xfire.spring.editors.ServiceFactoryEditor
  9. selenium火狐驱动_在Selenium Firefox驱动程序上运行测试
  10. 计算机组成原理第五版第四章课后答案,计算机组成原理第4章习题参考答案
  11. Windows 常用软件清单
  12. 23个适合logo设计的常用英文字体
  13. 微信小程序密码显示隐藏(小眼睛)
  14. 大学生简单静态HTML网页模板源码——家乡介绍美丽乡村11页
  15. OpenEuler上构建LFS8.4
  16. CSS盒模型之内边距、边框、外边距 十九问(持续更新)
  17. Alphabetic Removals
  18. OS实验-模拟实现首次/最佳/最坏适应算法的内存块分配和回收
  19. python判断性别_惊呆|根据三围数据判断出用户性别竟是python使用逻辑回归算法搞的鬼!...
  20. Word2016文档完美地在表格里的方框(□)中打钩(√)

热门文章

  1. 互联网公司的几种销售模式
  2. 网络附加存储(NAS)
  3. 《Understanding WiFi Signal Frequency Features for Position-Independent Gesture Sensing》论文总结
  4. 【Python+Appium】开展自动化测试(八)swipe()滑动页面
  5. 房东拿租金去还房贷是天经地义的嘛
  6. 简单Java Web 开发:Eclipse+Struts2+Tomcat+MySQL(workbench)+SAE
  7. 攻防演练中防守方的骚姿势
  8. 经典语录总结:识人篇
  9. 阅读笔记0001之聊聊数据分析现状
  10. python中 f代表什么_python 中下拉框中的f,v,m是什么意思??