iOS仿微信拍照和拍视频

公司项目,需要拍照片和拍摄视频,并添加水印。今天这里就先整理一下视频和照片的拍摄,水印另外讲解。
基于模块化思维,拍摄视频相关的方法、功能封装到唯一类中,这个类怎么设计,不用多讲,网上相关介绍很多。这里就讲解功能实现。

基本配置

我们通过AVFoundation框架来实现图片的获取和视频的获取

//捕获设备,通常是前置摄像头,后置摄像头,麦克风(音频输入)
@property (nonatomic ,strong)AVCaptureDevice *device;
//AVCaptureDeviceInput 代表输入设备,他使用AVCaptureDevice 来初始化
@property (nonatomic ,strong)AVCaptureDeviceInput *input;
//当启动摄像头开始捕获输入
@property (nonatomic ,strong)AVCaptureMetadataOutput *output;
//视频文件的输出
@property (nonatomic ,strong)AVCaptureMovieFileOutput *movieOutput;
//照片输出流
@property (nonatomic ,strong)AVCaptureStillImageOutput *ImageOutPut;
////图像预览层,实时显示捕获的图像
@property (nonatomic)AVCaptureVideoPreviewLayer *previewLayer;

获取图片的预览使用的是previewLayer 一个图层,需要添加到目标控制器view的layer上。

//使用AVMediaTypeVideo 指明self.device代表视频,默认使用后置摄像头进行初始化
  self.device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
//使用AVMediaTypeAudio,代表着音频(麦克风),如果需要音频可以不用
  AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
//初始化输入设备
self.input = [[AVCaptureDeviceInput alloc]initWithDevice:self.device error:nil];
AVCaptureDeviceInput *audioInput = [[AVCaptureDeviceInput alloc]initWithDevice:audioDevice error:nil];
//如果需要拍摄视频加上👇一句
 self.movieOutput = [[AVCaptureMovieFileOutput alloc]init];
//self.movieOutput.maxRecordedDuration = maxDuration;//拍摄时长,可以限定也可以无限
//self.movieOutput.minFreeDiskSpaceLimit = 10*1024*1024;//视频最小大小限制,如果限制了,会自动结束视频拍摄
//图片的输出
self.ImageOutPut = [[AVCaptureStillImageOutput alloc]init];
//生产多媒体回话session,通过会话链接输入、输出设备
self.session = [[AVCaptureSession alloc]init];
if ([self.session canSetSessionPreset:AVCaptureSessionPreset1920x1080]) {//设置清晰度
            [self.session setSessionPreset:AVCaptureSessionPreset1920x1080];
    }
//先判断是否可以添加设备,再执行add
if ([self.session canAddInput:self.input]) {
       [self.session addInput:self.input];
     }
if ([self.session canAddInput:audioInput]) {
            [self.session addInput:audioInput];
 }
 if ([self.session canAddOutput:self.ImageOutPut]) {
            [self.session addOutput:self.ImageOutPut];
  }
 if ([self.session canAddOutput:self.movieOutput]) {
            [self.session addOutput:self.movieOutput];
 }
//视频预览图层
self.previewLayer = [[AVCaptureVideoPreviewLayer alloc]initWithSession:self.session];
[self.previewLayer setBackgroundColor:[UIColor blackColor].CGColor];
[self.previewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
[self.previewLayer.connection setVideoOrientation:AVCaptureVideoOrientationLandscapeRight];
CGRect layerFrame = CGRectMake(0, 0, kScreenHeight, kScreenWidth);
[self.previewLayer setFrame:layerFrame];
[self.homeVc.view.layer insertSublayer:self.previewLayer atIndex:0];
//修改设备的属性,先加锁
 if ([self.device lockForConfiguration:nil]) {
       //自动白平衡
 if ([self.device isWhiteBalanceModeSupported:AVCaptureWhiteBalanceModeAutoWhiteBalance]) {
        [self.device setWhiteBalanceMode:AVCaptureWhiteBalanceModeAutoWhiteBalance];
            }
//解锁
   [self.device unlockForConfiguration];
        }
//开始启动做好拍照和录视频的准备,这个时候就可以在目标控制器上看到镜头采集的数据了
[self.session startRunning];

设备方向配置

[[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(orientationDidChangeNotification:)
                                                 name:UIDeviceOrientationDidChangeNotification
                                               object:nil]
//
if([UIDevice currentDevice].orientation == UIDeviceOrientationLandscapeLeft){
        [self.previewLayer.connection setVideoOrientation:AVCaptureVideoOrientationLandscapeRight];
}else if([UIDevice currentDevice].orientation == UIDeviceOrientationLandscapeRight){
        [self.previewLayer.connection setVideoOrientation:AVCaptureVideoOrientationLandscapeLeft];
}else{
        
}

拍照

AVCaptureConnection * videoConnection = [self.ImageOutPut connectionWithMediaType:AVMediaTypeVideo];
if (videoConnection ==  nil) {//视频的IO是否正常
        return;
}
AVCaptureVideoOrientation avcaptureOrientation = AVCaptureVideoOrientationLandscapeRight;
if([UIDevice currentDevice].orientation == UIDeviceOrientationLandscapeLeft){
        avcaptureOrientation = AVCaptureVideoOrientationLandscapeRight;
}else if([UIDevice currentDevice].orientation == UIDeviceOrientationLandscapeRight){
        avcaptureOrientation = AVCaptureVideoOrientationLandscapeLeft;
 }
[videoConnection setVideoOrientation:avcaptureOrientation];
[videoConnection setVideoScaleAndCropFactor:1];
//获取当前帧图片
[self.ImageOutPut captureStillImageAsynchronouslyFromConnection:videoConnection completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) {
if (imageDataSampleBuffer == nil) {
            return;
 }
 NSData *imageData =  [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];
//获取当前相机的方向(前还是后)
AVCaptureDevicePosition position = [[self.input device] position];
UIImage *image = [UIImage imageWithData:imageData];
if (position == AVCaptureDevicePositionFront) {
      UIImageOrientation flipImageOrientation = (image.imageOrientation + 3) % 8;
      finalImg = [UIImage imageWithCGImage: image.CGImage scale: image.scale orientation:flipImageOrientation];
 }

水印

UIImage *img = [UIImage imageNamed:@"ic_muyuan_logo"];//水印
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();//创建颜色
//Bitmap上下文
CGContextRef context = CGBitmapContextCreate(NULL, image.size.width, image.size.height, 8, 44 * image.size.width, colorSpace, kCGImageAlphaPremultipliedFirst);
CGContextDrawImage(context, CGRectMake(0, 0, image.size.width, image.size.height), image.CGImage);//将image绘至context上下文中
CGContextDrawImage(context, CGRectMake(image.size.width - 200,  image.size.height - 150, 150, 47), img.CGImage);//将img绘至context上下文中
//   CGContextSetRGBFillColor(context, 0.0, 1.0, 1.0, 1);//设置颜色
//Create image ref from the context
CGImageRef imageMasked = CGBitmapContextCreateImage(context);//创建CGImage
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
 UIImage *finalImg = image;
 finalImg = [UIImage imageWithCGImage:imageMasked];//加盖水印的图片

拍摄视频

视频的拍摄需要有开始和结束的操作
1、开始拍摄,需要设定视频文件的临时存放地址

NSString *path = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
NSURL *documentDirUrl = [NSURL fileURLWithPath:path isDirectory:YES];
NSURL *fileURL = [NSURL URLWithString:@"movieOut.mp4" relativeToURL:documentDirUrl];
//开启视频拍摄,拍摄的过程中出错和拍摄完成都在delegate中
[self.movieOutput startRecordingToOutputFileURL:fileURL recordingDelegate:self];

2、拍摄过程:

[self.movieOutput stopRecording];完成拍摄
[self.movieOutpu pauseRecording]暂停
[self.movieOutpu resumeRecording]重启拍摄,可以更新文件地址
3、拍摄过程中delegate

//暂停录制
- (void)captureOutput:(AVCaptureFileOutput *)output didPauseRecordingToOutputFileAtURL:(NSURL *)fileURL fromConnections:(NSArray<AVCaptureConnection *> *)connections{
    NSLog(@" fileURL is %@",fileURL.path);
    if (self.recordAction) {
        self.recordAction(fileURL, NSError.new);
    }
}
//结束录制、不论是主动完成还是被动停止,只要拍摄停止都会调用这个方法
- (void)captureOutput:(AVCaptureFileOutput *)output didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray<AVCaptureConnection *> *)connections error:(NSError *)error{
    NSLog(@" outputFileURL is %@",outputFileURL.path);
    if (self.recordAction) {
        self.recordAction(outputFileURL, error);
    }
}

长按圆环动画

环形动画很简单,就是两个CAShapeLayer,一个当作背景,一个在画弧度。

backCircleLayer = [CAShapeLayer layer];
backCircleLayer.frame=self.bounds;
backCircleLayer.fillColor=nil;
progressLayer = [CAShapeLayer layer];
progressLayer.frame = self.bounds;
progressLayer.lineCap = kCALineCapRound;
[self.layer addSublayer:backCircleLayer];
[self.layer addSublayer:progressLayer];

backCirclelayer的宽度、颜色、都可以作为属性暴露在接口中,让用户自己定义

-(void)setLineWidth:(CGFloat)lineWidth{
    _lineWidth = lineWidth;
    backCircleLayer.lineWidth = lineWidth;
    progressLayer.lineWidth = lineWidth;
    //
    UIBezierPath * backpath = [UIBezierPath bezierPathWithArcCenter:self.center radius:(CGRectGetWidth(self.bounds)-lineWidth)/2.f startAngle:0 endAngle:M_PI*2
                                                           clockwise:YES];
    backCircleLayer.path = backpath.CGPath;
}
-(void)setStrokeColor:(UIColor *)strokeColor{
    _strokeColor = strokeColor;
    if (_strokeColor) {
        backCircleLayer.strokeColor = strokeColor.CGColor;
    }
}
-(void)setProgressColor:(UIColor *)progressColor{
    _progressColor = progressColor;
    if (_progressColor) {
        progressLayer.strokeColor = progressColor.CGColor;
    }
}

动画的过程,需要通过计时器来累加绘制。👇初始化动画开始的初始弧度和弧度变化步长

preAngle = preInterval/self.duration * 2*M_PI;
    startAngle = -M_PI/2;
   if (!self.clockwise) {
       startAngle = 3 * M_PI/2;
       preAngle = -preAngle;
   }

动画的开始、变化、结束的过程比较简单就不细说了

-(void)start{
    if (_lineWidth<=0) {
        NSLog(@"进度条宽度需大于0");
        return;
    }
    if (_duration<=0) {
        NSLog(@"动画时间需大于0");
        return;
    }
    if (self.timer) {
        [self.timer invalidate];
        self.timer = nil;
    } 
   stopTimer = NO;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:preInterval target:self selector:@selector(changeProgress) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
-(void)changeProgress{
    if (stopTimer) {
        return;
    }
   UIBezierPath *progressPath =  [UIBezierPath bezierPathWithArcCenter:self.center radius:(CGRectGetWidth(self.bounds)-_lineWidth)/2.f startAngle: startAngle endAngle:startAngle+preAngle clockwise:self.clockwise];
    progressLayer.path = progressPath.CGPath;
    NSLog(@"curent Angle is %f",startAngle);
    startAngle = startAngle+preAngle;
    if ( fabs(startAngle - (self.clockwise? -M_PI/2:3*M_PI/2))<1e-6 ) {//判断两个Double值是否相等
        stopTimer = YES;
        [self.timer invalidate];
        self.timer = nil;
    }
}
-(void)end{
    stopTimer = YES;
    if (self.timer) {
        [self.timer invalidate];
        self.timer = nil;
        CGFloat tempflt =self.clockwise? -M_PI/2:3*M_PI/2;
        UIBezierPath *progressPath =  [UIBezierPath bezierPathWithArcCenter:self.center radius:(CGRectGetWidth(self.bounds)-_lineWidth)/2.f startAngle:tempflt endAngle:tempflt clockwise:self.clockwise];
         progressLayer.path = progressPath.CGPath;
    }
}

视频预览和使用

上面讲述了拍照、拍摄视频、进度圆环动画等实现的过程,还缺少最后一点儿,就是怎么预览拍摄的视频,这就需要一个视频播放器,如果只是简单的使用拍摄到的视频,那么使用AVPlayerViewController,就可以展示拍摄的视频。
如果要使用视频,或者要对视频进行二次加工,比如美颜、添加水印、或者添加音乐等等。这就需要在视频播放器上自定义功能按钮,这个时候就需要自定义视频播放器,微信就是这样。
如何创建自定义的视频播放器,网上教程很多,我这里就简单的讲解一下过程步骤(AVPlayer)
1、把网络和本地视频转化为NSURL,加载到播放器

AVPlayerItem *playItem = [AVPlayerItem playerItemWithURL:url];
AVPlayer *player = [AVPlayer playerWithPlayerItem:playItem];
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
playerLayer.frame = view.bounds;
playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;//视频填充模式
[view.layer addSublayer:playerLayer];

至于播放、暂停、快进等自定义的控制按钮,需要用户自己在view上展示,视频播放状态需要通过KVO或者NotificationCenter,自己处理。

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

推荐阅读更多精彩内容