capacitor-native-video-compressor 1.0.3 → 1.0.4
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.
|
@@ -6,123 +6,238 @@ import UIKit
|
|
|
6
6
|
@objc(NativeVideoCompressorPlugin)
|
|
7
7
|
public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
8
8
|
|
|
9
|
-
// --- BẮT ĐẦU PHẦN ĐĂNG KÝ PLUGIN (Thay thế cho file .m cũ) ---
|
|
10
9
|
public let identifier = "NativeVideoCompressorPlugin"
|
|
11
10
|
public let jsName = "NativeVideoCompressor"
|
|
12
|
-
public let pluginMethods:
|
|
11
|
+
public let pluginMethods:[CAPPluginMethod] = [
|
|
13
12
|
CAPPluginMethod(name: "compressVideo", returnType: CAPPluginReturnPromise)
|
|
14
13
|
]
|
|
15
|
-
|
|
14
|
+
|
|
16
15
|
private var isCompressing = false
|
|
17
16
|
|
|
18
17
|
@objc func compressVideo(_ call: CAPPluginCall) {
|
|
19
|
-
// 1. Lấy đường dẫn từ Javascript truyền vào
|
|
20
18
|
guard let sourcePath = call.getString("sourcePath") else {
|
|
21
19
|
call.reject("Missing sourcePath")
|
|
22
20
|
return
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
// 2. Xử lý đường dẫn (hỗ trợ cả dạng file:// và đường dẫn tuyệt đối)
|
|
26
23
|
let videoURL = sourcePath.hasPrefix("file://") ? URL(string: sourcePath)! : URL(fileURLWithPath: sourcePath)
|
|
27
|
-
|
|
28
|
-
// Tạo file đầu ra lưu ở thư mục Temp
|
|
29
24
|
let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("compressed_\(UUID().uuidString).mp4")
|
|
30
25
|
|
|
31
|
-
// Lấy thông số chất lượng truyền từ giao diện (Mặc định là MEDIUM nếu không có)
|
|
32
26
|
let qualityStr = call.getString("quality") ?? "MEDIUM"
|
|
33
|
-
var presetName: String
|
|
34
27
|
|
|
35
|
-
|
|
28
|
+
// 1. Định nghĩa Kích thước và Bitrate (Quyết định dung lượng file)
|
|
29
|
+
var targetWidth: CGFloat
|
|
30
|
+
var targetHeight: CGFloat
|
|
31
|
+
var targetBitrate: Int // Bitrate càng thấp file càng nhẹ
|
|
32
|
+
|
|
33
|
+
print("====== VIDEO COMPRESSION QUALITY: \(qualityStr) ======")
|
|
36
34
|
|
|
37
35
|
switch qualityStr {
|
|
38
36
|
case "VERY_HIGH":
|
|
39
|
-
|
|
37
|
+
targetWidth = 1920; targetHeight = 1080; targetBitrate = 4_500_000 // ~4.5 Mbps
|
|
40
38
|
case "HIGH":
|
|
41
|
-
|
|
39
|
+
targetWidth = 1280; targetHeight = 720; targetBitrate = 2_500_000 // ~2.5 Mbps
|
|
42
40
|
case "MEDIUM":
|
|
43
|
-
|
|
41
|
+
targetWidth = 960; targetHeight = 540; targetBitrate = 1_500_000 // ~1.5 Mbps (Giảm dung lượng cực tốt)
|
|
44
42
|
case "LOW":
|
|
45
|
-
|
|
46
|
-
case "360P"
|
|
47
|
-
|
|
43
|
+
targetWidth = 640; targetHeight = 480; targetBitrate = 1_000_000 // ~1.0 Mbps
|
|
44
|
+
case "360P":
|
|
45
|
+
targetWidth = 640; targetHeight = 360; targetBitrate = 700_000 // ~0.7 Mbps
|
|
48
46
|
case "VERY_LOW":
|
|
49
|
-
|
|
47
|
+
targetWidth = 426; targetHeight = 240; targetBitrate = 400_000 // ~0.4 Mbps
|
|
50
48
|
default:
|
|
51
|
-
|
|
49
|
+
targetWidth = 960; targetHeight = 540; targetBitrate = 1_500_000
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
let asset = AVAsset(url: videoURL)
|
|
55
53
|
|
|
56
|
-
// 3. Khởi tạo bộ nén phần cứng với preset tương ứng
|
|
57
|
-
guard let exportSession = AVAssetExportSession(asset: asset, presetName: presetName) else {
|
|
58
|
-
call.reject("Không thể khởi tạo bộ nén iOS")
|
|
59
|
-
return
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
exportSession.outputURL = outputURL
|
|
63
|
-
exportSession.outputFileType = .mp4
|
|
64
|
-
exportSession.shouldOptimizeForNetworkUse = true
|
|
65
|
-
|
|
66
|
-
// Báo cho giao diện biết là đã bắt đầu nén
|
|
67
54
|
self.notifyListeners("onProgress", data:["status": "started"])
|
|
68
|
-
|
|
69
55
|
self.isCompressing = true
|
|
70
|
-
let startTime = Date()
|
|
71
|
-
self.checkProgress(session: exportSession)
|
|
72
56
|
|
|
73
|
-
// Đăng ký Background Task để ứng dụng tiếp tục nén khi thu nhỏ
|
|
74
57
|
var backgroundTask: UIBackgroundTaskIdentifier = .invalid
|
|
75
|
-
|
|
76
|
-
// Cần gọi UIBackgroundTaskIdentifier trên Main Thread (nếu an toàn) hoặc dùng trực tiếp (beginBackgroundTask thread-safe)
|
|
77
58
|
backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "VideoCompression") {
|
|
78
|
-
// Callback khi hết thời gian chạy ngầm do OS cấp
|
|
79
59
|
UIApplication.shared.endBackgroundTask(backgroundTask)
|
|
80
60
|
backgroundTask = .invalid
|
|
81
61
|
}
|
|
82
62
|
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
63
|
+
// 2. Gọi hàm nén Custom bằng AVAssetWriter
|
|
64
|
+
self.compressWithAVAssetWriter(
|
|
65
|
+
asset: asset,
|
|
66
|
+
outputURL: outputURL,
|
|
67
|
+
targetWidth: targetWidth,
|
|
68
|
+
targetHeight: targetHeight,
|
|
69
|
+
targetBitrate: targetBitrate,
|
|
70
|
+
call: call,
|
|
71
|
+
backgroundTask: backgroundTask
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// MARK: - Core Compression Logic (AVAssetWriter)
|
|
76
|
+
private func compressWithAVAssetWriter(
|
|
77
|
+
asset: AVAsset,
|
|
78
|
+
outputURL: URL,
|
|
79
|
+
targetWidth: CGFloat,
|
|
80
|
+
targetHeight: CGFloat,
|
|
81
|
+
targetBitrate: Int,
|
|
82
|
+
call: CAPPluginCall,
|
|
83
|
+
backgroundTask: UIBackgroundTaskIdentifier
|
|
84
|
+
) {
|
|
85
|
+
guard let videoTrack = asset.tracks(withMediaType: .video).first else {
|
|
86
|
+
call.reject("Không tìm thấy track video")
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
let audioTrack = asset.tracks(withMediaType: .audio).first
|
|
90
|
+
|
|
91
|
+
do {
|
|
92
|
+
let reader = try AVAssetReader(asset: asset)
|
|
93
|
+
let writer = try AVAssetWriter(url: outputURL, fileType: .mp4)
|
|
94
|
+
writer.shouldOptimizeForNetworkUse = true // Tối ưu cho Web/Network
|
|
95
|
+
|
|
96
|
+
// --- XỬ LÝ KÍCH THƯỚC VÀ CHIỀU XOAY (ORIENTATION) ---
|
|
97
|
+
let naturalSize = videoTrack.naturalSize
|
|
98
|
+
let transform = videoTrack.preferredTransform
|
|
99
|
+
|
|
100
|
+
// Tính toán chiều thực tế khi hiển thị
|
|
101
|
+
var visualSize = naturalSize.applying(transform)
|
|
102
|
+
visualSize.width = abs(visualSize.width)
|
|
103
|
+
visualSize.height = abs(visualSize.height)
|
|
104
|
+
|
|
105
|
+
let isVisualPortrait = visualSize.height > visualSize.width
|
|
106
|
+
let boundingWidth = isVisualPortrait ? min(targetWidth, targetHeight) : max(targetWidth, targetHeight)
|
|
107
|
+
let boundingHeight = isVisualPortrait ? max(targetWidth, targetHeight) : min(targetWidth, targetHeight)
|
|
108
|
+
|
|
109
|
+
let isNaturalPortrait = naturalSize.height > naturalSize.width
|
|
110
|
+
let outputWidth = isNaturalPortrait ? min(boundingWidth, boundingHeight) : max(boundingWidth, boundingHeight)
|
|
111
|
+
let outputHeight = isNaturalPortrait ? max(boundingWidth, boundingHeight) : min(boundingWidth, boundingHeight)
|
|
112
|
+
|
|
113
|
+
// --- CẤU HÌNH VIDEO ---
|
|
114
|
+
let videoOutputSettings: [String: Any] = [
|
|
115
|
+
kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)
|
|
116
|
+
]
|
|
117
|
+
let videoOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: videoOutputSettings)
|
|
118
|
+
if reader.canAdd(videoOutput) { reader.add(videoOutput) }
|
|
119
|
+
|
|
120
|
+
let videoInputSettings:[String: Any] = [
|
|
121
|
+
AVVideoCodecKey: AVVideoCodecType.h264,
|
|
122
|
+
AVVideoWidthKey: Int(outputWidth),
|
|
123
|
+
AVVideoHeightKey: Int(outputHeight),
|
|
124
|
+
AVVideoCompressionPropertiesKey:[
|
|
125
|
+
AVVideoAverageBitRateKey: targetBitrate, // ÉP BITRATE TẠI ĐÂY
|
|
126
|
+
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel
|
|
127
|
+
]
|
|
128
|
+
]
|
|
129
|
+
let videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoInputSettings)
|
|
130
|
+
videoInput.transform = transform // Giữ nguyên chiều xoay gốc
|
|
131
|
+
if writer.canAdd(videoInput) { writer.add(videoInput) }
|
|
88
132
|
|
|
89
|
-
|
|
90
|
-
|
|
133
|
+
// --- CẤU HÌNH AUDIO ---
|
|
134
|
+
var audioOutput: AVAssetReaderTrackOutput?
|
|
135
|
+
var audioInput: AVAssetWriterInput?
|
|
136
|
+
|
|
137
|
+
if let audioTrack = audioTrack {
|
|
138
|
+
let audioOutputSettings:[String: Any] = [AVFormatIDKey: kAudioFormatLinearPCM]
|
|
139
|
+
audioOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: audioOutputSettings)
|
|
140
|
+
if reader.canAdd(audioOutput!) { reader.add(audioOutput!) }
|
|
141
|
+
|
|
142
|
+
let audioInputSettings:[String: Any] = [
|
|
143
|
+
AVFormatIDKey: kAudioFormatMPEG4AAC,
|
|
144
|
+
AVNumberOfChannelsKey: 2,
|
|
145
|
+
AVSampleRateKey: 44100,
|
|
146
|
+
AVEncoderBitRateKey: 128000 // Audio nén về 128kbps
|
|
147
|
+
]
|
|
148
|
+
audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioInputSettings)
|
|
149
|
+
if writer.canAdd(audioInput!) { writer.add(audioInput!) }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// --- BẮT ĐẦU NÉN ---
|
|
153
|
+
writer.startWriting()
|
|
154
|
+
reader.startReading()
|
|
155
|
+
writer.startSession(atSourceTime: .zero)
|
|
156
|
+
|
|
157
|
+
let group = DispatchGroup()
|
|
158
|
+
let videoQueue = DispatchQueue(label: "videoCompressQueue")
|
|
159
|
+
let audioQueue = DispatchQueue(label: "audioCompressQueue")
|
|
160
|
+
|
|
161
|
+
let duration = CMTimeGetSeconds(asset.duration)
|
|
162
|
+
var lastReportedProgress: Int = 0
|
|
163
|
+
|
|
164
|
+
// Xử lý Video Frame
|
|
165
|
+
group.enter()
|
|
166
|
+
videoInput.requestMediaDataWhenReady(on: videoQueue) {
|
|
167
|
+
while videoInput.isReadyForMoreMediaData {
|
|
168
|
+
autoreleasepool {
|
|
169
|
+
if reader.status == .reading, let buffer = videoOutput.copyNextSampleBuffer() {
|
|
170
|
+
videoInput.append(buffer)
|
|
171
|
+
|
|
172
|
+
// Tính toán Progress
|
|
173
|
+
if duration > 0 && !duration.isNaN {
|
|
174
|
+
let pts = CMSampleBufferGetPresentationTimeStamp(buffer)
|
|
175
|
+
let currentSeconds = CMTimeGetSeconds(pts)
|
|
176
|
+
let progress = Int((currentSeconds / duration) * 100)
|
|
177
|
+
|
|
178
|
+
if progress > lastReportedProgress && progress <= 100 {
|
|
179
|
+
lastReportedProgress = progress
|
|
180
|
+
DispatchQueue.main.async {
|
|
181
|
+
self.notifyListeners("onProgress", data:[
|
|
182
|
+
"status": "progress",
|
|
183
|
+
"percent": Double(progress)
|
|
184
|
+
])
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
videoInput.markAsFinished()
|
|
190
|
+
group.leave()
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Xử lý Audio Frame
|
|
197
|
+
if let aInput = audioInput, let aOutput = audioOutput {
|
|
198
|
+
group.enter()
|
|
199
|
+
aInput.requestMediaDataWhenReady(on: audioQueue) {
|
|
200
|
+
while aInput.isReadyForMoreMediaData {
|
|
201
|
+
autoreleasepool {
|
|
202
|
+
if reader.status == .reading, let buffer = aOutput.copyNextSampleBuffer() {
|
|
203
|
+
aInput.append(buffer)
|
|
204
|
+
} else {
|
|
205
|
+
aInput.markAsFinished()
|
|
206
|
+
group.leave()
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// --- KẾT THÚC ---
|
|
214
|
+
group.notify(queue: .main) {
|
|
215
|
+
self.isCompressing = false
|
|
91
216
|
if backgroundTask != .invalid {
|
|
92
217
|
UIApplication.shared.endBackgroundTask(backgroundTask)
|
|
93
|
-
backgroundTask = .invalid
|
|
94
218
|
}
|
|
95
219
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
220
|
+
if reader.status == .completed {
|
|
221
|
+
writer.finishWriting {
|
|
222
|
+
DispatchQueue.main.async {
|
|
223
|
+
if writer.status == .completed {
|
|
224
|
+
call.resolve([
|
|
225
|
+
"success": true,
|
|
226
|
+
"destPath": outputURL.path
|
|
227
|
+
])
|
|
228
|
+
} else {
|
|
229
|
+
call.reject("Lỗi khi ghi file: \(writer.error?.localizedDescription ?? "Unknown")")
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
writer.cancelWriting()
|
|
235
|
+
call.reject("Lỗi khi đọc file: \(reader.error?.localizedDescription ?? "Unknown")")
|
|
109
236
|
}
|
|
110
237
|
}
|
|
238
|
+
|
|
239
|
+
} catch {
|
|
240
|
+
call.reject("Lỗi khởi tạo bộ nén: \(error.localizedDescription)")
|
|
111
241
|
}
|
|
112
242
|
}
|
|
113
|
-
|
|
114
|
-
private func checkProgress(session: AVAssetExportSession) {
|
|
115
|
-
let progress = session.progress * 100
|
|
116
|
-
print("Export Progress: \(progress)%") // Log để debug
|
|
117
|
-
self.notifyListeners("onProgress", data: [
|
|
118
|
-
"status": "progress",
|
|
119
|
-
"percent": Double(progress)
|
|
120
|
-
])
|
|
121
|
-
|
|
122
|
-
if self.isCompressing && (session.status == .exporting || session.status == .waiting || session.status == .unknown) {
|
|
123
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
|
124
|
-
self?.checkProgress(session: session)
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
243
|
+
}
|
package/package.json
CHANGED