-
-
Save christianselig/50b9cd47ed9e5f7a7e580930f4c8c2b5 to your computer and use it in GitHub Desktop.
import UIKit | |
import AVFoundation | |
import Photos | |
import MobileCoreServices | |
class ViewController: UIViewController { | |
override func viewDidAppear(_ animated: Bool) { | |
super.viewDidAppear(animated) | |
startVideoToGIFProcess() | |
} | |
func startVideoToGIFProcess() { | |
// Download the video and write it to temp storage | |
print("Downloading video…") | |
let data = try! Data(contentsOf: URL(string: "https://i.imgur.com/dXxP7a9.mp4")!) | |
let fileName = String(format: "%@_%@", ProcessInfo.processInfo.globallyUniqueString, "html5gif.mp4") | |
let fileURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName) | |
try! data.write(to: fileURL, options: [.atomic]) | |
createGIF(fromVideoAtURL: fileURL) | |
} | |
func createGIF(fromVideoAtURL url: URL) { | |
print("Downloaded!") | |
let frameRate: Int = 20 | |
let duration: TimeInterval = 9.68 | |
let totalFrames = Int(duration * TimeInterval(frameRate)) | |
let delayBetweenFrames: TimeInterval = 1.0 / TimeInterval(frameRate) | |
var timeValues: [NSValue] = [] | |
for frameNumber in 0 ..< totalFrames { | |
let seconds = TimeInterval(delayBetweenFrames) * TimeInterval(frameNumber) | |
let time = CMTime(seconds: seconds, preferredTimescale: Int32(NSEC_PER_SEC)) | |
timeValues.append(NSValue(time: time)) | |
} | |
let asset = AVURLAsset(url: url) | |
let generator = AVAssetImageGenerator(asset: asset) | |
generator.requestedTimeToleranceBefore = CMTime(seconds: 0.05, preferredTimescale: 600) | |
generator.requestedTimeToleranceAfter = CMTime(seconds: 0.05, preferredTimescale: 600) | |
let sizeModifier: CGFloat = 0.1 | |
generator.maximumSize = CGSize(width: 450.0 * sizeModifier, height: 563.0 * sizeModifier) | |
// Set up resulting image | |
let fileProperties: [String: Any] = [ | |
kCGImagePropertyGIFDictionary as String: [ | |
kCGImagePropertyGIFLoopCount as String: 0 | |
] | |
] | |
let frameProperties: [String: Any] = [ | |
kCGImagePropertyGIFDictionary as String: [ | |
kCGImagePropertyGIFDelayTime: delayBetweenFrames | |
] | |
] | |
let resultingFilename = String(format: "%@_%@", ProcessInfo.processInfo.globallyUniqueString, "html5gif.gif") | |
let resultingFileURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(resultingFilename) | |
let destination = CGImageDestinationCreateWithURL(resultingFileURL as CFURL, kUTTypeGIF, totalFrames, nil)! | |
CGImageDestinationSetProperties(destination, fileProperties as CFDictionary) | |
print("Converting to GIF…") | |
var framesProcessed = 0 | |
let startTime = CFAbsoluteTimeGetCurrent() | |
generator.generateCGImagesAsynchronously(forTimes: timeValues) { (requestedTime, resultingImage, actualTime, result, error) in | |
guard let resultingImage = resultingImage else { return } | |
framesProcessed += 1 | |
CGImageDestinationAddImage(destination, resultingImage, frameProperties as CFDictionary) | |
if framesProcessed == totalFrames { | |
let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime | |
print("Done converting to GIF! Frames processed: \(framesProcessed) • Total time: \(timeElapsed) s.") | |
// Save to Photos just to check… | |
let result = CGImageDestinationFinalize(destination) | |
print("Did it succeed?", result) | |
if result { | |
print("Saving to Photos…") | |
PHPhotoLibrary.shared().performChanges({ | |
PHAssetCreationRequest.creationRequestForAssetFromImage(atFileURL: resultingFileURL) | |
}) { (saved, err) in | |
print("Saved?", saved) | |
} | |
} | |
} | |
} | |
} | |
} |
Hey @christianselig thanks so much for releasing this code! What's the license for the code?
@bl791 Oo, fair question! Let's go with MIT! Also for what it's worth I have a vague memory of getting the "native" version to a speed that matched ffmpeg-mobile. I don't totally remember what I did, but I don't think it was anything too intense, I think I was just getting the iOS native version to do more and had some unnecessary parts, so I don't think you need ffmpeg-mobile by any means (I definitely did not end up shipping it in my app purely because of the size of the library)
Nice, thank you so much!
Sorry, one more question - will this work for very large GIFs (ie over a minute)?
@bl791 I looked in Apollo and I have this code, so I'm going to guess right around a minute should be the maximum otherwise iOS will probably run out of memory due to GIFs being a very inefficient video storage format:
/// If the video is any longer than this, do not offer to save it as a GIF
static var upperLimitDurationThresholdForGIF: TimeInterval = 65.0
Thanks!
ffmpeg
version using mobile ffmpeg with themobile-ffmpeg-min
specification:About 10x faster on devices like iPhone 6s. 😃The size of ffmpeg means my app size would increase by about 30% for a single feature. 😭
Exporting this MP4 to GIF on an iPhone 6s takes 29 seconds with the AVFoundation solution and a little under 3 seconds with ffmpeg. Resulting GIF looks very similar in terms of quality (which is to say pretty good for a GIF).
ffmpeg GIF: https://christianselig.com/ffmpeg.gif
AVFoundation GIF: https://christianselig.com/avasset.gif