AV Foundation ⑪ 视频播放

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 开发一个自定义播放器时会用到大量的对象,下图是相关主要类及其关系:

relationship_of_classes_2x.png

AVPlayerItem

    在 AV Foundation中,用 AVAsset 描述一个媒体资源,可以获取媒体相关信息,但是其只包含静态信息,仅使用 AVAsset 无法实现播放功能。当播放视频时,不会直接提供AVAssetAVPlayer ,而是使用 AVPlayerItem,通过 AVPlayerItemAVPlayerItemTrack 动态构建相应的动态内容。在播放过程中,可以使用 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 并不一定意味着它已准备好立即播放。可以使用键值观察 AVPlayerItemstatus 属性以确定它是否以及何时准备好播放,status 属性类型为 AVPlayerItemStatus

  • AVPlayerItemStatusUnknown,还未添加到播放队列
  • AVPlayerItemStatusReadyToPlay,已经准备好播放
  • AVPlayerItemStatusFailed,加载失败

    在将 AVPlayerItemAVPlayer 关联之前,开发者需要将代码设置为 status 属性的观察者,如下面的示例所示:

 [playerItem addObserver:self                                       
                      forKeyPath:@"status"
                         options:0
                         context:NULL];

    当观察到 AVPlayerItemstatus 变为 AVPlayerItemStatusReadyToPlay 时,就可以开始播放了。

注:创建和准备用于播放的 HTTP 实时流时需要使用 URL 初始化AVPlayerItem 的实例。(不能直接创建一个 AVAsset 实例来表示 HTTP 直播流中的媒体。)当只需要播放直播时,可以直接使用 URL 初始化 AVPlayer 实例。

处理时间

    媒体播放是一项基于时间的活动,因为其在一定的持续时间内以固定速率呈现定时媒体样本,因此AVPlayerAVPlayerItem 都是基于时间的对象,但是在我们使用它们的功能前,需要了解在 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];
  1. 创建一个用于定义通知时间间隔的 CMTime 值,这里定义为0.5秒
  2. 定义发送回调通知的调度队列
  3. 定义一个回调块,在回调块内部通过 CMTimeGetSeconds 函数将代码块的 CMTime 值转换成一个 NSTimeInterval

边界观察

    观察时间的另一种方式是按边界。我们可以在媒体的时间轴内定义各种边界点,框架会在正常播放期间遍历这些时间时通知。边界观测的使用频率低于定期观测,但在某些情况下仍然可以证明是有用的。例如,如果正在呈现没有播放控件的视频,并且想要同步显示元素或呈现补充内容,那么可能会使用边界观察。

    要观察边界时间,可以使用 AVPlayeraddBoundaryTimeObserverForTimes: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]; 
}

    在检索图片前,可以通过设置 AVAssetImageGeneratormaximumSizeapertureMode 分别指定生成的图像的最大尺寸以及光圈模式:

imageGenerator.maximumSize = CGSizeMake(200.0f, 0.0f);    
imageGenerator.apertureMode = AVAssetImageGeneratorApertureModeProductionAperture;

    AVAssetImageGenerator 定义了两个方法实现从视频资源中检索图片,分别为:

  • copyCGImageAtTime:actualTime:error: 允许在指定时间点捕捉图片。AV Foundation 可能无法在请求的准确时间生成图像,因此可以将指向 CMTime 的指针作为第二个参数传递,该指针在返回时包含实际生成图像的时间。
  • generateCGImagesAsynchronouslyForTimes:completionHandler:允许按照第一个参数所指定的时间段生成一个图片序列。

显示字幕

    AV Foundation 在展示字幕或隐藏式字幕方面提供了可靠方法。 AVPLayerLayer 会自动渲染这些元素,并且可以让开发者告诉应用程序哪些元素需要渲染。完成这些操作要用到 AVMediaSelectionGroupAVMediaSelectionOption 两个类。

    AVMediaSelectionOption 表示 AVAsset 中的备用媒体呈现方式。一个资源可能包含备用媒体呈现方式,比如备用音频、视频或文本轨道。确定存在哪些备用轨道要用到 AVAsset 中一个名为availableMediaCharacteristicsWithMediaSelectionOptions 的属性,可以在 AVFoundation/AVMediaFormat.h 找到 AVMediaCharacteristic 的全部类型,这其中有一个 AVMediaCharacteristicLegible代表 字幕或隐藏式字幕。

NSURL *url = <#获取资产路径#>;
AVAsset *asset = [AVAsset assetWithURL:assetURL];
NSArray <AVMediaCharacteristic> *results = [asset availableMediaCharacteristicsWithMediaSelectionOptions];

    请求可用媒体特性数据后,调用 AVAssetmediaSelectionGroupForMediaCharacteristic: 方法为其传递要检索的选项的特定媒体特性。这个方法会返回一个 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

    当定义好需要的 AVMediaSelectionOptionAVMediaSelectionGroup 后 通过 AVPlayerItem 调用 selectMediaOption:inMediaSelectionGroup: 实现显示字幕

 [playerItem selectMediaOption:option                       
                         inMediaSelectionGroup:group];
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容