-
-
Notifications
You must be signed in to change notification settings - Fork 297
Expand file tree
/
Copy pathAttachmentViewModel+Compress.swift
More file actions
164 lines (145 loc) · 6.37 KB
/
AttachmentViewModel+Compress.swift
File metadata and controls
164 lines (145 loc) · 6.37 KB
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
//
// AttachmentViewModel+Compress.swift
//
//
// Created by MainasuK on 2022/11/11.
//
import UIKit
import AVKit
import MastodonCore
import SessionExporter
import Nuke
extension AttachmentViewModel {
func compressVideo(url: URL) async throws -> URL? {
let urlAsset = AVURLAsset(url: url)
guard let track = try await urlAsset.loadTracks(withMediaType: .video).first else {
return nil
}
let exporter = NextLevelSessionExporter(withAsset: urlAsset)
exporter.outputFileType = .mp4
let preferredSize = try await preferredSizeFor(
track: track,
maxLongestSide: 1280
)
let outputURL = try FileManager.default.createTemporaryFileURL(
filename: UUID().uuidString,
pathExtension: url.pathExtension
)
exporter.outputURL = outputURL
let compressionDict: [String: Any] = [
AVVideoAverageBitRateKey: NSNumber(integerLiteral: 3000000), // 3000k
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel as String,
AVVideoAverageNonDroppableFrameRateKey: NSNumber(floatLiteral: 30), // 30 FPS
]
exporter.videoOutputConfiguration = [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoWidthKey: NSNumber(floatLiteral: preferredSize.width),
AVVideoHeightKey: NSNumber(floatLiteral: preferredSize.height),
AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill,
AVVideoCompressionPropertiesKey: compressionDict
]
exporter.audioOutputConfiguration = [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVEncoderBitRateKey: NSNumber(integerLiteral: 128000), // 128k
AVNumberOfChannelsKey: NSNumber(integerLiteral: 2),
AVSampleRateKey: NSNumber(value: Float(44100))
]
// needs set to LOW priority to prevent priority inverse issue
let task = Task(priority: .utility) {
_ = try await exportVideo(by: exporter)
}
_ = try await task.value
return outputURL
}
private func preferredSizeFor(track: AVAssetTrack, maxLongestSide: CGFloat) async throws -> CGSize {
let trackSize = try await track.load(.naturalSize).applying(track.preferredTransform)
let actualSize = CGSize(width: abs(trackSize.width), height: abs(trackSize.height))
let isLandscape = actualSize.width >= actualSize.height
switch isLandscape {
case false: // portrait mode, needs height altered eventually
if actualSize.height > maxLongestSide {
// reduce height, keep aspect ratio
return CGSize(width: (maxLongestSide / (actualSize.height/actualSize.width)), height: maxLongestSide)
}
return actualSize
case true: // landscape mode, needs width altered eventually
if actualSize.width > maxLongestSide {
// reduce width, keep aspect ratio
return CGSize(width: maxLongestSide, height: (maxLongestSide * (actualSize.height/actualSize.width)))
}
return actualSize
}
}
private func exportVideo(by exporter: NextLevelSessionExporter) async throws -> URL {
guard let outputURL = exporter.outputURL else {
throw AppError.badRequest
}
return try await withCheckedThrowingContinuation { continuation in
exporter.export(progressHandler: { progress in
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.videoCompressProgress = Double(progress)
}
}, completionHandler: { result in
switch result {
case .success(let status):
switch status {
case .completed:
print("NextLevelSessionExporter, export completed, \(exporter.outputURL?.description ?? "")")
continuation.resume(with: .success(outputURL))
default:
if Task.isCancelled {
exporter.cancelExport()
}
print("NextLevelSessionExporter, did not complete")
}
case .failure(let error):
continuation.resume(with: .failure(error))
}
})
}
} // end func
}
extension AttachmentViewModel {
@AttachmentViewModelActor
func compressImage(data: Data, sizeLimit: SizeLimit) throws -> Output {
let maxPayloadSizeInBytes = max((sizeLimit.image ?? 10 * 1024 * 1024), 1 * 1024 * 1024)
guard let image = UIImage(data: data)?.normalized(),
var imageData = image.pngData()
else {
throw AttachmentError.invalidAttachmentType
}
repeat {
guard let image = UIImage(data: imageData) else {
throw AttachmentError.invalidAttachmentType
}
if AssetType(imageData) == .png {
// A. png image
if imageData.count > maxPayloadSizeInBytes {
guard let compressedJpegData = image.jpegData(compressionQuality: 0.8) else {
throw AttachmentError.invalidAttachmentType
}
imageData = compressedJpegData
} else {
break
}
} else {
// B. other image
if imageData.count > maxPayloadSizeInBytes {
let targetSize = CGSize(width: image.size.width * 0.8, height: image.size.height * 0.8)
let scaledImage = image.resized(size: targetSize)
guard let compressedJpegData = scaledImage.jpegData(compressionQuality: 0.8) else {
throw AttachmentError.invalidAttachmentType
}
imageData = compressedJpegData
} else {
break
}
}
} while (imageData.count > maxPayloadSizeInBytes)
return .image(imageData, imageKind: AssetType(imageData) == .png ? .png : .jpg)
}
}
@globalActor actor AttachmentViewModelActor {
static var shared = AttachmentViewModelActor()
}