@imgly/camera-react-native 1.76.1 → 1.78.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.
@@ -141,6 +141,6 @@ repositories {
141
141
  }
142
142
 
143
143
  dependencies {
144
- api "ly.img:camera:1.76.1"
144
+ api "ly.img:camera:1.78.0"
145
145
  implementation "com.facebook.react:react-native"
146
146
  }
@@ -11,14 +11,14 @@ import com.facebook.react.bridge.ReadableMap
11
11
  import com.facebook.react.bridge.WritableArray
12
12
  import com.facebook.react.bridge.WritableMap
13
13
  import ly.img.camera.CameraActivity
14
- import ly.img.camera.core.CaptureVideo
15
- import ly.img.camera.core.CaptureVideo.Companion.INTENT_KEY_CAMERA_INPUT
14
+ import ly.img.camera.core.CaptureMedia
15
+ import ly.img.camera.core.CaptureMedia.Companion.INTENT_KEY_CAMERA_INPUT
16
16
  import ly.img.camera.core.EngineConfiguration
17
17
  import ly.img.camera.reactnative.module.model.CameraResult
18
18
  import ly.img.camera.reactnative.module.model.CameraSettings
19
19
 
20
- /** A closure to specify a [CaptureVideo.Input] the camera session based on given *metadata*. */
21
- typealias CameraInputClosure = (metadata: Map<String, Any?>?) -> CaptureVideo.Input
20
+ /** A closure to specify a [CaptureMedia.Input] the camera session based on given *metadata*. */
21
+ typealias CameraInputClosure = (metadata: Map<String, Any?>?) -> CaptureMedia.Input
22
22
 
23
23
  /** A closure to modify the [CameraResult] before being send to the JS layer. */
24
24
  typealias CameraResultClosure = (result: CameraResult?) -> CameraResult?
@@ -29,7 +29,7 @@ class IMGLYCameraModule(
29
29
  ) : IMGLYCameraModuleSpec(reactContext),
30
30
  ActivityEventListener {
31
31
  companion object {
32
- /** A closure to specify a [CaptureVideo.Input] based on given *metadata*. */
32
+ /** A closure to specify a [CaptureMedia.Input] based on given *metadata*. */
33
33
  var configurationClosure: CameraInputClosure? = null
34
34
 
35
35
  /** A closure to modify the [CameraResult] before being send to the JS layer. */
@@ -128,9 +128,11 @@ class IMGLYCameraModule(
128
128
  this.completion = completion
129
129
 
130
130
  val engineConfiguration =
131
- EngineConfiguration(license = settings.license ?: "", userId = settings.userId)
132
- val input: CaptureVideo.Input =
133
- configurationClosure?.invoke(metadata) ?: CaptureVideo.Input(engineConfiguration)
131
+ EngineConfiguration(license = settings.license ?: "", userId = settings.userId, host = "react-native")
132
+ val cameraConfiguration =
133
+ settings.configuration?.toNative() ?: ly.img.camera.core.CameraConfiguration()
134
+ val input: CaptureMedia.Input =
135
+ configurationClosure?.invoke(metadata) ?: CaptureMedia.Input(engineConfiguration, cameraConfiguration)
134
136
  val intent = Intent(activity, CameraActivity::class.java).apply {
135
137
  putExtra(INTENT_KEY_CAMERA_INPUT, input)
136
138
  }
@@ -149,9 +151,9 @@ class IMGLYCameraModule(
149
151
  ) {
150
152
  if (requestCode == this.requestCode) {
151
153
  if (resultCode == Activity.RESULT_OK) {
152
- val result = intent?.getParcelableExtra<ly.img.camera.core.CameraResult>(CaptureVideo.INTENT_KEY_CAMERA_RESULT)
154
+ val result = intent?.getParcelableExtra<ly.img.camera.core.CameraResult>(CaptureMedia.INTENT_KEY_CAMERA_RESULT)
153
155
  when (result) {
154
- is ly.img.camera.core.CameraResult.Record -> {
156
+ is ly.img.camera.core.CameraResult.Captures -> {
155
157
  var modifiedResult: CameraResult? = CameraResult(result)
156
158
  val closure = resultClosure
157
159
  if (closure != null) {
@@ -1,57 +1,155 @@
1
1
  package ly.img.camera.reactnative.module.model
2
2
 
3
+ import android.net.Uri
3
4
  import android.os.Parcel
4
5
  import android.os.Parcelable
5
6
  import ly.img.camera.reactnative.module.model.CameraResult.CREATOR.readMap
6
7
  import ly.img.camera.reactnative.module.model.CameraResult.CREATOR.writeMap
8
+ import kotlin.time.Duration
9
+ import kotlin.time.Duration.Companion.milliseconds
10
+ import kotlin.time.DurationUnit
11
+ import ly.img.camera.core.CameraResult as CoreCameraResult
12
+ import ly.img.camera.core.Capture as CoreCapture
7
13
 
8
14
  /**
9
- * The result type for a camera recording session in standard mode.
15
+ * A captured still photo.
10
16
  *
11
- * @property recordings The recordings of the camera session.
17
+ * @property uri The location of the photo file in temporary storage.
18
+ * @property duration The duration stamped on the photo.
12
19
  */
13
- data class CameraRecording(
14
- val recordings: List<Recording>?,
20
+ data class Photo(
21
+ val uri: Uri,
22
+ val duration: Duration,
15
23
  ) : Parcelable {
16
- /**
17
- * Creates a new instance from a given [Parcel].
18
- * @param parcel The [Parcel].
19
- */
20
24
  constructor(parcel: Parcel) : this(
21
- parcel.createTypedArrayList(Recording),
25
+ uri = parcel.readParcelable(Uri::class.java.classLoader)!!,
26
+ duration = parcel.readLong().milliseconds,
22
27
  )
23
- constructor(record: ly.img.camera.core.CameraResult.Record) : this(
24
- record.recordings.map { Recording(it) },
28
+ constructor(photo: CoreCapture.Photo) : this(
29
+ uri = photo.uri,
30
+ duration = photo.clipDuration,
25
31
  )
26
32
 
27
33
  override fun writeToParcel(
28
34
  parcel: Parcel,
29
35
  flags: Int,
30
36
  ) {
31
- parcel.writeTypedList(recordings)
37
+ parcel.writeParcelable(uri, flags)
38
+ parcel.writeLong(duration.inWholeMilliseconds)
32
39
  }
33
40
 
34
41
  override fun describeContents(): Int = 0
35
42
 
36
- companion object CREATOR : Parcelable.Creator<CameraResult> {
37
- override fun createFromParcel(parcel: Parcel): CameraResult = CameraResult(parcel)
43
+ companion object CREATOR : Parcelable.Creator<Photo> {
44
+ private const val PHOTO_FRAME_WIDTH = 1080f
45
+ private const val PHOTO_FRAME_HEIGHT = 1920f
38
46
 
39
- override fun newArray(size: Int): Array<CameraResult?> = arrayOfNulls(size)
47
+ override fun createFromParcel(parcel: Parcel): Photo = Photo(parcel)
48
+
49
+ override fun newArray(size: Int): Array<Photo?> = arrayOfNulls(size)
50
+ }
51
+
52
+ fun toMap(): Map<String, Any?> {
53
+ // Android has no dual-camera mode — emit a single-element `images` list with the rect
54
+ // covering the full 1080×1920 preview frame so the wire format matches iOS's
55
+ // `Photo { images: [PhotoImage], duration }` contract.
56
+ val image = mapOf<String, Any?>(
57
+ "uri" to uri.toString(),
58
+ "rect" to mapOf<String, Any?>(
59
+ "x" to 0f,
60
+ "y" to 0f,
61
+ "width" to PHOTO_FRAME_WIDTH,
62
+ "height" to PHOTO_FRAME_HEIGHT,
63
+ ),
64
+ )
65
+ return mapOf(
66
+ "images" to listOf(image),
67
+ "duration" to duration.toDouble(DurationUnit.MILLISECONDS),
68
+ )
69
+ }
70
+ }
71
+
72
+ /**
73
+ * A single capture from the camera. Either a still [photo] or a video [video] recording.
74
+ */
75
+ data class Capture(
76
+ val photo: Photo?,
77
+ val video: Recording?,
78
+ ) : Parcelable {
79
+ constructor(parcel: Parcel) : this(
80
+ photo = parcel.readParcelable(Photo::class.java.classLoader),
81
+ video = parcel.readParcelable(Recording::class.java.classLoader),
82
+ )
83
+ constructor(capture: CoreCapture) : this(
84
+ photo = (capture as? CoreCapture.Photo)?.let { Photo(it) },
85
+ video = (capture as? CoreCapture.Video)?.recording?.let { Recording(it) },
86
+ )
87
+
88
+ override fun writeToParcel(
89
+ parcel: Parcel,
90
+ flags: Int,
91
+ ) {
92
+ parcel.writeParcelable(photo, flags)
93
+ parcel.writeParcelable(video, flags)
94
+ }
95
+
96
+ override fun describeContents(): Int = 0
97
+
98
+ companion object CREATOR : Parcelable.Creator<Capture> {
99
+ override fun createFromParcel(parcel: Parcel): Capture = Capture(parcel)
100
+
101
+ override fun newArray(size: Int): Array<Capture?> = arrayOfNulls(size)
102
+ }
103
+
104
+ fun toMap(): Map<String, Any?> = buildMap<String, Any?> {
105
+ photo?.let { put("photo", it.toMap()) }
106
+ video?.let { put("video", it.toMap()) }
107
+ }
108
+ }
109
+
110
+ /**
111
+ * The result type for a photo, video, or mixed camera capture session.
112
+ *
113
+ * @property captures The captures of the camera session.
114
+ */
115
+ data class CameraCapture(
116
+ val captures: List<Capture>?,
117
+ ) : Parcelable {
118
+ constructor(parcel: Parcel) : this(
119
+ captures = parcel.createTypedArrayList(Capture),
120
+ )
121
+ constructor(captures: CoreCameraResult.Captures) : this(
122
+ captures = captures.captures.map { Capture(it) },
123
+ )
124
+
125
+ override fun writeToParcel(
126
+ parcel: Parcel,
127
+ flags: Int,
128
+ ) {
129
+ parcel.writeTypedList(captures)
130
+ }
131
+
132
+ override fun describeContents(): Int = 0
133
+
134
+ companion object CREATOR : Parcelable.Creator<CameraCapture> {
135
+ override fun createFromParcel(parcel: Parcel): CameraCapture = CameraCapture(parcel)
136
+
137
+ override fun newArray(size: Int): Array<CameraCapture?> = arrayOfNulls(size)
40
138
  }
41
139
 
42
140
  fun toMap(): Map<String, Any?> = mapOf(
43
- "recordings" to recordings?.map { it.toMap() },
141
+ "captures" to captures?.map { it.toMap() },
44
142
  )
45
143
  }
46
144
 
47
145
  /**
48
146
  * A class representing the result of a camera session.
49
147
  *
50
- * @property recording The result for the camera session in standard mode.
148
+ * @property capture The result for a photo, video, or mixed capture session.
51
149
  * @property metadata The associated metadata.
52
150
  */
53
151
  data class CameraResult(
54
- val recording: CameraRecording?,
152
+ val capture: CameraCapture?,
55
153
  val metadata: Map<String, Any> = emptyMap(),
56
154
  ) : Parcelable {
57
155
  /**
@@ -59,19 +157,19 @@ data class CameraResult(
59
157
  * @param parcel The [Parcel].
60
158
  */
61
159
  constructor(parcel: Parcel) : this(
62
- CameraRecording(parcel),
63
- readMap(parcel),
160
+ capture = parcel.readParcelable(CameraCapture::class.java.classLoader),
161
+ metadata = readMap(parcel),
64
162
  )
65
- constructor(record: ly.img.camera.core.CameraResult.Record) : this(
66
- CameraRecording(record),
67
- emptyMap(),
163
+ constructor(captures: CoreCameraResult.Captures) : this(
164
+ capture = CameraCapture(captures),
165
+ metadata = emptyMap(),
68
166
  )
69
167
 
70
168
  override fun writeToParcel(
71
169
  parcel: Parcel,
72
170
  flags: Int,
73
171
  ) {
74
- recording?.writeToParcel(parcel, flags)
172
+ parcel.writeParcelable(capture, flags)
75
173
  writeMap(parcel, metadata, flags)
76
174
  }
77
175
 
@@ -110,7 +208,7 @@ data class CameraResult(
110
208
  }
111
209
 
112
210
  fun toMap(): Map<String, Any?> = mapOf(
113
- "recording" to recording?.toMap(),
211
+ "capture" to capture?.toMap(),
114
212
  "metadata" to metadata,
115
213
  )
116
214
  }
@@ -2,24 +2,31 @@ package ly.img.camera.reactnative.module.model
2
2
 
3
3
  import android.os.Parcel
4
4
  import android.os.Parcelable
5
+ import ly.img.camera.core.CaptureCount
6
+ import ly.img.camera.core.CaptureType
7
+ import kotlin.time.Duration.Companion.seconds
8
+ import ly.img.camera.core.CameraConfiguration as NativeCameraConfiguration
5
9
 
6
10
  /**
7
11
  * A class containing all necessary information to configure the camera.
8
12
  *
9
13
  * @property license The license key. Pass `null` to run the SDK in evaluation mode with a watermark.
10
14
  * @property userId The id of the current user.
15
+ * @property configuration Optional [CameraConfiguration] controlling how the camera captures media.
11
16
  */
12
17
  data class CameraSettings(
13
18
  val license: String? = null,
14
19
  val userId: String?,
20
+ val configuration: CameraConfiguration? = null,
15
21
  ) : Parcelable {
16
22
  /**
17
23
  * Creates a new instance from a given [Parcel].
18
24
  * @param parcel The [Parcel].
19
25
  */
20
26
  constructor(parcel: Parcel) : this(
21
- parcel.readString(),
22
- parcel.readString(),
27
+ license = parcel.readString(),
28
+ userId = parcel.readString(),
29
+ configuration = parcel.readParcelable(CameraConfiguration::class.java.classLoader),
23
30
  )
24
31
 
25
32
  override fun writeToParcel(
@@ -28,6 +35,7 @@ data class CameraSettings(
28
35
  ) {
29
36
  parcel.writeString(license)
30
37
  parcel.writeString(userId)
38
+ parcel.writeParcelable(configuration, flags)
31
39
  }
32
40
 
33
41
  override fun describeContents(): Int = 0
@@ -40,7 +48,70 @@ data class CameraSettings(
40
48
  fun createFromMap(map: Map<String, Any?>): CameraSettings {
41
49
  val license = map["license"] as? String
42
50
  val userId = map["userId"] as? String
43
- return CameraSettings(license, userId)
51
+ val configuration = (map["configuration"] as? Map<*, *>)?.let {
52
+ CameraConfiguration.createFromMap(it)
53
+ }
54
+ return CameraSettings(license, userId, configuration)
44
55
  }
45
56
  }
46
57
  }
58
+
59
+ /**
60
+ * Capture configuration forwarded from the TS layer. String fields stay loose at the bridge
61
+ * boundary and are mapped to the native enums when building [NativeCameraConfiguration].
62
+ *
63
+ * @property captureType One of `"photo"`, `"video"`, `"mixed"` or `null` for the native default.
64
+ * @property captureCount One of `"single"`, `"multi"` or `null` for the native default.
65
+ * @property photoClipDuration Duration in seconds stamped on each photo capture.
66
+ */
67
+ data class CameraConfiguration(
68
+ val captureType: String? = null,
69
+ val captureCount: String? = null,
70
+ val photoClipDuration: Double? = null,
71
+ ) : Parcelable {
72
+ constructor(parcel: Parcel) : this(
73
+ captureType = parcel.readString(),
74
+ captureCount = parcel.readString(),
75
+ photoClipDuration = parcel.readValue(Double::class.java.classLoader) as? Double,
76
+ )
77
+
78
+ override fun writeToParcel(
79
+ parcel: Parcel,
80
+ flags: Int,
81
+ ) {
82
+ parcel.writeString(captureType)
83
+ parcel.writeString(captureCount)
84
+ parcel.writeValue(photoClipDuration)
85
+ }
86
+
87
+ override fun describeContents(): Int = 0
88
+
89
+ /**
90
+ * Maps the wrapper configuration to a native [NativeCameraConfiguration]. Unset fields fall
91
+ * back to native defaults.
92
+ */
93
+ fun toNative(): NativeCameraConfiguration {
94
+ val defaults = NativeCameraConfiguration()
95
+ return NativeCameraConfiguration(
96
+ captureType = captureType?.let { type ->
97
+ CaptureType.entries.firstOrNull { it.name.equals(type, ignoreCase = true) }
98
+ } ?: defaults.captureType,
99
+ captureCount = captureCount?.let { count ->
100
+ CaptureCount.entries.firstOrNull { it.name.equals(count, ignoreCase = true) }
101
+ } ?: defaults.captureCount,
102
+ photoClipDuration = photoClipDuration?.seconds ?: defaults.photoClipDuration,
103
+ )
104
+ }
105
+
106
+ companion object CREATOR : Parcelable.Creator<CameraConfiguration> {
107
+ override fun createFromParcel(parcel: Parcel): CameraConfiguration = CameraConfiguration(parcel)
108
+
109
+ override fun newArray(size: Int): Array<CameraConfiguration?> = arrayOfNulls(size)
110
+
111
+ fun createFromMap(map: Map<*, *>) = CameraConfiguration(
112
+ captureType = map["captureType"] as? String,
113
+ captureCount = map["captureCount"] as? String,
114
+ photoClipDuration = (map["photoClipDuration"] as? Number)?.toDouble(),
115
+ )
116
+ }
117
+ }
@@ -6,7 +6,7 @@
6
6
  RCT_EXPORT_MODULE(IMGLYCamera)
7
7
 
8
8
  #ifdef RCT_NEW_ARCH_ENABLED
9
- - (void)openCamera:(JS::NativeIMGLYCamera::CameraSettings&)settings
9
+ - (void)openCamera:(JS::NativeIMGLYCamera::SpecCameraSettings&)settings
10
10
  video:(NSString*)video
11
11
  metadata:(NSDictionary*)metadata
12
12
  resolve:(RCTPromiseResolveBlock)resolve
@@ -18,6 +18,20 @@ RCT_EXPORT_MODULE(IMGLYCamera)
18
18
  if (settings.userId() != nil) {
19
19
  convertedSettings[@"userId"] = settings.userId();
20
20
  }
21
+ auto configuration = settings.configuration();
22
+ if (configuration.has_value()) {
23
+ NSMutableDictionary* convertedConfiguration = [NSMutableDictionary dictionary];
24
+ if (configuration->captureType() != nil) {
25
+ convertedConfiguration[@"captureType"] = configuration->captureType();
26
+ }
27
+ if (configuration->captureCount() != nil) {
28
+ convertedConfiguration[@"captureCount"] = configuration->captureCount();
29
+ }
30
+ if (configuration->photoClipDuration().has_value()) {
31
+ convertedConfiguration[@"photoClipDuration"] = @(*configuration->photoClipDuration());
32
+ }
33
+ convertedSettings[@"configuration"] = convertedConfiguration;
34
+ }
21
35
  [self open:convertedSettings video:video metadata:metadata resolve:resolve reject:reject];
22
36
  }
23
37
 
@@ -50,7 +50,36 @@ extension CameraBuilder {
50
50
  /// - Parameter settings: The `CameraSettings`.
51
51
  /// - Returns: The derived `EngineSettings`.
52
52
  private static func engineSettings(for settings: CameraSettings) -> EngineSettings {
53
- EngineSettings(license: settings.license, userID: settings.userId)
53
+ // Identify this engine as React-Native-hosted so v4 licenses scoped to
54
+ // `host: react-native` validate. Hardcoded (not customer-configurable).
55
+ EngineSettings(license: settings.license, userID: settings.userId, host: "react-native")
56
+ }
57
+
58
+ /// Builds a native `IMGLYCamera.CameraConfiguration` from the bridged config dict.
59
+ /// Missing or unknown fields fall back to the native defaults.
60
+ private static func nativeConfiguration(
61
+ from configuration: CameraConfiguration?,
62
+ ) -> IMGLYCamera.CameraConfiguration {
63
+ IMGLYCamera.CameraConfiguration(
64
+ captureType: nativeCaptureType(configuration?.captureType),
65
+ captureCount: nativeCaptureCount(configuration?.captureCount),
66
+ photoClipDuration: configuration?.photoClipDuration ?? 5,
67
+ )
68
+ }
69
+
70
+ private static func nativeCaptureType(_ raw: String?) -> IMGLYCamera.CaptureType {
71
+ switch raw {
72
+ case "photo": .photo
73
+ case "mixed": .mixed
74
+ default: .video
75
+ }
76
+ }
77
+
78
+ private static func nativeCaptureCount(_ raw: String?) -> IMGLYCamera.CaptureCount {
79
+ switch raw {
80
+ case "single": .single
81
+ default: .multi
82
+ }
54
83
  }
55
84
 
56
85
  /// A modal camera implementation.
@@ -58,21 +87,23 @@ extension CameraBuilder {
58
87
  private let settings: CameraSettings
59
88
  private let result: CameraBuilderResult
60
89
  private let url: URL?
90
+ private let config: IMGLYCamera.CameraConfiguration
61
91
  private let onDismiss: (Result<IMGLYCamera.CameraResult, CameraError>) -> Void
62
92
 
63
93
  public init(settings: CameraSettings, result: @escaping CameraBuilderResult, url: URL? = nil) {
64
94
  self.settings = settings
65
95
  self.result = result
66
96
  self.url = url
97
+ config = CameraBuilder.nativeConfiguration(from: settings.configuration)
67
98
 
68
99
  onDismiss = { cameraResult in
69
100
  switch cameraResult {
70
101
  case let .success(.reaction(video: recording, reaction: reaction)):
71
102
  let reaction = CameraReaction(video: recording, recordings: reaction)
72
103
  result(.success(CameraResult(reaction: reaction)))
73
- case let .success(.recording(recording)):
74
- let recording = CameraRecording(recordings: recording)
75
- result(.success(CameraResult(recording: recording)))
104
+ case let .success(.capture(captures)):
105
+ let capture = CameraCapture(captures: captures)
106
+ result(.success(CameraResult(capture: capture)))
76
107
  case let .failure(error):
77
108
  result(.failure(error))
78
109
  }
@@ -83,11 +114,12 @@ extension CameraBuilder {
83
114
  if let url {
84
115
  Camera(
85
116
  engineSettings(for: settings),
117
+ config: config,
86
118
  mode: .reaction(.horizontal, video: url, positionsSwapped: false),
87
119
  onDismiss: onDismiss,
88
120
  )
89
121
  } else {
90
- Camera(engineSettings(for: settings), onDismiss: onDismiss)
122
+ Camera(engineSettings(for: settings), config: config, onDismiss: onDismiss)
91
123
  }
92
124
  }
93
125
  }
@@ -1,33 +1,5 @@
1
1
  import IMGLYCamera
2
2
 
3
- /// The result type for a camera recording session in standard or dual camera mode.
4
- @objc @objcMembers public class CameraRecording: NSObject {
5
- /// The recorded videos.
6
- public let recordings: [Recording]
7
-
8
- /// Creates a new `CameraResult` from the `IMGLYCamera` result types.
9
- /// - Parameters:
10
- /// - recordings: The recorded `IMGLYCamera.Recording` videos.
11
- public convenience init(recordings: [IMGLYCamera.Recording]) {
12
- self.init(
13
- recordings: recordings.compactMap { Recording(recording: $0) },
14
- )
15
- }
16
-
17
- /// Creates a new `CameraResult`.
18
- /// - Parameters:
19
- /// - recordings: The recorded videos.
20
- public init(recordings: [Recording]) {
21
- self.recordings = recordings
22
- }
23
-
24
- func toDictionary() -> [String: Any] {
25
- [
26
- "recordings": recordings.compactMap { $0.toDictionary() },
27
- ]
28
- }
29
- }
30
-
31
3
  /// The result type for a camera reaction session.
32
4
  @objc @objcMembers public class CameraReaction: NSObject {
33
5
  /// The video that was reacted to.
@@ -64,50 +36,186 @@ import IMGLYCamera
64
36
  }
65
37
  }
66
38
 
39
+ /// The result type for a camera capture session producing photos, videos, or both.
40
+ @objc @objcMembers public class CameraCapture: NSObject {
41
+ /// The individual captures from the session.
42
+ public let captures: [Capture]
43
+
44
+ /// Creates a new `CameraCapture` from the `IMGLYCamera` result types.
45
+ /// - Parameter captures: The `IMGLYCamera.Capture` items.
46
+ public convenience init(captures: [IMGLYCamera.Capture]) {
47
+ self.init(captures: captures.map { Capture(capture: $0) })
48
+ }
49
+
50
+ /// Creates a new `CameraCapture` instance.
51
+ /// - Parameter captures: The captures from the session.
52
+ public init(captures: [Capture]) {
53
+ self.captures = captures
54
+ }
55
+
56
+ func toDictionary() -> [String: Any] {
57
+ [
58
+ "captures": captures.map { $0.toDictionary() },
59
+ ]
60
+ }
61
+ }
62
+
63
+ /// A single capture from the camera. Either a still photo or a video recording.
64
+ @objc @objcMembers public class Capture: NSObject {
65
+ /// The captured still photo, when this is a photo capture.
66
+ public let photo: Photo?
67
+
68
+ /// The video recording, when this is a video capture.
69
+ public let video: Recording?
70
+
71
+ /// Creates a new photo `Capture`.
72
+ public convenience init(photo: Photo) {
73
+ self.init(photo: photo, video: nil)
74
+ }
75
+
76
+ /// Creates a new video `Capture`.
77
+ public convenience init(video: Recording) {
78
+ self.init(photo: nil, video: video)
79
+ }
80
+
81
+ /// Creates a new `Capture` from the `IMGLYCamera.Capture` enum.
82
+ public convenience init(capture: IMGLYCamera.Capture) {
83
+ switch capture {
84
+ case let .photo(photo):
85
+ self.init(photo: Photo(photo: photo))
86
+ case let .video(recording):
87
+ self.init(video: Recording(recording: recording))
88
+ }
89
+ }
90
+
91
+ private init(photo: Photo?, video: Recording?) {
92
+ self.photo = photo
93
+ self.video = video
94
+ }
95
+
96
+ func toDictionary() -> [String: Any] {
97
+ var dictionary: [String: Any] = [:]
98
+ if let photo {
99
+ dictionary["photo"] = photo.toDictionary()
100
+ }
101
+ if let video {
102
+ dictionary["video"] = video.toDictionary()
103
+ }
104
+ return dictionary
105
+ }
106
+ }
107
+
108
+ /// A captured still photo. Contains one image in standard mode or two stacked images in dual camera mode.
109
+ @objc @objcMembers public class Photo: NSObject {
110
+ /// The individual image(s) of the photo capture.
111
+ public let images: [PhotoImage]
112
+
113
+ /// The duration in seconds stamped on the photo.
114
+ public let duration: Double
115
+
116
+ /// Creates a new `Photo`.
117
+ /// - Parameters:
118
+ /// - images: The individual image(s) of the photo capture.
119
+ /// - duration: The duration in seconds stamped on the photo.
120
+ public init(images: [PhotoImage], duration: Double) {
121
+ self.images = images
122
+ self.duration = duration
123
+ }
124
+
125
+ /// Creates a new `Photo` from an `IMGLYCamera.Photo`.
126
+ public convenience init(photo: IMGLYCamera.Photo) {
127
+ self.init(
128
+ images: photo.images.map { PhotoImage(image: $0) },
129
+ duration: photo.duration.seconds,
130
+ )
131
+ }
132
+
133
+ func toDictionary() -> [String: Any] {
134
+ [
135
+ "images": images.map { $0.toDictionary() },
136
+ "duration": duration,
137
+ ]
138
+ }
139
+ }
140
+
141
+ /// A single image inside a photo capture.
142
+ @objc @objcMembers public class PhotoImage: NSObject {
143
+ /// The `URL` to the source of the image.
144
+ public let url: URL
145
+
146
+ /// The position and size of the image inside the dual-camera layout.
147
+ public let rect: CGRect
148
+
149
+ /// Creates a new `PhotoImage`.
150
+ public init(url: URL, rect: CGRect) {
151
+ self.url = url
152
+ self.rect = rect
153
+ }
154
+
155
+ /// Creates a new `PhotoImage` from an `IMGLYCamera.Photo.Image`.
156
+ public convenience init(image: IMGLYCamera.Photo.Image) {
157
+ self.init(url: image.url, rect: image.rect)
158
+ }
159
+
160
+ func toDictionary() -> [String: Any] {
161
+ [
162
+ "uri": url.absoluteString,
163
+ "rect": [
164
+ "x": rect.origin.x,
165
+ "y": rect.origin.y,
166
+ "width": Double(rect.width),
167
+ "height": Double(rect.height),
168
+ ],
169
+ ]
170
+ }
171
+ }
172
+
67
173
  /// The result of a camera session.
68
174
  @objc @objcMembers public class CameraResult: NSObject {
69
- /// The video that was reacted to.
70
- public let recording: CameraRecording?
71
-
72
- /// The recorded videos.
175
+ /// The reaction session result.
73
176
  public let reaction: CameraReaction?
74
177
 
178
+ /// The capture result for a photo, video, or mixed session.
179
+ public let capture: CameraCapture?
180
+
75
181
  /// The associated metadata.
76
182
  public let metadata: [String: Any]
77
183
 
78
184
  /// Creates a new `CameraResult`.
79
185
  /// - Parameters:
80
- /// - video: The video that was reacted to.
81
- /// - recordings: The recorded videos.
186
+ /// - reaction: The reaction session result.
82
187
  /// - metadata: The associated metadata.
83
- public convenience init(recording: CameraRecording, metadata: [String: Any] = [:]) {
84
- self.init(recording: recording, reaction: nil, metadata: metadata)
188
+ public convenience init(reaction: CameraReaction, metadata: [String: Any] = [:]) {
189
+ self.init(reaction: reaction, capture: nil, metadata: metadata)
85
190
  }
86
191
 
87
- /// Creates a new `CameraResult` from the `IMGLYCamera` result types.
192
+ /// Creates a new `CameraResult`.
88
193
  /// - Parameters:
89
- /// - video: The `IMGLYCamera.Recording` video that was reacted to.
90
- /// - recordings: The recorded `IMGLYCamera.Recording` videos.
194
+ /// - capture: The capture session result.
91
195
  /// - metadata: The associated metadata.
92
- public convenience init(reaction: CameraReaction, metadata: [String: Any] = [:]) {
93
- self.init(recording: nil, reaction: reaction, metadata: metadata)
196
+ public convenience init(capture: CameraCapture, metadata: [String: Any] = [:]) {
197
+ self.init(reaction: nil, capture: capture, metadata: metadata)
94
198
  }
95
199
 
96
- private init(recording: CameraRecording?, reaction: CameraReaction?, metadata: [String: Any] = [:]) {
97
- self.recording = recording
200
+ private init(
201
+ reaction: CameraReaction?,
202
+ capture: CameraCapture?,
203
+ metadata: [String: Any] = [:],
204
+ ) {
98
205
  self.reaction = reaction
206
+ self.capture = capture
99
207
  self.metadata = metadata
100
208
  }
101
209
 
102
210
  /// Converts the instance to a dictionary.
103
211
  public func toDictionary() -> [String: Any] {
104
212
  var dictionary: [String: Any] = ["metadata": metadata]
105
- if let recording {
106
- dictionary["recording"] = recording.toDictionary()
107
- }
108
213
  if let reaction {
109
214
  dictionary["reaction"] = reaction.toDictionary()
110
215
  }
216
+ if let capture {
217
+ dictionary["capture"] = capture.toDictionary()
218
+ }
111
219
  return dictionary
112
220
  }
113
221
  }
@@ -117,13 +225,13 @@ import IMGLYCamera
117
225
  /// The individual video sections.
118
226
  public let videos: [Video]
119
227
 
120
- /// The duration in milliseconds.
228
+ /// The duration in seconds.
121
229
  public let duration: Double
122
230
 
123
231
  /// Creates a new `Recording` instance.
124
232
  /// - Parameters:
125
233
  /// - videos: The individual video sections.
126
- /// - duration: The duration in milliseconds.
234
+ /// - duration: The duration in seconds.
127
235
  public init(videos: [Video], duration: Double) {
128
236
  self.videos = videos
129
237
  self.duration = duration
@@ -1,3 +1,5 @@
1
+ import Foundation
2
+
1
3
  /// A struct containing all necessary settings to setup the camera.
2
4
  @objc @objcMembers public class CameraSettings: NSObject, Codable {
3
5
  /// The license key. Pass `nil` to run the SDK in evaluation mode with a watermark.
@@ -6,6 +8,9 @@
6
8
  /// The id of the current user.
7
9
  public let userId: String?
8
10
 
11
+ /// Optional camera configuration. When `nil`, native defaults apply.
12
+ public let configuration: CameraConfiguration?
13
+
9
14
  /// Creates a new instance from a given dictionary.
10
15
  ///
11
16
  /// - Parameters:
@@ -22,3 +27,15 @@
22
27
  }
23
28
  }
24
29
  }
30
+
31
+ /// Camera configuration options forwarded from the TypeScript layer.
32
+ @objc @objcMembers public class CameraConfiguration: NSObject, Codable {
33
+ /// The kind of media the camera captures (`"photo"`, `"video"`, `"mixed"`).
34
+ public let captureType: String?
35
+
36
+ /// How many captures the session produces (`"single"`, `"multi"`).
37
+ public let captureCount: String?
38
+
39
+ /// The duration in seconds stamped on each captured photo.
40
+ public let photoClipDuration: Double?
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imgly/camera-react-native",
3
- "version": "1.76.1",
3
+ "version": "1.78.0",
4
4
  "description": "Camera SDK for React Native - provide a versatile video camera to your users on iOS and Android.",
5
5
  "react-native": "index.js",
6
6
  "source": "index.js",
@@ -8,7 +8,7 @@ export interface Recording {
8
8
  /** The individual videos of the recording. */
9
9
  videos: Video[];
10
10
 
11
- /** The overall duration in milliseconds. */
11
+ /** The overall duration in seconds. */
12
12
  duration: number;
13
13
  }
14
14
 
@@ -46,10 +46,81 @@ export interface Rect {
46
46
  height: number;
47
47
  }
48
48
 
49
- /** The result for a default camera recording session. */
50
- interface CameraRecording {
51
- /** The recorded videos. */
52
- recordings: Recording[];
49
+ /** A captured still photo. Contains one image in standard mode or two stacked images in dual camera mode. */
50
+ export interface Photo {
51
+ /** The individual image(s) of the photo capture. */
52
+ images: PhotoImage[];
53
+
54
+ /**
55
+ * The duration stamped on the photo, in seconds.
56
+ * Note: the Flutter bridge passes this value in milliseconds instead.
57
+ */
58
+ duration: number;
59
+ }
60
+
61
+ /** A single image inside a photo capture. */
62
+ export interface PhotoImage {
63
+ /**
64
+ * A url to the photo file that is stored
65
+ * in a temporary location.
66
+ */
67
+ uri: string;
68
+
69
+ /**
70
+ * A rect that contains the position of
71
+ * the image inside the dual-camera layout
72
+ * (iOS only).
73
+ */
74
+ rect: Rect;
75
+ }
76
+
77
+ /**
78
+ * A single capture from the camera. Either a still
79
+ * photo or a video recording.
80
+ */
81
+ export interface Capture {
82
+ /** The captured still photo, when this is a photo capture. */
83
+ photo?: Photo;
84
+
85
+ /** The video recording, when this is a video capture. */
86
+ video?: Recording;
87
+ }
88
+
89
+ /** The kind of media the camera captures. */
90
+ export type CaptureType = 'photo' | 'video' | 'mixed';
91
+
92
+ /** How many captures the camera session produces. */
93
+ export type CaptureCount = 'single' | 'multi';
94
+
95
+ /**
96
+ * Configuration options that control how the camera captures media.
97
+ * All fields are optional; missing values fall back to native defaults
98
+ * (`video`, `multi`, `5` seconds).
99
+ */
100
+ export interface CameraConfiguration {
101
+ /** The kind of media the camera captures. */
102
+ captureType?: CaptureType;
103
+
104
+ /** How many captures the camera session produces. */
105
+ captureCount?: CaptureCount;
106
+
107
+ /** The duration in seconds stamped on each captured photo. */
108
+ photoClipDuration?: number;
109
+ }
110
+
111
+ /**
112
+ * Spec-level shape for the configuration field.
113
+ *
114
+ * The native turbo-module codegen does not support string literal unions
115
+ * inside nested struct fields, so the Spec sees this looser variant with
116
+ * plain strings while customers still author against the strict
117
+ * `CameraConfiguration` above. The literal types narrow it correctly at
118
+ * the call site through subtype assignability.
119
+ */
120
+ interface SpecCameraConfiguration {
121
+ captureType?: string;
122
+ captureCount?: string;
123
+ photoClipDuration?: number;
53
124
  }
54
125
 
55
126
  /** The result for a reaction camera recording session. */
@@ -61,14 +132,20 @@ interface CameraReaction {
61
132
  recordings: Recording[];
62
133
  }
63
134
 
135
+ /** The result for a photo, video, or mixed camera capture session. */
136
+ interface CameraCapture {
137
+ /** The individual captures from the session. */
138
+ captures: Capture[];
139
+ }
140
+
64
141
  /** The result of a camera session. */
65
142
  interface CameraResult {
66
- /** The video that was reacted to (iOS only). */
67
- recording?: CameraRecording;
68
-
69
- /** The recorded videos. */
143
+ /** The reaction result for a reaction camera session (iOS only). */
70
144
  reaction?: CameraReaction;
71
145
 
146
+ /** The capture result for a photo, video, or mixed camera session. */
147
+ capture?: CameraCapture;
148
+
72
149
  /** The associated metadata. */
73
150
  metadata: { [key: string]: unknown };
74
151
  }
@@ -84,6 +161,22 @@ export interface CameraSettings {
84
161
  * active users (MAU).
85
162
  */
86
163
  userId?: string;
164
+
165
+ /**
166
+ * Optional camera configuration. When omitted,
167
+ * native defaults apply.
168
+ */
169
+ configuration?: CameraConfiguration;
170
+ }
171
+
172
+ /**
173
+ * Spec-level shape for `CameraSettings`. See `SpecCameraConfiguration`
174
+ * for the reason this exists alongside the public `CameraSettings`.
175
+ */
176
+ interface SpecCameraSettings {
177
+ license?: string;
178
+ userId?: string;
179
+ configuration?: SpecCameraConfiguration;
87
180
  }
88
181
 
89
182
  /** TurboModule Spec. */
@@ -95,7 +188,7 @@ interface Spec extends TurboModule {
95
188
  * @param metadata The metadata to pass to the native module.
96
189
  */
97
190
  openCamera(
98
- settings: CameraSettings,
191
+ settings: SpecCameraSettings,
99
192
  video?: string,
100
193
  metadata?: { [key: string]: unknown }
101
194
  ): Promise<CameraResult | null>;
@@ -106,15 +199,6 @@ const NativeModule = TurboModuleRegistry.get<Spec>(
106
199
  'IMGLYCamera'
107
200
  ) as Spec | null;
108
201
 
109
- /** The result for a default camera recording session. */
110
- export interface CameraRecordingResult {
111
- /** The recorded videos. */
112
- recordings: Recording[];
113
-
114
- /** The associated metadata. */
115
- metadata: { [key: string]: unknown };
116
- }
117
-
118
202
  /** The result for a reaction camera recording session. */
119
203
  export interface CameraReactionResult {
120
204
  /** The video that was reacted to (iOS only). */
@@ -127,8 +211,37 @@ export interface CameraReactionResult {
127
211
  metadata: { [key: string]: unknown };
128
212
  }
129
213
 
214
+ /** The result for a photo, video, or mixed camera capture session. */
215
+ export interface CameraCaptureResult {
216
+ /** The individual captures from the session. */
217
+ captures: Capture[];
218
+
219
+ /** The associated metadata. */
220
+ metadata: { [key: string]: unknown };
221
+ }
222
+
130
223
  /** The IMG.LY Camera. */
131
224
  class IMGLYCamera {
225
+ /**
226
+ * Opens the camera in photo or mixed capture mode.
227
+ *
228
+ * Returns a `CameraCaptureResult` with photos, videos, or both, depending
229
+ * on the configured `captureType`.
230
+ *
231
+ * @param settings - Configuration settings for the camera. Must include
232
+ * a `configuration` with `captureType: 'photo'` or `'mixed'`.
233
+ * @param metadata - Optional metadata to pass to the native module.
234
+ * @returns A promise that resolves to the capture result, or null if cancelled.
235
+ */
236
+ static async openCamera(
237
+ settings: CameraSettings & {
238
+ configuration: CameraConfiguration & {
239
+ captureType: 'photo' | 'mixed';
240
+ };
241
+ },
242
+ metadata?: { [key: string]: unknown }
243
+ ): Promise<CameraCaptureResult | null>;
244
+
132
245
  /**
133
246
  * Opens the camera for reaction mode (iOS only).
134
247
  * @param settings - Configuration settings for the camera.
@@ -143,28 +256,28 @@ class IMGLYCamera {
143
256
  ): Promise<CameraReactionResult | null>;
144
257
 
145
258
  /**
146
- * Opens the standard camera.
259
+ * Opens the standard video camera.
147
260
  * @param settings - Configuration settings for the camera.
148
261
  * @param metadata - Optional metadata to pass to the native module.
149
- * @returns A promise that resolves to the recording result, or null if cancelled.
262
+ * @returns A promise that resolves to the capture result, or null if cancelled.
150
263
  */
151
264
  static async openCamera(
152
265
  settings: CameraSettings,
153
266
  metadata?: { [key: string]: unknown }
154
- ): Promise<CameraRecordingResult | null>;
267
+ ): Promise<CameraCaptureResult | null>;
155
268
 
156
269
  /**
157
270
  * Opens the camera with the specified settings and optional video or metadata.
158
271
  * @param settings - Configuration settings for the camera.
159
272
  * @param video - Optional video input to trigger reactions (iOS only) or metadata object.
160
273
  * @param metadata - Optional metadata to pass to the native module.
161
- * @returns A promise that resolves to either the recording result, reaction result, or null if unsuccessful.
274
+ * @returns A promise that resolves to a recording, reaction, or capture result, or null if unsuccessful.
162
275
  */
163
276
  static async openCamera(
164
277
  settings: CameraSettings,
165
278
  video?: string | { [key: string]: unknown },
166
279
  metadata: { [key: string]: unknown } = {}
167
- ): Promise<CameraRecordingResult | CameraReactionResult | null> {
280
+ ): Promise<CameraReactionResult | CameraCaptureResult | null> {
168
281
  if (!NativeModule) return null;
169
282
  const videoInput = typeof video === 'string' ? video : undefined;
170
283
  const metadataInput =
@@ -178,9 +291,9 @@ class IMGLYCamera {
178
291
  const { video, recordings } = result.reaction;
179
292
  return { video, recordings, metadata: result.metadata };
180
293
  }
181
- if (result?.recording) {
294
+ if (result?.capture) {
182
295
  return {
183
- recordings: result.recording.recordings,
296
+ captures: result.capture.captures,
184
297
  metadata: result.metadata
185
298
  };
186
299
  }