@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.
@@ -42999,17 +42999,103 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42999
42999
  }
43000
43000
  }
43001
43001
 
43002
+ // src/internal/telemetry.ts
43003
+ var SDK_VERSION = "web/0.4.0";
43004
+ var FLUSH_INTERVAL_MS = 15e3;
43005
+ var MAX_BATCH = 64;
43006
+ function describeDevice() {
43007
+ const nav = typeof navigator === "undefined" ? void 0 : navigator;
43008
+ const uaData = nav?.userAgentData;
43009
+ return { os: uaData?.platform || nav?.platform || void 0, sdk: SDK_VERSION };
43010
+ }
43011
+ function describeNetwork() {
43012
+ const conn = typeof navigator === "undefined" ? void 0 : navigator.connection;
43013
+ return conn?.effectiveType ? { type: conn.effectiveType } : void 0;
43014
+ }
43015
+ var QoeReporter = class {
43016
+ constructor(target, role, streamId, userId) {
43017
+ this.target = target;
43018
+ this.role = role;
43019
+ this.streamId = streamId;
43020
+ this.userId = userId;
43021
+ this.sessionId = randomId();
43022
+ this.buffer = [];
43023
+ this.timer = null;
43024
+ this.unloadHandler = null;
43025
+ }
43026
+ start() {
43027
+ if (this.timer) return;
43028
+ this.timer = setInterval(() => void this.flush(), FLUSH_INTERVAL_MS);
43029
+ if (typeof window !== "undefined") {
43030
+ this.unloadHandler = () => void this.flush(true);
43031
+ window.addEventListener("pagehide", this.unloadHandler);
43032
+ }
43033
+ }
43034
+ add(sample) {
43035
+ this.buffer.push(sample);
43036
+ if (this.buffer.length >= MAX_BATCH) void this.flush();
43037
+ }
43038
+ async stop() {
43039
+ if (this.timer) clearInterval(this.timer);
43040
+ this.timer = null;
43041
+ if (this.unloadHandler && typeof window !== "undefined") {
43042
+ window.removeEventListener("pagehide", this.unloadHandler);
43043
+ }
43044
+ this.unloadHandler = null;
43045
+ await this.flush();
43046
+ }
43047
+ /** Send and clear the buffer. `beacon` uses sendBeacon, for page-unload flushes. */
43048
+ async flush(beacon = false) {
43049
+ if (!this.buffer.length) return;
43050
+ const samples = this.buffer.splice(0, MAX_BATCH);
43051
+ const body = JSON.stringify({
43052
+ sessionId: this.sessionId,
43053
+ streamId: this.streamId,
43054
+ role: this.role,
43055
+ userId: this.userId,
43056
+ samples,
43057
+ device: describeDevice(),
43058
+ network: describeNetwork()
43059
+ });
43060
+ if (beacon && typeof navigator !== "undefined" && navigator.sendBeacon) {
43061
+ const url = `${this.target.url}${this.target.url.includes("?") ? "&" : "?"}token=${encodeURIComponent(this.target.token)}`;
43062
+ try {
43063
+ navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
43064
+ } catch {
43065
+ }
43066
+ return;
43067
+ }
43068
+ try {
43069
+ await fetch(this.target.url, {
43070
+ method: "POST",
43071
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.target.token}` },
43072
+ body,
43073
+ keepalive: true
43074
+ });
43075
+ } catch {
43076
+ }
43077
+ }
43078
+ };
43079
+ function randomId() {
43080
+ const c = typeof crypto === "undefined" ? void 0 : crypto;
43081
+ if (c?.randomUUID) return c.randomUUID();
43082
+ return `s-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
43083
+ }
43084
+
43002
43085
  // src/broadcaster.ts
43003
43086
  var STATS_INTERVAL_MS = 2e3;
43004
43087
  var MebiusBroadcaster = class extends TypedEmitter {
43005
43088
  /** @internal */
43006
- constructor(signaling, options) {
43089
+ constructor(signaling, options, telemetry = null, userId) {
43007
43090
  super();
43008
43091
  this.options = options;
43092
+ this.telemetry = telemetry;
43093
+ this.userId = userId;
43009
43094
  this.stream = null;
43010
43095
  this.facingMode = "user";
43011
43096
  this.statsTimer = null;
43012
43097
  this.started = false;
43098
+ this.reporter = null;
43013
43099
  this.transport = createPublishTransport(signaling);
43014
43100
  }
43015
43101
  /** Begin broadcasting under the given stream id. */
@@ -43018,12 +43104,18 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43018
43104
  this.stream = await this.capture();
43019
43105
  await this.transport.start(streamId, this.stream);
43020
43106
  this.started = true;
43107
+ if (this.telemetry) {
43108
+ this.reporter = new QoeReporter(this.telemetry, "pub", streamId, this.userId);
43109
+ this.reporter.start();
43110
+ }
43021
43111
  this.startStats();
43022
43112
  this.emit("started", { streamId });
43023
43113
  }
43024
43114
  /** Stop broadcasting and release the camera/microphone. */
43025
43115
  async stop() {
43026
43116
  this.stopStats();
43117
+ await this.reporter?.stop();
43118
+ this.reporter = null;
43027
43119
  await this.transport.stop();
43028
43120
  this.stream?.getTracks().forEach((t) => t.stop());
43029
43121
  this.stream = null;
@@ -43079,7 +43171,14 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43079
43171
  startStats() {
43080
43172
  this.statsTimer = setInterval(async () => {
43081
43173
  const stats = await this.transport.getStats();
43082
- if (stats) this.emit("stats", stats);
43174
+ if (!stats) return;
43175
+ this.emit("stats", stats);
43176
+ this.reporter?.add({
43177
+ ts: Math.floor(Date.now() / 1e3),
43178
+ bitrateKbps: stats.bitrateKbps,
43179
+ fps: stats.framesPerSecond,
43180
+ rttMs: stats.rttMs
43181
+ });
43083
43182
  }, STATS_INTERVAL_MS);
43084
43183
  }
43085
43184
  stopStats() {
@@ -43097,12 +43196,15 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43097
43196
  var FIRST_FRAME_TIMEOUT_MS = 8e3;
43098
43197
  var MebiusPlayer = class extends TypedEmitter {
43099
43198
  /** @internal */
43100
- constructor(signaling, options = {}, deliveries = []) {
43199
+ constructor(signaling, options = {}, deliveries = [], telemetry = null, userId) {
43101
43200
  super();
43201
+ this.telemetry = telemetry;
43202
+ this.userId = userId;
43102
43203
  this.transport = null;
43103
43204
  this.video = null;
43104
43205
  this.statsTimer = null;
43105
43206
  this.playing = false;
43207
+ this.reporter = null;
43106
43208
  this.candidates = createViewCandidates(options.mode ?? "auto", signaling, deliveries);
43107
43209
  }
43108
43210
  /** Start playing `streamId` into the given video element or selector. */
@@ -43110,6 +43212,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43110
43212
  if (this.playing) return;
43111
43213
  const video = resolveVideoElement(viewTarget);
43112
43214
  this.video = video;
43215
+ const startedAtMs = Date.now();
43113
43216
  let lastError = null;
43114
43217
  for (const candidate of this.candidates) {
43115
43218
  try {
@@ -43118,6 +43221,11 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43118
43221
  if (await hasFirstFrame(video)) {
43119
43222
  this.transport = candidate;
43120
43223
  this.playing = true;
43224
+ if (this.telemetry) {
43225
+ this.reporter = new QoeReporter(this.telemetry, "play", streamId, this.userId);
43226
+ this.reporter.start();
43227
+ this.reporter.add({ ts: Math.floor(Date.now() / 1e3), firstFrameMs: Date.now() - startedAtMs });
43228
+ }
43121
43229
  this.startStats();
43122
43230
  this.emit("playing", { streamId });
43123
43231
  return;
@@ -43134,6 +43242,8 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43134
43242
  /** Stop playback and detach from the video element. */
43135
43243
  async stop() {
43136
43244
  this.stopStats();
43245
+ await this.reporter?.stop();
43246
+ this.reporter = null;
43137
43247
  await this.transport?.stop();
43138
43248
  this.transport = null;
43139
43249
  this.video = null;
@@ -43149,6 +43259,8 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43149
43259
  if (this.transport !== transport) return;
43150
43260
  this.playing = false;
43151
43261
  this.stopStats();
43262
+ void this.reporter?.stop();
43263
+ this.reporter = null;
43152
43264
  this.emit("ended", void 0);
43153
43265
  });
43154
43266
  transport.onBuffering(() => {
@@ -43159,7 +43271,13 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43159
43271
  startStats() {
43160
43272
  this.statsTimer = setInterval(async () => {
43161
43273
  const stats = await this.transport?.getStats();
43162
- if (stats) this.emit("stats", stats);
43274
+ if (!stats) return;
43275
+ this.emit("stats", stats);
43276
+ this.reporter?.add({
43277
+ ts: Math.floor(Date.now() / 1e3),
43278
+ bitrateKbps: stats.bitrateKbps,
43279
+ fps: stats.framesPerSecond
43280
+ });
43163
43281
  }, STATS_INTERVAL_MS2);
43164
43282
  }
43165
43283
  stopStats() {
@@ -43294,10 +43412,12 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43294
43412
  // src/client.ts
43295
43413
  var MebiusClient = class extends TypedEmitter {
43296
43414
  /** @internal */
43297
- constructor(config2, token, deliveries = []) {
43415
+ constructor(config2, token, deliveries = [], telemetry = null, userId) {
43298
43416
  super();
43299
43417
  this.token = token;
43300
43418
  this.deliveries = deliveries;
43419
+ this.telemetry = telemetry;
43420
+ this.userId = userId;
43301
43421
  this.expiryTimer = null;
43302
43422
  this.connected = false;
43303
43423
  this.signaling = new SignalingClient(config2.gateway, token);
@@ -43322,12 +43442,12 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43322
43442
  /** Create a broadcaster bound to this connection. */
43323
43443
  createBroadcaster(options = {}) {
43324
43444
  this.assertConnected();
43325
- return new MebiusBroadcaster(this.signaling, options);
43445
+ return new MebiusBroadcaster(this.signaling, options, this.telemetry, this.userId);
43326
43446
  }
43327
43447
  /** Create a player bound to this connection. */
43328
43448
  createPlayer(options = {}) {
43329
43449
  this.assertConnected();
43330
- return new MebiusPlayer(this.signaling, options, this.deliveries);
43450
+ return new MebiusPlayer(this.signaling, options, this.deliveries, this.telemetry, this.userId);
43331
43451
  }
43332
43452
  /**
43333
43453
  * Create a monitor: a player tuned for watching a stream you are interacting
@@ -43342,7 +43462,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43342
43462
  */
43343
43463
  createMonitor() {
43344
43464
  this.assertConnected();
43345
- return new MebiusPlayer(this.signaling, { mode: "low-latency" }, this.deliveries);
43465
+ return new MebiusPlayer(this.signaling, { mode: "low-latency" }, this.deliveries, this.telemetry, this.userId);
43346
43466
  }
43347
43467
  /** Close the connection and release resources. */
43348
43468
  disconnect(reason) {
@@ -43375,7 +43495,8 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43375
43495
  throw mebiusError("UNKNOWN", "Call Mebius.init() before Mebius.connect().");
43376
43496
  }
43377
43497
  if (!options.token) throw mebiusError("UNKNOWN", "Mebius.connect requires a token.");
43378
- const client = new MebiusClient(config, options.token, options.deliveries ?? []);
43498
+ const telemetry = options.beaconToken && options.beaconUrl ? { token: options.beaconToken, url: options.beaconUrl } : null;
43499
+ const client = new MebiusClient(config, options.token, options.deliveries ?? [], telemetry, options.userId);
43379
43500
  client.open();
43380
43501
  return client;
43381
43502
  },