AVPlayer详解 —— 参数设置
https://juejin.cn/post/6844903824809787399
iOS 视频边下边播(缓存,预加载)
https://www.jianshu.com/p/c4599f947609
iOS 音频视频播放器实现边下载边播放缓存视频
https://www.jianshu.com/p/b767788e9d57
iOS音视频实现边下载边播放
https://www.jianshu.com/p/e334d9271356
KTVHTTPCache 实现视频缓存和预加载
https://www.jianshu.com/p/84bd89605150
简单版
在 Swift 中实现一个支持边下载边播放的网络视频播放器,可以使用 AVPlayer
和 AVAssetResourceLoaderDelegate
。以下是一个示例代码:
- 使用
AVURLAsset
加载视频资源。 - 自定义
AVAssetResourceLoaderDelegate
,通过拦截视频请求实现边下载边播放。 - 将视频缓存到本地,减少后续播放的网络消耗。
示例代码:
import UIKit
import AVFoundation
class VideoPlayerViewController: UIViewController {
private var player: AVPlayer?
private var playerLayer: AVPlayerLayer?
private var resourceLoaderDelegate: VideoResourceLoaderDelegate?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
// 视频 URL
let videoURL = URL(string: "https://www.example.com/video.mp4")!
// 创建自定义 AVAssetResourceLoaderDelegate
resourceLoaderDelegate = VideoResourceLoaderDelegate()
let asset = AVURLAsset(url: videoURL)
asset.resourceLoader.setDelegate(resourceLoaderDelegate, queue: DispatchQueue.global())
// 使用 AVPlayer 播放
let playerItem = AVPlayerItem(asset: asset)
player = AVPlayer(playerItem: playerItem)
// 创建播放器图层
playerLayer = AVPlayerLayer(player: player)
playerLayer?.frame = view.bounds
playerLayer?.videoGravity = .resizeAspect
if let playerLayer = playerLayer {
view.layer.addSublayer(playerLayer)
}
// 播放视频
player?.play()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
playerLayer?.frame = view.bounds
}
}
class VideoResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate {
private var cache = [URL: Data]() // 简单缓存
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
guard let url = loadingRequest.request.url else {
loadingRequest.finishLoading(with: NSError(domain: "Invalid URL", code: -1, userInfo: nil))
return false
}
// 模拟网络加载(实际使用 URLSession 下载)
DispatchQueue.global().async {
if let cachedData = self.cache[url] {
// 缓存中有数据
self.respond(to: loadingRequest, with: cachedData)
} else {
// 下载视频数据
self.downloadVideo(from: url) { data in
if let data = data {
self.cache[url] = data
self.respond(to: loadingRequest, with: data)
} else {
loadingRequest.finishLoading(with: NSError(domain: "Download Failed", code: -2, userInfo: nil))
}
}
}
}
return true
}
private func downloadVideo(from url: URL, completion: @escaping (Data?) -> Void) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
completion(data)
}
task.resume()
}
private func respond(to loadingRequest: AVAssetResourceLoadingRequest, with data: Data) {
guard let contentInformationRequest = loadingRequest.contentInformationRequest else { return }
// 填充内容信息
contentInformationRequest.contentType = "public.mp4"
contentInformationRequest.isByteRangeAccessSupported = true
contentInformationRequest.contentLength = Int64(data.count)
// 填充数据
if let dataRequest = loadingRequest.dataRequest {
let requestedOffset = Int(dataRequest.requestedOffset)
let requestedLength = dataRequest.requestedLength
let endOffset = requestedOffset + requestedLength
let subdata = data.subdata(in: requestedOffset..<min(endOffset, data.count))
dataRequest.respond(with: subdata)
}
loadingRequest.finishLoading()
}
}
VideoResourceLoaderDelegate
:
- 实现了
AVAssetResourceLoaderDelegate
协议,通过拦截资源请求来自定义视频加载逻辑。 - 在示例中,简单地使用内存缓存并通过
URLSession
下载视频。
边下载边播放:
-
AVAssetResourceLoaderDelegate
支持处理分段请求(如数据分块下载),使得视频播放可以在数据部分加载完成后立即开始。
优化建议:
- 将视频数据缓存到文件系统(如沙盒目录),以减少重复加载。
- 增加错误处理和超时机制,提升稳定性。
完善缓存逻辑、下载报错重试逻辑
实现一个边播放边下载并缓存的功能,同时支持下载失败后的重试逻辑,可以结合 AVAssetResourceLoader
和 URLSession
实现自定义的资源加载,并将数据流缓存到本地沙盒。以下是完整的实现步骤和代码示例:
以下是完整的实现代码,包括以下功能:
- 播放网络视频时,支持边下载边播放。
- 下载好的视频缓存到本地沙盒。
- 如果有缓存数据,优先播放缓存数据,同时继续下载剩余部分。
- 下载过程中支持出错重试逻辑。
- 退出界面时暂停下载,但保留缓存数据,下次进入继续播放和下载。
完整代码实现
1. VideoDownloader
类
负责处理视频的下载、缓存管理、以及断点续传逻辑。
import Foundation
class VideoDownloader {
private var downloadTask: URLSessionDataTask?
private var isDownloading = false
private var downloadedData = Data()
private let url: URL
private let cachePath: String
private let retryLimit = 3
private var retryCount = 0
init(url: URL, cacheFileName: String) {
self.url = url
self.cachePath = FileManager.default.temporaryDirectory.appendingPathComponent(cacheFileName).path
}
func startDownload(range: NSRange? = nil, completion: @escaping (Data?, Error?) -> Void) {
guard !isDownloading else { return }
isDownloading = true
var request = URLRequest(url: url)
if let range = range {
request.setValue("bytes=\(range.location)-\(range.location + range.length - 1)", forHTTPHeaderField: "Range")
}
downloadTask = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
guard let self = self else { return }
self.isDownloading = false
if let error = error {
self.retryCount += 1
if self.retryCount <= self.retryLimit {
print("Retrying download... (\(self.retryCount))")
self.startDownload(range: range, completion: completion)
} else {
completion(nil, error)
}
return
}
if let data = data {
self.retryCount = 0
self.saveToCache(data: data)
self.downloadedData.append(data)
completion(data, nil)
} else {
completion(nil, NSError(domain: "VideoDownloader", code: -1, userInfo: [NSLocalizedDescriptionKey: "No data received"]))
}
}
downloadTask?.resume()
}
func pauseDownload() {
downloadTask?.cancel()
isDownloading = false
}
private func saveToCache(data: Data) {
if FileManager.default.fileExists(atPath: cachePath) {
if let fileHandle = FileHandle(forWritingAtPath: cachePath) {
fileHandle.seekToEndOfFile()
fileHandle.write(data)
fileHandle.closeFile()
}
} else {
FileManager.default.createFile(atPath: cachePath, contents: data, attributes: nil)
}
}
func getCachedData() -> Data? {
return try? Data(contentsOf: URL(fileURLWithPath: cachePath))
}
}
2. VideoResourceLoader
类
自定义 AVAssetResourceLoader
实现数据的边下载边加载。
import AVFoundation
class VideoResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
private let downloader: VideoDownloader
init(url: URL, cacheFileName: String) {
self.downloader = VideoDownloader(url: url, cacheFileName: cacheFileName)
}
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
guard let dataRequest = loadingRequest.dataRequest else {
loadingRequest.finishLoading(with: NSError(domain: "ResourceLoader", code: -1, userInfo: nil))
return false
}
let cachedData = downloader.getCachedData() ?? Data()
let requestedOffset = Int(dataRequest.requestedOffset)
let requestedLength = dataRequest.requestedLength
if requestedOffset < cachedData.count {
// Respond with cached data
let availableData = cachedData.subdata(in: requestedOffset..<min(requestedOffset + requestedLength, cachedData.count))
dataRequest.respond(with: availableData)
if requestedOffset + requestedLength <= cachedData.count {
loadingRequest.finishLoading()
return true
}
}
// Continue downloading
let downloadRange = NSRange(location: cachedData.count, length: requestedOffset + requestedLength - cachedData.count)
downloader.startDownload(range: downloadRange) { [weak self] data, error in
guard let self = self else { return }
if let data = data {
dataRequest.respond(with: data)
loadingRequest.finishLoading()
} else {
loadingRequest.finishLoading(with: error)
}
}
return true
}
func pauseDownload() {
downloader.pauseDownload()
}
}
3. VideoPlayerViewController
类
主控制器,负责播放视频并管理资源加载器。
import UIKit
import AVFoundation
class VideoPlayerViewController: UIViewController {
private var player: AVPlayer?
private var resourceLoader: VideoResourceLoader?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
let videoURL = URL(string: "https://example.com/video.mp4")!
let cacheFileName = "cached_video.mp4"
// 初始化资源加载器
resourceLoader = VideoResourceLoader(url: videoURL, cacheFileName: cacheFileName)
// 创建自定义 AVAsset
let asset = AVURLAsset(url: URL(string: "streaming://video.mp4")!)
asset.resourceLoader.setDelegate(resourceLoader, queue: DispatchQueue(label: "resourceLoaderQueue"))
let playerItem = AVPlayerItem(asset: asset)
player = AVPlayer(playerItem: playerItem)
// 添加播放器视图
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = view.bounds
playerLayer.videoGravity = .resizeAspect
view.layer.addSublayer(playerLayer)
// 开始播放
player?.play()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// 暂停下载和播放
resourceLoader?.pauseDownload()
player?.pause()
}
}
测试建议
测试网络正常情况下的播放和缓存功能。
测试在网络中断或下载失败时的重试逻辑。
测试中途退出界面并重新进入,确保从上次缓存位置继续播放和下载。
-
下载管理器 (
VideoDownloader
)- 支持分片下载。
- 将数据写入沙盒缓存,避免重复下载。
- 内置重试逻辑,自动重新尝试失败的下载请求。
-
资源加载器 (
VideoResourceLoader
)- 拦截
AVPlayer
的加载请求。 - 使用
VideoDownloader
实现边下载边返回数据。
- 拦截
-
播放器集成
- 通过
AVURLAsset
和自定义的resourceLoader
播放视频。 - 使用
AVPlayer
播放从网络加载的数据。
- 通过
优化建议
缓存校验
添加缓存校验逻辑,避免重复下载同一资源。多线程优化
使用并发队列提升下载效率。断点续传
检查是否有未完成的下载,并从断点处恢复。