HTTP Live Streaming (HLS))是向播放应用提供媒体的理想方式。使用HLS,您可以以不同的比特率提供多个媒体流,并且您的播放客户端会随着网络带宽的变化动态选择适当的流。这可确保您始终根据用户当前的网络状况提供最优质的内容。本章介绍如何在播放应用中利用HLS的独特功能。
从iOS 10开始,您可以使用AVFoundation将HTTP Live Streaming资源下载到iOS设备。这项新功能允许用户在可以访问快速,可靠的网络时在其设备上下载和存储HLS电影,并在以后无需网络连接即可观看。通过引入此功能,HLS可以最小化不一致的网络可用性对用户体验的影响。
使用AVAssetDownloadURLSession来实现资源下载功能,他是NSURLSession的子类,主要用来创建和执行资源下载任务。
func setupAssetDownload() {
// Create new background session configuration.
configuration = URLSessionConfiguration.background(withIdentifier: downloadIdentifier)
// Create a new AVAssetDownloadURLSession with background configuration, delegate, and queue
downloadSession = AVAssetDownloadURLSession(configuration: configuration,
assetDownloadDelegate: self,
delegateQueue: OperationQueue.main)
}
配置完URLSession以后,创建AVAssetDownloadTask实例来开始下载任务
func setupAssetDownload() {
...
// Previous AVAssetDownloadURLSession configuration
...
let url = // HLS Asset URL
let asset = AVURLAsset(url: url)
// Create new AVAssetDownloadTask for the desired asset
let downloadTask = downloadSession.makeAssetDownloadTask(asset: asset,
assetTitle: assetTitle,
assetArtworkData: nil,
options: nil)
// Start task and begin download
downloadTask?.resume()
}
options选型可以传入一个Dictionary用来选择不同的比特和媒体选型。如果为Nil,会默认选择下载最高质量的音视频内容。由于可以在后台实现下载,当App被终止时,下次启动需要恢复上次的下载任务,此时可以通过一个上次任务在 NSURLSessionConfiguration中
配置的identifier 来新创建一个NSURLSessionConfiguration对象,重新创建
AVAssetDownloadURLSession对象,使用session的getTasksWithCompletionHandler:方法来获取终止的任务,来恢复下载任务。
func restorePendingDownloads() {
// Create session configuration with ORIGINAL download identifier
configuration = URLSessionConfiguration.background(withIdentifier: downloadIdentifier)
// Create a new AVAssetDownloadURLSession
downloadSession = AVAssetDownloadURLSession(configuration: configuration,
assetDownloadDelegate: self,
delegateQueue: OperationQueue.main)
// Grab all the pending tasks associated with the downloadSession
downloadSession.getAllTasks { tasksArray in
// For each task, restore the state in the app
for task in tasksArray {
guard let downloadTask = task as? AVAssetDownloadTask else { break }
// Restore asset, progress indicators, state, etc...
let asset = downloadTask.urlAsset
}
}
}
可以通过代理方法监控下载的进度:
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didLoad timeRange: CMTimeRange, totalTimeRangesLoaded loadedTimeRanges: [NSValue], timeRangeExpectedToLoad: CMTimeRange) {
var percentComplete = 0.0
// Iterate through the loaded time ranges
for value in loadedTimeRanges {
// Unwrap the CMTimeRange from the NSValue
let loadedTimeRange = value.timeRangeValue
// Calculate the percentage of the total expected asset duration
percentComplete += loadedTimeRange.duration.seconds / timeRangeExpectedToLoad.duration.seconds
}
percentComplete *= 100
// Update UI state: post notification, update KVO state, invoke callback, etc.
}
当下载完成时或下载失败了会调用session的代理方法,可以在此方法中设置文件保存位置,与NSURLSessionDownloadDelegate中URLSession:downloadTask:didFinishDownloadingToURL:方法不同的时,用户不应该修该下载资源的位置,它是在系统的控制之下的,传入的URL,代表了下载资源在磁盘上的最终位置。
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
// Do not move the asset from the download location
UserDefaults.standard.set(location.relativePath, forKey: "assetPath")
}
你可以更新下载的资源,例如以前服务器并不提供高清的资源或者一些 资源选项。当服务器有资源更新的时候 session的代理方法URLSession:assetDownloadTask:didResolveMediaSelection:会被调用,保存AVMediaSelection对象,用于随后的下载任务
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didResolve resolvedMediaSelection: AVMediaSelection) {
// Store away for later retrieval when main asset download is complete
// mediaSelectionMap is defined as: [AVAssetDownloadTask : AVMediaSelection]()
mediaSelectionMap[assetDownloadTask] = resolvedMediaSelection
}
通过以下方法获取未在本地缓存的内容
func nextMediaSelection(_ asset: AVURLAsset) -> (mediaSelectionGroup: AVMediaSelectionGroup?,
mediaSelectionOption: AVMediaSelectionOption?) {
// If the specified asset has not associated asset cache, return nil tuple
guard let assetCache = asset.assetCache else {
return (nil, nil)
}
// Iterate through audible and legible characteristics to find associated groups for asset
for characteristic in [AVMediaCharacteristicAudible, AVMediaCharacteristicLegible] {
if let mediaSelectionGroup = asset.mediaSelectionGroup(forMediaCharacteristic: characteristic) {
// Determine which offline media selection options exist for this asset
let savedOptions = assetCache.mediaSelectionOptions(in: mediaSelectionGroup)
// If there are still media options to download...
if savedOptions.count < mediaSelectionGroup.options.count {
for option in mediaSelectionGroup.options {
if !savedOptions.contains(option) {
// This option hasn't been downloaded. Return it so it can be.
return (mediaSelectionGroup, option)
}
}
}
}
}
// At this point all media options have been downloaded.
return (nil, nil)
}
下面代码检查并下载所有的可选媒体资源
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard error == nil else { return }
guard let task = task as? AVAssetDownloadTask else { return }
// Determine the next available AVMediaSelectionOption to download
let mediaSelectionPair = nextMediaSelection(task.urlAsset)
// If an undownloaded media selection option exists in the group...
if let group = mediaSelectionPair.mediaSelectionGroup,
option = mediaSelectionPair.mediaSelectionOption {
// Exit early if no corresponding AVMediaSelection exists for the current task
guard let originalMediaSelection = mediaSelectionMap[task] else { return }
// Create a mutable copy and select the media selection option in the media selection group
let mediaSelection = originalMediaSelection.mutableCopy() as! AVMutableMediaSelection
mediaSelection.select(option, in: group)
// Create a new download task with this media selection in its options
let options = [AVAssetDownloadTaskMediaSelectionKey: mediaSelection]
let task = downloadSession.makeAssetDownloadTask(asset: task.urlAsset,
assetTitle: assetTitle,
assetArtworkData: nil,
options: options)
// Start media selection download
task?.resume()
} else {
// All media selection downloads complete
}
}
一旦开始下载,就可以开始同步播放
func downloadAndPlayAsset(_ asset: AVURLAsset) {
// Create new AVAssetDownloadTask for the desired asset
// Passing a nil options value indicates the highest available bitrate should be downloaded
let downloadTask = downloadSession.makeAssetDownloadTask(asset: asset,
assetTitle: assetTitle,
assetArtworkData: nil,
options: nil)!
// Start task
downloadTask.resume()
// Create standard playback items and begin playback
let playerItem = AVPlayerItem(asset: downloadTask.urlAsset)
player = AVPlayer(playerItem: playerItem)
player.play()
}
也可以播放下载完成的离线资源,使用在下载完成后指定为资源位置
func playOfflineAsset() {
guard let assetPath = UserDefaults.standard.value(forKey: "assetPath") as? String else {
// Present Error: No offline version of this asset available
return
}
let baseURL = URL(fileURLWithPath: NSHomeDirectory())
let assetURL = baseURL.appendingPathComponent(assetPath)
let asset = AVURLAsset(url: assetURL)
if let cache = asset.assetCache, cache.isPlayableOffline {
// Set up player item and player and begin playback
} else {
// Present Error: No playable version of this asset exists offline
}
}
删除文件:
func deleteOfflineAsset() {
do {
let userDefaults = UserDefaults.standard
if let assetPath = userDefaults.value(forKey: "assetPath") as? String {
let baseURL = URL(fileURLWithPath: NSHomeDirectory())
let assetURL = baseURL.appendingPathComponent(assetPath)
try FileManager.default.removeItem(at: assetURL)
userDefaults.removeObject(forKey: "assetPath")
}
} catch {
print("An error occured deleting offline asset: \(error)")
}
}
AVPlayerItem的accessLog
and errorLog属性用来读取日志文件。