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