@meshstream/p2p-web 0.0.0-stage → 0.2.1
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/README.md +497 -2
- package/dist/adapters.d.ts +140 -0
- package/dist/adapters.js +1 -0
- package/dist/advanced.d.ts +1 -0
- package/dist/advanced.js +1 -0
- package/dist/capabilities.d.ts +27 -0
- package/dist/config.d.ts +100 -0
- package/dist/dash-p2p.d.ts +116 -0
- package/dist/dash.d.ts +110 -0
- package/dist/dashjs-p2p.d.ts +222 -0
- package/dist/direct-file.d.ts +8 -0
- package/dist/fallback.d.ts +35 -0
- package/dist/hls.d.ts +157 -0
- package/dist/ice.d.ts +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1 -0
- package/dist/meshstream-sw.d.ts +0 -0
- package/dist/meshstream-sw.js +1 -0
- package/dist/origin-headers.d.ts +36 -0
- package/dist/playback-auth.d.ts +14 -0
- package/dist/playback-lifecycle.d.ts +11 -0
- package/dist/player.d.ts +160 -0
- package/dist/public-capabilities.d.ts +11 -0
- package/dist/qoe.d.ts +25 -0
- package/dist/report.d.ts +140 -0
- package/dist/rx-player.d.ts +163 -0
- package/dist/segment-storage.d.ts +31 -0
- package/dist/service-worker-p2p.d.ts +166 -0
- package/dist/service-worker.d.ts +113 -0
- package/dist/service-worker.js +1 -0
- package/dist/session-token.d.ts +47 -0
- package/dist/url.d.ts +39 -0
- package/dist/vendor/meshstream-p2p-core.d.ts +536 -0
- package/dist/version.d.ts +2 -0
- package/package.json +53 -6
- package/src/vendor/MESHSTREAM_CORE_LICENSE.txt +202 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { SdkConfigResponse } from "./config.js";
|
|
2
|
+
import type { P2pEventListener } from "./hls.js";
|
|
3
|
+
import { type OriginHeaders, type OriginHeadersProvider } from "./origin-headers.js";
|
|
4
|
+
import type { P2pConnectionMetric, P2pStats } from "./report.js";
|
|
5
|
+
import type { MeshStreamSwSegmentProvider } from "./service-worker.js";
|
|
6
|
+
export interface MeshStreamServiceWorkerP2pEngineOptions {
|
|
7
|
+
manifestUrl: string;
|
|
8
|
+
/** `direct-file` enables bounded Range-based P2P for progressive media. */
|
|
9
|
+
resourceKind?: "hls" | "direct-file";
|
|
10
|
+
swarmId: string;
|
|
11
|
+
trackerUrl: string;
|
|
12
|
+
trackerUrls?: string[];
|
|
13
|
+
originHeaders?: OriginHeaders;
|
|
14
|
+
originHeadersProvider?: OriginHeadersProvider;
|
|
15
|
+
iceServers?: RTCIceServer[];
|
|
16
|
+
sdkConfig?: Pick<SdkConfigResponse, "iceServers">;
|
|
17
|
+
requestTimeoutMs?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface MeshStreamServiceWorkerP2pDiagnostics {
|
|
20
|
+
/** Whether the page-side engine is currently accepting P2P requests. */
|
|
21
|
+
p2pEnabled: boolean;
|
|
22
|
+
/** Whether direct-file metadata has been indexed for Range requests. */
|
|
23
|
+
directFileReady: boolean;
|
|
24
|
+
connectedPeers: number;
|
|
25
|
+
/** Info hashes of currently connected stream swarms, for transport diagnostics. */
|
|
26
|
+
peerInfoHashes: string[];
|
|
27
|
+
trackerErrors: number;
|
|
28
|
+
lastTrackerError?: string;
|
|
29
|
+
peerConnectErrors: number;
|
|
30
|
+
/** Time from engine creation until the first usable P2P peer. */
|
|
31
|
+
firstPeerConnectMs?: number;
|
|
32
|
+
/** CDN segment completions observed while no P2P peer was connected. */
|
|
33
|
+
fallbackCount: number;
|
|
34
|
+
lastPeerError?: string;
|
|
35
|
+
lastErrorAtMs?: number;
|
|
36
|
+
/** Cumulative terminal ICE outcomes emitted by p2p-media-loader Core. */
|
|
37
|
+
ice: P2pConnectionMetric;
|
|
38
|
+
/** Number of announcements received from connected peers. */
|
|
39
|
+
peerSegmentAnnouncements: number;
|
|
40
|
+
/** Loaded segment count reported by the latest peer announcement. */
|
|
41
|
+
peerAnnouncedSegments: number;
|
|
42
|
+
/** HTTP-loading segment count reported by the latest peer announcement. */
|
|
43
|
+
peerHttpLoadingSegments: number;
|
|
44
|
+
p2pAttempts: number;
|
|
45
|
+
httpAttempts: number;
|
|
46
|
+
p2pErrors: number;
|
|
47
|
+
httpErrors: number;
|
|
48
|
+
p2pUploaded: number;
|
|
49
|
+
p2pSegmentUploadSuccesses: number;
|
|
50
|
+
p2pSegmentUploadFailures: number;
|
|
51
|
+
lastLoadedBytes: number;
|
|
52
|
+
segmentRequests: number;
|
|
53
|
+
runtimeIdMisses: number;
|
|
54
|
+
segmentLoadSuccesses: number;
|
|
55
|
+
segmentLoadTimeouts: number;
|
|
56
|
+
indexedSegments: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Page-side P2P engine used by the Service Worker bridge. It deliberately uses
|
|
60
|
+
* p2p-media-loader-core directly, so native-HLS playback does not require MSE.
|
|
61
|
+
*/
|
|
62
|
+
export declare class MeshStreamServiceWorkerP2pEngine {
|
|
63
|
+
/** Actual Tracker swarm, including its configured hot-content bucket. */
|
|
64
|
+
readonly swarmId: string;
|
|
65
|
+
private core;
|
|
66
|
+
private readonly trackerCandidates;
|
|
67
|
+
private readonly createCore;
|
|
68
|
+
private readonly streamIdFactory;
|
|
69
|
+
private readonly manifestUrl;
|
|
70
|
+
private readonly resourceKind;
|
|
71
|
+
private readonly requestTimeoutMs;
|
|
72
|
+
/** HLS playlists update this after each parse; progressive media is known VOD. */
|
|
73
|
+
private playbackIsLive;
|
|
74
|
+
private readonly manifestRequestSetup;
|
|
75
|
+
private readonly originHeaders;
|
|
76
|
+
private readonly originHeadersProvider;
|
|
77
|
+
private readonly streams;
|
|
78
|
+
private readonly segmentIdsByRequestKey;
|
|
79
|
+
private readonly playbackPositionsByRuntimeId;
|
|
80
|
+
private readonly completedSources;
|
|
81
|
+
private readonly eventListeners;
|
|
82
|
+
private readonly startedAtMs;
|
|
83
|
+
private eventTimer;
|
|
84
|
+
private p2pReady;
|
|
85
|
+
private readonly stats;
|
|
86
|
+
private readonly pendingSegmentLoads;
|
|
87
|
+
private readonly directChunks;
|
|
88
|
+
private directFile;
|
|
89
|
+
private disposed;
|
|
90
|
+
private peerCount;
|
|
91
|
+
private readonly peerInfoHashes;
|
|
92
|
+
private trackerErrors;
|
|
93
|
+
private lastTrackerError;
|
|
94
|
+
private peerConnectErrors;
|
|
95
|
+
private firstPeerConnectMs;
|
|
96
|
+
private fallbackCount;
|
|
97
|
+
private lastPeerError;
|
|
98
|
+
private lastErrorAtMs;
|
|
99
|
+
private readonly ice;
|
|
100
|
+
private peerSegmentAnnouncements;
|
|
101
|
+
private peerAnnouncedSegments;
|
|
102
|
+
private peerHttpLoadingSegments;
|
|
103
|
+
private p2pAttempts;
|
|
104
|
+
private httpAttempts;
|
|
105
|
+
private p2pErrors;
|
|
106
|
+
private httpErrors;
|
|
107
|
+
private p2pUploaded;
|
|
108
|
+
private p2pSegmentUploadSuccesses;
|
|
109
|
+
private p2pSegmentUploadFailures;
|
|
110
|
+
private lastLoadedBytes;
|
|
111
|
+
private segmentRequests;
|
|
112
|
+
private runtimeIdMisses;
|
|
113
|
+
private segmentLoadSuccesses;
|
|
114
|
+
private segmentLoadTimeouts;
|
|
115
|
+
private indexedSegments;
|
|
116
|
+
private activeTrackerIndex;
|
|
117
|
+
private failoverInFlight;
|
|
118
|
+
private coreReady;
|
|
119
|
+
private coreGeneration;
|
|
120
|
+
private coreRebuildInFlight;
|
|
121
|
+
private coreRebuildGeneration;
|
|
122
|
+
private lastCoreRebuildAttemptAtMs;
|
|
123
|
+
private p2pEnabled;
|
|
124
|
+
/** Core has a single foreground engine request, so direct-file ranges queue here. */
|
|
125
|
+
private directFileLoadQueue;
|
|
126
|
+
/** Reset Core just before its first request so its P2P bootstrap window is usable. */
|
|
127
|
+
private hasIssuedCoreLoad;
|
|
128
|
+
private constructor();
|
|
129
|
+
private bindCoreEvents;
|
|
130
|
+
static create(options: MeshStreamServiceWorkerP2pEngineOptions): Promise<MeshStreamServiceWorkerP2pEngine>;
|
|
131
|
+
private failoverTracker;
|
|
132
|
+
private rebuildCoreState;
|
|
133
|
+
private scheduleCoreRebuild;
|
|
134
|
+
private ensureCoreState;
|
|
135
|
+
initialize(): Promise<void>;
|
|
136
|
+
createSegmentProvider(): MeshStreamSwSegmentProvider;
|
|
137
|
+
addEventListener(listener: P2pEventListener): void;
|
|
138
|
+
removeEventListener(listener: P2pEventListener): void;
|
|
139
|
+
/** Stops or recreates the Core while leaving the virtual media URL on CDN. */
|
|
140
|
+
setP2pEnabled(enabled: boolean): void;
|
|
141
|
+
diagnostics(): MeshStreamServiceWorkerP2pDiagnostics;
|
|
142
|
+
statistics(): P2pStats;
|
|
143
|
+
isLive(): boolean;
|
|
144
|
+
isPlaybackModeKnown(): boolean;
|
|
145
|
+
dispose(): void;
|
|
146
|
+
private notifyUnavailable;
|
|
147
|
+
private notify;
|
|
148
|
+
private notifyPlaybackState;
|
|
149
|
+
private elapsedSinceStartMs;
|
|
150
|
+
private provideSegment;
|
|
151
|
+
private loadSegmentResponse;
|
|
152
|
+
private initializeDirectFile;
|
|
153
|
+
private fetchDirectFileMetadata;
|
|
154
|
+
private createDirectFileMetadataRequest;
|
|
155
|
+
private rebuildDirectFileStream;
|
|
156
|
+
private loadDirectFileRange;
|
|
157
|
+
private isLargeDirectFileRange;
|
|
158
|
+
private streamDirectFileRange;
|
|
159
|
+
private loadDirectFileChunk;
|
|
160
|
+
private enqueueDirectFileLoad;
|
|
161
|
+
private resetInitialCoreRequestWindow;
|
|
162
|
+
private ensureDirectFileChunk;
|
|
163
|
+
private loadSegmentFromCore;
|
|
164
|
+
private refreshPlaylists;
|
|
165
|
+
private indexPlaylist;
|
|
166
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { OriginHeadersProvider } from "./origin-headers.js";
|
|
2
|
+
export declare const MESHSTREAM_SW_PROTOCOL_VERSION = 1;
|
|
3
|
+
export declare const MESHSTREAM_SW_PATH_PREFIX = "/__meshstream__/v1/";
|
|
4
|
+
export type MeshStreamServiceWorkerMessage = {
|
|
5
|
+
type: "MESHSTREAM_SW_PING";
|
|
6
|
+
protocolVersion: number;
|
|
7
|
+
} | {
|
|
8
|
+
type: "MESHSTREAM_SW_REGISTER_SESSION";
|
|
9
|
+
protocolVersion: number;
|
|
10
|
+
sessionId: string;
|
|
11
|
+
manifestUrl: string;
|
|
12
|
+
resourceKind?: "hls" | "direct-file";
|
|
13
|
+
originHeaders?: Record<string, string>;
|
|
14
|
+
p2pTimeoutMs: number;
|
|
15
|
+
} | {
|
|
16
|
+
type: "MESHSTREAM_SW_UNREGISTER_SESSION";
|
|
17
|
+
protocolVersion: number;
|
|
18
|
+
sessionId: string;
|
|
19
|
+
};
|
|
20
|
+
export interface MeshStreamServiceWorkerRegistrationOptions {
|
|
21
|
+
url?: string;
|
|
22
|
+
scope?: string;
|
|
23
|
+
registration?: ServiceWorkerRegistration;
|
|
24
|
+
readyTimeoutMs?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface MeshStreamServiceWorkerSessionOptions {
|
|
27
|
+
serviceWorker?: MeshStreamServiceWorkerClient;
|
|
28
|
+
sessionId?: string;
|
|
29
|
+
manifestUrl: string;
|
|
30
|
+
/** `direct-file` proxies one progressive media resource without playlist rewriting. */
|
|
31
|
+
resourceKind?: "hls" | "direct-file";
|
|
32
|
+
originHeaders?: Record<string, string>;
|
|
33
|
+
originHeadersProvider?: OriginHeadersProvider;
|
|
34
|
+
p2pTimeoutMs?: number;
|
|
35
|
+
segmentProvider?: MeshStreamSwSegmentProvider;
|
|
36
|
+
onSegmentStats?: (stats: MeshStreamSwSegmentStats) => void;
|
|
37
|
+
}
|
|
38
|
+
export interface MeshStreamSwSegmentRequest {
|
|
39
|
+
sessionId: string;
|
|
40
|
+
resourceId: string;
|
|
41
|
+
url: string;
|
|
42
|
+
headers: Headers;
|
|
43
|
+
}
|
|
44
|
+
export interface MeshStreamSwSegmentResponse {
|
|
45
|
+
/** A bounded response body. Exactly one of `body` or `stream` is required. */
|
|
46
|
+
body?: ArrayBuffer;
|
|
47
|
+
/** A backpressure-aware response for large progressive-media byte ranges. */
|
|
48
|
+
stream?: ReadableStream<Uint8Array>;
|
|
49
|
+
/** Total response bytes. Required when `stream` is used. */
|
|
50
|
+
bytes?: number;
|
|
51
|
+
status?: number;
|
|
52
|
+
statusText?: string;
|
|
53
|
+
headers?: Record<string, string>;
|
|
54
|
+
source?: "p2p" | "cdn";
|
|
55
|
+
/** Exact byte attribution for a response assembled from multiple chunks. */
|
|
56
|
+
p2pBytes?: number;
|
|
57
|
+
/** Exact byte attribution for a response assembled from multiple chunks. */
|
|
58
|
+
cdnBytes?: number;
|
|
59
|
+
/** Mutable counters used while a progressive-media stream is consumed. */
|
|
60
|
+
transferStats?: MeshStreamSwSegmentTransferStats;
|
|
61
|
+
}
|
|
62
|
+
export interface MeshStreamSwSegmentTransferStats {
|
|
63
|
+
p2pBytes: number;
|
|
64
|
+
cdnBytes: number;
|
|
65
|
+
}
|
|
66
|
+
export interface MeshStreamSwSegmentStats {
|
|
67
|
+
sessionId: string;
|
|
68
|
+
resourceId: string;
|
|
69
|
+
url: string;
|
|
70
|
+
source: "p2p" | "cdn";
|
|
71
|
+
bytes: number;
|
|
72
|
+
status: number;
|
|
73
|
+
p2pBytes?: number;
|
|
74
|
+
cdnBytes?: number;
|
|
75
|
+
}
|
|
76
|
+
export type MeshStreamSwSegmentProvider = (request: MeshStreamSwSegmentRequest) => Promise<MeshStreamSwSegmentResponse | undefined>;
|
|
77
|
+
export interface MeshStreamServiceWorkerSession {
|
|
78
|
+
readonly sessionId: string;
|
|
79
|
+
readonly manifestUrl: string;
|
|
80
|
+
dispose(): Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
export declare class MeshStreamServiceWorkerClient {
|
|
83
|
+
readonly registration: ServiceWorkerRegistration;
|
|
84
|
+
private readonly messageListener;
|
|
85
|
+
private readonly segmentProviders;
|
|
86
|
+
private readonly originHeadersProviders;
|
|
87
|
+
private readonly statsListeners;
|
|
88
|
+
private readonly sessionBridgePorts;
|
|
89
|
+
private bridgeRequests;
|
|
90
|
+
private bridgeUnavailableResponses;
|
|
91
|
+
constructor(registration: ServiceWorkerRegistration);
|
|
92
|
+
registerSession(options: {
|
|
93
|
+
sessionId: string;
|
|
94
|
+
manifestUrl: string;
|
|
95
|
+
resourceKind?: "hls" | "direct-file";
|
|
96
|
+
originHeaders?: Record<string, string>;
|
|
97
|
+
originHeadersProvider?: OriginHeadersProvider;
|
|
98
|
+
p2pTimeoutMs?: number;
|
|
99
|
+
segmentProvider?: MeshStreamSwSegmentProvider;
|
|
100
|
+
onSegmentStats?: (stats: MeshStreamSwSegmentStats) => void;
|
|
101
|
+
}): Promise<MeshStreamServiceWorkerSession>;
|
|
102
|
+
dispose(): void;
|
|
103
|
+
diagnostics(): {
|
|
104
|
+
serviceWorkerBridgeRequests: number;
|
|
105
|
+
serviceWorkerBridgeUnavailableResponses: number;
|
|
106
|
+
};
|
|
107
|
+
private removeSession;
|
|
108
|
+
private closeSessionBridge;
|
|
109
|
+
}
|
|
110
|
+
export declare function isMeshStreamServiceWorkerSupported(): boolean;
|
|
111
|
+
export declare function registerMeshStreamServiceWorker(options?: MeshStreamServiceWorkerRegistrationOptions): Promise<MeshStreamServiceWorkerClient>;
|
|
112
|
+
export declare function createMeshStreamServiceWorkerSession(options: MeshStreamServiceWorkerSessionOptions): Promise<MeshStreamServiceWorkerSession | undefined>;
|
|
113
|
+
export declare function buildMeshStreamServiceWorkerUrl(sessionId: string, resourceId: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=1,s="/__meshstream__/v1/",t=new Map,r=class{registration;messageListener;segmentProviders=new Map;originHeadersProviders=new Map;statsListeners=new Map;sessionBridgePorts=new Map;bridgeRequests=0;bridgeUnavailableResponses=0;constructor(e){this.registration=e,this.messageListener=e=>{const s=e.data;if("MESHSTREAM_SW_SEGMENT_RESULT"===s?.type&&s.sessionId){const e=this.statsListeners.get(s.sessionId);if(!e)return;const t={sessionId:s.sessionId,resourceId:s.resourceId??"",url:s.url??"",source:"p2p"===s.source?"p2p":"cdn",bytes:Number.isFinite(s.bytes)?Math.max(0,s.bytes):0,status:Number.isFinite(s.status)?s.status:200,p2pBytes:Number.isFinite(s.p2pBytes)?Math.max(0,s.p2pBytes):void 0,cdnBytes:Number.isFinite(s.cdnBytes)?Math.max(0,s.cdnBytes):void 0};try{e(t)}catch{}return}const r=e.ports?.[0];if("MESHSTREAM_SW_ORIGIN_HEADERS_REQUEST"===s?.type&&s.sessionId&&r){let e;try{e=this.originHeadersProviders.get(s.sessionId)?.({url:s.url??"",kind:(i=s.kind,"manifest"===i||"segment"===i||"key"===i||"direct-file"===i||"unknown"===i?s.kind:"unknown")})}catch{}return void r.postMessage({type:"MESHSTREAM_SW_ORIGIN_HEADERS_RESPONSE",headers:e??{}})}var i;if("MESHSTREAM_SW_SEGMENT_REQUEST"!==s?.type||!s.sessionId||!r)return;this.bridgeRequests+=1;const o=this.segmentProviders.get(s.sessionId);if(!o)return a=s.sessionId,void((t.get(a)??0)>0||(this.bridgeUnavailableResponses+=1,r.postMessage({type:"MESHSTREAM_SW_SEGMENT_UNAVAILABLE"})));var a;o({sessionId:s.sessionId,resourceId:s.resourceId??"",url:s.url??"",headers:new Headers(s.headers??{})}).then(e=>{if(!e)return this.bridgeUnavailableResponses+=1,void r.postMessage({type:"MESHSTREAM_SW_SEGMENT_UNAVAILABLE"});e.stream?function(e,s){const t=s.stream;if(!t||!Number.isFinite(s.bytes)||(s.bytes??0)<0)return void e.postMessage({type:"MESHSTREAM_SW_SEGMENT_UNAVAILABLE"});const r=t.getReader();let i=!1;const o=(t=!1,o=!1)=>{if(i)return;i=!0,t&&r.cancel().catch(()=>{});const a=s.transferStats?.p2pBytes??s.p2pBytes,c=s.transferStats?.cdnBytes??s.cdnBytes,d=(a??0)+(c??0);e.postMessage({type:o?"MESHSTREAM_SW_SEGMENT_STREAM_ERROR":"MESHSTREAM_SW_SEGMENT_STREAM_COMPLETE",source:n(a,c,s.source),bytes:d>0?d:s.bytes,p2pBytes:a,cdnBytes:c});try{e.close()}catch{}};e.onmessage=async s=>{const t=s.data;if("MESHSTREAM_SW_SEGMENT_STREAM_CANCEL"!==t?.type){if("MESHSTREAM_SW_SEGMENT_STREAM_PULL"===t?.type&&!i)try{const s=await r.read();if(s.done)return void o();const t=function(e){const s=new ArrayBuffer(e.byteLength);return new Uint8Array(s).set(e),s}(s.value);e.postMessage({type:"MESHSTREAM_SW_SEGMENT_STREAM_CHUNK",body:t},[t])}catch{o(!0,!0)}}else o(!0)},e.postMessage({type:"MESHSTREAM_SW_SEGMENT_STREAM_START",status:s.status,statusText:s.statusText,headers:s.headers,source:s.source,bytes:s.bytes})}(r,e):e.body?r.postMessage({type:"MESHSTREAM_SW_SEGMENT_RESPONSE",status:e.status,statusText:e.statusText,headers:e.headers,source:e.source,bytes:e.bytes??e.body.byteLength,p2pBytes:e.p2pBytes,cdnBytes:e.cdnBytes,body:e.body},[e.body]):r.postMessage({type:"MESHSTREAM_SW_SEGMENT_UNAVAILABLE"})}).catch(()=>{this.bridgeUnavailableResponses+=1,r.postMessage({type:"MESHSTREAM_SW_SEGMENT_UNAVAILABLE"})})},navigator.serviceWorker.addEventListener("message",this.messageListener)}async registerSession(e){const s=e.sessionId,r=this.segmentProviders.has(s);this.segmentProviders.set(s,e.segmentProvider??(async()=>{})),e.originHeadersProvider?this.originHeadersProviders.set(s,e.originHeadersProvider):this.originHeadersProviders.delete(s),r||function(e){t.set(e,(t.get(e)??0)+1)}(s),e.onSegmentStats&&this.statsListeners.set(s,e.onSegmentStats);const o=new MessageChannel;o.port1.onmessage=this.messageListener,o.port1.start?.(),this.closeSessionBridge(s),this.sessionBridgePorts.set(s,o.port1);try{await g(this.registration,{type:"MESHSTREAM_SW_REGISTER_SESSION",protocolVersion:1,sessionId:s,manifestUrl:e.manifestUrl,resourceKind:e.resourceKind,originHeaders:e.originHeaders,p2pTimeoutMs:(n=e.p2pTimeoutMs,Number.isFinite(n)?Math.max(100,Math.floor(n)):5e3)},[o.port2])}catch(e){throw this.closeSessionBridge(s),r||i(s),this.segmentProviders.delete(s),this.originHeadersProviders.delete(s),this.statsListeners.delete(s),e}var n;const a=S(s,"manifest");let c=!1;return{sessionId:s,manifestUrl:a,dispose:async()=>{c||(c=!0,this.removeSession(s),this.statsListeners.delete(s),await g(this.registration,{type:"MESHSTREAM_SW_UNREGISTER_SESSION",protocolVersion:1,sessionId:s}))}}}dispose(){navigator.serviceWorker.removeEventListener("message",this.messageListener);for(const e of this.segmentProviders.keys())i(e);this.segmentProviders.clear(),this.originHeadersProviders.clear(),this.statsListeners.clear();for(const e of this.sessionBridgePorts.values())o(e);this.sessionBridgePorts.clear()}diagnostics(){return{serviceWorkerBridgeRequests:this.bridgeRequests,serviceWorkerBridgeUnavailableResponses:this.bridgeUnavailableResponses}}removeSession(e){this.closeSessionBridge(e),this.segmentProviders.delete(e)&&(this.originHeadersProviders.delete(e),i(e))}closeSessionBridge(e){const s=this.sessionBridgePorts.get(e);s&&(this.sessionBridgePorts.delete(e),o(s))}};function i(e){const s=t.get(e)??0;s<=1?t.delete(e):t.set(e,s-1)}function o(e){try{e.close()}catch{}}function n(e,s,t){return(e??0)>0&&0===(s??0)||"p2p"===t?"p2p":"cdn"}function a(){return"undefined"!=typeof navigator&&"serviceWorker"in navigator&&!("undefined"!=typeof isSecureContext&&!isSecureContext)}async function c(e={}){if(!a())throw new Error("MeshStream Service Worker requires a secure context and navigator.serviceWorker");const s=e.registration??await navigator.serviceWorker.register(e.url??"/meshstream-sw.js",e.scope?{scope:e.scope}:void 0),t=e.readyTimeoutMs??1e4,i=await async function(e){return function(e,s){return new Promise((t,r)=>{const i=setTimeout(()=>r(new Error("Service Worker activation timed out")),s);e.then(e=>{clearTimeout(i),t(e)},e=>{clearTimeout(i),r(e)})})}(navigator.serviceWorker.ready,e)}(t);return await async function(e,s){!navigator.serviceWorker.controller&&s.active&&await new Promise((s,t)=>{const r=setTimeout(()=>{navigator.serviceWorker.removeEventListener("controllerchange",i),t(new Error("MeshStream Service Worker controller claim timed out"))},e),i=()=>{clearTimeout(r),navigator.serviceWorker.removeEventListener("controllerchange",i),s()};navigator.serviceWorker.addEventListener("controllerchange",i,{once:!0}),navigator.serviceWorker.controller&&i()})}(t,i),await E(s,t),new r(s)}async function d(e){const s=!e.serviceWorker,t=e.serviceWorker??await async function(){if(!a()||!navigator.serviceWorker.controller)return;const e=await navigator.serviceWorker.ready;try{return await E(e,750),new r(e)}catch{return}}();if(!t)return;const i=await t.registerSession({sessionId:e.sessionId??(o="sw",`${o}-${globalThis.crypto?.randomUUID?.()??Math.random().toString(36).slice(2)}`),manifestUrl:e.manifestUrl,resourceKind:e.resourceKind,originHeaders:e.originHeaders,originHeadersProvider:e.originHeadersProvider,p2pTimeoutMs:e.p2pTimeoutMs,segmentProvider:e.segmentProvider,onSegmentStats:e.onSegmentStats});var o;if(!s)return i;const n=i.dispose;return{sessionId:i.sessionId,manifestUrl:i.manifestUrl,dispose:async()=>{try{await n()}finally{t.dispose()}}}}function S(e,t){return"undefined"==typeof location?`${s}${encodeURIComponent(e)}/${t}`:new URL(`${s}${encodeURIComponent(e)}/${t}`,location.origin).toString()}async function E(e,s){const t=navigator.serviceWorker.controller??e.active??e.waiting??e.installing;if(!t)throw new Error("MeshStream Service Worker has no active worker");await new Promise((e,r)=>{const i=new MessageChannel,o=()=>{try{i.port1.close()}catch{}},n=setTimeout(()=>{o(),r(new Error("MeshStream Service Worker handshake timed out"))},s);i.port1.onmessage=s=>{clearTimeout(n),o(),"MESHSTREAM_SW_READY"===s.data?.type?e():r(new Error("Invalid MeshStream Service Worker handshake"))},t.postMessage({type:"MESHSTREAM_SW_PING",protocolVersion:1},[i.port2])})}async function g(e,s,t=[]){const r=navigator.serviceWorker.controller??e.active??e.waiting??e.installing;if(!r)throw new Error("MeshStream Service Worker is not active");await new Promise((e,i)=>{const o=new MessageChannel,n=()=>{try{o.port1.close()}catch{}},a=setTimeout(()=>{n(),i(new Error("MeshStream Service Worker session acknowledgement timed out"))},2e3);o.port1.onmessage=s=>{clearTimeout(a),n(),"MESHSTREAM_SW_SESSION_READY"===s.data?.type||"MESHSTREAM_SW_SESSION_REMOVED"===s.data?.type?e():i(new Error("Invalid MeshStream Service Worker session acknowledgement"))},r.postMessage(s,[o.port2,...t])})}export{s as MESHSTREAM_SW_PATH_PREFIX,e as MESHSTREAM_SW_PROTOCOL_VERSION,r as MeshStreamServiceWorkerClient,S as buildMeshStreamServiceWorkerUrl,d as createMeshStreamServiceWorkerSession,a as isMeshStreamServiceWorkerSupported,c as registerMeshStreamServiceWorker};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface WebSessionTokenRequest {
|
|
2
|
+
accessUrl?: string;
|
|
3
|
+
appId: string;
|
|
4
|
+
keyId?: string;
|
|
5
|
+
sessionId: string;
|
|
6
|
+
platform: "web";
|
|
7
|
+
}
|
|
8
|
+
export interface WebSessionTokenManagerOptions {
|
|
9
|
+
fetcher?: typeof fetch;
|
|
10
|
+
now?: () => number;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare const DEFAULT_ACCESS_TIMEOUT_MS = 3000;
|
|
14
|
+
export interface SessionTokenDiagnostics {
|
|
15
|
+
attempts: number;
|
|
16
|
+
successes: number;
|
|
17
|
+
failures: number;
|
|
18
|
+
durationMs: number;
|
|
19
|
+
}
|
|
20
|
+
export declare class WebSessionTokenManager {
|
|
21
|
+
private readonly accessUrl?;
|
|
22
|
+
private readonly appId;
|
|
23
|
+
private readonly keyId?;
|
|
24
|
+
private readonly sessionId;
|
|
25
|
+
private readonly platform;
|
|
26
|
+
private readonly fetcher;
|
|
27
|
+
private readonly now;
|
|
28
|
+
private readonly timeoutMs;
|
|
29
|
+
private token?;
|
|
30
|
+
private refreshAfterMs;
|
|
31
|
+
private expiresAtMs;
|
|
32
|
+
private refreshTimer;
|
|
33
|
+
private inFlight?;
|
|
34
|
+
private activeAbortController?;
|
|
35
|
+
private disposed;
|
|
36
|
+
private readonly authMetrics;
|
|
37
|
+
constructor(request: WebSessionTokenRequest, options?: WebSessionTokenManagerOptions);
|
|
38
|
+
getSessionToken(): Promise<string | undefined>;
|
|
39
|
+
getAuthorizationHeader(): Promise<string | undefined>;
|
|
40
|
+
getTrackerUrl(url: string): Promise<string | undefined>;
|
|
41
|
+
diagnostics(): SessionTokenDiagnostics;
|
|
42
|
+
dispose(): void;
|
|
43
|
+
private refreshToken;
|
|
44
|
+
private fetchSessionToken;
|
|
45
|
+
private scheduleRefresh;
|
|
46
|
+
}
|
|
47
|
+
export declare function createSessionId(prefix: string): string;
|
package/dist/url.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export declare function selectTrackerUrl(fallbackUrl: string | undefined, trackerUrls: string[] | undefined, appId: string | undefined, swarmId: string, keyId?: string, primaryTrackerUrl?: string): string | undefined;
|
|
2
|
+
/**
|
|
3
|
+
* Orders the Config API tracker pool with its authoritative primary first and
|
|
4
|
+
* keeps the remaining nodes as reconnect fallbacks. Identity and local proxy
|
|
5
|
+
* rewriting are applied consistently to every URL in the returned pool.
|
|
6
|
+
*/
|
|
7
|
+
export declare function selectTrackerUrls(fallbackUrl: string | undefined, trackerUrls: string[] | undefined, appId: string | undefined, swarmId: string, keyId?: string, primaryTrackerUrl?: string, scheduling?: TrackerSchedulingHints): string[];
|
|
8
|
+
export interface TrackerSchedulingHints {
|
|
9
|
+
/**
|
|
10
|
+
* A Tracker-only playback-window hint. `auto` asks MeshStream Tracker to
|
|
11
|
+
* derive a rolling live window on every announce, so long-running live
|
|
12
|
+
* sessions remain comparable without reconnecting their WebSocket.
|
|
13
|
+
*/
|
|
14
|
+
liveWindowBucket?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function browserProxyUrl(url: string | undefined, kind: "tracker" | "report"): string | undefined;
|
|
17
|
+
export declare function isPrivateOrLocalHostname(hostname: string): boolean;
|
|
18
|
+
export declare function browserProxyReportUrl(url: string | undefined): string | undefined;
|
|
19
|
+
export declare function deriveReportUrl(trackerUrl?: string): string | undefined;
|
|
20
|
+
export declare function withTrackerIdentity(url: string | undefined, config: {
|
|
21
|
+
appId?: string;
|
|
22
|
+
keyId?: string;
|
|
23
|
+
} & TrackerSchedulingHints): string | undefined;
|
|
24
|
+
export declare function withSessionToken(url: string | undefined, sessionToken: string | undefined): string | undefined;
|
|
25
|
+
export declare function deriveSwarmIdFromUrl(url: string): string;
|
|
26
|
+
export declare function deriveSwarmIdFromContentId(contentId: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Resolves the stable identity sent to bootstrap for tracker affinity.
|
|
29
|
+
* Customer-provided IDs take precedence; otherwise the canonical playback
|
|
30
|
+
* URL is reduced to an opaque, stable URL identity that excludes volatile
|
|
31
|
+
* signing parameters.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveBootstrapContentId(contentId: string | undefined, streamUrl: string): string;
|
|
34
|
+
export declare function deriveReportContentId(contentId: string | undefined, streamUrl: string): string;
|
|
35
|
+
export declare function redactPlaybackUrl(input: string): string;
|
|
36
|
+
export declare function deriveSwarmBucketId(seed: string, bucketCount: number): number;
|
|
37
|
+
export declare function getBucketedSwarmId(swarmId: string, bucketCount: number, bucketId: number): string;
|
|
38
|
+
export declare function deriveBucketedSwarmIdFromUrl(url: string, seed: string, bucketCount?: number, bucketId?: number): string;
|
|
39
|
+
export declare function canonicalizePlaybackUrl(input: string): string;
|