// // MP_AVURLAsset.swift // MusicPlayer // // Created by Mr.Zhou on 2024/5/23. // import UIKit import AVFoundation extension URL { func convertCustomSchemeURL(_ scheme: String) -> URL? { var components = URLComponents(url: self, resolvingAgainstBaseURL: false) components?.scheme = scheme return components?.url } } ///媒体加载协议 @objc protocol MP_AVPlayerItemDelegate { ///当媒体项目初次缓存后 @objc func playerItemReadyToPlay(_ playerItem: MP_AVPlayerItem) ///当媒体项目收到新缓存后 @objc func playerItem(_ playerItem: MP_AVPlayerItem, progress:Float) ///当媒体项目完全加载后 @objc func playerItem(_ playerItem: MP_AVPlayerItem, didFinishLoadingData data:Data) ///当媒体项目加载数据中断(比如断网了),导致停止播放时 @objc func playerItemPlaybackStalled(_ playerItem: MP_AVPlayerItem) ///当媒体项目加载错误时调用。 @objc func playerItem(_ playerItem: MP_AVPlayerItem, loadingError error:Error) } ///自定义媒体项目 class MP_AVPlayerItem: AVPlayerItem { ///拦截协议 var resourceLoaderDelegate:MP_ResourceLoaderDelegate! ///地址路径 fileprivate var url:URL ///原生Scheme fileprivate let initialScheme:String? ///自定义文件扩展名 fileprivate var customFileExtension:String? ///拦截式scheme fileprivate let customScheme = "myapp" ///媒体项目标题 fileprivate var title:String = "Random" ///媒体ID fileprivate var videoId:String = "Random" ///媒体资产 fileprivate var resourceAsset:MP_AVURLAsset! ///媒体项目协议 weak var delegate:MP_AVPlayerItemDelegate? ///自定义初始化 init(url: URL, bitrate:Int64, customFileExtension: String? = "mp4", title:String?, videoId:String?) { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), let scheme = components.scheme, var urlWithCustomScheme = url.convertCustomSchemeURL(customScheme) else { fatalError("不支持没有scheme的URL") } self.url = url self.initialScheme = scheme //将扩展文件名赋予自定义scheme if let stringPath = customFileExtension { //先删除扩展名(网络文件路径一般没有) urlWithCustomScheme.deletePathExtension() //扩展路径 urlWithCustomScheme.appendPathExtension(stringPath) //更新自定义文件扩展名 self.customFileExtension = stringPath } if let title = title, let videoId = videoId { //更新标题 self.title = title self.videoId = videoId } resourceLoaderDelegate = .init(bitrate) resourceAsset = MP_AVURLAsset(url: urlWithCustomScheme, delegate: resourceLoaderDelegate, title: title ?? "") //生成一个媒体资产 super.init(asset: resourceAsset, automaticallyLoadedAssetKeys: nil) //为拦截协议传入指定的playerItem resourceLoaderDelegate.playItem = self //kvo监听项目状态 addObserver(self, forKeyPath: "status", options: [.old,.new], context: nil) //kvo监听项目加载值 addObserver(self, forKeyPath: "loadedTimeRanges", options: [.old,.new], context: nil) //kvo监听项目准备情况 addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: [.old,.new], context: nil) //kvo监听项目报错 addObserver(self, forKeyPath: "error", options: [.old,.new], context: nil) NotificationCenter.default.addObserver(self, selector: #selector(playbackStalledHandler), name:NSNotification.Name.AVPlayerItemPlaybackStalled, object: self) } //MARK: - KVO实现 override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { guard let keyPath = keyPath else { return } //根据keyPath检索 switch keyPath { case "status"://playerItem状态 if let statuValue = change?[.newKey] as? Int, statuValue == AVPlayerItem.Status.readyToPlay.rawValue { //判断当前播放器是否在播放当前音乐中 print("当前音乐-\(title) 已经准备好播放") } case "loadedTimeRanges"://当前缓冲进度 guard self.videoId == MP_PlayerManager.shared.loadPlayer.currentVideo?.song.videoId else {return} let timeRanges = self.loadedTimeRanges.map({$0.timeRangeValue}) //获取当前播放Item的缓冲值组 if let first = timeRanges.first { //获取开始时间的秒数 let startSeconds = first.start.seconds //获取缓冲区的持续时间 let durationSeconds = first.duration.seconds //计算当前缓冲总时间 let bufferedSeconds = startSeconds + durationSeconds //获取当前播放音乐资源的最大时间值 let maxDuration = CMTimeGetSeconds(self.duration) let progress = bufferedSeconds/maxDuration //传递缓存值 delegate?.playerItem(self, progress: Float(progress)) } case "playbackLikelyToKeepUp"://是否存在足够的数据开始播放 if let playbackLikelyToKeepUp = change?[.newKey] as? Bool, playbackLikelyToKeepUp == true { //有足够的数据来支持播放 delegate?.playerItemReadyToPlay(self) }else { //没有足够的数据支持播放 delegate?.playerItemPlaybackStalled(self) } case "error": if let error = change?[.newKey] as? Error { print("当前音乐-\(title) 未做好准备播放,失败原因是\(error)") delegate?.playerItem(self, loadingError: error) } default: break } } //playerItem突然停止了 @objc func playbackStalledHandler() { delegate?.playerItemPlaybackStalled(self) } deinit { print("已销毁\(title)") NotificationCenter.default.removeObserver(self) removeObserver(self, forKeyPath: "status") removeObserver(self, forKeyPath: "loadedTimeRanges") removeObserver(self, forKeyPath: "playbackLikelyToKeepUp") removeObserver(self, forKeyPath: "error") //终止网络请求 resourceLoaderDelegate.session?.invalidateAndCancel() //清理资产空间 resourceAsset = nil } } ///自定义媒体资产 class MP_AVURLAsset: AVURLAsset { init(url URL: URL, delegate:MP_ResourceLoaderDelegate, title:String) { super.init(url: URL, options: nil) self.resourceLoader.setDelegate(delegate, queue: .main) //对该Asset实现预加载,以让Asset触发resourceLoaderdelegate // 加载关键的播放属性 let keys = ["playable"] self.loadValuesAsynchronously(forKeys: keys) { [weak self] in // 检查加载属性的结果 for key in keys { var error: NSError? let status = self?.statusOfValue(forKey: key, error: &error) if status == .loaded { print("开始对\(title)的预加载") } else { // 处理加载失败的情况 print("无法加载 \(key): \(String(describing: error))") //一般是网络数据问题,需要重新请求一遍 } } } } } ///媒体资源加载代理 class MP_ResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate, URLSessionDataDelegate, URLSessionTaskDelegate { //判断缓存空间的数据是否完整 private var iscompleted:Bool? // 从内存播放时需要 var mimeType: String? //比特率 var bitrate:Int64 //网络会话 var session: URLSession? //媒体数据 var mediaData: Data? //媒体数据块 var mediaDataBlocks: [MediaDataBlock]? //响应体 var response: URLResponse? //活跃的加载请求 var pendingRequests = Set() //缓存中的最长长度值 var maxCount:Int64? //缓存情况 var progress:Float = 0 weak var playItem: MP_AVPlayerItem? //最后加载范围 private var lastRequestedEndOffset:Int64? init(_ bitrate:Int64) { self.bitrate = bitrate super.init() //添加关于用手动seek的监听 NotificationCenter.notificationKey.add(observer: self, selector: #selector(seekAction(_ :)), notificationName: .positive_player_seek) } func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool { guard let initialUrl = playItem?.url else { fatalError("internal inconsistency") } //判断session是否存在 if session == nil { //判断加载数据方法 if let media = MP_CacheManager.shared.data(forKey: playItem?.videoId ?? "") { print("缓存中存在\(playItem?.title ?? ""),取出中") //从缓存中去获取数据,并覆盖响应数据 mediaData = media.data mediaDataBlocks = media.dataBlocks //判断是否完整 if media.isComplete == false { print("\(playItem?.title ?? "")数据不完整,开始网络请求相关数据") maxCount = media.maxCount //缓存中的数据是不完整的,继续网络请求 continuationDataRequest(with: initialUrl, count: Int64(media.data.count)) iscompleted = false }else { iscompleted = true DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in guard let self = self else {return} if MP_PlayerManager.shared.loadPlayer.currentVideo?.song.videoId == playItem?.videoId { playItem?.delegate?.playerItem(playItem!, didFinishLoadingData: media.data) } } } }else { print("开始网络请求\(playItem?.title ?? "")相关数据") //初次请求 startDataRequest(with: initialUrl) } }else { } pendingRequests.insert(loadingRequest) // processPendingRequests() return true } @objc private func seekAction(_ sender:Notification) { if playItem?.videoId == MP_PlayerManager.shared.loadPlayer.currentVideo.song.videoId { if let progress = sender.object as? Double { let requestedOffset = progress * Double(maxCount ?? 0) // 测试是否发送网络请求 if requestedOffset > Double(mediaData?.count ?? 0) { print("用户滚动到了当前缓存范围外") // 用户seek位置在媒体数据范围内,假设这里已经有了初始URL let initialUrl = playItem?.url continuationDataRequest(with: initialUrl!, count: Int64(requestedOffset)) } } } } /// 开始网络请求 /// - Parameters: /// - url: 网络请求地址 func startDataRequest(with url: URL) { //取消之前的请求方法 session?.invalidateAndCancel() let configuration = URLSessionConfiguration.default configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil) session?.dataTask(with: url).resume() } ///继续请求网络资源(因各种原因中断时) /// - Parameters: /// - url: 网络请求地址 /// - data: 数据内容 func continuationDataRequest(with url: URL, count:Int64) { if session == nil { let configuration = URLSessionConfiguration.default configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil) }else { //取消之前的请求方法 session?.getAllTasks(completionHandler: { tasks in //取消所有方法 tasks.forEach({$0.cancel()}) }) } //设置一个网络请求 var request = URLRequest(url: url) //设置请求头内容 let bytes = "bytes=\(count)-" request.addValue(bytes, forHTTPHeaderField: "Range") request.addValue(url.host ?? "", forHTTPHeaderField: "Host") request.addValue("video/mp4", forHTTPHeaderField: "Accept") session?.dataTask(with: request).resume() } func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) { pendingRequests.remove(loadingRequest) } // MARK: URLSession ///网络请求响应体内容 func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { guard response.mimeType?.contains("video") == true else { print("\(playItem?.title ?? "")网络地址不可用,响应类型是\(response.mimeType ?? "")") //重新请求数据 MP_NetWorkManager.shared.requestPlayer(playItem?.videoId ?? "", playlistId: "") {[weak self] resourceUrls, coverUrls in //只需要重新配置第一条网络资源地址 guard let self = self else {return} print("\(playItem?.title ?? "")重新加载一次") let url = URL(string: resourceUrls.0.first ?? "")! playItem?.url = url if mediaData == nil { startDataRequest(with: url) }else { continuationDataRequest(with: url, count: Int64(mediaData!.count)) } } return } completionHandler(Foundation.URLSession.ResponseDisposition.allow) if mediaData == nil { mediaData = Data() mediaDataBlocks = [] } self.response = response processPendingRequests() } ///网络请求数据更新 func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { guard let response = dataTask.response as? HTTPURLResponse, let contentRange = response.allHeaderFields["Accept-Ranges"] as? String else { print("\(playItem?.title ?? "")数据更新失败") return } //从 Content-Range 标头中提取范围信息 let rangeInfo = extractRangeInfo(contentRange) //使用范围的起始偏移量和接收到的数据创建一个 DataSegment let dataSegment = MediaDataBlock(offset: (rangeInfo?.start ?? 0), data: data) mediaData?.append(data) mediaDataBlocks?.append(dataSegment) if mediaData != nil { //存入缓存数据 MP_CacheManager.shared.cacheData(mediaData!, dataBlocks: mediaDataBlocks!, forKey: MP_PlayerManager.shared.loadPlayer.currentVideo?.song.videoId ?? "", isComplete: false, maxCount: self.response?.expectedContentLength ?? 0) } processPendingRequests() //更新最后一次加载完成的范围量 self.lastRequestedEndOffset = Int64(data.count) } //解析 Content-Range 标头以提取字节范围 private func extractRangeInfo(_ contentRange: String) -> (start: Int64, end: Int64)? { // 正则表达式用于匹配 "bytes start-end/total" 格式 let pattern = "bytes (\\d+)-(\\d+)/(\\d+|\\*)" // 尝试创建正则表达式 guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { return nil } // 在 Content-Range 标头中查找匹配项 let nsRange = NSRange(contentRange.startIndex..(pendingRequests.compactMap { self.fillInContentInformationRequest($0.contentInformationRequest) if self.haveEnoughDataToFulfillRequest($0.dataRequest!) { $0.finishLoading() return $0 } return nil }) // 从待处理的请求中删除已完成的请求 _ = requestsFulfilled.map { self.pendingRequests.remove($0) } } ///填写contentInformationRequest func fillInContentInformationRequest(_ contentInformationRequest: AVAssetResourceLoadingContentInformationRequest?) { //如果缓存数据加载完全了 if iscompleted == true { contentInformationRequest?.contentType = "video/mp4" self.maxCount = Int64(mediaData!.count) contentInformationRequest?.contentLength = Int64(mediaData!.count) contentInformationRequest?.isByteRangeAccessSupported = true return } //通过获取响应体,填写contentInformationRequest if let responseUnwrapped = response { contentInformationRequest?.contentType = responseUnwrapped.mimeType self.maxCount = responseUnwrapped.expectedContentLength contentInformationRequest?.contentLength = responseUnwrapped.expectedContentLength contentInformationRequest?.isByteRangeAccessSupported = true }else if let maxCount = maxCount { contentInformationRequest?.contentType = "video/mp4" contentInformationRequest?.contentLength = maxCount contentInformationRequest?.isByteRangeAccessSupported = true } } ///判读请求是否有足够的数据 func haveEnoughDataToFulfillRequest(_ dataRequest: AVAssetResourceLoadingDataRequest) -> Bool { let requestedOffset = Int64(dataRequest.requestedOffset) let requestedLength = dataRequest.requestedLength let currentOffset = Int64(dataRequest.currentOffset) // 需要构建一个包含请求范围数据的Data对象 var responseData = Data() // 遍历存储的数据块,查找并拼接满足请求范围的数据块 // 假设数据块已经按照偏移量升序排序 for item in (mediaDataBlocks ?? []).sorted(by: { $0.offset < $1.offset }) { // 判断当前数据块是否在请求范围内 if item.offset < currentOffset + Int64(requestedLength) && item.offset + Int64(item.data.count) > currentOffset { // 计算数据块在请求范围内的部分,并追加到responseData let start = max(currentOffset, item.offset) - item.offset let end = min(currentOffset + Int64(requestedLength), item.offset + Int64(item.data.count)) - item.offset if start < end { // 确保范围有效 responseData.append(item.data.subdata(in: Int(start)..= requestedLength { // 使用请求范围内的数据回应dataRequest dataRequest.respond(with: responseData) return true } // 得到的数据长度不满足请求长度,返回false return false // guard let songDataUnwrapped = mediaData, // songDataUnwrapped.count > currentOffset else { // return false // } // let bytesToRespond = min(songDataUnwrapped.count - currentOffset, requestedLength) // let dataToRespond = songDataUnwrapped.subdata(in: Range(uncheckedBounds: (currentOffset, currentOffset + bytesToRespond))) // dataRequest.respond(with: dataToRespond) // return songDataUnwrapped.count >= requestedLength + requestedOffset } deinit { NotificationCenter.default.removeObserver(self) session?.invalidateAndCancel() mediaData = nil mediaDataBlocks = nil } } ///媒体数据模型 struct MediaDataBlock: Codable { ///数据偏移量 var offset:Int64 ///数据块内容 var data:Data }