iOS 微信小视频&自定义相机&GPUImage使用心得

自定义相机&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表示。


94C370D1-E565-49A2-8AA4-A975CFEE3CAB.png

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写入失败");
        }
    }];
}

截图

IMG_8039.PNG

IMG_8042.PNG

IMG_8032.PNG

IMG_8035.PNG

IMG_8037.PNG

IMG_8043.PNG

IMG_8044.PNG

第一次写,比较仓促,后期想到什么了再补,一起交流学习。

Demo:
https://github.com/MarkBuster/MBSmartVideo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容