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,87 @@
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
+ /** Versions of the JavaScript wrapper and native engine used by this application. */
8
+ export interface MeshStreamVersionInfo {
9
+ /** Version of the installed react-native-meshstream package. */
10
+ packageVersion: string;
11
+ /** Version of the Android or Apple MeshStream engine loaded by the bridge. */
12
+ nativeSdkVersion: string;
13
+ /** Native platform that supplied nativeSdkVersion. */
14
+ platform: 'android' | 'ios';
15
+ }
16
+ export interface PlaybackSessionOptions {
17
+ url: string;
18
+ contentId?: string;
19
+ mimeType?: MeshStreamMimeType;
20
+ originHeaders?: Record<string, string>;
21
+ }
22
+ export type MeshStreamPlaybackMode = 'NATIVE_P2P' | 'NATIVE_CDN';
23
+ 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';
24
+ export interface MeshStreamStatus {
25
+ accountEnabled: boolean;
26
+ applicationEnabled: boolean;
27
+ keyEnabled: boolean;
28
+ platformEnabled: boolean;
29
+ effectiveP2pEnabled: boolean;
30
+ reason: MeshStreamUnavailableReason;
31
+ configVersion?: string;
32
+ edgeRegion?: string;
33
+ quotaStatus?: string;
34
+ trackerPoolReady?: boolean;
35
+ }
36
+ export interface MeshStreamStats {
37
+ httpDownloaded: number;
38
+ p2pDownloaded: number;
39
+ p2pUploaded: number;
40
+ httpSegmentDownloadSuccesses: number;
41
+ httpSegmentDownloadFailures: number;
42
+ p2pSegmentDownloadSuccesses: number;
43
+ p2pSegmentDownloadFailures: number;
44
+ p2pSegmentUploadSuccesses: number;
45
+ p2pSegmentUploadFailures: number;
46
+ connectedPeers: number;
47
+ peerConnectErrors: number;
48
+ trackerErrors: number;
49
+ downloadOffloadRatio: number;
50
+ p2pSegmentDownloadSuccessRatio: number;
51
+ p2pSegmentUploadSuccessRatio: number;
52
+ }
53
+ export interface MeshStreamColdStartSnapshot {
54
+ p2pDownloaded: number;
55
+ httpDownloaded: number;
56
+ }
57
+ export interface MeshStreamDiagnostics {
58
+ fallbackCount: number;
59
+ firstPeerConnectMs?: number;
60
+ coldStart?: MeshStreamColdStartSnapshot;
61
+ lastTrackerError?: string;
62
+ lastPeerError?: string;
63
+ lastErrorAtMs?: number;
64
+ iceAttempts: number;
65
+ iceSuccesses: number;
66
+ iceFailures: number;
67
+ iceDurationMs: number;
68
+ }
69
+ export interface MeshStreamSessionInfo {
70
+ clientId: string;
71
+ sessionId: string;
72
+ playbackUrl: string;
73
+ playbackMode: MeshStreamPlaybackMode;
74
+ fallbackReason?: MeshStreamUnavailableReason;
75
+ }
76
+ export type MeshStreamEventName = 'peerCountChanged' | 'playbackPrepared' | 'p2pReady' | 'p2pUnavailable' | 'statistics' | 'cdnFallback' | 'trackerError' | 'peerConnectError';
77
+ export interface MeshStreamEvent {
78
+ type: MeshStreamEventName;
79
+ clientId: string;
80
+ sessionId: string;
81
+ peerCount?: number;
82
+ playbackUrl?: string;
83
+ reason?: MeshStreamUnavailableReason;
84
+ statistics?: MeshStreamStats;
85
+ bytes?: number;
86
+ totalErrors?: number;
87
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Public npm package version. Keep this value aligned with package.json for each release.
3
+ * It is deliberately separate from the Android/iOS engine version.
4
+ */
5
+ export declare const MESHSTREAM_REACT_NATIVE_VERSION = "1.0.2";
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MESHSTREAM_REACT_NATIVE_VERSION = void 0;
4
+ /**
5
+ * Public npm package version. Keep this value aligned with package.json for each release.
6
+ * It is deliberately separate from the Android/iOS engine version.
7
+ */
8
+ exports.MESHSTREAM_REACT_NATIVE_VERSION = '1.0.2';
package/package.json CHANGED
@@ -1,6 +1,54 @@
1
1
  {
2
2
  "name": "react-native-meshstream",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.2",
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
+ "verify:version": "node scripts/verify-package-version.mjs",
26
+ "test:ios": "node --test tests/node/ios-regression-guards.test.mjs",
27
+ "verify:ios": "npm run test:ios && node scripts/verify-ios-package.mjs && node scripts/verify-ios-xctest.mjs",
28
+ "verify:ios:public": "npm run test:ios && node scripts/verify-ios-package.mjs && node scripts/verify-ios-xctest.mjs --repo-update",
29
+ "prepare": "npm run build",
30
+ "prepack": "npm run verify:version",
31
+ "test": "npm run build && node --test tests/node/*.test.mjs"
32
+ },
33
+ "keywords": [
34
+ "react-native",
35
+ "p2p",
36
+ "cdn",
37
+ "video",
38
+ "meshstream"
39
+ ],
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/Mesh-Stream/meshstream-p2p-react-native-sdk.git"
43
+ },
44
+ "license": "Apache-2.0",
45
+ "peerDependencies": {
46
+ "react": ">=18.0.0",
47
+ "react-native": ">=0.72.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/react": "^18.3.0",
51
+ "react-native": "^0.76.0",
52
+ "typescript": "^5.6.3"
53
+ }
54
+ }
@@ -0,0 +1,11 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ android: {
5
+ sourceDir: './android',
6
+ packageImportPath: 'import net.meshstream.reactnative.MeshStreamP2PPackage;',
7
+ packageInstance: 'new MeshStreamP2PPackage()',
8
+ },
9
+ },
10
+ },
11
+ };
@@ -0,0 +1,36 @@
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
+ getSdkVersion(): Promise<string>;
14
+ createClient(config: MeshStreamConfig): Promise<{ clientId: string; status: MeshStreamStatus }>;
15
+ createPlaybackSession(clientId: string, options: PlaybackSessionOptions): Promise<MeshStreamSessionInfo>;
16
+ preload(clientId: string, sessionId: string): Promise<boolean>;
17
+ setP2pEnabled(clientId: string, sessionId: string, enabled: boolean): Promise<void>;
18
+ updateBufferedDurationMs(clientId: string, sessionId: string, durationMs: number): Promise<void>;
19
+ updateCurrentPositionMs(clientId: string, sessionId: string, positionMs: number): Promise<void>;
20
+ getStats(clientId: string, sessionId: string): Promise<MeshStreamStats>;
21
+ getDiagnostics(clientId: string, sessionId: string): Promise<MeshStreamDiagnostics>;
22
+ getStatus(clientId: string): Promise<MeshStreamStatus>;
23
+ closeSession(clientId: string, sessionId: string): Promise<void>;
24
+ closeClient(clientId: string): Promise<void>;
25
+ }
26
+
27
+ const nativeModule = NativeModules.MeshStreamP2P as NativeMeshStreamP2P | undefined;
28
+
29
+ export function requireNativeModule(): NativeMeshStreamP2P {
30
+ if (!nativeModule) {
31
+ throw new Error(
32
+ 'MeshStreamP2P is not linked. Rebuild the iOS or Android app after installing react-native-meshstream.',
33
+ );
34
+ }
35
+ return nativeModule;
36
+ }
package/src/index.ts ADDED
@@ -0,0 +1,176 @@
1
+ import { DeviceEventEmitter, Platform, type EmitterSubscription } from 'react-native';
2
+
3
+ import { requireNativeModule } from './NativeMeshStreamP2P';
4
+ import { MESHSTREAM_REACT_NATIVE_VERSION } from './version';
5
+ import type {
6
+ MeshStreamConfig,
7
+ MeshStreamDiagnostics,
8
+ MeshStreamEvent,
9
+ MeshStreamEventName,
10
+ MeshStreamSessionInfo,
11
+ MeshStreamStats,
12
+ MeshStreamStatus,
13
+ MeshStreamVersionInfo,
14
+ PlaybackSessionOptions,
15
+ } from './types';
16
+
17
+ export * from './types';
18
+
19
+ const EVENT_NAME = 'MeshStreamP2P:event';
20
+
21
+ function assertNonEmpty(value: string | undefined, field: string): asserts value is string {
22
+ if (!value?.trim()) throw new Error(`${field} must not be empty.`);
23
+ }
24
+
25
+ function assertNonNegative(value: number, field: string): void {
26
+ if (!Number.isFinite(value) || value < 0) throw new Error(`${field} must be a finite non-negative number.`);
27
+ }
28
+
29
+ export class MeshStreamPlaybackSession {
30
+ readonly clientId: string;
31
+ readonly sessionId: string;
32
+ readonly playbackUrl: string;
33
+ readonly playbackMode: MeshStreamSessionInfo['playbackMode'];
34
+ readonly fallbackReason?: MeshStreamSessionInfo['fallbackReason'];
35
+ private readonly isClientClosed: () => boolean;
36
+ private closed = false;
37
+ private closing?: Promise<void>;
38
+
39
+ constructor(info: MeshStreamSessionInfo, isClientClosed: () => boolean = () => false) {
40
+ this.clientId = info.clientId;
41
+ this.sessionId = info.sessionId;
42
+ this.playbackUrl = info.playbackUrl;
43
+ this.playbackMode = info.playbackMode;
44
+ this.fallbackReason = info.fallbackReason;
45
+ this.isClientClosed = isClientClosed;
46
+ }
47
+
48
+ async preload(): Promise<boolean> {
49
+ return this.withOpen(() => requireNativeModule().preload(this.clientId, this.sessionId));
50
+ }
51
+
52
+ async setP2pEnabled(enabled: boolean): Promise<void> {
53
+ return this.withOpen(() => requireNativeModule().setP2pEnabled(this.clientId, this.sessionId, enabled));
54
+ }
55
+
56
+ async updateBufferedDurationMs(durationMs: number): Promise<void> {
57
+ assertNonNegative(durationMs, 'durationMs');
58
+ return this.withOpen(() => requireNativeModule().updateBufferedDurationMs(this.clientId, this.sessionId, durationMs));
59
+ }
60
+
61
+ async updateCurrentPositionMs(positionMs: number): Promise<void> {
62
+ assertNonNegative(positionMs, 'positionMs');
63
+ return this.withOpen(() => requireNativeModule().updateCurrentPositionMs(this.clientId, this.sessionId, positionMs));
64
+ }
65
+
66
+ async getStats(): Promise<MeshStreamStats> {
67
+ return this.withOpen(() => requireNativeModule().getStats(this.clientId, this.sessionId));
68
+ }
69
+
70
+ async getDiagnostics(): Promise<MeshStreamDiagnostics> {
71
+ return this.withOpen(() => requireNativeModule().getDiagnostics(this.clientId, this.sessionId));
72
+ }
73
+
74
+ async close(): Promise<void> {
75
+ if (this.closed) return;
76
+ if (this.isClientClosed()) {
77
+ return;
78
+ }
79
+ if (!this.closing) {
80
+ this.closing = requireNativeModule()
81
+ .closeSession(this.clientId, this.sessionId)
82
+ .then(() => { this.closed = true; })
83
+ .finally(() => { this.closing = undefined; });
84
+ }
85
+ return this.closing;
86
+ }
87
+
88
+ private async withOpen<T>(operation: () => Promise<T>): Promise<T> {
89
+ if (this.closed || this.closing || this.isClientClosed()) {
90
+ throw new Error('Playback session is closed.');
91
+ }
92
+ return operation();
93
+ }
94
+ }
95
+
96
+ export class MeshStreamClient {
97
+ readonly clientId: string;
98
+ private closed = false;
99
+ private closing?: Promise<void>;
100
+
101
+ constructor(clientId: string) {
102
+ this.clientId = clientId;
103
+ }
104
+
105
+ async createPlaybackSession(options: PlaybackSessionOptions): Promise<MeshStreamPlaybackSession> {
106
+ assertNonEmpty(options.url, 'options.url');
107
+ return this.withOpen(async () => {
108
+ const info = await requireNativeModule().createPlaybackSession(this.clientId, options);
109
+ if (this.closed || this.closing) throw new Error('MeshStream client is closed.');
110
+ return new MeshStreamPlaybackSession(info, () => this.closed || this.closing !== undefined);
111
+ });
112
+ }
113
+
114
+ async getStatus(): Promise<MeshStreamStatus> {
115
+ return this.withOpen(() => requireNativeModule().getStatus(this.clientId));
116
+ }
117
+
118
+ async close(): Promise<void> {
119
+ if (this.closed) return;
120
+ if (!this.closing) {
121
+ this.closing = requireNativeModule()
122
+ .closeClient(this.clientId)
123
+ .then(() => { this.closed = true; })
124
+ .finally(() => { this.closing = undefined; });
125
+ }
126
+ return this.closing;
127
+ }
128
+
129
+ private async withOpen<T>(operation: () => Promise<T>): Promise<T> {
130
+ if (this.closed || this.closing) throw new Error('MeshStream client is closed.');
131
+ return operation();
132
+ }
133
+ }
134
+
135
+ export const MeshStream = {
136
+ /** Returns the version of the native MeshStream SDK loaded by this application. */
137
+ async getSdkVersion(): Promise<string> {
138
+ return requireNativeModule().getSdkVersion();
139
+ },
140
+
141
+ /** Returns the React Native package version and the loaded native engine version. */
142
+ async getVersion(): Promise<MeshStreamVersionInfo> {
143
+ const platform = Platform.OS;
144
+ if (platform !== 'android' && platform !== 'ios') {
145
+ throw new Error(`react-native-meshstream does not support ${platform}.`);
146
+ }
147
+ return {
148
+ packageVersion: MESHSTREAM_REACT_NATIVE_VERSION,
149
+ nativeSdkVersion: await MeshStream.getSdkVersion(),
150
+ platform,
151
+ };
152
+ },
153
+
154
+ async create(config: MeshStreamConfig): Promise<MeshStreamClient> {
155
+ assertNonEmpty(config.key, 'config.key');
156
+ const result = await requireNativeModule().createClient(config);
157
+ return new MeshStreamClient(result.clientId);
158
+ },
159
+
160
+ addEventListener(listener: (event: MeshStreamEvent) => void): EmitterSubscription {
161
+ return DeviceEventEmitter.addListener(EVENT_NAME, listener);
162
+ },
163
+ };
164
+
165
+ /** Filtered event subscription for a particular client or playback session. */
166
+ export function addMeshStreamEventListener(
167
+ listener: (event: MeshStreamEvent) => void,
168
+ filter?: { clientId?: string; sessionId?: string; type?: MeshStreamEventName },
169
+ ): EmitterSubscription {
170
+ return MeshStream.addEventListener((event) => {
171
+ if (filter?.clientId && event.clientId !== filter.clientId) return;
172
+ if (filter?.sessionId && event.sessionId !== filter.sessionId) return;
173
+ if (filter?.type && event.type !== filter.type) return;
174
+ listener(event);
175
+ });
176
+ }
package/src/types.ts ADDED
@@ -0,0 +1,128 @@
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
+ /** Versions of the JavaScript wrapper and native engine used by this application. */
10
+ export interface MeshStreamVersionInfo {
11
+ /** Version of the installed react-native-meshstream package. */
12
+ packageVersion: string;
13
+ /** Version of the Android or Apple MeshStream engine loaded by the bridge. */
14
+ nativeSdkVersion: string;
15
+ /** Native platform that supplied nativeSdkVersion. */
16
+ platform: 'android' | 'ios';
17
+ }
18
+
19
+ export interface PlaybackSessionOptions {
20
+ url: string;
21
+ contentId?: string;
22
+ mimeType?: MeshStreamMimeType;
23
+ originHeaders?: Record<string, string>;
24
+ }
25
+
26
+ export type MeshStreamPlaybackMode = 'NATIVE_P2P' | 'NATIVE_CDN';
27
+
28
+ export type MeshStreamUnavailableReason =
29
+ | 'OK'
30
+ | 'ACCOUNT_DISABLED'
31
+ | 'APP_DISABLED'
32
+ | 'APP_NOT_FOUND'
33
+ | 'KEY_DISABLED'
34
+ | 'PLATFORM_DISABLED'
35
+ | 'ORIGIN_DENIED'
36
+ | 'PACKAGE_DENIED'
37
+ | 'BUNDLE_DENIED'
38
+ | 'SIGNING_CERT_DENIED'
39
+ | 'TEAM_DENIED'
40
+ | 'AUTH_FAILED'
41
+ | 'CONFIG_UNAVAILABLE'
42
+ | 'TRACKER_ERROR'
43
+ | 'NO_PEERS'
44
+ | 'UNSUPPORTED'
45
+ | 'INITIALIZATION_FAILED'
46
+ | 'DISABLED'
47
+ | 'KEY_P2P_DISABLED'
48
+ | 'QUOTA_EXHAUSTED'
49
+ | 'QUOTA_DISABLED';
50
+
51
+ export interface MeshStreamStatus {
52
+ accountEnabled: boolean;
53
+ applicationEnabled: boolean;
54
+ keyEnabled: boolean;
55
+ platformEnabled: boolean;
56
+ effectiveP2pEnabled: boolean;
57
+ reason: MeshStreamUnavailableReason;
58
+ configVersion?: string;
59
+ edgeRegion?: string;
60
+ quotaStatus?: string;
61
+ trackerPoolReady?: boolean;
62
+ }
63
+
64
+ export interface MeshStreamStats {
65
+ httpDownloaded: number;
66
+ p2pDownloaded: number;
67
+ p2pUploaded: number;
68
+ httpSegmentDownloadSuccesses: number;
69
+ httpSegmentDownloadFailures: number;
70
+ p2pSegmentDownloadSuccesses: number;
71
+ p2pSegmentDownloadFailures: number;
72
+ p2pSegmentUploadSuccesses: number;
73
+ p2pSegmentUploadFailures: number;
74
+ connectedPeers: number;
75
+ peerConnectErrors: number;
76
+ trackerErrors: number;
77
+ downloadOffloadRatio: number;
78
+ p2pSegmentDownloadSuccessRatio: number;
79
+ p2pSegmentUploadSuccessRatio: number;
80
+ }
81
+
82
+ export interface MeshStreamColdStartSnapshot {
83
+ p2pDownloaded: number;
84
+ httpDownloaded: number;
85
+ }
86
+
87
+ export interface MeshStreamDiagnostics {
88
+ fallbackCount: number;
89
+ firstPeerConnectMs?: number;
90
+ coldStart?: MeshStreamColdStartSnapshot;
91
+ lastTrackerError?: string;
92
+ lastPeerError?: string;
93
+ lastErrorAtMs?: number;
94
+ iceAttempts: number;
95
+ iceSuccesses: number;
96
+ iceFailures: number;
97
+ iceDurationMs: number;
98
+ }
99
+
100
+ export interface MeshStreamSessionInfo {
101
+ clientId: string;
102
+ sessionId: string;
103
+ playbackUrl: string;
104
+ playbackMode: MeshStreamPlaybackMode;
105
+ fallbackReason?: MeshStreamUnavailableReason;
106
+ }
107
+
108
+ export type MeshStreamEventName =
109
+ | 'peerCountChanged'
110
+ | 'playbackPrepared'
111
+ | 'p2pReady'
112
+ | 'p2pUnavailable'
113
+ | 'statistics'
114
+ | 'cdnFallback'
115
+ | 'trackerError'
116
+ | 'peerConnectError';
117
+
118
+ export interface MeshStreamEvent {
119
+ type: MeshStreamEventName;
120
+ clientId: string;
121
+ sessionId: string;
122
+ peerCount?: number;
123
+ playbackUrl?: string;
124
+ reason?: MeshStreamUnavailableReason;
125
+ statistics?: MeshStreamStats;
126
+ bytes?: number;
127
+ totalErrors?: number;
128
+ }
package/src/version.ts ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Public npm package version. Keep this value aligned with package.json for each release.
3
+ * It is deliberately separate from the Android/iOS engine version.
4
+ */
5
+ export const MESHSTREAM_REACT_NATIVE_VERSION = '1.0.2';
@@ -0,0 +1,35 @@
1
+ #import <XCTest/XCTest.h>
2
+ #import <MeshStreamKit/MeshStreamKit.h>
3
+
4
+ #import "../../ios/MeshStreamP2PValidation.h"
5
+
6
+ @interface MeshStreamP2PValidationTests : XCTestCase
7
+ @end
8
+
9
+ @implementation MeshStreamP2PValidationTests
10
+
11
+ - (void)testNativeVersionIsAvailableBeforeCreatingClient {
12
+ XCTAssertEqualObjects([P2PSDK getSdkVersion], @"1.0.6");
13
+ }
14
+
15
+ - (void)testTrimsURLsAndFiltersNonStringHeaders {
16
+ XCTAssertEqualObjects(
17
+ MeshStreamTrimmedString(@" content-42\n"),
18
+ @"content-42");
19
+ XCTAssertNil(MeshStreamTrimmedString(@42));
20
+ XCTAssertEqualObjects(
21
+ MeshStreamHTTPURLFromValue(@" https://example.com/live.m3u8 ").absoluteString,
22
+ @"https://example.com/live.m3u8");
23
+ XCTAssertNil(MeshStreamHTTPURLFromValue(@"ftp://example.com/live.m3u8"));
24
+ NSDictionary *headers = MeshStreamStringHeaders(@{
25
+ @"Authorization": @"Bearer token",
26
+ @"retry": @1,
27
+ @2: @"ignored",
28
+ });
29
+ NSDictionary *expectedHeaders = @{
30
+ @"Authorization": @"Bearer token",
31
+ };
32
+ XCTAssertEqualObjects(headers, expectedHeaders);
33
+ }
34
+
35
+ @end