From 4f77a2a504aff2bd7db8ab0f96905544618068ce Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 29 Sep 2017 21:20:04 -0400 Subject: [PATCH] Load GIFs progressively using stills. // FREEBIE --- .../GifPicker/GifPickerCell.swift | 118 +++++++++++++----- .../GifPicker/GifPickerViewController.swift | 2 +- Signal/src/network/GifDownloader.swift | 34 +++-- Signal/src/network/GifManager.swift | 101 +++++++++++---- 4 files changed, 196 insertions(+), 59 deletions(-) diff --git a/Signal/src/ViewControllers/GifPicker/GifPickerCell.swift b/Signal/src/ViewControllers/GifPicker/GifPickerCell.swift index 022ab55cc..d71b6cd37 100644 --- a/Signal/src/ViewControllers/GifPicker/GifPickerCell.swift +++ b/Signal/src/ViewControllers/GifPicker/GifPickerCell.swift @@ -25,8 +25,10 @@ class GifPickerCell: UICollectionViewCell { } } - var assetRequest: GiphyAssetRequest? - var asset: GiphyAsset? + var stillAssetRequest: GiphyAssetRequest? + var stillAsset: GiphyAsset? + var fullAssetRequest: GiphyAssetRequest? + var fullAsset: GiphyAsset? var imageView: YYAnimatedImageView? // MARK: Initializers @@ -46,16 +48,29 @@ class GifPickerCell: UICollectionViewCell { imageInfo = nil shouldLoad = false - asset = nil - assetRequest?.cancel() - assetRequest = nil + stillAsset = nil + stillAssetRequest?.cancel() + stillAssetRequest = nil + fullAsset = nil + fullAssetRequest?.cancel() + fullAssetRequest = nil imageView?.removeFromSuperview() imageView = nil } + private func clearStillAssetRequest() { + stillAssetRequest?.cancel() + stillAssetRequest = nil + } + + private func clearFullAssetRequest() { + fullAssetRequest?.cancel() + fullAssetRequest = nil + } + private func clearAssetRequest() { - assetRequest?.cancel() - assetRequest = nil + clearStillAssetRequest() + clearFullAssetRequest() } private func ensureLoad() { @@ -67,31 +82,60 @@ class GifPickerCell: UICollectionViewCell { clearAssetRequest() return } - guard self.assetRequest == nil else { + guard self.fullAsset == nil else { return } - guard let rendition = imageInfo.pickGifRendition() else { - Logger.warn("\(TAG) could not pick rendition") + guard let fullRendition = imageInfo.pickGifRendition() else { + Logger.warn("\(TAG) could not pick gif rendition: \(imageInfo.giphyId)") +// imageInfo.log() clearAssetRequest() return } -// Logger.verbose("\(TAG) picked rendition: \(rendition.name)") - - assetRequest = GifDownloader.sharedInstance.downloadAssetAsync(rendition:rendition, - success: { [weak self] asset in - guard let strongSelf = self else { return } - strongSelf.clearAssetRequest() - strongSelf.asset = asset - strongSelf.tryToDisplayAsset() - }, - failure: { [weak self] in - guard let strongSelf = self else { return } - strongSelf.clearAssetRequest() - }) + guard let stillRendition = imageInfo.pickStillRendition() else { + Logger.warn("\(TAG) could not pick still rendition: \(imageInfo.giphyId)") +// imageInfo.log() + clearAssetRequest() + return + } +// Logger.verbose("picked full: \(fullRendition.name)") +// Logger.verbose("picked still: \(stillRendition.name)") + + if stillAsset == nil && fullAsset == nil && stillAssetRequest == nil { + stillAssetRequest = GifDownloader.sharedInstance.downloadAssetAsync(rendition:stillRendition, + priority:.high, + success: { [weak self] asset in +// Logger.verbose("downloaded still") + guard let strongSelf = self else { return } + strongSelf.clearStillAssetRequest() + strongSelf.stillAsset = asset + strongSelf.tryToDisplayAsset() + }, + failure: { [weak self] in +// Logger.verbose("failed to download still") + guard let strongSelf = self else { return } + strongSelf.clearStillAssetRequest() + }) + } + if fullAsset == nil && fullAssetRequest == nil { + fullAssetRequest = GifDownloader.sharedInstance.downloadAssetAsync(rendition:fullRendition, + priority:.low, + success: { [weak self] asset in +// Logger.verbose("downloaded full") + guard let strongSelf = self else { return } + strongSelf.clearAssetRequest() + strongSelf.fullAsset = asset + strongSelf.tryToDisplayAsset() + }, + failure: { [weak self] in +// Logger.verbose("failed to download full") + guard let strongSelf = self else { return } + strongSelf.clearAssetRequest() + }) + } } private func tryToDisplayAsset() { - guard let asset = asset else { + guard let asset = pickBestAsset() else { owsFail("\(TAG) missing asset.") return } @@ -99,11 +143,27 @@ class GifPickerCell: UICollectionViewCell { owsFail("\(TAG) could not load asset.") return } - let imageView = YYAnimatedImageView() - self.imageView = imageView + if imageView == nil { + let imageView = YYAnimatedImageView() + self.imageView = imageView + self.contentView.addSubview(imageView) + imageView.autoPinWidthToSuperview() + imageView.autoPinHeightToSuperview() + } + guard let imageView = imageView else { + owsFail("\(TAG) missing imageview.") + return + } imageView.image = image - self.contentView.addSubview(imageView) - imageView.autoPinWidthToSuperview() - imageView.autoPinHeightToSuperview() + } + + private func pickBestAsset() -> GiphyAsset? { + if let fullAsset = fullAsset { + return fullAsset + } + if let stillAsset = stillAsset { + return stillAsset + } + return nil } } diff --git a/Signal/src/ViewControllers/GifPicker/GifPickerViewController.swift b/Signal/src/ViewControllers/GifPicker/GifPickerViewController.swift index a783f2e7a..57ad2603a 100644 --- a/Signal/src/ViewControllers/GifPicker/GifPickerViewController.swift +++ b/Signal/src/ViewControllers/GifPicker/GifPickerViewController.swift @@ -164,7 +164,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect owsFail("\(TAG) unexpected cell.") return } - guard let asset = cell.asset else { + guard let asset = cell.fullAsset else { Logger.info("\(TAG) unload cell selected.") return } diff --git a/Signal/src/network/GifDownloader.swift b/Signal/src/network/GifDownloader.swift index eac52618d..22f792547 100644 --- a/Signal/src/network/GifDownloader.swift +++ b/Signal/src/network/GifDownloader.swift @@ -5,20 +5,27 @@ import Foundation import ObjectiveC +enum GiphyRequestPriority { + case low, high +} + @objc class GiphyAssetRequest: NSObject { static let TAG = "[GiphyAssetRequest]" let rendition: GiphyRendition + let priority: GiphyRequestPriority let success: ((GiphyAsset) -> Void) let failure: (() -> Void) var wasCancelled = false var assetFilePath: String? init(rendition: GiphyRendition, + priority: GiphyRequestPriority, success:@escaping ((GiphyAsset) -> Void), failure:@escaping (() -> Void) ) { self.rendition = rendition + self.priority = priority self.success = success self.failure = failure } @@ -121,6 +128,7 @@ extension URLSessionTask { // The success and failure handlers are always called on main queue. // The success and failure handlers may be called synchronously on cache hit. public func downloadAssetAsync(rendition: GiphyRendition, + priority: GiphyRequestPriority, success:@escaping ((GiphyAsset) -> Void), failure:@escaping (() -> Void)) -> GiphyAssetRequest? { AssertIsOnMainThread() @@ -132,6 +140,7 @@ extension URLSessionTask { var hasRequestCompleted = false let assetRequest = GiphyAssetRequest(rendition:rendition, + priority:priority, success : { asset in DispatchQueue.main.async { // Ensure we call success or failure exactly once. @@ -171,14 +180,9 @@ extension URLSessionTask { guard !self.isDownloading else { return } - guard self.assetRequestQueue.count > 0 else { - return - } - guard let assetRequest = self.assetRequestQueue.first else { - owsFail("\(GiphyAsset.TAG) could not pop asset requests") + guard let assetRequest = self.popNextAssetRequest() else { return } - self.assetRequestQueue.removeFirst() guard !assetRequest.wasCancelled else { DispatchQueue.main.async { self.downloadIfNecessary() @@ -188,7 +192,7 @@ extension URLSessionTask { self.isDownloading = true if let asset = self.assetMap[assetRequest.rendition.url] { - // Deferred cache hit, avoids re-downloading assets already in the + // Deferred cache hit, avoids re-downloading assets already in the // asset cache. assetRequest.success(asset) return @@ -206,6 +210,22 @@ extension URLSessionTask { } } + private func popNextAssetRequest() -> GiphyAssetRequest? { + AssertIsOnMainThread() + +// var result : GiphyAssetRequest? + for priority in [GiphyRequestPriority.high, GiphyRequestPriority.low] { + for (assetRequestIndex, assetRequest) in assetRequestQueue.enumerated() { + if assetRequest.priority == priority { + assetRequestQueue.remove(at:assetRequestIndex) + return assetRequest + } + } + } + + return nil + } + // MARK: URLSessionDataDelegate @nonobjc diff --git a/Signal/src/network/GifManager.swift b/Signal/src/network/GifManager.swift index 0b1e8f4a5..01023259f 100644 --- a/Signal/src/network/GifManager.swift +++ b/Signal/src/network/GifManager.swift @@ -7,7 +7,7 @@ import ObjectiveC // There's no UTI type for webp! enum GiphyFormat { - case gif, mp4 + case gif, mp4, jpg } @objc class GiphyRendition: NSObject { @@ -38,6 +38,8 @@ enum GiphyFormat { return "gif" case .mp4: return "mp4" + case .jpg: + return "jpg" } } @@ -47,8 +49,14 @@ enum GiphyFormat { return kUTTypeGIF as String case .mp4: return kUTTypeMPEG4 as String + case .jpg: + return kUTTypeJPEG as String } } + + public func log() { + Logger.verbose("\t \(format), \(name), \(width), \(height), \(fileSize)") + } } @objc class GiphyImageInfo: NSObject { @@ -69,33 +77,67 @@ enum GiphyFormat { let kMinDimension = UInt(101) let kMaxFileSize = UInt(3 * 1024 * 1024) + public func log() { + Logger.verbose("giphyId: \(giphyId), \(renditions.count)") + for rendition in renditions { + rendition.log() + } + } + + public func pickStillRendition() -> GiphyRendition? { + return pickRendition(isStill:true) + } + public func pickGifRendition() -> GiphyRendition? { + return pickRendition(isStill:false) + } + + private func pickRendition(isStill: Bool) -> GiphyRendition? { var bestRendition: GiphyRendition? for rendition in renditions { - guard rendition.format == .gif else { - continue - } - guard !rendition.name.hasSuffix("_still") - else { + if isStill { + guard [.gif, .jpg].contains(rendition.format) else { continue - } - guard !rendition.name.hasSuffix("_downsampled") - else { - continue - } - guard rendition.width >= kMinDimension && - rendition.width <= kMaxDimension && - rendition.height >= kMinDimension && - rendition.height <= kMaxDimension && - rendition.fileSize <= kMaxFileSize - else { + } + guard rendition.name.hasSuffix("_still") else { + continue + } + guard rendition.width >= kMinDimension && + rendition.height >= kMinDimension && + rendition.fileSize <= kMaxFileSize + else { + continue + } + } else { + guard rendition.format == .gif else { continue + } + guard !rendition.name.hasSuffix("_still") else { + continue + } + guard !rendition.name.hasSuffix("_downsampled") else { + continue + } + guard rendition.width >= kMinDimension && + rendition.width <= kMaxDimension && + rendition.height >= kMinDimension && + rendition.height <= kMaxDimension && + rendition.fileSize <= kMaxFileSize + else { + continue + } } if let currentBestRendition = bestRendition { - if rendition.width > currentBestRendition.width { - bestRendition = rendition + if isStill { + if rendition.width < currentBestRendition.width { + bestRendition = rendition + } + } else { + if rendition.width > currentBestRendition.width { + bestRendition = rendition + } } } else { bestRendition = rendition @@ -191,6 +233,8 @@ enum GiphyFormat { // MARK: Parse API Responses private func parseGiphyImages(responseJson:Any?) -> [GiphyImageInfo]? { +// Logger.verbose("\(responseJson)") + guard let responseJson = responseJson else { Logger.error("\(GifManager.TAG) Missing response.") return nil @@ -292,14 +336,27 @@ enum GiphyFormat { Logger.warn("\(GifManager.TAG) Rendition url missing file extension.") return nil } - guard fileExtension.lowercased() == "gif" else { -// Logger.verbose("\(GifManager.TAG) Rendition has invalid type: \(fileExtension).") + var format = GiphyFormat.gif + if fileExtension.lowercased() == "gif" { + format = .gif + } else if fileExtension.lowercased() == "jpg" { + format = .jpg + } else if fileExtension.lowercased() == "mp4" { + format = .mp4 + } else if fileExtension.lowercased() == "webp" { + return nil + } else { + Logger.warn("\(GifManager.TAG) Invalid file extension: \(fileExtension).") return nil } +// guard fileExtension.lowercased() == "gif" else { +//// Logger.verbose("\(GifManager.TAG) Rendition has invalid type: \(fileExtension).") +// return nil +// } // Logger.debug("\(GifManager.TAG) Rendition successfully parsed.") return GiphyRendition( - format : .gif, + format : format, name : renditionName, width : width, height : height,