短视频编辑中的AVFoundation框架(二)素材添加与处理

2,067 阅读17分钟

前言

上一篇短视频编辑中的AVFoundation框架(一)框架概述与基础我们介绍了对AVFoundation有了初步的认识,本篇正式开始介绍短视频编辑的第一步添加和处理素材。

短视频编辑的素材通常来自相册或者拍摄,苹果的PhotosKit提供了管理相册资源的接口,而AVFoundation中的Capture模块则负责相机拍摄的部分。

一、 拍摄

1.1 基础拍照录制功能

我们首先通过实现一个简单的拍照和录制视频(不支持暂停继续)的功能来认识拍摄模块的使用方式,需要使用的核心类如下:

  • AVCaptureSession:AVCaptureSession是管理拍摄活动的开始和停止,并协调从输入设备到输出数据流的对象,接收来自摄像头和麦克风等录制设备的输入数据,将数据协调至适当的输出进行处理,最终生成视频、照片或元数据。
  • AVCaptureDevice:一个AVCaptureDevice对象表示一个物理录制设备和与该设备相关联的属性(曝光模式、聚焦模式等)。录制设备向AVCaptureSession对象提供输入数据,不过AVCaptureDevice不能直接添加至AVCaptureSession中,而是需要封装为AVCaptureDeviceInput对象,来作为AVCaptureSession的输入源。
  • AVCaptureOutput:决定了录制会话数据流的输出方式,通常我们使用其子类来决定输出什么样的数据格式,其中AVCaptureMetadataOutput用于处理定时元数据的输出,包含了人脸检测或机器码识别的数据;AVCapturePhotoOutput用于静态照片、实况照片的输出; AVCaptureVideoDataOutput用于记录视频并提供对视频帧进行处理的录制输出。AVCaptureMovieFileOutput继承自AVCaptureFileOutput:将视频和音频记录到QuickTime电影文件的录制输出。AVCaptureDepthDataOutput在兼容的摄像机设备上记录场景深度信息的录制输出。
  • AVCaptureConnection:用于连接AVCaptureSession中输入和输出的对象,要求音频和视频要对应。
  • AVCaptureVideoPreviewLayer:CALayer的子类,可以对录制视频数据进行实时预览。

学习AVFoundation相机拍摄功能最好的代码实例是苹果官方的demo-AVCam,苹果每年在相机功能方面进行改进的同时也会对该demo保持更新,这里不再附加实例代码。不过有些需要留意的点还是要提一下:

  • 相机和麦克风作为用户隐私功能,我们首先需要在info.plist中配置相应的访问说明,使用前也要检查设备授权状态AVCaptureDeviceAVAuthorizationStatus
  • 添加AVCaptureInput和AVCaptureOutput前都要进行canAddxx的判断。
  • 因为相机和麦克风设备可能不止一个应用程序在使用,对相机的闪光模式、曝光模式、聚焦模式等配置的修改需要放在[device lockForConfiguration:&error][device unLockForConfiguration:&error]之间,修改前还需要判断当前设备是否支持即将切换的配置。。
  • AVCaptureSession要运行在单独的线程,以免阻塞主线程。
  • 由于拍摄期间可能会被电话或其他意外情况打断,最好注册AVCaptureSessionWasInterruptedNotification以做出相应的处理。
  • 相机是一个CPU占用较高的硬件,如果设备承受过大的压力(例如过热),拍摄也可能会停止,最好通过KVO监听KeyPath"videoDeviceInput.device.systemPressureState",根据AVCaptureSystemPressureState调整相机性能。

上图是包含了最基础的照片拍摄和视频录制写入URLPath基本功能的流程。但是使用AVCaptureMovieFileOutput作为输出不能控制录制的暂停和继续。

1.2 控制录制过程

要控制录制写入过程,我们需要引入AVFoundaton中Assets模块的另一个类AVAssetWriterAVCaptureVideoDataOutputAVCaptureAudioDataOutput配合使用。

1.2.1 AVAssetReader

AVAsserReader用于从AVAsset实例中读取媒体样本,通常AVAsset包含多个轨道,所以必须给AVAsserReader配置一个或多个AVAssetReaderOutput实例,然后通过调用copyNextSampleBuffer持续访问音频样本或视频帧。AVAssetReaderOutput是一个抽象类,通常使用其子类来从不同来源读取数据,其中常用的有AVAssetReaderTrackOutput用于从资源的单个轨道读取媒体数据的对象; AVAssetReaderAudioMixOutput用于读取一个或多个轨道混合音频产生的音频样本的对象;AVAssetReaderVideoCompositionOutput用于从资源的一个或多个轨道读取组合视频帧的对象。

注意

  1. AVAsserReader在开始读取前可以设置读取的范围,开始读取后不可以进行修改,只能顺序向后读,不过可以在output中设置supportsRandomAccess = YES之后可以重置读取范围。虽然AVAssetReader的创建需要一个AVAsset实例,但是我们可以通过将多个AVAsset组合成一个AVAsset的子类AVComposition进行多个文件的读取,AVComposition会在视频编辑中详细介绍。
  2. AVAssetReader不适合读取实时媒体数据,例如HLS实时数据流。

下面是读取一个视频轨道数据的示例:

AVAsset *asset = ...;
// 获取视频轨道
AVAssetTrack *track = [[asset tracksWithMediaType:AVMediaTypeVideo]firstObject];
// 通过asset创建读取器
AVAssetReader *assetReader = [[AVAssetReader alloc] initWithAsset:asset error:nil];
// 配置outsettings
NSDictionary *readerOutputSettings = @{               
    (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA)
};
AVAssetReaderOutput *trackOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:track outputSettings:readerOutputSettings];
[assetReader addOutput:trackOutput];
// 调用开始读取,之后不断获取下一帧直到没有数据返回
[assetReader startReading];
while (assetReader.status == AVAssetReaderStatusReading && !completedOrFailed) {        
    CMSampleBufferRef sampleBuffer = [trackOutput copyNextSampleBuffer];   
    if (sampleBuffer) {        
        CMBlockBufferRef blockBufferRef =                               
        CMSampleBufferGetDataBuffer(sampleBuffer);    
        size_t length = CMBlockBufferGetDataLength(blockBufferRef);
        SInt16 sampleBytes[length];
        CMBlockBufferCopyDataBytes(blockBufferRef, 0, length, sampleBytes);
        // 你的处理xxx例如重新编码写入
        CMSampleBufferInvalidate(sampleBuffer);                         
        CFRelease(sampleBuffer);
    }
}
    if (assetReader.status == AVAssetReaderStatusCompleted) {             
        // Completed
        completedOrFailed = YES;
    }

1.2.2 AVAssetWriter

AVAssetWriter用于对资源进行编码并将其写入到容器文件中。它由一个或多个AVAssetWriterInput对象配置,用于附加媒体样本的CMSampleBuffer。在我们使用AVAssetWriter的时候,经常会用到AVAssetWriterInputPixelBufferAdaptor,用于将打包为像素缓冲区的视频样本追加到AVAssetWriter输入的缓冲区,用于把缓冲池中的像素打包追加到视频样本上,举例来说,当我们要将摄像头获取的原数据(一般是CMSampleBufferRef)写入文件的时候,需要将CMSampleBuffer转成CVPixelBuffer,而这个转换是在CVPixelBufferPool中完成的,AVAssetWriterInputPixelBufferAdaptor的实例提供了一个CVPixelBufferPool,可用于分配像素缓冲区来写入输出数据,苹果文档介绍,使用它提供的像素缓冲池进行缓冲区分配通常比使用额外创建的缓冲区更高效。

AVAssetWriter使用示例:

NSURL *outputURL = ...;
// 通过一个空文件的ur来创建写入器
AVAssetWriter *assetWriter = [[AVAssetWriter alloc] initWithURL:outputURL fileType:AVFileTypeQuickTimeMovie error:nil];
// 配置outsettings
NSDictionary *writerOutputSettings = @{
                                           AVVideoCodecKey : AVVideoCodecH264,
                                           AVVideoWidthKey : @1080,
                                           AVVideoHeightKey : @1920,
                                           AVVideoCompressionPropertiesKey : @{
                                                   AVVideoMaxKeyFrameIntervalKey : @1,
                                                   AVVideoAverageBitRateKey : @10500000,
                                                   AVVideoProfileLevelKey : AVVideoProfileLevelH264Main31
                                                   }
                                           };
// 使用视频格式文件作为输入
AVAssetWriterInput *writerInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:writerOutputSettings];
[assetWriter addInput:writerInput];
// 开始写入
[assetWriter startWriting];

AVAssetWriter可用于实时操作和离线操作两种情况,对于每个场景有不同的方法将样本buffer添加到写入对象的输入中:

实时:处理实时资源时,例如从AVCaptureVideoDataOutput写入录制的样本时,AVAssetWriterInput应该设置expectsMediaDataInRealTime属性为YES来确保isReadyForMoreMediaData值被正确设置,不过在写入开始后,无法再修改此属性。

离线: 当从离线资源读取媒体资源时,比如从AVAssetReader读取样本buffer,在附加样本前仍然需要观察写入的readyForMoreMediaData属性的状态,不过可以使用requestMediaDataWhenReadyOnQueue:usingBlock:方法控制数据的提供。传到这个方法中的代码会随写入器输入准备附加更多的样本而不断被调用,添加样本时开发者需要检索数据并从资源中找到下一个样本进行添加。

AVAssetReaderOutput和AVAssetWriterInput都可以配置outputSettings,outputSettings正是控制解、编码视频的核心。

AVVideoSettings

  • AVVideoCodecKey 编码方式
  • AVVideoWidthKey 像素宽
  • AVVideoHeightKey 像素高
  • AVVideoCompressionPropertiesKey 压缩设置:
    • AVVideoAverageBitRateKey 平均比特率
    • AVVideoProfileLevelKey 画质级别
    • AVVideoMaxKeyFrameIntervalKey 关键帧最大间隔 AVAudioSettings
  • AVFormatIDKey 音频格式
  • AVNumberOfChannelsKey 采样通道数
  • AVSampleRateKey 采样率
  • AVEncoderBitRateKey 编码码率 更多的设置,参见苹果官方文档Video Settings

读取时,outputSetting 传入nil,得到的将是未解码的数据。

AVAssetReader可以看做解码器,与AVAssetReaderOutput配套使用,决定以什么样的配置解码成buffer数据;AVAssetWriter可以看做编码器,与AVAssetWriterInput配套使用,决定将数据以什么配置编码成视频,CMSampleBuffer为编码的数据,视频经AVAssetReader后输出CMSampleBuffer,经AVAssetWriter可以重新将CMSampleBuffer编码成视频。

下面是AVAssetReader和AVAssetWriter成对使用用作视频转码示例:

- (BOOL)startAssetReaderAndWriter {
    // 尝试开始读取
     BOOL success = [self.assetReader startReading];
    if (success){
        // 尝试开始写
        success = [self.assetWriter startWriting];
     }
     if (success) {
          // 开启写入session
          self.dispatchGroup = dispatch_group_create();
          [self.assetWriter startSessionAtSourceTime:kCMTimeZero];
          self.videoFinished = NO;
          if (self.assetWriterVideoInput) {
               dispatch_group_enter(self.dispatchGroup);
               [self.assetWriterVideoInput requestMediaDataWhenReadyOnQueue:self.rwVideoSerializationQueue usingBlock:^{
                    BOOL completedOrFailed = NO;
                    // WriterVideoInput准备好元数据时开始读写
                    while ([self.assetWriterVideoInput isReadyForMoreMediaData] && !completedOrFailed) {
                         // 获取视频下一帧 加入 output中
                         CMSampleBufferRef sampleBuffer = [self.assetReaderVideoOutput copyNextSampleBuffer];
                         if (sampleBuffer != NULL) {
                              BOOL success = [self.assetWriterVideoInput appendSampleBuffer:sampleBuffer];
                              CFRelease(sampleBuffer);
                              sampleBuffer = NULL;
                              completedOrFailed = !success;
                         } else {
                              completedOrFailed = YES;
                         }
                    }
                    if (completedOrFailed) {
                         // 标记写入结束
                         BOOL oldFinished = self.videoFinished;
                         self.videoFinished = YES;
                         if (oldFinished == NO) {
                              [self.assetWriterVideoInput markAsFinished];
                         }
                         dispatch_group_leave(self.dispatchGroup);
                    }
               }];
          }
          // 监听读取写入完成状态
          dispatch_group_notify(self.dispatchGroup, self.mainSerializationQueue, ^{
               BOOL finalSuccess = YES;
              if ([self.assetReader status] == AVAssetReaderStatusFailed) {
                   finalSuccess = NO;
              }
              // 完成写入
              if (finalSuccess) {
                   finalSuccess = [self.assetWriter finishWriting];
              }
               // 处理写入完成
               [self readingAndWritingDidFinishSuccessfully:finalSuccess];
          });
     }
     return success;
}

但是两者并不要求一定成对使用,AVAssetWriter要处理的数据是前面介绍的CMSampleBufferCMSampleBuffer可以从相机拍摄视频时获取实时流,也可以通过图片数据转换得来(图片转视频)。

下面是通过AVAssetWiter将AVCaptureVideoDataOutput的代理方法中的CMSampleBuffer写入文件的核心代码。

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    [_writer startWriting];
    [_writer startSessionAtSourceTime:startTime];
    if(captureOutput == self.videoDataOutput) {
        //视频输入是否准备接受更多的媒体数据
    	if (_videoInput.readyForMoreMediaData == YES) {
            //拼接视频数据
            [_videoInput appendSampleBuffer:sampleBuffer];
        }
    } else {
        //音频输入是否准备接受更多的媒体数据
        if (_audioInput.readyForMoreMediaData) {
            //拼接音频数据
            [_audioInput appendSampleBuffer:sampleBuffer];
        }
    }
}

至此我们可以实现视频录制的暂停与继续,基本上已经介绍了大多数app相机模块实现的主要功能架构,如下:

1.3 相机的其他功能

苹果每年都会对设备的相机功能进行优化或扩展,除了简单的拍照和录像,我们还可以使用Capture模块得到更多数据。

1.3.1 人脸、身体和机器可读码

AVFoundation中的人脸检测AVMetadataFaceObject功能在iOS6.0就开始支持,iOS13.0增加了对身体的检测,包含人体AVMetadataHumanBodyObject、猫身体AVMetadataCatBodyObject、狗身体AVMetadataDogBodyObject。他们都继承自AVMetadataObject,除了各自增加了诸如faceIDbodyID这样的属性外,他们的属性主要来自AVMetadataObject,其中bounds是检测到的目标的轮廓,当然,人脸检测补充了沿着z轴旋转的人脸角度rollAngle和是沿着y轴旋转的人脸角度yawAngle

如果我们想要检测人脸的关键点数据,可以使用Vision框架中的VNDetectFaceRectanglesRequestARKit框架中的ARFaceTrackingConfiguration,都可以吊起相机获取人脸关键点的数据。

iOS7.0增加了机器可读码(AVMetadataMachineReadableCodeObject)的识别功能,返回了包含表示机器码的字符含义的stringValue数据,在WWDC2021What's new in camera capture中提到了辅助可读码识别功能一个重要的属性minimumFocusDistance,是指镜头能够合焦的最近拍摄距离,所有摄像头都会包含该参数,只是苹果在iOS15.0才公开该属性,我们可以使用该属性调整相机的放大倍数,以解决低于最近识别距离后无法识别的问题,详细源码可参考官网的demoAVCam​条码:检测条码和人脸

这里把这些并不相关的检测放在一块介绍是因为从API的角度,他们都以AVCaptureMetadataOutput作为输出,AVCaptureMetadataOutput提供了一个metadataObjectTypes数组属性,我们可以传入一个或多个要检测的类型,实现AVCaptureMetadataOutputObjectsDelegate协议的- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection;方法,从metadataObjects中获取想要的数据。

1.3.2 Live photo

live photo是iOS10.0推出的功能,系统相机app中选择“照片”项右上角的live标志控制是否开启拍摄live photo功能。开启live photo功能会拍摄下用户点击拍摄按钮前后各0-1.5秒(官网说的是1.5秒)的视频,取中间的一帧作为静态图片和一个3秒内的视频一起保存下来,在相册中长按照片可以播放其中的视频。

使用live photo拍摄API,需要使用AVCapturePhotoOutputisLivePhotoCaptureSupported属性判断是否支持该功能,将photoOutput的livePhotoCaptureEnabled属性设为YES,然后创建一个视频保存的路径为photoSettings的livePhotoMovieFileURL属性赋值,其他跟静态照片拍摄一样。

注意 live photo只能运行在AVCaptureSessionPresetPhoto预设模式下,且不能和AVCaptureMovieFileOutput共存。

live photo的拍摄有自己的两个回调方法:

// 已经完成整段视频的录制,还没写入沙盒
- (void) captureOutput:(AVCapturePhotoOutput *)captureOutput didFinishRecordingLivePhotoMovieForEventualFileAtURL:(NSURL *)outputFileURL resolvedSettings:(AVCaptureResolvedPhotoSetting s *)resolvedSettings;
// 视频已经写入沙盒
- (void) captureOutput:(AVCapturePhotoOutput *)captureOutput didFinishProcessingLivePhotoToMovieFileAtURL:(NSURL *)outputFileURL duration:(CMTime)duration photoDisplayTime:(CMTime)photoDisplayTime resolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings error:(NSError *)error;

注意:保存Live Photo必须和图片使用同一个PHAssetCreationRequest对象,才能将两者关联起来,要展示实况照片,需要使用PHLivePhotoView,它默认添加了长按播放实况照片的手势。

1.3.3 景深

苹果在相机方面的功能和Capture模块的API每年都会有很多的更新,但是像深度数据这样,推出以来从图片编辑到视频编辑从软件到硬件,不断优化不断拓宽应用领域和深度得并不多,景深数据值得我们持续关注。

景深是指摄像头拍照时获取到图片中的物体在现实世界的远近数据,苹果在iOS11.0在具有双摄像头的设备中推出了带有景深数据的人像模式,最初后置摄像头的景深数据是使用跳眼法通过两个摄像头的数据根据相似三角形原理计算得来,前置摄像头通过红外线探测,后来苹果引入了LiDAR模组,通过光线探测测距能够得到精确的景深数据,它对AR模块也有很大帮助。

用来描述景深数据的是AVDepthData类,其包含的核心属性如下:

depthDataType: 景深数据的数据类型,kCVPixelFormatType_DisparityX表示的是视差数据,kCVPixelFormatType_DepthX表示的是深度数据,可以转换。
depthDataMap: 景深的数据缓冲区,可以转成UIImage
isDepthDataFiltered: 是否启动插值
depthDataAccuracy: 景深数据的准确度

注意:通过UIImge创建的imag不会包含景深数据,需要使用photosKit框架读取。

在AVFoundation的Capture模块,景深数据录制分为静态景深录制和实时景深录制。

  • 静态景深录制:静态景深录制只需要配置AVCapturePhotoOutputAVCapturePhotoSettingsisDepthDataDeliveryEnabled为YES,在代理方法中即可获取photo.depthData数据,我们可以将景深数据中的depthDataMap转为图片存相册,也可以将数据写入原图,保存为一张带有景深数据的人像图。
  • 实时景深:顾名思义,要有数据流的支撑,需要同时使用AVCaptureVideoDataOutput和景深输出AVCaptureDepthDataOutput,但是景深输出的帧率和分辨率都远低于视频数据输出(性能考虑),为解决这一问题,苹果专门引入了AVCaptureDataOutputSynchronizer来协调各个流的输出。
self.dataOutputSynchronizer = [[AVCaptureDataOutputSynchronizer alloc] initWithDataOutputs:@[self.videoOutput, self.depthOutput]];
[self.dataOutputSynchronizer setDelegate:self queue: self.cameraProcessingQueue];

然后我们就可以在代理方法中得到AVCaptureSynchronizedDataCollection实例

- (void)dataOutputSynchronizer:(AVCaptureDataOutputSynchronizer *)synchronizer didOutputSynchronizedDataCollection:(AVCaptureSynchronizedDataCollection *)synchronizedDataCollection{
    AVCaptureSynchronizedDepthData *depthData = (AVCaptureSynchronizedDepthData *)[synchronizedDataCollection synchronizedDataForCaptureOutput:self.depthOutput];
}

有了深度数据,我们可以使用Core Image提供的各种遮罩、滤镜、更改焦点等效果,让照片显示出不同的效果的同时仍然保持层次感,具体的应用可以参考Video Depth Maps Tutorial for iOS

在添加了实时景深输出后,相机的架构变成了这样:

AVFoundation的Capture模块为我们提供了自定义相机的拍照、录像、实况照片、景深人像模式、人脸身体检测、机器码识别等等,此外诸如多相机拍摄、图像分割(头发、牙齿、眼镜、皮肤)。。。不再深入介绍。

二、 相册

相册是视频剪辑素材的另一个来源,苹果的系统相册可以保存图片、视频、实况照片、gif动图等,剪映、快影和wink等视频剪辑app对于从相册中选择的素材都统一转为了一段视频,下面分别介绍转为视频的方法。

2.1 静态图片转视频

静态图片转视频的功能所使用的核心类 AVAssetWriter 前面已经学习过了,和视频录制写入文件的差别在于数据的来源变成了相册中的图片,缺点是使用 AVAssetWriter 写入文件过程中不支持预览,这个问题我们会在视频编辑部分解决。

2.2 实况照片转视频

前面已经介绍了如何使用自定义相机拍摄和保存实况照片,而大多app从相册中直接获取去使用交给UIImage的往往是一张静态图片,要转为视频进行编辑,我们需要使用PhotosKit提供的API。

// 创建实况照片请求配置
PHLivePhotoRequestOptions* options = [[PHLivePhotoRequestOptions alloc] init];
options.deliveryMode = PHImageRequestOptionsDeliveryModeFastFormat;
[[PHImageManager defaultManager] requestLivePhotoForAsset:phAsset targetSize:[UIScreen mainScreen].bounds.size contentMode:PHImageContentModeDefault options:options resultHandler:^(PHLivePhoto * _Nullable livePhoto, NSDictionary * _Nullable info) {
    NSArray* assetResources = [PHAssetResource assetResourcesForLivePhoto:livePhoto];
    PHAssetResource* videoResource = nil;
    // 判断是否含有视频资源
    for(PHAssetResource* resource in assetResources){
        if (resource.type == PHAssetResourceTypePairedVideo) {
            videoResource = resource;
            break;
        }
    if(videoResource){
        // 将视频资源写入指定路径
        [[PHAssetResourceManager defaultManager] writeDataForAssetResource:videoResource toFile:fileUrl options:nil completionHandler:^(NSError * _Nullable error) {
    dispatch_async(dispatch_get_main_queue(), ^{
		// 去使用视频资源
        [self handleVideoWithPath:self.outPath];
    });
}];

值得一提的是,PHAsset还有一个私有方法fileURLForVideoComplementFile可以直接获取实况照片中视频文件的URL地址,不过私有API要避免在线上使用。

2.3 gif动图转视频

gif由多张图片组合,利用视觉暂留原理形成动画效果,要把gif转为视频的关键是获取gif中保存的单帧和每帧停留的时间,ImageIO.framework提供了相关的接口。

// 从相册读取gif
PHImageManager *manager = [PHImageManager defaultManager];
PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
[manager requestImageDataAndOrientationForAsset:asset options:options resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, CGImagePropertyOrientation orientation, NSDictionary * _Nullable info) {
    CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)imageData, NULL);
    CFRetain(imageSource);
    // 获取gif包含的帧数
    NSUInteger numberOfFrames = CGImageSourceGetCount(imageSource);
    NSDictionary *imageProperties = CFBridgingRelease(CGImageSourceCopyProperties(imageSource, NULL));
    NSDictionary *gifProperties = [imageProperties objectForKey:(NSString *)kCGImagePropertyGIFDictionary];
    NSTimeInterval totalDuratoin = 0;//开辟空间
    NSTimeInterval *frameDurations = (NSTimeInterval *)malloc(numberOfFrames * sizeof(NSTimeInterval));
    //读取循环次数
    NSUInteger loopCount = [gifProperties[(NSString *)kCGImagePropertyGIFLoopCount] unsignedIntegerValue];
    //创建所有图片的数值
    NSMutableArray *images = [NSMutableArray arrayWithCapacity:numberOfFrames];
    for (NSUInteger i = 0; i < numberOfFrames; ++i) {
        //读取每张的显示时间,添加到数组中,并计算总时间
        CGImageRef image = CGImageSourceCreateImageAtIndex(imageSource, i, NULL);
        [images addObject:[UIImage imageWithCGImage:image scale:1.0 orientation:UIImageOrientationUp]];
        CFRelease(image);
        NSTimeInterval frameDuration = [self getGifFrameDelayImageSourceRef:imageSource index:i];
        frameDurations[i] = frameDuration;
        totalDuratoin += frameDuration;
    }
CFRelease(imageSource);
}];

单帧的停留时间,保存在kCGImagePropertyGIFDictionary字典中,只是其中包含了两个看起来很相似的key:kCGImagePropertyGIFUnclampedDelayTime:数值可以为0,kCGImagePropertyGIFDelayTime:值不会小于100毫秒。很多gif图片为了得到最快的显示速度会把duration设置为0, 浏览器在显示他们的时候为了性能考虑就会给他们减速(clamp),通常我们会取先获取 kCGImagePropertyGIFUnclampedDelayTime 的值,如果没有就取 kCGImagePropertyGIFDelayTime的值, 如果这个值太小就设置为0.1,因为gif的标准中对这一数值有限制,不能太小。

- (NSTimeInterval)getGifFrameDelayImageSourceRef:(CGImageSourceRef)imageSource index:(NSUInteger)index
{
    NSTimeInterval frameDuration = 0;
    CFDictionaryRef theImageProperties;
    if ((theImageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, index, NULL))) {
        CFDictionaryRef gifProperties;
        if (CFDictionaryGetValueIfPresent(theImageProperties, kCGImagePropertyGIFDictionary, (const void **)&gifProperties)) {
        const void *frameDurationValue;
            // 先获取kCGImagePropertyGIFUnclampedDelayTime的值
            if (CFDictionaryGetValueIfPresent(gifProperties, kCGImagePropertyGIFUnclampedDelayTime, &frameDurationValue)) {
                frameDuration = [(__bridge NSNumber *)frameDurationValue doubleValue];
                // 如果值不可用,获取kCGImagePropertyGIFDelayTime的值
                if (frameDuration <= 0) {
                    if (CFDictionaryGetValueIfPresent(gifProperties, kCGImagePropertyGIFDelayTime, &frameDurationValue)) {
                        frameDuration = [(__bridge NSNumber *)frameDurationValue doubleValue];
                    }
                }
            }
        }
    CFRelease(theImageProperties);
    }
    // 如果值太小,则设置为0.1
    if (frameDuration < 0.02 - FLT_EPSILON) {
        frameDuration = 0.1;
    }
    return frameDuration;
}

在获取了所有的图片和图片停留时间后,我们就可以使用图片转视频的方法进行处理了。这部分内容我们在编辑部分对视频中添加gif表情包也会用到。

至此,无论是从相机拍摄还是相册获取,我们都能结合AVFoundation框架得到我们想要的视频文件来作为视频编辑的主素材,可以正式开始我们的编辑了。

总结

本篇从介绍了录制和相册两个途径进行短视频编辑需要的素材的添加与处理,为控制录制的过程,穿插了AVAssetReader和AVAssetWriter的介绍,在以后的视频编辑中我们还会深入讲解相关的应用。下一篇我们正式开始介绍使用AVFoundation进行短视频编辑。

参考链接

AVFoundation 构建一个相机app-苹果官方
wwdc2021-What’s new in camera capture