capacitor-native-video-compressor 1.0.2 → 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: [CAPPluginMethod] = [
11
+ public let pluginMethods:[CAPPluginMethod] = [
13
12
  CAPPluginMethod(name: "compressVideo", returnType: CAPPluginReturnPromise)
14
13
  ]
15
- // --- KẾT THÚC PHẦN ĐĂNG KÝ ---
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
- print("====== VIDEO COMPRESSION QUALITY RECEIVED: \(qualityStr) ======")
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
- presetName = AVAssetExportPresetHighestQuality
37
+ targetWidth = 1920; targetHeight = 1080; targetBitrate = 4_500_000 // ~4.5 Mbps
40
38
  case "HIGH":
41
- presetName = AVAssetExportPreset1280x720 // 720p cho mức High để nhận thấy sự giảm dung lượng rõ rệt
39
+ targetWidth = 1280; targetHeight = 720; targetBitrate = 2_500_000 // ~2.5 Mbps
42
40
  case "MEDIUM":
43
- presetName = AVAssetExportPreset960x540 // 540p cho mức Medium
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
- presetName = AVAssetExportPreset640x480 // 480p cho mức Low
46
- case "360P" :
47
- presetName = AVAssetExportPresetMediumQuality // 360p cho mức
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
- presetName = AVAssetExportPresetLowQuality // 124 cho mức Very Low
47
+ targetWidth = 426; targetHeight = 240; targetBitrate = 400_000 // ~0.4 Mbps
50
48
  default:
51
- presetName = AVAssetExportPreset960x540
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
- // 5. Bắt đầu tiến trình nén (Chạy ngầm trên GPU)
84
- exportSession.exportAsynchronously {
85
- self.isCompressing = false
86
- let elapsed = Date().timeIntervalSince(startTime)
87
- print("Compression finished in \(elapsed) seconds. Final status: \(exportSession.status.rawValue)")
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
- DispatchQueue.main.async {
90
- // Kết thúc Background Task khi xử lý xong
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
- switch exportSession.status {
97
- case .completed:
98
- // THÀNH CÔNG! Trả kết quả về cho Nuxt.js
99
- call.resolve([
100
- "success": true,
101
- "destPath": outputURL.path
102
- ])
103
- case .failed:
104
- call.reject("Nén thất bại: \(exportSession.error?.localizedDescription ?? "Lỗi không xác định")")
105
- case .cancelled:
106
- call.reject("Đã hủy nén")
107
- default:
108
- call.reject("Lỗi không xác định")
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-native-video-compressor",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Native video compressor using LightCompressor and AVFoundation",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -77,5 +77,9 @@
77
77
  "android": {
78
78
  "src": "android"
79
79
  }
80
+ },
81
+ "dependencies": {
82
+ "@ffmpeg/ffmpeg": "^0.12.15",
83
+ "@ffmpeg/util": "^0.12.2"
80
84
  }
81
85
  }