@trustchex/react-native-sdk 1.464.0 → 1.475.0

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 (64) hide show
  1. package/android/build.gradle +3 -3
  2. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +190 -12
  3. package/ios/Camera/TrustchexCameraView.swift +244 -46
  4. package/ios/Permission/PermissionModule.m +22 -0
  5. package/ios/Permission/PermissionModule.swift +67 -0
  6. package/lib/module/Screens/Debug/MRZTestScreen.js +121 -147
  7. package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +24 -3
  8. package/lib/module/Screens/Dynamic/VerbalConsentScreen.js +1 -1
  9. package/lib/module/Screens/Dynamic/VideoCallScreen.js +7 -12
  10. package/lib/module/Screens/Static/ResultScreen.js +9 -1
  11. package/lib/module/Shared/Components/EIDScanner.js +166 -163
  12. package/lib/module/Shared/Components/FaceCamera.js +14 -8
  13. package/lib/module/Shared/Components/IdentityDocumentCamera.js +45 -10
  14. package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +112 -0
  15. package/lib/module/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
  16. package/lib/module/Shared/Libs/index.js +20 -0
  17. package/lib/module/Shared/Libs/mrz.utils.js +570 -16
  18. package/lib/module/Shared/Libs/mrzFrameAggregator.js +301 -0
  19. package/lib/module/Shared/Libs/mrzOcrIntegration.js +109 -0
  20. package/lib/module/Shared/Libs/permissions.utils.js +199 -0
  21. package/lib/module/Translation/Resources/en.js +1 -0
  22. package/lib/module/Translation/Resources/tr.js +1 -0
  23. package/lib/module/version.js +1 -1
  24. package/lib/typescript/src/Screens/Debug/MRZTestScreen.d.ts.map +1 -1
  25. package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
  26. package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts.map +1 -1
  27. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  28. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  29. package/lib/typescript/src/Shared/Components/FaceCamera.d.ts.map +1 -1
  30. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  31. package/lib/typescript/src/Shared/Libs/index.d.ts +20 -0
  32. package/lib/typescript/src/Shared/Libs/index.d.ts.map +1 -0
  33. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts +8 -0
  34. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
  35. package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +76 -0
  36. package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -0
  37. package/lib/typescript/src/Shared/Libs/mrzOcrIntegration.d.ts +73 -0
  38. package/lib/typescript/src/Shared/Libs/mrzOcrIntegration.d.ts.map +1 -0
  39. package/lib/typescript/src/Shared/Libs/permissions.utils.d.ts +58 -0
  40. package/lib/typescript/src/Shared/Libs/permissions.utils.d.ts.map +1 -0
  41. package/lib/typescript/src/Translation/Resources/en.d.ts +1 -0
  42. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  43. package/lib/typescript/src/Translation/Resources/tr.d.ts +1 -0
  44. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  45. package/lib/typescript/src/version.d.ts +1 -1
  46. package/package.json +14 -10
  47. package/src/Screens/Debug/MRZTestScreen.tsx +137 -166
  48. package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +45 -5
  49. package/src/Screens/Dynamic/VerbalConsentScreen.tsx +1 -1
  50. package/src/Screens/Dynamic/VideoCallScreen.tsx +8 -19
  51. package/src/Screens/Static/ResultScreen.tsx +18 -9
  52. package/src/Shared/Components/EIDScanner.tsx +210 -160
  53. package/src/Shared/Components/FaceCamera.tsx +19 -16
  54. package/src/Shared/Components/IdentityDocumentCamera.tsx +53 -13
  55. package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +112 -0
  56. package/src/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
  57. package/src/Shared/Libs/index.ts +63 -0
  58. package/src/Shared/Libs/mrz.utils.ts +639 -11
  59. package/src/Shared/Libs/mrzFrameAggregator.ts +370 -0
  60. package/src/Shared/Libs/mrzOcrIntegration.ts +175 -0
  61. package/src/Shared/Libs/permissions.utils.ts +251 -0
  62. package/src/Translation/Resources/en.ts +1 -0
  63. package/src/Translation/Resources/tr.ts +1 -0
  64. package/src/version.ts +1 -1
@@ -115,7 +115,24 @@ class TrustchexCameraView: UIView {
115
115
  }
116
116
  }
117
117
 
118
+ private func configureAudioSession() {
119
+ do {
120
+ let audioSession = AVAudioSession.sharedInstance()
121
+ // Set category to playAndRecord so we can record audio during video
122
+ try audioSession.setCategory(
123
+ .playAndRecord,
124
+ options: [.defaultToSpeaker, .duckOthers]
125
+ )
126
+ try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
127
+ } catch {
128
+ // Audio session configuration failed - recording may not have audio
129
+ }
130
+ }
131
+
118
132
  private func configureCaptureSession() {
133
+ // Configure audio session for recording with microphone
134
+ configureAudioSession()
135
+
119
136
  let session = AVCaptureSession()
120
137
  session.beginConfiguration()
121
138
 
@@ -178,11 +195,16 @@ class TrustchexCameraView: UIView {
178
195
  let movieOutput = AVCaptureMovieFileOutput()
179
196
  if session.canAddOutput(movieOutput) {
180
197
  session.addOutput(movieOutput)
198
+ // Configure video connection
181
199
  if let connection = movieOutput.connection(with: .video) {
182
200
  if connection.isVideoOrientationSupported {
183
201
  connection.videoOrientation = .portrait
184
202
  }
185
203
  }
204
+ // Configure audio connection for recording
205
+ if let audioConnection = movieOutput.connection(with: .audio) {
206
+ audioConnection.isEnabled = true
207
+ }
186
208
  }
187
209
  self.movieFileOutput = movieOutput
188
210
 
@@ -235,36 +257,85 @@ class TrustchexCameraView: UIView {
235
257
  }
236
258
 
237
259
  // MARK: - Camera Selection
260
+
261
+ /// Selects the best camera for the requested position using the same strategy
262
+ /// as the Android side:
263
+ /// • Front (face detection / verbal consent): pick the device with the widest
264
+ /// field of view by measuring `videoFieldOfView` across all available front
265
+ /// cameras — mirrors Android's focal-length comparison via Camera2.
266
+ /// • Back (document / EID / barcode scanning): wide-angle camera gives the
267
+ /// least barrel distortion and best optical sharpness; ultra-wide is avoided
268
+ /// intentionally because its distortion degrades OCR/MRZ accuracy.
238
269
  private func selectBestCamera(for position: AVCaptureDevice.Position) -> AVCaptureDevice? {
239
270
  if position == .front {
240
- // Prefer the widest front camera available for better face framing
241
- let discovery = AVCaptureDevice.DiscoverySession(
242
- deviceTypes: [.builtInUltraWideCamera, .builtInWideAngleCamera],
243
- mediaType: .video,
244
- position: .front
245
- )
246
- // DiscoverySession lists in the order of deviceTypes, so ultra-wide comes first if available
247
- return discovery.devices.first
248
- ?? AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front)
271
+ return selectWidestFrontCamera()
272
+ }
273
+ return selectBestBackCamera()
274
+ }
275
+
276
+ /// Enumerates every front-facing camera the device exposes and returns the one
277
+ /// with the highest reported field-of-view, matching Android's
278
+ /// `selectWidestFrontCamera()` which picks the shortest focal length.
279
+ private func selectWidestFrontCamera() -> AVCaptureDevice? {
280
+ let discovery = AVCaptureDevice.DiscoverySession(
281
+ deviceTypes: [
282
+ .builtInUltraWideCamera,
283
+ .builtInTrueDepthCamera,
284
+ .builtInWideAngleCamera,
285
+ ],
286
+ mediaType: .video,
287
+ position: .front
288
+ )
289
+
290
+ let devices = discovery.devices
291
+ guard !devices.isEmpty else {
292
+ return AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front)
249
293
  }
250
294
 
251
- // For document scanning, prefer wide angle camera for all models
252
- // This provides consistent behavior across iPhone 15, 15 Pro, and other devices
253
- if let wideAngleCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) {
254
- return wideAngleCamera
295
+ // Single device no comparison needed.
296
+ if devices.count == 1 { return devices.first }
297
+
298
+ // Multiple front cameras: measure the maximum videoFieldOfView each device
299
+ // supports across all its formats and return the highest (widest) one.
300
+ var bestDevice: AVCaptureDevice? = nil
301
+ var widestFoV: Float = 0
302
+
303
+ for device in devices {
304
+ let maxFoV = device.formats
305
+ .map { $0.videoFieldOfView }
306
+ .max() ?? 0
307
+ if maxFoV > widestFoV {
308
+ widestFoV = maxFoV
309
+ bestDevice = device
310
+ }
255
311
  }
256
312
 
257
- // Fallback: Try dual camera
258
- if let dualCamera = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back) {
259
- return dualCamera
313
+ return bestDevice
314
+ ?? AVCaptureDevice.default(.builtInTrueDepthCamera, for: .video, position: .front)
315
+ ?? AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front)
316
+ }
317
+
318
+ /// Returns the best back-facing camera for document scanning scenarios.
319
+ /// Wide-angle is preferred over ultra-wide because it has negligible barrel
320
+ /// distortion — critical for OCR/MRZ accuracy. This matches Android which
321
+ /// resolves to DEFAULT_BACK_CAMERA (the main wide-angle lens).
322
+ private func selectBestBackCamera() -> AVCaptureDevice? {
323
+ // Primary: main wide-angle lens — best for document/EID/barcode scanning.
324
+ if let cam = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) {
325
+ return cam
260
326
  }
261
327
 
262
- // Fallback: Try triple camera
263
- if let tripleCamera = AVCaptureDevice.default(.builtInTripleCamera, for: .video, position: .back) {
264
- return tripleCamera
328
+ // Fallback: dual-camera system (manages wide + telephoto; exposes wide by default).
329
+ if let cam = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back) {
330
+ return cam
265
331
  }
266
332
 
267
- // Final fallback: any back camera
333
+ // Fallback: triple-camera system.
334
+ if let cam = AVCaptureDevice.default(.builtInTripleCamera, for: .video, position: .back) {
335
+ return cam
336
+ }
337
+
338
+ // Final fallback: any available camera.
268
339
  return AVCaptureDevice.default(for: .video)
269
340
  }
270
341
 
@@ -304,6 +375,12 @@ class TrustchexCameraView: UIView {
304
375
  } else if camera.isFocusModeSupported(.autoFocus) {
305
376
  camera.focusMode = .autoFocus
306
377
  }
378
+
379
+ // Use the widest front-camera framing available on the device.
380
+ let widestZoom = camera.minAvailableVideoZoomFactor
381
+ if camera.videoZoomFactor != widestZoom {
382
+ camera.videoZoomFactor = widestZoom
383
+ }
307
384
  } else {
308
385
  // Back camera: Optimized focus for document scanning across all models
309
386
  if camera.isFocusModeSupported(.continuousAutoFocus) {
@@ -476,6 +553,22 @@ class TrustchexCameraView: UIView {
476
553
  guard let session = self.captureSession, session.isRunning else { return }
477
554
  guard let movieOutput = self.movieFileOutput else { return }
478
555
 
556
+ // Ensure recording session is configured for video + microphone capture.
557
+ do {
558
+ let audioSession = AVAudioSession.sharedInstance()
559
+ try audioSession.setCategory(
560
+ .playAndRecord,
561
+ mode: .videoRecording,
562
+ options: [.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP]
563
+ )
564
+ try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
565
+ } catch {
566
+ DispatchQueue.main.async { [weak self] in
567
+ self?.onRecordingError?(["error": "Failed to activate audio session for recording"])
568
+ }
569
+ return
570
+ }
571
+
479
572
  // Check actual AVFoundation recording state
480
573
  if movieOutput.isRecording {
481
574
  movieOutput.stopRecording()
@@ -495,12 +588,26 @@ class TrustchexCameraView: UIView {
495
588
  return
496
589
  }
497
590
 
498
- // Verify video connection exists
499
- guard movieOutput.connection(with: .video) != nil else { return }
591
+ // Verify both video and audio connections exist before recording
592
+ guard movieOutput.connection(with: .video) != nil else {
593
+ DispatchQueue.main.async { [weak self] in
594
+ self?.onRecordingError?(["error": "Video connection not available"])
595
+ }
596
+ return
597
+ }
598
+
599
+ // Audio connection is required for verbal consent recordings.
600
+ guard let audioConnection = movieOutput.connection(with: .audio) else {
601
+ DispatchQueue.main.async { [weak self] in
602
+ self?.onRecordingError?(["error": "Microphone audio connection not available"])
603
+ }
604
+ return
605
+ }
606
+ audioConnection.isEnabled = true
500
607
 
501
608
  // Create temporary file URL
502
609
  let tempDir = NSTemporaryDirectory()
503
- let fileName = "recording_\(Date().timeIntervalSince1970).mp4"
610
+ let fileName = "recording_\(Date().timeIntervalSince1970).mov"
504
611
  let fileURL = URL(fileURLWithPath: tempDir).appendingPathComponent(fileName)
505
612
 
506
613
  // Remove existing file if any
@@ -655,10 +762,18 @@ class TrustchexCameraView: UIView {
655
762
 
656
763
  // Update mirroring on preview layer
657
764
  DispatchQueue.main.async { [weak self] in
658
- if let connection = self?.previewLayer?.connection {
659
- if connection.isVideoMirroringSupported {
660
- connection.isVideoMirrored = (type == "front")
661
- }
765
+ guard let connection = self?.previewLayer?.connection else { return }
766
+ guard connection.isVideoMirroringSupported else { return }
767
+
768
+ // Avoid AVFoundation exception when setting explicit mirroring while
769
+ // automatic mirroring is still enabled on the connection.
770
+ if connection.automaticallyAdjustsVideoMirroring {
771
+ connection.automaticallyAdjustsVideoMirroring = false
772
+ }
773
+
774
+ let shouldMirror = (type == "front")
775
+ if connection.isVideoMirrored != shouldMirror {
776
+ connection.isVideoMirrored = shouldMirror
662
777
  }
663
778
  }
664
779
  }
@@ -749,28 +864,30 @@ extension TrustchexCameraView: AVCaptureVideoDataOutputSampleBufferDelegate {
749
864
  // Compute brightness early for OCR-B enhancement decision
750
865
  let brightness = computeBrightness(from: pixelBuffer, width: portraitWidth, height: portraitHeight)
751
866
 
752
- // For text recognition, use original image directly
753
- // ML Kit works best with native camera frames
754
- let textEnhancedImage = orientedImage
755
-
756
- // Create VisionImage from the image (enhanced or original) for better text recognition
757
- // This ensures MLKit processes the image in the correct orientation
758
- guard let cgImage = self.ciContext.createCGImage(textEnhancedImage, from: textEnhancedImage.extent) else {
867
+ // Face detection / base64 use the full portrait frame.
868
+ guard let cgImage = self.ciContext.createCGImage(orientedImage, from: orientedImage.extent) else {
759
869
  resetProcessingState()
760
870
  return
761
871
  }
762
872
  let visionImage = VisionImage(image: UIImage(cgImage: cgImage))
763
873
  visionImage.orientation = .up // Already oriented correctly
764
-
765
- // Use image for text recognition
766
- // ML Kit text recognition best practices (iOS):
767
- // - Requires minimum 16x16 pixels per character (ideal 16-24px per character)
768
- // - Input image: 1080x1920 (portrait Full HD) provides excellent accuracy at real-time speed
769
- // - Each character at ~30px = 36 characters per line @ 1080px width
770
- // - Use synchronous results(in:) API from captureOutput(_:didOutput:from:)
771
- // - Set AVCaptureVideoDataOutput.alwaysDiscardsLateVideoFrames = true (throttle)
772
- // - Get results then render overlay in single step for optimal performance
773
- let textVisionImage = visionImage
874
+
875
+ // Text recognition: process ONLY the data inside the on-screen scan frame
876
+ // (matching Android). We crop the portrait frame to the scan-frame ROI
877
+ // (top 25% / bottom 12% / 4% side margins), UPSCALE it, and grayscale +
878
+ // contrast-enhance before OCR. This removes front-of-card text and glare
879
+ // noise and lifts a dense MRZ line above ML Kit's ~16px/char floor.
880
+ // `mrzRoi` carries the transform back to full portrait-frame coordinates.
881
+ let mrzRoi = enableTextRecognition
882
+ ? buildScanFrameRoiImage(orientedImage, portraitWidth: portraitWidth, portraitHeight: portraitHeight)
883
+ : nil
884
+ var textVisionImage = visionImage
885
+ if let roi = mrzRoi,
886
+ let roiCg = self.ciContext.createCGImage(roi.image, from: roi.image.extent) {
887
+ let img = VisionImage(image: UIImage(cgImage: roiCg))
888
+ img.orientation = .up
889
+ textVisionImage = img
890
+ }
774
891
 
775
892
  // Generate JPEG base64 only when JS side explicitly needs the image
776
893
  // NOTE: Do NOT auto-generate for face detection - too expensive, causes frame drops
@@ -846,8 +963,16 @@ extension TrustchexCameraView: AVCaptureVideoDataOutputSampleBufferDelegate {
846
963
  let blockY: Int
847
964
  let blockWidth: Int
848
965
  let blockHeight: Int
849
-
850
- if isBufferLandscape {
966
+
967
+ if let roi = mrzRoi {
968
+ // OCR ran on the upscaled, already-portrait scan-frame ROI.
969
+ // Map the box back to full portrait-frame coordinates:
970
+ // divide out the ROI upscale, then add the ROI origin.
971
+ blockX = Int(bb.origin.x / roi.scale) + roi.offsetX
972
+ blockY = Int(bb.origin.y / roi.scale) + roi.offsetY
973
+ blockWidth = Int(bb.width / roi.scale)
974
+ blockHeight = Int(bb.height / roi.scale)
975
+ } else if isBufferLandscape {
851
976
  // Rotate from landscape (1920x1080) to portrait (1080x1920)
852
977
  // When rotating 90° clockwise (.right):
853
978
  // new_x = old_y
@@ -926,6 +1051,69 @@ extension TrustchexCameraView: AVCaptureVideoDataOutputSampleBufferDelegate {
926
1051
  }
927
1052
  }
928
1053
 
1054
+ /// A scan-frame ROI prepared for OCR, with the transform back to full-frame coords.
1055
+ private struct ScanFrameRoi {
1056
+ let image: CIImage
1057
+ let offsetX: Int
1058
+ let offsetY: Int
1059
+ let scale: CGFloat
1060
+ }
1061
+
1062
+ /// Crops the portrait frame to the on-screen scan frame (top 25% / bottom 12%
1063
+ /// / 4% side margins — the same rectangle the UI guide draws), UPSCALES it so a
1064
+ /// dense MRZ line clears ML Kit's ~16px/char floor, and grayscale + contrast
1065
+ /// enhances it (the MRZ is dark OCR-B on a light background, so this makes
1066
+ /// glyphs pop and suppresses the "<" filler being misread as a letter under
1067
+ /// glare). Returns nil on failure so the caller falls back to full-frame OCR.
1068
+ /// Mirrors the Android `buildScanFrameRoiImage`.
1069
+ private func buildScanFrameRoiImage(_ portrait: CIImage, portraitWidth: Int, portraitHeight: Int) -> ScanFrameRoi? {
1070
+ let w = portrait.extent.width
1071
+ let h = portrait.extent.height
1072
+ if w < 8 || h < 8 { return nil }
1073
+
1074
+ // Scan-frame rectangle. CIImage origin is BOTTOM-left, so "top 25%" maps to
1075
+ // a y measured from the bottom: cropY = h*0.12 (bottom margin), height =
1076
+ // h*(1 - 0.25 - 0.12).
1077
+ let leftPct: CGFloat = 0.04
1078
+ let rightPct: CGFloat = 0.04
1079
+ let topPct: CGFloat = 0.25
1080
+ let bottomPct: CGFloat = 0.12
1081
+ let cropX = (w * leftPct).rounded()
1082
+ let cropW = (w * (1 - leftPct - rightPct)).rounded()
1083
+ let cropY = (h * bottomPct).rounded()
1084
+ let cropH = (h * (1 - topPct - bottomPct)).rounded()
1085
+ if cropW < 8 || cropH < 8 { return nil }
1086
+
1087
+ let cropRect = CGRect(x: portrait.extent.origin.x + cropX,
1088
+ y: portrait.extent.origin.y + cropY,
1089
+ width: cropW, height: cropH)
1090
+ var img = portrait.cropped(to: cropRect)
1091
+ // Move the crop origin to (0,0) so downstream extents are simple.
1092
+ img = img.transformed(by: CGAffineTransform(translationX: -cropRect.origin.x, y: -cropRect.origin.y))
1093
+
1094
+ // Upscale (≤2.5×, long edge capped at 2200) for more pixels per character.
1095
+ let longEdge = max(cropW, cropH)
1096
+ let rawScale = longEdge > 0 ? (2200.0 / longEdge) : 1.0
1097
+ let scale = min(max(rawScale, 1.0), 2.5)
1098
+ if scale != 1.0 {
1099
+ img = img.transformed(by: CGAffineTransform(scaleX: scale, y: scale))
1100
+ }
1101
+
1102
+ // Grayscale (saturation 0) + contrast boost.
1103
+ img = img.applyingFilter("CIColorControls", parameters: [
1104
+ kCIInputSaturationKey: 0.0,
1105
+ kCIInputContrastKey: 1.6
1106
+ ])
1107
+
1108
+ // The crop is taken from the TOP of the portrait frame (UIKit coords), but
1109
+ // CIImage y is bottom-up. The block coords ML Kit returns are top-left in
1110
+ // the OCR image, so the offset back to the portrait frame is the UIKit top
1111
+ // edge (= portraitHeight * topPct) and left edge.
1112
+ let offsetX = Int(cropX)
1113
+ let offsetY = Int((CGFloat(portraitHeight) * topPct).rounded())
1114
+ return ScanFrameRoi(image: img, offsetX: offsetX, offsetY: offsetY, scale: scale)
1115
+ }
1116
+
929
1117
  private func computeBrightness(from pixelBuffer: CVPixelBuffer, width: Int, height: Int) -> Double {
930
1118
  guard let yPlane = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0) else { return 128.0 }
931
1119
  let pixelWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0)
@@ -1047,6 +1235,16 @@ extension TrustchexCameraView: AVCaptureFileOutputRecordingDelegate {
1047
1235
 
1048
1236
  // Fire callback
1049
1237
  if recordingSucceeded {
1238
+ let asset = AVURLAsset(url: outputFileURL)
1239
+ let hasAudioTrack = !asset.tracks(withMediaType: .audio).isEmpty
1240
+
1241
+ if !hasAudioTrack {
1242
+ DispatchQueue.main.async { [weak self] in
1243
+ self?.onRecordingError?(["error": "Recorded video has no audio track"])
1244
+ }
1245
+ return
1246
+ }
1247
+
1050
1248
  DispatchQueue.main.async { [weak self] in
1051
1249
  self?.onRecordingFinished?(["path": outputFileURL.path])
1052
1250
  }
@@ -0,0 +1,22 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ @interface RCT_EXTERN_MODULE(PermissionModule, NSObject)
4
+
5
+ RCT_EXTERN_METHOD(requestCameraPermission:(RCTPromiseResolveBlock)resolve
6
+ withRejecter:(RCTPromiseRejectBlock)reject)
7
+
8
+ RCT_EXTERN_METHOD(requestMicrophonePermission:(RCTPromiseResolveBlock)resolve
9
+ withRejecter:(RCTPromiseRejectBlock)reject)
10
+
11
+ RCT_EXTERN_METHOD(checkCameraPermission:(RCTPromiseResolveBlock)resolve
12
+ withRejecter:(RCTPromiseRejectBlock)reject)
13
+
14
+ RCT_EXTERN_METHOD(checkMicrophonePermission:(RCTPromiseResolveBlock)resolve
15
+ withRejecter:(RCTPromiseRejectBlock)reject)
16
+
17
+ + (BOOL)requiresMainQueueSetup
18
+ {
19
+ return YES;
20
+ }
21
+
22
+ @end
@@ -0,0 +1,67 @@
1
+ import React
2
+ import AVFoundation
3
+
4
+ @objc(PermissionModule)
5
+ class PermissionModule: NSObject {
6
+
7
+ @objc(requestCameraPermission:withRejecter:)
8
+ func requestCameraPermission(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
9
+ DispatchQueue.main.async {
10
+ let status = AVCaptureDevice.authorizationStatus(for: .video)
11
+
12
+ switch status {
13
+ case .authorized:
14
+ resolve(true)
15
+ case .denied, .restricted:
16
+ resolve(false)
17
+ case .notDetermined:
18
+ AVCaptureDevice.requestAccess(for: .video) { granted in
19
+ resolve(granted)
20
+ }
21
+ @unknown default:
22
+ resolve(false)
23
+ }
24
+ }
25
+ }
26
+
27
+ @objc(requestMicrophonePermission:withRejecter:)
28
+ func requestMicrophonePermission(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
29
+ DispatchQueue.main.async {
30
+ let status = AVAudioSession.sharedInstance().recordPermission
31
+
32
+ switch status {
33
+ case .granted:
34
+ resolve(true)
35
+ case .denied:
36
+ resolve(false)
37
+ case .undetermined:
38
+ AVAudioSession.sharedInstance().requestRecordPermission { granted in
39
+ resolve(granted)
40
+ }
41
+ @unknown default:
42
+ resolve(false)
43
+ }
44
+ }
45
+ }
46
+
47
+ @objc(checkCameraPermission:withRejecter:)
48
+ func checkCameraPermission(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
49
+ DispatchQueue.main.async {
50
+ let status = AVCaptureDevice.authorizationStatus(for: .video)
51
+ resolve(status == .authorized)
52
+ }
53
+ }
54
+
55
+ @objc(checkMicrophonePermission:withRejecter:)
56
+ func checkMicrophonePermission(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
57
+ DispatchQueue.main.async {
58
+ let status = AVAudioSession.sharedInstance().recordPermission
59
+ resolve(status == .granted)
60
+ }
61
+ }
62
+
63
+ @objc
64
+ static func requiresMainQueueSetup() -> Bool {
65
+ return true
66
+ }
67
+ }