Skip to content

Instantly share code, notes, and snippets.

@johndpope
Created April 21, 2021 07:10
Show Gist options
  • Save johndpope/8d4ecbdeb1ecb3188b55afc5282d544e to your computer and use it in GitHub Desktop.
Save johndpope/8d4ecbdeb1ecb3188b55afc5282d544e to your computer and use it in GitHub Desktop.
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()
}
}
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