tirtc-react-native 2.3.0 → 2.4.0-alpha.3

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 (77) hide show
  1. package/README.md +23 -7
  2. package/TiRtcReactNative.podspec +1 -1
  3. package/android/build.gradle +1 -1
  4. package/android/src/main/AndroidManifest.xml +2 -1
  5. package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcModule.kt +585 -28
  6. package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeCleanup.kt +115 -0
  7. package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeEventBinder.kt +35 -8
  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 +62 -0
  10. package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeModuleSupport.kt +6 -6
  11. package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeRegistry.kt +27 -0
  12. package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeStoreSupport.kt +64 -0
  13. package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcVideoOutputViewManager.kt +17 -2
  14. package/ios/TiRtcReactNative+Conn.mm +173 -2
  15. package/ios/TiRtcReactNative+Input.mm +18 -4
  16. package/ios/TiRtcReactNative+Output.mm +41 -4
  17. package/ios/TiRtcReactNative+Private.h +4 -0
  18. package/ios/TiRtcReactNative+Store.mm +496 -0
  19. package/ios/TiRtcReactNative.h +2 -1
  20. package/ios/TiRtcReactNative.mm +109 -1
  21. package/ios/TiRtcReactNativeEventBinder.h +8 -0
  22. package/ios/TiRtcReactNativeEventBinder.mm +104 -0
  23. package/ios/TiRtcReactNativeEventSink.h +4 -0
  24. package/ios/TiRtcReactNativeEventSink.mm +20 -0
  25. package/ios/TiRtcReactNativeRegistry.h +5 -0
  26. package/ios/TiRtcReactNativeRegistry.mm +43 -0
  27. package/ios/TiRtcVideoOutputView.mm +11 -0
  28. package/lib/module/audio_input.js +33 -17
  29. package/lib/module/audio_input.js.map +1 -1
  30. package/lib/module/audio_output.js +27 -10
  31. package/lib/module/audio_output.js.map +1 -1
  32. package/lib/module/conn.js +31 -7
  33. package/lib/module/conn.js.map +1 -1
  34. package/lib/module/errors.js +4 -3
  35. package/lib/module/errors.js.map +1 -1
  36. package/lib/module/index.js +2 -0
  37. package/lib/module/index.js.map +1 -1
  38. package/lib/module/logging.js +9 -1
  39. package/lib/module/logging.js.map +1 -1
  40. package/lib/module/logging_internal.js +27 -0
  41. package/lib/module/logging_internal.js.map +1 -0
  42. package/lib/module/recording.js +210 -0
  43. package/lib/module/recording.js.map +1 -0
  44. package/lib/module/runtime.js +13 -5
  45. package/lib/module/runtime.js.map +1 -1
  46. package/lib/module/specs/NativeTiRtc.js.map +1 -1
  47. package/lib/module/store.js +698 -0
  48. package/lib/module/store.js.map +1 -0
  49. package/lib/module/video_input.js +40 -22
  50. package/lib/module/video_input.js.map +1 -1
  51. package/lib/module/video_output.js +51 -10
  52. package/lib/module/video_output.js.map +1 -1
  53. package/lib/typescript/audio_input.d.ts +1 -0
  54. package/lib/typescript/conn.d.ts +2 -0
  55. package/lib/typescript/errors.d.ts +3 -2
  56. package/lib/typescript/index.d.ts +4 -0
  57. package/lib/typescript/logging_internal.d.ts +2 -0
  58. package/lib/typescript/recording.d.ts +53 -0
  59. package/lib/typescript/runtime.d.ts +2 -0
  60. package/lib/typescript/specs/NativeTiRtc.d.ts +88 -0
  61. package/lib/typescript/store.d.ts +202 -0
  62. package/lib/typescript/video_input.d.ts +1 -0
  63. package/lib/typescript/video_output.d.ts +3 -0
  64. package/package.json +1 -1
  65. package/src/audio_input.ts +85 -59
  66. package/src/audio_output.ts +30 -17
  67. package/src/conn.ts +46 -28
  68. package/src/errors.ts +4 -3
  69. package/src/index.ts +54 -0
  70. package/src/logging.ts +7 -1
  71. package/src/logging_internal.ts +23 -0
  72. package/src/recording.ts +271 -0
  73. package/src/runtime.ts +14 -5
  74. package/src/specs/NativeTiRtc.ts +123 -0
  75. package/src/store.ts +760 -0
  76. package/src/video_input.ts +97 -62
  77. package/src/video_output.ts +54 -16
@@ -1,6 +1,11 @@
1
1
  package com.tange.ai.tirtc.reactnative
2
2
 
3
- import android.util.Log
3
+ import android.Manifest
4
+ import android.content.ContentValues
5
+ import android.content.pm.PackageManager
6
+ import android.os.Build
7
+ import android.os.Environment
8
+ import android.provider.MediaStore
4
9
  import com.facebook.react.bridge.Promise
5
10
  import com.facebook.react.bridge.ReactApplicationContext
6
11
  import com.facebook.react.bridge.ReactMethod
@@ -13,21 +18,44 @@ import com.tange.ai.tirtc.TiRtcAudioInput
13
18
  import com.tange.ai.tirtc.TiRtcAudioOutput
14
19
  import com.tange.ai.tirtc.TiRtcConn
15
20
  import com.tange.ai.tirtc.TiRtcInitOptions
21
+ import com.tange.ai.tirtc.TiRtcLogging
22
+ import com.tange.ai.tirtc.TiRtcRecordingFile
23
+ import com.tange.ai.tirtc.TiRtcSnapshotFile
16
24
  import com.tange.ai.tirtc.TiRtcVideoInput
17
25
  import com.tange.ai.tirtc.TiRtcVideoOutput
26
+ import com.tange.ai.tirtc.TiStore
27
+ import com.tange.ai.tirtc.TiStoreAudioOutput
28
+ import com.tange.ai.tirtc.TiStoreExportRequest
29
+ import com.tange.ai.tirtc.TiStoreRecordingFile
30
+ import com.tange.ai.tirtc.TiStoreRecordingResult
31
+ import com.tange.ai.tirtc.TiStoreReplaySpeed
32
+ import com.tange.ai.tirtc.TiStoreSnapshotFile
33
+ import com.tange.ai.tirtc.TiStoreVideoOutput
34
+ import java.util.concurrent.ConcurrentHashMap
35
+ import java.util.concurrent.ArrayBlockingQueue
36
+ import java.util.concurrent.RejectedExecutionException
37
+ import java.util.concurrent.ThreadPoolExecutor
38
+ import java.util.concurrent.TimeUnit
39
+ import java.util.concurrent.atomic.AtomicBoolean
40
+ import java.util.concurrent.atomic.AtomicInteger
18
41
 
19
42
  @ReactModule(name = TiRtcModule.NAME)
20
43
  class TiRtcModule(
21
44
  private val reactContext: ReactApplicationContext,
22
45
  ) : NativeTiRtcSpec(reactContext) {
23
46
  private val registry = TiRtcReactNativeRegistry
47
+ private val invalidated = AtomicBoolean(false)
24
48
  private val events =
25
49
  TiRtcReactNativeEventSink(registry) { event ->
26
- reactContext.runOnJSQueueThread {
27
- emitOnTiRtcEvent(event)
50
+ if (!invalidated.get()) {
51
+ reactContext.runOnJSQueueThread {
52
+ if (!invalidated.get()) emitOnTiRtcEvent(event)
53
+ }
28
54
  }
29
55
  }
30
56
  private val eventBinder = TiRtcReactNativeEventBinder(events)
57
+ private val nextMediaFileId = AtomicInteger(1)
58
+ private val mediaFiles = ConcurrentHashMap<Int, Any>()
31
59
 
32
60
  init {
33
61
  registry.bindEventSink(events)
@@ -35,13 +63,22 @@ class TiRtcModule(
35
63
 
36
64
  override fun getName(): String = NAME
37
65
 
66
+ override fun invalidate() {
67
+ if (!invalidated.compareAndSet(false, true)) return
68
+ val objects = registry.drain(events)
69
+ val files = mediaFiles.values.toList()
70
+ mediaFiles.clear()
71
+ TiRtcReactNativeCleanup.release(objects, files)
72
+ super.invalidate()
73
+ }
74
+
38
75
  @ReactMethod
39
76
  override fun initialize(
40
77
  options: ReadableMap,
41
78
  promise: Promise,
42
79
  ) {
43
80
  val code =
44
- TiRtc.initialize(
81
+ TiRtc.init(
45
82
  reactContext,
46
83
  TiRtcInitOptions(
47
84
  appId = options.getString("appId") ?: "",
@@ -54,6 +91,11 @@ class TiRtcModule(
54
91
 
55
92
  override fun shutdown(): Double {
56
93
  if (registry.hasLiveObjects()) {
94
+ TiRtcLogging.w(
95
+ TAG,
96
+ "[react_native_shutdown] rejected: dispose all React Native objects before shutdown; " +
97
+ "error_code=1007 reason=objects_live",
98
+ )
57
99
  return TiRtcReactNativeErrors.RUNTIME_OBJECTS_LIVE.toDouble()
58
100
  }
59
101
  return TiRtcReactNativeErrors.normalizeAndroid(TiRtc.shutdown()).toDouble()
@@ -67,16 +109,290 @@ class TiRtcModule(
67
109
  message: String,
68
110
  ) {
69
111
  when (level.toInt()) {
70
- 0 -> com.tange.ai.tirtc.TiRtcLogging.d(tag, message)
71
- 1 -> com.tange.ai.tirtc.TiRtcLogging.i(tag, message)
72
- 2 -> com.tange.ai.tirtc.TiRtcLogging.w(tag, message)
73
- 3 -> com.tange.ai.tirtc.TiRtcLogging.e(tag, message)
112
+ 0 -> TiRtcLogging.d(tag, message)
113
+ 1 -> TiRtcLogging.i(tag, message)
114
+ 2 -> TiRtcLogging.w(tag, message)
115
+ 3 -> TiRtcLogging.e(tag, message)
116
+ }
117
+ }
118
+
119
+ @ReactMethod
120
+ override fun storeInitialize(options: ReadableMap, promise: Promise) {
121
+ val code = TiStore.init(
122
+ reactContext,
123
+ options.getString("appId") ?: "",
124
+ options.getString("endpoint") ?: "",
125
+ options.getBoolean("consoleLogEnabled"),
126
+ )
127
+ promise.resolve(code.toDouble())
128
+ }
129
+
130
+ override fun storeShutdown(): Double = TiStore.shutdown().toDouble()
131
+
132
+ override fun storeCreate(token: String): WritableMap =
133
+ registerReactNativeObject(
134
+ registry,
135
+ factory = { TiStore(token) },
136
+ onRegistered = { entry, value -> eventBinder.wireStore(entry.id, value as TiStore) },
137
+ )
138
+
139
+ override fun storeUpdateToken(id: Double, token: String): Double =
140
+ registry.store(id.toInt())?.updateToken(token).normalized()
141
+
142
+ @ReactMethod
143
+ override fun storeListRecordings(id: Double, startTimeMs: Double, endTimeMs: Double, promise: Promise) {
144
+ val store = registry.store(id.toInt())
145
+ if (store == null) {
146
+ promise.resolve(storeRecordingsResult(TiRtcReactNativeErrors.OBJECT_RELEASED, emptyList()))
147
+ return
148
+ }
149
+ store.listRecordings(startTimeMs.toLong(), endTimeMs.toLong()) { result ->
150
+ reactContext.runOnJSQueueThread {
151
+ promise.resolve(storeRecordingsResult(result.code, result.recordings))
152
+ }
153
+ }
154
+ }
155
+
156
+ @ReactMethod
157
+ override fun storeListRecordingDays(
158
+ id: Double,
159
+ startDate: String,
160
+ endDate: String,
161
+ timeZoneId: String,
162
+ promise: Promise,
163
+ ) {
164
+ val store = registry.store(id.toInt())
165
+ if (store == null) {
166
+ promise.resolve(storeRecordingDaysResult(TiRtcReactNativeErrors.OBJECT_RELEASED, emptyList()))
167
+ return
168
+ }
169
+ store.listRecordingDays(startDate, endDate, timeZoneId) { result ->
170
+ reactContext.runOnJSQueueThread {
171
+ promise.resolve(storeRecordingDaysResult(result.code, result.days))
172
+ }
173
+ }
174
+ }
175
+
176
+ override fun storeCreateReplay(id: Double): WritableMap {
177
+ val store = registry.store(id.toInt())
178
+ ?: return createResult(TiRtcReactNativeErrors.OBJECT_RELEASED, 0, 0)
179
+ return registerReactNativeObject(
180
+ registry,
181
+ factory = { store.createReplay() },
182
+ onRegistered = { entry, value -> eventBinder.wireStoreReplay(entry.id, value as com.tange.ai.tirtc.TiStoreReplay) },
183
+ )
184
+ }
185
+
186
+ override fun storeExportRecording(
187
+ id: Double,
188
+ startTimeMs: Double,
189
+ endTimeMs: Double,
190
+ videoChannelId: Double,
191
+ audioChannelId: Double?,
192
+ ): WritableMap {
193
+ val store = registry.store(id.toInt())
194
+ ?: return recordingStartResult(TiRtcReactNativeErrors.OBJECT_RELEASED, null)
195
+ val bridge = StoreExportBridge(::resolveStoreRecording)
196
+ val started = store.exportRecording(
197
+ TiStoreExportRequest(
198
+ startTimeMs.toLong(),
199
+ endTimeMs.toLong(),
200
+ videoChannelId.toInt(),
201
+ audioChannelId?.toInt(),
202
+ ),
203
+ null,
204
+ ) { result -> bridge.complete(result) }
205
+ val task = started.task ?: return recordingStartResult(started.code, null)
206
+ bridge.task = task
207
+ val entry = registry.add(bridge)
208
+ bridge.onDrained = { registry.release(entry.id) }
209
+ return recordingStartResult(started.code, entry.id)
210
+ }
211
+
212
+ override fun storeExportProgress(id: Double): Double =
213
+ registry.storeExportTask(id.toInt())?.task?.progress ?: 0.0
214
+
215
+ @ReactMethod
216
+ override fun storeExportResult(id: Double, promise: Promise) {
217
+ registry.storeExportTask(id.toInt())?.await(promise)
218
+ ?: promise.resolve(mp4Result(TiRtcReactNativeErrors.OBJECT_RELEASED, null, null, null))
219
+ }
220
+
221
+ override fun storeExportStop(id: Double): Double =
222
+ registry.storeExportTask(id.toInt())?.task?.stop().normalized()
223
+
224
+ override fun storeDispose(id: Double): Double {
225
+ val objectId = id.toInt()
226
+ val code = registry.store(objectId)?.dispose().normalized()
227
+ if (code.toInt() == TiRtcReactNativeErrors.OK) registry.release(objectId)
228
+ return code
229
+ }
230
+
231
+ override fun storeReplayGetInfo(id: Double): WritableMap {
232
+ val replay = registry.storeReplay(id.toInt())
233
+ ?: return storeReplayInfoResult(TiRtcReactNativeErrors.OBJECT_RELEASED, 0, null)
234
+ val speed = when (replay.speed) {
235
+ TiStoreReplaySpeed.X1 -> 0
236
+ TiStoreReplaySpeed.X2 -> 1
237
+ TiStoreReplaySpeed.X4 -> 2
238
+ TiStoreReplaySpeed.X8 -> 3
239
+ }
240
+ return storeReplayInfoResult(0, speed, replay.currentTimeMs)
241
+ }
242
+
243
+ override fun storeReplayPlay(
244
+ id: Double,
245
+ startTimeMs: Double,
246
+ endTimeMs: Double,
247
+ initialTimeMs: Double?,
248
+ ): Double {
249
+ val replay = registry.storeReplay(id.toInt()) ?: return TiRtcReactNativeErrors.OBJECT_RELEASED.toDouble()
250
+ return replay.play(startTimeMs.toLong(), endTimeMs.toLong(), initialTimeMs?.toLong()).toDouble()
251
+ }
252
+
253
+ override fun storeReplayPause(id: Double): Double = registry.storeReplay(id.toInt())?.pause().normalized()
254
+
255
+ override fun storeReplayResume(id: Double): Double = registry.storeReplay(id.toInt())?.resume().normalized()
256
+
257
+ override fun storeReplaySeek(id: Double, timeMs: Double): Double =
258
+ registry.storeReplay(id.toInt())?.seek(timeMs.toLong()).normalized()
259
+
260
+ override fun storeReplaySetSpeed(id: Double, speed: Double): Double {
261
+ val value = when (speed.toInt()) {
262
+ 0 -> TiStoreReplaySpeed.X1
263
+ 1 -> TiStoreReplaySpeed.X2
264
+ 2 -> TiStoreReplaySpeed.X4
265
+ 3 -> TiStoreReplaySpeed.X8
266
+ else -> return TiRtcReactNativeErrors.INVALID_ARGUMENT.toDouble()
267
+ }
268
+ return registry.storeReplay(id.toInt())?.setSpeed(value).normalized()
269
+ }
270
+
271
+ override fun storeReplayStartRecording(
272
+ id: Double,
273
+ videoChannelId: Double,
274
+ audioChannelId: Double?,
275
+ ): WritableMap {
276
+ val replay = registry.storeReplay(id.toInt())
277
+ ?: return recordingStartResult(TiRtcReactNativeErrors.OBJECT_RELEASED, null)
278
+ val result = replay.startRecording(videoChannelId.toInt(), audioChannelId?.toInt())
279
+ val entry = result.task?.let(registry::add)
280
+ return recordingStartResult(result.code, entry?.id)
281
+ }
282
+
283
+ @ReactMethod
284
+ override fun storeRecordingTaskStop(id: Double, promise: Promise) {
285
+ val objectId = id.toInt()
286
+ val task = registry.storeRecordingTask(objectId)
287
+ if (task == null) {
288
+ promise.resolve(mp4Result(TiRtcReactNativeErrors.OBJECT_RELEASED, null, null, null))
289
+ return
290
+ }
291
+ task.stop { result ->
292
+ registry.release(objectId)
293
+ resolveStoreRecording(promise, result)
294
+ }
295
+ }
296
+
297
+ override fun storeReplayStop(id: Double): Double = registry.storeReplay(id.toInt())?.stop().normalized()
298
+
299
+ override fun storeReplayDispose(id: Double): Double {
300
+ val objectId = id.toInt()
301
+ val code = registry.storeReplay(objectId)?.dispose().normalized()
302
+ if (code.toInt() == TiRtcReactNativeErrors.OK) registry.release(objectId)
303
+ return code
304
+ }
305
+
306
+ override fun storeCreateAudioOutput(): WritableMap =
307
+ registerReactNativeObject(
308
+ registry,
309
+ factory = { TiStoreAudioOutput() },
310
+ onRegistered = { entry, value -> eventBinder.wireStoreAudioOutput(entry.id, value as TiStoreAudioOutput) },
311
+ )
312
+
313
+ override fun storeAudioOutputGetState(id: Double): WritableMap {
314
+ val output = registry.storeAudioOutput(id.toInt())
315
+ ?: return stateResult(TiRtcReactNativeErrors.OBJECT_RELEASED, null)
316
+ return stateResult(0, output.state.name.lowercase())
317
+ }
318
+
319
+ override fun storeAudioOutputSetVolume(id: Double, volumePercent: Double): Double =
320
+ registry.storeAudioOutput(id.toInt())?.setVolume(volumePercent.toInt()).normalized()
321
+
322
+ override fun storeAudioOutputAttach(outputId: Double, replayId: Double, channelId: Double): Double {
323
+ val replay = registry.storeReplay(replayId.toInt())
324
+ ?: return TiRtcReactNativeErrors.OBJECT_RELEASED.toDouble()
325
+ return registry.storeAudioOutput(outputId.toInt())?.attach(replay, channelId.toInt()).normalized()
326
+ }
327
+
328
+ override fun storeAudioOutputDetach(outputId: Double): Double =
329
+ registry.storeAudioOutput(outputId.toInt())?.detach().normalized()
330
+
331
+ override fun storeAudioOutputDispose(id: Double): Double {
332
+ val objectId = id.toInt()
333
+ val code = registry.storeAudioOutput(objectId)?.dispose().normalized()
334
+ if (code.toInt() == TiRtcReactNativeErrors.OK) registry.release(objectId)
335
+ return code
336
+ }
337
+
338
+ override fun storeCreateVideoOutput(): WritableMap =
339
+ registerReactNativeObject(
340
+ registry,
341
+ factory = { TiStoreVideoOutput() },
342
+ onRegistered = { entry, value -> eventBinder.wireStoreVideoOutput(entry.id, value as TiStoreVideoOutput) },
343
+ )
344
+
345
+ override fun storeVideoOutputGetState(id: Double): WritableMap {
346
+ val output = registry.storeVideoOutput(id.toInt())
347
+ ?: return stateResult(TiRtcReactNativeErrors.OBJECT_RELEASED, null)
348
+ return stateResult(0, output.state.name.lowercase())
349
+ }
350
+
351
+ override fun storeVideoOutputAttach(outputId: Double, replayId: Double, channelId: Double): Double {
352
+ val replay = registry.storeReplay(replayId.toInt())
353
+ ?: return TiRtcReactNativeErrors.OBJECT_RELEASED.toDouble()
354
+ return registry.storeVideoOutput(outputId.toInt())?.attach(replay, channelId.toInt()).normalized()
355
+ }
356
+
357
+ override fun storeVideoOutputDetach(outputId: Double): Double =
358
+ registry.storeVideoOutput(outputId.toInt())?.detach().normalized()
359
+
360
+ @ReactMethod
361
+ override fun storeVideoOutputTakeSnapshot(id: Double, promise: Promise) {
362
+ val output = registry.storeVideoOutput(id.toInt())
363
+ if (output == null) {
364
+ promise.resolve(mediaSnapshotResult(TiRtcReactNativeErrors.OBJECT_RELEASED, null, null))
365
+ return
366
+ }
367
+ output.takeSnapshot { result ->
368
+ val fileId = result.file?.let { file ->
369
+ nextMediaFileId.getAndIncrement().also { mediaFiles[it] = file }
370
+ }
371
+ reactContext.runOnJSQueueThread {
372
+ promise.resolve(mediaSnapshotResult(result.code, fileId, result.file?.path))
373
+ }
374
+ }
375
+ }
376
+
377
+ override fun storeVideoOutputDispose(id: Double): Double {
378
+ val objectId = id.toInt()
379
+ val code = registry.storeVideoOutput(objectId)?.dispose().normalized()
380
+ if (code.toInt() == TiRtcReactNativeErrors.OK) registry.release(objectId)
381
+ return code
382
+ }
383
+
384
+ private fun resolveStoreRecording(promise: Promise, result: TiStoreRecordingResult) {
385
+ val fileId = result.file?.let { file ->
386
+ nextMediaFileId.getAndIncrement().also { mediaFiles[it] = file }
387
+ }
388
+ reactContext.runOnJSQueueThread {
389
+ promise.resolve(mp4Result(result.code, fileId, result.file?.path, result.file?.durationMs))
74
390
  }
75
391
  }
76
392
 
77
393
  @ReactMethod
78
394
  override fun uploadLogs(promise: Promise) {
79
- uploadLogsToPromise(promise, TAG)
395
+ uploadLogsToPromise(promise)
80
396
  }
81
397
 
82
398
  override fun createConn(): WritableMap =
@@ -95,22 +411,9 @@ class TiRtcModule(
95
411
  id: Double,
96
412
  remoteId: String,
97
413
  token: String,
98
- ): Double {
99
- val objectId = id.toInt()
100
- val code = registry.conn(objectId)?.connect(remoteId, token).normalized()
101
- Log.i(
102
- TAG,
103
- "connConnect objectId=$objectId remoteIdPresent=${remoteId.isNotEmpty()} tokenPresent=${token.isNotEmpty()} code=${code.toInt()}",
104
- )
105
- return code
106
- }
414
+ ): Double = registry.conn(id.toInt())?.connect(remoteId, token).normalized()
107
415
 
108
- override fun connDisconnect(id: Double): Double {
109
- val objectId = id.toInt()
110
- val code = registry.conn(objectId)?.disconnect().normalized()
111
- Log.i(TAG, "connDisconnect objectId=$objectId code=${code.toInt()}")
112
- return code
113
- }
416
+ override fun connDisconnect(id: Double): Double = registry.conn(id.toInt())?.disconnect().normalized()
114
417
 
115
418
  override fun connSendCommand(
116
419
  id: Double,
@@ -143,6 +446,196 @@ class TiRtcModule(
143
446
  override fun connRequestKeyFrame(id: Double, streamId: Double): Double =
144
447
  registry.conn(id.toInt())?.requestKeyFrame(streamId.toInt()).normalized()
145
448
 
449
+ override fun connStartRecording(id: Double, options: ReadableMap): WritableMap {
450
+ val conn = registry.conn(id.toInt())
451
+ if (conn == null) {
452
+ return recordingStartResult(TiRtcReactNativeErrors.OBJECT_RELEASED, null)
453
+ }
454
+ val result = conn.startRecording(
455
+ videoStreamId = options.getInt("videoStreamId"),
456
+ audioStreamId = if (options.isNull("audioStreamId")) null else options.getInt("audioStreamId"),
457
+ )
458
+ val entry = result.task?.let { registry.add(it) }
459
+ return recordingStartResult(result.code, entry?.id)
460
+ }
461
+
462
+ @ReactMethod
463
+ override fun recordingTaskStop(id: Double, promise: Promise) {
464
+ val objectId = id.toInt()
465
+ val task = registry.recordingTask(objectId)
466
+ if (task == null) {
467
+ promise.resolve(mp4Result(TiRtcReactNativeErrors.OBJECT_RELEASED, null, null, null))
468
+ return
469
+ }
470
+ task.stop { result ->
471
+ registry.release(objectId)
472
+ val fileId = result.file?.let { file ->
473
+ nextMediaFileId.getAndIncrement().also { mediaFiles[it] = file }
474
+ }
475
+ reactContext.runOnJSQueueThread {
476
+ promise.resolve(mp4Result(result.code, fileId, result.file?.path, result.file?.durationMs))
477
+ }
478
+ }
479
+ }
480
+
481
+ @ReactMethod
482
+ override fun mediaFileMoveToGallery(id: Double, fileName: String?, promise: Promise) {
483
+ val fileId = id.toInt()
484
+ val mediaFile = mediaFiles[fileId]
485
+ val path = when (mediaFile) {
486
+ is TiRtcRecordingFile -> mediaFile.path
487
+ is TiRtcSnapshotFile -> mediaFile.path
488
+ is TiStoreRecordingFile -> mediaFile.path
489
+ is TiStoreSnapshotFile -> mediaFile.path
490
+ else -> {
491
+ promise.resolve(galleryResult(TiRtcReactNativeErrors.OBJECT_RELEASED, null))
492
+ return
493
+ }
494
+ }
495
+ val video = mediaFile is TiRtcRecordingFile || mediaFile is TiStoreRecordingFile
496
+ val normalizedName = normalizeGalleryFileName(fileName, video)
497
+ if (normalizedName == null) {
498
+ promise.resolve(galleryResult(ERROR_INVALID_ARGUMENT, null))
499
+ return
500
+ }
501
+ if (
502
+ Build.VERSION.SDK_INT <= Build.VERSION_CODES.P &&
503
+ reactContext.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
504
+ ) {
505
+ promise.resolve(galleryResult(ERROR_PERMISSION_DENIED, null))
506
+ return
507
+ }
508
+ try {
509
+ mediaFileExecutor.execute {
510
+ publishMediaFile(
511
+ fileId,
512
+ mediaFile,
513
+ path,
514
+ video,
515
+ normalizedName,
516
+ promise,
517
+ )
518
+ }
519
+ } catch (_: RejectedExecutionException) {
520
+ promise.resolve(galleryResult(ERROR_RESOURCE_EXHAUSTED, null))
521
+ }
522
+ }
523
+
524
+ private fun publishMediaFile(
525
+ fileId: Int,
526
+ mediaFile: Any,
527
+ path: String,
528
+ video: Boolean,
529
+ fileName: String,
530
+ promise: Promise,
531
+ ) {
532
+ val source = java.io.File(path)
533
+ if (!source.isFile) {
534
+ reactContext.runOnJSQueueThread { promise.resolve(galleryResult(ERROR_FILE_READ_FAILED, null)) }
535
+ return
536
+ }
537
+ val collection = if (video) MediaStore.Video.Media.EXTERNAL_CONTENT_URI else MediaStore.Images.Media.EXTERNAL_CONTENT_URI
538
+ val displayName = fileName
539
+ val legacyTarget = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
540
+ val directory = if (video) Environment.DIRECTORY_MOVIES else Environment.DIRECTORY_PICTURES
541
+ val targetDirectory = java.io.File(
542
+ Environment.getExternalStoragePublicDirectory(directory),
543
+ "TiRTC",
544
+ )
545
+ if (!targetDirectory.exists() && !targetDirectory.mkdirs()) {
546
+ reactContext.runOnJSQueueThread {
547
+ promise.resolve(galleryResult(ERROR_FILE_WRITE_FAILED, null))
548
+ }
549
+ return
550
+ }
551
+ java.io.File(targetDirectory, displayName)
552
+ } else {
553
+ null
554
+ }
555
+ val values = ContentValues().apply {
556
+ put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
557
+ put(MediaStore.MediaColumns.MIME_TYPE, if (video) "video/mp4" else "image/jpeg")
558
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
559
+ put(MediaStore.MediaColumns.RELATIVE_PATH, "${if (video) "Movies" else "Pictures"}/TiRTC")
560
+ put(MediaStore.MediaColumns.IS_PENDING, 1)
561
+ } else {
562
+ @Suppress("DEPRECATION")
563
+ put(MediaStore.MediaColumns.DATA, legacyTarget!!.absolutePath)
564
+ }
565
+ }
566
+ val resolver = reactContext.contentResolver
567
+ val target = try {
568
+ resolver.insert(collection, values)
569
+ } catch (_: SecurityException) {
570
+ reactContext.runOnJSQueueThread { promise.resolve(galleryResult(ERROR_PERMISSION_DENIED, null)) }
571
+ return
572
+ } catch (_: Throwable) {
573
+ null
574
+ }
575
+ if (target == null) {
576
+ reactContext.runOnJSQueueThread { promise.resolve(galleryResult(ERROR_FILE_WRITE_FAILED, null)) }
577
+ return
578
+ }
579
+ try {
580
+ resolver.openOutputStream(target)?.use { output ->
581
+ source.inputStream().use { input -> input.copyTo(output) }
582
+ } ?: error("unable to open media asset")
583
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
584
+ values.clear()
585
+ values.put(MediaStore.MediaColumns.IS_PENDING, 0)
586
+ if (resolver.update(target, values, null, null) != 1) error("unable to publish media asset")
587
+ }
588
+ } catch (error: Throwable) {
589
+ try {
590
+ resolver.delete(target, null, null)
591
+ } catch (_: Throwable) {
592
+ }
593
+ val code = if (error is SecurityException) ERROR_PERMISSION_DENIED else ERROR_FILE_WRITE_FAILED
594
+ reactContext.runOnJSQueueThread { promise.resolve(galleryResult(code, null)) }
595
+ return
596
+ }
597
+ val onDelete: (Int) -> Unit = { code ->
598
+ if (code == 0) {
599
+ mediaFiles.remove(fileId, mediaFile)
600
+ } else {
601
+ try {
602
+ resolver.delete(target, null, null)
603
+ } catch (_: Throwable) {
604
+ }
605
+ }
606
+ reactContext.runOnJSQueueThread {
607
+ promise.resolve(galleryResult(code, if (code == 0) target.toString() else null))
608
+ }
609
+ }
610
+ when (mediaFile) {
611
+ is TiRtcRecordingFile -> mediaFile.delete(onDelete)
612
+ is TiRtcSnapshotFile -> mediaFile.delete(onDelete)
613
+ is TiStoreRecordingFile -> mediaFile.delete(onDelete)
614
+ is TiStoreSnapshotFile -> mediaFile.delete(onDelete)
615
+ }
616
+ }
617
+
618
+ @ReactMethod
619
+ override fun mediaFileDelete(id: Double, promise: Promise) {
620
+ val fileId = id.toInt()
621
+ val mediaFile = mediaFiles[fileId]
622
+ val callback: (Int) -> Unit = { code ->
623
+ if (code == 0) mediaFiles.remove(fileId)
624
+ reactContext.runOnJSQueueThread { promise.resolve(code.toDouble()) }
625
+ }
626
+ when (mediaFile) {
627
+ is TiRtcRecordingFile -> mediaFile.delete(callback)
628
+ is TiRtcSnapshotFile -> mediaFile.delete(callback)
629
+ is TiStoreRecordingFile -> mediaFile.delete(callback)
630
+ is TiStoreSnapshotFile -> mediaFile.delete(callback)
631
+ else -> promise.resolve(TiRtcReactNativeErrors.OBJECT_RELEASED.toDouble())
632
+ }
633
+ }
634
+
635
+ override fun mediaFileRelease(id: Double) {
636
+ mediaFiles.remove(id.toInt())
637
+ }
638
+
146
639
  override fun connGetMetricsSnapshot(id: Double): WritableMap {
147
640
  val conn = registry.conn(id.toInt()) ?: return releasedSnapshotResult()
148
641
  return connMetricsResult(conn.getMetricsSnapshot())
@@ -181,6 +674,11 @@ class TiRtcModule(
181
674
  volumePercent < 0.0 ||
182
675
  volumePercent > 100.0
183
676
  ) {
677
+ TiRtcLogging.w(
678
+ TAG,
679
+ "[react_native_audio_output_set_volume] rejected: volume must be an integer from 0 to 100; " +
680
+ "error_code=6000 reason=invalid_volume",
681
+ )
184
682
  return TiRtcReactNativeErrors.INVALID_ARGUMENT.toDouble()
185
683
  }
186
684
  return registry.audioOutput(id.toInt())?.setVolume(volumePercent.toInt()).normalized()
@@ -259,6 +757,23 @@ class TiRtcModule(
259
757
  return videoDebugResult(output.getDebugSnapshot())
260
758
  }
261
759
 
760
+ @ReactMethod
761
+ override fun videoOutputTakeSnapshot(id: Double, promise: Promise) {
762
+ val output = registry.videoOutput(id.toInt())
763
+ if (output == null) {
764
+ promise.resolve(mediaSnapshotResult(TiRtcReactNativeErrors.OBJECT_RELEASED, null, null))
765
+ return
766
+ }
767
+ output.takeSnapshot { snapshotResult ->
768
+ val fileId = snapshotResult.file?.let { file ->
769
+ nextMediaFileId.getAndIncrement().also { mediaFiles[it] = file }
770
+ }
771
+ reactContext.runOnJSQueueThread {
772
+ promise.resolve(mediaSnapshotResult(snapshotResult.code, fileId, snapshotResult.file?.path))
773
+ }
774
+ }
775
+ }
776
+
262
777
  override fun videoOutputDispose(id: Double): Double {
263
778
  val objectId = id.toInt()
264
779
  val code = registry.videoOutput(objectId)?.dispose().normalized()
@@ -310,7 +825,6 @@ class TiRtcModule(
310
825
  ?: TiRtcReactNativeErrors.OBJECT_RELEASED,
311
826
  )
312
827
  }
313
- Log.i(TAG, "audioInputAttach objectId=${id.toInt()} connId=${connId.toInt()} streamId=${streamId.toInt()} code=$code")
314
828
  promise.resolve(code.toDouble())
315
829
  }
316
830
 
@@ -324,7 +838,6 @@ class TiRtcModule(
324
838
  registry.audioInput(id.toInt())?.start()
325
839
  ?: TiRtcReactNativeErrors.OBJECT_RELEASED,
326
840
  )
327
- Log.i(TAG, "audioInputStart objectId=${id.toInt()} code=$code")
328
841
  promise.resolve(code.toDouble())
329
842
  }
330
843
 
@@ -420,7 +933,6 @@ class TiRtcModule(
420
933
  ?: TiRtcReactNativeErrors.OBJECT_RELEASED,
421
934
  )
422
935
  }
423
- Log.i(TAG, "videoInputAttach objectId=${id.toInt()} connId=${connId.toInt()} streamId=${streamId.toInt()} code=$code")
424
936
  promise.resolve(code.toDouble())
425
937
  }
426
938
 
@@ -434,7 +946,6 @@ class TiRtcModule(
434
946
  registry.videoInput(id.toInt())?.start()
435
947
  ?: TiRtcReactNativeErrors.OBJECT_RELEASED,
436
948
  )
437
- Log.i(TAG, "videoInputStart objectId=${id.toInt()} code=$code")
438
949
  promise.resolve(code.toDouble())
439
950
  }
440
951
 
@@ -478,5 +989,51 @@ class TiRtcModule(
478
989
  companion object {
479
990
  const val NAME = "TiRtc"
480
991
  private const val TAG = "TiRtcReactNative"
992
+ private const val ERROR_INVALID_ARGUMENT = 6000
993
+ private const val ERROR_PERMISSION_DENIED = 6024
994
+ private const val ERROR_RESOURCE_EXHAUSTED = 6043
995
+ private const val ERROR_FILE_READ_FAILED = 6045
996
+ private const val ERROR_FILE_WRITE_FAILED = 6046
997
+ private val mediaFileExecutor =
998
+ ThreadPoolExecutor(
999
+ 1,
1000
+ 2,
1001
+ 30L,
1002
+ TimeUnit.SECONDS,
1003
+ ArrayBlockingQueue(16),
1004
+ { work -> Thread(work, "tirtc-rn-media-file").apply { isDaemon = true } },
1005
+ ThreadPoolExecutor.AbortPolicy(),
1006
+ )
1007
+
1008
+ private fun normalizeGalleryFileName(fileName: String?, video: Boolean): String? {
1009
+ if (fileName == null) {
1010
+ return "${System.currentTimeMillis()}.${if (video) "mp4" else "jpg"}"
1011
+ }
1012
+ if (
1013
+ fileName.isEmpty() ||
1014
+ fileName.isBlank() ||
1015
+ fileName == "." ||
1016
+ fileName == ".." ||
1017
+ fileName.contains('/') ||
1018
+ fileName.contains('\\') ||
1019
+ fileName.contains('\u0000')
1020
+ ) {
1021
+ return null
1022
+ }
1023
+ val extension = if (video) ".mp4" else ".jpg"
1024
+ val stem: String
1025
+ val normalized: String
1026
+ if (fileName.endsWith(extension, ignoreCase = true)) {
1027
+ stem = fileName.dropLast(extension.length)
1028
+ normalized = fileName
1029
+ } else if (fileName.contains('.')) {
1030
+ return null
1031
+ } else {
1032
+ stem = fileName
1033
+ normalized = fileName + extension
1034
+ }
1035
+ val stemBytes = stem.toByteArray(Charsets.UTF_8).size
1036
+ return normalized.takeIf { stemBytes in 1..200 }
1037
+ }
481
1038
  }
482
1039
  }