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的升级版. ... 
随机推荐
- docker安装redis主从以及哨兵
			docker安装redis主从以及哨兵 本文使用docker在四台机器上部署一主二从三哨兵的Redis主从结构. 服务器配置 192.168.102.128 主节点 centos7.5 192.168 ... 
- Java 设置PDF跨页表格重复显示表头行
			在创建表格时,如果表格内容出现跨页显示的时候,默认情况下该表格的表头不会在下一页显示,在阅读体验上不是很好.下面分享一个方法如何在表格跨页是显示表格的表头内容,这里只需要简单使用方法 grid.set ... 
- ESP32S2获取sht30温湿度
			static const char *TAG = "i2c-temp"; static unsigned char sht30_buf[6]={0}; static float g ... 
- Selenium的Css Selector使用方法
			什么是Css Selector? Css Selector定位实际就是HTML的Css选择器的标签定位 工具 Css Selector的练习建议大家安装火狐浏览器后,下载插件,FireFinder 或 ... 
- Python - 字符串常用函数详解
			str.index(sub, start=None, end=None) 作用:查看sub是否在字符串中,在的话返回索引,且只返回第一次匹配到的索引:若找不到则报错:可以指定统计的范围,[start, ... 
- Kubernetes全栈架构师(二进制高可用安装k8s集群扩展篇)--学习笔记
			目录 二进制Metrics&Dashboard安装 二进制高可用集群可用性验证 生产环境k8s集群关键性配置 Bootstrapping: Kubelet启动过程 Bootstrapping: ... 
- 记录APP 启动ACTIVITITY
			a.启动待测apkb.开启日志输出:adb logcat>D:/log.txt c.关闭日志输出:ctrl+cd.查看日志找寻: Displayed com.mm.android.hsy/.ui ... 
- Redis 6.0 新特性:带你 100% 掌握多线程模型
			Redis 官方在 2020 年 5 月正式推出 6.0 版本,提供很多振奋人心的新特性,所以备受关注. 码老湿,提供了啥特性呀?知道了我能加薪么? 主要特性如下: 多线程处理网络 IO: 客户端缓存 ... 
- P4334 [COI2007] Policija
			P4334 [COI2007] Policija 题意 一个无重边的无向图,每次询问删掉一条边或删掉一个点后两个点是否联通. 思路 连通性问题,我们可以考虑使用广义圆方树解决. 对于删掉一个点的情况: ... 
- Splay与FHQ-Treap
			两个一起学的,就放一块了. 主要是用来存板子. Splay //This is a Splay Tree. #include <cstdio> #include <cstring&g ... 
