react-native-meshstream 0.0.0-stage → 1.0.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/CHANGELOG.md +8 -0
- package/LICENSE +176 -0
- package/MeshStreamP2P.podspec +20 -0
- package/README.md +125 -2
- package/README.zh-CN.md +126 -0
- package/android/build.gradle +23 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/net/meshstream/reactnative/MeshStreamP2PModule.kt +367 -0
- package/android/src/main/java/net/meshstream/reactnative/MeshStreamP2PPackage.kt +13 -0
- package/ios/MeshStreamP2P.h +4 -0
- package/ios/MeshStreamP2P.m +339 -0
- package/ios/MeshStreamP2PValidation.h +9 -0
- package/ios/MeshStreamP2PValidation.m +23 -0
- package/lib/typescript/NativeMeshStreamP2P.d.ts +19 -0
- package/lib/typescript/NativeMeshStreamP2P.js +11 -0
- package/lib/typescript/index.d.ts +41 -0
- package/lib/typescript/index.js +129 -0
- package/lib/typescript/types.d.ts +78 -0
- package/lib/typescript/types.js +2 -0
- package/package.json +49 -4
- package/react-native.config.js +11 -0
- package/src/NativeMeshStreamP2P.ts +35 -0
- package/src/index.ts +147 -0
- package/src/types.ts +118 -0
- package/tests/ios/MeshStreamP2PValidationTests.m +30 -0
|
@@ -0,0 +1,339 @@
|
|
|
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 MeshStreamP2P () <P2PEventListener>
|
|
103
|
+
@property(nonatomic, strong) NSMutableDictionary<NSString *, MeshStreamBridgeClient *> *clients;
|
|
104
|
+
@property(nonatomic, strong) NSMutableDictionary<NSString *, NSMutableArray<NSDictionary *> *> *pendingEvents;
|
|
105
|
+
@end
|
|
106
|
+
|
|
107
|
+
@implementation MeshStreamP2P
|
|
108
|
+
|
|
109
|
+
RCT_EXPORT_MODULE(MeshStreamP2P)
|
|
110
|
+
|
|
111
|
+
+ (BOOL)requiresMainQueueSetup { return NO; }
|
|
112
|
+
|
|
113
|
+
- (instancetype)init {
|
|
114
|
+
if ((self = [super init])) {
|
|
115
|
+
_clients = [NSMutableDictionary dictionary];
|
|
116
|
+
_pendingEvents = [NSMutableDictionary dictionary];
|
|
117
|
+
}
|
|
118
|
+
return self;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
- (NSArray<NSString *> *)supportedEvents { return @[MeshStreamEventName]; }
|
|
122
|
+
|
|
123
|
+
RCT_REMAP_METHOD(createClient,
|
|
124
|
+
createClientWithConfig:(NSDictionary *)config
|
|
125
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
126
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
127
|
+
NSString *key = MeshStreamTrimmedString(config[@"key"]) ?: @"";
|
|
128
|
+
if (key.length == 0) {
|
|
129
|
+
reject(@"MESHSTREAM_CREATE_CLIENT_FAILED", @"key must not be empty", nil);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
P2PConfig *nativeConfig = [[P2PConfig alloc] initWithKey:key];
|
|
133
|
+
if ([config[@"p2pEnabled"] isKindOfClass:NSNumber.class]) nativeConfig.p2pEnabled = [config[@"p2pEnabled"] boolValue];
|
|
134
|
+
NSError *error = nil;
|
|
135
|
+
P2PClient *client = [P2PSDK clientWithConfig:nativeConfig error:&error];
|
|
136
|
+
if (client == nil) {
|
|
137
|
+
reject(@"MESHSTREAM_CREATE_CLIENT_FAILED", error.localizedDescription ?: @"Unable to create MeshStream client.", error);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
MeshStreamBridgeClient *bridgeClient = [MeshStreamBridgeClient new];
|
|
141
|
+
bridgeClient.client = client;
|
|
142
|
+
bridgeClient.sessions = [NSMutableDictionary dictionary];
|
|
143
|
+
[client addEventListener:self];
|
|
144
|
+
NSString *clientId = NSUUID.UUID.UUIDString;
|
|
145
|
+
@synchronized (self.clients) { self.clients[clientId] = bridgeClient; }
|
|
146
|
+
resolve(@{ @"clientId": clientId, @"status": MeshStreamStatusMap(client.sdkStatus) });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
RCT_EXPORT_METHOD(createPlaybackSession:(NSString *)clientId
|
|
150
|
+
options:(NSDictionary *)options
|
|
151
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
152
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
153
|
+
MeshStreamBridgeClient *bridgeClient = [self bridgeClient:clientId];
|
|
154
|
+
NSURL *url = MeshStreamHTTPURLFromValue(options[@"url"]);
|
|
155
|
+
if (bridgeClient == nil || url == nil) {
|
|
156
|
+
reject(@"MESHSTREAM_CREATE_SESSION_FAILED", bridgeClient == nil ? @"Unknown or closed MeshStream client." : @"options.url must be a valid URL.", nil);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
P2PPlaybackOptions *nativeOptions = [P2PPlaybackOptions new];
|
|
160
|
+
nativeOptions.contentId = MeshStreamTrimmedString(options[@"contentId"]);
|
|
161
|
+
nativeOptions.mimeType = MeshStreamTrimmedString(options[@"mimeType"]);
|
|
162
|
+
nativeOptions.originHeaders = MeshStreamStringHeaders(options[@"originHeaders"]);
|
|
163
|
+
NSError *error = nil;
|
|
164
|
+
PlaybackSession *session = [bridgeClient.client createPlaybackSessionWithURL:url options:nativeOptions error:&error];
|
|
165
|
+
if (session == nil) {
|
|
166
|
+
reject(@"MESHSTREAM_CREATE_SESSION_FAILED", error.localizedDescription ?: @"Unable to create MeshStream playback session.", error);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
NSString *sessionId = NSUUID.UUID.UUIDString;
|
|
170
|
+
@synchronized (bridgeClient.sessions) { bridgeClient.sessions[sessionId] = session; }
|
|
171
|
+
[self flushPendingEventsForNativeSessionId:session.sessionId clientId:clientId sessionId:sessionId];
|
|
172
|
+
NSMutableDictionary *result = [@{
|
|
173
|
+
@"clientId": clientId,
|
|
174
|
+
@"sessionId": sessionId,
|
|
175
|
+
@"playbackUrl": session.playbackUrl.absoluteString,
|
|
176
|
+
@"playbackMode": session.playbackMode == P2PPlaybackModeNativeP2P ? @"NATIVE_P2P" : @"NATIVE_CDN",
|
|
177
|
+
} mutableCopy];
|
|
178
|
+
if (session.playbackMode == P2PPlaybackModeNativeCDN) result[@"fallbackReason"] = MeshStreamReasonName(session.fallbackReason);
|
|
179
|
+
resolve(result);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
RCT_EXPORT_METHOD(preload:(NSString *)clientId sessionId:(NSString *)sessionId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
|
|
183
|
+
PlaybackSession *session = [self sessionForClient:clientId sessionId:sessionId reject:reject];
|
|
184
|
+
if (session != nil) resolve(@([session preload]));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
RCT_EXPORT_METHOD(setP2pEnabled:(NSString *)clientId sessionId:(NSString *)sessionId enabled:(BOOL)enabled resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
|
|
188
|
+
PlaybackSession *session = [self sessionForClient:clientId sessionId:sessionId reject:reject];
|
|
189
|
+
if (session != nil) { [session setP2PEnabled:enabled]; resolve(nil); }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
RCT_EXPORT_METHOD(updateBufferedDurationMs:(NSString *)clientId sessionId:(NSString *)sessionId durationMs:(double)durationMs resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
|
|
193
|
+
PlaybackSession *session = [self sessionForClient:clientId sessionId:sessionId reject:reject];
|
|
194
|
+
if (session != nil) { [session updateBufferedDurationMs:(int64_t)MAX(0, durationMs)]; resolve(nil); }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
RCT_EXPORT_METHOD(updateCurrentPositionMs:(NSString *)clientId sessionId:(NSString *)sessionId positionMs:(double)positionMs resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
|
|
198
|
+
PlaybackSession *session = [self sessionForClient:clientId sessionId:sessionId reject:reject];
|
|
199
|
+
if (session != nil) { [session updateCurrentPositionMs:(int64_t)MAX(0, positionMs)]; resolve(nil); }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
RCT_EXPORT_METHOD(getStats:(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(MeshStreamStatsMap(session.statistics));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
RCT_EXPORT_METHOD(getDiagnostics:(NSString *)clientId sessionId:(NSString *)sessionId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
|
|
208
|
+
PlaybackSession *session = [self sessionForClient:clientId sessionId:sessionId reject:reject];
|
|
209
|
+
if (session != nil) resolve(MeshStreamDiagnosticsMap(session.diagnostics));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
RCT_EXPORT_METHOD(getStatus:(NSString *)clientId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
|
|
213
|
+
MeshStreamBridgeClient *bridgeClient = [self bridgeClient:clientId];
|
|
214
|
+
if (bridgeClient == nil) { reject(@"MESHSTREAM_STATUS_FAILED", @"Unknown or closed MeshStream client.", nil); return; }
|
|
215
|
+
resolve(MeshStreamStatusMap(bridgeClient.client.sdkStatus));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
RCT_EXPORT_METHOD(closeSession:(NSString *)clientId sessionId:(NSString *)sessionId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
|
|
219
|
+
MeshStreamBridgeClient *bridgeClient = [self bridgeClient:clientId];
|
|
220
|
+
if (bridgeClient == nil) { reject(@"MESHSTREAM_CLOSE_SESSION_FAILED", @"Unknown or closed MeshStream client.", nil); return; }
|
|
221
|
+
PlaybackSession *session;
|
|
222
|
+
@synchronized (bridgeClient.sessions) { session = bridgeClient.sessions[sessionId]; [bridgeClient.sessions removeObjectForKey:sessionId]; }
|
|
223
|
+
if (session != nil) [self removePendingEventsForNativeSessionId:session.sessionId];
|
|
224
|
+
[session close];
|
|
225
|
+
resolve(nil);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
RCT_EXPORT_METHOD(closeClient:(NSString *)clientId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
|
|
229
|
+
MeshStreamBridgeClient *bridgeClient;
|
|
230
|
+
@synchronized (self.clients) { bridgeClient = self.clients[clientId]; [self.clients removeObjectForKey:clientId]; }
|
|
231
|
+
if (bridgeClient != nil) {
|
|
232
|
+
[bridgeClient.client removeEventListener:self];
|
|
233
|
+
NSArray<PlaybackSession *> *sessions;
|
|
234
|
+
@synchronized (bridgeClient.sessions) { sessions = bridgeClient.sessions.allValues; [bridgeClient.sessions removeAllObjects]; }
|
|
235
|
+
for (PlaybackSession *session in sessions) {
|
|
236
|
+
[self removePendingEventsForNativeSessionId:session.sessionId];
|
|
237
|
+
[session close];
|
|
238
|
+
}
|
|
239
|
+
[bridgeClient.client close];
|
|
240
|
+
bridgeClient.client = nil;
|
|
241
|
+
}
|
|
242
|
+
resolve(nil);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
- (MeshStreamBridgeClient *)bridgeClient:(NSString *)clientId {
|
|
246
|
+
@synchronized (self.clients) { return self.clients[clientId]; }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
- (PlaybackSession *)sessionForClient:(NSString *)clientId sessionId:(NSString *)sessionId reject:(RCTPromiseRejectBlock)reject {
|
|
250
|
+
MeshStreamBridgeClient *bridgeClient = [self bridgeClient:clientId];
|
|
251
|
+
if (bridgeClient == nil) {
|
|
252
|
+
reject(@"MESHSTREAM_SESSION_OPERATION_FAILED", @"Unknown or closed MeshStream client.", nil);
|
|
253
|
+
return nil;
|
|
254
|
+
}
|
|
255
|
+
PlaybackSession *session;
|
|
256
|
+
@synchronized (bridgeClient.sessions) { session = bridgeClient.sessions[sessionId]; }
|
|
257
|
+
if (session == nil) reject(@"MESHSTREAM_SESSION_OPERATION_FAILED", @"Unknown or closed MeshStream playback session.", nil);
|
|
258
|
+
return session;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
- (void)emitForNativeSessionId:(NSString *)nativeSessionId type:(NSString *)type extras:(NSDictionary *)extras {
|
|
262
|
+
__block NSString *clientId;
|
|
263
|
+
__block NSString *sessionId;
|
|
264
|
+
@synchronized (self.clients) {
|
|
265
|
+
[self.clients enumerateKeysAndObjectsUsingBlock:^(NSString *key, MeshStreamBridgeClient *bridgeClient, BOOL *stop) {
|
|
266
|
+
@synchronized (bridgeClient.sessions) {
|
|
267
|
+
[bridgeClient.sessions enumerateKeysAndObjectsUsingBlock:^(NSString *jsId, PlaybackSession *session, BOOL *sessionStop) {
|
|
268
|
+
if ([session.sessionId isEqualToString:nativeSessionId]) { clientId = key; sessionId = jsId; *stop = YES; *sessionStop = YES; }
|
|
269
|
+
}];
|
|
270
|
+
}
|
|
271
|
+
}];
|
|
272
|
+
}
|
|
273
|
+
if (clientId == nil || sessionId == nil) {
|
|
274
|
+
[self bufferPendingEventForNativeSessionId:nativeSessionId type:type extras:extras];
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
[self sendSessionEventForClientId:clientId sessionId:sessionId type:type extras:extras];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
- (void)sendSessionEventForClientId:(NSString *)clientId sessionId:(NSString *)sessionId type:(NSString *)type extras:(NSDictionary *)extras {
|
|
281
|
+
NSMutableDictionary *event = [@{ @"type": type, @"clientId": clientId, @"sessionId": sessionId } mutableCopy];
|
|
282
|
+
[event addEntriesFromDictionary:extras ?: @{}];
|
|
283
|
+
[self sendEventWithName:MeshStreamEventName body:event];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
- (void)bufferPendingEventForNativeSessionId:(NSString *)nativeSessionId type:(NSString *)type extras:(NSDictionary *)extras {
|
|
287
|
+
if (nativeSessionId.length == 0) return;
|
|
288
|
+
@synchronized (self.pendingEvents) {
|
|
289
|
+
NSMutableArray<NSDictionary *> *events = self.pendingEvents[nativeSessionId];
|
|
290
|
+
if (events == nil) {
|
|
291
|
+
if (self.pendingEvents.count >= 64) return;
|
|
292
|
+
events = [NSMutableArray array];
|
|
293
|
+
self.pendingEvents[nativeSessionId] = events;
|
|
294
|
+
}
|
|
295
|
+
if (events.count < 8) [events addObject:@{ @"type": type, @"extras": extras ?: @{} }];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
- (void)flushPendingEventsForNativeSessionId:(NSString *)nativeSessionId clientId:(NSString *)clientId sessionId:(NSString *)sessionId {
|
|
300
|
+
NSArray<NSDictionary *> *events;
|
|
301
|
+
@synchronized (self.pendingEvents) {
|
|
302
|
+
events = [self.pendingEvents[nativeSessionId] copy] ?: @[];
|
|
303
|
+
[self.pendingEvents removeObjectForKey:nativeSessionId];
|
|
304
|
+
}
|
|
305
|
+
for (NSDictionary *event in events) {
|
|
306
|
+
[self sendSessionEventForClientId:clientId sessionId:sessionId type:event[@"type"] extras:event[@"extras"]];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
- (void)removePendingEventsForNativeSessionId:(NSString *)nativeSessionId {
|
|
311
|
+
@synchronized (self.pendingEvents) { [self.pendingEvents removeObjectForKey:nativeSessionId]; }
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
- (void)onPlaybackPreparedWithSessionId:(NSString *)sessionId playbackUrl:(NSURL *)playbackUrl { [self emitForNativeSessionId:sessionId type:@"playbackPrepared" extras:@{ @"playbackUrl": playbackUrl.absoluteString ?: @"" }]; }
|
|
315
|
+
- (void)onStatisticsWithSessionId:(NSString *)sessionId stats:(P2PStats *)stats { [self emitForNativeSessionId:sessionId type:@"statistics" extras:@{ @"statistics": MeshStreamStatsMap(stats) }]; }
|
|
316
|
+
- (void)onPeerCountChangedWithSessionId:(NSString *)sessionId peerCount:(NSInteger)peerCount { [self emitForNativeSessionId:sessionId type:@"peerCountChanged" extras:@{ @"peerCount": @(peerCount) }]; }
|
|
317
|
+
- (void)onP2PReadyWithSessionId:(NSString *)sessionId { [self emitForNativeSessionId:sessionId type:@"p2pReady" extras:nil]; }
|
|
318
|
+
- (void)onP2PUnavailableWithSessionId:(NSString *)sessionId reason:(P2PUnavailableReason)reason { [self emitForNativeSessionId:sessionId type:@"p2pUnavailable" extras:@{ @"reason": MeshStreamReasonName(reason) }]; }
|
|
319
|
+
- (void)onCdnFallbackWithSessionId:(NSString *)sessionId bytes:(int64_t)bytes { [self emitForNativeSessionId:sessionId type:@"cdnFallback" extras:@{ @"bytes": @((double)bytes) }]; }
|
|
320
|
+
- (void)onTrackerErrorWithSessionId:(NSString *)sessionId totalErrors:(int64_t)totalErrors { [self emitForNativeSessionId:sessionId type:@"trackerError" extras:@{ @"totalErrors": @((double)totalErrors) }]; }
|
|
321
|
+
- (void)onPeerConnectErrorWithSessionId:(NSString *)sessionId totalErrors:(int64_t)totalErrors { [self emitForNativeSessionId:sessionId type:@"peerConnectError" extras:@{ @"totalErrors": @((double)totalErrors) }]; }
|
|
322
|
+
|
|
323
|
+
- (void)invalidate {
|
|
324
|
+
NSArray<NSString *> *clientIds;
|
|
325
|
+
@synchronized (self.clients) { clientIds = self.clients.allKeys; }
|
|
326
|
+
for (NSString *clientId in clientIds) {
|
|
327
|
+
MeshStreamBridgeClient *bridgeClient = [self bridgeClient:clientId];
|
|
328
|
+
[bridgeClient.client removeEventListener:self];
|
|
329
|
+
for (PlaybackSession *session in bridgeClient.sessions.allValues) [session close];
|
|
330
|
+
[bridgeClient.sessions removeAllObjects];
|
|
331
|
+
[bridgeClient.client close];
|
|
332
|
+
bridgeClient.client = nil;
|
|
333
|
+
}
|
|
334
|
+
@synchronized (self.clients) { [self.clients removeAllObjects]; }
|
|
335
|
+
@synchronized (self.pendingEvents) { [self.pendingEvents removeAllObjects]; }
|
|
336
|
+
[super invalidate];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
@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,19 @@
|
|
|
1
|
+
import type { MeshStreamConfig, MeshStreamDiagnostics, MeshStreamSessionInfo, MeshStreamStats, MeshStreamStatus, PlaybackSessionOptions } from './types';
|
|
2
|
+
interface NativeMeshStreamP2P {
|
|
3
|
+
createClient(config: MeshStreamConfig): Promise<{
|
|
4
|
+
clientId: string;
|
|
5
|
+
status: MeshStreamStatus;
|
|
6
|
+
}>;
|
|
7
|
+
createPlaybackSession(clientId: string, options: PlaybackSessionOptions): Promise<MeshStreamSessionInfo>;
|
|
8
|
+
preload(clientId: string, sessionId: string): Promise<boolean>;
|
|
9
|
+
setP2pEnabled(clientId: string, sessionId: string, enabled: boolean): Promise<void>;
|
|
10
|
+
updateBufferedDurationMs(clientId: string, sessionId: string, durationMs: number): Promise<void>;
|
|
11
|
+
updateCurrentPositionMs(clientId: string, sessionId: string, positionMs: number): Promise<void>;
|
|
12
|
+
getStats(clientId: string, sessionId: string): Promise<MeshStreamStats>;
|
|
13
|
+
getDiagnostics(clientId: string, sessionId: string): Promise<MeshStreamDiagnostics>;
|
|
14
|
+
getStatus(clientId: string): Promise<MeshStreamStatus>;
|
|
15
|
+
closeSession(clientId: string, sessionId: string): Promise<void>;
|
|
16
|
+
closeClient(clientId: string): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export declare function requireNativeModule(): NativeMeshStreamP2P;
|
|
19
|
+
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,41 @@
|
|
|
1
|
+
import { type EmitterSubscription } from 'react-native';
|
|
2
|
+
import type { MeshStreamConfig, MeshStreamDiagnostics, MeshStreamEvent, MeshStreamEventName, MeshStreamSessionInfo, MeshStreamStats, MeshStreamStatus, 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 closed;
|
|
11
|
+
private closing?;
|
|
12
|
+
constructor(info: MeshStreamSessionInfo);
|
|
13
|
+
preload(): Promise<boolean>;
|
|
14
|
+
setP2pEnabled(enabled: boolean): Promise<void>;
|
|
15
|
+
updateBufferedDurationMs(durationMs: number): Promise<void>;
|
|
16
|
+
updateCurrentPositionMs(positionMs: number): Promise<void>;
|
|
17
|
+
getStats(): Promise<MeshStreamStats>;
|
|
18
|
+
getDiagnostics(): Promise<MeshStreamDiagnostics>;
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
private withOpen;
|
|
21
|
+
}
|
|
22
|
+
export declare class MeshStreamClient {
|
|
23
|
+
readonly clientId: string;
|
|
24
|
+
private closed;
|
|
25
|
+
private closing?;
|
|
26
|
+
constructor(clientId: string);
|
|
27
|
+
createPlaybackSession(options: PlaybackSessionOptions): Promise<MeshStreamPlaybackSession>;
|
|
28
|
+
getStatus(): Promise<MeshStreamStatus>;
|
|
29
|
+
close(): Promise<void>;
|
|
30
|
+
private withOpen;
|
|
31
|
+
}
|
|
32
|
+
export declare const MeshStream: {
|
|
33
|
+
create(config: MeshStreamConfig): Promise<MeshStreamClient>;
|
|
34
|
+
addEventListener(listener: (event: MeshStreamEvent) => void): EmitterSubscription;
|
|
35
|
+
};
|
|
36
|
+
/** Filtered event subscription for a particular client or playback session. */
|
|
37
|
+
export declare function addMeshStreamEventListener(listener: (event: MeshStreamEvent) => void, filter?: {
|
|
38
|
+
clientId?: string;
|
|
39
|
+
sessionId?: string;
|
|
40
|
+
type?: MeshStreamEventName;
|
|
41
|
+
}): EmitterSubscription;
|
|
@@ -0,0 +1,129 @@
|
|
|
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
|
+
__exportStar(require("./types"), exports);
|
|
22
|
+
const EVENT_NAME = 'MeshStreamP2P:event';
|
|
23
|
+
function assertNonEmpty(value, field) {
|
|
24
|
+
if (!value?.trim())
|
|
25
|
+
throw new Error(`${field} must not be empty.`);
|
|
26
|
+
}
|
|
27
|
+
function assertNonNegative(value, field) {
|
|
28
|
+
if (!Number.isFinite(value) || value < 0)
|
|
29
|
+
throw new Error(`${field} must be a finite non-negative number.`);
|
|
30
|
+
}
|
|
31
|
+
class MeshStreamPlaybackSession {
|
|
32
|
+
constructor(info) {
|
|
33
|
+
this.closed = false;
|
|
34
|
+
this.clientId = info.clientId;
|
|
35
|
+
this.sessionId = info.sessionId;
|
|
36
|
+
this.playbackUrl = info.playbackUrl;
|
|
37
|
+
this.playbackMode = info.playbackMode;
|
|
38
|
+
this.fallbackReason = info.fallbackReason;
|
|
39
|
+
}
|
|
40
|
+
async preload() {
|
|
41
|
+
return this.withOpen(() => (0, NativeMeshStreamP2P_1.requireNativeModule)().preload(this.clientId, this.sessionId));
|
|
42
|
+
}
|
|
43
|
+
async setP2pEnabled(enabled) {
|
|
44
|
+
return this.withOpen(() => (0, NativeMeshStreamP2P_1.requireNativeModule)().setP2pEnabled(this.clientId, this.sessionId, enabled));
|
|
45
|
+
}
|
|
46
|
+
async updateBufferedDurationMs(durationMs) {
|
|
47
|
+
assertNonNegative(durationMs, 'durationMs');
|
|
48
|
+
return this.withOpen(() => (0, NativeMeshStreamP2P_1.requireNativeModule)().updateBufferedDurationMs(this.clientId, this.sessionId, durationMs));
|
|
49
|
+
}
|
|
50
|
+
async updateCurrentPositionMs(positionMs) {
|
|
51
|
+
assertNonNegative(positionMs, 'positionMs');
|
|
52
|
+
return this.withOpen(() => (0, NativeMeshStreamP2P_1.requireNativeModule)().updateCurrentPositionMs(this.clientId, this.sessionId, positionMs));
|
|
53
|
+
}
|
|
54
|
+
async getStats() {
|
|
55
|
+
return this.withOpen(() => (0, NativeMeshStreamP2P_1.requireNativeModule)().getStats(this.clientId, this.sessionId));
|
|
56
|
+
}
|
|
57
|
+
async getDiagnostics() {
|
|
58
|
+
return this.withOpen(() => (0, NativeMeshStreamP2P_1.requireNativeModule)().getDiagnostics(this.clientId, this.sessionId));
|
|
59
|
+
}
|
|
60
|
+
async close() {
|
|
61
|
+
if (this.closed)
|
|
62
|
+
return;
|
|
63
|
+
if (!this.closing) {
|
|
64
|
+
this.closing = (0, NativeMeshStreamP2P_1.requireNativeModule)()
|
|
65
|
+
.closeSession(this.clientId, this.sessionId)
|
|
66
|
+
.then(() => { this.closed = true; })
|
|
67
|
+
.finally(() => { this.closing = undefined; });
|
|
68
|
+
}
|
|
69
|
+
return this.closing;
|
|
70
|
+
}
|
|
71
|
+
async withOpen(operation) {
|
|
72
|
+
if (this.closed || this.closing)
|
|
73
|
+
throw new Error('Playback session is closed.');
|
|
74
|
+
return operation();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
exports.MeshStreamPlaybackSession = MeshStreamPlaybackSession;
|
|
78
|
+
class MeshStreamClient {
|
|
79
|
+
constructor(clientId) {
|
|
80
|
+
this.closed = false;
|
|
81
|
+
this.clientId = clientId;
|
|
82
|
+
}
|
|
83
|
+
async createPlaybackSession(options) {
|
|
84
|
+
assertNonEmpty(options.url, 'options.url');
|
|
85
|
+
return this.withOpen(async () => new MeshStreamPlaybackSession(await (0, NativeMeshStreamP2P_1.requireNativeModule)().createPlaybackSession(this.clientId, options)));
|
|
86
|
+
}
|
|
87
|
+
async getStatus() {
|
|
88
|
+
return this.withOpen(() => (0, NativeMeshStreamP2P_1.requireNativeModule)().getStatus(this.clientId));
|
|
89
|
+
}
|
|
90
|
+
async close() {
|
|
91
|
+
if (this.closed)
|
|
92
|
+
return;
|
|
93
|
+
if (!this.closing) {
|
|
94
|
+
this.closing = (0, NativeMeshStreamP2P_1.requireNativeModule)()
|
|
95
|
+
.closeClient(this.clientId)
|
|
96
|
+
.then(() => { this.closed = true; })
|
|
97
|
+
.finally(() => { this.closing = undefined; });
|
|
98
|
+
}
|
|
99
|
+
return this.closing;
|
|
100
|
+
}
|
|
101
|
+
async withOpen(operation) {
|
|
102
|
+
if (this.closed || this.closing)
|
|
103
|
+
throw new Error('MeshStream client is closed.');
|
|
104
|
+
return operation();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
exports.MeshStreamClient = MeshStreamClient;
|
|
108
|
+
exports.MeshStream = {
|
|
109
|
+
async create(config) {
|
|
110
|
+
assertNonEmpty(config.key, 'config.key');
|
|
111
|
+
const result = await (0, NativeMeshStreamP2P_1.requireNativeModule)().createClient(config);
|
|
112
|
+
return new MeshStreamClient(result.clientId);
|
|
113
|
+
},
|
|
114
|
+
addEventListener(listener) {
|
|
115
|
+
return react_native_1.DeviceEventEmitter.addListener(EVENT_NAME, listener);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
/** Filtered event subscription for a particular client or playback session. */
|
|
119
|
+
function addMeshStreamEventListener(listener, filter) {
|
|
120
|
+
return exports.MeshStream.addEventListener((event) => {
|
|
121
|
+
if (filter?.clientId && event.clientId !== filter.clientId)
|
|
122
|
+
return;
|
|
123
|
+
if (filter?.sessionId && event.sessionId !== filter.sessionId)
|
|
124
|
+
return;
|
|
125
|
+
if (filter?.type && event.type !== filter.type)
|
|
126
|
+
return;
|
|
127
|
+
listener(event);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export type MeshStreamMimeType = 'm3u8' | 'mpd' | 'mp4' | 'mp3';
|
|
2
|
+
/** A client can own multiple independent playback sessions. */
|
|
3
|
+
export interface MeshStreamConfig {
|
|
4
|
+
key: string;
|
|
5
|
+
p2pEnabled?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface PlaybackSessionOptions {
|
|
8
|
+
url: string;
|
|
9
|
+
contentId?: string;
|
|
10
|
+
mimeType?: MeshStreamMimeType;
|
|
11
|
+
originHeaders?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
export type MeshStreamPlaybackMode = 'NATIVE_P2P' | 'NATIVE_CDN';
|
|
14
|
+
export type MeshStreamUnavailableReason = 'OK' | 'ACCOUNT_DISABLED' | 'APP_DISABLED' | 'APP_NOT_FOUND' | 'KEY_DISABLED' | 'PLATFORM_DISABLED' | 'ORIGIN_DENIED' | 'PACKAGE_DENIED' | 'BUNDLE_DENIED' | 'SIGNING_CERT_DENIED' | 'TEAM_DENIED' | 'AUTH_FAILED' | 'CONFIG_UNAVAILABLE' | 'TRACKER_ERROR' | 'NO_PEERS' | 'UNSUPPORTED' | 'INITIALIZATION_FAILED' | 'DISABLED' | 'KEY_P2P_DISABLED' | 'QUOTA_EXHAUSTED' | 'QUOTA_DISABLED';
|
|
15
|
+
export interface MeshStreamStatus {
|
|
16
|
+
accountEnabled: boolean;
|
|
17
|
+
applicationEnabled: boolean;
|
|
18
|
+
keyEnabled: boolean;
|
|
19
|
+
platformEnabled: boolean;
|
|
20
|
+
effectiveP2pEnabled: boolean;
|
|
21
|
+
reason: MeshStreamUnavailableReason;
|
|
22
|
+
configVersion?: string;
|
|
23
|
+
edgeRegion?: string;
|
|
24
|
+
quotaStatus?: string;
|
|
25
|
+
trackerPoolReady?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface MeshStreamStats {
|
|
28
|
+
httpDownloaded: number;
|
|
29
|
+
p2pDownloaded: number;
|
|
30
|
+
p2pUploaded: number;
|
|
31
|
+
httpSegmentDownloadSuccesses: number;
|
|
32
|
+
httpSegmentDownloadFailures: number;
|
|
33
|
+
p2pSegmentDownloadSuccesses: number;
|
|
34
|
+
p2pSegmentDownloadFailures: number;
|
|
35
|
+
p2pSegmentUploadSuccesses: number;
|
|
36
|
+
p2pSegmentUploadFailures: number;
|
|
37
|
+
connectedPeers: number;
|
|
38
|
+
peerConnectErrors: number;
|
|
39
|
+
trackerErrors: number;
|
|
40
|
+
downloadOffloadRatio: number;
|
|
41
|
+
p2pSegmentDownloadSuccessRatio: number;
|
|
42
|
+
p2pSegmentUploadSuccessRatio: number;
|
|
43
|
+
}
|
|
44
|
+
export interface MeshStreamColdStartSnapshot {
|
|
45
|
+
p2pDownloaded: number;
|
|
46
|
+
httpDownloaded: number;
|
|
47
|
+
}
|
|
48
|
+
export interface MeshStreamDiagnostics {
|
|
49
|
+
fallbackCount: number;
|
|
50
|
+
firstPeerConnectMs?: number;
|
|
51
|
+
coldStart?: MeshStreamColdStartSnapshot;
|
|
52
|
+
lastTrackerError?: string;
|
|
53
|
+
lastPeerError?: string;
|
|
54
|
+
lastErrorAtMs?: number;
|
|
55
|
+
iceAttempts: number;
|
|
56
|
+
iceSuccesses: number;
|
|
57
|
+
iceFailures: number;
|
|
58
|
+
iceDurationMs: number;
|
|
59
|
+
}
|
|
60
|
+
export interface MeshStreamSessionInfo {
|
|
61
|
+
clientId: string;
|
|
62
|
+
sessionId: string;
|
|
63
|
+
playbackUrl: string;
|
|
64
|
+
playbackMode: MeshStreamPlaybackMode;
|
|
65
|
+
fallbackReason?: MeshStreamUnavailableReason;
|
|
66
|
+
}
|
|
67
|
+
export type MeshStreamEventName = 'peerCountChanged' | 'playbackPrepared' | 'p2pReady' | 'p2pUnavailable' | 'statistics' | 'cdnFallback' | 'trackerError' | 'peerConnectError';
|
|
68
|
+
export interface MeshStreamEvent {
|
|
69
|
+
type: MeshStreamEventName;
|
|
70
|
+
clientId: string;
|
|
71
|
+
sessionId: string;
|
|
72
|
+
peerCount?: number;
|
|
73
|
+
playbackUrl?: string;
|
|
74
|
+
reason?: MeshStreamUnavailableReason;
|
|
75
|
+
statistics?: MeshStreamStats;
|
|
76
|
+
bytes?: number;
|
|
77
|
+
totalErrors?: number;
|
|
78
|
+
}
|