自定义相机&GPUImage滤镜录像
业务需求照着微信小视频做,后来发现挺有意思的就研究了一下,主要是一些关键性代码和一些坑的心得体会,具体AVFoundation的使用和UI搭建具体看demo就好。目前性能方面有些粗糙,交流学习使用,后期会将相关优化代码合并进来。
相关功能:
视频循环播放,视频压缩(80%压缩率);
录像时摄像头切换的I/O处理;
GPUimage的使用心得与相关注意事项(图像旋转、画面闪烁)
1、微信小视频录制(半屏幕)
视频压缩:6秒原版视频10M上下,压缩后1.5M上下,具体大小跟像素和拍摄内容有关。
视频录制压缩具体看demo代码,别的也没什么难度。
这里放个进度条的伸缩动画。
CABasicAnimation *scaleXAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale.x"];
scaleXAnimation.duration = MAXDURATION;
scaleXAnimation.fromValue = @(1.f);
scaleXAnimation.toValue = @(0.f);
[self.processView.layer addAnimation:scaleXAnimation forKey:@"scaleXAnimation"];
2、微信小视频录制拍照 (全屏)录制拍摄后预览功能
长按录像,点击拍照,预览确认选择
[前后摄像头切换]
- (void)swapFrontAndBackCameras {
NSArray *inputs =self.session.inputs;
for (AVCaptureDeviceInput *input in inputs ) {
AVCaptureDevice *device = input.device;
if ( [device hasMediaType:AVMediaTypeVideo] ) {
AVCaptureDevicePosition position = device.position;
if (position ==AVCaptureDevicePositionFront)
self.captureDevice = [self cameraWithPosition:AVCaptureDevicePositionBack];
else
self.captureDevice = [self cameraWithPosition:AVCaptureDevicePositionFront];
self.videoDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:self.captureDevice error:nil];
[self.session beginConfiguration];
[self.session removeInput:input];
[self.session addInput:self.videoDeviceInput];
[self.session commitConfiguration];
break;
}
}
}
这个只是达到了切后前后摄像头的效果,但是实际录像中切换摄像头这样会导致捕捉不到数据,所以在切换的时候需要重新配置 AVCaptureDevice和AVCaptureDeviceInput,目前处理是切换过后重新配置了session。
- (void)swapFrontAndBackCameras {
NSArray *inputs =self.session.inputs;
for (AVCaptureDeviceInput *input in inputs ) {
AVCaptureDevice *device = input.device;
if ( [device hasMediaType:AVMediaTypeVideo] ) {
AVCaptureDevicePosition position = device.position;
if (position ==AVCaptureDevicePositionFront)
[self configurationSession];
else
[self configurationSessionFront];
break;
}
}
}
[手动点击对焦]
尝试了很多对焦方式,发现这种对焦效果最好。
点击手势添加到preview上,根据点击的坐标进行计算转换point
- (void)tapPreview:(UITapGestureRecognizer *)tap {
NSLog(@"%@", NSStringFromCGPoint([tap locationInView:self]));
CGPoint point = [tap locationInView:self];
[self showFouceView:point];
CGPoint focusPoint = CGPointMake(point.x/self.width, point.y/self.height);
[self.recorder setFocusPoint:focusPoint];
}
- (void)setFocusPoint:(CGPoint)point {
if (self.captureDevice.isFocusPointOfInterestSupported) {
NSError *error = nil;
[self.captureDevice lockForConfiguration:&error];
/*****必须先设定聚焦位置,在设定聚焦方式******/
//聚焦点的位置
if ([self.captureDevice isFocusPointOfInterestSupported]) {
[self.captureDevice setFocusPointOfInterest:point];
}
// 聚焦模式
if ([self.captureDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {
[self.captureDevice setFocusMode:AVCaptureFocusModeAutoFocus];
}else{
NSLog(@"聚焦模式修改失败");
}
//曝光点的位置
if ([self.captureDevice isExposurePointOfInterestSupported]) {
[self.captureDevice setExposurePointOfInterest:point];
}
//曝光模式
if ([self.captureDevice isExposureModeSupported:AVCaptureExposureModeAutoExpose]) {
[self.captureDevice setExposureMode:AVCaptureExposureModeAutoExpose];
}else{
NSLog(@"曝光模式修改失败");
}
[self.captureDevice unlockForConfiguration];
}
}
单机拍照处理,获取AVCaptureStillImageOutput类中的 capture方法即可
[self.recorder.imageDataOutput captureStillImageAsynchronouslyFromConnection:conntion
completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) {
if (imageDataSampleBuffer == nil) {
return; }
NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];
UIImage *img = [UIImage imageWithData:imageData];
[weakSelf previewPhoto:img];
}];
[录制 &拍摄完的 录像预览]
这里是在写了一个vc中WXVideoPreviewViewController,方便后期做滤镜处理等,拍摄完成后将view添加在录制view的上层,进行本地视频的循环播放。循环播放结束时会出现闪屏的情况,所以这里会将视频中的第一帧取出进行默认展示。如有好的方法留言告知我哈。
取本地视频第一帧画面
- (UIImage*) getVideoPreViewImage {
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:[NSURL URLWithString:self.url] options:nil];
AVAssetImageGenerator *gen = [[AVAssetImageGenerator alloc] initWithAsset:asset];
gen.appliesPreferredTrackTransform = YES;
CMTime time = CMTimeMakeWithSeconds(0.0, 600);
NSError *error = nil;
CMTime actualTime;
CGImageRef image = [gen copyCGImageAtTime:time actualTime:&actualTime error:&error];
UIImage *img = [[UIImage alloc] initWithCGImage:image];
CGImageRelease(image);
return img;
}
本地视频播放由AVPlayerItem、AVPlayer、AVPlaerLayer组成,所以封住了一个VideoPlayerView方便使用,其中_playerLayer.videoGravity这个属性需要注意到
- (instancetype)initWithFrame:(CGRect)frame videoUrl:(NSString *)url {
if (self = [super initWithFrame:frame]) {
_playItem = [AVPlayerItem playerItemWithURL:[self urlValidation:url]];
_player = [[AVPlayer alloc] initWithPlayerItem:_playItem];
_playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player];
_playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; // 不写这句是不会全屏的!
_playerLayer.frame = self.bounds;
[self.layer addSublayer:_playerLayer];
if (kSYSTEM_VERSION_iOS10Later) _player.automaticallyWaitsToMinimizeStalling = NO;
[self noticeAndKVO];
}
return self;
}
视频重复播放,只需要在AVPlayerItemDidPlayToEndTimeNotification监听方法中,将timer重制即可
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playerDidFinished:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:nil];
- (void)playerDidFinished:(NSNotification*)noti {
[_player pause];
[self cyclePlayVideo];
}
- (void)cyclePlayVideo {
[_player seekToTime:kCMTimeZero];
[_player play];
}
3、GPUImage 视频录制与拍照功能
[实时添加滤镜与前后镜头切换录制]
这里有个不解 _filterView.fillMode 中写 kGPUImageFillModePreserveAspectRatioAndFill有问题,不太懂什么鬼,所以这里用2表示。
camer初始化这里注意,如果出现添加滤镜闪屏的情况,尝试[self.camera removeAllTargets] 清除targets,并查看 addTarget相关代码顺序。
- (void)captureAndRecording {
[self addSubview:self.filterView];
[self.camera addTarget:self.filterView];
[self.camera startCameraCapture];
self.camera.audioEncodingTarget = self.writer;
}
- (GPUImageView *)filterView {
if (!_filterView) {
_filterView = [[GPUImageView alloc] initWithFrame:self.bounds];
_filterView.fillMode = 2;//kGPUImageFillModePreserveAspectRatioAndFill
}
return _filterView;
}
- (GPUImageStillCamera *)camera {
if (!_camera) {
self.camera = [[GPUImageStillCamera alloc] initWithSessionPreset:AVCaptureSessionPreset640x480 cameraPosition:AVCaptureDevicePositionBack];
self.camera.outputImageOrientation = UIInterfaceOrientationPortrait;
self.camera.horizontallyMirrorFrontFacingCamera = YES; // 前置摄像头需要 镜像反转
self.camera.horizontallyMirrorRearFacingCamera = NO; // 后置摄像头不需要 镜像反转 (default:NO)
[self.camera addAudioInputsAndOutputs]; //该句可防止允许声音通过的情况下,避免录制第一帧黑屏闪屏
}
return _camera;
}
滤镜切换, 由于GPUImage 无滤镜的时候不支持录像和拍照,所以正常模式不做处理。没有滤镜的时候如何进行录像和拍照,如有老司机知道望告知一下啊,交流学习。
这里GPUImageBeautifyFilter 美颜处理来自guikz ,感谢github开源精神。
- (void)filter:(UIButton *)btn {
FaceCameraFilterEnum filterEnum = btn.tag;
[self.camera removeAllTargets];
switch (filterEnum) {
case FaceCameraFilterNone:{
btn.tag = 1;
[self.camera addTarget:_filterView];
_tempFilter = nil;
[_beautifyBtn setTitle:@"无" forState:UIControlStateNormal];
}
break;
case FaceCameraFilterBeautify:{
btn.tag = 2;
// MARK: 添加 美颜滤镜
_beautifyFilter = [[GPUImageBeautifyFilter alloc] init];
[self.camera addTarget:_beautifyFilter];
[_beautifyFilter addTarget:_filterView];
[_beautifyFilter addTarget:self.writer];
_tempFilter = _beautifyFilter;
[_beautifyBtn setTitle:@"美颜" forState:UIControlStateNormal];
}
break;
case FaceCameraFilterSketch:{
btn.tag = 0;
GPUImageSketchFilter *filter = [[GPUImageSketchFilter alloc] init];
[self.camera addTarget:filter];
[filter addTarget:_filterView];
[filter addTarget:self.writer];
_tempFilter = filter;
[_beautifyBtn setTitle:@"黑白" forState:UIControlStateNormal];
}
break;
default:
break;
}
}
拍照存储这里有个坑,如果使用 writeImageDataToSavedPhotosAlbum 这个方法进行照片存储,图片会旋转90度,所以,这里使用 UIImageWriteToSavedPhotosAlbum进行图片写入到相册。
- (void)writerCurrentFrameToLibrary {
_savingImg = YES;
kWeakSelf(self)
if (_tempFilter) {
#warning 这是第二个坑,用这种方式保存照片到相册正常,官方demo种的相片保存会90度旋转
// ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
// [library writeImageDataToSavedPhotosAlbum:processedJPEG metadata:self.camera.currentCaptureMetadata completionBlock:^(NSURL *assetURL, NSError *error2) {
// }];
[self.camera capturePhotoAsJPEGProcessedUpToFilter:_tempFilter withCompletionHandler:^(NSData *processedJPEG, NSError *error){
UIImage *img = [UIImage imageWithData:processedJPEG];
[weakSelf saveImageWriteToPhotosAlbum:img];
}];
}else {
[self showAlterViewTitle:@"失败" message:@"没有滤镜不能进行拍照"];
}
}
- (void)saveImageWriteToPhotosAlbum:(UIImage *)img {
ALAuthorizationStatus author = [ALAssetsLibrary authorizationStatus];
if (author == ALAuthorizationStatusRestricted || author == ALAuthorizationStatusDenied){
//无权限
return ;
}
if (img) {
UIImageWriteToSavedPhotosAlbum(img, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);
}else {
[self showAlterViewTitle:@"失败" message:@"照片保存失败"];
}
}
- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo {
if (!error) {
[self showAlterViewTitle:@"成功" message:@"照片保存成功"];
}else {
[self showAlterViewTitle:@"失败" message:@"照片保存失败"];
}
}
4、本地&在线小视频播放(略粗糙)
没什么难度,感觉稍微有用点代码是视频下载这段
- (void)downloadVideo {
NSString *domainUrl = self.videoUrlString;
NSURLRequest *request =[NSURLRequest requestWithURL:[NSURL URLWithString:domainUrl] cachePolicy:1 timeoutInterval:15.0f];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSMutableString * path = [[NSMutableString alloc]initWithString:documentsDirectory];
NSString *timeString = [NSString stringWithFormat:@"%.0f", [[NSDate dateWithTimeIntervalSinceNow:0] timeIntervalSince1970]];
[path appendString:[NSString stringWithFormat:@"/%@.mov", timeString]];
NSLog(@"path == %@", path);
if ([data writeToFile:path atomically:YES])
{
NSLog(@"mov写入成功");
UIImageWriteToSavedPhotosAlbum([UIImage imageWithContentsOfFile:path], nil, nil, NULL);
BOOL compatible = UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(path);
if (compatible)
{
UISaveVideoAtPathToSavedPhotosAlbum(path, self, nil, NULL);
NSLog(@"视频保存成功");
}
else
{
NSLog(@"视频保存失败");
}
}
else
{
NSLog(@"mov写入失败");
}
}];
}
截图
第一次写,比较仓促,后期想到什么了再补,一起交流学习。