内容简介:「众所周知,视频可以 P」,今天我们来学习怎么给视频添加滤镜。在 iOS 中,对视频进行图像处理一般有两种方式:
「众所周知,视频可以 P」,今天我们来学习怎么给视频添加滤镜。
在 iOS 中,对视频进行图像处理一般有两种方式: GPUImage 和 AVFoundation 。
一、GPUImage
在之前的文章中,我们对 GPUImage 已经有了一定的了解。之前一般使用它对摄像头采集的图像数据进行处理,然而,它对本地视频的处理也一样方便。
直接看代码:
// movie NSString *path = [[NSBundle mainBundle] pathForResource:@"sample" ofType:@"mp4"]; NSURL *url = [NSURL fileURLWithPath:path]; GPUImageMovie *movie = [[GPUImageMovie alloc] initWithURL:url]; // filter GPUImageSmoothToonFilter *filter = [[GPUImageSmoothToonFilter alloc] init]; // view GPUImageView *imageView = [[GPUImageView alloc] initWithFrame:CGRectMake(0, 80, self.view.frame.size.width, self.view.frame.size.width)]; [self.view addSubview:imageView]; // chain [movie addTarget:filter]; [filter addTarget:imageView]; // processing [movie startProcessing];
核心代码一共就几行。 GPUImageMovie
负责视频文件的读取, GPUImageSmoothToonFilter
负责滤镜效果处理, GPUImageView
负责最终图像的展示。
通过滤镜链将三者串起来,然后调用 GPUImageMovie
的 startProcessing
方法开始处理。
虽然 GPUImage
在使用上简单,但是存在着 没有声音 、 在非主线程调用 UI 、 导出文件麻烦 、 无法进行播放控制 等诸多缺点。
小结:GPUImage 虽然使用很方便,但是存在诸多缺点,不满足生产环境需要。
二、AVFoundation
1、 AVPlayer 的使用
首先来复习一下 AVPlayer
最简单的使用方式:
NSURL *url = [[NSBundle mainBundle] URLForResource:@"sample" withExtension:@"mp4"]; AVURLAsset *asset = [AVURLAsset assetWithURL:url]; AVPlayerItem *playerItem = [[AVPlayerItem alloc] initWithAsset:asset]; AVPlayer *player = [[AVPlayer alloc] initWithPlayerItem:playerItem]; AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
第一步先构建 AVPlayerItem
,然后通过 AVPlayerItem
创建 AVPlayer
,最后通过 AVPlayer
创建 AVPlayerLayer
。
AVPlayerLayer
是 CALayer
的子类,可以把它添加到任意的 Layer
上。当 AVPlayer
调用 play
方法时, AVPlayerLayer
上就能将图像渲染出来。
AVPlayer
的使用方式十分简单。但是,按照上面的方式,最终只能在 AVPlayerLayer
上渲染出最原始的图像。如果我们希望在播放的同时,对原始图像进行处理,则需要修改 AVPlayer
的渲染过程。
2、修改 AVPlayer 的渲染过程
修改 AVPlayer
的渲染过程,要从 AVPlayerItem
下手,主要分为 四步 :
第一步:自定义 AVVideoCompositing 类
AVVideoCompositing
是一个协议,我们的自定义类要实现这个协议。在这个自定义类中,可以获取到每一帧的原始图像,进行处理并输出。
在这个协议中,最关键是 startVideoCompositionRequest
方法的实现:
// CustomVideoCompositing.m - (void)startVideoCompositionRequest:(AVAsynchronousVideoCompositionRequest *)asyncVideoCompositionRequest { dispatch_async(self.renderingQueue, ^{ @autoreleasepool { if (self.shouldCancelAllRequests) { [asyncVideoCompositionRequest finishCancelledRequest]; } else { CVPixelBufferRef resultPixels = [self newRenderdPixelBufferForRequest:asyncVideoCompositionRequest]; if (resultPixels) { [asyncVideoCompositionRequest finishWithComposedVideoFrame:resultPixels]; CVPixelBufferRelease(resultPixels); } else { // print error } } } }); }
通过 newRenderdPixelBufferForRequest
方法从 AVAsynchronousVideoCompositionRequest
中获取到处理后的 CVPixelBufferRef
后输出,看下这个方法的实现:
// CustomVideoCompositing.m - (CVPixelBufferRef)newRenderdPixelBufferForRequest:(AVAsynchronousVideoCompositionRequest *)request { CustomVideoCompositionInstruction *videoCompositionInstruction = (CustomVideoCompositionInstruction *)request.videoCompositionInstruction; NSArray<AVVideoCompositionLayerInstruction *> *layerInstructions = videoCompositionInstruction.layerInstructions; CMPersistentTrackID trackID = layerInstructions.firstObject.trackID; CVPixelBufferRef sourcePixelBuffer = [request sourceFrameByTrackID:trackID]; CVPixelBufferRef resultPixelBuffer = [videoCompositionInstruction applyPixelBuffer:sourcePixelBuffer]; if (!resultPixelBuffer) { CVPixelBufferRef emptyPixelBuffer = [self createEmptyPixelBuffer]; return emptyPixelBuffer; } else { return resultPixelBuffer; } }
在这个方法中,我们通过 trackID
从 AVAsynchronousVideoCompositionRequest
中获取到 sourcePixelBuffer
,也就是当前帧的原始图像。
然后调用 videoCompositionInstruction
的 applyPixelBuffer
方法,将 sourcePixelBuffer
作为输入,得到处理后的结果 resultPixelBuffer
。也就是说,我们对图像的处理操作,都发生在 applyPixelBuffer
方法中。
在 newRenderdPixelBufferForRequest
这个方法中,我们已经拿到了当前帧的原始图像 sourcePixelBuffer
,其实也可以直接在这个方法中对图像进行处理。
那为什么还需要把处理操作放在 CustomVideoCompositionInstruction
中呢?
因为在实际渲染的时候,自定义 AVVideoCompositing
类的实例创建是系统内部完成的。也就是说,我们访问不到最终的 AVVideoCompositing
对象。所以无法进行一些渲染参数的动态修改。而从 AVAsynchronousVideoCompositionRequest
中,可以获取到 AVVideoCompositionInstruction
对象,所以我们需要自定义 AVVideoCompositionInstruction
,这样就可以间接地通过修改 AVVideoCompositionInstruction
的属性,来动态修改渲染参数。
第二步:自定义 AVVideoCompositionInstruction
这个类的关键点是 applyPixelBuffer
方法的实现:
// CustomVideoCompositionInstruction.m - (CVPixelBufferRef)applyPixelBuffer:(CVPixelBufferRef)pixelBuffer { self.filter.pixelBuffer = pixelBuffer; CVPixelBufferRef outputPixelBuffer = self.filter.outputPixelBuffer; CVPixelBufferRetain(outputPixelBuffer); return outputPixelBuffer; }
这里把 OpenGL ES 的处理细节都封装到了 filter
中。这个类的实现细节可以先忽略,只需要知道它接受 原始的 CVPixelBufferRef
,返回 处理后的 CVPixelBufferRef
。
第三步:构建 AVMutableVideoComposition
构建的代码如下:
self.videoComposition = [self createVideoCompositionWithAsset:self.asset]; self.videoComposition.customVideoCompositorClass = [CustomVideoCompositing class];
- (AVMutableVideoComposition *)createVideoCompositionWithAsset:(AVAsset *)asset { AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoCompositionWithPropertiesOfAsset:asset]; NSArray *instructions = videoComposition.instructions; NSMutableArray *newInstructions = [NSMutableArray array]; for (AVVideoCompositionInstruction *instruction in instructions) { NSArray *layerInstructions = instruction.layerInstructions; // TrackIDs NSMutableArray *trackIDs = [NSMutableArray array]; for (AVVideoCompositionLayerInstruction *layerInstruction in layerInstructions) { [trackIDs addObject:@(layerInstruction.trackID)]; } CustomVideoCompositionInstruction *newInstruction = [[CustomVideoCompositionInstruction alloc] initWithSourceTrackIDs:trackIDs timeRange:instruction.timeRange]; newInstruction.layerInstructions = instruction.layerInstructions; [newInstructions addObject:newInstruction]; } videoComposition.instructions = newInstructions; return videoComposition; }
构建 AVMutableVideoComposition
的过程 主要做两件事情 。
第一件事情,把 videoComposition
的 customVideoCompositorClass
属性,设置为我们自定义的 CustomVideoCompositing
。
第二件事情,首先通过系统提供的方法 videoCompositionWithPropertiesOfAsset
构建出 AVMutableVideoComposition
对象,然后将它的 instructions
属性修改为自定义的 CustomVideoCompositionInstruction
类型。(就像「第一步」提到的,后续可以在 CustomVideoCompositing
中,拿到 CustomVideoCompositionInstruction
对象。)
注意:这里可以把 CustomVideoCompositionInstruction
保存下来,然后通过修改它的属性,去修改渲染参数。
第四步:构建 AVPlayerItem
有了 AVMutableVideoComposition
之后,后面的事情就简单多了。
只需要在创建 AVPlayerItem
的时候,多赋值一个 videoComposition
属性。
self.playerItem = [[AVPlayerItem alloc] initWithAsset:self.asset]; self.playerItem.videoComposition = self.videoComposition;
这样,整条链路就串起来了, AVPlayer
在播放时,就能在 CustomVideoCompositionInstruction
的 applyPixelBuffer
方法中接收到 原始图像的 CVPixelBufferRef
。
3、应用滤镜效果
这一步要做的事情是: 在 CVPixelBufferRef
上添加滤镜效果,并输出处理后的 CVPixelBufferRef
。
要做到这件事情,有很多种方式。包括但不限定于: OpenGL ES 、 CIImage 、 Metal 、 GPUImage 等。
为了同样使用前面用到的 GPUImageSmoothToonFilter
,这里介绍一下 GPUImage 的方式。
关键代码如下:
- (CVPixelBufferRef)renderByGPUImage:(CVPixelBufferRef)pixelBuffer { CVPixelBufferRetain(pixelBuffer); __block CVPixelBufferRef output = nil; runSynchronouslyOnVideoProcessingQueue(^{ [GPUImageContext useImageProcessingContext]; // (1) GLuint textureID = [self.pixelBufferHelper convertYUVPixelBufferToTexture:pixelBuffer]; CGSize size = CGSizeMake(CVPixelBufferGetWidth(pixelBuffer), CVPixelBufferGetHeight(pixelBuffer)); [GPUImageContext setActiveShaderProgram:nil]; // (2) GPUImageTextureInput *textureInput = [[GPUImageTextureInput alloc] initWithTexture:textureID size:size]; GPUImageSmoothToonFilter *filter = [[GPUImageSmoothToonFilter alloc] init]; [textureInput addTarget:filter]; GPUImageTextureOutput *textureOutput = [[GPUImageTextureOutput alloc] init]; [filter addTarget:textureOutput]; [textureInput processTextureWithFrameTime:kCMTimeZero]; // (3) output = [self.pixelBufferHelper convertTextureToPixelBuffer:textureOutput.texture textureSize:size]; [textureOutput doneWithTexture]; glDeleteTextures(1, &textureID); }); CVPixelBufferRelease(pixelBuffer); return output; }
(1)一开始读入的视频帧是 YUV 格式的,首先把 YUV 格式的 CVPixelBufferRef
转成 OpenGL 纹理。
(2)通过 GPUImageTextureInput
来构造滤镜链起点, GPUImageSmoothToonFilter
来添加滤镜效果, GPUImageTextureOutput
来构造滤镜链终点,最终也是输出 OpenGL 纹理。
(3)将处理后的 OpenGL 纹理转化为 CVPixelBufferRef
。
另外,由于 CIImage 使用简单,也顺便提一下用法。
关键代码如下:
- (CVPixelBufferRef)renderByCIImage:(CVPixelBufferRef)pixelBuffer { CVPixelBufferRetain(pixelBuffer); CGSize size = CGSizeMake(CVPixelBufferGetWidth(pixelBuffer), CVPixelBufferGetHeight(pixelBuffer)); // (1) CIImage *image = [[CIImage alloc] initWithCVPixelBuffer:pixelBuffer]; // (2) CIImage *filterImage = [CIImage imageWithColor:[CIColor colorWithRed:255.0 / 255 green:245.0 / 255 blue:215.0 / 255 alpha:0.1]]; // (3) image = [filterImage imageByCompositingOverImage:image]; // (4) CVPixelBufferRef output = [self.pixelBufferHelper createPixelBufferWithSize:size]; [self.context render:image toCVPixelBuffer:output]; CVPixelBufferRelease(pixelBuffer); return output; }
(1)将 CVPixelBufferRef
转化为 CIImage
。
(2)创建一个带透明度的 CIImage
。
(3)用系统方法将 CIImage
进行叠加。
(4)将叠加后的 CIImage
转化为 CVPixelBufferRef
。
4、导出处理后的视频
视频处理完成后,最终都希望能导出并保存。
导出的代码也很简单:
self.exportSession = [[AVAssetExportSession alloc] initWithAsset:self.asset presetName:AVAssetExportPresetHighestQuality]; self.exportSession.videoComposition = self.videoComposition; self.exportSession.outputFileType = AVFileTypeMPEG4; self.exportSession.outputURL = [NSURL fileURLWithPath:self.exportPath]; [self.exportSession exportAsynchronouslyWithCompletionHandler:^{ // 保存到相册 // ... }];
这里关键的地方在于将 videoComposition
设置为前面构造的 AVMutableVideoComposition
对象,然后设置好输出路径和文件格式后就可以开始导出。导出成功后,可以将视频文件转存到相册中。
小结: AVFoundation
虽然使用比较繁琐,但是功能强大,可以很方便地导出视频处理的结果,是用来做视频处理的不二之选。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 给Markdown添加视频支持
- PingPangChat 2.1.0 添加一对一的视频聊天功能
- iOS逆向之给腾讯视频App添加快进手势
- 主动模式和被动模式,添加监控主机,添加自定义模板,处理图像中的乱码,自动发现
- 苹果将为 Mac 添加 Face ID,为 Magic Keyboard 添加 Touch Bar
- android – 为什么AOSP添加新的API来支持库而不添加到SDK?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。