@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.cjs +171 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -9
- package/dist/index.d.ts +26 -9
- package/dist/index.global.js +171 -19
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +171 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -148,6 +148,8 @@ var WhipPublishTransport = class {
|
|
|
148
148
|
this.signaling = signaling;
|
|
149
149
|
this.pc = null;
|
|
150
150
|
this.resourceUrl = null;
|
|
151
|
+
/** Bytes sent and packet counters at the previous getStats() call. */
|
|
152
|
+
this.lastOutbound = null;
|
|
151
153
|
}
|
|
152
154
|
async start(streamId, stream) {
|
|
153
155
|
const pc = new RTCPeerConnection(DEFAULT_RTC_CONFIG);
|
|
@@ -180,26 +182,61 @@ var WhipPublishTransport = class {
|
|
|
180
182
|
this.pc?.close();
|
|
181
183
|
this.pc = null;
|
|
182
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Live broadcast statistics.
|
|
187
|
+
*
|
|
188
|
+
* Two corrections over the obvious reading of RTCStats:
|
|
189
|
+
*
|
|
190
|
+
* `bitrateKbps` is the delta of `outbound-rtp.bytesSent`, not
|
|
191
|
+
* `availableOutgoingBitrate`. The latter is the congestion controller's
|
|
192
|
+
* ESTIMATE of headroom, so a broadcaster on a fast link reported several
|
|
193
|
+
* megabits while actually sending a fraction of that — the dashboard's
|
|
194
|
+
* "bitrate adherence" score was measuring the network, not the encoder.
|
|
195
|
+
*
|
|
196
|
+
* `packetLossPct` comes from the receiver's report (`remote-inbound-rtp`),
|
|
197
|
+
* which is the only place that knows what did not arrive. It was never
|
|
198
|
+
* reported at all, and publishQualityScore treats a missing value as zero
|
|
199
|
+
* loss — so every publisher scored full marks on a fifth of the rubric no
|
|
200
|
+
* matter how bad the uplink was.
|
|
201
|
+
*/
|
|
183
202
|
async getStats() {
|
|
184
203
|
if (!this.pc) return null;
|
|
185
204
|
const report = await this.pc.getStats();
|
|
186
|
-
let
|
|
187
|
-
let framesPerSecond = 0;
|
|
205
|
+
let framesPerSecond;
|
|
188
206
|
let rttMs;
|
|
207
|
+
let packetLossPct;
|
|
208
|
+
let bytesSent;
|
|
209
|
+
let packetsSent;
|
|
210
|
+
let packetsLost;
|
|
189
211
|
report.forEach((stat) => {
|
|
190
212
|
if (stat.type === "outbound-rtp" && !stat.isRemote) {
|
|
191
213
|
if (typeof stat.framesPerSecond === "number") framesPerSecond = stat.framesPerSecond;
|
|
214
|
+
if (typeof stat.bytesSent === "number") bytesSent = (bytesSent ?? 0) + stat.bytesSent;
|
|
215
|
+
if (typeof stat.packetsSent === "number") packetsSent = (packetsSent ?? 0) + stat.packetsSent;
|
|
216
|
+
}
|
|
217
|
+
if (stat.type === "remote-inbound-rtp") {
|
|
218
|
+
if (typeof stat.packetsLost === "number") packetsLost = (packetsLost ?? 0) + stat.packetsLost;
|
|
219
|
+
if (typeof stat.roundTripTime === "number") rttMs = Math.round(stat.roundTripTime * 1e3);
|
|
192
220
|
}
|
|
193
221
|
if (stat.type === "candidate-pair" && stat.state === "succeeded") {
|
|
194
|
-
if (typeof stat.
|
|
195
|
-
bitrateKbps = Math.round(stat.availableOutgoingBitrate / 1e3);
|
|
196
|
-
}
|
|
197
|
-
if (typeof stat.currentRoundTripTime === "number") {
|
|
222
|
+
if (rttMs == null && typeof stat.currentRoundTripTime === "number") {
|
|
198
223
|
rttMs = Math.round(stat.currentRoundTripTime * 1e3);
|
|
199
224
|
}
|
|
200
225
|
}
|
|
201
226
|
});
|
|
202
|
-
|
|
227
|
+
let bitrateKbps;
|
|
228
|
+
const atMs = Date.now();
|
|
229
|
+
if (bytesSent != null) {
|
|
230
|
+
const prev = this.lastOutbound;
|
|
231
|
+
if (prev && atMs > prev.atMs && bytesSent >= prev.bytes) {
|
|
232
|
+
bitrateKbps = Math.round((bytesSent - prev.bytes) * 8 / 1e3 / ((atMs - prev.atMs) / 1e3));
|
|
233
|
+
}
|
|
234
|
+
this.lastOutbound = { bytes: bytesSent, atMs };
|
|
235
|
+
}
|
|
236
|
+
if (packetsLost != null && packetsSent != null && packetsSent > 0) {
|
|
237
|
+
packetLossPct = Math.max(0, Math.min(100, packetsLost / packetsSent * 100));
|
|
238
|
+
}
|
|
239
|
+
return { bitrateKbps, framesPerSecond, rttMs, packetLossPct };
|
|
203
240
|
}
|
|
204
241
|
};
|
|
205
242
|
|
|
@@ -229,6 +266,7 @@ function resetVideoElement(video) {
|
|
|
229
266
|
var WhepViewTransport = class {
|
|
230
267
|
constructor(signaling) {
|
|
231
268
|
this.signaling = signaling;
|
|
269
|
+
this.kind = "whep";
|
|
232
270
|
this.pc = null;
|
|
233
271
|
this.resourceUrl = null;
|
|
234
272
|
this.endedCb = null;
|
|
@@ -308,6 +346,9 @@ var WhepViewTransport = class {
|
|
|
308
346
|
};
|
|
309
347
|
|
|
310
348
|
// src/internal/scale-view-transport.ts
|
|
349
|
+
function retryWarmupNotFound(cfg, retryCount, res, retry) {
|
|
350
|
+
return retry || retryCount < (cfg?.maxNumRetry ?? 0) && res?.code === 404;
|
|
351
|
+
}
|
|
311
352
|
var HlsViewTransport = class {
|
|
312
353
|
/**
|
|
313
354
|
* deliveryPath, when given, is a gateway-relative path from the gateway's own
|
|
@@ -318,6 +359,7 @@ var HlsViewTransport = class {
|
|
|
318
359
|
constructor(signaling, deliveryPath) {
|
|
319
360
|
this.signaling = signaling;
|
|
320
361
|
this.deliveryPath = deliveryPath;
|
|
362
|
+
this.kind = "hls";
|
|
321
363
|
this.hls = null;
|
|
322
364
|
this.video = null;
|
|
323
365
|
this.endedCb = null;
|
|
@@ -354,7 +396,22 @@ var HlsViewTransport = class {
|
|
|
354
396
|
this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
|
|
355
397
|
return;
|
|
356
398
|
}
|
|
357
|
-
const hls = new Hls({
|
|
399
|
+
const hls = new Hls({
|
|
400
|
+
maxLiveSyncPlaybackRate: 1.1,
|
|
401
|
+
manifestLoadPolicy: {
|
|
402
|
+
default: {
|
|
403
|
+
maxTimeToFirstByteMs: 1e4,
|
|
404
|
+
maxLoadTimeMs: 2e4,
|
|
405
|
+
timeoutRetry: { maxNumRetry: 2, retryDelayMs: 0, maxRetryDelayMs: 0 },
|
|
406
|
+
errorRetry: {
|
|
407
|
+
maxNumRetry: 5,
|
|
408
|
+
retryDelayMs: 500,
|
|
409
|
+
maxRetryDelayMs: 2e3,
|
|
410
|
+
shouldRetry: (cfg, retryCount, _isTimeout, res, retry) => retryWarmupNotFound(cfg, retryCount, res, retry)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
});
|
|
358
415
|
this.hls = hls;
|
|
359
416
|
hls.on(Hls.Events.ERROR, (_evt, data) => {
|
|
360
417
|
if (data.fatal) this.bufferingCb?.();
|
|
@@ -423,6 +480,7 @@ var FlvViewTransport = class {
|
|
|
423
480
|
constructor(signaling, deliveryPath) {
|
|
424
481
|
this.signaling = signaling;
|
|
425
482
|
this.deliveryPath = deliveryPath;
|
|
483
|
+
this.kind = "flv_js";
|
|
426
484
|
this.player = null;
|
|
427
485
|
this.video = null;
|
|
428
486
|
this.endedCb = null;
|
|
@@ -431,6 +489,8 @@ var FlvViewTransport = class {
|
|
|
431
489
|
this.listeners = null;
|
|
432
490
|
/** True when playback only started because the element had to be muted. */
|
|
433
491
|
this.mutedByPolicy = false;
|
|
492
|
+
/** Decoded-frame count and timestamp of the previous getStats() call. */
|
|
493
|
+
this.lastFrames = null;
|
|
434
494
|
}
|
|
435
495
|
onEnded(cb) {
|
|
436
496
|
this.endedCb = cb;
|
|
@@ -494,13 +554,33 @@ var FlvViewTransport = class {
|
|
|
494
554
|
}
|
|
495
555
|
this.video = null;
|
|
496
556
|
}
|
|
557
|
+
/**
|
|
558
|
+
* Real playback statistics for this route.
|
|
559
|
+
*
|
|
560
|
+
* Both numbers used to be hardcoded zeros, which is worse than reporting
|
|
561
|
+
* nothing: the dashboard cannot tell a measured 0 kbps from an unmeasured
|
|
562
|
+
* one, so every flv.js viewer in production showed a downlink of 0 and the
|
|
563
|
+
* column read as a total outage. flv.js measures throughput itself
|
|
564
|
+
* (`statisticsInfo.speed`, KB/s), and the element counts decoded frames, so
|
|
565
|
+
* frame rate is the delta between two calls. Anything genuinely unavailable
|
|
566
|
+
* is left undefined rather than zeroed.
|
|
567
|
+
*/
|
|
497
568
|
async getStats() {
|
|
498
569
|
if (!this.video) return null;
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
570
|
+
const speedKBs = this.player?.statisticsInfo?.speed;
|
|
571
|
+
const bitrateKbps = typeof speedKBs === "number" ? Math.round(speedKBs * 8) : void 0;
|
|
572
|
+
let framesPerSecond;
|
|
573
|
+
const q = this.video.getVideoPlaybackQuality?.();
|
|
574
|
+
const count = q?.totalVideoFrames;
|
|
575
|
+
const atMs = Date.now();
|
|
576
|
+
if (typeof count === "number") {
|
|
577
|
+
const prev = this.lastFrames;
|
|
578
|
+
if (prev && atMs > prev.atMs && count >= prev.count) {
|
|
579
|
+
framesPerSecond = Math.round((count - prev.count) * 1e3 / (atMs - prev.atMs));
|
|
580
|
+
}
|
|
581
|
+
this.lastFrames = { count, atMs };
|
|
582
|
+
}
|
|
583
|
+
return { bitrateKbps, framesPerSecond, latencyMs: void 0 };
|
|
504
584
|
}
|
|
505
585
|
};
|
|
506
586
|
|
|
@@ -536,7 +616,7 @@ function createViewCandidates(mode, signaling, deliveries = []) {
|
|
|
536
616
|
}
|
|
537
617
|
|
|
538
618
|
// src/internal/telemetry.ts
|
|
539
|
-
var SDK_VERSION = "web/0.4.
|
|
619
|
+
var SDK_VERSION = "web/0.4.8";
|
|
540
620
|
var FLUSH_INTERVAL_MS = 15e3;
|
|
541
621
|
var MAX_BATCH = 64;
|
|
542
622
|
function describeDevice() {
|
|
@@ -549,11 +629,12 @@ function describeNetwork() {
|
|
|
549
629
|
return conn?.effectiveType ? { type: conn.effectiveType } : void 0;
|
|
550
630
|
}
|
|
551
631
|
var QoeReporter = class {
|
|
552
|
-
constructor(target, role, streamId, userId) {
|
|
632
|
+
constructor(target, role, streamId, userId, playerKind) {
|
|
553
633
|
this.target = target;
|
|
554
634
|
this.role = role;
|
|
555
635
|
this.streamId = streamId;
|
|
556
636
|
this.userId = userId;
|
|
637
|
+
this.playerKind = playerKind;
|
|
557
638
|
this.sessionId = randomId();
|
|
558
639
|
this.buffer = [];
|
|
559
640
|
this.timer = null;
|
|
@@ -589,6 +670,7 @@ var QoeReporter = class {
|
|
|
589
670
|
streamId: this.streamId,
|
|
590
671
|
role: this.role,
|
|
591
672
|
userId: this.userId,
|
|
673
|
+
playerKind: this.playerKind,
|
|
592
674
|
samples,
|
|
593
675
|
device: describeDevice(),
|
|
594
676
|
network: describeNetwork()
|
|
@@ -713,7 +795,8 @@ var MebiusBroadcaster = class extends TypedEmitter {
|
|
|
713
795
|
ts: Math.floor(Date.now() / 1e3),
|
|
714
796
|
bitrateKbps: stats.bitrateKbps,
|
|
715
797
|
fps: stats.framesPerSecond,
|
|
716
|
-
rttMs: stats.rttMs
|
|
798
|
+
rttMs: stats.rttMs,
|
|
799
|
+
packetLossPct: stats.packetLossPct
|
|
717
800
|
});
|
|
718
801
|
}, STATS_INTERVAL_MS);
|
|
719
802
|
}
|
|
@@ -727,6 +810,59 @@ function normalize(c, fallback) {
|
|
|
727
810
|
return c;
|
|
728
811
|
}
|
|
729
812
|
|
|
813
|
+
// src/internal/freeze-clock.ts
|
|
814
|
+
var FreezeClock = class {
|
|
815
|
+
constructor(now = Date.now) {
|
|
816
|
+
this.now = now;
|
|
817
|
+
/** When the current stall began, or null when playback is running. */
|
|
818
|
+
this.stalledSinceMs = null;
|
|
819
|
+
/** Stall time that has ended but has not yet been shipped with a sample. */
|
|
820
|
+
this.pendingMs = 0;
|
|
821
|
+
}
|
|
822
|
+
/** True while a stall is in progress. */
|
|
823
|
+
get stalled() {
|
|
824
|
+
return this.stalledSinceMs !== null;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Begin a stall. Re-entering while already stalled is ignored rather than
|
|
828
|
+
* restarting the clock: flv.js fires `waiting` repeatedly through a single
|
|
829
|
+
* long stall, and resetting the start on each would report a fraction of the
|
|
830
|
+
* freeze that actually happened.
|
|
831
|
+
*/
|
|
832
|
+
beginStall() {
|
|
833
|
+
if (this.stalledSinceMs === null) this.stalledSinceMs = this.now();
|
|
834
|
+
}
|
|
835
|
+
/** End the current stall and bank its duration. No-op when not stalled. */
|
|
836
|
+
endStall() {
|
|
837
|
+
if (this.stalledSinceMs === null) return;
|
|
838
|
+
this.pendingMs += Math.max(0, this.now() - this.stalledSinceMs);
|
|
839
|
+
this.stalledSinceMs = null;
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Freeze milliseconds to report on this tick, resetting the counter.
|
|
843
|
+
*
|
|
844
|
+
* A stall still in progress is counted up to now and its clock restarted, so
|
|
845
|
+
* a freeze longer than the sample interval is reported while it is happening
|
|
846
|
+
* rather than landing whole in whichever sample eventually follows it. Every
|
|
847
|
+
* millisecond is attributed exactly once — never dropped, never double-counted.
|
|
848
|
+
*/
|
|
849
|
+
take() {
|
|
850
|
+
if (this.stalledSinceMs !== null) {
|
|
851
|
+
const now = this.now();
|
|
852
|
+
this.pendingMs += Math.max(0, now - this.stalledSinceMs);
|
|
853
|
+
this.stalledSinceMs = now;
|
|
854
|
+
}
|
|
855
|
+
const ms = this.pendingMs;
|
|
856
|
+
this.pendingMs = 0;
|
|
857
|
+
return ms;
|
|
858
|
+
}
|
|
859
|
+
/** Forget everything. Called when a session ends. */
|
|
860
|
+
reset() {
|
|
861
|
+
this.stalledSinceMs = null;
|
|
862
|
+
this.pendingMs = 0;
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
|
|
730
866
|
// src/player.ts
|
|
731
867
|
var STATS_INTERVAL_MS2 = 2e3;
|
|
732
868
|
var FIRST_FRAME_TIMEOUT_MS = 8e3;
|
|
@@ -744,6 +880,8 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
744
880
|
this.reporter = null;
|
|
745
881
|
/** True between a `buffering` event and the element actually resuming. */
|
|
746
882
|
this.stalled = false;
|
|
883
|
+
/** Measures how long playback was actually frozen; see FreezeClock. */
|
|
884
|
+
this.freeze = new FreezeClock();
|
|
747
885
|
/** Cancels element listeners bound for the lifetime of one play(). */
|
|
748
886
|
this.elementListeners = null;
|
|
749
887
|
this.candidates = createViewCandidates(options.mode ?? "auto", signaling, deliveries);
|
|
@@ -762,6 +900,7 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
762
900
|
() => {
|
|
763
901
|
if (!this.stalled || !this.playing) return;
|
|
764
902
|
this.stalled = false;
|
|
903
|
+
this.freeze.endStall();
|
|
765
904
|
this.emit("playing", { streamId });
|
|
766
905
|
},
|
|
767
906
|
{ signal: this.elementListeners.signal }
|
|
@@ -777,7 +916,13 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
777
916
|
this.transport = candidate;
|
|
778
917
|
this.playing = true;
|
|
779
918
|
if (this.telemetry) {
|
|
780
|
-
this.reporter = new QoeReporter(
|
|
919
|
+
this.reporter = new QoeReporter(
|
|
920
|
+
this.telemetry,
|
|
921
|
+
"play",
|
|
922
|
+
streamId,
|
|
923
|
+
this.userId,
|
|
924
|
+
candidate.kind
|
|
925
|
+
);
|
|
781
926
|
this.reporter.start();
|
|
782
927
|
this.reporter.add({ ts: Math.floor(Date.now() / 1e3), firstFrameMs: Date.now() - startedAtMs });
|
|
783
928
|
}
|
|
@@ -805,6 +950,7 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
805
950
|
ELEMENT_OWNER.delete(this.video);
|
|
806
951
|
}
|
|
807
952
|
this.stalled = false;
|
|
953
|
+
this.freeze.reset();
|
|
808
954
|
this.stopStats();
|
|
809
955
|
await this.reporter?.stop();
|
|
810
956
|
this.reporter = null;
|
|
@@ -844,6 +990,7 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
844
990
|
});
|
|
845
991
|
transport.onBuffering(() => {
|
|
846
992
|
if (this.transport !== transport) return;
|
|
993
|
+
this.freeze.beginStall();
|
|
847
994
|
this.stalled = true;
|
|
848
995
|
this.emit("buffering", void 0);
|
|
849
996
|
});
|
|
@@ -851,12 +998,17 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
851
998
|
startStats() {
|
|
852
999
|
this.statsTimer = setInterval(async () => {
|
|
853
1000
|
const stats = await this.transport?.getStats();
|
|
854
|
-
|
|
1001
|
+
const freezeMs = this.freeze.take();
|
|
1002
|
+
if (!stats) {
|
|
1003
|
+
if (freezeMs > 0) this.reporter?.add({ ts: Math.floor(Date.now() / 1e3), freezeMs });
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
855
1006
|
this.emit("stats", stats);
|
|
856
1007
|
this.reporter?.add({
|
|
857
1008
|
ts: Math.floor(Date.now() / 1e3),
|
|
858
1009
|
bitrateKbps: stats.bitrateKbps,
|
|
859
|
-
fps: stats.framesPerSecond
|
|
1010
|
+
fps: stats.framesPerSecond,
|
|
1011
|
+
freezeMs
|
|
860
1012
|
});
|
|
861
1013
|
}, STATS_INTERVAL_MS2);
|
|
862
1014
|
}
|