@mebius-io/web 0.4.6 → 0.4.8
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 +153 -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 +153 -19
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +153 -19
- package/dist/index.js.map +1 -1
- package/package.json +13 -12
- package/LICENSE +0 -21
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;
|
|
@@ -318,6 +356,7 @@ var HlsViewTransport = class {
|
|
|
318
356
|
constructor(signaling, deliveryPath) {
|
|
319
357
|
this.signaling = signaling;
|
|
320
358
|
this.deliveryPath = deliveryPath;
|
|
359
|
+
this.kind = "hls";
|
|
321
360
|
this.hls = null;
|
|
322
361
|
this.video = null;
|
|
323
362
|
this.endedCb = null;
|
|
@@ -354,7 +393,7 @@ var HlsViewTransport = class {
|
|
|
354
393
|
this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
|
|
355
394
|
return;
|
|
356
395
|
}
|
|
357
|
-
const hls = new Hls({ maxLiveSyncPlaybackRate: 1.
|
|
396
|
+
const hls = new Hls({ maxLiveSyncPlaybackRate: 1.1 });
|
|
358
397
|
this.hls = hls;
|
|
359
398
|
hls.on(Hls.Events.ERROR, (_evt, data) => {
|
|
360
399
|
if (data.fatal) this.bufferingCb?.();
|
|
@@ -423,6 +462,7 @@ var FlvViewTransport = class {
|
|
|
423
462
|
constructor(signaling, deliveryPath) {
|
|
424
463
|
this.signaling = signaling;
|
|
425
464
|
this.deliveryPath = deliveryPath;
|
|
465
|
+
this.kind = "flv_js";
|
|
426
466
|
this.player = null;
|
|
427
467
|
this.video = null;
|
|
428
468
|
this.endedCb = null;
|
|
@@ -431,6 +471,8 @@ var FlvViewTransport = class {
|
|
|
431
471
|
this.listeners = null;
|
|
432
472
|
/** True when playback only started because the element had to be muted. */
|
|
433
473
|
this.mutedByPolicy = false;
|
|
474
|
+
/** Decoded-frame count and timestamp of the previous getStats() call. */
|
|
475
|
+
this.lastFrames = null;
|
|
434
476
|
}
|
|
435
477
|
onEnded(cb) {
|
|
436
478
|
this.endedCb = cb;
|
|
@@ -494,13 +536,33 @@ var FlvViewTransport = class {
|
|
|
494
536
|
}
|
|
495
537
|
this.video = null;
|
|
496
538
|
}
|
|
539
|
+
/**
|
|
540
|
+
* Real playback statistics for this route.
|
|
541
|
+
*
|
|
542
|
+
* Both numbers used to be hardcoded zeros, which is worse than reporting
|
|
543
|
+
* nothing: the dashboard cannot tell a measured 0 kbps from an unmeasured
|
|
544
|
+
* one, so every flv.js viewer in production showed a downlink of 0 and the
|
|
545
|
+
* column read as a total outage. flv.js measures throughput itself
|
|
546
|
+
* (`statisticsInfo.speed`, KB/s), and the element counts decoded frames, so
|
|
547
|
+
* frame rate is the delta between two calls. Anything genuinely unavailable
|
|
548
|
+
* is left undefined rather than zeroed.
|
|
549
|
+
*/
|
|
497
550
|
async getStats() {
|
|
498
551
|
if (!this.video) return null;
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
552
|
+
const speedKBs = this.player?.statisticsInfo?.speed;
|
|
553
|
+
const bitrateKbps = typeof speedKBs === "number" ? Math.round(speedKBs * 8) : void 0;
|
|
554
|
+
let framesPerSecond;
|
|
555
|
+
const q = this.video.getVideoPlaybackQuality?.();
|
|
556
|
+
const count = q?.totalVideoFrames;
|
|
557
|
+
const atMs = Date.now();
|
|
558
|
+
if (typeof count === "number") {
|
|
559
|
+
const prev = this.lastFrames;
|
|
560
|
+
if (prev && atMs > prev.atMs && count >= prev.count) {
|
|
561
|
+
framesPerSecond = Math.round((count - prev.count) * 1e3 / (atMs - prev.atMs));
|
|
562
|
+
}
|
|
563
|
+
this.lastFrames = { count, atMs };
|
|
564
|
+
}
|
|
565
|
+
return { bitrateKbps, framesPerSecond, latencyMs: void 0 };
|
|
504
566
|
}
|
|
505
567
|
};
|
|
506
568
|
|
|
@@ -536,7 +598,7 @@ function createViewCandidates(mode, signaling, deliveries = []) {
|
|
|
536
598
|
}
|
|
537
599
|
|
|
538
600
|
// src/internal/telemetry.ts
|
|
539
|
-
var SDK_VERSION = "web/0.4.
|
|
601
|
+
var SDK_VERSION = "web/0.4.8";
|
|
540
602
|
var FLUSH_INTERVAL_MS = 15e3;
|
|
541
603
|
var MAX_BATCH = 64;
|
|
542
604
|
function describeDevice() {
|
|
@@ -549,11 +611,12 @@ function describeNetwork() {
|
|
|
549
611
|
return conn?.effectiveType ? { type: conn.effectiveType } : void 0;
|
|
550
612
|
}
|
|
551
613
|
var QoeReporter = class {
|
|
552
|
-
constructor(target, role, streamId, userId) {
|
|
614
|
+
constructor(target, role, streamId, userId, playerKind) {
|
|
553
615
|
this.target = target;
|
|
554
616
|
this.role = role;
|
|
555
617
|
this.streamId = streamId;
|
|
556
618
|
this.userId = userId;
|
|
619
|
+
this.playerKind = playerKind;
|
|
557
620
|
this.sessionId = randomId();
|
|
558
621
|
this.buffer = [];
|
|
559
622
|
this.timer = null;
|
|
@@ -589,6 +652,7 @@ var QoeReporter = class {
|
|
|
589
652
|
streamId: this.streamId,
|
|
590
653
|
role: this.role,
|
|
591
654
|
userId: this.userId,
|
|
655
|
+
playerKind: this.playerKind,
|
|
592
656
|
samples,
|
|
593
657
|
device: describeDevice(),
|
|
594
658
|
network: describeNetwork()
|
|
@@ -713,7 +777,8 @@ var MebiusBroadcaster = class extends TypedEmitter {
|
|
|
713
777
|
ts: Math.floor(Date.now() / 1e3),
|
|
714
778
|
bitrateKbps: stats.bitrateKbps,
|
|
715
779
|
fps: stats.framesPerSecond,
|
|
716
|
-
rttMs: stats.rttMs
|
|
780
|
+
rttMs: stats.rttMs,
|
|
781
|
+
packetLossPct: stats.packetLossPct
|
|
717
782
|
});
|
|
718
783
|
}, STATS_INTERVAL_MS);
|
|
719
784
|
}
|
|
@@ -727,6 +792,59 @@ function normalize(c, fallback) {
|
|
|
727
792
|
return c;
|
|
728
793
|
}
|
|
729
794
|
|
|
795
|
+
// src/internal/freeze-clock.ts
|
|
796
|
+
var FreezeClock = class {
|
|
797
|
+
constructor(now = Date.now) {
|
|
798
|
+
this.now = now;
|
|
799
|
+
/** When the current stall began, or null when playback is running. */
|
|
800
|
+
this.stalledSinceMs = null;
|
|
801
|
+
/** Stall time that has ended but has not yet been shipped with a sample. */
|
|
802
|
+
this.pendingMs = 0;
|
|
803
|
+
}
|
|
804
|
+
/** True while a stall is in progress. */
|
|
805
|
+
get stalled() {
|
|
806
|
+
return this.stalledSinceMs !== null;
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Begin a stall. Re-entering while already stalled is ignored rather than
|
|
810
|
+
* restarting the clock: flv.js fires `waiting` repeatedly through a single
|
|
811
|
+
* long stall, and resetting the start on each would report a fraction of the
|
|
812
|
+
* freeze that actually happened.
|
|
813
|
+
*/
|
|
814
|
+
beginStall() {
|
|
815
|
+
if (this.stalledSinceMs === null) this.stalledSinceMs = this.now();
|
|
816
|
+
}
|
|
817
|
+
/** End the current stall and bank its duration. No-op when not stalled. */
|
|
818
|
+
endStall() {
|
|
819
|
+
if (this.stalledSinceMs === null) return;
|
|
820
|
+
this.pendingMs += Math.max(0, this.now() - this.stalledSinceMs);
|
|
821
|
+
this.stalledSinceMs = null;
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Freeze milliseconds to report on this tick, resetting the counter.
|
|
825
|
+
*
|
|
826
|
+
* A stall still in progress is counted up to now and its clock restarted, so
|
|
827
|
+
* a freeze longer than the sample interval is reported while it is happening
|
|
828
|
+
* rather than landing whole in whichever sample eventually follows it. Every
|
|
829
|
+
* millisecond is attributed exactly once — never dropped, never double-counted.
|
|
830
|
+
*/
|
|
831
|
+
take() {
|
|
832
|
+
if (this.stalledSinceMs !== null) {
|
|
833
|
+
const now = this.now();
|
|
834
|
+
this.pendingMs += Math.max(0, now - this.stalledSinceMs);
|
|
835
|
+
this.stalledSinceMs = now;
|
|
836
|
+
}
|
|
837
|
+
const ms = this.pendingMs;
|
|
838
|
+
this.pendingMs = 0;
|
|
839
|
+
return ms;
|
|
840
|
+
}
|
|
841
|
+
/** Forget everything. Called when a session ends. */
|
|
842
|
+
reset() {
|
|
843
|
+
this.stalledSinceMs = null;
|
|
844
|
+
this.pendingMs = 0;
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
|
|
730
848
|
// src/player.ts
|
|
731
849
|
var STATS_INTERVAL_MS2 = 2e3;
|
|
732
850
|
var FIRST_FRAME_TIMEOUT_MS = 8e3;
|
|
@@ -744,6 +862,8 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
744
862
|
this.reporter = null;
|
|
745
863
|
/** True between a `buffering` event and the element actually resuming. */
|
|
746
864
|
this.stalled = false;
|
|
865
|
+
/** Measures how long playback was actually frozen; see FreezeClock. */
|
|
866
|
+
this.freeze = new FreezeClock();
|
|
747
867
|
/** Cancels element listeners bound for the lifetime of one play(). */
|
|
748
868
|
this.elementListeners = null;
|
|
749
869
|
this.candidates = createViewCandidates(options.mode ?? "auto", signaling, deliveries);
|
|
@@ -762,6 +882,7 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
762
882
|
() => {
|
|
763
883
|
if (!this.stalled || !this.playing) return;
|
|
764
884
|
this.stalled = false;
|
|
885
|
+
this.freeze.endStall();
|
|
765
886
|
this.emit("playing", { streamId });
|
|
766
887
|
},
|
|
767
888
|
{ signal: this.elementListeners.signal }
|
|
@@ -777,7 +898,13 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
777
898
|
this.transport = candidate;
|
|
778
899
|
this.playing = true;
|
|
779
900
|
if (this.telemetry) {
|
|
780
|
-
this.reporter = new QoeReporter(
|
|
901
|
+
this.reporter = new QoeReporter(
|
|
902
|
+
this.telemetry,
|
|
903
|
+
"play",
|
|
904
|
+
streamId,
|
|
905
|
+
this.userId,
|
|
906
|
+
candidate.kind
|
|
907
|
+
);
|
|
781
908
|
this.reporter.start();
|
|
782
909
|
this.reporter.add({ ts: Math.floor(Date.now() / 1e3), firstFrameMs: Date.now() - startedAtMs });
|
|
783
910
|
}
|
|
@@ -805,6 +932,7 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
805
932
|
ELEMENT_OWNER.delete(this.video);
|
|
806
933
|
}
|
|
807
934
|
this.stalled = false;
|
|
935
|
+
this.freeze.reset();
|
|
808
936
|
this.stopStats();
|
|
809
937
|
await this.reporter?.stop();
|
|
810
938
|
this.reporter = null;
|
|
@@ -844,6 +972,7 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
844
972
|
});
|
|
845
973
|
transport.onBuffering(() => {
|
|
846
974
|
if (this.transport !== transport) return;
|
|
975
|
+
this.freeze.beginStall();
|
|
847
976
|
this.stalled = true;
|
|
848
977
|
this.emit("buffering", void 0);
|
|
849
978
|
});
|
|
@@ -851,12 +980,17 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
851
980
|
startStats() {
|
|
852
981
|
this.statsTimer = setInterval(async () => {
|
|
853
982
|
const stats = await this.transport?.getStats();
|
|
854
|
-
|
|
983
|
+
const freezeMs = this.freeze.take();
|
|
984
|
+
if (!stats) {
|
|
985
|
+
if (freezeMs > 0) this.reporter?.add({ ts: Math.floor(Date.now() / 1e3), freezeMs });
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
855
988
|
this.emit("stats", stats);
|
|
856
989
|
this.reporter?.add({
|
|
857
990
|
ts: Math.floor(Date.now() / 1e3),
|
|
858
991
|
bitrateKbps: stats.bitrateKbps,
|
|
859
|
-
fps: stats.framesPerSecond
|
|
992
|
+
fps: stats.framesPerSecond,
|
|
993
|
+
freezeMs
|
|
860
994
|
});
|
|
861
995
|
}, STATS_INTERVAL_MS2);
|
|
862
996
|
}
|