短视频编辑中的AVFoundation框架(一)基础与素材

AVFoundation是在 iOS、macOS、watchOS 和 tvOS 上处理多媒体数据的功能齐全的框架。使用AVFoundation,我们可以播放、创建和编辑 QuickTime movie和 MPEG-4 文件,播放 HLS 流,并将强大的媒体编辑功能构建到我们的应用程序中。

AVFouondation框架概述

我们首先来看AVFouondation框架在苹果的多媒体框架体系中的位置。在苹果的多媒体体系中,高层级的AVKit提供了 高度封装的播放器控制类AVPlayerViewController 、用于切换播放路由(投屏)的AVRoutePickerView,以及实现画中画播放效果的AVPictureInPictureController。低层级的框架主要以C接口为主,其中:

  • Core Audio 最底层的音频处理接口,直接驱动设备的音频硬件,对音乐游戏或专业的音频编辑软件提供了全面的支持。Audio Unit提供了包含合成乐器声音、回声消除、混音、声音均衡等相关接口。

Audio Unit已经迁移到 Audio Toolbox Framework了。

  • Core Video 为其相对的Core Media提供图片缓存(CVPixelBuffer)和缓存池(CVPixelBufferPool)支持,提供了一个能够对数字视频逐帧访问的接口,以及对Metal(CVMetalTexture) 、OpenGL(CVOpenGLTexture) 以及OpenGLES(CVOpenGLESTexture)的支持;
  • Core Media 定义和封装了AVFoundation等更上层的媒体框架需要的媒体处理流水线(包含时间信息)以及其中使用的接口和数据类型(CMSampleBufferCMTime)。使用 Core Media 层的接口和数据类型可以高效的处理媒体采样数据、管理采样数据队列(CMSimpleQueue、CMBufferQueue);
  • Core Animation 是iOS中动画相关的框架,AVFoundation结合Core Animation让开发者能够在视频编辑和播放过程中添加动画和贴纸效果。

而AVFoundation位于高层级框架和低层级框架之间,封装了低层级框架才能实现的功能,提供了OC和Swift接口,同时苹果在迭代过程中不断优化AVFoundation这种中间层框架的性能,很好地适配了新的设备和视频格式。因为位于UIKit之下,AVFoundation同样适用于苹果的其他平台:macOS、tvOS、watchOS。

1-1

AVFoundation官方文档介绍:AVFoundation结合了六个主要技术领域,这些领域覆盖了在Apple平台上录制、处理、合成、控制、导入和导出视听媒体的主要功能。API Dodument主要划分了Assets媒体资源、Playback播放、Capture录制、Editing编辑、Audio音频、Speech(朗读)六部分。

  • Assets:提供加载、检查和导出媒体资源和元数据信息的功能,也可以使用AVAssetReaderAVAssetWriter对媒体样本数据进行样本级读写,使用AVAssetImageGenerator获取视频缩略图,使用AVCaption进行字幕创作(mac os)等。
  • Playback:提供对AVAsset的播放和播放控制的功能,可以使用AVPlayer播放一个项目,也可以使用AVQueuePlayer播放多个项目,AVSynchronizedLayer可以让我们结合Core Animation将动画层与播放视图层进行同步,实现播放中的诸如贴纸、文字等效果。
  • Capture:用于拍摄照片、录制音频和视频,通过配置内置摄像头和麦克风或外部录制设备,可以构建自定义相机功能,控制照片和视频拍摄的输出格式,或者直接修改音频数据流作为自定义输出。
  • Editing:用于将来自多个来源的音频和视频轨道组合、编辑和重新混合到一个AVMutableComposition中,可以使用AVAudioMixAVVideoComposition分别控制音频混合和视频合成的细节。
  • Audio:播放、录制和处理音频;配置应用程序的系统音频行为。苹果又在iOS14.5整合了一个AVFAudio框架,内容与这部分完全相同,可能未来会把音频部分单独处理。
  • Speech:将文本转换为语音音频进行朗读。

Assets作为AVFoundation媒体处理的基础是需要首先学习的内容。

基础模块-Assets

AVAsset & AVAssetTrack

AVAsset是一个抽象类和不可变类,定义了媒体资源混合呈现的方式,将媒体资源的静态属性模块化成一个整体。它提供了对基本媒体格式的层抽象,这意味着无论是处理Quick Time影片还是MP3音频,对开发者和对框架其余部分而言,面对的只有资源这个概念。同时创建AVAsset实例的URL可以来自远程或者本地甚至是流媒体,使我们无须关注其来源,专注于处理AVAsset本身。

一个AVAsset实例是一个或多个AVAssetTrack实例的容器,该实例是对媒体的统一类型”轨道”进行的建模。一个简单的视频文件通常包含一个音频轨道和一个视频轨道,也可能包含一些补充内容,如隐藏式字幕、字幕,也可能包含描述媒体内容的元数据(AVMetadataItem)。

隐藏式字幕即“Closed Caption”,简称CC字幕。大多数CC字幕和剧本是一样的,里面除了对白之外,还有场景中产生的声音和配乐等的描述,主要是为了听障人士所设置的,“Closed”一词也说明了并不是默认开启的状态,与之相对的是“Open Caption”,也就是通常所说的字幕,而与对话语言相同的字幕称为”Caption“,不同的(即翻译)称为“Subtitle”。

创建一个AVAsset是一个轻量级操作,因为AVAsset的底层媒体数据采用了延迟加载的设计,直到获取时才会进行加载,如果没有进行提前进行异步加载直接去访问它的属性可能会阻塞调用线程,不过这也要取决于要访问的媒体数据的大小和位置。为了避免阻塞线程,我们最好在使用之前进行异步加载属性。AVAsset和AVAssetTrack都遵循了AVAsynchronousKeyValueLoading协议,可以进行异步加载属性和获取加载状态。

@protocol AVAsynchronousKeyValueLoading
// 异步加载包含在keys数组中的属性,在handler中使用statusOfValueForKey:error:方法判断加载是否完成。
- (void)loadValuesAsynchronouslyForKeys:(NSArray<NSString *> *)keys completionHandler:(nullable void (^)(void))handler;
// 获取key属性加载的状态,status为AVKeyValueStatusLoaded为加载完成。
- (AVKeyValueStatus)statusOfValueForKey:(NSString *)key error:(NSError * _Nullable * _Nullable)outError;
复制代码

WWDC2021What’s new in AVFoundation提到,针对swift引入了async / await ,让我们得以使用与同步编程类似的控制流来进行异步编程。

let asset = AVAsset (url: assetURL)
let duration = trv await asset.load(.duration)
// 我们也可以加载多个属性,使用元组接收返回值:
let (duration, tracks) = try await asset.load(.duration, .tracks)
复制代码

AVAsset的属性:

代码示例中tracks属性返回的是一个AVAsset实例包含的所有AVAssetTrack实例的数组,苹果也提供了根据特定标准(如标识符、媒体类型或特征)检索轨道子集的方法如下,这也是编辑模块中取出某种类型的轨道要调用的方法。

// 根据TrackID检索轨道
- (void)loadTrackWithTrackID:(CMPersistentTrackID)trackID completionHandler:(void (^)(AVAssetTrack * _Nullable_result, NSError * _Nullable))completionHandler;
// 根据媒体类型检索轨道子集
- (voidloadTracksWithMediaType:(AVMediaType)mediaType completionHandler:(void (^)(NSArray<AVAssetTrack *> * _Nullable NSError * _Nullable))completionHandler;
// 根据媒体特征检索轨道子集
- (void)loadTracksWithMediaCharacteristic:(AVMediaCharacteristic)mediaCharacteristic completionHandler:(void (^)(NSArray<AVAssetTrack *> * _Nullable, NSError * _Nullable))completionHandler;
复制代码

元数据

媒体容器格式存储关于其媒体的描述性元数据,每种容器格式都有自己独特的元数据格式,AVFoundation通过使用其AVMetadataItem类简化了对元数据的处理,在最基本的形式中,AVMetadataItem的实例是一个键值对,表示单个元数据值,比如电影的标题或专辑的插图。

要高效地使用AVMetadataItem,我们需要了解AVFoundation是如何组织元数据的。为了简化元数据项的查找和过滤,AVFoundation框架将相关元数据分组到键空间中:

  • 特定格式的键空间。AVFoundation框架定义了几个特定格式的键空间,它们大致与特定容器或文件格式相关,例如 QuickTime(QuickTime 元数据和用户数据)或 MP3 (ID3)。但是,单个资源可能包含跨多个键空间的元数据值。要检索资源的特定格式元数据的完整集合,可以使用metadata属性。
  • Common键空间。有几个常见的元数据值,例如电影的创建日期或描述,可以存在于多个键空间中。为了帮助规范化对这个公共元数据的访问,该框架提供了一个common键空间,它允许访问几个键空间共有的一组有限元数据值。要检索资源的公共元数据集合,可以直接使用commonMetadata属性。

除此之外,我们还可以通过调用AVAssetavailableMetadataFormats属性来确定资源包含哪些元数据格式。此属性返回包含每个元数据格式的字符串标识符数组。然后使用它的metadataForFormat:方法,通过传递适当的格式标识符来检索特定于格式的元数据值。

下面列举了一个视频文件的AVMetadataItem:

creationDate :  2022-03-01T18:16:17+0800
location : +39.9950+116.4749+044.903/
make: Apple
model:  iPhone 13 Pro
software: 15.3.1
复制代码

虽然可以不用关注,但是拿到一个视频文件(.mov),如果仍然想获取视频样本的编码类型(h264/hevc)、传递函数(ITU_R_709_2/ITU_R_2100_HLG)等,获取音频样本的采样率、通道数、位深等元数据样本格式信息,我们应该从哪里入手呢?前面我们介绍了在一个AVAsset资源中以轨道的形式把音频、视频等文件分别进行了单独的轨道建模,如果要获取视频样本格式的信息,只要根据媒体类型检索相应的轨道,获取assetTrack的formatDescriptions属性,即可拿到全部视频格式信息CMVideoFormatDescription的集合,同样还有CMAudioFormatDescriptionCMClosedCaptionFormatDescription等用于描述各自轨道样本的数据格式。

// 获取元数据样本格式信息
AVAssetTrack *videoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
NSArray *videoFormats = VideoTrack.formatDescriptions;
复制代码

认识了AVAsset和AVAssetTrack之后,我们建立了一种从轨道的角度去看待音视频文件的视角,之后会正式开始从视频编辑的角度学习AVFoundation框架。

添加素材

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

拍摄

基础拍照录制功能

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

  • 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调整相机性能。

图片[1]-短视频编辑中的AVFoundation框架(一)基础与素材-一一网
上图是包含了最基础的照片拍摄和视频录制写入URLPath基本功能的流程。但是使用AVCaptureMovieFileOutput作为输出不能控制录制的暂停和继续。

控制录制过程

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

AVAssetReader & AVAssetWriter

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;
    }
复制代码

CMSampleBuffer是读取器和写入器操作的对象,它是系统用来通过媒体管道移动媒体样本数据的核心基础对象,CMSampleBuffer的角色是将基础的样本数据进行封装并提供格式描述和时间信息。CMSampleBuffer有两种形式,一种是包含了压缩后的CMBlockBuffer,一种是包含了未解压状态的CVPixelBuffer。除了数据区外,还包含了两个视频帧的重要属性,一个是图像在视频中的 PTS:CMTime,以及对应的格式描述:CMFormatDesc.

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相机模块实现的主要功能架构,如下:

相机的其他功能

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

人脸、身体和机器可读码

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中获取想要的数据。

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,它默认添加了长按播放实况照片的手势。

景深

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

景深是指摄像头拍照时获取到图片中的物体在现实世界的远近数据,苹果在iOS11.0在具有双摄像头的设备中推出了带有景深数据的人像模式,最初后置摄像头的景深数据是使用跳眼法通过两个摄像头的数据根据相似三角形原理计算得来,前置摄像头通过红外线探测,后来苹果引入了LiDAR模组,通过光线探测测距能够得到精确的景深数据,它对AR模块也有很大帮助。
图片[2]-短视频编辑中的AVFoundation框架(一)基础与素材-一一网
用来描述景深数据的是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对于从相册中选择的素材都统一转为了一段视频,下面分别介绍转为视频的方法。

静态图片转视频

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

实况照片转视频

前面已经介绍了如何使用自定义相机拍摄和保存实况照片,而大多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要避免在线上使用。

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框架得到我们想要的视频文件来作为视频编辑的主素材,可以正式开始我们的编辑了。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享