@mebius-io/web 0.2.0 → 0.4.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 +77 -12
- package/dist/index.cjs +306 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +89 -10
- package/dist/index.d.ts +89 -10
- package/dist/index.global.js +9834 -37
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +306 -28
- package/dist/index.js.map +1 -1
- package/package.json +13 -11
- package/LICENSE +0 -21
package/dist/index.js
CHANGED
|
@@ -222,8 +222,15 @@ var WhepViewTransport = class {
|
|
|
222
222
|
|
|
223
223
|
// src/internal/scale-view-transport.ts
|
|
224
224
|
var HlsViewTransport = class {
|
|
225
|
-
|
|
225
|
+
/**
|
|
226
|
+
* deliveryPath, when given, is a gateway-relative path from the gateway's own
|
|
227
|
+
* delivery list — that is how a CDN-backed playlist gets used instead of the
|
|
228
|
+
* origin one. Without it this falls back to the origin playlist, which is
|
|
229
|
+
* still correct, just served from our own bandwidth.
|
|
230
|
+
*/
|
|
231
|
+
constructor(signaling, deliveryPath) {
|
|
226
232
|
this.signaling = signaling;
|
|
233
|
+
this.deliveryPath = deliveryPath;
|
|
227
234
|
this.hls = null;
|
|
228
235
|
this.video = null;
|
|
229
236
|
this.endedCb = null;
|
|
@@ -237,7 +244,7 @@ var HlsViewTransport = class {
|
|
|
237
244
|
}
|
|
238
245
|
async start(streamId, video) {
|
|
239
246
|
this.video = video;
|
|
240
|
-
const url = this.signaling.scalePlaylistUrl(streamId);
|
|
247
|
+
const url = this.deliveryPath ? this.signaling.deliveryUrl(this.deliveryPath) : this.signaling.scalePlaylistUrl(streamId);
|
|
241
248
|
video.addEventListener("ended", () => this.endedCb?.());
|
|
242
249
|
video.addEventListener("waiting", () => this.bufferingCb?.());
|
|
243
250
|
if (video.canPlayType("application/vnd.apple.mpegurl")) {
|
|
@@ -283,30 +290,195 @@ var HlsViewTransport = class {
|
|
|
283
290
|
}
|
|
284
291
|
};
|
|
285
292
|
|
|
293
|
+
// src/internal/balanced-view-transport.ts
|
|
294
|
+
var FlvViewTransport = class {
|
|
295
|
+
constructor(signaling, deliveryPath) {
|
|
296
|
+
this.signaling = signaling;
|
|
297
|
+
this.deliveryPath = deliveryPath;
|
|
298
|
+
this.player = null;
|
|
299
|
+
this.video = null;
|
|
300
|
+
this.endedCb = null;
|
|
301
|
+
this.bufferingCb = null;
|
|
302
|
+
}
|
|
303
|
+
onEnded(cb) {
|
|
304
|
+
this.endedCb = cb;
|
|
305
|
+
}
|
|
306
|
+
onBuffering(cb) {
|
|
307
|
+
this.bufferingCb = cb;
|
|
308
|
+
}
|
|
309
|
+
async start(_streamId, video) {
|
|
310
|
+
this.video = video;
|
|
311
|
+
const url = this.signaling.deliveryUrl(this.deliveryPath);
|
|
312
|
+
video.addEventListener("ended", () => this.endedCb?.());
|
|
313
|
+
video.addEventListener("waiting", () => this.bufferingCb?.());
|
|
314
|
+
let mod;
|
|
315
|
+
try {
|
|
316
|
+
mod = await import("flv.js");
|
|
317
|
+
} catch (cause) {
|
|
318
|
+
throw mebiusError("CONNECTION_FAILED", "Balanced playback support failed to load.", cause);
|
|
319
|
+
}
|
|
320
|
+
const flvjs = mod.default;
|
|
321
|
+
if (!flvjs.isSupported()) {
|
|
322
|
+
throw mebiusError("CONNECTION_FAILED", "Balanced playback is not supported in this browser.");
|
|
323
|
+
}
|
|
324
|
+
const player = flvjs.createPlayer({ type: "flv", url, isLive: true });
|
|
325
|
+
this.player = player;
|
|
326
|
+
player.on(flvjs.Events.ERROR ?? "error", () => this.bufferingCb?.());
|
|
327
|
+
player.attachMediaElement(video);
|
|
328
|
+
player.load();
|
|
329
|
+
await Promise.resolve(player.play()).catch(() => void 0);
|
|
330
|
+
}
|
|
331
|
+
async stop() {
|
|
332
|
+
if (this.player) {
|
|
333
|
+
this.player.unload();
|
|
334
|
+
this.player.detachMediaElement();
|
|
335
|
+
this.player.destroy();
|
|
336
|
+
this.player = null;
|
|
337
|
+
}
|
|
338
|
+
if (this.video) {
|
|
339
|
+
this.video.removeAttribute("src");
|
|
340
|
+
this.video.load();
|
|
341
|
+
}
|
|
342
|
+
this.video = null;
|
|
343
|
+
}
|
|
344
|
+
async getStats() {
|
|
345
|
+
if (!this.video) return null;
|
|
346
|
+
return {
|
|
347
|
+
bitrateKbps: 0,
|
|
348
|
+
framesPerSecond: 0,
|
|
349
|
+
latencyMs: void 0
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
286
354
|
// src/internal/transport.ts
|
|
287
355
|
function createPublishTransport(signaling) {
|
|
288
356
|
return new WhipPublishTransport(signaling);
|
|
289
357
|
}
|
|
290
|
-
|
|
358
|
+
var KIND_FAST = "fast";
|
|
359
|
+
var KIND_WIDE = "wide";
|
|
360
|
+
var KIND_LOCAL = "local";
|
|
361
|
+
function canPlayBuffered() {
|
|
362
|
+
return typeof MediaSource !== "undefined";
|
|
363
|
+
}
|
|
364
|
+
function transportFor(kind, path, signaling) {
|
|
365
|
+
if (kind === KIND_FAST) return canPlayBuffered() ? new FlvViewTransport(signaling, path) : null;
|
|
366
|
+
if (kind === KIND_WIDE || kind === KIND_LOCAL) return new HlsViewTransport(signaling, path);
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
function createViewCandidates(mode, signaling, deliveries = []) {
|
|
370
|
+
const fromGateway = (kinds) => deliveries.filter((d) => kinds.includes(d.kind)).map((d) => transportFor(d.kind, d.path, signaling)).filter((t) => t !== null);
|
|
371
|
+
const originFallback = new HlsViewTransport(signaling);
|
|
372
|
+
const allKinds = [KIND_FAST, KIND_WIDE, KIND_LOCAL];
|
|
291
373
|
switch (mode) {
|
|
292
374
|
case "low-latency":
|
|
293
|
-
return new WhepViewTransport(signaling);
|
|
375
|
+
return [new WhepViewTransport(signaling), ...fromGateway(allKinds), originFallback];
|
|
376
|
+
case "balanced":
|
|
377
|
+
return [...fromGateway(allKinds), originFallback];
|
|
294
378
|
case "scale":
|
|
295
|
-
return
|
|
379
|
+
return [...fromGateway([KIND_WIDE, KIND_LOCAL]), originFallback];
|
|
380
|
+
case "auto":
|
|
381
|
+
return [...fromGateway(allKinds), originFallback];
|
|
296
382
|
}
|
|
297
383
|
}
|
|
298
384
|
|
|
385
|
+
// src/internal/telemetry.ts
|
|
386
|
+
var SDK_VERSION = "web/0.4.0";
|
|
387
|
+
var FLUSH_INTERVAL_MS = 15e3;
|
|
388
|
+
var MAX_BATCH = 64;
|
|
389
|
+
function describeDevice() {
|
|
390
|
+
const nav = typeof navigator === "undefined" ? void 0 : navigator;
|
|
391
|
+
const uaData = nav?.userAgentData;
|
|
392
|
+
return { os: uaData?.platform || nav?.platform || void 0, sdk: SDK_VERSION };
|
|
393
|
+
}
|
|
394
|
+
function describeNetwork() {
|
|
395
|
+
const conn = typeof navigator === "undefined" ? void 0 : navigator.connection;
|
|
396
|
+
return conn?.effectiveType ? { type: conn.effectiveType } : void 0;
|
|
397
|
+
}
|
|
398
|
+
var QoeReporter = class {
|
|
399
|
+
constructor(target, role, streamId, userId) {
|
|
400
|
+
this.target = target;
|
|
401
|
+
this.role = role;
|
|
402
|
+
this.streamId = streamId;
|
|
403
|
+
this.userId = userId;
|
|
404
|
+
this.sessionId = randomId();
|
|
405
|
+
this.buffer = [];
|
|
406
|
+
this.timer = null;
|
|
407
|
+
this.unloadHandler = null;
|
|
408
|
+
}
|
|
409
|
+
start() {
|
|
410
|
+
if (this.timer) return;
|
|
411
|
+
this.timer = setInterval(() => void this.flush(), FLUSH_INTERVAL_MS);
|
|
412
|
+
if (typeof window !== "undefined") {
|
|
413
|
+
this.unloadHandler = () => void this.flush(true);
|
|
414
|
+
window.addEventListener("pagehide", this.unloadHandler);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
add(sample) {
|
|
418
|
+
this.buffer.push(sample);
|
|
419
|
+
if (this.buffer.length >= MAX_BATCH) void this.flush();
|
|
420
|
+
}
|
|
421
|
+
async stop() {
|
|
422
|
+
if (this.timer) clearInterval(this.timer);
|
|
423
|
+
this.timer = null;
|
|
424
|
+
if (this.unloadHandler && typeof window !== "undefined") {
|
|
425
|
+
window.removeEventListener("pagehide", this.unloadHandler);
|
|
426
|
+
}
|
|
427
|
+
this.unloadHandler = null;
|
|
428
|
+
await this.flush();
|
|
429
|
+
}
|
|
430
|
+
/** Send and clear the buffer. `beacon` uses sendBeacon, for page-unload flushes. */
|
|
431
|
+
async flush(beacon = false) {
|
|
432
|
+
if (!this.buffer.length) return;
|
|
433
|
+
const samples = this.buffer.splice(0, MAX_BATCH);
|
|
434
|
+
const body = JSON.stringify({
|
|
435
|
+
sessionId: this.sessionId,
|
|
436
|
+
streamId: this.streamId,
|
|
437
|
+
role: this.role,
|
|
438
|
+
userId: this.userId,
|
|
439
|
+
samples,
|
|
440
|
+
device: describeDevice(),
|
|
441
|
+
network: describeNetwork()
|
|
442
|
+
});
|
|
443
|
+
if (beacon && typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
444
|
+
const url = `${this.target.url}${this.target.url.includes("?") ? "&" : "?"}token=${encodeURIComponent(this.target.token)}`;
|
|
445
|
+
try {
|
|
446
|
+
navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
|
|
447
|
+
} catch {
|
|
448
|
+
}
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
await fetch(this.target.url, {
|
|
453
|
+
method: "POST",
|
|
454
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.target.token}` },
|
|
455
|
+
body,
|
|
456
|
+
keepalive: true
|
|
457
|
+
});
|
|
458
|
+
} catch {
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
function randomId() {
|
|
463
|
+
const c = typeof crypto === "undefined" ? void 0 : crypto;
|
|
464
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
465
|
+
return `s-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
466
|
+
}
|
|
467
|
+
|
|
299
468
|
// src/broadcaster.ts
|
|
300
469
|
var STATS_INTERVAL_MS = 2e3;
|
|
301
470
|
var MebiusBroadcaster = class extends TypedEmitter {
|
|
302
471
|
/** @internal */
|
|
303
|
-
constructor(signaling, options) {
|
|
472
|
+
constructor(signaling, options, telemetry = null, userId) {
|
|
304
473
|
super();
|
|
305
474
|
this.options = options;
|
|
475
|
+
this.telemetry = telemetry;
|
|
476
|
+
this.userId = userId;
|
|
306
477
|
this.stream = null;
|
|
307
478
|
this.facingMode = "user";
|
|
308
479
|
this.statsTimer = null;
|
|
309
480
|
this.started = false;
|
|
481
|
+
this.reporter = null;
|
|
310
482
|
this.transport = createPublishTransport(signaling);
|
|
311
483
|
}
|
|
312
484
|
/** Begin broadcasting under the given stream id. */
|
|
@@ -315,12 +487,18 @@ var MebiusBroadcaster = class extends TypedEmitter {
|
|
|
315
487
|
this.stream = await this.capture();
|
|
316
488
|
await this.transport.start(streamId, this.stream);
|
|
317
489
|
this.started = true;
|
|
490
|
+
if (this.telemetry) {
|
|
491
|
+
this.reporter = new QoeReporter(this.telemetry, "pub", streamId, this.userId);
|
|
492
|
+
this.reporter.start();
|
|
493
|
+
}
|
|
318
494
|
this.startStats();
|
|
319
495
|
this.emit("started", { streamId });
|
|
320
496
|
}
|
|
321
497
|
/** Stop broadcasting and release the camera/microphone. */
|
|
322
498
|
async stop() {
|
|
323
499
|
this.stopStats();
|
|
500
|
+
await this.reporter?.stop();
|
|
501
|
+
this.reporter = null;
|
|
324
502
|
await this.transport.stop();
|
|
325
503
|
this.stream?.getTracks().forEach((t) => t.stop());
|
|
326
504
|
this.stream = null;
|
|
@@ -376,7 +554,14 @@ var MebiusBroadcaster = class extends TypedEmitter {
|
|
|
376
554
|
startStats() {
|
|
377
555
|
this.statsTimer = setInterval(async () => {
|
|
378
556
|
const stats = await this.transport.getStats();
|
|
379
|
-
if (stats)
|
|
557
|
+
if (!stats) return;
|
|
558
|
+
this.emit("stats", stats);
|
|
559
|
+
this.reporter?.add({
|
|
560
|
+
ts: Math.floor(Date.now() / 1e3),
|
|
561
|
+
bitrateKbps: stats.bitrateKbps,
|
|
562
|
+
fps: stats.framesPerSecond,
|
|
563
|
+
rttMs: stats.rttMs
|
|
564
|
+
});
|
|
380
565
|
}, STATS_INTERVAL_MS);
|
|
381
566
|
}
|
|
382
567
|
stopStats() {
|
|
@@ -391,34 +576,59 @@ function normalize(c, fallback) {
|
|
|
391
576
|
|
|
392
577
|
// src/player.ts
|
|
393
578
|
var STATS_INTERVAL_MS2 = 2e3;
|
|
579
|
+
var FIRST_FRAME_TIMEOUT_MS = 8e3;
|
|
394
580
|
var MebiusPlayer = class extends TypedEmitter {
|
|
395
581
|
/** @internal */
|
|
396
|
-
constructor(signaling, options) {
|
|
582
|
+
constructor(signaling, options = {}, deliveries = [], telemetry = null, userId) {
|
|
397
583
|
super();
|
|
584
|
+
this.telemetry = telemetry;
|
|
585
|
+
this.userId = userId;
|
|
586
|
+
this.transport = null;
|
|
398
587
|
this.video = null;
|
|
399
588
|
this.statsTimer = null;
|
|
400
589
|
this.playing = false;
|
|
401
|
-
this.
|
|
402
|
-
this.
|
|
403
|
-
this.playing = false;
|
|
404
|
-
this.stopStats();
|
|
405
|
-
this.emit("ended", void 0);
|
|
406
|
-
});
|
|
407
|
-
this.transport.onBuffering(() => this.emit("buffering", void 0));
|
|
590
|
+
this.reporter = null;
|
|
591
|
+
this.candidates = createViewCandidates(options.mode ?? "auto", signaling, deliveries);
|
|
408
592
|
}
|
|
409
593
|
/** Start playing `streamId` into the given video element or selector. */
|
|
410
594
|
async play(streamId, viewTarget) {
|
|
411
595
|
if (this.playing) return;
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
596
|
+
const video = resolveVideoElement(viewTarget);
|
|
597
|
+
this.video = video;
|
|
598
|
+
const startedAtMs = Date.now();
|
|
599
|
+
let lastError = null;
|
|
600
|
+
for (const candidate of this.candidates) {
|
|
601
|
+
try {
|
|
602
|
+
this.attach(candidate);
|
|
603
|
+
await candidate.start(streamId, video);
|
|
604
|
+
if (await hasFirstFrame(video)) {
|
|
605
|
+
this.transport = candidate;
|
|
606
|
+
this.playing = true;
|
|
607
|
+
if (this.telemetry) {
|
|
608
|
+
this.reporter = new QoeReporter(this.telemetry, "play", streamId, this.userId);
|
|
609
|
+
this.reporter.start();
|
|
610
|
+
this.reporter.add({ ts: Math.floor(Date.now() / 1e3), firstFrameMs: Date.now() - startedAtMs });
|
|
611
|
+
}
|
|
612
|
+
this.startStats();
|
|
613
|
+
this.emit("playing", { streamId });
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
lastError = mebiusError("CONNECTION_FAILED", "A Mebius route delivered no video.");
|
|
617
|
+
} catch (cause) {
|
|
618
|
+
lastError = cause;
|
|
619
|
+
}
|
|
620
|
+
await candidate.stop().catch(() => void 0);
|
|
621
|
+
}
|
|
622
|
+
this.video = null;
|
|
623
|
+
throw lastError ?? mebiusError("CONNECTION_FAILED", "No Mebius route could play this stream.");
|
|
417
624
|
}
|
|
418
625
|
/** Stop playback and detach from the video element. */
|
|
419
626
|
async stop() {
|
|
420
627
|
this.stopStats();
|
|
421
|
-
await this.
|
|
628
|
+
await this.reporter?.stop();
|
|
629
|
+
this.reporter = null;
|
|
630
|
+
await this.transport?.stop();
|
|
631
|
+
this.transport = null;
|
|
422
632
|
this.video = null;
|
|
423
633
|
this.playing = false;
|
|
424
634
|
}
|
|
@@ -427,10 +637,30 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
427
637
|
const v = Math.min(1, Math.max(0, volume));
|
|
428
638
|
if (this.video) this.video.volume = v;
|
|
429
639
|
}
|
|
640
|
+
attach(transport) {
|
|
641
|
+
transport.onEnded(() => {
|
|
642
|
+
if (this.transport !== transport) return;
|
|
643
|
+
this.playing = false;
|
|
644
|
+
this.stopStats();
|
|
645
|
+
void this.reporter?.stop();
|
|
646
|
+
this.reporter = null;
|
|
647
|
+
this.emit("ended", void 0);
|
|
648
|
+
});
|
|
649
|
+
transport.onBuffering(() => {
|
|
650
|
+
if (this.transport !== transport) return;
|
|
651
|
+
this.emit("buffering", void 0);
|
|
652
|
+
});
|
|
653
|
+
}
|
|
430
654
|
startStats() {
|
|
431
655
|
this.statsTimer = setInterval(async () => {
|
|
432
|
-
const stats = await this.transport
|
|
433
|
-
if (stats)
|
|
656
|
+
const stats = await this.transport?.getStats();
|
|
657
|
+
if (!stats) return;
|
|
658
|
+
this.emit("stats", stats);
|
|
659
|
+
this.reporter?.add({
|
|
660
|
+
ts: Math.floor(Date.now() / 1e3),
|
|
661
|
+
bitrateKbps: stats.bitrateKbps,
|
|
662
|
+
fps: stats.framesPerSecond
|
|
663
|
+
});
|
|
434
664
|
}, STATS_INTERVAL_MS2);
|
|
435
665
|
}
|
|
436
666
|
stopStats() {
|
|
@@ -438,6 +668,21 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
438
668
|
this.statsTimer = null;
|
|
439
669
|
}
|
|
440
670
|
};
|
|
671
|
+
function hasFirstFrame(video) {
|
|
672
|
+
if (video.currentTime > 0 && !video.paused) return Promise.resolve(true);
|
|
673
|
+
return new Promise((resolve) => {
|
|
674
|
+
const done = (ok) => {
|
|
675
|
+
clearTimeout(timer);
|
|
676
|
+
video.removeEventListener("timeupdate", onTime);
|
|
677
|
+
resolve(ok);
|
|
678
|
+
};
|
|
679
|
+
const onTime = () => {
|
|
680
|
+
if (video.currentTime > 0) done(true);
|
|
681
|
+
};
|
|
682
|
+
const timer = setTimeout(() => done(false), FIRST_FRAME_TIMEOUT_MS);
|
|
683
|
+
video.addEventListener("timeupdate", onTime);
|
|
684
|
+
});
|
|
685
|
+
}
|
|
441
686
|
|
|
442
687
|
// src/internal/signaling.ts
|
|
443
688
|
var SignalingClient = class {
|
|
@@ -461,6 +706,20 @@ var SignalingClient = class {
|
|
|
461
706
|
// Build the playlist URL used by scale-mode playback (HLS path, hidden). The
|
|
462
707
|
// engine serves the playlist under /live/{id}/index.m3u8 and requires the
|
|
463
708
|
// token in the query; segment URIs in the playlist inherit it automatically.
|
|
709
|
+
/**
|
|
710
|
+
* Absolute, tokenized URL for a gateway-relative delivery path handed to us
|
|
711
|
+
* by the gateway (`deliveries[].path`). The gateway decides which paths exist
|
|
712
|
+
* and in what order; the SDK only resolves them against its own base and
|
|
713
|
+
* attaches the access token. Anything that is not a plain gateway-relative
|
|
714
|
+
* path is rejected rather than fetched: an absolute URL there would send the
|
|
715
|
+
* token to a host we did not choose.
|
|
716
|
+
*/
|
|
717
|
+
deliveryUrl(path) {
|
|
718
|
+
if (!path.startsWith("/") || path.startsWith("//") || path.includes("://")) {
|
|
719
|
+
throw mebiusError("CONNECTION_FAILED", "The gateway returned an unusable delivery path.");
|
|
720
|
+
}
|
|
721
|
+
return this.withToken(`${this.base()}${path}`);
|
|
722
|
+
}
|
|
464
723
|
/** Playlist URL for scale-mode playback. */
|
|
465
724
|
scalePlaylistUrl(streamId) {
|
|
466
725
|
return this.withToken(`${this.base()}/live/${encodeURIComponent(streamId)}/index.m3u8`);
|
|
@@ -536,9 +795,12 @@ function readToken(token) {
|
|
|
536
795
|
// src/client.ts
|
|
537
796
|
var MebiusClient = class extends TypedEmitter {
|
|
538
797
|
/** @internal */
|
|
539
|
-
constructor(config2, token) {
|
|
798
|
+
constructor(config2, token, deliveries = [], telemetry = null, userId) {
|
|
540
799
|
super();
|
|
541
800
|
this.token = token;
|
|
801
|
+
this.deliveries = deliveries;
|
|
802
|
+
this.telemetry = telemetry;
|
|
803
|
+
this.userId = userId;
|
|
542
804
|
this.expiryTimer = null;
|
|
543
805
|
this.connected = false;
|
|
544
806
|
this.signaling = new SignalingClient(config2.gateway, token);
|
|
@@ -563,12 +825,27 @@ var MebiusClient = class extends TypedEmitter {
|
|
|
563
825
|
/** Create a broadcaster bound to this connection. */
|
|
564
826
|
createBroadcaster(options = {}) {
|
|
565
827
|
this.assertConnected();
|
|
566
|
-
return new MebiusBroadcaster(this.signaling, options);
|
|
828
|
+
return new MebiusBroadcaster(this.signaling, options, this.telemetry, this.userId);
|
|
567
829
|
}
|
|
568
830
|
/** Create a player bound to this connection. */
|
|
569
|
-
createPlayer(options) {
|
|
831
|
+
createPlayer(options = {}) {
|
|
832
|
+
this.assertConnected();
|
|
833
|
+
return new MebiusPlayer(this.signaling, options, this.deliveries, this.telemetry, this.userId);
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Create a monitor: a player tuned for watching a stream you are interacting
|
|
837
|
+
* WITH rather than merely watching — the other side of a co-broadcast, where a
|
|
838
|
+
* second or two of delay makes the interaction feel broken.
|
|
839
|
+
*
|
|
840
|
+
* It is a player with the delay budget spent differently, not a different API:
|
|
841
|
+
* it starts on the real-time route and falls back on its own if that route
|
|
842
|
+
* delivers no frames. Apps used to hand-roll this (open a real-time view, run a
|
|
843
|
+
* timer, swap players when it stayed black); getting the fallback wrong showed a
|
|
844
|
+
* black frame to a live audience, so it belongs here rather than in every app.
|
|
845
|
+
*/
|
|
846
|
+
createMonitor() {
|
|
570
847
|
this.assertConnected();
|
|
571
|
-
return new MebiusPlayer(this.signaling,
|
|
848
|
+
return new MebiusPlayer(this.signaling, { mode: "low-latency" }, this.deliveries, this.telemetry, this.userId);
|
|
572
849
|
}
|
|
573
850
|
/** Close the connection and release resources. */
|
|
574
851
|
disconnect(reason) {
|
|
@@ -601,7 +878,8 @@ var Mebius = {
|
|
|
601
878
|
throw mebiusError("UNKNOWN", "Call Mebius.init() before Mebius.connect().");
|
|
602
879
|
}
|
|
603
880
|
if (!options.token) throw mebiusError("UNKNOWN", "Mebius.connect requires a token.");
|
|
604
|
-
const
|
|
881
|
+
const telemetry = options.beaconToken && options.beaconUrl ? { token: options.beaconToken, url: options.beaconUrl } : null;
|
|
882
|
+
const client = new MebiusClient(config, options.token, options.deliveries ?? [], telemetry, options.userId);
|
|
605
883
|
client.open();
|
|
606
884
|
return client;
|
|
607
885
|
},
|