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
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-meshstream",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MeshStream P2P playback URL SDK for React Native",
|
|
5
|
+
"main": "lib/typescript/index.js",
|
|
6
|
+
"module": "lib/typescript/index.js",
|
|
7
|
+
"types": "lib/typescript/index.d.ts",
|
|
8
|
+
"react-native": "src/index.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"lib",
|
|
12
|
+
"android/build.gradle",
|
|
13
|
+
"android/src",
|
|
14
|
+
"ios",
|
|
15
|
+
"react-native.config.js",
|
|
16
|
+
"tests/ios",
|
|
17
|
+
"MeshStreamP2P.podspec",
|
|
18
|
+
"README.md",
|
|
19
|
+
"README.zh-CN.md",
|
|
20
|
+
"CHANGELOG.md"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc --project tsconfig.json",
|
|
24
|
+
"typecheck": "tsc --project tsconfig.json --noEmit",
|
|
25
|
+
"test:ios": "node --test tests/node/ios-regression-guards.test.mjs",
|
|
26
|
+
"verify:ios": "npm run test:ios && node scripts/verify-ios-package.mjs && node scripts/verify-ios-xctest.mjs",
|
|
27
|
+
"verify:ios:public": "npm run test:ios && node scripts/verify-ios-package.mjs && node scripts/verify-ios-xctest.mjs --repo-update",
|
|
28
|
+
"prepare": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"react-native",
|
|
32
|
+
"p2p",
|
|
33
|
+
"cdn",
|
|
34
|
+
"video",
|
|
35
|
+
"meshstream"
|
|
36
|
+
],
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/Mesh-Stream/meshstream-p2p-react-native-sdk.git"
|
|
40
|
+
},
|
|
41
|
+
"license": "Apache-2.0",
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"react": ">=18.0.0",
|
|
44
|
+
"react-native": ">=0.72.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/react": "^18.3.0",
|
|
48
|
+
"react-native": "^0.76.0",
|
|
49
|
+
"typescript": "^5.6.3"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { NativeModules } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
MeshStreamConfig,
|
|
5
|
+
MeshStreamDiagnostics,
|
|
6
|
+
MeshStreamSessionInfo,
|
|
7
|
+
MeshStreamStats,
|
|
8
|
+
MeshStreamStatus,
|
|
9
|
+
PlaybackSessionOptions,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
interface NativeMeshStreamP2P {
|
|
13
|
+
createClient(config: MeshStreamConfig): Promise<{ clientId: string; status: MeshStreamStatus }>;
|
|
14
|
+
createPlaybackSession(clientId: string, options: PlaybackSessionOptions): Promise<MeshStreamSessionInfo>;
|
|
15
|
+
preload(clientId: string, sessionId: string): Promise<boolean>;
|
|
16
|
+
setP2pEnabled(clientId: string, sessionId: string, enabled: boolean): Promise<void>;
|
|
17
|
+
updateBufferedDurationMs(clientId: string, sessionId: string, durationMs: number): Promise<void>;
|
|
18
|
+
updateCurrentPositionMs(clientId: string, sessionId: string, positionMs: number): Promise<void>;
|
|
19
|
+
getStats(clientId: string, sessionId: string): Promise<MeshStreamStats>;
|
|
20
|
+
getDiagnostics(clientId: string, sessionId: string): Promise<MeshStreamDiagnostics>;
|
|
21
|
+
getStatus(clientId: string): Promise<MeshStreamStatus>;
|
|
22
|
+
closeSession(clientId: string, sessionId: string): Promise<void>;
|
|
23
|
+
closeClient(clientId: string): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const nativeModule = NativeModules.MeshStreamP2P as NativeMeshStreamP2P | undefined;
|
|
27
|
+
|
|
28
|
+
export function requireNativeModule(): NativeMeshStreamP2P {
|
|
29
|
+
if (!nativeModule) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'MeshStreamP2P is not linked. Rebuild the iOS or Android app after installing react-native-meshstream.',
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return nativeModule;
|
|
35
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { DeviceEventEmitter, type EmitterSubscription } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { requireNativeModule } from './NativeMeshStreamP2P';
|
|
4
|
+
import type {
|
|
5
|
+
MeshStreamConfig,
|
|
6
|
+
MeshStreamDiagnostics,
|
|
7
|
+
MeshStreamEvent,
|
|
8
|
+
MeshStreamEventName,
|
|
9
|
+
MeshStreamSessionInfo,
|
|
10
|
+
MeshStreamStats,
|
|
11
|
+
MeshStreamStatus,
|
|
12
|
+
PlaybackSessionOptions,
|
|
13
|
+
} from './types';
|
|
14
|
+
|
|
15
|
+
export * from './types';
|
|
16
|
+
|
|
17
|
+
const EVENT_NAME = 'MeshStreamP2P:event';
|
|
18
|
+
|
|
19
|
+
function assertNonEmpty(value: string | undefined, field: string): asserts value is string {
|
|
20
|
+
if (!value?.trim()) throw new Error(`${field} must not be empty.`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function assertNonNegative(value: number, field: string): void {
|
|
24
|
+
if (!Number.isFinite(value) || value < 0) throw new Error(`${field} must be a finite non-negative number.`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class MeshStreamPlaybackSession {
|
|
28
|
+
readonly clientId: string;
|
|
29
|
+
readonly sessionId: string;
|
|
30
|
+
readonly playbackUrl: string;
|
|
31
|
+
readonly playbackMode: MeshStreamSessionInfo['playbackMode'];
|
|
32
|
+
readonly fallbackReason?: MeshStreamSessionInfo['fallbackReason'];
|
|
33
|
+
private closed = false;
|
|
34
|
+
private closing?: Promise<void>;
|
|
35
|
+
|
|
36
|
+
constructor(info: MeshStreamSessionInfo) {
|
|
37
|
+
this.clientId = info.clientId;
|
|
38
|
+
this.sessionId = info.sessionId;
|
|
39
|
+
this.playbackUrl = info.playbackUrl;
|
|
40
|
+
this.playbackMode = info.playbackMode;
|
|
41
|
+
this.fallbackReason = info.fallbackReason;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async preload(): Promise<boolean> {
|
|
45
|
+
return this.withOpen(() => requireNativeModule().preload(this.clientId, this.sessionId));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async setP2pEnabled(enabled: boolean): Promise<void> {
|
|
49
|
+
return this.withOpen(() => requireNativeModule().setP2pEnabled(this.clientId, this.sessionId, enabled));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async updateBufferedDurationMs(durationMs: number): Promise<void> {
|
|
53
|
+
assertNonNegative(durationMs, 'durationMs');
|
|
54
|
+
return this.withOpen(() => requireNativeModule().updateBufferedDurationMs(this.clientId, this.sessionId, durationMs));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async updateCurrentPositionMs(positionMs: number): Promise<void> {
|
|
58
|
+
assertNonNegative(positionMs, 'positionMs');
|
|
59
|
+
return this.withOpen(() => requireNativeModule().updateCurrentPositionMs(this.clientId, this.sessionId, positionMs));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async getStats(): Promise<MeshStreamStats> {
|
|
63
|
+
return this.withOpen(() => requireNativeModule().getStats(this.clientId, this.sessionId));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async getDiagnostics(): Promise<MeshStreamDiagnostics> {
|
|
67
|
+
return this.withOpen(() => requireNativeModule().getDiagnostics(this.clientId, this.sessionId));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async close(): Promise<void> {
|
|
71
|
+
if (this.closed) return;
|
|
72
|
+
if (!this.closing) {
|
|
73
|
+
this.closing = requireNativeModule()
|
|
74
|
+
.closeSession(this.clientId, this.sessionId)
|
|
75
|
+
.then(() => { this.closed = true; })
|
|
76
|
+
.finally(() => { this.closing = undefined; });
|
|
77
|
+
}
|
|
78
|
+
return this.closing;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private async withOpen<T>(operation: () => Promise<T>): Promise<T> {
|
|
82
|
+
if (this.closed || this.closing) throw new Error('Playback session is closed.');
|
|
83
|
+
return operation();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class MeshStreamClient {
|
|
88
|
+
readonly clientId: string;
|
|
89
|
+
private closed = false;
|
|
90
|
+
private closing?: Promise<void>;
|
|
91
|
+
|
|
92
|
+
constructor(clientId: string) {
|
|
93
|
+
this.clientId = clientId;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async createPlaybackSession(options: PlaybackSessionOptions): Promise<MeshStreamPlaybackSession> {
|
|
97
|
+
assertNonEmpty(options.url, 'options.url');
|
|
98
|
+
return this.withOpen(async () => new MeshStreamPlaybackSession(
|
|
99
|
+
await requireNativeModule().createPlaybackSession(this.clientId, options),
|
|
100
|
+
));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async getStatus(): Promise<MeshStreamStatus> {
|
|
104
|
+
return this.withOpen(() => requireNativeModule().getStatus(this.clientId));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async close(): Promise<void> {
|
|
108
|
+
if (this.closed) return;
|
|
109
|
+
if (!this.closing) {
|
|
110
|
+
this.closing = requireNativeModule()
|
|
111
|
+
.closeClient(this.clientId)
|
|
112
|
+
.then(() => { this.closed = true; })
|
|
113
|
+
.finally(() => { this.closing = undefined; });
|
|
114
|
+
}
|
|
115
|
+
return this.closing;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private async withOpen<T>(operation: () => Promise<T>): Promise<T> {
|
|
119
|
+
if (this.closed || this.closing) throw new Error('MeshStream client is closed.');
|
|
120
|
+
return operation();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const MeshStream = {
|
|
125
|
+
async create(config: MeshStreamConfig): Promise<MeshStreamClient> {
|
|
126
|
+
assertNonEmpty(config.key, 'config.key');
|
|
127
|
+
const result = await requireNativeModule().createClient(config);
|
|
128
|
+
return new MeshStreamClient(result.clientId);
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
addEventListener(listener: (event: MeshStreamEvent) => void): EmitterSubscription {
|
|
132
|
+
return DeviceEventEmitter.addListener(EVENT_NAME, listener);
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/** Filtered event subscription for a particular client or playback session. */
|
|
137
|
+
export function addMeshStreamEventListener(
|
|
138
|
+
listener: (event: MeshStreamEvent) => void,
|
|
139
|
+
filter?: { clientId?: string; sessionId?: string; type?: MeshStreamEventName },
|
|
140
|
+
): EmitterSubscription {
|
|
141
|
+
return MeshStream.addEventListener((event) => {
|
|
142
|
+
if (filter?.clientId && event.clientId !== filter.clientId) return;
|
|
143
|
+
if (filter?.sessionId && event.sessionId !== filter.sessionId) return;
|
|
144
|
+
if (filter?.type && event.type !== filter.type) return;
|
|
145
|
+
listener(event);
|
|
146
|
+
});
|
|
147
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export type MeshStreamMimeType = 'm3u8' | 'mpd' | 'mp4' | 'mp3';
|
|
2
|
+
|
|
3
|
+
/** A client can own multiple independent playback sessions. */
|
|
4
|
+
export interface MeshStreamConfig {
|
|
5
|
+
key: string;
|
|
6
|
+
p2pEnabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface PlaybackSessionOptions {
|
|
10
|
+
url: string;
|
|
11
|
+
contentId?: string;
|
|
12
|
+
mimeType?: MeshStreamMimeType;
|
|
13
|
+
originHeaders?: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type MeshStreamPlaybackMode = 'NATIVE_P2P' | 'NATIVE_CDN';
|
|
17
|
+
|
|
18
|
+
export type MeshStreamUnavailableReason =
|
|
19
|
+
| 'OK'
|
|
20
|
+
| 'ACCOUNT_DISABLED'
|
|
21
|
+
| 'APP_DISABLED'
|
|
22
|
+
| 'APP_NOT_FOUND'
|
|
23
|
+
| 'KEY_DISABLED'
|
|
24
|
+
| 'PLATFORM_DISABLED'
|
|
25
|
+
| 'ORIGIN_DENIED'
|
|
26
|
+
| 'PACKAGE_DENIED'
|
|
27
|
+
| 'BUNDLE_DENIED'
|
|
28
|
+
| 'SIGNING_CERT_DENIED'
|
|
29
|
+
| 'TEAM_DENIED'
|
|
30
|
+
| 'AUTH_FAILED'
|
|
31
|
+
| 'CONFIG_UNAVAILABLE'
|
|
32
|
+
| 'TRACKER_ERROR'
|
|
33
|
+
| 'NO_PEERS'
|
|
34
|
+
| 'UNSUPPORTED'
|
|
35
|
+
| 'INITIALIZATION_FAILED'
|
|
36
|
+
| 'DISABLED'
|
|
37
|
+
| 'KEY_P2P_DISABLED'
|
|
38
|
+
| 'QUOTA_EXHAUSTED'
|
|
39
|
+
| 'QUOTA_DISABLED';
|
|
40
|
+
|
|
41
|
+
export interface MeshStreamStatus {
|
|
42
|
+
accountEnabled: boolean;
|
|
43
|
+
applicationEnabled: boolean;
|
|
44
|
+
keyEnabled: boolean;
|
|
45
|
+
platformEnabled: boolean;
|
|
46
|
+
effectiveP2pEnabled: boolean;
|
|
47
|
+
reason: MeshStreamUnavailableReason;
|
|
48
|
+
configVersion?: string;
|
|
49
|
+
edgeRegion?: string;
|
|
50
|
+
quotaStatus?: string;
|
|
51
|
+
trackerPoolReady?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface MeshStreamStats {
|
|
55
|
+
httpDownloaded: number;
|
|
56
|
+
p2pDownloaded: number;
|
|
57
|
+
p2pUploaded: number;
|
|
58
|
+
httpSegmentDownloadSuccesses: number;
|
|
59
|
+
httpSegmentDownloadFailures: number;
|
|
60
|
+
p2pSegmentDownloadSuccesses: number;
|
|
61
|
+
p2pSegmentDownloadFailures: number;
|
|
62
|
+
p2pSegmentUploadSuccesses: number;
|
|
63
|
+
p2pSegmentUploadFailures: number;
|
|
64
|
+
connectedPeers: number;
|
|
65
|
+
peerConnectErrors: number;
|
|
66
|
+
trackerErrors: number;
|
|
67
|
+
downloadOffloadRatio: number;
|
|
68
|
+
p2pSegmentDownloadSuccessRatio: number;
|
|
69
|
+
p2pSegmentUploadSuccessRatio: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface MeshStreamColdStartSnapshot {
|
|
73
|
+
p2pDownloaded: number;
|
|
74
|
+
httpDownloaded: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface MeshStreamDiagnostics {
|
|
78
|
+
fallbackCount: number;
|
|
79
|
+
firstPeerConnectMs?: number;
|
|
80
|
+
coldStart?: MeshStreamColdStartSnapshot;
|
|
81
|
+
lastTrackerError?: string;
|
|
82
|
+
lastPeerError?: string;
|
|
83
|
+
lastErrorAtMs?: number;
|
|
84
|
+
iceAttempts: number;
|
|
85
|
+
iceSuccesses: number;
|
|
86
|
+
iceFailures: number;
|
|
87
|
+
iceDurationMs: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface MeshStreamSessionInfo {
|
|
91
|
+
clientId: string;
|
|
92
|
+
sessionId: string;
|
|
93
|
+
playbackUrl: string;
|
|
94
|
+
playbackMode: MeshStreamPlaybackMode;
|
|
95
|
+
fallbackReason?: MeshStreamUnavailableReason;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type MeshStreamEventName =
|
|
99
|
+
| 'peerCountChanged'
|
|
100
|
+
| 'playbackPrepared'
|
|
101
|
+
| 'p2pReady'
|
|
102
|
+
| 'p2pUnavailable'
|
|
103
|
+
| 'statistics'
|
|
104
|
+
| 'cdnFallback'
|
|
105
|
+
| 'trackerError'
|
|
106
|
+
| 'peerConnectError';
|
|
107
|
+
|
|
108
|
+
export interface MeshStreamEvent {
|
|
109
|
+
type: MeshStreamEventName;
|
|
110
|
+
clientId: string;
|
|
111
|
+
sessionId: string;
|
|
112
|
+
peerCount?: number;
|
|
113
|
+
playbackUrl?: string;
|
|
114
|
+
reason?: MeshStreamUnavailableReason;
|
|
115
|
+
statistics?: MeshStreamStats;
|
|
116
|
+
bytes?: number;
|
|
117
|
+
totalErrors?: number;
|
|
118
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#import <XCTest/XCTest.h>
|
|
2
|
+
|
|
3
|
+
#import "../../ios/MeshStreamP2PValidation.h"
|
|
4
|
+
|
|
5
|
+
@interface MeshStreamP2PValidationTests : XCTestCase
|
|
6
|
+
@end
|
|
7
|
+
|
|
8
|
+
@implementation MeshStreamP2PValidationTests
|
|
9
|
+
|
|
10
|
+
- (void)testTrimsURLsAndFiltersNonStringHeaders {
|
|
11
|
+
XCTAssertEqualObjects(
|
|
12
|
+
MeshStreamTrimmedString(@" content-42\n"),
|
|
13
|
+
@"content-42");
|
|
14
|
+
XCTAssertNil(MeshStreamTrimmedString(@42));
|
|
15
|
+
XCTAssertEqualObjects(
|
|
16
|
+
MeshStreamHTTPURLFromValue(@" https://example.com/live.m3u8 ").absoluteString,
|
|
17
|
+
@"https://example.com/live.m3u8");
|
|
18
|
+
XCTAssertNil(MeshStreamHTTPURLFromValue(@"ftp://example.com/live.m3u8"));
|
|
19
|
+
NSDictionary *headers = MeshStreamStringHeaders(@{
|
|
20
|
+
@"Authorization": @"Bearer token",
|
|
21
|
+
@"retry": @1,
|
|
22
|
+
@2: @"ignored",
|
|
23
|
+
});
|
|
24
|
+
NSDictionary *expectedHeaders = @{
|
|
25
|
+
@"Authorization": @"Bearer token",
|
|
26
|
+
};
|
|
27
|
+
XCTAssertEqualObjects(headers, expectedHeaders);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@end
|