// // MP_AVURLAsset.swift // MusicPlayer // // Created by Mr.Zhou on 2024/5/23. // import UIKit import AVFoundation import MobileCoreServices /////自定义媒体资产 class MP_AVURLAsset: AVURLAsset { //加载器 private var playerResourceLoader:MP_PlayerResourceLoader? //标题 private var title:String //加载队列 private var assetQueue:DispatchQueue! ///加载网络媒体资源 init(_ url:URL, videoId:String, title:String) { self.title = title var customURL:URL! //判断这个文件是否存在 if let audioPath = MP_CacheAndArchiverManager.shared.zhoujunfeng_getCachePath(videoId) { //存在,使用缓存文件播放 customURL = .init(fileURLWithPath: audioPath) print("从缓存中播放\(title)") super.init(url: customURL, options: nil) }else { //不存在,使用网络加载器加载 customURL = MP_PlayerToolConfig.customURL(url)! //创建对应的加载队列 assetQueue = .init(label: "com.relax.offline.mp3.\(videoId)") playerResourceLoader = .init(videoId: videoId, url: customURL, title: title) print("从网络媒体中播放\(title)") super.init(url: customURL, options: nil) //将系统加载器切换为自定义加载器 self.resourceLoader.setDelegate(playerResourceLoader, queue: .global(qos: .background)) } //实现预加载 preloading() } ///加载本地媒体资源 init(LocalURL url: URL, videoId:String, title:String) { self.title = title super.init(url: url, options: nil) //本地加载,不需要调用加载器 //实现预加载 preloading() } deinit{ //销毁内容 playerResourceLoader = nil assetQueue = nil } //预加载 private func preloading() { //对该Asset实现预加载,以让Asset触发resourceLoaderdelegate //加载关键的播放属性 let keys = ["playable"] self.loadValuesAsynchronously(forKeys: keys) { [weak self] in guard let self = self else {return} // 检查加载属性的结果 for key in keys { var error: NSError? let status = statusOfValue(forKey: key, error: &error) if status == .loaded { print("开始对\(title)的预加载") } else { // 处理加载失败的情况 print("无法加载 \(key): \(String(describing: error))") //一般是网络数据问题,需要重新请求一遍 } } } } } ///媒体资源加载代理 protocol MP_PlayerResourceLoaderDelegate: NSObject { ///检测当前加载器是否缓存完毕 func loader(_ loader:MP_PlayerResourceLoader, isCached:Bool) ///检测当前加载器是否请求错误 func loader(_ loader:MP_PlayerResourceLoader, requestError errorCode:Int) } typealias CheckStatusBlock = (Int) -> Void ///媒体资源加载器 class MP_PlayerResourceLoader: NSObject, AVAssetResourceLoaderDelegate { ///传递来的videoId private var videoId:String ///传递来的标题 private var title:String ///当前的LoadRequest数组 private var requestListes:[AVAssetResourceLoadingRequest]! ///检索状态码闭包 var checkStatusBlock:CheckStatusBlock? //初始化方法 init(videoId key:String, url:URL, title:String) { self.videoId = key self.title = title super.init() //添加监听 NotificationCenter.notificationKey.add(observer: self, selector: #selector(requestManagerDidReceiveResponseAction(_ :)), notificationName: .asset_response) NotificationCenter.notificationKey.add(observer: self, selector: #selector(requestManagerDidReceiveDataAction(_ :)), notificationName: .asset_receiveData) //实现代理 // MP_PlayerTaskManager.shared.delegate = self //检索当前请求组数量,如果有,就全部移除,避免重复生成/请求 if requestListes == nil { requestListes = [] } } deinit { NotificationCenter.default.removeObserver(self) //移除对应的任务 MP_PlayerTaskManager.shared.removeTask(videoId, title: title) } //MARK: - AVAssetResourceLoaderDelegate ///拦截系统的网络请求 func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool { addLoadingRequest(loadingRequest) return true } //取消系统的加载请求 func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) { MP_PlayerTaskManager.shared.accessQueue.async { [weak self] in guard let self = self else {return} guard let index = requestListes.firstIndex(where: { $0 == loadingRequest }) else { return } // 移除找到的请求 requestListes.remove(at: index) } } //MARK: - 处理系统的LoadingRequest ///添加LoadingRequest private func addLoadingRequest(_ loadingRequest:AVAssetResourceLoadingRequest) { MP_PlayerTaskManager.shared.accessQueue.async { [weak self] in guard let self = self else {return} requestListes.append(loadingRequest) //获取当前请求的请求范围 let offset = loadingRequest.dataRequest?.requestedOffset ?? 0 //判断cacheLengths if let cacheLength = MP_PlayerTaskManager.shared.cacheLengths[videoId] { //获得已缓存长度 if (offset >= 0 && offset <= cacheLength) { //已缓存数据超过请求范围,直接执行数据补全 processRequestList(videoId) }else { //数据还没缓存到请求范围,等待数据下载 newTaskWithLoadingRequest(loadingRequest) } }else { //数据还未缓存,请求缓存 newTaskWithLoadingRequest(loadingRequest) } } } ///实现互斥锁 private func synchronized(_ lock: Any, closure: () -> Void) { objc_sync_enter(lock) closure() objc_sync_exit(lock) } ///实现请求任务下载 private func newTaskWithLoadingRequest(_ loadingRequest:AVAssetResourceLoadingRequest) { // var fileLength:Int64 = 0 // //获取资源长度 // if let length = MP_PlayerTaskManager.shared.fileLengths[videoId] { // fileLength = length //// MP_PlayerTaskManager.shared.cancels[videoId] = true // } guard let requestUrl = loadingRequest.request.url else {return} //创建请求任务 MP_PlayerTaskManager.shared.resumeRequestStart(requestUrl, videoId: videoId, title: title) } ///处理请求组 private func processRequestList(_ videoId:String) { MP_PlayerTaskManager.shared.accessQueue.async { [weak self] in guard let self = self else {return} if self.videoId == videoId { var finishRequestList:[AVAssetResourceLoadingRequest] = [] //是当前videoId,操作请求组 requestListes.forEach { loadingRequest in if self.finishLoadingWithLoadingRequest(loadingRequest, videoId: videoId) { finishRequestList.append(loadingRequest) } } //将所有完成的请求移除 self.requestListes = requestListes.filter({!finishRequestList.contains($0)}) } } } ///检索是否请求完成 private func finishLoadingWithLoadingRequest(_ loadingRequest: AVAssetResourceLoadingRequest, videoId:String) -> Bool { // print("处理\(videoId)的请求信息") if let contentInformationRequest = loadingRequest.contentInformationRequest { //设置contentInformationRequest if let contentType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (MP_PlayerTaskManager.shared.mimeTypes[videoId] ?? "") as CFString, nil)?.takeRetainedValue() { contentInformationRequest.contentType = contentType as String } if let length = MP_PlayerTaskManager.shared.fileLengths[videoId] { contentInformationRequest.contentLength = length } contentInformationRequest.isByteRangeAccessSupported = true } //读取文件,填充数据 if let cacheLength = MP_PlayerTaskManager.shared.cacheLengths[videoId], let dataRequest = loadingRequest.dataRequest{ var requestedOffset = dataRequest.requestedOffset if (dataRequest.currentOffset != 0) { //如果当前下载长度不为0 requestedOffset = dataRequest.currentOffset } //当前下载长度-要请求的长度 let canReadLength = cacheLength - requestedOffset let respondLength = min(Int(canReadLength), (dataRequest.requestedLength)) guard let data = MP_CacheAndArchiverManager.shared.readTempFileDataWithOffset(UInt64(requestedOffset), length: respondLength, videoId: videoId) else {return false} //对请求加载数据 dataRequest.respond(with: data) //如果完全响应了所需要的数据,则完成 let nowendOffset = requestedOffset + Int64(respondLength) let reqEndOffset = (dataRequest.requestedOffset)+Int64(dataRequest.requestedLength) if nowendOffset >= reqEndOffset { loadingRequest.finishLoading() return true } }else { print("\(videoId)没有加载数据") } return false } //MARK: - MP_PlayerTaskDelegate //得到服务器响应 @objc private func requestManagerDidReceiveResponseAction(_ notification:Notification) { MP_PlayerTaskManager.shared.accessQueue.async { [weak self] in guard let self = self, let object = notification.object as? [String:Any] else {return} let id = object["videoId"] as? String let code = object["statusCode"] as? Int if self.videoId == id { if let block = checkStatusBlock { block(code ?? 0) } } } } //从服务器接收到数据 @objc private func requestManagerDidReceiveDataAction(_ notification:Notification){ MP_PlayerTaskManager.shared.accessQueue.async { [weak self] in guard let self = self, let object = notification.object as? [String:Any] else {return} let id = object["videoId"] as? String processRequestList(id ?? "") } } /// 缓存结果 func requestManagerIsCached(_ isCached: Bool, videoId:String){ if self.videoId == videoId { } } /// 接收数据完成 func requestManagerDidComplete(withError errorCode: Int, videoId:String){ if self.videoId == videoId { } } } ///媒体播放器工具配置 class MP_PlayerToolConfig:NSObject { ///自定义URL链接 static func customURL(_ url: URL) -> URL? { var urlString = url.absoluteString if let range = urlString.range(of: ":") { let scheme = String(urlString[.. URL? { guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return nil } components.scheme = components.scheme?.replacingOccurrences(of: "-streaming", with: "") return components.url } } class MP_PlayerTaskManager:NSObject, URLSessionDataDelegate{ static var shared = MP_PlayerTaskManager() ///请求代理 // weak var delegate:MP_PlayerTaskDelegate? //默认的下载队列 var accessQueue:DispatchQueue = .init(label: "com.relax.offline.mp3.accessQueue") //会话Session var session:URLSession! //路径组 var urls:[String:URL]! //请求组 var requests:[String:URLRequest]! //任务组 var dataTasks:[String:URLSessionDataTask]! //响应体组 var responses:[String:URLResponse]! //资源长度组 var fileLengths:[String:Int64]! //已缓冲的长度组 var cacheLengths:[String:Int64]! //资源类型组 var mimeTypes:[String:String]! //是否取消组 var cancels:[String:Bool]! //保存的下载进度组 var mediaDatas:[String:Data]! //标题组 var titles:[String:String]! //后台加载执行任务 var sessionCompletionHandler: (() -> Void)? //默认任务最大数 private var maxCount = 4 override init() { super.init() //实现会话 let configuration = URLSessionConfiguration.background(withIdentifier: "com.relax.offline.mp3.PlayerTasksession") //设置回话超时时间 configuration.timeoutIntervalForRequest = 30 configuration.timeoutIntervalForResource = 30 configuration.allowsCellularAccess = true //实现会话 session = .init(configuration: configuration, delegate: self, delegateQueue: nil) //初始化 urls = [:] requests = [:] dataTasks = [:] responses = [:] fileLengths = [:] cacheLengths = [:] mimeTypes = [:] cancels = [:] mediaDatas = [:] titles = [:] } //创建网络请求 func resumeRequestStart(_ customURL:URL, videoId:String, title:String) { accessQueue.async { [weak self] in guard let self = self, let url = MP_PlayerToolConfig.originalURL(customURL) else {return} //判断管理器是否已经持有这个请求 if urls[videoId] == nil { urls[videoId] = url } if titles[videoId] == nil { titles[videoId] = title } //创建请求 let request = URLRequest(url: url, cachePolicy: .reloadIgnoringCacheData, timeoutInterval: 50) if requests[videoId] == nil { requests[videoId] = request } //执行请求 requestDataTask(request, videoId: videoId, title: title) } } //请求任务 private func requestDataTask(_ request:URLRequest, videoId:String, title:String) { accessQueue.async { [weak self] in guard let self = self else {return} guard dataTasks[videoId] == nil else {return} //判断当前任务组中是否达到最大值 // if dataTasks.count >= maxCount { // //达到或超出了,移除第一个任务 // if let first = dataTasks.first { //// first.value.cancel() // let firstVideoID = first.key // let firstTitle = titles[firstVideoID] ?? "" // //移除其他相关设置 // removeTask(firstVideoID, title: firstTitle) // } // } //创建一个临时文件 let _ = MP_CacheAndArchiverManager.shared.createTempFile(videoId) //创建任务 let dataTask = session.dataTask(with: request) dataTask.priority = URLSessionTask.highPriority dataTasks[videoId] = dataTask dataTask.resume() } } //MARK: - URLSessionDataDelegate //服务器响应 func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { accessQueue.async { [weak self] in guard let self = self else {return} //根据任务获取对应的VideoId guard let videoId = dataTasks.first(where: {$0.value == dataTask})?.key, cancels[videoId] != true else {return} // print("\(videoId)已连接到服务器") if responses[videoId] == nil { //更新响应头 responses[videoId] = response } guard let httpResponse = response as? HTTPURLResponse else {return} //获取状态码 let statusCode = httpResponse.statusCode if statusCode == 200 { //成功获取到数据 if let contentLength = httpResponse.allHeaderFields["Content-Length"] as? String, let contentLengthValue = Int64(contentLength) { //跟新对应文件长度 fileLengths[videoId] = contentLengthValue > 0 ? contentLengthValue : response.expectedContentLength } else { fileLengths[videoId] = response.expectedContentLength } //更新文件类型 if let contentType = httpResponse.allHeaderFields["Content-Type"] as? String { mimeTypes[videoId] = contentType }else { mimeTypes[videoId] = response.mimeType } }else if (statusCode == 206) {///携带Range,不需要处理 // if let contentLength = httpResponse.allHeaderFields["Content-Length"] as? String, let contentLengthValue = Int64(contentLength) { // //跟新对应文件长度 // fileLengths[videoId] = contentLengthValue > 0 ? contentLengthValue : response.expectedContentLength // } else { // fileLengths[videoId] = response.expectedContentLength // } // //更新文件类型 // if let contentType = httpResponse.allHeaderFields["Content-Type"] as? String { // mimeTypes[videoId] = contentType // }else { // mimeTypes[videoId] = response.mimeType // } }else if (statusCode == 403){//访问被拒绝了,通常是用户执行了切换IP的操作,导致油管服务器不准访问(IP限制) print("响应码403-\(titles[videoId] ?? "")将重新请求资源") //使用videoId重新获取权限数据 switchURLAction(task: dataTask, videoId: videoId) }else if (statusCode == 416) {//超出文件大小,手动重试 print("\(titles[videoId] ?? "")请求范围超出文件大小") retryTask(task: dataTask) }else { print("\(titles[videoId] ?? "")响应出错了,任务取消,状态码为\(statusCode)") } //执行代理 NotificationCenter.notificationKey.post(notificationName: .asset_response, object: ["statusCode":statusCode,"videoId":videoId]) // self.delegate?.requestManagerDidReceiveResponse(withStatusCode: statusCode, videoId: videoId) //允许继续响应 completionHandler(.allow) } } //服务器返回数据(多次调用) func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { accessQueue.async { [weak self] in guard let self = self else {return} guard let videoId = dataTasks.first(where: {$0.value == dataTask})?.key, cancels[videoId] != true else {return} //获取相应到的数据 let currentLength = self.cacheLengths[videoId] ?? 0 self.cacheLengths[videoId] = currentLength + Int64(data.count) //根据videoId,写入相应的临时缓冲文件 MP_CacheAndArchiverManager.shared.writeDataToAudioFileTempPathWithData(data, videoId: videoId) //回调下载事件代理 // self.delegate?.requestManagerDidReceiveData(videoId) NotificationCenter.notificationKey.post(notificationName: .asset_receiveData, object: ["videoId":videoId]) } } //请求完成会调用该方法,请求失败则error有值 func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) { accessQueue.async { [weak self] in guard let self = self else {return} guard let videoId = dataTasks.first(where: {$0.value == task})?.key, cancels[videoId] != true, let title = titles[videoId] else {return} //检索是否存在报错 if let error = error { //触发报错,打印报错内容,并重新调用 print("加载\(title)报错,错误详情:\(error.localizedDescription)") //处理错误 if let urlError = error as? URLError { switch urlError.code { case .notConnectedToInternet://网络连接错误 print("网络连接错误") case .timedOut://链接超时 print("\(title)链接超时") //启用重试机制 retryTask(task: task) case .cannotConnectToHost://服务器未响应 print("\(title)链接服务器未响应") //启用重试机制 retryTask(task: task) case .secureConnectionFailed://证书失效 print("\(title)链接证书失效") //证书失效了 retryTask(task: task) case .networkConnectionLost://链接中断 print("\(title)加载中断了") //启用重试机制 retryTask(task: task) case .cancelled: print("\(title)加载取消了") //启用重试机制 retryTask(task: task) default: break } } }else { //没有报错,下载完成 _ = MP_CacheAndArchiverManager.shared.moveAudioFileFromTempPathToCachePath(videoId) NotificationCenter.notificationKey.post(notificationName: .asset_isCached, object: ["videoId":videoId]) } } } //重试机制 private func retryTask(task:URLSessionTask) { accessQueue.async { [weak self] in guard let self = self else {return} //获取videoID guard let videoId = dataTasks.first(where: {$0.value == task})?.key, let title = titles[videoId] else {return} // 根据需要设定重试延时,例如使用指数退避策略 let retryDelay:Double = 3 // 每次延时3秒 accessQueue.asyncAfter(deadline: .now() + retryDelay) { [weak self] in //尝试重试 print("\(title)尝试重试") guard let self = self, var request = requests[videoId] else {return} //假如cacheLength有值,那说明是断点传续 if let cacheLength = cacheLengths[videoId] { var range:String! if let filePathLength = fileLengths[videoId] { if cacheLength < filePathLength { //计算最后的值 range = "bytes=\(cacheLength)-\(filePathLength-1)" request.setValue(range, forHTTPHeaderField: "Range") } }else { range = "bytes=\(cacheLength)-" //创建任务 request.setValue(range, forHTTPHeaderField: "Range") } } let dataTask = session.dataTask(with: request) requests[videoId] = request dataTasks[videoId] = dataTask dataTask.resume() } } } //403事件切换URL private func switchURLAction(task:URLSessionTask, videoId:String) { accessQueue.async { [weak self] in guard let self = self else {return} task.cancel() MP_NetWorkManager.shared.requestAndroidPlayer(videoId, playlistId: "") { [weak self] results, cs in //只需要获取results第一位,也就是新的资源路径 guard let self = self, let first = results?.0.first else {return} //通知更新 NotificationCenter.notificationKey.post(notificationName: .player_asset_403, object: first) guard let url = URL(string: first) else {return} accessQueue.async { [weak self] in guard let self = self else {return} //取消原来的任务 if dataTasks[videoId] != nil { dataTasks[videoId] = nil } //成功创建了新的资源路径,创建新的请求 var request = URLRequest(url: url, cachePolicy: .reloadIgnoringCacheData, timeoutInterval: 50) //假如cacheLength有值,那说明是断点传续 if let cacheLength = cacheLengths[videoId] { var range:String! if let filePathLength = fileLengths[videoId] { if cacheLength < filePathLength { //计算最后的值 range = "bytes=\(cacheLength)-\(filePathLength-1)" request.setValue(range, forHTTPHeaderField: "Range") } }else { range = "bytes=\(cacheLength)-" //创建任务 request.setValue(range, forHTTPHeaderField: "Range") } } //创建新的任务 let dataTask = session.dataTask(with: request) requests[videoId] = request dataTasks[videoId] = dataTask dataTask.resume() } } } } //下载任务移除 func removeTask(_ videoId:String, title:String) { accessQueue.async { [weak self] in guard let self = self else {return} urls[videoId] = nil requests[videoId] = nil dataTasks[videoId]?.cancel() dataTasks[videoId] = nil responses[videoId] = nil fileLengths[videoId] = nil cacheLengths[videoId] = nil mimeTypes[videoId] = nil cancels[videoId] = nil print("移除了\(title)") } } }