AVFoundation框架解析(五)—— 幾個關(guān)鍵問題之AVFoundation探索(二)

版本記錄

版本號 時間
V1.0 2017.08.30

前言

AVFoundation框架是ios中很重要的框架,所有與視頻音頻相關(guān)的軟硬件控制都在這個框架里面,接下來這幾篇就主要對這個框架進(jìn)行介紹和講解。感興趣的可以看我上幾篇。
1. AVFoundation框架解析(一)—— 基本概覽
2. AVFoundation框架解析(二)—— 實(shí)現(xiàn)視頻預(yù)覽錄制保存到相冊
3. AVFoundation框架解析(三)—— 幾個關(guān)鍵問題之關(guān)于框架的深度概括
4. AVFoundation框架解析(四)—— 幾個關(guān)鍵問題之AVFoundation探索(一)

媒體文件播放

上一節(jié)描述的資源模型是播放用例的基石。 資源代表您想要播放的媒體,但只是圖片的一部分。 本節(jié)討論播放媒體所需的附加對象,并顯示如何配置播放媒體,如下圖所示。

1. AVPlayer

AVPlayer是驅(qū)動播放用例的中心類。 播放器是控制媒體資源的播放和定時的控制器對象。 您可以使用它來播放本地,逐漸下載或流媒體,并以編程方式控制其演示。

注意:您一次使用AVPlayer播放單個媒體資源。 該框架還提供了AVPlayer的一個子類,稱為AVQueuePlayer,用于創(chuàng)建和管理要順序播放的媒體資源隊列。

2. AVPlayerItem

AVAsset僅限于媒體的靜態(tài)方面,如其持續(xù)時間或創(chuàng)建日期,并且本身不適合用AVPlayer播放 。要播放資源,您可以在AVPlayerItem中創(chuàng)建一個動態(tài)對象的實(shí)例。該對象模擬了AVPlayer播放資產(chǎn)的時間和呈現(xiàn)狀態(tài)。使用AVPlayerItem的屬性和方法,您可以在媒體中尋找不同的時間,確定其演示大小,識別其當(dāng)前時間等等。

3. AVKit 和 AVPlayerLayer

AVPlayerAVPlayerItem是非可視對象,并且自己無法在屏幕上呈現(xiàn)資源的視頻。您有兩種不同的選擇可供您在app中顯示影片內(nèi)容。

  • AVKit

    • 在iOS或tvOS中,演示您的視頻內(nèi)容的最佳方式是使用AVKit框架的AVPlayerViewController,或者在macOS中使用AVPlayerView。這些對象呈現(xiàn)視頻內(nèi)容,以及播放控件和其他媒體功能,為您提供全功能的播放體驗(yàn)。
  • AVPlayerLayer

    • 如果您為播放器構(gòu)建自定義界面,則可以使用由AVFoundation提供的稱為AVPlayerLayer的CALayer子類。播放器層可以設(shè)置為視圖的背襯層,或者可以直接添加到層次結(jié)構(gòu)。與AVPlayerViewAVPlayerViewController不同,AVPlayerLayer不提供任何播放控件,而只顯示播放器的視覺內(nèi)容。 建立播放傳輸控制來播放,暫停和seek媒體是由你決定的。

4. 設(shè)置播放對象

以下示例顯示了為播放場景創(chuàng)建完整對象圖所需的步驟。 該示例是為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
    }
 
}

創(chuàng)建播放對象后,您可以調(diào)用播放器的播放方式開始播放。

AVPlayerAVPlayerItem提供了播放器項(xiàng)目的媒體可以使用時控制播放的各種方式。 下一步是查看如何觀察播放對象的狀態(tài),以便您可以確定播放準(zhǔn)備狀態(tài)。


播放狀態(tài)的觀察

AVPlayer和AVPlayerItem是其狀態(tài)頻繁變化的動態(tài)對象。 您經(jīng)常想采取行動來回應(yīng)這些變化,而您的方式是通過使用鍵值觀察(KVO)。使用KVO,一個對象可以注冊以觀察另一個對象的狀態(tài)。 觀察對象狀態(tài)發(fā)生變化時,將通知狀態(tài)變化的細(xì)節(jié)。使用KVO,您可以輕松地觀察AVPlayer和AVPlayerItem的狀態(tài)更改,并采取行動作為響應(yīng)。

要觀察的最重要的AVPlayerItem屬性之一是其狀態(tài)。 該狀態(tài)指示播放器項(xiàng)目是否準(zhǔn)備好播放并且通??梢允褂?。 當(dāng)您首次創(chuàng)建播放器項(xiàng)目時,其狀態(tài)具有AVPlayerItemStatusUnknown的值,這意味著其媒體尚未加載或入隊進(jìn)行播放。當(dāng)您將播放器項(xiàng)目與AVPlayer相關(guān)聯(lián)時,它會立即開始對項(xiàng)目的媒體進(jìn)行排隊并準(zhǔn)備播放。 當(dāng)其狀態(tài)更改為AVPlayerItemStatusReadyToPlay時,播放器項(xiàng)目就可以使用了。 以下示例顯示如何觀察此狀態(tài)更改。

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觀察播放器的狀態(tài)屬性。 在將播放器項(xiàng)目與播放器關(guān)聯(lián)之前調(diào)用此方法,以確保將所有狀態(tài)更改捕獲到項(xiàng)目的狀態(tài)。

要通知狀態(tài)更改,您將實(shí)現(xiàn)observeValueForKeyPath:ofObject:change:context:方法。 每當(dāng)狀態(tài)發(fā)生變化時,都會調(diào)用此方法,讓您有機(jī)會采取一些措施。

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

該示例從更改字典中檢索新的狀態(tài)值并切換其值。 如果播放器的狀態(tài)為AVPlayerItemStatusReadyToPlay,則可以使用。 如果在嘗試加載播放器項(xiàng)目的媒體時遇到問題,則狀態(tài)為AVPlayerItemStatusFailed。 如果發(fā)生故障,您可以通過查詢播放器項(xiàng)的錯誤屬性來檢索提供故障詳細(xì)信息的NSError對象。


執(zhí)行基于時間的操作

媒體播放是一種基于時間的活動,您可以在一定時間內(nèi)以固定的速率提供定時媒體樣本。 基于時間的操作,例如通過媒體進(jìn)行搜索,在構(gòu)建媒體播放app時發(fā)揮核心作用。 AVPlayer和AVPlayerItem的許多關(guān)鍵特性與控制媒體時序有關(guān)。 要學(xué)習(xí)有效地使用這些功能,您應(yīng)該了解AVFoundation中如何表現(xiàn)時間。

幾個Apple框架,包括AVFoundation的一些部分,表示時間作為表示秒的浮點(diǎn)NSTimeInterval值。 在許多情況下,這提供了一種思考和表達(dá)時間的自然方式,但是在執(zhí)行定時的媒體操作時通常會遇到問題。 在使用媒體時保持采樣準(zhǔn)確的時序非常重要,而浮點(diǎn)不精確通常會導(dǎo)致定時漂移。 為了解決這些不精確,AVFoundation表示使用Core Media框架的CMTime數(shù)據(jù)類型的時間。

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

該結(jié)構(gòu)定義了時間的理性或分?jǐn)?shù)表示。 CMTime定義的兩個最重要的字段是它的值和時間尺度。 CMTimeValue是定義分?jǐn)?shù)時間分子的64位整數(shù),CMTimeScale是一個定義分母的32位整數(shù)。 這個結(jié)構(gòu)使得很容易表示以媒體幀率或采樣率表示的時間。

// 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提供了許多創(chuàng)建CMTime值的方法,并對它們進(jìn)行算術(shù),比較,驗(yàn)證和轉(zhuǎn)換操作。 如果您使用Swift,Core Media還會向CMTime添加一些擴(kuò)展和運(yùn)算符重載,從而使執(zhí)行許多常見操作變得簡單而自然。

1. 觀察時間

您通常希望觀察播放時間,以便您可以更新播放位置或以其他方式同步用戶界面的狀態(tài)。 早些時候,您看到了如何使用KVO觀察播放對象的狀態(tài)。KVO適用于一般狀態(tài)觀察,但不是觀察播放器時間的正確選擇,因?yàn)樗贿m合觀察連續(xù)狀態(tài)變化。 相反,AVPlayer提供了兩種不同的方式來觀察播放器時間變化:定期觀察和邊界觀察。

定期觀察

您可以按照定期,周期性的間隔觀察時間。 如果您正在構(gòu)建自定義播放器,定期觀察的最常見用例是更新用戶界面中的時間顯示。

要觀察周期性時間,您可以使用播放器的addPeriodicTimeObserverForInterval:queue:usingBlock:方法。 該方法采用表示時間間隔的CMTime,串行調(diào)度隊列和在指定時間間隔內(nèi)調(diào)用的回調(diào)塊。 以下示例顯示如何在正常播放期間每半秒設(shè)置一個被調(diào)用的塊。

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

邊界觀察

另一種方式可以觀察時間是邊界。 您可以在媒體時間軸內(nèi)定義各種感興趣的興趣點(diǎn),并且框架會在正常播放期間隨著時間的推移而呼叫您。 邊界觀測比定期觀察使用的頻率低,但在某些情況下仍然可以證明有用。 例如,如果您正在呈現(xiàn)無播放控件的視頻,則可能會使用邊界觀察,并希望同時顯示或顯示補(bǔ)充內(nèi)容的元素同步時間。

要觀察邊界時間,您可以使用播放器的addBoundaryTimeObserverForTimes:queue:usingBlock:方法。 該方法使用一個包含定義邊界時間的CMTime值的NSValue對象數(shù)組,一個串行調(diào)度隊列和一個回調(diào)塊。 以下示例顯示如何定義每四分之一回放的邊界時間。

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

除了正常的線性播放之外,用戶還希望能夠以非線性方式尋找或刷新以快速獲得媒體內(nèi)的各種興趣點(diǎn)。 AVKit自動為您提供刷新控件(如果媒體支持),但如果您正在構(gòu)建自定義播放器,則需要自己構(gòu)建此功能。 即使在使用AVKit的情況下,您仍然可能希望提供補(bǔ)充用戶界面,例如表視圖或集合視圖,可讓用戶快速跳到媒體中的各個位置。

您可以通過多種方式使用AVPlayer和AVPlayerItem的方法進(jìn)行seek。 最常見的方法是使用播放器的seekToTime:方法,傳遞一個目標(biāo)CMTime值,如下所示:

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

seekToTime:方法是一種快速seek的方便方法,但它更適合速度而不是精確度。 這意味著播放器移動的實(shí)際時間可能與您請求的時間不同。 如果您需要實(shí)現(xiàn)精確的搜索行為,請使用seekToTime:toleranceBefore:toleranceAfter:方法,它允許您指定與目標(biāo)時間(前后)的容忍偏差量。 如果您需要提供樣本精確的搜索行為,您可以指出允許零容限。

// 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)

這里要注意的是:調(diào)用seekToTime:toleranceBefore:toleranceAfter:具有小或零值容差的方法可能會產(chǎn)生額外的解碼延遲,這會影響app的seek行為。

了解如何表達(dá)和使用時間,觀察播放器的時序,并通過媒體進(jìn)行搜索,現(xiàn)在是時候來看看AVKit提供的更多平臺特性。

后記

未完,待續(xù)~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容