preface

Recently, I encountered the problem of converting GIF into MP4. There were too many restrictions on converting online, so I simply wrote a tool APP. Open source code and packaged APP are available at the end of the article. Please download it yourself if necessary.

rendering

The core code

source

import ImageIO #if os(iOS) import MobileCoreServices #elseif os(OSX) import CoreServices #endif class GIF { private let FrameDelayThreshold = 0.02 private(set) var duration = 0.0 private(set) var imageSource: CGImageSource! private(set) var frames: [CGImage?] ! private(set) lazy var frameDurations = [TimeInterval]() var size: CGSize { guard let f = frames.first, let cgImage = f else { return .zero } return CGSize(width: cgImage.width, height: cgImage.height) } private lazy var getFrameQueue: DispatchQueue = DispatchQueue(label: "gif.frame.queue", qos: .userInteractive) init?(data: Data) { guard let imgSource = CGImageSourceCreateWithData(data as CFData, nil), let imgType = CGImageSourceGetType(imgSource), UTTypeConformsTo(imgType, kUTTypeGIF) else { return nil } self.imageSource = imgSource let imgCount = CGImageSourceGetCount(imageSource) frames = [CGImage?](repeating: nil, count: imgCount) for i in 0.. <imgCount { let delay = getGIFFrameDuration(imgSource: imageSource, index: i) frameDurations.append(delay) duration += delay getFrameQueue.async { [unowned self] in self.frames[i] = CGImageSourceCreateImageAtIndex(self.imageSource, i, nil) } } } func getFrame(at index: Int) -> CGImage? { if index >= CGImageSourceGetCount(imageSource) { return nil } if let frame = frames[index] { return frame } else { let frame = CGImageSourceCreateImageAtIndex(imageSource, index, nil) frames[index] = frame return frame } } private func getGIFFrameDuration(imgSource: CGImageSource, index: Int) -> TimeInterval { guard let frameProperties = CGImageSourceCopyPropertiesAtIndex(imgSource, index, nil) as? [String: Any], let gifProperties = frameProperties[kCGImagePropertyGIFDictionary as String] as? NSDictionary, Let unclampedDelay = gifProperties [kCGImagePropertyGIFUnclampedDelayTime] as? TimeInterval else {return} 0.02 var frameDuration = TimeInterval(0) if unclampedDelay < 0 { frameDuration = gifProperties[kCGImagePropertyGIFDelayTime] as? TimeInterval?? 0.0} else {frameDuration = unclampedDelay} /* Implement as Browsers do: Supports frame Delays as Low as 0.02s, With anything below that being rounded up to 0.10s. http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser-compatibility */ if frameDuration < framedelaythresholddouble. UlpOfOne {frameDuration = 0.1} return frameDuration}}Copy the code
#if os(iOS) import UIKit #elseif os(OSX) import Cocoa #endif import Foundation import AVFoundation class GIF2MP4 { private(set) var gif: GIF private var outputURL: URL! private(set) var videoWriter: AVAssetWriter! private(set) var videoWriterInput: AVAssetWriterInput! private(set) var pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor! var videoSize: CGSize { //The size of the video must be a multiple of 16 return CGSize(width: max(1, floor(gif.size.width / 16)) * 16, height: max(1, floor(gif.size.height / 16)) * 16) } init? (data: Data) { guard let gif = GIF(data: data) else { return nil } self.gif = gif } private func prepare() { try? FileManager.default.removeItem(at: outputURL) let avOutputSettings: [String: Any] = [ AVVideoCodecKey: AVVideoCodecType.h264, AVVideoWidthKey: NSNumber(value: Float(videoSize.width)), AVVideoHeightKey: NSNumber(value: Float(videoSize.height)) ] let sourcePixelBufferAttributesDictionary = [ kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: kCVPixelFormatType_32ARGB), kCVPixelBufferWidthKey as String: NSNumber(value: Float(videoSize.width)), kCVPixelBufferHeightKey as String: NSNumber(value: Float(videoSize.height)) ] videoWriter = try! AVAssetWriter(outputURL: outputURL, fileType: AVFileType.mp4) videoWriterInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: avOutputSettings) videoWriter.add(videoWriterInput) pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoWriterInput, sourcePixelBufferAttributes: sourcePixelBufferAttributesDictionary) videoWriter.startWriting() videoWriter.startSession(atSourceTime: CMTime.zero) } func convertAndExport(to url: URL, completion: @escaping (Bool) -> Void) {outputURL = url prepare() var index = 0 var delay = 0.0-gif. frameDurations[0] let queue =  DispatchQueue(label: "mediaInputQueue") videoWriterInput.requestMediaDataWhenReady(on: queue) { var isFinished = true var isSuccess = true while index < self.gif.frames.count { if self.videoWriterInput.isReadyForMoreMediaData == false { isFinished = false break } if let cgImage = self.gif.getFrame(at: index) { let frameDuration = self.gif.frameDurations[index] delay += Double(frameDuration) let presentationTime = CMTime(seconds: delay, preferredTimescale: 600) let result = self.addImage(image: cgImage, withPresentationTime: presentationTime) if result == false { isSuccess = false break } else { index += 1 } } } if isFinished { self.videoWriterInput.markAsFinished() self.videoWriter.finishWriting { DispatchQueue.main.async { completion(isSuccess)  } } } else { // Fall through. The closure will be called again when the writer is ready. } } } private func addImage(image: CGImage, withPresentationTime presentationTime: CMTime) -> Bool { guard let pixelBufferPool = pixelBufferAdaptor.pixelBufferPool else { print("pixelBufferPool is nil ")  return false } let pixelBuffer = pixelBufferFromImage(image: image, pixelBufferPool: pixelBufferPool, size: videoSize) return pixelBufferAdaptor.append(pixelBuffer, withPresentationTime: presentationTime) } private func pixelBufferFromImage(image: CGImage, pixelBufferPool: CVPixelBufferPool, size: CGSize) -> CVPixelBuffer { var pixelBufferOut: CVPixelBuffer? let status = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pixelBufferPool, &pixelBufferOut) if status ! = kCVReturnSuccess { fatalError("CVPixelBufferPoolCreatePixelBuffer() failed") } let pixelBuffer = pixelBufferOut! CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0))) let data = CVPixelBufferGetBaseAddress(pixelBuffer) let rgbColorSpace = CGColorSpaceCreateDeviceRGB()  let context = CGContext(data: data, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer), space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue)! context.clear(CGRect(x: 0, y: 0, width: size.width, height: size.height)) let horizontalRatio = size.width / CGFloat(image.width) let verticalRatio = size.height / CGFloat(image.height) let aspectRatio = max(horizontalRatio, verticalRatio) // ScaleAspectFill //let aspectRatio = min(horizontalRatio, verticalRatio) // ScaleAspectFit let newSize = CGSize(width: CGFloat(image.width) * aspectRatio, height: CGFloat(image.height) * aspectRatio) let x = newSize.width < size.width ? (size.width - newSize.width) / 2: -(newSize.width-size.width)/2 let y = newSize.height < size.height ? (size.height - newSize.height) / 2: -(newSize.height-size.height)/2 context.draw(image, in: CGRect(x: x, y: y, width: newSize.width, height: newSize.height)) CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0))) return pixelBuffer } }Copy the code

Using the step

  1. Open the APP
  2. willgifDrag and drop into the window

Q&A

Macos 10.15 will cause damage to your computer and you should move it to the wastebasket

  1. APP right click – Show Profile – Override malware protection check box
  2. Open a terminalcodesign -f -s - --deep /Applications/appname.app

conclusion

The code supports iOS and macOS download address —–>>>GIF2MP4, if helpful to you, welcome Star.