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, 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
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:
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)
}
}