diff --git a/IndieMusic/IndieMusic.xcodeproj/project.pbxproj b/IndieMusic/IndieMusic.xcodeproj/project.pbxproj index 325fd26..5869638 100644 --- a/IndieMusic/IndieMusic.xcodeproj/project.pbxproj +++ b/IndieMusic/IndieMusic.xcodeproj/project.pbxproj @@ -121,6 +121,12 @@ 778B8AC12AF8ED280034AFD4 /* TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 778B8AB82AF8ED280034AFD4 /* TableView.swift */; }; 778B8AC22AF8ED280034AFD4 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 778B8AB92AF8ED280034AFD4 /* TableViewController.swift */; }; 778B8AC32AF8ED280034AFD4 /* TabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 778B8ABA2AF8ED280034AFD4 /* TabViewController.swift */; }; + 77A60D7C2B5B976900D4E435 /* ResourceLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A60D7B2B5B976900D4E435 /* ResourceLoader.swift */; }; + 77A60D7E2B5B977D00D4E435 /* ResourceLoaderRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A60D7D2B5B977D00D4E435 /* ResourceLoaderRequest.swift */; }; + 77A60D802B5B979000D4E435 /* AssetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A60D7F2B5B979000D4E435 /* AssetData.swift */; }; + 77A60D822B5B97A100D4E435 /* AssetDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A60D812B5B97A100D4E435 /* AssetDataManager.swift */; }; + 77A60D842B5B97B500D4E435 /* PINCacheAssetDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A60D832B5B97B500D4E435 /* PINCacheAssetDataManager.swift */; }; + 77A60D862B5B97C300D4E435 /* CachingAVURLAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A60D852B5B97C300D4E435 /* CachingAVURLAsset.swift */; }; 77A659CF2B4FDC4100B408C3 /* PullToDismissable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A659CE2B4FDC4100B408C3 /* PullToDismissable.swift */; }; 77A659D12B4FDC5200B408C3 /* PullToDismissTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A659D02B4FDC5200B408C3 /* PullToDismissTransition.swift */; }; 77A659DF2B51023200B408C3 /* TestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A659DE2B51023100B408C3 /* TestViewController.swift */; }; @@ -331,6 +337,12 @@ 778B8AB82AF8ED280034AFD4 /* TableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableView.swift; sourceTree = ""; }; 778B8AB92AF8ED280034AFD4 /* TableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = ""; }; 778B8ABA2AF8ED280034AFD4 /* TabViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabViewController.swift; sourceTree = ""; }; + 77A60D7B2B5B976900D4E435 /* ResourceLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceLoader.swift; sourceTree = ""; }; + 77A60D7D2B5B977D00D4E435 /* ResourceLoaderRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceLoaderRequest.swift; sourceTree = ""; }; + 77A60D7F2B5B979000D4E435 /* AssetData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetData.swift; sourceTree = ""; }; + 77A60D812B5B97A100D4E435 /* AssetDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetDataManager.swift; sourceTree = ""; }; + 77A60D832B5B97B500D4E435 /* PINCacheAssetDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PINCacheAssetDataManager.swift; sourceTree = ""; }; + 77A60D852B5B97C300D4E435 /* CachingAVURLAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachingAVURLAsset.swift; sourceTree = ""; }; 77A659CE2B4FDC4100B408C3 /* PullToDismissable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullToDismissable.swift; sourceTree = ""; }; 77A659D02B4FDC5200B408C3 /* PullToDismissTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullToDismissTransition.swift; sourceTree = ""; }; 77A659DE2B51023100B408C3 /* TestViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestViewController.swift; sourceTree = ""; }; @@ -696,6 +708,7 @@ 778B8A5A2AF8EAFD0034AFD4 /* Third Party */ = { isa = PBXGroup; children = ( + 77A60D7A2B5B975600D4E435 /* ResourceLoader */, 770228F22B5A224500E07F7A /* SliderControl */, 770228E32B54FA3600E07F7A /* RxActivityIndicator */, 778B8A602AF8ECC20034AFD4 /* RxErrorTracker */, @@ -838,6 +851,19 @@ path = "RxSwift+Extension"; sourceTree = ""; }; + 77A60D7A2B5B975600D4E435 /* ResourceLoader */ = { + isa = PBXGroup; + children = ( + 77A60D7B2B5B976900D4E435 /* ResourceLoader.swift */, + 77A60D7D2B5B977D00D4E435 /* ResourceLoaderRequest.swift */, + 77A60D7F2B5B979000D4E435 /* AssetData.swift */, + 77A60D812B5B97A100D4E435 /* AssetDataManager.swift */, + 77A60D832B5B97B500D4E435 /* PINCacheAssetDataManager.swift */, + 77A60D852B5B97C300D4E435 /* CachingAVURLAsset.swift */, + ); + path = ResourceLoader; + sourceTree = ""; + }; 77C9B9CB2B4B93D10006C83F /* Personal */ = { isa = PBXGroup; children = ( @@ -1147,6 +1173,7 @@ 770228E52B54FA3600E07F7A /* ActivityIndicator.swift in Sources */, 7751D3722B43B9ED00F1F2BD /* Search.swift in Sources */, 774A18072B06045200F56DF1 /* HomeView.swift in Sources */, + 77A60D802B5B979000D4E435 /* AssetData.swift in Sources */, 77FAF7642B075FEB00FC2CA1 /* JournalDetailViewModel.swift in Sources */, 778B8AB12AF8ED1B0034AFD4 /* Configs.swift in Sources */, 77FA0B482B0DFB0E00404C5E /* EditBindPhoneViewController.swift in Sources */, @@ -1192,8 +1219,10 @@ 778B8ABF2AF8ED280034AFD4 /* NavigationController.swift in Sources */, 774399AA2AFE3170006F8EEA /* PaddingLabel.swift in Sources */, 77FB7A702B48074600B64030 /* MusicStyleViewModel.swift in Sources */, + 77A60D7E2B5B977D00D4E435 /* ResourceLoaderRequest.swift in Sources */, 770228ED2B55284F00E07F7A /* Login.swift in Sources */, 7751D3502B42ABBF00F1F2BD /* SettingViewController.swift in Sources */, + 77A60D842B5B97B500D4E435 /* PINCacheAssetDataManager.swift in Sources */, 77A659DF2B51023200B408C3 /* TestViewController.swift in Sources */, 77FA0B4E2B0EF8C700404C5E /* PhoneCodeViewModel.swift in Sources */, 778B8A822AF8ECE50034AFD4 /* HomeViewModel.swift in Sources */, @@ -1265,6 +1294,7 @@ 778B8AA92AF8ED0E0034AFD4 /* UIImage+IndieMusic.swift in Sources */, 77C9B9D12B4B99600006C83F /* BadgeButton.swift in Sources */, 778B8A7F2AF8ECE50034AFD4 /* MineViewModel.swift in Sources */, + 77A60D862B5B97C300D4E435 /* CachingAVURLAsset.swift in Sources */, 778B8A9A2AF8ECFC0034AFD4 /* AudioManager.swift in Sources */, 774A180E2B07000C00F56DF1 /* JournalDetailView.swift in Sources */, 778B8A842AF8ECE50034AFD4 /* HomeTabBarController.swift in Sources */, @@ -1293,10 +1323,12 @@ 77FA0B3E2B0C573600404C5E /* Filter.swift in Sources */, 77165D742B464493002AE0A5 /* BarButtonItem.swift in Sources */, 7751D3662B42BE7F00F1F2BD /* FeedbackViewController.swift in Sources */, + 77A60D822B5B97A100D4E435 /* AssetDataManager.swift in Sources */, 77FB7A7B2B4A4FC900B64030 /* MessageViewController.swift in Sources */, 77FB7A722B48E93100B64030 /* SearchResultsViewModel.swift in Sources */, 774A18142B07329600F56DF1 /* MultiUserAvatarView.swift in Sources */, 778B8AAA2AF8ED0E0034AFD4 /* AVPlayer.swift in Sources */, + 77A60D7C2B5B976900D4E435 /* ResourceLoader.swift in Sources */, 77FA0B442B0DFABD00404C5E /* LoginView.swift in Sources */, 77FB7A7D2B4A4FD400B64030 /* MessageViewModel.swift in Sources */, 7743999E2AFA18C3006F8EEA /* PlayerViewController.swift in Sources */, diff --git a/IndieMusic/IndieMusic/Managers/AudioManager.swift b/IndieMusic/IndieMusic/Managers/AudioManager.swift index f24d518..665bc9e 100644 --- a/IndieMusic/IndieMusic/Managers/AudioManager.swift +++ b/IndieMusic/IndieMusic/Managers/AudioManager.swift @@ -80,7 +80,7 @@ class AudioManager { try AVAudioSession.sharedInstance().setMode(.default) try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation) - let playerItem: AVPlayerItem = AVPlayerItem(url: url) + let playerItem: AVPlayerItem = AVPlayerItem.init(asset: CachingAVURLAsset(url: url)) player?.pause() player = AVPlayer(playerItem: playerItem) @@ -90,17 +90,17 @@ class AudioManager { self.currentTrack = track NotificationCenter.default.post(name: .notiPlayAudioTrack, object: track) + setLockScreenDisplay(track: track) - - var nowPlayingInfo = [String: Any]() - nowPlayingInfo[MPMediaItemPropertyTitle] = "歌曲标题" - nowPlayingInfo[MPMediaItemPropertyArtist] = "艺术家名称" - nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = 100 - nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = 200 - nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1 - - MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo - +// var nowPlayingInfo = [String: Any]() +// nowPlayingInfo[MPMediaItemPropertyTitle] = "歌曲标题" +// nowPlayingInfo[MPMediaItemPropertyArtist] = "艺术家名称" +// nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = 100 +// nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = 200 +// nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1 +// +// MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo +// } catch { @@ -202,4 +202,17 @@ class AudioManager { } } + + func setLockScreenDisplay(track: AudioTrack) { + var info = Dictionary() + info[MPMediaItemPropertyTitle] = track.title//歌名 + info[MPMediaItemPropertyArtist] = track.artist//作者 + // [info setObject:self.model.filename forKey:MPMediaItemPropertyAlbumTitle];//专辑名 + info[MPMediaItemPropertyAlbumArtist] = track.artist//专辑作者 +// info[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(image: UIImage.init(named: track.pic!))//显示的图片 + info[MPMediaItemPropertyPlaybackDuration] = 200 + info[MPNowPlayingInfoPropertyPlaybackRate] = 1.0//播放速率 + MPNowPlayingInfoCenter.default().nowPlayingInfo = info + } + } diff --git a/IndieMusic/IndieMusic/Third Party/ResourceLoader/AssetData.swift b/IndieMusic/IndieMusic/Third Party/ResourceLoader/AssetData.swift new file mode 100644 index 0000000..9047a25 --- /dev/null +++ b/IndieMusic/IndieMusic/Third Party/ResourceLoader/AssetData.swift @@ -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() + } +} diff --git a/IndieMusic/IndieMusic/Third Party/ResourceLoader/AssetDataManager.swift b/IndieMusic/IndieMusic/Third Party/ResourceLoader/AssetDataManager.swift new file mode 100644 index 0000000..5d3e9cd --- /dev/null +++ b/IndieMusic/IndieMusic/Third Party/ResourceLoader/AssetDataManager.swift @@ -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.. 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 + } +} diff --git a/IndieMusic/IndieMusic/Third Party/ResourceLoader/PINCacheAssetDataManager.swift b/IndieMusic/IndieMusic/Third Party/ResourceLoader/PINCacheAssetDataManager.swift new file mode 100644 index 0000000..dec9ce4 --- /dev/null +++ b/IndieMusic/IndieMusic/Third Party/ResourceLoader/PINCacheAssetDataManager.swift @@ -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 + } +} + diff --git a/IndieMusic/IndieMusic/Third Party/ResourceLoader/ResourceLoader.swift b/IndieMusic/IndieMusic/Third Party/ResourceLoader/ResourceLoader.swift new file mode 100644 index 0000000..136fdf7 --- /dev/null +++ b/IndieMusic/IndieMusic/Third Party/ResourceLoader/ResourceLoader.swift @@ -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).. end) ? Int((end)) : (assetData.mediaData.count) + let subData = assetData.mediaData.subdata(in: Int(range.start)..) { + 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)) + } + } + } +} + diff --git a/IndieMusic/IndieMusic/Third Party/ResourceLoader/ResourceLoaderRequest.swift b/IndieMusic/IndieMusic/Third Party/ResourceLoader/ResourceLoaderRequest.swift new file mode 100644 index 0000000..6a744a7 --- /dev/null +++ b/IndieMusic/IndieMusic/Third Party/ResourceLoader/ResourceLoaderRequest.swift @@ -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) +} + +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) + } + } + } +} diff --git a/IndieMusic/Podfile b/IndieMusic/Podfile index 9157607..410ebf9 100644 --- a/IndieMusic/Podfile +++ b/IndieMusic/Podfile @@ -26,6 +26,7 @@ target 'IndieMusic' do pod 'IQKeyboardManagerSwift' pod 'lottie-ios' pod 'MarqueeLabel' + pod 'PINCache' pod 'NSObject+Rx'