自定義相機(jī)&GPUImage濾鏡錄像
業(yè)務(wù)需求照著微信小視頻做然爆,后來發(fā)現(xiàn)挺有意思的就研究了一下,主要是一些關(guān)鍵性代碼和一些坑的心得體會黍图,具體AVFoundation的使用和UI搭建具體看demo就好曾雕。目前性能方面有些粗糙,交流學(xué)習(xí)使用助被,后期會將相關(guān)優(yōu)化代碼合并進(jìn)來翻默。
相關(guān)功能:
視頻循環(huán)播放,視頻壓縮(80%壓縮率);
錄像時(shí)攝像頭切換的I/O處理;
GPUimage的使用心得與相關(guān)注意事項(xiàng)(圖像旋轉(zhuǎn)恰起、畫面閃爍)
1、微信小視頻錄制(半屏幕)
視頻壓縮:6秒原版視頻10M上下趾牧,壓縮后1.5M上下,具體大小跟像素和拍攝內(nèi)容有關(guān)检盼。
視頻錄制壓縮具體看demo代碼,別的也沒什么難度翘单。
這里放個(gè)進(jìn)度條的伸縮動畫吨枉。
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、微信小視頻錄制拍照 (全屏)錄制拍攝后預(yù)覽功能
長按錄像哄芜,點(diǎn)擊拍照貌亭,預(yù)覽確認(rèn)選擇
[前后攝像頭切換]
- (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;
}
}
}
這個(gè)只是達(dá)到了切后前后攝像頭的效果劣挫,但是實(shí)際錄像中切換攝像頭這樣會導(dǎo)致捕捉不到數(shù)據(jù)雳刺,所以在切換的時(shí)候需要重新配置 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;
}
}
}
[手動點(diǎn)擊對焦]
嘗試了很多對焦方式谣光,發(fā)現(xiàn)這種對焦效果最好丁恭。
點(diǎn)擊手勢添加到preview上半醉,根據(jù)點(diǎn)擊的坐標(biāo)進(jìn)行計(jì)算轉(zhuǎn)換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];
/*****必須先設(shè)定聚焦位置隔嫡,在設(shè)定聚焦方式******/
//聚焦點(diǎn)的位置
if ([self.captureDevice isFocusPointOfInterestSupported]) {
[self.captureDevice setFocusPointOfInterest:point];
}
// 聚焦模式
if ([self.captureDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {
[self.captureDevice setFocusMode:AVCaptureFocusModeAutoFocus];
}else{
NSLog(@"聚焦模式修改失敗");
}
//曝光點(diǎn)的位置
if ([self.captureDevice isExposurePointOfInterestSupported]) {
[self.captureDevice setExposurePointOfInterest:point];
}
//曝光模式
if ([self.captureDevice isExposureModeSupported:AVCaptureExposureModeAutoExpose]) {
[self.captureDevice setExposureMode:AVCaptureExposureModeAutoExpose];
}else{
NSLog(@"曝光模式修改失敗");
}
[self.captureDevice unlockForConfiguration];
}
}
單機(jī)拍照處理涂屁,獲取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];
}];
[錄制 &拍攝完的 錄像預(yù)覽]
這里是在寫了一個(gè)vc中WXVideoPreviewViewController书在,方便后期做濾鏡處理等,拍攝完成后將view添加在錄制view的上層拆又,進(jìn)行本地視頻的循環(huán)播放儒旬。循環(huán)播放結(jié)束時(shí)會出現(xiàn)閃屏的情況栏账,所以這里會將視頻中的第一幀取出進(jìn)行默認(rèn)展示。如有好的方法留言告知我哈栈源。
取本地視頻第一幀畫面
- (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組成凉翻,所以封住了一個(gè)VideoPlayerView方便使用了讨,其中_playerLayer.videoGravity這個(gè)屬性需要注意到
- (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;
}
視頻重復(fù)播放制轰,只需要在AVPlayerItemDidPlayToEndTimeNotification監(jiān)聽方法中前计,將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 視頻錄制與拍照功能
[實(shí)時(shí)添加濾鏡與前后鏡頭切換錄制]
這里有個(gè)不解 _filterView.fillMode 中寫 kGPUImageFillModePreserveAspectRatioAndFill有問題,不太懂什么鬼垃杖,所以這里用2表示男杈。
camer初始化這里注意,如果出現(xiàn)添加濾鏡閃屏的情況调俘,嘗試[self.camera removeAllTargets] 清除targets伶棒,并查看 addTarget相關(guān)代碼順序。
- (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; // 前置攝像頭需要 鏡像反轉(zhuǎn)
self.camera.horizontallyMirrorRearFacingCamera = NO; // 后置攝像頭不需要 鏡像反轉(zhuǎn) (default:NO)
[self.camera addAudioInputsAndOutputs]; //該句可防止允許聲音通過的情況下彩库,避免錄制第一幀黑屏閃屏
}
return _camera;
}
濾鏡切換, 由于GPUImage 無濾鏡的時(shí)候不支持錄像和拍照肤无,所以正常模式不做處理。沒有濾鏡的時(shí)候如何進(jìn)行錄像和拍照骇钦,如有老司機(jī)知道望告知一下啊宛渐,交流學(xué)習(xí)。
這里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;
}
}
拍照存儲這里有個(gè)坑,如果使用 writeImageDataToSavedPhotosAlbum 這個(gè)方法進(jìn)行照片存儲,圖片會旋轉(zhuǎn)90度鳞仙,所以寇蚊,這里使用 UIImageWriteToSavedPhotosAlbum進(jìn)行圖片寫入到相冊。
- (void)writerCurrentFrameToLibrary {
_savingImg = YES;
kWeakSelf(self)
if (_tempFilter) {
#warning 這是第二個(gè)坑棍好,用這種方式保存照片到相冊正常仗岸,官方demo種的相片保存會90度旋轉(zhuǎn)
// 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:@"沒有濾鏡不能進(jìn)行拍照"];
}
}
- (void)saveImageWriteToPhotosAlbum:(UIImage *)img {
ALAuthorizationStatus author = [ALAssetsLibrary authorizationStatus];
if (author == ALAuthorizationStatusRestricted || author == ALAuthorizationStatusDenied){
//無權(quán)限
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、本地&在線小視頻播放(略粗糙)
沒什么難度梳玫,感覺稍微有用點(diǎn)代碼是視頻下載這段
- (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寫入失敗");
}
}];
}
截圖
第一次寫爹梁,比較倉促,后期想到什么了再補(bǔ)提澎,一起交流學(xué)習(xí)姚垃。