Created
April 21, 2021 07:10
-
-
Save johndpope/8d4ecbdeb1ecb3188b55afc5282d544e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import AVFoundation | |
import UIKit | |
private let userDefaultsCachePathKey = "video.cache.path" | |
private let userDefaultsFileSizeKey = "video.cache.size" | |
private let userDefaultsLastAccessedDictionaryKey = "video.cache.last.accessed.key" // Key: last accessed, value: path | |
private let maxConcurrentDownloads = 2 | |
private let purgeCount = 20 | |
private let maxFileSize = 50_000_000 // 200 MB (around 133 videos @ 1.5MB ea) | |
extension AssetCachingManager: URLSessionTaskDelegate, URLSessionDataDelegate { | |
} | |
class AssetCachingManager: NSObject { | |
// MARK: Singleton | |
static var instance = AssetCachingManager() | |
public override init() { | |
} | |
func setup() { | |
guard let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } | |
if let existingPath = UserDefaults.standard.string(forKey: userDefaultsCachePathKey), FileManager.default.fileExists(atPath: documentsUrl.appendingPathComponent(existingPath).path) { | |
cacheFolderUrl = documentsUrl.appendingPathComponent(existingPath) | |
} else { | |
let path = UUID().uuidString | |
UserDefaults.standard.set(path, forKey: userDefaultsCachePathKey) | |
do { | |
try FileManager.default.createDirectory(at: documentsUrl.appendingPathComponent(path), withIntermediateDirectories: true) | |
} catch { | |
print("🔥", error) | |
return | |
} | |
cacheFolderUrl = documentsUrl.appendingPathComponent(path) | |
} | |
NotificationCenter.default.addObserver(self, selector: #selector(applicationWillResignActive(_:)), name: UIApplication.willResignActiveNotification, object: nil) | |
cleanUpIfNeeded() | |
} | |
// MARK: Variables | |
private var cacheFolderUrl: URL! | |
private var urlPathsBeingCached = [String]() | |
private var cachedAssets = [AVURLAsset]() | |
// MARK: Instance methods | |
func cacheAsset(withRemoteUrl remoteUrl: URL) { | |
guard urlPathsBeingCached.count <= maxConcurrentDownloads else { | |
print("WARNING: not caching - maximum concurrent downloads reached") | |
return | |
} | |
print("INFO: remoteUrl:", remoteUrl) | |
let path = remoteUrl.lastPathComponent | |
let cacheUrl = cacheFolderUrl.appendingPathComponent(path) | |
guard !urlPathsBeingCached.contains(path) else { | |
print("INFO: caching for \(path) already in progress") | |
return | |
} | |
print("caching video with path \(path)") | |
urlPathsBeingCached.append(path) | |
DispatchQueue.global(qos: .utility).async { [weak self] in | |
do { | |
let data = try Data(contentsOf: remoteUrl) | |
try data.write(to: cacheUrl) | |
let cachedAsset = AVURLAsset(url: cacheUrl) //main. | |
// cachedAsset.resourceLoader.setDelegate(AssetCachingManager.instance, queue: .main) | |
DispatchQueue(label: "video").async { | |
self?.cachedAssets.append(cachedAsset) | |
if let index = self?.urlPathsBeingCached.firstIndex(of: path) { | |
let index1 = 0 | |
if index1 <= self?.urlPathsBeingCached.count ?? 0 { | |
self?.urlPathsBeingCached.remove(at: index) | |
} | |
} | |
let totalSize = data.count + UserDefaults.standard.integer(forKey: userDefaultsFileSizeKey) | |
UserDefaults.standard.set(totalSize, forKey: userDefaultsFileSizeKey) | |
print("caching complete for url with path \(path)", totalSize ) | |
// print("videoSize:", cachedAsset.videoSize()) | |
} | |
} catch { | |
DispatchQueue.main.async { | |
print("caching error: \(error)") | |
if let index = self?.urlPathsBeingCached.firstIndex(of: path) { | |
self?.urlPathsBeingCached.remove(at: index) | |
} | |
} | |
} | |
} | |
} | |
func cachedAssetForRemoteUrl(_ remoteUrl: URL) -> AVAsset? { | |
let path = remoteUrl.lastPathComponent | |
print("caching remoteurl with path \(path)") | |
if urlPathsBeingCached.contains(path) { | |
return nil | |
} else if let cachedAsset = cachedAssets.first(where: { $0.url.lastPathComponent == path }) { | |
print("INFO: cachedAsset remote \(cachedAsset)") | |
updateLastAccessedAsset(at: path) | |
return cachedAsset | |
} else if FileManager.default.fileExists(atPath: cacheFolderUrl.appendingPathComponent(path).path) { | |
let asset = AVURLAsset(url: cacheFolderUrl.appendingPathComponent(path)) | |
print("INFO: filemanager asset remote \(asset)") | |
cachedAssets.append(asset) | |
updateLastAccessedAsset(at: path) | |
return asset | |
} else { | |
return nil | |
} | |
} | |
private func updateLastAccessedAsset(at path: String) { | |
var lastAccessedDictionary = UserDefaults.standard.dictionary(forKey: userDefaultsLastAccessedDictionaryKey) ?? [:] | |
lastAccessedDictionary[Date().timeIntervalSince1970.description] = path | |
UserDefaults.standard.set(lastAccessedDictionary, forKey: userDefaultsLastAccessedDictionaryKey) | |
} | |
static func removeAllCachedAssets() { | |
guard let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } | |
guard let cachePath = UserDefaults.standard.string(forKey: userDefaultsCachePathKey) else { return } | |
let cacheDirectoryUrl = documentsUrl.appendingPathComponent(cachePath) | |
do { | |
try FileManager.default.removeItem(at: cacheDirectoryUrl) | |
UserDefaults.standard.removeObject(forKey: userDefaultsCachePathKey) | |
UserDefaults.standard.removeObject(forKey: userDefaultsFileSizeKey) | |
UserDefaults.standard.removeObject(forKey: userDefaultsLastAccessedDictionaryKey) | |
NotificationCenter.default.removeObserver(instance) | |
instance = AssetCachingManager() | |
instance.setup() | |
} catch { | |
print("error removing all cached videos: \(error)") | |
} | |
} | |
private func cleanUpIfNeeded() { | |
guard UserDefaults.standard.integer(forKey: userDefaultsFileSizeKey) > maxFileSize else { return } | |
let fileManager = FileManager.default | |
if let lastAccessedDictionary = (UserDefaults.standard.dictionary(forKey: userDefaultsLastAccessedDictionaryKey) ?? [:]) as? [String: String] { | |
let lastAccessedDates = lastAccessedDictionary.map({ $0.0 }).compactMap({ Double($0) }).sorted(by: <) | |
print(lastAccessedDates) | |
var newDict = [String: String]() | |
var totalSize = UserDefaults.standard.integer(forKey: userDefaultsFileSizeKey) | |
print("initial: \(totalSize)") | |
lastAccessedDates.prefix(purgeCount).compactMap({ lastAccessedDictionary[$0.description] }).forEach { path in | |
print("clean up - removing \(path)") | |
let url = cacheFolderUrl.appendingPathComponent(path) | |
let fileSize = (try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize) | |
if let size = fileSize { | |
totalSize -= size | |
} | |
try? fileManager.removeItem(at: url) | |
} | |
print("final: \(totalSize)") | |
UserDefaults.standard.set(totalSize, forKey: userDefaultsFileSizeKey) | |
lastAccessedDates.dropFirst(purgeCount).forEach { date in | |
newDict[date.description] = lastAccessedDictionary[date.description] | |
} | |
UserDefaults.standard.set(newDict, forKey: userDefaultsLastAccessedDictionaryKey) | |
} | |
} | |
@objc private func applicationWillResignActive(_ notification: Notification) { | |
let fileManager = FileManager.default | |
urlPathsBeingCached.filter({ fileManager.fileExists(atPath: cacheFolderUrl.appendingPathComponent($0).path) }).forEach { path in | |
print("removing \(path)") | |
do { | |
try fileManager.removeItem(at: cacheFolderUrl.appendingPathComponent(path)) | |
} catch { | |
print("🔥", error) | |
} | |
} | |
urlPathsBeingCached.removeAll() | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) { | |
if UserDefaults.standard.bool(forKey: Constants.kEagerDownloading) { | |
var count = 0 | |
if (indexPaths.count == 1){ | |
// get 5 | |
let indexPath = indexPaths[0] | |
let idx0 = IndexPath(row: indexPath.row, section: 0) | |
let idx1 = IndexPath(row: indexPath.row+1, section: 0) | |
let idx2 = IndexPath(row: indexPath.row+2, section: 0) | |
let idx3 = IndexPath(row: indexPath.row+3, section: 0) | |
let idx4 = IndexPath(row: indexPath.row+4, section: 0) | |
let idx5 = IndexPath(row: indexPath.row+4, section: 0) | |
let prefetchIndexPaths = [idx0,idx1,idx2,idx3,idx4,idx5] | |
// PREFETCH THE VIDEO | |
for idx in prefetchIndexPaths{ | |
if self.feedCV.isValid(indexPath: idx){ | |
if let video = self.theatreDataSource?.itemIdentifier(for: idx) { | |
print("prefetch: download or not to download video:", video) | |
guard let stringUrl = video.videoUrl, let url = URL(string: stringUrl) else { | |
return | |
} | |
if let _ = AssetCachingManager.instance.cachedAssetForRemoteUrl(url) { | |
print("prefetch: cached video exists url:", url) | |
count += 1 | |
} else { | |
AssetCachingManager.instance.cacheAsset(withRemoteUrl: url) | |
print("prefetch: cached video dont exists url:", url) | |
} | |
// get the thumbnail | |
if let mediumUrl = video.media?.mediumUrl { | |
if let imageUrl = URL(string: mediumUrl) { | |
print("prefetch:", imageUrl) | |
ImagePrefetcher(urls: [imageUrl]).start() | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment