tirtc-react-native 2.5.0-alpha.1 → 2.5.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 (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
package/README.md CHANGED
@@ -6,11 +6,11 @@
6
6
  npm install tirtc-react-native
7
7
  ```
8
8
 
9
- The package ships ESM JavaScript, TypeScript declarations, Codegen specs, Android native wrapper code and iOS native wrapper code. RTC and CloudStorage use the same native Runtime delivery but keep separate initialization and resource lifecycles. The package requires React Native `>=0.80.0`, Android `com.tange.ai:tirtc:2.5.0-alpha.1`, and iOS `TiRTC` `2.5.0-alpha.1` with iOS minimum `15.1`.
9
+ The package ships ESM JavaScript, TypeScript declarations, Codegen specs, Android native wrapper code and iOS native wrapper code. RTC and CloudStorage use the same native Runtime delivery but keep separate initialization and resource lifecycles. The package requires React Native `>=0.80.0`, Android `com.tange.ai:tirtc:2.5.0`, and iOS `TiRTC` `2.5.0` with iOS minimum `15.1`.
10
10
 
11
11
  The iOS package supports arm64 devices and Apple Silicon iOS Simulator. Simulator validation covers build, launch, connection, downlink audio, and downlink video; capture and iOS behavioral qualification remain real-device gates.
12
12
 
13
- Version `2.5.0-alpha.1` is the React Native prerelease aligned with the TiRTC 2.5.0-alpha.1 Android and Darwin SDKs.
13
+ Version `2.5.0` is the stable React Native release aligned with the TiRTC 2.5.0 Android and Darwin SDKs.
14
14
 
15
15
  ## Imports
16
16
 
@@ -37,7 +37,7 @@ The root export is the public API. Deep imports are not supported.
37
37
 
38
38
  String values are exported as stable `as const` objects and matching TypeScript union types:
39
39
 
40
- `TiRtcOutputBufferStrategy`, `TiRtcConnState`, `TiRtcInputState`, `TiRtcAudioCodec`, `TiRtcAudioSampleRate`, `TiRtcAudioChannelCount`, `TiRtcAudioAecMode`, `TiRtcAudioAgcLevel`, `TiRtcAudioAnsLevel`, `TiRtcVideoCodec`, `TiRtcVideoEncoderCodec`, `TiRtcVideoFrameRate`, `TiRtcCameraFacing`, `TiRtcVideoDecoderPreference`, `TiRtcVideoDecoderBackend`, `TiRtcVideoBitstreamFormat`, `TiRtcAudioOutputState`, `TiRtcVideoOutputState`.
40
+ `TiRtcOutputBufferStrategy`, `TiRtcConnState`, `TiRtcInputState`, `TiRtcAudioCodec`, `TiRtcAudioSampleRate`, `TiRtcAudioChannelCount`, `TiRtcAudioAecMode`, `TiRtcAudioAgcLevel`, `TiRtcAudioAnsLevel`, `TiRtcVideoCodec`, `TiRtcVideoFrameRate`, `TiRtcCameraFacing`, `TiRtcVideoDecoderPreference`, `TiRtcVideoDecoderBackend`, `TiRtcVideoBitstreamFormat`, `TiRtcAudioOutputState`, `TiRtcVideoOutputState`.
41
41
 
42
42
  Use the exported values instead of hard-coded strings in app code.
43
43
 
@@ -95,7 +95,7 @@ import {TiRtcAudioCodec, TiRtcAudioInput} from 'tirtc-react-native';
95
95
  const audioInput = new TiRtcAudioInput();
96
96
 
97
97
  await audioInput.setOptions({
98
- codec: TiRtcAudioCodec.g711a,
98
+ media: 2,
99
99
  });
100
100
  await audioInput.attach(conn, talkbackStreamId);
101
101
  await audioInput.start();
@@ -139,7 +139,7 @@ Constructors create native owner identities, so outputs and inputs can attach to
139
139
 
140
140
  ## Release
141
141
 
142
- Version `2.5.0-alpha.1` is the current React Native prerelease for TiRTC and Ti Cloud Storage. It supports React Native New Architecture apps on Android and iOS and pins Android `com.tange.ai:tirtc:2.5.0-alpha.1` and iOS `TiRTC` `2.5.0-alpha.1`.
142
+ Version `2.5.0` is the current stable React Native release for TiRTC and Ti Cloud Storage. It supports React Native New Architecture apps on Android and iOS and pins Android `com.tange.ai:tirtc:2.5.0` and iOS `TiRTC` `2.5.0`.
143
143
 
144
144
  ## License
145
145
 
@@ -155,12 +155,45 @@ npm run build
155
155
  npm run codegen:check
156
156
  ```
157
157
 
158
+ On macOS, `python3 test/cloud_storage_callbacks_test.py` compiles and executes the iOS Foundation callback queue to check acknowledgement, bounded delivery, and invalidation. Android exercises its native callback queue in the Cloud Storage integration instrumentation. These checks supplement the owner product lanes defined in `react-native-test`.
159
+
158
160
  The external application sample is published under `react-native/` in
159
161
  `tirtc-api-samples`. The owner-local projection and recovery entry is:
160
162
 
161
163
  ```sh
162
- ./scripts/publish_api_samples.sh --version 2.5.0-alpha.1
164
+ ./scripts/publish_api_samples.sh --version 2.5.0
163
165
  ```
164
166
 
165
167
  Use `--no-push` for a complete local projection/build rehearsal without writing
166
168
  the public repository.
169
+
170
+ ## Microphone media configuration
171
+
172
+ `TiRtcAudioInputOptions.media` accepts the integer audio media number returned by
173
+ Nano device capabilities:
174
+
175
+ | media | Audio format |
176
+ | --- | --- |
177
+ | 1 | PCM |
178
+ | 2 | G711A (A-law) |
179
+ | 3 | AAC |
180
+ | 4 | Opus |
181
+ | 5 | AMR (8 kHz mono) |
182
+
183
+ ```ts
184
+ const code = await input.setOptions({media: 2});
185
+ ```
186
+
187
+ Omitting `media` or setting it to `-1` uses the deprecated `codec` field, whose
188
+ default remains G711A. Existing AAC calls keep selecting AAC. An explicit supported
189
+ `media` takes precedence over `codec`; any other value returns the existing
190
+ parameter error and leaves the accepted configuration intact. Stop capture before
191
+ applying new options, and follow the input's existing detach requirements.
192
+
193
+ The deprecated audio codec values keep their original meanings. Codec fields on
194
+ received frames and metrics retain their output numbering; they are not Nano
195
+ input media numbers.
196
+
197
+ ## Targeted input validation
198
+
199
+ Prepare the current native dependency, then run `python3 react-native/tirtc/example/tool/integration.py --platform android --case rn_input_media_all` from the repository root. For iOS, select `--platform ios-arm64-simulator --prepared-state <ios-consumer-state>`. Simulator evidence distinguishes decoded constant audio and an unavailable camera from real capture signal verification. Individual cases are `rn_input_media_g711a_h264`, `rn_input_media_aac_mjpeg` and `rn_input_media_legacy_aac_h264`. The test-owned React fixture consumes the public facade; its peer records and decodes the encoded microphone/camera payload. Full integration requirements remain separate.
@@ -13,7 +13,7 @@ Pod::Spec.new do |s|
13
13
  s.source = { :git => 'https://gitlab.tange-ai.com/tirtc-nexus/tirtc.git', :tag => s.version.to_s }
14
14
  s.source_files = 'ios/**/*.{h,m,mm,swift}'
15
15
  s.dependency 'React-Core'
16
- s.dependency 'TiRTC', '2.5.0-alpha.1'
16
+ s.dependency 'TiRTC', '2.5.0'
17
17
 
18
18
  if respond_to?(:install_modules_dependencies, true)
19
19
  install_modules_dependencies(s)
@@ -29,7 +29,7 @@ repositories {
29
29
 
30
30
  dependencies {
31
31
  implementation "com.facebook.react:react-android"
32
- implementation "com.tange.ai:tirtc:${safeExtGet("tirtcAndroidVersion", "2.5.0-alpha.1")}"
32
+ implementation "com.tange.ai:tirtc:${safeExtGet("tirtcAndroidVersion", "2.5.0")}"
33
33
  }
34
34
 
35
35
  def safeExtGet(prop, fallback) {
@@ -25,9 +25,15 @@ import com.tange.ai.tirtc.TiRtcRecordingFile
25
25
  import com.tange.ai.tirtc.TiRtcSnapshotFile
26
26
  import com.tange.ai.tirtc.TiRtcVideoInput
27
27
  import com.tange.ai.tirtc.TiRtcVideoOutput
28
+ import com.tange.ai.tirtc.TiRtcRawDumpOptions
29
+ import com.tange.ai.tirtc.TiCloudStorageRawDumpOptions
28
30
  import com.tange.ai.tirtc.TiCloudStorage
31
+ import com.tange.ai.tirtc.TiCloudStorageCallbackThread
32
+ import com.tange.ai.tirtc.TiCloudStorageErrorCode
33
+ import com.facebook.react.bridge.Arguments
29
34
  import com.tange.ai.tirtc.TiCloudStorageAudioOutput
30
35
  import com.tange.ai.tirtc.TiCloudStorageExportRequest
36
+ import com.tange.ai.tirtc.TiCloudStorageExportResult
31
37
  import com.tange.ai.tirtc.TiCloudStorageRecordingFile
32
38
  import com.tange.ai.tirtc.TiCloudStorageRecordingResult
33
39
  import com.tange.ai.tirtc.TiCloudStorageReplaySpeed
@@ -48,14 +54,28 @@ class TiRtcModule(
48
54
  ) : NativeTiRtcSpec(reactContext) {
49
55
  private val registry = TiRtcReactNativeRegistry
50
56
  private val invalidated = AtomicBoolean(false)
51
- private val events =
52
- TiRtcReactNativeEventSink(registry) { event ->
53
- if (!invalidated.get()) {
54
- reactContext.runOnJSQueueThread {
55
- if (!invalidated.get()) emitOnTiRtcEvent(event)
56
- }
57
- }
57
+ private val cloudStorageCallbacks = TiRtcReactNativeCloudStorageCallbacks {
58
+ emitEvent(Arguments.createMap().apply {
59
+ putString("type", "tiCloudStorageCallbackReady")
60
+ putInt("objectId", 0)
61
+ putInt("generation", 0)
62
+ })
63
+ }
64
+ private val events = TiRtcReactNativeEventSink(registry) { event ->
65
+ val type = event.getString("type") ?: ""
66
+ if (type.startsWith("tiCloudStorageReplay") || type.startsWith("tiCloudStorageExport")) {
67
+ cloudStorageCallbacks.deliver(event)
68
+ } else {
69
+ emitEvent(event)
58
70
  }
71
+ }
72
+
73
+ private fun emitEvent(event: WritableMap) {
74
+ if (!invalidated.get()) reactContext.runOnJSQueueThread {
75
+ if (!invalidated.get()) emitOnTiRtcEvent(event)
76
+ }
77
+ }
78
+
59
79
  private val eventBinder = TiRtcReactNativeEventBinder(events)
60
80
  private val nextMediaFileId = AtomicInteger(1)
61
81
  private val mediaFiles = ConcurrentHashMap<Int, Any>()
@@ -69,6 +89,7 @@ class TiRtcModule(
69
89
 
70
90
  override fun invalidate() {
71
91
  if (!invalidated.compareAndSet(false, true)) return
92
+ cloudStorageCallbacks.close()
72
93
  val objects = registry.drain(events)
73
94
  val files = mediaFiles.values.toList()
74
95
  mediaFiles.clear()
@@ -182,7 +203,7 @@ class TiRtcModule(
182
203
  ?: return createResult(TiRtcReactNativeErrors.OBJECT_RELEASED, 0, 0)
183
204
  return registerReactNativeObject(
184
205
  registry,
185
- factory = { cloudStorage.createReplay() },
206
+ factory = { cloudStorage.createReplay(TiCloudStorageCallbackThread.BACKGROUND) },
186
207
  onRegistered = { entry, value -> eventBinder.wireCloudStorageReplay(entry.id, value as com.tange.ai.tirtc.TiCloudStorageReplay) },
187
208
  )
188
209
  }
@@ -196,7 +217,10 @@ class TiRtcModule(
196
217
  ): WritableMap {
197
218
  val cloudStorage = registry.cloudStorage(id.toInt())
198
219
  ?: return recordingStartResult(TiRtcReactNativeErrors.OBJECT_RELEASED, null)
199
- val bridge = CloudStorageExportBridge(::resolveCloudStorageRecording)
220
+ val bridge = CloudStorageExportBridge(::resolveCloudStorageExport, events)
221
+ val entry = registry.add(bridge)
222
+ bridge.objectId = entry.id
223
+ bridge.onDrained = { registry.release(entry.id) }
200
224
  val started = cloudStorage.exportRecording(
201
225
  TiCloudStorageExportRequest(
202
226
  startTimeMs.toLong(),
@@ -205,17 +229,34 @@ class TiRtcModule(
205
229
  audioChannelId?.toInt(),
206
230
  ),
207
231
  null,
232
+ { progress -> bridge.progress(progress) },
233
+ { gap -> bridge.recordingGap(gap) },
234
+ TiCloudStorageCallbackThread.BACKGROUND,
208
235
  ) { result -> bridge.complete(result) }
209
- val task = started.task ?: return recordingStartResult(started.code, null)
210
- bridge.task = task
211
- val entry = registry.add(bridge)
212
- bridge.onDrained = { registry.release(entry.id) }
213
- return recordingStartResult(started.code, entry.id)
236
+ val task = started.task
237
+ if (task == null) {
238
+ bridge.rejectStart()
239
+ registry.release(entry.id)
240
+ return recordingStartResult(started.code, null)
241
+ }
242
+ bridge.bindTask(task)
243
+ return recordingStartResult(started.code, entry.id, entry.generation)
214
244
  }
215
245
 
246
+ override fun tiCloudStorageTakeCallback(): WritableMap? = cloudStorageCallbacks.take()
247
+
248
+ override fun tiCloudStorageAcknowledgeCallback(deliveryId: Double): Double =
249
+ cloudStorageCallbacks.acknowledge(deliveryId)
250
+
216
251
  override fun tiCloudStorageExportProgress(id: Double): Double =
217
252
  registry.tiCloudStorageExportTask(id.toInt())?.task?.progress ?: 0.0
218
253
 
254
+ override fun tiCloudStorageExportProgressDetail(id: Double): WritableMap =
255
+ tiCloudStorageExportProgress(
256
+ registry.tiCloudStorageExportTask(id.toInt())?.task?.progressDetail
257
+ ?: com.tange.ai.tirtc.TiCloudStorageExportProgress(0.0, 0),
258
+ )
259
+
219
260
  @ReactMethod
220
261
  override fun tiCloudStorageExportResult(id: Double, promise: Promise) {
221
262
  registry.tiCloudStorageExportTask(id.toInt())?.await(promise)
@@ -225,6 +266,9 @@ class TiRtcModule(
225
266
  override fun tiCloudStorageExportStop(id: Double): Double =
226
267
  registry.tiCloudStorageExportTask(id.toInt())?.task?.stop().normalized()
227
268
 
269
+ override fun tiCloudStorageExportCancel(id: Double): Double =
270
+ registry.tiCloudStorageExportTask(id.toInt())?.task?.cancel().normalized()
271
+
228
272
  override fun tiCloudStorageDispose(id: Double): Double {
229
273
  val objectId = id.toInt()
230
274
  val code = registry.cloudStorage(objectId)?.dispose().normalized()
@@ -290,6 +334,30 @@ class TiRtcModule(
290
334
  return recordingStartResult(result.code, entry?.id)
291
335
  }
292
336
 
337
+ @ReactMethod
338
+ override fun tiCloudStorageReplayStartRawDump(
339
+ id: Double,
340
+ audioChannelIds: ReadableArray,
341
+ videoChannelIds: ReadableArray,
342
+ promise: Promise,
343
+ ) {
344
+ val replay = registry.tiCloudStorageReplay(id.toInt())
345
+ if (replay == null) {
346
+ promise.resolve(rawDumpStartResult(TiRtcReactNativeErrors.OBJECT_RELEASED, null))
347
+ return
348
+ }
349
+ val options = TiCloudStorageRawDumpOptions(
350
+ IntArray(audioChannelIds.size()) { audioChannelIds.getInt(it) },
351
+ IntArray(videoChannelIds.size()) { videoChannelIds.getInt(it) },
352
+ )
353
+ replay.startRawDump(options) { result ->
354
+ val entry = result.dump?.let(registry::add)
355
+ reactContext.runOnJSQueueThread {
356
+ promise.resolve(rawDumpStartResult(result.code, entry?.id))
357
+ }
358
+ }
359
+ }
360
+
293
361
  @ReactMethod
294
362
  override fun tiCloudStorageRecordingTaskStop(id: Double, promise: Promise) {
295
363
  val objectId = id.toInt()
@@ -426,6 +494,18 @@ class TiRtcModule(
426
494
  }
427
495
  }
428
496
 
497
+ private fun resolveCloudStorageExport(
498
+ promise: Promise, result: TiCloudStorageExportResult,
499
+ progress: com.tange.ai.tirtc.TiCloudStorageExportProgress?,
500
+ ) {
501
+ val fileId = result.file?.let { file ->
502
+ nextMediaFileId.getAndIncrement().also { mediaFiles[it] = file }
503
+ }
504
+ reactContext.runOnJSQueueThread {
505
+ promise.resolve(tiCloudStorageExportResult(result, fileId, progress))
506
+ }
507
+ }
508
+
429
509
  @ReactMethod
430
510
  override fun uploadLogs(promise: Promise) {
431
511
  uploadLogsToPromise(promise)
@@ -495,6 +575,49 @@ class TiRtcModule(
495
575
  return recordingStartResult(result.code, entry?.id)
496
576
  }
497
577
 
578
+ @ReactMethod
579
+ override fun connStartRawDump(
580
+ id: Double,
581
+ audioStreamIds: ReadableArray,
582
+ videoStreamIds: ReadableArray,
583
+ uplinkAudioStreamIds: ReadableArray,
584
+ promise: Promise,
585
+ ) {
586
+ val conn = registry.conn(id.toInt())
587
+ if (conn == null) {
588
+ promise.resolve(rawDumpStartResult(TiRtcReactNativeErrors.OBJECT_RELEASED, null))
589
+ return
590
+ }
591
+ val options = TiRtcRawDumpOptions(
592
+ IntArray(audioStreamIds.size()) { audioStreamIds.getInt(it) },
593
+ IntArray(videoStreamIds.size()) { videoStreamIds.getInt(it) },
594
+ IntArray(uplinkAudioStreamIds.size()) { uplinkAudioStreamIds.getInt(it) },
595
+ )
596
+ conn.startRawDump(options) { result ->
597
+ val entry = result.dump?.let(registry::add)
598
+ reactContext.runOnJSQueueThread {
599
+ promise.resolve(rawDumpStartResult(result.code, entry?.id))
600
+ }
601
+ }
602
+ }
603
+
604
+ @ReactMethod
605
+ override fun rawDumpStop(id: Double, promise: Promise) {
606
+ val objectId = id.toInt()
607
+ val dump = registry.rawDump(objectId)
608
+ if (dump == null) {
609
+ promise.resolve(rawDumpStopResult(TiRtcReactNativeErrors.OBJECT_RELEASED, null))
610
+ return
611
+ }
612
+ dump.stop { result ->
613
+ val terminal = result.code != TiCloudStorageErrorCode.IN_USE
614
+ if (terminal) registry.release(objectId)
615
+ reactContext.runOnJSQueueThread {
616
+ promise.resolve(rawDumpStopResult(result.code, result.archive, terminal))
617
+ }
618
+ }
619
+ }
620
+
498
621
  @ReactMethod
499
622
  override fun recordingTaskStop(id: Double, promise: Promise) {
500
623
  val objectId = id.toInt()
@@ -12,7 +12,9 @@ import com.tange.ai.tirtc.TiRtcRecordingTask
12
12
  import com.tange.ai.tirtc.TiRtcSnapshotFile
13
13
  import com.tange.ai.tirtc.TiRtcVideoInput
14
14
  import com.tange.ai.tirtc.TiRtcVideoOutput
15
+ import com.tange.ai.tirtc.TiRawDump
15
16
  import com.tange.ai.tirtc.TiCloudStorage
17
+ import com.tange.ai.tirtc.TiCloudStorageErrorCode
16
18
  import com.tange.ai.tirtc.TiCloudStorageAudioOutput
17
19
  import com.tange.ai.tirtc.TiCloudStorageRecordingFile
18
20
  import com.tange.ai.tirtc.TiCloudStorageRecordingTask
@@ -31,7 +33,7 @@ internal object TiRtcReactNativeCleanup {
31
33
  }
32
34
  fun finish() {
33
35
  if (pending.decrementAndGet() == 0) {
34
- Handler(Looper.getMainLooper()).post { releaseOwners(objects) }
36
+ Handler(Looper.getMainLooper()).post { releaseOwners(objects.toMutableList()) }
35
37
  }
36
38
  }
37
39
  fun delete(file: Any?) {
@@ -53,6 +55,10 @@ internal object TiRtcReactNativeCleanup {
53
55
  files.forEach(::delete)
54
56
  objects.forEach { value ->
55
57
  when (value) {
58
+ is TiRawDump -> {
59
+ retain()
60
+ value.stop { finish() }
61
+ }
56
62
  is TiRtcRecordingTask -> {
57
63
  retain()
58
64
  value.stop { result ->
@@ -79,31 +85,60 @@ internal object TiRtcReactNativeCleanup {
79
85
  finish()
80
86
  }
81
87
 
82
- private fun releaseOwners(objects: List<Any>) {
88
+ private fun releaseOwners(objects: MutableList<Any>, delayMs: Long = 10) {
89
+ fun released(operation: String, code: Int): Boolean {
90
+ if (code == 0 || code == TiRtcReactNativeErrors.OBJECT_RELEASED) return true
91
+ if (delayMs == 10L || code != TiCloudStorageErrorCode.IN_USE) logFailure(operation, code)
92
+ // Closing the JS handoff wakes its waiter; the Native callback may still be
93
+ // unwinding. Retain the owners and yield the main queue until disposal succeeds.
94
+ Handler(Looper.getMainLooper()).postDelayed({ releaseOwners(objects, minOf(1000, delayMs * 2)) }, delayMs)
95
+ return false
96
+ }
97
+ objects.filterIsInstance<TiCloudStorageReplay>().forEach { replay ->
98
+ if (!released("ti-cloud-storage-replay-stop", replay.stop())) return
99
+ }
83
100
  objects.filterIsInstance<TiCloudStorageAudioOutput>().forEach { output ->
84
- logFailure("ti-cloud-storage-audio-detach", output.detach())
85
- logFailure("ti-cloud-storage-audio-dispose", output.dispose())
101
+ if (!released("ti-cloud-storage-audio-detach", output.detach())) return
102
+ if (!released("ti-cloud-storage-audio-dispose", output.dispose())) return
103
+ objects.remove(output)
86
104
  }
87
105
  objects.filterIsInstance<TiCloudStorageVideoOutput>().forEach { output ->
88
- logFailure("ti-cloud-storage-video-detach", output.detach())
89
- logFailure("ti-cloud-storage-video-view-detach", output.detachView())
90
- logFailure("ti-cloud-storage-video-dispose", output.dispose())
91
- }
92
- objects.filterIsInstance<TiRtcAudioOutput>().forEach { logFailure("audio_output_dispose", it.dispose()) }
93
- objects.filterIsInstance<TiRtcVideoOutput>().forEach { logFailure("video_output_dispose", it.dispose()) }
94
- objects.filterIsInstance<TiRtcAudioInput>().forEach { logFailure("audio_input_dispose", it.dispose()) }
95
- objects.filterIsInstance<TiRtcVideoInput>().forEach { logFailure("video_input_dispose", it.dispose()) }
106
+ if (!released("ti-cloud-storage-video-detach", output.detach())) return
107
+ if (!released("ti-cloud-storage-video-view-detach", output.detachView())) return
108
+ if (!released("ti-cloud-storage-video-dispose", output.dispose())) return
109
+ objects.remove(output)
110
+ }
111
+ objects.filterIsInstance<TiRtcAudioOutput>().forEach {
112
+ if (!released("audio_output_dispose", it.dispose())) return
113
+ objects.remove(it)
114
+ }
115
+ objects.filterIsInstance<TiRtcVideoOutput>().forEach {
116
+ if (!released("video_output_dispose", it.dispose())) return
117
+ objects.remove(it)
118
+ }
119
+ objects.filterIsInstance<TiRtcAudioInput>().forEach {
120
+ if (!released("audio_input_dispose", it.dispose())) return
121
+ objects.remove(it)
122
+ }
123
+ objects.filterIsInstance<TiRtcVideoInput>().forEach {
124
+ if (!released("video_input_dispose", it.dispose())) return
125
+ objects.remove(it)
126
+ }
96
127
  objects.filterIsInstance<TiCloudStorageReplay>().forEach { replay ->
97
- logFailure("ti-cloud-storage-replay-stop", replay.stop())
98
- logFailure("ti-cloud-storage-replay-dispose", replay.dispose())
128
+ if (!released("ti-cloud-storage-replay-dispose", replay.dispose())) return
129
+ objects.remove(replay)
99
130
  }
100
131
  objects.filterIsInstance<TiRtcConn>().forEach { conn ->
101
- logFailure("conn_disconnect", conn.disconnect())
102
- logFailure("conn_dispose", conn.dispose())
132
+ if (!released("conn_disconnect", conn.disconnect())) return
133
+ if (!released("conn_dispose", conn.dispose())) return
134
+ objects.remove(conn)
135
+ }
136
+ objects.filterIsInstance<TiCloudStorage>().forEach {
137
+ if (!released("ti-cloud-storage-dispose", it.dispose())) return
138
+ objects.remove(it)
103
139
  }
104
- objects.filterIsInstance<TiCloudStorage>().forEach { logFailure("ti-cloud-storage-dispose", it.dispose()) }
105
- logFailure("ti-cloud-storage-shutdown", TiCloudStorage.shutdown())
106
- logFailure("runtime_shutdown", TiRtc.shutdown())
140
+ if (!released("ti-cloud-storage-shutdown", TiCloudStorage.shutdown())) return
141
+ if (!released("runtime_shutdown", TiRtc.shutdown())) return
107
142
  TiRtcLogging.i(TAG, "[module_invalidate] native owners released")
108
143
  }
109
144
 
@@ -0,0 +1,60 @@
1
+ package com.tange.ai.tirtc.reactnative
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+
6
+ /** The SDK's single serial background queue permits one borrowed event per module. */
7
+ internal class TiRtcReactNativeCloudStorageCallbacks(private val wake: () -> Unit) {
8
+ private val monitor = Object()
9
+ private var closed = false
10
+ private var pending: WritableMap? = null
11
+ private var claimed = false
12
+ private var sequence = 0L
13
+
14
+ fun deliver(event: WritableMap) {
15
+ val deliveryId = synchronized(monitor) {
16
+ if (closed) return
17
+ check(pending == null) { "Cloud Storage callbacks require serial SDK delivery" }
18
+ sequence += 1
19
+ pending = event
20
+ claimed = false
21
+ sequence
22
+ }
23
+ var interrupted = false
24
+ try {
25
+ wake()
26
+ synchronized(monitor) {
27
+ while (!closed && pending != null && sequence == deliveryId) {
28
+ try { monitor.wait() } catch (_: InterruptedException) { interrupted = true }
29
+ }
30
+ }
31
+ } finally {
32
+ if (interrupted) Thread.currentThread().interrupt()
33
+ }
34
+ }
35
+
36
+ fun take(): WritableMap? = synchronized(monitor) {
37
+ val event = pending ?: return@synchronized null
38
+ if (closed || claimed) return@synchronized null
39
+ claimed = true
40
+ Arguments.createMap().apply {
41
+ putDouble("deliveryId", sequence.toDouble())
42
+ putMap("event", event)
43
+ }
44
+ }
45
+
46
+ fun acknowledge(deliveryId: Double): Double = synchronized(monitor) {
47
+ if (!closed && claimed && deliveryId == sequence.toDouble()) {
48
+ pending = null
49
+ claimed = false
50
+ monitor.notifyAll()
51
+ }
52
+ 0.0
53
+ }
54
+
55
+ fun close() = synchronized(monitor) {
56
+ closed = true
57
+ pending = null
58
+ monitor.notifyAll()
59
+ }
60
+ }
@@ -23,6 +23,7 @@ import com.tange.ai.tirtc.TiCloudStorageOutputErrorListener
23
23
  import com.tange.ai.tirtc.TiCloudStorageReplay
24
24
  import com.tange.ai.tirtc.TiCloudStorageReplayCompletedListener
25
25
  import com.tange.ai.tirtc.TiCloudStorageReplayErrorListener
26
+ import com.tange.ai.tirtc.TiCloudStorageRecordingGapListener
26
27
  import com.tange.ai.tirtc.TiCloudStorageTimeChangedListener
27
28
  import com.tange.ai.tirtc.TiCloudStorageVideoOutput
28
29
  import com.tange.ai.tirtc.TiCloudStorageVideoOutputStateListener
@@ -36,6 +37,7 @@ internal class TiRtcReactNativeEventBinder(
36
37
  replay.onTimeChanged = TiCloudStorageTimeChangedListener { events.tiCloudStorageReplayTimeChanged(id, it) }
37
38
  replay.onCompleted = TiCloudStorageReplayCompletedListener { events.tiCloudStorageReplayCompleted(id) }
38
39
  replay.onError = TiCloudStorageReplayErrorListener { events.tiCloudStorageReplayError(id, it) }
40
+ replay.onRecordingGap = TiCloudStorageRecordingGapListener { events.tiCloudStorageReplayRecordingGap(id, it) }
39
41
  }
40
42
 
41
43
  fun wireCloudStorageAudioOutput(id: Int, output: TiCloudStorageAudioOutput) {
@@ -4,6 +4,8 @@ import android.util.Size
4
4
  import com.facebook.react.bridge.Arguments
5
5
  import com.facebook.react.bridge.WritableArray
6
6
  import com.facebook.react.bridge.WritableMap
7
+ import com.tange.ai.tirtc.TiCloudStorageExportProgress
8
+ import com.tange.ai.tirtc.TiCloudStorageRecordingGap
7
9
 
8
10
  internal class TiRtcReactNativeEventSink(
9
11
  private val registry: TiRtcReactNativeRegistry,
@@ -60,6 +62,24 @@ internal class TiRtcReactNativeEventSink(
60
62
  })
61
63
  }
62
64
 
65
+ fun tiCloudStorageReplayRecordingGap(objectId: Int, gap: TiCloudStorageRecordingGap) {
66
+ emit(base("tiCloudStorageReplayRecordingGap", objectId).apply {
67
+ putMap("recordingGap", tiCloudStorageRecordingGap(gap))
68
+ })
69
+ }
70
+
71
+ fun tiCloudStorageExportProgress(objectId: Int, progress: TiCloudStorageExportProgress) {
72
+ emit(base("tiCloudStorageExportProgress", objectId).apply {
73
+ putMap("exportProgress", tiCloudStorageExportProgress(progress))
74
+ })
75
+ }
76
+
77
+ fun tiCloudStorageExportRecordingGap(objectId: Int, gap: TiCloudStorageRecordingGap) {
78
+ emit(base("tiCloudStorageExportRecordingGap", objectId).apply {
79
+ putMap("recordingGap", tiCloudStorageRecordingGap(gap))
80
+ })
81
+ }
82
+
63
83
  fun audioOutputStateChanged(
64
84
  objectId: Int,
65
85
  state: String,