tirtc-react-native 2.5.0-alpha.1 → 2.5.2

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 (62) hide show
  1. package/README.md +39 -6
  2. package/TiRtcReactNative.podspec +1 -1
  3. package/android/build.gradle +1 -1
  4. package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcModule.kt +137 -14
  5. package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeCleanup.kt +54 -19
  6. package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeCloudStorageCallbacks.kt +60 -0
  7. package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeEventBinder.kt +2 -0
  8. package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeEventSink.kt +20 -0
  9. package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeMaps.kt +112 -5
  10. package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeOptions.kt +2 -7
  11. package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeRegistry.kt +3 -0
  12. package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeStorageSupport.kt +46 -22
  13. package/ios/TiRtcReactNative+Conn.mm +61 -0
  14. package/ios/TiRtcReactNative+Output.mm +1 -7
  15. package/ios/TiRtcReactNative+Private.h +1 -0
  16. package/ios/TiRtcReactNative+Storage.mm +133 -35
  17. package/ios/TiRtcReactNative.mm +137 -48
  18. package/ios/TiRtcReactNativeCloudStorageCallbacks.h +13 -0
  19. package/ios/TiRtcReactNativeCloudStorageCallbacks.mm +67 -0
  20. package/ios/TiRtcReactNativeEventBinder.h +4 -0
  21. package/ios/TiRtcReactNativeEventBinder.mm +11 -0
  22. package/ios/TiRtcReactNativeEventSink.h +5 -0
  23. package/ios/TiRtcReactNativeEventSink.mm +18 -0
  24. package/ios/TiRtcReactNativeMaps.h +6 -0
  25. package/ios/TiRtcReactNativeMaps.mm +47 -0
  26. package/ios/TiRtcReactNativeOptions.mm +2 -6
  27. package/lib/module/audio_input.js +23 -4
  28. package/lib/module/audio_input.js.map +1 -1
  29. package/lib/module/build_identity.js +1 -1
  30. package/lib/module/conn.js +19 -0
  31. package/lib/module/conn.js.map +1 -1
  32. package/lib/module/events.js +16 -9
  33. package/lib/module/events.js.map +1 -1
  34. package/lib/module/index.js +3 -2
  35. package/lib/module/index.js.map +1 -1
  36. package/lib/module/raw_dump.js +53 -0
  37. package/lib/module/raw_dump.js.map +1 -0
  38. package/lib/module/specs/NativeTiRtc.js.map +1 -1
  39. package/lib/module/storage.js +185 -33
  40. package/lib/module/storage.js.map +1 -1
  41. package/lib/module/types.js +0 -4
  42. package/lib/module/types.js.map +1 -1
  43. package/lib/module/video_input.js +16 -6
  44. package/lib/module/video_input.js.map +1 -1
  45. package/lib/typescript/conn.d.ts +2 -0
  46. package/lib/typescript/index.d.ts +5 -3
  47. package/lib/typescript/native.d.ts +1 -1
  48. package/lib/typescript/raw_dump.d.ts +35 -0
  49. package/lib/typescript/specs/NativeTiRtc.d.ts +74 -2
  50. package/lib/typescript/storage.d.ts +82 -3
  51. package/lib/typescript/types.d.ts +5 -6
  52. package/package.json +2 -2
  53. package/src/audio_input.ts +30 -11
  54. package/src/conn.ts +23 -0
  55. package/src/events.ts +14 -9
  56. package/src/index.ts +17 -1
  57. package/src/native.ts +7 -1
  58. package/src/raw_dump.ts +84 -0
  59. package/src/specs/NativeTiRtc.ts +79 -2
  60. package/src/storage.ts +256 -39
  61. package/src/types.ts +5 -8
  62. package/src/video_input.ts +22 -12
@@ -4,11 +4,121 @@ import com.facebook.react.bridge.Arguments
4
4
  import com.facebook.react.bridge.WritableMap
5
5
  import com.tange.ai.tirtc.TiCloudStorageRecordingRange
6
6
  import com.tange.ai.tirtc.TiCloudStorageRecordingDay
7
+ import com.tange.ai.tirtc.TiCloudStorageRecordingGap
8
+ import com.tange.ai.tirtc.TiCloudStorageExportProgress
9
+ import com.tange.ai.tirtc.TiCloudStorageExportReport
10
+ import com.tange.ai.tirtc.TiCloudStorageExportResult
11
+ import com.tange.ai.tirtc.TiRawDumpArchive
7
12
 
8
- internal fun recordingStartResult(code: Int, taskId: Int?): WritableMap =
13
+ internal fun recordingStartResult(code: Int, taskId: Int?, generation: Int = 0): WritableMap =
9
14
  Arguments.createMap().apply {
10
15
  putInt("code", TiRtcReactNativeErrors.normalizeAndroid(code))
11
16
  if (taskId == null) putNull("taskId") else putInt("taskId", taskId)
17
+ putInt("generation", generation)
18
+ }
19
+
20
+ internal fun rawDumpStartResult(code: Int, dumpId: Int?): WritableMap =
21
+ Arguments.createMap().apply {
22
+ putInt("code", TiRtcReactNativeErrors.normalizeAndroid(code))
23
+ if (dumpId == null) putNull("dumpId") else putInt("dumpId", dumpId)
24
+ }
25
+
26
+ private fun rawDumpArchive(value: TiRawDumpArchive): WritableMap =
27
+ Arguments.createMap().apply {
28
+ putString("captureId", value.captureId)
29
+ putString("path", value.path)
30
+ putDouble("size", value.size.toDouble())
31
+ putString("sha256", value.sha256)
32
+ putDouble("capturedDurationMs", value.capturedDurationMs.toDouble())
33
+ putBoolean("captureComplete", value.captureComplete)
34
+ putInt("stopReason", value.stopReason.ordinal)
35
+ putDouble("unsavedPacketCount", value.unsavedPacketCount.toDouble())
36
+ putDouble("unsavedByteCount", value.unsavedByteCount.toDouble())
37
+ putBoolean("empty", value.empty)
38
+ }
39
+
40
+ internal fun rawDumpStopResult(
41
+ code: Int,
42
+ value: TiRawDumpArchive?,
43
+ terminal: Boolean = false,
44
+ ): WritableMap =
45
+ Arguments.createMap().apply {
46
+ putInt("code", TiRtcReactNativeErrors.normalizeAndroid(code))
47
+ if (value == null) putNull("archive") else putMap("archive", rawDumpArchive(value))
48
+ putBoolean("terminal", terminal)
49
+ }
50
+
51
+ private fun tiCloudStorageRecordingRange(range: TiCloudStorageRecordingRange): WritableMap =
52
+ Arguments.createMap().apply {
53
+ putDouble("startTimeMs", range.startTimeMs.toDouble())
54
+ putDouble("endTimeMs", range.endTimeMs.toDouble())
55
+ }
56
+
57
+ internal fun tiCloudStorageRecordingGap(gap: TiCloudStorageRecordingGap): WritableMap =
58
+ Arguments.createMap().apply {
59
+ putMap("range", tiCloudStorageRecordingRange(gap.range))
60
+ putArray("tracks", Arguments.createArray().apply {
61
+ gap.tracks.forEach { track ->
62
+ pushMap(Arguments.createMap().apply {
63
+ putInt("kind", track.kind.ordinal + 1)
64
+ putInt("channelId", track.channelId)
65
+ })
66
+ }
67
+ })
68
+ putArray("reasons", Arguments.createArray().apply {
69
+ gap.reasons.forEach { pushInt(it.ordinal) }
70
+ })
71
+ }
72
+
73
+ internal fun tiCloudStorageExportProgress(progress: TiCloudStorageExportProgress): WritableMap =
74
+ Arguments.createMap().apply {
75
+ putDouble("fraction", progress.fraction)
76
+ putDouble("coveredDurationMs", progress.coveredDurationMs.toDouble())
77
+ }
78
+
79
+ private fun tiCloudStorageExportReport(report: TiCloudStorageExportReport): WritableMap =
80
+ Arguments.createMap().apply {
81
+ putMap("requestedRange", tiCloudStorageRecordingRange(report.requestedRange))
82
+ putDouble("coveredDurationMs", report.coveredDurationMs.toDouble())
83
+ putArray("segments", Arguments.createArray().apply {
84
+ report.segments.forEach { segment ->
85
+ pushMap(Arguments.createMap().apply {
86
+ putMap("sourceRange", tiCloudStorageRecordingRange(segment.sourceRange))
87
+ putDouble("outputStartMs", segment.outputStartMs.toDouble())
88
+ putDouble("outputEndMs", segment.outputEndMs.toDouble())
89
+ })
90
+ }
91
+ })
92
+ putArray("gaps", Arguments.createArray().apply {
93
+ report.gaps.forEach { pushMap(tiCloudStorageRecordingGap(it)) }
94
+ })
95
+ putArray("unprocessedRanges", Arguments.createArray().apply {
96
+ report.unprocessedRanges.forEach { pushMap(tiCloudStorageRecordingRange(it)) }
97
+ })
98
+ putBoolean("complete", report.complete)
99
+ putInt("termination", report.termination.ordinal)
100
+ putInt("cause", TiRtcReactNativeErrors.normalizeAndroid(report.cause))
101
+ }
102
+
103
+ internal fun tiCloudStorageExportResult(
104
+ result: TiCloudStorageExportResult,
105
+ fileId: Int?,
106
+ progress: TiCloudStorageExportProgress?,
107
+ ): WritableMap =
108
+ Arguments.createMap().apply {
109
+ putInt("code", TiRtcReactNativeErrors.normalizeAndroid(result.code))
110
+ if (fileId == null) putNull("fileId") else putInt("fileId", fileId)
111
+ val file = result.file
112
+ val report = result.report
113
+ if (file == null) {
114
+ putNull("filePath")
115
+ putNull("durationMs")
116
+ } else {
117
+ putString("filePath", file.path)
118
+ putDouble("durationMs", file.durationMs.toDouble())
119
+ }
120
+ if (report == null) putNull("report") else putMap("report", tiCloudStorageExportReport(report))
121
+ if (progress == null) putNull("progressDetail") else putMap("progressDetail", tiCloudStorageExportProgress(progress))
12
122
  }
13
123
 
14
124
  internal fun tiCloudStorageRecordingsResult(code: Int, ranges: List<TiCloudStorageRecordingRange>): WritableMap =
@@ -16,10 +126,7 @@ internal fun tiCloudStorageRecordingsResult(code: Int, ranges: List<TiCloudStora
16
126
  putInt("code", code)
17
127
  putArray("recordings", Arguments.createArray().apply {
18
128
  ranges.forEach { range ->
19
- pushMap(Arguments.createMap().apply {
20
- putDouble("startTimeMs", range.startTimeMs.toDouble())
21
- putDouble("endTimeMs", range.endTimeMs.toDouble())
22
- })
129
+ pushMap(tiCloudStorageRecordingRange(range))
23
130
  }
24
131
  })
25
132
  }
@@ -12,7 +12,6 @@ import com.tange.ai.tirtc.TiRtcAudioSampleRate
12
12
  import com.tange.ai.tirtc.TiRtcCameraFacing
13
13
  import com.tange.ai.tirtc.TiRtcOutputBufferStrategy
14
14
  import com.tange.ai.tirtc.TiRtcVideoDecoderPreference
15
- import com.tange.ai.tirtc.TiRtcVideoEncoderCodec
16
15
  import com.tange.ai.tirtc.TiRtcVideoFrameRate
17
16
  import com.tange.ai.tirtc.TiRtcVideoInputOptions
18
17
  import com.tange.ai.tirtc.TiRtcVideoOutputOptions
@@ -59,16 +58,12 @@ internal fun audioInputOptions(options: ReadableMap): TiRtcAudioInputOptions {
59
58
  },
60
59
  agcLevel = audioAgcLevel(options.getString("agcLevel")),
61
60
  ansLevel = audioAnsLevel(options.getString("ansLevel")),
62
- )
61
+ ).apply { media = options.getInt("media") }
63
62
  }
64
63
 
65
64
  internal fun videoInputOptions(options: ReadableMap): TiRtcVideoInputOptions {
66
65
  return TiRtcVideoInputOptions(
67
- codec =
68
- when (options.getString("codec")) {
69
- "mjpeg" -> TiRtcVideoEncoderCodec.MJPEG
70
- else -> TiRtcVideoEncoderCodec.H264
71
- },
66
+ media = options.getInt("media"),
72
67
  width = options.getInt("width"),
73
68
  height = options.getInt("height"),
74
69
  frameRate =
@@ -6,6 +6,7 @@ import com.tange.ai.tirtc.TiRtcConn
6
6
  import com.tange.ai.tirtc.TiRtcVideoInput
7
7
  import com.tange.ai.tirtc.TiRtcVideoOutput
8
8
  import com.tange.ai.tirtc.TiRtcRecordingTask
9
+ import com.tange.ai.tirtc.TiRawDump
9
10
  import com.tange.ai.tirtc.TiCloudStorage
10
11
  import com.tange.ai.tirtc.TiCloudStorageAudioOutput
11
12
  import com.tange.ai.tirtc.TiCloudStorageRecordingTask
@@ -63,6 +64,8 @@ internal object TiRtcReactNativeRegistry {
63
64
 
64
65
  fun recordingTask(id: Int): TiRtcRecordingTask? = entries[id]?.value as? TiRtcRecordingTask
65
66
 
67
+ fun rawDump(id: Int): TiRawDump? = entries[id]?.value as? TiRawDump
68
+
66
69
  fun cloudStorage(id: Int): TiCloudStorage? = entries[id]?.value as? TiCloudStorage
67
70
 
68
71
  fun tiCloudStorageReplay(id: Int): TiCloudStorageReplay? = entries[id]?.value as? TiCloudStorageReplay
@@ -2,19 +2,49 @@ package com.tange.ai.tirtc.reactnative
2
2
 
3
3
  import com.facebook.react.bridge.Promise
4
4
  import com.tange.ai.tirtc.TiCloudStorageExportTask
5
- import com.tange.ai.tirtc.TiCloudStorageRecordingResult
5
+ import com.tange.ai.tirtc.TiCloudStorageExportProgress
6
+ import com.tange.ai.tirtc.TiCloudStorageExportResult
7
+ import com.tange.ai.tirtc.TiCloudStorageRecordingGap
6
8
 
7
9
  internal class CloudStorageExportBridge(
8
- private val resolve: (Promise, TiCloudStorageRecordingResult) -> Unit,
10
+ private val resolve: (Promise, TiCloudStorageExportResult, TiCloudStorageExportProgress?) -> Unit,
11
+ private val events: TiRtcReactNativeEventSink,
9
12
  ) {
10
13
  @Volatile var task: TiCloudStorageExportTask? = null
14
+ private set
11
15
  @Volatile var onDrained: (() -> Unit)? = null
12
- private var result: TiCloudStorageRecordingResult? = null
16
+ private var result: TiCloudStorageExportResult? = null
13
17
  private val waiters = mutableListOf<Promise>()
14
18
  private var invalidated = false
15
- private var invalidationCompletion: ((TiCloudStorageRecordingResult?) -> Unit)? = null
19
+ private var startRejected = false
20
+ private var invalidationCompletion: ((TiCloudStorageExportResult?) -> Unit)? = null
21
+ var objectId = 0
16
22
 
17
- fun complete(value: TiCloudStorageRecordingResult) {
23
+ fun bindTask(value: TiCloudStorageExportTask) {
24
+ val cancel = synchronized(this) {
25
+ task = value
26
+ invalidated
27
+ }
28
+ if (cancel) value.cancel()
29
+ }
30
+
31
+ fun rejectStart() {
32
+ val completed = synchronized(this) {
33
+ startRejected = true
34
+ invalidationCompletion.also { invalidationCompletion = null }
35
+ }
36
+ completed?.invoke(null)
37
+ }
38
+
39
+ fun progress(value: TiCloudStorageExportProgress) {
40
+ events.tiCloudStorageExportProgress(objectId, value)
41
+ }
42
+
43
+ fun recordingGap(value: TiCloudStorageRecordingGap) {
44
+ events.tiCloudStorageExportRecordingGap(objectId, value)
45
+ }
46
+
47
+ fun complete(value: TiCloudStorageExportResult) {
18
48
  val completed = synchronized(this) {
19
49
  if (result != null) return
20
50
  result = value
@@ -22,7 +52,7 @@ internal class CloudStorageExportBridge(
22
52
  waiters.clear()
23
53
  pending to invalidationCompletion.also { invalidationCompletion = null }
24
54
  }
25
- completed.first.forEach { resolve(it, value) }
55
+ completed.first.forEach { resolve(it, value, task?.progressDetail) }
26
56
  if (completed.first.isNotEmpty()) onDrained?.invoke()
27
57
  completed.second?.invoke(value)
28
58
  }
@@ -35,30 +65,24 @@ internal class CloudStorageExportBridge(
35
65
  }
36
66
  }
37
67
  if (completed != null) {
38
- resolve(promise, completed)
68
+ resolve(promise, completed, task?.progressDetail)
39
69
  onDrained?.invoke()
40
70
  }
41
71
  }
42
72
 
43
- fun invalidate(onTerminal: (TiCloudStorageRecordingResult?) -> Unit) {
44
- val completed = synchronized(this) {
73
+ fun invalidate(onTerminal: (TiCloudStorageExportResult?) -> Unit) {
74
+ val state = synchronized(this) {
45
75
  invalidated = true
46
76
  waiters.clear()
47
- result?.also { return@synchronized it } ?: run {
48
- invalidationCompletion = onTerminal
49
- null
50
- }
77
+ if (result == null && !startRejected) invalidationCompletion = onTerminal
78
+ Triple(result, startRejected, task)
51
79
  }
52
- if (completed != null) {
53
- onTerminal(completed)
80
+ if (state.first != null || state.second) {
81
+ onTerminal(state.first)
54
82
  return
55
83
  }
56
- val code = task?.stop() ?: TiRtcReactNativeErrors.OBJECT_RELEASED
57
- if (code != 0) {
58
- synchronized(this) {
59
- invalidationCompletion = null
60
- }
61
- onTerminal(null)
62
- }
84
+ // An accepted task always reaches completion. If start is still in
85
+ // flight, bindTask requests cancellation once its handle is available.
86
+ state.third?.cancel()
63
87
  }
64
88
  }
@@ -39,6 +39,19 @@ static NSString* TiRtcReactNativeGalleryFileName(NSString* fileName, BOOL video)
39
39
  #pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
40
40
  @implementation TiRtcReactNative (Conn)
41
41
 
42
+ static NSDictionary* TiRtcReactNativeRawDumpArchiveResult(TiRawDumpStopResult* result) {
43
+ TiRawDumpArchive* value = result.archive;
44
+ if (value == nil) return @{ @"code": @(TiRtcReactNativeNormalizeDarwin(result.code)),
45
+ @"archive": (id)kCFNull, @"terminal": @NO };
46
+ return @{ @"code": @0, @"archive": @{
47
+ @"captureId": value.captureId, @"path": value.path, @"size": @(value.size),
48
+ @"sha256": value.sha256, @"capturedDurationMs": @(value.capturedDurationMs),
49
+ @"captureComplete": @(value.captureComplete), @"stopReason": @(value.stopReason),
50
+ @"unsavedPacketCount": @(value.unsavedPacketCount),
51
+ @"unsavedByteCount": @(value.unsavedByteCount), @"empty": @(value.empty),
52
+ }, @"terminal": @NO };
53
+ }
54
+
42
55
  - (NSDictionary*)createConn {
43
56
  return [self
44
57
  registerObject:^id {
@@ -147,6 +160,54 @@ static NSString* TiRtcReactNativeGalleryFileName(NSString* fileName, BOOL video)
147
160
  return @{@"code" : @(TiRtcReactNativeErrorOK), @"taskId" : @(taskId)};
148
161
  }
149
162
 
163
+ - (void)connStartRawDump:(double)objectId
164
+ audioStreamIds:(NSArray*)audioStreamIds
165
+ videoStreamIds:(NSArray*)videoStreamIds
166
+ uplinkAudioStreamIds:(NSArray*)uplinkAudioStreamIds
167
+ resolve:(RCTPromiseResolveBlock)resolve
168
+ reject:(RCTPromiseRejectBlock)reject {
169
+ TiRtcConn* conn = [self conn:objectId];
170
+ if (conn == nil) {
171
+ resolve(@{ @"code": @(TiRtcReactNativeErrorObjectReleased), @"dumpId": (id)kCFNull });
172
+ return;
173
+ }
174
+ TiRtcRawDumpOptions* options = [[TiRtcRawDumpOptions alloc]
175
+ initWithAudioStreamIds:audioStreamIds
176
+ videoStreamIds:videoStreamIds
177
+ uplinkAudioStreamIds:uplinkAudioStreamIds];
178
+ [conn startRawDumpWithOptions:options completion:^(TiRawDumpStartResult* result) {
179
+ if (result.code != 0 || result.dump == nil) {
180
+ resolve(@{ @"code": @(TiRtcReactNativeNormalizeDarwin(result.code)),
181
+ @"dumpId": (id)kCFNull });
182
+ return;
183
+ }
184
+ NSInteger dumpId = [[self registry] addObject:result.dump];
185
+ resolve(@{ @"code": @0, @"dumpId": @(dumpId) });
186
+ }];
187
+ }
188
+
189
+ - (void)rawDumpStop:(double)objectId
190
+ resolve:(RCTPromiseResolveBlock)resolve
191
+ reject:(RCTPromiseRejectBlock)reject {
192
+ id object = [[self registry] objectForId:(NSInteger)objectId];
193
+ if (![object isKindOfClass:[TiRawDump class]]) {
194
+ resolve(@{
195
+ @"code": @(TiRtcReactNativeErrorObjectReleased),
196
+ @"archive": (id)kCFNull,
197
+ @"terminal": @NO
198
+ });
199
+ return;
200
+ }
201
+ [(TiRawDump*)object stopWithCompletion:^(TiRawDumpStopResult* result) {
202
+ BOOL terminal = result.code != [TiCloudStorageErrorCode inUse];
203
+ NSMutableDictionary* payload =
204
+ [TiRtcReactNativeRawDumpArchiveResult(result) mutableCopy];
205
+ payload[@"terminal"] = @(terminal);
206
+ if (terminal) [[self registry] releaseObject:(NSInteger)objectId];
207
+ resolve(payload);
208
+ }];
209
+ }
210
+
150
211
  - (void)recordingTaskStop:(double)objectId
151
212
  resolve:(RCTPromiseResolveBlock)resolve
152
213
  reject:(RCTPromiseRejectBlock)reject {
@@ -182,13 +182,7 @@
182
182
  }
183
183
  dispatch_async(dispatch_get_main_queue(), ^{
184
184
  [output takeSnapshotWithCompletion:^(TiRtcSnapshotResult* snapshotResult) {
185
- NSInteger fileId =
186
- snapshotResult.file == nil ? 0 : [[self registry] addTransientObject:snapshotResult.file];
187
- resolve(@{
188
- @"code" : @(TiRtcReactNativeNormalizeDarwin(snapshotResult.code)),
189
- @"fileId" : fileId == 0 ? (id)kCFNull : @(fileId),
190
- @"filePath" : snapshotResult.file.path ?: (id)kCFNull
191
- });
185
+ resolve([self snapshotResultWithCode:snapshotResult.code file:snapshotResult.file]);
192
186
  }];
193
187
  });
194
188
  }
@@ -28,5 +28,6 @@
28
28
  - (TiRtcVideoOutput*)videoOutput:(double)objectId;
29
29
  - (TiRtcAudioInput*)audioInput:(double)objectId;
30
30
  - (TiRtcVideoInput*)videoInput:(double)objectId;
31
+ - (NSDictionary*)snapshotResultWithCode:(int32_t)code file:(id)file;
31
32
 
32
33
  @end
@@ -76,31 +76,79 @@ static NSDictionary* TiRtcReactNativeCloudStorageMp4Result(
76
76
  };
77
77
  }
78
78
 
79
+ static NSDictionary* TiRtcReactNativeCloudStorageExportResult(
80
+ TiRtcReactNativeRegistry* registry, TiCloudStorageExportResult* result,
81
+ TiCloudStorageExportProgress* progress) {
82
+ NSInteger fileId = result.file == nil ? 0 : [registry addTransientObject:result.file];
83
+ return @{
84
+ @"code" : @(TiRtcReactNativeNormalizeDarwin(result.code)),
85
+ @"fileId" : fileId == 0 ? (id)kCFNull : @(fileId),
86
+ @"filePath" : result.file.path ?: (id)kCFNull,
87
+ @"durationMs" : result.file == nil ? (id)kCFNull : @(result.file.durationMs),
88
+ @"report" : result.report == nil ? (id)kCFNull : TiRtcReactNativeCloudStorageExportReport(result.report),
89
+ @"progressDetail" : progress == nil ? (id)kCFNull : TiRtcReactNativeCloudStorageExportProgress(progress),
90
+ };
91
+ }
92
+
79
93
  @interface TiRtcReactNativeCloudStorageExportBridge : NSObject <TiRtcReactNativeReloadTask>
80
94
  @property(nonatomic, strong) TiCloudStorageExportTask* task;
81
95
  @property(nonatomic, assign) NSInteger objectId;
82
- - (instancetype)initWithRegistry:(TiRtcReactNativeRegistry*)registry;
83
- - (void)complete:(TiCloudStorageRecordingResult*)result;
96
+ - (instancetype)initWithRegistry:(TiRtcReactNativeRegistry*)registry binder:(TiRtcReactNativeEventBinder*)binder;
97
+ - (void)bindTask:(TiCloudStorageExportTask*)task;
98
+ - (void)rejectStart;
99
+ - (void)progress:(TiCloudStorageExportProgress*)progress;
100
+ - (void)recordingGap:(TiCloudStorageRecordingGap*)gap;
101
+ - (void)complete:(TiCloudStorageExportResult*)result;
84
102
  - (void)await:(RCTPromiseResolveBlock)resolve;
85
103
  @end
86
104
 
87
105
  @implementation TiRtcReactNativeCloudStorageExportBridge {
88
106
  TiRtcReactNativeRegistry* _registry;
89
- TiCloudStorageRecordingResult* _result;
107
+ TiRtcReactNativeEventBinder* _binder;
108
+ TiCloudStorageExportResult* _result;
90
109
  NSMutableArray<RCTPromiseResolveBlock>* _waiters;
91
110
  BOOL _invalidated;
111
+ BOOL _startRejected;
92
112
  void (^_invalidationCompletion)(id file);
93
113
  }
94
114
 
95
- - (instancetype)initWithRegistry:(TiRtcReactNativeRegistry*)registry {
115
+ - (instancetype)initWithRegistry:(TiRtcReactNativeRegistry*)registry binder:(TiRtcReactNativeEventBinder*)binder {
96
116
  if (self = [super init]) {
97
117
  _registry = registry;
118
+ _binder = binder;
98
119
  _waiters = [NSMutableArray new];
99
120
  }
100
121
  return self;
101
122
  }
102
123
 
103
- - (void)complete:(TiCloudStorageRecordingResult*)result {
124
+ - (void)bindTask:(TiCloudStorageExportTask*)task {
125
+ BOOL cancel;
126
+ @synchronized(self) {
127
+ self.task = task;
128
+ cancel = _invalidated;
129
+ }
130
+ if (cancel) [task cancel];
131
+ }
132
+
133
+ - (void)rejectStart {
134
+ void (^completion)(id file);
135
+ @synchronized(self) {
136
+ _startRejected = YES;
137
+ completion = _invalidationCompletion;
138
+ _invalidationCompletion = nil;
139
+ }
140
+ if (completion != nil) completion(nil);
141
+ }
142
+
143
+ - (void)progress:(TiCloudStorageExportProgress*)progress {
144
+ [_binder cloudStorageExportProgress:progress objectId:self.objectId];
145
+ }
146
+
147
+ - (void)recordingGap:(TiCloudStorageRecordingGap*)gap {
148
+ [_binder cloudStorageExportRecordingGap:gap objectId:self.objectId];
149
+ }
150
+
151
+ - (void)complete:(TiCloudStorageExportResult*)result {
104
152
  NSArray<RCTPromiseResolveBlock>* waiters;
105
153
  void (^invalidationCompletion)(id file);
106
154
  @synchronized(self) {
@@ -112,14 +160,14 @@ static NSDictionary* TiRtcReactNativeCloudStorageMp4Result(
112
160
  _invalidationCompletion = nil;
113
161
  }
114
162
  for (RCTPromiseResolveBlock resolve in waiters) {
115
- resolve(TiRtcReactNativeCloudStorageMp4Result(_registry, result));
163
+ resolve(TiRtcReactNativeCloudStorageExportResult(_registry, result, self.task.progressDetail));
116
164
  }
117
165
  if (waiters.count > 0 && self.objectId > 0) [_registry releaseObject:self.objectId];
118
166
  if (invalidationCompletion != nil) invalidationCompletion(result.file);
119
167
  }
120
168
 
121
169
  - (void)await:(RCTPromiseResolveBlock)resolve {
122
- TiCloudStorageRecordingResult* result;
170
+ TiCloudStorageExportResult* result;
123
171
  @synchronized(self) {
124
172
  result = _result;
125
173
  if (result == nil) {
@@ -127,29 +175,28 @@ static NSDictionary* TiRtcReactNativeCloudStorageMp4Result(
127
175
  return;
128
176
  }
129
177
  }
130
- resolve(TiRtcReactNativeCloudStorageMp4Result(_registry, result));
178
+ resolve(TiRtcReactNativeCloudStorageExportResult(_registry, result, self.task.progressDetail));
131
179
  if (self.objectId > 0) [_registry releaseObject:self.objectId];
132
180
  }
133
181
 
134
182
  - (void)stopForReactNativeInvalidation:(void (^)(id file))completion {
135
- TiCloudStorageRecordingResult* result;
183
+ TiCloudStorageExportResult* result;
184
+ TiCloudStorageExportTask* task;
185
+ BOOL rejected;
136
186
  @synchronized(self) {
137
187
  _invalidated = YES;
138
188
  [_waiters removeAllObjects];
139
189
  result = _result;
140
- if (result == nil) _invalidationCompletion = [completion copy];
190
+ task = self.task;
191
+ rejected = _startRejected;
192
+ if (result == nil && !rejected) _invalidationCompletion = [completion copy];
141
193
  }
142
- if (result != nil) {
194
+ if (result != nil || rejected) {
143
195
  completion(result.file);
144
196
  return;
145
197
  }
146
- int32_t code = [self.task stop];
147
- if (code != 0) {
148
- @synchronized(self) {
149
- _invalidationCompletion = nil;
150
- }
151
- completion(nil);
152
- }
198
+ // bindTask cancels after an in-flight start returns its accepted handle.
199
+ [task cancel];
153
200
  }
154
201
 
155
202
  @end
@@ -240,7 +287,7 @@ static NSDictionary* TiRtcReactNativeCloudStorageMp4Result(
240
287
  return TiRtcReactNativeCreateResult(TiRtcReactNativeErrorObjectReleased, 0, 0, nil);
241
288
  }
242
289
  return [self
243
- registerObject:^id { return [(TiCloudStorage*)object createReplay]; }
290
+ registerObject:^id { return [(TiCloudStorage*)object createReplayWithCallbackThread:TiCloudStorageCallbackThreadBackground]; }
244
291
  wire:^id(NSInteger replayId, id replay) {
245
292
  return [[self eventBinder] wireCloudStorageReplay:(TiCloudStorageReplay*)replay objectId:replayId];
246
293
  }];
@@ -253,25 +300,34 @@ static NSDictionary* TiRtcReactNativeCloudStorageMp4Result(
253
300
  audioChannelId:(NSNumber*)audioChannelId {
254
301
  id object = [[self registry] objectForId:(NSInteger)objectId];
255
302
  if (![object isKindOfClass:[TiCloudStorage class]]) {
256
- return @{@"code" : @(TiRtcReactNativeErrorObjectReleased), @"taskId" : (id)kCFNull};
303
+ return @{@"code" : @(TiRtcReactNativeErrorObjectReleased), @"taskId" : (id)kCFNull, @"generation" : @0};
257
304
  }
258
305
  TiRtcReactNativeCloudStorageExportBridge* bridge =
259
- [[TiRtcReactNativeCloudStorageExportBridge alloc] initWithRegistry:[self registry]];
306
+ [[TiRtcReactNativeCloudStorageExportBridge alloc] initWithRegistry:[self registry]
307
+ binder:[self eventBinder]];
260
308
  TiCloudStorageExportRequest* request = [[TiCloudStorageExportRequest alloc]
261
309
  initWithStartTimeMs:(int64_t)startTimeMs
262
310
  endTimeMs:(int64_t)endTimeMs
263
311
  videoChannelId:(NSInteger)videoChannelId
264
312
  audioChannelId:audioChannelId];
313
+ NSInteger generation = 0;
314
+ NSInteger taskId = [[self registry] addObject:bridge generation:&generation];
315
+ bridge.objectId = taskId;
265
316
  TiCloudStorageExportTaskStartResult* started = [(TiCloudStorage*)object
266
317
  exportRecording:request
267
318
  progress:nil
268
- completion:^(TiCloudStorageRecordingResult* result) { [bridge complete:result]; }];
319
+ progressDetail:^(TiCloudStorageExportProgress* progress) { [bridge progress:progress]; }
320
+ onRecordingGap:^(TiCloudStorageRecordingGap* gap) { [bridge recordingGap:gap]; }
321
+ callbackThread:TiCloudStorageCallbackThreadBackground
322
+ completion:^(TiCloudStorageExportResult* result) { [bridge complete:result]; }];
269
323
  if (started.code != 0 || started.task == nil) {
270
- return @{@"code" : @(TiRtcReactNativeNormalizeDarwin(started.code)), @"taskId" : (id)kCFNull};
324
+ [bridge rejectStart];
325
+ [[self registry] releaseObject:taskId];
326
+ return @{@"code" : @(TiRtcReactNativeNormalizeDarwin(started.code)), @"taskId" : (id)kCFNull,
327
+ @"generation" : @0};
271
328
  }
272
- bridge.task = started.task;
273
- bridge.objectId = [[self registry] addObject:bridge];
274
- return @{@"code" : @0, @"taskId" : @(bridge.objectId)};
329
+ [bridge bindTask:started.task];
330
+ return @{@"code" : @0, @"taskId" : @(taskId), @"generation" : @(generation)};
275
331
  }
276
332
 
277
333
  - (NSNumber*)tiCloudStorageExportProgress:(double)objectId {
@@ -281,13 +337,23 @@ static NSDictionary* TiRtcReactNativeCloudStorageMp4Result(
281
337
  : @0;
282
338
  }
283
339
 
340
+ - (NSDictionary*)tiCloudStorageExportProgressDetail:(double)objectId {
341
+ id object = [[self registry] objectForId:(NSInteger)objectId];
342
+ TiCloudStorageExportProgress* progress =
343
+ [object isKindOfClass:[TiRtcReactNativeCloudStorageExportBridge class]]
344
+ ? ((TiRtcReactNativeCloudStorageExportBridge*)object).task.progressDetail
345
+ : [[TiCloudStorageExportProgress alloc] initWithFraction:0 coveredDurationMs:0];
346
+ return TiRtcReactNativeCloudStorageExportProgress(progress);
347
+ }
348
+
284
349
  - (void)tiCloudStorageExportResult:(double)objectId
285
350
  resolve:(RCTPromiseResolveBlock)resolve
286
351
  reject:(RCTPromiseRejectBlock)reject {
287
352
  id object = [[self registry] objectForId:(NSInteger)objectId];
288
353
  if (![object isKindOfClass:[TiRtcReactNativeCloudStorageExportBridge class]]) {
289
354
  resolve(@{@"code" : @(TiRtcReactNativeErrorObjectReleased), @"fileId" : (id)kCFNull,
290
- @"filePath" : (id)kCFNull, @"durationMs" : (id)kCFNull});
355
+ @"filePath" : (id)kCFNull, @"durationMs" : (id)kCFNull, @"report" : (id)kCFNull,
356
+ @"progressDetail" : (id)kCFNull});
291
357
  return;
292
358
  }
293
359
  [(TiRtcReactNativeCloudStorageExportBridge*)object await:resolve];
@@ -302,6 +368,15 @@ static NSDictionary* TiRtcReactNativeCloudStorageMp4Result(
302
368
  return @(TiRtcReactNativeNormalizeDarwin([task stop]));
303
369
  }
304
370
 
371
+ - (NSNumber*)tiCloudStorageExportCancel:(double)objectId {
372
+ id object = [[self registry] objectForId:(NSInteger)objectId];
373
+ if (![object isKindOfClass:[TiRtcReactNativeCloudStorageExportBridge class]]) {
374
+ return @(TiRtcReactNativeErrorObjectReleased);
375
+ }
376
+ TiCloudStorageExportTask* task = ((TiRtcReactNativeCloudStorageExportBridge*)object).task;
377
+ return @(TiRtcReactNativeNormalizeDarwin([task cancel]));
378
+ }
379
+
305
380
  - (NSNumber*)tiCloudStorageDispose:(double)objectId {
306
381
  id object = [[self registry] objectForId:(NSInteger)objectId];
307
382
  if (![object isKindOfClass:[TiCloudStorage class]]) return @(TiRtcReactNativeErrorObjectReleased);
@@ -358,15 +433,41 @@ static NSDictionary* TiRtcReactNativeCloudStorageMp4Result(
358
433
  audioChannelId:(NSNumber*)audioChannelId {
359
434
  id object = [[self registry] objectForId:(NSInteger)objectId];
360
435
  if (![object isKindOfClass:[TiCloudStorageReplay class]]) {
361
- return @{@"code" : @(TiRtcReactNativeErrorObjectReleased), @"taskId" : (id)kCFNull};
436
+ return @{@"code" : @(TiRtcReactNativeErrorObjectReleased), @"taskId" : (id)kCFNull, @"generation" : @0};
362
437
  }
363
438
  TiCloudStorageRecordingTaskStartResult* started = [(TiCloudStorageReplay*)object
364
439
  startRecordingWithVideoChannelId:(NSInteger)videoChannelId audioChannelId:audioChannelId];
365
440
  if (started.code != 0 || started.task == nil) {
366
- return @{@"code" : @(TiRtcReactNativeNormalizeDarwin(started.code)), @"taskId" : (id)kCFNull};
441
+ return @{@"code" : @(TiRtcReactNativeNormalizeDarwin(started.code)), @"taskId" : (id)kCFNull,
442
+ @"generation" : @0};
443
+ }
444
+ NSInteger generation = 0;
445
+ NSInteger taskId = [[self registry] addObject:started.task generation:&generation];
446
+ return @{@"code" : @0, @"taskId" : @(taskId), @"generation" : @(generation)};
447
+ }
448
+
449
+ - (void)tiCloudStorageReplayStartRawDump:(double)objectId
450
+ audioChannelIds:(NSArray*)audioChannelIds
451
+ videoChannelIds:(NSArray*)videoChannelIds
452
+ resolve:(RCTPromiseResolveBlock)resolve
453
+ reject:(RCTPromiseRejectBlock)reject {
454
+ id object = [[self registry] objectForId:(NSInteger)objectId];
455
+ if (![object isKindOfClass:[TiCloudStorageReplay class]]) {
456
+ resolve(@{ @"code": @(TiRtcReactNativeErrorObjectReleased), @"dumpId": (id)kCFNull });
457
+ return;
367
458
  }
368
- NSInteger taskId = [[self registry] addObject:started.task];
369
- return @{@"code" : @0, @"taskId" : @(taskId)};
459
+ TiCloudStorageRawDumpOptions* options = [[TiCloudStorageRawDumpOptions alloc]
460
+ initWithAudioChannelIds:audioChannelIds videoChannelIds:videoChannelIds];
461
+ [(TiCloudStorageReplay*)object startRawDumpWithOptions:options
462
+ completion:^(TiRawDumpStartResult* result) {
463
+ if (result.code != 0 || result.dump == nil) {
464
+ resolve(@{ @"code": @(TiRtcReactNativeNormalizeDarwin(result.code)),
465
+ @"dumpId": (id)kCFNull });
466
+ return;
467
+ }
468
+ NSInteger dumpId = [[self registry] addObject:result.dump];
469
+ resolve(@{ @"code": @0, @"dumpId": @(dumpId) });
470
+ }];
370
471
  }
371
472
 
372
473
  - (void)tiCloudStorageRecordingTaskStop:(double)objectId
@@ -491,10 +592,7 @@ static NSDictionary* TiRtcReactNativeCloudStorageMp4Result(
491
592
  TiCloudStorageVideoOutput* output = (TiCloudStorageVideoOutput*)object;
492
593
  dispatch_async(dispatch_get_main_queue(), ^{
493
594
  [output takeSnapshotWithCompletion:^(TiCloudStorageSnapshotResult* result) {
494
- NSInteger fileId = result.file == nil ? 0 : [[self registry] addTransientObject:result.file];
495
- resolve(@{@"code" : @(TiRtcReactNativeNormalizeDarwin(result.code)),
496
- @"fileId" : fileId == 0 ? (id)kCFNull : @(fileId),
497
- @"filePath" : result.file.path ?: (id)kCFNull});
595
+ resolve([self snapshotResultWithCode:result.code file:result.file]);
498
596
  }];
499
597
  });
500
598
  }