@mebius-io/web 0.4.1 → 0.4.3

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.js CHANGED
@@ -88,6 +88,20 @@ var DEFAULT_RTC_CONFIG = {
88
88
  };
89
89
 
90
90
  // src/internal/publish-transport.ts
91
+ function preferH264(pc) {
92
+ const caps = RTCRtpSender.getCapabilities?.("video");
93
+ if (!caps?.codecs) return;
94
+ const h264 = caps.codecs.filter((c) => c.mimeType.toLowerCase() === "video/h264");
95
+ if (h264.length === 0) return;
96
+ const rest = caps.codecs.filter((c) => c.mimeType.toLowerCase() !== "video/h264");
97
+ for (const tr of pc.getTransceivers()) {
98
+ if (tr.sender.track?.kind !== "video") continue;
99
+ try {
100
+ tr.setCodecPreferences?.([...h264, ...rest]);
101
+ } catch {
102
+ }
103
+ }
104
+ }
91
105
  var WhipPublishTransport = class {
92
106
  constructor(signaling) {
93
107
  this.signaling = signaling;
@@ -100,6 +114,7 @@ var WhipPublishTransport = class {
100
114
  for (const track of stream.getTracks()) {
101
115
  pc.addTrack(track, stream);
102
116
  }
117
+ preferH264(pc);
103
118
  const offer = await pc.createOffer();
104
119
  await pc.setLocalDescription(offer);
105
120
  await waitForIceGathering(pc);
@@ -266,6 +281,8 @@ var HlsViewTransport = class {
266
281
  this.video = null;
267
282
  this.endedCb = null;
268
283
  this.bufferingCb = null;
284
+ /** Aborts this attempt's element listeners; see start(). */
285
+ this.listeners = null;
269
286
  /** True when playback only started because the element had to be muted. */
270
287
  this.mutedByPolicy = false;
271
288
  }
@@ -278,8 +295,10 @@ var HlsViewTransport = class {
278
295
  async start(streamId, video) {
279
296
  this.video = video;
280
297
  const url = this.deliveryPath ? this.signaling.deliveryUrl(this.deliveryPath) : this.signaling.scalePlaylistUrl(streamId);
281
- video.addEventListener("ended", () => this.endedCb?.());
282
- video.addEventListener("waiting", () => this.bufferingCb?.());
298
+ this.listeners = new AbortController();
299
+ const { signal } = this.listeners;
300
+ video.addEventListener("ended", () => this.endedCb?.(), { signal });
301
+ video.addEventListener("waiting", () => this.bufferingCb?.(), { signal });
283
302
  if (video.canPlayType("application/vnd.apple.mpegurl")) {
284
303
  video.src = url;
285
304
  this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
@@ -295,7 +314,7 @@ var HlsViewTransport = class {
295
314
  if (!Hls.isSupported()) {
296
315
  throw mebiusError("CONNECTION_FAILED", "Scale playback is not supported in this browser.");
297
316
  }
298
- const hls = new Hls();
317
+ const hls = new Hls({ maxLiveSyncPlaybackRate: 1.5 });
299
318
  this.hls = hls;
300
319
  hls.on(Hls.Events.ERROR, (_evt, data) => {
301
320
  if (data.fatal) this.bufferingCb?.();
@@ -305,6 +324,8 @@ var HlsViewTransport = class {
305
324
  this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
306
325
  }
307
326
  async stop() {
327
+ this.listeners?.abort();
328
+ this.listeners = null;
308
329
  this.hls?.destroy();
309
330
  this.hls = null;
310
331
  if (this.video) {
@@ -324,6 +345,40 @@ var HlsViewTransport = class {
324
345
  };
325
346
 
326
347
  // src/internal/balanced-view-transport.ts
348
+ var LIVE_FLV_CONFIG = {
349
+ enableStashBuffer: false,
350
+ stashInitialSize: 128,
351
+ lazyLoad: false,
352
+ autoCleanupSourceBuffer: true,
353
+ autoCleanupMaxBackwardDuration: 30,
354
+ autoCleanupMinBackwardDuration: 10,
355
+ reuseRedirectedURL: true
356
+ };
357
+ var MAX_DRIFT_S = 2;
358
+ var EDGE_MARGIN_S = 0.4;
359
+ var AUDIO_RETRY_MS = 2500;
360
+ function stalledAtZero(video, ms) {
361
+ if (video.currentTime > 0) return Promise.resolve(false);
362
+ return new Promise((resolve) => {
363
+ const done = (stalled) => {
364
+ clearTimeout(timer);
365
+ video.removeEventListener("timeupdate", onTime);
366
+ resolve(stalled);
367
+ };
368
+ const onTime = () => {
369
+ if (video.currentTime > 0) done(false);
370
+ };
371
+ const timer = setTimeout(() => done(true), ms);
372
+ video.addEventListener("timeupdate", onTime);
373
+ });
374
+ }
375
+ function chaseLiveEdge(video) {
376
+ const ranges = video.buffered;
377
+ if (ranges.length === 0) return;
378
+ const edge = ranges.end(ranges.length - 1);
379
+ if (edge - video.currentTime <= MAX_DRIFT_S) return;
380
+ video.currentTime = edge - EDGE_MARGIN_S;
381
+ }
327
382
  var FlvViewTransport = class {
328
383
  constructor(signaling, deliveryPath) {
329
384
  this.signaling = signaling;
@@ -332,6 +387,8 @@ var FlvViewTransport = class {
332
387
  this.video = null;
333
388
  this.endedCb = null;
334
389
  this.bufferingCb = null;
390
+ /** Aborts this attempt's element listeners; see start(). */
391
+ this.listeners = null;
335
392
  /** True when playback only started because the element had to be muted. */
336
393
  this.mutedByPolicy = false;
337
394
  }
@@ -344,8 +401,11 @@ var FlvViewTransport = class {
344
401
  async start(_streamId, video) {
345
402
  this.video = video;
346
403
  const url = this.signaling.deliveryUrl(this.deliveryPath);
347
- video.addEventListener("ended", () => this.endedCb?.());
348
- video.addEventListener("waiting", () => this.bufferingCb?.());
404
+ this.listeners = new AbortController();
405
+ const { signal } = this.listeners;
406
+ video.addEventListener("ended", () => this.endedCb?.(), { signal });
407
+ video.addEventListener("waiting", () => this.bufferingCb?.(), { signal });
408
+ video.addEventListener("timeupdate", () => chaseLiveEdge(video), { signal });
349
409
  let mod;
350
410
  try {
351
411
  mod = await import("flv.js");
@@ -356,20 +416,38 @@ var FlvViewTransport = class {
356
416
  if (!flvjs.isSupported()) {
357
417
  throw mebiusError("CONNECTION_FAILED", "Balanced playback is not supported in this browser.");
358
418
  }
359
- const player = flvjs.createPlayer({ type: "flv", url, isLive: true });
419
+ this.attachPlayer(flvjs, video, url, true);
420
+ const firstPlay = playWithAutoplayFallback(video);
421
+ firstPlay.catch(() => void 0);
422
+ if (await stalledAtZero(video, AUDIO_RETRY_MS)) {
423
+ this.teardownPlayer();
424
+ this.attachPlayer(flvjs, video, url, false);
425
+ this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
426
+ return;
427
+ }
428
+ this.mutedByPolicy = (await firstPlay).mutedByPolicy;
429
+ }
430
+ attachPlayer(flvjs, video, url, withAudio) {
431
+ const player = flvjs.createPlayer(
432
+ { type: "flv", url, isLive: true, ...withAudio ? {} : { hasAudio: false } },
433
+ LIVE_FLV_CONFIG
434
+ );
360
435
  this.player = player;
361
436
  player.on(flvjs.Events.ERROR ?? "error", () => this.bufferingCb?.());
362
437
  player.attachMediaElement(video);
363
438
  player.load();
364
- this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
439
+ }
440
+ teardownPlayer() {
441
+ if (!this.player) return;
442
+ this.player.unload();
443
+ this.player.detachMediaElement();
444
+ this.player.destroy();
445
+ this.player = null;
365
446
  }
366
447
  async stop() {
367
- if (this.player) {
368
- this.player.unload();
369
- this.player.detachMediaElement();
370
- this.player.destroy();
371
- this.player = null;
372
- }
448
+ this.listeners?.abort();
449
+ this.listeners = null;
450
+ this.teardownPlayer();
373
451
  if (this.video) {
374
452
  this.video.removeAttribute("src");
375
453
  this.video.load();
@@ -418,7 +496,7 @@ function createViewCandidates(mode, signaling, deliveries = []) {
418
496
  }
419
497
 
420
498
  // src/internal/telemetry.ts
421
- var SDK_VERSION = "web/0.4.1";
499
+ var SDK_VERSION = "web/0.4.3";
422
500
  var FLUSH_INTERVAL_MS = 15e3;
423
501
  var MAX_BATCH = 64;
424
502
  function describeDevice() {
@@ -478,7 +556,7 @@ var QoeReporter = class {
478
556
  if (beacon && typeof navigator !== "undefined" && navigator.sendBeacon) {
479
557
  const url = `${this.target.url}${this.target.url.includes("?") ? "&" : "?"}token=${encodeURIComponent(this.target.token)}`;
480
558
  try {
481
- navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
559
+ navigator.sendBeacon(url, new Blob([body], { type: "text/plain" }));
482
560
  } catch {
483
561
  }
484
562
  return;
@@ -612,6 +690,7 @@ function normalize(c, fallback) {
612
690
  // src/player.ts
613
691
  var STATS_INTERVAL_MS2 = 2e3;
614
692
  var FIRST_FRAME_TIMEOUT_MS = 8e3;
693
+ var ELEMENT_OWNER = /* @__PURE__ */ new WeakMap();
615
694
  var MebiusPlayer = class extends TypedEmitter {
616
695
  /** @internal */
617
696
  constructor(signaling, options = {}, deliveries = [], telemetry = null, userId) {
@@ -623,13 +702,30 @@ var MebiusPlayer = class extends TypedEmitter {
623
702
  this.statsTimer = null;
624
703
  this.playing = false;
625
704
  this.reporter = null;
705
+ /** True between a `buffering` event and the element actually resuming. */
706
+ this.stalled = false;
707
+ /** Cancels element listeners bound for the lifetime of one play(). */
708
+ this.elementListeners = null;
626
709
  this.candidates = createViewCandidates(options.mode ?? "auto", signaling, deliveries);
627
710
  }
628
711
  /** Start playing `streamId` into the given video element or selector. */
629
712
  async play(streamId, viewTarget) {
630
713
  if (this.playing) return;
631
714
  const video = resolveVideoElement(viewTarget);
715
+ const previous = ELEMENT_OWNER.get(video);
716
+ if (previous && previous !== this) await previous.stop();
717
+ ELEMENT_OWNER.set(video, this);
632
718
  this.video = video;
719
+ this.elementListeners = new AbortController();
720
+ video.addEventListener(
721
+ "playing",
722
+ () => {
723
+ if (!this.stalled || !this.playing) return;
724
+ this.stalled = false;
725
+ this.emit("playing", { streamId });
726
+ },
727
+ { signal: this.elementListeners.signal }
728
+ );
633
729
  const startedAtMs = Date.now();
634
730
  let lastError = null;
635
731
  for (const candidate of this.candidates) {
@@ -660,6 +756,12 @@ var MebiusPlayer = class extends TypedEmitter {
660
756
  }
661
757
  /** Stop playback and detach from the video element. */
662
758
  async stop() {
759
+ this.elementListeners?.abort();
760
+ this.elementListeners = null;
761
+ if (this.video && ELEMENT_OWNER.get(this.video) === this) {
762
+ ELEMENT_OWNER.delete(this.video);
763
+ }
764
+ this.stalled = false;
663
765
  this.stopStats();
664
766
  await this.reporter?.stop();
665
767
  this.reporter = null;
@@ -684,6 +786,7 @@ var MebiusPlayer = class extends TypedEmitter {
684
786
  });
685
787
  transport.onBuffering(() => {
686
788
  if (this.transport !== transport) return;
789
+ this.stalled = true;
687
790
  this.emit("buffering", void 0);
688
791
  });
689
792
  }