react-native-meshstream 0.0.0-stage → 1.0.2

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.
@@ -0,0 +1,380 @@
1
+ package net.meshstream.reactnative
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.Promise
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
7
+ import com.facebook.react.bridge.ReactMethod
8
+ import com.facebook.react.bridge.ReadableMap
9
+ import com.facebook.react.bridge.ReadableMapKeySetIterator
10
+ import com.facebook.react.bridge.ReadableType
11
+ import com.facebook.react.bridge.WritableMap
12
+ import com.facebook.react.modules.core.DeviceEventManagerModule
13
+ import io.meshstream.sdk.P2pClient
14
+ import io.meshstream.sdk.P2pConfig
15
+ import io.meshstream.sdk.P2pDiagnosticsSnapshot
16
+ import io.meshstream.sdk.P2pEventListener
17
+ import io.meshstream.sdk.P2pSdk
18
+ import io.meshstream.sdk.P2pSdkStatus
19
+ import io.meshstream.sdk.P2pStats
20
+ import io.meshstream.sdk.P2pUnavailableReason
21
+ import io.meshstream.sdk.PlaybackOptions
22
+ import io.meshstream.sdk.PlaybackSession
23
+ import java.util.UUID
24
+ import java.util.concurrent.ConcurrentHashMap
25
+
26
+ /**
27
+ * Classic React Native module backed by the Android MeshStream SDK. Native state is
28
+ * entirely keyed by the JS-visible client/session IDs so several players can coexist.
29
+ */
30
+ class MeshStreamP2PModule(
31
+ reactContext: ReactApplicationContext,
32
+ ) : ReactContextBaseJavaModule(reactContext) {
33
+ private data class BridgeClient(
34
+ val client: P2pClient,
35
+ val sessions: ConcurrentHashMap<String, PlaybackSession> = ConcurrentHashMap(),
36
+ val nativeSessionIds: ConcurrentHashMap<Long, String> = ConcurrentHashMap(),
37
+ val pendingEvents: ConcurrentHashMap<Long, MutableList<PendingEvent>> = ConcurrentHashMap(),
38
+ )
39
+
40
+ private data class PendingEvent(
41
+ val type: String,
42
+ val extras: WritableMap.() -> Unit,
43
+ )
44
+
45
+ private val clients = ConcurrentHashMap<String, BridgeClient>()
46
+
47
+ override fun getName(): String = MODULE_NAME
48
+
49
+ @ReactMethod
50
+ fun getSdkVersion(promise: Promise) {
51
+ try {
52
+ promise.resolve(P2pSdk.getSdkVersion())
53
+ } catch (error: Throwable) {
54
+ promise.reject("MESHSTREAM_VERSION_FAILED", error.message, error)
55
+ }
56
+ }
57
+
58
+ @ReactMethod
59
+ fun createClient(config: ReadableMap, promise: Promise) {
60
+ try {
61
+ val key = config.requiredString("key")
62
+ val client = P2pSdk.create(
63
+ reactApplicationContext.applicationContext,
64
+ P2pConfig(
65
+ key = key,
66
+ p2pEnabled = config.optionalBoolean("p2pEnabled") ?: true,
67
+ ),
68
+ )
69
+ val clientId = UUID.randomUUID().toString()
70
+ val bridgeClient = BridgeClient(client)
71
+ client.addEventListener(createEventListener(clientId, bridgeClient))
72
+ clients[clientId] = bridgeClient
73
+ promise.resolve(Arguments.createMap().apply {
74
+ putString("clientId", clientId)
75
+ putMap("status", client.status.toWritableMap())
76
+ })
77
+ } catch (error: Throwable) {
78
+ promise.reject("MESHSTREAM_CREATE_CLIENT_FAILED", error.message, error)
79
+ }
80
+ }
81
+
82
+ @ReactMethod
83
+ fun createPlaybackSession(clientId: String, options: ReadableMap, promise: Promise) {
84
+ try {
85
+ val bridgeClient = clients.requireClient(clientId)
86
+ val url = options.requiredString("url")
87
+ val session = bridgeClient.client.createPlaybackSession(
88
+ url,
89
+ PlaybackOptions(
90
+ contentId = options.optionalString("contentId"),
91
+ mimeType = options.optionalString("mimeType"),
92
+ originHeaders = options.stringMap("originHeaders"),
93
+ ),
94
+ )
95
+ val sessionId = UUID.randomUUID().toString()
96
+ bridgeClient.sessions[sessionId] = session
97
+ bridgeClient.nativeSessionIds[session.sessionId] = sessionId
98
+ flushPendingEvents(clientId, sessionId, session, bridgeClient)
99
+ promise.resolve(session.toWritableMap(clientId, sessionId))
100
+ } catch (error: Throwable) {
101
+ promise.reject("MESHSTREAM_CREATE_SESSION_FAILED", error.message, error)
102
+ }
103
+ }
104
+
105
+ @ReactMethod
106
+ fun preload(clientId: String, sessionId: String, promise: Promise) = withSession(clientId, sessionId, promise) {
107
+ it.preload()
108
+ }
109
+
110
+ @ReactMethod
111
+ fun setP2pEnabled(clientId: String, sessionId: String, enabled: Boolean, promise: Promise) =
112
+ withSession(clientId, sessionId, promise) {
113
+ it.setP2pEnabled(enabled)
114
+ null
115
+ }
116
+
117
+ @ReactMethod
118
+ fun updateBufferedDurationMs(clientId: String, sessionId: String, durationMs: Double, promise: Promise) =
119
+ withSession(clientId, sessionId, promise) {
120
+ it.updateBufferedDurationMs(durationMs.toLong().coerceAtLeast(0))
121
+ null
122
+ }
123
+
124
+ @ReactMethod
125
+ fun updateCurrentPositionMs(clientId: String, sessionId: String, positionMs: Double, promise: Promise) =
126
+ withSession(clientId, sessionId, promise) {
127
+ it.updateCurrentPositionMs(positionMs.toLong().coerceAtLeast(0))
128
+ null
129
+ }
130
+
131
+ @ReactMethod
132
+ fun getStats(clientId: String, sessionId: String, promise: Promise) = withSession(clientId, sessionId, promise) {
133
+ it.statistics().toWritableMap()
134
+ }
135
+
136
+ @ReactMethod
137
+ fun getDiagnostics(clientId: String, sessionId: String, promise: Promise) = withSession(clientId, sessionId, promise) {
138
+ it.diagnostics().toWritableMap()
139
+ }
140
+
141
+ @ReactMethod
142
+ fun getStatus(clientId: String, promise: Promise) {
143
+ try {
144
+ promise.resolve(clients.requireClient(clientId).client.status.toWritableMap())
145
+ } catch (error: Throwable) {
146
+ promise.reject("MESHSTREAM_STATUS_FAILED", error.message, error)
147
+ }
148
+ }
149
+
150
+ @ReactMethod
151
+ fun closeSession(clientId: String, sessionId: String, promise: Promise) {
152
+ try {
153
+ val bridgeClient = clients.requireClient(clientId)
154
+ bridgeClient.sessions.remove(sessionId)?.let { session ->
155
+ bridgeClient.nativeSessionIds.remove(session.sessionId, sessionId)
156
+ bridgeClient.pendingEvents.remove(session.sessionId)
157
+ session.close()
158
+ }
159
+ promise.resolve(null)
160
+ } catch (error: Throwable) {
161
+ promise.reject("MESHSTREAM_CLOSE_SESSION_FAILED", error.message, error)
162
+ }
163
+ }
164
+
165
+ @ReactMethod
166
+ fun closeClient(clientId: String, promise: Promise) {
167
+ try {
168
+ clients.remove(clientId)?.let { bridgeClient ->
169
+ bridgeClient.sessions.values.forEach { it.close() }
170
+ bridgeClient.sessions.clear()
171
+ bridgeClient.nativeSessionIds.clear()
172
+ bridgeClient.pendingEvents.clear()
173
+ bridgeClient.client.close()
174
+ }
175
+ promise.resolve(null)
176
+ } catch (error: Throwable) {
177
+ promise.reject("MESHSTREAM_CLOSE_CLIENT_FAILED", error.message, error)
178
+ }
179
+ }
180
+
181
+ override fun invalidate() {
182
+ clients.values.forEach { bridgeClient ->
183
+ bridgeClient.sessions.values.forEach { it.close() }
184
+ bridgeClient.nativeSessionIds.clear()
185
+ bridgeClient.pendingEvents.clear()
186
+ bridgeClient.client.close()
187
+ }
188
+ clients.clear()
189
+ super.invalidate()
190
+ }
191
+
192
+ private fun withSession(clientId: String, sessionId: String, promise: Promise, operation: (PlaybackSession) -> Any?) {
193
+ try {
194
+ promise.resolve(operation(clients.requireClient(clientId).sessions.requireSession(sessionId)))
195
+ } catch (error: Throwable) {
196
+ promise.reject("MESHSTREAM_SESSION_OPERATION_FAILED", error.message, error)
197
+ }
198
+ }
199
+
200
+ private fun createEventListener(clientId: String, bridgeClient: BridgeClient) = object : P2pEventListener {
201
+ private fun sessionId(nativeSessionId: Long): String? = bridgeClient.nativeSessionIds[nativeSessionId]
202
+
203
+ private fun emit(nativeSessionId: Long, type: String, extras: WritableMap.() -> Unit = {}) {
204
+ val jsSessionId = sessionId(nativeSessionId)
205
+ if (jsSessionId == null) {
206
+ bufferPendingEvent(bridgeClient, nativeSessionId, PendingEvent(type, extras))
207
+ return
208
+ }
209
+ sendSessionEvent(clientId, jsSessionId, type, extras)
210
+ }
211
+
212
+ override fun onStatistics(sessionId: Long, stats: P2pStats) = emit(sessionId, "statistics") {
213
+ putMap("statistics", stats.toWritableMap())
214
+ }
215
+
216
+ override fun onPeerCountChanged(sessionId: Long, peerCount: Int) = emit(sessionId, "peerCountChanged") {
217
+ putInt("peerCount", peerCount)
218
+ }
219
+
220
+ override fun onPlaybackPrepared(sessionId: Long, playbackUrl: String) = emit(sessionId, "playbackPrepared") {
221
+ putString("playbackUrl", playbackUrl)
222
+ }
223
+
224
+ override fun onP2pReady(sessionId: Long) = emit(sessionId, "p2pReady")
225
+
226
+ override fun onP2pUnavailable(sessionId: Long, reason: P2pUnavailableReason) = emit(sessionId, "p2pUnavailable") {
227
+ putString("reason", reason.name)
228
+ }
229
+
230
+ override fun onTrackerError(sessionId: Long, totalErrors: Long) = emit(sessionId, "trackerError") {
231
+ putDouble("totalErrors", totalErrors.toDouble())
232
+ }
233
+
234
+ override fun onPeerConnectError(sessionId: Long, totalErrors: Long) = emit(sessionId, "peerConnectError") {
235
+ putDouble("totalErrors", totalErrors.toDouble())
236
+ }
237
+
238
+ override fun onCdnFallback(sessionId: Long, bytes: Long) = emit(sessionId, "cdnFallback") {
239
+ putDouble("bytes", bytes.toDouble())
240
+ }
241
+ }
242
+
243
+ private fun bufferPendingEvent(bridgeClient: BridgeClient, nativeSessionId: Long, event: PendingEvent) {
244
+ synchronized(bridgeClient.pendingEvents) {
245
+ val events = bridgeClient.pendingEvents[nativeSessionId]
246
+ if (events != null) {
247
+ if (events.size < MAX_PENDING_EVENTS_PER_SESSION) events += event
248
+ return
249
+ }
250
+ if (bridgeClient.pendingEvents.size >= MAX_PENDING_EVENT_SESSIONS) return
251
+ bridgeClient.pendingEvents[nativeSessionId] = mutableListOf(event)
252
+ }
253
+ }
254
+
255
+ private fun flushPendingEvents(
256
+ clientId: String,
257
+ jsSessionId: String,
258
+ session: PlaybackSession,
259
+ bridgeClient: BridgeClient,
260
+ ) {
261
+ val events = synchronized(bridgeClient.pendingEvents) {
262
+ bridgeClient.pendingEvents.remove(session.sessionId)?.toList().orEmpty()
263
+ }
264
+ events.forEach { event -> sendSessionEvent(clientId, jsSessionId, event.type, event.extras) }
265
+ }
266
+
267
+ private fun sendSessionEvent(
268
+ clientId: String,
269
+ sessionId: String,
270
+ type: String,
271
+ extras: WritableMap.() -> Unit,
272
+ ) {
273
+ sendEvent(Arguments.createMap().apply {
274
+ putString("type", type)
275
+ putString("clientId", clientId)
276
+ putString("sessionId", sessionId)
277
+ extras()
278
+ })
279
+ }
280
+
281
+ private fun sendEvent(event: WritableMap) {
282
+ if (reactApplicationContext.hasActiveReactInstance()) {
283
+ reactApplicationContext
284
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
285
+ .emit(EVENT_NAME, event)
286
+ }
287
+ }
288
+
289
+ private fun ConcurrentHashMap<String, BridgeClient>.requireClient(clientId: String): BridgeClient =
290
+ this[clientId] ?: error("Unknown or closed MeshStream client: $clientId")
291
+
292
+ private fun ConcurrentHashMap<String, PlaybackSession>.requireSession(sessionId: String): PlaybackSession =
293
+ this[sessionId] ?: error("Unknown or closed MeshStream playback session: $sessionId")
294
+
295
+ private fun ReadableMap.requiredString(name: String): String = optionalString(name)
296
+ ?.takeIf { it.isNotBlank() }
297
+ ?: error("$name must not be empty")
298
+
299
+ private fun ReadableMap.optionalString(name: String): String? =
300
+ if (hasKey(name) && !isNull(name) && getType(name) == ReadableType.String) getString(name)?.trim() else null
301
+
302
+ private fun ReadableMap.optionalBoolean(name: String): Boolean? =
303
+ if (hasKey(name) && !isNull(name) && getType(name) == ReadableType.Boolean) getBoolean(name) else null
304
+
305
+ private fun ReadableMap.stringMap(name: String): Map<String, String> {
306
+ if (!hasKey(name) || isNull(name) || getType(name) != ReadableType.Map) return emptyMap()
307
+ val result = linkedMapOf<String, String>()
308
+ val map = getMap(name) ?: return emptyMap()
309
+ val iterator: ReadableMapKeySetIterator = map.keySetIterator()
310
+ while (iterator.hasNextKey()) {
311
+ val key = iterator.nextKey()
312
+ if (map.getType(key) == ReadableType.String) map.getString(key)?.let { result[key] = it }
313
+ }
314
+ return result
315
+ }
316
+
317
+ private fun PlaybackSession.toWritableMap(clientId: String, sessionId: String): WritableMap = Arguments.createMap().apply {
318
+ putString("clientId", clientId)
319
+ putString("sessionId", sessionId)
320
+ putString("playbackUrl", playbackUrl)
321
+ putString("playbackMode", playbackMode.name)
322
+ fallbackReason?.let { putString("fallbackReason", it.name) }
323
+ }
324
+
325
+ private fun P2pSdkStatus.toWritableMap(): WritableMap = Arguments.createMap().apply {
326
+ putBoolean("accountEnabled", accountEnabled)
327
+ putBoolean("applicationEnabled", applicationEnabled)
328
+ putBoolean("keyEnabled", keyEnabled)
329
+ putBoolean("platformEnabled", platformEnabled)
330
+ putBoolean("effectiveP2pEnabled", effectiveP2pEnabled)
331
+ putString("reason", reason.name)
332
+ configVersion?.let { putString("configVersion", it) }
333
+ edgeRegion?.let { putString("edgeRegion", it) }
334
+ quotaStatus?.let { putString("quotaStatus", it) }
335
+ trackerPoolReady?.let { putBoolean("trackerPoolReady", it) }
336
+ }
337
+
338
+ private fun P2pStats.toWritableMap(): WritableMap = Arguments.createMap().apply {
339
+ putDouble("httpDownloaded", httpDownloaded.toDouble())
340
+ putDouble("p2pDownloaded", p2pDownloaded.toDouble())
341
+ putDouble("p2pUploaded", p2pUploaded.toDouble())
342
+ putDouble("httpSegmentDownloadSuccesses", httpSegmentDownloadSuccesses.toDouble())
343
+ putDouble("httpSegmentDownloadFailures", httpSegmentDownloadFailures.toDouble())
344
+ putDouble("p2pSegmentDownloadSuccesses", p2pSegmentDownloadSuccesses.toDouble())
345
+ putDouble("p2pSegmentDownloadFailures", p2pSegmentDownloadFailures.toDouble())
346
+ putDouble("p2pSegmentUploadSuccesses", p2pSegmentUploadSuccesses.toDouble())
347
+ putDouble("p2pSegmentUploadFailures", p2pSegmentUploadFailures.toDouble())
348
+ putInt("connectedPeers", connectedPeers)
349
+ putDouble("peerConnectErrors", peerConnectErrors.toDouble())
350
+ putDouble("trackerErrors", trackerErrors.toDouble())
351
+ putDouble("downloadOffloadRatio", downloadOffloadRatio)
352
+ putDouble("p2pSegmentDownloadSuccessRatio", p2pSegmentDownloadSuccessRatio)
353
+ putDouble("p2pSegmentUploadSuccessRatio", p2pSegmentUploadSuccessRatio)
354
+ }
355
+
356
+ private fun P2pDiagnosticsSnapshot.toWritableMap(): WritableMap = Arguments.createMap().apply {
357
+ putDouble("fallbackCount", fallbackCount.toDouble())
358
+ firstPeerConnectMs?.let { putDouble("firstPeerConnectMs", it.toDouble()) }
359
+ coldStart?.let {
360
+ putMap("coldStart", Arguments.createMap().apply {
361
+ putDouble("p2pDownloaded", it.p2pDownloaded.toDouble())
362
+ putDouble("httpDownloaded", it.httpDownloaded.toDouble())
363
+ })
364
+ }
365
+ lastTrackerError?.let { putString("lastTrackerError", it) }
366
+ lastPeerError?.let { putString("lastPeerError", it) }
367
+ lastErrorAtMs?.let { putDouble("lastErrorAtMs", it.toDouble()) }
368
+ putDouble("iceAttempts", iceAttempts.toDouble())
369
+ putDouble("iceSuccesses", iceSuccesses.toDouble())
370
+ putDouble("iceFailures", iceFailures.toDouble())
371
+ putDouble("iceDurationMs", iceDurationMs.toDouble())
372
+ }
373
+
374
+ companion object {
375
+ private const val MODULE_NAME = "MeshStreamP2P"
376
+ private const val EVENT_NAME = "MeshStreamP2P:event"
377
+ private const val MAX_PENDING_EVENT_SESSIONS = 64
378
+ private const val MAX_PENDING_EVENTS_PER_SESSION = 8
379
+ }
380
+ }
@@ -0,0 +1,13 @@
1
+ package net.meshstream.reactnative
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ class MeshStreamP2PPackage : ReactPackage {
9
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> =
10
+ listOf(MeshStreamP2PModule(reactContext))
11
+
12
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> = emptyList()
13
+ }
@@ -0,0 +1,4 @@
1
+ #import <React/RCTEventEmitter.h>
2
+
3
+ @interface MeshStreamP2P : RCTEventEmitter
4
+ @end