Music_Player3/relax.offline.mp3.music/MP/Common/Tool(工具封装)/MP_AVURLAsset.swift

622 lines
27 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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() {
//AssetAssetresourceLoaderdelegate
//
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[..<range.lowerBound])
let newScheme = scheme + "-streaming"
urlString = urlString.replacingOccurrences(of: scheme, with: newScheme)
return URL(string: urlString)
} else {
return nil
}
}
///URL
static func originalURL(_ url: URL) -> 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()
}
}
}
//403URL
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)")
}
}
}