Skip to content

Instantly share code, notes, and snippets.

@christianselig
Last active February 9, 2024 16:50
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save christianselig/50b9cd47ed9e5f7a7e580930f4c8c2b5 to your computer and use it in GitHub Desktop.
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)
}
}
}
}
}
}
@christianselig
Copy link
Author

christianselig commented Mar 20, 2020

ffmpeg version using mobile ffmpeg with the mobile-ffmpeg-min specification:

do {
    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])

    print("Downloaded, starting GIF conversion…")

    let outfileName = String(format: "%@_%@", ProcessInfo.processInfo.globallyUniqueString, "outfile.gif")
    let outfileURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(outfileName)

    let startTime = CFAbsoluteTimeGetCurrent()

    let _ = MobileFFmpeg.execute("-i \(fileURL.path) -vf fps=20,scale=450:-1 \(outfileURL.path)")

    let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
    print("Time elapsed: \(timeElapsed) s.")

    PHPhotoLibrary.shared().performChanges({
        PHAssetCreationRequest.creationRequestForAssetFromImage(atFileURL: outfileURL)
    }) { (saved, err) in
        print("Saved?", saved)
    }
} catch {
    print("Error was:", error)
}

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

@bl791
Copy link

bl791 commented Feb 9, 2024

Hey @christianselig thanks so much for releasing this code! What's the license for the code?

@christianselig
Copy link
Author

@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)

@bl791
Copy link

bl791 commented Feb 9, 2024

Nice, thank you so much!

@bl791
Copy link

bl791 commented Feb 9, 2024

Sorry, one more question - will this work for very large GIFs (ie over a minute)?

@christianselig
Copy link
Author

@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

@bl791
Copy link

bl791 commented Feb 9, 2024

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment