在 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 中给视频添加滤镜

相关文章
相关标签/搜索