İOS Swift ile Video İndirme ve Cache Disk

Video, günümüzün en önemli içerik türlerinden biridir. Her uygulamanın kullanıcılarını cezbetmek için bir tür video içeriğine sahip olması gerekebilir. 

swift - 06-09-2020 17:47

Ancak sorun, uygulamanın aynı sayıda video için veri alması gerektiğinde ortaya çıkar, bu, kullanıcı ağ verilerini gereksiz yere tükettiğimiz için uygulama kullanıcımız için adil değildir. Özellikle kısa videolar için, bir kullanıcı aynı parçayı günde beş kez çalabilir ve her seferinde görünüyorsa yükleme aktivitesine bakarken uykuya dalabilir, diğer durumda sanki Mars'taymışsınız gibi  veya bir mağaradaymış gibi En sevdiğiniz videoların keyfini çıkarmak için "İnternet Yok" gibi bir mesaj görebilirsiniz.

Uygulama Geliştiricisi olarak, aklınıza ilk soru geliyor - Kullanıcı deneyimimizi iyileştirmek için en son birkaç videoyu önbelleğe alabilir miyiz? Bu yazıda, AVFoundation ailesini kullanarak video içeriğini diskte önbelleğe almak için birkaç tekniği keşfedeceğim.

AVAssetExportSession'ı kullanma

AVAssetExportSession, videoyu indirmenin ve diske kaydetmenin en basit yollarından biridir. İlk olarak AVURLAsset'i oluşturuyoruz ve aşağıdaki kod parçacığında gösterildiği gibi ses ve video dosyalarını oluşturuyoruz:

private func exportVideo(forAsset asset: AVURLAsset) { if !asset.isExportable { return } //1 let composition = AVMutableComposition() //2 if let compositionVideoTrack = composition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: CMPersistentTrackID(kCMPersistentTrackID_Invalid)), let sourceVideoTrack = asset.tracks(withMediaType: .video).first { do { try compositionVideoTrack.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: asset.duration), of: sourceVideoTrack, at: CMTime.zero) } catch { print("Failed to compose video file") return } } //3 if let compositionAudioTrack = composition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: CMPersistentTrackID(kCMPersistentTrackID_Invalid)), let sourceAudioTrack = asset.tracks(withMediaType: .audio).first { do { try compositionAudioTrack.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: asset.duration), of: sourceAudioTrack, at: CMTime.zero) } catch { print("Failed to compose audio file") return } } //4 guard let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetMediumQuality) else { print("Failed to create export session") return } //5 let fileName = asset.url.lastPathComponent let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last! let dirPath = documentsDirectory.appendingPathComponent("Video") let outputURL = dirPath.appendingPathComponent(fileName) print("File path: (outputURL)") exporter.outputURL = outputURL exporter.outputFileType = AVFileType.mp4 //6 exporter.exportAsynchronously { if let error = exporter.error { print("Error (error)") } else { print("Exporter completed") } } }

Yukarıdaki kod yorumlarında gösterildiği gibi adım adım anlayalım

Ses ve video dosyalarını oluşturmak için AVMutableComposition nesnesi oluşturun. Video parçası oluşturun. Ses parçası oluşturun. Varlık kompozisyonu ve önceden ayarlanmış kalite ile AVAssetExportSession nesnesi oluşturun. Dizin yolu oluşturun, bu yolu dışa aktaran URL'ye verin. Ayrıca dışa aktarıcı çıktı dosyası türünü ayarlayın. İşlemi tamamlamak için exportAsynchronously çağrısı yapın. İhracatçı size ayrıca bilinmeyen, bekliyor, dışa aktarım, tamamlandı, başarısız oldu veya iptal edildi gibi durum durumları da verir. Bunları ihtiyacınıza göre halledebilirsiniz.

Not: Bu tekniğin uygulanması daha hızlıdır ve kısa videolar için faydalı olabilir. Uzun videolar için bu en iyi seçim değildir. Aynı anda video oynatıp indirmeniz gerekiyorsa, bu dışa aktarma oturumuna ek olarak iyi bir seçim olmayacaktır.

2. AVAssetResourceLoadingRequests'i kullanma

Tartışıldığı gibi, AVAssetExportSession uzun videolar için iyi bir bahis değildir. Bu yüzden bu sefer AVAssetResourceLoaderDelegate'i kullanacağız . İhracatçıya ve daha fazlasını sağlayan benzer mantığı uygulamamıza yardımcı olur. Buradaki zor kısım, AVAssetResourceLoaderDelegate'i çağırmaktır. ResourceLoaderDelegate'imizin çağrılmasını sağlamak için https://www.varuntomar.com/demo.mp4 gibi orijinal URL'yi değil, biraz değiştirilmiş https-testloader: //www.varuntomar.com/demo.mp4'ü iletmeliyiz . Bundan sonra biz atayabilirsiniz loaderDelegate için videoAsset. Örnek bir kaynak temsilcisi sınıfı yazdım, aşağıya bir göz atalım:

import Foundation import AVFoundation class VideoResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDataDelegate, URLSessionTaskDelegate { typealias Completion = (URL?) -> Void private static let SchemeSuffix = "-varun" // MARK: - Properties // MARK: Public var completion: Completion? //1 lazy var streamingAssetURL: URL? = { guard var components = URLComponents(url: self.url, resolvingAgainstBaseURL: false) else { return nil } components.scheme = (components.scheme ?? "") + VideoResourceLoaderDelegate.SchemeSuffix guard let retURL = components.url else { return nil } return retURL }() // MARK: Private private let url: URL private var infoResponse: URLResponse? private var urlSession: URLSession? private lazy var mediaData = Data() private var loadingRequests = [AVAssetResourceLoadingRequest]() //2 init(withURL url: URL) { self.url = url super.init() } //3 func invalidate() { self.loadingRequests.forEach { $0.finishLoading() } self.invalidateURLSession() } // MARK: - AVAssetResourceLoaderDelegate //4 func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool { if self.urlSession == nil { let session = self.createURLSession() self.urlSession = session let task = session.dataTask(with: self.url) task.resume() } self.loadingRequests.append(loadingRequest) return true } func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) { if let index = self.loadingRequests.firstIndex(of: loadingRequest) { self.loadingRequests.remove(at: index) } } // MARK: - URLSessionTaskDelegate //4 func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { if let error = error { print("Failed to download media file with error: (error)") taskCompleted(for: nil) } else { saveMediaDataToLocalFile() } } // MARK: - URLSessionDataDelegate //5 func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { self.infoResponse = response self.processRequests() // allow to continue loading completionHandler(.allow) } //6 func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { self.mediaData.append(data) self.processRequests() } //7 private func createURLSession() -> URLSession { let config = URLSessionConfiguration.default let operationQueue = OperationQueue() operationQueue.maxConcurrentOperationCount = 1 return URLSession(configuration: config, delegate: self, delegateQueue: operationQueue) } //8 private func invalidateURLSession() { self.urlSession?.invalidateAndCancel() self.urlSession = nil } //9 private func isInfo(request: AVAssetResourceLoadingRequest) -> Bool { return request.contentInformationRequest != nil } //10 private func fillInfoRequest(request: inout AVAssetResourceLoadingRequest, response: URLResponse) { request.contentInformationRequest?.isByteRangeAccessSupported = true request.contentInformationRequest?.contentType = response.mimeType request.contentInformationRequest?.contentLength = response.expectedContentLength } //11 private func processRequests() { var finishedRequests = Set<AVAssetResourceLoadingRequest>() self.loadingRequests.forEach { var request = $0 if self.isInfo(request: request), let response = self.infoResponse { self.fillInfoRequest(request: &request, response: response) } if let dataRequest = request.dataRequest, self.checkAndRespond(forRequest: dataRequest) { finishedRequests.insert(request) request.finishLoading() } } self.loadingRequests = self.loadingRequests.filter { !finishedRequests.contains($0) } } //12 private func checkAndRespond(forRequest dataRequest: AVAssetResourceLoadingDataRequest) -> Bool { let downloadedData = self.mediaData let downloadedDataLength = Int64(downloadedData.count) let requestRequestedOffset = dataRequest.requestedOffset let requestRequestedLength = Int64(dataRequest.requestedLength) let requestCurrentOffset = dataRequest.currentOffset if downloadedDataLength < requestCurrentOffset { return false } let downloadedUnreadDataLength = downloadedDataLength - requestCurrentOffset let requestUnreadDataLength = requestRequestedOffset + requestRequestedLength - requestCurrentOffset let respondDataLength = min(requestUnreadDataLength, downloadedUnreadDataLength) dataRequest.respond(with: downloadedData.subdata(in: Range(NSMakeRange(Int(requestCurrentOffset), Int(respondDataLength)))!)) let requestEndOffset = requestRequestedOffset + requestRequestedLength return requestCurrentOffset >= requestEndOffset } //13 private func taskCompleted(for url: URL?) { if let fileUrl = url { self.completion?(fileUrl) } else { self.completion?(nil) } self.invalidateURLSession() } //14 private func saveMediaDataToLocalFile() { let fileName = self.url.path let modifiedfileName = fileName.replacingOccurrences(of: "/", with: "") guard let dirPath = DBCommonUtils.videoExportBaseUrl() else { taskCompleted(for: nil) return } let fileURL = dirPath.appendingPathComponent(modifiedfileName) if FileManager.default.fileExists(atPath: fileURL.path) { do { try FileManager.default.removeItem(at: fileURL) } catch let error { print("Failed to delete file with error: (error)") } } do { try self.mediaData.write(to: fileURL, options: .atomic) print("Media data saved") taskCompleted(for: fileURL) } catch let error { print("Failed to save data with error: (error)") taskCompleted(for: nil) } } }

Oyuncu yeni bir veri grubu istediğinde ilk yöntem çağırır. Temsilcimizle, istekleri durdurur ve URLSession ile tüm verileri kendimiz yükleriz ve sahip olduğumuz orijinal AVAssetResourceLoadingRequest verilerini geri sağlar.Şimdi, birincil sorumluluğunuz veri sağlamaktır. Daha iyi anlamak için size kodun üzerinden geçmeme izin verin, lütfen yukarıdaki kod pasajındaki yorumlara bakın:

Orijinal video URL'sini burada yakalayarak AVAssetResourceLoaderDelagate çağrılır, aksi takdirde çağrılmaz ve veri yükleme AVPlayedItem tarafından işlenir. AVUrlAsset nesnesinin temsilcisini video url ile başlat Görevimiz herhangi bir nedenle bittiğinde veya engellendiğinde bekleyen isteği ve URLSession görevlerini geçersiz kılın. Veri talebinde bulunmak için URLSession'ımızı oluşturduğumuz kaynak yükleyici temsilcisi. İstekleri işlemek için URLSession temsilcisi. Alınan medya verilerini geçici olarak belleğe ekleme. Tüm bekleyen istekler tamamlandığında bu medya verileri diske yazılır. URLSession oluşturma işlevi URLSession burada geçersiz kılınıyor. AVAssetResourceLoadingRequest bilgileri istenirse doğru döndürülür. Burada gerekli talep bilgileri doldurulur. AVAssetResourceLoadingRequests'i burada işliyoruz. Talep yüklenen verileri ve ofseti kontrol etme Görev tamamlandığında, işleri bitirmek için bu çağrılır. Medya verilerini kaynaklardaki yerel dosyaya kaydedin.

Aşağıda, medya indirme sürecini başlatmak için örnek kod verilmiştir;

private func downloadVideoFromUrl(completion: @escaping (_ isDone: Bool) -> Void) { self.loaderDelegate = VideoResourceLoaderDelegate(withURL: videoUrl) if let loaderDelegate = self.loaderDelegate, let assetUrl = loaderDelegate.streamingAssetURL { let videoAsset = AVURLAsset(url: assetUrl) // Not creating seperate queue here, global queue would work videoAsset.resourceLoader.setDelegate(loaderDelegate, queue: DispatchQueue.global()) loaderDelegate.completion = { localFileURL in if let localFileURL = localFileURL { print("Video file saved to: (localFileURL)") } else { print("Failed to download video file") } completion(true) } let playerItem = AVPlayerItem(asset: videoAsset) // This AVPlayer object is to tirgger AVAssetResourceLoaderDelegate. We are not using this player object anywhere else let player = AVPlayer(playerItem: playerItem) player.isMuted = true } else { completion(true) } }
Günün Diğer Haberleri