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的升级版. ...
随机推荐
- 搭建Nexus3私服(含使用说明,支持CentOS、Windows)
官方文档 Nexus仓库介绍(支持maven.yum.docker私服等) 仓库分为三种: proxy:是远程仓库的代理.比如说在nexus中配置了一个central repository的proxy ...
- webpack(9)plugin插件功能的使用
plugin 插件是 webpack 的支柱功能.webpack 自身也是构建于你在 webpack 配置中用到的相同的插件系统之上! 插件目的在于解决 loader 无法实现的其他事. 常用的插件 ...
- SpringBoot:Sqlite3+SpringBoot2.1.3+Mybatis-Puls整合项目
应公司要求完成sqlite3数据库的增改查小功能,特此记录一下. 1.建造项目 结构如下 因为是提供给前端调用所以做了接口. 2.Pom依赖文件 下面是这个项目所依赖的jar包. <parent ...
- springboot 使用yml配置文件自定义属性
springboot 中在application.yml文件里自定义属性值,配合@Value注解可以在代码中直接取到相应的值,如在application.yml中添加 mqtt: serverURI: ...
- CG-CTF WxyVM2
一.原本以为要动调,因为出现了这个,函数太长,无法反编译 后面才知道这玩意可以在ida的配置文件里面去改,直接改成1024. 里面的MAXFUNSIZE改成1024,就可以反编译了,这个长度是超过这个 ...
- XCTF EasyRE
一.查壳 无壳,并且发现是vc++编译的 二.拖入ida,来静态分析,这题让我深刻感觉到汇编的nb. 这段算是灵性的一段了,单从静态语句来看,发现分析不出啥,只能靠猜一下,我当时猜的是将输入的字符串又 ...
- netcore3.1 + vue (前后端分离) ElementUI多文件带参数上传
vue前端代码 前端主要使用了ElementUI的el-uploda插件,除去业务代码需要注意的是使用formdata存储片上传时所需的参数 <el-upload class="upl ...
- 修改gitlab默认的nginx
目录 1. 修改gitlab的配置文件 2. nginx配置 3. 重载 前言: 本文将介绍,如何禁用gitlab自带的nginx,用已经安装的nginx提供web服务. 1. 修改gitlab的配置 ...
- Codeforces Round#687 Div2 题解
打这场的时候迷迷糊糊的,然后掉分了( A Prison Break: 题面很复杂,但是题意很简单,仅需求出从这个点到四个角的最大的曼哈顿距离即可 #include <bits/stdc++.h& ...
- 安装react后运行报错
错误提示:npm WARN checkPermissions Missing write access to C:\Users\LXD\Desktop\webpack-base\node_module ...