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,362 @@
1
+ #import "MeshStreamP2P.h"
2
+ #import "MeshStreamP2PValidation.h"
3
+ #import <React/RCTBridgeModule.h>
4
+ #import <MeshStreamKit/MeshStreamKit.h>
5
+
6
+ static NSString * const MeshStreamEventName = @"MeshStreamP2P:event";
7
+
8
+ static NSString *MeshStreamReasonName(P2PUnavailableReason reason) {
9
+ switch (reason) {
10
+ case P2PUnavailableReasonOK: return @"OK";
11
+ case P2PUnavailableReasonNoPeers: return @"NO_PEERS";
12
+ case P2PUnavailableReasonTrackerError: return @"TRACKER_ERROR";
13
+ case P2PUnavailableReasonAuthFailed: return @"AUTH_FAILED";
14
+ case P2PUnavailableReasonAccountDisabled: return @"ACCOUNT_DISABLED";
15
+ case P2PUnavailableReasonAppDisabled: return @"APP_DISABLED";
16
+ case P2PUnavailableReasonAppNotFound: return @"APP_NOT_FOUND";
17
+ case P2PUnavailableReasonKeyDisabled: return @"KEY_DISABLED";
18
+ case P2PUnavailableReasonPlatformDisabled: return @"PLATFORM_DISABLED";
19
+ case P2PUnavailableReasonOriginDenied: return @"ORIGIN_DENIED";
20
+ case P2PUnavailableReasonPackageDenied: return @"PACKAGE_DENIED";
21
+ case P2PUnavailableReasonBundleDenied: return @"BUNDLE_DENIED";
22
+ case P2PUnavailableReasonSigningCertDenied: return @"SIGNING_CERT_DENIED";
23
+ case P2PUnavailableReasonTeamDenied: return @"TEAM_DENIED";
24
+ case P2PUnavailableReasonConfigUnavailable: return @"CONFIG_UNAVAILABLE";
25
+ case P2PUnavailableReasonInitializationFailed: return @"INITIALIZATION_FAILED";
26
+ case P2PUnavailableReasonUnsupported: return @"UNSUPPORTED";
27
+ case P2PUnavailableReasonDisabled: return @"DISABLED";
28
+ case P2PUnavailableReasonKeyP2PDisabled: return @"KEY_P2P_DISABLED";
29
+ case P2PUnavailableReasonQuotaExhausted: return @"QUOTA_EXHAUSTED";
30
+ case P2PUnavailableReasonQuotaDisabled: return @"QUOTA_DISABLED";
31
+ }
32
+ return @"INITIALIZATION_FAILED";
33
+ }
34
+
35
+ static NSDictionary *MeshStreamStatusMap(P2PSdkStatus *status) {
36
+ NSMutableDictionary *result = [@{
37
+ @"accountEnabled": @(status.accountEnabled),
38
+ @"applicationEnabled": @(status.applicationEnabled),
39
+ @"keyEnabled": @(status.keyEnabled),
40
+ @"platformEnabled": @(status.platformEnabled),
41
+ @"effectiveP2pEnabled": @(status.effectiveP2PEnabled),
42
+ @"reason": MeshStreamReasonName(status.reason),
43
+ } mutableCopy];
44
+ if (status.configVersion != nil) result[@"configVersion"] = status.configVersion;
45
+ if (status.edgeRegion != nil) result[@"edgeRegion"] = status.edgeRegion;
46
+ if (status.quotaStatus != nil) result[@"quotaStatus"] = status.quotaStatus;
47
+ return result;
48
+ }
49
+
50
+ static NSDictionary *MeshStreamStatsMap(P2PStats *stats) {
51
+ double downloadTotal = (double)(stats.httpDownloaded + stats.p2pDownloaded);
52
+ double segmentDownloadTotal = (double)(stats.p2pSegmentDownloadSuccesses + stats.p2pSegmentDownloadFailures);
53
+ double segmentUploadTotal = (double)(stats.p2pSegmentUploadSuccesses + stats.p2pSegmentUploadFailures);
54
+ return @{
55
+ @"httpDownloaded": @((double)stats.httpDownloaded),
56
+ @"p2pDownloaded": @((double)stats.p2pDownloaded),
57
+ @"p2pUploaded": @((double)stats.p2pUploaded),
58
+ @"httpSegmentDownloadSuccesses": @((double)stats.httpSegmentDownloadSuccesses),
59
+ @"httpSegmentDownloadFailures": @((double)stats.httpSegmentDownloadFailures),
60
+ @"p2pSegmentDownloadSuccesses": @((double)stats.p2pSegmentDownloadSuccesses),
61
+ @"p2pSegmentDownloadFailures": @((double)stats.p2pSegmentDownloadFailures),
62
+ @"p2pSegmentUploadSuccesses": @((double)stats.p2pSegmentUploadSuccesses),
63
+ @"p2pSegmentUploadFailures": @((double)stats.p2pSegmentUploadFailures),
64
+ @"connectedPeers": @(stats.connectedPeers),
65
+ @"peerConnectErrors": @((double)stats.peerConnectErrors),
66
+ @"trackerErrors": @((double)stats.trackerErrors),
67
+ @"downloadOffloadRatio": @(downloadTotal == 0 ? 0 : stats.p2pDownloaded / downloadTotal),
68
+ @"p2pSegmentDownloadSuccessRatio": @(segmentDownloadTotal == 0 ? 0 : stats.p2pSegmentDownloadSuccesses / segmentDownloadTotal),
69
+ @"p2pSegmentUploadSuccessRatio": @(segmentUploadTotal == 0 ? 0 : stats.p2pSegmentUploadSuccesses / segmentUploadTotal),
70
+ };
71
+ }
72
+
73
+ static NSDictionary *MeshStreamDiagnosticsMap(P2PDiagnostics *diagnostics) {
74
+ NSMutableDictionary *result = [@{
75
+ @"fallbackCount": @((double)diagnostics.fallbackCount),
76
+ @"iceAttempts": @((double)diagnostics.iceAttempts),
77
+ @"iceSuccesses": @((double)diagnostics.iceSuccesses),
78
+ @"iceFailures": @((double)diagnostics.iceFailures),
79
+ @"iceDurationMs": @((double)diagnostics.iceDurationMs),
80
+ } mutableCopy];
81
+ if (diagnostics.firstPeerConnectMs > 0) result[@"firstPeerConnectMs"] = @((double)diagnostics.firstPeerConnectMs);
82
+ if (diagnostics.coldStartP2PDownloaded > 0 || diagnostics.coldStartHTTPDownloaded > 0) {
83
+ result[@"coldStart"] = @{
84
+ @"p2pDownloaded": @((double)diagnostics.coldStartP2PDownloaded),
85
+ @"httpDownloaded": @((double)diagnostics.coldStartHTTPDownloaded),
86
+ };
87
+ }
88
+ if (diagnostics.lastTrackerError != nil) result[@"lastTrackerError"] = diagnostics.lastTrackerError;
89
+ if (diagnostics.lastPeerError != nil) result[@"lastPeerError"] = diagnostics.lastPeerError;
90
+ if (diagnostics.lastErrorAtMs > 0) result[@"lastErrorAtMs"] = @((double)diagnostics.lastErrorAtMs);
91
+ return result;
92
+ }
93
+
94
+ @interface MeshStreamBridgeClient : NSObject
95
+ @property(nonatomic, strong) P2PClient *client;
96
+ @property(nonatomic, strong) NSMutableDictionary<NSString *, PlaybackSession *> *sessions;
97
+ @end
98
+
99
+ @implementation MeshStreamBridgeClient
100
+ @end
101
+
102
+ @interface MeshStreamSessionRoute : NSObject
103
+ @property(nonatomic, copy) NSString *clientId;
104
+ @property(nonatomic, copy) NSString *sessionId;
105
+ @end
106
+
107
+ @implementation MeshStreamSessionRoute
108
+ @end
109
+
110
+ @interface MeshStreamP2P () <P2PEventListener>
111
+ @property(nonatomic, strong) NSMutableDictionary<NSString *, MeshStreamBridgeClient *> *clients;
112
+ @property(nonatomic, strong) NSMutableDictionary<NSString *, MeshStreamSessionRoute *> *sessionRoutes;
113
+ @property(nonatomic, strong) NSMutableDictionary<NSString *, NSMutableArray<NSDictionary *> *> *pendingEvents;
114
+ @end
115
+
116
+ @implementation MeshStreamP2P
117
+
118
+ RCT_EXPORT_MODULE(MeshStreamP2P)
119
+
120
+ + (BOOL)requiresMainQueueSetup { return NO; }
121
+
122
+ - (instancetype)init {
123
+ if ((self = [super init])) {
124
+ _clients = [NSMutableDictionary dictionary];
125
+ _sessionRoutes = [NSMutableDictionary dictionary];
126
+ _pendingEvents = [NSMutableDictionary dictionary];
127
+ }
128
+ return self;
129
+ }
130
+
131
+ - (NSArray<NSString *> *)supportedEvents { return @[MeshStreamEventName]; }
132
+
133
+ RCT_REMAP_METHOD(getSdkVersion,
134
+ getSdkVersionWithResolver:(RCTPromiseResolveBlock)resolve
135
+ rejecter:(RCTPromiseRejectBlock)reject) {
136
+ resolve([P2PSDK getSdkVersion] ?: @"unknown");
137
+ }
138
+
139
+ RCT_REMAP_METHOD(createClient,
140
+ createClientWithConfig:(NSDictionary *)config
141
+ resolver:(RCTPromiseResolveBlock)resolve
142
+ rejecter:(RCTPromiseRejectBlock)reject) {
143
+ NSString *key = MeshStreamTrimmedString(config[@"key"]) ?: @"";
144
+ if (key.length == 0) {
145
+ reject(@"MESHSTREAM_CREATE_CLIENT_FAILED", @"key must not be empty", nil);
146
+ return;
147
+ }
148
+ P2PConfig *nativeConfig = [[P2PConfig alloc] initWithKey:key];
149
+ if ([config[@"p2pEnabled"] isKindOfClass:NSNumber.class]) nativeConfig.p2pEnabled = [config[@"p2pEnabled"] boolValue];
150
+ NSError *error = nil;
151
+ P2PClient *client = [P2PSDK clientWithConfig:nativeConfig error:&error];
152
+ if (client == nil) {
153
+ reject(@"MESHSTREAM_CREATE_CLIENT_FAILED", error.localizedDescription ?: @"Unable to create MeshStream client.", error);
154
+ return;
155
+ }
156
+ MeshStreamBridgeClient *bridgeClient = [MeshStreamBridgeClient new];
157
+ bridgeClient.client = client;
158
+ bridgeClient.sessions = [NSMutableDictionary dictionary];
159
+ [client addEventListener:self];
160
+ NSString *clientId = NSUUID.UUID.UUIDString;
161
+ @synchronized (self.clients) { self.clients[clientId] = bridgeClient; }
162
+ resolve(@{ @"clientId": clientId, @"status": MeshStreamStatusMap(client.sdkStatus) });
163
+ }
164
+
165
+ RCT_EXPORT_METHOD(createPlaybackSession:(NSString *)clientId
166
+ options:(NSDictionary *)options
167
+ resolver:(RCTPromiseResolveBlock)resolve
168
+ rejecter:(RCTPromiseRejectBlock)reject) {
169
+ MeshStreamBridgeClient *bridgeClient = [self bridgeClient:clientId];
170
+ NSURL *url = MeshStreamHTTPURLFromValue(options[@"url"]);
171
+ if (bridgeClient == nil || url == nil) {
172
+ reject(@"MESHSTREAM_CREATE_SESSION_FAILED", bridgeClient == nil ? @"Unknown or closed MeshStream client." : @"options.url must be a valid URL.", nil);
173
+ return;
174
+ }
175
+ P2PPlaybackOptions *nativeOptions = [P2PPlaybackOptions new];
176
+ nativeOptions.contentId = MeshStreamTrimmedString(options[@"contentId"]);
177
+ nativeOptions.mimeType = MeshStreamTrimmedString(options[@"mimeType"]);
178
+ nativeOptions.originHeaders = MeshStreamStringHeaders(options[@"originHeaders"]);
179
+ NSError *error = nil;
180
+ PlaybackSession *session = [bridgeClient.client createPlaybackSessionWithURL:url options:nativeOptions error:&error];
181
+ if (session == nil) {
182
+ reject(@"MESHSTREAM_CREATE_SESSION_FAILED", error.localizedDescription ?: @"Unable to create MeshStream playback session.", error);
183
+ return;
184
+ }
185
+ NSString *sessionId = NSUUID.UUID.UUIDString;
186
+ @synchronized (bridgeClient.sessions) { bridgeClient.sessions[sessionId] = session; }
187
+ MeshStreamSessionRoute *route = [MeshStreamSessionRoute new];
188
+ route.clientId = clientId;
189
+ route.sessionId = sessionId;
190
+ @synchronized (self.sessionRoutes) { self.sessionRoutes[session.sessionId] = route; }
191
+ [self flushPendingEventsForNativeSessionId:session.sessionId clientId:clientId sessionId:sessionId];
192
+ NSMutableDictionary *result = [@{
193
+ @"clientId": clientId,
194
+ @"sessionId": sessionId,
195
+ @"playbackUrl": session.playbackUrl.absoluteString,
196
+ @"playbackMode": session.playbackMode == P2PPlaybackModeNativeP2P ? @"NATIVE_P2P" : @"NATIVE_CDN",
197
+ } mutableCopy];
198
+ if (session.playbackMode == P2PPlaybackModeNativeCDN) result[@"fallbackReason"] = MeshStreamReasonName(session.fallbackReason);
199
+ resolve(result);
200
+ }
201
+
202
+ RCT_EXPORT_METHOD(preload:(NSString *)clientId sessionId:(NSString *)sessionId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
203
+ PlaybackSession *session = [self sessionForClient:clientId sessionId:sessionId reject:reject];
204
+ if (session != nil) resolve(@([session preload]));
205
+ }
206
+
207
+ RCT_EXPORT_METHOD(setP2pEnabled:(NSString *)clientId sessionId:(NSString *)sessionId enabled:(BOOL)enabled resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
208
+ PlaybackSession *session = [self sessionForClient:clientId sessionId:sessionId reject:reject];
209
+ if (session != nil) { [session setP2PEnabled:enabled]; resolve(nil); }
210
+ }
211
+
212
+ RCT_EXPORT_METHOD(updateBufferedDurationMs:(NSString *)clientId sessionId:(NSString *)sessionId durationMs:(double)durationMs resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
213
+ PlaybackSession *session = [self sessionForClient:clientId sessionId:sessionId reject:reject];
214
+ if (session != nil) { [session updateBufferedDurationMs:(int64_t)MAX(0, durationMs)]; resolve(nil); }
215
+ }
216
+
217
+ RCT_EXPORT_METHOD(updateCurrentPositionMs:(NSString *)clientId sessionId:(NSString *)sessionId positionMs:(double)positionMs resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
218
+ PlaybackSession *session = [self sessionForClient:clientId sessionId:sessionId reject:reject];
219
+ if (session != nil) { [session updateCurrentPositionMs:(int64_t)MAX(0, positionMs)]; resolve(nil); }
220
+ }
221
+
222
+ RCT_EXPORT_METHOD(getStats:(NSString *)clientId sessionId:(NSString *)sessionId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
223
+ PlaybackSession *session = [self sessionForClient:clientId sessionId:sessionId reject:reject];
224
+ if (session != nil) resolve(MeshStreamStatsMap(session.statistics));
225
+ }
226
+
227
+ RCT_EXPORT_METHOD(getDiagnostics:(NSString *)clientId sessionId:(NSString *)sessionId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
228
+ PlaybackSession *session = [self sessionForClient:clientId sessionId:sessionId reject:reject];
229
+ if (session != nil) resolve(MeshStreamDiagnosticsMap(session.diagnostics));
230
+ }
231
+
232
+ RCT_EXPORT_METHOD(getStatus:(NSString *)clientId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
233
+ MeshStreamBridgeClient *bridgeClient = [self bridgeClient:clientId];
234
+ if (bridgeClient == nil) { reject(@"MESHSTREAM_STATUS_FAILED", @"Unknown or closed MeshStream client.", nil); return; }
235
+ resolve(MeshStreamStatusMap(bridgeClient.client.sdkStatus));
236
+ }
237
+
238
+ RCT_EXPORT_METHOD(closeSession:(NSString *)clientId sessionId:(NSString *)sessionId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
239
+ MeshStreamBridgeClient *bridgeClient = [self bridgeClient:clientId];
240
+ if (bridgeClient == nil) { reject(@"MESHSTREAM_CLOSE_SESSION_FAILED", @"Unknown or closed MeshStream client.", nil); return; }
241
+ PlaybackSession *session;
242
+ @synchronized (bridgeClient.sessions) { session = bridgeClient.sessions[sessionId]; [bridgeClient.sessions removeObjectForKey:sessionId]; }
243
+ if (session != nil) {
244
+ [self removeSessionRouteForNativeSessionId:session.sessionId];
245
+ [self removePendingEventsForNativeSessionId:session.sessionId];
246
+ }
247
+ [session close];
248
+ resolve(nil);
249
+ }
250
+
251
+ RCT_EXPORT_METHOD(closeClient:(NSString *)clientId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
252
+ MeshStreamBridgeClient *bridgeClient;
253
+ @synchronized (self.clients) { bridgeClient = self.clients[clientId]; [self.clients removeObjectForKey:clientId]; }
254
+ if (bridgeClient != nil) {
255
+ [bridgeClient.client removeEventListener:self];
256
+ NSArray<PlaybackSession *> *sessions;
257
+ @synchronized (bridgeClient.sessions) { sessions = bridgeClient.sessions.allValues; [bridgeClient.sessions removeAllObjects]; }
258
+ for (PlaybackSession *session in sessions) {
259
+ [self removeSessionRouteForNativeSessionId:session.sessionId];
260
+ [self removePendingEventsForNativeSessionId:session.sessionId];
261
+ [session close];
262
+ }
263
+ [bridgeClient.client close];
264
+ bridgeClient.client = nil;
265
+ }
266
+ resolve(nil);
267
+ }
268
+
269
+ - (MeshStreamBridgeClient *)bridgeClient:(NSString *)clientId {
270
+ @synchronized (self.clients) { return self.clients[clientId]; }
271
+ }
272
+
273
+ - (PlaybackSession *)sessionForClient:(NSString *)clientId sessionId:(NSString *)sessionId reject:(RCTPromiseRejectBlock)reject {
274
+ MeshStreamBridgeClient *bridgeClient = [self bridgeClient:clientId];
275
+ if (bridgeClient == nil) {
276
+ reject(@"MESHSTREAM_SESSION_OPERATION_FAILED", @"Unknown or closed MeshStream client.", nil);
277
+ return nil;
278
+ }
279
+ PlaybackSession *session;
280
+ @synchronized (bridgeClient.sessions) { session = bridgeClient.sessions[sessionId]; }
281
+ if (session == nil) reject(@"MESHSTREAM_SESSION_OPERATION_FAILED", @"Unknown or closed MeshStream playback session.", nil);
282
+ return session;
283
+ }
284
+
285
+ - (void)emitForNativeSessionId:(NSString *)nativeSessionId type:(NSString *)type extras:(NSDictionary *)extras {
286
+ MeshStreamSessionRoute *route;
287
+ @synchronized (self.sessionRoutes) { route = self.sessionRoutes[nativeSessionId]; }
288
+ if (route == nil) {
289
+ [self bufferPendingEventForNativeSessionId:nativeSessionId type:type extras:extras];
290
+ return;
291
+ }
292
+ [self sendSessionEventForClientId:route.clientId sessionId:route.sessionId type:type extras:extras];
293
+ }
294
+
295
+ - (void)sendSessionEventForClientId:(NSString *)clientId sessionId:(NSString *)sessionId type:(NSString *)type extras:(NSDictionary *)extras {
296
+ NSMutableDictionary *event = [@{ @"type": type, @"clientId": clientId, @"sessionId": sessionId } mutableCopy];
297
+ [event addEntriesFromDictionary:extras ?: @{}];
298
+ [self sendEventWithName:MeshStreamEventName body:event];
299
+ }
300
+
301
+ - (void)bufferPendingEventForNativeSessionId:(NSString *)nativeSessionId type:(NSString *)type extras:(NSDictionary *)extras {
302
+ if (nativeSessionId.length == 0) return;
303
+ @synchronized (self.pendingEvents) {
304
+ NSMutableArray<NSDictionary *> *events = self.pendingEvents[nativeSessionId];
305
+ if (events == nil) {
306
+ if (self.pendingEvents.count >= 64) return;
307
+ events = [NSMutableArray array];
308
+ self.pendingEvents[nativeSessionId] = events;
309
+ }
310
+ if (events.count < 8) [events addObject:@{ @"type": type, @"extras": extras ?: @{} }];
311
+ }
312
+ }
313
+
314
+ - (void)flushPendingEventsForNativeSessionId:(NSString *)nativeSessionId clientId:(NSString *)clientId sessionId:(NSString *)sessionId {
315
+ NSArray<NSDictionary *> *events;
316
+ @synchronized (self.pendingEvents) {
317
+ events = [self.pendingEvents[nativeSessionId] copy] ?: @[];
318
+ [self.pendingEvents removeObjectForKey:nativeSessionId];
319
+ }
320
+ for (NSDictionary *event in events) {
321
+ [self sendSessionEventForClientId:clientId sessionId:sessionId type:event[@"type"] extras:event[@"extras"]];
322
+ }
323
+ }
324
+
325
+ - (void)removePendingEventsForNativeSessionId:(NSString *)nativeSessionId {
326
+ @synchronized (self.pendingEvents) { [self.pendingEvents removeObjectForKey:nativeSessionId]; }
327
+ }
328
+
329
+ - (void)removeSessionRouteForNativeSessionId:(NSString *)nativeSessionId {
330
+ @synchronized (self.sessionRoutes) { [self.sessionRoutes removeObjectForKey:nativeSessionId]; }
331
+ }
332
+
333
+ - (void)onPlaybackPreparedWithSessionId:(NSString *)sessionId playbackUrl:(NSURL *)playbackUrl { [self emitForNativeSessionId:sessionId type:@"playbackPrepared" extras:@{ @"playbackUrl": playbackUrl.absoluteString ?: @"" }]; }
334
+ - (void)onStatisticsWithSessionId:(NSString *)sessionId stats:(P2PStats *)stats { [self emitForNativeSessionId:sessionId type:@"statistics" extras:@{ @"statistics": MeshStreamStatsMap(stats) }]; }
335
+ - (void)onPeerCountChangedWithSessionId:(NSString *)sessionId peerCount:(NSInteger)peerCount { [self emitForNativeSessionId:sessionId type:@"peerCountChanged" extras:@{ @"peerCount": @(peerCount) }]; }
336
+ - (void)onP2PReadyWithSessionId:(NSString *)sessionId { [self emitForNativeSessionId:sessionId type:@"p2pReady" extras:nil]; }
337
+ - (void)onP2PUnavailableWithSessionId:(NSString *)sessionId reason:(P2PUnavailableReason)reason { [self emitForNativeSessionId:sessionId type:@"p2pUnavailable" extras:@{ @"reason": MeshStreamReasonName(reason) }]; }
338
+ - (void)onCdnFallbackWithSessionId:(NSString *)sessionId bytes:(int64_t)bytes { [self emitForNativeSessionId:sessionId type:@"cdnFallback" extras:@{ @"bytes": @((double)bytes) }]; }
339
+ - (void)onTrackerErrorWithSessionId:(NSString *)sessionId totalErrors:(int64_t)totalErrors { [self emitForNativeSessionId:sessionId type:@"trackerError" extras:@{ @"totalErrors": @((double)totalErrors) }]; }
340
+ - (void)onPeerConnectErrorWithSessionId:(NSString *)sessionId totalErrors:(int64_t)totalErrors { [self emitForNativeSessionId:sessionId type:@"peerConnectError" extras:@{ @"totalErrors": @((double)totalErrors) }]; }
341
+
342
+ - (void)invalidate {
343
+ NSArray<NSString *> *clientIds;
344
+ @synchronized (self.clients) { clientIds = self.clients.allKeys; }
345
+ for (NSString *clientId in clientIds) {
346
+ MeshStreamBridgeClient *bridgeClient = [self bridgeClient:clientId];
347
+ [bridgeClient.client removeEventListener:self];
348
+ for (PlaybackSession *session in bridgeClient.sessions.allValues) {
349
+ [self removeSessionRouteForNativeSessionId:session.sessionId];
350
+ [session close];
351
+ }
352
+ [bridgeClient.sessions removeAllObjects];
353
+ [bridgeClient.client close];
354
+ bridgeClient.client = nil;
355
+ }
356
+ @synchronized (self.clients) { [self.clients removeAllObjects]; }
357
+ @synchronized (self.sessionRoutes) { [self.sessionRoutes removeAllObjects]; }
358
+ @synchronized (self.pendingEvents) { [self.pendingEvents removeAllObjects]; }
359
+ [super invalidate];
360
+ }
361
+
362
+ @end
@@ -0,0 +1,9 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ FOUNDATION_EXPORT NSString * _Nullable MeshStreamTrimmedString(id _Nullable value);
6
+ FOUNDATION_EXPORT NSURL * _Nullable MeshStreamHTTPURLFromValue(id _Nullable value);
7
+ FOUNDATION_EXPORT NSDictionary<NSString *, NSString *> *MeshStreamStringHeaders(id _Nullable value);
8
+
9
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,23 @@
1
+ #import "MeshStreamP2PValidation.h"
2
+
3
+ NSString *MeshStreamTrimmedString(id value) {
4
+ if (![value isKindOfClass:NSString.class]) return nil;
5
+ return [(NSString *)value stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
6
+ }
7
+
8
+ NSURL *MeshStreamHTTPURLFromValue(id value) {
9
+ NSString *text = MeshStreamTrimmedString(value);
10
+ NSURL *url = text.length > 0 ? [NSURL URLWithString:text] : nil;
11
+ NSString *scheme = url.scheme.lowercaseString;
12
+ if (!([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"]) || url.host.length == 0) return nil;
13
+ return url;
14
+ }
15
+
16
+ NSDictionary<NSString *, NSString *> *MeshStreamStringHeaders(id value) {
17
+ if (![value isKindOfClass:NSDictionary.class]) return @{};
18
+ NSMutableDictionary<NSString *, NSString *> *headers = [NSMutableDictionary dictionary];
19
+ [(NSDictionary *)value enumerateKeysAndObjectsUsingBlock:^(id key, id item, BOOL *stop) {
20
+ if ([key isKindOfClass:NSString.class] && [item isKindOfClass:NSString.class]) headers[key] = item;
21
+ }];
22
+ return headers;
23
+ }
@@ -0,0 +1,20 @@
1
+ import type { MeshStreamConfig, MeshStreamDiagnostics, MeshStreamSessionInfo, MeshStreamStats, MeshStreamStatus, PlaybackSessionOptions } from './types';
2
+ interface NativeMeshStreamP2P {
3
+ getSdkVersion(): Promise<string>;
4
+ createClient(config: MeshStreamConfig): Promise<{
5
+ clientId: string;
6
+ status: MeshStreamStatus;
7
+ }>;
8
+ createPlaybackSession(clientId: string, options: PlaybackSessionOptions): Promise<MeshStreamSessionInfo>;
9
+ preload(clientId: string, sessionId: string): Promise<boolean>;
10
+ setP2pEnabled(clientId: string, sessionId: string, enabled: boolean): Promise<void>;
11
+ updateBufferedDurationMs(clientId: string, sessionId: string, durationMs: number): Promise<void>;
12
+ updateCurrentPositionMs(clientId: string, sessionId: string, positionMs: number): Promise<void>;
13
+ getStats(clientId: string, sessionId: string): Promise<MeshStreamStats>;
14
+ getDiagnostics(clientId: string, sessionId: string): Promise<MeshStreamDiagnostics>;
15
+ getStatus(clientId: string): Promise<MeshStreamStatus>;
16
+ closeSession(clientId: string, sessionId: string): Promise<void>;
17
+ closeClient(clientId: string): Promise<void>;
18
+ }
19
+ export declare function requireNativeModule(): NativeMeshStreamP2P;
20
+ export {};
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requireNativeModule = requireNativeModule;
4
+ const react_native_1 = require("react-native");
5
+ const nativeModule = react_native_1.NativeModules.MeshStreamP2P;
6
+ function requireNativeModule() {
7
+ if (!nativeModule) {
8
+ throw new Error('MeshStreamP2P is not linked. Rebuild the iOS or Android app after installing react-native-meshstream.');
9
+ }
10
+ return nativeModule;
11
+ }
@@ -0,0 +1,46 @@
1
+ import { type EmitterSubscription } from 'react-native';
2
+ import type { MeshStreamConfig, MeshStreamDiagnostics, MeshStreamEvent, MeshStreamEventName, MeshStreamSessionInfo, MeshStreamStats, MeshStreamStatus, MeshStreamVersionInfo, PlaybackSessionOptions } from './types';
3
+ export * from './types';
4
+ export declare class MeshStreamPlaybackSession {
5
+ readonly clientId: string;
6
+ readonly sessionId: string;
7
+ readonly playbackUrl: string;
8
+ readonly playbackMode: MeshStreamSessionInfo['playbackMode'];
9
+ readonly fallbackReason?: MeshStreamSessionInfo['fallbackReason'];
10
+ private readonly isClientClosed;
11
+ private closed;
12
+ private closing?;
13
+ constructor(info: MeshStreamSessionInfo, isClientClosed?: () => boolean);
14
+ preload(): Promise<boolean>;
15
+ setP2pEnabled(enabled: boolean): Promise<void>;
16
+ updateBufferedDurationMs(durationMs: number): Promise<void>;
17
+ updateCurrentPositionMs(positionMs: number): Promise<void>;
18
+ getStats(): Promise<MeshStreamStats>;
19
+ getDiagnostics(): Promise<MeshStreamDiagnostics>;
20
+ close(): Promise<void>;
21
+ private withOpen;
22
+ }
23
+ export declare class MeshStreamClient {
24
+ readonly clientId: string;
25
+ private closed;
26
+ private closing?;
27
+ constructor(clientId: string);
28
+ createPlaybackSession(options: PlaybackSessionOptions): Promise<MeshStreamPlaybackSession>;
29
+ getStatus(): Promise<MeshStreamStatus>;
30
+ close(): Promise<void>;
31
+ private withOpen;
32
+ }
33
+ export declare const MeshStream: {
34
+ /** Returns the version of the native MeshStream SDK loaded by this application. */
35
+ getSdkVersion(): Promise<string>;
36
+ /** Returns the React Native package version and the loaded native engine version. */
37
+ getVersion(): Promise<MeshStreamVersionInfo>;
38
+ create(config: MeshStreamConfig): Promise<MeshStreamClient>;
39
+ addEventListener(listener: (event: MeshStreamEvent) => void): EmitterSubscription;
40
+ };
41
+ /** Filtered event subscription for a particular client or playback session. */
42
+ export declare function addMeshStreamEventListener(listener: (event: MeshStreamEvent) => void, filter?: {
43
+ clientId?: string;
44
+ sessionId?: string;
45
+ type?: MeshStreamEventName;
46
+ }): EmitterSubscription;
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.MeshStream = exports.MeshStreamClient = exports.MeshStreamPlaybackSession = void 0;
18
+ exports.addMeshStreamEventListener = addMeshStreamEventListener;
19
+ const react_native_1 = require("react-native");
20
+ const NativeMeshStreamP2P_1 = require("./NativeMeshStreamP2P");
21
+ const version_1 = require("./version");
22
+ __exportStar(require("./types"), exports);
23
+ const EVENT_NAME = 'MeshStreamP2P:event';
24
+ function assertNonEmpty(value, field) {
25
+ if (!value?.trim())
26
+ throw new Error(`${field} must not be empty.`);
27
+ }
28
+ function assertNonNegative(value, field) {
29
+ if (!Number.isFinite(value) || value < 0)
30
+ throw new Error(`${field} must be a finite non-negative number.`);
31
+ }
32
+ class MeshStreamPlaybackSession {
33
+ constructor(info, isClientClosed = () => false) {
34
+ this.closed = false;
35
+ this.clientId = info.clientId;
36
+ this.sessionId = info.sessionId;
37
+ this.playbackUrl = info.playbackUrl;
38
+ this.playbackMode = info.playbackMode;
39
+ this.fallbackReason = info.fallbackReason;
40
+ this.isClientClosed = isClientClosed;
41
+ }
42
+ async preload() {
43
+ return this.withOpen(() => (0, NativeMeshStreamP2P_1.requireNativeModule)().preload(this.clientId, this.sessionId));
44
+ }
45
+ async setP2pEnabled(enabled) {
46
+ return this.withOpen(() => (0, NativeMeshStreamP2P_1.requireNativeModule)().setP2pEnabled(this.clientId, this.sessionId, enabled));
47
+ }
48
+ async updateBufferedDurationMs(durationMs) {
49
+ assertNonNegative(durationMs, 'durationMs');
50
+ return this.withOpen(() => (0, NativeMeshStreamP2P_1.requireNativeModule)().updateBufferedDurationMs(this.clientId, this.sessionId, durationMs));
51
+ }
52
+ async updateCurrentPositionMs(positionMs) {
53
+ assertNonNegative(positionMs, 'positionMs');
54
+ return this.withOpen(() => (0, NativeMeshStreamP2P_1.requireNativeModule)().updateCurrentPositionMs(this.clientId, this.sessionId, positionMs));
55
+ }
56
+ async getStats() {
57
+ return this.withOpen(() => (0, NativeMeshStreamP2P_1.requireNativeModule)().getStats(this.clientId, this.sessionId));
58
+ }
59
+ async getDiagnostics() {
60
+ return this.withOpen(() => (0, NativeMeshStreamP2P_1.requireNativeModule)().getDiagnostics(this.clientId, this.sessionId));
61
+ }
62
+ async close() {
63
+ if (this.closed)
64
+ return;
65
+ if (this.isClientClosed()) {
66
+ return;
67
+ }
68
+ if (!this.closing) {
69
+ this.closing = (0, NativeMeshStreamP2P_1.requireNativeModule)()
70
+ .closeSession(this.clientId, this.sessionId)
71
+ .then(() => { this.closed = true; })
72
+ .finally(() => { this.closing = undefined; });
73
+ }
74
+ return this.closing;
75
+ }
76
+ async withOpen(operation) {
77
+ if (this.closed || this.closing || this.isClientClosed()) {
78
+ throw new Error('Playback session is closed.');
79
+ }
80
+ return operation();
81
+ }
82
+ }
83
+ exports.MeshStreamPlaybackSession = MeshStreamPlaybackSession;
84
+ class MeshStreamClient {
85
+ constructor(clientId) {
86
+ this.closed = false;
87
+ this.clientId = clientId;
88
+ }
89
+ async createPlaybackSession(options) {
90
+ assertNonEmpty(options.url, 'options.url');
91
+ return this.withOpen(async () => {
92
+ const info = await (0, NativeMeshStreamP2P_1.requireNativeModule)().createPlaybackSession(this.clientId, options);
93
+ if (this.closed || this.closing)
94
+ throw new Error('MeshStream client is closed.');
95
+ return new MeshStreamPlaybackSession(info, () => this.closed || this.closing !== undefined);
96
+ });
97
+ }
98
+ async getStatus() {
99
+ return this.withOpen(() => (0, NativeMeshStreamP2P_1.requireNativeModule)().getStatus(this.clientId));
100
+ }
101
+ async close() {
102
+ if (this.closed)
103
+ return;
104
+ if (!this.closing) {
105
+ this.closing = (0, NativeMeshStreamP2P_1.requireNativeModule)()
106
+ .closeClient(this.clientId)
107
+ .then(() => { this.closed = true; })
108
+ .finally(() => { this.closing = undefined; });
109
+ }
110
+ return this.closing;
111
+ }
112
+ async withOpen(operation) {
113
+ if (this.closed || this.closing)
114
+ throw new Error('MeshStream client is closed.');
115
+ return operation();
116
+ }
117
+ }
118
+ exports.MeshStreamClient = MeshStreamClient;
119
+ exports.MeshStream = {
120
+ /** Returns the version of the native MeshStream SDK loaded by this application. */
121
+ async getSdkVersion() {
122
+ return (0, NativeMeshStreamP2P_1.requireNativeModule)().getSdkVersion();
123
+ },
124
+ /** Returns the React Native package version and the loaded native engine version. */
125
+ async getVersion() {
126
+ const platform = react_native_1.Platform.OS;
127
+ if (platform !== 'android' && platform !== 'ios') {
128
+ throw new Error(`react-native-meshstream does not support ${platform}.`);
129
+ }
130
+ return {
131
+ packageVersion: version_1.MESHSTREAM_REACT_NATIVE_VERSION,
132
+ nativeSdkVersion: await exports.MeshStream.getSdkVersion(),
133
+ platform,
134
+ };
135
+ },
136
+ async create(config) {
137
+ assertNonEmpty(config.key, 'config.key');
138
+ const result = await (0, NativeMeshStreamP2P_1.requireNativeModule)().createClient(config);
139
+ return new MeshStreamClient(result.clientId);
140
+ },
141
+ addEventListener(listener) {
142
+ return react_native_1.DeviceEventEmitter.addListener(EVENT_NAME, listener);
143
+ },
144
+ };
145
+ /** Filtered event subscription for a particular client or playback session. */
146
+ function addMeshStreamEventListener(listener, filter) {
147
+ return exports.MeshStream.addEventListener((event) => {
148
+ if (filter?.clientId && event.clientId !== filter.clientId)
149
+ return;
150
+ if (filter?.sessionId && event.sessionId !== filter.sessionId)
151
+ return;
152
+ if (filter?.type && event.type !== filter.type)
153
+ return;
154
+ listener(event);
155
+ });
156
+ }