mp4视频播放不渐进播放的问题

mp4视频文件头中,包含一些元数据。元数据包含:视频的宽度高度、视频时长、编码格式等。mp4元数据通常在视频文件的头部,这样播放器在读取文件时会最先读取视频的元数据,然后开始播放视频。

当然也存在这样一种情况:mp4视频的元数据处于视频文件最后,这样播放器在加载视频文件时,一直读取到最后,才读取到视频信息,然后开始播放。如果缺少元数据,也是这样的情况。这就出现了mp4视频不支持边加载、边播放的问题, 当然 现在很多浏览器h5 等 都会自动处理这个问题,并自动查找到后面的元数据信息, 然后可以渐进播放, 但是flash 确无法做到,
我们 mp4info.exe, 查看这些信息

如图

moov 信息得在mdat 前面 才行
如果不是那就得处理下视频, 可以用ffmpeg 处理 自行google 很简单,
在深入说下: 应该可行方法,
1 像现在的浏览器一样 去自动查询, flash 中做 感觉没意思,都快死了,
2 服务端去检查处理, 然后返回给flash
这两个方案都比较艰巨,投入和回收不成正比, 最好还是直接转换文件简单明了 速度快,资源消耗也少.

iOS中为直播APP集成美颜功能

iOS中为直播APP集成美颜功能 获取GPUImage 处理后的buffer

From: http://www.jianshu.com/p/dde412cab8db

最近需要给直播项目中添加美颜的功能,调研了很多SDK和开源代码(视决,涂图,七牛,金山云,videoCore等),综合成本/效果/对项目侵入性,最后决定使用一款基于GPUImage实现的 BeautifyFaceDemo美颜滤镜。

关于滤镜代码和实现思路可以到BeautifyFace Github和作者琨君简书中查看。

集成GPUImageBeautifyFilter和GPUImage Framework

首先需要集成好GPUImage,通过观察目前iOS平台,90%以上美颜方案都是基于这个框架来做的。
原来项目中的AVCaptureDevice需要替换成GPUImageVideoCamera,删除诸如AVCaptureSession/AVCaptureDeviceInput/AVCaptureVideoDataOutput这种GPUImage实现了的部分。修改一些生命周期,摄像头切换,横竖屏旋转等相关逻辑,保证前后行为统一。

声明需要的属性

@property (nonatomic, strong) GPUImageVideoCamera *videoCamera;  
//屏幕上显示的View
@property (nonatomic, strong) GPUImageView *filterView;
//BeautifyFace美颜滤镜
@property (nonatomic, strong) GPUImageBeautifyFilter *beautifyFilter;

然后初始化

self.sessionPreset = AVCaptureSessionPreset1280x720;
self.videoCamera = [[GPUImageVideoCamera alloc] initWithSessionPreset:self.sessionPreset cameraPosition:AVCaptureDevicePositionBack];

self.filterView = [[GPUImageView alloc] init];
[self.view insertSubview:self.filterView atIndex:1]; //省略frame的相关设置

//这里我在GPUImageBeautifyFilter中增加个了初始化方法用来设置美颜程度intensity
self.beautifyFilter = [[GPUImageBeautifyFilter alloc] initWithIntensity:0.6];

为filterView增加美颜滤镜

[self.videoCamera addTarget:self.beautifyFilter];
[self.beautifyFilter addTarget:self.filterView];

然后调用startCameraCapture方法就可以看到效果了

[self.videoCamera startCameraCapture];

到这里,仅仅是屏幕显示的内容带有滤镜效果,而作为直播应用,还需要输出带有美颜效果的视频流

输出带有美颜效果的视频流

刚开始集成的时候碰见一个坑,原本的逻辑是实现AVCaptureVideoDataOutputSampleBufferDelegate方法来获得原始帧

- (void) captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection;

而GPUImageVideoCamera也实现了一个类似的代理:

@protocol GPUImageVideoCameraDelegate <NSObject>
@optional
- (void)willOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer;
@end

而替换之后发现输出的流依旧是未经美颜的图像,看了实现后发现果不其然,GPUImageVideoCameraDelegate还是通过AVCaptureVideoDataOutputSampleBufferDelegate直接返回的数据,所以想输出带有滤镜的流这里就得借助GPUImageRawDataOutput了

CGSize outputSize = {720, 1280};
GPUImageRawDataOutput *rawDataOutput = [[GPUImageRawDataOutput alloc] initWithImageSize:CGSizeMake(outputSize.width, outputSize.height) resultsInBGRAFormat:YES];
[self.beautifyFilter addTarget:rawDataOutput];

这个GPUImageRawDataOutput其实就是beautifyFilter的输出工具,可在setNewFrameAvailableBlock方法的block中获得带有滤镜效果的数据

__weak GPUImageRawDataOutput *weakOutput = rawDataOutput;
__weak typeof(self) weakSelf = self;

[rawDataOutput setNewFrameAvailableBlock:^{
    __strong GPUImageRawDataOutput *strongOutput = weakOutput;
    [strongOutput lockFramebufferForReading];

    // 这里就可以获取到添加滤镜的数据了
    GLubyte *outputBytes = [strongOutput rawBytesForImage];
    NSInteger bytesPerRow = [strongOutput bytesPerRowInOutput];
    CVPixelBufferRef pixelBuffer = NULL;
    CVPixelBufferCreateWithBytes(kCFAllocatorDefault, outputSize.width, outputSize.height, kCVPixelFormatType_32BGRA, outputBytes, bytesPerRow, nil, nil, nil, &pixelBuffer);

    // 之后可以利用VideoToolBox进行硬编码再结合rtmp协议传输视频流了
    [weakSelf encodeWithCVPixelBufferRef:pixelBuffer];

    [strongOutput unlockFramebufferAfterReading];
    CFRelease(pixelBuffer);

}];

目前依旧存在的问题

经过和其他产品对比,GPUImageBeautifyFilter磨皮效果和花椒最为类似。这里采用双边滤波, 花椒应该用了高斯模糊实现。同印客对比,美白效果一般。

还存在些关于性能的问题:

1 调用setNewFrameAvailableBlock后很多机型只能跑到不多不少15fps
2 在6s这代机型上温度很高,帧率可到30fps但不稳定

Update(8-13)

  1. 关于性能问题,最近把项目中集成的美颜滤镜(BeautifyFace)里用到的 GPUImageCannyEdgeDetectionFilter 替换为 GPUImageSobelEdgeDetectionFilter 会有很大改善,而且效果几乎一致,6s经过长时间测试没有再次出现高温警告了。(替换也十分简单,直接改俩处类名/变量名就可以了)

  2. 分享一个BUG,最近发现当开启美颜的时候,关闭直播内存竟然没有释放。分析得出GPUImageRawDataOutput的setNewFrameAvailableBlock方法的block参数仍然保持着self,解决思路就是将GPUImageRawDataOutput移除。

先附上之前的相关release代码:

[self.videoCamera stopCameraCapture];
[self.videoCamera removeInputsAndOutputs];
[self.videoCamera removeAllTargets];

开始以为camera调用removeAllTargets会把camera上面的filter,以及filter的output一同释放,但实际camera并不会’帮忙’移除filter的target,所以需要添加:

[self.beautifyFilter removeAllTargets]; //修复开启美颜内存无法释放的问题

关闭美颜output是直接加在camera上,camera直接removeAllTargets就可以;
开启美颜output加在filter上,camera和filter都需要removeAllTargets。


oc 版本的转图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/ CIContext 的 - render:toBitmap:rowBytes:bounds:format:colorSpace 据说这个会因为系统问题ios9.0 ,产生内存泄漏
// AVFoundation 捕捉视频帧,很多时候都需要把某一帧转换成 image
+ (CGImageRef)imageFromSampleBufferRef:(CMSampleBufferRef)sampleBufferRef
{
@autoreleasepool { // 可以加一个自动内存管理
// 为媒体数据设置一个CMSampleBufferRef
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBufferRef);
// 锁定 pixel buffer 的基地址
CVPixelBufferLockBaseAddress(imageBuffer, 0);
// 得到 pixel buffer 的基地址
void *baseAddress = CVPixelBufferGetBaseAddress(imageBuffer);
// 得到 pixel buffer 的行字节数
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
// 得到 pixel buffer 的宽和高
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
// 创建一个依赖于设备的 RGB 颜色空间
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();//CGColorSpaceCreateDeviceGray
/// 据说GPUImage 只能只用这个
//CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
// 用抽样缓存的数据创建一个位图格式的图形上下文(graphic context)对象
CGContextRef context = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
//根据这个位图 context 中的像素创建一个 Quartz image 对象
CGImageRef quartzImage = CGBitmapContextCreateImage(context);
// 解锁 pixel buffer
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
// 释放 context 和颜色空间
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
// 用 Quzetz image 创建一个 UIImage 对象
// UIImage *image = [UIImage imageWithCGImage:quartzImage];
// 释放 Quartz image 对象
// CGImageRelease(quartzImage);
return quartzImage;
}
}

swift 版本获取buffer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
///摄像头
var videoCamera:GPUImageVideoCamera!
var movieWriter:GPUImageMovieWriter!
var filterGroup:GPUImageFilterGroup!
var gpuImageView:GPUImageView!
var recSize = CGSize(width: 640, height: 480)
/// 创建测试GPUIamge 放假录
func createGPUImage(){
filterGroup = GPUImageFilterGroup()
videoCamera = GPUImageVideoCamera(sessionPreset: AVCaptureSessionPreset640x480, cameraPosition: AVCaptureDevicePosition.back)
videoCamera.outputImageOrientation = .portrait
videoCamera.addAudioInputsAndOutputs() // 开启声音捕获
videoCamera.horizontallyMirrorRearFacingCamera = false
videoCamera.horizontallyMirrorFrontFacingCamera = false// 镜像策略
let filter = GPUImageSepiaFilter()
videoCamera.addTarget(filterGroup)
filterGroup.addFilter(filter)
videoCamera.delegate = self
// videoCamera.outputTextureOptions =
gpuImageView = GPUImageView(frame: self.view.bounds)
self.view.addSubview(gpuImageView)
// 必须设置开始和结尾不然白屏
filterGroup.initialFilters = [filter]
filterGroup.terminalFilter = filter
filterGroup.addTarget(gpuImageView)
let dataoutput = GPUImageRawDataOutput(imageSize: recSize, resultsInBGRAFormat: true)
filterGroup.addTarget(dataoutput)
weak var weakDataoutput = dataoutput
weak var weakSelf = self
dataoutput?.newFrameAvailableBlock = {
if let weakDataoutput = weakDataoutput{
weakDataoutput.lockFramebufferForReading()
let outbytes = weakDataoutput.rawBytesForImage
let bytesPerRow = weakDataoutput.bytesPerRowInOutput()
var pixelBuffer:CVPixelBuffer? = nil
CVPixelBufferCreateWithBytes(kCFAllocatorDefault, Int(weakSelf!.recSize.width), Int(weakSelf!.recSize.height), kCVPixelFormatType_32BGRA, outbytes!, Int(bytesPerRow), nil, nil, nil, &pixelBuffer)
// 如果直播就可以发送这个流的 pixelBuffer
weakDataoutput.unlockFramebufferAfterReading()
//swift 不需要CFRelease(pixelBuffer)
}
}
videoCamera.startCapture()
// DispatchQueue.main.async {
// self.startREC()
// DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+5) {
// self.stopREC()
// }
// }
}
func startREC(){
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0]+"/test.mov"
unlink(path.cString(using: String.Encoding.utf8))
movieWriter = GPUImageMovieWriter(movieURL: URL(fileURLWithPath: path), size: recSize)
movieWriter.encodingLiveVideo = true
videoCamera.audioEncodingTarget = movieWriter
filterGroup.addTarget(movieWriter)
movieWriter.startRecording()
}
func stopREC(){
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0]+"/test.mov"
filterGroup.removeAllTargets()
videoCamera.audioEncodingTarget = nil
movieWriter.finishRecording()
UISaveVideoAtPathToSavedPhotosAlbum(path, self, nil, nil)
}

swift 3 的转码 CMSampleBuffer -> UIImage

    func willOutputSampleBuffer(_ sampleBuffer: CMSampleBuffer!) {
                                               // CMSampleBufferRef
        if  sampleBuffer != nil{
//        let cimage = BuilderVideo.image(fromSampleBufferRef: sampleBuffer)
            let myPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
            let myCIimage         = CIImage(cvPixelBuffer: myPixelBuffer!)
           let videoImage        = UIImage(ciImage: myCIimage)
            DispatchQueue.main.async {
                self.testImageView.image = videoImage
            }

        }
//        CGImageRelease


    }

}


    //参考http://stackoverflow.com/questions/41623186/cmsamplebuffer-from-avcapturevideodataoutput-unexpectedly-found-nil

iOS GPUImage源码解读(一)

iOS GPUImage源码解读(一)
From: https://syxblog.com/2017/04/1884.html

GPUImage是iOS上一个基于OpenGL进行图像处理的开源框架,内置大量滤镜,架构灵活,可以在其基础上很轻松地实现各种图像处理功能。本文主要向大家分享一下项目的核心架构、源码解读及使用心得。

GPUImage有哪些特性

1.丰富的输入组件

摄像头、图片、视频、OpenGL纹理、二进制数据、UIElement(UIView, CALayer)

2.大量现成的内置滤镜(4大类)

1). 颜色类(亮度、色度、饱和度、对比度、曲线、白平衡…)

2). 图像类(仿射变换、裁剪、高斯模糊、毛玻璃效果…)

3). 颜色混合类(差异混合、alpha混合、遮罩混合…)

4). 效果类(像素化、素描效果、压花效果、球形玻璃效果…)

3.丰富的输出组件

UIView、视频文件、GPU纹理、二进制数据

4.灵活的滤镜链

滤镜效果之间可以相互串联、并联,调用管理相当灵活。

5.接口易用

滤镜和OpenGL资源的创建及使用都做了统一的封装,简单易用,并且内置了一个cache模块实现了framebuffer的复用。

6.线程管理

OpenGLContext不是多线程安全的,GPUImage创建了专门的contextQueue,所有的滤镜都会扔到统一的线程中处理。

7.轻松实现自定义滤镜效果

继承GPUImageFilter自动获得上面全部特性,无需关注上下文的环境搭建,专注于效果的核心算法实现即可。

基本用法

// 获取一张图片

效果如图:

iOS GPUImage源码解读(一)

整个框架的目录结构

iOS GPUImage源码解读(一)

核心架构

iOS GPUImage源码解读(一)

基本上每个滤镜都继承自GPUImageFilter;

而GPUImageFilter作为整套框架的核心;

接收一个GPUImageFrameBuffer输入;

调用GLProgram渲染处理;

输出一个GPUImageFrameBuffer;

把输出的GPUImageFrameBuffer传给通过targets属性关联的下级滤镜;

直到传递至最终的输出组件;

核心架构可以整体划分为三块:输入、滤镜处理、输出

接下来我们就深入源码,看看GPUImage是如何获取数据、传递数据、处理数据和输出数据的

获取数据

GPUImage提供了多种不同的输入组件,但是无论是哪种输入源,获取数据的本质都是把图像数据转换成OpenGL纹理。这里就以视频拍摄组件(GPUImageVideoCamera)为例,来讲讲GPUImage是如何把每帧采样数据传入到GPU的。

GPUImageVideoCamera里大部分代码都是对摄像头的调用管理,不了解的同学可以去学习一下AVFoundation(传送门)。摄像头拍摄过程中每一帧都会有一个数据回调,在GPUImageVideoCamera中对应的处理回调的方法为:

- (void)processVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer;

iOS的每一帧摄像头采样数据都会封装成CMSampleBufferRef;CMSampleBufferRef除了包含图像数据、还包含一些格式信息、图像宽高、时间戳等额外属性;

摄像头默认的采样格式为YUV420,关于YUV格式大家可以自行搜索学习一下(传送门):

iOS GPUImage源码解读(一)

YUV420按照数据的存储方式又可以细分成若干种格式,这里主要是kCVPixelFormatType_420YpCbCr8BiPlanarFullRange和kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange两种;

两种格式都是planar类型的存储方式,y数据和uv数据分开放在两个plane中;这样的数据没法直接传给GPU去用,GPUImageVideoCamera把两个plane的数据分别取出:

-
 (void)processVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer { // 
一大坨的代码用于获取采样数据的基本属性(宽、高、格式等等) ...... if ([GPUImageContext 
supportsFastTextureUpload] && captureAsYUV) { 
CVOpenGLESTextureRef luminanceTextureRef = ; CVOpenGLESTextureRef 
chrominanceTextureRef = ; if (CVPixelBufferGetPlaneCount(cameraFrame) 
> 0) // Check for YUV planar inputs to do RGB conversion {

注意CVOpenGLESTextureCacheCreateTextureFromImage中对于internalFormat的设置;

通常我们创建一般纹理的时候都会设成GL_RGBA,传入的图像数据也会是rgba格式的;

而这里y数据因为只包含一个通道,所以设成了GL_LUMINANCE(灰度图);

uv数据则包含2个通道,所以设成了GL_LUMINANCE_ALPHA(带alpha的灰度图);

另外uv纹理的宽高只设成了图像宽高的一半,这是因为yuv420中,每个相邻的2x2格子共用一份uv数据;

数据传到GPU纹理后,再通过一个颜色转换(yuv->rgb)的shader(shader是OpenGL可编程着色器,可以理解为GPU侧的代码,关于shader需要一些OpenGL编程基础(传送门)),绘制到目标纹理:

 // fullrange varying highp vec2 textureCoordinate; uniform sampler2D 
luminanceTexture; uniform sampler2D chrominanceTexture; uniform mediump 
mat3 colorConversionMatrix; void main { mediump vec3 yuv; lowp vec3 rgb;
 yuv.x = texture2D(luminanceTexture, textureCoordinate).r; yuv.yz = 
texture2D(chrominanceTexture, textureCoordinate).ra - vec2(0.5, 0.5); 
rgb = colorConversionMatrix * yuv; gl_FragColor = vec4(rgb, 1); }


 // videorange varying highp vec2 textureCoordinate; uniform sampler2D 
luminanceTexture; uniform sampler2D chrominanceTexture; uniform mediump 
mat3 colorConversionMatrix; void main { mediump vec3 yuv; lowp vec3 rgb;
 yuv.x = texture2D(luminanceTexture, textureCoordinate).r - 
(16.0/255.0); yuv.yz = texture2D(chrominanceTexture, 
textureCoordinate).ra - vec2(0.5, 0.5); rgb = colorConversionMatrix * 
yuv; gl_FragColor = vec4(rgb, 1); }

注意yuv420fullrange和yuv420videorange的数值范围是不同的,因此转换公式也不同,这里会有2个颜色转换shader,根据实际的采样格式选择正确的shader;

渲染输出到目标纹理后就得到一个转换成rgb格式的GPU纹理,完成了获取输入数据的工作;

传递数据

GPUImage的图像处理过程,被设计成了滤镜链的形式;输入组件、效果滤镜、输出组件串联在一起,每次推动渲染的时候,输入数据就会按顺序传递,经过处理,最终输出。

GPUImage设计了一个GPUImageInput协议,定义了GPUImageFilter之间传入数据的方法:

-
 (void)setInputFramebuffer:(GPUImageFramebuffer *)newInputFramebuffer 
atIndex:(NSInteger)textureIndex { firstInputFramebuffer = 
newInputFramebuffer; [firstInputFramebuffer lock]; }

firstInputFramebuffer属性用来保存输入纹理;

GPUImageFilter作为单输入滤镜基类遵守了GPUImageInput协议,GPUImage还提供了GPUImageTwoInputFilter, GPUImageThreeInputFilter等多输入filter的基类。

这里还有一个很重要的入口方法用于推动数据流转:

-
 (void)newFrameReadyAtTime:(CMTime)frameTime 
atIndex:(NSInteger)textureIndex { ...... [self 
renderToTextureWithVertices:imageVertices textureCoordinates:[[self 
class] textureCoordinatesForRotation:inputRotation]]; [self 
informTargetsAboutNewFrameAtTime:frameTime]; }

每个滤镜都是由这个入口方法开始启动,这个方法包含2个调用

1). 首先调用render方法进行效果渲染

2). 调用informTargets方法将渲染结果推到下级滤镜

GPUImageFilter继承自GPUImageOutput,定义了输出数据,向后传递的方法:

- (void)notifyTargetsAboutNewOutputTexture;

但是这里比较奇怪的是滤镜链的传递实际并没有用notifyTargets方法,而是用了前面提到的informTargets方法:

-
 (void)informTargetsAboutNewFrameAtTime:(CMTime)frameTime { ...... // 
Get all targets the framebuffer so they can grab a lock on it for 
(id<GPUImageInput> currentTarget in targets) { if (currentTarget 
!= self.targetToIgnoreForUpdates) { NSInteger indexOfObject = [targets 
indexOfObject:currentTarget]; NSInteger textureIndex = 
[[targetTextureIndices objectAtIndex:indexOfObject] integerValue]; [self
 setInputFramebufferForTarget:currentTarget atIndex:textureIndex]; 
[currentTarget setInputSize:[self outputFrameSize] 
atIndex:textureIndex]; } } ...... // Trigger processing last, so that 
our unlock comes first in serial execution, avoiding the need for a 
callback for (id<GPUImageInput> currentTarget in targets) { if 
(currentTarget != self.targetToIgnoreForUpdates) { NSInteger 
indexOfObject = [targets indexOfObject:currentTarget]; NSInteger 
textureIndex = [[targetTextureIndices objectAtIndex:indexOfObject] 
integerValue]; [currentTarget newFrameReadyAtTime:frameTime 
atIndex:textureIndex]; } } }

GPUImageOutput定义了一个targets属性来保存下一级滤镜,这里可以注意到targets是个数组,因此滤镜链也支持并联结构。可以看到这个方法主要做了2件事情:

1). 对每个target调用setInputFramebuffer方法把自己的渲染结果传给下级滤镜作为输入

2). 对每个target调用newFrameReadyAtTime方法推动下级滤镜启动渲染

滤镜之间通过targets属性相互衔接串在一起,完成了数据传递工作。

iOS GPUImage源码解读(一)

处理数据

前面提到的renderToTextureWithVertices:方法便是每个滤镜必经的渲染入口。每个滤镜都可以设置自己的shader,重写该渲染方法,实现自己的效果:

-
 (void)renderToTextureWithVertices:(const GLfloat *)vertices 
textureCoordinates:(const GLfloat *)textureCoordinates { ...... 
[GPUImageContext setActiveShaderProgram:filterProgram]; 
outputFramebuffer = [[GPUImageContext sharedFramebufferCache] 
fetchFramebufferForSize:[self sizeOfFBO] 
textureOptions:self.outputTextureOptions onlyTexture:NO]; 
[outputFramebuffer activateFramebuffer]; ...... [self 
setUniformsForProgramAtIndex:0]; glClearColor(backgroundColorRed, 
backgroundColorGreen, backgroundColorBlue, backgroundColorAlpha); 
glClear(GL_COLOR_BUFFER_BIT); glActiveTexture(GL_TEXTURE2); 
glBindTexture(GL_TEXTURE_2D, [firstInputFramebuffer texture]); 
glUniform1i(filterInputTextureUniform, 2); 
glVertexAttribPointer(filterPositionAttribute, 2, GL_FLOAT, 0, 0, 
vertices); glVertexAttribPointer(filterTextureCoordinateAttribute, 2, 
GL_FLOAT, 0, 0, textureCoordinates); glDrawArrays(GL_TRIANGLE_STRIP, 0, 
4); ...... }

上面这个是GPUImageFilter的默认方法,大致做了这么几件事情:

1). 向frameBufferCache申请一个outputFrameBuffer

2). 将申请得到的outputFrameBuffer激活并设为渲染对象

3). glClear清除画布

4). 设置输入纹理

5). 传入顶点

6). 传入纹理坐标

7). 调用绘制方法

再来看看GPUImageFilter使用的默认shader:

 // vertex shader attribute vec4 position; attribute vec4 
inputTextureCoordinate; varying vec2 textureCoordinate; void main { 
gl_Position = position; textureCoordinate = inputTextureCoordinate.xy; }


 // fragment shader varying highp vec2 textureCoordinate; uniform 
sampler2D inputImageTexture; void main { gl_FragColor = 
texture2D(inputImageTexture, textureCoordinate); }

这个shader实际上啥也没做,VertexShader(顶点着色器)就是把传入的顶点坐标和纹理坐标原样做光栅化处理,FragmentShader(片段着色器)就是从纹理取出原始色值直接输出,最终效果就是把图片原样渲染到画面。

输出数据

比较常用的主要是GPUImageView和GPUImageMovieWriter。

GPUImageView继承自UIView,用于实时预览,用法非常简单

1). 创建GPUImageView

2). 串入滤镜链

3). 插到视图里去

UIView的contentMode、hidden、backgroundColor等属性都可以正常使用

里面比较关键的方法主要有这么2个:

// 申明自己的CALayer为CAEAGLLayer+ (Class)layerClass { return [CAEAGLLayer class]; }

-
 (void)createDisplayFramebuffer { [GPUImageContext 
useImageProcessingContext]; glGenFramebuffers(1, 
&displayFramebuffer); glBindFramebuffer(GL_FRAMEBUFFER, 
displayFramebuffer); glGenRenderbuffers(1, &displayRenderbuffer); 
glBindRenderbuffer(GL_RENDERBUFFER, displayRenderbuffer); 
[[[GPUImageContext sharedImageProcessingContext] context] 
renderbufferStorage:GL_RENDERBUFFER 
fromDrawable:(CAEAGLLayer*)self.layer]; GLint backingWidth, 
backingHeight; glGetRenderbufferParameteriv(GL_RENDERBUFFER, 
GL_RENDERBUFFER_WIDTH, &backingWidth); 
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, 
&backingHeight); ...... glFramebufferRenderbuffer(GL_FRAMEBUFFER, 
GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, displayRenderbuffer); ...... }

创建frameBuffer和renderBuffer时把renderBuffer和CALayer关联在一起;这是iOS内建的一种GPU渲染输出的联动方法;

这样newFrameReadyAtTime渲染过后画面就会输出到CALayer。

GPUImageMovieWriter主要用于将视频输出到磁盘;

里面大量的代码都是在设置和使用AVAssetWriter,不了解的同学还是得去看AVFoundation;

这里主要是重写了newFrameReadyAtTime:方法:

-
 (void)newFrameReadyAtTime:(CMTime)frameTime 
atIndex:(NSInteger)textureIndex { ...... GPUImageFramebuffer 
*inputFramebufferForBlock = firstInputFramebuffer; glFinish; 
runAsynchronouslyOnContextQueue(_movieWriterContext, ^{ ...... // Render
 the frame with swizzled colors, so that they can be uploaded quickly as
 BGRA frames [_movieWriterContext useAsCurrentContext]; [self 
renderAtInternalSizeUsingFramebuffer:inputFramebufferForBlock]; 
CVPixelBufferRef pixel_buffer = ; if ([GPUImageContext 
supportsFastTextureUpload]) { pixel_buffer = renderTarget; 
CVPixelBufferLockBaseAddress(pixel_buffer, 0); } else { CVReturn status =
 CVPixelBufferPoolCreatePixelBuffer (, [assetWriterPixelBufferInput 
pixelBufferPool], &pixel_buffer); if ((pixel_buffer == ) || (status 
!= kCVReturnSuccess)) { CVPixelBufferRelease(pixel_buffer); return; } 
else { CVPixelBufferLockBaseAddress(pixel_buffer, 0); GLubyte 
*pixelBufferData = (GLubyte *)CVPixelBufferGetBaseAddress(pixel_buffer);
 glReadPixels(0, 0, videoSize.width, videoSize.height, GL_RGBA, 
GL_UNSIGNED_BYTE, pixelBufferData); } } ......

这里有几个地方值得注意:

1). 在取数据之前先调了一下glFinish,CPU和GPU之间是类似于client-server的关系,CPU侧调用OpenGL命令后并不是同步等待OpenGL完成渲染再继续执行的,而glFinish命令可以确保OpenGL把队列中的命令都渲染完再继续执行,这样可以保证后面取到的数据是正确的当次渲染结果。

2). 取数据时用了supportsFastTextureUpload判断,这是个从iOS5开始支持的一种CVOpenGLESTextureCacheRef和CVImageBufferRef的映射(映射的创建可以参看获取数据中的

CVOpenGLESTextureCacheCreateTextureFromImage),通过这个映射可以直接拿到CVPixelBufferRef而不需要再用glReadPixel来读取数据,这样性能更好。

最后归纳一下本文涉及到的知识点

  1. AVFoundation

摄像头调用、输出视频都会用到AVFoundation

  1. YUV420

视频采集的数据格式

  1. OpenGL shader

GPU的可编程着色器

  1. CAEAGLLayer

iOS内建的GPU到屏幕的联动方法

  1. fastTextureUpload

iOS5开始支持的一种CVOpenGLESTextureCacheRef和CVImageBufferRef的映射

实现微信小视频iOS代码

From: http://www.pclic.com/ios/108553.html

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

这里先罗列遇到的主要问题:  
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); }

至此,微信小视频的难点(我认为的)就解决了,至于其他的实现代码请看demo就基本实现了,demo可以从这里下载。

视频暂停录制 http://www.gdcl.co.uk/2013/02/20/iPhone-Pause.html
视频crop绿边解决 http://stackoverflow.com/questions/22883525/avassetexportsession-giving-me-a-green-border-on-right-and-bottom-of-output-vide
视频裁剪:http://stackoverflow.com/questions/15737781/video-capture-with-11-aspect-ratio-in-ios/16910263#16910263
CMSampleBufferRef转image https://developer.apple.com/library/ios/qa/qa1702/_index.html
微信小视频分析 http://www.jianshu.com/p/3d5ccbde0de1

感谢以上文章的作者

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

IOS解码MP4播放

From: http://hawk0620.github.io/blog/2015/11/17/ios-play-video/

从体验说起

  对比微信和Instagram可以发现播放视频的两个思路:微信的处理是把视频加载好后播放,这样确保了视频是完整的,用户很直观视频是否下载完成,不影响用户观看视频的体验;而Instagram的做法是边加载边播,当网络不给力的时候,视频就卡在那里,给用户增加了观看视频的焦虑,并且用户还得自己判断下视频是不是加载完成了,最不幸的是,当视频的网络请求不可达时,不能给出加载失败的提示引导用户重新加载,只能滑动列表触发刷新。

播放视频的实现

1、通过实践,我发现Instagram采用的应该是AVPlayer实现的。

AVPlayerItem可以通过远程URL创建出来,并且支持流的形式播放,还可以添加视频播放卡住和视频播放完成的两个观察者:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(itemDidBufferPlaying:) name:AVPlayerItemPlaybackStalledNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(itemDidFinishPlaying:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];

但遗憾的是,我没有找到视频加载失败的观察者。

2、结合微信团队的技术分享[链接][1],得知微信的视频播放是采用AVAssetReader+AVAssetReaderTrackOutput,根据微信的思路,自己也尝试实现了一番: buffer的转换:

[1]: http://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=207686973&idx=1&sn=1883a6c9fa0462dd5596b8890b6fccf6&scene=0#wechat_redirect
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (CGImageRef) imageFromSampleBuffer:(CMSampleBufferRef) sampleBuffer {
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
// Lock the base address of the pixel buffer
CVPixelBufferLockBaseAddress(imageBuffer, 0);
// Get the number of bytes per row for the pixel buffer
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
// Get the pixel buffer width and height
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
//Generate image to edit
unsigned char* pixel = (unsigned char *)CVPixelBufferGetBaseAddress(imageBuffer);
CGColorSpaceRef colorSpace=CGColorSpaceCreateDeviceRGB();
CGContextRef context=CGBitmapContextCreate(pixel, width, height, 8, bytesPerRow, colorSpace, kCGBitmapByteOrder32Little|kCGImageAlphaPremultipliedFirst);
CGImageRef image = CGBitmapContextCreateImage(context);
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
return image;
}

视频的解码:

AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[[NSURL alloc] initFileURLWithPath:path] options:nil];
NSError *error;
AVAssetReader* reader = [[AVAssetReader alloc] initWithAsset:asset error:&error];
NSArray* videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];
AVAssetTrack* videoTrack = [videoTracks objectAtIndex:0];

int m_pixelFormatType = kCVPixelFormatType_32BGRA;
NSDictionary* options = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt: (int)m_pixelFormatType] forKey:(id)kCVPixelBufferPixelFormatTypeKey];
AVAssetReaderTrackOutput* videoReaderOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:videoTrack outputSettings:options];
[reader addOutput:videoReaderOutput];
[reader startReading];

// 读取视频每一个buffer转换成CGImageRef
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
   CMSampleBufferRef audioSampleBuffer = NULL;
   while ([reader status] == AVAssetReaderStatusReading && videoTrack.nominalFrameRate > 0) {
   CMSampleBufferRef sampleBuffer = [videoReaderOutput copyNextSampleBuffer];
   CGImageRef image = [self imageFromSampleBuffer:sampleBuffer];
   if (self.delegate && [self.delegate respondsToSelector:@selector(mMovieDecoder:onNewVideoFrameReady:)]) {
        [self.delegate mMovieDecoder:self onNewVideoFrameReady:image];
    }
   if(sampleBuffer) {
       if(audioSampleBuffer) { // release old buffer.
            CFRelease(audioSampleBuffer);
            audioSampleBuffer = nil;
       }
       audioSampleBuffer = sampleBuffer;
   } else {
       break;
   }

// 休眠的间隙刚好是每一帧的间隔
   [NSThread sleepForTimeInterval:CMTimeGetSeconds(videoTrack.minFrameDuration)];
 }
 // decode finish
 float durationInSeconds = CMTimeGetSeconds(asset.duration);
  if (self.delegate && [self.delegate respondsToSelector:@selector(mMovieDecoderOnDecodeFinished:duration:)]) {
     [self.delegate mMovieDecoderOnDecodeFinished:self duration:durationInSeconds];
   }
});

处理每一帧CGImageRef的回调:

- (void)mMovieDecoder:(VideoDecoder *)decoder onNewVideoFrameReady:(CGImageRef)imgRef {
    __weak PlayBackView *weakView = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        weakView.layer.contents = (__bridge id _Nullable)(imgRef);
    });
}

处理视频解码完成的回调:

images即每一帧传上来的CGImageRef的数组
- (void)mMovieDecoderOnDecodeFinished:(VideoDecoder *)decoder images:(NSArray *)imgs duration:(float)duration {
    __weak PlayBackView *weakView = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        weakView.layer.contents = nil;

        CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath: @"contents"];
        animation.calculationMode = kCAAnimationDiscrete;
        animation.duration = duration;
        animation.repeatCount = HUGE; //循环播放
        animation.values = images; // NSArray of CGImageRefs
        [weakView.layer addAnimation:animation forKey: @"contents"];
    });
}

//写在最后: 看到多种这样的方式,基本出处差不多, 然后发现,10秒的视频还行 超过的话基本上都会爆内存 , 30fps 算的话 10秒300张图片, 我现在的处理方式是间隔取图 ,想当于减少fps

iOS中block的循环引用问题

From: http://www.jianshu.com/p/492be28d63c4

本文主要介绍ARC下block的循环引用问题,举例说明引起循环引用的场景和相应的解决方案。

在讲block的循环引用问题之前,我们需要先了解一下iOS的内存管理机制和block的基本知识

iOS的内存管理机制

Objective-C在iOS中不支持GC(垃圾回收)机制,而是采用的引用计数的方式管理内存。

引用计数(Reference Count)

在引用计数中,每一个对象负责维护对象所有引用的计数值。当一个新的引用指向对象时,引用计数器就递增,当去掉一个引用时,引用计数就递减。当引用计数到零时,该对象就将释放占有的资源。

我们通过开关房间的灯为例来说明引用计数机制。

引用《Pro Multithreading and Memory Management for iOS and OS X》中的图片

图中,“需要照明的人数”即对应我们要说的引用计数值。

  1. 第一个人进入办公室,“需要照明的人数”加1,计数值从0变为1,因此需要开灯;
  2. 之后每当有人进入办公室,“需要照明的人数”就加1。如计数值从1变成2;
  3. 每当有人下班离开办公室,“需要照明的人数”加减1如计数值从2变成1;
  4. 最后一个人下班离开办公室时,“需要照明的人数”减1。计数值从1变成0,因此需要关灯。

在Objective-C中,”对象“相当于办公室的照明设备,”对象的使用环境“相当于进入办公室的人。上班进入办公室的人对办公室照明设备发出的动作,与Objective-C中的对应关系如下表

对照明设备所做的动作对Objective-C对象所做的动作

开灯
生成对象

需要照明
持有对象

不需要照明
释放对象

关灯
废弃对象

使用计数功能计算需要照明的人数,使办公室的照明得到了很好的管理。同样,使用引用计数功能,对象也就能得到很好的管理,这就是Objective-C内存管理,如下图所示

引用《Pro Multithreading and Memory Management for iOS and OS X》中的图片

MRC(Manual Reference Counting)中引起应用计数变化的方法

Objective-C对象方法说明

alloc/new/copy/mutableCopy
创建对象,引用计数加1

retain
引用计数加1

release
引用计数减1

dealloc
当引用计数为0时调用

[NSArray array]
引用计数不增加,由自动释放池管理

[NSDictionary dictionary]
引用计数不增加,由自动释放池管理

自动释放池

关于自动释放,不是本文的重点,这里就不讲了。

ARC(Automatic Reference Counting)中内存管理

Objective-C对象所有权修饰符说明

__strong
对象默认修饰符,对象强引用,在对象超出作用域时失效。其实就相当于retain操作,超出作用域时执行release操作

__weak
弱引用,不持有对象,对象释放时会将对象置nil。

__unsafe_unretained
弱引用,不持有对象,对象释放时不会将对象置nil。

__autoreleasing
自动释放,由自动释放池管理对象

block的基本知识

block的基本知识这里就不细说了,可以看看我的文章说说Objective-C中的block

循环引用问题

两个对象相互持有,这样就会造成循环引用,如下图所示

两个对象相互持有

图中,对象A持有对象B,对象B持有对象A,相互持有,最终导致两个对象都不能释放。

block中循环引用问题

由于block会对block中的对象进行持有操作,就相当于持有了其中的对象,而如果此时block中的对象又持有了该block,则会造成循环引用。如下,

typedef void(^block)();

@property (copy, nonatomic) block myBlock;
@property (copy, nonatomic) NSString *blockString;

- (void)testBlock {
    self.myBlock = ^() {
        //其实注释中的代码,同样会造成循环引用
        NSString *localString = self.blockString;
          //NSString *localString = _blockString;
          //[self doSomething];
    };
}

注:以下调用注释掉的代码同样会造成循环引用,因为不管是通过self.blockString还是_blockString,或是函数调用[self doSomething],因为只要 block中用到了对象的属性或者函数,block就会持有该对象而不是该对象中的某个属性或者函数。

当有someObj持有self对象,此时的关系图如下。

当someObj对象release self对象时,self和myblock相互引用,retainCount都为1,造成循环引用

解决方法:

__weak typeof(self) weakSelf = self;
self.myBlock = ^() {
    NSString *localString = weakSelf.blockString;
};

使用__weak修饰self,使其在block中不被持有,打破循环引用。开始状态如下

当someObj对象释放self对象时,Self的retainCount为0,走dealloc,释放myBlock对象,使其retainCount也为0。

其实以上循环引用的情况很容易发现,因为此时Xcode就会报警告。而发生在多个对象间的时候,Xcode就检测不出来了,这往往就容易被忽略。

//ClassB
@interface ClassB : NSObject
@property (strong, nonatomic) ClassA *objA;
- (void)doSomething;
@end

//ClassA
@property (strong, nonatomic) ClassB *objB;
@property (copy, nonatomic) block myBlock;

- (void)testBlockRetainCycle {
    ClassB* objB = [[ClassB alloc] init];
    self.myBlock = ^() {
        [objB doSomething];
    };
    objB.objA = self;
}

解决方法:

- (void)testBlockRetainCycle {
    ClassB* objB = [[ClassB alloc] init];
    __weak typeof(objB) weakObjB = objB;
    self.myBlock = ^() {
        [weakObjB doSomething];
    };
    objB.objA = self;
}

将objA对象weak,使其不在block中被持有

注:以上使用__weak打破循环的方法只在ARC下才有效,在MRC下应该使用__block

或者,在block执行完后,将block置nil,这样也可以打破循环引用

- (void)testBlockRetainCycle {
    ClassB* objB = [[ClassB alloc] init];
    self.myBlock = ^() {
        [objB doSomething];
    };
    objA.objA = self;
    self.myBlock();
    self.myBlock = nil;
}

这样做的缺点是,block只会执行一次,因为block被置nil了,要再次使用的话,需要重新赋值。

一些不会造成循环引用的block

在开发工程中,发现一些同学并没有完全理解循环引用,以为只要有block的地方就会要用__weak来修饰对象,这样完全没有必要,以下几种block是不会造成循环引用的。

大部分GCD方法

dispatch_async(dispatch_get_main_queue(), ^{
    [self doSomething];
});

因为self并没有对GCD的block进行持有,没有形成循环引用。目前我还没碰到使用GCD导致循环引用的场景,如果某种场景self对GCD的block进行了持有,则才有可能造成循环引用。

block并不是属性值,而是临时变量

- (void)doSomething {
    [self testWithBlock:^{
        [self test];
    }];
}

- (void)testWithBlock:(void(^)())block {
    block();
}

- (void)test {
    NSLog(@"test");
}

这里因为block只是一个临时变量,self并没有对其持有,所以没有造成循环引用

block使用对象被提前释放

看下面例子,有这种情况,如果不只是ClassA持有了myBlock,ClassB也持有了myBlock。

当ClassA被someObj对象释放后

此时,ClassA对象已经被释放,而myBlock还是被ClassB持有,没有释放;如果myBlock这个时被调度,而此时ClassA已经被释放,此时访问的ClassA将是一个nil对象(使用__weak修饰,对象释放时会置为nil),而引发错误。

另一个常见错误使用是,开发者担心循环引用错误(如上所述不会出现循环引用的情况),使用__weak。比如

__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
    [weakSelf doSomething];
});

因为将block作为参数传给dispatch_async时,系统会将block拷贝到堆上,而且block会持有block中用到的对象,因为dispatch_async并不知道block中对象会在什么时候被释放,为了确保系统调度执行block中的任务时其对象没有被意外释放掉,dispatch_async必须自己retain一次对象(即self),任务完成后再release对象(即self)。但这里使用__weak,使dispatch_async没有增加self的引用计数,这使得在系统在调度执行block之前,self可能已被销毁,但系统并不知道这个情况,导致block执行时访问已经被释放的self,而达不到预期的结果。

注:如果是在MRC模式下,使用__block修饰self,则此时block访问被释放的self,则会导致crash。

该场景下的代码

// ClassA.m
- (void)test {
    __weak MyClass* weakSelf = self;
    double delayInSeconds = 10.0f;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        NSLog(@"%@", weakSelf);
    });
}

// ClassB.m
- (void)doSomething {
    NSLog(@"do something");
    ClassA *objA = [[ClassA alloc] init];
    [objA test];
}

运行结果

[5988:435396] do something
[5988:435396] self:(null)

解决方法:

对于这种场景,就不应该使用__weak来修饰对象,让dispatch_after对self进行持有,保证block执行时self还未被释放。

block执行过程中对象被释放

还有一种场景,在block执行开始时self对象还未被释放,而执行过程中,self被释放了,此时访问self时,就会发生错误。

对于这种场景,应该在block中对 对象使用__strong修饰,使得在block期间对 对象持有,block执行结束后,解除其持有。

- (void)testBlockRetainCycle {
    ClassA* objA = [[ClassA alloc] init];
    __weak typeof(objA) weakObjA = objA;
    self.myBlock = ^() {
        __strong typeof(weakObjA) strongWeakObjA = weakObjA;
        [strongWeakObjA doSomething];
    };
    objA.objA = self;
}

注:此方法只能保证在block执行期间对象不被释放,如果对象在block执行执行之前已经被释放了,该方法也无效。

GPUImage自定义滤镜

From: http://www.itiger.me/?p=143

Tiger | iOS |

GPUImage 是一个基于 GPU 图像和视频处理的开源 iOS 框架。由于使用 GPU 来处理图像和视频,所以速度非常快,它的作者 BradLarson 称在 iPhone4 上其处理速度是使用 CPU 来处理的 100 倍 (CoreImage 也能使用 GPU 来处理图像,但我觉得 CoreImage 还是慢)。除了速度上的优势,GPUImage 还提供了很多很棒的图像处理滤镜,但有时候这些基本功能仍然无法满足实际开发中的需求,不用担心 GPUImage 支持自定义滤镜。

GPUImage 自定义滤镜需要使用 OpenGL 着色语言( GLSL )编写 Fragment Shader(片段着色器),除此之外你可能还需要一点点图像处理相关的知识。下面我将尝试通过 GPUImage 中的 GPUImageColorInvertFilter(反色滤镜)来讲解一下它的运作过程。

先看[.h 文件:][4]

[4]: https://github.com/BradLarson/GPUImage/blob/2f8e61c4ffcdfa93ee3d8f39f8110e61cfc3daf1/framework/Source/GPUImageColorInvertFilter.h

#import “GPUImageFilter.h”

@interface GPUImageColorInvertFilter : GPUImageFilter
{
}

@end

很简单,可以看出 GPUImageColorInvertFilter 是继承了 GPUImageFilter

然后看 [.m 文件][5] 中 @implementation 之前的一段代码

[5]: https://github.com/BradLarson/GPUImage/blob/2f8e61c4ffcdfa93ee3d8f39f8110e61cfc3daf1/framework/Source/GPUImageColorInvertFilter.m
1
2
3
4
5
6
7
8
9
10
11
12
13
NSString *const kGPUImageInvertFragmentShaderString = SHADER_STRING
(
varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
void main()
{
lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
gl_FragColor = vec4((1.0 - textureColor.rgb), textureColor.a);
}
);

第 1 行,可以看到 SHADER_STRING 宏中包含着我们的 Shader (着色器)代码,我们的着色器字符串赋给一个 const NSString 对象(这个常量将在 GPUImageFilter 及其子类的初始化过程中用来设置 filter)。

第 2、3 行声明了两个变量。

varying 变量是Vertex 和 Fragment Shader(顶点着色器和片段着色器)之间做数据传递用的,一般 Vertex Shader(顶点着色器) 修改 varying 变量的值,然后 Fragment Shader(片段着色器)使用该varying变量的值。因此varying 变量在 Vertex 和 Fragment Shader 中声明必须一致。放到这里,也就是说 textureCoordinate 必须叫这个名字不能改。

highp 声明 textureCoordinate 精度(相应的还有mediumplowp)。

vec2 声明textureCoordinate 是一个二维向量。

uniform 声明 inputImageTexture 是外部程序传递给 Shader 的变量, Shader 程序内部只能用,不能改。 sampler2D 声明变量是一个2D纹理。

第 4 行,相信你并不陌生,没错儿 Shader 也是从 main() 函数开始执行的。

第 5 行,texture2D 纹理取样器,根据纹理坐标返回纹理单元的值。

第 6 行,(1.0 - textureColor.rgb)textureColor也就是原图的 RGB 值,做了一个向量的减法,这是图像的反色算法,然后把经过反色的 RGB 值和原图的 Alpha 值组成一个新的 vec4(四维向量)值赋给 gl_FragColorgl_FragColor 是 Fragment Shader 预先定义的变量,赋给它的值就是该片段最终的颜色值。

Shader 到这里已经解释完了,可能你依然云里雾里,我来说一下我对这部分功能的理解,可能不对,但目前是管用的,方便你理解:GPUImage 中应该有一个 Vertex Shader,它对图像逐个像素扫描,通过 textureCoordinate 变量将当前的扫描坐标传递给我们的 Fragment Shader,inputImageTexture 包含我们要处理的图像的全部信息,在 Shader 程序内部通过 texture2D 得到 inputImageTexture 在当前位置 textureCoordinate 的 RGBA 值,运用图像处理知识,算出想要的新的 RGBA 值,把结果值赋给 gl_FragColor就算完成了。

现在我们继续看代码,在 Shader 之后是 GPUImageColorInvertFilter 的实现:

@implementation GPUImageColorInvertFilter

- (id)init;
{
    if (!(self = [super initWithFragmentShaderFromString:kGPUImageInvertFragmentShaderString]))
    {
        return nil;
    }

    return self;
}

@end     

很简单,就是使用刚才的着色器代码来设置 filter。这样一个新的滤镜就诞生了~

网路上关于 GPUImage 自定义滤镜 和 GLSL 的资料不是特别多,我斗胆把自己摸索到的理解在这里和你分享,希望对你有所帮助,如有错误指出欢迎指出,另外,可以去这里查阅 OpenGL GLSL 文档,玩的愉快 (●°u°●)​ 」

Custom, Filter, Fragment, GLSL, GPUImage, iOS, OpenGL, Shader, Vertex, 图像处理, 滤镜, 自定义

iOS 自定义相机, UIImagePickerController && AVCaptureSession (附微信小视频模仿demo)

iOS 自定义相机, UIImagePickerController && AVCaptureSession (附微信小视频模仿demo)

From: http://www.jianshu.com/p/fe37cab68f7d

今天介绍自定义相机的两种方式,一种是UIImagePickerController,一种是AVCaptureSession.

UIImagePickerController

UIImagePickerController非常方便简单,是苹果自己封装好了的一套API,效果如下:

AVCaptureSession

但是上面的 API只能进行简单的相机视图修改,有时无法满足我们的需求.例如我们需要更加复杂的OverlayerView(自定义相机视图),这时候我们就要自定义一个相机了.AVCaptureSession能帮助我们.

心急的人,完整Demo在最下边儿.其他朋友可依次看下去

UIImagePickerController

属性

首先看到,需要遵守的协议有2个,分别是UINavigationControllerDelegateUIImagePickerControllerDelegate

@property(nullable,nonatomic,weak)      id <UINavigationControllerDelegate, UIImagePickerControllerDelegate> delegate;    //需要遵守的协议有2个

还有一些基本设置,如输入源/媒体类型,还有allowsEditing为 YES 即可允许用户拍照后立即进行编辑,并且出现编辑框

//基本设置
@property(nonatomic)           UIImagePickerControllerSourceType     sourceType;            //图片输入源,相机或者相册,图库
@property(nonatomic,copy)      NSArray<NSString *>                   *mediaTypes;            //媒体类型
@property(nonatomic)           BOOL                                  allowsEditing             //是否允许用户编辑
@property(nonatomic)           BOOL                                  allowsImageEditing     //废弃,用上面的即可

这2个属性和视频相关

@property(nonatomic)           NSTimeInterval                        videoMaximumDuration     //最长摄制时间
@property(nonatomic)           UIImagePickerControllerQualityType    videoQuality             //视频摄制质量

这2个和自定义相机视图有关,注意,下面的只有当输入源为UIImagePickerControllerSourceTypeCamera才可用,否则崩溃

@property(nonatomic)           BOOL                                  showsCameraControls     //默认 YES, 设置为 NO 即可关闭所有默认 UI
@property(nullable, nonatomic,strong) __kindof UIView                *cameraOverlayView      //自定义相机覆盖视图
@property(nonatomic)           CGAffineTransform                     cameraViewTransform     //拍摄时屏幕view的transform属性,可以实现旋转,缩放功能

@property(nonatomic) UIImagePickerControllerCameraCaptureMode cameraCaptureMode             //设置模式为拍照或者摄像
@property(nonatomic) UIImagePickerControllerCameraDevice      cameraDevice                  //默认开启前置/后置摄像头
@property(nonatomic) UIImagePickerControllerCameraFlashMode   cameraFlashMode                //设置默认的闪光灯模式,有开/关/自动

方法

第一个是判断输入源的,一般用来判断是否支持拍照

typedef NS_ENUM(NSInteger, UIImagePickerControllerSourceType) {
    UIImagePickerControllerSourceTypePhotoLibrary,        //图库
    UIImagePickerControllerSourceTypeCamera,            //相机-->这个就是拍照
    UIImagePickerControllerSourceTypeSavedPhotosAlbum    //相册
};

+ (BOOL)isSourceTypeAvailable:(UIImagePickerControllerSourceType)sourceType;              //判断是否支持某种输入源

这个是判断是否支持前/后置摄像头

typedef NS_ENUM(NSInteger, UIImagePickerControllerCameraDevice) {
    UIImagePickerControllerCameraDeviceRear,            //后置
    UIImagePickerControllerCameraDeviceFront            //前置
};

+ (BOOL)isCameraDeviceAvailable:(UIImagePickerControllerCameraDevice)cameraDevice     //是否支持前/后置摄像头

拍照/摄像的方法

- (void)takePicture;        //手动调取拍照的方法,也可当做`-imagePickerController:didFinishPickingMediaWithInfo:`代理的回调,无法捕捉动态图像
- (BOOL)startVideoCapture;    //开始摄像
- (void)stopVideoCapture;    //停止摄像

代理相关

注意,UIImagePickerController不能直接取消,必须在收到以下代理方法后才可以 dismiss

@protocol UIImagePickerControllerDelegate<NSObject>
@optional

//这个已经废弃了,不用管
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:(UIImage *)image editingInfo:(nullable NSDictionary<NSString *,id> *)editingInfo;


//主要是下面这2个
//拍照后,点击使用照片和向此选取照片后都会调用以下代理方法
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info;

//点击取消的时候会调用,通常在此 dismiss 
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker;

@end

保存照片/视频到手机,三个比较重要的方法,记得导入AssetsLibrary.framework,并且在需要调用存储方法的的地方导入<AssetsLibrary/AssetsLibrary.h>

// 将照片保存到相册,回调方法必须用 - (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo;
UIKIT_EXTERN void UIImageWriteToSavedPhotosAlbum(UIImage *image, __nullable id completionTarget, __nullable SEL completionSelector, void * __nullable contextInfo);


//是否可以将视频保存到相册,通常在调用下面的方法之前会调用这个
UIKIT_EXTERN BOOL UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(NSString *videoPath) NS_AVAILABLE_IOS(3_1);

// 将视频保存到相册,回调方法必须用 - (void)video:(NSString *)videoPath didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo; 
UIKIT_EXTERN void UISaveVideoAtPathToSavedPhotosAlbum(NSString *videoPath, __nullable id completionTarget, __nullable SEL completionSelector, void * __nullable contextInfo) NS_AVAILABLE_IOS(3_1);

用法如下

- (void)savePic
{
    UIImageWriteToSavedPhotosAlbum(imgPath, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);    
}


- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo{

    if (error) {
        NSLog(@"保存照片过程中发生错误,错误信息:%@",error.localizedDescription);
    }else{
        NSLog(@"照片保存成功.");
    }
}

- (void)saveVideo
{
    UISaveVideoAtPathToSavedPhotosAlbum(videoPath,self, @selector(video:didFinishSavingWithError:contextInfo:), nil);//保存视频到相簿    
}

- (void)video:(NSString *)videoPath didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo
{
    if (error) {
        NSLog(@"保存视频过程中发生错误,错误信息:%@",error.localizedDescription);
    }else{
        NSLog(@"视频保存成功.");
    }
}

另外一个是ALAssetsLibrary,不过在 iOS9.0 已经全部被废弃了,改成了PHPhotoLibrary(这是 Photos 框架的一个组成部分,完整的可以点这个链接,关于这个框架的用法,改天再写一个吧)

9.0及以前的写法

//保存照片到相册
 [[[ALAssetsLibrary alloc]init] writeImageToSavedPhotosAlbum:[img CGImage] orientation:(ALAssetOrientation)img.imageOrientation completionBlock:^(NSURL *assetURL, NSError *error) {
    if (error) {
        NSLog(@"Save image fail:%@",error);
    }else{
        NSLog(@"Save image succeed.");
    }
}];

//保存视频到相册
[[[ALAssetsLibrary alloc]init] writeVideoAtPathToSavedPhotosAlbum:[NSURL URLWithString:videoPath] completionBlock:^(NSURL *assetURL, NSError *error) {

    if (error) {
        NSLog(@"Save video fail:%@",error);
    }else{
        NSLog(@"Save video succeed.");
    }

}];

AVCaptureSession

先放上一个微信小视频的模仿 Demo, 因为只能真机,所以比较嘈杂~大家别介意

流程

AVCaptureSession通过把设备的麦克风/摄像头(AVCaptureDevice)实例化成数据流输入对象(AVCaptureDeviceInput)后,再通过建立连接(AVCaptionConnection)将录制数据通过数据流输出对象(AVCaptureOutput)导出,而录制的时候咱们可以同步预览当前的录制界面(AVCaptureVideoPreviewLayer).

`AVCaptureSession`是一个会话对象,是设备音频/视频整个录制期间的管理者.
`AVCaptureDevice`其实是咱们的物理设备映射到程序中的一个对象.咱们可以通过其来操作:闪光灯,手电筒,聚焦模式等
`AVCaptureDeviceInput`是录制期间输入流数据的管理对象.
`AVCaptionConnection`是将输入流/输出流连接起来的连接对象,视频/音频稳定,预览与录制方向一致都在这里设置,还有audioChannels声道
`AVCaptureOutput`是输出流数据的管理对象,通过头文件可以看到有很多子类,而我们通常也使用其子类
`AVCaptureVideoPreviewLayer`是一个 `CALyer` ,可以让我们预览拍摄过程中的图像

详解各部分

此处,咱们就模仿一个微信小视频的 demo, 然后把各个主要步骤写在下面.

授权

首先获取授权AVAuthorizationStatus,我们需要获取哪个设备的使用权限,就进行请求,需要注意的是,如果用户未进行授权授权选择,咱们还要重复请求一次.

//获取授权
- (void)getAuthorization
{

    //此处获取摄像头授权
    switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo])
    {
        case AVAuthorizationStatusAuthorized:       //已授权,可使用    The client is authorized to access the hardware supporting a media type.
        {
            break;
        }
        case AVAuthorizationStatusNotDetermined:    //未进行授权选择     Indicates that the user has not yet made a choice regarding whether the client can access the hardware.
        {
            //则再次请求授权
            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
                if(granted){    //用户授权成功
                    return;
                } else {        //用户拒绝授权
                    return;
                }
            }];
            break;
        }
        default:                                    //用户拒绝授权/未授权
        {
            break;
        }
    }

}

相关枚举

/*
     AVAuthorizationStatusNotDetermined = 0,// 未进行授权选择

     AVAuthorizationStatusRestricted,    // 未授权,且用户无法更新,如家长控制情况下

     AVAuthorizationStatusDenied,       // 用户拒绝App使用

     AVAuthorizationStatusAuthorized,    // 已授权,可使用
     */

根据流程创建对象

AVCaptureSession

先创建本次小视频的会话对象_captureSession,设置视频分辨率.注意,这个地方设置的模式/分辨率大小将影响你后面拍摄照片/视频的大小

- (void)addSession
{
    _captureSession = [[AVCaptureSession alloc] init];

    if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset640x480]) {
        [_captureSession setSessionPreset:AVCaptureSessionPreset640x480];
    }
}

相关枚举

/*  通常支持如下格式
 (
 AVAssetExportPresetLowQuality,
 AVAssetExportPreset960x540,
 AVAssetExportPreset640x480,
 AVAssetExportPresetMediumQuality,
 AVAssetExportPreset1920x1080,
 AVAssetExportPreset1280x720,
 AVAssetExportPresetHighestQuality,
 AVAssetExportPresetAppleM4A
 )
 */
AVCaptureDevice

然后咱们要把设备接入进来了,依次是_videoDevice_audioDevice,还有注意,在给会话信息添加设备对象的时候,需要调用_captureSession的一个方法组beginConfigurationcommitConfiguration,最后,咱们把录制过程中的预览图层PreviewLayer也添加进来,设置完毕后,开启会话startRunning–>注意,不等于开始录制,在不再需要使用会话相关时,还需要stopRunning

[_captureSession beginConfiguration];

[self addVideo];
[self addAudio];
[self addPreviewLayer];

[_captureSession commitConfiguration];
[_captureSession startRunning];
video 相关
- (void)addVideo
{

    // 获取摄像头输入设备, 创建 AVCaptureDeviceInput 对象
    _videoDevice = [self deviceWithMediaType:AVMediaTypeVideo preferringPosition:AVCaptureDevicePositionBack];

    [self addVideoInput];
    [self addMovieOutput];
}

相关枚举

/* MediaType
     AVF_EXPORT NSString *const AVMediaTypeVideo                 NS_AVAILABLE(10_7, 4_0);       //视频
     AVF_EXPORT NSString *const AVMediaTypeAudio                 NS_AVAILABLE(10_7, 4_0);       //音频
     AVF_EXPORT NSString *const AVMediaTypeText                  NS_AVAILABLE(10_7, 4_0);
     AVF_EXPORT NSString *const AVMediaTypeClosedCaption         NS_AVAILABLE(10_7, 4_0);
     AVF_EXPORT NSString *const AVMediaTypeSubtitle              NS_AVAILABLE(10_7, 4_0);
     AVF_EXPORT NSString *const AVMediaTypeTimecode              NS_AVAILABLE(10_7, 4_0);
     AVF_EXPORT NSString *const AVMediaTypeMetadata              NS_AVAILABLE(10_8, 6_0);
     AVF_EXPORT NSString *const AVMediaTypeMuxed                 NS_AVAILABLE(10_7, 4_0);
     */

    /* AVCaptureDevicePosition
     typedef NS_ENUM(NSInteger, AVCaptureDevicePosition) {
     AVCaptureDevicePositionUnspecified         = 0,
     AVCaptureDevicePositionBack                = 1,            //后置摄像头
     AVCaptureDevicePositionFront               = 2             //前置摄像头
     } NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;
     */

下面是获取摄像头的方法

#pragma mark 获取摄像头-->前/后

- (AVCaptureDevice *)deviceWithMediaType:(NSString *)mediaType preferringPosition:(AVCaptureDevicePosition)position
{
    NSArray *devices = [AVCaptureDevice devicesWithMediaType:mediaType];
    AVCaptureDevice *captureDevice = devices.firstObject;

    for ( AVCaptureDevice *device in devices ) {
        if ( device.position == position ) {
            captureDevice = device;
            break;
        }
    }

    return captureDevice;
}

//下面这2个也可以获取前后摄像头,不过有一定的风险,假如手机又问题,找不到对应的 UniqueID 设备,则呵呵了
//- (AVCaptureDevice *)frontCamera
//{
//    return [AVCaptureDevice deviceWithUniqueID:@"com.apple.avfoundation.avcapturedevice.built-in_video:1"];
//}
//
//- (AVCaptureDevice *)backCamera
//{
//    return [AVCaptureDevice deviceWithUniqueID:@"com.apple.avfoundation.avcapturedevice.built-in_video:0"];
//}

添加视频输入对象AVCaptureDeviceInput,根据输入设备初始化输入对象,用户获取输入数据,将视频输入对象添加到会话 (AVCaptureSession) 中

- (void)addVideoInput
{
    NSError *videoError;

    _videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:_videoDevice error:&videoError];
    if (videoError) {
        NSLog(@"---- 取得摄像头设备时出错 ------ %@",videoError);
        return;
    }

    if ([_captureSession canAddInput:_videoInput]) {
        [_captureSession addInput:_videoInput];
    }
}

接下来是视频输出对象AVCaptureMovieFileOutput及连接管理对象AVCaptureConnection,还有视频稳定设置preferredVideoStabilizationMode,视频旋转方向setVideoOrientation

- (void)addMovieOutput
{
    _movieOutput = [[AVCaptureMovieFileOutput alloc] init];

    if ([_captureSession canAddOutput:_movieOutput]) {
        [_captureSession addOutput:_movieOutput];

        AVCaptureConnection *captureConnection = [_movieOutput connectionWithMediaType:AVMediaTypeVideo];
        if ([captureConnection isVideoStabilizationSupported]) {
            captureConnection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
        }
        captureConnection.videoScaleAndCropFactor = captureConnection.videoMaxScaleAndCropFactor;
    }

}

相关枚举

//设置视频旋转方向
/*
 typedef NS_ENUM(NSInteger, AVCaptureVideoOrientation) {
 AVCaptureVideoOrientationPortrait           = 1,
 AVCaptureVideoOrientationPortraitUpsideDown = 2,
 AVCaptureVideoOrientationLandscapeRight     = 3,
 AVCaptureVideoOrientationLandscapeLeft      = 4,
 } NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;
 */
audio 相关

咱们再接着添加音频相关的,包括音频输入设备AVCaptureDevice,音频输入对象AVCaptureDeviceInput,并且将音频输入对象添加到会话

- (void)addAudio
{
    NSError *audioError;
    _audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
    _audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:_audioDevice error:&audioError];
    if (audioError) {
        NSLog(@"取得录音设备时出错 ------ %@",audioError);
        return;
    }
    if ([_captureSession canAddInput:_audioInput]) {
        [_captureSession addInput:_audioInput];
    }
}
AVCaptureVideoPreviewLayer

通过会话AVCaptureSession创建预览层AVCaptureVideoPreviewLayer,设置填充模式videoGravity,预览图层方向videoOrientation,并且设置 layer 想要显示的位置

- (void)addPreviewLayer
{
    _captureVideoPreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_captureSession];
    _captureVideoPreviewLayer.frame = self.view.layer.bounds;
//    _captureVideoPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;

    _captureVideoPreviewLayer.connection.videoOrientation = [_movieOutput connectionWithMediaType:AVMediaTypeVideo].videoOrientation;
    _captureVideoPreviewLayer.position = CGPointMake(self.view.width*0.5,self.videoView.height*0.5);

    CALayer *layer = self.videoView.layer;
    layer.masksToBounds = true;
    [self.view layoutIfNeeded];
    [layer addSublayer:_captureVideoPreviewLayer];

}

相关枚举

/* 填充模式
     Options are AVLayerVideoGravityResize, AVLayerVideoGravityResizeAspect and AVLayerVideoGravityResizeAspectFill. AVLayerVideoGravityResizeAspect is default.
     */

最后,开始录制视频,结束录制视频,重新录制

- (void)startRecord
{
    [_movieOutput startRecordingToOutputFileURL:[self outPutFileURL] recordingDelegate:self];
}

- (NSURL *)outPutFileURL
{
    return [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@%@", NSTemporaryDirectory(), @"outPut.mov"]];
}

- (void)stopRecord
{
    [_movieOutput stopRecording];
}
录制相关delegate

包括开始录制,录制结束等

- (void)captureOutput:(AVCaptureFileOutput *)captureOutput didStartRecordingToOutputFileAtURL:(NSURL *)fileURL fromConnections:(NSArray *)connections
{
    NSLog(@"---- 开始录制 ----");
}

- (void)captureOutput:(AVCaptureFileOutput *)captureOutput didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray *)connections error:(NSError *)error
{
    NSLog(@"---- 录制结束 ----%@ ",captureOutput.outputFileURL);

    if (self.canSave) {
        [self pushToPlay:captureOutput.outputFileURL];
        self.canSave = NO;
    }
}
压缩/保存视频

咱们需要把录制完毕的视频保存下来.而通常录制完毕的视频是很大的,咱们需要压缩一下再保存.

可以通过AVAssetExportSession来进行压缩,并且可以优化网络shouldOptimizeForNetworkUse,设置转后的格式outputFileType,并且开启异步压缩exportAsynchronouslyWithCompletionHandler

#pragma mark 保存压缩
- (NSURL *)compressedURL
{
    return [NSURL fileURLWithPath:[[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true) lastObject] stringByAppendingPathComponent:[NSString stringWithFormat:@"compressed.mp4"]]];
}

- (CGFloat)fileSize:(NSURL *)path
{
    return [[NSData dataWithContentsOfURL:path] length]/1024.00 /1024.00;
}

// 压缩视频
- (IBAction)compressVideo:(id)sender
{
    NSLog(@"开始压缩,压缩前大小 %f MB",[self fileSize:self.videoUrl]);

    AVURLAsset *avAsset = [[AVURLAsset alloc] initWithURL:self.videoUrl options:nil];
    NSArray *compatiblePresets = [AVAssetExportSession exportPresetsCompatibleWithAsset:avAsset];
    if ([compatiblePresets containsObject:AVAssetExportPresetLowQuality]) {

        AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:avAsset presetName:AVAssetExportPreset640x480];
        exportSession.outputURL = [self compressedURL];
        exportSession.shouldOptimizeForNetworkUse = true;
        exportSession.outputFileType = AVFileTypeMPEG4;
        [exportSession exportAsynchronouslyWithCompletionHandler:^{
            if ([exportSession status] == AVAssetExportSessionStatusCompleted) {
                NSLog(@"压缩完毕,压缩后大小 %f MB",[self fileSize:[self compressedURL]]);
                [self saveVideo:[self compressedURL]];
            }else{
                NSLog(@"当前压缩进度:%f",exportSession.progress);
            }
        }];
    }
}

- (void)saveVideo:(NSURL *)outputFileURL
{
    ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
    [library writeVideoAtPathToSavedPhotosAlbum:outputFileURL
                                completionBlock:^(NSURL *assetURL, NSError *error) {
                                    if (error) {
                                        NSLog(@"保存视频失败:%@",error);
                                    } else {
                                        NSLog(@"保存视频到相册成功");
                                    }
                                }];
}
播放录制完的视频以及重复播放

播放录制的视频,以及重复播放

- (void)create
{
    _playItem = [AVPlayerItem playerItemWithURL:self.videoUrl];
    _player = [AVPlayer playerWithPlayerItem:_playItem];
    _playerLayer =[AVPlayerLayer playerLayerWithPlayer:_player];
    _playerLayer.frame = CGRectMake(200, 200, 100, 100);
    _playerLayer.videoGravity=AVLayerVideoGravityResizeAspectFill;//视频填充模式
    [self.view.layer addSublayer:_playerLayer];
    [_player play];
}

-(void)playbackFinished:(NSNotification *)notification
{
    [_player seekToTime:CMTimeMake(0, 1)];
    [_player play];
}

交互相关

接下来,是一些和交互相关的,比如切换摄像头,开关闪光灯,还有白平衡啥的.

注意,改变设备属性前一定要首先调用lockForConfiguration方法加锁,调用完之后使用unlockForConfiguration方法解锁.

意义是—进行设备属性修改期间,先锁定设备,防止多处同时修改设备.因为可能有多处不同的修改,咱们将其封装起来最好

-(void)changeDevicePropertySafety:(void (^)(AVCaptureDevice *captureDevice))propertyChange{

    //也可以直接用_videoDevice,但是下面这种更好
    AVCaptureDevice *captureDevice= [_videoInput device];
    NSError *error;

    BOOL lockAcquired = [captureDevice lockForConfiguration:&error];
    if (!lockAcquired) {
        NSLog(@"锁定设备过程error,错误信息:%@",error.localizedDescription);
    }else{
        [_captureSession beginConfiguration];
        propertyChange(captureDevice);
        [captureDevice unlockForConfiguration];
        [_captureSession commitConfiguration];
    }
}
开/关闪光灯

闪光模式开启后,并无明显感觉,所以还需要开启手电筒,并且开启前先判断是否自持,否则崩溃

- (IBAction)changeFlashlight:(UIButton *)sender {

    BOOL con1 = [_videoDevice hasTorch];    //支持手电筒模式
    BOOL con2 = [_videoDevice hasFlash];    //支持闪光模式

    if (con1 && con2)
    {
        [self changeDevicePropertySafety:^(AVCaptureDevice *captureDevice) {
            if (_videoDevice.flashMode == AVCaptureFlashModeOn)         //闪光灯开
            {
                [_videoDevice setFlashMode:AVCaptureFlashModeOff];
                [_videoDevice setTorchMode:AVCaptureTorchModeOff];
            }else if (_videoDevice.flashMode == AVCaptureFlashModeOff)  //闪光灯关
            {
                [_videoDevice setFlashMode:AVCaptureFlashModeOn];
                [_videoDevice setTorchMode:AVCaptureTorchModeOn];
            }
        }];
        sender.selected=!sender.isSelected;
    }else{
        NSLog(@"不能切换闪光模式");
    }
}
切换摄像头

根据现在正在使用的摄像头来判断需要切换的摄像头

- (IBAction)changeCamera{

    switch (_videoDevice.position) {
        case AVCaptureDevicePositionBack:
            _videoDevice = [self deviceWithMediaType:AVMediaTypeVideo preferringPosition:AVCaptureDevicePositionFront];
            break;
        case AVCaptureDevicePositionFront:
            _videoDevice = [self deviceWithMediaType:AVMediaTypeVideo preferringPosition:AVCaptureDevicePositionBack];
            break;
        default:
            return;
            break;
    }

    [self changeDevicePropertySafety:^(AVCaptureDevice *captureDevice) {
        NSError *error;
        AVCaptureDeviceInput *newVideoInput = [[AVCaptureDeviceInput alloc] initWithDevice:_videoDevice error:&error];

        if (newVideoInput != nil) {
            //必选先 remove 才能询问 canAdd
            [_captureSession removeInput:_videoInput];
            if ([_captureSession canAddInput:newVideoInput]) {
                [_captureSession addInput:newVideoInput];
                _videoInput = newVideoInput;
            }else{
                [_captureSession addInput:_videoInput];
            }

        } else if (error) {
            NSLog(@"切换前/后摄像头失败, error = %@", error);
        }
    }];

}
聚焦模式,曝光模式,拉近/远镜头(焦距)

同时存在单击和双击的手势,咱们如下设置,requireGestureRecognizerToFail的作用就是每次只生效一个手势

-(void)addGenstureRecognizer{

    UITapGestureRecognizer *singleTapGesture=[[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(singleTap:)];
    singleTapGesture.numberOfTapsRequired = 1;
    singleTapGesture.delaysTouchesBegan = YES;

    UITapGestureRecognizer *doubleTapGesture=[[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(doubleTap:)];
    doubleTapGesture.numberOfTapsRequired = 2;
    doubleTapGesture.delaysTouchesBegan = YES;

    [singleTapGesture requireGestureRecognizerToFail:doubleTapGesture];
    [self.videoView addGestureRecognizer:singleTapGesture];
    [self.videoView addGestureRecognizer:doubleTapGesture];
}

单击修改聚焦模式setFocusMode及聚焦点setFocusPointOfInterest,还有曝光模式setExposureMode及曝光点setExposurePointOfInterest

注意,摄像头的点范围是0~1,咱们需要把点转化一下,使用captureDevicePointOfInterestForPoint

-(void)singleTap:(UITapGestureRecognizer *)tapGesture{

    CGPoint point= [tapGesture locationInView:self.videoView];

    CGPoint cameraPoint= [_captureVideoPreviewLayer captureDevicePointOfInterestForPoint:point];
    [self setFocusCursorAnimationWithPoint:point];

    [self changeDevicePropertySafety:^(AVCaptureDevice *captureDevice) {

        //聚焦
        if ([captureDevice isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) {
            [captureDevice setFocusMode:AVCaptureFocusModeContinuousAutoFocus];
        }else{
            NSLog(@"聚焦模式修改失败");
        }

        //聚焦点的位置
        if ([captureDevice isFocusPointOfInterestSupported]) {
            [captureDevice setFocusPointOfInterest:cameraPoint];
        }

        //曝光模式
        if ([captureDevice isExposureModeSupported:AVCaptureExposureModeAutoExpose]) {
            [captureDevice setExposureMode:AVCaptureExposureModeAutoExpose];
        }else{
            NSLog(@"曝光模式修改失败");
        }

        //曝光点的位置
        if ([captureDevice isExposurePointOfInterestSupported]) {
            [captureDevice setExposurePointOfInterest:cameraPoint];
        }

    }];
}

下面是双击设置焦距videoZoomFactor

-(void)doubleTap:(UITapGestureRecognizer *)tapGesture{

    NSLog(@"双击");

    [self changeDevicePropertySafety:^(AVCaptureDevice *captureDevice) {
        if (captureDevice.videoZoomFactor == 1.0) {
            CGFloat current = 1.5;
            if (current < captureDevice.activeFormat.videoMaxZoomFactor) {
                [captureDevice rampToVideoZoomFactor:current withRate:10];
            }
        }else{
            [captureDevice rampToVideoZoomFactor:1.0 withRate:10];
        }
    }];
}

相关枚举

/*
         @constant AVCaptureFocusModeLocked 锁定在当前焦距
         Indicates that the focus should be locked at the lens' current position.

         @constant AVCaptureFocusModeAutoFocus 自动对焦一次,然后切换到焦距锁定
         Indicates that the device should autofocus once and then change the focus mode to AVCaptureFocusModeLocked.

         @constant AVCaptureFocusModeContinuousAutoFocus 当需要时.自动调整焦距
         Indicates that the device should automatically focus when needed.
         */

/*
         @constant AVCaptureExposureModeLocked  曝光锁定在当前值
         Indicates that the exposure should be locked at its current value.

         @constant AVCaptureExposureModeAutoExpose 曝光自动调整一次然后锁定
         Indicates that the device should automatically adjust exposure once and then change the exposure mode to AVCaptureExposureModeLocked.

         @constant AVCaptureExposureModeContinuousAutoExposure 曝光自动调整
         Indicates that the device should automatically adjust exposure when needed.

         @constant AVCaptureExposureModeCustom 曝光只根据设定的值来
         Indicates that the device should only adjust exposure according to user provided ISO, exposureDuration values.

         */

Demo

终于说完了,感觉越到后面越无力,还是放上一个 demo 吧,里面包含了上面介绍的两种方式.

Demo 下载地址

原创文章,转载请注明地址: https://kevinmky.github.io

在 iOS 上捕获视频

在 iOS 上捕获视频
转自https://objccn.io/issue-23-1/

随着每一代 iPhone 处理能力和相机硬件配置的提高,使用它来捕获视频也变得更加有意思。它们小巧,轻便,低调,而且与专业摄像机之间的差距已经变得非常小,小到在某些情况下,iPhone 可以真正替代它们。

这篇文章讨论了关于如何配置视频捕获管线 (pipeline) 和最大限度地利用硬件性能的一些不同选择。 这里有个使用了不同管线的样例 app,可以在 GitHub 查看。

UIImagePickerController

目前,将视频捕获集成到你的应用中的最简单的方法是使用 UIImagePickerController。这是一个封装了完整视频捕获管线和相机 UI 的 view controller。

在实例化相机之前,首先要检查设备是否支持相机录制:

1
2
3
4
5
6
7
8
if ([UIImagePickerController
isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
NSArray *availableMediaTypes = [UIImagePickerController
availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera];
if ([availableMediaTypes containsObject:(NSString *)kUTTypeMovie]) {
// 支持视频录制
}
}

然后创建一个 UIImagePickerController 对象,设置好代理便于进一步处理录制好的视频 (比如存到相册) 以及对于用户关闭相机作出响应:

1
2
3
4
UIImagePickerController *camera = [UIImagePickerController new];
camera.sourceType = UIImagePickerControllerSourceTypeCamera;
camera.mediaTypes = @[(NSString *)kUTTypeMovie];
camera.delegate = self;

这是你实现一个功能完善的摄像机所需要写的所有代码。

###相机配置
UIImagePickerController 提供了额外的配置选项。

通过设置 cameraDevice 属性可以选择一个特定的相机。这是一个 UIImagePickerControllerCameraDevice 枚举,默认情况下是 UIImagePickerControllerCameraDeviceRear,你也可以把它设置为 UIImagePickerControllerCameraDeviceFront。每次都应事先确认你想要设置的相机是可用的:

1
2
3
4
UIImagePickerController *camera = …
if ([UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront]) {
[camera setCameraDevice:UIImagePickerControllerCameraDeviceFront];
}

videoQuality 属性用于控制录制视频的质量。它允许你设置一个特定的编码预设,从而改变视频的比特率和分辨率。以下是六种预设:

前三种为相对预设 (low, medium, high)。这些预设的编码配置会因设备不同而不同。如果选择 high,那么你选定的相机会提供给你该设备所能支持的最高画质。后面三种是特定分辨率的预设 (640x480 VGA, 960x540 iFrame, 和 1280x720 iFrame)。

###自定义 UI
就像上面提到的,UIImagePickerController 自带一套相机 UI,可以直接使用。然而,你也可以自定义相机的控件,通过隐藏默认控件,然后创建带有控件的自定义视图,并覆盖在相机预览图层上面

1
2
3
UIView *cameraOverlay = …
picker.showsCameraControls = NO;
picker.cameraOverlayView = cameraOverlay;

然后你需要将你覆盖层上的控件关联上 UIImagePickerController 的控制方法 (比如,startVideoCapture 和 stopVideoCapture)。


#AVFoundation

如果你想要更多关于处理捕获视频的方法,而这些方法是 UIImagePickerController 所不能提供的,那么你需要使用 AVFoundation。

AVFoundation 中关于视频捕获的主要的类是 AVCaptureSession。它负责调配影音输入与输出之间的数据流:

使用一个 capture session,你需要先实例化,添加输入与输出,接着启动从输入到输出之间的数据流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
AVCaptureSession *captureSession = [AVCaptureSession new];
AVCaptureDeviceInput *cameraDeviceInput = …
AVCaptureDeviceInput *micDeviceInput = …
AVCaptureMovieFileOutput *movieFileOutput = …
if ([captureSession canAddInput:cameraDeviceInput]) {
[captureSession addInput:cameraDeviceInput];
}
if ([captureSession canAddInput:micDeviceInput]) {
[captureSession addInput:micDeviceInput];
}
if ([captureSession canAddOutput:movieFileOutput]) {
[captureSession addOutput:movieFileOutput];
}
[captureSession startRunning];

(为了简单起见,调度队列 (dispatch queue) 的相关代码已经从上面那段代码中省略了。所有对 capture session 的调用都是阻塞的,因此建议将它们分配到后台串行队列中。)

capture session 可以通过一个 sessionPreset 来进一步配置,这可以用来指定输出质量的等级。有 11 种不同的预设模式:

1
2
3
4
5
6
7
8
9
10
11
NSString *const AVCaptureSessionPresetPhoto;
NSString *const AVCaptureSessionPresetHigh;
NSString *const AVCaptureSessionPresetMedium;
NSString *const AVCaptureSessionPresetLow;
NSString *const AVCaptureSessionPreset352x288;
NSString *const AVCaptureSessionPreset640x480;
NSString *const AVCaptureSessionPreset1280x720;
NSString *const AVCaptureSessionPreset1920x1080;
NSString *const AVCaptureSessionPresetiFrame960x540;
NSString *const AVCaptureSessionPresetiFrame1280x720;
NSString *const AVCaptureSessionPresetInputPriority;

第一个代表高像素图片输出。 接下来的九个和之前我们在设置 UIImagePickerController 的 videoQuality 时看到过的 UIImagePickerControllerQualityType 选项非常相似,不同的是,这里有一些额外可用于 capture session 的预设。 最后一个 (AVCaptureSessionPresetInputPriority) 代表 capture session 不去控制音频与视频输出设置。而是通过已连接的捕获设备的 activeFormat 来反过来控制 capture session 的输出质量等级。在下一节,我们将会看到更多关于设备和设备格式的细节。

##输入
AVCaptureSession 的输入其实就是一个或多个的 AVCaptureDevice 对象,这些对象通过 AVCaptureDeviceInput 连接上 capture session。

我们可以使用 [AVCaptureDevice devices] 来寻找可用的捕获设备。以 iPhone 6 为例:

1
2
3
4
5
(
“<AVCaptureFigVideoDevice: 0x136514db0 [Back Camera][com.apple.avfoundation.avcapturedevice.built-in_video:0]>”,
“<AVCaptureFigVideoDevice: 0x13660be80 [Front Camera][com.apple.avfoundation.avcapturedevice.built-in_video:1]>”,
“<AVCaptureFigAudioDevice: 0x174265e80 [iPhone Microphone][com.apple.avfoundation.avcapturedevice.built-in_audio:0]>”
)

##视频输入
配置相机输入,需要实例化一个 AVCaptureDeviceInput 对象,参数是你期望的相机设备,然后把它添加到 capture session:

1
2
3
4
5
6
7
AVCaptureSession *captureSession = …
AVCaptureDevice *cameraDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error;
AVCaptureDeviceInput *cameraDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:cameraDevice error:&error];
if ([captureSession canAddInput:input]) {
[captureSession addInput:cameraDeviceInput];
}

如果上面提到的 capture session 预设列表里能满足你的需求,那你就不需要做更多的事情了。如果不够,比如你想要高的帧率,你将需要配置具体的设备格式。一个视频捕获设备有许多设备格式,每个都带有特定的属性和功能。下面是对于 iPhone6 的后置摄像头的一些例子 (一共有 22 种可用格式):

格式 分辨率 FPS HRSI FOV VIS 最大放大比例 Upscales AF ISO SS HDR
420v 1280x720 5 - 240 1280x720 54.626 YES 49.12 1.09 1 29.0 - 928 0.000003-0.200000 NO
420f 1280x720 5 - 240 1280x720 54.626 YES 49.12 1.09 1 29.0 - 928 0.000003-0.200000 NO
420v 1920x1080 2 - 30 3264x1836 58.040 YES 95.62 1.55 2 29.0 - 464 0.000013-0.500000 YES
420f 1920x1080 2 - 30 3264x1836 58.040 YES 95.62 1.55 2 29.0 - 464 0.000013-0.500000 YES
420v 1920x1080 2 - 60 3264x1836 58.040 YES 95.62 1.55 2 29.0 - 464 0.000008-0.500000 YES
420f 1920x1080 2 - 60 3264x1836 58.040 YES 95.62 1.55 2 29.0 - 464 0.000008-0.500000 YES
格式 = 像素格式
FPS = 支持帧数范围
HRSI = 高像素静态图片尺寸
FOV = 视角
VIS = 该格式支持视频防抖
Upscales = 加入数字 upscaling 时的放大比例
AF = 自动对焦系统(1 是反差对焦,2 是相位对焦)
ISO = 支持感光度范围
SS = 支持曝光时间范围
HDR = 支持高动态范围图像
通过上面的那些格式,你会发现如果要录制 240 帧每秒的视频的话,可以根据想要的像素格式选用第一个或第二个格式。另外若是要捕获 1920x1080 的分辨率的视频的话,是不支持 240 帧每秒的。

配置一个具体设备格式,你首先需要调用 lockForConfiguration: 来获取设备的配置属性的独占访问权限。接着你简单地使用 setActiveFormat: 来设置设备的捕获格式。这将会自动把 capture session 的预设设置为 AVCaptureSessionPresetInputPriority。

一旦你设置了预想的设备格式,你就可以在这种设备格式的约束参数范围内进行进一步的配置了。

对于视频捕获的对焦,曝光和白平衡的设置,与图像捕获时一样,具体可参考第 21 期“iOS 上的相机捕捉”。除了那些,这里还有一些视频特有的配置选项。

你可以用捕获设备的 activeVideoMinFrameDuration 和 activeVideoMaxFrameDuration 属性设置帧速率,一帧的时长是帧速率的倒数。设置帧速率之前,要先确认它是否在设备格式所支持的范围内,然后锁住捕获设备来进行配置。为了确保帧速率恒定,可以将最小与最大的帧时长设置成一样的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NSError *error;
CMTime frameDuration = CMTimeMake(1, 60);
NSArray *supportedFrameRateRanges = [device.activeFormat videoSupportedFrameRateRanges];
BOOL frameRateSupported = NO;
for (AVFrameRateRange *range in supportedFrameRateRanges) {
if (CMTIME_COMPARE_INLINE(frameDuration, >=, range.minFrameDuration) &&
CMTIME_COMPARE_INLINE(frameDuration, <=, range.maxFrameDuration)) {
frameRateSupported = YES;
}
}
if (frameRateSupported && [device lockForConfiguration:&error]) {
[device setActiveVideoMaxFrameDuration:frameDuration];
[device setActiveVideoMinFrameDuration:frameDuration];
[device unlockForConfiguration];
}

视频防抖 是在 iOS 6 和 iPhone 4S 发布时引入的功能。到了 iPhone 6,增加了更强劲和流畅的防抖模式,被称为影院级的视频防抖动。相关的 API 也有所改动 (目前为止并没有在文档中反映出来,不过可以查看头文件)。防抖并不是在捕获设备上配置的,而是在 AVCaptureConnection 上设置。由于不是所有的设备格式都支持全部的防抖模式,所以在实际应用中应事先确认具体的防抖模式是否支持:

1
2
3
4
5
6
7
AVCaptureDevice *device = ...;
AVCaptureConnection *connection = ...;
AVCaptureVideoStabilizationMode stabilizationMode = AVCaptureVideoStabilizationModeCinematic;
if ([device.activeFormat isVideoStabilizationModeSupported:stabilizationMode]) {
[connection setPreferredVideoStabilizationMode:stabilizationMode];
}

iPhone 6 的另一个新特性就是视频 HDR (高动态范围图像),它是“高动态范围的视频流,与传统的将不同曝光度的静态图像合成成一张高动态范围图像的方法完全不同”,它是内建在传感器中的。有两种方法可以配置视频 HDR:直接将 capture device 的 videoHDREnabled 设置为启用或禁用,或者使用 automaticallyAdjustsVideoHDREnabled 属性来留给系统处理。

技术参考:iPhone 6 和 iPhone Plus 的新 AV Foundation 相机特性

###音频输入

之前展示的捕获设备列表里面只有一个音频设备,你可能觉得奇怪,毕竟 iPhone 6 有 3 个麦克风。然而因为有时会放在一起使用,便于优化性能,因此可能被当做一个设备来使用。例如在 iPhone 5 及以上的手机录制视频时,会同时使用前置和后置麦克风,用于定向降噪。

Technical Q&A: AVAudioSession - Microphone Selection
大多数情况下,设置成默认的麦克风配置即可。后置麦克风会自动搭配后置摄像头使用 (前置麦克风则用于降噪),前置麦克风和前置摄像头也是一样。

然而想要访问和配置单独的麦克风也是可行的。例如,当用户正在使用后置摄像头捕获场景的时候,使用前置麦克风来录制解说也应是可能的。这就要依赖于 AVAudioSession。 为了变更要访问的音频,audio session 首先需要设置为支持这样做的类别。然后我们需要遍历 audio session 的输入端口和端口数据来源,来找到我们想要的麦克风:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 配置 audio session
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
[audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
[audioSession setActive:YES error:nil];
// 寻找期望的输入端口
NSArray* inputs = [audioSession availableInputs];
AVAudioSessionPortDescription *builtInMic = nil;
for (AVAudioSessionPortDescription* port in inputs) {
if ([port.portType isEqualToString:AVAudioSessionPortBuiltInMic]) {
builtInMic = port;
break;
}
}
// 寻找期望的麦克风
for (AVAudioSessionDataSourceDescription* source in builtInMic.dataSources) {
if ([source.orientation isEqual:AVAudioSessionOrientationFront]) {
[builtInMic setPreferredDataSource:source error:nil];
[audioSession setPreferredInput:builtInMic error:&error];
break;
}
}

除了设置非默认的麦克风配置,你也可以使用 AVAudioSession 来配置其他音频设置,比如音频增益和采样率等。

###访问权限

有件事你需要记住,访问相机和麦克风需要先获得用户授权。当你给视频或音频创建第一个 AVCaptureDeviceInput 对象时,iOS 会自动弹出一次对话框,请求用户授权,但你最好还是自己实现下。之后你就可以在还没有被授权的时候,使用相同的代码来提示用户进行授权。当用户未授权时,对于录制视频或音频的尝试,得到的将是黑色画面和无声。

###输出
输入配置完了,现在把我们的注意力转向 capture session 的输出。

AVCaptureMovieFileOutput

将视频写入文件,最简单的选择就是使用 AVCaptureMovieFileOutput 对象。把它作为输出添加到 capture session 中,就可以将视频和音频写入 QuickTime 文件,这只需很少的配置。

1
2
3
4
5
6
7
8
AVCaptureMovieFileOutput *movieFileOutput = [AVCaptureMovieFileOutput new];
if([captureSession canAddOutput:movieFileOutput]){
[captureSession addOutput:movieFileOutput];
}
// 开始录制
NSURL *outputURL = …
[movieFileOutput startRecordingToOutputFileURL:outputURL recordingDelegate:self];

当实际的录制开始或停止时,想要接收回调的话就必须要一个录制代理。当录制停止时,输出通常还在写入数据,等它完成之后会调用代理方法。

AVCaptureMovieFileOutput 有一些其他的配置选项,比如在某段时间后,在达到某个指定的文件尺寸时,或者当设备的最小磁盘剩余空间达到某个阈值时停止录制。如果你还需要更多设置,比如自定义视频音频的压缩率,或者你想要在写入文件之前,处理视频音频的样本,那么你需要一些更复杂的操作。

AVCaptureDataOutput 和 AVAssetWriter

如果你想要对影音输出有更多的操作,你可以使用 AVCaptureVideoDataOutput 和 AVCaptureAudioDataOutput 而不是我们上节讨论的 AVCaptureMovieFileOutput。

这些输出将会各自捕获视频和音频的样本缓存,接着发送到它们的代理。代理要么对采样缓冲进行处理 (比如给视频加滤镜),要么保持原样传送。使用 AVAssetWriter 对象可以将样本缓存写入文件:

配置一个 asset writer 需要定义一个输出 URL 和文件格式,并添加一个或多个输入来接收采样的缓冲。我们还需要将输入的 expectsMediaInRealTime 属性设置为 YES,因为它们需要从 capture session 实时获得数据。

1
2
3
4
5
6
7
8
9
10
11
12
NSURL *url = …;
AVAssetWriter *assetWriter = [AVAssetWriter assetWriterWithURL:url fileType:AVFileTypeMPEG4 error:nil];
AVAssetWriterInput *videoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:nil];
videoInput.expectsMediaDataInRealTime = YES;
AVAssetWriterInput *audioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:nil];
audioInput.expectsMediaDataInRealTime = YES;
if ([assetWriter canAddInput:videoInput]) {
[assetWriter addInput:videoInput];
}
if ([assetWriter canAddInput:audioInput]) {
[assetWriter addInput:audioInput];
}

(这里推荐将 asset writer 派送到后台串行队列中调用。)

在上面的示例代码中,我们将 asset writer 的 outputSettings 设置为 nil。这就意味着附加上来的样本不会再被重新编码。如果你确实想要重新编码这些样本,那么需要提供一个包含具体输出参数的字典。关于音频输出设置的键值被定义在这里, 关于视频输出设置的键值定义在这里。

为了更简单点,AVCaptureVideoDataOutput 和 AVCaptureAudioDataOutput 分别带有 recommendedVideoSettingsForAssetWriterWithOutputFileType: 和 recommendedAudioSettingsForAssetWriterWithOutputFileType: 方法,可以生成与 asset writer 兼容的带有全部键值对的字典。所以你可以通过在这个字典里调整你想要重写的属性,来简单地定义你自己的输出设置。比如,增加视频比特率来提高视频质量等。

或者,你也可以使用 AVOutputSettingsAssistant 来配置输出设置的字典,但是从我的经验来看,使用上面的方法会更好,它们会提供更实用的输出设置,比如视频比特率。另外,AVOutputSettingsAssistant 似乎存在一些缺点,例如,当你改变希望的视频的帧速率时,视频的比特率并不会改变。

###实时预览

当使用 AVFoundation 来做图像捕获时,我们必须提供一套自定义的用户界面。其中一个关键的相机交互组件是实时预览图。最简单的实现方式是通过把 AVCaptureVideoPreviewLayer 对象作为一个 sublayer 加到相机图层上去:

1
2
3
4
5
AVCaptureSession *captureSession = ...;
AVCaptureVideoPreviewLayer *previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:captureSession];
UIView *cameraView = ...;
previewLayer.frame = cameraView.bounds;
[cameraView.layer addSublayer:previewLayer];

如果你想要更进一步操作,比如,在实时预览图加滤镜,你需要将 AVCaptureVideoDataOutput 对象加到 capture session,并且使用 OpenGL 展示画面,具体可查看该文“iOS 上的相机捕捉”

###总结

有许多不同的方法可以给 iOS 上的视频捕获配置管线,从最直接的 UIImagePickerController,到精密配合的 AVCaptureSession 与 AVAssetWriter 。如何抉择取决于你的项目要求,比如期望的视频质量和压缩率,或者是你想要展示给用户的相机控件。

GIF解析和生成

//gif的解析, 和生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
网络转载,忘记出处了
//制作gif是基于ImageIO.framework,所以要先添加这个库gif分解图片
- (void)viewDidLoad
{
[super viewDidLoad];
NSData *data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle]pathForResource:@"test101" ofType:@"gif"]];
//通过data获取image的数据源
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
//获取帧数
size_t count = CGImageSourceGetCount(source);
NSMutableArray* tmpArray = [NSMutableArray array];
for (size_t i = 0; i < count; i++)
{
//获取图像
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, i, NULL);
//生成image
UIImage *image = [UIImage imageWithCGImage:imageRef scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp];
[tmpArray addObject:image];
CGImageRelease(imageRef);
}
CFRelease(source);
int i = 0;
for (UIImage *img in tmpArray) {
//写文件
NSData *imageData = UIImagePNGRepresentation(img);
NSString *pathNum = [[self backPath] stringByAppendingPathComponent:[NSString stringWithFormat:@"%d.png",i]];
[imageData writeToFile:pathNum atomically:NO];
i++;
}
}
//返回保存图片的路径
-(NSString *)backPath{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *path = [UniversalMethod backDocumentDirectoryPath];
NSString *imageDirectory = [path stringByAppendingPathComponent:@"Normal"];
[fileManager createDirectoryAtPath:imageDirectory withIntermediateDirectories:YES attributes:nil error:nil];
return imageDirectory;
}
gif的制作
制作gif需要依赖MobileCoreServices.framework
//gif的制作
//获取源数据image
NSMutableArray *imgs = [[NSMutableArray alloc]initWithObjects:[UIImage imageNamed:@"bear_1"],[UIImage imageNamed:@"bear_2"], nil];
//图像目标
CGImageDestinationRef destination;
//创建输出路径
NSArray *document = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentStr = [document objectAtIndex:0];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *textDirectory = [documentStr stringByAppendingPathComponent:@"gif"];
[fileManager createDirectoryAtPath:textDirectory withIntermediateDirectories:YES attributes:nil error:nil];
NSString *path = [textDirectory stringByAppendingPathComponent:@"test101.gif"];
NSLog(@"%@",path);
//创建CFURL对象
/*
CFURLCreateWithFileSystemPath(CFAllocatorRef allocator, CFStringRef filePath, CFURLPathStyle pathStyle, Boolean isDirectory)
allocator : 分配器,通常使用kCFAllocatorDefault
filePath : 路径
pathStyle : 路径风格,我们就填写kCFURLPOSIXPathStyle 更多请打问号自己进去帮助看
isDirectory : 一个布尔值,用于指定是否filePath被当作一个目录路径解决时相对路径组件
*/
CFURLRef url = CFURLCreateWithFileSystemPath (
kCFAllocatorDefault,
(CFStringRef)path,
kCFURLPOSIXPathStyle,
false);
//通过一个url返回图像目标
destination = CGImageDestinationCreateWithURL(url, kUTTypeGIF, imgs.count, NULL);
//设置gif的信息,播放间隔时间,基本数据,和delay时间
NSDictionary *frameProperties = [NSDictionary
dictionaryWithObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithFloat:0.3], (NSString *)kCGImagePropertyGIFDelayTime, nil]
forKey:(NSString *)kCGImagePropertyGIFDictionary];
//设置gif信息
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:2];
[dict setObject:[NSNumber numberWithBool:YES] forKey:(NSString*)kCGImagePropertyGIFHasGlobalColorMap];
[dict setObject:(NSString *)kCGImagePropertyColorModelRGB forKey:(NSString *)kCGImagePropertyColorModel];
[dict setObject:[NSNumber numberWithInt:8] forKey:(NSString*)kCGImagePropertyDepth];
[dict setObject:[NSNumber numberWithInt:0] forKey:(NSString *)kCGImagePropertyGIFLoopCount];
NSDictionary *gifProperties = [NSDictionary dictionaryWithObject:dict
forKey:(NSString *)kCGImagePropertyGIFDictionary];
//合成gif
for (UIImage* dImg in imgs)
{
CGImageDestinationAddImage(destination, dImg.CGImage, (__bridge CFDictionaryRef)frameProperties);
}
CGImageDestinationSetProperties(destination, (__bridge CFDictionaryRef)gifProperties);
CGImageDestinationFinalize(destination);
CFRelease(destination);

gif 解析方法

From: http://blog.csdn.net/qxuewei/article/details/50782855

原生方法:

1.UIWebView
特点:加载速度略长,性能更优,播放的gif动态图更加流畅。

-(void)showGifImageWithWebView{

    NSData *gifData = [NSData dataWithContentsOfFile: [[NSBundle mainBundle] pathForResource:@"earthGif" ofType:@"gif"]];

    UIWebView *imageWebView = [[UIWebView alloc] initWithFrame:CGRectMake(112, 302, 132, 102)];

    imageWebView.userInteractionEnabled = NO;

    [imageWebView loadData:gifData MIMEType:@"image/gif" textEncodingName:nil baseURL:nil];

    [self.view addSubview:imageWebView];
}

2.UIImagView
加载的方式更加快速,性能不如UIWebView,优点:易于扩展

1)
增加一个UIImageView的类别(category),增加两个方法
UIImage+Tool
.h

#import <UIKit/UIKit.h>

@interface UIImageView (Tool)

/** 解析gif文件数据的方法 block中会将解析的数据传递出来 */
-(void)getGifImageWithUrk:(NSURL *)url returnData:(void(^)(NSArray<UIImage *> * imageArray,NSArray<NSNumber *>*timeArray,CGFloat totalTime, NSArray<NSNumber *>* widths, NSArray<NSNumber *>* heights))dataBlock;

/** 为UIImageView添加一个设置gif图内容的方法: */
-(void)yh_setImage:(NSURL *)imageUrl;

@end

.m

#import "UIImageView+Tool.h"

#import <ImageIO/ImageIO.h>

@implementation UIImageView (Tool)




-(void)getGifImageWithUrk:(NSURL *)url returnData:(void(^)(NSArray<UIImage *> * imageArray, NSArray<NSNumber *>*timeArray,CGFloat totalTime, NSArray<NSNumber *>* widths,NSArray<NSNumber *>* heights))dataBlock{

    CGImageSourceRef source = CGImageSourceCreateWithURL((CFURLRef)url, NULL);

    size_t count = CGImageSourceGetCount(source);

    float allTime=0;

    NSMutableArray * imageArray = [[NSMutableArray alloc]init];

    NSMutableArray * timeArray = [[NSMutableArray alloc]init];

    NSMutableArray * widthArray = [[NSMutableArray alloc]init];

    NSMutableArray * heightArray = [[NSMutableArray alloc]init];

    for (size_t i=0; i<count; i++) {
        CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL);
        [imageArray addObject:(__bridge UIImage *)(image)];
        CGImageRelease(image);

        NSDictionary * info = (__bridge NSDictionary*)CGImageSourceCopyPropertiesAtIndex(source, i, NULL);
        CGFloat width = [[info objectForKey:(__bridge NSString *)kCGImagePropertyPixelWidth] floatValue];
        CGFloat height = [[info objectForKey:(__bridge NSString *)kCGImagePropertyPixelHeight] floatValue];
        [widthArray addObject:[NSNumber numberWithFloat:width]];
        [heightArray addObject:[NSNumber numberWithFloat:height]];
        NSDictionary * timeDic = [info objectForKey:(__bridge NSString *)kCGImagePropertyGIFDictionary];
        CGFloat time = [[timeDic objectForKey:(__bridge NSString *)kCGImagePropertyGIFDelayTime]floatValue];
        allTime+=time;
        [timeArray addObject:[NSNumber numberWithFloat:time]];
    }
    CFRelease(source);
    dataBlock(imageArray,timeArray,allTime,widthArray,heightArray);
}


-(void)yh_setImage:(NSURL *)imageUrl{
    __weak id __self = self;
    [self getGifImageWithUrk:imageUrl returnData:^(NSArray<UIImage *> *imageArray, NSArray<NSNumber *> *timeArray, CGFloat totalTime, NSArray<NSNumber *> *widths, NSArray<NSNumber *> *heights) {

        CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"];
        NSMutableArray * times = [[NSMutableArray alloc]init];
        float currentTime = 0;

        for (int i=0; i<imageArray.count; i++) {
            [times addObject:[NSNumber numberWithFloat:currentTime/totalTime]];
            currentTime+=[timeArray[i] floatValue];
        }
        [animation setKeyTimes:times];
        [animation setValues:imageArray];
        [animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]];

        animation.repeatCount= MAXFLOAT;

        animation.duration = totalTime;

        [[(UIImageView *)__self layer]addAnimation:animation forKey:@"gifAnimation"];
    }];
}

@end

在加载gif的地方使用
导入 UIImageView+Tool

-(void)showGifImageWithImageView{

    UIImageView * imageView = [[UIImageView alloc]initWithFrame:CGRectMake(112, 342, 132, 102)];
    NSURL * url = [[NSURL alloc]initFileURLWithPath:[[NSBundle mainBundle] pathForResource:@"earthGif.gif" ofType:nil]];
    [imageView yh_setImage:url];
    [self.view addSubview:imageView];

}

第三方:
1.YLGIFImage
github链接: [https://github.com/liyong03/YLGIFImage][1]

[1]: https://github.com/liyong03/YLGIFImage

#import “YLGIFImage.h”

#import "YLImageView.h"

-(void)showGifImageWithYLImageView{
    YLImageView* imageView = [[YLImageView alloc] initWithFrame:CGRectMake(112, 342, 132, 102)];
    CGFloat centerX = self.view.center.x;
    [imageView setCenter:CGPointMake(centerX, 402)];
    [self.view addSubview:imageView];
    imageView.image = [YLGIFImage imageNamed:@"earthGif.gif"];
}

2.FLAnimatedImage
github链接:[https://github.com/Flipboard/FLAnimatedImage][2]

[2]: https://github.com/Flipboard/FLAnimatedImage

-(void)showGifImageWithFLAnimatedImage{

    NSString *pathForFile = [[NSBundle mainBundle] pathForResource: @"earthGif" ofType:@"gif"];

    NSData *dataOfGif = [NSData dataWithContentsOfFile: pathForFile];

    FLAnimatedImage *image = [FLAnimatedImage animatedImageWithGIFData:dataOfGif];

    FLAnimatedImageView *imageView = [[FLAnimatedImageView alloc] init];

    imageView.animatedImage = image;
    imageView.frame = CGRectMake(112, 342, 132, 102);
    [self.view addSubview:imageView];
}