@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 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.3.0/packages/web/standalone/mebius.min.js`
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.3.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) this.emit("stats", 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) this.emit("stats", 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 client = new MebiusClient(config, options.token, options.deliveries ?? []);
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
  },