「衆所周知,視頻能夠 P」,今天咱們來學習怎麼給視頻添加濾鏡。ios
在 iOS 中,對視頻進行圖像處理通常有兩種方式:GPUImage 和 AVFoundation 。git
在以前的文章中,咱們對 GPUImage 已經有了必定的瞭解。以前通常使用它對攝像頭採集的圖像數據進行處理,然而,它對本地視頻的處理也同樣方便。github
直接看代碼:markdown
// 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
負責最終圖像的展現。app
經過濾鏡鏈將三者串起來,而後調用 GPUImageMovie
的 startProcessing
方法開始處理。async
雖然 GPUImage
在使用上簡單,可是存在着 沒有聲音、在非主線程調用 UI、導出文件麻煩、沒法進行播放控制 等諸多缺點。ide
小結:GPUImage
雖然使用很方便,可是存在諸多缺點,不知足生產環境須要。oop
首先來複習一下 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
。ui
AVPlayerLayer
是 CALayer
的子類,能夠把它添加到任意的 Layer
上。當 AVPlayer
調用 play
方法時, AVPlayerLayer
上就能將圖像渲染出來。
AVPlayer
的使用方式十分簡單。可是,按照上面的方式,最終只能在 AVPlayerLayer
上渲染出最原始的圖像。若是咱們但願在播放的同時,對原始圖像進行處理,則須要修改 AVPlayer
的渲染過程。
修改 AVPlayer
的渲染過程,要從 AVPlayerItem
下手,主要分爲四步:
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
的屬性,來動態修改渲染參數。
這個類的關鍵點是 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
。
構建的代碼以下:
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
保存下來,而後經過修改它的屬性,去修改渲染參數。
有了 AVMutableVideoComposition
以後,後面的事情就簡單多了。
只須要在建立 AVPlayerItem
的時候,多賦值一個 videoComposition
屬性。
self.playerItem = [[AVPlayerItem alloc] initWithAsset:self.asset]; self.playerItem.videoComposition = self.videoComposition; 複製代碼
這樣,整條鏈路就串起來了,AVPlayer
在播放時,就能在 CustomVideoCompositionInstruction
的 applyPixelBuffer
方法中接收到原始圖像的 CVPixelBufferRef
。
這一步要作的事情是:在 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
。
視頻處理完成後,最終都但願能導出並保存。
導出的代碼也很簡單:
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
雖然使用比較繁瑣,可是功能強大,能夠很方便地導出視頻處理的結果,是用來作視頻處理的不二之選。
請到 GitHub 上查看完整代碼。
獲取更佳的閱讀體驗,請訪問原文地址【Lyman's Blog】在 iOS 中給視頻添加濾鏡