iOS开发之Video转GIF
前言
最近遇到需要将video转化为gif的问题,网上找的在线转换限制太多,索性就自己写了一个工具APP。文章末尾有开源代码和打包好的APP,如有需要请自行下载。
效果图

核心代码
class MP4ToGIF {
    private var videoUrl: URL!
    init(videoUrl: URL) {
        self.videoUrl = videoUrl
    }
    func convertAndExport(to url: URL, cappedResolution: CGFloat?, desiredFrameRate: CGFloat?, completion: ((Bool) -> ())) {
        var isSuccess: Bool = false
        defer {
            completion(isSuccess)
        }
        // Do the converties
        let asset = AVURLAsset(url: videoUrl)
        guard let reader = try? AVAssetReader(asset: asset),
              let videoTrack = asset.tracks(withMediaType: .video).first
        else {
            return
        }
        let videoSize = videoTrack.naturalSize.applying(videoTrack.preferredTransform)
        // Restrict it to 480p (max in either dimension), it's a GIF, no need to have it in crazy 1080p (saves encoding time a lot, too)
        let aspectRatio = videoSize.width / videoSize.height
        let duration: CGFloat = CGFloat(asset.duration.seconds)
        let nominalFrameRate = CGFloat(videoTrack.nominalFrameRate)
        let nominalTotalFrames = Int(round(duration * nominalFrameRate))
        let resultingSize: CGSize = {
            if let cappedResolution = cappedResolution {
                if videoSize.width > videoSize.height {
                    let cappedWidth = round(min(cappedResolution, videoSize.width))
                    return CGSize(width: cappedWidth, height: round(cappedWidth / aspectRatio))
                } else {
                    let cappedHeight = round(min(cappedResolution, videoSize.height))
                    return CGSize(width: round(cappedHeight * aspectRatio), height: cappedHeight)
                }
            }else {
                return videoSize
            }
        }()
        // In order to convert from, say 30 FPS to 20, we'd need to remove 1/3 of the frames, this applies that math and decides which frames to remove/not process
        let framesToRemove: [Int] = {
            // Ensure the actual/nominal frame rate isn't already lower than the desired, in which case don't even worry about it
            if let desiredFrameRate = desiredFrameRate, desiredFrameRate < nominalFrameRate {
                let percentageOfFramesToRemove = 1.0 - (desiredFrameRate / nominalFrameRate)
                let totalFramesToRemove = Int(round(CGFloat(nominalTotalFrames) * percentageOfFramesToRemove))
                // We should remove a frame every `frameRemovalInterval` frames…
                // Since we can't remove e.g.: the 3.7th frame, round that up to 4, and we'd remove the 4th frame, then the 7.4th -> 7th, etc.
                let frameRemovalInterval = CGFloat(nominalTotalFrames) / CGFloat(totalFramesToRemove)
                var framesToRemove: [Int] = []
                var sum: CGFloat = 0.0
                while sum <= CGFloat(nominalTotalFrames) {
                    sum += frameRemovalInterval
                    if sum > CGFloat(nominalTotalFrames) { break }
                    let roundedFrameToRemove = Int(round(sum))
                    framesToRemove.append(roundedFrameToRemove)
                }
                return framesToRemove
            } else {
                return []
            }
        }()
        let totalFrames = nominalTotalFrames - framesToRemove.count
        let outputSettings: [String: Any] = [
            kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32ARGB,
            kCVPixelBufferWidthKey as String: resultingSize.width,
            kCVPixelBufferHeightKey as String: resultingSize.height
        ]
        let readerOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: outputSettings)
        reader.add(readerOutput)
        reader.startReading()
        let delayBetweenFrames: CGFloat = 1.0 / min(desiredFrameRate ?? nominalFrameRate, nominalFrameRate)
        print("Nominal total frames: \(nominalTotalFrames), totalFramesUsed: \(totalFrames), totalFramesToRemove: \(framesToRemove.count), nominalFrameRate: \(nominalFrameRate), delayBetweenFrames: \(delayBetweenFrames)")
        let fileProperties: [String: Any] = [
            kCGImagePropertyGIFDictionary as String: [
                kCGImagePropertyGIFLoopCount as String: 0
            ]
        ]
        let frameProperties: [String: Any] = [
            kCGImagePropertyGIFDictionary as String: [
                kCGImagePropertyGIFDelayTime: delayBetweenFrames
            ]
        ]
        guard let destination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypeGIF, totalFrames, nil) else {
            return
        }
        CGImageDestinationSetProperties(destination, fileProperties as CFDictionary)
        let operationQueue = OperationQueue()
        operationQueue.maxConcurrentOperationCount = 1
        var framesCompleted = 0
        var currentFrameIndex = 0
        while currentFrameIndex < nominalTotalFrames {
            if let sample = readerOutput.copyNextSampleBuffer() {
                currentFrameIndex += 1
                if framesToRemove.contains(currentFrameIndex) {
                    continue
                }
                // Create it as an optional and manually nil it out every time it's finished otherwise weird Swift bug where memory will balloon enormously (see https://twitter.com/ChristianSelig/status/1241572433095770114)
                var cgImage: CGImage? = self.cgImageFromSampleBuffer(sample)
                operationQueue.addOperation {
                    framesCompleted += 1
                    if let cgImage = cgImage {
                        CGImageDestinationAddImage(destination, cgImage, frameProperties as CFDictionary)
                    }
                    cgImage = nil
                    //                    let progress = CGFloat(framesCompleted) / CGFloat(totalFrames)
                    // GIF progress is a little fudged so it works with downloading progress reports
                    //                    let progressToReport = Int(progress * 100.0)
                    //                    print(progressToReport)
                }
            }
        }
        operationQueue.waitUntilAllOperationsAreFinished()
        isSuccess = CGImageDestinationFinalize(destination)
    }
}
extension MP4ToGIF {
    private func cgImageFromSampleBuffer(_ buffer: CMSampleBuffer) -> CGImage? {
        guard let pixelBuffer = CMSampleBufferGetImageBuffer(buffer) else {
            return nil
        }
        CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
        let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer)
        let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
        let width = CVPixelBufferGetWidth(pixelBuffer)
        let height = CVPixelBufferGetHeight(pixelBuffer)
        guard let context = CGContext(data: baseAddress, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue) else { return nil }
        let image = context.makeImage()
        CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
        return image
    }
}
使用步骤
- 打开APP
 - 将
video文件拖拽到窗口中 
常见问题
macos 10.15 将对您的电脑造成伤害,您应该将它移到废纸篓
- APP右键-显示简介-覆盖恶意软件保护 打勾
 - 打开终端 
codesign -f -s - --deep /Applications/appname.app 
总结
代码支持iOS和macOS
下载地址----->>>MP4ToGIF,如果对你有所帮助,欢迎Star。
iOS开发之Video转GIF的更多相关文章
- iOS 开发之 GCD 不同场景使用
		
header{font-size:1em;padding-top:1.5em;padding-bottom:1.5em} .markdown-body{overflow:hidden} .markdo ...
 - iOS 开发之 GCD 基础
		
header{font-size:1em;padding-top:1.5em;padding-bottom:1.5em} .markdown-body{overflow:hidden} .markdo ...
 - iOS开发之Socket通信实战--Request请求数据包编码模块
		
实际上在iOS很多应用开发中,大部分用的网络通信都是http/https协议,除非有特殊的需求会用到Socket网络协议进行网络数 据传输,这时候在iOS客户端就需要很好的第三方CocoaAsyncS ...
 - iOS开发之UISearchBar初探
		
iOS开发之UISearchBar初探 UISearchBar也是iOS开发常用控件之一,点进去看看里面的属性barStyle.text.placeholder等等.但是这些属性显然不足矣满足我们的开 ...
 - iOS开发之UIImage等比缩放
		
iOS开发之UIImage等比缩放 评论功能真不错 评论开通后,果然有很多人吐槽.谢谢大家的支持和关爱,如果有做的不到的地方,还请海涵.毕竟我一个人的力量是有限的,我会尽自己最大的努力大家准备一些干货 ...
 - iOS开发之  Xcode6 添加xib文件,去掉storyboard的hello world应用
		
iOS开发之 Xcode6.1创建仅xib文件,无storyboard的hello world应用 由于Xcode6之后,默认创建storyboard而非xib文件,而作为初学,了解xib的加载原理 ...
 - iOS开发之loadView、viewDidLoad及viewDidUnload的关系
		
iOS开发之loadView.viewDidLoad及viewDidUnload的关系 iOS开发之loadView.viewDidLoad及viewDidUnload的关系 标题中所说的3个方 ...
 - iOS开发之info.pist文件和.pch文件
		
iOS开发之info.pist文件和.pch文件 如果你是iOS开发初学者,不用过多的关注项目中各个文件的作用.因为iOS开发的学习路线起点不在这里,这些文件只会给你学习带来困扰. 打开一个项目,我们 ...
 - iOS开发之WKWebView简单使用
		
iOS开发之WKWebView简单使用 iOS开发之 WKWebVeiw使用 想用UIWebVeiw做的,但是突然想起来在iOS8中出了一个新的WKWebView,算是UIWebVeiw的升级版. ...
 
随机推荐
- Redisson 分布式锁源码 09:RedLock 红锁的故事
			
前言 RedLock 红锁,是分布式锁中必须要了解的一个概念. 所以本文会先介绍什么是 RedLock,当大家对 RedLock 有一个基本的了解.然后再看 Redisson 中是如何实现 RedLo ...
 - 接口自动化框架搭建Unittes+HTMLTestRunner
			
本次主要尝试搭建接口自动化框架,基于 unittest+HTMLTestRunner 框架主要模块: config: 存放配置文件 lib: 封装了一些接口前置函数:处理各种事物 log: 存放生成的 ...
 - mysql 去重的两种方式
			
1.distinct一般用于获取不重复字段的条数 使用原则: 1)distinct必须放在要查询字段的开头,不能放在查询字段的中间或者后面 select distinct name from user ...
 - 使用过redis做异步队列么,你是怎么用的?有什么缺点?
			
Redis设计主要是用来做缓存的,但是由于它自身的某种特性使得它可以用来做消息队列. 它有几个阻塞式的API可以使用,正是这些阻塞式的API让其有能力做消息队列: 另外,做消息队列的其他特性例如FIF ...
 - 9 shell 退出状态
			
退出状态和逻辑运算符的组合 Shell 逻辑运算符 举栗 命令的退出状态(exit statu)是指执行完Linux命令或shell函数后,该命令或函数返回给调用它的程序的一个比较小的整数值.if 语 ...
 - 各种学位&不同学段的表达
			
1.学士 B.S.=Bachelor of Science 2.硕士 Master  MA.Sc(master of Science科学硕士)  MA.Eng(master of engineer ...
 - 安装PyTorch后,又安装TensorFlow,CUDA相关问题思考
			
下面的话是我的观察和思考,请多多批评. TensorFlow 要用 CUDA.CUDA toolkit.CUDNN,看好版本的对应关系再安装,磨刀不误砍柴工. 1)NVIDIA Panel 里显示的N ...
 - C语言:逻辑运算符||
			
#include <stdio.h> //逻辑运算符||特点:左右两边的表达式先做左边,如果左边为1则右边不用执行,整个结果为1:如果左边为0,再执行右边: main() { int x= ...
 - DataFrame的创建
			
DataFrame的创建从Spark2.0以上版本开始,Spark使用全新的SparkSession接口替代Spark1.6中的SQLContext及HiveContext接口来实现其对数据加载.转换 ...
 - Scala学习——模式匹配
			
scala模式匹配 1.基础match case(类似java里switch case,但功能强大些) object MatchApp { def main(args: Array[String]): ...