AV Foundation 与 AVKit 的选择
AV Foundation 提供执行各种媒体处理任务,包括媒体捕获、编辑和低级处理,但它最常用的功能之一是媒体播放。使用AV Foundation,可以有效地加载和控制媒体资产的播放,例如 QuickTime 电影、MP3 音频文件,甚至是通过 HTTP Live Streaming 远程提供的视听媒体。
AV Foundation 不仅提供基本的媒体播放。该框架可以轻松检索和呈现描述性媒体元数据、显示字幕和隐藏式字幕,以及选择替代音频和视频演示文稿。甚至可以在播放期间对媒体样本进行实时处理,从而完全控制媒体的处理和呈现方式。但是,由于该框架位于用户界面框架之下,因此它不提供用于控制播放的标准用户界面。
AVKit 是建立在 AV Foundation 之上的配套框架。AVKit 可以轻松地为应用程序提供与平台原生播放体验相匹配的播放器界面。AVKit 使用 AV Foundation 的播放基础架构来提供播放器界面,该界面会自动适应以最好地匹配正在播放的内容。使用 AVKit,播放器会自动显示字幕和隐藏式字幕,呈现可导航的章节标记,并提供控制以选择替代媒体选项。因为 AVKit 是一个系统框架,播放应用程序会自动采用未来操作系统更新的新美学和功能,而无需进行任何额外的工作。
当我们需要构建定制自定义播放器界面时可以使用 AV Foundation ,当我们只需要简单的实现播放功能是更好的解决方案是依赖 AVKit 框架提供的功能。
播放媒体相关类
当使用 AV Foundation 开发一个自定义播放器时会用到大量的对象,下图是相关主要类及其关系:
AVPlayerItem
在 AV Foundation中,用 AVAsset
描述一个媒体资源,可以获取媒体相关信息,但是其只包含静态信息,仅使用 AVAsset
无法实现播放功能。当播放视频时,不会直接提供AVAsset
给 AVPlayer
,而是使用 AVPlayerItem
,通过 AVPlayerItem
和 AVPlayerItemTrack
动态构建相应的动态内容。在播放过程中,可以使用 AVPlayerItem
实例来管理整个资产的呈现状态,并使用 AVPlayerItemTrack
对象来管理单个轨道的呈现状态。
AVPlayer
AV Foundation的播放都围绕 AVPlayer
类展开, AVPlayer
是一个用来播放基于时基视听媒体的控制器对象,支持从本地、分步下载或者通过 HTTP Live Streaming 协议流媒体播放,并在多种播放场景中播放这些视频资源。这里的”控制器对象“并不是一个视图或窗口控制器,是指一个对播放和资源时间信息进行管理的对象。
AVPlayer
只管理一个单独资源的播放,AV Foundation 还提供了 AVPlayer
的一个子类 AVQueuePlayer
,可以用来管理一个资源队列,当需要在一个序列中播放多个条目或者为视频资源设置播放循环时可以使用该子类。
AVPlayerLayer
AVPlayer
提供有关播放状态的信息,是一个不可见组件,如果要显示视频需要将播放器的输出定向到专门的核心动画层 AVPlayerLayer
。创建一个 AVPlayerLayer
需要一个 AVPlayer
实例,使得两者保持同步。AVPlayerLayer
构建于 Core Animation ,其扩展了 CALayer
类,并通过框架在屏幕上显示视频, 这一图层不提供任何播放控件,只是在屏幕上显示播放器的视觉内容。
创建一个简单的视频播放器
开发者可以使用 AVAsset
初始化播放器 AVPlayer
,也可以直接从 URL
初始化 AVPlayer
,以便可以在特定位置播放资源。下面示例使用加载和播放基于文件的资产。播放基于文件的资产有几个步骤:
- 使用
AVURLAsset
创建资产。 - 使用
AVURLAsset
的实例创建AVPlayerItem
。 - 将
AVPlayerItem
的实例与AVPlayer
相关联。
NSURL *url = <#获取资产路径#>;
AVAsset *asset = [AVAsset assetWithURL:assetURL];
NSArray *keys = @[
@"tracks",
@"duration",
@"commonMetadata",
@"availableMediaCharacteristicsWithMediaSelectionOptions"
];
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:asset
automaticallyLoadedAssetKeys:keys];
AVPlayer *player = [AVPlayer playerWithPlayerItem:playerItem];
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
和 AVAsset
一样,简单地初始化 AVPlayerItem
并不一定意味着它已准备好立即播放。可以使用键值观察 AVPlayerItem
的 status
属性以确定它是否以及何时准备好播放,status
属性类型为 AVPlayerItemStatus
:
-
AVPlayerItemStatusUnknown
,还未添加到播放队列 -
AVPlayerItemStatusReadyToPlay
,已经准备好播放 -
AVPlayerItemStatusFailed
,加载失败
在将 AVPlayerItem
与 AVPlayer
关联之前,开发者需要将代码设置为 status
属性的观察者,如下面的示例所示:
[playerItem addObserver:self
forKeyPath:@"status"
options:0
context:NULL];
当观察到 AVPlayerItem
的 status
变为 AVPlayerItemStatusReadyToPlay
时,就可以开始播放了。
注:创建和准备用于播放的 HTTP 实时流时需要使用
URL
初始化AVPlayerItem
的实例。(不能直接创建一个AVAsset
实例来表示 HTTP 直播流中的媒体。)当只需要播放直播时,可以直接使用URL
初始化AVPlayer
实例。
处理时间
媒体播放是一项基于时间的活动,因为其在一定的持续时间内以固定速率呈现定时媒体样本,因此AVPlayer
和 AVPlayerItem
都是基于时间的对象,但是在我们使用它们的功能前,需要了解在 AV Foundation 框架中呈现时间的方式。
通常 Apple 框架使用 NSTimeInterval
类型的浮点值表示时间中的秒,但在执行定时媒体操作时通常会出现问题。使用媒体时保持采样精确的时序很重要,浮点不精确通常会导致时序漂移。为了解决这些不精确性,AV Foundation 使用一种可靠性更高的方法来展示时间信息,由 Core Media 框架中的 CMTime
表示时间长度,CMTimeRange
表示时间范围。
CMTime
CMTime
为时间的正确表示给出了一种 C 结构,即分数值的方式。将时间表示为一个有理数,有一个 int64_t
类型的 value
(分子)和一个 int32_t
类型的 timescale
(分母,一个时间刻度)。从概念上讲,时间刻度指定分子中每个单位占用的秒数。因此,如果时间刻度为 4,则每个单位表示四分之一秒;如果时间刻度为 10,则每个单位表示十分之一秒,依此类推。具体定义如下:
typedef struct
{
CMTimeValue value;
CMTimeScale timescale;
CMTimeFlags flags;
CMTimeEpoch epoch;
} CMTime
可以使用CMTimeMake
或其中一个相关函数创建时间,例如CMTimeMakeWithSeconds
(允许使用浮点值创建时间并指定首选时间刻度)。有几个函数用于基于时间的算术和比较时间,如下例所示:
CMTime time1 = CMTimeMake(200, 2); // 200 half-seconds
CMTime time2 = CMTimeMake(400, 4); // 400 quarter-seconds
// time1 and time2 both represent 100 seconds, but using different timescales.
if (CMTimeCompare(time1, time2) == 0) {
NSLog(@"time1 and time2 are the same");
}
Float64 float64Seconds = 200.0 / 3;
CMTime time3 = CMTimeMakeWithSeconds(float64Seconds , 3); // 66.66... third-seconds
time3 = CMTimeMultiply(time3, 3);
// time3 now represents 200 seconds; next subtract time1 (100 seconds).
time3 = CMTimeSubtract(time3, time1);
CMTimeShow(time3);
if (CMTIME_COMPARE_INLINE(time2, ==, time3)) {
NSLog(@"time2 and time3 are the same");
}
CMTimeRange
CMTimeRange
是一个具有开始时间和持续时间的 C 结构,两者都表示为CMTime
结构。时间范围不包括开始时间加上持续时间的时间。使用 CMTimeRangeMake
或者 CMTimeRangeFromTimeToTime
函数可以创建CMTimeRange
。
时间监听
通常我们希望在播放过程中观察播放时间,以便更新播放位置或同步用户界面的状态。为了满足监听时间变化,AVPlayer
提供了两种基于时间的监听方法:定时监听和边界监听。让应用程序可以对时间变化进行精准的监听。
定时监听
通常情况下,我们希望以一定的时间间隔获得通知,随着时间的变化移动播放头位置或者更新时间显示。利用 AVPlayer 的 addPeriodicTimeObserverForInterval:queue:usingBlock:
,此方法需要传递如下参数:
-
interval
: 时间间隔,使用CMTime
值 -
queue
: 通知发送的串行调度队列 -
block
:要在指定时间间隔调用的回调块。
CMTime interval =
CMTimeMakeWithSeconds(0.5, NSEC_PER_SEC);
// Main dispatch queue
dispatch_queue_t queue = dispatch_get_main_queue();
void (^callback)(CMTime time) = ^(CMTime time) {
NSTimeInterval currentTime = CMTimeGetSeconds(time);
NSTimeInterval duration = CMTimeGetSeconds(playerItem.duration);
};
// Add observer and store pointer for future use
id timeObserver = [player addPeriodicTimeObserverForInterval:interval
queue:queue
usingBlock:callback];
- 创建一个用于定义通知时间间隔的 CMTime 值,这里定义为0.5秒
- 定义发送回调通知的调度队列
- 定义一个回调块,在回调块内部通过
CMTimeGetSeconds
函数将代码块的CMTime
值转换成一个NSTimeInterval
边界观察
观察时间的另一种方式是按边界。我们可以在媒体的时间轴内定义各种边界点,框架会在正常播放期间遍历这些时间时通知。边界观测的使用频率低于定期观测,但在某些情况下仍然可以证明是有用的。例如,如果正在呈现没有播放控件的视频,并且想要同步显示元素或呈现补充内容,那么可能会使用边界观察。
要观察边界时间,可以使用 AVPlayer
的 addBoundaryTimeObserverForTimes:queue:usingBlock:
方法。并提供以下参数:
-
times
: 由CMTime
转成NSValue
存储的对象数组定义了需要通知边界时间的值; -
queue
:通知发送的串行调度队列; -
block
:要在指定时间间隔调用的回调块。
条目结束监听
还有一个需要监听的事件就是条目播放完毕的时间,当播放完成时, AVPlayerItem
会发送一个 AVPlayerItemDidPlayToEndTimeNotification
通知。
[[NSNotificationCenter defaultCenter] addObserver:self selector:<#(nonnull SEL)#> name:AVPlayerItemDidPlayToEndTimeNotification object:<#(nullable id)#>]
控制 AVPLayer
由于 AVPlayerLayer
它不显示任何播放控件,而只是在屏幕上显示播放器的视觉内容,因此我们需要可以构建播放传输控件来播放、暂停和搓擦条跳转到时间轴上的任意位置等等。 AVPlayer
提供了方法使得我们可以控制:
-
play
播放 -
pause
暂停 -
stop
停止 -
seekToTime:
跳转到媒体时间轴任意位置 -
rate
播放速率 -
allowsExternalPlayback
允许启用AirPlay
播放功能。
获取视频缩略图
AV Foundation 提供了一个 AVAssetImageGenerator
工具类,可以用来从一个 AVAsset
视频资源中获取图片。 AVAssetImageGenerator
使用默认启用的视频轨道来生成图像。
在使用
AVAssetImageGenerator
前,有必要检查资产是否有任何具有视觉特征的轨道:
AVAsset *anAsset = <#获取资产#>;
if ([[anAsset trackingWithMediaType:AVMediaTypeVideo] count] > 0) {
AVAssetImageGenerator *imageGenerator = [AVAssetImageGeneratorassetImageGeneratorWithAsset:anAsset];
}
在检索图片前,可以通过设置 AVAssetImageGenerator
的 maximumSize
和apertureMode
分别指定生成的图像的最大尺寸以及光圈模式:
imageGenerator.maximumSize = CGSizeMake(200.0f, 0.0f);
imageGenerator.apertureMode = AVAssetImageGeneratorApertureModeProductionAperture;
AVAssetImageGenerator
定义了两个方法实现从视频资源中检索图片,分别为:
-
copyCGImageAtTime:actualTime:error:
允许在指定时间点捕捉图片。AV Foundation 可能无法在请求的准确时间生成图像,因此可以将指向CMTime
的指针作为第二个参数传递,该指针在返回时包含实际生成图像的时间。 -
generateCGImagesAsynchronouslyForTimes:completionHandler:
允许按照第一个参数所指定的时间段生成一个图片序列。
显示字幕
AV Foundation 在展示字幕或隐藏式字幕方面提供了可靠方法。 AVPLayerLayer
会自动渲染这些元素,并且可以让开发者告诉应用程序哪些元素需要渲染。完成这些操作要用到 AVMediaSelectionGroup
和 AVMediaSelectionOption
两个类。
AVMediaSelectionOption
表示 AVAsset
中的备用媒体呈现方式。一个资源可能包含备用媒体呈现方式,比如备用音频、视频或文本轨道。确定存在哪些备用轨道要用到 AVAsset
中一个名为availableMediaCharacteristicsWithMediaSelectionOptions
的属性,可以在 AVFoundation/AVMediaFormat.h
找到 AVMediaCharacteristic
的全部类型,这其中有一个 AVMediaCharacteristicLegible
代表 字幕或隐藏式字幕。
NSURL *url = <#获取资产路径#>;
AVAsset *asset = [AVAsset assetWithURL:assetURL];
NSArray *results = [asset availableMediaCharacteristicsWithMediaSelectionOptions];
请求可用媒体特性数据后,调用 AVAsset
的 mediaSelectionGroupForMediaCharacteristic:
方法为其传递要检索的选项的特定媒体特性。这个方法会返回一个 AVMediaSelectionGroup
,它作为一个或多个互斥的 AVMediaSelectionOption
实例的容器。
for (AVMediaCharacteristic charcteristic in results){
NSLog(@"Characteristic:%@",characteristic);
AVMediaSelectionGroup *group = [asset mediaSelectionGroupForMediaCharacteristic:charcteristic];
for (AVMediaSelectionOption *option in group.options){
NSLog(@"Option:%@",option.displayName);
}
}
包含音频和字幕媒体选项的资产的输出类似于以下内容:
[AVMediaCharacteristicAudible]
Option: English
Option: Spanish
[AVMediaCharacteristicLegible]
Option: English
Option: German
Option: Spanish
Option: French
当定义好需要的 AVMediaSelectionOption
和 AVMediaSelectionGroup
后 通过 AVPlayerItem
调用 selectMediaOption:inMediaSelectionGroup:
实现显示字幕
[playerItem selectMediaOption:option
inMediaSelectionGroup:group];