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

版本記錄

版本號 時間
V1.0 2017.08.31

前言

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

Refining The User Experience

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


Presenting Chapter Markers - 展現章節的標記

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

章節標記是一種定時元數據。 這是前面幾篇文章其他部分所討論的相同的元數據,但不適用于整個資產,它僅適用于資產時間表內的時間范圍。 您可以使用chapterMetadataGroupsBestMatchingPreferredLanguages:chapterMetadataGroupsWithTitleLocale:containsItemsWithCommonKeys:方法來檢索資產的章節元數據。 在異步加載資源的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)
    }
}

通過相關數據轉換,您可以構建一個章節選擇界面,并使用該章節對象的時間值,使用播放器的seekToTime:方法來查找當前的演示。


Selecting Media Options - 選擇媒體選項

作為開發人員,您希望讓您的應用程序盡可能廣泛的受眾群體訪問。 擴展應用程序覆蓋面的一種方法是使用戶可以使用其母語,并為有聽力障礙或其他輔助功能需求的用戶提供支持。 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提供的視頻顯示中顯示相關文本。 選擇替代音頻或視頻選項將當前呈現的媒體替換為新選擇的媒體。

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


Working with the iOS Audio Environment - 處理ios的音頻環境

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

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

許多媒體播放應用程序的一個常見功能是在應用程序發送到后臺時繼續播放音頻。 這可能是用戶切換應用程序或鎖定設備的結果。 要啟用應用程序播放背景音頻,請首先配置應用的功能和音頻會話,如配置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
}

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

如果您的應用程序在后臺播放音頻,則應在“控制中心”和“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

3. Responding to Interruptions - 響應中斷

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

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

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

4. Responding to Route Changes - 路徑更改響應

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

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

AVPlayer知道您的應用程序的音頻會話,并適當地響應路由更改。 連接耳機時,播放會按預期方式繼續播放。 拔下耳機后,播放會自動暫停。 要觀察此AVPlayer行為,請使用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的當前路由確定當前路由的音頻輸出位置。 這將返回列出所有音頻會話的輸入和輸出的AVAudioSessionRouteDescription。 刪除設備后,從用戶信息字典中檢索先前路由的AVAudioSessionRouteDescription。 在這兩種情況下,您可以查詢路由描述的輸出,它返回一個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: ()
    }
}

后記

未完,待續~~~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內容