Skip to content

Commit 34bf19e

Browse files
committed
perf: Compress thumbnail size to avoid OOM crashes
1 parent bb11664 commit 34bf19e

2 files changed

Lines changed: 61 additions & 17 deletions

File tree

FileBrowserClient/FileBrowserClient/Configuration/Constants.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ struct Constants {
3030
static let maxConcurrentThumbnailRender: Int = 4
3131
static let thumbnailQuality: CGFloat = 0.1
3232
static let thumbnailRetryLimit: Int = 5
33+
static let maxThumbnailSize: CGFloat = 320
3334

3435
// MediaPlayerView:
3536
// Minimum duration (in secs) of a video to be considered resume worthy

FileBrowserClient/FileBrowserClient/DataManagers/RemoteThumbnail.swift

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ struct RemoteThumbnail: View {
136136
}
137137
}, threshold: 0))
138138
.id(file.path) // Ensure view resets when file changes
139+
.onDisappear {
140+
image = nil
141+
gifData = nil
142+
}
139143
}
140144

141145
func overlayPlayIcon(on image: UIImage) -> UIImage {
@@ -198,6 +202,36 @@ struct RemoteThumbnail: View {
198202
}
199203
}
200204

205+
func downsampleImage(
206+
data: Data,
207+
maxDimension: CGFloat
208+
) -> UIImage? {
209+
let sourceOptions: [CFString: Any] = [
210+
kCGImageSourceShouldCache: false
211+
]
212+
213+
guard let source = CGImageSourceCreateWithData(data as CFData, sourceOptions as CFDictionary) else {
214+
return nil
215+
}
216+
217+
let downsampleOptions: [CFString: Any] = [
218+
kCGImageSourceCreateThumbnailFromImageAlways: true,
219+
kCGImageSourceThumbnailMaxPixelSize: maxDimension,
220+
kCGImageSourceCreateThumbnailWithTransform: true,
221+
kCGImageSourceShouldCacheImmediately: true
222+
]
223+
224+
guard let cgImage = CGImageSourceCreateThumbnailAtIndex(
225+
source,
226+
0,
227+
downsampleOptions as CFDictionary
228+
) else {
229+
return nil
230+
}
231+
232+
return UIImage(cgImage: cgImage)
233+
}
234+
201235
private func actuallyLoadThumbnail(retryCount: Int = 0) {
202236
let fileName = file.name.lowercased()
203237
let isGIF = fileName.hasSuffix(".gif")
@@ -214,7 +248,7 @@ struct RemoteThumbnail: View {
214248
if let cached = existingCache {
215249
if isGIF {
216250
GlobalThumbnailLoader.shared.finish(filePath: file.path, image: nil, gifData: cached, failed: false)
217-
} else if let img = UIImage(data: cached) {
251+
} else if let img = downsampleImage(data: cached, maxDimension: 320) {
218252
GlobalThumbnailLoader.shared.finish(filePath: file.path, image: img, gifData: nil, failed: false)
219253
} else {
220254
// Cached data is corrupted, continue with network load
@@ -261,13 +295,18 @@ struct RemoteThumbnail: View {
261295
imageData = thumbImage.pngData()
262296
}
263297
if let data = imageData, advancedSettings.cacheThumbnail {
264-
FileCache.shared.store(
265-
for: serverURL,
266-
data: data,
267-
path: file.path,
268-
modified: file.modified,
269-
fileID: "thumb"
270-
)
298+
if let img = downsampleImage(data: data, maxDimension: Constants.maxThumbnailSize),
299+
let compressed = img.jpegData(compressionQuality: Constants.thumbnailQuality),
300+
advancedSettings.cacheThumbnail {
301+
302+
FileCache.shared.store(
303+
for: serverURL,
304+
data: compressed,
305+
path: file.path,
306+
modified: file.modified,
307+
fileID: "thumb"
308+
)
309+
}
271310
}
272311
GlobalThumbnailLoader.shared.finish(filePath: file.path, image: thumbImage, gifData: nil, failed: false)
273312
} catch {
@@ -298,20 +337,24 @@ struct RemoteThumbnail: View {
298337
}
299338
autoreleasepool {
300339
if advancedSettings.cacheThumbnail {
301-
FileCache.shared.store(
302-
for: serverURL,
303-
data: data,
304-
path: file.path,
305-
modified: file.modified,
306-
fileID: "thumb"
307-
)
340+
if let img = downsampleImage(data: data, maxDimension: Constants.maxThumbnailSize),
341+
let compressed = img.jpegData(compressionQuality: Constants.thumbnailQuality),
342+
advancedSettings.cacheThumbnail {
343+
FileCache.shared.store(
344+
for: serverURL,
345+
data: compressed,
346+
path: file.path,
347+
modified: file.modified,
348+
fileID: "thumb"
349+
)
350+
}
308351
}
309352
if isGIF {
310353
GlobalThumbnailLoader.shared.finish(filePath: file.path, image: nil, gifData: data, failed: false)
311-
} else if let img = UIImage(data: data) {
354+
} else if let img = downsampleImage(data: data, maxDimension: Constants.maxThumbnailSize) {
312355
GlobalThumbnailLoader.shared.finish(filePath: file.path, image: img, gifData: nil, failed: false)
313356
} else {
314-
let errMsg = "Failed to decode image thumbnail for file: \(file.name) — data size: \(data.count) bytes"
357+
let errMsg = "Failed to downsample image thumbnail for file: \(file.name)"
315358
retryHandler(retryCount: retryCount, errMsg: errMsg)
316359
}
317360
}

0 commit comments

Comments
 (0)