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.
Files changed (33) hide show
  1. package/README.md +36 -9
  2. package/android/src/main/java/com/reactnativecompressor/Audio/AudioCompressor.kt +58 -27
  3. package/android/src/main/java/com/reactnativecompressor/Audio/AudioHelper.kt +49 -0
  4. package/android/src/main/java/com/reactnativecompressor/Audio/AudioMain.kt +2 -6
  5. package/android/src/main/java/com/reactnativecompressor/Utils/createVideoThumbnail.kt +0 -2
  6. package/android/src/main/java/com/reactnativecompressor/Video/VideoMain.kt +6 -6
  7. package/ios/Audio/AudioHelper.swift +49 -0
  8. package/ios/Audio/AudioMain.swift +46 -82
  9. package/ios/Audio/AudioOptions.swift +37 -0
  10. package/ios/Audio/FormatConverter/FormatConverter+Compressed.swift +326 -0
  11. package/ios/Audio/FormatConverter/FormatConverter+PCM.swift +239 -0
  12. package/ios/Audio/FormatConverter/FormatConverter+Utilities.swift +103 -0
  13. package/ios/Audio/FormatConverter/FormatConverter.swift +264 -0
  14. package/ios/Compressor.xcodeproj/xcuserdata/apple.xcuserdatad/xcschemes/xcschememanagement.plist +14 -0
  15. package/ios/Video/NextLevelSessionExporter.swift +694 -0
  16. package/ios/Video/VideoMain.swift +9 -8
  17. package/lib/commonjs/Audio/index.js +1 -3
  18. package/lib/commonjs/Audio/index.js.map +1 -1
  19. package/lib/commonjs/utils/index.js +0 -2
  20. package/lib/commonjs/utils/index.js.map +1 -1
  21. package/lib/module/Audio/index.js +1 -3
  22. package/lib/module/Audio/index.js.map +1 -1
  23. package/lib/module/utils/index.js +0 -3
  24. package/lib/module/utils/index.js.map +1 -1
  25. package/lib/typescript/Audio/index.d.ts.map +1 -1
  26. package/lib/typescript/index.d.ts +7 -1
  27. package/lib/typescript/index.d.ts.map +1 -1
  28. package/lib/typescript/utils/index.d.ts +11 -2
  29. package/lib/typescript/utils/index.d.ts.map +1 -1
  30. package/package.json +1 -1
  31. package/react-native-compressor.podspec +0 -1
  32. package/src/Audio/index.tsx +1 -3
  33. 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
+ }