@mebius-io/web 0.4.0 → 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.
@@ -42705,6 +42705,20 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42705
42705
  };
42706
42706
 
42707
42707
  // src/internal/publish-transport.ts
42708
+ function preferH264(pc) {
42709
+ const caps = RTCRtpSender.getCapabilities?.("video");
42710
+ if (!caps?.codecs) return;
42711
+ const h264 = caps.codecs.filter((c) => c.mimeType.toLowerCase() === "video/h264");
42712
+ if (h264.length === 0) return;
42713
+ const rest = caps.codecs.filter((c) => c.mimeType.toLowerCase() !== "video/h264");
42714
+ for (const tr of pc.getTransceivers()) {
42715
+ if (tr.sender.track?.kind !== "video") continue;
42716
+ try {
42717
+ tr.setCodecPreferences?.([...h264, ...rest]);
42718
+ } catch {
42719
+ }
42720
+ }
42721
+ }
42708
42722
  var WhipPublishTransport = class {
42709
42723
  constructor(signaling) {
42710
42724
  this.signaling = signaling;
@@ -42717,6 +42731,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42717
42731
  for (const track of stream.getTracks()) {
42718
42732
  pc.addTrack(track, stream);
42719
42733
  }
42734
+ preferH264(pc);
42720
42735
  const offer = await pc.createOffer();
42721
42736
  await pc.setLocalDescription(offer);
42722
42737
  await waitForIceGathering(pc);
@@ -42764,6 +42779,28 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42764
42779
  }
42765
42780
  };
42766
42781
 
42782
+ // src/internal/autoplay.ts
42783
+ function isAutoplayBlocked(e) {
42784
+ return typeof e === "object" && e !== null && e.name === "NotAllowedError";
42785
+ }
42786
+ async function playWithAutoplayFallback(video) {
42787
+ video.playsInline = true;
42788
+ try {
42789
+ await video.play();
42790
+ return { mutedByPolicy: false };
42791
+ } catch (e) {
42792
+ if (!isAutoplayBlocked(e) || video.muted) throw e;
42793
+ video.muted = true;
42794
+ await video.play();
42795
+ return { mutedByPolicy: true };
42796
+ }
42797
+ }
42798
+ function resetVideoElement(video) {
42799
+ video.srcObject = null;
42800
+ video.removeAttribute("src");
42801
+ video.load();
42802
+ }
42803
+
42767
42804
  // src/internal/ll-view-transport.ts
42768
42805
  var WhepViewTransport = class {
42769
42806
  constructor(signaling) {
@@ -42772,6 +42809,10 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42772
42809
  this.resourceUrl = null;
42773
42810
  this.endedCb = null;
42774
42811
  this.bufferingCb = null;
42812
+ /** Element this route attached a MediaStream to, so stop() can release it. */
42813
+ this.video = null;
42814
+ /** True when playback only started because the element had to be muted. */
42815
+ this.mutedByPolicy = false;
42775
42816
  }
42776
42817
  onEnded(cb) {
42777
42818
  this.endedCb = cb;
@@ -42780,6 +42821,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42780
42821
  this.bufferingCb = cb;
42781
42822
  }
42782
42823
  async start(streamId, video) {
42824
+ this.video = video;
42783
42825
  const pc = new RTCPeerConnection(DEFAULT_RTC_CONFIG);
42784
42826
  this.pc = pc;
42785
42827
  const remote = new MediaStream();
@@ -42788,7 +42830,9 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42788
42830
  pc.ontrack = (ev) => {
42789
42831
  remote.addTrack(ev.track);
42790
42832
  video.srcObject = remote;
42791
- void video.play().catch(() => {
42833
+ void playWithAutoplayFallback(video).then((o) => {
42834
+ this.mutedByPolicy = o.mutedByPolicy;
42835
+ }).catch(() => {
42792
42836
  });
42793
42837
  };
42794
42838
  pc.onconnectionstatechange = () => {
@@ -42815,6 +42859,8 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42815
42859
  this.resourceUrl = null;
42816
42860
  this.pc?.close();
42817
42861
  this.pc = null;
42862
+ if (this.video) resetVideoElement(this.video);
42863
+ this.video = null;
42818
42864
  }
42819
42865
  async getStats() {
42820
42866
  if (!this.pc) return null;
@@ -42852,6 +42898,10 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42852
42898
  this.video = null;
42853
42899
  this.endedCb = null;
42854
42900
  this.bufferingCb = null;
42901
+ /** Aborts this attempt's element listeners; see start(). */
42902
+ this.listeners = null;
42903
+ /** True when playback only started because the element had to be muted. */
42904
+ this.mutedByPolicy = false;
42855
42905
  }
42856
42906
  onEnded(cb) {
42857
42907
  this.endedCb = cb;
@@ -42862,11 +42912,13 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42862
42912
  async start(streamId, video) {
42863
42913
  this.video = video;
42864
42914
  const url = this.deliveryPath ? this.signaling.deliveryUrl(this.deliveryPath) : this.signaling.scalePlaylistUrl(streamId);
42865
- video.addEventListener("ended", () => this.endedCb?.());
42866
- video.addEventListener("waiting", () => this.bufferingCb?.());
42915
+ this.listeners = new AbortController();
42916
+ const { signal } = this.listeners;
42917
+ video.addEventListener("ended", () => this.endedCb?.(), { signal });
42918
+ video.addEventListener("waiting", () => this.bufferingCb?.(), { signal });
42867
42919
  if (video.canPlayType("application/vnd.apple.mpegurl")) {
42868
42920
  video.src = url;
42869
- await video.play().catch(() => void 0);
42921
+ this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
42870
42922
  return;
42871
42923
  }
42872
42924
  let mod;
@@ -42879,16 +42931,18 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42879
42931
  if (!Hls2.isSupported()) {
42880
42932
  throw mebiusError("CONNECTION_FAILED", "Scale playback is not supported in this browser.");
42881
42933
  }
42882
- const hls = new Hls2({ lowLatencyMode: true });
42934
+ const hls = new Hls2({ maxLiveSyncPlaybackRate: 1.5 });
42883
42935
  this.hls = hls;
42884
42936
  hls.on(Hls2.Events.ERROR, (_evt, data) => {
42885
42937
  if (data.fatal) this.bufferingCb?.();
42886
42938
  });
42887
42939
  hls.loadSource(url);
42888
42940
  hls.attachMedia(video);
42889
- await video.play().catch(() => void 0);
42941
+ this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
42890
42942
  }
42891
42943
  async stop() {
42944
+ this.listeners?.abort();
42945
+ this.listeners = null;
42892
42946
  this.hls?.destroy();
42893
42947
  this.hls = null;
42894
42948
  if (this.video) {
@@ -42908,6 +42962,40 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42908
42962
  };
42909
42963
 
42910
42964
  // src/internal/balanced-view-transport.ts
42965
+ var LIVE_FLV_CONFIG = {
42966
+ enableStashBuffer: false,
42967
+ stashInitialSize: 128,
42968
+ lazyLoad: false,
42969
+ autoCleanupSourceBuffer: true,
42970
+ autoCleanupMaxBackwardDuration: 30,
42971
+ autoCleanupMinBackwardDuration: 10,
42972
+ reuseRedirectedURL: true
42973
+ };
42974
+ var MAX_DRIFT_S = 2;
42975
+ var EDGE_MARGIN_S = 0.4;
42976
+ var AUDIO_RETRY_MS = 2500;
42977
+ function stalledAtZero(video, ms) {
42978
+ if (video.currentTime > 0) return Promise.resolve(false);
42979
+ return new Promise((resolve) => {
42980
+ const done = (stalled) => {
42981
+ clearTimeout(timer);
42982
+ video.removeEventListener("timeupdate", onTime);
42983
+ resolve(stalled);
42984
+ };
42985
+ const onTime = () => {
42986
+ if (video.currentTime > 0) done(false);
42987
+ };
42988
+ const timer = setTimeout(() => done(true), ms);
42989
+ video.addEventListener("timeupdate", onTime);
42990
+ });
42991
+ }
42992
+ function chaseLiveEdge(video) {
42993
+ const ranges = video.buffered;
42994
+ if (ranges.length === 0) return;
42995
+ const edge = ranges.end(ranges.length - 1);
42996
+ if (edge - video.currentTime <= MAX_DRIFT_S) return;
42997
+ video.currentTime = edge - EDGE_MARGIN_S;
42998
+ }
42911
42999
  var FlvViewTransport = class {
42912
43000
  constructor(signaling, deliveryPath) {
42913
43001
  this.signaling = signaling;
@@ -42916,6 +43004,10 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42916
43004
  this.video = null;
42917
43005
  this.endedCb = null;
42918
43006
  this.bufferingCb = null;
43007
+ /** Aborts this attempt's element listeners; see start(). */
43008
+ this.listeners = null;
43009
+ /** True when playback only started because the element had to be muted. */
43010
+ this.mutedByPolicy = false;
42919
43011
  }
42920
43012
  onEnded(cb) {
42921
43013
  this.endedCb = cb;
@@ -42926,8 +43018,11 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42926
43018
  async start(_streamId, video) {
42927
43019
  this.video = video;
42928
43020
  const url = this.signaling.deliveryUrl(this.deliveryPath);
42929
- video.addEventListener("ended", () => this.endedCb?.());
42930
- video.addEventListener("waiting", () => this.bufferingCb?.());
43021
+ this.listeners = new AbortController();
43022
+ const { signal } = this.listeners;
43023
+ video.addEventListener("ended", () => this.endedCb?.(), { signal });
43024
+ video.addEventListener("waiting", () => this.bufferingCb?.(), { signal });
43025
+ video.addEventListener("timeupdate", () => chaseLiveEdge(video), { signal });
42931
43026
  let mod;
42932
43027
  try {
42933
43028
  mod = await Promise.resolve().then(() => __toESM(require_flv(), 1));
@@ -42938,20 +43033,38 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
42938
43033
  if (!flvjs.isSupported()) {
42939
43034
  throw mebiusError("CONNECTION_FAILED", "Balanced playback is not supported in this browser.");
42940
43035
  }
42941
- const player = flvjs.createPlayer({ type: "flv", url, isLive: true });
43036
+ this.attachPlayer(flvjs, video, url, true);
43037
+ const firstPlay = playWithAutoplayFallback(video);
43038
+ firstPlay.catch(() => void 0);
43039
+ if (await stalledAtZero(video, AUDIO_RETRY_MS)) {
43040
+ this.teardownPlayer();
43041
+ this.attachPlayer(flvjs, video, url, false);
43042
+ this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
43043
+ return;
43044
+ }
43045
+ this.mutedByPolicy = (await firstPlay).mutedByPolicy;
43046
+ }
43047
+ attachPlayer(flvjs, video, url, withAudio) {
43048
+ const player = flvjs.createPlayer(
43049
+ { type: "flv", url, isLive: true, ...withAudio ? {} : { hasAudio: false } },
43050
+ LIVE_FLV_CONFIG
43051
+ );
42942
43052
  this.player = player;
42943
43053
  player.on(flvjs.Events.ERROR ?? "error", () => this.bufferingCb?.());
42944
43054
  player.attachMediaElement(video);
42945
43055
  player.load();
42946
- await Promise.resolve(player.play()).catch(() => void 0);
43056
+ }
43057
+ teardownPlayer() {
43058
+ if (!this.player) return;
43059
+ this.player.unload();
43060
+ this.player.detachMediaElement();
43061
+ this.player.destroy();
43062
+ this.player = null;
42947
43063
  }
42948
43064
  async stop() {
42949
- if (this.player) {
42950
- this.player.unload();
42951
- this.player.detachMediaElement();
42952
- this.player.destroy();
42953
- this.player = null;
42954
- }
43065
+ this.listeners?.abort();
43066
+ this.listeners = null;
43067
+ this.teardownPlayer();
42955
43068
  if (this.video) {
42956
43069
  this.video.removeAttribute("src");
42957
43070
  this.video.load();
@@ -43000,7 +43113,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43000
43113
  }
43001
43114
 
43002
43115
  // src/internal/telemetry.ts
43003
- var SDK_VERSION = "web/0.4.0";
43116
+ var SDK_VERSION = "web/0.4.2";
43004
43117
  var FLUSH_INTERVAL_MS = 15e3;
43005
43118
  var MAX_BATCH = 64;
43006
43119
  function describeDevice() {
@@ -43060,7 +43173,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43060
43173
  if (beacon && typeof navigator !== "undefined" && navigator.sendBeacon) {
43061
43174
  const url = `${this.target.url}${this.target.url.includes("?") ? "&" : "?"}token=${encodeURIComponent(this.target.token)}`;
43062
43175
  try {
43063
- navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
43176
+ navigator.sendBeacon(url, new Blob([body], { type: "text/plain" }));
43064
43177
  } catch {
43065
43178
  }
43066
43179
  return;
@@ -43216,6 +43329,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43216
43329
  let lastError = null;
43217
43330
  for (const candidate of this.candidates) {
43218
43331
  try {
43332
+ resetVideoElement(video);
43219
43333
  this.attach(candidate);
43220
43334
  await candidate.start(streamId, video);
43221
43335
  if (await hasFirstFrame(video)) {