AVFoundation 简单介绍和使用

AVFoundation

AVFoundation 是苹果 OS X系统和 iOS 系统中,用于处理基于时间的媒体数据的高级 Objective-C 框架。

1、体系结构

2、核心功能

音频录制和播放

AVAudioPlayer 和 AVAudioRecorder 提供了最简单和功能最强的音频播放和录制的功能(不是 AVFoundation 的唯一方式)。

媒体文件属性查看

AVAsset :是否可以用于回放、编辑、导出;获取该媒体的内容持续时间、创建日期、首选播放音量等技术参数。

AVMetadataItem :提供元数据支持,允许开发者读、写媒体的描述信息,比如唱片和艺术家信息。

视频播放

支持播放从本地或远程流中获取的视频资源,并控制视频播放和内容的展示。核心类是 AVPlayer 和 AVPlayerItem。

媒体录制

AVCaptureSession :作为所有活动的汇集点来接收摄像头拍摄的电影和图片。

媒体编辑

允许将多个音频和视频资源进行组合,允许修改和编辑独立的媒体片段、随时修改音频文件的参数以及添加动画标题和场景切换效果。

媒体处理

AVAssetReader 和 AVAssetWriter :实现对媒体资源更底层数据(字节级)的操作。

3、采样(Sampling)

我们日常的音频、视频都是模拟信息,通过耳朵、眼睛将这些信息转换为我们大脑能够解析的电信号。而这些模拟信号要转换成数字信号才能在数字世界进行储存和传输,这个模拟-数字转换的过程,称为采样。

数字媒体的采样

时间采样:捕捉一个信号周期内的变化。如在iPhone记录一个音频备忘录,所有音高、声调的变化都会捕捉下来。

空间采样:对一幅图片在一定分辨率下捕捉其亮度和色度,进而创建由该图片的像素数据所构成的数字信息。对视频来说,这两种方式都可以使用,因为视频信号既有空间属性又有时间属性。

音频采样

音色
音调:振动的速率或频率(单位赫兹,Hz)。
响度:频率的相对强度。

视频采样

视频文件由一系列称为“帧”的图片组成,在视频文件的时间轴上每一帧都表示一个场景。视频文件一秒中内所能展现的帧数称为“帧率”,并用FPS作为单位。常见的帧率是24FPS、25FPS、30FPS。

4、数字媒体压缩

对数字媒体进行压缩可以大幅度缩小文件的尺寸,同时通常在质量上有小幅度可见的衰弱。

色彩二次抽样(Chroma Subsampling)

Y'CbCr(数字信号)或YUV(模拟信号)颜色模型把图片分离成亮度通道 Y 和色彩(颜色)通道 UV。人眼对亮度的敏感度高于对色彩的敏感度,图片所有细节都保存在亮度通道,大幅度减少颜色信息不会让图片质量严重受损(比如彩色照片变成黑白照片)。这个减少颜色数据的过程就称为色彩二次抽样。

摄像头或其他设备中提到4:4:4、4:2:2、4:2:0时,这些值就是设备所使用的色彩二次抽样的参数,格式为:J:a:b。
J:关联色块所包含的像素数(一般是4个)。
a:J个像素中,第一行色素像素的个数。
b:J个像素中,第二行色素像素的个数。

编解码器(codec)压缩

无损压缩,比如 zip、gzip。
有损压缩:编解码器在压缩过程中会有部分数据损失掉。

视频编解码器

AVFoundation 只提供了苹果公司认定的目前最主流的类型支持,如 H.264 和 Apple ProRes(只在 OS X 上可用)。

H.264

H.264 是 MPEG(Motion Picture Experts Group)所定义的 MPEG-4 的一部分。和其他形式的 MPEG 压缩一样,通过以下两个维度缩小视频文件的尺寸。
空间:压缩独立视频帧,被称为帧内压缩。
时间:通过以组为单位的视频帧压缩冗余数据,这一过程称为帧间压缩。

音频编解码器

只要是 Core Audio 框架支持的音频编解码,AVFoundation 都支持,这意味着 AVFoundation 能够支持大量不同格式的资源。然而在不用线性 PCM(LPCM)音频的情况下,更多只能使用 AAC (Advanced Audio Coding)。

AAC

高级音频编码(AAC)是 H.264 标准相应的音频处理方式,目前是音频流和下载的音频资源中最主流的编码方式。这种格式比 MP3 个市有显著的提升,可以在低比特率下提供更高质量的音频,是在 Web 上发布和传播的音频格式中最为理想的。

容器格式

我们平常看到的文件,比如以 .mov、.m4v、.mpg、.m4a 为扩展名结尾的文件,通常认为他们是文件的文件格式,但其正确的定义是这些类型都是文件的容器格式(container format)。

可将容器格式视为包含一种或多种媒体类型(以及描述其内容的元数据)的目录。比如QuickTime文件可包含多种不同的媒体类型,包括视频、音频、字幕、章节信息和描述每个媒体片段细节的元数据等。

当开发者在使用 AVFoundation 时,主要遇到两类容器格式:QuickTime(.mov) 和 MPEG-4(.mp4、.m4v、.m4a)。

5、播放和录制音频

配置音频会话

AVAudioSession 提供了与应用程序音频会话交互的接口,通过设置合适的分类,可以音频播放指定需要的音频会话,定制一些行为。

AVAudioSession *session = [AVAudioSession sharedInstance];
NSError *error;
    
if (![session setCategory:AVAudioSessionCategoryPlayback error:&error])
{
    NSLog(@"Category Error : %@", [error localizedDescription]);
}

if (![session setActive:YES error:&error])
{
    NSLog(@"Activation Error : %@", [error localizedDescription]);
}

播放音频

AVAudioPlayer 提供了播放文件或内存中的音频数据的方法。

NSURL *fileUrl = [[NSBundle mainBundle] URLForResource:@"rock" withExtension:@"mp3"];
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithContentsOfURL:fileUrl error:nil];
if (player)
{
    [player prepareToPlay];
}

对播放进行控制

play方法:立即播放音频。
pause方法:暂停播放。
stop方法:停止播放(会撤销调用prepareToPlay时所做的设置)。

修改播放器的音量 volume:可以实现声音渐隐的效果,音量为从 0.0(静音) 到 1.0(最大音量)之间的浮点值。
修改播放器的 pan 值,允许使用立体声播放:pan 值范围从 -1.0(极左)到 1.0 (极右),默认值为0.0(居中)。
调整播放率:不改变音调的情况下,范围从0.5(半速)到2.0(2倍速)。
通过设置 numberOfLoops 属性实现音频无缝循环:设置大于0的数,可以实现播放器n次循环播放。设置为-1为无限循环。
进行音频计量:读取音量力度的平均值及峰值时,可将这些数据提供给 VU 计量器或其他可视化元件,向用户提供可视化反馈效果。

处理中断事件

当出现中断事件时,比如电话或FaceTime请求。如果不监听做处理,可能会出现一些问题。这时候可以监听 AVAudioSessionInterruptionNotification 通知。

NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self
           selector:@selector(handleInterruption:)
               name:AVAudioSessionInterruptionNotification
             object:[AVAudioSession sharedInstance]];
- (void)handleInterruption:(NSNotification *)notification
{
    NSDictionary *info = notification.userInfo;
    AVAudioSessionInterruptionType type = [info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
    
    if (type == AVAudioSessionInterruptionTypeBegan){}
    else if(type == AVAudioSessionInterruptionTypeEnded){}
}

处理线路改变

在iOS设备上添加和一处音频输入、输出线路时,会发生线路改变。比如我们用耳机听音频时,当断开耳机连接时,该音频应该处于静音状态。当线路发生变化时我们可以监听通知。

NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self
           selector:@selector(handlerRouteChange:)
               name:AVAudioSessionRouteChangeNotification
             object:[AVAudioSession sharedInstance]];
- (void)handlerRouteChange:(NSNotification *)notification
{
    NSDictionary *info = notification.userInfo;
    AVAudioSessionRouteChangeReason reason = [info[AVAudioSessionRouteChangeReasonKey] unsignedIntegerValue];
    
    if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable)
    {
        AVAudioSessionRouteDescription *previousRoute = info[AVAudioSessionRouteChangePreviousRouteKey];
        
        AVAudioSessionPortDescription *previousOutput = previousRoute.outputs[0];
        NSString *portType = previousOutput.portType;
        
        if ([portType isEqualToString:AVAudioSessionPortHeadphones]){}
    }
}

录制音频

AVAudioRecorder 和 AVAudioPlayer 一样,构建于 Audio Queue Services 上,我们可以在 Mac 或 iOS 设备上使用这个类来从内置麦克风或外部音频设备来录制音频。

NSString *directory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *filePath = [directory stringByAppendingString:@"voice.m4a"];
NSURL *url = [NSURL URLWithString:filePath];
    
NSDictionary *setting = @{AVFormatIDKey : @(kAudioFormatMPEG4AAC),
                          AVSampleRateKey : @22050.0f,
                          AVNumberOfChannelsKey : @1};
NSError *error;
AVAudioRecorder *recorder = [[AVAudioRecorder alloc] initWithURL:url settings:setting error:&error];
if (recorder)
{
    [recorder prepareToRecord];
}
else{}

音频测量

AVAudioRecorder 和 AVAudioPlayer 中一个很实用和强大的特性就是音频测量,Audio Metering 可让开发者读取音频的平均分贝和峰值分贝数据,并使用这些数据以可视化方式将声音的大小呈现给最终用户。

AVAudioRecorder 和 AVAudioPlayer 使用的方法都是 averagePowerForChannel: 和 peakPowerForChannel: ,他们都返回一个用于表示声音分贝(dB)等级的浮点值,这个值范围从最大分贝0dB(full scale)到最小分贝-160dB(静音)。

[recorder updateMeters];
[recorder averagePowerForChannel:0];
[recorder peakPowerForChannel:0];

在读取这些值之前,要先设置录音器的 meteringEnabled 属性为 YES。需要读取值时,要调用 updateMeters 方法才能获取最新的值。

recorder.meteringEnabled = YES;

Ps:音频测量的可视化绘制,如果时间短,可使用Quartz(占用CPU资源)。如果录制长时间的音频内容,应该选择更高效的绘制方法,比如OpenGL ES。

6、资源和元数据

AVFoundation 中最重要的类就是 AVAsset,它是 AVFoundation 设计的核心。
AVAsset 本身并不是媒体资源,它可以视为时基媒体(timed media)的容器。它由一个或多个带有描述自身元数据的媒体组成。我们使用 AVAssetTrack 类代表保存在资源中的媒体。AVAssetTrack最常见的形态就是音频和视频流,还可以表示文本、副标题、字幕等。

创建资源

NSURL *assetURL = [NSURL URLWithString:NSTemporaryDirectory()];
AVAsset *asset = [AVAsset assetWithURL:assetURL];
NSURL *assetURL = [NSURL URLWithString:NSTemporaryDirectory()];
NSDictionary *options = @{AVURLAssetPreferPreciseDurationAndTimingKey : @YES};
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:assetURL options:options];

从照片库中访问视频文件

以下例子展示了如何获取保存在 Saved Photos组中的视频资源,适用于 iOS4~iOS9。

ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
[library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos
                       usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
                           [group setAssetsFilter:[ALAssetsFilter allVideos]];
                           [group enumerateAssetsAtIndexes:[NSIndexSet indexSetWithIndex:0]
                                                   options:0
                                                usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {
                                                    if (result)
                                                    {
                                                        id representation = [result defaultRepresentation];
                                                        NSURL *url = [representation url];
                                                        AVAsset *asset = [AVAsset assetWithURL:url];
                                                        //Asset created. Perform some AVFoundation magic.
                                                    }
                                                }];
                       }
                     failureBlock:^(NSError *error) {
                         NSLog(@"Error : %@", [error description]);
                     }];

异步载入

AVAsset 可以提供资源的相关信息,比如时长、创建日期和元数据等。同时 AVAsset 使用了一种高效的设计方法,即延迟载入资源的属性,知道请求时才载入。这种情况会带来一些问题,比如请求资源的 duration,如果在头文件中没有设置 TLEN 标签(用于定义 duration 值),则整个音频曲目都需要进行解析来准确确定它的值,如果这个请求发生在主线程,就会阻塞主线程。

要解决以上问题,开发者应该使用异步的方式来查询资源的属性。AVAsset 和 AVAssetTrack 都采用 AVAsychronousKeyValueLoading 协议,通过下面的方法实现了异步查询属性的功能。

NSURL *assetURL = [[NSBundle mainBundle] URLForResource:@"sunset" withExtension:@"mov"];
AVAsset *asset = [AVAsset assetWithURL:assetURL];
NSArray *keys = @[@"tracks"];
[asset loadValuesAsynchronouslyForKeys:keys completionHandler:^{
    NSError *error = nil;
    AVKeyValueStatus status = [asset statusOfValueForKey:@"tracks" error:&error];
    switch (status)
    {
        case AVKeyValueStatusLoaded:
            break;
        case AVKeyValueStatusFailed:
            break;
        case AVKeyValueStatusCancelled:
            break;
        default:
            break;
    }
}];

该例为存储在应用程序bundle中的资源创建一个AVAsset,并异步载入该对象的tracks属性。实际情况中,可以在一个调用中请求多个属性。当请求多个属性时,ladValuesAsynchronouslyForKeys:completionHandler: 方法只会调用一次 completionHandler 块,而需要为每个请求的属性调用 statusOfValueForKey:error: 方法。

媒体元数据

AVFoundation 中主要的媒体格式都可以嵌入描述其内容的元数据。

元数据格式

使用元数据

AVAsset 和 AVAsset 都可以实现查询相关元数据的功能,大部分情况下我们会使用 AVAsset 提供的元数据。

7、视频播放

AVFoundation 的播放围绕 AVPlayer 展开,AVPlayer 是一个用来播放基于时间的视听媒体的播放器对象。支持从本地、分步下载或通过 HTTP Live Streaming 协议得到的流媒体。

AVPlayer 是一个不可见组件,如果播放 MP3 或 AAC 音频文件,那么没有可视化的用户界面不会有问题。如果要播放一个QuickTime 电影或一个 MPEG-4 视频,需要将视频导出到用户界面的目标位置,需要使用 AVPlayLayer类。

Ps:AVPlayer 只管理一个单独资源的播放。如果需要播放多个条目或为音视频设置循环播放,可以使用它的子类 AVQueuePlayer。

AVPlayerLayer

AVPlayLayer 扩展了 Core Animation 的 CALayer 类,并通过框架在屏幕上显示视频内容。AVPlayLayer是一个相对简单的类,只有一个 videoGravity 可供开发者自定义。

AVPlayerItem

如果查看 AVAsset 文档,可以找到比如创建日期、元数据和时长等信息,但无法获取当前时间。AVAsset 只包含媒体资源的静态信息,当我们需要对一个资源以及相关曲目进行播放时,需要先通过 AVPlayerItem 和 AVPlayerItemTrack 类构建相应的动态内容。

AVPlayerItem 会建立媒体资源动态视角的数据模型并保存 AVPlayer在播放时的呈现状态,在这个类的 seekToTime: 方法和 currentTime 和 PresentationSize 属性。

NSURL *assetUrl = [[NSBundle mainBundle] URLForResource:@"waves" withExtension:@"mp4"];
AVAsset *asset = [AVAsset assetWithURL:assetUrl];
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:asset];
AVPlayer *player = [AVPlayer playerWithPlayerItem:playerItem];
AVPlayerLayer *playLayer = [AVPlayerLayer playerLayerWithPlayer:player];
[self.view.layer addSublayer:playLayer];

在实际内容播放中,还需要一个步骤———— 使用 KVO 监听 AVPlayerItem 的 AVPlayerItemStatus 属性。当媒体还未载入、不在播放列表时,AVPlayerItemStatus 值为 AVPlayerItemStatusUnknown ,当状态值为 AVPlayerItemStatusReadyToPlay 时,才能开始播放。

NSURL *assetUrl = [[NSBundle mainBundle] URLForResource:@"waves" withExtension:@"mp4"];
AVAsset *asset = [AVAsset assetWithURL:assetUrl];
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:asset];
[playerItem addObserver:self
             forKeyPath:@"status"
                options:0
                context:@"PlayerItemStatusContext"];
AVPlayer *player = [AVPlayer playerWithPlayerItem:playerItem];
AVPlayerLayer *playLayer = [AVPlayerLayer playerLayerWithPlayer:player];
[self.view.layer addSublayer:playLayer];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    if (context == @"PlayerItemStatusContext")
    {
        AVPlayerItem *playItem = (AVPlayerItem *)object;
        if (playItem.status == AVPlayerItemStatusReadyToPlay) {}
    }
}

CMTime

AVFoundation 使用 CMTime 这种比浮点型数据(比如 NSTimeInterval)更可靠的方法来展示时间。AVFoundation 是基于 Core Media 的高层封装,我们经常能接触到的部分就是 CMTime。

typedef struct
{
    CMTimeValue value;  
    CMTimeScale timescale;  
    CMTimeFlags flags;      
    CMTimeEpoch epoch;      
} CMTime;
这个结构最关键的两个值是 value 和 timescale,在时间呈现样式中分别作为分子和分母。

// 0.5 seconds
CMTime halfSecond = CMTimeMake(1, 2);
    
// 5 seconds
CMTime fiveSecond = CMTimeMake(5, 1);
    
//One sample from a 44.1 kHz audio file
CMTime oneSample = CMTimeMake(1, 441000);
    
//Zero time value
CMTime zeroTime = kCMTimeZero;