前言

最近遇到需要将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
}
}

使用步骤

  1. 打开APP
  2. video文件拖拽到窗口中

常见问题

macos 10.15 将对您的电脑造成伤害,您应该将它移到废纸篓

  1. APP右键-显示简介-覆盖恶意软件保护 打勾
  2. 打开终端 codesign -f -s - --deep /Applications/appname.app

总结

代码支持iOSmacOS

下载地址----->>>MP4ToGIF,如果对你有所帮助,欢迎Star。

iOS开发之Video转GIF的更多相关文章

  1. iOS 开发之 GCD 不同场景使用

    header{font-size:1em;padding-top:1.5em;padding-bottom:1.5em} .markdown-body{overflow:hidden} .markdo ...

  2. iOS 开发之 GCD 基础

    header{font-size:1em;padding-top:1.5em;padding-bottom:1.5em} .markdown-body{overflow:hidden} .markdo ...

  3. iOS开发之Socket通信实战--Request请求数据包编码模块

    实际上在iOS很多应用开发中,大部分用的网络通信都是http/https协议,除非有特殊的需求会用到Socket网络协议进行网络数 据传输,这时候在iOS客户端就需要很好的第三方CocoaAsyncS ...

  4. iOS开发之UISearchBar初探

    iOS开发之UISearchBar初探 UISearchBar也是iOS开发常用控件之一,点进去看看里面的属性barStyle.text.placeholder等等.但是这些属性显然不足矣满足我们的开 ...

  5. iOS开发之UIImage等比缩放

    iOS开发之UIImage等比缩放 评论功能真不错 评论开通后,果然有很多人吐槽.谢谢大家的支持和关爱,如果有做的不到的地方,还请海涵.毕竟我一个人的力量是有限的,我会尽自己最大的努力大家准备一些干货 ...

  6. iOS开发之 Xcode6 添加xib文件,去掉storyboard的hello world应用

    iOS开发之  Xcode6.1创建仅xib文件,无storyboard的hello world应用 由于Xcode6之后,默认创建storyboard而非xib文件,而作为初学,了解xib的加载原理 ...

  7. iOS开发之loadView、viewDidLoad及viewDidUnload的关系

    iOS开发之loadView.viewDidLoad及viewDidUnload的关系 iOS开发之loadView.viewDidLoad及viewDidUnload的关系    标题中所说的3个方 ...

  8. iOS开发之info.pist文件和.pch文件

    iOS开发之info.pist文件和.pch文件 如果你是iOS开发初学者,不用过多的关注项目中各个文件的作用.因为iOS开发的学习路线起点不在这里,这些文件只会给你学习带来困扰. 打开一个项目,我们 ...

  9. iOS开发之WKWebView简单使用

    iOS开发之WKWebView简单使用   iOS开发之 WKWebVeiw使用 想用UIWebVeiw做的,但是突然想起来在iOS8中出了一个新的WKWebView,算是UIWebVeiw的升级版. ...

随机推荐

  1. Java的类加载器种类

    Java类加载器采用双亲委派模型: 1.启动类加载器:这个类加载器负责放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别 ...

  2. PHP中“简单工厂模式”实例讲解(转)

      ? 1 2 3 4 5 6 7 8 原创文章,转载请注明出处:http://www.cnblogs.com/hongfei/archive/2012/07/07/2580776.html   简单 ...

  3. 关于mysql binlog二进制

    binlog 在mysql中,当发生数据变更时,都会将变更数据的语句,通过二进制形式,存储到binlog日志文件中. 通过binlog文件,你可以查看mysql一段时间内,对数据库的所有改动. 也可以 ...

  4. 63. Unique Paths II 动态规划

    description: https://leetcode.com/problems/unique-paths/ 机器人从一堆方格的左上角走到右下角,只能往右或者往下走 ,问有几种走法,这个加了难度, ...

  5. 深入理解Java容器——HashMap

    目录 存储结构 初始化 put resize 树化 get 为什么equals和hashCode要同时重写? 为何HashMap的数组长度一定是2的次幂? 线程安全 参考 存储结构 JDK1.8前是数 ...

  6. Python3.7 lxml引入etree

    用xml代替lxml,Python3.7中已经没有etree这个模块了 import xml.etree.ElementTree as etree from lxml import etree 这种方 ...

  7. java集合(2)-Collection与Iterator接口

    1 package com.j1803.collectionOfIterator; 2 import java.util.ArrayList; 3 import java.util.Collectio ...

  8. final修饰符(2)

    final局部变量 系统不会对局部变量进行初始化,局部变量必须又程序员显示初始化,因此使用final修饰局部变量,可以在声明时指定默认值,也可以在后面的代码中对该final变量赋初始值,但只能赋值一次 ...

  9. python之数据驱动Excel操作(方法一)

    一.Mail163.xlsx数据如下: 二.Mail163.py脚本如下 import xlrdimport unittestfrom selenium import webdriverfrom se ...

  10. php使用curl模拟post请求

    废话不多说,直接上代码,做个笔记. $url="http://localhost/header_server.php"; $body = array("mobile&qu ...