-
Notifications
You must be signed in to change notification settings - Fork 15
/
Copy pathRegift.swift
360 lines (299 loc) · 15.8 KB
/
Regift.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
//
// Regift.swift
// Regift
//
// Created by Matthew Palmer on 27/12/2014.
// Copyright (c) 2014 Matthew Palmer. All rights reserved.
//
// Minor changes made by Xue Yu, add parameters for the gif width/height
//
#if os(iOS)
import UIKit
import MobileCoreServices
#elseif os(OSX)
import AppKit
#endif
import ImageIO
import AVFoundation
public typealias TimePoint = CMTime
/// Errors thrown by Regift
public enum RegiftError: String, Error {
case DestinationNotFound = "The temp file destination could not be created or found"
case SourceFormatInvalid = "The source file does not appear to be a valid format"
case AddFrameToDestination = "An error occurred when adding a frame to the destination"
case DestinationFinalize = "An error occurred when finalizing the destination"
}
// Convenience struct for managing dispatch groups.
private struct Group {
let group = DispatchGroup()
func enter() { group.enter() }
func leave() { group.leave() }
func wait() { let _ = group.wait(timeout: DispatchTime.distantFuture) }
}
/// Easily convert a video to a GIF. It can convert the whole thing, or you can choose a section to trim out.
///
/// Synchronous Usage:
///
/// let regift = Regift(sourceFileURL: movieFileURL, frameCount: 24, delayTime: 0.5, loopCount: 7, width: 240, height: 240)
/// print(regift.createGif())
///
/// // OR
///
/// let trimmedRegift = Regift(sourceFileURL: movieFileURL, startTime: 30, duration: 15, frameRate: 15)
/// print(trimmedRegift.createGif())
///
/// Asynchronous Usage:
///
/// let regift = Regift.createGIFFromSource(movieFileURL, frameCount: 24, delayTime: 0.5, loopCount: 7) { (result) in
/// print(result)
/// }
///
/// // OR
///
/// let trimmedRegift = Regift.createGIFFromSource(movieFileURL, startTime: 30, duration: 15, frameRate: 15, loopCount: 0, width: 240, height: 240) { (result) in
/// print(result)
/// }
///
public struct Regift {
// Static conversion methods, for convenient and easy-to-use API:
/**
Create a GIF from a movie stored at the given URL. This converts the whole video to a GIF meeting the requested output parameters.
- parameters:
- sourceFileURL: The source file to create the GIF from.
- destinationFileURL: An optional destination file to write the GIF to. If you don't include this, a default path will be provided.
- frameCount: The number of frames to include in the gif; each frame has the same duration and is spaced evenly over the video.
- delayTime: The amount of time each frame exists for in the GIF.
- loopCount: The number of times the GIF will repeat. This defaults to `0`, which means that the GIF will repeat infinitely.
- width: The maximum width of generated GIF. This defaults to `0`, means not compressed.
- height: The maximum height of generated GIF. This defaults to `0`, means not compressed. Setting width/height will not change the image aspect ratio.
- completion: A block that will be called when the GIF creation is completed. The `result` parameter provides the path to the file, or will be `nil` if there was an error.
*/
public static func createGIFFromSource(
_ sourceFileURL: URL,
destinationFileURL: URL? = nil,
frameCount: Int,
delayTime: Float,
loopCount: Int = 0,
width: Int = 0,
height: Int = 0,
completion: (_ result: URL?) -> Void) {
let gift = Regift(
sourceFileURL: sourceFileURL,
destinationFileURL: destinationFileURL,
frameCount: frameCount,
delayTime: delayTime,
loopCount: loopCount,
width: width,
height: height
)
completion(gift.createGif())
}
/**
Create a GIF from a movie stored at the given URL. This allows you to choose a start time and duration in the source material that will be used to create the GIF which meets the output parameters.
- parameters:
- sourceFileURL: The source file to create the GIF from.
- destinationFileURL: An optional destination file to write the GIF to. If you don't include this, a default path will be provided.
- startTime: The time in seconds in the source material at which you want the GIF to start.
- duration: The duration in seconds that you want to pull from the source material.
- frameRate: The desired frame rate of the outputted GIF.
- loopCount: The number of times the GIF will repeat. This defaults to `0`, which means that the GIF will repeat infinitely.
- width: The maximum width of generated GIF. This defaults to `0`, means not compressed.
- height: The maximum height of generated GIF. This defaults to `0`, means not compressed. Setting width/height will not change the image aspect ratio.
- completion: A block that will be called when the GIF creation is completed. The `result` parameter provides the path to the file, or will be `nil` if there was an error.
*/
public static func createGIFFromSource(
_ sourceFileURL: URL,
destinationFileURL: URL? = nil,
startTime: Float,
duration: Float,
frameRate: Int,
loopCount: Int = 0,
width: Int = 0,
height: Int = 0,
completion: (_ result: URL?) -> Void) {
let gift = Regift(
sourceFileURL: sourceFileURL,
destinationFileURL: destinationFileURL,
startTime: startTime,
duration: duration,
frameRate: frameRate,
loopCount: loopCount,
width: width,
height: height
)
completion(gift.createGif())
}
fileprivate struct Constants {
static let FileName = "regift.gif"
static let TimeInterval: Int32 = 600
static let Tolerance = 0.01
}
/// A reference to the asset we are converting.
fileprivate var asset: AVAsset
/// The url for the source file.
fileprivate let sourceFileURL: URL
/// The point in time in the source which we will start from.
fileprivate var startTime: Float = 0
/// The desired duration of the gif.
fileprivate var duration: Float
/// The total length of the movie, in seconds.
fileprivate var movieLength: Float
/// The number of frames we are going to use to create the gif.
fileprivate let frameCount: Int
/// The amount of time each frame will remain on screen in the gif.
fileprivate let delayTime: Float
/// The number of times the gif will loop (0 is infinite).
fileprivate let loopCount: Int
/// The destination path for the generated file.
fileprivate var destinationFileURL: URL?
/// The maximum width/height for the generated file (0 will not compress).compress
fileprivate var width: Int
fileprivate var height: Int
/**
Create a GIF from a movie stored at the given URL. This converts the whole video to a GIF meeting the requested output parameters.
- parameters:
- sourceFileURL: The source file to create the GIF from.
- destinationFileURL: An optional destination file to write the GIF to. If you don't include this, a default path will be provided.
- frameCount: The number of frames to include in the gif; each frame has the same duration and is spaced evenly over the video.
- delayTime: The amount of time each frame exists for in the GIF.
- loopCount: The number of times the GIF will repeat. This defaults to `0`, which means that the GIF will repeat infinitely.
- width: The maximum width of generated GIF. This defaults to `0`, means not compressed.
- height: The maximum height of generated GIF. This defaults to `0`, means not compressed. Setting width/height will not change the image aspect ratio.
*/
public init(sourceFileURL: URL, destinationFileURL: URL? = nil, frameCount: Int, delayTime: Float, loopCount: Int = 0, width: Int = 0, height: Int = 0) {
self.sourceFileURL = sourceFileURL
self.asset = AVURLAsset(url: sourceFileURL, options: nil)
self.movieLength = Float(asset.duration.value) / Float(asset.duration.timescale)
self.duration = movieLength
self.delayTime = delayTime
self.loopCount = loopCount
self.destinationFileURL = destinationFileURL
self.frameCount = frameCount
self.width = width
self.height = height
}
/**
Create a GIF from a movie stored at the given URL. This allows you to choose a start time and duration in the source material that will be used to create the GIF which meets the output parameters.
- parameters:
- sourceFileURL: The source file to create the GIF from.
- destinationFileURL: An optional destination file to write the GIF to. If you don't include this, a default path will be provided.
- startTime: The time in seconds in the source material at which you want the GIF to start.
- duration: The duration in seconds that you want to pull from the source material.
- frameRate: The desired frame rate of the outputted GIF.
- loopCount: The number of times the GIF will repeat. This defaults to `0`, which means that the GIF will repeat infinitely.
- width: The maximum width of generated GIF. This defaults to `0`, means not compressed.
- height: The maximum height of generated GIF. This defaults to `0`, means not compressed. Setting width/height will not change the image aspect ratio.
*/
public init(sourceFileURL: URL, destinationFileURL: URL? = nil, startTime: Float, duration: Float, frameRate: Int, loopCount: Int = 0, width: Int = 0, height: Int = 0) {
self.sourceFileURL = sourceFileURL
self.asset = AVURLAsset(url: sourceFileURL, options: nil)
self.destinationFileURL = destinationFileURL
self.startTime = startTime
self.duration = duration
// The delay time is based on the desired framerate of the gif.
self.delayTime = (1.0 / Float(frameRate))
// The frame count is based on the desired length and framerate of the gif.
self.frameCount = Int(duration * Float(frameRate))
// The total length of the file, in seconds.
self.movieLength = Float(asset.duration.value) / Float(asset.duration.timescale)
self.loopCount = loopCount
self.width = width
self.height = height
}
/**
Get the URL of the GIF created with the attributes provided in the initializer.
- returns: The path to the created GIF, or `nil` if there was an error creating it.
*/
public func createGif() -> URL? {
let fileProperties = [kCGImagePropertyGIFDictionary as String:[
kCGImagePropertyGIFLoopCount as String: NSNumber(value: Int32(loopCount) as Int32)],
kCGImagePropertyGIFHasGlobalColorMap as String: NSValue(nonretainedObject: true)
] as [String : Any]
let frameProperties = [
kCGImagePropertyGIFDictionary as String:[
kCGImagePropertyGIFDelayTime as String:delayTime
]
]
// How far along the video track we want to move, in seconds.
let increment = Float(duration) / Float(frameCount)
// Add each of the frames to the buffer
var timePoints: [TimePoint] = []
for frameNumber in 0 ..< frameCount {
let seconds: Float64 = Float64(startTime) + (Float64(increment) * Float64(frameNumber))
let time = CMTimeMakeWithSeconds(seconds, Constants.TimeInterval)
timePoints.append(time)
}
do {
return try createGIFForTimePoints(timePoints, fileProperties: fileProperties as [String : AnyObject], frameProperties: frameProperties as [String : AnyObject], frameCount: frameCount, width: width, height: height)
} catch {
return nil
}
}
/**
Create a GIF using the given time points in a movie file stored in this Regift's `asset`.
- parameters:
- timePoints: timePoints An array of `TimePoint`s (which are typealiased `CMTime`s) to use as the frames in the GIF.
- fileProperties: The desired attributes of the resulting GIF.
- frameProperties: The desired attributes of each frame in the resulting GIF.
- frameCount: The desired number of frames for the GIF. *NOTE: This seems redundant to me, as `timePoints.count` should really be what we are after, but I'm hesitant to change the API here.*
- width: The maximum width of generated GIF. This defaults to `0`, means not compressed.
- height: The maximum height of generated GIF. This defaults to `0`, means not compressed. Setting width/height will not change the image aspect ratio.
- returns: The path to the created GIF, or `nil` if there was an error creating it.
*/
public func createGIFForTimePoints(_ timePoints: [TimePoint], fileProperties: [String: AnyObject], frameProperties: [String: AnyObject], frameCount: Int, width: Int, height: Int) throws -> URL {
// Ensure the source media is a valid file.
guard asset.tracks(withMediaCharacteristic: AVMediaCharacteristicVisual).count > 0 else {
throw RegiftError.SourceFormatInvalid
}
var fileURL:URL?
if self.destinationFileURL != nil {
fileURL = self.destinationFileURL
} else {
let temporaryFile = (NSTemporaryDirectory() as NSString).appendingPathComponent(Constants.FileName)
fileURL = URL(fileURLWithPath: temporaryFile)
}
guard let destination = CGImageDestinationCreateWithURL(fileURL! as CFURL, kUTTypeGIF, frameCount, nil) else {
throw RegiftError.DestinationNotFound
}
CGImageDestinationSetProperties(destination, fileProperties as CFDictionary)
let generator = AVAssetImageGenerator(asset: asset)
generator.appliesPreferredTrackTransform = true
generator.maximumSize = CGSize(width: width, height: height)
let tolerance = CMTimeMakeWithSeconds(Constants.Tolerance, Constants.TimeInterval)
generator.requestedTimeToleranceBefore = tolerance
generator.requestedTimeToleranceAfter = tolerance
// Transform timePoints to times for the async asset generator method.
var times = [NSValue]()
for time in timePoints {
times.append(NSValue(time: time))
}
// Create a dispatch group to force synchronous behavior on an asynchronous method.
let gifGroup = Group()
var dispatchError: Bool = false
gifGroup.enter()
generator.generateCGImagesAsynchronously(forTimes: times, completionHandler: { (requestedTime, image, actualTime, result, error) in
guard let imageRef = image , error == nil else {
print("An error occurred: \(error), image is \(image)")
dispatchError = true
gifGroup.leave()
return
}
CGImageDestinationAddImage(destination, imageRef, frameProperties as CFDictionary)
if requestedTime == times.last?.timeValue {
gifGroup.leave()
}
})
// Wait for the asynchronous generator to finish.
gifGroup.wait()
// If there was an error in the generator, throw the error.
if dispatchError {
throw RegiftError.AddFrameToDestination
}
CGImageDestinationSetProperties(destination, fileProperties as CFDictionary)
// Finalize the gif
if !CGImageDestinationFinalize(destination) {
throw RegiftError.DestinationFinalize
}
return fileURL!
}
}