parent
5146380302
commit
815ee05c7f
@ -0,0 +1,52 @@
|
||||
//
|
||||
// AssetData.swift
|
||||
// IndieMusic
|
||||
//
|
||||
// Created by WenLei on 2024/1/20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
class AssetDataContentInformation: NSObject, NSCoding {
|
||||
@objc var contentLength: Int64 = 0
|
||||
@objc var contentType: String = ""
|
||||
@objc var isByteRangeAccessSupported: Bool = false
|
||||
|
||||
func encode(with coder: NSCoder) {
|
||||
coder.encode(self.contentLength, forKey: #keyPath(AssetDataContentInformation.contentLength))
|
||||
coder.encode(self.contentType, forKey: #keyPath(AssetDataContentInformation.contentType))
|
||||
coder.encode(self.isByteRangeAccessSupported, forKey: #keyPath(AssetDataContentInformation.isByteRangeAccessSupported))
|
||||
}
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init()
|
||||
self.contentLength = coder.decodeInt64(forKey: #keyPath(AssetDataContentInformation.contentLength))
|
||||
self.contentType = coder.decodeObject(forKey: #keyPath(AssetDataContentInformation.contentType)) as? String ?? ""
|
||||
self.isByteRangeAccessSupported = coder.decodeObject(forKey: #keyPath(AssetDataContentInformation.isByteRangeAccessSupported)) as? Bool ?? false
|
||||
}
|
||||
}
|
||||
|
||||
class AssetData: NSObject, NSCoding {
|
||||
@objc var contentInformation: AssetDataContentInformation = AssetDataContentInformation()
|
||||
@objc var mediaData: Data = Data()
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
func encode(with coder: NSCoder) {
|
||||
coder.encode(self.contentInformation, forKey: #keyPath(AssetData.contentInformation))
|
||||
coder.encode(self.mediaData, forKey: #keyPath(AssetData.mediaData))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init()
|
||||
self.contentInformation = coder.decodeObject(forKey: #keyPath(AssetData.contentInformation)) as? AssetDataContentInformation ?? AssetDataContentInformation()
|
||||
self.mediaData = coder.decodeObject(forKey: #keyPath(AssetData.mediaData)) as? Data ?? Data()
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
//
|
||||
// AssetDataManager.swift
|
||||
// IndieMusic
|
||||
//
|
||||
// Created by WenLei on 2024/1/20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol AssetDataManager: NSObject {
|
||||
func retrieveAssetData() -> AssetData?
|
||||
func saveContentInformation(_ contentInformation: AssetDataContentInformation)
|
||||
func saveDownloadedData(_ data: Data, offset: Int)
|
||||
func mergeDownloadedDataIfIsContinuted(from: Data, with: Data, offset: Int) -> Data?
|
||||
}
|
||||
|
||||
extension AssetDataManager {
|
||||
func mergeDownloadedDataIfIsContinuted(from: Data, with: Data, offset: Int) -> Data? {
|
||||
if offset <= from.count && (offset + with.count) > from.count {
|
||||
let start = from.count - offset
|
||||
var data = from
|
||||
data.append(with.subdata(in: start..<with.count))
|
||||
return data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
//
|
||||
// CachingAVURLAsset.swift
|
||||
// IndieMusic
|
||||
//
|
||||
// Created by WenLei on 2024/1/20.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
|
||||
class CachingAVURLAsset: AVURLAsset {
|
||||
|
||||
static let customScheme = "cachingPlayerItemScheme"
|
||||
let originalURL: URL
|
||||
private var _resourceLoader: ResourceLoader?
|
||||
|
||||
var cacheKey: String {
|
||||
return self.url.lastPathComponent
|
||||
}
|
||||
|
||||
static func isSchemeSupport(_ url: URL) -> Bool {
|
||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
|
||||
return false
|
||||
}
|
||||
|
||||
return ["http", "https"].contains(components.scheme)
|
||||
}
|
||||
|
||||
override init(url URL: URL, options: [String: Any]? = nil) {
|
||||
self.originalURL = URL
|
||||
|
||||
guard var components = URLComponents(url: URL, resolvingAgainstBaseURL: false) else {
|
||||
super.init(url: URL, options: options)
|
||||
return
|
||||
}
|
||||
|
||||
components.scheme = CachingAVURLAsset.customScheme
|
||||
guard let url = components.url else {
|
||||
super.init(url: URL, options: options)
|
||||
return
|
||||
}
|
||||
|
||||
super.init(url: url, options: options)
|
||||
|
||||
let resourceLoader = ResourceLoader(asset: self)
|
||||
self.resourceLoader.setDelegate(resourceLoader, queue: resourceLoader.loaderQueue)
|
||||
self._resourceLoader = resourceLoader
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
//
|
||||
// PINCacheAssetDataManager.swift
|
||||
// IndieMusic
|
||||
//
|
||||
// Created by WenLei on 2024/1/20.
|
||||
//
|
||||
|
||||
import PINCache
|
||||
import Foundation
|
||||
|
||||
class PINCacheAssetDataManager: NSObject, AssetDataManager {
|
||||
|
||||
static let Cache: PINCache = PINCache(name: "ResourceLoader")
|
||||
let cacheKey: String
|
||||
|
||||
init(cacheKey: String) {
|
||||
self.cacheKey = cacheKey
|
||||
super.init()
|
||||
}
|
||||
|
||||
func saveContentInformation(_ contentInformation: AssetDataContentInformation) {
|
||||
let assetData = AssetData()
|
||||
assetData.contentInformation = contentInformation
|
||||
PINCacheAssetDataManager.Cache.setObjectAsync(assetData, forKey: cacheKey, completion: nil)
|
||||
}
|
||||
|
||||
func saveDownloadedData(_ data: Data, offset: Int) {
|
||||
guard let assetData = self.retrieveAssetData() else {
|
||||
return
|
||||
}
|
||||
|
||||
if let mediaData = self.mergeDownloadedDataIfIsContinuted(from: assetData.mediaData, with: data, offset: offset) {
|
||||
assetData.mediaData = mediaData
|
||||
|
||||
PINCacheAssetDataManager.Cache.setObjectAsync(assetData, forKey: cacheKey, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveAssetData() -> AssetData? {
|
||||
guard let assetData = PINCacheAssetDataManager.Cache.object(forKey: cacheKey) as? AssetData else {
|
||||
return nil
|
||||
}
|
||||
return assetData
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,151 @@
|
||||
//
|
||||
// ResourceLoader.swift
|
||||
// IndieMusic
|
||||
//
|
||||
// Created by WenLei on 2024/1/20.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
|
||||
class ResourceLoader: NSObject {
|
||||
|
||||
let loaderQueue = DispatchQueue(label: "li.zhgchg.resourceLoader.queue")
|
||||
|
||||
private var requests: [AVAssetResourceLoadingRequest: ResourceLoaderRequest] = [:]
|
||||
private let cacheKey: String
|
||||
private let originalURL: URL
|
||||
|
||||
init(asset: CachingAVURLAsset) {
|
||||
self.cacheKey = asset.cacheKey
|
||||
self.originalURL = asset.originalURL
|
||||
super.init()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.requests.forEach { (request) in
|
||||
request.value.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ResourceLoader: AVAssetResourceLoaderDelegate {
|
||||
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
|
||||
|
||||
let type = ResourceLoader.resourceLoaderRequestType(loadingRequest)
|
||||
let assetDataManager = PINCacheAssetDataManager(cacheKey: self.cacheKey)
|
||||
|
||||
if let assetData = assetDataManager.retrieveAssetData() {
|
||||
if type == .contentInformation {
|
||||
loadingRequest.contentInformationRequest?.contentLength = assetData.contentInformation.contentLength
|
||||
loadingRequest.contentInformationRequest?.contentType = assetData.contentInformation.contentType
|
||||
loadingRequest.contentInformationRequest?.isByteRangeAccessSupported = assetData.contentInformation.isByteRangeAccessSupported
|
||||
loadingRequest.finishLoading()
|
||||
return true
|
||||
} else {
|
||||
let range = ResourceLoader.resourceLoaderRequestRange(type, loadingRequest)
|
||||
if assetData.mediaData.count > 0 {
|
||||
let end: Int64
|
||||
switch range.end {
|
||||
case .requestTo(let rangeEnd):
|
||||
end = rangeEnd
|
||||
case .requestToEnd:
|
||||
end = assetData.contentInformation.contentLength
|
||||
}
|
||||
|
||||
if assetData.mediaData.count >= end {
|
||||
let subData = assetData.mediaData.subdata(in: Int(range.start)..<Int(end))
|
||||
loadingRequest.dataRequest?.respond(with: subData)
|
||||
loadingRequest.finishLoading()
|
||||
return true
|
||||
} else if range.start <= assetData.mediaData.count {
|
||||
// has cache data...but not enough
|
||||
let subEnd = (assetData.mediaData.count > end) ? Int((end)) : (assetData.mediaData.count)
|
||||
let subData = assetData.mediaData.subdata(in: Int(range.start)..<subEnd)
|
||||
loadingRequest.dataRequest?.respond(with: subData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let range = ResourceLoader.resourceLoaderRequestRange(type, loadingRequest)
|
||||
let resourceLoaderRequest = ResourceLoaderRequest(originalURL: self.originalURL, type: type, loaderQueue: self.loaderQueue, assetDataManager: assetDataManager)
|
||||
resourceLoaderRequest.delegate = self
|
||||
self.requests[loadingRequest]?.cancel()
|
||||
self.requests[loadingRequest] = resourceLoaderRequest
|
||||
resourceLoaderRequest.start(requestRange: range)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) {
|
||||
guard let resourceLoaderRequest = self.requests[loadingRequest] else {
|
||||
return
|
||||
}
|
||||
|
||||
resourceLoaderRequest.cancel()
|
||||
requests.removeValue(forKey: loadingRequest)
|
||||
}
|
||||
}
|
||||
|
||||
extension ResourceLoader: ResourceLoaderRequestDelegate {
|
||||
func contentInformationDidComplete(_ resourceLoaderRequest: ResourceLoaderRequest, _ result: Result<AssetDataContentInformation, Error>) {
|
||||
guard let loadingRequest = self.requests.first(where: { $0.value == resourceLoaderRequest })?.key else {
|
||||
return
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let contentInformation):
|
||||
loadingRequest.contentInformationRequest?.contentType = contentInformation.contentType
|
||||
loadingRequest.contentInformationRequest?.contentLength = contentInformation.contentLength
|
||||
loadingRequest.contentInformationRequest?.isByteRangeAccessSupported = contentInformation.isByteRangeAccessSupported
|
||||
loadingRequest.finishLoading()
|
||||
case .failure(let error):
|
||||
loadingRequest.finishLoading(with: error)
|
||||
}
|
||||
}
|
||||
|
||||
func dataRequestDidReceive(_ resourceLoaderRequest: ResourceLoaderRequest, _ data: Data) {
|
||||
guard let loadingRequest = self.requests.first(where: { $0.value == resourceLoaderRequest })?.key else {
|
||||
return
|
||||
}
|
||||
|
||||
loadingRequest.dataRequest?.respond(with: data)
|
||||
}
|
||||
|
||||
func dataRequestDidComplete(_ resourceLoaderRequest: ResourceLoaderRequest, _ error: Error?, _ downloadedData: Data) {
|
||||
guard let loadingRequest = self.requests.first(where: { $0.value == resourceLoaderRequest })?.key else {
|
||||
return
|
||||
}
|
||||
|
||||
loadingRequest.finishLoading(with: error)
|
||||
requests.removeValue(forKey: loadingRequest)
|
||||
}
|
||||
}
|
||||
|
||||
extension ResourceLoader {
|
||||
static func resourceLoaderRequestType(_ loadingRequest: AVAssetResourceLoadingRequest) -> ResourceLoaderRequest.RequestType {
|
||||
if let _ = loadingRequest.contentInformationRequest {
|
||||
return .contentInformation
|
||||
} else {
|
||||
return .dataRequest
|
||||
}
|
||||
}
|
||||
|
||||
static func resourceLoaderRequestRange(_ type: ResourceLoaderRequest.RequestType, _ loadingRequest: AVAssetResourceLoadingRequest) -> ResourceLoaderRequest.RequestRange {
|
||||
if type == .contentInformation {
|
||||
return ResourceLoaderRequest.RequestRange(start: 0, end: .requestTo(1))
|
||||
} else {
|
||||
if loadingRequest.dataRequest?.requestsAllDataToEndOfResource == true {
|
||||
let lowerBound = loadingRequest.dataRequest?.currentOffset ?? 0
|
||||
return ResourceLoaderRequest.RequestRange(start: lowerBound, end: .requestToEnd)
|
||||
} else {
|
||||
let lowerBound = loadingRequest.dataRequest?.currentOffset ?? 0
|
||||
let length = Int64(loadingRequest.dataRequest?.requestedLength ?? 1)
|
||||
let upperBound = lowerBound + length
|
||||
return ResourceLoaderRequest.RequestRange(start: lowerBound, end: .requestTo(upperBound))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,167 @@
|
||||
//
|
||||
// ResourceLoaderRequest.swift
|
||||
// IndieMusic
|
||||
//
|
||||
// Created by WenLei on 2024/1/20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreServices
|
||||
|
||||
protocol ResourceLoaderRequestDelegate: AnyObject {
|
||||
func dataRequestDidReceive(_ resourceLoaderRequest: ResourceLoaderRequest, _ data: Data)
|
||||
func dataRequestDidComplete(_ resourceLoaderRequest: ResourceLoaderRequest, _ error: Error?, _ downloadedData: Data)
|
||||
func contentInformationDidComplete(_ resourceLoaderRequest: ResourceLoaderRequest, _ result: Result<AssetDataContentInformation, Error>)
|
||||
}
|
||||
|
||||
class ResourceLoaderRequest: NSObject, URLSessionDataDelegate {
|
||||
struct RequestRange {
|
||||
var start: Int64
|
||||
var end: RequestRangeEnd
|
||||
|
||||
enum RequestRangeEnd {
|
||||
case requestTo(Int64)
|
||||
case requestToEnd
|
||||
}
|
||||
}
|
||||
|
||||
enum RequestType {
|
||||
case contentInformation
|
||||
case dataRequest
|
||||
}
|
||||
|
||||
struct ResponseUnExpectedError: Error { }
|
||||
|
||||
private let loaderQueue: DispatchQueue
|
||||
|
||||
let originalURL: URL
|
||||
let type: RequestType
|
||||
|
||||
private var session: URLSession?
|
||||
private var dataTask: URLSessionDataTask?
|
||||
private var assetDataManager: AssetDataManager?
|
||||
|
||||
private(set) var requestRange: RequestRange?
|
||||
private(set) var response: URLResponse?
|
||||
private(set) var downloadedData: Data = Data()
|
||||
|
||||
private(set) var isCancelled: Bool = false {
|
||||
didSet {
|
||||
if isCancelled {
|
||||
self.dataTask?.cancel()
|
||||
self.session?.invalidateAndCancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
private(set) var isFinished: Bool = false {
|
||||
didSet {
|
||||
if isFinished {
|
||||
self.session?.finishTasksAndInvalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
weak var delegate: ResourceLoaderRequestDelegate?
|
||||
|
||||
init(originalURL: URL, type: RequestType, loaderQueue: DispatchQueue, assetDataManager: AssetDataManager?) {
|
||||
self.originalURL = originalURL
|
||||
self.type = type
|
||||
self.loaderQueue = loaderQueue
|
||||
self.assetDataManager = assetDataManager
|
||||
super.init()
|
||||
}
|
||||
|
||||
func start(requestRange: RequestRange) {
|
||||
guard isCancelled == false, isFinished == false else {
|
||||
return
|
||||
}
|
||||
|
||||
self.loaderQueue.async { [weak self] in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: self.originalURL)
|
||||
self.requestRange = requestRange
|
||||
let start = String(requestRange.start)
|
||||
let end: String
|
||||
switch requestRange.end {
|
||||
case .requestTo(let rangeEnd):
|
||||
end = String(rangeEnd)
|
||||
case .requestToEnd:
|
||||
end = ""
|
||||
}
|
||||
|
||||
let rangeHeader = "bytes=\(start)-\(end)"
|
||||
request.setValue(rangeHeader, forHTTPHeaderField: "Range")
|
||||
|
||||
let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
|
||||
self.session = session
|
||||
let dataTask = session.dataTask(with: request)
|
||||
self.dataTask = dataTask
|
||||
dataTask.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
self.isCancelled = true
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
|
||||
guard self.type == .dataRequest else {
|
||||
return
|
||||
}
|
||||
|
||||
self.loaderQueue.async {
|
||||
self.delegate?.dataRequestDidReceive(self, data)
|
||||
self.downloadedData.append(data)
|
||||
}
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
|
||||
self.response = response
|
||||
completionHandler(.allow)
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
self.isFinished = true
|
||||
self.loaderQueue.async {
|
||||
if self.type == .contentInformation {
|
||||
guard error == nil,
|
||||
let response = self.response as? HTTPURLResponse else {
|
||||
let responseError = error ?? ResponseUnExpectedError()
|
||||
self.delegate?.contentInformationDidComplete(self, .failure(responseError))
|
||||
return
|
||||
}
|
||||
|
||||
let contentInformation = AssetDataContentInformation()
|
||||
|
||||
if let rangeString = response.allHeaderFields["Content-Range"] as? String,
|
||||
let bytesString = rangeString.split(separator: "/").map({String($0)}).last,
|
||||
let bytes = Int64(bytesString) {
|
||||
contentInformation.contentLength = bytes
|
||||
}
|
||||
|
||||
if let mimeType = response.mimeType,
|
||||
let contentType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType as CFString, nil)?.takeRetainedValue() {
|
||||
contentInformation.contentType = contentType as String
|
||||
}
|
||||
|
||||
if let value = response.allHeaderFields["Accept-Ranges"] as? String,
|
||||
value == "bytes" {
|
||||
contentInformation.isByteRangeAccessSupported = true
|
||||
} else {
|
||||
contentInformation.isByteRangeAccessSupported = false
|
||||
}
|
||||
|
||||
self.assetDataManager?.saveContentInformation(contentInformation)
|
||||
self.delegate?.contentInformationDidComplete(self, .success(contentInformation))
|
||||
} else {
|
||||
if let offset = self.requestRange?.start, self.downloadedData.count > 0 {
|
||||
self.assetDataManager?.saveDownloadedData(self.downloadedData, offset: Int(offset))
|
||||
}
|
||||
self.delegate?.dataRequestDidComplete(self, error, self.downloadedData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue