@mebius-io/web 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -12
- package/dist/index.cjs +172 -88
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +59 -13
- package/dist/index.d.ts +59 -13
- package/dist/index.global.js +9700 -97
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +172 -88
- package/dist/index.js.map +1 -1
- package/package.json +2 -9
package/README.md
CHANGED
|
@@ -50,12 +50,17 @@ external deps. `Mebius` becomes a global.
|
|
|
50
50
|
<script src="mebius.min.js"></script>
|
|
51
51
|
<script>
|
|
52
52
|
Mebius.init({ appId: "app_123", gateway: "https://gateway.mebius.io" });
|
|
53
|
-
const client = Mebius.connect({ token }); //
|
|
53
|
+
const client = Mebius.connect({ token, deliveries }); // dari backend kamu
|
|
54
54
|
</script>
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
File + full PHP example: [`standalone/`](./standalone/). Raw download:
|
|
58
|
-
`https://raw.githubusercontent.com/russimobiledroidx/mebius-web-sdk/v0.
|
|
58
|
+
`https://raw.githubusercontent.com/russimobiledroidx/mebius-web-sdk/v0.3.0/packages/web/standalone/mebius.min.js`
|
|
59
|
+
|
|
60
|
+
The drop-in file is not part of the npm package — `files` ships only `dist` — so it
|
|
61
|
+
is fetched from the tag, and the tag must match the version you installed. For a
|
|
62
|
+
page with a build step, or one that can use an import map, prefer the ESM path:
|
|
63
|
+
`https://esm.sh/@mebius-io/web@0.3.0`.
|
|
59
64
|
|
|
60
65
|
## Quick Start
|
|
61
66
|
|
|
@@ -106,7 +111,7 @@ await broadcaster.stop();
|
|
|
106
111
|
### d. Watch
|
|
107
112
|
|
|
108
113
|
```ts
|
|
109
|
-
const player = client.createPlayer(
|
|
114
|
+
const player = client.createPlayer(); // mode default "auto"
|
|
110
115
|
|
|
111
116
|
player.on("playing", ({ streamId }) => console.log("playing", streamId));
|
|
112
117
|
player.on("buffering", () => console.log("buffering..."));
|
|
@@ -118,8 +123,44 @@ player.setVolume(0.8);
|
|
|
118
123
|
await player.stop();
|
|
119
124
|
```
|
|
120
125
|
|
|
121
|
-
|
|
122
|
-
|
|
126
|
+
### Mode playback
|
|
127
|
+
|
|
128
|
+
| Mode | Kapan dipakai |
|
|
129
|
+
| --- | --- |
|
|
130
|
+
| `"auto"` (default) | Rekomendasi. Mebius pilih rute per penonton, dan pindah sendiri kalau rute yang dipakai berhenti mengirim frame. |
|
|
131
|
+
| `"low-latency"` | Interaktif dua arah (mis. co-broadcast), delay sub-detik. Browser saja. |
|
|
132
|
+
| `"balanced"` | Delay rendah tapi tetap skala besar. Butuh browser dengan Media Source (bukan Safari iOS). |
|
|
133
|
+
| `"scale"` | Audiens paling besar, delay paling tinggi, jalan di semua platform termasuk Safari iOS. |
|
|
134
|
+
|
|
135
|
+
Ganti mode kapan saja dengan membuat player baru.
|
|
136
|
+
|
|
137
|
+
### Menonton lawan bicara (`createMonitor`)
|
|
138
|
+
|
|
139
|
+
Kalau kamu menonton stream yang sedang kamu **ajak interaksi** (sisi lain dari
|
|
140
|
+
co-broadcast), delay satu-dua detik membuat interaksinya terasa rusak:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const monitor = client.createMonitor();
|
|
144
|
+
await monitor.play(opponentStreamId, "#opponent");
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Sama seperti player biasa, hanya budget delay-nya beda. Monitor mulai dari rute
|
|
148
|
+
real-time dan **pindah sendiri** kalau rute itu tidak mengirim frame dalam 8
|
|
149
|
+
detik — logika yang sebelumnya harus ditulis ulang di setiap aplikasi, dan kalau
|
|
150
|
+
salah hasilnya frame hitam di depan penonton live.
|
|
151
|
+
|
|
152
|
+
### `deliveries`
|
|
153
|
+
|
|
154
|
+
`Mebius.connect()` menerima `deliveries` yang dikirim backend bareng token:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const { token, deliveries } = await (await fetch("/api/mebius-token")).json();
|
|
158
|
+
const client = Mebius.connect({ token, deliveries });
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Teruskan apa adanya — isinya opaque dan Mebius yang mengurutkan serta memilih.
|
|
162
|
+
Opsional: tanpa itu playback tetap jalan, tapi setiap penonton dilayani dari
|
|
163
|
+
origin Mebius, bukan edge terdekat.
|
|
123
164
|
|
|
124
165
|
## Integrasi per framework
|
|
125
166
|
|
|
@@ -152,7 +193,7 @@ import { useMebius, usePlayer } from "@mebius-io/react";
|
|
|
152
193
|
|
|
153
194
|
function Watch({ token, streamId }) {
|
|
154
195
|
const { client } = useMebius({ appId, gateway, token });
|
|
155
|
-
const { videoRef, play } = usePlayer(client, {
|
|
196
|
+
const { videoRef, play } = usePlayer(client, {});
|
|
156
197
|
return <video ref={videoRef} onClick={() => play(streamId)} autoPlay />;
|
|
157
198
|
}
|
|
158
199
|
```
|
|
@@ -181,7 +222,7 @@ export function useWatch(streamId: string) {
|
|
|
181
222
|
onMounted(async () => {
|
|
182
223
|
Mebius.init({ appId, gateway });
|
|
183
224
|
const client = Mebius.connect({ token: await getToken() });
|
|
184
|
-
player = client.createPlayer(
|
|
225
|
+
player = client.createPlayer();
|
|
185
226
|
await player.play(streamId, video.value!);
|
|
186
227
|
});
|
|
187
228
|
onUnmounted(() => player?.stop());
|
|
@@ -191,18 +232,19 @@ export function useWatch(streamId: string) {
|
|
|
191
232
|
|
|
192
233
|
### Vite
|
|
193
234
|
|
|
194
|
-
Tidak ada config khusus. ESM langsung jalan; engine playback
|
|
195
|
-
|
|
196
|
-
|
|
235
|
+
Tidak ada config khusus. ESM langsung jalan; engine playback per mode di-load
|
|
236
|
+
lazy hanya saat mode itu dipakai, jadi aplikasi yang cuma pakai `"low-latency"`
|
|
237
|
+
tidak membawa bundle mode lain.
|
|
197
238
|
|
|
198
239
|
## API Reference
|
|
199
240
|
|
|
200
241
|
| Class | Method | Return | Keterangan |
|
|
201
242
|
|---|---|---|---|
|
|
202
243
|
| `Mebius` | `init({ appId, gateway })` | `void` | Konfigurasi sekali di awal. |
|
|
203
|
-
| `Mebius` | `connect({ token })` | `MebiusClient` | Buka koneksi. |
|
|
244
|
+
| `Mebius` | `connect({ token, deliveries? })` | `MebiusClient` | Buka koneksi. |
|
|
204
245
|
| `MebiusClient` | `createBroadcaster({ video?, audio? })` | `MebiusBroadcaster` | |
|
|
205
|
-
| `MebiusClient` | `createPlayer({ mode })` | `MebiusPlayer` | `mode: "low-latency" \| "scale"` |
|
|
246
|
+
| `MebiusClient` | `createPlayer({ mode? })` | `MebiusPlayer` | `mode: "auto" \| "low-latency" \| "balanced" \| "scale"`, default `"auto"` |
|
|
247
|
+
| `MebiusClient` | `createMonitor()` | `MebiusPlayer` | Player untuk stream yang kamu ajak interaksi. |
|
|
206
248
|
| `MebiusClient` | `disconnect(reason?)` | `void` | |
|
|
207
249
|
| `MebiusBroadcaster` | `start(streamId)` | `Promise<void>` | |
|
|
208
250
|
| `MebiusBroadcaster` | `stop()` | `Promise<void>` | |
|
package/dist/index.cjs
CHANGED
|
@@ -261,11 +261,18 @@ var WhepViewTransport = class {
|
|
|
261
261
|
}
|
|
262
262
|
};
|
|
263
263
|
|
|
264
|
-
// src/internal/
|
|
265
|
-
var
|
|
266
|
-
|
|
264
|
+
// src/internal/scale-view-transport.ts
|
|
265
|
+
var HlsViewTransport = class {
|
|
266
|
+
/**
|
|
267
|
+
* deliveryPath, when given, is a gateway-relative path from the gateway's own
|
|
268
|
+
* delivery list — that is how a CDN-backed playlist gets used instead of the
|
|
269
|
+
* origin one. Without it this falls back to the origin playlist, which is
|
|
270
|
+
* still correct, just served from our own bandwidth.
|
|
271
|
+
*/
|
|
272
|
+
constructor(signaling, deliveryPath) {
|
|
267
273
|
this.signaling = signaling;
|
|
268
|
-
this.
|
|
274
|
+
this.deliveryPath = deliveryPath;
|
|
275
|
+
this.hls = null;
|
|
269
276
|
this.video = null;
|
|
270
277
|
this.endedCb = null;
|
|
271
278
|
this.bufferingCb = null;
|
|
@@ -278,37 +285,36 @@ var FlvViewTransport = class {
|
|
|
278
285
|
}
|
|
279
286
|
async start(streamId, video) {
|
|
280
287
|
this.video = video;
|
|
281
|
-
const url = this.signaling.
|
|
288
|
+
const url = this.deliveryPath ? this.signaling.deliveryUrl(this.deliveryPath) : this.signaling.scalePlaylistUrl(streamId);
|
|
282
289
|
video.addEventListener("ended", () => this.endedCb?.());
|
|
283
290
|
video.addEventListener("waiting", () => this.bufferingCb?.());
|
|
291
|
+
if (video.canPlayType("application/vnd.apple.mpegurl")) {
|
|
292
|
+
video.src = url;
|
|
293
|
+
await video.play().catch(() => void 0);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
284
296
|
let mod;
|
|
285
297
|
try {
|
|
286
|
-
|
|
287
|
-
mod = await import(
|
|
288
|
-
/* @vite-ignore */
|
|
289
|
-
spec
|
|
290
|
-
);
|
|
298
|
+
mod = await import("hls.js");
|
|
291
299
|
} catch (cause) {
|
|
292
|
-
throw mebiusError("CONNECTION_FAILED", "
|
|
300
|
+
throw mebiusError("CONNECTION_FAILED", "Scale playback support failed to load.", cause);
|
|
293
301
|
}
|
|
294
|
-
const
|
|
295
|
-
if (!
|
|
296
|
-
throw mebiusError("CONNECTION_FAILED", "
|
|
302
|
+
const Hls = mod.default;
|
|
303
|
+
if (!Hls.isSupported()) {
|
|
304
|
+
throw mebiusError("CONNECTION_FAILED", "Scale playback is not supported in this browser.");
|
|
297
305
|
}
|
|
298
|
-
const
|
|
299
|
-
this.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
306
|
+
const hls = new Hls({ lowLatencyMode: true });
|
|
307
|
+
this.hls = hls;
|
|
308
|
+
hls.on(Hls.Events.ERROR, (_evt, data) => {
|
|
309
|
+
if (data.fatal) this.bufferingCb?.();
|
|
310
|
+
});
|
|
311
|
+
hls.loadSource(url);
|
|
312
|
+
hls.attachMedia(video);
|
|
313
|
+
await video.play().catch(() => void 0);
|
|
304
314
|
}
|
|
305
315
|
async stop() {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
this.player.detachMediaElement();
|
|
309
|
-
this.player.destroy();
|
|
310
|
-
this.player = null;
|
|
311
|
-
}
|
|
316
|
+
this.hls?.destroy();
|
|
317
|
+
this.hls = null;
|
|
312
318
|
if (this.video) {
|
|
313
319
|
this.video.removeAttribute("src");
|
|
314
320
|
this.video.load();
|
|
@@ -317,19 +323,20 @@ var FlvViewTransport = class {
|
|
|
317
323
|
}
|
|
318
324
|
async getStats() {
|
|
319
325
|
if (!this.video) return null;
|
|
326
|
+
const level = this.hls?.levels?.[this.hls.currentLevel];
|
|
320
327
|
return {
|
|
321
|
-
bitrateKbps: 0,
|
|
322
|
-
framesPerSecond: 0
|
|
323
|
-
latencyMs: void 0
|
|
328
|
+
bitrateKbps: level ? Math.round(level.bitrate / 1e3) : 0,
|
|
329
|
+
framesPerSecond: 0
|
|
324
330
|
};
|
|
325
331
|
}
|
|
326
332
|
};
|
|
327
333
|
|
|
328
|
-
// src/internal/
|
|
329
|
-
var
|
|
330
|
-
constructor(signaling) {
|
|
334
|
+
// src/internal/balanced-view-transport.ts
|
|
335
|
+
var FlvViewTransport = class {
|
|
336
|
+
constructor(signaling, deliveryPath) {
|
|
331
337
|
this.signaling = signaling;
|
|
332
|
-
this.
|
|
338
|
+
this.deliveryPath = deliveryPath;
|
|
339
|
+
this.player = null;
|
|
333
340
|
this.video = null;
|
|
334
341
|
this.endedCb = null;
|
|
335
342
|
this.bufferingCb = null;
|
|
@@ -340,38 +347,35 @@ var HlsViewTransport = class {
|
|
|
340
347
|
onBuffering(cb) {
|
|
341
348
|
this.bufferingCb = cb;
|
|
342
349
|
}
|
|
343
|
-
async start(
|
|
350
|
+
async start(_streamId, video) {
|
|
344
351
|
this.video = video;
|
|
345
|
-
const url = this.signaling.
|
|
352
|
+
const url = this.signaling.deliveryUrl(this.deliveryPath);
|
|
346
353
|
video.addEventListener("ended", () => this.endedCb?.());
|
|
347
354
|
video.addEventListener("waiting", () => this.bufferingCb?.());
|
|
348
|
-
if (video.canPlayType("application/vnd.apple.mpegurl")) {
|
|
349
|
-
video.src = url;
|
|
350
|
-
await video.play().catch(() => void 0);
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
355
|
let mod;
|
|
354
356
|
try {
|
|
355
|
-
mod = await import("
|
|
357
|
+
mod = await import("flv.js");
|
|
356
358
|
} catch (cause) {
|
|
357
|
-
throw mebiusError("CONNECTION_FAILED", "
|
|
359
|
+
throw mebiusError("CONNECTION_FAILED", "Balanced playback support failed to load.", cause);
|
|
358
360
|
}
|
|
359
|
-
const
|
|
360
|
-
if (!
|
|
361
|
-
throw mebiusError("CONNECTION_FAILED", "
|
|
361
|
+
const flvjs = mod.default;
|
|
362
|
+
if (!flvjs.isSupported()) {
|
|
363
|
+
throw mebiusError("CONNECTION_FAILED", "Balanced playback is not supported in this browser.");
|
|
362
364
|
}
|
|
363
|
-
const
|
|
364
|
-
this.
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
hls.attachMedia(video);
|
|
370
|
-
await video.play().catch(() => void 0);
|
|
365
|
+
const player = flvjs.createPlayer({ type: "flv", url, isLive: true });
|
|
366
|
+
this.player = player;
|
|
367
|
+
player.on(flvjs.Events.ERROR ?? "error", () => this.bufferingCb?.());
|
|
368
|
+
player.attachMediaElement(video);
|
|
369
|
+
player.load();
|
|
370
|
+
await Promise.resolve(player.play()).catch(() => void 0);
|
|
371
371
|
}
|
|
372
372
|
async stop() {
|
|
373
|
-
this.
|
|
374
|
-
|
|
373
|
+
if (this.player) {
|
|
374
|
+
this.player.unload();
|
|
375
|
+
this.player.detachMediaElement();
|
|
376
|
+
this.player.destroy();
|
|
377
|
+
this.player = null;
|
|
378
|
+
}
|
|
375
379
|
if (this.video) {
|
|
376
380
|
this.video.removeAttribute("src");
|
|
377
381
|
this.video.load();
|
|
@@ -380,10 +384,10 @@ var HlsViewTransport = class {
|
|
|
380
384
|
}
|
|
381
385
|
async getStats() {
|
|
382
386
|
if (!this.video) return null;
|
|
383
|
-
const level = this.hls?.levels?.[this.hls.currentLevel];
|
|
384
387
|
return {
|
|
385
|
-
bitrateKbps:
|
|
386
|
-
framesPerSecond: 0
|
|
388
|
+
bitrateKbps: 0,
|
|
389
|
+
framesPerSecond: 0,
|
|
390
|
+
latencyMs: void 0
|
|
387
391
|
};
|
|
388
392
|
}
|
|
389
393
|
};
|
|
@@ -392,14 +396,30 @@ var HlsViewTransport = class {
|
|
|
392
396
|
function createPublishTransport(signaling) {
|
|
393
397
|
return new WhipPublishTransport(signaling);
|
|
394
398
|
}
|
|
395
|
-
|
|
399
|
+
var KIND_FAST = "fast";
|
|
400
|
+
var KIND_WIDE = "wide";
|
|
401
|
+
var KIND_LOCAL = "local";
|
|
402
|
+
function canPlayBuffered() {
|
|
403
|
+
return typeof MediaSource !== "undefined";
|
|
404
|
+
}
|
|
405
|
+
function transportFor(kind, path, signaling) {
|
|
406
|
+
if (kind === KIND_FAST) return canPlayBuffered() ? new FlvViewTransport(signaling, path) : null;
|
|
407
|
+
if (kind === KIND_WIDE || kind === KIND_LOCAL) return new HlsViewTransport(signaling, path);
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
function createViewCandidates(mode, signaling, deliveries = []) {
|
|
411
|
+
const fromGateway = (kinds) => deliveries.filter((d) => kinds.includes(d.kind)).map((d) => transportFor(d.kind, d.path, signaling)).filter((t) => t !== null);
|
|
412
|
+
const originFallback = new HlsViewTransport(signaling);
|
|
413
|
+
const allKinds = [KIND_FAST, KIND_WIDE, KIND_LOCAL];
|
|
396
414
|
switch (mode) {
|
|
397
415
|
case "low-latency":
|
|
398
|
-
return new WhepViewTransport(signaling);
|
|
416
|
+
return [new WhepViewTransport(signaling), ...fromGateway(allKinds), originFallback];
|
|
399
417
|
case "balanced":
|
|
400
|
-
return
|
|
418
|
+
return [...fromGateway(allKinds), originFallback];
|
|
401
419
|
case "scale":
|
|
402
|
-
return
|
|
420
|
+
return [...fromGateway([KIND_WIDE, KIND_LOCAL]), originFallback];
|
|
421
|
+
case "auto":
|
|
422
|
+
return [...fromGateway(allKinds), originFallback];
|
|
403
423
|
}
|
|
404
424
|
}
|
|
405
425
|
|
|
@@ -498,34 +518,48 @@ function normalize(c, fallback) {
|
|
|
498
518
|
|
|
499
519
|
// src/player.ts
|
|
500
520
|
var STATS_INTERVAL_MS2 = 2e3;
|
|
521
|
+
var FIRST_FRAME_TIMEOUT_MS = 8e3;
|
|
501
522
|
var MebiusPlayer = class extends TypedEmitter {
|
|
502
523
|
/** @internal */
|
|
503
|
-
constructor(signaling, options) {
|
|
524
|
+
constructor(signaling, options = {}, deliveries = []) {
|
|
504
525
|
super();
|
|
526
|
+
this.transport = null;
|
|
505
527
|
this.video = null;
|
|
506
528
|
this.statsTimer = null;
|
|
507
529
|
this.playing = false;
|
|
508
|
-
this.
|
|
509
|
-
this.transport.onEnded(() => {
|
|
510
|
-
this.playing = false;
|
|
511
|
-
this.stopStats();
|
|
512
|
-
this.emit("ended", void 0);
|
|
513
|
-
});
|
|
514
|
-
this.transport.onBuffering(() => this.emit("buffering", void 0));
|
|
530
|
+
this.candidates = createViewCandidates(options.mode ?? "auto", signaling, deliveries);
|
|
515
531
|
}
|
|
516
532
|
/** Start playing `streamId` into the given video element or selector. */
|
|
517
533
|
async play(streamId, viewTarget) {
|
|
518
534
|
if (this.playing) return;
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
this.
|
|
523
|
-
|
|
535
|
+
const video = resolveVideoElement(viewTarget);
|
|
536
|
+
this.video = video;
|
|
537
|
+
let lastError = null;
|
|
538
|
+
for (const candidate of this.candidates) {
|
|
539
|
+
try {
|
|
540
|
+
this.attach(candidate);
|
|
541
|
+
await candidate.start(streamId, video);
|
|
542
|
+
if (await hasFirstFrame(video)) {
|
|
543
|
+
this.transport = candidate;
|
|
544
|
+
this.playing = true;
|
|
545
|
+
this.startStats();
|
|
546
|
+
this.emit("playing", { streamId });
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
lastError = mebiusError("CONNECTION_FAILED", "A Mebius route delivered no video.");
|
|
550
|
+
} catch (cause) {
|
|
551
|
+
lastError = cause;
|
|
552
|
+
}
|
|
553
|
+
await candidate.stop().catch(() => void 0);
|
|
554
|
+
}
|
|
555
|
+
this.video = null;
|
|
556
|
+
throw lastError ?? mebiusError("CONNECTION_FAILED", "No Mebius route could play this stream.");
|
|
524
557
|
}
|
|
525
558
|
/** Stop playback and detach from the video element. */
|
|
526
559
|
async stop() {
|
|
527
560
|
this.stopStats();
|
|
528
|
-
await this.transport
|
|
561
|
+
await this.transport?.stop();
|
|
562
|
+
this.transport = null;
|
|
529
563
|
this.video = null;
|
|
530
564
|
this.playing = false;
|
|
531
565
|
}
|
|
@@ -534,9 +568,21 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
534
568
|
const v = Math.min(1, Math.max(0, volume));
|
|
535
569
|
if (this.video) this.video.volume = v;
|
|
536
570
|
}
|
|
571
|
+
attach(transport) {
|
|
572
|
+
transport.onEnded(() => {
|
|
573
|
+
if (this.transport !== transport) return;
|
|
574
|
+
this.playing = false;
|
|
575
|
+
this.stopStats();
|
|
576
|
+
this.emit("ended", void 0);
|
|
577
|
+
});
|
|
578
|
+
transport.onBuffering(() => {
|
|
579
|
+
if (this.transport !== transport) return;
|
|
580
|
+
this.emit("buffering", void 0);
|
|
581
|
+
});
|
|
582
|
+
}
|
|
537
583
|
startStats() {
|
|
538
584
|
this.statsTimer = setInterval(async () => {
|
|
539
|
-
const stats = await this.transport
|
|
585
|
+
const stats = await this.transport?.getStats();
|
|
540
586
|
if (stats) this.emit("stats", stats);
|
|
541
587
|
}, STATS_INTERVAL_MS2);
|
|
542
588
|
}
|
|
@@ -545,6 +591,21 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
545
591
|
this.statsTimer = null;
|
|
546
592
|
}
|
|
547
593
|
};
|
|
594
|
+
function hasFirstFrame(video) {
|
|
595
|
+
if (video.currentTime > 0 && !video.paused) return Promise.resolve(true);
|
|
596
|
+
return new Promise((resolve) => {
|
|
597
|
+
const done = (ok) => {
|
|
598
|
+
clearTimeout(timer);
|
|
599
|
+
video.removeEventListener("timeupdate", onTime);
|
|
600
|
+
resolve(ok);
|
|
601
|
+
};
|
|
602
|
+
const onTime = () => {
|
|
603
|
+
if (video.currentTime > 0) done(true);
|
|
604
|
+
};
|
|
605
|
+
const timer = setTimeout(() => done(false), FIRST_FRAME_TIMEOUT_MS);
|
|
606
|
+
video.addEventListener("timeupdate", onTime);
|
|
607
|
+
});
|
|
608
|
+
}
|
|
548
609
|
|
|
549
610
|
// src/internal/signaling.ts
|
|
550
611
|
var SignalingClient = class {
|
|
@@ -568,17 +629,24 @@ var SignalingClient = class {
|
|
|
568
629
|
// Build the playlist URL used by scale-mode playback (HLS path, hidden). The
|
|
569
630
|
// engine serves the playlist under /live/{id}/index.m3u8 and requires the
|
|
570
631
|
// token in the query; segment URIs in the playlist inherit it automatically.
|
|
632
|
+
/**
|
|
633
|
+
* Absolute, tokenized URL for a gateway-relative delivery path handed to us
|
|
634
|
+
* by the gateway (`deliveries[].path`). The gateway decides which paths exist
|
|
635
|
+
* and in what order; the SDK only resolves them against its own base and
|
|
636
|
+
* attaches the access token. Anything that is not a plain gateway-relative
|
|
637
|
+
* path is rejected rather than fetched: an absolute URL there would send the
|
|
638
|
+
* token to a host we did not choose.
|
|
639
|
+
*/
|
|
640
|
+
deliveryUrl(path) {
|
|
641
|
+
if (!path.startsWith("/") || path.startsWith("//") || path.includes("://")) {
|
|
642
|
+
throw mebiusError("CONNECTION_FAILED", "The gateway returned an unusable delivery path.");
|
|
643
|
+
}
|
|
644
|
+
return this.withToken(`${this.base()}${path}`);
|
|
645
|
+
}
|
|
571
646
|
/** Playlist URL for scale-mode playback. */
|
|
572
647
|
scalePlaylistUrl(streamId) {
|
|
573
648
|
return this.withToken(`${this.base()}/live/${encodeURIComponent(streamId)}/index.m3u8`);
|
|
574
649
|
}
|
|
575
|
-
// Build the HTTP-FLV pull URL used by balanced-mode playback. Served by the
|
|
576
|
-
// CDN (CDN_PULL_FORMAT `.flv`) or an SRS/nginx-rtmp edge in front of the
|
|
577
|
-
// engine — MediaMTX itself does not vend HTTP-FLV. Hidden from the public API.
|
|
578
|
-
/** Pull URL for balanced-mode playback. */
|
|
579
|
-
balancedStreamUrl(streamId) {
|
|
580
|
-
return this.withToken(`${this.base()}/flv/${encodeURIComponent(streamId)}.flv`);
|
|
581
|
-
}
|
|
582
650
|
// Maps a neutral session kind to the concrete signaling path segment. This
|
|
583
651
|
// mapping (publish -> WHIP, view -> WHEP) lives ONLY in this method body, so
|
|
584
652
|
// the protocol names never appear in any exported type signature.
|
|
@@ -650,9 +718,10 @@ function readToken(token) {
|
|
|
650
718
|
// src/client.ts
|
|
651
719
|
var MebiusClient = class extends TypedEmitter {
|
|
652
720
|
/** @internal */
|
|
653
|
-
constructor(config2, token) {
|
|
721
|
+
constructor(config2, token, deliveries = []) {
|
|
654
722
|
super();
|
|
655
723
|
this.token = token;
|
|
724
|
+
this.deliveries = deliveries;
|
|
656
725
|
this.expiryTimer = null;
|
|
657
726
|
this.connected = false;
|
|
658
727
|
this.signaling = new SignalingClient(config2.gateway, token);
|
|
@@ -680,9 +749,24 @@ var MebiusClient = class extends TypedEmitter {
|
|
|
680
749
|
return new MebiusBroadcaster(this.signaling, options);
|
|
681
750
|
}
|
|
682
751
|
/** Create a player bound to this connection. */
|
|
683
|
-
createPlayer(options) {
|
|
752
|
+
createPlayer(options = {}) {
|
|
753
|
+
this.assertConnected();
|
|
754
|
+
return new MebiusPlayer(this.signaling, options, this.deliveries);
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Create a monitor: a player tuned for watching a stream you are interacting
|
|
758
|
+
* WITH rather than merely watching — the other side of a co-broadcast, where a
|
|
759
|
+
* second or two of delay makes the interaction feel broken.
|
|
760
|
+
*
|
|
761
|
+
* It is a player with the delay budget spent differently, not a different API:
|
|
762
|
+
* it starts on the real-time route and falls back on its own if that route
|
|
763
|
+
* delivers no frames. Apps used to hand-roll this (open a real-time view, run a
|
|
764
|
+
* timer, swap players when it stayed black); getting the fallback wrong showed a
|
|
765
|
+
* black frame to a live audience, so it belongs here rather than in every app.
|
|
766
|
+
*/
|
|
767
|
+
createMonitor() {
|
|
684
768
|
this.assertConnected();
|
|
685
|
-
return new MebiusPlayer(this.signaling,
|
|
769
|
+
return new MebiusPlayer(this.signaling, { mode: "low-latency" }, this.deliveries);
|
|
686
770
|
}
|
|
687
771
|
/** Close the connection and release resources. */
|
|
688
772
|
disconnect(reason) {
|
|
@@ -715,7 +799,7 @@ var Mebius = {
|
|
|
715
799
|
throw mebiusError("UNKNOWN", "Call Mebius.init() before Mebius.connect().");
|
|
716
800
|
}
|
|
717
801
|
if (!options.token) throw mebiusError("UNKNOWN", "Mebius.connect requires a token.");
|
|
718
|
-
const client = new MebiusClient(config, options.token);
|
|
802
|
+
const client = new MebiusClient(config, options.token, options.deliveries ?? []);
|
|
719
803
|
client.open();
|
|
720
804
|
return client;
|
|
721
805
|
},
|