AVFoundation框架解析(五)—— 几个关键问题之AVFoundation探索(二)

版本记录

版本号 时间
V1.0 2017.08.30

前言

AVFoundation框架是ios中很重要的框架,所有与视频音频相关的软硬件控制都在这个框架里面,接下来这几篇就主要对这个框架进行介绍和讲解。感兴趣的可以看我上几篇。
1. AVFoundation框架解析(一)—— 基本概览
2. AVFoundation框架解析(二)—— 实现视频预览录制保存到相册
3. AVFoundation框架解析(三)—— 几个关键问题之关于框架的深度概括
4. AVFoundation框架解析(四)—— 几个关键问题之AVFoundation探索(一)

媒体文件播放

上一节描述的资源模型是播放用例的基石。 资源代表您想要播放的媒体,但只是图片的一部分。 本节讨论播放媒体所需的附加对象,并显示如何配置播放媒体,如下图所示。

1. AVPlayer

AVPlayer是驱动播放用例的中心类。 播放器是控制媒体资源的播放和定时的控制器对象。 您可以使用它来播放本地,逐渐下载或流媒体,并以编程方式控制其演示。

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

2. AVPlayerItem

AVAsset仅限于媒体的静态方面,如其持续时间或创建日期,并且本身不适合用AVPlayer播放 。要播放资源,您可以在AVPlayerItem中创建一个动态对象的实例。该对象模拟了AVPlayer播放资产的时间和呈现状态。使用AVPlayerItem的属性和方法,您可以在媒体中寻找不同的时间,确定其演示大小,识别其当前时间等等。

3. AVKit 和 AVPlayerLayer

AVPlayerAVPlayerItem是非可视对象,并且自己无法在屏幕上呈现资源的视频。您有两种不同的选择可供您在app中显示影片内容。

  • AVKit

    • 在iOS或tvOS中,演示您的视频内容的最佳方式是使用AVKit框架的AVPlayerViewController,或者在macOS中使用AVPlayerView。这些对象呈现视频内容,以及播放控件和其他媒体功能,为您提供全功能的播放体验。
  • AVPlayerLayer

    • 如果您为播放器构建自定义界面,则可以使用由AVFoundation提供的称为AVPlayerLayer的CALayer子类。播放器层可以设置为视图的背衬层,或者可以直接添加到层次结构。与AVPlayerViewAVPlayerViewController不同,AVPlayerLayer不提供任何播放控件,而只显示播放器的视觉内容。 建立播放传输控制来播放,暂停和seek媒体是由你决定的。

4. 设置播放对象

以下示例显示了为播放场景创建完整对象图所需的步骤。 该示例是为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
    }
 
}

创建播放对象后,您可以调用播放器的播放方式开始播放。

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


播放状态的观察

AVPlayer和AVPlayerItem是其状态频繁变化的动态对象。 您经常想采取行动来回应这些变化,而您的方式是通过使用键值观察(KVO)。使用KVO,一个对象可以注册以观察另一个对象的状态。 观察对象状态发生变化时,将通知状态变化的细节。使用KVO,您可以轻松地观察AVPlayer和AVPlayerItem的状态更改,并采取行动作为响应。

要观察的最重要的AVPlayerItem属性之一是其状态。 该状态指示播放器项目是否准备好播放并且通常可以使用。 当您首次创建播放器项目时,其状态具有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方法注册以使用addObserver:forKeyPath:options:context:method观察播放器的状态属性。 在将播放器项目与播放器关联之前调用此方法,以确保将所有状态更改捕获到项目的状态。

要通知状态更改,您将实现observeValueForKeyPath:ofObject:change:context:方法。 每当状态发生变化时,都会调用此方法,让您有机会采取一些措施。

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.
        }
    }
}

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


执行基于时间的操作

媒体播放是一种基于时间的活动,您可以在一定时间内以固定的速率提供定时媒体样本。 基于时间的操作,例如通过媒体进行搜索,在构建媒体播放app时发挥核心作用。 AVPlayer和AVPlayerItem的许多关键特性与控制媒体时序有关。 要学习有效地使用这些功能,您应该了解AVFoundation中如何表现时间。

几个Apple框架,包括AVFoundation的一些部分,表示时间作为表示秒的浮点NSTimeInterval值。 在许多情况下,这提供了一种思考和表达时间的自然方式,但是在执行定时的媒体操作时通常会遇到问题。 在使用媒体时保持采样准确的时序非常重要,而浮点不精确通常会导致定时漂移。 为了解决这些不精确,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添加一些扩展和运算符重载,从而使执行许多常见操作变得简单而自然。

1. 观察时间

您通常希望观察播放时间,以便您可以更新播放位置或以其他方式同步用户界面的状态。 早些时候,您看到了如何使用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:方法。 该方法使用一个包含定义边界时间的CMTime值的NSValue对象数组,一个串行调度队列和一个回调块。 以下示例显示如何定义每四分之一回放的边界时间。

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
   }
}

2.媒体 seek

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

您可以通过多种方式使用AVPlayer和AVPlayerItem的方法进行seek。 最常见的方法是使用播放器的seekToTime:方法,传递一个目标CMTime值,如下所示:

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

seekToTime:方法是一种快速seek的方便方法,但它更适合速度而不是精确度。 这意味着播放器移动的实际时间可能与您请求的时间不同。 如果您需要实现精确的搜索行为,请使用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:具有小或零值容差的方法可能会产生额外的解码延迟,这会影响app的seek行为。

了解如何表达和使用时间,观察播放器的时序,并通过媒体进行搜索,现在是时候来看看AVKit提供的更多平台特性。

后记

未完,待续~~~

推荐阅读更多精彩内容