tirtc-react-native 2.3.1 → 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
@@ -3,13 +3,14 @@ import {
3
3
  isPositiveInt32,
4
4
  isUInt8,
5
5
  normalizeCreateResult,
6
- returnReleasedIfDisposed,
6
+ returnNotInitializedIfDisposed,
7
7
  TIRTC_ERROR_INVALID_ARGUMENT,
8
8
  TIRTC_ERROR_OK,
9
9
  } from './errors';
10
10
  import {registerNativeEventHandler, unregisterNativeEventHandler} from './events';
11
11
  import {readNativeIdentity, clearNativeIdentity, setNativeIdentity, type NativeIdentity} from './native_object_id';
12
12
  import {NativeTiRtc, type NativeTiRtcEvent} from './native';
13
+ import {logRtcOutcome, logRtcState} from './logging_internal';
13
14
  import {TiRtcConn} from './conn';
14
15
  import type {
15
16
  TiRtcAudioOutputDebugSnapshotResult,
@@ -27,11 +28,11 @@ export class TiRtcAudioOutput {
27
28
  private currentState: TiRtcAudioOutputState = 'idle';
28
29
  private options: Required<Omit<TiRtcAudioOutputOptions, 'maxBufferWatermarkMs'>> &
29
30
  Pick<TiRtcAudioOutputOptions, 'maxBufferWatermarkMs'> = {
30
- agcLevel: 'disabled',
31
- ansLevel: 'disabled',
32
- bufferStrategy: 'automatic',
33
- maxBufferWatermarkMs: undefined,
34
- };
31
+ agcLevel: 'disabled',
32
+ ansLevel: 'disabled',
33
+ bufferStrategy: 'automatic',
34
+ maxBufferWatermarkMs: undefined,
35
+ };
35
36
 
36
37
  onStateChanged: TiRtcOnAudioOutputStateChanged | null = null;
37
38
  onError: TiRtcOnAudioOutputError | null = null;
@@ -40,8 +41,12 @@ export class TiRtcAudioOutput {
40
41
  const result = normalizeCreateResult(NativeTiRtc.createAudioOutput());
41
42
  this.createCode = result.code;
42
43
  if (result.code === TIRTC_ERROR_OK) {
43
- this.adoptIdentity({objectId: result.objectId, generation: result.generation});
44
+ this.adoptIdentity({
45
+ objectId: result.objectId,
46
+ generation: result.generation,
47
+ });
44
48
  }
49
+ logRtcOutcome('audio_output_create', result.code);
45
50
  }
46
51
 
47
52
  get state(): TiRtcAudioOutputState {
@@ -61,10 +66,7 @@ export class TiRtcAudioOutput {
61
66
  bufferStrategy: options.bufferStrategy ?? 'automatic',
62
67
  maxBufferWatermarkMs: options.maxBufferWatermarkMs,
63
68
  };
64
- if (
65
- normalized.maxBufferWatermarkMs !== undefined
66
- && !isPositiveInt32(normalized.maxBufferWatermarkMs)
67
- ) {
69
+ if (normalized.maxBufferWatermarkMs !== undefined && !isPositiveInt32(normalized.maxBufferWatermarkMs)) {
68
70
  return TIRTC_ERROR_INVALID_ARGUMENT;
69
71
  }
70
72
  const identity = this.ensureIdentity();
@@ -93,15 +95,20 @@ export class TiRtcAudioOutput {
93
95
  if (typeof identity === 'number') {
94
96
  return identity;
95
97
  }
96
- return NativeTiRtc.audioOutputAttach(identity.objectId, connIdentity.objectId, streamId);
98
+ const code = NativeTiRtc.audioOutputAttach(identity.objectId, connIdentity.objectId, streamId);
99
+ logRtcOutcome('audio_output_attach', code);
100
+ return code;
97
101
  }
98
102
 
99
103
  detach(): number {
100
- const released = returnReleasedIfDisposed(this.disposed);
104
+ const released = returnNotInitializedIfDisposed(this.disposed);
101
105
  if (released !== null) {
102
106
  return released;
103
107
  }
104
- return this.identity === null ? TIRTC_ERROR_OK : NativeTiRtc.audioOutputDetach(this.identity.objectId);
108
+ const code = this.identity === null ? TIRTC_ERROR_OK : NativeTiRtc.audioOutputDetach(this.identity.objectId);
109
+ if (code === TIRTC_ERROR_OK) this.currentState = 'idle';
110
+ logRtcOutcome('audio_output_detach', code);
111
+ return code;
105
112
  }
106
113
 
107
114
  setVolume(volumePercent: number): number {
@@ -159,11 +166,12 @@ export class TiRtcAudioOutput {
159
166
  this.clearCallbacks();
160
167
  this.currentState = 'idle';
161
168
  }
169
+ logRtcOutcome('audio_output_dispose', code);
162
170
  return code;
163
171
  }
164
172
 
165
173
  private ensureIdentity(): NativeIdentity | number {
166
- const released = returnReleasedIfDisposed(this.disposed);
174
+ const released = returnNotInitializedIfDisposed(this.disposed);
167
175
  if (released !== null) {
168
176
  return released;
169
177
  }
@@ -193,10 +201,15 @@ export class TiRtcAudioOutput {
193
201
  return;
194
202
  }
195
203
  if (event.type === 'audioOutputStateChanged' && event.state !== undefined) {
196
- this.currentState = event.state as TiRtcAudioOutputState;
204
+ const nextState = event.state as TiRtcAudioOutputState;
205
+ const changed = this.currentState !== nextState;
206
+ this.currentState = nextState;
207
+ if (changed) logRtcState('audio_output_state', nextState);
197
208
  this.onStateChanged?.(this.currentState);
198
209
  } else if (event.type === 'audioOutputError') {
199
- this.onError?.(event.code ?? TIRTC_ERROR_INVALID_ARGUMENT);
210
+ const code = event.code ?? TIRTC_ERROR_INVALID_ARGUMENT;
211
+ logRtcOutcome('audio_output', code);
212
+ this.onError?.(code);
200
213
  }
201
214
  }
202
215
  }
package/src/conn.ts CHANGED
@@ -3,14 +3,23 @@ import {
3
3
  isUInt8,
4
4
  isUInt32,
5
5
  normalizeCreateResult,
6
- returnReleasedIfDisposed,
6
+ returnNotInitializedIfDisposed,
7
7
  TIRTC_ERROR_INVALID_ARGUMENT,
8
8
  TIRTC_ERROR_OK,
9
9
  } from './errors';
10
10
  import {registerNativeEventHandler, unregisterNativeEventHandler} from './events';
11
11
  import {clearNativeIdentity, setNativeIdentity, type NativeIdentity} from './native_object_id';
12
12
  import {NativeTiRtc, type NativeTiRtcEvent} from './native';
13
+ import {logRtcOutcome, logRtcState} from './logging_internal';
13
14
  import {toNativeBytes} from './errors';
15
+ import {
16
+ recordingFailure,
17
+ startRecording,
18
+ validateStartRecordingOptions,
19
+ type Resp,
20
+ type TiRtcRecordingTask,
21
+ type TiRtcStartRecordingOptions,
22
+ } from './recording';
14
23
  import type {
15
24
  TiRtcConnMetricsResult,
16
25
  TiRtcConnState,
@@ -29,14 +38,27 @@ export class TiRtcConn {
29
38
  const result = normalizeCreateResult(NativeTiRtc.createConn());
30
39
  this.createCode = result.code;
31
40
  if (result.code === TIRTC_ERROR_OK) {
32
- this.adoptIdentity({objectId: result.objectId, generation: result.generation});
41
+ this.adoptIdentity({
42
+ objectId: result.objectId,
43
+ generation: result.generation,
44
+ });
33
45
  }
46
+ logRtcOutcome('connection_create', result.code);
34
47
  }
35
48
 
36
49
  onStateChanged: TiRtcOnConnStateChanged | null = null;
37
50
  onCommand: TiRtcOnConnCommand | null = null;
38
51
  onStreamMessage: TiRtcOnConnStreamMessage | null = null;
39
52
 
53
+ startRecording(options: TiRtcStartRecordingOptions): Resp<TiRtcRecordingTask> {
54
+ const validationCode = validateStartRecordingOptions(options);
55
+ if (validationCode !== TIRTC_ERROR_OK) {
56
+ return recordingFailure(validationCode);
57
+ }
58
+ const identity = this.ensureIdentity();
59
+ return typeof identity === 'number' ? recordingFailure(identity) : startRecording(identity.objectId, options);
60
+ }
61
+
40
62
  get state(): TiRtcConnState {
41
63
  if (this.identity !== null && !this.disposed) {
42
64
  const result = NativeTiRtc.connGetState(this.identity.objectId);
@@ -55,15 +77,19 @@ export class TiRtcConn {
55
77
  if (typeof identity === 'number') {
56
78
  return identity;
57
79
  }
58
- return NativeTiRtc.connConnect(identity.objectId, remoteId, token);
80
+ const code = NativeTiRtc.connConnect(identity.objectId, remoteId, token);
81
+ logRtcOutcome('connection_connect', code);
82
+ return code;
59
83
  }
60
84
 
61
85
  disconnect(): number {
62
- const released = returnReleasedIfDisposed(this.disposed);
86
+ const released = returnNotInitializedIfDisposed(this.disposed);
63
87
  if (released !== null) {
64
88
  return released;
65
89
  }
66
- return this.identity === null ? TIRTC_ERROR_OK : NativeTiRtc.connDisconnect(this.identity.objectId);
90
+ const code = this.identity === null ? TIRTC_ERROR_OK : NativeTiRtc.connDisconnect(this.identity.objectId);
91
+ logRtcOutcome('connection_disconnect', code);
92
+ return code;
67
93
  }
68
94
 
69
95
  sendCommand(commandId: number, data: Uint8Array): number {
@@ -93,37 +119,27 @@ export class TiRtcConn {
93
119
  if (typeof identity === 'number') {
94
120
  return identity;
95
121
  }
96
- return NativeTiRtc.connSendStreamMessage(
97
- identity.objectId,
98
- streamId,
99
- timestampMs,
100
- nativeBytes,
101
- );
122
+ return NativeTiRtc.connSendStreamMessage(identity.objectId, streamId, timestampMs, nativeBytes);
102
123
  }
103
124
 
104
125
  subscribeVideo(streamId: number): number {
105
- return this.callWithStream(streamId, (identity) =>
106
- NativeTiRtc.connSubscribeVideo(identity.objectId, streamId));
126
+ return this.callWithStream(streamId, (identity) => NativeTiRtc.connSubscribeVideo(identity.objectId, streamId));
107
127
  }
108
128
 
109
129
  unsubscribeVideo(streamId: number): number {
110
- return this.callWithStream(streamId, (identity) =>
111
- NativeTiRtc.connUnsubscribeVideo(identity.objectId, streamId));
130
+ return this.callWithStream(streamId, (identity) => NativeTiRtc.connUnsubscribeVideo(identity.objectId, streamId));
112
131
  }
113
132
 
114
133
  subscribeAudio(streamId: number): number {
115
- return this.callWithStream(streamId, (identity) =>
116
- NativeTiRtc.connSubscribeAudio(identity.objectId, streamId));
134
+ return this.callWithStream(streamId, (identity) => NativeTiRtc.connSubscribeAudio(identity.objectId, streamId));
117
135
  }
118
136
 
119
137
  unsubscribeAudio(streamId: number): number {
120
- return this.callWithStream(streamId, (identity) =>
121
- NativeTiRtc.connUnsubscribeAudio(identity.objectId, streamId));
138
+ return this.callWithStream(streamId, (identity) => NativeTiRtc.connUnsubscribeAudio(identity.objectId, streamId));
122
139
  }
123
140
 
124
141
  requestKeyFrame(streamId: number): number {
125
- return this.callWithStream(streamId, (identity) =>
126
- NativeTiRtc.connRequestKeyFrame(identity.objectId, streamId));
142
+ return this.callWithStream(streamId, (identity) => NativeTiRtc.connRequestKeyFrame(identity.objectId, streamId));
127
143
  }
128
144
 
129
145
  getMetricsSnapshot(): TiRtcConnMetricsResult {
@@ -153,6 +169,7 @@ export class TiRtcConn {
153
169
  this.clearCallbacks();
154
170
  this.currentState = 'disconnected';
155
171
  }
172
+ logRtcOutcome('connection_dispose', code);
156
173
  return code;
157
174
  }
158
175
 
@@ -168,7 +185,7 @@ export class TiRtcConn {
168
185
  }
169
186
 
170
187
  private ensureIdentity(): NativeIdentity | number {
171
- const released = returnReleasedIfDisposed(this.disposed);
188
+ const released = returnNotInitializedIfDisposed(this.disposed);
172
189
  if (released !== null) {
173
190
  return released;
174
191
  }
@@ -197,7 +214,12 @@ export class TiRtcConn {
197
214
  switch (event.type) {
198
215
  case 'connStateChanged':
199
216
  if (event.state !== undefined && event.errorCode !== undefined) {
200
- this.currentState = event.state as TiRtcConnState;
217
+ const nextState = event.state as TiRtcConnState;
218
+ const changed = this.currentState !== nextState;
219
+ this.currentState = nextState;
220
+ if (changed || event.errorCode !== 0) {
221
+ logRtcState('connection_state', nextState, event.errorCode);
222
+ }
201
223
  this.onStateChanged?.(this.currentState, event.errorCode);
202
224
  }
203
225
  break;
@@ -208,11 +230,7 @@ export class TiRtcConn {
208
230
  break;
209
231
  case 'connStreamMessage':
210
232
  if (event.streamId !== undefined && event.timestampMs !== undefined) {
211
- this.onStreamMessage?.(
212
- event.streamId,
213
- event.timestampMs,
214
- fromNativeBytes(event.data),
215
- );
233
+ this.onStreamMessage?.(event.streamId, event.timestampMs, fromNativeBytes(event.data));
216
234
  }
217
235
  break;
218
236
  default:
package/src/errors.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export const TIRTC_ERROR_OK = 0;
2
2
  export const TIRTC_ERROR_INVALID_ARGUMENT = 6000;
3
- export const TIRTC_HOST_ERROR_OBJECT_RELEASED = 1008;
3
+ export const TIRTC_ERROR_NOT_INITIALIZED = 6001;
4
+ export const TIRTC_ERROR_IN_USE = 6026;
4
5
 
5
6
  export type NativeCreateResultLike = Readonly<{
6
7
  code: number;
@@ -9,8 +10,8 @@ export type NativeCreateResultLike = Readonly<{
9
10
  message: string | null;
10
11
  }>;
11
12
 
12
- export function returnReleasedIfDisposed(disposed: boolean): number | null {
13
- return disposed ? TIRTC_HOST_ERROR_OBJECT_RELEASED : null;
13
+ export function returnNotInitializedIfDisposed(disposed: boolean): number | null {
14
+ return disposed ? TIRTC_ERROR_NOT_INITIALIZED : null;
14
15
  }
15
16
 
16
17
  export function normalizeCreateResult(result: NativeCreateResultLike): NativeCreateResultLike {
package/src/index.ts CHANGED
@@ -59,3 +59,57 @@ export {TiRtcVideoOutput, TiRtcVideoOutputView} from './video_output';
59
59
  export {TiRtcAudioInput} from './audio_input';
60
60
  export {TiRtcVideoInput, TiRtcVideoInputPreview} from './video_input';
61
61
  export {TiRtcLogging} from './logging';
62
+ export {TiRtcRecordingTask} from './recording';
63
+ export type {
64
+ Resp,
65
+ TiRtcGalleryAsset,
66
+ TiRtcRecordingFile,
67
+ TiRtcSnapshotFile,
68
+ TiRtcStartRecordingOptions,
69
+ } from './recording';
70
+ export {
71
+ TISTORE_ERROR_OK,
72
+ TISTORE_ERROR_INVALID_ARGUMENT,
73
+ TISTORE_ERROR_NOT_INITIALIZED,
74
+ TISTORE_ERROR_TOKEN_EXPIRED,
75
+ TISTORE_ERROR_ALREADY_INITIALIZED,
76
+ TISTORE_ERROR_PERMISSION_DENIED,
77
+ TISTORE_ERROR_IN_USE,
78
+ TISTORE_ERROR_NOT_STARTED,
79
+ TISTORE_ERROR_NOT_BOUND,
80
+ TISTORE_ERROR_NOT_CONFIGURED,
81
+ TISTORE_ERROR_RESOURCE_EXHAUSTED,
82
+ TISTORE_ERROR_FILE_WRITE_FAILED,
83
+ TISTORE_ERROR_UNSUPPORTED_FORMAT,
84
+ TISTORE_ERROR_IO_FAILED,
85
+ TISTORE_ERROR_CANCELLED,
86
+ TISTORE_ERROR_RANGE_TOO_LARGE,
87
+ TISTORE_ERROR_NO_FRAME,
88
+ TISTORE_ERROR_NO_RECORDABLE_MEDIA,
89
+ TISTORE_ERROR_RECORDING_OVERRUN,
90
+ TISTORE_ERROR_RECORDING_UNREADABLE,
91
+ TISTORE_ERROR_STORE_UNAVAILABLE,
92
+ TISTORE_ERROR_STOPPED,
93
+ TiStoreReplaySpeed,
94
+ TiStoreAudioOutputState,
95
+ TiStoreVideoOutputState,
96
+ TiStore,
97
+ TiStoreReplay,
98
+ TiStoreAudioOutput,
99
+ TiStoreVideoOutput,
100
+ TiStoreVideoOutputView,
101
+ TiStoreRecordingTask,
102
+ TiStoreExportTask,
103
+ } from './store';
104
+ export type {
105
+ TiStoreRecordingRange,
106
+ TiStoreRecordingRangesResult,
107
+ TiStoreRecordingDay,
108
+ TiStoreRecordingDaysResult,
109
+ TiStoreStartRecordingOptions,
110
+ TiStoreExportOptions,
111
+ TiStoreGalleryAsset,
112
+ TiStoreRecordingFile,
113
+ TiStoreSnapshotFile,
114
+ TiStoreVideoOutputViewProps,
115
+ } from './store';
package/src/logging.ts CHANGED
@@ -28,6 +28,12 @@ export class TiRtcLogging {
28
28
  }
29
29
 
30
30
  static upload(): Promise<TiRtcLoggingUploadResult> {
31
- return NativeTiRtc.uploadLogs();
31
+ TiRtcLogging.i('TIRTC_REACT_NATIVE', '[log_upload] started');
32
+ return NativeTiRtc.uploadLogs().then((result) => {
33
+ const message = `[log_upload] completed code=${result.code}`;
34
+ if (result.code === 0) TiRtcLogging.i('TIRTC_REACT_NATIVE', message);
35
+ else TiRtcLogging.w('TIRTC_REACT_NATIVE', message);
36
+ return result;
37
+ });
32
38
  }
33
39
  }
@@ -0,0 +1,23 @@
1
+ import {TiRtcLogging} from './logging';
2
+
3
+ const rtcLogTag = 'TIRTC_REACT_NATIVE';
4
+
5
+ export function logRtcOutcome(operation: string, code: number, details = ''): void {
6
+ const message = `[${operation}] completed code=${code}${details}`;
7
+ try {
8
+ if (code === 0) TiRtcLogging.i(rtcLogTag, message);
9
+ else TiRtcLogging.w(rtcLogTag, message);
10
+ } catch {
11
+ // Diagnostics must never change the SDK operation result.
12
+ }
13
+ }
14
+
15
+ export function logRtcState(operation: string, state: string, code = 0): void {
16
+ const message = `[${operation}] state=${state} code=${code}`;
17
+ try {
18
+ if (code === 0) TiRtcLogging.i(rtcLogTag, message);
19
+ else TiRtcLogging.w(rtcLogTag, message);
20
+ } catch {
21
+ // Diagnostics must never change callback delivery.
22
+ }
23
+ }
@@ -0,0 +1,271 @@
1
+ import {isUInt8, TIRTC_ERROR_INVALID_ARGUMENT} from './errors';
2
+ import {TiRtcLogging} from './logging';
3
+ import {logRtcOutcome} from './logging_internal';
4
+ import {NativeTiRtc} from './native';
5
+
6
+ export type Resp<T> = Readonly<{
7
+ success: boolean;
8
+ code: number | null;
9
+ message: string | null;
10
+ data: T | null;
11
+ }>;
12
+
13
+ export type TiRtcStartRecordingOptions = Readonly<{
14
+ videoStreamId: number;
15
+ audioStreamId?: number;
16
+ }>;
17
+
18
+ export type TiRtcGalleryAsset = Readonly<{
19
+ uri: string;
20
+ }>;
21
+
22
+ function success<T>(data: T): Resp<T> {
23
+ return {success: true, code: null, message: null, data};
24
+ }
25
+
26
+ export function recordingFailure<T>(code: number, message: string | null = null): Resp<T> {
27
+ return {success: false, code, message, data: null};
28
+ }
29
+
30
+ const mediaFileFinalizer =
31
+ typeof FinalizationRegistry === 'undefined'
32
+ ? null
33
+ : new FinalizationRegistry<number>((nativeFileId) => NativeTiRtc.mediaFileRelease(nativeFileId));
34
+
35
+ function logMediaOutcome(operation: string, code: number): void {
36
+ const message = `[${operation}] outcome=${code === 0 ? 'ok' : 'failed'} error_code=${code}`;
37
+ if (code === 0) TiRtcLogging.i('TIRTC_REACT_NATIVE_MEDIA', message);
38
+ else TiRtcLogging.e('TIRTC_REACT_NATIVE_MEDIA', message);
39
+ }
40
+
41
+ export interface TiRtcMediaFile {
42
+ readonly path: string;
43
+ moveToGallery(fileName?: string): Promise<Resp<TiRtcGalleryAsset>>;
44
+ delete(): Promise<number>;
45
+ }
46
+
47
+ export interface TiRtcRecordingFile extends TiRtcMediaFile {
48
+ readonly durationMs: number;
49
+ }
50
+
51
+ export interface TiRtcSnapshotFile extends TiRtcMediaFile {}
52
+
53
+ export abstract class NativeMediaFileBase implements TiRtcMediaFile {
54
+ private movePromise: Promise<Resp<TiRtcGalleryAsset>> | null = null;
55
+ private moveFileName: string | null = null;
56
+ private deletePromise: Promise<number> | null = null;
57
+ private deleted = false;
58
+ private galleryAsset: TiRtcGalleryAsset | null = null;
59
+
60
+ protected constructor(
61
+ readonly path: string,
62
+ private readonly nativeFileId: number,
63
+ private readonly mediaKind: 'video' | 'image',
64
+ ) {
65
+ mediaFileFinalizer?.register(this, nativeFileId, this);
66
+ }
67
+
68
+ moveToGallery(fileName?: string): Promise<Resp<TiRtcGalleryAsset>> {
69
+ const normalized = normalizeGalleryFileName(fileName, this.mediaKind);
70
+ if (normalized.code !== 0) {
71
+ return Promise.resolve(recordingFailure<TiRtcGalleryAsset>(normalized.code));
72
+ }
73
+ if (this.galleryAsset !== null) {
74
+ return Promise.resolve(success(this.galleryAsset));
75
+ }
76
+ if (this.deleted) {
77
+ return Promise.resolve(recordingFailure<TiRtcGalleryAsset>(6045));
78
+ }
79
+ if (this.movePromise !== null) {
80
+ return normalized.fileName === this.moveFileName
81
+ ? this.movePromise
82
+ : Promise.resolve(recordingFailure<TiRtcGalleryAsset>(6026));
83
+ }
84
+ if (this.deletePromise !== null) {
85
+ return Promise.resolve(recordingFailure<TiRtcGalleryAsset>(6026));
86
+ }
87
+ this.moveFileName = normalized.fileName;
88
+ this.movePromise = NativeTiRtc.mediaFileMoveToGallery(this.nativeFileId, normalized.fileName)
89
+ .then((result) => {
90
+ if (
91
+ result.code === 0
92
+ && (result.uri === null || !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(result.uri))
93
+ ) {
94
+ logMediaOutcome('gallery_move', 1001);
95
+ this.deleted = true;
96
+ NativeTiRtc.mediaFileRelease(this.nativeFileId);
97
+ mediaFileFinalizer?.unregister(this);
98
+ return recordingFailure<TiRtcGalleryAsset>(1001);
99
+ }
100
+ logMediaOutcome('gallery_move', result.code);
101
+ if (result.code !== 0 || result.uri === null) {
102
+ return recordingFailure<TiRtcGalleryAsset>(result.code);
103
+ }
104
+ this.galleryAsset = {uri: result.uri};
105
+ this.deleted = true;
106
+ NativeTiRtc.mediaFileRelease(this.nativeFileId);
107
+ mediaFileFinalizer?.unregister(this);
108
+ return success(this.galleryAsset);
109
+ })
110
+ .finally(() => {
111
+ this.movePromise = null;
112
+ this.moveFileName = null;
113
+ });
114
+ return this.movePromise;
115
+ }
116
+
117
+ delete(): Promise<number> {
118
+ if (this.deleted || this.galleryAsset !== null) {
119
+ return Promise.resolve(0);
120
+ }
121
+ if (this.deletePromise !== null) {
122
+ return this.deletePromise;
123
+ }
124
+ if (this.movePromise !== null) {
125
+ return Promise.resolve(6026);
126
+ }
127
+ this.deletePromise = this.deleteOnce().finally(() => {
128
+ this.deletePromise = null;
129
+ });
130
+ return this.deletePromise;
131
+ }
132
+
133
+ private async deleteOnce(): Promise<number> {
134
+ const code = await NativeTiRtc.mediaFileDelete(this.nativeFileId);
135
+ logMediaOutcome('media_delete', code);
136
+ if (code === 0) {
137
+ this.deleted = true;
138
+ NativeTiRtc.mediaFileRelease(this.nativeFileId);
139
+ mediaFileFinalizer?.unregister(this);
140
+ }
141
+ return code;
142
+ }
143
+ }
144
+
145
+ type NormalizedGalleryFileName = Readonly<{
146
+ code: number;
147
+ fileName: string | null;
148
+ }>;
149
+
150
+ export function normalizeGalleryFileName(
151
+ fileName: string | undefined,
152
+ mediaType: 'video' | 'image',
153
+ ): NormalizedGalleryFileName {
154
+ if (fileName === undefined) {
155
+ return {code: 0, fileName: null};
156
+ }
157
+ if (
158
+ typeof fileName !== 'string'
159
+ || fileName.length === 0
160
+ || fileName.trim().length === 0
161
+ || fileName === '.'
162
+ || fileName === '..'
163
+ || fileName.includes('/')
164
+ || fileName.includes('\\')
165
+ || fileName.includes('\0')
166
+ ) {
167
+ return {code: TIRTC_ERROR_INVALID_ARGUMENT, fileName: null};
168
+ }
169
+ const extension = mediaType === 'video' ? '.mp4' : '.jpg';
170
+ const lowerName = fileName.toLowerCase();
171
+ let stem: string;
172
+ let normalizedName: string;
173
+ if (lowerName.endsWith(extension)) {
174
+ stem = fileName.slice(0, -extension.length);
175
+ normalizedName = fileName;
176
+ } else if (fileName.includes('.')) {
177
+ return {code: TIRTC_ERROR_INVALID_ARGUMENT, fileName: null};
178
+ } else {
179
+ stem = fileName;
180
+ normalizedName = `${fileName}${extension}`;
181
+ }
182
+ const stemBytes = utf8ByteLength(stem);
183
+ return stemBytes >= 1 && stemBytes <= 200
184
+ ? {code: 0, fileName: normalizedName}
185
+ : {code: TIRTC_ERROR_INVALID_ARGUMENT, fileName: null};
186
+ }
187
+
188
+ function utf8ByteLength(value: string): number {
189
+ let bytes = 0;
190
+ for (const character of value) {
191
+ const codePoint = character.codePointAt(0) ?? 0;
192
+ bytes += codePoint <= 0x7f ? 1 : codePoint <= 0x7ff ? 2 : codePoint <= 0xffff ? 3 : 4;
193
+ }
194
+ return bytes;
195
+ }
196
+
197
+ class TiRtcRecordingFileImpl extends NativeMediaFileBase implements TiRtcRecordingFile {
198
+ constructor(
199
+ path: string,
200
+ readonly durationMs: number,
201
+ nativeFileId: number,
202
+ ) {
203
+ super(path, nativeFileId, 'video');
204
+ }
205
+ }
206
+
207
+ class TiRtcSnapshotFileImpl extends NativeMediaFileBase implements TiRtcSnapshotFile {
208
+ constructor(path: string, nativeFileId: number) {
209
+ super(path, nativeFileId, 'image');
210
+ }
211
+ }
212
+
213
+ export function validateStartRecordingOptions(options: TiRtcStartRecordingOptions): number {
214
+ return options != null &&
215
+ isUInt8(options.videoStreamId) &&
216
+ options.videoStreamId <= 15 &&
217
+ (options.audioStreamId === undefined || (isUInt8(options.audioStreamId) && options.audioStreamId <= 15)) &&
218
+ options.audioStreamId !== options.videoStreamId
219
+ ? 0
220
+ : TIRTC_ERROR_INVALID_ARGUMENT;
221
+ }
222
+
223
+ export class TiRtcRecordingTask {
224
+ private stopPromise: Promise<Resp<TiRtcRecordingFile>> | null = null;
225
+
226
+ protected constructor(private readonly nativeTaskId: number) {}
227
+
228
+ stop(): Promise<Resp<TiRtcRecordingFile>> {
229
+ if (this.stopPromise === null) {
230
+ this.stopPromise = NativeTiRtc.recordingTaskStop(this.nativeTaskId).then((result) => {
231
+ logRtcOutcome('recording_stop', result.code, ` duration_ms=${result.durationMs ?? 0}`);
232
+ if (result.code !== 0 || result.fileId === null || result.filePath === null || result.durationMs === null) {
233
+ return recordingFailure<TiRtcRecordingFile>(result.code);
234
+ }
235
+ return success(new TiRtcRecordingFileImpl(result.filePath, result.durationMs, result.fileId));
236
+ });
237
+ }
238
+ return this.stopPromise;
239
+ }
240
+ }
241
+
242
+ class NativeTiRtcRecordingTask extends TiRtcRecordingTask {
243
+ constructor(nativeTaskId: number) {
244
+ super(nativeTaskId);
245
+ }
246
+ }
247
+
248
+ export function startRecording(
249
+ nativeConnectionId: number,
250
+ options: TiRtcStartRecordingOptions,
251
+ ): Resp<TiRtcRecordingTask> {
252
+ const validationCode = validateStartRecordingOptions(options);
253
+ if (validationCode !== 0) {
254
+ return recordingFailure(validationCode);
255
+ }
256
+ const result = NativeTiRtc.connStartRecording(nativeConnectionId, {
257
+ videoStreamId: options.videoStreamId,
258
+ audioStreamId: options.audioStreamId ?? null,
259
+ });
260
+ logRtcOutcome('recording_start', result.code);
261
+ return result.code === 0 && result.taskId !== null
262
+ ? success(new NativeTiRtcRecordingTask(result.taskId))
263
+ : recordingFailure(result.code);
264
+ }
265
+
266
+ export async function takeSnapshot(nativeOutputId: number): Promise<Resp<TiRtcSnapshotFile>> {
267
+ const result = await NativeTiRtc.videoOutputTakeSnapshot(nativeOutputId);
268
+ return result.code === 0 && result.fileId !== null && result.filePath !== null
269
+ ? success(new TiRtcSnapshotFileImpl(result.filePath, result.fileId))
270
+ : recordingFailure(result.code);
271
+ }