İOS Swift ile Video İndirme ve Cache Disk

2 hafta önce , Okuma süresi 5 dakika.

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. 
İOS Swift ile Video İndirme ve Cache Disk

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 (*****) 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)
            }
        }
    #ios #swift