@mebius-io/web 0.4.7 → 0.4.9

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/dist/index.d.ts CHANGED
@@ -90,21 +90,36 @@ interface PlayerOptions {
90
90
  * resolves to one.
91
91
  */
92
92
  type ViewTarget = HTMLVideoElement | string;
93
- /** Live broadcast statistics, emitted periodically on the `"stats"` event. */
93
+ /**
94
+ * Live broadcast statistics, emitted periodically on the `"stats"` event.
95
+ *
96
+ * Every field is optional: the first tick of a session has no previous sample
97
+ * to difference against, and a value that was never measured must stay absent
98
+ * rather than be reported as a confident zero.
99
+ */
94
100
  interface BroadcastStats {
95
- /** Outbound bitrate in kilobits per second. */
96
- bitrateKbps: number;
101
+ /** Outbound bitrate in kilobits per second — what is actually being sent. */
102
+ bitrateKbps?: number;
97
103
  /** Frames per second currently being sent. */
98
- framesPerSecond: number;
104
+ framesPerSecond?: number;
99
105
  /** Round-trip time to the gateway in milliseconds, if known. */
100
106
  rttMs?: number;
107
+ /** Percentage of sent packets the receiver reported missing, if known. */
108
+ packetLossPct?: number;
101
109
  }
102
- /** Live playback statistics, emitted periodically on the `"stats"` event. */
110
+ /**
111
+ * Live playback statistics, emitted periodically on the `"stats"` event.
112
+ *
113
+ * Fields are optional because not every route can measure every one, and a
114
+ * transport that reports 0 for something it never measured is indistinguishable
115
+ * from a stream that is genuinely delivering nothing — which is exactly how
116
+ * "0 kbps downlink" ended up on every viewer in the dashboard.
117
+ */
103
118
  interface PlaybackStats {
104
- /** Inbound bitrate in kilobits per second. */
105
- bitrateKbps: number;
106
- /** Frames per second currently being rendered. */
107
- framesPerSecond: number;
119
+ /** Inbound bitrate in kilobits per second, when the route can measure it. */
120
+ bitrateKbps?: number;
121
+ /** Frames per second currently being rendered, when known. */
122
+ framesPerSecond?: number;
108
123
  /** Estimated end-to-end latency in milliseconds, if known. */
109
124
  latencyMs?: number;
110
125
  }
@@ -275,6 +290,8 @@ declare class MebiusPlayer extends TypedEmitter<PlayerEventMap> {
275
290
  private reporter;
276
291
  /** True between a `buffering` event and the element actually resuming. */
277
292
  private stalled;
293
+ /** Measures how long playback was actually frozen; see FreezeClock. */
294
+ private readonly freeze;
278
295
  /** Cancels element listeners bound for the lifetime of one play(). */
279
296
  private elementListeners;
280
297
  /** @internal */
@@ -42724,6 +42724,8 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42724
42724
  this.signaling = signaling;
42725
42725
  this.pc = null;
42726
42726
  this.resourceUrl = null;
42727
+ /** Bytes sent and packet counters at the previous getStats() call. */
42728
+ this.lastOutbound = null;
42727
42729
  }
42728
42730
  async start(streamId, stream) {
42729
42731
  const pc = new RTCPeerConnection(DEFAULT_RTC_CONFIG);
@@ -42756,26 +42758,61 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42756
42758
  this.pc?.close();
42757
42759
  this.pc = null;
42758
42760
  }
42761
+ /**
42762
+ * Live broadcast statistics.
42763
+ *
42764
+ * Two corrections over the obvious reading of RTCStats:
42765
+ *
42766
+ * `bitrateKbps` is the delta of `outbound-rtp.bytesSent`, not
42767
+ * `availableOutgoingBitrate`. The latter is the congestion controller's
42768
+ * ESTIMATE of headroom, so a broadcaster on a fast link reported several
42769
+ * megabits while actually sending a fraction of that — the dashboard's
42770
+ * "bitrate adherence" score was measuring the network, not the encoder.
42771
+ *
42772
+ * `packetLossPct` comes from the receiver's report (`remote-inbound-rtp`),
42773
+ * which is the only place that knows what did not arrive. It was never
42774
+ * reported at all, and publishQualityScore treats a missing value as zero
42775
+ * loss — so every publisher scored full marks on a fifth of the rubric no
42776
+ * matter how bad the uplink was.
42777
+ */
42759
42778
  async getStats() {
42760
42779
  if (!this.pc) return null;
42761
42780
  const report = await this.pc.getStats();
42762
- let bitrateKbps = 0;
42763
- let framesPerSecond = 0;
42781
+ let framesPerSecond;
42764
42782
  let rttMs;
42783
+ let packetLossPct;
42784
+ let bytesSent;
42785
+ let packetsSent;
42786
+ let packetsLost;
42765
42787
  report.forEach((stat) => {
42766
42788
  if (stat.type === "outbound-rtp" && !stat.isRemote) {
42767
42789
  if (typeof stat.framesPerSecond === "number") framesPerSecond = stat.framesPerSecond;
42790
+ if (typeof stat.bytesSent === "number") bytesSent = (bytesSent ?? 0) + stat.bytesSent;
42791
+ if (typeof stat.packetsSent === "number") packetsSent = (packetsSent ?? 0) + stat.packetsSent;
42792
+ }
42793
+ if (stat.type === "remote-inbound-rtp") {
42794
+ if (typeof stat.packetsLost === "number") packetsLost = (packetsLost ?? 0) + stat.packetsLost;
42795
+ if (typeof stat.roundTripTime === "number") rttMs = Math.round(stat.roundTripTime * 1e3);
42768
42796
  }
42769
42797
  if (stat.type === "candidate-pair" && stat.state === "succeeded") {
42770
- if (typeof stat.availableOutgoingBitrate === "number") {
42771
- bitrateKbps = Math.round(stat.availableOutgoingBitrate / 1e3);
42772
- }
42773
- if (typeof stat.currentRoundTripTime === "number") {
42798
+ if (rttMs == null && typeof stat.currentRoundTripTime === "number") {
42774
42799
  rttMs = Math.round(stat.currentRoundTripTime * 1e3);
42775
42800
  }
42776
42801
  }
42777
42802
  });
42778
- return { bitrateKbps, framesPerSecond, rttMs };
42803
+ let bitrateKbps;
42804
+ const atMs = Date.now();
42805
+ if (bytesSent != null) {
42806
+ const prev = this.lastOutbound;
42807
+ if (prev && atMs > prev.atMs && bytesSent >= prev.bytes) {
42808
+ bitrateKbps = Math.round((bytesSent - prev.bytes) * 8 / 1e3 / ((atMs - prev.atMs) / 1e3));
42809
+ }
42810
+ this.lastOutbound = { bytes: bytesSent, atMs };
42811
+ }
42812
+ if (packetsLost != null && packetsSent != null && packetsSent > 0) {
42813
+ packetLossPct = Math.max(0, Math.min(100, packetsLost / packetsSent * 100));
42814
+ }
42815
+ return { bitrateKbps, framesPerSecond, rttMs, packetLossPct };
42779
42816
  }
42780
42817
  };
42781
42818
 
@@ -42805,6 +42842,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42805
42842
  var WhepViewTransport = class {
42806
42843
  constructor(signaling) {
42807
42844
  this.signaling = signaling;
42845
+ this.kind = "whep";
42808
42846
  this.pc = null;
42809
42847
  this.resourceUrl = null;
42810
42848
  this.endedCb = null;
@@ -42884,6 +42922,9 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42884
42922
  };
42885
42923
 
42886
42924
  // src/internal/scale-view-transport.ts
42925
+ function retryWarmupNotFound(cfg, retryCount, res, retry) {
42926
+ return retry || retryCount < (cfg?.maxNumRetry ?? 0) && res?.code === 404;
42927
+ }
42887
42928
  var HlsViewTransport = class {
42888
42929
  /**
42889
42930
  * deliveryPath, when given, is a gateway-relative path from the gateway's own
@@ -42894,6 +42935,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42894
42935
  constructor(signaling, deliveryPath) {
42895
42936
  this.signaling = signaling;
42896
42937
  this.deliveryPath = deliveryPath;
42938
+ this.kind = "hls";
42897
42939
  this.hls = null;
42898
42940
  this.video = null;
42899
42941
  this.endedCb = null;
@@ -42930,7 +42972,22 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42930
42972
  this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
42931
42973
  return;
42932
42974
  }
42933
- const hls = new Hls2({ maxLiveSyncPlaybackRate: 1.1 });
42975
+ const hls = new Hls2({
42976
+ maxLiveSyncPlaybackRate: 1.1,
42977
+ manifestLoadPolicy: {
42978
+ default: {
42979
+ maxTimeToFirstByteMs: 1e4,
42980
+ maxLoadTimeMs: 2e4,
42981
+ timeoutRetry: { maxNumRetry: 2, retryDelayMs: 0, maxRetryDelayMs: 0 },
42982
+ errorRetry: {
42983
+ maxNumRetry: 5,
42984
+ retryDelayMs: 500,
42985
+ maxRetryDelayMs: 2e3,
42986
+ shouldRetry: (cfg, retryCount, _isTimeout, res, retry) => retryWarmupNotFound(cfg, retryCount, res, retry)
42987
+ }
42988
+ }
42989
+ }
42990
+ });
42934
42991
  this.hls = hls;
42935
42992
  hls.on(Hls2.Events.ERROR, (_evt, data) => {
42936
42993
  if (data.fatal) this.bufferingCb?.();
@@ -42999,6 +43056,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42999
43056
  constructor(signaling, deliveryPath) {
43000
43057
  this.signaling = signaling;
43001
43058
  this.deliveryPath = deliveryPath;
43059
+ this.kind = "flv_js";
43002
43060
  this.player = null;
43003
43061
  this.video = null;
43004
43062
  this.endedCb = null;
@@ -43007,6 +43065,8 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43007
43065
  this.listeners = null;
43008
43066
  /** True when playback only started because the element had to be muted. */
43009
43067
  this.mutedByPolicy = false;
43068
+ /** Decoded-frame count and timestamp of the previous getStats() call. */
43069
+ this.lastFrames = null;
43010
43070
  }
43011
43071
  onEnded(cb) {
43012
43072
  this.endedCb = cb;
@@ -43070,13 +43130,33 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43070
43130
  }
43071
43131
  this.video = null;
43072
43132
  }
43133
+ /**
43134
+ * Real playback statistics for this route.
43135
+ *
43136
+ * Both numbers used to be hardcoded zeros, which is worse than reporting
43137
+ * nothing: the dashboard cannot tell a measured 0 kbps from an unmeasured
43138
+ * one, so every flv.js viewer in production showed a downlink of 0 and the
43139
+ * column read as a total outage. flv.js measures throughput itself
43140
+ * (`statisticsInfo.speed`, KB/s), and the element counts decoded frames, so
43141
+ * frame rate is the delta between two calls. Anything genuinely unavailable
43142
+ * is left undefined rather than zeroed.
43143
+ */
43073
43144
  async getStats() {
43074
43145
  if (!this.video) return null;
43075
- return {
43076
- bitrateKbps: 0,
43077
- framesPerSecond: 0,
43078
- latencyMs: void 0
43079
- };
43146
+ const speedKBs = this.player?.statisticsInfo?.speed;
43147
+ const bitrateKbps = typeof speedKBs === "number" ? Math.round(speedKBs * 8) : void 0;
43148
+ let framesPerSecond;
43149
+ const q = this.video.getVideoPlaybackQuality?.();
43150
+ const count = q?.totalVideoFrames;
43151
+ const atMs = Date.now();
43152
+ if (typeof count === "number") {
43153
+ const prev = this.lastFrames;
43154
+ if (prev && atMs > prev.atMs && count >= prev.count) {
43155
+ framesPerSecond = Math.round((count - prev.count) * 1e3 / (atMs - prev.atMs));
43156
+ }
43157
+ this.lastFrames = { count, atMs };
43158
+ }
43159
+ return { bitrateKbps, framesPerSecond, latencyMs: void 0 };
43080
43160
  }
43081
43161
  };
43082
43162
 
@@ -43112,7 +43192,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43112
43192
  }
43113
43193
 
43114
43194
  // src/internal/telemetry.ts
43115
- var SDK_VERSION = "web/0.4.6";
43195
+ var SDK_VERSION = "web/0.4.8";
43116
43196
  var FLUSH_INTERVAL_MS = 15e3;
43117
43197
  var MAX_BATCH = 64;
43118
43198
  function describeDevice() {
@@ -43125,11 +43205,12 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43125
43205
  return conn?.effectiveType ? { type: conn.effectiveType } : void 0;
43126
43206
  }
43127
43207
  var QoeReporter = class {
43128
- constructor(target, role, streamId, userId) {
43208
+ constructor(target, role, streamId, userId, playerKind) {
43129
43209
  this.target = target;
43130
43210
  this.role = role;
43131
43211
  this.streamId = streamId;
43132
43212
  this.userId = userId;
43213
+ this.playerKind = playerKind;
43133
43214
  this.sessionId = randomId();
43134
43215
  this.buffer = [];
43135
43216
  this.timer = null;
@@ -43165,6 +43246,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43165
43246
  streamId: this.streamId,
43166
43247
  role: this.role,
43167
43248
  userId: this.userId,
43249
+ playerKind: this.playerKind,
43168
43250
  samples,
43169
43251
  device: describeDevice(),
43170
43252
  network: describeNetwork()
@@ -43289,7 +43371,8 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43289
43371
  ts: Math.floor(Date.now() / 1e3),
43290
43372
  bitrateKbps: stats.bitrateKbps,
43291
43373
  fps: stats.framesPerSecond,
43292
- rttMs: stats.rttMs
43374
+ rttMs: stats.rttMs,
43375
+ packetLossPct: stats.packetLossPct
43293
43376
  });
43294
43377
  }, STATS_INTERVAL_MS);
43295
43378
  }
@@ -43303,6 +43386,59 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43303
43386
  return c;
43304
43387
  }
43305
43388
 
43389
+ // src/internal/freeze-clock.ts
43390
+ var FreezeClock = class {
43391
+ constructor(now2 = Date.now) {
43392
+ this.now = now2;
43393
+ /** When the current stall began, or null when playback is running. */
43394
+ this.stalledSinceMs = null;
43395
+ /** Stall time that has ended but has not yet been shipped with a sample. */
43396
+ this.pendingMs = 0;
43397
+ }
43398
+ /** True while a stall is in progress. */
43399
+ get stalled() {
43400
+ return this.stalledSinceMs !== null;
43401
+ }
43402
+ /**
43403
+ * Begin a stall. Re-entering while already stalled is ignored rather than
43404
+ * restarting the clock: flv.js fires `waiting` repeatedly through a single
43405
+ * long stall, and resetting the start on each would report a fraction of the
43406
+ * freeze that actually happened.
43407
+ */
43408
+ beginStall() {
43409
+ if (this.stalledSinceMs === null) this.stalledSinceMs = this.now();
43410
+ }
43411
+ /** End the current stall and bank its duration. No-op when not stalled. */
43412
+ endStall() {
43413
+ if (this.stalledSinceMs === null) return;
43414
+ this.pendingMs += Math.max(0, this.now() - this.stalledSinceMs);
43415
+ this.stalledSinceMs = null;
43416
+ }
43417
+ /**
43418
+ * Freeze milliseconds to report on this tick, resetting the counter.
43419
+ *
43420
+ * A stall still in progress is counted up to now and its clock restarted, so
43421
+ * a freeze longer than the sample interval is reported while it is happening
43422
+ * rather than landing whole in whichever sample eventually follows it. Every
43423
+ * millisecond is attributed exactly once — never dropped, never double-counted.
43424
+ */
43425
+ take() {
43426
+ if (this.stalledSinceMs !== null) {
43427
+ const now2 = this.now();
43428
+ this.pendingMs += Math.max(0, now2 - this.stalledSinceMs);
43429
+ this.stalledSinceMs = now2;
43430
+ }
43431
+ const ms = this.pendingMs;
43432
+ this.pendingMs = 0;
43433
+ return ms;
43434
+ }
43435
+ /** Forget everything. Called when a session ends. */
43436
+ reset() {
43437
+ this.stalledSinceMs = null;
43438
+ this.pendingMs = 0;
43439
+ }
43440
+ };
43441
+
43306
43442
  // src/player.ts
43307
43443
  var STATS_INTERVAL_MS2 = 2e3;
43308
43444
  var FIRST_FRAME_TIMEOUT_MS = 8e3;
@@ -43320,6 +43456,8 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43320
43456
  this.reporter = null;
43321
43457
  /** True between a `buffering` event and the element actually resuming. */
43322
43458
  this.stalled = false;
43459
+ /** Measures how long playback was actually frozen; see FreezeClock. */
43460
+ this.freeze = new FreezeClock();
43323
43461
  /** Cancels element listeners bound for the lifetime of one play(). */
43324
43462
  this.elementListeners = null;
43325
43463
  this.candidates = createViewCandidates(options.mode ?? "auto", signaling, deliveries);
@@ -43338,6 +43476,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43338
43476
  () => {
43339
43477
  if (!this.stalled || !this.playing) return;
43340
43478
  this.stalled = false;
43479
+ this.freeze.endStall();
43341
43480
  this.emit("playing", { streamId });
43342
43481
  },
43343
43482
  { signal: this.elementListeners.signal }
@@ -43353,7 +43492,13 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43353
43492
  this.transport = candidate;
43354
43493
  this.playing = true;
43355
43494
  if (this.telemetry) {
43356
- this.reporter = new QoeReporter(this.telemetry, "play", streamId, this.userId);
43495
+ this.reporter = new QoeReporter(
43496
+ this.telemetry,
43497
+ "play",
43498
+ streamId,
43499
+ this.userId,
43500
+ candidate.kind
43501
+ );
43357
43502
  this.reporter.start();
43358
43503
  this.reporter.add({ ts: Math.floor(Date.now() / 1e3), firstFrameMs: Date.now() - startedAtMs });
43359
43504
  }
@@ -43381,6 +43526,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43381
43526
  ELEMENT_OWNER.delete(this.video);
43382
43527
  }
43383
43528
  this.stalled = false;
43529
+ this.freeze.reset();
43384
43530
  this.stopStats();
43385
43531
  await this.reporter?.stop();
43386
43532
  this.reporter = null;
@@ -43420,6 +43566,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43420
43566
  });
43421
43567
  transport.onBuffering(() => {
43422
43568
  if (this.transport !== transport) return;
43569
+ this.freeze.beginStall();
43423
43570
  this.stalled = true;
43424
43571
  this.emit("buffering", void 0);
43425
43572
  });
@@ -43427,12 +43574,17 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43427
43574
  startStats() {
43428
43575
  this.statsTimer = setInterval(async () => {
43429
43576
  const stats = await this.transport?.getStats();
43430
- if (!stats) return;
43577
+ const freezeMs = this.freeze.take();
43578
+ if (!stats) {
43579
+ if (freezeMs > 0) this.reporter?.add({ ts: Math.floor(Date.now() / 1e3), freezeMs });
43580
+ return;
43581
+ }
43431
43582
  this.emit("stats", stats);
43432
43583
  this.reporter?.add({
43433
43584
  ts: Math.floor(Date.now() / 1e3),
43434
43585
  bitrateKbps: stats.bitrateKbps,
43435
- fps: stats.framesPerSecond
43586
+ fps: stats.framesPerSecond,
43587
+ freezeMs
43436
43588
  });
43437
43589
  }, STATS_INTERVAL_MS2);
43438
43590
  }