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的升级版. ...
随机推荐
- CentOS-配置jar包自启动(SpringBoot)
在pom.xml文件<plugin>中添加配置后,再打包(开发人员) <plugin> <groupId>org.springframework.boot& ...
- Java线程池中的核心线程是如何被重复利用的?
真的!讲得太清楚了!https://blog.csdn.net/MingHuang2017/article/details/79571529 真的是解惑了 本文所说的"核心线程". ...
- 【重学Java】Stream流
Stream流 体验Stream流[理解] 案例需求 按照下面的要求完成集合的创建和遍历 创建一个集合,存储多个字符串元素 把集合中所有以"张"开头的元素存储到一个新的集合 把&q ...
- ARTS第七周
补上.瞎忙,看来还是效率的问题. 1.Algorithm:每周至少做一个 leetcode 的算法题2.Review:阅读并点评至少一篇英文技术文章3.Tip:学习至少一个技术技巧4.Share:分享 ...
- CF1444D Rectangular Polyline[题解]
Rectangular Polyline 题目大意 给定 \(h\) 条长度分别为 \(l_1,l_2,--,l_h\) 的水平线段以及 \(v\) 条长度分别为 \(p_1,p_2,--.p_v\) ...
- P4293 [WC2010]能量场
P4293 [WC2010]能量场 题意 给你 \(n\) 个粒子,每个粒子有两个权值 \(m_i,c_i\) 每个相邻有序对 \((a,b)\) 会产生 \(m_am_b(c_a-c_b)\) 的贡 ...
- Splay与FHQ-Treap
两个一起学的,就放一块了. 主要是用来存板子. Splay //This is a Splay Tree. #include <cstdio> #include <cstring&g ...
- 第七篇 -- 添加CSV Data Set Config
参考链接:https://blog.csdn.net/vv19910825/article/details/82773220 先来看看我们开启的接口 @RequestMapping(value = & ...
- ts 学习笔记 - 类
目录 类 类的概念 类的用法 属性和方法 类的继承 存取器 静态属性 Typescript 中的用法 抽象类 类的类型 类与接口 类实现接口 接口继承接口 接口继承类 混合类型 类 类的概念 类 (c ...
- 深入刨析tomcat 之---第12篇 how tomcat works( 第17章 ) 解析catalina.bat 梳理启动流程
我们如何启动tomcat呢? 答案是双击startup.bat文件,这个文件在bin目录下 @echo off 不显示批处理命令 rem Licensed to the Apache Softw ...