@meshstream/p2p-web 1.0.0 → 1.0.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 CHANGED
@@ -1,501 +1,501 @@
1
- # MeshStream P2P Web SDK
2
-
3
- SDK-owned console diagnostics respect the client `logLevel` (default `none`), with redaction and
4
- repeated-error limiting. Web does not write local log files. See the [logging guide](../../docs/sdk-logging.md).
5
-
6
- Browser HLS and DASH P2P delivery SDK for MeshStream P2P-CDN. The SDK wraps
7
- `hls.js`, Shaka Player, and `p2p-media-loader`, fetches runtime configuration
8
- from Config API, joins a WebRTC tracker swarm, reports playback/P2P statistics,
9
- and keeps CDN playback as the fallback path.
10
-
11
- ## Install
12
-
13
- ```bash
14
- # SDK-managed player: MeshStream bundles its HLS, DASH, and P2P runtimes.
15
- npm install @meshstream/p2p-web
16
- ```
17
-
18
- For an application-managed `hls.js` instance, install the player in the
19
- application as well. MeshStream will bind P2P to that instance without taking
20
- over its configuration or lifecycle:
21
-
22
- ```bash
23
- npm install @meshstream/p2p-web hls.js
24
- ```
25
-
26
- In this monorepo the SDK is consumed as a workspace package:
27
-
28
- ```bash
29
- npm run build:web-sdk
30
- npm run build -w @p2pcdn/web-demo
31
- ```
32
-
33
- ## Quick Start
34
-
35
- ```ts
36
- import { attach } from "@meshstream/p2p-web";
37
-
38
- const player = attach({
39
- key: "your_sdk_key",
40
- url: "https://cdn.example.com/live/index.m3u8", // or .mpd / .mp4 / .mp3
41
- video: document.querySelector("video"),
42
- });
43
-
44
- await player.start();
45
-
46
- window.addEventListener("pagehide", () => player.dispose(), { once: true });
47
- ```
48
-
49
- The historic `video` option accepts any `HTMLMediaElement`; pass an `<audio>`
50
- element for progressive MP3 playback.
51
-
52
- The SDK fetches SaaS bootstrap configuration, selects trackers/reporting
53
- endpoints, detects browser capability, starts WebRTC P2P when possible, and
54
- keeps CDN playback as the automatic fallback path.
55
-
56
- When MeshStream creates hls.js, use `hlsConfig` to retain normal player tuning
57
- such as `maxBufferLength`, ABR limits, or low-latency settings:
58
-
59
- ```ts
60
- const player = attach({
61
- key,
62
- url,
63
- video,
64
- hlsConfig: { maxBufferLength: 45, lowLatencyMode: true },
65
- });
66
- ```
67
-
68
- MeshStream always reapplies its P2P loader and safe origin request settings
69
- after the instance is created. For `attachToHls`, configure the caller-owned
70
- hls.js instance directly instead.
71
-
72
- To display compatibility information before playback, call `getCapability()`.
73
- It returns only `mseSupported`, `webrtcSupported`, and `serviceWorkerSupported`;
74
- it does not override the SDK's playback or fallback decision.
75
-
76
- The web transport selects one content-affine Tracker for a playback session.
77
- Tracker nodes keep independent in-memory peer maps; switching or retrying a
78
- Tracker does not copy peers between nodes. If signaling becomes unavailable,
79
- the player continues through CDN rather than registering the same session with
80
- every shard. Configure tested hot-content buckets and same-region fallback
81
- nodes when a stream needs faster recovery.
82
-
83
- `player.statistics()` always returns the same complete counter shape. During
84
- startup and CDN fallback, P2P counters are `0` instead of being omitted, so
85
- application dashboards can use one schema for HLS, DASH, MP4/MP3, and RxPlayer.
86
-
87
- ## Optional Service Worker Transport
88
-
89
- Service Worker support is an optional transport layer. MSE P2P remains the
90
- default and is preferred whenever it is available. To deploy the SW runtime,
91
- copy the published `dist/meshstream-sw.js` file to a same-origin HTTPS path
92
- such as `/meshstream-sw.js`, then opt in:
93
-
94
- ```ts
95
- import { attach } from "@meshstream/p2p-web";
96
-
97
- const player = attach({
98
- key,
99
- url,
100
- video,
101
- transportPreference: "auto",
102
- serviceWorker: {
103
- url: "/meshstream-sw.js",
104
- scope: "/",
105
- autoRegister: true,
106
- p2pTimeoutMs: 5000,
107
- onSegmentStats: ({ source, bytes, url }) => {
108
- console.debug("segment", source, bytes, url);
109
- },
110
- },
111
- });
112
- await player.start();
113
- ```
114
-
115
- `auto` uses MSE P2P first. If MSE is unavailable and the page is controlled by
116
- the registered SW, playback uses a virtual manifest URL under
117
- `/__meshstream__/v1/`; failed or unavailable page-side P2P requests fall back
118
- to the original CDN URL. `transportPreference: "service-worker"` is available
119
- for explicit testing. When bootstrap returns a tracker configuration, the SDK
120
- also starts a page-side `p2p-media-loader-core` engine that works without MSE,
121
- indexes HLS master/media playlists, and answers SW segment requests from the
122
- WebRTC swarm with CDN fallback. A custom `segmentProvider` can still be passed
123
- to replace that engine for application-specific routing.
124
-
125
- For manual registration, import `registerMeshStreamServiceWorker()` and
126
- `createMeshStreamServiceWorkerSession()` from `@meshstream/p2p-web/service-worker`.
127
- Registration requires HTTPS (localhost is allowed by browsers), and the SW script
128
- must be same-origin with the page. Session metadata and rewritten resource mappings
129
- are kept in IndexedDB so a browser-restarted SW can recover a registered session;
130
- unregistering the player removes that metadata. `onSegmentStats` reports whether
131
- each media request was served by the page provider (`p2p`) or CDN (`cdn`).
132
-
133
- For a local browser smoke check after building, run
134
- `node sdks/web/test/sw-browser-fixture.mjs` and open the printed URL. The page
135
- reports the SW controller state, rewritten segment count, response bodies, and
136
- per-segment source statistics.
137
-
138
- For the real two-page WebRTC check, run the automated smoke from the repository
139
- root:
140
-
141
- ```powershell
142
- npm run test:web-sw-real
143
- ```
144
-
145
- It builds the SDK and Tracker, opens seed/follower Chromium pages, and fails
146
- unless the follower receives a non-empty segment through P2P. For manual
147
- inspection, build the SDK and start
148
- `node sdks/web/test/sw-real-p2p-browser-fixture.mjs`, then open the seed URL
149
- first (`?role=seed`) and the follower URL (`?role=follower`).
150
-
151
- ### Staging rollout checklist
152
-
153
- 1. Build the browser package with `npm run build:web-sdk:release` and publish the
154
- generated npm tarball. Copy `dist/meshstream-sw.js` from the package to a stable
155
- same-origin HTTPS URL such as `/meshstream-sw.js`; do not serve it from a CDN
156
- origin or a path outside the intended registration scope.
157
- 2. Deploy the page with `transportPreference: "auto"` and the SW `url` configured.
158
- Confirm the normal browser path reports `p2p-hls`; test Safari/iOS or a forced
159
- `transportPreference: "service-worker"` path and confirm `sw-hls-cdn`.
160
- 3. Check the browser Network panel for the virtual manifest under
161
- `/__meshstream__/v1/` and verify segment requests return either `p2p` or `cdn`
162
- in the SDK `onSegmentStats` callback.
163
- 4. Verify the configured report endpoint receives `reportType: "stats"` with
164
- `p2pDownloaded`, `httpDownloaded`, `connectedPeers`, and tracker diagnostics.
165
- The SDK sends an initial report, periodic reports, and a final `pagehide`
166
- report; report failures must not interrupt playback.
167
- 5. Roll back by removing the `serviceWorker` option or setting
168
- `transportPreference: "mse"`; the SDK then keeps the existing MSE/CDN path.
169
-
170
- ## Advanced Player Adapters
171
-
172
- Use `@meshstream/p2p-web/adapters` for an existing player instance. Controlled
173
- test environments that need to construct `MeshStreamHlsPlayer` directly can
174
- import it from `@meshstream/p2p-web/advanced`; standard integrations should
175
- continue to use root-level `attach`.
176
-
177
- For a plain video element:
178
-
179
- ```ts
180
- import { attach } from "@meshstream/p2p-web";
181
-
182
- const player = attach({ key, url, video });
183
- await player.start();
184
- ```
185
-
186
- ### Choosing an HLS integration
187
-
188
- Both HLS modes use the same P2P scheduling, authorization, and telemetry.
189
- Use `attach` when the SDK should create and manage hls.js. Use `attachToHls`
190
- when the application already owns an hls.js instance and must retain its
191
- buffering, ABR, event, or plugin configuration. In the latter mode, the
192
- application remains responsible for `loadSource`, `attachMedia`, and
193
- `destroy`; call `session.start()` before `loadSource`. Do not use both modes
194
- for the same media element or playback URL.
195
-
196
- For an existing hls.js instance, bind MeshStream before the application starts
197
- media loading. This is a native HLS P2P integration: the SDK keeps the caller's
198
- hls.js configuration and P2P-enables that same instance. It never calls
199
- `loadSource`, `attachMedia`, or `destroy` on the caller-owned player.
200
-
201
- ```ts
202
- import Hls from "hls.js";
203
- import { attachToHls } from "@meshstream/p2p-web";
204
-
205
- const hls = new Hls({ maxBufferLength: 30 });
206
- const session = attachToHls({ key, url, video, player: hls });
207
-
208
- await session.start(); // Bind P2P before hls.js makes media requests.
209
- hls.loadSource(url);
210
- hls.attachMedia(video);
211
-
212
- window.addEventListener("pagehide", () => {
213
- session.dispose(); // Does not destroy hls.
214
- hls.destroy(); // The application owns this lifecycle.
215
- }, { once: true });
216
- ```
217
-
218
- For rotating signed URLs, pass a stable business identity so viewers join the
219
- same swarm. `mimeType` is useful when the media URL has no `.m3u8`, `.mpd`, `.mp4`, or `.mp3`
220
- suffix. `originHeaders` are sent only to same-origin manifest, segment, and key requests on the
221
- configured media origin. SDK-controlled headers such as `Range` and hop-by-hop headers are ignored;
222
- the browser still applies its own forbidden-header and CORS rules. For Shaka and dash.js
223
- adapter objects, the player must expose its request interception API; otherwise
224
- the SDK fails explicitly instead of starting without the requested headers.
225
-
226
- ```ts
227
- const player = attach({
228
- key,
229
- url: signedManifestUrl,
230
- video,
231
- contentId: "live-channel-123",
232
- mimeType: "m3u8",
233
- originHeaders: { Authorization: "Bearer ...", "X-Playback-Session": "..." },
234
- });
235
- await player.start();
236
- ```
237
-
238
- For tokens that rotate during a long playback session, provide the current
239
- value with a synchronous callback. The callback runs immediately before an
240
- eligible CDN-origin request; it must not refresh a token or return a Promise.
241
- Refresh credentials in application state first. Dynamic values override static
242
- headers with the same name and, like static values, are never sent to peers.
243
-
244
- ```ts
245
- let playbackToken = initialToken;
246
-
247
- const player = attach({
248
- key,
249
- url: signedManifestUrl,
250
- video,
251
- originHeaders: { "X-Playback-Session": playbackSession },
252
- originHeadersProvider: ({ kind }) =>
253
- kind === "segment" ? { Authorization: `Bearer ${playbackToken}` } : undefined,
254
- });
255
- ```
256
-
257
- For Video.js:
258
-
259
- ```ts
260
- import { attachToVideoJs } from "@meshstream/p2p-web/adapters";
261
-
262
- const player = attachToVideoJs({
263
- key,
264
- url,
265
- player: videojs("player"),
266
- });
267
- await player.start();
268
- ```
269
-
270
- This adapter follows the Video.js instance lifecycle and disposes the
271
- MeshStream session automatically when Video.js is destroyed. It is a
272
- media-element bridge: MeshStream owns protocol-compatible playback for that
273
- element. For HLS, do not also load the same URL through Video.js VHS.
274
-
275
- For Video.js HLS playback that must retain VHS and Video.js ABR ownership, use
276
- the native VHS adapter instead. It gives VHS a same-origin virtual manifest and
277
- routes the resulting segment traffic through the Service Worker P2P path. The
278
- SDK releases its session automatically when Video.js is disposed; if the
279
- Service Worker cannot start, VHS continues with the original CDN manifest.
280
-
281
- ```ts
282
- import { attachToVhs } from "@meshstream/p2p-web/adapters";
283
-
284
- const session = attachToVhs({
285
- key,
286
- url: "https://cdn.example.com/live/index.m3u8",
287
- player: videojs("player"),
288
- serviceWorker: { url: "/meshstream-sw.js" },
289
- });
290
- await session.start();
291
- ```
292
-
293
- For players built on top of a standard `<video>` element:
294
-
295
- ```ts
296
- import { attachToArtPlayer, attachToDPlayer, attachToPlyr } from "@meshstream/p2p-web/adapters";
297
-
298
- await attachToPlyr({ key, url, player: plyr }).start();
299
- await attachToDPlayer({ key, url, player: dplayer }).start();
300
- await attachToArtPlayer({ key, url, player: art }).start();
301
- ```
302
-
303
- The named adapters resolve common `video` or `media` fields, or a `<video>` nested
304
- inside the player's root element, and then use the
305
- same automatic HLS, DASH, and progressive-media path as the plain video adapter. They are media-element bridges,
306
- not native Video.js, Plyr, DPlayer, or ArtPlayer engine plugins: MeshStream
307
- owns the playback instance used for the supplied media element.
308
-
309
- For DASH with the standard SDK path:
310
-
311
- ```ts
312
- import { attach } from "@meshstream/p2p-web";
313
-
314
- const player = attach({
315
- key,
316
- url: "https://cdn.example.com/live/index.mpd",
317
- video,
318
- });
319
- await player.start();
320
- ```
321
-
322
- `attach` selects DASH for `.mpd` URLs and HLS for `.m3u8` URLs. Customers
323
- do not pass a protocol flag. DASH P2P uses Shaka Player internally for the
324
- standard SDK path.
325
-
326
- For an existing Shaka Player:
327
-
328
- ```ts
329
- import { attachToShaka } from "@meshstream/p2p-web/adapters";
330
-
331
- const player = attachToShaka({
332
- key,
333
- url: "https://cdn.example.com/live/index.mpd",
334
- player: shakaPlayer,
335
- });
336
- await player.start();
337
- ```
338
-
339
- `attachToShaka()` binds and releases only the MeshStream P2P session. The
340
- application retains ownership of the supplied Shaka instance and must destroy
341
- it when appropriate.
342
-
343
- For an existing dash.js player:
344
-
345
- ```ts
346
- import { attachToDashJs } from "@meshstream/p2p-web/adapters";
347
-
348
- const player = attachToDashJs({
349
- key,
350
- url: "https://cdn.example.com/live/index.mpd",
351
- video,
352
- player: dashJsPlayer,
353
- });
354
- await player.start();
355
- ```
356
-
357
- `attachToDashJs` provides native DASH P2P for dash.js `>= 3.1.0 < 6`. Create
358
- the dash.js instance first, but do not initialize it or load the MPD before
359
- `session.start()`: MeshStream installs its `XHRLoader` extension first, then
360
- initializes the supplied instance. Regular audio/video initialization, index,
361
- and media segments use P2P with CDN fallback. MPD, DRM licenses, subtitles,
362
- low-latency partial segments, and credentialed cookie requests remain on the
363
- customer's dash.js/CDN path. When passing
364
- `originHeaders` to an existing dash.js or Shaka instance, its request interception
365
- API is required; native Safari HLS cannot accept arbitrary custom headers, so use
366
- hls.js or signed URLs for that case.
367
-
368
- ### Adapter capability matrix
369
-
370
- | Player path | P2P integration | Notes |
371
- | --- | --- | --- |
372
- | `attach` with HLS | Native hls.js P2P | MeshStream creates and owns hls.js. |
373
- | `attachToHls` | Native existing hls.js P2P | Application keeps the hls.js lifecycle. |
374
- | `attach` with DASH; `attachToShaka` with HLS/DASH | Native Shaka P2P | Uses Shaka's networking integration. |
375
- | `attachToRxPlayer` with DASH | Native RxPlayer segment P2P | Regular DASH segments; same-origin origin headers are supported. |
376
- | `attachToDashJs` with dash.js `>= 3.1.0 < 6` | Native dash.js P2P | Installs a supported `XHRLoader` extension before dash.js initialization. |
377
- | `attachToVhs` with Video.js VHS | Native VHS via Service Worker | VHS retains player and ABR ownership; requires a same-origin Service Worker. |
378
- | `attachToVideoJs`, Plyr, DPlayer, ArtPlayer | Media-element bridge | Not a native plugin for the player's own media engine. |
379
-
380
- For an existing RxPlayer DASH instance:
381
-
382
- ```ts
383
- import RxPlayer from "rx-player";
384
- import { attachToRxPlayer } from "@meshstream/p2p-web";
385
-
386
- const rxPlayer = new RxPlayer({ videoElement: video });
387
- const player = attachToRxPlayer({ key, url: dashMpdUrl, contentId: "asset-123", player: rxPlayer });
388
- await player.start();
389
- ```
390
-
391
- `attachToRxPlayer` intercepts RxPlayer's actual DASH audio/video segment loader and sends those
392
- requests through MeshStream P2P. When `originHeaders` or
393
- `originHeadersProvider` are configured, same-origin MPD and regular
394
- audio/video fallback requests retain those headers; dynamic values override
395
- static values. Cross-origin media, text, DRM, non-contiguous ranges, and
396
- RxPlayer low-latency chunked loading remain on the customer's RxPlayer/CDN
397
- path without SDK-supplied headers.
398
-
399
- Telemetry keeps the playback host and path for diagnostics but strips query and fragment data,
400
- so signed playback credentials are not included in reports.
401
-
402
- ## Runtime Contract
403
-
404
- - `key` is the customer-facing SDK Key and can be embedded in Web pages.
405
- - `homeRegion` is resolved from the SDK Key and identifies the tenant's registered data-home
406
- region. It is returned by bootstrap for diagnostics and future regional routing; customer code
407
- cannot override it.
408
- - Request-network GeoIP is used only for access-side Tracker/ICE locality and is not a data-residency
409
- selection.
410
- - Customer integrations should only need `key`, the media `url`, and a `video`
411
- element for the standard Web path.
412
- - The SaaS bootstrap endpoint, tracker pool, ICE servers, reporting route, and
413
- scheduling policy are internal service configuration.
414
- - P2P, tracker, authorization, and reporting failures must not block `hls.js`
415
- playback.
416
- - `detectWebP2pCapability` should be called before constructing the P2P client.
417
- iOS Safari HLS without MSE is routed to native HLS CDN playback.
418
- - Fallback reporting uses the service-owned endpoint returned by bootstrap.
419
- - Signed CDN URL query fields such as `token`, `signature`, `expires`,
420
- `x-amz-*`, `x-oss-*`, and `x-goog-*` are excluded from URL-derived swarm IDs.
421
- - `contentId` is optional. When supplied, the SDK derives the internal swarm ID
422
- from it so rotating signed URL paths remain in the same swarm; otherwise it
423
- derives identity from the canonical playback URL.
424
- - `swarmId` and rendition/quality identifiers are internal runtime values. They
425
- are intentionally not part of the customer-facing configuration; the SDK
426
- derives and manages them from the content and manifest.
427
- - Direct progressive `.mp4` and `.mp3` files use the same P2P Service Worker transport as a
428
- byte-range stream. The SDK registers the Worker by default for this path and divides the file
429
- into deterministic 1 MiB pieces. Large and open-ended ranges are returned as a backpressure-aware
430
- stream, so they do not require buffering the whole response in the page. Full-file requests
431
- without `Range`, suffix ranges, multi-range requests, and metadata remain on CDN.
432
- Use a stable `contentId` when signed URLs have per-viewer path differences.
433
- - The direct-file origin must support byte ranges, expose `Content-Length`, `Content-Type`, and
434
- `Accept-Ranges` through CORS, and allow `GET`, `HEAD`, `OPTIONS`, and the `Range` request header.
435
- `HEAD` is recommended; if the origin explicitly returns `405` or `501`, extensionless URL detection
436
- uses a one-byte range `GET` instead. Missing metadata or any P2P error falls back to the original CDN URL.
437
-
438
- ## Public API
439
-
440
- ```ts
441
- // Standard customer-facing entry point
442
- attach(options)
443
-
444
- // Advanced player adapters
445
- import { attachToShaka, attachToDashJs, attachToVhs, attachToVideoJs } from "@meshstream/p2p-web/adapters";
446
-
447
- // P2P runtime controls
448
- player.start()
449
- player.dispose()
450
- player.setP2pEnabled(enabled)
451
- player.addEventListener(listener) // returns unsubscribe
452
- player.statistics()
453
- player.diagnostics()
454
- player.qoe()
455
- ```
456
-
457
- `setP2pEnabled(false)` may be called before `start()` to force a CDN-only
458
- session, or during playback to stop P2P without interrupting CDN delivery.
459
- Repeated `start()` calls share the active session; call `dispose()` before
460
- starting a fresh session for a different source or player lifecycle.
461
-
462
- `start()` returns `{ mode, capability, fallbackReason? }`. On a CDN fallback,
463
- `fallbackReason` is one of `CAPABILITY_UNSUPPORTED`, `P2P_DISABLED`,
464
- `SDK_CONFIG_UNAVAILABLE`, `TRACKER_UNAVAILABLE`, or
465
- `P2P_INITIALIZATION_FAILED`. It is intended for customer telemetry and does
466
- not expose the server bootstrap configuration.
467
-
468
- `addEventListener()` accepts `onPlaybackStateChanged`, `onP2pReady`,
469
- `onP2pUnavailable`, `onPeerCountChanged`, `onTrackerError`,
470
- `onPeerConnectError`, `onCdnFallback`, and `onStatistics`. Register before `start()` when the
471
- initial connection state matters. Startup CDN fallbacks emit `UNSUPPORTED`,
472
- `DISABLED`, `CONFIG_UNAVAILABLE`, `TRACKER_ERROR`, or
473
- `INITIALIZATION_FAILED`; call the returned function during cleanup.
474
-
475
- `onPlaybackStateChanged` reports `idle`, `initializing`, `connecting`,
476
- `p2p-ready`, `cdn-fallback`, and `disposed`. `qoe()` returns a safe
477
- application-telemetry snapshot: total/P2P/CDN download bytes, P2P download
478
- ratio, connected peers, first-peer latency when available, cold-start CDN
479
- bytes, and fallback count. It intentionally excludes Tracker, ICE, and peer
480
- identifiers.
481
-
482
- Low-latency HLS/DASH partial CMAF segment requests remain on the player CDN
483
- path. P2P only distributes complete segments: sharing a response before its
484
- partial body is complete breaks player cancellation semantics and causes
485
- latency jitter. Playback continues normally through CDN for those parts.
486
-
487
- ## Release Checks
488
-
489
- ```bash
490
- npm test -w @meshstream/p2p-web
491
- npm run build:web-sdk:release
492
- npm run pack:dry-run -w @meshstream/p2p-web
493
- ```
494
-
495
- `prepack` runs the SDK test suite, bundles the runtime dependencies into the
496
- public entry points, and then minifies the release output before package
497
- creation. The published package contains only bundled JavaScript, `.d.ts`
498
- types, the Service Worker, README, and licenses; customers do not need to
499
- install the internal player or P2P packages separately. Public exports and
500
- object property names remain stable, while readable formatting and source maps
501
- are omitted from the published JavaScript.
1
+ # MeshStream P2P Web SDK
2
+
3
+ SDK-owned console diagnostics respect the client `logLevel` (default `none`), with redaction and
4
+ repeated-error limiting. Web does not write local log files. See the [logging guide](../../docs/sdk-logging.md).
5
+
6
+ Browser HLS and DASH P2P delivery SDK for MeshStream P2P-CDN. The SDK wraps
7
+ `hls.js`, Shaka Player, and `p2p-media-loader`, fetches runtime configuration
8
+ from Config API, joins a WebRTC tracker swarm, reports playback/P2P statistics,
9
+ and keeps CDN playback as the fallback path.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ # SDK-managed player: MeshStream bundles its HLS, DASH, and P2P runtimes.
15
+ npm install @meshstream/p2p-web
16
+ ```
17
+
18
+ For an application-managed `hls.js` instance, install the player in the
19
+ application as well. MeshStream will bind P2P to that instance without taking
20
+ over its configuration or lifecycle:
21
+
22
+ ```bash
23
+ npm install @meshstream/p2p-web hls.js
24
+ ```
25
+
26
+ In this monorepo the SDK is consumed as a workspace package:
27
+
28
+ ```bash
29
+ npm run build:web-sdk
30
+ npm run build -w @p2pcdn/web-demo
31
+ ```
32
+
33
+ ## Quick Start
34
+
35
+ ```ts
36
+ import { attach } from "@meshstream/p2p-web";
37
+
38
+ const player = attach({
39
+ key: "your_sdk_key",
40
+ url: "https://cdn.example.com/live/index.m3u8", // or .mpd / .mp4 / .mp3
41
+ video: document.querySelector("video"),
42
+ });
43
+
44
+ await player.start();
45
+
46
+ window.addEventListener("pagehide", () => player.dispose(), { once: true });
47
+ ```
48
+
49
+ The historic `video` option accepts any `HTMLMediaElement`; pass an `<audio>`
50
+ element for progressive MP3 playback.
51
+
52
+ The SDK fetches SaaS bootstrap configuration, selects trackers/reporting
53
+ endpoints, detects browser capability, starts WebRTC P2P when possible, and
54
+ keeps CDN playback as the automatic fallback path.
55
+
56
+ When MeshStream creates hls.js, use `hlsConfig` to retain normal player tuning
57
+ such as `maxBufferLength`, ABR limits, or low-latency settings:
58
+
59
+ ```ts
60
+ const player = attach({
61
+ key,
62
+ url,
63
+ video,
64
+ hlsConfig: { maxBufferLength: 45, lowLatencyMode: true },
65
+ });
66
+ ```
67
+
68
+ MeshStream always reapplies its P2P loader and safe origin request settings
69
+ after the instance is created. For `attachToHls`, configure the caller-owned
70
+ hls.js instance directly instead.
71
+
72
+ To display compatibility information before playback, call `getCapability()`.
73
+ It returns only `mseSupported`, `webrtcSupported`, and `serviceWorkerSupported`;
74
+ it does not override the SDK's playback or fallback decision.
75
+
76
+ The web transport selects one content-affine Tracker for a playback session.
77
+ Tracker nodes keep independent in-memory peer maps; switching or retrying a
78
+ Tracker does not copy peers between nodes. If signaling becomes unavailable,
79
+ the player continues through CDN rather than registering the same session with
80
+ every shard. Configure tested hot-content buckets and same-region fallback
81
+ nodes when a stream needs faster recovery.
82
+
83
+ `player.statistics()` always returns the same complete counter shape. During
84
+ startup and CDN fallback, P2P counters are `0` instead of being omitted, so
85
+ application dashboards can use one schema for HLS, DASH, MP4/MP3, and RxPlayer.
86
+
87
+ ## Optional Service Worker Transport
88
+
89
+ Service Worker support is an optional transport layer. MSE P2P remains the
90
+ default and is preferred whenever it is available. To deploy the SW runtime,
91
+ copy the published `dist/meshstream-sw.js` file to a same-origin HTTPS path
92
+ such as `/meshstream-sw.js`, then opt in:
93
+
94
+ ```ts
95
+ import { attach } from "@meshstream/p2p-web";
96
+
97
+ const player = attach({
98
+ key,
99
+ url,
100
+ video,
101
+ transportPreference: "auto",
102
+ serviceWorker: {
103
+ url: "/meshstream-sw.js",
104
+ scope: "/",
105
+ autoRegister: true,
106
+ p2pTimeoutMs: 5000,
107
+ onSegmentStats: ({ source, bytes, url }) => {
108
+ console.debug("segment", source, bytes, url);
109
+ },
110
+ },
111
+ });
112
+ await player.start();
113
+ ```
114
+
115
+ `auto` uses MSE P2P first. If MSE is unavailable and the page is controlled by
116
+ the registered SW, playback uses a virtual manifest URL under
117
+ `/__meshstream__/v1/`; failed or unavailable page-side P2P requests fall back
118
+ to the original CDN URL. `transportPreference: "service-worker"` is available
119
+ for explicit testing. When bootstrap returns a tracker configuration, the SDK
120
+ also starts a page-side `p2p-media-loader-core` engine that works without MSE,
121
+ indexes HLS master/media playlists, and answers SW segment requests from the
122
+ WebRTC swarm with CDN fallback. A custom `segmentProvider` can still be passed
123
+ to replace that engine for application-specific routing.
124
+
125
+ For manual registration, import `registerMeshStreamServiceWorker()` and
126
+ `createMeshStreamServiceWorkerSession()` from `@meshstream/p2p-web/service-worker`.
127
+ Registration requires HTTPS (localhost is allowed by browsers), and the SW script
128
+ must be same-origin with the page. Session metadata and rewritten resource mappings
129
+ are kept in IndexedDB so a browser-restarted SW can recover a registered session;
130
+ unregistering the player removes that metadata. `onSegmentStats` reports whether
131
+ each media request was served by the page provider (`p2p`) or CDN (`cdn`).
132
+
133
+ For a local browser smoke check after building, run
134
+ `node sdks/web/test/sw-browser-fixture.mjs` and open the printed URL. The page
135
+ reports the SW controller state, rewritten segment count, response bodies, and
136
+ per-segment source statistics.
137
+
138
+ For the real two-page WebRTC check, run the automated smoke from the repository
139
+ root:
140
+
141
+ ```powershell
142
+ npm run test:web-sw-real
143
+ ```
144
+
145
+ It builds the SDK and Tracker, opens seed/follower Chromium pages, and fails
146
+ unless the follower receives a non-empty segment through P2P. For manual
147
+ inspection, build the SDK and start
148
+ `node sdks/web/test/sw-real-p2p-browser-fixture.mjs`, then open the seed URL
149
+ first (`?role=seed`) and the follower URL (`?role=follower`).
150
+
151
+ ### Staging rollout checklist
152
+
153
+ 1. Build the browser package with `npm run build:web-sdk:release` and publish the
154
+ generated npm tarball. Copy `dist/meshstream-sw.js` from the package to a stable
155
+ same-origin HTTPS URL such as `/meshstream-sw.js`; do not serve it from a CDN
156
+ origin or a path outside the intended registration scope.
157
+ 2. Deploy the page with `transportPreference: "auto"` and the SW `url` configured.
158
+ Confirm the normal browser path reports `p2p-hls`; test Safari/iOS or a forced
159
+ `transportPreference: "service-worker"` path and confirm `sw-hls-cdn`.
160
+ 3. Check the browser Network panel for the virtual manifest under
161
+ `/__meshstream__/v1/` and verify segment requests return either `p2p` or `cdn`
162
+ in the SDK `onSegmentStats` callback.
163
+ 4. Verify the configured report endpoint receives `reportType: "stats"` with
164
+ `p2pDownloaded`, `httpDownloaded`, `connectedPeers`, and tracker diagnostics.
165
+ The SDK sends an initial report, periodic reports, and a final `pagehide`
166
+ report; report failures must not interrupt playback.
167
+ 5. Roll back by removing the `serviceWorker` option or setting
168
+ `transportPreference: "mse"`; the SDK then keeps the existing MSE/CDN path.
169
+
170
+ ## Advanced Player Adapters
171
+
172
+ Use `@meshstream/p2p-web/adapters` for an existing player instance. Controlled
173
+ test environments that need to construct `MeshStreamHlsPlayer` directly can
174
+ import it from `@meshstream/p2p-web/advanced`; standard integrations should
175
+ continue to use root-level `attach`.
176
+
177
+ For a plain video element:
178
+
179
+ ```ts
180
+ import { attach } from "@meshstream/p2p-web";
181
+
182
+ const player = attach({ key, url, video });
183
+ await player.start();
184
+ ```
185
+
186
+ ### Choosing an HLS integration
187
+
188
+ Both HLS modes use the same P2P scheduling, authorization, and telemetry.
189
+ Use `attach` when the SDK should create and manage hls.js. Use `attachToHls`
190
+ when the application already owns an hls.js instance and must retain its
191
+ buffering, ABR, event, or plugin configuration. In the latter mode, the
192
+ application remains responsible for `loadSource`, `attachMedia`, and
193
+ `destroy`; call `session.start()` before `loadSource`. Do not use both modes
194
+ for the same media element or playback URL.
195
+
196
+ For an existing hls.js instance, bind MeshStream before the application starts
197
+ media loading. This is a native HLS P2P integration: the SDK keeps the caller's
198
+ hls.js configuration and P2P-enables that same instance. It never calls
199
+ `loadSource`, `attachMedia`, or `destroy` on the caller-owned player.
200
+
201
+ ```ts
202
+ import Hls from "hls.js";
203
+ import { attachToHls } from "@meshstream/p2p-web";
204
+
205
+ const hls = new Hls({ maxBufferLength: 30 });
206
+ const session = attachToHls({ key, url, video, player: hls });
207
+
208
+ await session.start(); // Bind P2P before hls.js makes media requests.
209
+ hls.loadSource(url);
210
+ hls.attachMedia(video);
211
+
212
+ window.addEventListener("pagehide", () => {
213
+ session.dispose(); // Does not destroy hls.
214
+ hls.destroy(); // The application owns this lifecycle.
215
+ }, { once: true });
216
+ ```
217
+
218
+ For rotating signed URLs, pass a stable business identity so viewers join the
219
+ same swarm. `mimeType` is useful when the media URL has no `.m3u8`, `.mpd`, `.mp4`, or `.mp3`
220
+ suffix. `originHeaders` are sent only to same-origin manifest, segment, and key requests on the
221
+ configured media origin. SDK-controlled headers such as `Range` and hop-by-hop headers are ignored;
222
+ the browser still applies its own forbidden-header and CORS rules. For Shaka and dash.js
223
+ adapter objects, the player must expose its request interception API; otherwise
224
+ the SDK fails explicitly instead of starting without the requested headers.
225
+
226
+ ```ts
227
+ const player = attach({
228
+ key,
229
+ url: signedManifestUrl,
230
+ video,
231
+ contentId: "live-channel-123",
232
+ mimeType: "m3u8",
233
+ originHeaders: { Authorization: "Bearer ...", "X-Playback-Session": "..." },
234
+ });
235
+ await player.start();
236
+ ```
237
+
238
+ For tokens that rotate during a long playback session, provide the current
239
+ value with a synchronous callback. The callback runs immediately before an
240
+ eligible CDN-origin request; it must not refresh a token or return a Promise.
241
+ Refresh credentials in application state first. Dynamic values override static
242
+ headers with the same name and, like static values, are never sent to peers.
243
+
244
+ ```ts
245
+ let playbackToken = initialToken;
246
+
247
+ const player = attach({
248
+ key,
249
+ url: signedManifestUrl,
250
+ video,
251
+ originHeaders: { "X-Playback-Session": playbackSession },
252
+ originHeadersProvider: ({ kind }) =>
253
+ kind === "segment" ? { Authorization: `Bearer ${playbackToken}` } : undefined,
254
+ });
255
+ ```
256
+
257
+ For Video.js:
258
+
259
+ ```ts
260
+ import { attachToVideoJs } from "@meshstream/p2p-web/adapters";
261
+
262
+ const player = attachToVideoJs({
263
+ key,
264
+ url,
265
+ player: videojs("player"),
266
+ });
267
+ await player.start();
268
+ ```
269
+
270
+ This adapter follows the Video.js instance lifecycle and disposes the
271
+ MeshStream session automatically when Video.js is destroyed. It is a
272
+ media-element bridge: MeshStream owns protocol-compatible playback for that
273
+ element. For HLS, do not also load the same URL through Video.js VHS.
274
+
275
+ For Video.js HLS playback that must retain VHS and Video.js ABR ownership, use
276
+ the native VHS adapter instead. It gives VHS a same-origin virtual manifest and
277
+ routes the resulting segment traffic through the Service Worker P2P path. The
278
+ SDK releases its session automatically when Video.js is disposed; if the
279
+ Service Worker cannot start, VHS continues with the original CDN manifest.
280
+
281
+ ```ts
282
+ import { attachToVhs } from "@meshstream/p2p-web/adapters";
283
+
284
+ const session = attachToVhs({
285
+ key,
286
+ url: "https://cdn.example.com/live/index.m3u8",
287
+ player: videojs("player"),
288
+ serviceWorker: { url: "/meshstream-sw.js" },
289
+ });
290
+ await session.start();
291
+ ```
292
+
293
+ For players built on top of a standard `<video>` element:
294
+
295
+ ```ts
296
+ import { attachToArtPlayer, attachToDPlayer, attachToPlyr } from "@meshstream/p2p-web/adapters";
297
+
298
+ await attachToPlyr({ key, url, player: plyr }).start();
299
+ await attachToDPlayer({ key, url, player: dplayer }).start();
300
+ await attachToArtPlayer({ key, url, player: art }).start();
301
+ ```
302
+
303
+ The named adapters resolve common `video` or `media` fields, or a `<video>` nested
304
+ inside the player's root element, and then use the
305
+ same automatic HLS, DASH, and progressive-media path as the plain video adapter. They are media-element bridges,
306
+ not native Video.js, Plyr, DPlayer, or ArtPlayer engine plugins: MeshStream
307
+ owns the playback instance used for the supplied media element.
308
+
309
+ For DASH with the standard SDK path:
310
+
311
+ ```ts
312
+ import { attach } from "@meshstream/p2p-web";
313
+
314
+ const player = attach({
315
+ key,
316
+ url: "https://cdn.example.com/live/index.mpd",
317
+ video,
318
+ });
319
+ await player.start();
320
+ ```
321
+
322
+ `attach` selects DASH for `.mpd` URLs and HLS for `.m3u8` URLs. Customers
323
+ do not pass a protocol flag. DASH P2P uses Shaka Player internally for the
324
+ standard SDK path.
325
+
326
+ For an existing Shaka Player:
327
+
328
+ ```ts
329
+ import { attachToShaka } from "@meshstream/p2p-web/adapters";
330
+
331
+ const player = attachToShaka({
332
+ key,
333
+ url: "https://cdn.example.com/live/index.mpd",
334
+ player: shakaPlayer,
335
+ });
336
+ await player.start();
337
+ ```
338
+
339
+ `attachToShaka()` binds and releases only the MeshStream P2P session. The
340
+ application retains ownership of the supplied Shaka instance and must destroy
341
+ it when appropriate.
342
+
343
+ For an existing dash.js player:
344
+
345
+ ```ts
346
+ import { attachToDashJs } from "@meshstream/p2p-web/adapters";
347
+
348
+ const player = attachToDashJs({
349
+ key,
350
+ url: "https://cdn.example.com/live/index.mpd",
351
+ video,
352
+ player: dashJsPlayer,
353
+ });
354
+ await player.start();
355
+ ```
356
+
357
+ `attachToDashJs` provides native DASH P2P for dash.js `>= 3.1.0 < 6`. Create
358
+ the dash.js instance first, but do not initialize it or load the MPD before
359
+ `session.start()`: MeshStream installs its `XHRLoader` extension first, then
360
+ initializes the supplied instance. Regular audio/video initialization, index,
361
+ and media segments use P2P with CDN fallback. MPD, DRM licenses, subtitles,
362
+ low-latency partial segments, and credentialed cookie requests remain on the
363
+ customer's dash.js/CDN path. When passing
364
+ `originHeaders` to an existing dash.js or Shaka instance, its request interception
365
+ API is required; native Safari HLS cannot accept arbitrary custom headers, so use
366
+ hls.js or signed URLs for that case.
367
+
368
+ ### Adapter capability matrix
369
+
370
+ | Player path | P2P integration | Notes |
371
+ | --- | --- | --- |
372
+ | `attach` with HLS | Native hls.js P2P | MeshStream creates and owns hls.js. |
373
+ | `attachToHls` | Native existing hls.js P2P | Application keeps the hls.js lifecycle. |
374
+ | `attach` with DASH; `attachToShaka` with HLS/DASH | Native Shaka P2P | Uses Shaka's networking integration. |
375
+ | `attachToRxPlayer` with DASH | Native RxPlayer segment P2P | Regular DASH segments; same-origin origin headers are supported. |
376
+ | `attachToDashJs` with dash.js `>= 3.1.0 < 6` | Native dash.js P2P | Installs a supported `XHRLoader` extension before dash.js initialization. |
377
+ | `attachToVhs` with Video.js VHS | Native VHS via Service Worker | VHS retains player and ABR ownership; requires a same-origin Service Worker. |
378
+ | `attachToVideoJs`, Plyr, DPlayer, ArtPlayer | Media-element bridge | Not a native plugin for the player's own media engine. |
379
+
380
+ For an existing RxPlayer DASH instance:
381
+
382
+ ```ts
383
+ import RxPlayer from "rx-player";
384
+ import { attachToRxPlayer } from "@meshstream/p2p-web";
385
+
386
+ const rxPlayer = new RxPlayer({ videoElement: video });
387
+ const player = attachToRxPlayer({ key, url: dashMpdUrl, contentId: "asset-123", player: rxPlayer });
388
+ await player.start();
389
+ ```
390
+
391
+ `attachToRxPlayer` intercepts RxPlayer's actual DASH audio/video segment loader and sends those
392
+ requests through MeshStream P2P. When `originHeaders` or
393
+ `originHeadersProvider` are configured, same-origin MPD and regular
394
+ audio/video fallback requests retain those headers; dynamic values override
395
+ static values. Cross-origin media, text, DRM, non-contiguous ranges, and
396
+ RxPlayer low-latency chunked loading remain on the customer's RxPlayer/CDN
397
+ path without SDK-supplied headers.
398
+
399
+ Telemetry keeps the playback host and path for diagnostics but strips query and fragment data,
400
+ so signed playback credentials are not included in reports.
401
+
402
+ ## Runtime Contract
403
+
404
+ - `key` is the customer-facing SDK Key and can be embedded in Web pages.
405
+ - `homeRegion` is resolved from the SDK Key and identifies the tenant's registered data-home
406
+ region. It is returned by bootstrap for diagnostics and future regional routing; customer code
407
+ cannot override it.
408
+ - Request-network GeoIP is used only for access-side Tracker/ICE locality and is not a data-residency
409
+ selection.
410
+ - Customer integrations should only need `key`, the media `url`, and a `video`
411
+ element for the standard Web path.
412
+ - The SaaS bootstrap endpoint, tracker pool, ICE servers, reporting route, and
413
+ scheduling policy are internal service configuration.
414
+ - P2P, tracker, authorization, and reporting failures must not block `hls.js`
415
+ playback.
416
+ - `detectWebP2pCapability` should be called before constructing the P2P client.
417
+ iOS Safari HLS without MSE is routed to native HLS CDN playback.
418
+ - Fallback reporting uses the service-owned endpoint returned by bootstrap.
419
+ - Signed CDN URL query fields such as `token`, `signature`, `expires`,
420
+ `x-amz-*`, `x-oss-*`, and `x-goog-*` are excluded from URL-derived swarm IDs.
421
+ - `contentId` is optional. When supplied, the SDK derives the internal swarm ID
422
+ from it so rotating signed URL paths remain in the same swarm; otherwise it
423
+ derives identity from the canonical playback URL.
424
+ - `swarmId` and rendition/quality identifiers are internal runtime values. They
425
+ are intentionally not part of the customer-facing configuration; the SDK
426
+ derives and manages them from the content and manifest.
427
+ - Direct progressive `.mp4` and `.mp3` files use the same P2P Service Worker transport as a
428
+ byte-range stream. The SDK registers the Worker by default for this path and divides the file
429
+ into deterministic 1 MiB pieces. Large and open-ended ranges are returned as a backpressure-aware
430
+ stream, so they do not require buffering the whole response in the page. Full-file requests
431
+ without `Range`, suffix ranges, multi-range requests, and metadata remain on CDN.
432
+ Use a stable `contentId` when signed URLs have per-viewer path differences.
433
+ - The direct-file origin must support byte ranges, expose `Content-Length`, `Content-Type`, and
434
+ `Accept-Ranges` through CORS, and allow `GET`, `HEAD`, `OPTIONS`, and the `Range` request header.
435
+ `HEAD` is recommended; if the origin explicitly returns `405` or `501`, extensionless URL detection
436
+ uses a one-byte range `GET` instead. Missing metadata or any P2P error falls back to the original CDN URL.
437
+
438
+ ## Public API
439
+
440
+ ```ts
441
+ // Standard customer-facing entry point
442
+ attach(options)
443
+
444
+ // Advanced player adapters
445
+ import { attachToShaka, attachToDashJs, attachToVhs, attachToVideoJs } from "@meshstream/p2p-web/adapters";
446
+
447
+ // P2P runtime controls
448
+ player.start()
449
+ player.dispose()
450
+ player.setP2pEnabled(enabled)
451
+ player.addEventListener(listener) // returns unsubscribe
452
+ player.statistics()
453
+ player.diagnostics()
454
+ player.qoe()
455
+ ```
456
+
457
+ `setP2pEnabled(false)` may be called before `start()` to force a CDN-only
458
+ session, or during playback to stop P2P without interrupting CDN delivery.
459
+ Repeated `start()` calls share the active session; call `dispose()` before
460
+ starting a fresh session for a different source or player lifecycle.
461
+
462
+ `start()` returns `{ mode, capability, fallbackReason? }`. On a CDN fallback,
463
+ `fallbackReason` is one of `CAPABILITY_UNSUPPORTED`, `P2P_DISABLED`,
464
+ `SDK_CONFIG_UNAVAILABLE`, `TRACKER_UNAVAILABLE`, or
465
+ `P2P_INITIALIZATION_FAILED`. It is intended for customer telemetry and does
466
+ not expose the server bootstrap configuration.
467
+
468
+ `addEventListener()` accepts `onPlaybackStateChanged`, `onP2pReady`,
469
+ `onP2pUnavailable`, `onPeerCountChanged`, `onTrackerError`,
470
+ `onPeerConnectError`, `onCdnFallback`, and `onStatistics`. Register before `start()` when the
471
+ initial connection state matters. Startup CDN fallbacks emit `UNSUPPORTED`,
472
+ `DISABLED`, `CONFIG_UNAVAILABLE`, `TRACKER_ERROR`, or
473
+ `INITIALIZATION_FAILED`; call the returned function during cleanup.
474
+
475
+ `onPlaybackStateChanged` reports `idle`, `initializing`, `connecting`,
476
+ `p2p-ready`, `cdn-fallback`, and `disposed`. `qoe()` returns a safe
477
+ application-telemetry snapshot: total/P2P/CDN download bytes, P2P download
478
+ ratio, connected peers, first-peer latency when available, cold-start CDN
479
+ bytes, and fallback count. It intentionally excludes Tracker, ICE, and peer
480
+ identifiers.
481
+
482
+ Low-latency HLS/DASH partial CMAF segment requests remain on the player CDN
483
+ path. P2P only distributes complete segments: sharing a response before its
484
+ partial body is complete breaks player cancellation semantics and causes
485
+ latency jitter. Playback continues normally through CDN for those parts.
486
+
487
+ ## Release Checks
488
+
489
+ ```bash
490
+ npm test -w @meshstream/p2p-web
491
+ npm run build:web-sdk:release
492
+ npm run pack:dry-run -w @meshstream/p2p-web
493
+ ```
494
+
495
+ `prepack` runs the SDK test suite, bundles the runtime dependencies into the
496
+ public entry points, and then minifies the release output before package
497
+ creation. The published package contains only bundled JavaScript, `.d.ts`
498
+ types, the Service Worker, README, and licenses; customers do not need to
499
+ install the internal player or P2P packages separately. Public exports and
500
+ object property names remain stable, while readable formatting and source maps
501
+ are omitted from the published JavaScript.