@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.
- package/README.md +25 -2
- package/dist/index.cjs +171 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -3
- package/dist/index.d.ts +32 -3
- package/dist/index.global.js +171 -14
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +171 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -147,6 +147,28 @@ var WhipPublishTransport = class {
|
|
|
147
147
|
}
|
|
148
148
|
};
|
|
149
149
|
|
|
150
|
+
// src/internal/autoplay.ts
|
|
151
|
+
function isAutoplayBlocked(e) {
|
|
152
|
+
return typeof e === "object" && e !== null && e.name === "NotAllowedError";
|
|
153
|
+
}
|
|
154
|
+
async function playWithAutoplayFallback(video) {
|
|
155
|
+
video.playsInline = true;
|
|
156
|
+
try {
|
|
157
|
+
await video.play();
|
|
158
|
+
return { mutedByPolicy: false };
|
|
159
|
+
} catch (e) {
|
|
160
|
+
if (!isAutoplayBlocked(e) || video.muted) throw e;
|
|
161
|
+
video.muted = true;
|
|
162
|
+
await video.play();
|
|
163
|
+
return { mutedByPolicy: true };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function resetVideoElement(video) {
|
|
167
|
+
video.srcObject = null;
|
|
168
|
+
video.removeAttribute("src");
|
|
169
|
+
video.load();
|
|
170
|
+
}
|
|
171
|
+
|
|
150
172
|
// src/internal/ll-view-transport.ts
|
|
151
173
|
var WhepViewTransport = class {
|
|
152
174
|
constructor(signaling) {
|
|
@@ -155,6 +177,10 @@ var WhepViewTransport = class {
|
|
|
155
177
|
this.resourceUrl = null;
|
|
156
178
|
this.endedCb = null;
|
|
157
179
|
this.bufferingCb = null;
|
|
180
|
+
/** Element this route attached a MediaStream to, so stop() can release it. */
|
|
181
|
+
this.video = null;
|
|
182
|
+
/** True when playback only started because the element had to be muted. */
|
|
183
|
+
this.mutedByPolicy = false;
|
|
158
184
|
}
|
|
159
185
|
onEnded(cb) {
|
|
160
186
|
this.endedCb = cb;
|
|
@@ -163,6 +189,7 @@ var WhepViewTransport = class {
|
|
|
163
189
|
this.bufferingCb = cb;
|
|
164
190
|
}
|
|
165
191
|
async start(streamId, video) {
|
|
192
|
+
this.video = video;
|
|
166
193
|
const pc = new RTCPeerConnection(DEFAULT_RTC_CONFIG);
|
|
167
194
|
this.pc = pc;
|
|
168
195
|
const remote = new MediaStream();
|
|
@@ -171,7 +198,9 @@ var WhepViewTransport = class {
|
|
|
171
198
|
pc.ontrack = (ev) => {
|
|
172
199
|
remote.addTrack(ev.track);
|
|
173
200
|
video.srcObject = remote;
|
|
174
|
-
void video
|
|
201
|
+
void playWithAutoplayFallback(video).then((o) => {
|
|
202
|
+
this.mutedByPolicy = o.mutedByPolicy;
|
|
203
|
+
}).catch(() => {
|
|
175
204
|
});
|
|
176
205
|
};
|
|
177
206
|
pc.onconnectionstatechange = () => {
|
|
@@ -198,6 +227,8 @@ var WhepViewTransport = class {
|
|
|
198
227
|
this.resourceUrl = null;
|
|
199
228
|
this.pc?.close();
|
|
200
229
|
this.pc = null;
|
|
230
|
+
if (this.video) resetVideoElement(this.video);
|
|
231
|
+
this.video = null;
|
|
201
232
|
}
|
|
202
233
|
async getStats() {
|
|
203
234
|
if (!this.pc) return null;
|
|
@@ -235,6 +266,8 @@ var HlsViewTransport = class {
|
|
|
235
266
|
this.video = null;
|
|
236
267
|
this.endedCb = null;
|
|
237
268
|
this.bufferingCb = null;
|
|
269
|
+
/** True when playback only started because the element had to be muted. */
|
|
270
|
+
this.mutedByPolicy = false;
|
|
238
271
|
}
|
|
239
272
|
onEnded(cb) {
|
|
240
273
|
this.endedCb = cb;
|
|
@@ -249,7 +282,7 @@ var HlsViewTransport = class {
|
|
|
249
282
|
video.addEventListener("waiting", () => this.bufferingCb?.());
|
|
250
283
|
if (video.canPlayType("application/vnd.apple.mpegurl")) {
|
|
251
284
|
video.src = url;
|
|
252
|
-
await video
|
|
285
|
+
this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
|
|
253
286
|
return;
|
|
254
287
|
}
|
|
255
288
|
let mod;
|
|
@@ -262,14 +295,14 @@ var HlsViewTransport = class {
|
|
|
262
295
|
if (!Hls.isSupported()) {
|
|
263
296
|
throw mebiusError("CONNECTION_FAILED", "Scale playback is not supported in this browser.");
|
|
264
297
|
}
|
|
265
|
-
const hls = new Hls(
|
|
298
|
+
const hls = new Hls();
|
|
266
299
|
this.hls = hls;
|
|
267
300
|
hls.on(Hls.Events.ERROR, (_evt, data) => {
|
|
268
301
|
if (data.fatal) this.bufferingCb?.();
|
|
269
302
|
});
|
|
270
303
|
hls.loadSource(url);
|
|
271
304
|
hls.attachMedia(video);
|
|
272
|
-
await video
|
|
305
|
+
this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
|
|
273
306
|
}
|
|
274
307
|
async stop() {
|
|
275
308
|
this.hls?.destroy();
|
|
@@ -299,6 +332,8 @@ var FlvViewTransport = class {
|
|
|
299
332
|
this.video = null;
|
|
300
333
|
this.endedCb = null;
|
|
301
334
|
this.bufferingCb = null;
|
|
335
|
+
/** True when playback only started because the element had to be muted. */
|
|
336
|
+
this.mutedByPolicy = false;
|
|
302
337
|
}
|
|
303
338
|
onEnded(cb) {
|
|
304
339
|
this.endedCb = cb;
|
|
@@ -326,7 +361,7 @@ var FlvViewTransport = class {
|
|
|
326
361
|
player.on(flvjs.Events.ERROR ?? "error", () => this.bufferingCb?.());
|
|
327
362
|
player.attachMediaElement(video);
|
|
328
363
|
player.load();
|
|
329
|
-
await
|
|
364
|
+
this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
|
|
330
365
|
}
|
|
331
366
|
async stop() {
|
|
332
367
|
if (this.player) {
|
|
@@ -382,17 +417,103 @@ function createViewCandidates(mode, signaling, deliveries = []) {
|
|
|
382
417
|
}
|
|
383
418
|
}
|
|
384
419
|
|
|
420
|
+
// src/internal/telemetry.ts
|
|
421
|
+
var SDK_VERSION = "web/0.4.1";
|
|
422
|
+
var FLUSH_INTERVAL_MS = 15e3;
|
|
423
|
+
var MAX_BATCH = 64;
|
|
424
|
+
function describeDevice() {
|
|
425
|
+
const nav = typeof navigator === "undefined" ? void 0 : navigator;
|
|
426
|
+
const uaData = nav?.userAgentData;
|
|
427
|
+
return { os: uaData?.platform || nav?.platform || void 0, sdk: SDK_VERSION };
|
|
428
|
+
}
|
|
429
|
+
function describeNetwork() {
|
|
430
|
+
const conn = typeof navigator === "undefined" ? void 0 : navigator.connection;
|
|
431
|
+
return conn?.effectiveType ? { type: conn.effectiveType } : void 0;
|
|
432
|
+
}
|
|
433
|
+
var QoeReporter = class {
|
|
434
|
+
constructor(target, role, streamId, userId) {
|
|
435
|
+
this.target = target;
|
|
436
|
+
this.role = role;
|
|
437
|
+
this.streamId = streamId;
|
|
438
|
+
this.userId = userId;
|
|
439
|
+
this.sessionId = randomId();
|
|
440
|
+
this.buffer = [];
|
|
441
|
+
this.timer = null;
|
|
442
|
+
this.unloadHandler = null;
|
|
443
|
+
}
|
|
444
|
+
start() {
|
|
445
|
+
if (this.timer) return;
|
|
446
|
+
this.timer = setInterval(() => void this.flush(), FLUSH_INTERVAL_MS);
|
|
447
|
+
if (typeof window !== "undefined") {
|
|
448
|
+
this.unloadHandler = () => void this.flush(true);
|
|
449
|
+
window.addEventListener("pagehide", this.unloadHandler);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
add(sample) {
|
|
453
|
+
this.buffer.push(sample);
|
|
454
|
+
if (this.buffer.length >= MAX_BATCH) void this.flush();
|
|
455
|
+
}
|
|
456
|
+
async stop() {
|
|
457
|
+
if (this.timer) clearInterval(this.timer);
|
|
458
|
+
this.timer = null;
|
|
459
|
+
if (this.unloadHandler && typeof window !== "undefined") {
|
|
460
|
+
window.removeEventListener("pagehide", this.unloadHandler);
|
|
461
|
+
}
|
|
462
|
+
this.unloadHandler = null;
|
|
463
|
+
await this.flush();
|
|
464
|
+
}
|
|
465
|
+
/** Send and clear the buffer. `beacon` uses sendBeacon, for page-unload flushes. */
|
|
466
|
+
async flush(beacon = false) {
|
|
467
|
+
if (!this.buffer.length) return;
|
|
468
|
+
const samples = this.buffer.splice(0, MAX_BATCH);
|
|
469
|
+
const body = JSON.stringify({
|
|
470
|
+
sessionId: this.sessionId,
|
|
471
|
+
streamId: this.streamId,
|
|
472
|
+
role: this.role,
|
|
473
|
+
userId: this.userId,
|
|
474
|
+
samples,
|
|
475
|
+
device: describeDevice(),
|
|
476
|
+
network: describeNetwork()
|
|
477
|
+
});
|
|
478
|
+
if (beacon && typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
479
|
+
const url = `${this.target.url}${this.target.url.includes("?") ? "&" : "?"}token=${encodeURIComponent(this.target.token)}`;
|
|
480
|
+
try {
|
|
481
|
+
navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
|
|
482
|
+
} catch {
|
|
483
|
+
}
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
await fetch(this.target.url, {
|
|
488
|
+
method: "POST",
|
|
489
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.target.token}` },
|
|
490
|
+
body,
|
|
491
|
+
keepalive: true
|
|
492
|
+
});
|
|
493
|
+
} catch {
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
function randomId() {
|
|
498
|
+
const c = typeof crypto === "undefined" ? void 0 : crypto;
|
|
499
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
500
|
+
return `s-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
501
|
+
}
|
|
502
|
+
|
|
385
503
|
// src/broadcaster.ts
|
|
386
504
|
var STATS_INTERVAL_MS = 2e3;
|
|
387
505
|
var MebiusBroadcaster = class extends TypedEmitter {
|
|
388
506
|
/** @internal */
|
|
389
|
-
constructor(signaling, options) {
|
|
507
|
+
constructor(signaling, options, telemetry = null, userId) {
|
|
390
508
|
super();
|
|
391
509
|
this.options = options;
|
|
510
|
+
this.telemetry = telemetry;
|
|
511
|
+
this.userId = userId;
|
|
392
512
|
this.stream = null;
|
|
393
513
|
this.facingMode = "user";
|
|
394
514
|
this.statsTimer = null;
|
|
395
515
|
this.started = false;
|
|
516
|
+
this.reporter = null;
|
|
396
517
|
this.transport = createPublishTransport(signaling);
|
|
397
518
|
}
|
|
398
519
|
/** Begin broadcasting under the given stream id. */
|
|
@@ -401,12 +522,18 @@ var MebiusBroadcaster = class extends TypedEmitter {
|
|
|
401
522
|
this.stream = await this.capture();
|
|
402
523
|
await this.transport.start(streamId, this.stream);
|
|
403
524
|
this.started = true;
|
|
525
|
+
if (this.telemetry) {
|
|
526
|
+
this.reporter = new QoeReporter(this.telemetry, "pub", streamId, this.userId);
|
|
527
|
+
this.reporter.start();
|
|
528
|
+
}
|
|
404
529
|
this.startStats();
|
|
405
530
|
this.emit("started", { streamId });
|
|
406
531
|
}
|
|
407
532
|
/** Stop broadcasting and release the camera/microphone. */
|
|
408
533
|
async stop() {
|
|
409
534
|
this.stopStats();
|
|
535
|
+
await this.reporter?.stop();
|
|
536
|
+
this.reporter = null;
|
|
410
537
|
await this.transport.stop();
|
|
411
538
|
this.stream?.getTracks().forEach((t) => t.stop());
|
|
412
539
|
this.stream = null;
|
|
@@ -462,7 +589,14 @@ var MebiusBroadcaster = class extends TypedEmitter {
|
|
|
462
589
|
startStats() {
|
|
463
590
|
this.statsTimer = setInterval(async () => {
|
|
464
591
|
const stats = await this.transport.getStats();
|
|
465
|
-
if (stats)
|
|
592
|
+
if (!stats) return;
|
|
593
|
+
this.emit("stats", stats);
|
|
594
|
+
this.reporter?.add({
|
|
595
|
+
ts: Math.floor(Date.now() / 1e3),
|
|
596
|
+
bitrateKbps: stats.bitrateKbps,
|
|
597
|
+
fps: stats.framesPerSecond,
|
|
598
|
+
rttMs: stats.rttMs
|
|
599
|
+
});
|
|
466
600
|
}, STATS_INTERVAL_MS);
|
|
467
601
|
}
|
|
468
602
|
stopStats() {
|
|
@@ -480,12 +614,15 @@ var STATS_INTERVAL_MS2 = 2e3;
|
|
|
480
614
|
var FIRST_FRAME_TIMEOUT_MS = 8e3;
|
|
481
615
|
var MebiusPlayer = class extends TypedEmitter {
|
|
482
616
|
/** @internal */
|
|
483
|
-
constructor(signaling, options = {}, deliveries = []) {
|
|
617
|
+
constructor(signaling, options = {}, deliveries = [], telemetry = null, userId) {
|
|
484
618
|
super();
|
|
619
|
+
this.telemetry = telemetry;
|
|
620
|
+
this.userId = userId;
|
|
485
621
|
this.transport = null;
|
|
486
622
|
this.video = null;
|
|
487
623
|
this.statsTimer = null;
|
|
488
624
|
this.playing = false;
|
|
625
|
+
this.reporter = null;
|
|
489
626
|
this.candidates = createViewCandidates(options.mode ?? "auto", signaling, deliveries);
|
|
490
627
|
}
|
|
491
628
|
/** Start playing `streamId` into the given video element or selector. */
|
|
@@ -493,14 +630,21 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
493
630
|
if (this.playing) return;
|
|
494
631
|
const video = resolveVideoElement(viewTarget);
|
|
495
632
|
this.video = video;
|
|
633
|
+
const startedAtMs = Date.now();
|
|
496
634
|
let lastError = null;
|
|
497
635
|
for (const candidate of this.candidates) {
|
|
498
636
|
try {
|
|
637
|
+
resetVideoElement(video);
|
|
499
638
|
this.attach(candidate);
|
|
500
639
|
await candidate.start(streamId, video);
|
|
501
640
|
if (await hasFirstFrame(video)) {
|
|
502
641
|
this.transport = candidate;
|
|
503
642
|
this.playing = true;
|
|
643
|
+
if (this.telemetry) {
|
|
644
|
+
this.reporter = new QoeReporter(this.telemetry, "play", streamId, this.userId);
|
|
645
|
+
this.reporter.start();
|
|
646
|
+
this.reporter.add({ ts: Math.floor(Date.now() / 1e3), firstFrameMs: Date.now() - startedAtMs });
|
|
647
|
+
}
|
|
504
648
|
this.startStats();
|
|
505
649
|
this.emit("playing", { streamId });
|
|
506
650
|
return;
|
|
@@ -517,6 +661,8 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
517
661
|
/** Stop playback and detach from the video element. */
|
|
518
662
|
async stop() {
|
|
519
663
|
this.stopStats();
|
|
664
|
+
await this.reporter?.stop();
|
|
665
|
+
this.reporter = null;
|
|
520
666
|
await this.transport?.stop();
|
|
521
667
|
this.transport = null;
|
|
522
668
|
this.video = null;
|
|
@@ -532,6 +678,8 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
532
678
|
if (this.transport !== transport) return;
|
|
533
679
|
this.playing = false;
|
|
534
680
|
this.stopStats();
|
|
681
|
+
void this.reporter?.stop();
|
|
682
|
+
this.reporter = null;
|
|
535
683
|
this.emit("ended", void 0);
|
|
536
684
|
});
|
|
537
685
|
transport.onBuffering(() => {
|
|
@@ -542,7 +690,13 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
542
690
|
startStats() {
|
|
543
691
|
this.statsTimer = setInterval(async () => {
|
|
544
692
|
const stats = await this.transport?.getStats();
|
|
545
|
-
if (stats)
|
|
693
|
+
if (!stats) return;
|
|
694
|
+
this.emit("stats", stats);
|
|
695
|
+
this.reporter?.add({
|
|
696
|
+
ts: Math.floor(Date.now() / 1e3),
|
|
697
|
+
bitrateKbps: stats.bitrateKbps,
|
|
698
|
+
fps: stats.framesPerSecond
|
|
699
|
+
});
|
|
546
700
|
}, STATS_INTERVAL_MS2);
|
|
547
701
|
}
|
|
548
702
|
stopStats() {
|
|
@@ -677,10 +831,12 @@ function readToken(token) {
|
|
|
677
831
|
// src/client.ts
|
|
678
832
|
var MebiusClient = class extends TypedEmitter {
|
|
679
833
|
/** @internal */
|
|
680
|
-
constructor(config2, token, deliveries = []) {
|
|
834
|
+
constructor(config2, token, deliveries = [], telemetry = null, userId) {
|
|
681
835
|
super();
|
|
682
836
|
this.token = token;
|
|
683
837
|
this.deliveries = deliveries;
|
|
838
|
+
this.telemetry = telemetry;
|
|
839
|
+
this.userId = userId;
|
|
684
840
|
this.expiryTimer = null;
|
|
685
841
|
this.connected = false;
|
|
686
842
|
this.signaling = new SignalingClient(config2.gateway, token);
|
|
@@ -705,12 +861,12 @@ var MebiusClient = class extends TypedEmitter {
|
|
|
705
861
|
/** Create a broadcaster bound to this connection. */
|
|
706
862
|
createBroadcaster(options = {}) {
|
|
707
863
|
this.assertConnected();
|
|
708
|
-
return new MebiusBroadcaster(this.signaling, options);
|
|
864
|
+
return new MebiusBroadcaster(this.signaling, options, this.telemetry, this.userId);
|
|
709
865
|
}
|
|
710
866
|
/** Create a player bound to this connection. */
|
|
711
867
|
createPlayer(options = {}) {
|
|
712
868
|
this.assertConnected();
|
|
713
|
-
return new MebiusPlayer(this.signaling, options, this.deliveries);
|
|
869
|
+
return new MebiusPlayer(this.signaling, options, this.deliveries, this.telemetry, this.userId);
|
|
714
870
|
}
|
|
715
871
|
/**
|
|
716
872
|
* Create a monitor: a player tuned for watching a stream you are interacting
|
|
@@ -725,7 +881,7 @@ var MebiusClient = class extends TypedEmitter {
|
|
|
725
881
|
*/
|
|
726
882
|
createMonitor() {
|
|
727
883
|
this.assertConnected();
|
|
728
|
-
return new MebiusPlayer(this.signaling, { mode: "low-latency" }, this.deliveries);
|
|
884
|
+
return new MebiusPlayer(this.signaling, { mode: "low-latency" }, this.deliveries, this.telemetry, this.userId);
|
|
729
885
|
}
|
|
730
886
|
/** Close the connection and release resources. */
|
|
731
887
|
disconnect(reason) {
|
|
@@ -758,7 +914,8 @@ var Mebius = {
|
|
|
758
914
|
throw mebiusError("UNKNOWN", "Call Mebius.init() before Mebius.connect().");
|
|
759
915
|
}
|
|
760
916
|
if (!options.token) throw mebiusError("UNKNOWN", "Mebius.connect requires a token.");
|
|
761
|
-
const
|
|
917
|
+
const telemetry = options.beaconToken && options.beaconUrl ? { token: options.beaconToken, url: options.beaconUrl } : null;
|
|
918
|
+
const client = new MebiusClient(config, options.token, options.deliveries ?? [], telemetry, options.userId);
|
|
762
919
|
client.open();
|
|
763
920
|
return client;
|
|
764
921
|
},
|