我正在尝试下载HLS视频。我使用来自Apple资源project的代码。
问题在于电影启动xml文件中的“仅第一个正在下载”并且在标记中为true。要下载其余的流,我必须播放视频。当我播放视频时,会将更多流添加到boot.xml文件中。
出于保密原因,我更改了ID为ID的ID和链接为link的链接。
boot.xml文件播放前
<?xml version="1.0" encoding="UTF-8"?>
<HLSMoviePackage xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://apple.com/IMG/Schemas/HLSMoviePackage" xsi:schemaLocation="http://apple.com/IMG/Schemas/HLSMoviePackage /System/Library/Schemas/HLSMoviePackage.xsd">
<Version>1.0</Version>
<HLSMoviePackageType>PersistedStore</HLSMoviePackageType>
<Streams>
<Stream ID="0-IDIDIDIDIDIDIDID-IDIDIDID" Path="0-IDIDIDIDIDIDIDID-00000" NetworkURL="https://link">
<Complete>YES</Complete>
</Stream></Streams>
<MasterPlaylist>
<NetworkURL>https://link</NetworkURL>
</MasterPlaylist>
<DataItems Directory="Data">
<DataItem><ID>IDIDIDID-IDIDIDID-IDIDIDID-IDIDIDID-IDIDIDID</ID><Category>Playlist</Category>
<Name>master.m3u8</Name>
<DataPath>Playlist-master.m3u8-IDIDIDID-IDIDIDID-IDIDIDID-IDIDIDID-IDIDIDID.data</DataPath>
<Role>Master</Role>
</DataItem></DataItems>
</HLSMoviePackage>
boot.xml文件播放后
<?xml version="1.0" encoding="UTF-8"?>
<HLSMoviePackage xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://apple.com/IMG/Schemas/HLSMoviePackage" xsi:schemaLocation="http://apple.com/IMG/Schemas/HLSMoviePackage /System/Library/Schemas/HLSMoviePackage.xsd">
<Version>1.0</Version>
<HLSMoviePackageType>PersistedStore</HLSMoviePackageType>
<Streams><Stream ID="0-IDIDIDIDIDIDIDID-00000" Path="0-IDIDIDIDIDIDIDIDIDIDIDID-451000" NetworkURL="https://link">
<Complete>YES</Complete>
</Stream><Stream ID="0-IDIDIDIDIDIDIDIDIDIDIDID-726000" Path="0-IDIDIDIDIDIDIDIDIDIDIDID-726000" NetworkURL="https://link">
<Complete>NO</Complete>
</Stream><Stream ID="0-IDIDIDIDIDIDIDIDIDIDIDID-451000" Path="0-IDIDIDIDIDIDIDIDIDIDIDID-451000" NetworkURL="https://link/">
<Complete>NO</Complete>
</Stream><Stream ID="0-IDIDIDIDIDIDIDIDIDIDIDID-1723000" Path="0-IDIDIDIDIDIDIDIDIDIDIDID-1723000" NetworkURL="https://link">
<Complete>NO</Complete>
</Stream><Stream ID="0-IDIDIDIDIDIDIDIDIDIDIDID-3949000" Path="0-IDIDIDIDIDIDIDIDIDIDIDID-3949000" NetworkURL="https://link">
<Complete>NO</Complete>
</Stream><Stream ID="1-BXAW3XDNMB5PM5XNWROUH47VBIN7CFPC-0" Path="1-IDIDIDIDIDIDIDIDIDIDIDID-0" NetworkURL="https://link/">
<Complete>NO</Complete></Stream></Streams>
<MasterPlaylist>
<NetworkURL>https://link2</NetworkURL></MasterPlaylist><DataItems Directory="Data">
<DataItem>
<ID>IDIDIDID-IDIDIDID-IDIDIDID-IDIDIDID-IDIDIDID</ID><Category>Playlist</Category>
<Name>master.m3u8</Name><DataPath>Playlist-master.m3u8-IDIDIDID-IDIDIDID-IDIDIDID-IDIDIDID-IDIDIDID.data</DataPath>
<Role>Master</Role>
</DataItem></DataItems></HLSMoviePackage>
完整的下载类
import Foundation
import AVFoundation
/// - Tag: AssetPersistenceManager
class AssetPersistenceManager: NSObject {
// MARK: Properties
/// Singleton for AssetPersistenceManager.
static let sharedManager = AssetPersistenceManager()
/// Internal Bool used to track if the AssetPersistenceManager finished restoring its state.
private var didRestorePersistenceManager = false
/// The AVAssetDownloadURLSession to use for managing AVAssetDownloadTasks.
fileprivate var assetDownloadURLSession: AVAssetDownloadURLSession!
/// Internal map of AVAggregateAssetDownloadTask to its corresponding Asset.
fileprivate var activeDownloadsMap = [AVAggregateAssetDownloadTask: Asset]()
/// Internal map of AVAggregateAssetDownloadTask to download URL.
fileprivate var willDownloadToUrlMap = [AVAggregateAssetDownloadTask: URL]()
// MARK: Intialization
override private init() {
super.init()
// Create the configuration for the AVAssetDownloadURLSession.
let backgroundConfiguration = URLSessionConfiguration.background(withIdentifier: "AAPL-Identifier")
// Create the AVAssetDownloadURLSession using the configuration.
assetDownloadURLSession =
AVAssetDownloadURLSession(configuration: backgroundConfiguration,
assetDownloadDelegate: self, delegateQueue: OperationQueue.main)
}
/// Restores the Application state by getting all the AVAssetDownloadTasks and restoring their Asset structs.
func restorePersistenceManager() {
guard !didRestorePersistenceManager else { return }
didRestorePersistenceManager = true
// Grab all the tasks associated with the assetDownloadURLSession
assetDownloadURLSession.getAllTasks { tasksArray in
// For each task, restore the state in the app by recreating Asset structs and reusing existing AVURLAsset objects.
for task in tasksArray {
guard let assetDownloadTask = task as? AVAggregateAssetDownloadTask, let assetName = task.taskDescription else { break }
let stream = StreamListManager.shared.stream(withName: assetName)
let urlAsset = assetDownloadTask.urlAsset
let asset = Asset(stream: stream, urlAsset: urlAsset)
self.activeDownloadsMap[assetDownloadTask] = asset
}
NotificationCenter.default.post(name: .AssetPersistenceManagerDidRestoreState, object: nil)
}
}
/// Triggers the initial AVAssetDownloadTask for a given Asset.
/// - Tag: DownloadStream
func downloadStream(for asset: Asset) {
// Get the default media selections for the asset's media selection groups.
let allMediaSelections = asset.urlAsset.allMediaSelections
/*
Creates and initializes an AVAggregateAssetDownloadTask to download multiple AVMediaSelections
on an AVURLAsset.
For the initial download, we ask the URLSession for an AVAssetDownloadTask with a minimum bitrate
corresponding with one of the lower bitrate variants in the asset.
*/
guard let task =
assetDownloadURLSession.aggregateAssetDownloadTask(with: asset.urlAsset,
mediaSelections: allMediaSelections,
assetTitle: asset.stream.name,
assetArtworkData: nil,
options:
[AVAssetDownloadTaskMinimumRequiredMediaBitrateKey: 265_000]) else { return }
// To better track the AVAssetDownloadTask, set the taskDescription to something unique for the sample.
task.taskDescription = asset.stream.name
activeDownloadsMap[task] = asset
task.resume()
var userInfo = [String: Any]()
userInfo[Asset.Keys.name] = asset.stream.name
userInfo[Asset.Keys.downloadState] = Asset.DownloadState.downloading.rawValue
userInfo[Asset.Keys.downloadSelectionDisplayName] = displayNamesForSelectedMediaOptions(allMediaSelections.first!)
NotificationCenter.default.post(name: .AssetDownloadStateChanged, object: nil, userInfo: userInfo)
}
func getIfAssitIsDownloaded(withID: String) -> Bool {
guard let asset = localAssetForStream(withName: withID) else {
return false
}
return true
}
/// Returns an Asset given a specific name if that Asset is associated with an active download.
func assetForStream(withName name: String) -> Asset? {
var asset: Asset?
for (_, assetValue) in activeDownloadsMap where name == assetValue.stream.name {
asset = assetValue
break
}
return asset
}
/// Returns an Asset pointing to a file on disk if it exists.
func localAssetForStream(withName name: String) -> Asset? {
let userDefaults = UserDefaults.standard
guard let localFileLocation = userDefaults.value(forKey: name) as? Data else { return nil }
var asset: Asset?
var bookmarkDataIsStale = false
do {
let url = (try URL(resolvingBookmarkData: localFileLocation,
bookmarkDataIsStale: &bookmarkDataIsStale))!
if bookmarkDataIsStale {
//fatalError("Bookmark data is stale!")
}
let urlAsset = AVURLAsset(url: url)
let stream = StreamListManager.shared.stream(withName: name)
asset = Asset(stream: stream, urlAsset: urlAsset)
return asset
} catch {
fatalError("Failed to create URL from bookmark with error: \(error)")
}
}
/// Returns the current download state for a given Asset.
func downloadState(for asset: Asset) -> Asset.DownloadState {
// Check if there is a file URL stored for this asset.
if let localFileLocation = localAssetForStream(withName: asset.stream.name)?.urlAsset.url {
// Check if the file exists on disk
if FileManager.default.fileExists(atPath: localFileLocation.path) {
return .downloaded
}
}
// Check if there are any active downloads in flight.
for (_, assetValue) in activeDownloadsMap where asset.stream.name == assetValue.stream.name {
return .downloading
}
return .notDownloaded
}
/// Deletes an Asset on disk if possible.
/// - Tag: RemoveDownload
func deleteAsset(_ asset: Asset) {
let userDefaults = UserDefaults.standard
do {
if let localFileLocation = localAssetForStream(withName: asset.stream.name)?.urlAsset.url {
try FileManager.default.removeItem(at: localFileLocation)
userDefaults.removeObject(forKey: asset.stream.name)
StreamListManager.shared.deleteDownloadedStreamFromCoreData(id: asset.stream.name) {
APIDownloadManagerCoreDataHandler.shared.removeVodFromCoreData(vodID: asset.stream.name) { (error) in
}
}
AssetListManager.sharedManager.deleteAssetWithID(id: asset.stream.name)
var userInfo = [String: Any]()
userInfo[Asset.Keys.name] = asset.stream.name
userInfo[Asset.Keys.downloadState] = Asset.DownloadState.notDownloaded.rawValue
NotificationCenter.default.post(name: .AssetDownloadStateChanged, object: nil,
userInfo: userInfo)
}
} catch {
print("An error occured deleting the file: \(error)")
}
}
/// Cancels an AVAssetDownloadTask given an Asset.
/// - Tag: CancelDownload
func cancelDownload(for asset: Asset) {
var task: AVAggregateAssetDownloadTask?
for (taskKey, assetVal) in activeDownloadsMap where asset == assetVal {
task = taskKey
break
}
task?.cancel()
}
}
/// Return the display names for the media selection options that are currently selected in the specified group
func displayNamesForSelectedMediaOptions(_ mediaSelection: AVMediaSelection) -> String {
var displayNames = ""
guard let asset = mediaSelection.asset else {
return displayNames
}
// Iterate over every media characteristic in the asset in which a media selection option is available.
for mediaCharacteristic in asset.availableMediaCharacteristicsWithMediaSelectionOptions {
/*
Obtain the AVMediaSelectionGroup object that contains one or more options with the
specified media characteristic, then get the media selection option that's currently
selected in the specified group.
*/
guard let mediaSelectionGroup =
asset.mediaSelectionGroup(forMediaCharacteristic: mediaCharacteristic),
let option = mediaSelection.selectedMediaOption(in: mediaSelectionGroup) else { continue }
// Obtain the display string for the media selection option.
if displayNames.isEmpty {
displayNames += " " + option.displayName
} else {
displayNames += ", " + option.displayName
}
}
return displayNames
}
/**
Extend `AssetPersistenceManager` to conform to the `AVAssetDownloadDelegate` protocol.
*/
extension AssetPersistenceManager: AVAssetDownloadDelegate {
/// Tells the delegate that the task finished transferring data.
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
let userDefaults = UserDefaults.standard
/*
This is the ideal place to begin downloading additional media selections
once the asset itself has finished downloading.
*/
guard let task = task as? AVAggregateAssetDownloadTask,
let asset = activeDownloadsMap.removeValue(forKey: task) else { return }
guard let downloadURL = willDownloadToUrlMap.removeValue(forKey: task) else { return }
// Prepare the basic userInfo dictionary that will be posted as part of our notification.
var userInfo = [String: Any]()
userInfo[Asset.Keys.name] = asset.stream.name
if let error = error as NSError? {
switch (error.domain, error.code) {
case (NSURLErrorDomain, NSURLErrorCancelled):
/*
This task was canceled, you should perform cleanup using the
URL saved from AVAssetDownloadDelegate.urlSession(_:assetDownloadTask:didFinishDownloadingTo:).
*/
guard let localFileLocation = localAssetForStream(withName: asset.stream.name)?.urlAsset.url else { return }
do {
try FileManager.default.removeItem(at: localFileLocation)
userDefaults.removeObject(forKey: asset.stream.name)
} catch {
print("An error occured trying to delete the contents on disk for \(asset.stream.name): \(error)")
}
userInfo[Asset.Keys.downloadState] = Asset.DownloadState.notDownloaded.rawValue
case (NSURLErrorDomain, NSURLErrorUnknown):
//fatalError("Downloading HLS streams is not supported in the simulator.")
return
default:
//fatalError("An unexpected error occured \(error.domain)")
return
}
} else {
do {
let bookmark = try downloadURL.bookmarkData()
userDefaults.set(bookmark, forKey: asset.stream.name)
} catch {
print("Failed to create bookmarkData for download URL.")
}
userInfo[Asset.Keys.downloadState] = Asset.DownloadState.downloaded.rawValue
userInfo[Asset.Keys.downloadSelectionDisplayName] = ""
}
NotificationCenter.default.post(name: .AssetDownloadStateChanged, object: nil, userInfo: userInfo)
}
/// Method called when the an aggregate download task determines the location this asset will be downloaded to.
func urlSession(_ session: URLSession, aggregateAssetDownloadTask: AVAggregateAssetDownloadTask,
willDownloadTo location: URL) {
/*
This delegate callback should only be used to save the location URL
somewhere in your application. Any additional work should be done in
`URLSessionTaskDelegate.urlSession(_:task:didCompleteWithError:)`.
*/
willDownloadToUrlMap[aggregateAssetDownloadTask] = location
}
/// Method called when a child AVAssetDownloadTask completes.
func urlSession(_ session: URLSession, aggregateAssetDownloadTask: AVAggregateAssetDownloadTask,
didCompleteFor mediaSelection: AVMediaSelection) {
/*
This delegate callback provides an AVMediaSelection object which is now fully available for
offline use. You can perform any additional processing with the object here.
*/
guard let asset = activeDownloadsMap[aggregateAssetDownloadTask] else { return }
// Prepare the basic userInfo dictionary that will be posted as part of our notification.
var userInfo = [String: Any]()
userInfo[Asset.Keys.name] = asset.stream.name
aggregateAssetDownloadTask.taskDescription = asset.stream.name
aggregateAssetDownloadTask.resume()
userInfo[Asset.Keys.downloadState] = Asset.DownloadState.downloading.rawValue
userInfo[Asset.Keys.downloadSelectionDisplayName] = displayNamesForSelectedMediaOptions(mediaSelection)
NotificationCenter.default.post(name: .AssetDownloadStateChanged, object: nil, userInfo: userInfo)
}
/// Method to adopt to subscribe to progress updates of an AVAggregateAssetDownloadTask.
func urlSession(_ session: URLSession, aggregateAssetDownloadTask: AVAggregateAssetDownloadTask,
didLoad timeRange: CMTimeRange, totalTimeRangesLoaded loadedTimeRanges: [NSValue],
timeRangeExpectedToLoad: CMTimeRange, for mediaSelection: AVMediaSelection) {
// This delegate callback should be used to provide download progress for your AVAssetDownloadTask.
guard let asset = activeDownloadsMap[aggregateAssetDownloadTask] else { return }
var percentComplete = 0.0
for value in loadedTimeRanges {
let loadedTimeRange: CMTimeRange = value.timeRangeValue
percentComplete +=
loadedTimeRange.duration.seconds / timeRangeExpectedToLoad.duration.seconds
}
var userInfo = [String: Any]()
userInfo[Asset.Keys.name] = asset.stream.name
userInfo[Asset.Keys.percentDownloaded] = percentComplete
// print("progress: \(Int(percentComplete * 100))%")
NotificationCenter.default.post(name: .AssetDownloadProgress, object: nil, userInfo: userInfo)
}
}
extension Notification.Name {
/// Notification for when download progress has changed.
static let AssetDownloadProgress = Notification.Name(rawValue: "AssetDownloadProgressNotification")
/// Notification for when the download state of an Asset has changed.
static let AssetDownloadStateChanged = Notification.Name(rawValue: "AssetDownloadStateChangedNotification")
/// Notification for when AssetPersistenceManager has completely restored its state.
static let AssetPersistenceManagerDidRestoreState = Notification.Name(rawValue: "AssetPersistenceManagerDidRestoreStateNotification")
}