@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.
File without changes
@@ -0,0 +1 @@
1
+ (()=>{const e="/__meshstream__/v1/",t="sessions",s=globalThis,n=new Map,r=new Set(["connection","content-length","host","proxy-connection","range","transfer-encoding","upgrade"]);async function o(e,t,s,n=s.manifestUrl){const r=await d(e,t,n,s);if(!r.ok)return r;const o=function(e,t,s){return e.split(/\r?\n/).map(e=>!e||e.startsWith("#")?e.replace(/URI="([^"]+)"/g,(e,n)=>`URI="${i(n,t,s)}"`):i(e,t,s)).join("\n")}(await r.text(),s,n);await f(s);const a=new Headers(r.headers);return a.delete("content-length"),new Response(o,{status:r.status,statusText:r.statusText,headers:a})}function i(t,s,n){const r=new URL(t,n).toString();let o=[...s.resources.entries()].find(e=>e[1]===r)?.[0];return o||(o=`r${s.resources.size+1}`,s.resources.set(o,r)),`${e}${encodeURIComponent(s.sessionId)}/${o}`}async function a(e,t,s,n){const r=t.pageBridge,o=r?void 0:await S(e,t);if(!r&&!o)return;const i=new MessageChannel,a=e.request,d=new Promise(e=>{let s,n,r=!1,o=!1,a=!1;const d=new Promise(e=>{n=e}),u=()=>{if(!o){o=!0;try{i.port1.close()}catch{}}},p=t=>{r||(r=!0,clearTimeout(l),t||u(),e(t))},l=setTimeout(()=>{p(void 0)},t.p2pTimeoutMs);i.port1.onmessage=e=>{const t=e.data||{};if("MESHSTREAM_SW_SEGMENT_RESPONSE"===t.type&&t.body){const e=new Headers(t.headers||{}),s="cdn"===t.source?"cdn":"p2p";"cdn"===s&&e.set("x-meshstream-source","cdn");const n=Number.isFinite(t.bytes)?Math.max(0,t.bytes):t.body.byteLength;return e.has("content-length")||e.set("content-length",String(n)),p({response:new Response(t.body,{status:t.status||200,statusText:t.statusText||"",headers:e}),completion:Promise.resolve({source:s,bytes:n,status:t.status||200,p2pBytes:c(t.p2pBytes),cdnBytes:c(t.cdnBytes)})}),void u()}if("MESHSTREAM_SW_SEGMENT_STREAM_START"===t.type){const e=c(t.bytes);if(void 0===e||r)return void p(void 0);const n=new Headers(t.headers||{});"cdn"===t.source&&n.set("x-meshstream-source","cdn"),n.has("content-length")||n.set("content-length",String(e));const u=new ReadableStream({pull(e){s=e,a||o||(a=!0,i.port1.postMessage({type:"MESHSTREAM_SW_SEGMENT_STREAM_PULL"}))},cancel(){i.port1.postMessage({type:"MESHSTREAM_SW_SEGMENT_STREAM_CANCEL"})}});return void p({response:new Response(u,{status:t.status||200,statusText:t.statusText||"",headers:n}),completion:d})}if("MESHSTREAM_SW_SEGMENT_STREAM_CHUNK"===t.type&&t.body instanceof ArrayBuffer)return a=!1,void s?.enqueue(new Uint8Array(t.body));"MESHSTREAM_SW_SEGMENT_STREAM_COMPLETE"!==t.type&&"MESHSTREAM_SW_SEGMENT_STREAM_ERROR"!==t.type?r||p(void 0):((e,t=!1)=>{if(r){try{t?s?.error(new Error("MeshStream progressive-media stream failed")):s?.close()}catch{}n?.(e),u()}else p(void 0)})({source:"p2p"===t.source?"p2p":"cdn",bytes:c(t.bytes)??0,status:t.status||200,p2pBytes:c(t.p2pBytes),cdnBytes:c(t.cdnBytes)},"MESHSTREAM_SW_SEGMENT_STREAM_ERROR"===t.type)}});try{const e={type:"MESHSTREAM_SW_SEGMENT_REQUEST",protocolVersion:1,sessionId:t.sessionId,resourceId:s,url:n,headers:Object.fromEntries(a.headers.entries())};r?r.postMessage(e,[i.port2]):o?.postMessage(e,[i.port2])}catch{try{i.port1.close()}catch{}return}return d}function c(e){return"number"==typeof e&&Number.isFinite(e)?Math.max(0,e):void 0}async function d(e,t,n,r){const o=new Headers(t.headers),i=new URL(n.toString(),s.location?.origin||void 0);return i.origin===r.manifestUrl.origin&&(u(o,r.originHeaders),u(o,await async function(e,t,s){const n=t.pageBridge??await S(e,t);if(!n)return{};const r=new MessageChannel;return new Promise(e=>{let o=!1;const i=t=>{if(!o){o=!0,clearTimeout(a);try{r.port1.close()}catch{}var s;e((s=t)&&"object"==typeof s&&Object.values(s).every(e=>"string"==typeof e)?t:{})}},a=setTimeout(()=>i({}),750);r.port1.onmessage=e=>{const t=e.data||{};"MESHSTREAM_SW_ORIGIN_HEADERS_RESPONSE"===t.type?i(t.headers):i({})};try{n.postMessage({type:"MESHSTREAM_SW_ORIGIN_HEADERS_REQUEST",sessionId:t.sessionId,url:s,kind:p(s)},[r.port2])}catch{i({})}})}(e,r,i.toString()))),fetch(i,{method:t.method,headers:o,redirect:"follow",referrer:t.referrer,referrerPolicy:t.referrerPolicy})}function u(e,t){for(const[s,n]of Object.entries(t||{})){const t=s.trim().toLowerCase();if(t&&!r.has(t)&&!t.startsWith("proxy-")&&n.trim())try{e.set(s,n)}catch{}}}function p(e){const t=new URL(e,s.location?.origin||void 0).pathname.toLowerCase();return/\.(m3u8|mpd)$/.test(t)?"manifest":/\.(key|lic)$/.test(t)?"key":/\.(mp4|mp3)$/.test(t)?"direct-file":/\.(ts|m4s|m4a|aac|mp4a|cmfv|cmfa|webm)$/.test(t)?"segment":"unknown"}async function l(e,t,s,n,r,o,i,a,c){try{const d={type:"MESHSTREAM_SW_SEGMENT_RESULT",sessionId:t.sessionId,resourceId:s,url:n,source:r,bytes:o,status:i,...void 0!==a?{p2pBytes:a}:{},...void 0!==c?{cdnBytes:c}:{}};if(t.pageBridge)return void t.pageBridge.postMessage(d);const u=await S(e,t);u?.postMessage(d)}catch{}}async function S(e,t){const n=e.clientId||t.ownerClientId;if(n)try{return await s.clients.get(n)}catch{return}}async function E(e,t="GET"){if("HEAD"===t.toUpperCase())return 0;const s=e.headers.get("content-length");if(s&&Number.isFinite(Number(s)))return Math.max(0,Number(s));try{return(await e.clone().arrayBuffer()).byteLength}catch{return 0}}async function f(e){try{const s=await m();await y(s.transaction(t,"readwrite").objectStore(t).put({sessionId:e.sessionId,manifestUrl:e.manifestUrl.toString(),resourceKind:e.resourceKind,originHeaders:e.originHeaders,p2pTimeoutMs:e.p2pTimeoutMs,ownerClientId:e.ownerClientId,resources:[...e.resources.entries()]}))}catch{}}function m(){const e=s.indexedDB;return e?new Promise((s,n)=>{const r=e.open("meshstream-sw",1);r.onupgradeneeded=()=>{r.result.createObjectStore(t,{keyPath:"sessionId"})},r.onsuccess=()=>s(r.result),r.onerror=()=>n(r.error||new Error("IndexedDB open failed"))}):Promise.reject(new Error("IndexedDB is unavailable"))}function y(e){return new Promise((t,s)=>{e.onsuccess=()=>t(e.result),e.onerror=()=>s(e.error||new Error("IndexedDB request failed"))})}s.addEventListener("install",e=>{e.waitUntil?.(s.skipWaiting())}),s.addEventListener("activate",e=>{e.waitUntil?.(s.clients.claim())}),s.addEventListener("message",e=>{const r=e.data||{};if(1===r.protocolVersion)if("MESHSTREAM_SW_PING"!==r.type){if("MESHSTREAM_SW_REGISTER_SESSION"===r.type&&r.sessionId&&r.manifestUrl){const t={sessionId:r.sessionId,manifestUrl:new URL(r.manifestUrl,s.location?.origin||void 0),resourceKind:"direct-file"===r.resourceKind?"direct-file":"hls",originHeaders:r.originHeaders||{},p2pTimeoutMs:Number.isFinite(r.p2pTimeoutMs)?r.p2pTimeoutMs:5e3,ownerClientId:"string"==typeof e.source?.id?e.source.id:void 0,pageBridge:e.ports?.[1],resources:new Map};n.set(r.sessionId,t);const o=f(t).finally(()=>{e.ports?.[0]?.postMessage({type:"MESHSTREAM_SW_SESSION_READY",sessionId:r.sessionId})});return e.waitUntil?.(o),void e.waitUntil}if("MESHSTREAM_SW_UNREGISTER_SESSION"===r.type&&r.sessionId){n.delete(r.sessionId);const s=async function(e){try{const s=await m();await y(s.transaction(t,"readwrite").objectStore(t).delete(e))}catch{}}(r.sessionId).finally(()=>{e.ports?.[0]?.postMessage({type:"MESHSTREAM_SW_SESSION_REMOVED",sessionId:r.sessionId})});e.waitUntil?.(s),e.waitUntil}}else e.ports?.[0]?.postMessage({type:"MESHSTREAM_SW_READY",protocolVersion:1})}),s.addEventListener("fetch",s=>{s.request&&s.respondWith&&new URL(s.request.url).pathname.startsWith(e)&&s.respondWith(async function(s){const r=s.request,i=function(t){if(!t.startsWith(e))return;const s=t.slice(19).split("/");return s.length<2?void 0:{sessionId:decodeURIComponent(s[0]),resourceId:s.slice(1).join("/")}}(new URL(r.url).pathname);if(!i)return new Response("MeshStream Service Worker request unavailable",{status:400});const c=await async function(e){const s=n.get(e);if(s)return s;const r=await async function(e){try{const s=await m(),n=await y(s.transaction(t,"readonly").objectStore(t).get(e));if(!n?.manifestUrl)return;return{sessionId:e,manifestUrl:new URL(n.manifestUrl),resourceKind:"direct-file"===n.resourceKind?"direct-file":"hls",originHeaders:n.originHeaders||{},p2pTimeoutMs:Number.isFinite(n.p2pTimeoutMs)?n.p2pTimeoutMs:5e3,ownerClientId:"string"==typeof n.ownerClientId?n.ownerClientId:void 0,resources:new Map(Array.isArray(n.resources)?n.resources:[])}}catch{return}}(e);return r?(n.set(e,r),r):void 0}(i.sessionId);if(!c)return new Response("MeshStream Service Worker session unavailable",{status:503});if("manifest"===i.resourceId)return"direct-file"===c.resourceKind?async function(e,t){const s=e.request,n=t.manifestUrl.toString(),r=await a(e,t,"direct",n);if(r){r.response.headers.delete("x-meshstream-source");const s=r.completion.then(s=>l(e,t,"direct",n,s.source,s.bytes,s.status,s.p2pBytes,s.cdnBytes));return e.waitUntil?.(s),r.response}const o=await d(e,s,t.manifestUrl,t),i=l(e,t,"direct",n,"cdn",await E(o,s.method),o.status);return e.waitUntil?.(i),o}(s,c):o(s,r,c);const u=c.resources.get(i.resourceId);if(!u)return new Response("MeshStream resource unavailable",{status:404});if(/\.m3u8(?:$|[?#])/i.test(u))return o(s,r,c,new URL(u));const p=await a(s,c,i.resourceId,u);if(p){p.response.headers.delete("x-meshstream-source");const e=p.completion.then(e=>l(s,c,i.resourceId,u,e.source,e.bytes,e.status,e.p2pBytes,e.cdnBytes));return s.waitUntil?.(e),p.response}const S=await d(s,r,u,c),f=l(s,c,i.resourceId,u,"cdn",await E(S,r.method),S.status);return s.waitUntil?.(f),S}(s))})})();
@@ -0,0 +1,36 @@
1
+ export type OriginHeaders = Record<string, string>;
2
+ /** Media request classes exposed to the origin-header callback. */
3
+ export type OriginRequestKind = "manifest" | "segment" | "key" | "direct-file" | "unknown";
4
+ /**
5
+ * Resolves origin-only request headers immediately before a CDN request.
6
+ * The callback is synchronous by design: media loaders cannot safely pause an
7
+ * in-flight request while an application refreshes credentials. Keep a fresh
8
+ * token in application state and return it here. These headers are never sent
9
+ * over a WebRTC peer connection.
10
+ */
11
+ export type OriginHeadersProvider = (request: {
12
+ url: string;
13
+ kind: OriginRequestKind;
14
+ }) => OriginHeaders | undefined;
15
+ export interface SegmentByteRange {
16
+ start: number;
17
+ end?: number;
18
+ }
19
+ export type OriginHttpRequestSetup = (segmentUrl: string, segmentByteRange: SegmentByteRange | undefined, requestAbortSignal: AbortSignal | undefined, requestByteRange: SegmentByteRange | undefined) => Promise<Request | undefined>;
20
+ export declare function hasOriginHeaders(headers?: OriginHeaders, provider?: OriginHeadersProvider): boolean;
21
+ export declare function createOriginRequestSetup(headers?: OriginHeaders, provider?: OriginHeadersProvider, mediaUrl?: string): OriginHttpRequestSetup | undefined;
22
+ export declare function applyOriginHeadersToXhrSetup(config: {
23
+ xhrSetup?: (xhr: XMLHttpRequest, url: string) => void;
24
+ }, headers?: OriginHeaders, provider?: OriginHeadersProvider): void;
25
+ export declare function applyOriginHeadersToShaka(player: {
26
+ getNetworkingEngine?: () => unknown;
27
+ }, headers?: OriginHeaders, mediaUrl?: string, provider?: OriginHeadersProvider): boolean;
28
+ export interface DashRequest {
29
+ url?: string;
30
+ headers?: Record<string, string>;
31
+ }
32
+ export type DashRequestInterceptor = (request: DashRequest) => Promise<DashRequest>;
33
+ export declare function createDashOriginHeadersInterceptor(headers?: OriginHeaders, mediaUrl?: string, provider?: OriginHeadersProvider): DashRequestInterceptor | undefined;
34
+ /** Resolves static and callback headers with callback values taking precedence. */
35
+ export declare function resolveOriginHeaders(headers: OriginHeaders | undefined, provider: OriginHeadersProvider | undefined, url: string, kind?: OriginRequestKind): OriginHeaders;
36
+ export declare function inferOriginRequestKind(url: string): OriginRequestKind;
@@ -0,0 +1,14 @@
1
+ import type { SdkPlaybackAuthPolicy } from "./config.js";
2
+ export type PlaybackAuthMode = "none" | "signed-url" | string;
3
+ export type PlaybackAuthStatus = "none" | "valid" | "missing-signature" | "expired-url" | "invalid-url";
4
+ export interface PlaybackAuthDiagnostics {
5
+ mode: PlaybackAuthMode;
6
+ status: PlaybackAuthStatus;
7
+ signatureKey?: string;
8
+ expiresKey?: string;
9
+ expiresAtSeconds?: number;
10
+ }
11
+ export interface EvaluatePlaybackAuthOptions {
12
+ nowSeconds?: () => number;
13
+ }
14
+ export declare function evaluatePlaybackAuth(playbackUrl: string, policy: SdkPlaybackAuthPolicy | undefined, options?: EvaluatePlaybackAuthOptions): PlaybackAuthDiagnostics;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Guards asynchronous player startup against a component being disposed or
3
+ * replaced while configuration, token, or player setup is still in flight.
4
+ */
5
+ export declare class MeshStreamPlaybackLifecycle {
6
+ private generation;
7
+ begin(): number;
8
+ cancel(): void;
9
+ isCurrent(generation: number): boolean;
10
+ assertCurrent(generation: number): void;
11
+ }
@@ -0,0 +1,160 @@
1
+ import type { WebP2pCapabilityResult } from "./capabilities.js";
2
+ import type { OriginHeadersProvider } from "./origin-headers.js";
3
+ import type { LoadSdkConfigOptions, SdkConfigRequest, SdkConfigResponse } from "./config.js";
4
+ import { reportWebP2pFallback } from "./fallback.js";
5
+ import type { HlsP2pConfig, P2pEventListener, P2pUnavailableReason } from "./hls.js";
6
+ import { type P2pDiagnostics, type P2pStats } from "./report.js";
7
+ import { type MeshStreamServiceWorkerClient, type MeshStreamServiceWorkerRegistrationOptions, type MeshStreamServiceWorkerSession, type MeshStreamServiceWorkerSessionOptions, type MeshStreamSwSegmentProvider, type MeshStreamSwSegmentStats } from "./service-worker.js";
8
+ import { type MeshStreamServiceWorkerP2pDiagnostics } from "./service-worker-p2p.js";
9
+ import { type MeshStreamQoeSnapshot } from "./qoe.js";
10
+ export type MeshStreamPlaybackMode = "p2p-hls" | "sw-hls-p2p" | "sw-hls-cdn" | "cdn-hls" | "native-hls-cdn" | "sw-direct-p2p" | "sw-direct-cdn" | "cdn-direct" | "unsupported";
11
+ export type MeshStreamTransportPreference = "auto" | "mse" | "service-worker" | "cdn";
12
+ export type MeshStreamP2pFallbackReason = "CAPABILITY_UNSUPPORTED" | "P2P_DISABLED" | "SDK_CONFIG_UNAVAILABLE" | "KEY_P2P_DISABLED" | "QUOTA_EXHAUSTED" | "QUOTA_DISABLED" | "TRACKER_UNAVAILABLE" | "P2P_INITIALIZATION_FAILED";
13
+ export declare function toP2pUnavailableReason(fallbackReason: MeshStreamP2pFallbackReason | undefined): P2pUnavailableReason | undefined;
14
+ export declare function sdkConfigFallbackReason(config: SdkConfigResponse | undefined): MeshStreamP2pFallbackReason;
15
+ export interface MeshStreamServiceWorkerTransportOptions extends Omit<MeshStreamServiceWorkerRegistrationOptions, "registration"> {
16
+ autoRegister?: boolean;
17
+ p2pTimeoutMs?: number;
18
+ segmentProvider?: MeshStreamSwSegmentProvider;
19
+ onSegmentStats?: (stats: MeshStreamSwSegmentStats) => void;
20
+ }
21
+ export interface MeshStreamHlsPlayerOptions {
22
+ url: string;
23
+ video: HTMLMediaElement;
24
+ contentId?: string;
25
+ mimeType?: "m3u8" | "mpd" | "mp4" | "mp3";
26
+ originHeaders?: Record<string, string>;
27
+ /** Resolves dynamic headers for CDN-origin media requests only. */
28
+ originHeadersProvider?: OriginHeadersProvider;
29
+ bootstrapUrl?: string;
30
+ key?: string;
31
+ networkType?: string;
32
+ liveWindowBucket?: string;
33
+ autoplay?: boolean;
34
+ p2pEnabled?: boolean;
35
+ httpDownloadInitialTimeoutMs?: number;
36
+ /**
37
+ * hls.js options for SDK-owned HLS playback. MeshStream always overrides its
38
+ * loader settings afterwards so P2P and CDN fallback remain intact.
39
+ */
40
+ hlsConfig?: Record<string, unknown>;
41
+ iceServers?: RTCIceServer[];
42
+ logLevel?: "none" | "error" | "warn" | "info" | "debug";
43
+ transportPreference?: MeshStreamTransportPreference;
44
+ serviceWorker?: MeshStreamServiceWorkerTransportOptions;
45
+ /**
46
+ * An existing hls.js instance. MeshStream binds P2P to it but never loads a
47
+ * source, attaches media, or destroys the caller-owned instance.
48
+ */
49
+ hlsjsInstance?: HlsLike;
50
+ /**
51
+ * Internal adapter hook for an external HLS engine such as Video.js VHS.
52
+ * The owner keeps playback and ABR control while MeshStream supplies either
53
+ * a virtual Service Worker manifest or the original CDN manifest.
54
+ */
55
+ loadExternalHls?: (url: string) => void | Promise<void>;
56
+ onHlsCreated?: (hls: HlsLike) => void;
57
+ dependencies?: MeshStreamHlsPlayerDependencies;
58
+ }
59
+ export interface MeshStreamHlsStartResult {
60
+ mode: MeshStreamPlaybackMode;
61
+ capability: WebP2pCapabilityResult;
62
+ sdkConfig?: SdkConfigResponse;
63
+ fallbackReason?: MeshStreamP2pFallbackReason;
64
+ }
65
+ export interface MeshStreamHlsPlayerDependencies {
66
+ loadSdkConfig?: (request: SdkConfigRequest, options?: LoadSdkConfigOptions) => Promise<SdkConfigResponse | undefined>;
67
+ detectCapability?: () => WebP2pCapabilityResult;
68
+ createHls?: (config?: Record<string, unknown>) => HlsLike | Promise<HlsLike>;
69
+ createP2pClient?: (config: HlsP2pConfig) => HlsP2pClientLike | Promise<HlsP2pClientLike>;
70
+ createServiceWorkerSession?: (options: MeshStreamServiceWorkerSessionOptions) => Promise<MeshStreamServiceWorkerSession | undefined>;
71
+ registerServiceWorker?: (options?: MeshStreamServiceWorkerRegistrationOptions) => Promise<MeshStreamServiceWorkerClient>;
72
+ isServiceWorkerSupported?: () => boolean;
73
+ reportFallback?: typeof reportWebP2pFallback;
74
+ hlsIsSupported?: () => boolean | Promise<boolean>;
75
+ }
76
+ export interface HlsP2pClientLike {
77
+ swarmId: string;
78
+ addEventListener(listener: P2pEventListener): void;
79
+ removeEventListener(listener: P2pEventListener): void;
80
+ getHlsConfig(): Record<string, unknown>;
81
+ statistics(): P2pStats;
82
+ diagnostics(): Partial<P2pDiagnostics>;
83
+ setP2pEnabled(enabled: boolean): void;
84
+ dispose(): void;
85
+ }
86
+ export interface HlsLike {
87
+ config: Record<string, unknown>;
88
+ loadSource(url: string): void;
89
+ attachMedia(video: HTMLMediaElement): void;
90
+ on(event: string, listener: (...args: unknown[]) => void): void;
91
+ destroy(): void;
92
+ }
93
+ export declare class MeshStreamHlsPlayer {
94
+ readonly key: string;
95
+ readonly url: string;
96
+ readonly video: HTMLMediaElement;
97
+ private readonly options;
98
+ private readonly ownsHlsInstance;
99
+ private hls?;
100
+ private p2p?;
101
+ private serviceWorkerClient?;
102
+ private serviceWorkerSession?;
103
+ private serviceWorkerP2pEngine?;
104
+ private serviceWorkerStats;
105
+ private serviceWorkerReportUrl?;
106
+ private serviceWorkerReportTimer?;
107
+ private serviceWorkerReportInFlight;
108
+ private serviceWorkerReportTask?;
109
+ private serviceWorkerTeardownTask?;
110
+ private serviceWorkerHasReportedContext;
111
+ private serviceWorkerTrackerUrl?;
112
+ private serviceWorkerAppId?;
113
+ private serviceWorkerKeyId?;
114
+ private serviceWorkerSwarmId?;
115
+ private serviceWorkerUploadSlots;
116
+ private serviceWorkerDownloadSlots;
117
+ private readonly serviceWorkerPagehideHandler;
118
+ private sessionId;
119
+ private sessionTokenManager?;
120
+ private mode;
121
+ private p2pEnabledOverride?;
122
+ private readonly eventListeners;
123
+ private playbackState;
124
+ private readonly lifecycle;
125
+ private startPromise?;
126
+ private startResult?;
127
+ constructor(options: MeshStreamHlsPlayerOptions);
128
+ get p2pClient(): HlsP2pClientLike | undefined;
129
+ get playbackMode(): MeshStreamPlaybackMode;
130
+ start(): Promise<MeshStreamHlsStartResult>;
131
+ private startInternal;
132
+ statistics(): P2pStats;
133
+ diagnostics(): Partial<P2pDiagnostics> & Partial<MeshStreamServiceWorkerP2pDiagnostics>;
134
+ qoe(): MeshStreamQoeSnapshot;
135
+ setP2pEnabled(enabled: boolean): void;
136
+ addEventListener(listener: P2pEventListener): () => void;
137
+ dispose(): void;
138
+ private teardown;
139
+ private startServiceWorkerReporting;
140
+ private bindEventListeners;
141
+ private bindServiceWorkerEventListeners;
142
+ private completeFallbackStart;
143
+ private removeEventListener;
144
+ private setPlaybackState;
145
+ private resolveQoePlaybackState;
146
+ private stopServiceWorkerReporting;
147
+ private flushServiceWorkerReport;
148
+ private reportServiceWorkerStats;
149
+ private startDirectFilePlayback;
150
+ private startDirectFileCdnFallback;
151
+ private recordServiceWorkerSegmentStats;
152
+ private startServiceWorkerFallback;
153
+ private createServiceWorkerP2pEngine;
154
+ private resolveServiceWorkerSwarmId;
155
+ private startCdnFallback;
156
+ private attachHls;
157
+ private autoplay;
158
+ private canPlayNativeHls;
159
+ private effectiveP2pEnabled;
160
+ }
@@ -0,0 +1,11 @@
1
+ /** Browser capabilities exposed to customer code before starting playback. */
2
+ export interface MeshStreamCapability {
3
+ mseSupported: boolean;
4
+ webrtcSupported: boolean;
5
+ serviceWorkerSupported: boolean;
6
+ }
7
+ /**
8
+ * Returns the browser capabilities that affect MeshStream Web transport choice.
9
+ * This is informational only; attach().start() still owns the final fallback decision.
10
+ */
11
+ export declare function getCapability(): MeshStreamCapability;
package/dist/qoe.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { P2pDiagnostics, P2pStats } from "./report.js";
2
+ /** Stable customer-facing lifecycle for a single playback session. */
3
+ export type MeshStreamPlaybackState = "idle" | "initializing" | "connecting" | "p2p-ready" | "cdn-fallback" | "disposed";
4
+ /**
5
+ * Compact playback-health snapshot. It intentionally excludes Tracker, ICE,
6
+ * and peer identifiers so it is safe to send to application telemetry.
7
+ */
8
+ export interface MeshStreamQoeSnapshot {
9
+ state: MeshStreamPlaybackState;
10
+ downloadedBytes: number;
11
+ p2pDownloadedBytes: number;
12
+ cdnDownloadedBytes: number;
13
+ /** `0..1`; zero when no media bytes have been downloaded yet. */
14
+ p2pDownloadRatio: number;
15
+ connectedPeers: number;
16
+ firstPeerConnectMs?: number;
17
+ fallbackCount: number;
18
+ coldStartCdnBytes?: number;
19
+ /** Present for live HLS after the first parsed media playlist. */
20
+ liveLatencyMs?: number;
21
+ liveTargetLatencyMs?: number;
22
+ liveMaxLatencyMs?: number;
23
+ liveLatencyOverMax?: boolean;
24
+ }
25
+ export declare function buildMeshStreamQoe(state: MeshStreamPlaybackState, stats: P2pStats, diagnostics: Partial<P2pDiagnostics>): MeshStreamQoeSnapshot;
@@ -0,0 +1,140 @@
1
+ import type { WebP2pCapabilityResult } from "./capabilities.js";
2
+ import type { PlaybackAuthDiagnostics } from "./playback-auth.js";
3
+ export interface P2pStats {
4
+ httpDownloaded: number;
5
+ p2pDownloaded: number;
6
+ p2pUploaded: number;
7
+ httpSegmentDownloadSuccesses: number;
8
+ httpSegmentDownloadFailures: number;
9
+ p2pSegmentDownloadSuccesses: number;
10
+ p2pSegmentDownloadFailures: number;
11
+ p2pSegmentUploadSuccesses: number;
12
+ p2pSegmentUploadFailures: number;
13
+ connectedPeers: number;
14
+ peerConnectErrors: number;
15
+ trackerErrors: number;
16
+ p2pAttempts?: number;
17
+ httpAttempts?: number;
18
+ p2pErrors?: number;
19
+ httpErrors?: number;
20
+ lastLoadedBytes?: number;
21
+ }
22
+ /**
23
+ * Returns the stable customer-facing statistics shape. Playback on CDN has
24
+ * valid zero P2P counters, so dashboards do not need adapter-specific null
25
+ * handling while a session is starting or falling back.
26
+ */
27
+ export declare function normalizeP2pStats(stats: Partial<P2pStats> | undefined): P2pStats;
28
+ export interface P2pDiagnostics {
29
+ firstPeerConnectMs?: number;
30
+ /** Bytes received before the first usable P2P peer is connected. */
31
+ coldStart?: {
32
+ p2pDownloaded: number;
33
+ httpDownloaded: number;
34
+ };
35
+ /**
36
+ * HLS live-edge observations accumulated by the SDK. The three duration
37
+ * fields are sums in milliseconds; the `last*` values are a current local
38
+ * snapshot for `qoe()` and are not used for aggregation.
39
+ */
40
+ liveLatency?: {
41
+ samples: number;
42
+ latencyDurationMs: number;
43
+ targetLatencyDurationMs: number;
44
+ maxLatencyDurationMs: number;
45
+ overMaxLatencySamples: number;
46
+ lastLatencyMs?: number;
47
+ lastTargetLatencyMs?: number;
48
+ lastMaxLatencyMs?: number;
49
+ };
50
+ /** Live-only deterministic cohort used to retain an extra uploadable fragment. */
51
+ liveSeed?: {
52
+ cohort: boolean;
53
+ active: boolean;
54
+ };
55
+ fallbackCount: number;
56
+ lastTrackerError?: string;
57
+ lastPeerError?: string;
58
+ lastErrorAtMs?: number;
59
+ playbackAuth?: PlaybackAuthDiagnostics;
60
+ ice?: {
61
+ attempts: number;
62
+ successes: number;
63
+ failures: number;
64
+ durationMs: number;
65
+ };
66
+ auth?: P2pConnectionMetric;
67
+ websocket?: P2pConnectionMetric;
68
+ }
69
+ /**
70
+ * Captures diagnostics before asynchronous authorization or transport work.
71
+ * A client may be disposed while its final report is waiting for a token.
72
+ */
73
+ export declare function snapshotP2pDiagnostics(diagnostics: P2pDiagnostics, auth?: P2pConnectionMetric): P2pDiagnostics;
74
+ /** Cumulative request/connection metric collected for this playback session. */
75
+ export interface P2pConnectionMetric {
76
+ attempts: number;
77
+ successes: number;
78
+ failures: number;
79
+ /** Present only when the integration can measure each attempt boundary. */
80
+ durationMs: number;
81
+ }
82
+ export interface P2pReportLoad {
83
+ uploadSlots: number;
84
+ activeUploadPeers: number;
85
+ downloadSlots: number;
86
+ activeDownloadPeers: number;
87
+ }
88
+ export interface P2pReportInput {
89
+ platform: string;
90
+ /** Explicit playback class supplied by the integrator; never inferred from a URL. */
91
+ playbackMode: "live" | "vod";
92
+ /** Actual client delivery runtime, independent from the requested preference. */
93
+ runtimeMode?: "mse" | "service-worker" | "cdn" | "unknown";
94
+ protocol?: "hls" | "dash" | "mp4" | "mp3";
95
+ playerType?: "hlsjs" | "shaka" | "dashjs" | "native" | string;
96
+ appId?: string;
97
+ keyId?: string;
98
+ sdkVersion: string;
99
+ sessionId: string;
100
+ contentId?: string;
101
+ swarmId: string;
102
+ playbackUrl: string;
103
+ stats: P2pStats;
104
+ load: P2pReportLoad;
105
+ diagnostics: P2pDiagnostics;
106
+ capability?: WebP2pCapabilityResult;
107
+ }
108
+ /** A usage snapshot, not a transport keepalive or peer heartbeat. */
109
+ export interface P2pReportPayload {
110
+ reportType: "stats";
111
+ reportId: string;
112
+ reportSeq: number;
113
+ /** Client wall-clock timestamp fixed for this cumulative report snapshot. */
114
+ reportedAtMs: number;
115
+ platform: string;
116
+ playbackMode: "live" | "vod";
117
+ runtimeMode?: P2pReportInput["runtimeMode"];
118
+ appId?: string;
119
+ keyId?: string;
120
+ sessionId: string;
121
+ stats: P2pStats;
122
+ load: Partial<P2pReportLoad>;
123
+ diagnostics: P2pDiagnostics;
124
+ protocol?: P2pReportInput["protocol"];
125
+ playerType?: string;
126
+ sdkVersion?: string;
127
+ contentId?: string;
128
+ swarmId?: string;
129
+ playbackUrl?: string;
130
+ capability?: WebP2pCapabilityResult;
131
+ }
132
+ export declare const DEFAULT_REPORT_INTERVAL_MS = 30000;
133
+ export declare const MIN_REPORT_INTERVAL_MS = 20000;
134
+ export declare function normalizeReportIntervalMs(value: number | undefined): number;
135
+ export interface PostP2pReportOptions {
136
+ fetcher?: typeof fetch;
137
+ authorizationHeader?: string;
138
+ }
139
+ export declare function buildP2pReportPayload(input: P2pReportInput, includeContext?: boolean): P2pReportPayload;
140
+ export declare function postP2pReport(reportUrl: string | undefined, payload: P2pReportPayload, options?: PostP2pReportOptions): Promise<Response | undefined>;
@@ -0,0 +1,163 @@
1
+ import { type WebP2pCapabilityResult } from "./capabilities.js";
2
+ import { type LoadSdkConfigOptions, type SdkConfigRequest, type SdkConfigResponse } from "./config.js";
3
+ import { type P2pEventListener } from "./hls.js";
4
+ import { type OriginHeaders, type OriginHeadersProvider } from "./origin-headers.js";
5
+ import { type MeshStreamP2pFallbackReason } from "./player.js";
6
+ import { type P2pDiagnostics, type P2pStats } from "./report.js";
7
+ import { type MeshStreamQoeSnapshot } from "./qoe.js";
8
+ import { WebSessionTokenManager } from "./session-token.js";
9
+ import { reportWebP2pFallback } from "./fallback.js";
10
+ export type MeshStreamRxPlayerPlaybackMode = "p2p-rxplayer" | "cdn-rxplayer";
11
+ export interface RxPlayerSegmentLoaderContext {
12
+ url?: string;
13
+ timeout?: number;
14
+ isInit?: boolean;
15
+ byteRanges?: Array<[number, number]>;
16
+ trackType: "video" | "audio" | "text";
17
+ }
18
+ export interface RxPlayerSegmentLoaderCallbacks {
19
+ resolve(result: {
20
+ data: ArrayBuffer | Uint8Array;
21
+ sendingTime?: number;
22
+ receivingTime?: number;
23
+ size?: number;
24
+ duration?: number;
25
+ }): void;
26
+ reject(error?: unknown): void;
27
+ /** Use RxPlayer's built-in CDN loader for this request. */
28
+ fallback(): void;
29
+ progress(info: {
30
+ duration: number;
31
+ size: number;
32
+ totalSize?: number;
33
+ }): void;
34
+ }
35
+ export type RxPlayerSegmentLoader = (context: RxPlayerSegmentLoaderContext, callbacks: RxPlayerSegmentLoaderCallbacks) => (() => void) | void;
36
+ export interface RxPlayerManifestLoaderContext {
37
+ url?: string;
38
+ timeout?: number;
39
+ }
40
+ export interface RxPlayerManifestLoaderCallbacks {
41
+ resolve(result: {
42
+ data: ArrayBuffer | string;
43
+ url?: string;
44
+ sendingTime?: number;
45
+ receivingTime?: number;
46
+ size?: number;
47
+ duration?: number;
48
+ }): void;
49
+ reject(error?: Error): void;
50
+ /** Use RxPlayer's built-in CDN loader for this request. */
51
+ fallback(): void;
52
+ }
53
+ export type RxPlayerManifestLoader = (context: RxPlayerManifestLoaderContext, callbacks: RxPlayerManifestLoaderCallbacks) => (() => void) | void;
54
+ export interface RxPlayerLoadVideoOptions {
55
+ transport: "dash";
56
+ url: string;
57
+ autoPlay?: boolean;
58
+ /** Low-latency chunk streaming bypasses RxPlayer's custom segment loader. */
59
+ lowLatencyMode?: false;
60
+ manifestLoader?: RxPlayerManifestLoader;
61
+ segmentLoader?: RxPlayerSegmentLoader;
62
+ }
63
+ /** Structural subset of RxPlayer's public player API. No RxPlayer runtime dependency is imposed on the SDK. */
64
+ export interface RxPlayerLike {
65
+ getVideoElement?: () => HTMLMediaElement | null;
66
+ loadVideo: (options: RxPlayerLoadVideoOptions) => void;
67
+ stop?: () => void;
68
+ }
69
+ export interface MeshStreamRxPlayerOptions {
70
+ key: string;
71
+ url: string;
72
+ player: RxPlayerLike;
73
+ contentId?: string;
74
+ originHeaders?: OriginHeaders;
75
+ /** Resolves dynamic headers immediately before a same-origin CDN request. */
76
+ originHeadersProvider?: OriginHeadersProvider;
77
+ bootstrapUrl?: string;
78
+ autoplay?: boolean;
79
+ p2pEnabled?: boolean;
80
+ iceServers?: RTCIceServer[];
81
+ dependencies?: MeshStreamRxPlayerDependencies;
82
+ }
83
+ export interface MeshStreamRxPlayerStartResult {
84
+ mode: MeshStreamRxPlayerPlaybackMode;
85
+ capability: WebP2pCapabilityResult;
86
+ sdkConfig?: SdkConfigResponse;
87
+ fallbackReason?: MeshStreamP2pFallbackReason;
88
+ }
89
+ export interface MeshStreamRxPlayerDependencies {
90
+ loadSdkConfig?: (request: SdkConfigRequest, options?: LoadSdkConfigOptions) => Promise<SdkConfigResponse | undefined>;
91
+ detectCapability?: () => WebP2pCapabilityResult;
92
+ createP2pClient?: (config: RxP2pClientConfig) => RxP2pClientLike | Promise<RxP2pClientLike>;
93
+ reportFallback?: typeof reportWebP2pFallback;
94
+ }
95
+ export interface RxP2pClientLike {
96
+ swarmId: string;
97
+ createSegmentLoader(originFallbackLoader?: RxPlayerSegmentLoader): RxPlayerSegmentLoader;
98
+ addEventListener(listener: P2pEventListener): void;
99
+ removeEventListener(listener: P2pEventListener): void;
100
+ statistics(): Partial<P2pStats>;
101
+ diagnostics(): Partial<P2pDiagnostics>;
102
+ /** Internal manifest callback. Customer integrations do not call this. */
103
+ setLiveMode?(isLive: boolean): void;
104
+ setP2pEnabled(enabled: boolean): void;
105
+ dispose(): void;
106
+ }
107
+ export interface RxP2pClientConfig {
108
+ streamUrl: string;
109
+ contentId?: string;
110
+ sdkConfig: Pick<SdkConfigResponse, "applicationId" | "keyId" | "reportUrl" | "trackerUrls" | "primaryTrackerUrl" | "iceServers" | "playbackAuth" | "defaults">;
111
+ appId?: string;
112
+ sessionId: string;
113
+ trackerUrls: string[];
114
+ iceServers?: RTCIceServer[];
115
+ p2pEnabled?: boolean;
116
+ originHeaders?: OriginHeaders;
117
+ originHeadersProvider?: OriginHeadersProvider;
118
+ capability?: WebP2pCapabilityResult;
119
+ sessionTokenManager?: WebSessionTokenManager;
120
+ }
121
+ /**
122
+ * Attach MeshStream P2P loading to an existing RxPlayer instance.
123
+ *
124
+ * RxPlayer provides a real segment-loader interception point for DASH. We use
125
+ * that point instead of running a second player on the same video element.
126
+ */
127
+ export declare function attachToRxPlayer(options: MeshStreamRxPlayerOptions): MeshStreamRxPlayer;
128
+ export declare class MeshStreamRxPlayer {
129
+ readonly key: string;
130
+ readonly url: string;
131
+ readonly player: RxPlayerLike;
132
+ private readonly options;
133
+ private p2p?;
134
+ private sessionTokenManager?;
135
+ private mode;
136
+ private p2pEnabledOverride?;
137
+ private readonly eventListeners;
138
+ private playbackState;
139
+ private readonly lifecycle;
140
+ private startPromise?;
141
+ private startResult?;
142
+ constructor(options: MeshStreamRxPlayerOptions);
143
+ get p2pClient(): RxP2pClientLike | undefined;
144
+ get playbackMode(): MeshStreamRxPlayerPlaybackMode;
145
+ start(): Promise<MeshStreamRxPlayerStartResult>;
146
+ private startInternal;
147
+ statistics(): P2pStats;
148
+ diagnostics(): Partial<P2pDiagnostics>;
149
+ qoe(): MeshStreamQoeSnapshot;
150
+ setP2pEnabled(enabled: boolean): void;
151
+ addEventListener(listener: P2pEventListener): () => void;
152
+ dispose(): void;
153
+ private teardown;
154
+ private startCdn;
155
+ private effectiveP2pEnabled;
156
+ private createOriginManifestLoader;
157
+ private createOriginSegmentLoader;
158
+ private completeFallbackStart;
159
+ private bindEventListeners;
160
+ private removeEventListener;
161
+ private setPlaybackState;
162
+ private resolveQoePlaybackState;
163
+ }
@@ -0,0 +1,31 @@
1
+ import type { CommonCoreConfig, StreamConfig, StreamType } from "p2p-media-loader-core";
2
+ type StorageUsage = {
3
+ totalCapacity: number;
4
+ usedCapacity: number;
5
+ };
6
+ export declare class MeshStreamSegmentStorage {
7
+ private limitBytes;
8
+ private usedBytes;
9
+ private mainLiveRetentionSeconds;
10
+ private secondaryLiveRetentionSeconds;
11
+ private playbackPosition;
12
+ private readonly cache;
13
+ private segmentChangeCallback;
14
+ initialize(coreConfig: Pick<CommonCoreConfig, "segmentMemoryStorageLimit">, mainStreamConfig: Pick<StreamConfig, "highDemandTimeWindow">, secondaryStreamConfig: Pick<StreamConfig, "highDemandTimeWindow">): Promise<void>;
15
+ onPlaybackUpdated(position: number, _rate: number): void;
16
+ onSegmentRequested(_swarmId: string, _streamId: string, _segmentId: number, _startTime: number, _endTime: number, _streamType: StreamType, _isLiveStream: boolean): void;
17
+ storeSegment(_swarmId: string, streamId: string, segmentId: number, data: ArrayBuffer, _startTime: number, endTime: number, streamType: StreamType, isLiveStream: boolean): Promise<void>;
18
+ getSegmentData(_swarmId: string, streamId: string, segmentId: number): Promise<ArrayBuffer | undefined>;
19
+ getUsage(): StorageUsage;
20
+ hasSegment(_swarmId: string, streamId: string, segmentId: number): boolean;
21
+ getStoredSegmentIds(_swarmId: string, streamId: string): number[];
22
+ setSegmentChangeCallback(callback: ((streamId: string) => void) | undefined): void;
23
+ destroy(): void;
24
+ private trimToLimit;
25
+ private trimExpiredLiveSegments;
26
+ private liveRetentionSeconds;
27
+ private notifyChanged;
28
+ private publishDebugSnapshot;
29
+ }
30
+ export declare function createMeshStreamSegmentStorage(_isLive?: boolean): MeshStreamSegmentStorage;
31
+ export {};