Apple - Media Playback Programming Guide

本文翻译整理自:Media Playback Programming Guide(Updated: 2018-01-16
https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/MediaPlaybackGuide/Contents/Resources/en.lproj/Introduction/Introduction.html#//apple_ref/doc/uid/TP40016757


文章目录


一、关于媒体播放

重要提示: 本文档不再更新。
有关Apple SDK的最新信息,请访问留档网站

您使用AVKit和AVFoundation框架来播放和呈现视听媒体。
本指南为您提供了有关如何利用这些框架的强大特性和功能来构建媒体播放应用程序的详细信息。


1、概览

Apple平台提供强大的媒体播放功能,使您能够处理简单和高级用例。
播放支持由下图所示的AVKit和AVFoundation框架提供:
在这里插入图片描述


AVFoundation

AVFoundation是Apple的iOS、tvOS和macOS媒体框架。
您可以使用它来执行各种媒体处理任务,包括媒体捕获、编辑和低级处理,但其最常用的功能之一是媒体播放。
借助AVFoundation,您可以有效地加载和控制媒体资产的播放,如QuickTime电影、MP3音频文件,甚至通过HTTP Live Streaming远程提供的视听媒体。

AVFoundation的功能超越了基本的媒体播放。
该框架使您可以轻松检索和呈现描述性媒体元信息、显示字幕和隐藏式字幕以及选择替代音频和视频演示。
您甚至可以在播放过程中对媒体样本进行实时处理,让您完全控制媒体的处理和呈现方式。
本指南后面的部分描述了如何使用这些高级功能。

AVFoundation为您提供了一组丰富的功能来构建强大的回放应用程序。
然而,由于该框架位于用户界面框架之下,它没有提供用于控制回放的标准用户界面。
虽然可以构建自己的自定义播放器界面,但这样做通常需要大量的工作,并且需要您对较低级别的AVFoundation界面有深入的了解。
当然,在某些情况下,完全控制用户界面是可取的,但通常更好的解决方案是依靠AVKit框架提供的功能。

相关章节: 构建一个基本的播放应用程序探索AVFoundation


AVKit

AVKit是一个建立在AVFoundation之上的配套框架。
AVKit使您可以轻松地为您的应用程序提供与平台原生播放体验相匹配的播放器界面。
AVKit使用AVFoundation的播放基础架构,提供自动适应最佳拟合正在播放的内容的播放器界面。
使用AVKit,您的播放器会自动显示字幕和隐藏式字幕,呈现可导航的章节标记,并提供选择替代媒体选项的控件。
由于AVKit是一个系统框架,您的播放应用程序会自动采用未来操作系统更新的新美学和功能,而无需您进行任何额外的工作。

该框架适用于iOS、tvOS和macOS。
尽管它在全平台上共享许多核心功能,但它也提供了许多可在应用程序中使用的平台特定功能。
这些功能将在本指南的后面部分中描述。

相关章节: 构建一个基本的播放应用程序使用AVKit平台功能


iOS

过去,向应用程序添加全功能视频播放的简单方法是使用MediaPlayer框架的MPMoviePlayerViewController类。
它提供标准的播放控件,可以以多种方式呈现,支持AirPlay流媒体,并提供许多其他有用的功能。
该类建立在AVFoundation之上,但隐藏了这些基础,阻止您使用AVFoundation的更高级功能。
在iOS9中,MPMoviePlayerController被弃用并被AVKit和AVFoundation取代。
您可以通过使用AVKit框架的AVPlayerViewController类获得所有相同的功能,但您也可以直接访问AVFoundation,从而可以为iOS构建简单高级媒体播放应用程序。

相关章节: 构建一个基本的播放应用程序使用AVKit平台功能


TVOS电视

在tvOS上向用户呈现熟悉的播放体验非常重要。
用户对如何使用Siri Remote控制播放和与媒体内容交互有一定的期望。
当您使用AVKit框架的AVPlayerViewController类时,您可以使用Apple内置应用程序中的相同播放体验。
使用此类,您可以提供相同的直观传输行为,包括特技播放支持,使用户可以轻松控制您的媒体播放。
它还为您提供了一个可定制的信息面板,您可以使用它来显示元信息和章节标记,以进一步增强浏览您的内容。
tvOS中AVKit最引人注目的功能之一是它使用语音命令(如“从头开始播放”、“向前跳过30秒”或“他们说了什么?”)来控制播放,从而提供对您的内容的完整Siri Remote控制。

相关章节: 构建一个基本的播放应用程序使用AVKit平台功能


苹果操作系统

您可以使用AVKit框架的AVPlayerView类为macOS构建具有QuickTime Player相同核心功能的媒体播放器。
播放器视图允许您在各种控件样式之间进行选择,从而可以轻松定制播放器的演示文稿,以最好地拟合您的应用程序的需求。
AVPlayerView提供了一个动态播放界面,可以自动适应正在播放的内容。
这使得您可以轻松地提供最佳用户体验来控制所呈现媒体的功能。
您还可以使用AVPlayerView轻松地将QuickTime Player中的相同修剪功能添加到您的应用程序中。
AVKit for macOS构建时考虑到了现代应用程序,并自动支持所有标准macOS功能,例如本地化、状态恢复、全屏播放、高分辨率显示和可访问性。

相关章节: 构建一个基本的播放应用程序使用AVKit平台功能


2、如何使用本文档

本文档介绍了如何使用AVKit和AVFoundation的功能来构建媒体播放应用程序。
首先阅读构建一个基本播放应用程序,它将引导您完成创建第一个播放应用程序的步骤。
接下来,阅读探索AVFoundation,它为您提供了使用AVFoundation的媒体播放功能的要点。
完成探索AVFoundation后,您可以浏览其余章节,了解更多关于使用AVKit和AVFoundation的细节。


3、先决条件

本指南假定您之前没有媒体编程经验。
但是,需要以下领域的知识才能利用AVKit和AVFoundation的一些更高级的功能:

注意:本指南中提供的示例是用Swift编写的,但使用Objective-C的应用程序也支持所有AVKit和AVFoundation功能。


4、另见

有关媒体播放编程指南的相关信息,请参阅以下文档:


二、构建基本播放应用程序

您学习AVKit和AVFoundation的最佳方式是潜入并构建您的第一个播放应用程序。
本章向您展示如何开始使用这些框架,方法是引导您开发用于iOS、tvOS和macOS的基本应用程序,以播放使用HTTP Live Streaming提供的媒体。
该项目要求您熟悉为这些平台中的至少一个开发应用程序。
有关详细信息,请参阅 开始开发iOS应用程序(Swift)Mac应用程序编程指南
本章中的示例项目是用Swift 3编写的,需要Xcode 8.0或更高版本。


1、iOS和tvOS

使用单视图应用程序模板为iOS或tvOS应用程序创建新的Xcode项目。

  • 产品名称:AVBasicPlayback
  • 语言:Swift
  • 设备:通用(仅限iOS)

配置项目的应用程序传输安全性

首先配置项目的应用传输安全性,以便应用可以成功连接到远程服务器。

  1. 在项目导航器中,找到应用的Info.plist文件。
    右键单击此文件并选择打开为>源代码。
  2. 在结束标记之前添加以下条目</dict>
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>devimages-cdn.apple.com</key>
        <dict>
            <key>NSExceptionRequiresForwardSecrecy</key>
            <false/>
        </dict>
    </dict>
</dict> 

添加此条目可确保应用程序可以成功检索从devimages.apple.com.edgekey.net提供的媒体。


设置音频会话

  1. 打开AppDelegate.swift类。
    在类定义上方,导入AVFoundation框架。
import AVFoundation
  1. application:didFinishLaunchingWithOptions:方法中,将应用的音频会话类别设置为AVAudioSessionCategoryPlayback
func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
 
    let audioSession = AVAudioSession.sharedInstance()
    do {
        try audioSession.setCategory(AVAudioSessionCategoryPlayback)
    }
    catch {
        print("Setting category to AVAudioSessionCategoryPlayback failed.")
    }
 
    return true
} 

设置音频会话类别可确保应用程序具有媒体播放应用程序所需的音频行为。


配置用户界面

  1. 选择Main.storyboard文件。
    在对象库的搜索字段中,键入button以查找Button对象。
  2. Button对象拖到视图控制器场景的视图中,并赋予其标题Play Video
  3. 添加对齐约束,使按钮水平和垂直居中。

在这里插入图片描述


实现播放行为

  1. 在项目导航器中,选择Main.storyboard文件并打开助理编辑器。
  2. 控制从Play Video按钮拖动到ViewController.swift类以添加一个名为playVideo的新@IBAction方法。
@IBAction func playVideo(_ sender: AnyObject) {
    // TODO
}

  1. 关闭辅助编辑器并在项目导航器中选择ViewController.swift类。
    在类定义上方,导入AVKit和AVFoundation框架。
import AVKit
import AVFoundation

  1. playVideo方法中,添加以下实现:
@IBAction func playVideo(_ sender: AnyObject) {
    guard let url = URL(string: "https://devimages-cdn.apple.com/samplecode/avfoundationMedia/AVFoundationQueuePlayer_HLS2/master.m3u8") else {
        return
    }
    // Create an AVPlayer, passing it the HTTP Live Streaming URL.
    let player = AVPlayer(url: url)
 
    // Create a new AVPlayerViewController and pass it a reference to the player.
    let controller = AVPlayerViewController()
    controller.player = player
 
    // Modally present the player and call the player's play() method when complete.
    present(controller, animated: true) {
        player.play()
    }
}

您的应用程序已完成,您可以在模拟器或iOS或tvOS设备上运行它。
只需几行代码,您就创建了一个功能齐全的播放应用程序。


2、苹果操作系统

使用Cocoa Application模板为Cocoa应用程序创建一个新的Xcode项目。

  • 产品名称:AVBasicPlayback
  • 语言:Swift
  • 使用故事板:true
  • 创建基于文档的应用程序:false

配置项目的应用程序传输安全性

首先配置项目的应用传输安全性,以便应用可以成功连接到远程服务器。

  1. 在项目导航器中,找到应用的Info.plist文件。
    右键单击此文件并选择打开为>源代码。
  2. 在结束标记之前添加以下条目</dict>
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>devimages-cdn.apple.com</key>
        <dict>
            <key>NSExceptionRequiresForwardSecrecy</key>
            <false/>
        </dict>
    </dict>
</dict>

添加此条目可确保应用程序可以成功检索从devimages.apple.com.edgekey.net提供的媒体。


配置用户界面

  1. 在项目导航器中,选择Main.storyboard文件。
    在对象库的搜索字段中,键入player以查找AVKit Player View对象。
  2. AVKit Player View对象拖到View控制器Scene的视图中。
  3. 向玩家视图添加固定约束,将其置顶到其超级视图的边缘并保持其长宽比。

在这里插入图片描述


  1. 选择玩家视图。
    在属性检查器中,将控件样式选择更改为浮动。
    此样式显示与Quicktime Player中找到的控件匹配的控件。
    ../Art/controlsStyle.shot/Resources/shot_2x.png

实现播放行为

  1. Main.storyboard文件中,打开助理编辑器。
  2. 控制从播放器视图对象拖动到ViewController.swift类,并添加一个名为playerView的新@IBOutlet
@IBOutlet weak var playerView: AVPlayerView!
  1. 关闭辅助编辑器并在项目导航器中选择ViewController.swift类。
    在类定义上方,导入AVKit和AVFoundation框架。
import AVKit
import AVFoundation

  1. viewDidLoad方法中,添加以下实现:
override func viewDidLoad() {
    super.viewDidLoad()
    guard let url = URL(string: "https://devimages-cdn.apple.com/samplecode/avfoundationMedia/AVFoundationQueuePlayer_HLS2/master.m3u8") else {
        return
    }
    // Create a new AVPlayer and associate it with the player view
    let player = AVPlayer(url: url)
    playerView.player = player
 
}

您的应用程序已完成,您可以运行它。
只需几行代码,您就创建了一个功能齐全的播放应用程序。


三、为iOS和tvOS配置音频设置

iOS和tvOS的媒体播放应用程序要求您对音频会话执行一些配置,以启用某些行为和功能。
本章讨论如何执行此配置,以确保您为用户提供最佳的播放体验。


配置您的iOS和tvOS音频会话

音频会话充当应用程序和操作系统之间的中介,进而也是底层音频硬件之间的中介。
您使用它向操作系统传达应用程序音频的性质,而无需详细说明特定行为或与音频硬件的所需交互。
这将这些细节的管理委托给音频会话,从而确保操作系统能够最好地管理用户的音频体验。

所有iOS和tvOS应用程序都有一个默认的音频会话,预先配置如下:

  • 支持音频播放,但不允许录音(tvOS不支持录音)。
  • 在iOS,设置铃声/静音切换到静音模式静音任何音频正在播放的应用程序。
  • 在iOS,当设备被锁定时,应用程序的音频会被静音。
  • 当应用程序播放音频时,任何其他背景音频都会静音。

您可以从默认音频会话中获得许多有用的行为,但这不是构建媒体播放应用程序时需要的行为。
要更改此行为,请配置应用程序的音频会话类别。

音频会话类别定义了您的应用所需的一般音频行为。
您可以使用七个可能的类别(请参阅音频会话类别和模式),但大多数播放应用所需的类别称为AVAudioSessionCategoryPlayback
该类别表明音频播放是您的应用的核心特征。
当您指定此类别时,您的应用的音频继续,环形/静音交换机设置为静音模式(仅iOS)。
使用此类别,如果您在图片背景模式下使用音频、AirPlay和图片,您的应用也可以播放背景音频。
有关详细信息,请参阅启用背景音频.

您可以使用AVAudioSession对象来配置应用的音频会话。
这是一个单例对象,用于设置音频会话类别以及执行其他配置设置。
您可以在应用的整个生命周期中与音频会话进行交互,但在应用启动时执行此配置通常很有用,如以下示例所示:

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]?) -> Bool {
    let audioSession = AVAudioSession.sharedInstance()
    do {
        try audioSession.setCategory(AVAudioSessionCategoryPlayback)
    } catch {
        print("Setting category to AVAudioSessionCategoryPlayback failed.")
    }
    // Other project setup
    return true
}
}

当您使用setActive: error:setActive:error:setActive:withOptions:error:方法激活音频会话时,将使用此类别。

注意:设置类别后,您可以随时激活音频会话,但通常最好将此调用推迟到您的应用程序开始音频播放。
这可确保您不会过早中断可能进行中的任何其他背景音频。

设置类别是您与AVAudioSession所需的最小交互,但您也可以使用许多其他配置选项和功能。
请参阅 音频会话编程指南 以了解有关使用音频会话的更多信息。


启用背景音频

iOS和tvOS应用程序要求您为某些后台操作启用某些功能。
播放应用程序所需的一个常见功能是播放背景音频。
启用此功能后,当用户切换到另一个应用程序或锁定iOS设备时,您的应用程序的音频可以继续。
在iOS中启用AirPlay流媒体和画中画播放等高级播放功能也需要此功能。

配置这些功能的最简单方法是使用Xcode。
在Xcode中选择应用的目标,然后选择“功能”选项卡。
在“功能”选项卡下,设置背景模式切换到ON,并在可用模式列表下选择“音频、AirPlay和画中画”选项。

在这里插入图片描述


启用此模式并配置音频会话后,您的应用就可以播放背景音频了。


四、探索AVFoundation

构建一个基本播放应用程序中的示例项目展示了使用AVKit创建播放应用程序是多么容易。
对于基本视频播放,该章中的示例可能是您所需要的,但是要利用AVKit提供的所有功能,您应该了解驱动播放的AVFoundation框架对象。
本章探讨了AVFoundation的基本要素,并为您提供了使用AVKit和AVFoundation构建功能齐全的视频播放应用程序所需的信息。


1、了解资产模型

AVFoundation的许多关键特性和功能与播放和处理媒体资产有关。
该框架使用AVAsset类对资产进行建模,该类是表示单个媒体资源的抽象、不可变类型。
它提供了媒体资产的复合视图,将媒体的静态方面建模为一个整体。
AVAsset的一个实例可以对基于本地文件的媒体进行建模,例如QuickTime电影或MP3音频文件,但也可以表示从远程主机逐步下载或使用HTTP Live Streaming(HLS)流式传输的资产。

AVAsset通过两种重要方式简化了媒体的工作。
首先,它提供了一定程度的独立于媒体格式的功能。
它为您提供了一个一致的界面,用于管理和与媒体交互,而不管其底层类型如何。
使用容器格式和编解码器类型的细节留给框架,让您专注于如何在应用程序中使用这些资产。
其次,AVAsset提供了一定程度的独立于媒体位置的功能。
您可以通过使用媒体的URL初始化资产实例来创建资产实例。
这可以是一个本地URL,例如包含在应用程序包或文件系统的其他地方的URL,也可以是一个资源,例如托管在远程服务器上的HLS流。
在任何一种情况下,框架都会执行必要的工作,代表您及时有效地检索和加载媒体。
消除处理媒体格式和位置的负担极大地简化了视听媒体的工作。

AVAsset是一个容器对象,由一个或多个AVAssetTrack实例组成,它对资产的统一类型媒体流进行建模。
最常用的轨道类型是音频和视频轨道,但AVAssetTrack还对其他补充轨道进行建模,如隐藏式字幕、字幕和定时元信息(见图3-1)。


图3-1资产构成
在这里插入图片描述


您可以使用资产的tracks属性检索资产的磁道集合。
在许多情况下,您希望对资产的磁道子集执行操作,而不是对其完整集合执行操作。
在这些情况下,AVAsset还提供了根据标识符、媒体类型或特征等条件检索磁道子集的方法。


2、创建资产

您可以通过使用指向媒体资源的本地或远程URL对其进行初始化来创建AVAsset,如下例所示:

let url: URL = // Local or Remote Asset URL
let asset = AVAsset(url: url)

AVAsset是一个抽象类,因此,当您创建一个资产时,如示例所示,您实际上是在创建一个名为AVURLAsset的具体子类的实例。
在许多情况下,这是创建资产的合适方法,但您也可以直接实例化一个AVURLAsset,当您需要对其初始化进行更细粒度的控制时。
AVURLAsset的初始化程序接受一个options字典,它允许您根据特定的使用案例定制资产的初始化。
对于实例,如果您正在为HLS流创建资产,您可能希望在用户连接到蜂窝网络时阻止它检索其媒体。
您可以按照以下示例执行此操作:

let url: URL = // Remote Asset URL
let options = [AVURLAssetAllowsCellularAccessKey: false]
let asset = AVURLAsset(url: url, options: options)

AVURLAssetAllowsCellularAccessKey选项传递false值表示您希望此资产仅在用户连接到Wi-Fi网络时检索其媒体。
有关其可用初始化选项的信息,请参阅 AVURLAsset类参考


3、准备资产以供使用

您可以使用AVAsset的属性来确定它的特性和功能,例如它对回放的适用性、持续时间、创建日期和元信息。
创建资产不会自动加载其属性或为任何特定用途做准备。
相反,资产的属性值的加载会推迟到请求它们之前。
因为属性访问是同步的,如果请求的属性之前没有被加载,框架可能需要执行大量工作来返回值。
在macOS中,如果从主线程访问卸载的属性,这可能会导致无响应的用户界面。
在iOS和tvOS中,情况可能会更加严重,因为媒体操作是由共享媒体服务守护程序执行的。
如果检索卸载属性值的请求被阻塞太久,则会发生超时,导致媒体服务终止。
为防止这种情况发生,请异步加载资产的属性。

AVAssetAVAssetTrack采用AVAsynchronousKeyValueLoading协议,它定义了用于查询属性的当前加载状态以及在需要时异步加载一个或多个属性值的方法。
该协议定义了两种方法:

public func loadValuesAsynchronously(forKeys keys: [String], completionHandler handler: (() -> Void)?)
public func statusOfValue(forKey key: String, error outError: NSErrorPointer) -> AVKeyValueStatus

您可以使用loadValuesAsynchronouslyForKeys:completionHandler:方法异步加载一个或多个属性值。
您向它传递一个数组,其中包含要加载的属性的名称,以及在确定状态后调用的完成屏蔽。
以下示例显示了如何异步加载资产的playable属性:

// URL of a bundle asset called 'example.mp4'
let url = Bundle.main.url(forResource: "example", withExtension: "mp4")!
let asset = AVAsset(url: url)
let playableKey = "playable"
 
// Load the "playable" property
asset.loadValuesAsynchronously(forKeys: [playableKey]) {
    var error: NSError? = nil
    let status = asset.statusOfValue(forKey: playableKey, error: &error)
    switch status {
    case .loaded:
        // Sucessfully loaded. Continue processing.
    case .failed:
        // Handle error
    case .cancelled:
        // Terminate processing
    default:
        // Handle all other cases
    }
}

您可以使用statusOfValueForKey:error:方法检查完成回调/回传中属性的状态。
AVKeyValueStatusLoaded的状态表示属性值已成功加载,并且可以在不阻塞的情况下检索。
AVKeyValueStatusFailed的状态表示由于尝试加载其数据时遇到错误,属性值不可用。
您可以通过检查NSError指针来确定错误的原因。
在所有情况下,都要注意完成回调/回传是在任意后台队列上调用的。
在执行任何与用户界面相关的操作之前,将方法调用返回到主队列。


4、使用元信息

媒体容器格式可以存储有关其媒体的描述性元信息。
作为开发人员,使用元信息通常具有挑战性,因为每个容器格式都有自己独特的元信息格式
您通常需要对格式有较低的理解才能读取和写入容器的元信息,但AVFoundation通过使用其AVMetadataItem类简化了元信息的处理。

在最基本的形式中,AVMetadataItem的实例是一个键值对单个元信息值的表示,例如电影的标题或相册的图稿。
AVAsset提供规范化的媒体视图,AVMetadataItem提供规范化的相关元信息视图。


检索元信息集合

为了有效地使用AVMetadataItem,您应该了解AVFoundation如何组织元信息。
为了简化元信息项的查找和过滤,框架将相关的元信息分组到关键空间中:

  • 特定于格式的键空间。
    该框架定义了许多特定于格式的键空间。
    这些键空间大致与特定的容器或文件格式相关,例如QuickTime(QuickTime元信息和用户数据)或MP3(ID3)。
    但是,单个资产可能包含跨多个键空间的元信息值。
    您可以使用资产的metadata属性检索资产特定于格式的元信息的完整集合。
  • 公共键空间。
    有许多公共元信息值,例如电影的创作日期或描述,可以跨多个键空间存在。
    为了帮助规范化对这种公共元信息的访问,框架提供了一个公共键空间,允许访问几个键空间共有的一组有限的元信息值。
    这使得您可以轻松检索常用元信息,而无需担心特定格式。
    您可以使用资产的commonMetadata属性检索资产的公共元信息集合。

您可以通过调用资产的availableMetadataFormats属性来确定资产包含哪些元信息格式。
此属性为其包含的每个元信息格式返回字符串标识符数组。
然后,您可以使用其metadataForFormat:方法通过传递适当的格式标识符来检索特定于格式的元信息值,如下所示:

let url = Bundle.main.url(forResource: "audio", withExtension: "m4a")!
let asset = AVAsset(url: url)
let formatsKey = "availableMetadataFormats"
asset.loadValuesAsynchronously(forKeys: [formatsKey]) {
    var error: NSError? = nil
    let status = asset.statusOfValue(forKey: formatsKey, error: &error)
    if status == .loaded {
        for format in asset.availableMetadataFormats {
            let metadata = asset.metadata(forFormat: format)
            // process format-specific metadata collection
        }
    }
}

查找和使用元信息值

检索到元信息集合后,下一步是查找其中感兴趣的特定值。
您可以使用AVMetadataItem的各种类方法将元信息集合滤波器到离散型值集。
查找特定元信息项的最简单方法是按identifier滤波器,它将键空间和键的概念组合成一个单元。
以下示例显示了如何从公共键空间中检索标题项:

let metadata = asset.commonMetadata
let titleID = AVMetadataCommonIdentifierTitle
let titleItems = AVMetadataItem.metadataItems(from: metadata, filteredByIdentifier: titleID)
if let item = titleItems.first {
    // process title item
}

注意: AVMetadataItem的AVMetadataItem返回项目集合,而不是单个实例。
在许多情况下,返回的集合包含单个元素,但如果媒体包含本地化的元信息,或者如果您从公共键空间检索数据并且相同的值存在于多个键空间中,则会返回与每个语言环境或键空间匹配的不同值。

检索到特定的元信息项后,下一步是调用它的value属性。
返回的值是采用NSObjectNSCopying协议的对象类型。
您可以手动将值转换为适当的类型,但使用元信息项的类型强制属性更安全、更容易。
您可以使用它的stringValuenumberValuedateValuedataValue属性轻松地将值强制转换为适当的类型。
对于实例,以下示例显示了如何检索与iTunes音乐曲目关联的艺术品:

// Collection of "common" metadata
let metadata = asset.commonMetadata
// Filter metadata to find the asset's artwork
let artworkItems =
    AVMetadataItem.metadataItems(from: metadata,
                                 filteredByIdentifier: AVMetadataCommonIdentifierArtwork)
if let artworkItem = artworkItems.first {
    // Coerce the value to an NSData using its dataValue property
    if let imageData = artworkItem.dataValue {
        let image = UIImage(data: imageData)
        // process image
    } else {
        // No image data found
    }
} 

元信息在许多媒体应用程序中发挥着重要作用。
本指南的后面部分解释了如何使用静态和定时元信息增强播放应用程序的功能。


5、播放媒体

上一节中描述的资产模型是回放使用案例的基石,资产代表您想要播放的媒体,但只是画面的一部分,本节讨论播放您的媒体所需的附加对象,并展示如何配置它们以进行回放(参见图3-2)。

图3-2主要播放对象
在这里插入图片描述


AVPlayer

AVPlayer是驱动播放使用案例的中心类。
播放器是一个控制器对象,用于管理媒体资产的播放和计时。
您可以使用它来播放本地、逐步下载或流式传输的媒体,并以编程方式控制其呈现。

注意: 您使用AVPlayer一次播放单个媒体资产。
该框架还提供了AVPlayer的一个子类,称为AVQueuePlayer,用于创建和管理要按顺序播放的媒体资产队列。


AVPlayerItem

AVAsset仅对媒体的静态方面进行建模,例如其持续时间或创建日期,其本身不适合使用AVPlayer进行回放。
要播放资产,您需要创建其动态对应物的实例,该实例位于AVPlayerItem中。
此对象对AVPlayer播放的资产的时间和呈现状态进行建模。
使用AVPlayerItem的属性和方法,您可以查找媒体中的各种时间,确定其呈现大小,确定其当前时间等等。


AVKit和AVPlayerLayer

AVPlayerAVPlayerItem是非视觉对象,它们本身无法在屏幕上显示资产的视频。
您有两种不同的选项可用于在应用中显示视频内容:

  • AVKit。
    呈现视频内容的最佳方式是在iOS或tvOS中使用AVKit框架的AVPlayerViewController,或在macOS中使用AVPlayerView
    这些对象呈现视频内容,以及播放控件和其他媒体功能,为您提供全功能的播放体验。
  • AVPlayerLayer。
    如果您要为您的播放器构建自定义界面,您将使用AVFoundation提供的CALayer子类,称为AVPlayerLayer
    播放器层可以设置为视图的支持层,也可以直接添加到层层次结构中。
    AVPlayerViewAVPlayerViewController不同,AVPlayerLayer不提供任何播放控件,而是在屏幕上简单地显示播放器的视觉内容。
    由您来构建播放传输控件以通过媒体播放、暂停和查找。

设置播放对象

以下示例显示了为回放场景创建完整对象图所采取的步骤。
该示例是为iOS和tvOS编写的,但同样的基本步骤也适用于macOS。

class PlayerViewController: UIViewController {
 
    @IBOutlet weak var playerViewController: AVPlayerViewController!
 
    var player: AVPlayer!
    var playerItem: AVPlayerItem!
 
    override func viewDidLoad() {
        super.viewDidLoad()
 
        // 1) Define asset URL
        let url: URL = // URL to local or streamed media
 
        // 2) Create asset instance
        let asset = AVAsset(url: url)
 
        // 3) Create player item
        playerItem = AVPlayerItem(asset: asset)
 
        // 4) Create player instance
        player = AVPlayer(playerItem: playerItem)
 
        // 5) Associate player with view controller
        playerViewController.player = player
    } 
}

创建回放对象后,调用播放器的play方法开始回放。

AVPlayerAVPlayerItem提供了多种方法来控制播放器项目的媒体何时可以使用。
下一步是查看如何观察播放对象的状态,以便确定它们的播放准备情况。


6、观察播放状态

AVPlayerAVPlayerItem是状态经常变化的动态对象。
您经常希望采取行动来响应这些变化,而您这样做的方式是通过使用键值观察(KVO)(参见 键值观察编程指南 )。
使用KVO,一个对象可以注册来观察另一个对象的状态。
当观察到的对象的状态发生变化时,观察者会收到状态变化的详细信息通知。
使用KVO可以让您轻松地观察到AVPlayerAVPlayerItem的状态变化并采取行动来响应。

要观察的最重要的AVPlayerItem属性之一是它的status
status指示播放器项目是否已准备好播放并且通常可以使用。
当您首次创建播放器项目时,它的status值为AVPlayerItemStatusUnknown,这意味着它的媒体尚未加载或排队播放。
当您将播放器项目与AVPlayer关联时,它会立即开始对项目的媒体进行排队并准备播放。
当播放器项目的状态更改为AVPlayerItemStatusReadyToPlay时,它就可以使用了。
以下示例显示了如何观察此状态更改:

let url: URL = // Asset URL
 
var asset: AVAsset!
var player: AVPlayer!
var playerItem: AVPlayerItem!
 
// Key-value observing context
private var playerItemContext = 0
 
let requiredAssetKeys = [
    "playable",
    "hasProtectedContent"
]
 
func prepareToPlay() {
    // Create the asset to play
    asset = AVAsset(url: url)
 
    // Create a new AVPlayerItem with the asset and an
    // array of asset keys to be automatically loaded
    playerItem = AVPlayerItem(asset: asset,
                              automaticallyLoadedAssetKeys: requiredAssetKeys)
 
    // Register as an observer of the player item's status property
    playerItem.addObserver(self,
                           forKeyPath: #keyPath(AVPlayerItem.status),
                           options: [.old, .new],
                           context: &playerItemContext)
 
    // Associate the player item with the player
    player = AVPlayer(playerItem: playerItem)


使用prepareToPlay方法注册以观察玩家项目的status属性,addObserver:forKeyPath:options:context:方法。
在将玩家项目与玩家关联之前调用此方法,以确保捕获项目status的所有状态更改。

要获得status更改的通知,您可以实现observeValueForKeyPath:ofObject:change:context:方法。
每当status更改时都会调用此方法,从而让您有机会采取一些操作:

override func observeValue(forKeyPath keyPath: String?,
                           of object: Any?,
                           change: [NSKeyValueChangeKey : Any]?,
                           context: UnsafeMutableRawPointer?) {
 
    // Only handle observations for the playerItemContext
    guard context == &playerItemContext else {
        super.observeValue(forKeyPath: keyPath,
                           of: object,
                           change: change,
                           context: context)
        return
    }
 
    if keyPath == #keyPath(AVPlayerItem.status) {
        let status: AVPlayerItemStatus
        if let statusNumber = change?[.newKey] as? NSNumber {
            status = AVPlayerItemStatus(rawValue: statusNumber.intValue)!
        } else {
            status = .unknown
        }
        // Switch over status value
        switch status {
        case .readyToPlay:
            // Player item is ready to play.
        case .failed:
            // Player item failed. See error.
        case .unknown:
            // Player item is not yet ready.
        }
    }
}

该示例从更改字典中检索新的status值并切换其值。
如果播放器项目的statusAVPlayerItemStatusReadyToPlay,则可以使用。
如果在尝试加载播放器项目的媒体时遇到问题,则statusAVPlayerItemStatusFailed
如果发生故障,您可以通过查询播放器项目的error属性来检索提供故障详细信息的NSError对象。


7、执行基于时间的操作

媒体播放是一项基于时间的活动——您在特定持续时间内以固定速率呈现定时媒体样本。
基于时间的操作,例如通过媒体查找,在构建媒体播放应用程序时起着核心作用。
AVPlayerAVPlayerItem的许多关键功能都与控制媒体时序有关。
要学习有效使用这些功能,您应该了解AVFoundation中的时间表示方式。

几个Apple框架,包括AVFoundation的某些部分,将时间表示为表示秒的浮点NSTimeInterval值。
在许多情况下,这提供了一种思考和表示时间的自然方式,但在执行定时媒体操作时,这通常是有问题的。
在使用媒体时,保持样本准确的计时很重要,浮点不精确通常会导致计时漂移。
为了解决这些不精确问题,AVFoundation使用Core Media框架的CMTime数据类型表示时间。

Core Media是一个低级C框架,它提供了AVFoundation和Apple平台上更高级别媒体框架中的大部分功能。
在大多数情况下,您将通过AVFoundation提供的更高级别接口使用Core Media,但它提供的一种常用数据类型称为CMTime

public struct CMTime {
    public var value: CMTimeValue
    public var timescale: CMTimeScale
    public var flags: CMTimeFlags
    public var epoch: CMTimeEpoch
}

这个结构定义了时间的有理或分数表示。
CMTime定义的两个最重要的字段是它的值和时间刻度。
CMTimeValue是一个64位整数,定义分数时间的分子,CMTimeScale是一个32位整数,定义分母。
这个结构可以很容易地表示以媒体帧率或采样率表示的时间。

// 0.25 seconds
let quarterSecond = CMTime(value: 1, timescale: 4)
 
// 10 second mark in a 44.1 kHz audio file
let tenSeconds = CMTime(value: 441000, timescale: 44100)
 
// 3 seconds into a 30fps video
let cursor = CMTime(value: 90, timescale: 30)

Core Media提供了多种方法来创建CMTime值并对其执行算术、比较、验证和转换操作。
如果您使用的是Swift,Core Media还为CMTime添加了许多扩展和运算符重载,从而可以轻松自然地执行许多常见操作。
有关详细信息,请参阅 核心媒体框架参考


观察时间

您通常希望观察播放时间的进展,以便更新播放位置或同步用户界面的状态。
之前您了解了如何使用KVO观察播放对象的状态。
KVO适用于一般状态观察,但不是观察播放器计时的正确选择,因为它不太适合观察连续型状态变化。
相反,AVPlayer为您提供了两种不同的观察播放器时间变化的方法:定期观察和边界观察。


定期观察

您可以观察到时间在某个规则的周期性区间流逝。
如果您正在构建自定义播放器,定期观察最常见的使用案例是更新用户界面中的时间显示。

要观察周期性计时,您可以使用播放器的addPeriodicTimeObserverForInterval:queue:usingBlock:方法。
此方法采用表示时间区间的CMTime串行调度队列和在指定时间区间调用的回调/回传屏蔽。
以下示例显示了如何设置在正常回放期间每半秒调用一次屏蔽:

var player: AVPlayer!
var playerItem: AVPlayerItem!
var timeObserverToken: Any?
 
func addPeriodicTimeObserver() {
    // Notify every half second
    let timeScale = CMTimeScale(NSEC_PER_SEC)
    let time = CMTime(seconds: 0.5, preferredTimescale: timeScale)
    timeObserverToken = player.addPeriodicTimeObserver(forInterval: time,
                                                       queue: .main) {
        [weak self] time in
        // update player transport UI
    }
}
 
func removePeriodicTimeObserver() {
    if let timeObserverToken = timeObserverToken {
        player.removeTimeObserver(timeObserverToken)
        self.timeObserverToken = nil
    }
}


边界观测

观察时间的另一种方法是边界。
您可以在媒体的时间推进表中定义各种兴趣点,当这些时间在正常回放过程中被遍历时,框架会回调您。
边界观察的使用频率低于周期性观察,但在某些情况下仍然有用。
对于实例,如果您呈现的视频没有回放控件,并且想要同步显示元素或在遍历这些时间时呈现补充内容,您可以使用边界观察。

要观察边界时间,您可以使用播放器的addBoundaryTimeObserverForTimes:queue:usingBlock:方法。
此方法采用数组NSValue对象包装定义边界时间的CMTime值、串行调度队列和回调/回传屏蔽。
以下示例显示了如何为每个回放季度定义边界时间:

var asset: AVAsset!
var player: AVPlayer!
var playerItem: AVPlayerItem!
var timeObserverToken: Any?
 
func addBoundaryTimeObserver() {
 
    // Divide the asset's duration into quarters.
    let interval = CMTimeMultiplyByFloat64(asset.duration, 0.25)
    var currentTime = kCMTimeZero
    var times = [NSValue]()
 
    // Calculate boundary times
    while currentTime < asset.duration {
        currentTime = currentTime + interval
        times.append(NSValue(time:currentTime))
    }
 
    timeObserverToken = player.addBoundaryTimeObserver(forTimes: times,
                                                       queue: .main) {
        // Update UI
    }
}
 
func removeBoundaryTimeObserver() {
    if let timeObserverToken = timeObserverToken {
        player.removeTimeObserver(timeObserverToken)
        self.timeObserverToken = nil
    }
}


寻求媒体

除了正常的线性播放之外,用户还希望能够以非线性方式查找或擦洗,以快速到达媒体中的各种兴趣点。
AVKit自动为您提供擦洗控件(如果媒体支持),但如果您正在构建自定义播放器,则需要自己构建此特征。
即使在使用AVKit的情况下,您可能仍然希望提供补充用户界面,例如表格视图或集合视图,让用户快速跳到媒体中的各种位置。

您可以使用AVPlayerAVPlayerItem的方法以多种方式查找。
最常见的方法是使用播放器的seekToTime:方法,将目标CMTime值传递给它,如下所示:

// Seek to the 2 minute mark
let time = CMTime(value: 120, timescale: 1)
player.seek(to: time)

通过seekToTime:方法是一种方便的快速查找方法,但它更多的是针对速度而非精确度进行调整。
这意味着玩家移动到的实际时间可能与您请求的时间不同。
如果您需要实现精确查找行为,请使用seekToTime:toleranceBefore:toleranceAfter:方法,它允许您指示偏离目标时间(之前和之后)的容忍量。
如果您需要提供样本精确查找行为,您可以指示允许零容忍:

// Seek to the first frame at 3:25 mark
let seekTime = CMTime(seconds: 205, preferredTimescale: Int32(NSEC_PER_SEC))
player.seek(to: seekTime, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero)

重要提示: 调用具有小值或零值公差的seekToTime:toleranceBefore:toleranceAfter:方法可能会产生额外的解码延迟,这会影响应用的查找行为。

随着对如何表示和使用时间、观察玩家计时以及通过媒体寻找的深入了解,现在是时候研究AVKit提供的一些更具平台特定的功能了。

下一个上一页


五、使用AVKit平台功能

AVKit使您可以轻松创建具有与原生平台播放器相同的用户界面和功能的播放应用程序。
本章探讨如何在您自己的应用程序中利用某些特定于平台的功能。


1、使用画中画(iOS)

画中画(PiP)播放于iOS9推出。
它允许iPad用户在屏幕上漂浮在应用程序上的可移动、可调整大小的窗口中播放视频。
它为iPad带来了新的多任务处理功能,使用户能够在设备上执行其他活动的同时继续播放。
这一特征存在于Apple的内置视频播放应用程序中,并通过AVKit框架提供给您的应用程序。

支持PiP回放的应用程序在视频回放窗口的右下角显示一个小按钮。
点击此按钮可将视频显示最小化为一个小的浮动窗口,让用户在主应用程序甚至在另一个应用程序中执行其他活动(参见图4-1)。


图4-1 PiP在行动img

您可以使用AVKit框架的AVPlayerViewController类将PiP播放添加到您的应用,如果您有自定义播放器,则可以添加AVPictureInPictureController类。
要使您的应用有资格使用PiP播放,请按照为iOS和tvOS配置音频设置中的说明配置应用的音频会话和功能。


采用带AVPlayerViewController的PiP

将PiP播放添加到应用程序的最简单方法是使用AVPlayerViewController
事实上,在您配置了音频会话并设置了项目功能后,如为iOS和tvOS配置音频设置,您的播放器会自动支持PiP播放。
如果您的应用程序在受支持的iPad设备上运行,您将在播放器的右下角看到一个新按钮,如图4-2所示。

图4-2支持PiP的播放器

在这里插入图片描述


当用户点击播放器界面中的PiP按钮时,PiP播放开始。
如果您的视频以全屏模式播放,并且您通过按下主页按钮退出应用程序,播放也会自动开始。
在任何一种情况下,播放器窗口都被最小化为可移动的浮动窗口,如图4-3所示。

图4-3 PiP播放控制img


在这里插入图片描述


提示:用户可以在设置>常规>多任务处理>持久视频覆盖中禁用PiP的自动调用。
如果您认为您已经正确设置了所有内容,但发现您的视频在按下主页按钮时没有进入PiP,请检查此设置。

当视频在PiP模式下播放时,用户有基本的控制来播放和暂停视频以及退出PiP播放。
点击此界面中最左边的按钮会退出PiP并将控制权返回给您的应用程序,但默认情况下会立即终止视频播放。
原因是AVKit无法假设您的应用程序是如何结构的,也不知道如何正确恢复您的视频播放界面。
相反,它会将责任委托给您。

要处理还原过程,您的代码需要采用AVPlayerViewControllerDelegate协议并实现playerViewController:restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:方法。
当控制权返回到您的应用程序时调用此方法,让您有机会确定如何正确恢复您的视频播放器的界面。
如果您最初使用UIViewControllerpresentViewController:animated:completion:方法呈现您的视频播放器,您可以在委托回调/回传方法中以相同的方式恢复您的播放器界面。

func playerViewController(_ playerViewController: AVPlayerViewController,
                          restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: (Bool) -> Void) {
    present(playerViewController, animated: true) {
        completionHandler(false)
    }
}

重要提示: 要让系统完成恢复用户界面,必须调用值为true的完成处理程序。


在自定义播放器中采用PiP

您可以使用AVKit框架的AVPictureInPictureController类将PiP回放添加到自定义播放器中。
此类允许您实现与AVPlayerViewController中相同的PiP行为,但需要您做一些额外的工作。

首先在自定义播放器界面中添加一个UI,使用户能够开始播放PiP。
使此UI与AVPlayerViewController显示的系统默认UI保持一致。
您可以使用AVPictureInPictureControllerpictureInPictureButtonStartImageCompatibleWithTraitCollection:pictureInPictureButtonStopImageCompatibleWithTraitCollection:class方法访问用于控制PiP播放的标准图像。
这些方法返回您可以在UI中显示的系统默认图像:

@IBOutlet weak var startButton: UIButton!
@IBOutlet weak var stopButton: UIButton!
 
override func viewDidLoad() {
    super.viewDidLoad()
 
    let startImage = AVPictureInPictureController.pictureInPictureButtonStartImage(compatibleWith: nil)
    let stopImage = AVPictureInPictureController.pictureInPictureButtonStopImage(compatibleWith: nil)
 
    startButton.setImage(startImage, for: .normal)
    stopButton.setImage(stopImage, for: .normal)


您在应用中创建了一个控制PiP播放的AVPictureInPictureController实例。
只有当当前硬件支持该对象时,您才应该尝试创建该对象,您可以使用控制器的isPictureInPictureSupported方法确定该对象,如下例所示:

func setupPictureInPicture() {
    // Ensure PiP is supported by current device
    if AVPictureInPictureController.isPictureInPictureSupported() {
        // Create new controller passing reference to the AVPlayerLayer
        pictureInPictureController = AVPictureInPictureController(playerLayer: playerLayer)
        pictureInPictureController.delegate = self
        let keyPath = #keyPath(AVPictureInPictureController.isPictureInPicturePossible)
        pictureInPictureController.addObserver(self,
                                               forKeyPath: keyPath,
                                               options: [.initial, .new],
                                               context: &pictureInPictureControllerContext)
    } else {
        // PiP not supported by current device. Disable PiP button.
        pictureInPictureButton.isEnabled = false
    }
}

此示例创建一个AVPictureInPictureController的新实例,向其传递对用于呈现视频内容的AVPlayerLayer的引用。
您需要维护对控制器对象的强引用才能使PiP功能正常工作。

注意: 传递给AVPictureInPictureControllerAVPlayerLayer不会被PiP显示器使用,但AVFoundation会在PiP处于活动状态时停止向其出售视频帧。

要参与PiP生命周期事件,您的代码应采用AVPictureInPictureControllerDelegate协议并将自身设置为控制器的delegate
此外,在控制器的pictureInPicturePossible属性上使用键值监听(KVO)。
此属性指示在当前上下文中是否可以使用PiP,例如是否显示活动FaceTime窗口。
观察此属性使您能够确定何时适合更改PiP按钮的enabled状态。

完成AVPictureInPictureController设置后,接下来添加一个@IBAction方法来处理用户发起的启动或停止PiP播放的请求。

@IBAction func togglePictureInPictureMode(_ sender: UIButton) {
    if pictureInPictureController.isPictureInPictureActive {
        pictureInPictureController.stopPictureInPicture()
    } else {
        pictureInPictureController.startPictureInPicture()
    }
}

重要提示:仅在响应用户交互时开始PiP播放,而不是以编程方式开始。
否则将导致应用商店审核团队拒绝您的应用。

用户可以点击PiP窗口中的按钮将控制权返回给您的应用程序。
默认情况下,当控制权返回给应用程序时,此操作会终止播放。
原因是AVKit无法假设您的应用程序是如何结构的,也不知道如何正确恢复您的视频播放界面。
相反,它将责任委托给您。

要处理恢复过程,请实现pictureInPictureController:restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:委托方法并根据需要恢复播放器界面,在恢复完成时调用值为true的完成处理程序,如下例所示:

func picture(_ pictureInPictureController: AVPictureInPictureController,
             restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
    // Restore user interface
    completionHandler(true)
}

当PiP处于活动状态时,关闭主播放器中的回放控件,并在其边界内显示艺术品以指示PiP正在进行中。
要实现此功能,您可以使用pictureInPictureControllerWillStartPictureInPicture:pictureInPictureControllerDidStopPictureInPicture:委托方法并执行所需的操作,如以下示例所示:

func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
    // hide playback controls
    // show placeholder artwork
}
 
func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
    // hide placeholder artwork
    // show playback controls
}

2、呈现媒体元信息(tvOS)

当您在Siri Remote触摸板上向下滑动时,tvOS中的AVPlayerViewController会显示一个信息面板,显示当前演示文稿的信息和控件。
面板信息选项卡的顶部显示描述当前呈现媒体的元信息(见图4-4)。

图4-4 Info Panel元信息img

如果呈现的媒体包含支持的元信息,则会自动填充信息面板。
但是,在某些情况下,此元信息不存在、不完整或只能在运行时确定。
为了处理这些情况,tvOS中的AVKit将externalMetadata属性添加到AVPlayerItem
您可以使用此属性设置定义自定义元信息的AVMetadataItem对象数组。

AVFoundation定义了大量元信息标识符(参见AVMetadataIdentifiers.h),但Info面板目前仅支持表4-1中列出的五个:

类型数据类型元信息标识符
艺术品NSDataAVMetadataCommonIdentifierArtwork
标题NSStringAVMetadataCommonIdentifierTitle
描述NSStringAVMetadataCommonIdentifierDescription
体裁NSStringAVMetadataIdentifierQuickTimeMetadataGenre
内容评级NSStringAVMetadataIdentifieriTunesMetadataContentRating

AVMetadataItem是不可变类型,因此要创建自定义元信息,您可以使用其可变子类AVMutableMetadataItem
对于要在Info面板中显示的元信息项,您需要提供该项的identifiervalueextendedLanguageTag的值。
对于extendedLanguageTag属性,提供有效的BCP-47字符串值,例如enzh-TW(如果未确定,则und)。

注意: 除非您显示本地化元信息,否则使用und作为extendedLanguageTag值。

以下示例显示如何创建用于填充玩家信息面板的玩家项目的externalMetadata

func setupPlayback() {
    ...
    playerItem.externalMetadata = makeExternalMetadata()
    ...
}
 
func makeExternalMetadata() -> [AVMetadataItem] {
 
    var metadata = [AVMetadataItem]()
 
    // Build title item
    let titleItem =
        makeMetadataItem(AVMetadataCommonIdentifierTitle, value: "My Movie Title")
    metadata.append(titleItem)
 
    // Build artwork item
    if let image = UIImage(named: "poster"), let pngData = UIImagePNGRepresentation(image) {
        let artworkItem =
            makeMetadataItem(AVMetadataCommonIdentifierArtwork, value: pngData)
        metadata.append(artworkItem)
    }
 
    // Build description item
    let descItem =
        makeMetadataItem(AVMetadataCommonIdentifierDescription, value: "My Movie Description")
    metadata.append(descItem)
 
    // Build rating item
    let ratingItem =
        makeMetadataItem(AVMetadataIdentifieriTunesMetadataContentRating, value: "PG-13")
    metadata.append(ratingItem)
 
    // Build genre item
    let genreItem =
        makeMetadataItem(AVMetadataIdentifierQuickTimeMetadataGenre, value: "Comedy")
    metadata.append(genreItem)
 
    return metadata
}
 
private func makeMetadataItem(_ identifier: String,
                       value: Any) -> AVMetadataItem {
    let item = AVMutableMetadataItem()
    item.identifier = identifier
    item.value = value as? NSCopying & NSObjectProtocol
    item.extendedLanguageTag = "und"
    return item.copy() as! AVMetadataItem
}


3、定义导航标记(tvOS)

除了显示元信息,信息面板还可以显示导航标记,以帮助用户快速导航您的内容。
这些标记代表媒体时间推进表中的兴趣点,您可以通过使用Siri Remote选择所需的标记跳过这些标记(参见图4-5)。

图4-5信息面板导航标记

在这里插入图片描述


AVPlayerItem的AVPlayerItem添加了一个navigationMarkerGroups属性。
您可以使用AVNavigationMarkersGroup对象数组设置此属性,以定义当前媒体的导航标记。

注意: 虽然navigationMarkerGroups定义为数组,但目前仅支持数组中的第一个组。

一个AVNavigationMarkersGroup由一个或多个AVTimedMetadataGroup对象组成,每个对象代表玩家信息面板中显示的单个标记。
每个AVTimedMetadataGroup由该标记应用的资产时间推进表中的时间范围和AVMetadataItem对象数组组成,用于定义标记的标题和可选的缩略图(参见图4-6)。


图4-6导航标记组组成
在这里插入图片描述


以下代码示例显示了如何为您的媒体呈现章节列表:

func setupPlayback() {
    ...
    playerItem.navigationMarkerGroups = makeNavigationMarkerGroups()
    ...
}
 
private func makeNavigationMarkerGroups() -> [AVNavigationMarkersGroup] {
 
    let chapters = [
        ["title": "Chapter 1", "imageName": "chapter1", "start": 0.0, "end": 60.0],
        ["title": "Chapter 2", "imageName": "chapter2", "start": 60.0, "end": 120.0],
        ["title": "Chapter 3", "imageName": "chapter3", "start": 120.0, "end": 180.0],
        ["title": "Chapter 4", "imageName": "chapter4", "start": 180.0, "end": 240.0]
    ]
 
    var metadataGroups = [AVTimedMetadataGroup]()
 
    // Iterate over the defined chapters, building an AVTimedMetadataGroup for each
    for chapter in chapters {
        metadataGroups.append(makeTimedMetadataGroup(for: chapter))
    }
 
    return [AVNavigationMarkersGroup(title: nil, timedNavigationMarkers: metadataGroups)]
}
 
private func makeTimedMetadataGroup(for chapter: [String: Any]) -> AVTimedMetadataGroup {
 
    var metadata = [AVMetadataItem]()
 
    // Create chapter title item
    let title = chapter["title"] as! String
    let titleItem = makeMetadataItem(AVMetadataCommonIdentifierTitle, value: title)
    metadata.append(titleItem)
 
    let imageName = chapter["imageName"] as! String
    if let image = UIImage(named: imageName), let pngData = UIImagePNGRepresentation(image) {
        // Create chapter thumbnail item
        let imageItem = makeMetadataItem(AVMetadataCommonIdentifierArtwork, value: pngData)
        metadata.append(imageItem)
    }
 
    // Create CMTimeRange
    let timescale: Int32 = 600
    let cmStartTime = CMTimeMakeWithSeconds(chapter["start"] as! TimeInterval, timescale)
    let cmEndTime = CMTimeMakeWithSeconds(chapter["end"] as! TimeInterval, timescale)
    let timeRange = CMTimeRangeFromTimeToTime(cmStartTime, cmEndTime)
 
    return AVTimedMetadataGroup(items: metadata, timeRange: timeRange)
}
 
private func makeMetadataItem(_ identifier: String,
                       value: Any) -> AVMetadataItem {
    let item = AVMutableMetadataItem()
    item.identifier = identifier
    item.value = value as? NSCopying & NSObjectProtocol
    item.extendedLanguageTag = "und"
    return item.copy() as! AVMetadataItem
}


4、使用间隙内容(tvOS)

媒体播放应用程序通常会在其主要媒体内容旁边显示附加内容,如法律文本、内容警告或广告。
呈现此类内容的最佳解决方案是使用HTTP Live Streaming支持来提供拼接播放列表,这些播放列表用EXT-X-DISCONTINUITY标签分隔(参见 用于HTTP Live Streaming的示例播放列表文件 )。
拼接播放列表允许您将多个媒体播放列表组合成一个统一的播放列表,作为单个流交付给客户端。
这为用户提供了流畅的播放体验,而在呈现插页内容时不会出现任何中断或口吃。

tvOS中的AVKit可以轻松处理作为拼接播放列表的一部分交付的插页内容。
您可以在演示文稿中定义包含插页内容的时间范围,并且在播放过程中遍历它们时,您可以在它们开始和结束时接收回调,从而让您有机会实施业务规则或捕获分析。

AVPlayerItem的AVPlayerItem添加了一个interstitialTimeRanges属性,可以使用AVInterstitialTimeRange对象数组进行设置,每个对象定义一个CMTimeRange,标记媒体时间推进表中的间隙时间范围。
以下代码示例显示了如何创建间隙时间范围:

func setupPlayback() {
    ...
    playerItem.interstitialTimeRanges = makeInterstitialTimeRanges()
    ...
}
 
private func makeInterstitialTimeRanges() -> [AVInterstitialTimeRange] {
    // 10 second time range at beginning of video (Content Warning)
    let timeRange1 = CMTimeRange(start: kCMTimeZero,
                                 duration: CMTime(value: 10, timescale: 1))
 
    // 1 minute time range at 10:00 (Advertisements)
    let timeRange2 = CMTimeRange(start: CMTime(value: 600, timescale: 1),
                                 duration: CMTime(value: 60, timescale: 1))
 
    // Return array of AVInterstitialTimeRange objects
    return [
        AVInterstitialTimeRange(timeRange: timeRange1),
        AVInterstitialTimeRange(timeRange: timeRange2)
    ]
}

当间隙时间范围被定义后,AVPlayerViewController通过两种重要方式更新其用户界面(见图4-7)。
首先,任何间隙时间范围都在播放器的时间推进表上表示为小点。
这使得用户很容易理解他们在间隙时间之间的位置,并帮助他们定位到他们在您的程序中的位置。
其次,间隙时间范围从播放器的界面折叠。
呈现的当前时间和持续时间仅代表您的主要内容,从而更好地了解主程序的时间安排。

图4-7插页内容的播放器演示img

重要提示:玩家界面的时间范围折叠只是视觉上的。
您执行的任何程序化操作,例如寻找,都是在完整的资产时间推进表上完成的,包括间隙内容。

通过采用AVPlayerViewControllerDelegate协议,您可以在遍历间隙时间范围时收到通知,这有助于帮助您执行业务规则。
对于实例,呈现广告时的一个常见要求是防止用户跳过它们。
您可以使用AVPlayerViewControllerrequiresLinearPlayback属性来控制用户是否可以使用Siri Remote浏览内容。
在播放过程中,此属性通常设置为false,但在呈现广告时,您可以将其设置为true以防止用户导航,如以下示例所示:

public func playerViewController(_ playerViewController: AVPlayerViewController,
                                 willPresent interstitial: AVInterstitialTimeRange) {
    playerViewController.requiresLinearPlayback = true
}
 
public func playerViewController(_ playerViewController: AVPlayerViewController,
                                 didPresent interstitial: AVInterstitialTimeRange) {
    playerViewController.requiresLinearPlayback = false
}

如果您的应用呈现插页式内容,您可能还希望防止用户跳过过去的广告或法律文本。
您可以通过实现playerViewController:timeToSeekAfterUserNavigatedFromTime:toTime:委托方法来实现此功能。
每当用户使用Siri Remote执行查找操作时,都会调用此方法,这种操作是通过在远程触摸板上向左或向右滑动或通过在信息面板中导航章节标记来实现的。
以下提供了一个简单的示例,说明如何实现此方法来防止用户跳过过去的广告:

public func playerViewController(_ playerViewController: AVPlayerViewController, timeToSeekAfterUserNavigatedFrom oldTime: CMTime, to targetTime: CMTime) -> CMTime {
    // Only evaluate if the user performed a forward seek
    guard !canSkipInterstitials && oldTime < targetTime else {
        return targetTime
    }
 
    // Define time range of the user's seek operation
    let seekRange = CMTimeRange(start: oldTime, end: targetTime)
 
    // Iterate over the defined interstitial time ranges...
    for interstitialRange in playerItem.interstitialTimeRanges {
        // If the current interstitial is contained within the user's
        // seek range, return the interstitial's start time
        if seekRange.containsTimeRange(interstitialRange.timeRange) {
            return interstitialRange.timeRange.start
        }
    }
 
    // No match, return the target time
    return targetTime
}

对于任何前向搜索,示例代码确保用户不能跳过广告中断。
它试图找到存在于用户搜索请求时间范围内的间隙时间范围。
如果找到间隙时间范围,代码将返回其开始时间,强制播放从广告开始时开始。


5、展示内容提案(tvOS)

呈现序列化内容的媒体应用程序,如电视节目,通常在您看完当前节目时显示该系列下一集的预览。
此预览通常包括描述建议内容的艺术品和信息,以及观看下一集或返回主菜单的用户界面。
在早期版本的tvOS中实现此特征通常具有挑战性,但从tvOS 10开始,使用AVKit内容提案将此功能添加到您的应用程序中很容易。

您可以使用AVContentProposal类创建内容提案。
此类对有关提案内容的数据进行建模,例如标题、预览图像、元信息和内容URL,以及呈现提案的时间。
您可以创建和配置AVContentProposal实例,如下所示:

func makeProposal() -> AVContentProposal {
    // Present 10 seconds prior to the end of current presentation
    let time = currentAsset.duration - CMTime(value: 10, timescale: 1)
    let title = "My Show: Episode 2"
    let image = UIImage(named: "ms_ep2")!
    let proposal = AVContentProposal(contentTimeForTransition: time, title: title, previewImage: image)
    // Set custom metadata
    proposal.metadata = [
        makeMetadataItem(identifier: AVMetadataCommonIdentifierDescription, value: "Episode 2 Description"),
        makeMetadataItem(identifier: AVMetadataIdentifieriTunesMetadataContentRating, value: "TV-14"),
        makeMetadataItem(identifier: AVMetadataIdentifierQuickTimeMetadataGenre, value: "Comedy")
    ]
    // Set content URL
    proposal.url = // The upcoming asset's URL
    return proposal
}
 
private func makeMetadataItem(_ identifier: String, value: Any) -> AVMetadataItem {
    let item = AVMutableMetadataItem()
    item.identifier = identifier
    item.value = value as? NSCopying & NSObjectProtocol
    item.extendedLanguageTag = "und"
    return item.copy() as! AVMetadataItem
}

除了定义内容提案的数据之外,您还需要创建一个用户界面来向用户呈现这些数据。
您可以通过子类化AVKit框架的AVContentProposalViewController类来创建这个界面。
在运行时,您的子类实例被传递到当前的AVContentProposal,为您提供要呈现的数据。
您的界面应该提供描述提议内容的任何视觉和描述性信息以及用户界面,以便用户接受或拒绝提议。

当您提出建议时,它会显示在当前播放的全屏视频上。
您可能希望将此视频缩放到更小的尺寸,以便腾出更多空间来显示建议内容的详细信息。
为此,您需要覆盖视图控制器的preferredPlayerViewFrame属性并返回所需的视频帧。

override var preferredPlayerViewFrame: CGRect {
    guard let frame = playerViewController?.view.frame else { return CGRect.zero }
    // Present the current video in a 960x540 window centered at the top of the window
    return CGRect(x: frame.midX / 2.0, y: 0, width: 960, height: 540)
}

当呈现内容提议时,玩家的视图会自动动画到指定的CGRect

注意: 要相对于新大小和定位的视频帧布局内容,请使用视图控制器的playerLayoutGuide属性提供的UILayoutGuide

您呈现的用户界面还应该提供控件,以便用户可以接受或拒绝提议。
这些操作的事件处理程序应该调用控制器的dismissContentProposalForAction:animated:completion:方法,指示用户的选择,如下所示:

// Handle acceptance
@IBAction func acceptContentProposal(_ sender: AnyObject) {
    dismissContentProposal(for: .accept, animated: true, completion: nil)
}
// Handle rejection
@IBAction func rejectContentProposal(_ sender: AnyObject){
    dismissContentProposal(for: .reject, animated: true, completion: nil)
}

要使您的内容提案符合展示条件,您需要将其设置为当前AVPlayerItemnextContentProposal
以下示例显示了如何在管理Video对象队列的播放应用程序中设置这一点——这是一种自定义值类型,对队列中单个视频的数据进行建模。
该示例创建所需的播放对象,为队列中的下一个视频创建一个新的AVContentProposal,并将其设置为播放器项的nextContentProposal,如以下示例所示:

func prepareToPlay() {
    // Associate the AVPlayer with the AVPlayerViewController
    playerViewController.player = player
    playerViewController.delegate = self
    // Create a new AVPlayerItem with the current video's URL
    let playerItem = AVPlayerItem(url: currentVideo.url)
    player.replaceCurrentItem(with: playerItem)
    // Create an AVContentProposal for the next video (if it exists)
    playerItem.nextContentProposal = makeContentProposal(for: currentVideo.nextVideo)
    player.play()
}
 
func makeContentProposal(for video: Video?) -> AVContentProposal? {
    guard let video = video else { return nil }
    guard let currentAsset = player.currentItem?.asset else { return nil }
    // Start the proposal within presentationInterval of the asset's duration
    let time = currentAsset.duration - video.presentationInterval
    let title = video.title
    let image = video.image
    // Create a new content proposal with the time, title, and image
    let contentProposal = AVContentProposal(contentTimeForTransition: time, title: title, previewImage: image)
    // Set the content URL for the proposal
    contentProposal.url = video.url
    // Automatically accept the proposal 1 second from playback end time
    contentProposal.automaticAcceptanceInterval = -1.0
    return contentProposal
}

将内容提议设置为播放器项目的nextContentProposal后,下一步是实现AVPlayerViewControllerDelegate协议的方法。
您可以使用这些方法来定义内容提议是否以及如何呈现,以及处理提议内容的验收或拒绝。

要显示自定义视图控制器以响应显示下一个内容提案的请求,请实现playerViewController:shouldPresentContentProposal:方法。
在此方法中,您将自定义AVContentProposalViewController的实例设置为播放器视图控制器的contentProposalViewController属性,如下所示:

func playerViewController(_ playerViewController: AVPlayerViewController, shouldPresent proposal: AVContentProposal) -> Bool {
    // Set the presentation to use on the player view controller for this content proposal
    playerViewController.contentProposalViewController = NextVideoProposalViewController()
    return true
}

如果呈现的AVContentProposal提供了有效的内容URL,AVPlayerViewController可以自动处理其验收或拒绝。
但是,如果您需要对这些操作的处理进行更多控制,可以实现playerViewController:didAcceptContentProposal:playerViewController:didRejectContentProposal:方法。
例如,以下示例实现了playerViewController:didAcceptContentProposal:方法来播放提议的视频,并为队列中的下一个视频创建新的内容提议:

func playerViewController(_ playerViewController: AVPlayerViewController, didAccept proposal: AVContentProposal) {
    guard let player = playerViewController.player, let url = proposal.url else { return }
    guard let video = currentVideo.nextVideo else { return }
 
    currentVideo = video
 
    // Create a new player item using the content proposal's URL
    let playerItem = AVPlayerItem(url: url)
    player.replaceCurrentItem(with: playerItem)
    player.play()
 
    // Prepare new player item's next content proposal (if it exists)
    playerItem.nextContentProposal = makeContentProposal(for: currentVideo.nextVideo)
}


6、实施修剪(macOS)

您使用AVPlayerView提供类似macOS中QuickTime Player的播放体验。
然而,AVPlayerView不仅提供QuickTime播放界面,还提供QuickTime媒体修剪体验。

您可以使用AVPlayerView上的beginTrimmingWithCompletionHandler:方法将播放器置于修剪模式(参见图4-8)。

图4-8播放器视图修剪用户界面

在这里插入图片描述


在尝试将播放器放入修剪模式之前,通过查询播放器视图的canBeginTrimming属性来验证是否允许修剪。
如果您正在播放通过HTTP Live Streaming传递的资产,或者该资产受内容保护,则此属性返回false
如果您要显示一个菜单项来启动修剪,执行此检查的好地方是NSDocumentvalidateUserInterfaceItem:方法,以便在不允许修剪时自动禁用菜单项。

override func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
    if item.action == #selector(beginTrimming) {
        return playerView.canBeginTrimming
    }
    return super.validateUserInterfaceItem(item)
}

确定媒体支持修剪后,调用beginTrimmingWithCompletionHandler:
此方法采用完成屏蔽,用于确定用户是完成修剪还是取消操作。

@IBAction func beginTrimming(_ sender: AnyObject) {
    playerView.beginTrimming { result in
        if result == .okButton {
            // user selected Trim button (AVPlayerViewTrimResult.okButton)...
        } else {
            // user selected Cancel button (AVPlayerViewTrimResult.cancelButton)...
        }
    }
}

因为AVAsset是一个不可变的对象,您可能想知道当您按下修剪按钮时,它的持续时间是如何改变的。
修剪依赖于AVPlayerItem的一个特征来调整呈现的时间范围。
AVPlayerItem提供了reversePlaybackEndTimeforwardPlaybackEndTime属性来设置一段媒体的进出点。
它不会改变标的资产,但本质上改变了您对它的有效视图。
要保存用户修剪操作的结果,您可以导出资产的新副本,将其修剪到指定的时间。
您最简单的方法是使用AVAssetExportSession,它为您提供了一种简单且高性能的方式来转码资产的媒体。
您创建一个新的导出会话,将要导出的资产与要使用的转码预设一起传递给它,如下所示:

// Transcoding preset
let preset = AVAssetExportPresetAppleM4V720pHD
let exportSession = AVAssetExportSession(asset: playerItem.asset, presetName: preset)!
exportSession.outputFileType = AVFileTypeAppleM4V
exportSession.outputURL = // Output URL

此示例使用预设将媒体导出为720p、M4V文件,但AVAssetExportSession支持多种导出预设。
要了解流动资产支持哪些导出会话预设,可以使用会话的exportPresetsCompatibleWithAsset:class方法,将要导出的资产传递给它。
此方法返回可在导出中使用的有效预设数组。

要仅导出用户修剪的内容,您可以使用当前播放器项的反向和正向结束时间值来定义要在导出会话上设置的CMTimeRange

// Create CMTimeRange with the trim in/out point times
let startTime = self.playerItem.reversePlaybackEndTime
let endTime = self.playerItem.forwardPlaybackEndTime
let timeRange = CMTimeRangeFromTimeToTime(startTime, endTime)
exportSession.timeRange = timeRange

要执行实际的导出操作,可以调用它的exportAsynchronouslyWithCompletionHandler:方法。

exportSession.exportAsynchronously {
    switch exportSession.status {
    case .completed:
        // Export Complete
    case .failed:
        // failed
    default:
        // handle others
    }
}

六、提炼用户体验

您已经在本指南中了解了如何使用AVKit和AVFoundation构建引人注目的播放应用程序。
本章介绍了AVFoundation的一些附加功能,您可以使用这些功能进一步自定义和优化应用程序的播放体验。


1、呈现章节标记

章节标记使用户能够快速浏览您的内容。
AVPlayerViewController在tvOS和macOS中,如果在当前播放的资产中找到标记,则会自动呈现章节选择界面。
每当您想创建自己的自定义章节选择界面时,您也可以直接检索这些数据。

章节标记是一种定时元信息
这与本指南其他部分中讨论的元信息类型相同,但它不适用于整个资产,而是仅适用于资产时间推进表中的时间范围。
您可以使用chapterMetadataGroupsBestMatchingPreferredLanguages:chapterMetadataGroupsWithTitleLocale:containingItemsWithCommonKeys:方法检索资产的章节元信息。
在异步加载资产的availableChapterLocales键值后,这些方法变得可调用而不会阻塞:

let asset = AVAsset(url: <# Asset URL #>)
let chapterLocalesKey = "availableChapterLocales"
 
asset.loadValuesAsynchronously(forKeys: [chapterLocalesKey]) {
    var error: NSError?
    let status = asset.statusOfValue(forKey: chapterLocalesKey, error: &error)
    if status == .loaded {
        let languages = Locale.preferredLanguages
        let chapterMetadata = asset.chapterMetadataGroups(bestMatchingPreferredLanguages: languages)
        // Process chapter metadata
    }
    else {
        // Handle other status cases
    }
}

这些方法返回的值是AVTimedMetadataGroup对象的数组,每个对象代表一个单独的章节标记。
AVTimedMetadataGroup对象由定义其元信息应用的时间范围的CMTimeRange、表示章节标题的AVMetadataItem对象数组以及可选的缩略图图像组成。
以下示例显示了如何将AVTimedMetadataGroup数据转换为自定义模型对象数组,称为Chapter,以显示在应用的视图层中:

func convertTimedMetadataGroupsToChapters(groups: [AVTimedMetadataGroup]) -> [Chapter] {
    return groups.map { group -> Chapter in
 
        // Retrieve AVMetadataCommonIdentifierTitle metadata items
        let titleItems =
            AVMetadataItem.metadataItems(from: group.items,
                                         filteredByIdentifier: AVMetadataCommonIdentifierTitle)
 
        // Retrieve AVMetadataCommonIdentifierTitle metadata items
        let artworkItems =
            AVMetadataItem.metadataItems(from: group.items,
                                         filteredByIdentifier: AVMetadataCommonIdentifierArtwork)
 
        var title = "Default Title"
        var image = UIImage(named: "placeholder")!
 
        if let titleValue = titleItems.first?.stringValue {
            title = titleValue
        }
 
        if let imgData = artworkItems.first?.dataValue, imageValue = UIImage(data: imgData) {
            image = imageValue
        }
 
        return Chapter(time: group.timeRange.start, title: title, image: image)
    }
} 

转换相关数据后,您可以构建章节选择界面,并使用章节对象的time值使用播放器的查找seekToTime:方法查找当前演示文稿。


2、选择媒体选项

作为开发人员,您希望让尽可能广泛的受众可以访问您的应用程序。
扩展应用程序覆盖范围的一种方法是以用户的母语向用户提供应用程序,并为有听力障碍或其他可访问性需求的用户提供支持。
AVKit和AVFoundation通过提供对显示字幕和隐藏式字幕以及选择替代音频和视频轨道的内置支持,可以轻松处理这些问题。
如果您正在构建自己的自定义播放器或想展示自己的媒体选择界面,您可以使用AVFoundation的AVMediaSelectionGroupAVMediaSelectionOption类提供的功能。

对源媒体中包含的替代音频、视频或文本轨道进行建模。
媒体选项用于选择替代摄像机角度、呈现以用户母语配音的音频或显示字幕和隐藏式字幕。
您可以通过异步加载和调用资产的AVMediaSelectionOption``availableMediaCharacteristicsWithMediaSelectionOptions属性来确定哪些替代媒体演示可用,该属性返回指示可用媒体特征的数组字符串。
返回的可能值是AVMediaCharacteristicAudible(替代音轨)、AVMediaCharacteristicVisual(替代视频轨道)和AVMediaCharacteristicLegible(字幕和隐藏式字幕)。

检索到可用选项的数组后,调用资产的mediaSelectionGroupForMediaCharacteristic:方法,向其传递所需的特征。
此方法返回关联的AVMediaSelectionGroup对象,如果指定特征不存在组,则返回nil

AVMediaSelectionGroup充当互斥AVMediaSelectionOption对象集合的容器。
以下示例显示了如何检索资产的媒体选择组并显示其可用选项:

for characteristic in asset.availableMediaCharacteristicsWithMediaSelectionOptions {
 
    print("\(characteristic)")
 
    // Retrieve the AVMediaSelectionGroup for the specified characteristic.
    if let group = asset.mediaSelectionGroup(forMediaCharacteristic: characteristic) {
        // Print its options.
        for option in group.options {
            print("  Option: \(option.displayName)")
        }
    }
}

包含音频和字幕媒体选项的资产的输出类似于以下内容:

[AVMediaCharacteristicAudible]
  Option: English
  Option: Spanish
[AVMediaCharacteristicLegible]
  Option: English
  Option: German
  Option: Spanish
  Option: French

检索特定媒体特征的AVMediaSelectionGroup对象并确定所需的AVMediaSelectionOption对象后,下一步是选择它。
您可以通过调用活动AVPlayerItem上的selectMediaOption:inMediaSelectionGroup:来选择媒体选项。
对于实例,要显示资产的西班牙语字幕选项,您可以按如下方式选择它:

if let group = asset.mediaSelectionGroup(forMediaCharacteristic: AVMediaCharacteristicLegible) {
    let locale = Locale(identifier: "es-ES")
    let options =
        AVMediaSelectionGroup.mediaSelectionOptions(from: group.options, with: locale)
    if let option = options.first {
        // Select Spanish-language subtitle option
        playerItem.select(option, in: group)
    }
}

选择媒体选项可立即显示。
选择字幕或隐藏字幕选项会在AVPlayerViewControllerAVPlayerViewAVPlayerLayer提供的视频显示中显示相关文本。
选择替代音频或视频选项会将当前显示的媒体替换为新选择的媒体。

注意: 从iOS7.0和OS X 10.9开始,AVPlayer提供基于用户系统偏好的自动媒体选择作为其默认行为。
要控制何时呈现媒体选择,请通过将播放器的appliesMediaSelectionCriteriaAutomatically值设置为NO来禁用默认行为。


3、使用iOS音频环境

您可以使用iOS音频会话API来定义应用程序的一般音频行为及其在运行它的设备的整体音频上下文中的角色。
以下部分描述了管理和控制应用程序音频播放的其他方法,以及如何响应更大iOS音频环境中的更改。


播放背景音频

许多媒体播放应用程序的共同特征是在应用程序发送到后台时继续播放音频。
这可能是用户切换应用程序或锁定设备的结果。
要使您的应用程序能够播放背景音频,您首先要配置应用程序的功能和音频会话,如为iOS和tvOS配置音频设置中所述。

如果您正在播放纯音频资产,例如MP3或M4A文件,则您的设置已完成,您的应用程序可以播放背景音频。
如果您需要播放视频资产的音频部分,则需要额外的步骤。
如果播放器的当前项目正在设备的显示器上显示视频,则当应用程序发送到后台时,AVPlayer的播放会自动暂停。
如果您想继续播放音频,请在进入后台时断开演示文稿中的AVPlayer实例,并在返回前台时重新连接,如以下示例所示:

func applicationDidEnterBackground(_ application: UIApplication) {
    // Disconnect the AVPlayer from the presentation when entering background
 
    // If presenting video with AVPlayerViewController
    playerViewController.player = nil
 
    // If presenting video with AVPlayerLayer
    playerLayer.player = nil
}
 
func applicationWillEnterForeground(_ application: UIApplication) {
    // Reconnect the AVPlayer to the presentation when returning to foreground
 
    // If presenting video with AVPlayerViewController
    playerViewController.player = player
 
    // If presenting video with AVPlayerLayer
    playerLayer.player = player
}


控制背景音频

如果您的应用在后台播放音频,您应该支持在控制中心和iOS锁屏中远程控制播放。
除了控制播放,您还应该在这些界面中提供有关当前播放内容的有意义的信息。
要实现此功能,您可以使用MediaPlayer框架的MPRemoteCommandCenterMPNowPlayingInfoCenter类。

MPRemoteCommandCenter类出售用于处理外部附件和系统传输控件发送的远程控制事件的对象。
它以MPRemoteCommand对象的形式定义了各种命令,您可以将自定义事件处理程序附加到这些命令上以控制应用中的播放。
对于实例,要远程控制应用的播放和暂停行为,您可以获得对共享命令中心的引用,并为相应的命令提供处理程序,如下所示:

func setupRemoteTransportControls() {
    // Get the shared MPRemoteCommandCenter
    let commandCenter = MPRemoteCommandCenter.shared()
 
    // Add handler for Play Command
    commandCenter.playCommand.addTarget { [unowned self] event in
        if self.player.rate == 0.0 {
            self.player.play()
            return .success
        }
        return .commandFailed
    }
 
    // Add handler for Pause Command
    commandCenter.pauseCommand.addTarget { [unowned self] event in
        if self.player.rate == 1.0 {
            self.player.pause()
            return .success
        }
        return .commandFailed
    }
}

前面的示例显示了如何使用addTargetWithHandler:方法为命令中心的playCommandpauseCommand添加处理程序。
您给这个方法的回调/回传屏蔽要求您返回一个MPRemoteCommandHandlerStatus值,指示命令是成功还是失败。

配置远程命令处理程序后,下一步是提供元信息以显示在iOS锁屏和控制中心的传输区域中。
您使用MPMediaItemMPNowPlayingInfoCenter定义的键提供元信息字典,并在MPNowPlayingInfoCenter的默认实例上设置该字典。
以下示例向您展示如何为当前呈现的媒体设置标题和图稿,以及播放时间值:

func setupNowPlaying() {
    // Define Now Playing Info
    var nowPlayingInfo = [String : Any]()
    nowPlayingInfo[MPMediaItemPropertyTitle] = "My Movie"
    if let image = UIImage(named: "lockscreen") {
        nowPlayingInfo[MPMediaItemPropertyArtwork] =
            MPMediaItemArtwork(boundsSize: image.size) { size in
                return image
        }
    }
    nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = playerItem.currentTime().seconds
    nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = playerItem.asset.duration.seconds
    nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player.rate
 
    // Set the metadata
    MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}

此示例将动态计时作为nowPlayingInfo字典的一部分传递,它允许您在远程控制界面中显示播放器计时和进度。
这样做时,请在您的应用进入后台时提供此计时的最新快照。
如果媒体信息或计时发生重大变化,您还可以在应用处于后台时更新nowPlayingInfo


应对中断

中断是iOS用户体验的常见部分。
例如,考虑一下如果您在Videos应用程序中观看电影并收到电话或FaceTime请求会发生什么情况。
在这种情况下,您的电影音频会很快淡出,播放会暂停,铃声也会淡入。
如果您拒绝呼叫或请求,控制权将返回给Videos应用程序,并在电影音频淡入时重新开始播放。
这种行为的中心是您的应用程序的AVAudioSession
随着中断的开始和结束,它会通知任何注册的观察者,以便他们采取适当的行动。
AVPlayer知道您的音频会话,并自动暂停和恢复播放以响应AVAudioSession中断事件。
要观察此AVPlayer行为,请在播放器的rate属性上使用键值监听(KVO),以便在播放器暂停和恢复以响应中断时更新用户界面。

您也可以直接观察AVAudioSession发布的中断通知。
如果您想知道播放是否因中断或其他原因(如路由更改)而暂停,这可能很有用。
要观察音频中断,首先注册以观察AVAudioSessionInterruptionNotification类型的通知。

func setupNotifications() {
    let notificationCenter = NotificationCenter.default
    notificationCenter.addObserver(self,
                                   selector: #selector(handleInterruption),
                                   name: .AVAudioSessionInterruption,
                                   object: nil)
}
 
func handleInterruption(notification: Notification) {
 
}

发布的NSNotification对象包含一个填充的userInfo字典,提供中断的详细信息。
您可以通过从userInfo字典中检索AVAudioSessionInterruptionType值来确定中断类型。
中断类型指示中断是已经开始还是已经结束。

func handleInterruption(notification: Notification) {
    guard let userInfo = notification.userInfo,
        let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
        let type = AVAudioSessionInterruptionType(rawValue: typeValue) else {
            return
    }
    if type == .began {
        // Interruption began, take appropriate actions
    }
    else if type == .ended {
        if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
            let options = AVAudioSessionInterruptionOptions(rawValue: optionsValue)
            if options.contains(.shouldResume) {
                // Interruption Ended - playback should resume
            } else {
                // Interruption Ended - playback should NOT resume
            }
        }
    }
}

如果中断类型AVAudioSessionInterruptionTypeEnded,则userInfo字典包含一个AVAudioSessionInterruptionOptions值,用于确定是否应自动恢复播放。


应对路线变更

AVAudioSession负责的一项重要职责是管理音频路由更改。
当音频输入或输出被添加到iOS设备或从iOS设备中删除时,就会发生路由更改。
路由更改的发生有多种原因,包括用户插入一副耳机、连接蓝牙LE耳机或拔掉USB音频接口。
发生这些更改时,AVAudioSession会相应地重新路由音频信号,并向任何注册的观察者广播包含更改详细信息的通知。

当用户插入或移除一副耳机(参见iOS人机界面指南中的声音)时,就会出现与路由更改相关的重要要求。
当用户连接一副有线或无线耳机时,他们隐含地表示音频播放应该继续,但要私下进行。
他们期望当前正在播放媒体的应用程序继续播放而不会暂停。
当用户拔掉耳机时,他们不想自动与他人分享他们正在听的内容。
应用程序应该尊重这种隐含的隐私请求,并在耳机被移除时自动暂停播放。

AVPlayer知道您应用的音频会话,并对路由更改做出适当响应。
连接耳机后,播放会按预期继续。
移除耳机后,播放会自动暂停。
要观察此AVPlayer行为,请在播放器的rate属性上使用KVO,以便您可以在播放器暂停以响应音频路由更改时更新用户界面。

您还可以直接观察AVAudioSession发布的任何路由更改通知。
如果您想在用户连接或移除耳机时收到通知,以便在播放器界面中显示图标或消息,这可能会很有用。
要观察音频路由更改,您首先要注册以观察AVAudioSessionRouteChangeNotification类型的通知。

func setupNotifications() {
    let notificationCenter = NotificationCenter.default
    notificationCenter.addObserver(self,
                                   selector: #selector(handleRouteChange),
                                   name: .AVAudioSessionRouteChange,
                                   object: nil)
}
 
func handleRouteChange(notification: Notification) {
 
}

发布的NSNotification对象包含一个填充的userInfo字典,提供路由更改的详细信息。
您可以通过从userInfo字典中检索AVAudioSessionRouteChangeReason值来确定此更改的原因。
当连接新设备时,原因是AVAudioSessionRouteChangeReasonNewDeviceAvailable,当删除一个设备时,原因是AVAudioSessionRouteChangeReasonOldDeviceUnavailable

func handleRouteChange(notification: Notification) {
    guard let userInfo = notification.userInfo,
        let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
        let reason = AVAudioSessionRouteChangeReason(rawValue:reasonValue) else {
            return
    }
    switch reason {
    case .newDeviceAvailable:
        // Handle new device available.
    case .oldDeviceUnavailable:
        // Handle old device removed.
    default: ()
    }
}

当有新设备可用时,您要求AVAudioSession提供其currentRoute以确定音频输出当前路由的位置。
这将返回一个AVAudioSessionRouteDescription,列出音频会话的所有inputsoutputs
删除设备时,您可以从userInfo字典中检索上一个路由的AVAudioSessionRouteDescription
在这两种情况下,您都要查询其outputs的路由描述,它返回一个AVAudioSessionPortDescription对象数组,提供音频输出路由的详细信息。

func handleRouteChange(notification: Notification) {
    guard let userInfo = notification.userInfo,
        let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
        let reason = AVAudioSessionRouteChangeReason(rawValue:reasonValue) else {
            return
    }
    switch reason {
    case .newDeviceAvailable:
        let session = AVAudioSession.sharedInstance()
        for output in session.currentRoute.outputs where output.portType == AVAudioSessionPortHeadphones {
            headphonesConnected = true
            break
        }
    case .oldDeviceUnavailable:
        if let previousRoute =
            userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
            for output in previousRoute.outputs where output.portType == AVAudioSessionPortHeadphones {
                headphonesConnected = false
                break
            }
        }
    default: ()
    }
}

七、使用HTTP Live Streaming

HTTP Live Streaming(HLS)是向回放应用传送媒体的理想方式。
使用HLS,您可以以不同的比特率提供多个媒体流,并且您的回放客户端会随着网络带宽的变化动态选择适当的流。
这可确保您始终在给定用户当前网络条件的情况下传送最优质的内容。
本章介绍如何在回放应用中利用HLS的独特特性和功能。


1、播放离线HLS内容

从iOS10开始,您可以使用AVFoundation将HTTP Live Streaming资产下载到iOS设备。
这一新功能允许用户在访问快速、可靠的网络时在他们的设备上下载和存储HLS电影,并且稍后在没有网络连接的情况下观看。
随着这一功能的引入,HLS变得更加通用,最大限度地减少了不一致的网络可用性对用户体验的影响。

iOS中的AVFoundation引入了几个新类来支持下载HLS资产以供离线使用。
以下部分讨论这些类以及用于将此功能添加到应用程序的基本工作流程。


准备下载

您可以使用AVAssetDownloadURLSession的实例来管理资产下载的执行。
这是NSURLSession的子类,它共享大部分功能,但专门用于创建和执行资产下载任务。
就像使用NSURLSession一样,您可以通过传递一个NSURLSessionConfiguration来创建一个AVAssetDownloadURLSession,定义其基本配置设置。
此会话配置必须是后台配置,以便在您的应用处于后台时可以继续进行资产下载。
创建AVAssetDownloadURLSession实例时,您还会向它传递对采用AVAssetDownloadDelegate协议的对象和一个NSOperationQueue对象的引用。
下载会话将通过调用指定队列上的委托方法来使用下载进度更新其委托。

func setupAssetDownload() {
    // Create new background session configuration.
    configuration = URLSessionConfiguration.background(withIdentifier: downloadIdentifier)
 
    // Create a new AVAssetDownloadURLSession with background configuration, delegate, and queue
    downloadSession = AVAssetDownloadURLSession(configuration: configuration,
                                                assetDownloadDelegate: self,
                                                delegateQueue: OperationQueue.main)
}

创建和配置下载会话后,您可以使用它来创建AVAssetDownloadTask的实例,使用会话的assetDownloadTaskWithURLAsset:assetTitle:assetArtworkData:options:方法。
您可以使用此方法提供要下载的AVURLAsset以及标题、可选图稿和下载选项字典。
您可以使用选项字典来定位要下载的特定变体码率或特定媒体选择。
如果未指定选项,则下载用户主要音频和视频内容的最高质量变体。

func setupAssetDownload() {
    ...
    // Previous AVAssetDownloadURLSession configuration
    ...
 
    let url = // HLS Asset URL
    let asset = AVURLAsset(url: url)
 
    // Create new AVAssetDownloadTask for the desired asset
    let downloadTask = downloadSession.makeAssetDownloadTask(asset: asset,
                                                             assetTitle: assetTitle,
                                                             assetArtworkData: nil,
                                                             options: nil)
    // Start task and begin download
    downloadTask?.resume()
}

AVAssetDownloadTask继承自NSURLSessionTask,这意味着您可以分别使用其suspendcancel方法暂停或取消下载任务。
如果下载被取消,并且无意恢复,您的应用负责删除已下载到用户设备的资产部分。

由于下载任务可以在后台进程中继续执行,因此应考虑应用程序在后台终止时任务仍在进行中的情况。
应用程序启动时,可以使用NSURLSessionAPI的标准功能来恢复任何待处理任务的状态。
为此,您需要使用最初启动这些任务时使用的会话配置标识符创建一个新的NSURLSessionConfiguration实例,并重新创建AVAssetDownloadURLSession
您可以使用会话的getTasksWithCompletionHandler:方法来查找任何待处理任务并恢复用户界面的状态,如下所示:

func restorePendingDownloads() {
    // Create session configuration with ORIGINAL download identifier
    configuration = URLSessionConfiguration.background(withIdentifier: downloadIdentifier)
 
    // Create a new AVAssetDownloadURLSession
    downloadSession = AVAssetDownloadURLSession(configuration: configuration,
                                                assetDownloadDelegate: self,
                                                delegateQueue: OperationQueue.main)
 
    // Grab all the pending tasks associated with the downloadSession
    downloadSession.getAllTasks { tasksArray in
        // For each task, restore the state in the app
        for task in tasksArray {
            guard let downloadTask = task as? AVAssetDownloadTask else { break }
            // Restore asset, progress indicators, state, etc...
            let asset = downloadTask.urlAsset
        }
    }
}

监控下载进度

当资产正在下载时,您可以通过实现下载委托的URLSession:assetDownloadTask:didLoadTimeRange:totalTimeRangesLoaded:timeRangeExpectedToLoad:方法。
与其他NSURLSessionAPI不同,资产下载进度以加载的时间范围而不是字节表示。
您可以使用此回调/回传中返回的时间范围值计算资产的下载进度,如以下示例所示:

func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didLoad timeRange: CMTimeRange, totalTimeRangesLoaded loadedTimeRanges: [NSValue], timeRangeExpectedToLoad: CMTimeRange) {
    var percentComplete = 0.0
    // Iterate through the loaded time ranges
    for value in loadedTimeRanges {
        // Unwrap the CMTimeRange from the NSValue
        let loadedTimeRange = value.timeRangeValue
        // Calculate the percentage of the total expected asset duration
        percentComplete += loadedTimeRange.duration.seconds / timeRangeExpectedToLoad.duration.seconds
    }
    percentComplete *= 100
    // Update UI state: post notification, update KVO state, invoke callback, etc.
}


保存下载位置

当资产下载完成时,或者因为资产已成功下载到用户设备,或者因为下载任务被取消,委托的URLSession:assetDownloadTask:didFinishDownloadingToURL:方法被调用,提供下载资产的本地文件URL。
保存对资产相对路径的持久引用,以便以后可以找到它:

func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
    // Do not move the asset from the download location
    UserDefaults.standard.set(location.relativePath, forKey: "assetPath")
}

您将使用此引用重新创建资产以供以后播放,或者在用户希望将其从设备中删除时将其删除。
NSURLSessionDownloadDelegateURLSession:downloadTask:didFinishDownloadingToURL:方法不同,客户端不应移动下载的资产。
下载资产的管理在很大程度上由系统控制,传递给此方法的URL代表资产包在磁盘上的最终位置。

重要提示:下载的HLS资产以私有捆绑格式存储在磁盘上。
这种捆绑格式可能会随着时间的推移而改变,开发人员不应尝试直接访问或存储捆绑包中的文件,而应使用AVFoundation和其他iOSAPI与下载的资产进行交互。

注意: 使用NSURLIsExcludedFromBackupKey键自动从iCloud备份中排除HLS资产下载。


下载其他媒体选择

您可以使用额外的音频和视频变体或替代媒体选择来更新下载的资产。
如果最初下载的电影不包含服务器上可用的最高质量视频码率,或者如果用户想向下载的资产添加补充音频或字幕选择,此功能非常有用。

AVAssetDownloadTask下载单个媒体选择集。
在初始资源下载期间,会下载用户的默认媒体选择——他们的主要音频和视频轨道。
如果找到其他媒体选择,如字幕、隐藏式字幕或替代音轨,则会话委托的URLSession:assetDownloadTask:didResolveMediaSelection:方法被调用,表明服务器上存在其他媒体选择。
要下载其他媒体选择,请保存对此已解析AVMediaSelection对象的引用,以便您可以创建后续下载任务以串行执行。

func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didResolve resolvedMediaSelection: AVMediaSelection) {
    // Store away for later retrieval when main asset download is complete
    // mediaSelectionMap is defined as: [AVAssetDownloadTask : AVMediaSelection]()
    mediaSelectionMap[assetDownloadTask] = resolvedMediaSelection
}

在下载其他媒体选择之前,请确定已缓存到磁盘的内容。
脱机资产提供了一个关联的AVAssetCache对象,您可以使用该对象来访问资产缓存媒体的状态。
使用选择媒体选项中讨论的AVAsset方法,您可以确定哪些媒体选择可用于该资产,并使用资产缓存来确定哪些值可脱机使用。
以下方法为您提供了一种查找 尚未在本地缓存的 所有可听和可读选项的方法:

func nextMediaSelection(_ asset: AVURLAsset) -> (mediaSelectionGroup: AVMediaSelectionGroup?,
                                                 mediaSelectionOption: AVMediaSelectionOption?) {
 
    // If the specified asset has not associated asset cache, return nil tuple
    guard let assetCache = asset.assetCache else {
        return (nil, nil)
    }
 
    // Iterate through audible and legible characteristics to find associated groups for asset
    for characteristic in [AVMediaCharacteristicAudible, AVMediaCharacteristicLegible] {
 
        if let mediaSelectionGroup = asset.mediaSelectionGroup(forMediaCharacteristic: characteristic) {
 
            // Determine which offline media selection options exist for this asset
            let savedOptions = assetCache.mediaSelectionOptions(in: mediaSelectionGroup)
 
            // If there are still media options to download...
            if savedOptions.count < mediaSelectionGroup.options.count {
                for option in mediaSelectionGroup.options {
                    if !savedOptions.contains(option) {
                        // This option hasn't been downloaded. Return it so it can be.
                        return (mediaSelectionGroup, option)
                    }
                }
            }
        }
    }
    // At this point all media options have been downloaded.
    return (nil, nil)
}

此方法检索与可听和可读特征相关联的资产的可用AVMediaSelectionGroup对象,并确定它们的哪些AVMediaSelectionOption对象已被下载。
如果它找到尚未下载的新媒体选择选项,则将该组选项对以元组形式返回给调用方。
您可以使用此方法帮助下载委托的URLSession:task:didCompleteWithError:method中的其他媒体选择,如以下示例所示:

注意:以下代码示例显示了如何下载所有可用的可听和可读选项。
在大多数情况下,您只会下载用户特别请求的媒体选项。

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
 
    guard error == nil else { return }
    guard let task = task as? AVAssetDownloadTask else { return }
 
    // Determine the next available AVMediaSelectionOption to download
    let mediaSelectionPair = nextMediaSelection(task.urlAsset)
 
    // If an undownloaded media selection option exists in the group...
    if let group = mediaSelectionPair.mediaSelectionGroup,
           option = mediaSelectionPair.mediaSelectionOption {
 
        // Exit early if no corresponding AVMediaSelection exists for the current task
        guard let originalMediaSelection = mediaSelectionMap[task] else { return }
 
        // Create a mutable copy and select the media selection option in the media selection group
        let mediaSelection = originalMediaSelection.mutableCopy() as! AVMutableMediaSelection
        mediaSelection.select(option, in: group)
 
        // Create a new download task with this media selection in its options
        let options = [AVAssetDownloadTaskMediaSelectionKey: mediaSelection]
        let task = downloadSession.makeAssetDownloadTask(asset: task.urlAsset,
                                                         assetTitle: assetTitle,
                                                         assetArtworkData: nil,
                                                         options: options)
 
        // Start media selection download
        task?.resume()
 
    } else {
        // All media selection downloads complete
    }
}


玩离线资产

启动下载后,应用可以通过创建具有用于初始化AVAssetDownloadTask的相同资产实例的AVPlayerItem实例来同时开始播放资产,如下例所示:

func downloadAndPlayAsset(_ asset: AVURLAsset) {
    // Create new AVAssetDownloadTask for the desired asset
    // Passing a nil options value indicates the highest available bitrate should be downloaded
    let downloadTask = downloadSession.makeAssetDownloadTask(asset: asset,
                                                             assetTitle: assetTitle,
                                                             assetArtworkData: nil,
                                                             options: nil)!
    // Start task
    downloadTask.resume()
 
    // Create standard playback items and begin playback
    let playerItem = AVPlayerItem(asset: downloadTask.urlAsset)
    player = AVPlayer(playerItem: playerItem)
    player.play()
}

当用户同时下载和播放资产时,视频的某些部分可能会以低于下载任务配置中指定的质量播放。
如果网络带宽限制阻止以下载请求的质量流式传输,就会发生这种情况。
发生这种情况时,AVAssetDownloadURLSession会继续超过资产播放时间,直到下载了所有具有请求质量的媒体片段。
完成后,磁盘上的资产将包含整个电影请求质量级别的视频。
AVAssetDownloadURLSession

只要有可能,重用用于配置下载任务的相同资产实例进行回放。
这种方法在上述场景中效果很好,但是当下载完成并且原始资产引用或其下载任务不再存在时,您该怎么办?在这种情况下,您需要通过为您在保存下载位置中保存的相对路径创建一个URL来初始化一个新的资产以进行回放。
此URL提供存储在文件系统上的资产的本地引用,如以下示例所示:

func playOfflineAsset() {
    guard let assetPath = UserDefaults.standard.value(forKey: "assetPath") as? String else {
        // Present Error: No offline version of this asset available
        return
    }
    let baseURL = URL(fileURLWithPath: NSHomeDirectory())
    let assetURL = baseURL.appendingPathComponent(assetPath)
    let asset = AVURLAsset(url: assetURL)
    if let cache = asset.assetCache, cache.isPlayableOffline {
        // Set up player item and player and begin playback
    } else {
        // Present Error: No playable version of this asset exists offline
    }
}

此示例检索存储的相对路径并创建文件URL以初始化新的AVURLAsset实例。
它进行测试以确保资产具有关联的资产缓存,并且至少有一个资产再现可以离线播放。
您的应用应检查下载资产的可用性并优雅地处理缺失的资产。

注意:当iOS设备未连接到网络时,唯一可用于播放的选项是存储在用户设备上的选项。
应用程序必须通过将用户界面上的可用选项限制为下载资产中存在的选项来防止用户选择不可用的选项。

如果iOS设备具有网络连接,并且资产被配置为回放下载的资产中不可用的变体或再现,则回放引擎从服务器流式传输请求的变体或再现,并将其与本地存储的资产片段组合回放。

重要提示:在磁盘空间极小的情况下,操作系统可能会自动删除下载的资产。
在您向用户展示资产可供播放之前,请验证该资产是否存在并且可以离线播放。


管理资产生命周期

当您将离线HLS功能添加到您的应用程序时,您负责管理下载到用户iOS设备的资产的生命周期。
确保您的应用程序为用户提供了一种查看永久存储在其设备上的资产列表的方法,包括每个资产的大小,以及用户在需要释放磁盘空间时删除资产的方法。
您的应用程序还应该为用户提供适当的UI,以区分本地存储在设备上的资产和云中可用的资产。

您可以使用NSFileManager的removeItemAtURL: error:removeItemAtURL:error:下载的HLS资产,并将资产的本地URL传递给它,如下所示:

func deleteOfflineAsset() {
    do {
        let userDefaults = UserDefaults.standard
        if let assetPath = userDefaults.value(forKey: "assetPath") as? String {
            let baseURL = URL(fileURLWithPath: NSHomeDirectory())
            let assetURL = baseURL.appendingPathComponent(assetPath)
            try FileManager.default.removeItem(at: assetURL)
            userDefaults.removeObject(forKey: "assetPath")
        }
    } catch {
        print("An error occured deleting offline asset: \(error)")
    }
}


2、观察网络接口层和错误记录

AVPlayerItem有许多信息属性,如loadedTimeRangesplaybackLikelyToKeepUp,可以帮助您确定其当前播放状态。
它还允许您通过在其accessLog和errorLogerrorLog属性中找到的两个日志工具访问其较低级别状态的详细信息。
这些日志提供了在使用HLS资产时可用于离线分析的附加信息。

访问日志是在远程主机上播放资源时发生的所有与网络相关的访问的运行日志。
AVPlayerItemAccessLog在这些事件发生时收集它们,让您深入了解播放器项目的活动。
使用日志的events属性检索日志事件的完整集合。
这将返回AVPlayerItemAccessLogEvent对象的数组,每个对象代表一个唯一的日志条目。
您可以从该条目中检索许多有用的详细信息,例如当前播放的变体流的URI、遇到的停顿数量和观看的持续时间。
要在新条目写入访问日志时收到通知,您可以注册以观察类型为AVPlayerItemNewAccessLogEntryNotification的通知。

与访问日志类似,AVPlayerItem也提供了一个错误日志来访问回放过程中遇到的错误信息。
AVPlayerItemErrorLog维护由AVPlayerItemErrorLogEvent类建模的错误事件的累积集合。
每个事件代表一个唯一的日志条目,并提供回放会话ID、错误状态代码和错误状态注释等详细信息。
AVPlayerItemAccessLog一样,当新条目写入错误日志时,可以通过注册以观察AVPlayerItemNewErrorLogEntryNotification类型的通知来通知您。

因为访问日志和错误日志主要用于脱机分析,所以可以很容易地以符合W3C扩展日志文件格式的文本格式创建日志的完整快照(参见http://www.w3.org/pub/WWW/TR/WD-logfile.html)。
两个日志都提供extendedLogDataextendedLogDataStringEncoding属性,从而可以很容易地创建日志内容的字符串版本:

if let log = playerItem.accessLog() {
    let data = log.extendedLogData()!
    let encoding = String.Encoding(rawValue: log.extendedLogDataStringEncoding)
    let offlineLog = String(data: data, encoding: encoding)
    // process log
}


3、使用网络链接调节器进行测试

AVFoundation使您可以轻松地在您的应用中播放HLS内容。
框架的回放类为您处理了大部分艰苦的工作,但您应该测试您的应用如何随着网络条件的变化而响应。
一个名为网络链接调节器的实用程序可以帮助进行此测试。

网络链接调节器可用于iOS、tvOS和macOS,并使您可以轻松模拟不同的网络条件(参见图6-1)。


图6-1 macOS中的网络链接调节器

在这里插入图片描述


此实用程序可让您轻松地在不同的网络性能预设之间进行交换机,以确保您的回放行为按预期工作。
在iOS和tvOS中,您可以在设置中的开发人员菜单下找到此实用程序。
在macOS中,您可以通过选择Xcode>打开开发人员工具>更多开发人员工具来下载此实用程序。
此选择会将您带到developer.apple.com的下载区域;该实用程序可作为“Xcode的附加工具”包的一部分使用。


2024-06-18(二)

  • 38
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ez_scope

请我喝杯伯爵奶茶~!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值