@mebius-io/web 0.3.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 +25 -2
- package/dist/index.cjs +130 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -3
- package/dist/index.d.ts +32 -3
- package/dist/index.global.js +130 -9
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +130 -9
- package/dist/index.js.map +1 -1
- package/package.json +12 -11
- package/LICENSE +0 -21
package/README.md
CHANGED
|
@@ -55,12 +55,12 @@ external deps. `Mebius` becomes a global.
|
|
|
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.4.0/packages/web/standalone/mebius.min.js`
|
|
59
59
|
|
|
60
60
|
The drop-in file is not part of the npm package — `files` ships only `dist` — so it
|
|
61
61
|
is fetched from the tag, and the tag must match the version you installed. For a
|
|
62
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.
|
|
63
|
+
`https://esm.sh/@mebius-io/web@0.4.0`.
|
|
64
64
|
|
|
65
65
|
## Quick Start
|
|
66
66
|
|
|
@@ -162,6 +162,29 @@ Teruskan apa adanya — isinya opaque dan Mebius yang mengurutkan serta memilih.
|
|
|
162
162
|
Opsional: tanpa itu playback tetap jalan, tapi setiap penonton dilayani dari
|
|
163
163
|
origin Mebius, bukan edge terdekat.
|
|
164
164
|
|
|
165
|
+
### `beaconToken` / `beaconUrl` — quality di dashboard
|
|
166
|
+
|
|
167
|
+
Response token juga membawa dua field opsional. Teruskan keduanya dan SDK
|
|
168
|
+
melaporkan kualitas stream ini (bitrate, fps, rtt, jeda first-frame) tiap ~15
|
|
169
|
+
detik:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
const { token, deliveries, beaconToken, beaconUrl } = await (await fetch("/api/mebius-token")).json();
|
|
173
|
+
const client = Mebius.connect({ token, deliveries, beaconToken, beaconUrl });
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Itu yang mengisi **Quality → Publish / Play** di dashboard Mebius, dan yang jadi
|
|
177
|
+
dasar hitung **viewer minutes** — laporan sisi penonton adalah satu-satunya sumber
|
|
178
|
+
data itu, karena cuma client yang bisa melihat pengalaman penonton sebenarnya.
|
|
179
|
+
|
|
180
|
+
Aman di browser: kredensialnya terikat klaim bertanda tangan ke **satu** stream dan
|
|
181
|
+
**satu** project, jadi tak bisa dipakai menulis telemetri milik stream lain. SDK
|
|
182
|
+
tidak pernah tahu tenant-mu — informasi itu ada di dalam klaim, bukan di kode client.
|
|
183
|
+
|
|
184
|
+
Tanpa dua field itu stream tetap jalan normal; kamu hanya tidak melihat data
|
|
185
|
+
kualitasnya. Tambahkan `userId` di `connect()` kalau ingin laporan itu ikut membawa
|
|
186
|
+
id pengguna versimu.
|
|
187
|
+
|
|
165
188
|
## Integrasi per framework
|
|
166
189
|
|
|
167
190
|
### Vanilla JS
|
package/dist/index.cjs
CHANGED
|
@@ -423,17 +423,103 @@ function createViewCandidates(mode, signaling, deliveries = []) {
|
|
|
423
423
|
}
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
+
// src/internal/telemetry.ts
|
|
427
|
+
var SDK_VERSION = "web/0.4.0";
|
|
428
|
+
var FLUSH_INTERVAL_MS = 15e3;
|
|
429
|
+
var MAX_BATCH = 64;
|
|
430
|
+
function describeDevice() {
|
|
431
|
+
const nav = typeof navigator === "undefined" ? void 0 : navigator;
|
|
432
|
+
const uaData = nav?.userAgentData;
|
|
433
|
+
return { os: uaData?.platform || nav?.platform || void 0, sdk: SDK_VERSION };
|
|
434
|
+
}
|
|
435
|
+
function describeNetwork() {
|
|
436
|
+
const conn = typeof navigator === "undefined" ? void 0 : navigator.connection;
|
|
437
|
+
return conn?.effectiveType ? { type: conn.effectiveType } : void 0;
|
|
438
|
+
}
|
|
439
|
+
var QoeReporter = class {
|
|
440
|
+
constructor(target, role, streamId, userId) {
|
|
441
|
+
this.target = target;
|
|
442
|
+
this.role = role;
|
|
443
|
+
this.streamId = streamId;
|
|
444
|
+
this.userId = userId;
|
|
445
|
+
this.sessionId = randomId();
|
|
446
|
+
this.buffer = [];
|
|
447
|
+
this.timer = null;
|
|
448
|
+
this.unloadHandler = null;
|
|
449
|
+
}
|
|
450
|
+
start() {
|
|
451
|
+
if (this.timer) return;
|
|
452
|
+
this.timer = setInterval(() => void this.flush(), FLUSH_INTERVAL_MS);
|
|
453
|
+
if (typeof window !== "undefined") {
|
|
454
|
+
this.unloadHandler = () => void this.flush(true);
|
|
455
|
+
window.addEventListener("pagehide", this.unloadHandler);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
add(sample) {
|
|
459
|
+
this.buffer.push(sample);
|
|
460
|
+
if (this.buffer.length >= MAX_BATCH) void this.flush();
|
|
461
|
+
}
|
|
462
|
+
async stop() {
|
|
463
|
+
if (this.timer) clearInterval(this.timer);
|
|
464
|
+
this.timer = null;
|
|
465
|
+
if (this.unloadHandler && typeof window !== "undefined") {
|
|
466
|
+
window.removeEventListener("pagehide", this.unloadHandler);
|
|
467
|
+
}
|
|
468
|
+
this.unloadHandler = null;
|
|
469
|
+
await this.flush();
|
|
470
|
+
}
|
|
471
|
+
/** Send and clear the buffer. `beacon` uses sendBeacon, for page-unload flushes. */
|
|
472
|
+
async flush(beacon = false) {
|
|
473
|
+
if (!this.buffer.length) return;
|
|
474
|
+
const samples = this.buffer.splice(0, MAX_BATCH);
|
|
475
|
+
const body = JSON.stringify({
|
|
476
|
+
sessionId: this.sessionId,
|
|
477
|
+
streamId: this.streamId,
|
|
478
|
+
role: this.role,
|
|
479
|
+
userId: this.userId,
|
|
480
|
+
samples,
|
|
481
|
+
device: describeDevice(),
|
|
482
|
+
network: describeNetwork()
|
|
483
|
+
});
|
|
484
|
+
if (beacon && typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
485
|
+
const url = `${this.target.url}${this.target.url.includes("?") ? "&" : "?"}token=${encodeURIComponent(this.target.token)}`;
|
|
486
|
+
try {
|
|
487
|
+
navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
|
|
488
|
+
} catch {
|
|
489
|
+
}
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
try {
|
|
493
|
+
await fetch(this.target.url, {
|
|
494
|
+
method: "POST",
|
|
495
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.target.token}` },
|
|
496
|
+
body,
|
|
497
|
+
keepalive: true
|
|
498
|
+
});
|
|
499
|
+
} catch {
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
function randomId() {
|
|
504
|
+
const c = typeof crypto === "undefined" ? void 0 : crypto;
|
|
505
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
506
|
+
return `s-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
507
|
+
}
|
|
508
|
+
|
|
426
509
|
// src/broadcaster.ts
|
|
427
510
|
var STATS_INTERVAL_MS = 2e3;
|
|
428
511
|
var MebiusBroadcaster = class extends TypedEmitter {
|
|
429
512
|
/** @internal */
|
|
430
|
-
constructor(signaling, options) {
|
|
513
|
+
constructor(signaling, options, telemetry = null, userId) {
|
|
431
514
|
super();
|
|
432
515
|
this.options = options;
|
|
516
|
+
this.telemetry = telemetry;
|
|
517
|
+
this.userId = userId;
|
|
433
518
|
this.stream = null;
|
|
434
519
|
this.facingMode = "user";
|
|
435
520
|
this.statsTimer = null;
|
|
436
521
|
this.started = false;
|
|
522
|
+
this.reporter = null;
|
|
437
523
|
this.transport = createPublishTransport(signaling);
|
|
438
524
|
}
|
|
439
525
|
/** Begin broadcasting under the given stream id. */
|
|
@@ -442,12 +528,18 @@ var MebiusBroadcaster = class extends TypedEmitter {
|
|
|
442
528
|
this.stream = await this.capture();
|
|
443
529
|
await this.transport.start(streamId, this.stream);
|
|
444
530
|
this.started = true;
|
|
531
|
+
if (this.telemetry) {
|
|
532
|
+
this.reporter = new QoeReporter(this.telemetry, "pub", streamId, this.userId);
|
|
533
|
+
this.reporter.start();
|
|
534
|
+
}
|
|
445
535
|
this.startStats();
|
|
446
536
|
this.emit("started", { streamId });
|
|
447
537
|
}
|
|
448
538
|
/** Stop broadcasting and release the camera/microphone. */
|
|
449
539
|
async stop() {
|
|
450
540
|
this.stopStats();
|
|
541
|
+
await this.reporter?.stop();
|
|
542
|
+
this.reporter = null;
|
|
451
543
|
await this.transport.stop();
|
|
452
544
|
this.stream?.getTracks().forEach((t) => t.stop());
|
|
453
545
|
this.stream = null;
|
|
@@ -503,7 +595,14 @@ var MebiusBroadcaster = class extends TypedEmitter {
|
|
|
503
595
|
startStats() {
|
|
504
596
|
this.statsTimer = setInterval(async () => {
|
|
505
597
|
const stats = await this.transport.getStats();
|
|
506
|
-
if (stats)
|
|
598
|
+
if (!stats) return;
|
|
599
|
+
this.emit("stats", stats);
|
|
600
|
+
this.reporter?.add({
|
|
601
|
+
ts: Math.floor(Date.now() / 1e3),
|
|
602
|
+
bitrateKbps: stats.bitrateKbps,
|
|
603
|
+
fps: stats.framesPerSecond,
|
|
604
|
+
rttMs: stats.rttMs
|
|
605
|
+
});
|
|
507
606
|
}, STATS_INTERVAL_MS);
|
|
508
607
|
}
|
|
509
608
|
stopStats() {
|
|
@@ -521,12 +620,15 @@ var STATS_INTERVAL_MS2 = 2e3;
|
|
|
521
620
|
var FIRST_FRAME_TIMEOUT_MS = 8e3;
|
|
522
621
|
var MebiusPlayer = class extends TypedEmitter {
|
|
523
622
|
/** @internal */
|
|
524
|
-
constructor(signaling, options = {}, deliveries = []) {
|
|
623
|
+
constructor(signaling, options = {}, deliveries = [], telemetry = null, userId) {
|
|
525
624
|
super();
|
|
625
|
+
this.telemetry = telemetry;
|
|
626
|
+
this.userId = userId;
|
|
526
627
|
this.transport = null;
|
|
527
628
|
this.video = null;
|
|
528
629
|
this.statsTimer = null;
|
|
529
630
|
this.playing = false;
|
|
631
|
+
this.reporter = null;
|
|
530
632
|
this.candidates = createViewCandidates(options.mode ?? "auto", signaling, deliveries);
|
|
531
633
|
}
|
|
532
634
|
/** Start playing `streamId` into the given video element or selector. */
|
|
@@ -534,6 +636,7 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
534
636
|
if (this.playing) return;
|
|
535
637
|
const video = resolveVideoElement(viewTarget);
|
|
536
638
|
this.video = video;
|
|
639
|
+
const startedAtMs = Date.now();
|
|
537
640
|
let lastError = null;
|
|
538
641
|
for (const candidate of this.candidates) {
|
|
539
642
|
try {
|
|
@@ -542,6 +645,11 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
542
645
|
if (await hasFirstFrame(video)) {
|
|
543
646
|
this.transport = candidate;
|
|
544
647
|
this.playing = true;
|
|
648
|
+
if (this.telemetry) {
|
|
649
|
+
this.reporter = new QoeReporter(this.telemetry, "play", streamId, this.userId);
|
|
650
|
+
this.reporter.start();
|
|
651
|
+
this.reporter.add({ ts: Math.floor(Date.now() / 1e3), firstFrameMs: Date.now() - startedAtMs });
|
|
652
|
+
}
|
|
545
653
|
this.startStats();
|
|
546
654
|
this.emit("playing", { streamId });
|
|
547
655
|
return;
|
|
@@ -558,6 +666,8 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
558
666
|
/** Stop playback and detach from the video element. */
|
|
559
667
|
async stop() {
|
|
560
668
|
this.stopStats();
|
|
669
|
+
await this.reporter?.stop();
|
|
670
|
+
this.reporter = null;
|
|
561
671
|
await this.transport?.stop();
|
|
562
672
|
this.transport = null;
|
|
563
673
|
this.video = null;
|
|
@@ -573,6 +683,8 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
573
683
|
if (this.transport !== transport) return;
|
|
574
684
|
this.playing = false;
|
|
575
685
|
this.stopStats();
|
|
686
|
+
void this.reporter?.stop();
|
|
687
|
+
this.reporter = null;
|
|
576
688
|
this.emit("ended", void 0);
|
|
577
689
|
});
|
|
578
690
|
transport.onBuffering(() => {
|
|
@@ -583,7 +695,13 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
583
695
|
startStats() {
|
|
584
696
|
this.statsTimer = setInterval(async () => {
|
|
585
697
|
const stats = await this.transport?.getStats();
|
|
586
|
-
if (stats)
|
|
698
|
+
if (!stats) return;
|
|
699
|
+
this.emit("stats", stats);
|
|
700
|
+
this.reporter?.add({
|
|
701
|
+
ts: Math.floor(Date.now() / 1e3),
|
|
702
|
+
bitrateKbps: stats.bitrateKbps,
|
|
703
|
+
fps: stats.framesPerSecond
|
|
704
|
+
});
|
|
587
705
|
}, STATS_INTERVAL_MS2);
|
|
588
706
|
}
|
|
589
707
|
stopStats() {
|
|
@@ -718,10 +836,12 @@ function readToken(token) {
|
|
|
718
836
|
// src/client.ts
|
|
719
837
|
var MebiusClient = class extends TypedEmitter {
|
|
720
838
|
/** @internal */
|
|
721
|
-
constructor(config2, token, deliveries = []) {
|
|
839
|
+
constructor(config2, token, deliveries = [], telemetry = null, userId) {
|
|
722
840
|
super();
|
|
723
841
|
this.token = token;
|
|
724
842
|
this.deliveries = deliveries;
|
|
843
|
+
this.telemetry = telemetry;
|
|
844
|
+
this.userId = userId;
|
|
725
845
|
this.expiryTimer = null;
|
|
726
846
|
this.connected = false;
|
|
727
847
|
this.signaling = new SignalingClient(config2.gateway, token);
|
|
@@ -746,12 +866,12 @@ var MebiusClient = class extends TypedEmitter {
|
|
|
746
866
|
/** Create a broadcaster bound to this connection. */
|
|
747
867
|
createBroadcaster(options = {}) {
|
|
748
868
|
this.assertConnected();
|
|
749
|
-
return new MebiusBroadcaster(this.signaling, options);
|
|
869
|
+
return new MebiusBroadcaster(this.signaling, options, this.telemetry, this.userId);
|
|
750
870
|
}
|
|
751
871
|
/** Create a player bound to this connection. */
|
|
752
872
|
createPlayer(options = {}) {
|
|
753
873
|
this.assertConnected();
|
|
754
|
-
return new MebiusPlayer(this.signaling, options, this.deliveries);
|
|
874
|
+
return new MebiusPlayer(this.signaling, options, this.deliveries, this.telemetry, this.userId);
|
|
755
875
|
}
|
|
756
876
|
/**
|
|
757
877
|
* Create a monitor: a player tuned for watching a stream you are interacting
|
|
@@ -766,7 +886,7 @@ var MebiusClient = class extends TypedEmitter {
|
|
|
766
886
|
*/
|
|
767
887
|
createMonitor() {
|
|
768
888
|
this.assertConnected();
|
|
769
|
-
return new MebiusPlayer(this.signaling, { mode: "low-latency" }, this.deliveries);
|
|
889
|
+
return new MebiusPlayer(this.signaling, { mode: "low-latency" }, this.deliveries, this.telemetry, this.userId);
|
|
770
890
|
}
|
|
771
891
|
/** Close the connection and release resources. */
|
|
772
892
|
disconnect(reason) {
|
|
@@ -799,7 +919,8 @@ var Mebius = {
|
|
|
799
919
|
throw mebiusError("UNKNOWN", "Call Mebius.init() before Mebius.connect().");
|
|
800
920
|
}
|
|
801
921
|
if (!options.token) throw mebiusError("UNKNOWN", "Mebius.connect requires a token.");
|
|
802
|
-
const
|
|
922
|
+
const telemetry = options.beaconToken && options.beaconUrl ? { token: options.beaconToken, url: options.beaconUrl } : null;
|
|
923
|
+
const client = new MebiusClient(config, options.token, options.deliveries ?? [], telemetry, options.userId);
|
|
803
924
|
client.open();
|
|
804
925
|
return client;
|
|
805
926
|
},
|