在 iOS 中给视频添加滤镜

在 iOS 中给视频添加滤镜_第1张图片
「众所周知,视频可以 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 *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 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 。

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 虽然使用比较繁琐,但是功能强大,可以很方便地导出视频处理的结果,是用来做视频处理的不二之选。

源码

请到 GitHub 上查看完整代码。

推荐:

  • 020 持续更新,精品小圈子每日都有新内容,干货浓度极高。

  • 结实人脉、讨论技术 你想要的这里都有!

  • 抢先入群,跑赢同龄人!(入群无需任何费用)

  • (直接搜索群号:789143298,快速入群)
  • 点击此处,与iOS开发大牛一起交流学习

申请即送:

  • BAT大厂面试题、独家面试工具包,

  • 资料免费领取,包括 数据结构、底层进阶、图形视觉、音视频、架构设计、逆向安防、RxSwift、flutter,

你可能感兴趣的:(ios,objective-c,xcode)