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
package/src/store.ts ADDED
@@ -0,0 +1,760 @@
1
+ import * as React from 'react';
2
+ import {View, type ViewProps} from 'react-native';
3
+
4
+ import {
5
+ isFiniteSafeInteger,
6
+ isPlainObject,
7
+ isUInt8,
8
+ normalizeCreateResult,
9
+ TIRTC_ERROR_INVALID_ARGUMENT,
10
+ TIRTC_ERROR_OK,
11
+ } from './errors';
12
+ import {registerNativeEventHandler, unregisterNativeEventHandler} from './events';
13
+ import {TiRtcLogging} from './logging';
14
+ import {NativeTiRtc, NativeTiRtcVideoOutputView, type NativeTiRtcEvent} from './native';
15
+ import {
16
+ clearNativeIdentity,
17
+ readNativeIdentity,
18
+ setNativeIdentity,
19
+ subscribeNativeIdentity,
20
+ type NativeIdentity,
21
+ } from './native_object_id';
22
+ import {NativeMediaFileBase, recordingFailure, type Resp, type TiRtcGalleryAsset} from './recording';
23
+
24
+ export const TISTORE_ERROR_OK = 0 as const;
25
+ export const TISTORE_ERROR_INVALID_ARGUMENT = 6000 as const;
26
+ export const TISTORE_ERROR_NOT_INITIALIZED = 6001 as const;
27
+ export const TISTORE_ERROR_TOKEN_EXPIRED = 6014 as const;
28
+ export const TISTORE_ERROR_ALREADY_INITIALIZED = 6022 as const;
29
+ export const TISTORE_ERROR_PERMISSION_DENIED = 6024 as const;
30
+ export const TISTORE_ERROR_IN_USE = 6026 as const;
31
+ export const TISTORE_ERROR_NOT_STARTED = 6027 as const;
32
+ export const TISTORE_ERROR_NOT_BOUND = 6029 as const;
33
+ export const TISTORE_ERROR_NOT_CONFIGURED = 6030 as const;
34
+ export const TISTORE_ERROR_RESOURCE_EXHAUSTED = 6043 as const;
35
+ export const TISTORE_ERROR_FILE_WRITE_FAILED = 6046 as const;
36
+ export const TISTORE_ERROR_UNSUPPORTED_FORMAT = 6113 as const;
37
+ export const TISTORE_ERROR_IO_FAILED = 6114 as const;
38
+ export const TISTORE_ERROR_CANCELLED = 6115 as const;
39
+ export const TISTORE_ERROR_RANGE_TOO_LARGE = 6117 as const;
40
+ export const TISTORE_ERROR_NO_FRAME = 6118 as const;
41
+ export const TISTORE_ERROR_NO_RECORDABLE_MEDIA = 6119 as const;
42
+ export const TISTORE_ERROR_RECORDING_OVERRUN = 6120 as const;
43
+ export const TISTORE_ERROR_RECORDING_UNREADABLE = 6122 as const;
44
+ export const TISTORE_ERROR_STORE_UNAVAILABLE = 6123 as const;
45
+ export const TISTORE_ERROR_STOPPED = 6124 as const;
46
+
47
+ export type TiStoreRecordingRange = Readonly<{
48
+ startTimeMs: number;
49
+ endTimeMs: number;
50
+ }>;
51
+ export type TiStoreRecordingRangesResult = Readonly<{
52
+ code: number;
53
+ recordings: ReadonlyArray<TiStoreRecordingRange>;
54
+ }>;
55
+ export type TiStoreRecordingDay = Readonly<{
56
+ date: string;
57
+ hasRecording: boolean;
58
+ }>;
59
+ export type TiStoreRecordingDaysResult = Readonly<{
60
+ code: number;
61
+ days: ReadonlyArray<TiStoreRecordingDay>;
62
+ }>;
63
+
64
+ export const TiStoreReplaySpeed = {
65
+ x1: 'x1',
66
+ x2: 'x2',
67
+ x4: 'x4',
68
+ x8: 'x8',
69
+ } as const;
70
+ export type TiStoreReplaySpeed = (typeof TiStoreReplaySpeed)[keyof typeof TiStoreReplaySpeed];
71
+ const storeLogTag = 'TIRTC_REACT_NATIVE_STORE';
72
+
73
+ function logStoreOutcome(operation: string, code: number, details = ''): void {
74
+ const message = `[${operation}] outcome=${code === TISTORE_ERROR_OK ? 'ok' : 'failed'} error_code=${code}${details}`;
75
+ if (code === TISTORE_ERROR_OK) TiRtcLogging.i(storeLogTag, message);
76
+ else TiRtcLogging.e(storeLogTag, message);
77
+ }
78
+
79
+ export const TiStoreAudioOutputState = {
80
+ idle: 'idle',
81
+ buffering: 'buffering',
82
+ playing: 'playing',
83
+ failed: 'failed',
84
+ paused: 'paused',
85
+ completed: 'completed',
86
+ } as const;
87
+ export type TiStoreAudioOutputState = (typeof TiStoreAudioOutputState)[keyof typeof TiStoreAudioOutputState];
88
+ export const TiStoreVideoOutputState = {
89
+ idle: 'idle',
90
+ buffering: 'buffering',
91
+ rendering: 'rendering',
92
+ failed: 'failed',
93
+ paused: 'paused',
94
+ completed: 'completed',
95
+ } as const;
96
+ export type TiStoreVideoOutputState = (typeof TiStoreVideoOutputState)[keyof typeof TiStoreVideoOutputState];
97
+
98
+ export type TiStoreStartRecordingOptions = Readonly<{
99
+ videoChannelId: number;
100
+ audioChannelId?: number;
101
+ }>;
102
+ export type TiStoreExportOptions = Readonly<{
103
+ startTimeMs: number;
104
+ endTimeMs: number;
105
+ videoChannelId: number;
106
+ audioChannelId?: number;
107
+ }>;
108
+ export type TiStoreGalleryAsset = TiRtcGalleryAsset;
109
+
110
+ export interface TiStoreRecordingFile {
111
+ readonly path: string;
112
+ readonly durationMs: number;
113
+ moveToGallery(fileName?: string): Promise<Resp<TiStoreGalleryAsset>>;
114
+ delete(): Promise<number>;
115
+ }
116
+ export interface TiStoreSnapshotFile {
117
+ readonly path: string;
118
+ moveToGallery(fileName?: string): Promise<Resp<TiStoreGalleryAsset>>;
119
+ delete(): Promise<number>;
120
+ }
121
+ class RecordingFile extends NativeMediaFileBase implements TiStoreRecordingFile {
122
+ constructor(
123
+ path: string,
124
+ readonly durationMs: number,
125
+ id: number,
126
+ ) {
127
+ super(path, id, 'video');
128
+ }
129
+ }
130
+ class SnapshotFile extends NativeMediaFileBase implements TiStoreSnapshotFile {
131
+ constructor(path: string, id: number) {
132
+ super(path, id, 'image');
133
+ }
134
+ }
135
+
136
+ function success<T>(data: T): Resp<T> {
137
+ return {success: true, code: null, message: null, data};
138
+ }
139
+ function validTime(value: number): boolean {
140
+ return isFiniteSafeInteger(value) && value >= 0;
141
+ }
142
+ function speedToNative(speed: TiStoreReplaySpeed): number | null {
143
+ switch (speed) {
144
+ case TiStoreReplaySpeed.x1:
145
+ return 0;
146
+ case TiStoreReplaySpeed.x2:
147
+ return 1;
148
+ case TiStoreReplaySpeed.x4:
149
+ return 2;
150
+ case TiStoreReplaySpeed.x8:
151
+ return 3;
152
+ default:
153
+ return null;
154
+ }
155
+ }
156
+ function speedFromNative(speed: number): TiStoreReplaySpeed | null {
157
+ switch (speed) {
158
+ case 0:
159
+ return TiStoreReplaySpeed.x1;
160
+ case 1:
161
+ return TiStoreReplaySpeed.x2;
162
+ case 2:
163
+ return TiStoreReplaySpeed.x4;
164
+ case 3:
165
+ return TiStoreReplaySpeed.x8;
166
+ default:
167
+ return null;
168
+ }
169
+ }
170
+ function audioStateFromNative(state: unknown): TiStoreAudioOutputState {
171
+ switch (state) {
172
+ case TiStoreAudioOutputState.idle:
173
+ case TiStoreAudioOutputState.buffering:
174
+ case TiStoreAudioOutputState.playing:
175
+ case TiStoreAudioOutputState.failed:
176
+ case TiStoreAudioOutputState.paused:
177
+ case TiStoreAudioOutputState.completed:
178
+ return state;
179
+ default:
180
+ return TiStoreAudioOutputState.failed;
181
+ }
182
+ }
183
+ function videoStateFromNative(state: unknown): TiStoreVideoOutputState {
184
+ switch (state) {
185
+ case TiStoreVideoOutputState.idle:
186
+ case TiStoreVideoOutputState.buffering:
187
+ case TiStoreVideoOutputState.rendering:
188
+ case TiStoreVideoOutputState.failed:
189
+ case TiStoreVideoOutputState.paused:
190
+ case TiStoreVideoOutputState.completed:
191
+ return state;
192
+ default:
193
+ return TiStoreVideoOutputState.failed;
194
+ }
195
+ }
196
+
197
+ export class TiStore {
198
+ private identity: NativeIdentity | null = null;
199
+ private disposed = false;
200
+ onTokenExpired: (() => void) | null = null;
201
+ constructor(private token: string) {}
202
+
203
+ static init(
204
+ options: Readonly<{
205
+ appId: string;
206
+ endpoint?: string;
207
+ consoleLogEnabled?: boolean;
208
+ }>,
209
+ ): Promise<number> {
210
+ if (!isPlainObject(options) || typeof options.appId !== 'string' || options.appId.length === 0) {
211
+ logStoreOutcome('store_initialize', TISTORE_ERROR_INVALID_ARGUMENT);
212
+ return Promise.resolve(TISTORE_ERROR_INVALID_ARGUMENT);
213
+ }
214
+ return NativeTiRtc.storeInitialize({
215
+ appId: options.appId,
216
+ endpoint: options.endpoint ?? '',
217
+ consoleLogEnabled: options.consoleLogEnabled ?? false,
218
+ }).then((code) => {
219
+ logStoreOutcome('store_initialize', code);
220
+ return code;
221
+ });
222
+ }
223
+ static shutdown(): number {
224
+ const nativeCode = NativeTiRtc.storeShutdown();
225
+ const code = nativeCode === TISTORE_ERROR_NOT_INITIALIZED ? TISTORE_ERROR_OK : nativeCode;
226
+ logStoreOutcome('store_shutdown', code);
227
+ return code;
228
+ }
229
+ static errorToString(code: number): string {
230
+ return NativeTiRtc.errorToString(code);
231
+ }
232
+
233
+ updateToken(token: string): number {
234
+ if (token.length === 0) return TISTORE_ERROR_INVALID_ARGUMENT;
235
+ const identity = this.ensureIdentity();
236
+ if (typeof identity === 'number') return identity;
237
+ const code = NativeTiRtc.storeUpdateToken(identity.objectId, token);
238
+ if (code === 0) this.token = token;
239
+ logStoreOutcome('store_update_token', code);
240
+ return code;
241
+ }
242
+ listRecordings(startTimeMs: number, endTimeMs: number): Promise<TiStoreRecordingRangesResult> {
243
+ if (!validTime(startTimeMs) || !validTime(endTimeMs) || endTimeMs <= startTimeMs) {
244
+ return Promise.resolve({
245
+ code: TISTORE_ERROR_INVALID_ARGUMENT,
246
+ recordings: [],
247
+ });
248
+ }
249
+ const identity = this.ensureIdentity();
250
+ if (typeof identity === 'number') {
251
+ logStoreOutcome('recording_list', identity);
252
+ return Promise.resolve({code: identity, recordings: []});
253
+ }
254
+ TiRtcLogging.i(storeLogTag, `[recording_list] outcome=started range_duration_ms=${endTimeMs - startTimeMs}`);
255
+ return NativeTiRtc.storeListRecordings(identity.objectId, startTimeMs, endTimeMs).then((result) => {
256
+ logStoreOutcome('recording_list', result.code, ` recording_count=${result.recordings.length}`);
257
+ return result;
258
+ });
259
+ }
260
+ listRecordingDays(
261
+ startDate: string,
262
+ endDate: string,
263
+ timeZoneId = 'Asia/Shanghai',
264
+ ): Promise<TiStoreRecordingDaysResult> {
265
+ if (
266
+ typeof startDate !== 'string' ||
267
+ typeof endDate !== 'string' ||
268
+ typeof timeZoneId !== 'string' ||
269
+ timeZoneId.length === 0
270
+ ) {
271
+ return Promise.resolve({code: TISTORE_ERROR_INVALID_ARGUMENT, days: []});
272
+ }
273
+ const identity = this.ensureIdentity();
274
+ if (typeof identity === 'number') {
275
+ logStoreOutcome('recording_days_list', identity);
276
+ return Promise.resolve({code: identity, days: []});
277
+ }
278
+ TiRtcLogging.i(storeLogTag, '[recording_days_list] outcome=started');
279
+ return NativeTiRtc.storeListRecordingDays(
280
+ identity.objectId,
281
+ startDate,
282
+ endDate,
283
+ timeZoneId,
284
+ ).then((result) => {
285
+ logStoreOutcome('recording_days_list', result.code, ` day_count=${result.days.length}`);
286
+ return result;
287
+ });
288
+ }
289
+ createReplay(): TiStoreReplay {
290
+ return new TiStoreReplay(this);
291
+ }
292
+ exportRecording(options: TiStoreExportOptions, onProgress?: (progress: number) => void): Resp<TiStoreExportTask> {
293
+ if (
294
+ !validTime(options.startTimeMs) ||
295
+ !validTime(options.endTimeMs) ||
296
+ options.endTimeMs <= options.startTimeMs ||
297
+ !isUInt8(options.videoChannelId) ||
298
+ (options.audioChannelId !== undefined && !isUInt8(options.audioChannelId))
299
+ ) {
300
+ return recordingFailure(TISTORE_ERROR_INVALID_ARGUMENT);
301
+ }
302
+ const identity = this.ensureIdentity();
303
+ if (typeof identity === 'number') return recordingFailure(identity);
304
+ const result = NativeTiRtc.storeExportRecording(
305
+ identity.objectId,
306
+ options.startTimeMs,
307
+ options.endTimeMs,
308
+ options.videoChannelId,
309
+ options.audioChannelId ?? null,
310
+ );
311
+ logStoreOutcome('export_start', result.code, ` range_duration_ms=${options.endTimeMs - options.startTimeMs}`);
312
+ return result.code === 0 && result.taskId !== null
313
+ ? success(new TiStoreExportTask(result.taskId, this, onProgress))
314
+ : recordingFailure(result.code);
315
+ }
316
+ dispose(): number {
317
+ if (this.disposed) return 0;
318
+ if (this.identity === null) {
319
+ this.disposed = true;
320
+ this.token = '';
321
+ this.onTokenExpired = null;
322
+ return 0;
323
+ }
324
+ const identity = this.identity;
325
+ const code = NativeTiRtc.storeDispose(identity.objectId);
326
+ if (code === 0) {
327
+ unregisterNativeEventHandler(identity);
328
+ clearNativeIdentity(this);
329
+ this.identity = null;
330
+ this.disposed = true;
331
+ this.token = '';
332
+ this.onTokenExpired = null;
333
+ }
334
+ return code;
335
+ }
336
+ private ensureIdentity(): NativeIdentity | number {
337
+ if (this.disposed) return TISTORE_ERROR_NOT_INITIALIZED;
338
+ if (this.identity !== null) return this.identity;
339
+ if (this.token.length === 0) return TISTORE_ERROR_INVALID_ARGUMENT;
340
+ const result = normalizeCreateResult(NativeTiRtc.storeCreate(this.token));
341
+ if (result.code !== 0) return result.code;
342
+ this.identity = {
343
+ objectId: result.objectId,
344
+ generation: result.generation,
345
+ };
346
+ setNativeIdentity(this, this.identity);
347
+ registerNativeEventHandler(this.identity, (event) => {
348
+ if (event.type === 'storeTokenExpired') this.onTokenExpired?.();
349
+ });
350
+ return this.identity;
351
+ }
352
+ }
353
+
354
+ function nativeStoreIdentity(store: TiStore): NativeIdentity | number {
355
+ return (store as unknown as {ensureIdentity(): NativeIdentity | number}).ensureIdentity();
356
+ }
357
+
358
+ export class TiStoreReplay {
359
+ private identity: NativeIdentity | null = null;
360
+ private disposed = false;
361
+ private currentSpeed: TiStoreReplaySpeed = 'x1';
362
+ private currentTime: number | null = null;
363
+ onTimeChanged: ((timeMs: number) => void) | null = null;
364
+ onError: ((code: number) => void) | null = null;
365
+ onCompleted: (() => void) | null = null;
366
+ constructor(private readonly store: TiStore) {}
367
+ get speed(): TiStoreReplaySpeed {
368
+ if (this.identity !== null) {
369
+ const info = NativeTiRtc.storeReplayGetInfo(this.identity.objectId);
370
+ if (info.code === 0) this.currentSpeed = speedFromNative(info.speed) ?? this.currentSpeed;
371
+ }
372
+ return this.currentSpeed;
373
+ }
374
+ get currentTimeMs(): number | null {
375
+ if (this.identity !== null) {
376
+ const info = NativeTiRtc.storeReplayGetInfo(this.identity.objectId);
377
+ if (info.code === 0) this.currentTime = info.currentTimeMs;
378
+ }
379
+ return this.currentTime;
380
+ }
381
+ play(startTimeMs: number, endTimeMs: number, initialTimeMs?: number): number {
382
+ if (
383
+ !validTime(startTimeMs) ||
384
+ !validTime(endTimeMs) ||
385
+ endTimeMs <= startTimeMs ||
386
+ (initialTimeMs !== undefined &&
387
+ (!validTime(initialTimeMs) || initialTimeMs < startTimeMs || initialTimeMs >= endTimeMs))
388
+ )
389
+ return TISTORE_ERROR_INVALID_ARGUMENT;
390
+ const identity = this.ensureIdentity();
391
+ const code =
392
+ typeof identity === 'number'
393
+ ? identity
394
+ : NativeTiRtc.storeReplayPlay(identity.objectId, startTimeMs, endTimeMs, initialTimeMs ?? null);
395
+ logStoreOutcome('replay_play', code, ` range_duration_ms=${endTimeMs - startTimeMs}`);
396
+ return code;
397
+ }
398
+ pause(): number {
399
+ const code = this.command(NativeTiRtc.storeReplayPause);
400
+ logStoreOutcome('replay_pause', code);
401
+ return code;
402
+ }
403
+ resume(): number {
404
+ const code = this.command(NativeTiRtc.storeReplayResume);
405
+ logStoreOutcome('replay_resume', code);
406
+ return code;
407
+ }
408
+ seek(timeMs: number): number {
409
+ if (!validTime(timeMs)) return TISTORE_ERROR_INVALID_ARGUMENT;
410
+ const identity = this.ensureIdentity();
411
+ const code = typeof identity === 'number' ? identity : NativeTiRtc.storeReplaySeek(identity.objectId, timeMs);
412
+ logStoreOutcome('replay_seek', code);
413
+ return code;
414
+ }
415
+ setSpeed(speed: TiStoreReplaySpeed): number {
416
+ const index = speedToNative(speed);
417
+ if (index === null) return TISTORE_ERROR_INVALID_ARGUMENT;
418
+ const identity = this.ensureIdentity();
419
+ if (typeof identity === 'number') return identity;
420
+ const code = NativeTiRtc.storeReplaySetSpeed(identity.objectId, index);
421
+ if (code === 0) this.currentSpeed = speed;
422
+ logStoreOutcome('replay_speed', code, ` speed=${speed}`);
423
+ return code;
424
+ }
425
+ startRecording(options: TiStoreStartRecordingOptions): Resp<TiStoreRecordingTask> {
426
+ if (
427
+ !isUInt8(options.videoChannelId) ||
428
+ (options.audioChannelId !== undefined && !isUInt8(options.audioChannelId))
429
+ ) {
430
+ return recordingFailure(TISTORE_ERROR_INVALID_ARGUMENT);
431
+ }
432
+ const identity = this.ensureIdentity();
433
+ if (typeof identity === 'number') return recordingFailure(identity);
434
+ const result = NativeTiRtc.storeReplayStartRecording(
435
+ identity.objectId,
436
+ options.videoChannelId,
437
+ options.audioChannelId ?? null,
438
+ );
439
+ logStoreOutcome('replay_recording_start', result.code);
440
+ return result.code === 0 && result.taskId !== null
441
+ ? success(new TiStoreRecordingTask(result.taskId, this))
442
+ : recordingFailure(result.code);
443
+ }
444
+ stop(): number {
445
+ const code = this.command(NativeTiRtc.storeReplayStop);
446
+ if (code === 0) this.currentTime = null;
447
+ logStoreOutcome('replay_stop', code);
448
+ return code;
449
+ }
450
+ dispose(): number {
451
+ if (this.disposed) return 0;
452
+ if (this.identity === null) {
453
+ this.disposed = true;
454
+ this.onTimeChanged = null;
455
+ this.onError = null;
456
+ this.onCompleted = null;
457
+ return 0;
458
+ }
459
+ const identity = this.identity;
460
+ const code = NativeTiRtc.storeReplayDispose(identity.objectId);
461
+ if (code === 0) {
462
+ unregisterNativeEventHandler(identity);
463
+ clearNativeIdentity(this);
464
+ this.identity = null;
465
+ this.disposed = true;
466
+ this.onTimeChanged = null;
467
+ this.onError = null;
468
+ this.onCompleted = null;
469
+ }
470
+ logStoreOutcome('replay_dispose', code);
471
+ return code;
472
+ }
473
+ private command(operation: (id: number) => number): number {
474
+ const identity = this.ensureIdentity();
475
+ return typeof identity === 'number' ? identity : operation(identity.objectId);
476
+ }
477
+ private ensureIdentity(): NativeIdentity | number {
478
+ if (this.disposed) return TISTORE_ERROR_NOT_INITIALIZED;
479
+ if (this.identity !== null) return this.identity;
480
+ const store = nativeStoreIdentity(this.store);
481
+ if (typeof store === 'number') return store;
482
+ const result = normalizeCreateResult(NativeTiRtc.storeCreateReplay(store.objectId));
483
+ if (result.code !== 0) return result.code;
484
+ this.identity = {
485
+ objectId: result.objectId,
486
+ generation: result.generation,
487
+ };
488
+ setNativeIdentity(this, this.identity);
489
+ registerNativeEventHandler(this.identity, (event) => this.handleEvent(event));
490
+ return this.identity;
491
+ }
492
+ private handleEvent(event: NativeTiRtcEvent): void {
493
+ if (event.type === 'storeReplayTimeChanged' && event.timestampMs !== undefined) {
494
+ this.currentTime = event.timestampMs;
495
+ this.onTimeChanged?.(event.timestampMs);
496
+ } else if (event.type === 'storeReplayCompleted') this.onCompleted?.();
497
+ else if (event.type === 'storeReplayError') this.onError?.(event.code ?? TISTORE_ERROR_STORE_UNAVAILABLE);
498
+ }
499
+ }
500
+
501
+ function nativeReplayIdentity(replay: TiStoreReplay): NativeIdentity | number {
502
+ return (replay as unknown as {ensureIdentity(): NativeIdentity | number}).ensureIdentity();
503
+ }
504
+
505
+ abstract class StoreOutputBase {
506
+ protected identity: NativeIdentity | null = null;
507
+ protected disposed = false;
508
+ protected bound = false;
509
+ protected abstract create(): ReturnType<typeof NativeTiRtc.storeCreateAudioOutput>;
510
+ protected ensureIdentity(): NativeIdentity | number {
511
+ if (this.disposed) return TISTORE_ERROR_NOT_INITIALIZED;
512
+ if (this.identity !== null) return this.identity;
513
+ const result = normalizeCreateResult(this.create());
514
+ if (result.code !== 0) return result.code;
515
+ this.identity = {
516
+ objectId: result.objectId,
517
+ generation: result.generation,
518
+ };
519
+ setNativeIdentity(this, this.identity);
520
+ registerNativeEventHandler(this.identity, (event) => this.handleEvent(event));
521
+ return this.identity;
522
+ }
523
+ protected abstract handleEvent(event: NativeTiRtcEvent): void;
524
+ protected releaseIdentity(): void {
525
+ if (this.identity !== null) {
526
+ unregisterNativeEventHandler(this.identity);
527
+ clearNativeIdentity(this);
528
+ }
529
+ this.identity = null;
530
+ this.disposed = true;
531
+ }
532
+ }
533
+
534
+ export class TiStoreAudioOutput extends StoreOutputBase {
535
+ private currentState: TiStoreAudioOutputState = 'idle';
536
+ onStateChanged: ((state: TiStoreAudioOutputState) => void) | null = null;
537
+ onError: ((code: number) => void) | null = null;
538
+ protected create() {
539
+ return NativeTiRtc.storeCreateAudioOutput();
540
+ }
541
+ get state(): TiStoreAudioOutputState {
542
+ if (this.identity !== null)
543
+ this.currentState = audioStateFromNative(
544
+ NativeTiRtc.storeAudioOutputGetState(this.identity.objectId).state ?? this.currentState,
545
+ );
546
+ return this.currentState;
547
+ }
548
+ attach(replay: TiStoreReplay, channelId: number): number {
549
+ if (!isUInt8(channelId)) return TISTORE_ERROR_INVALID_ARGUMENT;
550
+ const output = this.ensureIdentity();
551
+ const replayId = nativeReplayIdentity(replay);
552
+ if (typeof output === 'number') return output;
553
+ if (typeof replayId === 'number') return replayId;
554
+ const code = NativeTiRtc.storeAudioOutputAttach(output.objectId, replayId.objectId, channelId);
555
+ if (code === 0) this.bound = true;
556
+ logStoreOutcome('audio_output_attach', code);
557
+ return code;
558
+ }
559
+ setVolume(volumePercent: number): number {
560
+ if (!isFiniteSafeInteger(volumePercent) || volumePercent < 0 || volumePercent > 100)
561
+ return TISTORE_ERROR_INVALID_ARGUMENT;
562
+ const output = this.ensureIdentity();
563
+ return typeof output === 'number' ? output : NativeTiRtc.storeAudioOutputSetVolume(output.objectId, volumePercent);
564
+ }
565
+ detach(): number {
566
+ if (this.identity === null) return 0;
567
+ const code = NativeTiRtc.storeAudioOutputDetach(this.identity.objectId);
568
+ if (code === 0) {
569
+ this.bound = false;
570
+ this.currentState = TiStoreAudioOutputState.idle;
571
+ }
572
+ logStoreOutcome('audio_output_detach', code);
573
+ return code;
574
+ }
575
+ dispose(): number {
576
+ if (this.disposed) return 0;
577
+ if (this.bound) return TISTORE_ERROR_IN_USE;
578
+ if (this.identity === null) {
579
+ this.disposed = true;
580
+ this.currentState = TiStoreAudioOutputState.idle;
581
+ this.onStateChanged = null;
582
+ this.onError = null;
583
+ return 0;
584
+ }
585
+ const code = NativeTiRtc.storeAudioOutputDispose(this.identity.objectId);
586
+ if (code === 0) {
587
+ this.releaseIdentity();
588
+ this.currentState = TiStoreAudioOutputState.idle;
589
+ this.onStateChanged = null;
590
+ this.onError = null;
591
+ }
592
+ logStoreOutcome('audio_output_dispose', code);
593
+ return code;
594
+ }
595
+ protected handleEvent(event: NativeTiRtcEvent): void {
596
+ if (event.type === 'audioOutputStateChanged' && event.state !== undefined) {
597
+ this.currentState = audioStateFromNative(event.state);
598
+ this.onStateChanged?.(this.currentState);
599
+ } else if (event.type === 'audioOutputError') this.onError?.(event.code ?? TISTORE_ERROR_IO_FAILED);
600
+ }
601
+ }
602
+
603
+ export type TiStoreVideoOutputViewProps = ViewProps &
604
+ Readonly<{
605
+ output: TiStoreVideoOutput;
606
+ resizeMode?: 'contain' | 'cover' | 'stretch';
607
+ }>;
608
+ export class TiStoreVideoOutput extends StoreOutputBase {
609
+ private currentState: TiStoreVideoOutputState = 'idle';
610
+ onStateChanged: ((state: TiStoreVideoOutputState) => void) | null = null;
611
+ onError: ((code: number) => void) | null = null;
612
+ protected create() {
613
+ return NativeTiRtc.storeCreateVideoOutput();
614
+ }
615
+ get state(): TiStoreVideoOutputState {
616
+ if (this.identity !== null)
617
+ this.currentState = videoStateFromNative(
618
+ NativeTiRtc.storeVideoOutputGetState(this.identity.objectId).state ?? this.currentState,
619
+ );
620
+ return this.currentState;
621
+ }
622
+ view(props: Omit<TiStoreVideoOutputViewProps, 'output'> = {}): React.ReactElement {
623
+ this.ensureIdentity();
624
+ return React.createElement(TiStoreVideoOutputView, {
625
+ ...props,
626
+ output: this,
627
+ });
628
+ }
629
+ attach(replay: TiStoreReplay, channelId: number): number {
630
+ if (!isUInt8(channelId)) return TISTORE_ERROR_INVALID_ARGUMENT;
631
+ const output = this.ensureIdentity();
632
+ const replayId = nativeReplayIdentity(replay);
633
+ if (typeof output === 'number') return output;
634
+ if (typeof replayId === 'number') return replayId;
635
+ const code = NativeTiRtc.storeVideoOutputAttach(output.objectId, replayId.objectId, channelId);
636
+ if (code === 0) this.bound = true;
637
+ logStoreOutcome('video_output_attach', code);
638
+ return code;
639
+ }
640
+ async takeSnapshot(): Promise<Resp<TiStoreSnapshotFile>> {
641
+ const output = this.ensureIdentity();
642
+ if (typeof output === 'number') return recordingFailure(output);
643
+ const result = await NativeTiRtc.storeVideoOutputTakeSnapshot(output.objectId);
644
+ logStoreOutcome('snapshot', result.code);
645
+ return result.code === 0 && result.fileId !== null && result.filePath !== null
646
+ ? success(new SnapshotFile(result.filePath, result.fileId))
647
+ : recordingFailure(result.code);
648
+ }
649
+ detach(): number {
650
+ if (this.identity === null) return 0;
651
+ const code = NativeTiRtc.storeVideoOutputDetach(this.identity.objectId);
652
+ if (code === 0) {
653
+ this.bound = false;
654
+ this.currentState = TiStoreVideoOutputState.idle;
655
+ }
656
+ logStoreOutcome('video_output_detach', code);
657
+ return code;
658
+ }
659
+ dispose(): number {
660
+ if (this.disposed) return 0;
661
+ if (this.bound) return TISTORE_ERROR_IN_USE;
662
+ if (this.identity === null) {
663
+ this.disposed = true;
664
+ this.currentState = TiStoreVideoOutputState.idle;
665
+ this.onStateChanged = null;
666
+ this.onError = null;
667
+ return 0;
668
+ }
669
+ const code = NativeTiRtc.storeVideoOutputDispose(this.identity.objectId);
670
+ if (code === 0) {
671
+ this.releaseIdentity();
672
+ this.currentState = TiStoreVideoOutputState.idle;
673
+ this.onStateChanged = null;
674
+ this.onError = null;
675
+ }
676
+ logStoreOutcome('video_output_dispose', code);
677
+ return code;
678
+ }
679
+ protected handleEvent(event: NativeTiRtcEvent): void {
680
+ if (event.type === 'videoOutputStateChanged' && event.state !== undefined) {
681
+ this.currentState = videoStateFromNative(event.state);
682
+ this.onStateChanged?.(this.currentState);
683
+ } else if (event.type === 'videoOutputError') this.onError?.(event.code ?? TISTORE_ERROR_IO_FAILED);
684
+ }
685
+ }
686
+
687
+ export function TiStoreVideoOutputView(props: TiStoreVideoOutputViewProps): React.ReactElement {
688
+ const {output, resizeMode = 'contain', ...viewProps} = props;
689
+ const identity = useOwnerIdentity(output);
690
+ return identity === null
691
+ ? React.createElement(View, viewProps)
692
+ : React.createElement(NativeTiRtcVideoOutputView, {
693
+ ...viewProps,
694
+ nativeObjectId: identity.objectId,
695
+ generation: identity.generation,
696
+ resizeMode,
697
+ });
698
+ }
699
+ function useOwnerIdentity(owner: TiStoreVideoOutput): NativeIdentity | null {
700
+ const [, update] = React.useReducer((value: number) => value + 1, 0);
701
+ React.useEffect(() => subscribeNativeIdentity(owner, update), [owner]);
702
+ return readNativeIdentity(owner);
703
+ }
704
+
705
+ export class TiStoreRecordingTask {
706
+ private stopPromise: Promise<Resp<TiStoreRecordingFile>> | null = null;
707
+ constructor(
708
+ private readonly taskId: number,
709
+ private readonly owner: TiStoreReplay,
710
+ ) {}
711
+ stop(): Promise<Resp<TiStoreRecordingFile>> {
712
+ void this.owner;
713
+ return (this.stopPromise ??= NativeTiRtc.storeRecordingTaskStop(this.taskId).then((result) => {
714
+ logStoreOutcome('replay_recording_stop', result.code, ` duration_ms=${result.durationMs ?? 0}`);
715
+ return result.code === 0 && result.fileId !== null && result.filePath !== null && result.durationMs !== null
716
+ ? success(new RecordingFile(result.filePath, result.durationMs, result.fileId))
717
+ : recordingFailure(result.code);
718
+ }));
719
+ }
720
+ }
721
+
722
+ export class TiStoreExportTask {
723
+ private resultPromise: Promise<Resp<TiStoreRecordingFile>>;
724
+ constructor(
725
+ private readonly taskId: number,
726
+ private readonly owner: TiStore,
727
+ private readonly onProgress?: (progress: number) => void,
728
+ ) {
729
+ this.resultPromise = NativeTiRtc.storeExportResult(taskId).then((result) => {
730
+ logStoreOutcome('export_terminal', result.code, ` duration_ms=${result.durationMs ?? 0}`);
731
+ return result.code === 0 && result.fileId !== null && result.filePath !== null && result.durationMs !== null
732
+ ? success(new RecordingFile(result.filePath, result.durationMs, result.fileId))
733
+ : recordingFailure(result.code);
734
+ });
735
+ if (onProgress !== undefined) {
736
+ let lastProgress = -1;
737
+ const timer = setInterval(() => {
738
+ const progress = NativeTiRtc.storeExportProgress(taskId);
739
+ if (progress !== lastProgress) {
740
+ lastProgress = progress;
741
+ onProgress(progress);
742
+ }
743
+ }, 100);
744
+ const clear = () => clearInterval(timer);
745
+ void this.resultPromise.then(clear, clear);
746
+ }
747
+ }
748
+ get progress(): number {
749
+ void this.owner;
750
+ return NativeTiRtc.storeExportProgress(this.taskId);
751
+ }
752
+ get result(): Promise<Resp<TiStoreRecordingFile>> {
753
+ return this.resultPromise;
754
+ }
755
+ stop(): Promise<Resp<TiStoreRecordingFile>> {
756
+ const code = NativeTiRtc.storeExportStop(this.taskId);
757
+ logStoreOutcome('export_stop', code);
758
+ return this.resultPromise;
759
+ }
760
+ }