AVFoundation框架解析(八)—— 優(yōu)化用戶的播放體驗

版本記錄

版本號 時間
V1.0 2017.08.31

前言

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

Refining The User Experience

紹了AVFoundation的一些其他功能,可用于進一步自定義和優(yōu)化應(yīng)用的播放體驗。


Presenting Chapter Markers - 展現(xiàn)章節(jié)的標記

章節(jié)標記使用戶能夠快速瀏覽您的內(nèi)容。 如果在當前播放的資源中找到標記,tvOS和macOS中的AVPlayerViewController將自動顯示章節(jié)選擇界面。 只要您想創(chuàng)建自己的自定義章節(jié)選擇界面,您可以直接檢索此數(shù)據(jù)。

章節(jié)標記是一種定時元數(shù)據(jù)。 這是前面幾篇文章其他部分所討論的相同的元數(shù)據(jù),但不適用于整個資產(chǎn),它僅適用于資產(chǎn)時間表內(nèi)的時間范圍。 您可以使用chapterMetadataGroupsBestMatchingPreferredLanguages:chapterMetadataGroupsWithTitleLocale:containsItemsWithCommonKeys:方法來檢索資產(chǎn)的章節(jié)元數(shù)據(jù)。 在異步加載資源的availableChapterLocales鍵的值之后,這些方法可以被調(diào)用而不阻塞。

下面看一下代碼

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對象的數(shù)組,每個對象代表一個單獨的章標記。 AVTimedMetadataGroup對象由CMTimeRange組成,定義其元數(shù)據(jù)應(yīng)用的時間范圍,表示章節(jié)標題的AVMetadataItem對象數(shù)組,以及可選的縮略圖。 以下示例顯示如何將AVTimedMetadataGroup數(shù)據(jù)轉(zhuǎn)換為名為Chapter的自定義模型對象數(shù)組,以在應(yīng)用程序的視圖層中顯示:

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

通過相關(guān)數(shù)據(jù)轉(zhuǎn)換,您可以構(gòu)建一個章節(jié)選擇界面,并使用該章節(jié)對象的時間值,使用播放器的seekToTime:方法來查找當前的演示。


Selecting Media Options - 選擇媒體選項

作為開發(fā)人員,您希望讓您的應(yīng)用程序盡可能廣泛的受眾群體訪問。 擴展應(yīng)用程序覆蓋面的一種方法是使用戶可以使用其母語,并為有聽力障礙或其他輔助功能需求的用戶提供支持。 AVKit和AVFoundation可以通過提供內(nèi)容支持來顯示字幕和隱藏字幕以及選擇其他音頻和視頻軌跡來輕松處理這些問題。 如果您正在構(gòu)建自己的自定義播放器或想呈現(xiàn)自己的媒體選擇界面,則可以使用AVFoundation的AVMediaSelectionGroupAVMediaSelectionOption類提供的功能。

AVMediaSelectionOption模擬源媒體中包含的替代音頻,視頻或文本軌道。 媒體選項用于選擇替代攝像機角度,呈現(xiàn)用戶母語配音,或顯示字幕和隱藏字幕。 您可以通過異步加載和調(diào)用資產(chǎn)的availableMediaCharacteristicsWithMediaSelectionOptions屬性來確定可用的其他媒體演示文稿,該屬性返回一個字符串數(shù)組,指示可用的媒體特性。 返回的可能值是AVMediaCharacteristicAudible(替代音軌),AVMediaCharacteristicVisual(替代視頻軌道)和AVMediaCharacteristicLegible(字幕和隱藏式字幕)。

在您檢索到可用選項數(shù)組之后,您可以調(diào)用資產(chǎn)的mediaSelectionGroupForMediaCharacteristic:方法,傳遞所需的特征。 此方法返回相關(guān)聯(lián)的AVMediaSelectionGroup對象,如果不存在指定特征的組,則返回nil。

AVMediaSelectionGroup充當相互排斥的AVMediaSelectionOption對象集合的容器。 以下示例顯示如何檢索資產(chǎn)的媒體選擇組并顯示其可用選項:

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

包含音頻和字幕媒體選項的資源的輸出類似于以下內(nèi)容:

[AVMediaCharacteristicAudible]
  Option: English
  Option: Spanish

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

在為特定媒體特性檢索到AVMediaSelectionGroup對象并標識所需的AVMediaSelectionOption對象后,下一步是選擇它。 通過在激活的或者可用的AVPlayerItem上調(diào)用selectMediaOption:inMediaSelectionGroup:來選擇媒體選項。 例如,要呈現(xiàn)資產(chǎn)的西班牙語字幕選項,您可以選擇如下:

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提供的視頻顯示中顯示相關(guān)文本。 選擇替代音頻或視頻選項將當前呈現(xiàn)的媒體替換為新選擇的媒體。

注意:從iOS 7.0和OS X 10.9開始,AVPlayer根據(jù)用戶的系統(tǒng)偏好設(shè)置提供自動媒體選擇作為其默認行為。 要控制媒體選擇的顯示方式,請通過將播放器的applicMediaSelectionCriteriaAutomatically值設(shè)置為NO來禁用默認行為。


Working with the iOS Audio Environment - 處理ios的音頻環(huán)境

您可以使用iOS音頻會話API來定義應(yīng)用程序的一般音頻行為及其在其運行的設(shè)備的整體音頻環(huán)境中的作用。 以下部分介紹了管理和控制應(yīng)用程序音頻播放的其他方法以及如何響應(yīng)較大的iOS音頻環(huán)境中的更改。

1. Playing Background Audio - 播放背景音頻

許多媒體播放應(yīng)用程序的一個常見功能是在應(yīng)用程序發(fā)送到后臺時繼續(xù)播放音頻。 這可能是用戶切換應(yīng)用程序或鎖定設(shè)備的結(jié)果。 要啟用應(yīng)用程序播放背景音頻,請首先配置應(yīng)用的功能和音頻會話,如配置iOS和tvOS的音頻設(shè)置中所述。

如果您正在播放音頻資源(如MP3或M4A文件),則您的設(shè)置完成,您的應(yīng)用程序可以播放背景音頻。 如果您需要播放視頻資產(chǎn)的音頻部分,則需要執(zhí)行額外的步驟。 如果播放器的當前項目在設(shè)備的顯示屏上顯示視頻,則當應(yīng)用程序發(fā)送到后臺時,AVPlayer的播放會自動暫停。 如果要繼續(xù)播放音頻,請在進入后臺時斷開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
}

2. Controlling Background Audio - 控制背景音頻

如果您的應(yīng)用程序在后臺播放音頻,則應(yīng)在“控制中心”和“iOS鎖定”屏幕中支持遠程控制播放。 除了控制播放,您還應(yīng)該在這些界面中提供有關(guān)當前正在播放的內(nèi)容的有意義的信息。 要實現(xiàn)此功能,您可以使用MediaPlayer框架的MPRemoteCommandCenterMPNowPlayingInfoCenter類。

MPRemoteCommandCenter類可用于處理由外部附件和系統(tǒng)傳輸控件發(fā)送的遠程控制事件的對象。 它以MPRemoteCommand對象的形式定義了各種命令,您可以在其中附加自定義事件處理程序來控制應(yīng)用程序中的播放。 例如,為了遠程控制應(yīng)用程序的播放和暫停行為,您可以參考共享的命令中心,并為相應(yīng)的命令提供處理程序,如下所示:

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的處理程序。 您給此方法的回調(diào)塊要求您返回MPRemoteCommandHandlerStatus值,指示命令是成功還是失敗。

配置遠程命令處理程序后,下一步是提供元數(shù)據(jù)以顯示在iOS鎖定屏幕和控制中心的傳輸區(qū)域中。 您使用MPMediaItemMPNowPlayingInfoCenter定義的鍵提供元數(shù)據(jù)字典,并在MPNowPlayingInfoCenter的默認實例上設(shè)置該字典。 以下示例說明如何為當前呈現(xiàn)的媒體設(shè)置標題和圖稿以及播放時間值:

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
}

該示例通過動態(tài)時序作為nowPlayingInfo字典的一部分,它允許您在遠程控制接口中呈現(xiàn)播放器時間和進度。 當您這樣做時,請在應(yīng)用程序進入后臺提供此時間的最新快照。 如果以重大的方式媒體信息或時間更改,您還可以在應(yīng)用程序在后臺更新nowPlayingInfo

3. Responding to Interruptions - 響應(yīng)中斷

中斷是iOS用戶體驗的常見部分。 例如,如果您在“視頻”應(yīng)用中觀看電影,并且您收到電話或FaceTime請求,會發(fā)生什么。 在這種情況下,您的電影的音頻會快速淡出,播放暫停,鈴聲的聲音會消失。如果您拒絕通話或請求,則會將控制權(quán)返回給“視頻”應(yīng)用程序,并重新開始播放電影的音頻 在這個行為的中心是你的應(yīng)用程序的AVAudioSession。 隨著中斷的開始和結(jié)束,它會通知任何已注冊的觀察者,以便他們可以采取適當?shù)拇胧?AVPlayer知道您的音頻會話,并響應(yīng)AVAudioSession中斷事件自動暫停和恢復播放。 要觀察此AVPlayer的行為,請使用鍵值觀察(KVO)播放器的速率屬性,以便您可以在播放器暫停并恢復響應(yīng)中斷時更新用戶界面。

您還可以直接觀察AVAudioSession發(fā)布的中斷通知。 如果您想知道播放是否由于中斷或其他原因(例如路由更改)而暫停,這可能很有用。 要觀察音頻中斷,首先注冊以觀察AVAudioSessionInterruptNotification類型的通知。

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

發(fā)布的NSNotification對象包含一個填充的userInfo字典,提供中斷的詳細信息。 您可以通過從userInfo字典中檢索AVAudioSessionInterruptionType值來確定中斷的類型。 中斷類型表示中斷是開始還是已經(jīng)結(jié)束。

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字典包含AVAudioSessionInterruptOptions值,用于確定播放是否應(yīng)自動恢復。

4. Responding to Route Changes - 路徑更改響應(yīng)

AVAudioSession處理的重要職責是管理音頻路由更改。 當音頻輸入或輸出添加到iOS設(shè)備或從iOS設(shè)備中刪除時,會發(fā)生路由更改。 發(fā)生路由更改的原因有很多,其中包括用戶插入一副耳機,連接藍牙LE耳機或拔下USB音頻接口。 發(fā)生這些更改時,AVAudioSession會相應(yīng)地重新路由音頻信號,并向任何已注冊的觀察者廣播包含更改詳情的通知。

當用戶插入或刪除一副耳機時,會發(fā)生與路線變化相關(guān)的重要要求(請參閱iOS人機接口指南中的聲音)。 當用戶連接一對有線或無線耳機時,它們隱含地指示音頻播放應(yīng)該繼續(xù),但是是私有的。 他們期待一個正在播放媒體的應(yīng)用程序繼續(xù)播放,而不會暫停。 當用戶拔掉耳機時,他們不想自動分享他們正在聽的內(nèi)容。 應(yīng)用程序應(yīng)遵守隱私隱私請求,并在耳機被移除時自動暫停播放。

AVPlayer知道您的應(yīng)用程序的音頻會話,并適當?shù)仨憫?yīng)路由更改。 連接耳機時,播放會按預期方式繼續(xù)播放。 拔下耳機后,播放會自動暫停。 要觀察此AVPlayer行為,請使用KVO播放器的速率屬性,以便您可以在播放器暫停以響應(yīng)音頻路由更改時更新用戶界面。

您還可以直接觀察AVAudioSession發(fā)布的任何路線更改通知。 如果您希望在用戶連接或刪除耳機時收到通知,這樣您可以在播放器界面中顯示圖標或信息,這可能很有用。 要觀察音頻路由更改,您首先注冊以觀察AVAudioSessionRouteChangeNotification類型的通知。

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

發(fā)布的NSNotification對象包含一個填充的userInfo字典,提供路由更改的詳細信息。 您可以通過從userInfo字典中檢索AVAudioSessionRouteChangeReason值來確定此更改的原因。 當連接一個新設(shè)備時,原因是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: ()
    }
}

當新設(shè)備可用時,您要求AVAudioSession的當前路由確定當前路由的音頻輸出位置。 這將返回列出所有音頻會話的輸入和輸出的AVAudioSessionRouteDescription。 刪除設(shè)備后,從用戶信息字典中檢索先前路由的AVAudioSessionRouteDescription。 在這兩種情況下,您可以查詢路由描述的輸出,它返回一個AVAudioSessionPortDescription對象數(shù)組,提供音頻輸出路由的詳細信息。

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: ()
    }
}

后記

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

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

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

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