diff --git a/relax.offline.mp3.music/MP/Common/Base(公用基类)/Controllers/MP_LunchViewController.swift b/relax.offline.mp3.music/MP/Common/Base(公用基类)/Controllers/MP_LunchViewController.swift index 311428e..631eb8c 100644 --- a/relax.offline.mp3.music/MP/Common/Base(公用基类)/Controllers/MP_LunchViewController.swift +++ b/relax.offline.mp3.music/MP/Common/Base(公用基类)/Controllers/MP_LunchViewController.swift @@ -33,7 +33,7 @@ class MP_LunchViewController: UIViewController, GADFullScreenContentDelegate { view.backgroundColor = .init(hex: "#000000") timer = CADisplayLink(target: self, selector: #selector(timerActionClick(_ :))) //一秒执行多少次 - timer.preferredFramesPerSecond = 40 + timer.preferredFramesPerSecond = 10 //开辟线程 timer.add(to: RunLoop.current, forMode: .common) //启动计时器 @@ -113,7 +113,7 @@ class MP_LunchViewController: UIViewController, GADFullScreenContentDelegate { @objc fileprivate func timerActionClick(_ link:CADisplayLink) { if maxTimes > currentTimes { //未加载完 - currentTimes += 0.025 + currentTimes += 0.1 let value = (currentTimes/maxTimes).isNaN ? 0:(currentTimes/maxTimes) DispatchQueue.main.async { [weak self] in diff --git a/relax.offline.mp3.music/MP/Common/Base(公用基类)/Views/MP_Lunch_ProgressView.swift b/relax.offline.mp3.music/MP/Common/Base(公用基类)/Views/MP_Lunch_ProgressView.swift index 465283d..6afa3f0 100644 --- a/relax.offline.mp3.music/MP/Common/Base(公用基类)/Views/MP_Lunch_ProgressView.swift +++ b/relax.offline.mp3.music/MP/Common/Base(公用基类)/Views/MP_Lunch_ProgressView.swift @@ -8,87 +8,72 @@ import UIKit class MP_Lunch_ProgressView: UIView { - struct Constant { - //进度条宽度 - static let lineWidth: CGFloat = 6*width - //进度槽颜色 - static let trackColor:UIColor = .init(hex: "#FFFFFF") - //进度条颜色 - static let progressColors:[CGColor] = [ - UIColor(red: 0.109, green: 0.784, blue: 0.932, alpha: 1).cgColor, - UIColor(red: 0.412, green: 0.996, blue: 0.451, alpha: 1).cgColor, - UIColor(red: 0.796, green: 0.839, blue: 0.294, alpha: 1).cgColor - ] + // 渐变层 + private var gradientLayer: CAGradientLayer! + // 进度条层(作为渐变层的遮罩) + private var progressLayer: CALayer! + + // 当前进度 (0.0 - 1.0) + var progress: CGFloat = 0.0 { + didSet { + updateProgressLayer() + } } - //渐变层 - private var gradientLayer:CAGradientLayer! - //进度槽 - private var trackLayer:CAShapeLayer! - //进度条路径(水平线) - private var path:UIBezierPath! + override init(frame: CGRect) { super.init(frame: frame) - setUpLayers() + setupGradientLayer() } required init?(coder: NSCoder) { super.init(coder: coder) - setUpLayers() + setupGradientLayer() } + override func layoutSubviews() { super.layoutSubviews() + // 保证子图层的尺寸和视图尺寸一致 + gradientLayer.frame = self.bounds + updateProgressLayer() } - override func draw(_ rect: CGRect) { - //初始化圆角矩形路径 - path = UIBezierPath() - //获取起点 - let startPoint = CGPoint(x: bounds.minX, y: bounds.maxY) - //获取终点 - let endPoint = CGPoint(x: bounds.maxX, y: bounds.maxY) - //路径起始点 - path.move(to: startPoint) - //增加路线 - path.addLine(to: endPoint) - //初始化进度槽 - trackLayer = .init() - //路径宽 - trackLayer.lineWidth = bounds.height - trackLayer.fillColor = UIColor.clear.cgColor - //路径颜色 - trackLayer.strokeColor = Constant.trackColor.cgColor - //设置起始点 - trackLayer.strokeStart = 0 - //设置当前点 - trackLayer.strokeEnd = 0 - //路径端口为圆形 - trackLayer.lineCap = .round - trackLayer.path = path.cgPath - layer.addSublayer(trackLayer) - //初始化渐变色进度条 - gradientLayer = .init() - //渐变层大小 - gradientLayer.frame = .init(x: 0, y: -2, width: bounds.size.width, height: bounds.size.height+1) -// gradientLayer.shadowPath = trackLayer.path - //渐变层颜色 - gradientLayer.colors = Constant.progressColors - //渐变层起始点 - gradientLayer.startPoint = .init(x: 0, y: 1) - //渐变层终点 - gradientLayer.endPoint = .init(x: 1, y: 1) + + private func setupGradientLayer() { + backgroundColor = .init(hex: "#FFFFFF",alpha: 0.1) + // 初始化并设置渐变层 + gradientLayer = CAGradientLayer() + gradientLayer.colors = [ + UIColor(red: 0.109, green: 0.784, blue: 0.932, alpha: 1).cgColor, + UIColor(red: 0.412, green: 0.996, blue: 0.451, alpha: 1).cgColor, + UIColor(red: 0.796, green: 0.839, blue: 0.294, alpha: 1).cgColor + ] + gradientLayer.startPoint = CGPoint(x: 0, y: 0.5) + gradientLayer.endPoint = CGPoint(x: 1, y: 0.5) layer.addSublayer(gradientLayer) - //渐变层遮罩 - gradientLayer.mask = trackLayer + + // 初始化进度条层并设置为渐变层的遮罩 + progressLayer = CALayer() + progressLayer.backgroundColor = UIColor.white.cgColor // 颜色可以是任意的,因为这个图层只是用来作为形状遮罩 + gradientLayer.mask = progressLayer } - //layer添加 - private func setUpLayers() { - backgroundColor = .init(hex: "#FFFFFF", alpha: 0.15) + + private func updateProgressLayer() { + // 根据当前进度和视图的宽度调整进度条层的大小 + let progressWidth = self.bounds.width * progress + progressLayer.frame = CGRect(x: 0, y: 0, width: progressWidth, height: self.bounds.height) } - /// 渐变色执行方法 - /// - Parameter progress: 进度值 - func setProgress(_ progress: CGFloat) { - if progress <= 1 { - //进度条动画 - trackLayer.strokeEnd = progress + + // 当需要设置进度时,调用此方法 + func setProgress(_ progress: CGFloat, animated: Bool = false) { + self.progress = min(max(progress, 0.0), 1.0) // 确保进度值在有效范围内 + if animated { + let animation = CABasicAnimation(keyPath: "bounds.size.width") + animation.toValue = self.bounds.width * self.progress + animation.duration = 0.1 // 动画持续时间 + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + animation.fillMode = .forwards + animation.isRemovedOnCompletion = false + progressLayer.add(animation, forKey: "progressWidth") } } } + diff --git a/relax.offline.mp3.music/MP/Common/Extension(扩展)/Notification.swift b/relax.offline.mp3.music/MP/Common/Extension(扩展)/Notification.swift index 4bcedf2..7e5738d 100644 --- a/relax.offline.mp3.music/MP/Common/Extension(扩展)/Notification.swift +++ b/relax.offline.mp3.music/MP/Common/Extension(扩展)/Notification.swift @@ -97,6 +97,16 @@ extension NotificationCenter{ case positive_nav_pop ///b面网络请求报错 case netWork_error_deal + ///b面媒体资源得到服务器响应 + case asset_response + ///b面媒体资源从服务器接收到数据 + case asset_receiveData + ///b面媒体资源缓存结果 + case asset_isCached + ///b面媒体资源是否报错 + case asset_errorCode + ///b面播放器403事件 + case player_asset_403 } } } diff --git a/relax.offline.mp3.music/MP/Common/Tool(工具封装)/MP_AVURLAsset.swift b/relax.offline.mp3.music/MP/Common/Tool(工具封装)/MP_AVURLAsset.swift index bda0a2f..126f5ad 100644 --- a/relax.offline.mp3.music/MP/Common/Tool(工具封装)/MP_AVURLAsset.swift +++ b/relax.offline.mp3.music/MP/Common/Tool(工具封装)/MP_AVURLAsset.swift @@ -7,166 +7,70 @@ 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 - } -} -///自定义媒体资产 +import MobileCoreServices + +/////自定义媒体资产 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) + //加载器 + private var playerResourceLoader:MP_PlayerResourceLoader? + //标题 + private var title:String + //加载队列 + private var assetQueue:DispatchQueue! + ///加载网络媒体资源 + init(_ url:URL, videoId:String, title:String) { + self.title = title + //判断是否缓存区中是否存在这个媒体资源 + let path = MP_CacheAndArchiverManager.shared.createCachePath() ?? "" + let audioName = "/"+videoId+".mp4" + //缓存文件地址 + let audioPath = path+audioName + var customURL:URL! + //判断这个文件是否存在 + if MP_CacheAndArchiverManager.shared.fileManager.fileExists(atPath: audioPath) { + //存在,使用缓存文件播放 + 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 = self?.statusOfValue(forKey: key, error: &error) + let status = statusOfValue(forKey: key, error: &error) if status == .loaded { print("开始对\(title)的预加载") } else { @@ -179,323 +83,544 @@ class MP_AVURLAsset: AVURLAsset { } } ///媒体资源加载代理 -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 +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() - //添加关于用手动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") + //添加监听 + 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 = [] } - //判断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 + + } + 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 { - 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) - } + //数据还没缓存到请求范围,等待数据下载 + 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]! + //默认任务最大数 + private var maxCount = 4 + override init() { + super.init() + //实现会话 +// let operationQueue = OperationQueue() +// //同时支持最多5个任务并发执行 +// operationQueue.maxConcurrentOperationCount = 4 + let configuration = URLSessionConfiguration.default + //设置回话超时时间 + configuration.timeoutIntervalForRequest = 30 + configuration.timeoutIntervalForResource = 30 + //实现会话 + 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 + } + //创建请求 + var 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) + 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 { - print("开始网络请求\(playItem?.title ?? "")相关数据") - //初次请求 - startDataRequest(with: initialUrl) + //没有报错,下载完成 + let success = MP_CacheAndArchiverManager.shared.moveAudioFileFromTempPathToCachePath(videoId) +// self.delegate?.requestManagerIsCached(success, videoId: videoId) + NotificationCenter.notificationKey.post(notificationName: .asset_isCached, object: ["videoId":videoId]) } - }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)) + //重试机制 + 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() } } } } - - - /// 开始网络请求 - /// - 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 } diff --git a/relax.offline.mp3.music/MP/Common/Tool(工具封装)/MP_CacheManager.swift b/relax.offline.mp3.music/MP/Common/Tool(工具封装)/MP_CacheManager.swift index f3a3e43..80dd1e0 100644 --- a/relax.offline.mp3.music/MP/Common/Tool(工具封装)/MP_CacheManager.swift +++ b/relax.offline.mp3.music/MP/Common/Tool(工具封装)/MP_CacheManager.swift @@ -6,138 +6,185 @@ // import Foundation -///缓存实体资源 -class CachedMedia: NSObject, Codable { - //数据 - let data: Data - //数据块 - let dataBlocks:[MediaDataBlock] - //是否完整 - let isComplete: Bool - //最大长度 - let maxCount:Int64 - init(data: Data, dataBlocks:[MediaDataBlock], isComplete: Bool, maxCount:Int64) { - self.data = data - self.dataBlocks = dataBlocks - self.isComplete = isComplete - self.maxCount = maxCount - } -} -///缓存管理工具 -class MP_CacheManager { +///缓存/归档管理工具 +class MP_CacheAndArchiverManager { // 单例模式,提供全局访问点 - static let shared = MP_CacheManager() - // 缓存实体,字典形式,键值1为videoID,键值2为对应的资源数据 - private let memoryCache = NSCache() + static let shared = MP_CacheAndArchiverManager() //文件管理器 - private let fileManager = FileManager.default - //缓存文件地址 - private let cacheDirectory: URL - //缓存专用线程 - private var cacheQueue = DispatchQueue(label: "com.MP_CacheManager.cacheQueue") - //缓存专用 - private var cacheOperations: [String: DispatchWorkItem] = [:] - //专用节流时间间隔 - private let throttleInterval: TimeInterval = 1.0 - //固定缓存时间 - private let expirationInterval: TimeInterval = 86400 // 24小时 + let fileManager = FileManager.default + ///归档指定数据字典 +// private var archiverDic:[String: MP_PlayerRequestModel]! + ///缓存状态更新 + private var isCompleteds:[String:Bool] = [:] private init() { - //获取全部文件地址 - let urls = fileManager.urls(for: .cachesDirectory, in: .userDomainMask) - //设置缓存文件地址 - cacheDirectory = urls[0].appendingPathComponent("MyAssetCacheManager") - //检查缓存文件地址是否存在 - if !fileManager.fileExists(atPath: cacheDirectory.path) { - //不存在就创建一个 - try? fileManager.createDirectory(at: cacheDirectory, withIntermediateDirectories: true, attributes: nil) - } + } - - /// 保存缓存数据 - /// - Parameters: - /// - data: 数据本身 - /// - key: 使用videoId作为对应的键值 - func cacheData(_ data: Data, dataBlocks:[MediaDataBlock], forKey key: String, isComplete: Bool = false, maxCount:Int64) { - // 在你的 cacheQueue 里执行操作来确保线程安全 - cacheQueue.async { [weak self] in - // 取消这个 key 的任何现有缓存操作 - self?.cacheOperations[key]?.cancel() - - // 创建一个新的缓存操作 - let operation = DispatchWorkItem { - self?.performCacheData(data, dataBlocks: dataBlocks, forKey: key, isComplete: isComplete, maxCount: maxCount) - // 甚至删除值也包含在cacheQueue的异步调用中 - self?.cacheQueue.async { - self?.cacheOperations.removeValue(forKey: key) - } - } - - // 把 operation 保存进字典 - self?.cacheOperations[key] = operation - - // 安排 operation 在节流间隔后运行 - self?.cacheQueue.asyncAfter(deadline: .now() + self!.throttleInterval, execute: operation) + ///创建播放器缓存文件 + func createCachePath() -> String? { + // 所有缓存文件都放在了沙盒Cache文件夹下MP_PlayerCache文件夹里 + let cacheDirectory = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first! + let path = cacheDirectory+"/"+"MP_PlayerCache" + //检索这个缓存文件夹是否存在 + guard fileManager.fileExists(atPath: path) == false else { + //存在,直接返回这个文件夹 + return path } - } - //保存缓存数据 - private func performCacheData(_ data: Data, dataBlocks:[MediaDataBlock], forKey key: String, isComplete: Bool = false, maxCount:Int64) { - let contentToCache = CachedMedia(data: data, dataBlocks: dataBlocks, isComplete: isComplete, maxCount: maxCount) - memoryCache.setObject(contentToCache, forKey: key as NSString) - //文件路径 - let fileURL = self.cacheDirectory.appendingPathComponent(key) - //临时文件 - let tempURL = fileURL.appendingPathExtension("tmp") - let encoder = JSONEncoder() + //不存在 do { - let newData = try encoder.encode(contentToCache) - // 写入到文件 - try newData.write(to: tempURL, options: .atomicWrite) - // 检查目标位置是否已经存在文件,如果存在,则删除 - if self.fileManager.fileExists(atPath: fileURL.path) { - try self.fileManager.removeItem(at: fileURL) - } - // 然后将临时文件移动到最终位置 - try self.fileManager.moveItem(at: tempURL, to: fileURL) - // 设置过期时间元数据 - let expirationDate = Date().addingTimeInterval(self.expirationInterval) - try self.fileManager.setAttributes([.modificationDate: expirationDate], ofItemAtPath: fileURL.path) + //创建这个缓存文件夹 + try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) + return path } catch { - // 如果发生错误,删除临时文件 - try? self.fileManager.removeItem(at: tempURL) - print("无法将密钥数据写入磁盘 \(key) - 错误: \(error)") - } - } - /// 取出数据 - /// - Parameter key: 使用videoId作为对应的键值 - /// - Returns: 返回的数据 - func data(forKey key: String) -> CachedMedia? { - if let cachedContent = memoryCache.object(forKey: key as NSString){ - return cachedContent - } - return cacheQueue.sync { - [weak self] in - guard let self = self else { return nil } - let fileURL = self.cacheDirectory.appendingPathComponent(key) - guard let jsonData = try? Data(contentsOf: fileURL) else { return nil } - // 将从磁盘中读取到的 JSON 数据反序列化为 CachedMedia 对象 - let decoder = JSONDecoder() - do{ - let cachedContent = try decoder.decode(CachedMedia.self, from: jsonData) - // 检查数据是否过期(同时可以在此处加入完整性检查) - let attributes = try fileManager.attributesOfItem(atPath: fileURL.path) - guard let modificationDate = attributes[.modificationDate] as? Date else { return nil } - if Date().timeIntervalSince(modificationDate) < expirationInterval{ - // 数据未过期,返回数据并更新内存缓存 - memoryCache.setObject(cachedContent, forKey: key as NSString) - return cachedContent - } else { - // 数据已过期,删除缓存文件 - try fileManager.removeItem(at: fileURL) - } - } catch { - print("读取缓存数据失败 \(key) - 错误: \(error)") - } + //创建失败 + print("创建播放器缓存文件夹失败,失败原因:\(error)") return nil } } + ///创建一个临时文件 + func createTempFile(_ videoID:String) -> Bool { + //设置临时文件 + let path = tempPath(videoID) + if fileManager.fileExists(atPath: path) { + //存在这个临时文件,将其删除 + do{ + try fileManager.removeItem(atPath: path) + }catch { + print("删除临时文件失败,error: \(error)") + } + } + //生成临时文件 + return fileManager.createFile(atPath: path, contents: nil, attributes: nil) + } + ///往临时文件中写入缓存数据 + func writeDataToAudioFileTempPathWithData(_ data:Data, videoId:String) { + guard let handle = FileHandle(forWritingAtPath: tempPath(videoId)) else {return} + if #available(iOS 13.4, *) { + do{ + try handle.seekToEnd() + }catch { + print("Seek到末尾失败,失败原因:\(error)") + } + } else { + handle.seekToEndOfFile() + } + //在最后写入数据 + if #available(iOS 13.4, *) { + do{ + try handle.write(contentsOf: data) + }catch{ + print("写入数据失败,失败原因:\(error)") + } + } else { + handle.write(data) + } + do { + if #available(iOS 13.4, *) { + try? handle.close() // 尝试关闭文件句柄 + } else { + handle.closeFile() + } + } + } + ///读取临时文件数据 + func readTempFileDataWithOffset(_ offset:UInt64, length:Int, videoId:String) -> Data? { + var allhandle:FileHandle! + //检索当前数据是否下载完毕了 + if isCompleteds[videoId] == true { + //临时文件已经销毁,当前下载内容已经移动到缓存文件中,从缓存文件中读取 + let path = audioCachedPath() ?? "" + let audioName = "/"+videoId+".mp4" + let cachePath = path+audioName + guard let handle = FileHandle(forReadingAtPath: cachePath) else {return nil} + allhandle = handle + }else { + let path = tempPath(videoId) + guard let handle = FileHandle(forReadingAtPath: path) else {return nil} + allhandle = handle + } + if #available(iOS 13.0, *) { + do{ + try allhandle.seek(toOffset: offset) + }catch{ + print("Seek到指定位置失败,失败原因:\(error)") + try? allhandle.close() + } + } else { + allhandle.seek(toFileOffset: offset) + } + if #available(iOS 13.4, *) { + do{ + let data = try allhandle.read(upToCount: length) + try allhandle.close() + return data + }catch{ + print("读取到指定位置失败,失败原因:\(error)") + return nil + } + } else { + let data = allhandle.readData(ofLength: length) + allhandle.closeFile() + return data + } + } + + /// 保存临时文件到缓存文件夹 + /// - Parameter videoId: 使用videoId作为文件名 + func moveAudioFileFromTempPathToCachePath(_ videoId: String) -> Bool { + guard let path = audioCachedPath() else {return false } + // 1. 判断文件夹是否存在 + if !(fileManager.fileExists(atPath: path)) { + //不存在 + do{ + try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true) + }catch { + print("创建缓存夹失败,失败原因:\(error)") + return false + } + } + let audioName = "/"+videoId+".mp4" + let cachePath = path+audioName + // 检查目标文件是否存在,如果存在,则删除 + if fileManager.fileExists(atPath: cachePath) { +// do { +// print("缓存文件已经存在") +// try fileManager.removeItem(atPath: cachePath) +// } catch { +// print("删除现有文件失败,失败原因: \(error)") +// return false +// } + return true + } + // 2. 确保源文件存在 + let tempFilePath = tempPath(videoId) + guard fileManager.fileExists(atPath: tempFilePath) else { +// print("源文件不存在于临时路径: \(tempFilePath)") + return false + } + // 3. 尝试移动文件 + do { + try fileManager.moveItem(atPath: tempFilePath, toPath: cachePath) +// print("\(videoId)下载完毕,移动到:\(cachePath)") + //当前加载器加载完成 + isCompleteds[videoId] = true + return true + } catch { + print("移动 \(videoId) 缓存文件失败,失败原因: \(error)") +// deleteKeyValueIfHaveArchivedWithVideoId(videoId) // 执行额外的清理操作 + return false + } + } + ///生成缓存文件 + private func audioCachedPath() -> String? { + return (createCachePath() ?? "") + } + + ///创建一个固定的临时文件路径 + private func tempPath(_ videoID:String) -> String { + return (NSTemporaryDirectory())+"/"+"\(videoID).mp4" + } + ///创建一个固定的归档文件路径 + private func archiverPath() -> String { + return ((createCachePath() ?? ""))+"/"+"MP_Player.archiver" + } + } diff --git a/relax.offline.mp3.music/MP/Common/Tool(工具封装)/MP_NetWorkManager.swift b/relax.offline.mp3.music/MP/Common/Tool(工具封装)/MP_NetWorkManager.swift index 09e7711..177df52 100644 --- a/relax.offline.mp3.music/MP/Common/Tool(工具封装)/MP_NetWorkManager.swift +++ b/relax.offline.mp3.music/MP/Common/Tool(工具封装)/MP_NetWorkManager.swift @@ -38,13 +38,11 @@ class MP_NetWorkManager: NSObject { configuration.httpMaximumConnectionsPerHost = 4 return Alamofire.Session(configuration: configuration, interceptor: MP_CustomRetrier()) }() + ///播放相关内容ID和歌词ID记录组 + private var relatedRequests: [String:DataRequest] = [:] ///播放资源请求记录组 - private var playerRequests: [PlayerRequest] = [] - ///播放资源请求结构体 - struct PlayerRequest { - let request: DataRequest - let onCancel: (() -> Void) - } + private var playerRequests: [String:DataRequest] = [:] + //MARK: - API接口 ///IP获取 private let iPInfo:String = "https://api.tikustok.com/app/common/getIPInfo" @@ -665,14 +663,14 @@ extension MP_NetWorkManager { ] ] //guard netWorkStatu != .notReachable else {return} - requestPostNextLyricsAndRelated(url, parameters: parameters) { result in + requestPostNextLyricsAndRelated(url, videoId: item.videoId, parameters: parameters) { result in completion(result) } } //请求请求Next歌词/相关内容 - private func requestPostNextLyricsAndRelated(_ url:URL, parameters:Parameters, completion:@escaping(((String?,String?)) -> Void)) { + private func requestPostNextLyricsAndRelated(_ url:URL, videoId:String, parameters:Parameters, completion:@escaping(((String?,String?)) -> Void)) { //发送post请求 - MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonNext.self) { [weak self] (response) in + let request = MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonNext.self) { [weak self] (response) in guard let self = self else {return} switch response.result { case .success(let value): @@ -684,6 +682,7 @@ extension MP_NetWorkManager { handleError(url, error: error) } } + relatedRequests[videoId] = request } //MARK: - 请求player播放资源 @@ -713,21 +712,11 @@ extension MP_NetWorkManager { ] ] //guard netWorkStatu != .notReachable else {return} - requestAndroidPostPlayer(url, parameters: parameters){ resourceUlrs, coverUrls in + requestAndroidPostPlayer(url, videoId: videoId, parameters: parameters){ resourceUlrs, coverUrls in completion(resourceUlrs, coverUrls) } } - private func requestAndroidPostPlayer(_ url:URL, parameters:Parameters, completion:@escaping((([String],[Int],[String])?, [String]?) -> Void)) { - //删除已完成或者取消的请求 - playerRequests = playerRequests.filter({$0.request.task?.state == .running}) - // 检查活跃请求的数量,如果达到上限,则取消第一个请求 - if playerRequests.count >= 4, let requestToCancel = playerRequests.first { - requestToCancel.request.cancel() - //执行请求取消 - requestToCancel.onCancel() - playerRequests.removeFirst() - print("取消多余的Player资源请求: \(requestToCancel)") - } + private func requestAndroidPostPlayer(_ url:URL, videoId:String, parameters:Parameters, completion:@escaping((([String],[Int],[String])?, [String]?) -> Void)) { //发送post请求 let request = PlayerSeesion.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonAndroidPlayer.self) { [weak self] (response) in guard let self = self else {return} @@ -741,10 +730,15 @@ extension MP_NetWorkManager { handleError(url, error: error) } } - // 将新请求添加到追踪数组 - playerRequests.append(.init(request: request, onCancel: { - completion(nil,nil) - })) + //根据当前videoId添加request + playerRequests[videoId] = request + } + ///移除补全任务请求任务 + func removeVideoResource(_ videoId: String) { + playerRequests[videoId]?.cancel() + playerRequests.removeValue(forKey: videoId) + relatedRequests[videoId]?.cancel() + relatedRequests.removeValue(forKey: videoId) } // func requestPlayer(_ videoId: String, playlistId: String, completion:@escaping ((([String],[Float],[String]), [String]?) -> Void)){ // //拼接出player路径 diff --git a/relax.offline.mp3.music/MP/Common/Tool(工具封装)/MP_PlayerManager.swift b/relax.offline.mp3.music/MP/Common/Tool(工具封装)/MP_PlayerManager.swift index a21a37a..1bca16f 100644 --- a/relax.offline.mp3.music/MP/Common/Tool(工具封装)/MP_PlayerManager.swift +++ b/relax.offline.mp3.music/MP/Common/Tool(工具封装)/MP_PlayerManager.swift @@ -142,6 +142,14 @@ class MP_PlayerManager:NSObject{ var runActionBlock:MP_PlayTimerRunAction! ///播放器缓存值闭包 var cacheValueBlock:MP_PlayCacheValueAction! + ///播放实例状态的监听器 + private var statusObservation:NSKeyValueObservation? + ///播放实例缓存值的监听器 + private var loadedTimeRangesObservation:NSKeyValueObservation? + ///播放实例是否支持播放的监听器 + private var playbackLikelyToKeepUpObservation:NSKeyValueObservation? + ///播放实例报错的监听器 + private var errorObservation:NSKeyValueObservation? private override init() { super.init() //初始化计时器 @@ -230,11 +238,66 @@ class MP_PlayerManager:NSObject{ //对当前播放PlayerItem设置监听状态 if loadPlayer.currentVideo?.isKVO == false { //准备状态 - loadPlayer?.currentVideo?.resourcePlayerItem?.addObserver(self, forKeyPath: "status", options: [.old,.new], context: nil) + statusObservation?.invalidate() + statusObservation = loadPlayer?.currentVideo?.resourcePlayerItem?.observe(\.status, options: [.old,.new], changeHandler: { [weak self] item, change in + guard let self = self else {return} + if item.status == .readyToPlay { + //判断当前播放器是否在播放当前音乐中 + if playState != .Playing { + //当statuVlaue值等于playerItem准备播放的值,说明已经准备好播放 + print("当前音乐-\(loadPlayer?.currentVideo?.title ?? "") 已经准备好播放") + } + }else { + print("当前音乐-\(loadPlayer.currentVideo?.title ?? "") 未做好准备播放,失败原因是\(loadPlayer.currentVideo?.resourcePlayerItem.error?.localizedDescription ?? "")") + if loadPlayer?.currentVideo?.isKVO == true { + suspendTimer() + MP_AnalyticsManager.shared.player_b_failure_errorAction(loadPlayer?.currentVideo?.song.videoId ?? "", videoname: loadPlayer?.currentVideo?.title ?? "", artistname: loadPlayer?.currentVideo?.song.shortBylineText ?? "", error: loadPlayer.currentVideo?.resourcePlayerItem.error?.localizedDescription ?? "") + loadPlayer?.currentVideo?.isKVO = false + //重新配置数据 + loadPlayer.remakeImproveData { + [weak self] in + self?.play() + } + } + } + }) //当前缓冲值 - loadPlayer?.currentVideo?.resourcePlayerItem?.addObserver(self, forKeyPath: "loadedTimeRanges", options: [.old,.new], context: nil) + loadedTimeRangesObservation?.invalidate() + loadedTimeRangesObservation = loadPlayer?.currentVideo?.resourcePlayerItem?.observe(\.loadedTimeRanges, options: [.old,.new], changeHandler: { [weak self] item, change in + guard let self = self else {return} + cacheLoadTimes() + }) //是否具备足够播放的能力 - loadPlayer?.currentVideo?.resourcePlayerItem?.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: [.old,.new], context: nil) + playbackLikelyToKeepUpObservation?.invalidate() + playbackLikelyToKeepUpObservation = loadPlayer?.currentVideo?.resourcePlayerItem?.observe(\.isPlaybackLikelyToKeepUp, options: [.old,.new], changeHandler: { [weak self] item, change in + guard let self = self else {return} + if let playbackLikelyToKeepUp = change.newValue, playbackLikelyToKeepUp == true { + if playState != .Playing && playState != .Pause { + //还未播放当前音乐,启动播放 + player.play() + playState = .Playing + //暂停计时器,并获取延时值 + suspendTimer() + MP_AnalyticsManager.shared.player_b_success_actionAction(loadPlayer?.currentVideo?.song.videoId ?? "", videoname: loadPlayer?.currentVideo?.title ?? "", artistname: loadPlayer?.currentVideo?.song.shortBylineText ?? "") + //执行开始播放闭包 + if startActionBlock != nil { + startActionBlock!() + } + } + }else { + //没有足够的数据支持播放 + player.pause() + playState = .Null + } + }) + //报错提醒 + errorObservation?.invalidate() + errorObservation = loadPlayer?.currentVideo?.resourcePlayerItem?.observe(\.error, options: [.old,.new], changeHandler: { [weak self] item, change in + guard let self = self else {return} + if let error = change.newValue, let nsError = error { + print("当前音乐-\(loadPlayer?.currentVideo?.title ?? "") 未做好准备播放,失败原因是\(nsError.localizedDescription)") + } + }) loadPlayer.currentVideo.isKVO = true //将进度回归为0 player.seek(to: .zero) @@ -305,65 +368,66 @@ class MP_PlayerManager:NSObject{ // } //实现KVO监听 - override 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 { - //判断当前播放器是否在播放当前音乐中 - if playState != .Playing { - //当statuVlaue值等于playerItem准备播放的值,说明已经准备好播放 - print("当前音乐-\(loadPlayer?.currentVideo?.title ?? "") 已经准备好播放") - } - }else { - print("当前音乐-\(loadPlayer.currentVideo?.title ?? "") 未做好准备播放,失败原因是\(loadPlayer.currentVideo?.resourcePlayerItem.error?.localizedDescription ?? "")") - if loadPlayer?.currentVideo?.isKVO == true { - suspendTimer() - MP_AnalyticsManager.shared.player_b_failure_errorAction(loadPlayer?.currentVideo?.song.videoId ?? "", videoname: loadPlayer?.currentVideo?.title ?? "", artistname: loadPlayer?.currentVideo?.song.shortBylineText ?? "", error: loadPlayer.currentVideo?.resourcePlayerItem.error?.localizedDescription ?? "") - //切歌时移除KVO监听 - loadPlayer?.currentVideo?.resourcePlayerItem.removeObserver(self, forKeyPath: "status") - loadPlayer?.currentVideo?.resourcePlayerItem.removeObserver(self, forKeyPath: "loadedTimeRanges") - loadPlayer?.currentVideo?.resourcePlayerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp") - loadPlayer?.currentVideo?.isKVO = false - //重新配置数据 - loadPlayer.remakeImproveData { - [weak self] in - self?.play() - } - } - } - case "loadedTimeRanges"://当前缓冲进度 - cacheLoadTimes() - case "playbackLikelyToKeepUp"://是否存在足够的数据开始播放 - if let playbackLikelyToKeepUp = change?[.newKey] as? Bool, playbackLikelyToKeepUp == true { - if playState != .Playing && playState != .Pause { - //还未播放当前音乐,启动播放 - player.play() - playState = .Playing - //暂停计时器,并获取延时值 - suspendTimer() - MP_AnalyticsManager.shared.player_b_success_actionAction(loadPlayer?.currentVideo?.song.videoId ?? "", videoname: loadPlayer?.currentVideo?.title ?? "", artistname: loadPlayer?.currentVideo?.song.shortBylineText ?? "") - //执行开始播放闭包 - if startActionBlock != nil { - startActionBlock!() - } - } - }else { - //没有足够的数据支持播放 - player.pause() - playState = .Null - } - default: - break - } - } +// override 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 { +// //判断当前播放器是否在播放当前音乐中 +// if playState != .Playing { +// //当statuVlaue值等于playerItem准备播放的值,说明已经准备好播放 +// print("当前音乐-\(loadPlayer?.currentVideo?.title ?? "") 已经准备好播放") +// } +// }else { +// print("当前音乐-\(loadPlayer.currentVideo?.title ?? "") 未做好准备播放,失败原因是\(loadPlayer.currentVideo?.resourcePlayerItem.error?.localizedDescription ?? "")") +// if loadPlayer?.currentVideo?.isKVO == true { +// suspendTimer() +// MP_AnalyticsManager.shared.player_b_failure_errorAction(loadPlayer?.currentVideo?.song.videoId ?? "", videoname: loadPlayer?.currentVideo?.title ?? "", artistname: loadPlayer?.currentVideo?.song.shortBylineText ?? "", error: loadPlayer.currentVideo?.resourcePlayerItem.error?.localizedDescription ?? "") +// //切歌时移除KVO监听 +// loadPlayer?.currentVideo?.resourcePlayerItem.removeObserver(self, forKeyPath: "status") +// loadPlayer?.currentVideo?.resourcePlayerItem.removeObserver(self, forKeyPath: "loadedTimeRanges") +// loadPlayer?.currentVideo?.resourcePlayerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp") +// loadPlayer?.currentVideo?.resourcePlayerItem.removeObserver(self, forKeyPath: "error") +// loadPlayer?.currentVideo?.isKVO = false +// //重新配置数据 +// loadPlayer.remakeImproveData { +// [weak self] in +// self?.play() +// } +// } +// } +// case "loadedTimeRanges"://当前缓冲进度 +// cacheLoadTimes() +// case "playbackLikelyToKeepUp"://是否存在足够的数据开始播放 +// if let playbackLikelyToKeepUp = change?[.newKey] as? Bool, playbackLikelyToKeepUp == true { +// if playState != .Playing && playState != .Pause { +// //还未播放当前音乐,启动播放 +// player.play() +// playState = .Playing +// //暂停计时器,并获取延时值 +// suspendTimer() +// MP_AnalyticsManager.shared.player_b_success_actionAction(loadPlayer?.currentVideo?.song.videoId ?? "", videoname: loadPlayer?.currentVideo?.title ?? "", artistname: loadPlayer?.currentVideo?.song.shortBylineText ?? "") +// //执行开始播放闭包 +// if startActionBlock != nil { +// startActionBlock!() +// } +// } +// }else { +// //没有足够的数据支持播放 +// player.pause() +// playState = .Null +// } +// default: +// break +// } +// } //获取缓冲值 private func cacheLoadTimes() { //获取当前播放Item的缓冲值组 - if let timeRanges = loadPlayer?.currentVideo?.resourcePlayerItem.loadedTimeRanges.map({$0.timeRangeValue}), let first = timeRanges.first { + if let timeRanges = loadPlayer?.currentVideo?.resourcePlayerItem?.loadedTimeRanges.map({$0.timeRangeValue}), let first = timeRanges.first { //获取开始时间的秒数 let startSeconds = first.start.seconds //获取缓冲区的持续时间 @@ -597,9 +661,10 @@ class MP_PlayerManager:NSObject{ if let video = sender.object as? MPPositive_SongViewModel { if video.isKVO == true { //切歌时移除KVO监听 - video.resourcePlayerItem.removeObserver(self, forKeyPath: "status") - video.resourcePlayerItem.removeObserver(self, forKeyPath: "loadedTimeRanges") - video.resourcePlayerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp") + statusObservation?.invalidate() + playbackLikelyToKeepUpObservation?.invalidate() + errorObservation?.invalidate() + loadedTimeRangesObservation?.invalidate() video.isKVO = false } } diff --git a/relax.offline.mp3.music/MP/MPPositive/Models/ViewModels/ListViewModels/MPPositive_SongViewModel.swift b/relax.offline.mp3.music/MP/MPPositive/Models/ViewModels/ListViewModels/MPPositive_SongViewModel.swift index 62c63ce..b8f7b92 100644 --- a/relax.offline.mp3.music/MP/MPPositive/Models/ViewModels/ListViewModels/MPPositive_SongViewModel.swift +++ b/relax.offline.mp3.music/MP/MPPositive/Models/ViewModels/ListViewModels/MPPositive_SongViewModel.swift @@ -13,9 +13,9 @@ class MPPositive_SongViewModel: NSObject { ///排序号 var index:Int! ///播放实例 - var resourcePlayerItem:AVPlayerItem! + @objc dynamic var resourcePlayerItem:AVPlayerItem! ///播放媒体 - var resourcePlayerAsset:AVURLAsset! + var resourcePlayerAsset:MP_AVURLAsset! ///播放路径 var resourcePlayerURL:URL! ///封面 @@ -40,11 +40,16 @@ class MPPositive_SongViewModel: NSObject { var song:MPPositive_SongItemModel! init(_ song:MPPositive_SongItemModel) { super.init() + NotificationCenter.notificationKey.add(observer: self, selector: #selector(switchURL403Action(_ :)), notificationName: .player_asset_403) self.song = song configure() } deinit { - print("\(title ?? "")已经销毁了") + print("\(title ?? "")被销毁了") + NotificationCenter.default.removeObserver(self) + //同步取消任务 + MP_NetWorkManager.shared.removeVideoResource(song.videoId) + //销毁实例 resourcePlayerItem = nil resourcePlayerAsset = nil resourcePlayerURL = nil @@ -62,97 +67,94 @@ class MPPositive_SongViewModel: NSObject { if song.shortBylineText != nil { subtitle = song.shortBylineText! } - if let first = song.resourceUrls?.first { - resourcePlayerURL = .init(string:first) - resourcePlayerAsset = .init(url: resourcePlayerURL) - preloadAsset(resourcePlayerAsset) - resourcePlayerItem = .init(asset: resourcePlayerAsset) - } - //封面路径默认取最后一条 if song.reviewUrls?.first != nil { coverUrl = .init(string: song.reviewUrls!.last!) } - //歌词 - if song.lyrics != nil { - lyrics = song.lyrics + //创建队列组 + let group = DispatchGroup() + //执行数据补全 + group.enter() + //补全歌词id和相关内容id + if song.lyricsID == nil || song.relatedID == nil { + //需要网络请求补全数据 + improveDataforLycirsAndRelated(song) {[weak self] (result) in + guard let self = self else {return} + song.lyricsID = result.0 + song.relatedID = result.1 + group.leave() + } }else { - if song.lyricsID != nil { - //执行网络请求拿到歌词数据 - MP_NetWorkManager.shared.requestLyric(song.lyricsID!) {[weak self] lyrics in - self?.lyrics = lyrics - self?.song.lyrics = lyrics + //歌词id和相关内容id是完整的 + group.leave() + } + group.enter() + //补全资源路径和封面路径 + //判断当前videoID是否进行过下载 + if let resource = getDocumentsFileURL(song.videoId) { + //下载过,将资源添加 + song.resourceUrls = [resource] + group.leave() + }else { + //没有下载过 + //调用网络请求补全数据 + improveDataforResouceAndCover(song) { [weak self] resourceUrls, coverUrls in + guard let self = self else {return} + if let resourceUrls = resourceUrls { + song.resourceUrls = resourceUrls.0 + song.itags = resourceUrls.1 + song.mimeTypes = resourceUrls.2 } + song.coverUrls = coverUrls + group.leave() } } - //相关内容 - if song.relatedID != nil { - relatedId = song.relatedID + //当所有内容都修改完毕后 + group.notify(queue: .main) { + [weak self] in + guard let self = self else {return} + //更新播放资源 + if let first = song.resourceUrls?.first { + resourcePlayerURL = .init(string:first) + if isDlownd == true { + //下载了 + resourcePlayerAsset = .init(LocalURL: resourcePlayerURL!, videoId: song.videoId, title: title ?? "") + }else { + //未下载 + resourcePlayerAsset = .init(resourcePlayerURL!, videoId: song.videoId, title: title ?? "") + } + //装填数据 + print("成功装填了\(song.title ?? "")的媒体数据") + resourcePlayerItem = .init(asset: resourcePlayerAsset) + } + //更新歌词 + if song.lyrics != nil { + lyrics = song.lyrics + }else { + if song.lyricsID != nil { + //执行网络请求拿到歌词数据 + MP_NetWorkManager.shared.requestLyric(song.lyricsID!) {[weak self] lyrics in + self?.lyrics = lyrics + self?.song.lyrics = lyrics + } + } + } + //更新相关内容 + if song.relatedID != nil { + relatedId = song.relatedID + } } } //页面状态更新 func reloadCollectionAndDownLoad() { //检索是否收藏 isCollection = MPPositive_CollectionSongModel.fetch(.init(format: "videoId == %@", song.videoId)).count != 0 - //检索是否下载 isDlownd = MPPositive_DownloadItemModel.fetch(.init(format: "videoId == %@", song.videoId)).count != 0 } - //执行预加载 - func preloadAsset(_ asset:AVURLAsset) { - guard isPreload == false else { - return - } - print("\(title ?? "")开始预加载") - //执行预加载 - if #available(iOS 16, *) { - //ios16以上的情况 - Task{ - do{ - let playable = try await asset.load(.isPlayable) - if playable == true { - print("\(self.title ?? "")预加载成功") - isPreload = true - }else { - //检索预加载失败原因 - switch asset.status(of: .isPlayable) { - case .failed(let erro): - print("\(title ?? "")预加载失败,失败原因:\(erro.localizedDescription)") - preloadAsset(asset) - default: - break - } - } - }catch{ - print("预加载失败:\(error.localizedDescription)") - } - } - }else { - //ios16以下的情况 - let keys = ["playable"] - asset.loadValuesAsynchronously(forKeys: keys) { - [weak self] in - guard let self = self else {return} - for key in keys { - var error: NSError? = nil - let status = asset.statusOfValue(forKey: key, error: &error) - switch status { - case .loaded: - // key成功加载,资源准备就绪 - DispatchQueue.main.async { - print("\(self.title ?? "")预加载成功") - self.isPreload = true - } - case .failed: - print("\(title ?? "")预加载失败,失败原因:\(error?.localizedDescription ?? "")") - preloadAsset(asset) - case .cancelled: - print("\(title ?? "")预加载被取消了") - default: - break - } - } - } - } + @objc private func switchURL403Action(_ notification:Notification) { + guard let path = notification.object as? String, song != nil else {return} + song.resourceUrls?[0] = path + resourcePlayerURL = .init(string: path) } } diff --git a/relax.offline.mp3.music/MP/MPPositive/Models/ViewModels/LoadViewModels/MPPositive_PlayerLoadViewModel.swift b/relax.offline.mp3.music/MP/MPPositive/Models/ViewModels/LoadViewModels/MPPositive_PlayerLoadViewModel.swift index 9c38c43..32cf3dc 100644 --- a/relax.offline.mp3.music/MP/MPPositive/Models/ViewModels/LoadViewModels/MPPositive_PlayerLoadViewModel.swift +++ b/relax.offline.mp3.music/MP/MPPositive/Models/ViewModels/LoadViewModels/MPPositive_PlayerLoadViewModel.swift @@ -14,28 +14,23 @@ class MPPositive_PlayerLoadViewModel: NSObject { var randomVideos:[MPPositive_SongItemModel]! ///当前播放音乐ViewModel var currentVideo:MPPositive_SongViewModel!{ - willSet{ - DispatchQueue.main.asyncAfter(deadline: .now()) { + didSet{ + DispatchQueue.main.async { [weak self] in guard let self = self else {return} - if newValue != nil { - MP_AnalyticsManager.shared.player_b_pvAction(newValue.song.videoId, videoname: newValue.title ?? "", artistname: newValue.song.shortBylineText ?? "") - if currentVideo != nil { - //当值变化时通知播放器页面,更新UI - NotificationCenter.notificationKey.post(notificationName: .positive_player_reload, object: currentVideo) - }else { - //当值变化时通知播放器页面,更新UI - NotificationCenter.notificationKey.post(notificationName: .positive_player_reload) - } - } + print("当前播放音乐是:\(currentVideo.song.title ?? "")") + //启动监听器 + startObservingCurrentVideoItem() } } } - + //监听器 + private var itemObservation: NSKeyValueObservation? +// private var loadQueue:DispatchQueue! + ///异步请求组 +// private var group:DispatchGroup! ///单曲播放队列ViewModel var listViewVideos:[MPPositive_SongViewModel]! - ///异步请求组 - var group:DispatchGroup? ///palyer管理model初始化方法 /// - Parameters: /// - songs: 全部歌曲列表 @@ -46,8 +41,47 @@ class MPPositive_PlayerLoadViewModel: NSObject { //根据列表生成一份随机播放列表 self.randomVideos = self.songVideos.shuffled() self.listViewVideos = [] + } - + //启动对于当前音乐的监听器 + private func startObservingCurrentVideoItem() { + itemObservation?.invalidate() // 先移除旧的监听 + //判断当前currentVideo是否已经加载完数据了 + if currentVideo?.resourcePlayerItem != nil { + //数据已经加载完毕了 + postNoticeAction() + }else { + //添加新的监听 + itemObservation = currentVideo?.observe(\.resourcePlayerItem, options: [.new], changeHandler: { [weak self] video, change in + guard let self = self else {return} + if let newItem = change.newValue { + // 在这里处理 newItem 的值变化 + postNoticeAction() + }else { + //获取到值是nil + print("无效更新") + } + }) + } + } + //发起通知告诉播放器已经切换了播放音乐 + private func postNoticeAction() { + DispatchQueue.main.asyncAfter(deadline: .now()) { + [weak self] in + guard let self = self else {return} + if currentVideo != nil { + MP_AnalyticsManager.shared.player_b_pvAction(currentVideo.song.videoId, videoname: currentVideo.title ?? "", artistname: currentVideo.song.shortBylineText ?? "") + if currentVideo != nil { + //当值变化时通知播放器页面,更新UI + NotificationCenter.notificationKey.post(notificationName: .positive_player_reload, object: currentVideo) + }else { + //当值变化时通知播放器页面,更新UI + NotificationCenter.notificationKey.post(notificationName: .positive_player_reload) + } + } + } + } + ///将选中Video的上1位,下两位项包括本身总计4项Video进行补全转为ViewModel,并播放这首音乐 func improveData(_ targetVideoId:String, isRandom:Bool = false) { //对于选中Video的集合 @@ -58,11 +92,11 @@ class MPPositive_PlayerLoadViewModel: NSObject { return } array.append(self.randomVideos[targetIndex]) - //获取上一位 - let previousIndex = targetIndex-1 - if previousIndex >= 0 { - array.append(self.randomVideos[previousIndex]) - } +// //获取上一位 +// let previousIndex = targetIndex-1 +// if previousIndex >= 0 { +// array.append(self.randomVideos[previousIndex]) +// } let nextIndex = targetIndex+1 let lastIndex = targetIndex+2 if nextIndex < randomVideos.count { @@ -77,11 +111,11 @@ class MPPositive_PlayerLoadViewModel: NSObject { return } array.append(self.songVideos[targetIndex]) - //获取上一位 - let previousIndex = targetIndex-1 - if previousIndex >= 0 { - array.append(self.songVideos[previousIndex]) - } +// //获取上一位 +// let previousIndex = targetIndex-1 +// if previousIndex >= 0 { +// array.append(self.songVideos[previousIndex]) +// } let nextIndex = targetIndex+1 let lastIndex = targetIndex+2 if nextIndex < songVideos.count { @@ -95,54 +129,14 @@ class MPPositive_PlayerLoadViewModel: NSObject { let videoIDs = Set(listViewVideos.map({$0.song.videoId})) //比较videoID,去掉已经补完的内容 array = array.filter({!videoIDs.contains($0.videoId)}) - - group = DispatchGroup() - - //去重完毕,对剩下内容补完 - for item in array { - group?.enter() - //补全歌词id和相关内容id - if item.lyricsID == nil || item.relatedID == nil { - improveDataforLycirsAndRelated(item) {[weak self] (result) in - item.lyricsID = result.0 - item.relatedID = result.1 - self?.group?.leave() - } - }else { - self.group?.leave() - } - group?.enter() - //判断当前videoID是否进行过下载 - if let resource = getDocumentsFileURL(item.videoId) { - //下载过,resource直接填入 - item.resourceUrls = [resource] - //补全完成,转化为ViewModel,并添加进listViewVideos - listViewVideos.append(.init(item)) - self.group?.leave() - }else { - //没有下载过 - //补全资源路径组和封面路径组 - improveDataforResouceAndCover(item) {[weak self] resourceUrls, coverUrls in - if let resourceUrls = resourceUrls { - item.resourceUrls = resourceUrls.0 - item.itags = resourceUrls.1 - item.mimeTypes = resourceUrls.2 - } - item.coverUrls = coverUrls - //补全完成,转化为ViewModel,并添加进listViewVideos - self?.listViewVideos.append(.init(item)) - self?.group?.leave() - } - } - } - group?.notify(queue: .main, execute: { - [weak self] in - //确定播放音乐 - self?.currentVideo = self?.listViewVideos.first(where: {$0.song.videoId == targetVideoId}) - //只保留最后四首 - self?.listViewVideos = self?.listViewVideos.suffix(4) - self?.group = nil - }) + //全部转化为MPPositive_SongViewModel + let viewModels = array.map({(MPPositive_SongViewModel($0))}) + //添加新的ViewModel + self.listViewVideos.append(contentsOf: viewModels) + //只保留最后三首 + self.listViewVideos = self.listViewVideos.suffix(3) + //更新当前播放音乐 + self.currentVideo = self.listViewVideos.first(where: {$0.song.videoId == targetVideoId}) } ///重新获取指定歌曲资源 func remakeImproveData(_ completion:@escaping (() -> Void)) { @@ -176,4 +170,8 @@ class MPPositive_PlayerLoadViewModel: NSObject { private func findVideoIdForDocument(_ videoId:String) -> Bool { return MPPositive_DownloadItemModel.fetch(.init(format: "videoId == %@", videoId)).count != 0 } + //销毁时 + deinit { + itemObservation?.invalidate() + } } diff --git a/relax.offline.mp3.music/MP/MPPositive/ViewControllers/Player(播放器)/MPPositive_PlayerViewController.swift b/relax.offline.mp3.music/MP/MPPositive/ViewControllers/Player(播放器)/MPPositive_PlayerViewController.swift index 6c7175d..098a92d 100644 --- a/relax.offline.mp3.music/MP/MPPositive/ViewControllers/Player(播放器)/MPPositive_PlayerViewController.swift +++ b/relax.offline.mp3.music/MP/MPPositive/ViewControllers/Player(播放器)/MPPositive_PlayerViewController.swift @@ -335,7 +335,7 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont private func uploadUI() { DispatchQueue.main.async { [weak self] in - guard let self = self else {return} + guard let self = self, MP_PlayerManager.shared.loadPlayer?.currentVideo != nil else {return} print("\(MP_PlayerManager.shared.loadPlayer?.currentVideo?.title ?? "")刷新了页面") //填充数据 backImageView.kf.setImage(with: MP_PlayerManager.shared.loadPlayer.currentVideo?.coverUrl, placeholder: placeholderImage) diff --git a/relax.offline.mp3.music/MP/MPPositive/Views/Home/MPPositive_MusicItemShowTableViewCell.swift b/relax.offline.mp3.music/MP/MPPositive/Views/Home/MPPositive_MusicItemShowTableViewCell.swift index 3406880..a85528c 100644 --- a/relax.offline.mp3.music/MP/MPPositive/Views/Home/MPPositive_MusicItemShowTableViewCell.swift +++ b/relax.offline.mp3.music/MP/MPPositive/Views/Home/MPPositive_MusicItemShowTableViewCell.swift @@ -84,7 +84,7 @@ class MPPositive_MusicItemShowTableViewCell: UITableViewCell { } contentView.addSubview(rankLabel) rankLabel.snp.makeConstraints { make in - make.centerX.equalTo(coverImageView) + make.center.equalTo(coverImageView) make.width.equalTo(coverImageView) } contentView.addSubview(moreBtn)