在 iOS 中給視頻添加濾鏡

「衆所周知,視頻能夠 P」,今天咱們來學習怎麼給視頻添加濾鏡。ios

在 iOS 中,對視頻進行圖像處理通常有兩種方式:GPUImageAVFoundationgit

1、GPUImage

在以前的文章中,咱們對 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

經過濾鏡鏈將三者串起來,而後調用 GPUImageMoviestartProcessing 方法開始處理。async

雖然 GPUImage 在使用上簡單,可是存在着 沒有聲音在非主線程調用 UI導出文件麻煩沒法進行播放控制 等諸多缺點。ide

小結:GPUImage 雖然使用很方便,可是存在諸多缺點,不知足生產環境須要oop

2、AVFoundation

一、 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 建立 AVPlayerLayerui

AVPlayerLayerCALayer 的子類,能夠把它添加到任意的 Layer 上。當 AVPlayer 調用 play 方法時, AVPlayerLayer 上就能將圖像渲染出來。

AVPlayer 的使用方式十分簡單。可是,按照上面的方式,最終只能在 AVPlayerLayer 上渲染出最原始的圖像。若是咱們但願在播放的同時,對原始圖像進行處理,則須要修改 AVPlayer 的渲染過程。

二、修改 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;
    }
}
複製代碼

在這個方法中,咱們經過 trackIDAVAsynchronousVideoCompositionRequest 中獲取到 sourcePixelBuffer,也就是當前幀的原始圖像。

而後調用 videoCompositionInstructionapplyPixelBuffer 方法,將 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 的過程主要作兩件事情

第一件事情,把 videoCompositioncustomVideoCompositorClass 屬性,設置爲咱們自定義的 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 在播放時,就能在 CustomVideoCompositionInstructionapplyPixelBuffer 方法中接收到原始圖像的 CVPixelBufferRef

三、應用濾鏡效果

這一步要作的事情是:CVPixelBufferRef 上添加濾鏡效果,並輸出處理後的 CVPixelBufferRef

要作到這件事情,有不少種方式。包括但不限定於:OpenGL ESCIImageMetalGPUImage 等。

爲了一樣使用前面用到的 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 中給視頻添加濾鏡

相關文章
相關標籤/搜索