@mebius-io/web 0.3.0 → 0.4.1

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.
@@ -42764,6 +42764,28 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42764
42764
  }
42765
42765
  };
42766
42766
 
42767
+ // src/internal/autoplay.ts
42768
+ function isAutoplayBlocked(e) {
42769
+ return typeof e === "object" && e !== null && e.name === "NotAllowedError";
42770
+ }
42771
+ async function playWithAutoplayFallback(video) {
42772
+ video.playsInline = true;
42773
+ try {
42774
+ await video.play();
42775
+ return { mutedByPolicy: false };
42776
+ } catch (e) {
42777
+ if (!isAutoplayBlocked(e) || video.muted) throw e;
42778
+ video.muted = true;
42779
+ await video.play();
42780
+ return { mutedByPolicy: true };
42781
+ }
42782
+ }
42783
+ function resetVideoElement(video) {
42784
+ video.srcObject = null;
42785
+ video.removeAttribute("src");
42786
+ video.load();
42787
+ }
42788
+
42767
42789
  // src/internal/ll-view-transport.ts
42768
42790
  var WhepViewTransport = class {
42769
42791
  constructor(signaling) {
@@ -42772,6 +42794,10 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42772
42794
  this.resourceUrl = null;
42773
42795
  this.endedCb = null;
42774
42796
  this.bufferingCb = null;
42797
+ /** Element this route attached a MediaStream to, so stop() can release it. */
42798
+ this.video = null;
42799
+ /** True when playback only started because the element had to be muted. */
42800
+ this.mutedByPolicy = false;
42775
42801
  }
42776
42802
  onEnded(cb) {
42777
42803
  this.endedCb = cb;
@@ -42780,6 +42806,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42780
42806
  this.bufferingCb = cb;
42781
42807
  }
42782
42808
  async start(streamId, video) {
42809
+ this.video = video;
42783
42810
  const pc = new RTCPeerConnection(DEFAULT_RTC_CONFIG);
42784
42811
  this.pc = pc;
42785
42812
  const remote = new MediaStream();
@@ -42788,7 +42815,9 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42788
42815
  pc.ontrack = (ev) => {
42789
42816
  remote.addTrack(ev.track);
42790
42817
  video.srcObject = remote;
42791
- void video.play().catch(() => {
42818
+ void playWithAutoplayFallback(video).then((o) => {
42819
+ this.mutedByPolicy = o.mutedByPolicy;
42820
+ }).catch(() => {
42792
42821
  });
42793
42822
  };
42794
42823
  pc.onconnectionstatechange = () => {
@@ -42815,6 +42844,8 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42815
42844
  this.resourceUrl = null;
42816
42845
  this.pc?.close();
42817
42846
  this.pc = null;
42847
+ if (this.video) resetVideoElement(this.video);
42848
+ this.video = null;
42818
42849
  }
42819
42850
  async getStats() {
42820
42851
  if (!this.pc) return null;
@@ -42852,6 +42883,8 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42852
42883
  this.video = null;
42853
42884
  this.endedCb = null;
42854
42885
  this.bufferingCb = null;
42886
+ /** True when playback only started because the element had to be muted. */
42887
+ this.mutedByPolicy = false;
42855
42888
  }
42856
42889
  onEnded(cb) {
42857
42890
  this.endedCb = cb;
@@ -42866,7 +42899,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42866
42899
  video.addEventListener("waiting", () => this.bufferingCb?.());
42867
42900
  if (video.canPlayType("application/vnd.apple.mpegurl")) {
42868
42901
  video.src = url;
42869
- await video.play().catch(() => void 0);
42902
+ this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
42870
42903
  return;
42871
42904
  }
42872
42905
  let mod;
@@ -42879,14 +42912,14 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42879
42912
  if (!Hls2.isSupported()) {
42880
42913
  throw mebiusError("CONNECTION_FAILED", "Scale playback is not supported in this browser.");
42881
42914
  }
42882
- const hls = new Hls2({ lowLatencyMode: true });
42915
+ const hls = new Hls2();
42883
42916
  this.hls = hls;
42884
42917
  hls.on(Hls2.Events.ERROR, (_evt, data) => {
42885
42918
  if (data.fatal) this.bufferingCb?.();
42886
42919
  });
42887
42920
  hls.loadSource(url);
42888
42921
  hls.attachMedia(video);
42889
- await video.play().catch(() => void 0);
42922
+ this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
42890
42923
  }
42891
42924
  async stop() {
42892
42925
  this.hls?.destroy();
@@ -42916,6 +42949,8 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42916
42949
  this.video = null;
42917
42950
  this.endedCb = null;
42918
42951
  this.bufferingCb = null;
42952
+ /** True when playback only started because the element had to be muted. */
42953
+ this.mutedByPolicy = false;
42919
42954
  }
42920
42955
  onEnded(cb) {
42921
42956
  this.endedCb = cb;
@@ -42943,7 +42978,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42943
42978
  player.on(flvjs.Events.ERROR ?? "error", () => this.bufferingCb?.());
42944
42979
  player.attachMediaElement(video);
42945
42980
  player.load();
42946
- await Promise.resolve(player.play()).catch(() => void 0);
42981
+ this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
42947
42982
  }
42948
42983
  async stop() {
42949
42984
  if (this.player) {
@@ -42999,17 +43034,103 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42999
43034
  }
43000
43035
  }
43001
43036
 
43037
+ // src/internal/telemetry.ts
43038
+ var SDK_VERSION = "web/0.4.1";
43039
+ var FLUSH_INTERVAL_MS = 15e3;
43040
+ var MAX_BATCH = 64;
43041
+ function describeDevice() {
43042
+ const nav = typeof navigator === "undefined" ? void 0 : navigator;
43043
+ const uaData = nav?.userAgentData;
43044
+ return { os: uaData?.platform || nav?.platform || void 0, sdk: SDK_VERSION };
43045
+ }
43046
+ function describeNetwork() {
43047
+ const conn = typeof navigator === "undefined" ? void 0 : navigator.connection;
43048
+ return conn?.effectiveType ? { type: conn.effectiveType } : void 0;
43049
+ }
43050
+ var QoeReporter = class {
43051
+ constructor(target, role, streamId, userId) {
43052
+ this.target = target;
43053
+ this.role = role;
43054
+ this.streamId = streamId;
43055
+ this.userId = userId;
43056
+ this.sessionId = randomId();
43057
+ this.buffer = [];
43058
+ this.timer = null;
43059
+ this.unloadHandler = null;
43060
+ }
43061
+ start() {
43062
+ if (this.timer) return;
43063
+ this.timer = setInterval(() => void this.flush(), FLUSH_INTERVAL_MS);
43064
+ if (typeof window !== "undefined") {
43065
+ this.unloadHandler = () => void this.flush(true);
43066
+ window.addEventListener("pagehide", this.unloadHandler);
43067
+ }
43068
+ }
43069
+ add(sample) {
43070
+ this.buffer.push(sample);
43071
+ if (this.buffer.length >= MAX_BATCH) void this.flush();
43072
+ }
43073
+ async stop() {
43074
+ if (this.timer) clearInterval(this.timer);
43075
+ this.timer = null;
43076
+ if (this.unloadHandler && typeof window !== "undefined") {
43077
+ window.removeEventListener("pagehide", this.unloadHandler);
43078
+ }
43079
+ this.unloadHandler = null;
43080
+ await this.flush();
43081
+ }
43082
+ /** Send and clear the buffer. `beacon` uses sendBeacon, for page-unload flushes. */
43083
+ async flush(beacon = false) {
43084
+ if (!this.buffer.length) return;
43085
+ const samples = this.buffer.splice(0, MAX_BATCH);
43086
+ const body = JSON.stringify({
43087
+ sessionId: this.sessionId,
43088
+ streamId: this.streamId,
43089
+ role: this.role,
43090
+ userId: this.userId,
43091
+ samples,
43092
+ device: describeDevice(),
43093
+ network: describeNetwork()
43094
+ });
43095
+ if (beacon && typeof navigator !== "undefined" && navigator.sendBeacon) {
43096
+ const url = `${this.target.url}${this.target.url.includes("?") ? "&" : "?"}token=${encodeURIComponent(this.target.token)}`;
43097
+ try {
43098
+ navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
43099
+ } catch {
43100
+ }
43101
+ return;
43102
+ }
43103
+ try {
43104
+ await fetch(this.target.url, {
43105
+ method: "POST",
43106
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.target.token}` },
43107
+ body,
43108
+ keepalive: true
43109
+ });
43110
+ } catch {
43111
+ }
43112
+ }
43113
+ };
43114
+ function randomId() {
43115
+ const c = typeof crypto === "undefined" ? void 0 : crypto;
43116
+ if (c?.randomUUID) return c.randomUUID();
43117
+ return `s-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
43118
+ }
43119
+
43002
43120
  // src/broadcaster.ts
43003
43121
  var STATS_INTERVAL_MS = 2e3;
43004
43122
  var MebiusBroadcaster = class extends TypedEmitter {
43005
43123
  /** @internal */
43006
- constructor(signaling, options) {
43124
+ constructor(signaling, options, telemetry = null, userId) {
43007
43125
  super();
43008
43126
  this.options = options;
43127
+ this.telemetry = telemetry;
43128
+ this.userId = userId;
43009
43129
  this.stream = null;
43010
43130
  this.facingMode = "user";
43011
43131
  this.statsTimer = null;
43012
43132
  this.started = false;
43133
+ this.reporter = null;
43013
43134
  this.transport = createPublishTransport(signaling);
43014
43135
  }
43015
43136
  /** Begin broadcasting under the given stream id. */
@@ -43018,12 +43139,18 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43018
43139
  this.stream = await this.capture();
43019
43140
  await this.transport.start(streamId, this.stream);
43020
43141
  this.started = true;
43142
+ if (this.telemetry) {
43143
+ this.reporter = new QoeReporter(this.telemetry, "pub", streamId, this.userId);
43144
+ this.reporter.start();
43145
+ }
43021
43146
  this.startStats();
43022
43147
  this.emit("started", { streamId });
43023
43148
  }
43024
43149
  /** Stop broadcasting and release the camera/microphone. */
43025
43150
  async stop() {
43026
43151
  this.stopStats();
43152
+ await this.reporter?.stop();
43153
+ this.reporter = null;
43027
43154
  await this.transport.stop();
43028
43155
  this.stream?.getTracks().forEach((t) => t.stop());
43029
43156
  this.stream = null;
@@ -43079,7 +43206,14 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43079
43206
  startStats() {
43080
43207
  this.statsTimer = setInterval(async () => {
43081
43208
  const stats = await this.transport.getStats();
43082
- if (stats) this.emit("stats", stats);
43209
+ if (!stats) return;
43210
+ this.emit("stats", stats);
43211
+ this.reporter?.add({
43212
+ ts: Math.floor(Date.now() / 1e3),
43213
+ bitrateKbps: stats.bitrateKbps,
43214
+ fps: stats.framesPerSecond,
43215
+ rttMs: stats.rttMs
43216
+ });
43083
43217
  }, STATS_INTERVAL_MS);
43084
43218
  }
43085
43219
  stopStats() {
@@ -43097,12 +43231,15 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43097
43231
  var FIRST_FRAME_TIMEOUT_MS = 8e3;
43098
43232
  var MebiusPlayer = class extends TypedEmitter {
43099
43233
  /** @internal */
43100
- constructor(signaling, options = {}, deliveries = []) {
43234
+ constructor(signaling, options = {}, deliveries = [], telemetry = null, userId) {
43101
43235
  super();
43236
+ this.telemetry = telemetry;
43237
+ this.userId = userId;
43102
43238
  this.transport = null;
43103
43239
  this.video = null;
43104
43240
  this.statsTimer = null;
43105
43241
  this.playing = false;
43242
+ this.reporter = null;
43106
43243
  this.candidates = createViewCandidates(options.mode ?? "auto", signaling, deliveries);
43107
43244
  }
43108
43245
  /** Start playing `streamId` into the given video element or selector. */
@@ -43110,14 +43247,21 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43110
43247
  if (this.playing) return;
43111
43248
  const video = resolveVideoElement(viewTarget);
43112
43249
  this.video = video;
43250
+ const startedAtMs = Date.now();
43113
43251
  let lastError = null;
43114
43252
  for (const candidate of this.candidates) {
43115
43253
  try {
43254
+ resetVideoElement(video);
43116
43255
  this.attach(candidate);
43117
43256
  await candidate.start(streamId, video);
43118
43257
  if (await hasFirstFrame(video)) {
43119
43258
  this.transport = candidate;
43120
43259
  this.playing = true;
43260
+ if (this.telemetry) {
43261
+ this.reporter = new QoeReporter(this.telemetry, "play", streamId, this.userId);
43262
+ this.reporter.start();
43263
+ this.reporter.add({ ts: Math.floor(Date.now() / 1e3), firstFrameMs: Date.now() - startedAtMs });
43264
+ }
43121
43265
  this.startStats();
43122
43266
  this.emit("playing", { streamId });
43123
43267
  return;
@@ -43134,6 +43278,8 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43134
43278
  /** Stop playback and detach from the video element. */
43135
43279
  async stop() {
43136
43280
  this.stopStats();
43281
+ await this.reporter?.stop();
43282
+ this.reporter = null;
43137
43283
  await this.transport?.stop();
43138
43284
  this.transport = null;
43139
43285
  this.video = null;
@@ -43149,6 +43295,8 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43149
43295
  if (this.transport !== transport) return;
43150
43296
  this.playing = false;
43151
43297
  this.stopStats();
43298
+ void this.reporter?.stop();
43299
+ this.reporter = null;
43152
43300
  this.emit("ended", void 0);
43153
43301
  });
43154
43302
  transport.onBuffering(() => {
@@ -43159,7 +43307,13 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43159
43307
  startStats() {
43160
43308
  this.statsTimer = setInterval(async () => {
43161
43309
  const stats = await this.transport?.getStats();
43162
- if (stats) this.emit("stats", stats);
43310
+ if (!stats) return;
43311
+ this.emit("stats", stats);
43312
+ this.reporter?.add({
43313
+ ts: Math.floor(Date.now() / 1e3),
43314
+ bitrateKbps: stats.bitrateKbps,
43315
+ fps: stats.framesPerSecond
43316
+ });
43163
43317
  }, STATS_INTERVAL_MS2);
43164
43318
  }
43165
43319
  stopStats() {
@@ -43294,10 +43448,12 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43294
43448
  // src/client.ts
43295
43449
  var MebiusClient = class extends TypedEmitter {
43296
43450
  /** @internal */
43297
- constructor(config2, token, deliveries = []) {
43451
+ constructor(config2, token, deliveries = [], telemetry = null, userId) {
43298
43452
  super();
43299
43453
  this.token = token;
43300
43454
  this.deliveries = deliveries;
43455
+ this.telemetry = telemetry;
43456
+ this.userId = userId;
43301
43457
  this.expiryTimer = null;
43302
43458
  this.connected = false;
43303
43459
  this.signaling = new SignalingClient(config2.gateway, token);
@@ -43322,12 +43478,12 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43322
43478
  /** Create a broadcaster bound to this connection. */
43323
43479
  createBroadcaster(options = {}) {
43324
43480
  this.assertConnected();
43325
- return new MebiusBroadcaster(this.signaling, options);
43481
+ return new MebiusBroadcaster(this.signaling, options, this.telemetry, this.userId);
43326
43482
  }
43327
43483
  /** Create a player bound to this connection. */
43328
43484
  createPlayer(options = {}) {
43329
43485
  this.assertConnected();
43330
- return new MebiusPlayer(this.signaling, options, this.deliveries);
43486
+ return new MebiusPlayer(this.signaling, options, this.deliveries, this.telemetry, this.userId);
43331
43487
  }
43332
43488
  /**
43333
43489
  * Create a monitor: a player tuned for watching a stream you are interacting
@@ -43342,7 +43498,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43342
43498
  */
43343
43499
  createMonitor() {
43344
43500
  this.assertConnected();
43345
- return new MebiusPlayer(this.signaling, { mode: "low-latency" }, this.deliveries);
43501
+ return new MebiusPlayer(this.signaling, { mode: "low-latency" }, this.deliveries, this.telemetry, this.userId);
43346
43502
  }
43347
43503
  /** Close the connection and release resources. */
43348
43504
  disconnect(reason) {
@@ -43375,7 +43531,8 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43375
43531
  throw mebiusError("UNKNOWN", "Call Mebius.init() before Mebius.connect().");
43376
43532
  }
43377
43533
  if (!options.token) throw mebiusError("UNKNOWN", "Mebius.connect requires a token.");
43378
- const client = new MebiusClient(config, options.token, options.deliveries ?? []);
43534
+ const telemetry = options.beaconToken && options.beaconUrl ? { token: options.beaconToken, url: options.beaconUrl } : null;
43535
+ const client = new MebiusClient(config, options.token, options.deliveries ?? [], telemetry, options.userId);
43379
43536
  client.open();
43380
43537
  return client;
43381
43538
  },