react-native-compressor 1.8.15 → 1.8.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -9
- package/android/src/main/java/com/reactnativecompressor/Audio/AudioCompressor.kt +58 -27
- package/android/src/main/java/com/reactnativecompressor/Audio/AudioHelper.kt +49 -0
- package/android/src/main/java/com/reactnativecompressor/Audio/AudioMain.kt +2 -6
- package/android/src/main/java/com/reactnativecompressor/Utils/createVideoThumbnail.kt +0 -2
- package/android/src/main/java/com/reactnativecompressor/Video/VideoMain.kt +6 -6
- package/ios/Audio/AudioHelper.swift +49 -0
- package/ios/Audio/AudioMain.swift +46 -82
- package/ios/Audio/AudioOptions.swift +37 -0
- package/ios/Audio/FormatConverter/FormatConverter+Compressed.swift +326 -0
- package/ios/Audio/FormatConverter/FormatConverter+PCM.swift +239 -0
- package/ios/Audio/FormatConverter/FormatConverter+Utilities.swift +103 -0
- package/ios/Audio/FormatConverter/FormatConverter.swift +264 -0
- package/ios/Compressor.xcodeproj/xcuserdata/apple.xcuserdatad/xcschemes/xcschememanagement.plist +14 -0
- package/ios/Video/NextLevelSessionExporter.swift +694 -0
- package/ios/Video/VideoMain.swift +9 -8
- package/lib/commonjs/Audio/index.js +1 -3
- package/lib/commonjs/Audio/index.js.map +1 -1
- package/lib/commonjs/utils/index.js +0 -2
- package/lib/commonjs/utils/index.js.map +1 -1
- package/lib/module/Audio/index.js +1 -3
- package/lib/module/Audio/index.js.map +1 -1
- package/lib/module/utils/index.js +0 -3
- package/lib/module/utils/index.js.map +1 -1
- package/lib/typescript/Audio/index.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +7 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/utils/index.d.ts +11 -2
- package/lib/typescript/utils/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/react-native-compressor.podspec +0 -1
- package/src/Audio/index.tsx +1 -3
- package/src/utils/index.tsx +12 -3
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
// from: https://github.com/NextLevel/NextLevelSessionExporter
|
|
2
|
+
|
|
3
|
+
import Foundation
|
|
4
|
+
import AVFoundation
|
|
5
|
+
|
|
6
|
+
// MARK: - types
|
|
7
|
+
|
|
8
|
+
/// Session export errors.
|
|
9
|
+
public enum NextLevelSessionExporterError: Error, CustomStringConvertible {
|
|
10
|
+
case setupFailure
|
|
11
|
+
case readingFailure
|
|
12
|
+
case writingFailure
|
|
13
|
+
case cancelled
|
|
14
|
+
|
|
15
|
+
public var description: String {
|
|
16
|
+
get {
|
|
17
|
+
switch self {
|
|
18
|
+
case .setupFailure:
|
|
19
|
+
return "Setup failure"
|
|
20
|
+
case .readingFailure:
|
|
21
|
+
return "Reading failure"
|
|
22
|
+
case .writingFailure:
|
|
23
|
+
return "Writing failure"
|
|
24
|
+
case .cancelled:
|
|
25
|
+
return "Cancelled"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// MARK: - NextLevelSessionExporter
|
|
32
|
+
|
|
33
|
+
/// 🔄 NextLevelSessionExporter, export and transcode media in Swift
|
|
34
|
+
open class NextLevelSessionExporter: NSObject {
|
|
35
|
+
|
|
36
|
+
/// Input asset for export, provided when initialized.
|
|
37
|
+
public var asset: AVAsset?
|
|
38
|
+
|
|
39
|
+
/// Enables video composition and parameters for the session.
|
|
40
|
+
public var videoComposition: AVVideoComposition?
|
|
41
|
+
|
|
42
|
+
/// Enables audio mixing and parameters for the session.
|
|
43
|
+
public var audioMix: AVAudioMix?
|
|
44
|
+
|
|
45
|
+
/// Output file location for the session.
|
|
46
|
+
public var outputURL: URL?
|
|
47
|
+
|
|
48
|
+
/// Output file type. UTI string defined in `AVMediaFormat.h`.
|
|
49
|
+
public var outputFileType: AVFileType? = AVFileType.mp4
|
|
50
|
+
|
|
51
|
+
/// Time range or limit of an export from `kCMTimeZero` to `kCMTimePositiveInfinity`
|
|
52
|
+
public var timeRange: CMTimeRange
|
|
53
|
+
|
|
54
|
+
/// Indicates if an export session should expect media data in real time.
|
|
55
|
+
public var expectsMediaDataInRealTime: Bool = false
|
|
56
|
+
|
|
57
|
+
/// Indicates if an export should be optimized for network use.
|
|
58
|
+
public var optimizeForNetworkUse: Bool = false
|
|
59
|
+
|
|
60
|
+
/// Metadata to be added to an export.
|
|
61
|
+
public var metadata: [AVMetadataItem]?
|
|
62
|
+
|
|
63
|
+
/// Video input configuration dictionary, using keys defined in `<CoreVideo/CVPixelBuffer.h>`
|
|
64
|
+
public var videoInputConfiguration: [String : Any]?
|
|
65
|
+
|
|
66
|
+
/// Video output configuration dictionary, using keys defined in `<AVFoundation/AVVideoSettings.h>`
|
|
67
|
+
public var videoOutputConfiguration: [String : Any]?
|
|
68
|
+
|
|
69
|
+
/// Audio output configuration dictionary, using keys defined in `<AVFoundation/AVAudioSettings.h>`
|
|
70
|
+
public var audioOutputConfiguration: [String : Any]?
|
|
71
|
+
|
|
72
|
+
/// Export session status state.
|
|
73
|
+
public var status: AVAssetExportSession.Status {
|
|
74
|
+
get {
|
|
75
|
+
if let writer = self._writer {
|
|
76
|
+
switch writer.status {
|
|
77
|
+
case .writing:
|
|
78
|
+
return .exporting
|
|
79
|
+
case .failed:
|
|
80
|
+
return .failed
|
|
81
|
+
case .completed:
|
|
82
|
+
return .completed
|
|
83
|
+
case.cancelled:
|
|
84
|
+
return .cancelled
|
|
85
|
+
case .unknown:
|
|
86
|
+
fallthrough
|
|
87
|
+
@unknown default:
|
|
88
|
+
break
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return .unknown
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Session exporting progress from 0 to 1.
|
|
96
|
+
public var progress: Float {
|
|
97
|
+
get {
|
|
98
|
+
return self._progress
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// private instance vars
|
|
103
|
+
|
|
104
|
+
fileprivate let InputQueueLabel = "NextLevelSessionExporterInputQueue"
|
|
105
|
+
|
|
106
|
+
fileprivate var _writer: AVAssetWriter?
|
|
107
|
+
fileprivate var _reader: AVAssetReader?
|
|
108
|
+
fileprivate var _pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor?
|
|
109
|
+
|
|
110
|
+
fileprivate var _inputQueue: DispatchQueue
|
|
111
|
+
|
|
112
|
+
fileprivate var _videoOutput: AVAssetReaderVideoCompositionOutput?
|
|
113
|
+
fileprivate var _audioOutput: AVAssetReaderAudioMixOutput?
|
|
114
|
+
fileprivate var _videoInput: AVAssetWriterInput?
|
|
115
|
+
fileprivate var _audioInput: AVAssetWriterInput?
|
|
116
|
+
|
|
117
|
+
fileprivate var _progress: Float = 0
|
|
118
|
+
|
|
119
|
+
fileprivate var _progressHandler: ProgressHandler?
|
|
120
|
+
fileprivate var _renderHandler: RenderHandler?
|
|
121
|
+
fileprivate var _completionHandler: CompletionHandler?
|
|
122
|
+
|
|
123
|
+
fileprivate var _duration: TimeInterval = 0
|
|
124
|
+
fileprivate var _lastSamplePresentationTime: CMTime = .invalid
|
|
125
|
+
|
|
126
|
+
// MARK: - object lifecycle
|
|
127
|
+
|
|
128
|
+
/// Initializes a session with an asset to export.
|
|
129
|
+
///
|
|
130
|
+
/// - Parameter asset: The asset to export.
|
|
131
|
+
public convenience init(withAsset asset: AVAsset) {
|
|
132
|
+
self.init()
|
|
133
|
+
self.asset = asset
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public override init() {
|
|
137
|
+
self._inputQueue = DispatchQueue(label: InputQueueLabel, autoreleaseFrequency: .workItem, target: DispatchQueue.global())
|
|
138
|
+
self.timeRange = CMTimeRange(start: CMTime.zero, end: CMTime.positiveInfinity)
|
|
139
|
+
super.init()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
deinit {
|
|
143
|
+
self._writer = nil
|
|
144
|
+
self._reader = nil
|
|
145
|
+
self._pixelBufferAdaptor = nil
|
|
146
|
+
self._videoOutput = nil
|
|
147
|
+
self._audioOutput = nil
|
|
148
|
+
self._videoInput = nil
|
|
149
|
+
self._audioInput = nil
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// MARK: - export
|
|
154
|
+
|
|
155
|
+
extension NextLevelSessionExporter {
|
|
156
|
+
|
|
157
|
+
/// Completion handler type for when an export finishes.
|
|
158
|
+
public typealias CompletionHandler = (Swift.Result<AVAssetExportSession.Status, Error>) -> Void
|
|
159
|
+
|
|
160
|
+
/// Progress handler type
|
|
161
|
+
public typealias ProgressHandler = (_ progress: Float) -> Void
|
|
162
|
+
|
|
163
|
+
/// Render handler type for frame processing
|
|
164
|
+
public typealias RenderHandler = (_ renderFrame: CVPixelBuffer, _ presentationTime: CMTime, _ resultingBuffer: CVPixelBuffer) -> Void
|
|
165
|
+
|
|
166
|
+
/// Initiates an export session.
|
|
167
|
+
///
|
|
168
|
+
/// - Parameter completionHandler: Handler called when an export session completes.
|
|
169
|
+
/// - Throws: Failure indication thrown when an error has occurred during export.
|
|
170
|
+
public func export(renderHandler: RenderHandler? = nil,
|
|
171
|
+
progressHandler: ProgressHandler? = nil,
|
|
172
|
+
completionHandler: CompletionHandler? = nil) {
|
|
173
|
+
guard let asset = self.asset,
|
|
174
|
+
let outputURL = self.outputURL,
|
|
175
|
+
let outputFileType = self.outputFileType else {
|
|
176
|
+
print("NextLevelSessionExporter, an asset and output URL are required for encoding")
|
|
177
|
+
DispatchQueue.main.async {
|
|
178
|
+
self._completionHandler?(.failure(NextLevelSessionExporterError.setupFailure))
|
|
179
|
+
}
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if self._writer?.status == .writing {
|
|
184
|
+
self._writer?.cancelWriting()
|
|
185
|
+
self._writer = nil
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if self._reader?.status == .reading {
|
|
189
|
+
self._reader?.cancelReading()
|
|
190
|
+
self._reader = nil
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
self._progress = 0
|
|
194
|
+
|
|
195
|
+
do {
|
|
196
|
+
self._reader = try AVAssetReader(asset: asset)
|
|
197
|
+
} catch {
|
|
198
|
+
print("NextLevelSessionExporter, could not setup a reader for the provided asset \(asset)")
|
|
199
|
+
DispatchQueue.main.async {
|
|
200
|
+
self._completionHandler?(.failure(NextLevelSessionExporterError.setupFailure))
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
do {
|
|
205
|
+
self._writer = try AVAssetWriter(outputURL: outputURL, fileType: outputFileType)
|
|
206
|
+
} catch {
|
|
207
|
+
print("NextLevelSessionExporter, could not setup a reader for the provided asset \(asset)")
|
|
208
|
+
DispatchQueue.main.async {
|
|
209
|
+
self._completionHandler?(.failure(NextLevelSessionExporterError.setupFailure))
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// if a video configuration exists, validate it (otherwise, proceed as audio)
|
|
214
|
+
if let _ = self.videoOutputConfiguration, self.validateVideoOutputConfiguration() == false {
|
|
215
|
+
print("NextLevelSessionExporter, could not setup with the specified video output configuration")
|
|
216
|
+
DispatchQueue.main.async {
|
|
217
|
+
self._completionHandler?(.failure(NextLevelSessionExporterError.setupFailure))
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
self._progressHandler = progressHandler
|
|
222
|
+
self._renderHandler = renderHandler
|
|
223
|
+
self._completionHandler = completionHandler
|
|
224
|
+
|
|
225
|
+
self._reader?.timeRange = self.timeRange
|
|
226
|
+
self._writer?.shouldOptimizeForNetworkUse = self.optimizeForNetworkUse
|
|
227
|
+
|
|
228
|
+
if let metadata = self.metadata {
|
|
229
|
+
self._writer?.metadata = metadata
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if self.timeRange.duration.isValid && self.timeRange.duration.isPositiveInfinity == false {
|
|
233
|
+
self._duration = CMTimeGetSeconds(self.timeRange.duration)
|
|
234
|
+
} else {
|
|
235
|
+
self._duration = CMTimeGetSeconds(asset.duration)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if self.videoOutputConfiguration?.keys.contains(AVVideoCodecKey) == false {
|
|
239
|
+
print("NextLevelSessionExporter, warning a video output configuration codec wasn't specified")
|
|
240
|
+
if #available(iOS 11.0, *) {
|
|
241
|
+
self.videoOutputConfiguration?[AVVideoCodecKey] = AVVideoCodecType.h264
|
|
242
|
+
} else {
|
|
243
|
+
self.videoOutputConfiguration?[AVVideoCodecKey] = AVVideoCodecH264
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
self.setupVideoOutput(withAsset: asset)
|
|
248
|
+
self.setupAudioOutput(withAsset: asset)
|
|
249
|
+
self.setupAudioInput()
|
|
250
|
+
|
|
251
|
+
// export
|
|
252
|
+
|
|
253
|
+
self._writer?.startWriting()
|
|
254
|
+
self._reader?.startReading()
|
|
255
|
+
self._writer?.startSession(atSourceTime: self.timeRange.start)
|
|
256
|
+
|
|
257
|
+
let audioSemaphore = DispatchSemaphore(value: 0)
|
|
258
|
+
let videoSemaphore = DispatchSemaphore(value: 0)
|
|
259
|
+
|
|
260
|
+
let videoTracks = asset.tracks(withMediaType: AVMediaType.video)
|
|
261
|
+
if let videoInput = self._videoInput,
|
|
262
|
+
let videoOutput = self._videoOutput,
|
|
263
|
+
videoTracks.count > 0 {
|
|
264
|
+
videoInput.requestMediaDataWhenReady(on: self._inputQueue, using: {
|
|
265
|
+
if self.encode(readySamplesFromReaderOutput: videoOutput, toWriterInput: videoInput) == false {
|
|
266
|
+
videoSemaphore.signal()
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
} else {
|
|
270
|
+
videoSemaphore.signal()
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if let audioInput = self._audioInput,
|
|
274
|
+
let audioOutput = self._audioOutput {
|
|
275
|
+
audioInput.requestMediaDataWhenReady(on: self._inputQueue, using: {
|
|
276
|
+
if self.encode(readySamplesFromReaderOutput: audioOutput, toWriterInput: audioInput) == false {
|
|
277
|
+
audioSemaphore.signal()
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
} else {
|
|
281
|
+
audioSemaphore.signal()
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
DispatchQueue.global().async {
|
|
285
|
+
audioSemaphore.wait()
|
|
286
|
+
videoSemaphore.wait()
|
|
287
|
+
DispatchQueue.main.async {
|
|
288
|
+
self.finish()
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/// Cancels any export in progress.
|
|
294
|
+
public func cancelExport() {
|
|
295
|
+
self._inputQueue.async {
|
|
296
|
+
if self._writer?.status == .writing {
|
|
297
|
+
self._writer?.cancelWriting()
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if self._reader?.status == .reading {
|
|
301
|
+
self._reader?.cancelReading()
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
DispatchQueue.main.async {
|
|
305
|
+
self.complete()
|
|
306
|
+
self.reset()
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// MARK: - setup funcs
|
|
314
|
+
|
|
315
|
+
extension NextLevelSessionExporter {
|
|
316
|
+
|
|
317
|
+
private func setupVideoOutput(withAsset asset: AVAsset) {
|
|
318
|
+
let videoTracks = asset.tracks(withMediaType: AVMediaType.video)
|
|
319
|
+
|
|
320
|
+
guard videoTracks.count > 0 else {
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
self._videoOutput = AVAssetReaderVideoCompositionOutput(videoTracks: videoTracks, videoSettings: self.videoInputConfiguration)
|
|
325
|
+
self._videoOutput?.alwaysCopiesSampleData = false
|
|
326
|
+
|
|
327
|
+
if let videoComposition = self.videoComposition {
|
|
328
|
+
self._videoOutput?.videoComposition = videoComposition
|
|
329
|
+
} else {
|
|
330
|
+
self._videoOutput?.videoComposition = self.createVideoComposition()
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if let videoOutput = self._videoOutput,
|
|
334
|
+
let reader = self._reader {
|
|
335
|
+
if reader.canAdd(videoOutput) {
|
|
336
|
+
reader.add(videoOutput)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// video input
|
|
341
|
+
if self._writer?.canApply(outputSettings: self.videoOutputConfiguration, forMediaType: AVMediaType.video) == true {
|
|
342
|
+
self._videoInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: self.videoOutputConfiguration)
|
|
343
|
+
self._videoInput?.expectsMediaDataInRealTime = self.expectsMediaDataInRealTime
|
|
344
|
+
} else {
|
|
345
|
+
print("Unsupported output configuration")
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if let writer = self._writer,
|
|
350
|
+
let videoInput = self._videoInput {
|
|
351
|
+
if writer.canAdd(videoInput) {
|
|
352
|
+
writer.add(videoInput)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// setup pixelbuffer adaptor
|
|
356
|
+
|
|
357
|
+
var pixelBufferAttrib: [String : Any] = [:]
|
|
358
|
+
pixelBufferAttrib[kCVPixelBufferPixelFormatTypeKey as String] = NSNumber(integerLiteral: Int(kCVPixelFormatType_32RGBA))
|
|
359
|
+
if let videoComposition = self._videoOutput?.videoComposition {
|
|
360
|
+
pixelBufferAttrib[kCVPixelBufferWidthKey as String] = NSNumber(integerLiteral: Int(videoComposition.renderSize.width))
|
|
361
|
+
pixelBufferAttrib[kCVPixelBufferHeightKey as String] = NSNumber(integerLiteral: Int(videoComposition.renderSize.height))
|
|
362
|
+
}
|
|
363
|
+
pixelBufferAttrib["IOSurfaceOpenGLESTextureCompatibility"] = NSNumber(booleanLiteral: true)
|
|
364
|
+
pixelBufferAttrib["IOSurfaceOpenGLESFBOCompatibility"] = NSNumber(booleanLiteral: true)
|
|
365
|
+
|
|
366
|
+
self._pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoInput, sourcePixelBufferAttributes: pixelBufferAttrib)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private func setupAudioOutput(withAsset asset: AVAsset) {
|
|
371
|
+
let audioTracks = asset.tracks(withMediaType: AVMediaType.audio)
|
|
372
|
+
|
|
373
|
+
guard audioTracks.count > 0 else {
|
|
374
|
+
self._audioOutput = nil
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
self._audioOutput = AVAssetReaderAudioMixOutput(audioTracks: audioTracks, audioSettings: nil)
|
|
379
|
+
self._audioOutput?.alwaysCopiesSampleData = false
|
|
380
|
+
self._audioOutput?.audioMix = self.audioMix
|
|
381
|
+
if let reader = self._reader,
|
|
382
|
+
let audioOutput = self._audioOutput {
|
|
383
|
+
if reader.canAdd(audioOutput) {
|
|
384
|
+
reader.add(audioOutput)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private func setupAudioInput() {
|
|
390
|
+
guard let _ = self._audioOutput else {
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
self._audioInput = AVAssetWriterInput(mediaType: AVMediaType.audio, outputSettings: self.audioOutputConfiguration)
|
|
395
|
+
self._audioInput?.expectsMediaDataInRealTime = self.expectsMediaDataInRealTime
|
|
396
|
+
if let writer = self._writer, let audioInput = self._audioInput {
|
|
397
|
+
if writer.canAdd(audioInput) {
|
|
398
|
+
writer.add(audioInput)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// MARK: - internal funcs
|
|
406
|
+
|
|
407
|
+
extension NextLevelSessionExporter {
|
|
408
|
+
|
|
409
|
+
// called on the inputQueue
|
|
410
|
+
internal func encode(readySamplesFromReaderOutput output: AVAssetReaderOutput, toWriterInput input: AVAssetWriterInput) -> Bool {
|
|
411
|
+
while input.isReadyForMoreMediaData {
|
|
412
|
+
guard self._reader?.status == .reading && self._writer?.status == .writing,
|
|
413
|
+
let sampleBuffer = output.copyNextSampleBuffer() else {
|
|
414
|
+
input.markAsFinished()
|
|
415
|
+
return false
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
var handled = false
|
|
419
|
+
var error = false
|
|
420
|
+
if self._videoOutput == output {
|
|
421
|
+
// determine progress
|
|
422
|
+
self._lastSamplePresentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) - self.timeRange.start
|
|
423
|
+
let progress = self._duration == 0 ? 1 : Float(CMTimeGetSeconds(self._lastSamplePresentationTime) / self._duration)
|
|
424
|
+
self.updateProgress(progress: progress)
|
|
425
|
+
|
|
426
|
+
// prepare progress frames
|
|
427
|
+
if let pixelBufferAdaptor = self._pixelBufferAdaptor,
|
|
428
|
+
let pixelBufferPool = pixelBufferAdaptor.pixelBufferPool,
|
|
429
|
+
let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
|
|
430
|
+
|
|
431
|
+
var toRenderBuffer: CVPixelBuffer? = nil
|
|
432
|
+
let result = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pixelBufferPool, &toRenderBuffer)
|
|
433
|
+
if result == kCVReturnSuccess {
|
|
434
|
+
if let toBuffer = toRenderBuffer {
|
|
435
|
+
self._renderHandler?(pixelBuffer, self._lastSamplePresentationTime, toBuffer)
|
|
436
|
+
if pixelBufferAdaptor.append(toBuffer, withPresentationTime:self._lastSamplePresentationTime) == false {
|
|
437
|
+
error = true
|
|
438
|
+
}
|
|
439
|
+
handled = true
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if handled == false && input.append(sampleBuffer) == false {
|
|
446
|
+
error = true
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if error {
|
|
450
|
+
return false
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return true
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
internal func createVideoComposition() -> AVMutableVideoComposition {
|
|
457
|
+
let videoComposition = AVMutableVideoComposition()
|
|
458
|
+
|
|
459
|
+
if let asset = self.asset,
|
|
460
|
+
let videoTrack = asset.tracks(withMediaType: AVMediaType.video).first {
|
|
461
|
+
|
|
462
|
+
// determine the framerate
|
|
463
|
+
|
|
464
|
+
var frameRate: Float = 0
|
|
465
|
+
if let videoConfiguration = self.videoOutputConfiguration {
|
|
466
|
+
if let videoCompressionConfiguration = videoConfiguration[AVVideoCompressionPropertiesKey] as? [String: Any] {
|
|
467
|
+
if let trackFrameRate = videoCompressionConfiguration[AVVideoAverageNonDroppableFrameRateKey] as? NSNumber {
|
|
468
|
+
frameRate = trackFrameRate.floatValue
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
} else {
|
|
472
|
+
frameRate = videoTrack.nominalFrameRate
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if frameRate == 0 {
|
|
476
|
+
frameRate = 30
|
|
477
|
+
}
|
|
478
|
+
videoComposition.frameDuration = CMTimeMake(value: 1, timescale: Int32(frameRate))
|
|
479
|
+
|
|
480
|
+
// determine the appropriate size and transform
|
|
481
|
+
|
|
482
|
+
if let videoConfiguration = self.videoOutputConfiguration {
|
|
483
|
+
|
|
484
|
+
let videoWidth = videoConfiguration[AVVideoWidthKey] as? NSNumber
|
|
485
|
+
let videoHeight = videoConfiguration[AVVideoHeightKey] as? NSNumber
|
|
486
|
+
|
|
487
|
+
// validated to be non-nil byt this point
|
|
488
|
+
let width = videoWidth!.intValue
|
|
489
|
+
let height = videoHeight!.intValue
|
|
490
|
+
|
|
491
|
+
let targetSize = CGSize(width: width, height: height)
|
|
492
|
+
var naturalSize = videoTrack.naturalSize
|
|
493
|
+
|
|
494
|
+
var transform = videoTrack.preferredTransform
|
|
495
|
+
|
|
496
|
+
let rect = CGRect(x: 0, y: 0, width: naturalSize.width, height: naturalSize.height)
|
|
497
|
+
let transformedRect = rect.applying(transform)
|
|
498
|
+
// transformedRect should have origin at 0 if correct; otherwise add offset to correct it
|
|
499
|
+
transform.tx -= transformedRect.origin.x;
|
|
500
|
+
transform.ty -= transformedRect.origin.y;
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
let videoAngleInDegrees = atan2(transform.b, transform.a) * 180 / .pi
|
|
504
|
+
if videoAngleInDegrees == 90 || videoAngleInDegrees == -90 {
|
|
505
|
+
let tempWidth = naturalSize.width
|
|
506
|
+
naturalSize.width = naturalSize.height
|
|
507
|
+
naturalSize.height = tempWidth
|
|
508
|
+
}
|
|
509
|
+
videoComposition.renderSize = naturalSize
|
|
510
|
+
|
|
511
|
+
// center the video
|
|
512
|
+
|
|
513
|
+
var ratio: CGFloat = 0
|
|
514
|
+
let xRatio: CGFloat = targetSize.width / naturalSize.width
|
|
515
|
+
let yRatio: CGFloat = targetSize.height / naturalSize.height
|
|
516
|
+
ratio = min(xRatio, yRatio)
|
|
517
|
+
|
|
518
|
+
let postWidth = naturalSize.width * ratio
|
|
519
|
+
let postHeight = naturalSize.height * ratio
|
|
520
|
+
let transX = (targetSize.width - postWidth) * 0.5
|
|
521
|
+
let transY = (targetSize.height - postHeight) * 0.5
|
|
522
|
+
|
|
523
|
+
var matrix = CGAffineTransform(translationX: (transX / xRatio), y: (transY / yRatio))
|
|
524
|
+
matrix = matrix.scaledBy(x: (ratio / xRatio), y: (ratio / yRatio))
|
|
525
|
+
transform = transform.concatenating(matrix)
|
|
526
|
+
|
|
527
|
+
// make the composition
|
|
528
|
+
|
|
529
|
+
let compositionInstruction = AVMutableVideoCompositionInstruction()
|
|
530
|
+
compositionInstruction.timeRange = CMTimeRange(start: CMTime.zero, duration: asset.duration)
|
|
531
|
+
|
|
532
|
+
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
|
|
533
|
+
layerInstruction.setTransform(transform, at: CMTime.zero)
|
|
534
|
+
|
|
535
|
+
compositionInstruction.layerInstructions = [layerInstruction]
|
|
536
|
+
videoComposition.instructions = [compositionInstruction]
|
|
537
|
+
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return videoComposition
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
internal func updateProgress(progress: Float) {
|
|
545
|
+
self.willChangeValue(forKey: "progress")
|
|
546
|
+
self._progress = progress
|
|
547
|
+
self.didChangeValue(forKey: "progress")
|
|
548
|
+
self._progressHandler?(progress)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// always called on the main thread
|
|
552
|
+
internal func finish() {
|
|
553
|
+
if self._reader?.status == .cancelled || self._writer?.status == .cancelled {
|
|
554
|
+
self.complete()
|
|
555
|
+
} else if self._writer?.status == .failed {
|
|
556
|
+
self._reader?.cancelReading()
|
|
557
|
+
self.complete()
|
|
558
|
+
} else if self._reader?.status == .failed {
|
|
559
|
+
self._writer?.cancelWriting()
|
|
560
|
+
self.complete()
|
|
561
|
+
} else {
|
|
562
|
+
self._writer?.finishWriting {
|
|
563
|
+
self.complete()
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// always called on the main thread
|
|
569
|
+
internal func complete() {
|
|
570
|
+
if self._reader?.status == .cancelled || self._writer?.status == .cancelled {
|
|
571
|
+
guard let outputURL = self.outputURL else {
|
|
572
|
+
self._completionHandler?(.failure(NextLevelSessionExporterError.cancelled))
|
|
573
|
+
return
|
|
574
|
+
}
|
|
575
|
+
if FileManager.default.fileExists(atPath: outputURL.absoluteString) {
|
|
576
|
+
try? FileManager.default.removeItem(at: outputURL)
|
|
577
|
+
}
|
|
578
|
+
self._completionHandler?(.failure(NextLevelSessionExporterError.cancelled))
|
|
579
|
+
return
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
guard let reader = self._reader else {
|
|
583
|
+
self._completionHandler?(.failure(NextLevelSessionExporterError.setupFailure))
|
|
584
|
+
self._completionHandler = nil
|
|
585
|
+
return
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
guard let writer = self._writer else {
|
|
589
|
+
self._completionHandler?(.failure(NextLevelSessionExporterError.setupFailure))
|
|
590
|
+
self._completionHandler = nil
|
|
591
|
+
return
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
switch reader.status {
|
|
595
|
+
case .failed:
|
|
596
|
+
guard let outputURL = self.outputURL else {
|
|
597
|
+
self._completionHandler?(.failure(reader.error ?? NextLevelSessionExporterError.readingFailure))
|
|
598
|
+
return
|
|
599
|
+
}
|
|
600
|
+
if FileManager.default.fileExists(atPath: outputURL.absoluteString) {
|
|
601
|
+
try? FileManager.default.removeItem(at: outputURL)
|
|
602
|
+
}
|
|
603
|
+
self._completionHandler?(.failure(reader.error ?? NextLevelSessionExporterError.readingFailure))
|
|
604
|
+
return
|
|
605
|
+
default:
|
|
606
|
+
// do nothing
|
|
607
|
+
break
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
switch writer.status {
|
|
611
|
+
case .failed:
|
|
612
|
+
guard let outputURL = self.outputURL else {
|
|
613
|
+
self._completionHandler?(.failure(writer.error ?? NextLevelSessionExporterError.writingFailure))
|
|
614
|
+
return
|
|
615
|
+
}
|
|
616
|
+
if FileManager.default.fileExists(atPath: outputURL.absoluteString) {
|
|
617
|
+
try? FileManager.default.removeItem(at: outputURL)
|
|
618
|
+
}
|
|
619
|
+
self._completionHandler?(.failure(writer.error ?? NextLevelSessionExporterError.writingFailure))
|
|
620
|
+
return
|
|
621
|
+
default:
|
|
622
|
+
// do nothing
|
|
623
|
+
break
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
self._completionHandler?(.success(self.status))
|
|
627
|
+
self._completionHandler = nil
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// subclass and add more checks, if needed
|
|
631
|
+
open func validateVideoOutputConfiguration() -> Bool {
|
|
632
|
+
guard let videoOutputConfiguration = self.videoOutputConfiguration else {
|
|
633
|
+
return false
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
let videoWidth = videoOutputConfiguration[AVVideoWidthKey] as? NSNumber
|
|
637
|
+
let videoHeight = videoOutputConfiguration[AVVideoHeightKey] as? NSNumber
|
|
638
|
+
if videoWidth == nil || videoHeight == nil {
|
|
639
|
+
return false
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return true
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
internal func reset() {
|
|
646
|
+
self._progress = 0
|
|
647
|
+
self._writer = nil
|
|
648
|
+
self._reader = nil
|
|
649
|
+
self._pixelBufferAdaptor = nil
|
|
650
|
+
|
|
651
|
+
self._videoOutput = nil
|
|
652
|
+
self._audioOutput = nil
|
|
653
|
+
self._videoInput = nil
|
|
654
|
+
self._audioInput = nil
|
|
655
|
+
|
|
656
|
+
self._progressHandler = nil
|
|
657
|
+
self._renderHandler = nil
|
|
658
|
+
self._completionHandler = nil
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// MARK: - AVAsset extension
|
|
664
|
+
|
|
665
|
+
extension AVAsset {
|
|
666
|
+
|
|
667
|
+
/// Initiates a NextLevelSessionExport on the asset
|
|
668
|
+
///
|
|
669
|
+
/// - Parameters:
|
|
670
|
+
/// - outputFileType: type of resulting file to create
|
|
671
|
+
/// - outputURL: location of resulting file
|
|
672
|
+
/// - metadata: data to embed in the result
|
|
673
|
+
/// - videoInputConfiguration: video input configuration
|
|
674
|
+
/// - videoOutputConfiguration: video output configuration
|
|
675
|
+
/// - audioOutputConfiguration: audio output configuration
|
|
676
|
+
/// - progressHandler: progress fraction handler
|
|
677
|
+
/// - completionHandler: completion handler
|
|
678
|
+
public func nextlevel_export(outputFileType: AVFileType? = AVFileType.mp4,
|
|
679
|
+
outputURL: URL,
|
|
680
|
+
metadata: [AVMetadataItem]? = nil,
|
|
681
|
+
videoInputConfiguration: [String : Any]? = nil,
|
|
682
|
+
videoOutputConfiguration: [String : Any],
|
|
683
|
+
audioOutputConfiguration: [String : Any],
|
|
684
|
+
progressHandler: NextLevelSessionExporter.ProgressHandler? = nil,
|
|
685
|
+
completionHandler: NextLevelSessionExporter.CompletionHandler? = nil) {
|
|
686
|
+
let exporter = NextLevelSessionExporter(withAsset: self)
|
|
687
|
+
exporter.outputFileType = outputFileType
|
|
688
|
+
exporter.outputURL = outputURL
|
|
689
|
+
exporter.videoOutputConfiguration = videoOutputConfiguration
|
|
690
|
+
exporter.audioOutputConfiguration = audioOutputConfiguration
|
|
691
|
+
exporter.export(progressHandler: progressHandler, completionHandler: completionHandler)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
}
|