react-native-compressor 1.8.16 → 1.8.18
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 +103 -10
- package/android/build.gradle +2 -2
- 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/CompressorModule.kt +5 -0
- package/android/src/main/java/com/reactnativecompressor/Utils/HttpCallManager.kt +41 -0
- package/android/src/main/java/com/reactnativecompressor/Utils/Uploader.kt +70 -33
- package/android/src/main/java/com/reactnativecompressor/Utils/createVideoThumbnail.kt +0 -2
- package/android/src/oldarch/CompressorSpec.kt +1 -0
- 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.mm +4 -1
- package/ios/Compressor.xcodeproj/xcuserdata/apple.xcuserdatad/xcschemes/xcschememanagement.plist +14 -0
- package/ios/CompressorManager.swift +5 -0
- package/ios/Utils/Uploader.swift +18 -1
- package/ios/Utils/UrlTaskManager.swift +58 -0
- package/ios/Video/NextLevelSessionExporter.swift +694 -0
- package/ios/Video/VideoMain.swift +1 -2
- package/lib/commonjs/Audio/index.js +1 -3
- package/lib/commonjs/Audio/index.js.map +1 -1
- package/lib/commonjs/Spec/NativeCompressor.js.map +1 -1
- package/lib/commonjs/index.js +6 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/utils/Uploader.js +13 -2
- package/lib/commonjs/utils/Uploader.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/Spec/NativeCompressor.js.map +1 -1
- package/lib/module/index.js +2 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/Uploader.js +11 -1
- package/lib/module/utils/Uploader.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/Spec/NativeCompressor.d.ts +1 -0
- package/lib/typescript/Spec/NativeCompressor.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +3 -3
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/utils/Uploader.d.ts +3 -1
- package/lib/typescript/utils/Uploader.d.ts.map +1 -1
- package/lib/typescript/utils/index.d.ts +4 -1
- 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/Spec/NativeCompressor.ts +2 -0
- package/src/index.tsx +2 -0
- package/src/utils/Uploader.tsx +18 -1
- package/src/utils/index.tsx +5 -2
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//
|
|
2
|
+
// AudioHelper.swift
|
|
3
|
+
// react-native-compressor
|
|
4
|
+
//
|
|
5
|
+
// Created by Numan on 12/11/2023.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import AVFoundation
|
|
9
|
+
|
|
10
|
+
class AudioHelper {
|
|
11
|
+
|
|
12
|
+
static func getAudioBitrate(path: String) -> Int {
|
|
13
|
+
let audioURL = URL(fileURLWithPath: path)
|
|
14
|
+
let avAsset = AVURLAsset(url: audioURL)
|
|
15
|
+
let keys: Set<URLResourceKey> = [.totalFileSizeKey, .fileSizeKey]
|
|
16
|
+
let resourceValues = try? audioURL.resourceValues(forKeys: keys)
|
|
17
|
+
let fileSize = resourceValues?.fileSize ?? resourceValues?.totalFileSize
|
|
18
|
+
|
|
19
|
+
// Calculate bitrate in kbps
|
|
20
|
+
if let fileSize = fileSize, avAsset.duration.seconds > 0 {
|
|
21
|
+
return Int(Double(fileSize) * 8 / avAsset.duration.seconds)
|
|
22
|
+
}
|
|
23
|
+
return 0
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static func getDestinationBitrateByQuality(path: String, quality: String) -> Int {
|
|
27
|
+
let originalBitrate = getAudioBitrate(path: path)
|
|
28
|
+
var destinationBitrate = originalBitrate
|
|
29
|
+
print("source bitrate: \(originalBitrate)")
|
|
30
|
+
|
|
31
|
+
// Calculate the percentage of the original bitrate relative to the range 64000 to 320000
|
|
32
|
+
let percentage = Double(originalBitrate - 64000) / Double(320000 - 64000)
|
|
33
|
+
|
|
34
|
+
switch quality.lowercased() {
|
|
35
|
+
case "low":
|
|
36
|
+
destinationBitrate = max(64000, Int(Double(originalBitrate) * 0.3))
|
|
37
|
+
case "medium":
|
|
38
|
+
// Set destination bitrate to 60% of the original bitrate
|
|
39
|
+
destinationBitrate = Int(Double(originalBitrate) * 0.5)
|
|
40
|
+
case "high":
|
|
41
|
+
// Set destination bitrate to 80% of the original bitrate
|
|
42
|
+
destinationBitrate = min(320000,Int(Double(originalBitrate) * 0.7))
|
|
43
|
+
default:
|
|
44
|
+
print("Invalid quality level. Please enter 'low', 'medium', or 'high'.")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return destinationBitrate
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -5,16 +5,21 @@
|
|
|
5
5
|
// Created by Numan on 10/09/2023.
|
|
6
6
|
//
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
import AVFoundation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
10
13
|
let AlAsset_Library_Scheme = "assets-library"
|
|
11
14
|
class AudioMain{
|
|
12
15
|
static func compress_audio(_ fileUrl: String, optionMap: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
13
16
|
do {
|
|
14
17
|
var fileUrl = fileUrl
|
|
18
|
+
|
|
15
19
|
if fileUrl.hasPrefix("file://") {
|
|
16
20
|
fileUrl = fileUrl.replacingOccurrences(of: "file://", with: "")
|
|
17
21
|
}
|
|
22
|
+
|
|
18
23
|
let fileManager = FileManager.default
|
|
19
24
|
var isDir: ObjCBool = false
|
|
20
25
|
if !fileManager.fileExists(atPath: fileUrl, isDirectory: &isDir) || isDir.boolValue {
|
|
@@ -23,93 +28,52 @@ class AudioMain{
|
|
|
23
28
|
return
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
let
|
|
27
|
-
let
|
|
28
|
-
|
|
29
|
-
let
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
} catch {
|
|
38
|
-
reject(error.localizedDescription, error.localizedDescription, nil)
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
static func getAudioQualityConstant(_ quality: String) -> String {
|
|
43
|
-
let audioQualityArray = ["low", "medium", "high"]
|
|
44
|
-
if let index = audioQualityArray.firstIndex(of: quality) {
|
|
45
|
-
switch index {
|
|
46
|
-
case 0:
|
|
47
|
-
return AVAssetExportPresetLowQuality
|
|
48
|
-
case 1:
|
|
49
|
-
return AVAssetExportPresetMediumQuality
|
|
50
|
-
case 2:
|
|
51
|
-
return AVAssetExportPresetHighestQuality
|
|
52
|
-
default:
|
|
53
|
-
return AVAssetExportPresetMediumQuality
|
|
31
|
+
let audioOptions = AudioOptions.fromDictionary((optionMap as! [String : Any]))
|
|
32
|
+
let outputMp3Path = "file://\(Utils.generateCacheFilePath("m4a"))"
|
|
33
|
+
|
|
34
|
+
let oldUfileUrlRL = URL(string: fileUrl)!
|
|
35
|
+
let newURL = URL(string: outputMp3Path)!
|
|
36
|
+
|
|
37
|
+
var options = FormatConverter.Options()
|
|
38
|
+
|
|
39
|
+
if(audioOptions.samplerate != -1){
|
|
40
|
+
options.sampleRate = Double(audioOptions.samplerate)
|
|
54
41
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
static func auto_compress_helper(_ avAsset: AVURLAsset, qualityConstant: String, complete completeCallback: @escaping (_ mp3Path: String?, _ finished: Bool) -> Void) {
|
|
60
|
-
var path: String
|
|
61
|
-
if avAsset.url.scheme == AlAsset_Library_Scheme {
|
|
62
|
-
path = avAsset.url.query ?? ""
|
|
63
|
-
if path.isEmpty {
|
|
64
|
-
completeCallback(nil, false)
|
|
65
|
-
return
|
|
42
|
+
|
|
43
|
+
if(audioOptions.bitrate != -1){
|
|
44
|
+
options.bitRate = UInt32(audioOptions.bitrate)
|
|
66
45
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
46
|
+
else
|
|
47
|
+
{
|
|
48
|
+
let bitrate = AudioHelper.getDestinationBitrateByQuality(path: fileUrl,quality: audioOptions.quality)
|
|
49
|
+
print("output bitrate: \(bitrate)")
|
|
50
|
+
options.bitRate = UInt32(bitrate)
|
|
72
51
|
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
let mp3Path = Utils.generateCacheFilePath("m4a")
|
|
76
|
-
|
|
77
|
-
if FileManager.default.fileExists(atPath: mp3Path) {
|
|
78
|
-
completeCallback(mp3Path, true)
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
let compatiblePresets = AVAssetExportSession.exportPresets(compatibleWith: avAsset)
|
|
83
|
-
|
|
84
|
-
if compatiblePresets.contains(qualityConstant) {
|
|
85
|
-
let exportSession = AVAssetExportSession(asset: avAsset, presetName: AVAssetExportPresetAppleM4A)
|
|
86
52
|
|
|
87
|
-
let mp3Url = URL(fileURLWithPath: mp3Path)
|
|
88
|
-
exportSession?.outputURL = mp3Url
|
|
89
|
-
exportSession?.shouldOptimizeForNetworkUse = true
|
|
90
|
-
exportSession?.outputFileType = .m4a
|
|
91
53
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
print("
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
default:
|
|
109
|
-
break
|
|
54
|
+
if(audioOptions.channels != -1){
|
|
55
|
+
options.channels = UInt32(audioOptions.channels)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
options.format = .m4a
|
|
59
|
+
options.eraseFile = false
|
|
60
|
+
options.bitDepthRule = .any
|
|
61
|
+
|
|
62
|
+
let converter = FormatConverter(inputURL: oldUfileUrlRL, outputURL: newURL, options: options)
|
|
63
|
+
converter.start { error in
|
|
64
|
+
// check to see if error isn't nil, otherwise you're good
|
|
65
|
+
if((error) != nil)
|
|
66
|
+
{
|
|
67
|
+
print("error=> \(error?.localizedDescription)")
|
|
68
|
+
reject(error?.localizedDescription,error?.localizedDescription, error)
|
|
69
|
+
return
|
|
110
70
|
}
|
|
111
|
-
|
|
112
|
-
|
|
71
|
+
|
|
72
|
+
resolve(newURL.absoluteString)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
} catch {
|
|
76
|
+
reject(error.localizedDescription, error.localizedDescription, nil)
|
|
113
77
|
}
|
|
114
78
|
}
|
|
115
79
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
class AudioOptions: NSObject {
|
|
2
|
+
static func fromDictionary(_ dictionary: [String: Any]?) -> AudioOptions {
|
|
3
|
+
let options = AudioOptions()
|
|
4
|
+
|
|
5
|
+
guard let dictionary = dictionary else {
|
|
6
|
+
return options
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
for (key, value) in dictionary {
|
|
10
|
+
switch key {
|
|
11
|
+
case "quality":
|
|
12
|
+
options.quality = (value as? String) ?? "medium"
|
|
13
|
+
case "bitrate":
|
|
14
|
+
options.bitrate = (value as? Int) ?? -1
|
|
15
|
+
case "samplerate":
|
|
16
|
+
options.samplerate = (value as? Int) ?? -1
|
|
17
|
+
case "channels":
|
|
18
|
+
options.channels = (value as? Int) ?? -1
|
|
19
|
+
|
|
20
|
+
default:
|
|
21
|
+
break
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return options
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var quality: String = "medium"
|
|
29
|
+
var bitrate: Int = -1
|
|
30
|
+
var samplerate: Int = -1
|
|
31
|
+
var channels: Int = -1
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
override init() {
|
|
35
|
+
super.init()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/
|
|
2
|
+
|
|
3
|
+
import AVFoundation
|
|
4
|
+
|
|
5
|
+
// MARK: - internal helper functions
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
public extension AVURLAsset {
|
|
9
|
+
/// Audio format for the file in the URL asset
|
|
10
|
+
var audioFormat: AVAudioFormat? {
|
|
11
|
+
// pull the input format out of the audio file...
|
|
12
|
+
if let source = try? AVAudioFile(forReading: url) {
|
|
13
|
+
return source.fileFormat
|
|
14
|
+
|
|
15
|
+
// if that fails it might be a video, so check the tracks for audio
|
|
16
|
+
} else {
|
|
17
|
+
let audioTracks = tracks.filter { $0.mediaType == .audio }
|
|
18
|
+
|
|
19
|
+
guard !audioTracks.isEmpty else { return nil }
|
|
20
|
+
|
|
21
|
+
let formatDescriptions = audioTracks.compactMap {
|
|
22
|
+
$0.formatDescriptions as? [CMFormatDescription]
|
|
23
|
+
}.reduce([], +)
|
|
24
|
+
|
|
25
|
+
let audioFormats: [AVAudioFormat] = formatDescriptions.compactMap {
|
|
26
|
+
AVAudioFormat(cmAudioFormatDescription: $0)
|
|
27
|
+
}
|
|
28
|
+
return audioFormats.first
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
extension FormatConverter {
|
|
35
|
+
/// Example of the most simplistic AVFoundation conversion.
|
|
36
|
+
/// With this approach you can't really specify any settings other than the limited presets.
|
|
37
|
+
/// No sample rate conversion in this. This isn't used in the public methods but is here
|
|
38
|
+
/// for example.
|
|
39
|
+
///
|
|
40
|
+
/// see `AVAssetExportSession`:
|
|
41
|
+
/// *Prior to initializing an instance of AVAssetExportSession, you can invoke
|
|
42
|
+
/// +allExportPresets to obtain the complete list of presets available. Use
|
|
43
|
+
/// +exportPresetsCompatibleWithAsset: to obtain a list of presets that are compatible
|
|
44
|
+
/// with a specific AVAsset.*
|
|
45
|
+
///
|
|
46
|
+
/// This is no longer used in this class as it's not possible to convert sample rate or other
|
|
47
|
+
/// required options. It will use the next function instead
|
|
48
|
+
func convertCompressed(presetName: String, completionHandler: FormatConverterCallback? = nil) {
|
|
49
|
+
guard let inputURL = inputURL else {
|
|
50
|
+
completionHandler?(Self.createError(message: "Input file can't be nil."))
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
guard let outputURL = outputURL else {
|
|
54
|
+
completionHandler?(Self.createError(message: "Output file can't be nil."))
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let asset = AVURLAsset(url: inputURL)
|
|
59
|
+
guard let session = AVAssetExportSession(asset: asset,
|
|
60
|
+
presetName: presetName) else { return }
|
|
61
|
+
|
|
62
|
+
session.determineCompatibleFileTypes { list in
|
|
63
|
+
|
|
64
|
+
guard let outputFileType: AVFileType = list.first else {
|
|
65
|
+
let error = Self.createError(message: "Unable to determine a compatible file type from \(inputURL.path)")
|
|
66
|
+
completionHandler?(error)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// session.progress could be sent out via a delegate for this session
|
|
71
|
+
session.outputURL = outputURL
|
|
72
|
+
session.outputFileType = outputFileType
|
|
73
|
+
session.exportAsynchronously {
|
|
74
|
+
completionHandler?(session.error)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Convert to compressed first creating a tmp file to PCM to allow more flexible conversion
|
|
80
|
+
/// options to work.
|
|
81
|
+
func convertCompressed(completionHandler: FormatConverterCallback? = nil) {
|
|
82
|
+
guard let inputURL = inputURL else {
|
|
83
|
+
completionHandler?(Self.createError(message: "Input file can't be nil."))
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
guard let outputURL = outputURL else {
|
|
87
|
+
completionHandler?(Self.createError(message: "Output file can't be nil."))
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
guard let options = options else {
|
|
92
|
+
completionHandler?(Self.createError(message: "Options can't be nil."))
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let tempName = outputURL.deletingPathExtension().lastPathComponent + "_TEMP.wav"
|
|
97
|
+
let tempFile = outputURL.deletingLastPathComponent().appendingPathComponent(tempName)
|
|
98
|
+
|
|
99
|
+
var tempOptions = FormatConverter.Options()
|
|
100
|
+
tempOptions.bitDepthRule = .lessThanOrEqual
|
|
101
|
+
tempOptions.bitDepth = 24
|
|
102
|
+
tempOptions.sampleRate = options.sampleRate
|
|
103
|
+
tempOptions.channels = options.channels
|
|
104
|
+
tempOptions.format = .wav
|
|
105
|
+
|
|
106
|
+
let tempConverter = FormatConverter(inputURL: inputURL,
|
|
107
|
+
outputURL: tempFile,
|
|
108
|
+
options: tempOptions)
|
|
109
|
+
|
|
110
|
+
tempConverter.start { error in
|
|
111
|
+
if let error = error {
|
|
112
|
+
completionHandler?(Self.createError(message: "Failed to convert input to PCM: \(error.localizedDescription)"))
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
self.inputURL = tempFile
|
|
117
|
+
|
|
118
|
+
self.convertPCMToCompressed { error in
|
|
119
|
+
try? FileManager.default.removeItem(at: tempFile)
|
|
120
|
+
completionHandler?(error)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// The AVFoundation way. *This doesn't currently handle compressed input - only compressed output.*
|
|
126
|
+
func convertPCMToCompressed(completionHandler: FormatConverterCallback? = nil) {
|
|
127
|
+
guard let inputURL = inputURL else {
|
|
128
|
+
completionHandler?(Self.createError(message: "Input file can't be nil."))
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
guard let outputURL = outputURL else {
|
|
132
|
+
completionHandler?(Self.createError(message: "Output file can't be nil."))
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
guard let options = options, let outputFormat = options.format else {
|
|
137
|
+
completionHandler?(Self.createError(message: "Options can't be nil."))
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// verify outputFormat
|
|
142
|
+
guard FormatConverter.outputFormats.contains(outputFormat) else {
|
|
143
|
+
completionHandler?(Self.createError(message: "The output file format isn't able to be produced by this class."))
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let asset = AVURLAsset(url: inputURL)
|
|
148
|
+
do {
|
|
149
|
+
self.reader = try AVAssetReader(asset: asset)
|
|
150
|
+
|
|
151
|
+
} catch let err as NSError {
|
|
152
|
+
completionHandler?(err)
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
guard let reader = reader else {
|
|
157
|
+
completionHandler?(Self.createError(message: "Unable to setup the AVAssetReader."))
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
guard let inputFormat = asset.audioFormat else {
|
|
162
|
+
completionHandler?(Self.createError(message: "Unable to read the input file format."))
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
var format: AVFileType
|
|
167
|
+
var formatKey: AudioFormatID
|
|
168
|
+
|
|
169
|
+
switch outputFormat {
|
|
170
|
+
case .m4a, .mp4:
|
|
171
|
+
format = .m4a
|
|
172
|
+
formatKey = kAudioFormatMPEG4AAC
|
|
173
|
+
case .aif:
|
|
174
|
+
format = .aiff
|
|
175
|
+
formatKey = kAudioFormatLinearPCM
|
|
176
|
+
case .caf:
|
|
177
|
+
format = .caf
|
|
178
|
+
formatKey = kAudioFormatLinearPCM
|
|
179
|
+
case .wav:
|
|
180
|
+
format = .wav
|
|
181
|
+
formatKey = kAudioFormatLinearPCM
|
|
182
|
+
default:
|
|
183
|
+
print("Unsupported output format: \(outputFormat)")
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
do {
|
|
188
|
+
self.writer = try AVAssetWriter(outputURL: outputURL, fileType: format)
|
|
189
|
+
} catch let err as NSError {
|
|
190
|
+
completionHandler?(err)
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
guard let writer = writer else {
|
|
195
|
+
completionHandler?(Self.createError(message: "Unable to setup the AVAssetWriter."))
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 1. chosen option. 2. same as input file. 3. 16 bit
|
|
200
|
+
// optional in case of compressed audio. That said, the other conversion methods are actually used in
|
|
201
|
+
// that case
|
|
202
|
+
let bitDepth = (options.bitDepth ?? inputFormat.settings[AVLinearPCMBitDepthKey] ?? 16) as Any
|
|
203
|
+
var isFloat = false
|
|
204
|
+
if let intDepth = bitDepth as? Int {
|
|
205
|
+
isFloat = intDepth == 32
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
var sampleRate = options.sampleRate ?? inputFormat.sampleRate
|
|
209
|
+
let channels = options.channels ?? inputFormat.channelCount
|
|
210
|
+
|
|
211
|
+
if sampleRate == 0 {
|
|
212
|
+
print("Sample rate can't be 0 - assigning to default format of 48k. inputFormat is", inputFormat)
|
|
213
|
+
sampleRate = 48000
|
|
214
|
+
}
|
|
215
|
+
var outputSettings: [String: Any]?
|
|
216
|
+
|
|
217
|
+
// Note: AVAssetReaderOutput does not currently support compressed audio
|
|
218
|
+
if formatKey == kAudioFormatMPEG4AAC {
|
|
219
|
+
if sampleRate > 48000 {
|
|
220
|
+
sampleRate = 48000
|
|
221
|
+
}
|
|
222
|
+
// mono should be 1/2 the shown bitrate
|
|
223
|
+
let perChannel = channels == 1 ? 2 : 1
|
|
224
|
+
|
|
225
|
+
// reset these for m4a:
|
|
226
|
+
outputSettings = [
|
|
227
|
+
AVFormatIDKey: formatKey,
|
|
228
|
+
AVSampleRateKey: sampleRate,
|
|
229
|
+
AVNumberOfChannelsKey: channels,
|
|
230
|
+
AVEncoderBitRateKey: Int(options.bitRate) / perChannel,
|
|
231
|
+
AVEncoderBitRateStrategyKey: AVAudioBitRateStrategy_Constant,
|
|
232
|
+
]
|
|
233
|
+
} else {
|
|
234
|
+
outputSettings = [
|
|
235
|
+
AVFormatIDKey: formatKey,
|
|
236
|
+
AVSampleRateKey: sampleRate,
|
|
237
|
+
AVNumberOfChannelsKey: channels,
|
|
238
|
+
AVLinearPCMBitDepthKey: bitDepth,
|
|
239
|
+
AVLinearPCMIsFloatKey: isFloat,
|
|
240
|
+
AVLinearPCMIsBigEndianKey: format != .wav,
|
|
241
|
+
AVLinearPCMIsNonInterleaved: !(options.isInterleaved ?? inputFormat.isInterleaved),
|
|
242
|
+
]
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let hint = asset.audioFormat?.formatDescription
|
|
246
|
+
|
|
247
|
+
let writerInput = AVAssetWriterInput(mediaType: .audio, outputSettings: outputSettings, sourceFormatHint: hint)
|
|
248
|
+
writer.add(writerInput)
|
|
249
|
+
|
|
250
|
+
guard let track = asset.tracks(withMediaType: .audio).first else {
|
|
251
|
+
completionProxy(error: Self.createError(message: "No audio was found in the input file."),
|
|
252
|
+
completionHandler: completionHandler)
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let readerOutput = AVAssetReaderTrackOutput(track: track, outputSettings: nil)
|
|
257
|
+
guard reader.canAdd(readerOutput) else {
|
|
258
|
+
completionProxy(error: Self.createError(message: "Unable to add reader output."),
|
|
259
|
+
completionHandler: completionHandler)
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
reader.add(readerOutput)
|
|
263
|
+
|
|
264
|
+
if !writer.startWriting() {
|
|
265
|
+
print("Failed to start writing. Error:", writer.error?.localizedDescription)
|
|
266
|
+
completionProxy(error: writer.error,
|
|
267
|
+
completionHandler: completionHandler)
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
writer.startSession(atSourceTime: CMTime.zero)
|
|
272
|
+
|
|
273
|
+
if !reader.startReading() {
|
|
274
|
+
print("Failed to start reading. Error:", reader.error?.localizedDescription)
|
|
275
|
+
completionProxy(error: reader.error,
|
|
276
|
+
completionHandler: completionHandler)
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let queue = DispatchQueue(label: "com.audiodesigndesk.ADD.FormatConverter.convertAsset")
|
|
281
|
+
|
|
282
|
+
// session.progress could be sent out via a delegate for this session
|
|
283
|
+
writerInput.requestMediaDataWhenReady(on: queue, using: {
|
|
284
|
+
var processing = true // safety flag to prevent runaway loops if errors
|
|
285
|
+
|
|
286
|
+
while writerInput.isReadyForMoreMediaData, processing {
|
|
287
|
+
if reader.status == .reading,
|
|
288
|
+
let buffer = readerOutput.copyNextSampleBuffer()
|
|
289
|
+
{
|
|
290
|
+
writerInput.append(buffer)
|
|
291
|
+
|
|
292
|
+
} else {
|
|
293
|
+
writerInput.markAsFinished()
|
|
294
|
+
|
|
295
|
+
switch reader.status {
|
|
296
|
+
case .failed:
|
|
297
|
+
print("Conversion failed with error", reader.error)
|
|
298
|
+
writer.cancelWriting()
|
|
299
|
+
self.completionProxy(error: reader.error, completionHandler: completionHandler)
|
|
300
|
+
case .cancelled:
|
|
301
|
+
print("Conversion cancelled")
|
|
302
|
+
self.completionProxy(error: Self.createError(message: "Process canceled"),
|
|
303
|
+
completionHandler: completionHandler)
|
|
304
|
+
case .completed:
|
|
305
|
+
writer.finishWriting {
|
|
306
|
+
switch writer.status {
|
|
307
|
+
case .failed:
|
|
308
|
+
print("Conversion failed at finishWriting")
|
|
309
|
+
self.completionProxy(error: writer.error,
|
|
310
|
+
completionHandler: completionHandler)
|
|
311
|
+
default:
|
|
312
|
+
// no errors
|
|
313
|
+
completionHandler?(nil)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
default:
|
|
317
|
+
break
|
|
318
|
+
}
|
|
319
|
+
processing = false
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}) // requestMediaDataWhenReady
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|