@mebius-io/web 0.4.1 → 0.4.2

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 CHANGED
@@ -55,12 +55,12 @@ external deps. `Mebius` becomes a global.
55
55
  ```
56
56
 
57
57
  File + full PHP example: [`standalone/`](./standalone/). Raw download:
58
- `https://raw.githubusercontent.com/russimobiledroidx/mebius-web-sdk/v0.4.1/packages/web/standalone/mebius.min.js`
58
+ `https://raw.githubusercontent.com/russimobiledroidx/mebius-web-sdk/v0.4.2/packages/web/standalone/mebius.min.js`
59
59
 
60
60
  The drop-in file is not part of the npm package — `files` ships only `dist` — so it
61
61
  is fetched from the tag, and the tag must match the version you installed. For a
62
62
  page with a build step, or one that can use an import map, prefer the ESM path:
63
- `https://esm.sh/@mebius-io/web@0.4.1`.
63
+ `https://esm.sh/@mebius-io/web@0.4.2`.
64
64
 
65
65
  ## Quick Start
66
66
 
package/dist/index.cjs CHANGED
@@ -129,6 +129,20 @@ var DEFAULT_RTC_CONFIG = {
129
129
  };
130
130
 
131
131
  // src/internal/publish-transport.ts
132
+ function preferH264(pc) {
133
+ const caps = RTCRtpSender.getCapabilities?.("video");
134
+ if (!caps?.codecs) return;
135
+ const h264 = caps.codecs.filter((c) => c.mimeType.toLowerCase() === "video/h264");
136
+ if (h264.length === 0) return;
137
+ const rest = caps.codecs.filter((c) => c.mimeType.toLowerCase() !== "video/h264");
138
+ for (const tr of pc.getTransceivers()) {
139
+ if (tr.sender.track?.kind !== "video") continue;
140
+ try {
141
+ tr.setCodecPreferences?.([...h264, ...rest]);
142
+ } catch {
143
+ }
144
+ }
145
+ }
132
146
  var WhipPublishTransport = class {
133
147
  constructor(signaling) {
134
148
  this.signaling = signaling;
@@ -141,6 +155,7 @@ var WhipPublishTransport = class {
141
155
  for (const track of stream.getTracks()) {
142
156
  pc.addTrack(track, stream);
143
157
  }
158
+ preferH264(pc);
144
159
  const offer = await pc.createOffer();
145
160
  await pc.setLocalDescription(offer);
146
161
  await waitForIceGathering(pc);
@@ -307,6 +322,8 @@ var HlsViewTransport = class {
307
322
  this.video = null;
308
323
  this.endedCb = null;
309
324
  this.bufferingCb = null;
325
+ /** Aborts this attempt's element listeners; see start(). */
326
+ this.listeners = null;
310
327
  /** True when playback only started because the element had to be muted. */
311
328
  this.mutedByPolicy = false;
312
329
  }
@@ -319,8 +336,10 @@ var HlsViewTransport = class {
319
336
  async start(streamId, video) {
320
337
  this.video = video;
321
338
  const url = this.deliveryPath ? this.signaling.deliveryUrl(this.deliveryPath) : this.signaling.scalePlaylistUrl(streamId);
322
- video.addEventListener("ended", () => this.endedCb?.());
323
- video.addEventListener("waiting", () => this.bufferingCb?.());
339
+ this.listeners = new AbortController();
340
+ const { signal } = this.listeners;
341
+ video.addEventListener("ended", () => this.endedCb?.(), { signal });
342
+ video.addEventListener("waiting", () => this.bufferingCb?.(), { signal });
324
343
  if (video.canPlayType("application/vnd.apple.mpegurl")) {
325
344
  video.src = url;
326
345
  this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
@@ -336,7 +355,7 @@ var HlsViewTransport = class {
336
355
  if (!Hls.isSupported()) {
337
356
  throw mebiusError("CONNECTION_FAILED", "Scale playback is not supported in this browser.");
338
357
  }
339
- const hls = new Hls();
358
+ const hls = new Hls({ maxLiveSyncPlaybackRate: 1.5 });
340
359
  this.hls = hls;
341
360
  hls.on(Hls.Events.ERROR, (_evt, data) => {
342
361
  if (data.fatal) this.bufferingCb?.();
@@ -346,6 +365,8 @@ var HlsViewTransport = class {
346
365
  this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
347
366
  }
348
367
  async stop() {
368
+ this.listeners?.abort();
369
+ this.listeners = null;
349
370
  this.hls?.destroy();
350
371
  this.hls = null;
351
372
  if (this.video) {
@@ -365,6 +386,40 @@ var HlsViewTransport = class {
365
386
  };
366
387
 
367
388
  // src/internal/balanced-view-transport.ts
389
+ var LIVE_FLV_CONFIG = {
390
+ enableStashBuffer: false,
391
+ stashInitialSize: 128,
392
+ lazyLoad: false,
393
+ autoCleanupSourceBuffer: true,
394
+ autoCleanupMaxBackwardDuration: 30,
395
+ autoCleanupMinBackwardDuration: 10,
396
+ reuseRedirectedURL: true
397
+ };
398
+ var MAX_DRIFT_S = 2;
399
+ var EDGE_MARGIN_S = 0.4;
400
+ var AUDIO_RETRY_MS = 2500;
401
+ function stalledAtZero(video, ms) {
402
+ if (video.currentTime > 0) return Promise.resolve(false);
403
+ return new Promise((resolve) => {
404
+ const done = (stalled) => {
405
+ clearTimeout(timer);
406
+ video.removeEventListener("timeupdate", onTime);
407
+ resolve(stalled);
408
+ };
409
+ const onTime = () => {
410
+ if (video.currentTime > 0) done(false);
411
+ };
412
+ const timer = setTimeout(() => done(true), ms);
413
+ video.addEventListener("timeupdate", onTime);
414
+ });
415
+ }
416
+ function chaseLiveEdge(video) {
417
+ const ranges = video.buffered;
418
+ if (ranges.length === 0) return;
419
+ const edge = ranges.end(ranges.length - 1);
420
+ if (edge - video.currentTime <= MAX_DRIFT_S) return;
421
+ video.currentTime = edge - EDGE_MARGIN_S;
422
+ }
368
423
  var FlvViewTransport = class {
369
424
  constructor(signaling, deliveryPath) {
370
425
  this.signaling = signaling;
@@ -373,6 +428,8 @@ var FlvViewTransport = class {
373
428
  this.video = null;
374
429
  this.endedCb = null;
375
430
  this.bufferingCb = null;
431
+ /** Aborts this attempt's element listeners; see start(). */
432
+ this.listeners = null;
376
433
  /** True when playback only started because the element had to be muted. */
377
434
  this.mutedByPolicy = false;
378
435
  }
@@ -385,8 +442,11 @@ var FlvViewTransport = class {
385
442
  async start(_streamId, video) {
386
443
  this.video = video;
387
444
  const url = this.signaling.deliveryUrl(this.deliveryPath);
388
- video.addEventListener("ended", () => this.endedCb?.());
389
- video.addEventListener("waiting", () => this.bufferingCb?.());
445
+ this.listeners = new AbortController();
446
+ const { signal } = this.listeners;
447
+ video.addEventListener("ended", () => this.endedCb?.(), { signal });
448
+ video.addEventListener("waiting", () => this.bufferingCb?.(), { signal });
449
+ video.addEventListener("timeupdate", () => chaseLiveEdge(video), { signal });
390
450
  let mod;
391
451
  try {
392
452
  mod = await import("flv.js");
@@ -397,20 +457,38 @@ var FlvViewTransport = class {
397
457
  if (!flvjs.isSupported()) {
398
458
  throw mebiusError("CONNECTION_FAILED", "Balanced playback is not supported in this browser.");
399
459
  }
400
- const player = flvjs.createPlayer({ type: "flv", url, isLive: true });
460
+ this.attachPlayer(flvjs, video, url, true);
461
+ const firstPlay = playWithAutoplayFallback(video);
462
+ firstPlay.catch(() => void 0);
463
+ if (await stalledAtZero(video, AUDIO_RETRY_MS)) {
464
+ this.teardownPlayer();
465
+ this.attachPlayer(flvjs, video, url, false);
466
+ this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
467
+ return;
468
+ }
469
+ this.mutedByPolicy = (await firstPlay).mutedByPolicy;
470
+ }
471
+ attachPlayer(flvjs, video, url, withAudio) {
472
+ const player = flvjs.createPlayer(
473
+ { type: "flv", url, isLive: true, ...withAudio ? {} : { hasAudio: false } },
474
+ LIVE_FLV_CONFIG
475
+ );
401
476
  this.player = player;
402
477
  player.on(flvjs.Events.ERROR ?? "error", () => this.bufferingCb?.());
403
478
  player.attachMediaElement(video);
404
479
  player.load();
405
- this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
480
+ }
481
+ teardownPlayer() {
482
+ if (!this.player) return;
483
+ this.player.unload();
484
+ this.player.detachMediaElement();
485
+ this.player.destroy();
486
+ this.player = null;
406
487
  }
407
488
  async stop() {
408
- if (this.player) {
409
- this.player.unload();
410
- this.player.detachMediaElement();
411
- this.player.destroy();
412
- this.player = null;
413
- }
489
+ this.listeners?.abort();
490
+ this.listeners = null;
491
+ this.teardownPlayer();
414
492
  if (this.video) {
415
493
  this.video.removeAttribute("src");
416
494
  this.video.load();
@@ -459,7 +537,7 @@ function createViewCandidates(mode, signaling, deliveries = []) {
459
537
  }
460
538
 
461
539
  // src/internal/telemetry.ts
462
- var SDK_VERSION = "web/0.4.1";
540
+ var SDK_VERSION = "web/0.4.2";
463
541
  var FLUSH_INTERVAL_MS = 15e3;
464
542
  var MAX_BATCH = 64;
465
543
  function describeDevice() {
@@ -519,7 +597,7 @@ var QoeReporter = class {
519
597
  if (beacon && typeof navigator !== "undefined" && navigator.sendBeacon) {
520
598
  const url = `${this.target.url}${this.target.url.includes("?") ? "&" : "?"}token=${encodeURIComponent(this.target.token)}`;
521
599
  try {
522
- navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
600
+ navigator.sendBeacon(url, new Blob([body], { type: "text/plain" }));
523
601
  } catch {
524
602
  }
525
603
  return;