tirtc-react-native 2.3.1 → 2.4.0-alpha.4

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 +622 -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 +32 -8
  8. package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeEventSink.kt +16 -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/TiRtcReactNativeStorageSupport.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+Storage.mm +513 -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 +102 -0
  23. package/ios/TiRtcReactNativeEventSink.h +3 -0
  24. package/ios/TiRtcReactNativeEventSink.mm +16 -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/storage.js +706 -0
  48. package/lib/module/storage.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/storage.d.ts +204 -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/storage.ts +769 -0
  76. package/src/video_input.ts +97 -62
  77. package/src/video_output.ts +54 -16
@@ -0,0 +1,115 @@
1
+ package com.tange.ai.tirtc.reactnative
2
+
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+ import com.tange.ai.tirtc.TiRtc
6
+ import com.tange.ai.tirtc.TiRtcAudioInput
7
+ import com.tange.ai.tirtc.TiRtcAudioOutput
8
+ import com.tange.ai.tirtc.TiRtcConn
9
+ import com.tange.ai.tirtc.TiRtcLogging
10
+ import com.tange.ai.tirtc.TiRtcRecordingFile
11
+ import com.tange.ai.tirtc.TiRtcRecordingTask
12
+ import com.tange.ai.tirtc.TiRtcSnapshotFile
13
+ import com.tange.ai.tirtc.TiRtcVideoInput
14
+ import com.tange.ai.tirtc.TiRtcVideoOutput
15
+ import com.tange.ai.tirtc.TiCloudStorage
16
+ import com.tange.ai.tirtc.TiCloudStorageAudioOutput
17
+ import com.tange.ai.tirtc.TiCloudStorageRecordingFile
18
+ import com.tange.ai.tirtc.TiCloudStorageRecordingTask
19
+ import com.tange.ai.tirtc.TiCloudStorageReplay
20
+ import com.tange.ai.tirtc.TiCloudStorageSnapshotFile
21
+ import com.tange.ai.tirtc.TiCloudStorageVideoOutput
22
+ import java.util.concurrent.atomic.AtomicInteger
23
+
24
+ internal object TiRtcReactNativeCleanup {
25
+ private const val TAG = "TIRTC_REACT_NATIVE"
26
+
27
+ fun release(objects: List<Any>, files: List<Any>) {
28
+ val pending = AtomicInteger(1)
29
+ fun retain() {
30
+ pending.incrementAndGet()
31
+ }
32
+ fun finish() {
33
+ if (pending.decrementAndGet() == 0) {
34
+ Handler(Looper.getMainLooper()).post { releaseOwners(objects) }
35
+ }
36
+ }
37
+ fun delete(file: Any?) {
38
+ if (file == null) return
39
+ retain()
40
+ val completed: (Int) -> Unit = { code ->
41
+ if (code != 0) logFailure("media_delete", code)
42
+ finish()
43
+ }
44
+ when (file) {
45
+ is TiRtcRecordingFile -> file.delete(completed)
46
+ is TiRtcSnapshotFile -> file.delete(completed)
47
+ is TiCloudStorageRecordingFile -> file.delete(completed)
48
+ is TiCloudStorageSnapshotFile -> file.delete(completed)
49
+ else -> finish()
50
+ }
51
+ }
52
+
53
+ files.forEach(::delete)
54
+ objects.forEach { value ->
55
+ when (value) {
56
+ is TiRtcRecordingTask -> {
57
+ retain()
58
+ value.stop { result ->
59
+ delete(result.file)
60
+ finish()
61
+ }
62
+ }
63
+ is TiCloudStorageRecordingTask -> {
64
+ retain()
65
+ value.stop { result ->
66
+ delete(result.file)
67
+ finish()
68
+ }
69
+ }
70
+ is CloudStorageExportBridge -> {
71
+ retain()
72
+ value.invalidate { result ->
73
+ delete(result?.file)
74
+ finish()
75
+ }
76
+ }
77
+ }
78
+ }
79
+ finish()
80
+ }
81
+
82
+ private fun releaseOwners(objects: List<Any>) {
83
+ objects.filterIsInstance<TiCloudStorageAudioOutput>().forEach { output ->
84
+ logFailure("ti-cloud-storage-audio-detach", output.detach())
85
+ logFailure("ti-cloud-storage-audio-dispose", output.dispose())
86
+ }
87
+ 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()) }
96
+ objects.filterIsInstance<TiCloudStorageReplay>().forEach { replay ->
97
+ logFailure("ti-cloud-storage-replay-stop", replay.stop())
98
+ logFailure("ti-cloud-storage-replay-dispose", replay.dispose())
99
+ }
100
+ objects.filterIsInstance<TiRtcConn>().forEach { conn ->
101
+ logFailure("conn_disconnect", conn.disconnect())
102
+ logFailure("conn_dispose", conn.dispose())
103
+ }
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())
107
+ TiRtcLogging.i(TAG, "[module_invalidate] native owners released")
108
+ }
109
+
110
+ private fun logFailure(operation: String, code: Int) {
111
+ if (code != 0 && code != TiRtcReactNativeErrors.OBJECT_RELEASED) {
112
+ TiRtcLogging.w(TAG, "[module_invalidate] operation=$operation error_code=$code")
113
+ }
114
+ }
115
+ }
@@ -1,6 +1,5 @@
1
1
  package com.tange.ai.tirtc.reactnative
2
2
 
3
- import android.util.Log
4
3
  import com.tange.ai.tirtc.TiRtcAudioInput
5
4
  import com.tange.ai.tirtc.TiRtcAudioOutput
6
5
  import com.tange.ai.tirtc.TiRtcAudioOutputErrorListener
@@ -17,16 +16,47 @@ import com.tange.ai.tirtc.TiRtcVideoOutput
17
16
  import com.tange.ai.tirtc.TiRtcVideoOutputErrorListener
18
17
  import com.tange.ai.tirtc.TiRtcVideoOutputRenderSizeListener
19
18
  import com.tange.ai.tirtc.TiRtcVideoOutputStateListener
19
+ import com.tange.ai.tirtc.TiCloudStorage
20
+ import com.tange.ai.tirtc.TiCloudStorageAudioOutput
21
+ import com.tange.ai.tirtc.TiCloudStorageAudioOutputStateListener
22
+ import com.tange.ai.tirtc.TiCloudStorageOutputErrorListener
23
+ import com.tange.ai.tirtc.TiCloudStorageReplay
24
+ import com.tange.ai.tirtc.TiCloudStorageReplayCompletedListener
25
+ import com.tange.ai.tirtc.TiCloudStorageReplayErrorListener
26
+ import com.tange.ai.tirtc.TiCloudStorageTimeChangedListener
27
+ import com.tange.ai.tirtc.TiCloudStorageVideoOutput
28
+ import com.tange.ai.tirtc.TiCloudStorageVideoOutputStateListener
20
29
 
21
30
  internal class TiRtcReactNativeEventBinder(
22
31
  private val events: TiRtcReactNativeEventSink,
23
32
  ) {
33
+ fun wireCloudStorage(id: Int, cloudStorage: TiCloudStorage) = Unit
34
+
35
+ fun wireCloudStorageReplay(id: Int, replay: TiCloudStorageReplay) {
36
+ replay.onTimeChanged = TiCloudStorageTimeChangedListener { events.tiCloudStorageReplayTimeChanged(id, it) }
37
+ replay.onCompleted = TiCloudStorageReplayCompletedListener { events.tiCloudStorageReplayCompleted(id) }
38
+ replay.onError = TiCloudStorageReplayErrorListener { events.tiCloudStorageReplayError(id, it) }
39
+ }
40
+
41
+ fun wireCloudStorageAudioOutput(id: Int, output: TiCloudStorageAudioOutput) {
42
+ output.onStateChanged = TiCloudStorageAudioOutputStateListener {
43
+ events.audioOutputStateChanged(id, it.name.lowercase())
44
+ }
45
+ output.onError = TiCloudStorageOutputErrorListener { events.audioOutputError(id, it) }
46
+ }
47
+
48
+ fun wireCloudStorageVideoOutput(id: Int, output: TiCloudStorageVideoOutput) {
49
+ output.onStateChanged = TiCloudStorageVideoOutputStateListener {
50
+ events.videoOutputStateChanged(id, it.name.lowercase())
51
+ }
52
+ output.onError = TiCloudStorageOutputErrorListener { events.videoOutputError(id, it) }
53
+ }
54
+
24
55
  fun wireConn(
25
56
  id: Int,
26
57
  conn: TiRtcConn,
27
58
  ) {
28
59
  conn.onStateChanged = TiRtcConnStateListener { state, errorCode ->
29
- Log.i(TAG, "connStateChanged objectId=$id state=${state.name.lowercase()} code=$errorCode")
30
60
  events.connStateChanged(id, state.name.lowercase(), errorCode)
31
61
  }
32
62
  conn.onCommand = TiRtcConnCommandListener { command, data ->
@@ -69,7 +99,6 @@ internal class TiRtcReactNativeEventBinder(
69
99
  input: TiRtcAudioInput,
70
100
  ) {
71
101
  input.onStateChanged = TiRtcInputStateListener { state ->
72
- Log.i(TAG, "audioInputStateChanged objectId=$id state=${state.name.lowercase()}")
73
102
  events.audioInputStateChanged(id, state.name.lowercase())
74
103
  }
75
104
  input.onError = TiRtcInputErrorListener { code, message ->
@@ -82,7 +111,6 @@ internal class TiRtcReactNativeEventBinder(
82
111
  input: TiRtcVideoInput,
83
112
  ) {
84
113
  input.onStateChanged = TiRtcInputStateListener { state ->
85
- Log.i(TAG, "videoInputStateChanged objectId=$id state=${state.name.lowercase()}")
86
114
  events.videoInputStateChanged(id, state.name.lowercase())
87
115
  }
88
116
  input.onActualConfigChanged = TiRtcVideoInputActualConfigListener { size, fps ->
@@ -92,8 +120,4 @@ internal class TiRtcReactNativeEventBinder(
92
120
  events.videoInputError(id, code, message)
93
121
  }
94
122
  }
95
-
96
- private companion object {
97
- const val TAG = "TiRtcReactNative"
98
- }
99
123
  }
@@ -44,6 +44,22 @@ internal class TiRtcReactNativeEventSink(
44
44
  })
45
45
  }
46
46
 
47
+ fun tiCloudStorageReplayTimeChanged(objectId: Int, timestampMs: Long) {
48
+ emit(base("tiCloudStorageReplayTimeChanged", objectId).apply {
49
+ putDouble("timestampMs", timestampMs.toDouble())
50
+ })
51
+ }
52
+
53
+ fun tiCloudStorageReplayCompleted(objectId: Int) {
54
+ emit(base("tiCloudStorageReplayCompleted", objectId))
55
+ }
56
+
57
+ fun tiCloudStorageReplayError(objectId: Int, code: Int) {
58
+ emit(base("tiCloudStorageReplayError", objectId).apply {
59
+ putInt("code", TiRtcReactNativeErrors.normalizeAndroid(code))
60
+ })
61
+ }
62
+
47
63
  fun audioOutputStateChanged(
48
64
  objectId: Int,
49
65
  state: String,
@@ -2,6 +2,68 @@ package com.tange.ai.tirtc.reactnative
2
2
 
3
3
  import com.facebook.react.bridge.Arguments
4
4
  import com.facebook.react.bridge.WritableMap
5
+ import com.tange.ai.tirtc.TiCloudStorageRecordingRange
6
+ import com.tange.ai.tirtc.TiCloudStorageRecordingDay
7
+
8
+ internal fun recordingStartResult(code: Int, taskId: Int?): WritableMap =
9
+ Arguments.createMap().apply {
10
+ putInt("code", TiRtcReactNativeErrors.normalizeAndroid(code))
11
+ if (taskId == null) putNull("taskId") else putInt("taskId", taskId)
12
+ }
13
+
14
+ internal fun tiCloudStorageRecordingsResult(code: Int, ranges: List<TiCloudStorageRecordingRange>): WritableMap =
15
+ Arguments.createMap().apply {
16
+ putInt("code", code)
17
+ putArray("recordings", Arguments.createArray().apply {
18
+ ranges.forEach { range ->
19
+ pushMap(Arguments.createMap().apply {
20
+ putDouble("startTimeMs", range.startTimeMs.toDouble())
21
+ putDouble("endTimeMs", range.endTimeMs.toDouble())
22
+ })
23
+ }
24
+ })
25
+ }
26
+
27
+ internal fun tiCloudStorageRecordingDaysResult(code: Int, days: List<TiCloudStorageRecordingDay>): WritableMap =
28
+ Arguments.createMap().apply {
29
+ putInt("code", code)
30
+ putArray("days", Arguments.createArray().apply {
31
+ days.forEach { day ->
32
+ pushMap(Arguments.createMap().apply {
33
+ putString("date", day.date)
34
+ putBoolean("hasRecording", day.hasRecording)
35
+ })
36
+ }
37
+ })
38
+ }
39
+
40
+ internal fun tiCloudStorageReplayInfoResult(code: Int, speed: Int, currentTimeMs: Long?): WritableMap =
41
+ Arguments.createMap().apply {
42
+ putInt("code", code)
43
+ putInt("speed", speed)
44
+ if (currentTimeMs == null) putNull("currentTimeMs") else putDouble("currentTimeMs", currentTimeMs.toDouble())
45
+ }
46
+
47
+ internal fun mp4Result(code: Int, fileId: Int?, filePath: String?, durationMs: Long?): WritableMap =
48
+ Arguments.createMap().apply {
49
+ putInt("code", TiRtcReactNativeErrors.normalizeAndroid(code))
50
+ if (fileId == null) putNull("fileId") else putInt("fileId", fileId)
51
+ if (filePath == null) putNull("filePath") else putString("filePath", filePath)
52
+ if (durationMs == null) putNull("durationMs") else putDouble("durationMs", durationMs.toDouble())
53
+ }
54
+
55
+ internal fun mediaSnapshotResult(code: Int, fileId: Int?, filePath: String?): WritableMap =
56
+ Arguments.createMap().apply {
57
+ putInt("code", TiRtcReactNativeErrors.normalizeAndroid(code))
58
+ if (fileId == null) putNull("fileId") else putInt("fileId", fileId)
59
+ if (filePath == null) putNull("filePath") else putString("filePath", filePath)
60
+ }
61
+
62
+ internal fun galleryResult(code: Int, uri: String?): WritableMap =
63
+ Arguments.createMap().apply {
64
+ putInt("code", TiRtcReactNativeErrors.normalizeAndroid(code))
65
+ if (uri == null) putNull("uri") else putString("uri", uri)
66
+ }
5
67
 
6
68
  internal fun createResult(
7
69
  code: Int,
@@ -2,7 +2,6 @@ package com.tange.ai.tirtc.reactnative
2
2
 
3
3
  import android.os.Handler
4
4
  import android.os.Looper
5
- import android.util.Log
6
5
  import com.facebook.react.bridge.Arguments
7
6
  import com.facebook.react.bridge.Promise
8
7
  import com.facebook.react.bridge.ReadableArray
@@ -11,18 +10,21 @@ import com.tange.ai.tirtc.TiRtcLogging
11
10
  import java.util.concurrent.atomic.AtomicBoolean
12
11
 
13
12
  private const val LOG_UPLOAD_TIMEOUT_MS = 30_000L
13
+ private const val TAG = "TiRtcReactNative"
14
14
 
15
15
  internal fun uploadLogsToPromise(
16
16
  promise: Promise,
17
- tag: String,
18
17
  ) {
19
- Log.i(tag, "uploadLogs start")
20
18
  val settled = AtomicBoolean(false)
21
19
  val timeoutHandler = Handler(Looper.getMainLooper())
22
20
  val timeout =
23
21
  Runnable {
24
22
  if (settled.compareAndSet(false, true)) {
25
- Log.w(tag, "uploadLogs timeout")
23
+ TiRtcLogging.e(
24
+ TAG,
25
+ "[react_native_log_upload] failed: native upload did not settle the Promise before timeout; " +
26
+ "error_code=1001 reason=promise_timeout",
27
+ )
26
28
  promise.resolve(
27
29
  Arguments.createMap().apply {
28
30
  putInt("code", TiRtcReactNativeErrors.PLATFORM_INTERNAL)
@@ -37,7 +39,6 @@ internal fun uploadLogsToPromise(
37
39
  return@upload
38
40
  }
39
41
  timeoutHandler.removeCallbacks(timeout)
40
- Log.i(tag, "uploadLogs completed code=$code logIdPresent=${!logId.isNullOrEmpty()}")
41
42
  promise.resolve(
42
43
  Arguments.createMap().apply {
43
44
  putInt("code", TiRtcReactNativeErrors.normalizeAndroid(code))
@@ -50,7 +51,6 @@ internal fun uploadLogsToPromise(
50
51
  return
51
52
  }
52
53
  timeoutHandler.removeCallbacks(timeout)
53
- Log.w(tag, "uploadLogs failed_to_start code=$startCode")
54
54
  promise.resolve(
55
55
  Arguments.createMap().apply {
56
56
  putInt("code", TiRtcReactNativeErrors.normalizeAndroid(startCode))
@@ -5,6 +5,12 @@ import com.tange.ai.tirtc.TiRtcAudioOutput
5
5
  import com.tange.ai.tirtc.TiRtcConn
6
6
  import com.tange.ai.tirtc.TiRtcVideoInput
7
7
  import com.tange.ai.tirtc.TiRtcVideoOutput
8
+ import com.tange.ai.tirtc.TiRtcRecordingTask
9
+ import com.tange.ai.tirtc.TiCloudStorage
10
+ import com.tange.ai.tirtc.TiCloudStorageAudioOutput
11
+ import com.tange.ai.tirtc.TiCloudStorageRecordingTask
12
+ import com.tange.ai.tirtc.TiCloudStorageReplay
13
+ import com.tange.ai.tirtc.TiCloudStorageVideoOutput
8
14
  import java.util.concurrent.ConcurrentHashMap
9
15
  import java.util.concurrent.atomic.AtomicInteger
10
16
 
@@ -32,6 +38,13 @@ internal object TiRtcReactNativeRegistry {
32
38
  events = eventSink
33
39
  }
34
40
 
41
+ fun drain(eventSink: TiRtcReactNativeEventSink): List<Any> {
42
+ val values = entries.values.map(Entry::value)
43
+ entries.clear()
44
+ if (events === eventSink) events = null
45
+ return values
46
+ }
47
+
35
48
  fun release(id: Int) {
36
49
  entries.remove(id)
37
50
  }
@@ -48,6 +61,20 @@ internal object TiRtcReactNativeRegistry {
48
61
 
49
62
  fun videoInput(id: Int): TiRtcVideoInput? = entries[id]?.value as? TiRtcVideoInput
50
63
 
64
+ fun recordingTask(id: Int): TiRtcRecordingTask? = entries[id]?.value as? TiRtcRecordingTask
65
+
66
+ fun cloudStorage(id: Int): TiCloudStorage? = entries[id]?.value as? TiCloudStorage
67
+
68
+ fun tiCloudStorageReplay(id: Int): TiCloudStorageReplay? = entries[id]?.value as? TiCloudStorageReplay
69
+
70
+ fun tiCloudStorageRecordingTask(id: Int): TiCloudStorageRecordingTask? = entries[id]?.value as? TiCloudStorageRecordingTask
71
+
72
+ fun tiCloudStorageExportTask(id: Int): CloudStorageExportBridge? = entries[id]?.value as? CloudStorageExportBridge
73
+
74
+ fun tiCloudStorageAudioOutput(id: Int): TiCloudStorageAudioOutput? = entries[id]?.value as? TiCloudStorageAudioOutput
75
+
76
+ fun tiCloudStorageVideoOutput(id: Int): TiCloudStorageVideoOutput? = entries[id]?.value as? TiCloudStorageVideoOutput
77
+
51
78
  fun reportVideoOutputError(
52
79
  id: Int,
53
80
  code: Int,
@@ -0,0 +1,64 @@
1
+ package com.tange.ai.tirtc.reactnative
2
+
3
+ import com.facebook.react.bridge.Promise
4
+ import com.tange.ai.tirtc.TiCloudStorageExportTask
5
+ import com.tange.ai.tirtc.TiCloudStorageRecordingResult
6
+
7
+ internal class CloudStorageExportBridge(
8
+ private val resolve: (Promise, TiCloudStorageRecordingResult) -> Unit,
9
+ ) {
10
+ @Volatile var task: TiCloudStorageExportTask? = null
11
+ @Volatile var onDrained: (() -> Unit)? = null
12
+ private var result: TiCloudStorageRecordingResult? = null
13
+ private val waiters = mutableListOf<Promise>()
14
+ private var invalidated = false
15
+ private var invalidationCompletion: ((TiCloudStorageRecordingResult?) -> Unit)? = null
16
+
17
+ fun complete(value: TiCloudStorageRecordingResult) {
18
+ val completed = synchronized(this) {
19
+ if (result != null) return
20
+ result = value
21
+ val pending = if (invalidated) emptyList() else waiters.toList()
22
+ waiters.clear()
23
+ pending to invalidationCompletion.also { invalidationCompletion = null }
24
+ }
25
+ completed.first.forEach { resolve(it, value) }
26
+ if (completed.first.isNotEmpty()) onDrained?.invoke()
27
+ completed.second?.invoke(value)
28
+ }
29
+
30
+ fun await(promise: Promise) {
31
+ val completed = synchronized(this) {
32
+ result?.also { return@synchronized it } ?: run {
33
+ waiters += promise
34
+ null
35
+ }
36
+ }
37
+ if (completed != null) {
38
+ resolve(promise, completed)
39
+ onDrained?.invoke()
40
+ }
41
+ }
42
+
43
+ fun invalidate(onTerminal: (TiCloudStorageRecordingResult?) -> Unit) {
44
+ val completed = synchronized(this) {
45
+ invalidated = true
46
+ waiters.clear()
47
+ result?.also { return@synchronized it } ?: run {
48
+ invalidationCompletion = onTerminal
49
+ null
50
+ }
51
+ }
52
+ if (completed != null) {
53
+ onTerminal(completed)
54
+ return
55
+ }
56
+ val code = task?.stop() ?: TiRtcReactNativeErrors.OBJECT_RELEASED
57
+ if (code != 0) {
58
+ synchronized(this) {
59
+ invalidationCompletion = null
60
+ }
61
+ onTerminal(null)
62
+ }
63
+ }
64
+ }
@@ -22,7 +22,7 @@ class TiRtcVideoOutputViewManager :
22
22
 
23
23
  override fun onDropViewInstance(view: FrameLayout) {
24
24
  (view.getTag(ATTACHED_ID_TAG) as? Int)?.let { id ->
25
- TiRtcReactNativeRegistry.videoOutput(id)?.detachView()
25
+ detachView(id)
26
26
  }
27
27
  super.onDropViewInstance(view)
28
28
  }
@@ -63,7 +63,7 @@ class TiRtcVideoOutputViewManager :
63
63
  val attachedId = view.getTag(ATTACHED_ID_TAG) as? Int
64
64
  val attachedGeneration = view.getTag(ATTACHED_GENERATION_TAG) as? Int
65
65
  if (attachedId != null && (attachedId != nativeObjectId || attachedGeneration != generation)) {
66
- TiRtcReactNativeRegistry.videoOutput(attachedId)?.detachView()
66
+ detachView(attachedId)
67
67
  view.setTag(ATTACHED_ID_TAG, null)
68
68
  view.setTag(ATTACHED_GENERATION_TAG, null)
69
69
  }
@@ -86,7 +86,22 @@ class TiRtcVideoOutputViewManager :
86
86
  }
87
87
  view.setTag(ATTACHED_ID_TAG, nativeObjectId)
88
88
  view.setTag(ATTACHED_GENERATION_TAG, generation)
89
+ return
89
90
  }
91
+ TiRtcReactNativeRegistry.tiCloudStorageVideoOutput(nativeObjectId)?.let { output ->
92
+ val code = output.attachView(view)
93
+ if (TiRtcReactNativeErrors.normalizeAndroid(code) != TiRtcReactNativeErrors.OK) {
94
+ TiRtcReactNativeRegistry.reportVideoOutputError(nativeObjectId, code)
95
+ return
96
+ }
97
+ view.setTag(ATTACHED_ID_TAG, nativeObjectId)
98
+ view.setTag(ATTACHED_GENERATION_TAG, generation)
99
+ }
100
+ }
101
+
102
+ private fun detachView(id: Int) {
103
+ TiRtcReactNativeRegistry.videoOutput(id)?.detachView()
104
+ ?: TiRtcReactNativeRegistry.tiCloudStorageVideoOutput(id)?.detachView()
90
105
  }
91
106
 
92
107
  private fun resizeModeForView(view: FrameLayout): TiRtcVideoViewResizeMode =
@@ -1,6 +1,39 @@
1
1
  #import "TiRtcReactNative+Private.h"
2
2
  #import "TiRtcReactNativeErrors.h"
3
3
  #import "TiRtcReactNativeMaps.h"
4
+ #import <Photos/Photos.h>
5
+
6
+ static NSString* TiRtcReactNativeGalleryFileName(NSString* fileName, BOOL video) {
7
+ NSString* extension = video ? @".mp4" : @".jpg";
8
+ if (fileName == nil) {
9
+ return [NSString stringWithFormat:@"%lld%@",
10
+ (long long)(NSDate.date.timeIntervalSince1970 * 1000),
11
+ extension];
12
+ }
13
+ unichar nul = 0;
14
+ NSString* nulString = [NSString stringWithCharacters:&nul length:1];
15
+ if (fileName.length == 0 ||
16
+ [[fileName stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]
17
+ length] == 0 ||
18
+ [fileName isEqualToString:@"."] || [fileName isEqualToString:@".."] ||
19
+ [fileName containsString:@"/"] || [fileName containsString:@"\\"] ||
20
+ [fileName containsString:nulString]) {
21
+ return nil;
22
+ }
23
+ NSString* stem = nil;
24
+ NSString* normalized = nil;
25
+ if ([fileName.lowercaseString hasSuffix:extension]) {
26
+ stem = [fileName substringToIndex:fileName.length - extension.length];
27
+ normalized = fileName;
28
+ } else if ([fileName containsString:@"."]) {
29
+ return nil;
30
+ } else {
31
+ stem = fileName;
32
+ normalized = [fileName stringByAppendingString:extension];
33
+ }
34
+ NSUInteger stemBytes = [stem lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
35
+ return stemBytes >= 1 && stemBytes <= 200 ? normalized : nil;
36
+ }
4
37
 
5
38
  #pragma clang diagnostic push
6
39
  #pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
@@ -96,6 +129,138 @@
96
129
  }];
97
130
  }
98
131
 
132
+ - (NSDictionary*)connStartRecording:(double)objectId
133
+ options:(JS::NativeTiRtc::NativeRecordingOptions&)options {
134
+ TiRtcConn* conn = [self conn:objectId];
135
+ if (conn == nil) {
136
+ return @{@"code" : @(TiRtcReactNativeErrorObjectReleased), @"taskId" : (id)kCFNull};
137
+ }
138
+ NSNumber* audioStreamId =
139
+ options.audioStreamId().has_value() ? @(options.audioStreamId().value()) : nil;
140
+ TiRtcRecordingTaskStartResult* result =
141
+ [conn startRecordingWithVideoStreamId:(int32_t)options.videoStreamId()
142
+ audioStreamId:audioStreamId];
143
+ if (result.code != 0 || result.task == nil) {
144
+ return @{@"code" : @(TiRtcReactNativeNormalizeDarwin(result.code)), @"taskId" : (id)kCFNull};
145
+ }
146
+ NSInteger taskId = [[self registry] addObject:result.task];
147
+ return @{@"code" : @(TiRtcReactNativeErrorOK), @"taskId" : @(taskId)};
148
+ }
149
+
150
+ - (void)recordingTaskStop:(double)objectId
151
+ resolve:(RCTPromiseResolveBlock)resolve
152
+ reject:(RCTPromiseRejectBlock)reject {
153
+ TiRtcRecordingTask* task = [[self registry] objectForId:(NSInteger)objectId];
154
+ if (![task isKindOfClass:[TiRtcRecordingTask class]]) {
155
+ resolve(@{
156
+ @"code" : @(TiRtcReactNativeErrorObjectReleased),
157
+ @"fileId" : (id)kCFNull,
158
+ @"filePath" : (id)kCFNull,
159
+ @"durationMs" : (id)kCFNull
160
+ });
161
+ return;
162
+ }
163
+ [task stopWithCompletion:^(TiRtcRecordingResult* result) {
164
+ [[self registry] releaseObject:(NSInteger)objectId];
165
+ NSInteger fileId = result.file == nil ? 0 : [[self registry] addTransientObject:result.file];
166
+ resolve(@{
167
+ @"code" : @(TiRtcReactNativeNormalizeDarwin(result.code)),
168
+ @"fileId" : fileId == 0 ? (id)kCFNull : @(fileId),
169
+ @"filePath" : result.file.path ?: (id)kCFNull,
170
+ @"durationMs" : result.file != nil ? @(result.file.durationMs) : (id)kCFNull
171
+ });
172
+ }];
173
+ }
174
+
175
+ - (void)mediaFileMoveToGallery:(double)objectId
176
+ fileName:(NSString*)fileName
177
+ resolve:(RCTPromiseResolveBlock)resolve
178
+ reject:(RCTPromiseRejectBlock)reject {
179
+ id file = [[self registry] transientObjectForId:(NSInteger)objectId];
180
+ if (![file isKindOfClass:[TiRtcRecordingFile class]] &&
181
+ ![file isKindOfClass:[TiRtcSnapshotFile class]] &&
182
+ ![file isKindOfClass:[TiCloudStorageRecordingFile class]] &&
183
+ ![file isKindOfClass:[TiCloudStorageSnapshotFile class]]) {
184
+ resolve(@{@"code" : @(TiRtcReactNativeErrorObjectReleased), @"uri" : (id)kCFNull});
185
+ return;
186
+ }
187
+ NSString* path = [file path];
188
+ BOOL video = [file isKindOfClass:[TiRtcRecordingFile class]] ||
189
+ [file isKindOfClass:[TiCloudStorageRecordingFile class]];
190
+ NSString* normalizedName = TiRtcReactNativeGalleryFileName(fileName, video);
191
+ if (normalizedName == nil) {
192
+ resolve(@{@"code" : @6000, @"uri" : (id)kCFNull});
193
+ return;
194
+ }
195
+ if (![[NSFileManager defaultManager] isReadableFileAtPath:path]) {
196
+ resolve(@{@"code" : @6045, @"uri" : (id)kCFNull});
197
+ return;
198
+ }
199
+ PHAuthorizationStatus status =
200
+ [PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelAddOnly];
201
+ if (status != PHAuthorizationStatusAuthorized && status != PHAuthorizationStatusLimited) {
202
+ resolve(@{@"code" : @6024, @"uri" : (id)kCFNull});
203
+ return;
204
+ }
205
+ __block NSString* localIdentifier = nil;
206
+ [[PHPhotoLibrary sharedPhotoLibrary]
207
+ performChanges:^{
208
+ PHAssetCreationRequest* request = [PHAssetCreationRequest creationRequestForAsset];
209
+ PHAssetResourceCreationOptions* options = [[PHAssetResourceCreationOptions alloc] init];
210
+ options.originalFilename = normalizedName;
211
+ [request addResourceWithType:video ? PHAssetResourceTypeVideo : PHAssetResourceTypePhoto
212
+ fileURL:[NSURL fileURLWithPath:path]
213
+ options:options];
214
+ localIdentifier = request.placeholderForCreatedAsset.localIdentifier;
215
+ }
216
+ completionHandler:^(BOOL success, __unused NSError* error) {
217
+ if (!success || localIdentifier.length == 0) {
218
+ resolve(@{@"code" : @6046, @"uri" : (id)kCFNull});
219
+ return;
220
+ }
221
+ void (^deleted)(int32_t) = ^(int32_t code) {
222
+ if (code == 0) {
223
+ [[self registry] releaseTransientObject:(NSInteger)objectId];
224
+ resolve(@{@"code" : @0,
225
+ @"uri" : [@"ph://" stringByAppendingString:localIdentifier]});
226
+ return;
227
+ }
228
+ PHFetchResult<PHAsset*>* assets =
229
+ [PHAsset fetchAssetsWithLocalIdentifiers:@[ localIdentifier ] options:nil];
230
+ [[PHPhotoLibrary sharedPhotoLibrary]
231
+ performChanges:^{ [PHAssetChangeRequest deleteAssets:assets]; }
232
+ completionHandler:^(__unused BOOL rolledBack, __unused NSError* rollbackError) {
233
+ resolve(@{@"code" : @(TiRtcReactNativeNormalizeDarwin(code)),
234
+ @"uri" : (id)kCFNull});
235
+ }];
236
+ };
237
+ [file deleteWithCompletion:deleted];
238
+ }];
239
+ }
240
+
241
+ - (void)mediaFileDelete:(double)objectId
242
+ resolve:(RCTPromiseResolveBlock)resolve
243
+ reject:(RCTPromiseRejectBlock)reject {
244
+ id file = [[self registry] transientObjectForId:(NSInteger)objectId];
245
+ if (![file isKindOfClass:[TiRtcRecordingFile class]] &&
246
+ ![file isKindOfClass:[TiRtcSnapshotFile class]] &&
247
+ ![file isKindOfClass:[TiCloudStorageRecordingFile class]] &&
248
+ ![file isKindOfClass:[TiCloudStorageSnapshotFile class]]) {
249
+ resolve(@(TiRtcReactNativeErrorObjectReleased));
250
+ return;
251
+ }
252
+ [file deleteWithCompletion:^(int32_t code) {
253
+ if (code == 0) {
254
+ [[self registry] releaseTransientObject:(NSInteger)objectId];
255
+ }
256
+ resolve(@(TiRtcReactNativeNormalizeDarwin(code)));
257
+ }];
258
+ }
259
+
260
+ - (void)mediaFileRelease:(double)objectId {
261
+ [[self registry] releaseTransientObject:(NSInteger)objectId];
262
+ }
263
+
99
264
  - (NSDictionary*)connGetMetricsSnapshot:(double)objectId {
100
265
  TiRtcConn* conn = [self conn:objectId];
101
266
  return conn == nil ? TiRtcReactNativeReleasedSnapshotResult()
@@ -104,8 +269,14 @@
104
269
 
105
270
  - (NSNumber*)connDispose:(double)objectId {
106
271
  TiRtcConn* conn = [self conn:objectId];
107
- [conn dispose];
108
- return [self releaseRegisteredObject:(NSInteger)objectId object:conn];
272
+ if (conn == nil) {
273
+ return @(TiRtcReactNativeErrorObjectReleased);
274
+ }
275
+ NSInteger code = TiRtcReactNativeNormalizeDarwin([conn dispose]);
276
+ if (code == TiRtcReactNativeErrorOK) {
277
+ [self releaseRegisteredObject:(NSInteger)objectId object:conn];
278
+ }
279
+ return @(code);
109
280
  }
110
281
 
111
282
  @end