版本記錄
版本號 | 時間 |
---|---|
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的AVMediaSelectionGroup
和AVMediaSelectionOption
類提供的功能。
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)
}
}
選擇媒體選項可使其立即可用于演示。 選擇字幕或閉路字幕選項會在AVPlayerViewController
,AVPlayerView
和AVPlayerLayer
提供的視頻顯示中顯示相關文本。 選擇替代音頻或視頻選項將當前呈現的媒體替換為新選擇的媒體。
注意:從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框架的MPRemoteCommandCenter
和MPNowPlayingInfoCenter
類。
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:
方法添加命令中心的playCommand
和pauseCommand
的處理程序。 您給此方法的回調塊要求您返回MPRemoteCommandHandlerStatus
值,指示命令是成功還是失敗。
配置遠程命令處理程序后,下一步是提供元數據以顯示在iOS鎖定屏幕和控制中心的傳輸區域中。 您使用MPMediaItem
和MPNowPlayingInfoCenter
定義的鍵提供元數據字典,并在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: ()
}
}
后記
未完,待續~~~