WWDC Quick Look 💓 By SwiftGGTeam
Discover how to download and play HLS offline

Discover how to download and play HLS offline

观看原视频

Highlight

Apple 用 AVAssetDownloadTask 下载单个 HLS 媒体选择,用 AVAggregateAssetDownloadTask 下载多组音频和字幕选择,并通过 AVAssetCacheAVContentKeySessionAVAssetDownloadStorageManager 处理离线播放、FairPlay 密钥和系统存储管理。

核心内容

很多视频 App 已经用 HLS 做在线播放。用户上飞机前、出门前,或者不想消耗蜂窝流量时,会希望把同一份内容提前下载到设备上。Apple 在这场 Session 里给出的思路很直接:同一套 HLS 资源既可以用于在线播放,也可以通过 AVFoundation 下载后离线播放,受保护内容仍然可以使用 FairPlay Streaming。

离线 HLS 的入口是一个 download task。它运行在 URLSession 体系里,所以任务会按照系统资源和网络状态调度。网络差时不一定立刻开始,遇到超时也会由 AVFoundation 自动重试。App 需要做的,是创建任务、监听进度、保存下载位置,再用下载好的资源创建播放器项目。

下载策略分两种。AVAssetDownloadTask 适合下载一组视频、音频和字幕组合,它会按设备地区和本地化偏好自动选择 media selection。AVAggregateAssetDownloadTask 适合让用户明确选择多组音频和字幕,例如英语音轨、西班牙语音轨和法语字幕一起离线保存。进度统计也随之变复杂,演讲建议把视频、音频、字幕按权重拆开估算,示例里视频约占 70% 的下载时间。

下载完成后,播放不必重新发明一套路径。下载还在进行时,可以复用创建任务时的 AVAsset,让 AVFoundation 在播放和下载之间共享资源。下载完成后,如果原来的 task 对象已经不在内存里,就用保存下来的 file URL 重新创建 AVURLAssetAVAssetCache 负责回答当前 asset 是否可离线播放,以及哪些 media selection 已经可离线播放。

受保护内容的关键是离线 key。在线播放时,播放器可以按需联系 key server;离线播放时,设备可能没有网络,所以 App 要在下载阶段通过 AVContentKeySession 生成并保存 offline key。key server 可以设置过期时间。用户删除下载时,App 还应该主动 invalidate 对应的 persistable content key。

iOS 14 的更新集中在体验和治理上。下载质量可以按最低码率、最小展示尺寸、HDR 偏好和多声道音频偏好控制。存储管理可以交给 AVAssetDownloadStorageManager,让系统按过期时间和优先级清理离线媒体,也允许用户在系统设置里删除这些内容。

详细内容

创建单组 HLS 下载任务

02:52AVAssetDownloadTask 下载一组视频、音频和字幕组合。创建任务前,先用 HLS URL 创建 AVURLAsset,再用 background URLSessionConfiguration 创建 AVAssetDownloadURLSession。示例里通过 AVAssetDownloadTaskMinimumRequiredMediaBitrateKey 请求 2 Mbps 的下载版本。

let hlsAsset = AVURLAsset(url: assetURL)

let backgroundConfiguration = URLSessionConfiguration.background(
    withIdentifier: "assetDownloadConfigurationIdentifier")
let assetURLSession = AVAssetDownloadURLSession(configuration: backgroundConfiguration,
    assetDownloadDelegate: self, delegateQueue: OperationQueue.main())

// Download a Movie at 2 mbps
let assetDownloadTask = assetURLSession.makeAssetDownloadTask(asset: hlsAsset,
    assetTitle: "My Movie", assetArtworkData: myAssetArtwork,
    options: [AVAssetDownloadTaskMinimumRequiredMediaBitrateKey: 2000000])!
assetDownloadTask.resume()



// AVAssetDownloadTask uses automatic media selection

关键点:

  • AVURLAsset(url:) 指向原始 HLS master playlist,下载和播放都围绕这个 asset 展开。
  • background configuration 让下载任务可以在 App 进入后台后继续运行。
  • AVAssetDownloadURLSession 使用 AVAssetDownloadDelegate 回调下载状态。
  • makeAssetDownloadTask 创建单组 media selection 的下载任务。
  • AVAssetDownloadTaskMinimumRequiredMediaBitrateKey 用来限制下载版本的最低码率。
  • AVAssetDownloadTask 使用 automatic media selection,会根据地区和本地化偏好选择音频和字幕。

监听单个下载任务进度

03:41)下载进度回调用媒体时间范围表达,不使用字节数。这样进度更接近播放器界面里的时长概念。完成状态则通过 didCompleteWithError 通知。

// Monitor AVAssetDownloadTask
public protocol AVAssetDownloadDelegate: URLSessionTaskDelegate {


	// Use to monitor progress
	func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask,
		didLoad timeRange: CMTimeRange, totalTimeRangesLoaded loadedTimeRanges: [NSValue],
		timeRangeExpectedToLoad: CMTimeRange)


	// Listen for completion
	func urlSession(_ session: URLSession, task: URLSessionTask,
		didCompleteWithError error: Error?)

}

关键点:

  • didLoad timeRange 回调本次加载的媒体时间范围。
  • loadedTimeRanges 汇总已经下载完成的时间范围。
  • timeRangeExpectedToLoad 是预计要加载的总时间范围,适合计算百分比。
  • didCompleteWithError 既表示成功完成,也承载下载失败的错误信息。

让用户选择多组音频和字幕

04:55)如果用户需要下载多语言音轨或字幕,先从 asset 的 media selection group 中挑选目标 option。演讲示例把每个选择写入 myMediaSelections,再交给 aggregate download task。

let hlsAsset = AVURLAsset(url: assetURL)
let myMediaSelections = [] // audio media-selections followed by subtitle media-selections

guard hlsAsset.statusOfValue(forKey: "availableMediaCharacteristicsWithMediaSelectionOptions", error: nil)
   == AVKeyValueStatus.loaded else { return }

let mediaCharacteristic = //AVMediaCharacteristic.audible or AVMediaCharacteristic.legible
let mediaSelectionGroup = hlsAsset.mediaSelectionGroup(forMediaCharacteristic: mediaCharacteristic)
if let options = mediaSelectionGroup?.options {
    for option in options {
        // chose your media selection option
        if /* this is my option */ {
            let mutableMediaSelection = hlsAsset.preferredMediaSelection.mutableCopy()
            mutableMediaSelection.select(option, in: mediaSelectionGroup)
            myMediaSelections.append(mutableMediaSelection)
        }
    }
}

关键点:

  • availableMediaCharacteristicsWithMediaSelectionOptions 必须先加载,才能枚举可选音频和字幕。
  • AVMediaCharacteristic.audible 对应音频,AVMediaCharacteristic.legible 对应字幕。
  • mediaSelectionGroup(forMediaCharacteristic:) 取出指定类型的可选项集合。
  • mutableMediaSelection.select 把用户选择的 option 写入一个 media selection。
  • myMediaSelections 可以按音频、字幕顺序收集多组下载目标。

创建聚合下载任务

05:11AVAggregateAssetDownloadTask 用于下载多组 media selection。它仍然使用 AVAssetDownloadURLSession,差别在于创建任务时传入 media selections 数组。

let hlsAsset = AVURLAsset(url: assetURL)
let myMediaSelections = ...

let backgroundConfiguration = URLSessionConfiguration.background(
    withIdentifier: "assetDownloadConfigurationIdentifier")
let assetURLSession = AVAssetDownloadURLSession(configuration: backgroundConfiguration,
    assetDownloadDelegate: self, delegateQueue: OperationQueue.main())

// Download a Movie at 2 mbps
let aggDownloadTask = assetURLSession.aggregateAssetDownloadTask(with: hlsAsset,
    mediaSelections: myMediaSelections,
    assetTitle: "My Movie",
    assetArtworkData: myAssetArtwork,
    options:[AVAssetDownloadTaskMinimumRequiredMediaBitrateKey: 2000000])!
aggDownloadTask.resume()

关键点:

  • aggregateAssetDownloadTask 绑定同一个 HLS asset 和多组 media selection。
  • assetTitleassetArtworkData 会在系统管理离线内容时展示给用户。
  • 码率选项仍然通过 AVAssetDownloadTaskMinimumRequiredMediaBitrateKey 传入。
  • 对多音轨、多字幕下载,进度 UI 需要按 media selection 分开估算。

恢复后台下载任务

07:04)下载任务在后台继续运行。App 重新启动时,要用相同的 background configuration identifier 创建 session,再通过 getAllTasks 找回现有任务。

// Restore Tasks on App Launch
class MyAppDelegate: UIResponder, UIApplicationDelegate {
	func application(_ application: UIApplication,
			didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
		let configuration = URLSessionConfiguration.background(withIdentifier:
			"assetDownloadConfigurationIdentifier")
		let session = URLSession(configuration: configuration)
		session.getAllTasks { tasks in
			for task in tasks {
				if let assetDownloadTask = task as? AVAssetDownloadTask {
					// restore progress indicators, state, etc...
				}
			}
		}
	}
}

关键点:

  • configuration identifier 必须与创建下载任务时相同。
  • getAllTasks 会返回当前 session 下的任务。
  • 找回 AVAssetDownloadTask 后,可以恢复进度条、状态和任务管理界面。
  • transcript 还提到可以从原 download task 取回原始 AVAsset 继续使用。

判断哪些内容可离线播放

09:16)下载完成后,AVAssetCache 告诉 App 当前 asset 是否可以离线播放,以及哪些 media selection 已经在本地可用。

// What can I play offline?

public class AVURLAsset {

	public var assetCache: AVAssetCache? { get }

}

public class AVAssetCache {

	public var isPlayableOffline: Bool { get }

	public func mediaSelectionOptions(in mediaSelectionGroup: AVMediaSelectionGroup)
		-> [AVMediaSelectionOption]

}

关键点:

  • AVURLAsset.assetCache 是离线可播放状态的入口。
  • isPlayableOffline 回答整个 asset 是否能离线播放。
  • mediaSelectionOptions(in:) 返回某个 media selection group 中已经可离线播放的选项。
  • 这适合驱动“已下载音轨”“已下载字幕”等界面状态。

失效离线 FairPlay 密钥

11:33)离线 HLS 可以继续使用 FairPlay Streaming。App 在下载阶段创建 offline key 并保存,离线播放时用它响应 key request。用户删除下载或需要提前收回授权时,用 AVContentKeySession 失效对应 key。

// Invalidate Offline Key

public class AVContentKeySession {

	func invalidatePersistableContentKey(_ persistableContentKeyData: Data,
		options: [AVContentKeySessionServerPlaybackContextOption : Any]? = nil,
		completionHandler handler: @escaping (Data?, Error?) -> Void)


	func invalidateAllPersistableContentKeys(forApp appIdentifier: Data,
		options: [AVContentKeySessionServerPlaybackContextOption : Any]? = nil,
		completionHandler handler: @escaping (Data?, Error?) -> Void)


}

关键点:

  • invalidatePersistableContentKey 用于失效单个已保存的 offline key。
  • invalidateAllPersistableContentKeys 可以按 FairPlay streaming application certificate 对应的 app 标识清理全部 key。
  • transcript 说明 key server 可以设置 offline key 过期时间。
  • 如果 key 在一次已开始的播放期间过期,当前播放会继续到结束,避免中途突然停止。

控制离线下载质量

13:54)下载越快,质量通常越低;质量越高,占用时间和空间越多。Session 建议给用户提供快速下载和最佳质量两种选择,再用下载任务选项约束具体版本。iOS 14 增加了展示尺寸和 HDR 偏好控制。

// Quality Selection

public class AVAssetDownloadTask {

	public let AVAssetDownloadTaskMinimumRequiredMediaBitrateKey: String

	//Starting in iOS 14

	public let AVAssetDownloadTaskMinimumRequiredPresentationSizeKey: String

	public let AVAssetDownloadTaskPrefersHDRKey: String

}

关键点:

  • AVAssetDownloadTaskMinimumRequiredMediaBitrateKey 用最低码率筛选下载版本。
  • AVAssetDownloadTaskMinimumRequiredPresentationSizeKey 从 iOS 14 开始可用,用展示尺寸约束质量。
  • AVAssetDownloadTaskPrefersHDRKey 控制是否偏好 HDR presentation。
  • transcript 说明 download task 默认会偏好下载可用的 HDR presentation。

把离线内容交给系统管理

15:51)Session 建议让操作系统管理离线下载。系统可以在存储紧张或软件更新时回收空间,用户也可以在系统设置里删除媒体。App 通过 storage manager 设置过期时间和优先级。

// AVAssetDownloadStorageManager
// Get the singleton
let storageManager = AVAssetDownloadStorageManager.shared()

// Create the policy
let newPolicy = AVMutableAssetDownloadStorageManagementPolicy()

newPolicy.expirationDate = myExpiryDate

newPolicy.priority = .important

// Set the policy
storageManager.setStorageManagementPolicy(newPolicy, forURL: myDownloadStorageURL)

关键点:

  • AVAssetDownloadStorageManager.shared() 取得系统提供的下载存储管理器。
  • AVMutableAssetDownloadStorageManagementPolicy 描述这份内容的清理策略。
  • expirationDate 先参与清理判断,priority 再决定保留优先级。
  • setStorageManagementPolicy 把策略应用到下载内容的系统位置。
  • transcript 特别提醒,要把下载内容留在系统提供的位置,并准备好处理系统删除后的状态。

核心启发

  • 做离线下载前的版本选择页

    • 做什么:在视频详情页提供“快速下载”和“最佳质量”两个入口,并显示预计音轨、字幕和画质选择。
    • 为什么值得做:Session 明确建议在下载时间和质量之间给用户选择空间,下载任务也提供码率、展示尺寸、HDR 和多声道偏好。
    • 怎么开始:用 AVAssetDownloadTaskMinimumRequiredMediaBitrateKey 做快速版本,用 AVAssetDownloadTaskMinimumRequiredPresentationSizeKeyAVAssetDownloadTaskPrefersHDRKey 做高质量版本。
  • 做多语言离线包

    • 做什么:让用户在下载前选择多个音轨和字幕,例如英文音频、西班牙语音频和法语字幕。
    • 为什么值得做AVAggregateAssetDownloadTask 可以一次下载多组 media selection,解决单任务自动选择不够明确的问题。
    • 怎么开始:读取 AVMediaCharacteristic.audibleAVMediaCharacteristic.legible 的 media selection group,把用户选项组装进 mediaSelections
  • 做下载恢复中心

    • 做什么:App 启动后恢复所有后台 HLS 下载任务,继续显示进度、状态和错误。
    • 为什么值得做:下载任务会在后台继续运行,重新启动时需要用同一个 session identifier 找回任务。
    • 怎么开始:在 app launch 创建相同 identifier 的 background session,调用 getAllTasks,为 AVAssetDownloadTask 恢复 UI 状态。
  • 做离线授权到期提醒

    • 做什么:在离线视频列表里显示 FairPlay offline key 的到期状态,并在联网时提前刷新即将过期的 key。
    • 为什么值得做:transcript 说明 key server 可以设置 offline key 过期时间,App 需要在过期前创建新的 offline key。
    • 怎么开始:保存 offline key 时记录授权有效期,App 回到前台或进入下载页时扫描即将过期的项目。
  • 做系统存储策略面板

    • 做什么:给离线视频设置过期日期和重要程度,并提示用户系统可能在空间不足时清理内容。
    • 为什么值得做AVAssetDownloadStorageManager 允许系统按过期时间和优先级回收离线媒体,还能让用户在设置里删除内容。
    • 怎么开始:为每个下载 URL 设置 AVMutableAssetDownloadStorageManagementPolicy,并在播放前用 AVAssetCache.isPlayableOffline 重新确认本地状态。

关联 Session

评论

GitHub Issues · utterances