tirtc-react-native 2.4.2 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -6
- package/TiRtcReactNative.podspec +1 -1
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcModule.kt +140 -17
- package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeCleanup.kt +54 -19
- package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeCloudStorageCallbacks.kt +60 -0
- package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeEventBinder.kt +2 -0
- package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeEventSink.kt +20 -0
- package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeMaps.kt +112 -5
- package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeOptions.kt +2 -7
- package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeRegistry.kt +3 -0
- package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeStorageSupport.kt +46 -22
- package/ios/TiRtcReactNative+Conn.mm +61 -0
- package/ios/TiRtcReactNative+Output.mm +1 -7
- package/ios/TiRtcReactNative+Private.h +1 -0
- package/ios/TiRtcReactNative+Storage.mm +133 -35
- package/ios/TiRtcReactNative.mm +142 -51
- package/ios/TiRtcReactNativeCloudStorageCallbacks.h +13 -0
- package/ios/TiRtcReactNativeCloudStorageCallbacks.mm +67 -0
- package/ios/TiRtcReactNativeEventBinder.h +4 -0
- package/ios/TiRtcReactNativeEventBinder.mm +11 -0
- package/ios/TiRtcReactNativeEventSink.h +5 -0
- package/ios/TiRtcReactNativeEventSink.mm +18 -0
- package/ios/TiRtcReactNativeMaps.h +6 -0
- package/ios/TiRtcReactNativeMaps.mm +47 -0
- package/ios/TiRtcReactNativeOptions.mm +2 -6
- package/lib/module/audio_input.js +23 -4
- package/lib/module/audio_input.js.map +1 -1
- package/lib/module/build_identity.js +1 -0
- package/lib/module/conn.js +19 -0
- package/lib/module/conn.js.map +1 -1
- package/lib/module/events.js +16 -9
- package/lib/module/events.js.map +1 -1
- package/lib/module/index.js +3 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/logging_internal.js +16 -0
- package/lib/module/logging_internal.js.map +1 -1
- package/lib/module/raw_dump.js +53 -0
- package/lib/module/raw_dump.js.map +1 -0
- package/lib/module/runtime.js +3 -1
- package/lib/module/runtime.js.map +1 -1
- package/lib/module/specs/NativeTiRtc.js.map +1 -1
- package/lib/module/storage.js +190 -33
- package/lib/module/storage.js.map +1 -1
- package/lib/module/types.js +0 -4
- package/lib/module/types.js.map +1 -1
- package/lib/module/video_input.js +16 -6
- package/lib/module/video_input.js.map +1 -1
- package/lib/typescript/build_identity.d.ts +6 -0
- package/lib/typescript/conn.d.ts +2 -0
- package/lib/typescript/index.d.ts +5 -3
- package/lib/typescript/logging_internal.d.ts +1 -0
- package/lib/typescript/native.d.ts +1 -1
- package/lib/typescript/raw_dump.d.ts +35 -0
- package/lib/typescript/specs/NativeTiRtc.d.ts +75 -3
- package/lib/typescript/storage.d.ts +84 -3
- package/lib/typescript/types.d.ts +5 -6
- package/package.json +3 -3
- package/src/audio_input.ts +30 -11
- package/src/build_identity.ts +6 -0
- package/src/conn.ts +23 -0
- package/src/events.ts +14 -9
- package/src/index.ts +19 -1
- package/src/logging_internal.ts +18 -0
- package/src/native.ts +7 -1
- package/src/raw_dump.ts +84 -0
- package/src/runtime.ts +2 -1
- package/src/specs/NativeTiRtc.ts +80 -3
- package/src/storage.ts +260 -39
- package/src/types.ts +5 -8
- package/src/video_input.ts +22 -12
|
@@ -4,11 +4,121 @@ import com.facebook.react.bridge.Arguments
|
|
|
4
4
|
import com.facebook.react.bridge.WritableMap
|
|
5
5
|
import com.tange.ai.tirtc.TiCloudStorageRecordingRange
|
|
6
6
|
import com.tange.ai.tirtc.TiCloudStorageRecordingDay
|
|
7
|
+
import com.tange.ai.tirtc.TiCloudStorageRecordingGap
|
|
8
|
+
import com.tange.ai.tirtc.TiCloudStorageExportProgress
|
|
9
|
+
import com.tange.ai.tirtc.TiCloudStorageExportReport
|
|
10
|
+
import com.tange.ai.tirtc.TiCloudStorageExportResult
|
|
11
|
+
import com.tange.ai.tirtc.TiRawDumpArchive
|
|
7
12
|
|
|
8
|
-
internal fun recordingStartResult(code: Int, taskId: Int
|
|
13
|
+
internal fun recordingStartResult(code: Int, taskId: Int?, generation: Int = 0): WritableMap =
|
|
9
14
|
Arguments.createMap().apply {
|
|
10
15
|
putInt("code", TiRtcReactNativeErrors.normalizeAndroid(code))
|
|
11
16
|
if (taskId == null) putNull("taskId") else putInt("taskId", taskId)
|
|
17
|
+
putInt("generation", generation)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
internal fun rawDumpStartResult(code: Int, dumpId: Int?): WritableMap =
|
|
21
|
+
Arguments.createMap().apply {
|
|
22
|
+
putInt("code", TiRtcReactNativeErrors.normalizeAndroid(code))
|
|
23
|
+
if (dumpId == null) putNull("dumpId") else putInt("dumpId", dumpId)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private fun rawDumpArchive(value: TiRawDumpArchive): WritableMap =
|
|
27
|
+
Arguments.createMap().apply {
|
|
28
|
+
putString("captureId", value.captureId)
|
|
29
|
+
putString("path", value.path)
|
|
30
|
+
putDouble("size", value.size.toDouble())
|
|
31
|
+
putString("sha256", value.sha256)
|
|
32
|
+
putDouble("capturedDurationMs", value.capturedDurationMs.toDouble())
|
|
33
|
+
putBoolean("captureComplete", value.captureComplete)
|
|
34
|
+
putInt("stopReason", value.stopReason.ordinal)
|
|
35
|
+
putDouble("unsavedPacketCount", value.unsavedPacketCount.toDouble())
|
|
36
|
+
putDouble("unsavedByteCount", value.unsavedByteCount.toDouble())
|
|
37
|
+
putBoolean("empty", value.empty)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
internal fun rawDumpStopResult(
|
|
41
|
+
code: Int,
|
|
42
|
+
value: TiRawDumpArchive?,
|
|
43
|
+
terminal: Boolean = false,
|
|
44
|
+
): WritableMap =
|
|
45
|
+
Arguments.createMap().apply {
|
|
46
|
+
putInt("code", TiRtcReactNativeErrors.normalizeAndroid(code))
|
|
47
|
+
if (value == null) putNull("archive") else putMap("archive", rawDumpArchive(value))
|
|
48
|
+
putBoolean("terminal", terminal)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private fun tiCloudStorageRecordingRange(range: TiCloudStorageRecordingRange): WritableMap =
|
|
52
|
+
Arguments.createMap().apply {
|
|
53
|
+
putDouble("startTimeMs", range.startTimeMs.toDouble())
|
|
54
|
+
putDouble("endTimeMs", range.endTimeMs.toDouble())
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
internal fun tiCloudStorageRecordingGap(gap: TiCloudStorageRecordingGap): WritableMap =
|
|
58
|
+
Arguments.createMap().apply {
|
|
59
|
+
putMap("range", tiCloudStorageRecordingRange(gap.range))
|
|
60
|
+
putArray("tracks", Arguments.createArray().apply {
|
|
61
|
+
gap.tracks.forEach { track ->
|
|
62
|
+
pushMap(Arguments.createMap().apply {
|
|
63
|
+
putInt("kind", track.kind.ordinal + 1)
|
|
64
|
+
putInt("channelId", track.channelId)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
putArray("reasons", Arguments.createArray().apply {
|
|
69
|
+
gap.reasons.forEach { pushInt(it.ordinal) }
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
internal fun tiCloudStorageExportProgress(progress: TiCloudStorageExportProgress): WritableMap =
|
|
74
|
+
Arguments.createMap().apply {
|
|
75
|
+
putDouble("fraction", progress.fraction)
|
|
76
|
+
putDouble("coveredDurationMs", progress.coveredDurationMs.toDouble())
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private fun tiCloudStorageExportReport(report: TiCloudStorageExportReport): WritableMap =
|
|
80
|
+
Arguments.createMap().apply {
|
|
81
|
+
putMap("requestedRange", tiCloudStorageRecordingRange(report.requestedRange))
|
|
82
|
+
putDouble("coveredDurationMs", report.coveredDurationMs.toDouble())
|
|
83
|
+
putArray("segments", Arguments.createArray().apply {
|
|
84
|
+
report.segments.forEach { segment ->
|
|
85
|
+
pushMap(Arguments.createMap().apply {
|
|
86
|
+
putMap("sourceRange", tiCloudStorageRecordingRange(segment.sourceRange))
|
|
87
|
+
putDouble("outputStartMs", segment.outputStartMs.toDouble())
|
|
88
|
+
putDouble("outputEndMs", segment.outputEndMs.toDouble())
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
putArray("gaps", Arguments.createArray().apply {
|
|
93
|
+
report.gaps.forEach { pushMap(tiCloudStorageRecordingGap(it)) }
|
|
94
|
+
})
|
|
95
|
+
putArray("unprocessedRanges", Arguments.createArray().apply {
|
|
96
|
+
report.unprocessedRanges.forEach { pushMap(tiCloudStorageRecordingRange(it)) }
|
|
97
|
+
})
|
|
98
|
+
putBoolean("complete", report.complete)
|
|
99
|
+
putInt("termination", report.termination.ordinal)
|
|
100
|
+
putInt("cause", TiRtcReactNativeErrors.normalizeAndroid(report.cause))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
internal fun tiCloudStorageExportResult(
|
|
104
|
+
result: TiCloudStorageExportResult,
|
|
105
|
+
fileId: Int?,
|
|
106
|
+
progress: TiCloudStorageExportProgress?,
|
|
107
|
+
): WritableMap =
|
|
108
|
+
Arguments.createMap().apply {
|
|
109
|
+
putInt("code", TiRtcReactNativeErrors.normalizeAndroid(result.code))
|
|
110
|
+
if (fileId == null) putNull("fileId") else putInt("fileId", fileId)
|
|
111
|
+
val file = result.file
|
|
112
|
+
val report = result.report
|
|
113
|
+
if (file == null) {
|
|
114
|
+
putNull("filePath")
|
|
115
|
+
putNull("durationMs")
|
|
116
|
+
} else {
|
|
117
|
+
putString("filePath", file.path)
|
|
118
|
+
putDouble("durationMs", file.durationMs.toDouble())
|
|
119
|
+
}
|
|
120
|
+
if (report == null) putNull("report") else putMap("report", tiCloudStorageExportReport(report))
|
|
121
|
+
if (progress == null) putNull("progressDetail") else putMap("progressDetail", tiCloudStorageExportProgress(progress))
|
|
12
122
|
}
|
|
13
123
|
|
|
14
124
|
internal fun tiCloudStorageRecordingsResult(code: Int, ranges: List<TiCloudStorageRecordingRange>): WritableMap =
|
|
@@ -16,10 +126,7 @@ internal fun tiCloudStorageRecordingsResult(code: Int, ranges: List<TiCloudStora
|
|
|
16
126
|
putInt("code", code)
|
|
17
127
|
putArray("recordings", Arguments.createArray().apply {
|
|
18
128
|
ranges.forEach { range ->
|
|
19
|
-
pushMap(
|
|
20
|
-
putDouble("startTimeMs", range.startTimeMs.toDouble())
|
|
21
|
-
putDouble("endTimeMs", range.endTimeMs.toDouble())
|
|
22
|
-
})
|
|
129
|
+
pushMap(tiCloudStorageRecordingRange(range))
|
|
23
130
|
}
|
|
24
131
|
})
|
|
25
132
|
}
|
|
@@ -12,7 +12,6 @@ import com.tange.ai.tirtc.TiRtcAudioSampleRate
|
|
|
12
12
|
import com.tange.ai.tirtc.TiRtcCameraFacing
|
|
13
13
|
import com.tange.ai.tirtc.TiRtcOutputBufferStrategy
|
|
14
14
|
import com.tange.ai.tirtc.TiRtcVideoDecoderPreference
|
|
15
|
-
import com.tange.ai.tirtc.TiRtcVideoEncoderCodec
|
|
16
15
|
import com.tange.ai.tirtc.TiRtcVideoFrameRate
|
|
17
16
|
import com.tange.ai.tirtc.TiRtcVideoInputOptions
|
|
18
17
|
import com.tange.ai.tirtc.TiRtcVideoOutputOptions
|
|
@@ -59,16 +58,12 @@ internal fun audioInputOptions(options: ReadableMap): TiRtcAudioInputOptions {
|
|
|
59
58
|
},
|
|
60
59
|
agcLevel = audioAgcLevel(options.getString("agcLevel")),
|
|
61
60
|
ansLevel = audioAnsLevel(options.getString("ansLevel")),
|
|
62
|
-
)
|
|
61
|
+
).apply { media = options.getInt("media") }
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
internal fun videoInputOptions(options: ReadableMap): TiRtcVideoInputOptions {
|
|
66
65
|
return TiRtcVideoInputOptions(
|
|
67
|
-
|
|
68
|
-
when (options.getString("codec")) {
|
|
69
|
-
"mjpeg" -> TiRtcVideoEncoderCodec.MJPEG
|
|
70
|
-
else -> TiRtcVideoEncoderCodec.H264
|
|
71
|
-
},
|
|
66
|
+
media = options.getInt("media"),
|
|
72
67
|
width = options.getInt("width"),
|
|
73
68
|
height = options.getInt("height"),
|
|
74
69
|
frameRate =
|
|
@@ -6,6 +6,7 @@ import com.tange.ai.tirtc.TiRtcConn
|
|
|
6
6
|
import com.tange.ai.tirtc.TiRtcVideoInput
|
|
7
7
|
import com.tange.ai.tirtc.TiRtcVideoOutput
|
|
8
8
|
import com.tange.ai.tirtc.TiRtcRecordingTask
|
|
9
|
+
import com.tange.ai.tirtc.TiRawDump
|
|
9
10
|
import com.tange.ai.tirtc.TiCloudStorage
|
|
10
11
|
import com.tange.ai.tirtc.TiCloudStorageAudioOutput
|
|
11
12
|
import com.tange.ai.tirtc.TiCloudStorageRecordingTask
|
|
@@ -63,6 +64,8 @@ internal object TiRtcReactNativeRegistry {
|
|
|
63
64
|
|
|
64
65
|
fun recordingTask(id: Int): TiRtcRecordingTask? = entries[id]?.value as? TiRtcRecordingTask
|
|
65
66
|
|
|
67
|
+
fun rawDump(id: Int): TiRawDump? = entries[id]?.value as? TiRawDump
|
|
68
|
+
|
|
66
69
|
fun cloudStorage(id: Int): TiCloudStorage? = entries[id]?.value as? TiCloudStorage
|
|
67
70
|
|
|
68
71
|
fun tiCloudStorageReplay(id: Int): TiCloudStorageReplay? = entries[id]?.value as? TiCloudStorageReplay
|
package/android/src/main/java/com/tange/ai/tirtc/reactnative/TiRtcReactNativeStorageSupport.kt
CHANGED
|
@@ -2,19 +2,49 @@ package com.tange.ai.tirtc.reactnative
|
|
|
2
2
|
|
|
3
3
|
import com.facebook.react.bridge.Promise
|
|
4
4
|
import com.tange.ai.tirtc.TiCloudStorageExportTask
|
|
5
|
-
import com.tange.ai.tirtc.
|
|
5
|
+
import com.tange.ai.tirtc.TiCloudStorageExportProgress
|
|
6
|
+
import com.tange.ai.tirtc.TiCloudStorageExportResult
|
|
7
|
+
import com.tange.ai.tirtc.TiCloudStorageRecordingGap
|
|
6
8
|
|
|
7
9
|
internal class CloudStorageExportBridge(
|
|
8
|
-
private val resolve: (Promise,
|
|
10
|
+
private val resolve: (Promise, TiCloudStorageExportResult, TiCloudStorageExportProgress?) -> Unit,
|
|
11
|
+
private val events: TiRtcReactNativeEventSink,
|
|
9
12
|
) {
|
|
10
13
|
@Volatile var task: TiCloudStorageExportTask? = null
|
|
14
|
+
private set
|
|
11
15
|
@Volatile var onDrained: (() -> Unit)? = null
|
|
12
|
-
private var result:
|
|
16
|
+
private var result: TiCloudStorageExportResult? = null
|
|
13
17
|
private val waiters = mutableListOf<Promise>()
|
|
14
18
|
private var invalidated = false
|
|
15
|
-
private var
|
|
19
|
+
private var startRejected = false
|
|
20
|
+
private var invalidationCompletion: ((TiCloudStorageExportResult?) -> Unit)? = null
|
|
21
|
+
var objectId = 0
|
|
16
22
|
|
|
17
|
-
fun
|
|
23
|
+
fun bindTask(value: TiCloudStorageExportTask) {
|
|
24
|
+
val cancel = synchronized(this) {
|
|
25
|
+
task = value
|
|
26
|
+
invalidated
|
|
27
|
+
}
|
|
28
|
+
if (cancel) value.cancel()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fun rejectStart() {
|
|
32
|
+
val completed = synchronized(this) {
|
|
33
|
+
startRejected = true
|
|
34
|
+
invalidationCompletion.also { invalidationCompletion = null }
|
|
35
|
+
}
|
|
36
|
+
completed?.invoke(null)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fun progress(value: TiCloudStorageExportProgress) {
|
|
40
|
+
events.tiCloudStorageExportProgress(objectId, value)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fun recordingGap(value: TiCloudStorageRecordingGap) {
|
|
44
|
+
events.tiCloudStorageExportRecordingGap(objectId, value)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fun complete(value: TiCloudStorageExportResult) {
|
|
18
48
|
val completed = synchronized(this) {
|
|
19
49
|
if (result != null) return
|
|
20
50
|
result = value
|
|
@@ -22,7 +52,7 @@ internal class CloudStorageExportBridge(
|
|
|
22
52
|
waiters.clear()
|
|
23
53
|
pending to invalidationCompletion.also { invalidationCompletion = null }
|
|
24
54
|
}
|
|
25
|
-
completed.first.forEach { resolve(it, value) }
|
|
55
|
+
completed.first.forEach { resolve(it, value, task?.progressDetail) }
|
|
26
56
|
if (completed.first.isNotEmpty()) onDrained?.invoke()
|
|
27
57
|
completed.second?.invoke(value)
|
|
28
58
|
}
|
|
@@ -35,30 +65,24 @@ internal class CloudStorageExportBridge(
|
|
|
35
65
|
}
|
|
36
66
|
}
|
|
37
67
|
if (completed != null) {
|
|
38
|
-
resolve(promise, completed)
|
|
68
|
+
resolve(promise, completed, task?.progressDetail)
|
|
39
69
|
onDrained?.invoke()
|
|
40
70
|
}
|
|
41
71
|
}
|
|
42
72
|
|
|
43
|
-
fun invalidate(onTerminal: (
|
|
44
|
-
val
|
|
73
|
+
fun invalidate(onTerminal: (TiCloudStorageExportResult?) -> Unit) {
|
|
74
|
+
val state = synchronized(this) {
|
|
45
75
|
invalidated = true
|
|
46
76
|
waiters.clear()
|
|
47
|
-
result
|
|
48
|
-
|
|
49
|
-
null
|
|
50
|
-
}
|
|
77
|
+
if (result == null && !startRejected) invalidationCompletion = onTerminal
|
|
78
|
+
Triple(result, startRejected, task)
|
|
51
79
|
}
|
|
52
|
-
if (
|
|
53
|
-
onTerminal(
|
|
80
|
+
if (state.first != null || state.second) {
|
|
81
|
+
onTerminal(state.first)
|
|
54
82
|
return
|
|
55
83
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
invalidationCompletion = null
|
|
60
|
-
}
|
|
61
|
-
onTerminal(null)
|
|
62
|
-
}
|
|
84
|
+
// An accepted task always reaches completion. If start is still in
|
|
85
|
+
// flight, bindTask requests cancellation once its handle is available.
|
|
86
|
+
state.third?.cancel()
|
|
63
87
|
}
|
|
64
88
|
}
|
|
@@ -39,6 +39,19 @@ static NSString* TiRtcReactNativeGalleryFileName(NSString* fileName, BOOL video)
|
|
|
39
39
|
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
|
|
40
40
|
@implementation TiRtcReactNative (Conn)
|
|
41
41
|
|
|
42
|
+
static NSDictionary* TiRtcReactNativeRawDumpArchiveResult(TiRawDumpStopResult* result) {
|
|
43
|
+
TiRawDumpArchive* value = result.archive;
|
|
44
|
+
if (value == nil) return @{ @"code": @(TiRtcReactNativeNormalizeDarwin(result.code)),
|
|
45
|
+
@"archive": (id)kCFNull, @"terminal": @NO };
|
|
46
|
+
return @{ @"code": @0, @"archive": @{
|
|
47
|
+
@"captureId": value.captureId, @"path": value.path, @"size": @(value.size),
|
|
48
|
+
@"sha256": value.sha256, @"capturedDurationMs": @(value.capturedDurationMs),
|
|
49
|
+
@"captureComplete": @(value.captureComplete), @"stopReason": @(value.stopReason),
|
|
50
|
+
@"unsavedPacketCount": @(value.unsavedPacketCount),
|
|
51
|
+
@"unsavedByteCount": @(value.unsavedByteCount), @"empty": @(value.empty),
|
|
52
|
+
}, @"terminal": @NO };
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
- (NSDictionary*)createConn {
|
|
43
56
|
return [self
|
|
44
57
|
registerObject:^id {
|
|
@@ -147,6 +160,54 @@ static NSString* TiRtcReactNativeGalleryFileName(NSString* fileName, BOOL video)
|
|
|
147
160
|
return @{@"code" : @(TiRtcReactNativeErrorOK), @"taskId" : @(taskId)};
|
|
148
161
|
}
|
|
149
162
|
|
|
163
|
+
- (void)connStartRawDump:(double)objectId
|
|
164
|
+
audioStreamIds:(NSArray*)audioStreamIds
|
|
165
|
+
videoStreamIds:(NSArray*)videoStreamIds
|
|
166
|
+
uplinkAudioStreamIds:(NSArray*)uplinkAudioStreamIds
|
|
167
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
168
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
169
|
+
TiRtcConn* conn = [self conn:objectId];
|
|
170
|
+
if (conn == nil) {
|
|
171
|
+
resolve(@{ @"code": @(TiRtcReactNativeErrorObjectReleased), @"dumpId": (id)kCFNull });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
TiRtcRawDumpOptions* options = [[TiRtcRawDumpOptions alloc]
|
|
175
|
+
initWithAudioStreamIds:audioStreamIds
|
|
176
|
+
videoStreamIds:videoStreamIds
|
|
177
|
+
uplinkAudioStreamIds:uplinkAudioStreamIds];
|
|
178
|
+
[conn startRawDumpWithOptions:options completion:^(TiRawDumpStartResult* result) {
|
|
179
|
+
if (result.code != 0 || result.dump == nil) {
|
|
180
|
+
resolve(@{ @"code": @(TiRtcReactNativeNormalizeDarwin(result.code)),
|
|
181
|
+
@"dumpId": (id)kCFNull });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
NSInteger dumpId = [[self registry] addObject:result.dump];
|
|
185
|
+
resolve(@{ @"code": @0, @"dumpId": @(dumpId) });
|
|
186
|
+
}];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
- (void)rawDumpStop:(double)objectId
|
|
190
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
191
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
192
|
+
id object = [[self registry] objectForId:(NSInteger)objectId];
|
|
193
|
+
if (![object isKindOfClass:[TiRawDump class]]) {
|
|
194
|
+
resolve(@{
|
|
195
|
+
@"code": @(TiRtcReactNativeErrorObjectReleased),
|
|
196
|
+
@"archive": (id)kCFNull,
|
|
197
|
+
@"terminal": @NO
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
[(TiRawDump*)object stopWithCompletion:^(TiRawDumpStopResult* result) {
|
|
202
|
+
BOOL terminal = result.code != [TiCloudStorageErrorCode inUse];
|
|
203
|
+
NSMutableDictionary* payload =
|
|
204
|
+
[TiRtcReactNativeRawDumpArchiveResult(result) mutableCopy];
|
|
205
|
+
payload[@"terminal"] = @(terminal);
|
|
206
|
+
if (terminal) [[self registry] releaseObject:(NSInteger)objectId];
|
|
207
|
+
resolve(payload);
|
|
208
|
+
}];
|
|
209
|
+
}
|
|
210
|
+
|
|
150
211
|
- (void)recordingTaskStop:(double)objectId
|
|
151
212
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
152
213
|
reject:(RCTPromiseRejectBlock)reject {
|
|
@@ -182,13 +182,7 @@
|
|
|
182
182
|
}
|
|
183
183
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
184
184
|
[output takeSnapshotWithCompletion:^(TiRtcSnapshotResult* snapshotResult) {
|
|
185
|
-
|
|
186
|
-
snapshotResult.file == nil ? 0 : [[self registry] addTransientObject:snapshotResult.file];
|
|
187
|
-
resolve(@{
|
|
188
|
-
@"code" : @(TiRtcReactNativeNormalizeDarwin(snapshotResult.code)),
|
|
189
|
-
@"fileId" : fileId == 0 ? (id)kCFNull : @(fileId),
|
|
190
|
-
@"filePath" : snapshotResult.file.path ?: (id)kCFNull
|
|
191
|
-
});
|
|
185
|
+
resolve([self snapshotResultWithCode:snapshotResult.code file:snapshotResult.file]);
|
|
192
186
|
}];
|
|
193
187
|
});
|
|
194
188
|
}
|
|
@@ -76,31 +76,79 @@ static NSDictionary* TiRtcReactNativeCloudStorageMp4Result(
|
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
static NSDictionary* TiRtcReactNativeCloudStorageExportResult(
|
|
80
|
+
TiRtcReactNativeRegistry* registry, TiCloudStorageExportResult* result,
|
|
81
|
+
TiCloudStorageExportProgress* progress) {
|
|
82
|
+
NSInteger fileId = result.file == nil ? 0 : [registry addTransientObject:result.file];
|
|
83
|
+
return @{
|
|
84
|
+
@"code" : @(TiRtcReactNativeNormalizeDarwin(result.code)),
|
|
85
|
+
@"fileId" : fileId == 0 ? (id)kCFNull : @(fileId),
|
|
86
|
+
@"filePath" : result.file.path ?: (id)kCFNull,
|
|
87
|
+
@"durationMs" : result.file == nil ? (id)kCFNull : @(result.file.durationMs),
|
|
88
|
+
@"report" : result.report == nil ? (id)kCFNull : TiRtcReactNativeCloudStorageExportReport(result.report),
|
|
89
|
+
@"progressDetail" : progress == nil ? (id)kCFNull : TiRtcReactNativeCloudStorageExportProgress(progress),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
79
93
|
@interface TiRtcReactNativeCloudStorageExportBridge : NSObject <TiRtcReactNativeReloadTask>
|
|
80
94
|
@property(nonatomic, strong) TiCloudStorageExportTask* task;
|
|
81
95
|
@property(nonatomic, assign) NSInteger objectId;
|
|
82
|
-
- (instancetype)initWithRegistry:(TiRtcReactNativeRegistry*)registry;
|
|
83
|
-
- (void)
|
|
96
|
+
- (instancetype)initWithRegistry:(TiRtcReactNativeRegistry*)registry binder:(TiRtcReactNativeEventBinder*)binder;
|
|
97
|
+
- (void)bindTask:(TiCloudStorageExportTask*)task;
|
|
98
|
+
- (void)rejectStart;
|
|
99
|
+
- (void)progress:(TiCloudStorageExportProgress*)progress;
|
|
100
|
+
- (void)recordingGap:(TiCloudStorageRecordingGap*)gap;
|
|
101
|
+
- (void)complete:(TiCloudStorageExportResult*)result;
|
|
84
102
|
- (void)await:(RCTPromiseResolveBlock)resolve;
|
|
85
103
|
@end
|
|
86
104
|
|
|
87
105
|
@implementation TiRtcReactNativeCloudStorageExportBridge {
|
|
88
106
|
TiRtcReactNativeRegistry* _registry;
|
|
89
|
-
|
|
107
|
+
TiRtcReactNativeEventBinder* _binder;
|
|
108
|
+
TiCloudStorageExportResult* _result;
|
|
90
109
|
NSMutableArray<RCTPromiseResolveBlock>* _waiters;
|
|
91
110
|
BOOL _invalidated;
|
|
111
|
+
BOOL _startRejected;
|
|
92
112
|
void (^_invalidationCompletion)(id file);
|
|
93
113
|
}
|
|
94
114
|
|
|
95
|
-
- (instancetype)initWithRegistry:(TiRtcReactNativeRegistry*)registry {
|
|
115
|
+
- (instancetype)initWithRegistry:(TiRtcReactNativeRegistry*)registry binder:(TiRtcReactNativeEventBinder*)binder {
|
|
96
116
|
if (self = [super init]) {
|
|
97
117
|
_registry = registry;
|
|
118
|
+
_binder = binder;
|
|
98
119
|
_waiters = [NSMutableArray new];
|
|
99
120
|
}
|
|
100
121
|
return self;
|
|
101
122
|
}
|
|
102
123
|
|
|
103
|
-
- (void)
|
|
124
|
+
- (void)bindTask:(TiCloudStorageExportTask*)task {
|
|
125
|
+
BOOL cancel;
|
|
126
|
+
@synchronized(self) {
|
|
127
|
+
self.task = task;
|
|
128
|
+
cancel = _invalidated;
|
|
129
|
+
}
|
|
130
|
+
if (cancel) [task cancel];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
- (void)rejectStart {
|
|
134
|
+
void (^completion)(id file);
|
|
135
|
+
@synchronized(self) {
|
|
136
|
+
_startRejected = YES;
|
|
137
|
+
completion = _invalidationCompletion;
|
|
138
|
+
_invalidationCompletion = nil;
|
|
139
|
+
}
|
|
140
|
+
if (completion != nil) completion(nil);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
- (void)progress:(TiCloudStorageExportProgress*)progress {
|
|
144
|
+
[_binder cloudStorageExportProgress:progress objectId:self.objectId];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
- (void)recordingGap:(TiCloudStorageRecordingGap*)gap {
|
|
148
|
+
[_binder cloudStorageExportRecordingGap:gap objectId:self.objectId];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
- (void)complete:(TiCloudStorageExportResult*)result {
|
|
104
152
|
NSArray<RCTPromiseResolveBlock>* waiters;
|
|
105
153
|
void (^invalidationCompletion)(id file);
|
|
106
154
|
@synchronized(self) {
|
|
@@ -112,14 +160,14 @@ static NSDictionary* TiRtcReactNativeCloudStorageMp4Result(
|
|
|
112
160
|
_invalidationCompletion = nil;
|
|
113
161
|
}
|
|
114
162
|
for (RCTPromiseResolveBlock resolve in waiters) {
|
|
115
|
-
resolve(
|
|
163
|
+
resolve(TiRtcReactNativeCloudStorageExportResult(_registry, result, self.task.progressDetail));
|
|
116
164
|
}
|
|
117
165
|
if (waiters.count > 0 && self.objectId > 0) [_registry releaseObject:self.objectId];
|
|
118
166
|
if (invalidationCompletion != nil) invalidationCompletion(result.file);
|
|
119
167
|
}
|
|
120
168
|
|
|
121
169
|
- (void)await:(RCTPromiseResolveBlock)resolve {
|
|
122
|
-
|
|
170
|
+
TiCloudStorageExportResult* result;
|
|
123
171
|
@synchronized(self) {
|
|
124
172
|
result = _result;
|
|
125
173
|
if (result == nil) {
|
|
@@ -127,29 +175,28 @@ static NSDictionary* TiRtcReactNativeCloudStorageMp4Result(
|
|
|
127
175
|
return;
|
|
128
176
|
}
|
|
129
177
|
}
|
|
130
|
-
resolve(
|
|
178
|
+
resolve(TiRtcReactNativeCloudStorageExportResult(_registry, result, self.task.progressDetail));
|
|
131
179
|
if (self.objectId > 0) [_registry releaseObject:self.objectId];
|
|
132
180
|
}
|
|
133
181
|
|
|
134
182
|
- (void)stopForReactNativeInvalidation:(void (^)(id file))completion {
|
|
135
|
-
|
|
183
|
+
TiCloudStorageExportResult* result;
|
|
184
|
+
TiCloudStorageExportTask* task;
|
|
185
|
+
BOOL rejected;
|
|
136
186
|
@synchronized(self) {
|
|
137
187
|
_invalidated = YES;
|
|
138
188
|
[_waiters removeAllObjects];
|
|
139
189
|
result = _result;
|
|
140
|
-
|
|
190
|
+
task = self.task;
|
|
191
|
+
rejected = _startRejected;
|
|
192
|
+
if (result == nil && !rejected) _invalidationCompletion = [completion copy];
|
|
141
193
|
}
|
|
142
|
-
if (result != nil) {
|
|
194
|
+
if (result != nil || rejected) {
|
|
143
195
|
completion(result.file);
|
|
144
196
|
return;
|
|
145
197
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
@synchronized(self) {
|
|
149
|
-
_invalidationCompletion = nil;
|
|
150
|
-
}
|
|
151
|
-
completion(nil);
|
|
152
|
-
}
|
|
198
|
+
// bindTask cancels after an in-flight start returns its accepted handle.
|
|
199
|
+
[task cancel];
|
|
153
200
|
}
|
|
154
201
|
|
|
155
202
|
@end
|
|
@@ -240,7 +287,7 @@ static NSDictionary* TiRtcReactNativeCloudStorageMp4Result(
|
|
|
240
287
|
return TiRtcReactNativeCreateResult(TiRtcReactNativeErrorObjectReleased, 0, 0, nil);
|
|
241
288
|
}
|
|
242
289
|
return [self
|
|
243
|
-
registerObject:^id { return [(TiCloudStorage*)object
|
|
290
|
+
registerObject:^id { return [(TiCloudStorage*)object createReplayWithCallbackThread:TiCloudStorageCallbackThreadBackground]; }
|
|
244
291
|
wire:^id(NSInteger replayId, id replay) {
|
|
245
292
|
return [[self eventBinder] wireCloudStorageReplay:(TiCloudStorageReplay*)replay objectId:replayId];
|
|
246
293
|
}];
|
|
@@ -253,25 +300,34 @@ static NSDictionary* TiRtcReactNativeCloudStorageMp4Result(
|
|
|
253
300
|
audioChannelId:(NSNumber*)audioChannelId {
|
|
254
301
|
id object = [[self registry] objectForId:(NSInteger)objectId];
|
|
255
302
|
if (![object isKindOfClass:[TiCloudStorage class]]) {
|
|
256
|
-
return @{@"code" : @(TiRtcReactNativeErrorObjectReleased), @"taskId" : (id)kCFNull};
|
|
303
|
+
return @{@"code" : @(TiRtcReactNativeErrorObjectReleased), @"taskId" : (id)kCFNull, @"generation" : @0};
|
|
257
304
|
}
|
|
258
305
|
TiRtcReactNativeCloudStorageExportBridge* bridge =
|
|
259
|
-
[[TiRtcReactNativeCloudStorageExportBridge alloc] initWithRegistry:[self registry]
|
|
306
|
+
[[TiRtcReactNativeCloudStorageExportBridge alloc] initWithRegistry:[self registry]
|
|
307
|
+
binder:[self eventBinder]];
|
|
260
308
|
TiCloudStorageExportRequest* request = [[TiCloudStorageExportRequest alloc]
|
|
261
309
|
initWithStartTimeMs:(int64_t)startTimeMs
|
|
262
310
|
endTimeMs:(int64_t)endTimeMs
|
|
263
311
|
videoChannelId:(NSInteger)videoChannelId
|
|
264
312
|
audioChannelId:audioChannelId];
|
|
313
|
+
NSInteger generation = 0;
|
|
314
|
+
NSInteger taskId = [[self registry] addObject:bridge generation:&generation];
|
|
315
|
+
bridge.objectId = taskId;
|
|
265
316
|
TiCloudStorageExportTaskStartResult* started = [(TiCloudStorage*)object
|
|
266
317
|
exportRecording:request
|
|
267
318
|
progress:nil
|
|
268
|
-
|
|
319
|
+
progressDetail:^(TiCloudStorageExportProgress* progress) { [bridge progress:progress]; }
|
|
320
|
+
onRecordingGap:^(TiCloudStorageRecordingGap* gap) { [bridge recordingGap:gap]; }
|
|
321
|
+
callbackThread:TiCloudStorageCallbackThreadBackground
|
|
322
|
+
completion:^(TiCloudStorageExportResult* result) { [bridge complete:result]; }];
|
|
269
323
|
if (started.code != 0 || started.task == nil) {
|
|
270
|
-
|
|
324
|
+
[bridge rejectStart];
|
|
325
|
+
[[self registry] releaseObject:taskId];
|
|
326
|
+
return @{@"code" : @(TiRtcReactNativeNormalizeDarwin(started.code)), @"taskId" : (id)kCFNull,
|
|
327
|
+
@"generation" : @0};
|
|
271
328
|
}
|
|
272
|
-
bridge
|
|
273
|
-
|
|
274
|
-
return @{@"code" : @0, @"taskId" : @(bridge.objectId)};
|
|
329
|
+
[bridge bindTask:started.task];
|
|
330
|
+
return @{@"code" : @0, @"taskId" : @(taskId), @"generation" : @(generation)};
|
|
275
331
|
}
|
|
276
332
|
|
|
277
333
|
- (NSNumber*)tiCloudStorageExportProgress:(double)objectId {
|
|
@@ -281,13 +337,23 @@ static NSDictionary* TiRtcReactNativeCloudStorageMp4Result(
|
|
|
281
337
|
: @0;
|
|
282
338
|
}
|
|
283
339
|
|
|
340
|
+
- (NSDictionary*)tiCloudStorageExportProgressDetail:(double)objectId {
|
|
341
|
+
id object = [[self registry] objectForId:(NSInteger)objectId];
|
|
342
|
+
TiCloudStorageExportProgress* progress =
|
|
343
|
+
[object isKindOfClass:[TiRtcReactNativeCloudStorageExportBridge class]]
|
|
344
|
+
? ((TiRtcReactNativeCloudStorageExportBridge*)object).task.progressDetail
|
|
345
|
+
: [[TiCloudStorageExportProgress alloc] initWithFraction:0 coveredDurationMs:0];
|
|
346
|
+
return TiRtcReactNativeCloudStorageExportProgress(progress);
|
|
347
|
+
}
|
|
348
|
+
|
|
284
349
|
- (void)tiCloudStorageExportResult:(double)objectId
|
|
285
350
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
286
351
|
reject:(RCTPromiseRejectBlock)reject {
|
|
287
352
|
id object = [[self registry] objectForId:(NSInteger)objectId];
|
|
288
353
|
if (![object isKindOfClass:[TiRtcReactNativeCloudStorageExportBridge class]]) {
|
|
289
354
|
resolve(@{@"code" : @(TiRtcReactNativeErrorObjectReleased), @"fileId" : (id)kCFNull,
|
|
290
|
-
@"filePath" : (id)kCFNull, @"durationMs" : (id)kCFNull
|
|
355
|
+
@"filePath" : (id)kCFNull, @"durationMs" : (id)kCFNull, @"report" : (id)kCFNull,
|
|
356
|
+
@"progressDetail" : (id)kCFNull});
|
|
291
357
|
return;
|
|
292
358
|
}
|
|
293
359
|
[(TiRtcReactNativeCloudStorageExportBridge*)object await:resolve];
|
|
@@ -302,6 +368,15 @@ static NSDictionary* TiRtcReactNativeCloudStorageMp4Result(
|
|
|
302
368
|
return @(TiRtcReactNativeNormalizeDarwin([task stop]));
|
|
303
369
|
}
|
|
304
370
|
|
|
371
|
+
- (NSNumber*)tiCloudStorageExportCancel:(double)objectId {
|
|
372
|
+
id object = [[self registry] objectForId:(NSInteger)objectId];
|
|
373
|
+
if (![object isKindOfClass:[TiRtcReactNativeCloudStorageExportBridge class]]) {
|
|
374
|
+
return @(TiRtcReactNativeErrorObjectReleased);
|
|
375
|
+
}
|
|
376
|
+
TiCloudStorageExportTask* task = ((TiRtcReactNativeCloudStorageExportBridge*)object).task;
|
|
377
|
+
return @(TiRtcReactNativeNormalizeDarwin([task cancel]));
|
|
378
|
+
}
|
|
379
|
+
|
|
305
380
|
- (NSNumber*)tiCloudStorageDispose:(double)objectId {
|
|
306
381
|
id object = [[self registry] objectForId:(NSInteger)objectId];
|
|
307
382
|
if (![object isKindOfClass:[TiCloudStorage class]]) return @(TiRtcReactNativeErrorObjectReleased);
|
|
@@ -358,15 +433,41 @@ static NSDictionary* TiRtcReactNativeCloudStorageMp4Result(
|
|
|
358
433
|
audioChannelId:(NSNumber*)audioChannelId {
|
|
359
434
|
id object = [[self registry] objectForId:(NSInteger)objectId];
|
|
360
435
|
if (![object isKindOfClass:[TiCloudStorageReplay class]]) {
|
|
361
|
-
return @{@"code" : @(TiRtcReactNativeErrorObjectReleased), @"taskId" : (id)kCFNull};
|
|
436
|
+
return @{@"code" : @(TiRtcReactNativeErrorObjectReleased), @"taskId" : (id)kCFNull, @"generation" : @0};
|
|
362
437
|
}
|
|
363
438
|
TiCloudStorageRecordingTaskStartResult* started = [(TiCloudStorageReplay*)object
|
|
364
439
|
startRecordingWithVideoChannelId:(NSInteger)videoChannelId audioChannelId:audioChannelId];
|
|
365
440
|
if (started.code != 0 || started.task == nil) {
|
|
366
|
-
return @{@"code" : @(TiRtcReactNativeNormalizeDarwin(started.code)), @"taskId" : (id)kCFNull
|
|
441
|
+
return @{@"code" : @(TiRtcReactNativeNormalizeDarwin(started.code)), @"taskId" : (id)kCFNull,
|
|
442
|
+
@"generation" : @0};
|
|
443
|
+
}
|
|
444
|
+
NSInteger generation = 0;
|
|
445
|
+
NSInteger taskId = [[self registry] addObject:started.task generation:&generation];
|
|
446
|
+
return @{@"code" : @0, @"taskId" : @(taskId), @"generation" : @(generation)};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
- (void)tiCloudStorageReplayStartRawDump:(double)objectId
|
|
450
|
+
audioChannelIds:(NSArray*)audioChannelIds
|
|
451
|
+
videoChannelIds:(NSArray*)videoChannelIds
|
|
452
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
453
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
454
|
+
id object = [[self registry] objectForId:(NSInteger)objectId];
|
|
455
|
+
if (![object isKindOfClass:[TiCloudStorageReplay class]]) {
|
|
456
|
+
resolve(@{ @"code": @(TiRtcReactNativeErrorObjectReleased), @"dumpId": (id)kCFNull });
|
|
457
|
+
return;
|
|
367
458
|
}
|
|
368
|
-
|
|
369
|
-
|
|
459
|
+
TiCloudStorageRawDumpOptions* options = [[TiCloudStorageRawDumpOptions alloc]
|
|
460
|
+
initWithAudioChannelIds:audioChannelIds videoChannelIds:videoChannelIds];
|
|
461
|
+
[(TiCloudStorageReplay*)object startRawDumpWithOptions:options
|
|
462
|
+
completion:^(TiRawDumpStartResult* result) {
|
|
463
|
+
if (result.code != 0 || result.dump == nil) {
|
|
464
|
+
resolve(@{ @"code": @(TiRtcReactNativeNormalizeDarwin(result.code)),
|
|
465
|
+
@"dumpId": (id)kCFNull });
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
NSInteger dumpId = [[self registry] addObject:result.dump];
|
|
469
|
+
resolve(@{ @"code": @0, @"dumpId": @(dumpId) });
|
|
470
|
+
}];
|
|
370
471
|
}
|
|
371
472
|
|
|
372
473
|
- (void)tiCloudStorageRecordingTaskStop:(double)objectId
|
|
@@ -491,10 +592,7 @@ static NSDictionary* TiRtcReactNativeCloudStorageMp4Result(
|
|
|
491
592
|
TiCloudStorageVideoOutput* output = (TiCloudStorageVideoOutput*)object;
|
|
492
593
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
493
594
|
[output takeSnapshotWithCompletion:^(TiCloudStorageSnapshotResult* result) {
|
|
494
|
-
|
|
495
|
-
resolve(@{@"code" : @(TiRtcReactNativeNormalizeDarwin(result.code)),
|
|
496
|
-
@"fileId" : fileId == 0 ? (id)kCFNull : @(fileId),
|
|
497
|
-
@"filePath" : result.file.path ?: (id)kCFNull});
|
|
595
|
+
resolve([self snapshotResultWithCode:result.code file:result.file]);
|
|
498
596
|
}];
|
|
499
597
|
});
|
|
500
598
|
}
|