@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.
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);
@@ -147,6 +162,28 @@ var WhipPublishTransport = class {
147
162
  }
148
163
  };
149
164
 
165
+ // src/internal/autoplay.ts
166
+ function isAutoplayBlocked(e) {
167
+ return typeof e === "object" && e !== null && e.name === "NotAllowedError";
168
+ }
169
+ async function playWithAutoplayFallback(video) {
170
+ video.playsInline = true;
171
+ try {
172
+ await video.play();
173
+ return { mutedByPolicy: false };
174
+ } catch (e) {
175
+ if (!isAutoplayBlocked(e) || video.muted) throw e;
176
+ video.muted = true;
177
+ await video.play();
178
+ return { mutedByPolicy: true };
179
+ }
180
+ }
181
+ function resetVideoElement(video) {
182
+ video.srcObject = null;
183
+ video.removeAttribute("src");
184
+ video.load();
185
+ }
186
+
150
187
  // src/internal/ll-view-transport.ts
151
188
  var WhepViewTransport = class {
152
189
  constructor(signaling) {
@@ -155,6 +192,10 @@ var WhepViewTransport = class {
155
192
  this.resourceUrl = null;
156
193
  this.endedCb = null;
157
194
  this.bufferingCb = null;
195
+ /** Element this route attached a MediaStream to, so stop() can release it. */
196
+ this.video = null;
197
+ /** True when playback only started because the element had to be muted. */
198
+ this.mutedByPolicy = false;
158
199
  }
159
200
  onEnded(cb) {
160
201
  this.endedCb = cb;
@@ -163,6 +204,7 @@ var WhepViewTransport = class {
163
204
  this.bufferingCb = cb;
164
205
  }
165
206
  async start(streamId, video) {
207
+ this.video = video;
166
208
  const pc = new RTCPeerConnection(DEFAULT_RTC_CONFIG);
167
209
  this.pc = pc;
168
210
  const remote = new MediaStream();
@@ -171,7 +213,9 @@ var WhepViewTransport = class {
171
213
  pc.ontrack = (ev) => {
172
214
  remote.addTrack(ev.track);
173
215
  video.srcObject = remote;
174
- void video.play().catch(() => {
216
+ void playWithAutoplayFallback(video).then((o) => {
217
+ this.mutedByPolicy = o.mutedByPolicy;
218
+ }).catch(() => {
175
219
  });
176
220
  };
177
221
  pc.onconnectionstatechange = () => {
@@ -198,6 +242,8 @@ var WhepViewTransport = class {
198
242
  this.resourceUrl = null;
199
243
  this.pc?.close();
200
244
  this.pc = null;
245
+ if (this.video) resetVideoElement(this.video);
246
+ this.video = null;
201
247
  }
202
248
  async getStats() {
203
249
  if (!this.pc) return null;
@@ -235,6 +281,10 @@ var HlsViewTransport = class {
235
281
  this.video = null;
236
282
  this.endedCb = null;
237
283
  this.bufferingCb = null;
284
+ /** Aborts this attempt's element listeners; see start(). */
285
+ this.listeners = null;
286
+ /** True when playback only started because the element had to be muted. */
287
+ this.mutedByPolicy = false;
238
288
  }
239
289
  onEnded(cb) {
240
290
  this.endedCb = cb;
@@ -245,11 +295,13 @@ var HlsViewTransport = class {
245
295
  async start(streamId, video) {
246
296
  this.video = video;
247
297
  const url = this.deliveryPath ? this.signaling.deliveryUrl(this.deliveryPath) : this.signaling.scalePlaylistUrl(streamId);
248
- video.addEventListener("ended", () => this.endedCb?.());
249
- 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 });
250
302
  if (video.canPlayType("application/vnd.apple.mpegurl")) {
251
303
  video.src = url;
252
- await video.play().catch(() => void 0);
304
+ this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
253
305
  return;
254
306
  }
255
307
  let mod;
@@ -262,16 +314,18 @@ var HlsViewTransport = class {
262
314
  if (!Hls.isSupported()) {
263
315
  throw mebiusError("CONNECTION_FAILED", "Scale playback is not supported in this browser.");
264
316
  }
265
- const hls = new Hls({ lowLatencyMode: true });
317
+ const hls = new Hls({ maxLiveSyncPlaybackRate: 1.5 });
266
318
  this.hls = hls;
267
319
  hls.on(Hls.Events.ERROR, (_evt, data) => {
268
320
  if (data.fatal) this.bufferingCb?.();
269
321
  });
270
322
  hls.loadSource(url);
271
323
  hls.attachMedia(video);
272
- await video.play().catch(() => void 0);
324
+ this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
273
325
  }
274
326
  async stop() {
327
+ this.listeners?.abort();
328
+ this.listeners = null;
275
329
  this.hls?.destroy();
276
330
  this.hls = null;
277
331
  if (this.video) {
@@ -291,6 +345,40 @@ var HlsViewTransport = class {
291
345
  };
292
346
 
293
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
+ }
294
382
  var FlvViewTransport = class {
295
383
  constructor(signaling, deliveryPath) {
296
384
  this.signaling = signaling;
@@ -299,6 +387,10 @@ var FlvViewTransport = class {
299
387
  this.video = null;
300
388
  this.endedCb = null;
301
389
  this.bufferingCb = null;
390
+ /** Aborts this attempt's element listeners; see start(). */
391
+ this.listeners = null;
392
+ /** True when playback only started because the element had to be muted. */
393
+ this.mutedByPolicy = false;
302
394
  }
303
395
  onEnded(cb) {
304
396
  this.endedCb = cb;
@@ -309,8 +401,11 @@ var FlvViewTransport = class {
309
401
  async start(_streamId, video) {
310
402
  this.video = video;
311
403
  const url = this.signaling.deliveryUrl(this.deliveryPath);
312
- video.addEventListener("ended", () => this.endedCb?.());
313
- 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 });
314
409
  let mod;
315
410
  try {
316
411
  mod = await import("flv.js");
@@ -321,20 +416,38 @@ var FlvViewTransport = class {
321
416
  if (!flvjs.isSupported()) {
322
417
  throw mebiusError("CONNECTION_FAILED", "Balanced playback is not supported in this browser.");
323
418
  }
324
- 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
+ );
325
435
  this.player = player;
326
436
  player.on(flvjs.Events.ERROR ?? "error", () => this.bufferingCb?.());
327
437
  player.attachMediaElement(video);
328
438
  player.load();
329
- await Promise.resolve(player.play()).catch(() => void 0);
439
+ }
440
+ teardownPlayer() {
441
+ if (!this.player) return;
442
+ this.player.unload();
443
+ this.player.detachMediaElement();
444
+ this.player.destroy();
445
+ this.player = null;
330
446
  }
331
447
  async stop() {
332
- if (this.player) {
333
- this.player.unload();
334
- this.player.detachMediaElement();
335
- this.player.destroy();
336
- this.player = null;
337
- }
448
+ this.listeners?.abort();
449
+ this.listeners = null;
450
+ this.teardownPlayer();
338
451
  if (this.video) {
339
452
  this.video.removeAttribute("src");
340
453
  this.video.load();
@@ -383,7 +496,7 @@ function createViewCandidates(mode, signaling, deliveries = []) {
383
496
  }
384
497
 
385
498
  // src/internal/telemetry.ts
386
- var SDK_VERSION = "web/0.4.0";
499
+ var SDK_VERSION = "web/0.4.2";
387
500
  var FLUSH_INTERVAL_MS = 15e3;
388
501
  var MAX_BATCH = 64;
389
502
  function describeDevice() {
@@ -443,7 +556,7 @@ var QoeReporter = class {
443
556
  if (beacon && typeof navigator !== "undefined" && navigator.sendBeacon) {
444
557
  const url = `${this.target.url}${this.target.url.includes("?") ? "&" : "?"}token=${encodeURIComponent(this.target.token)}`;
445
558
  try {
446
- navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
559
+ navigator.sendBeacon(url, new Blob([body], { type: "text/plain" }));
447
560
  } catch {
448
561
  }
449
562
  return;
@@ -599,6 +712,7 @@ var MebiusPlayer = class extends TypedEmitter {
599
712
  let lastError = null;
600
713
  for (const candidate of this.candidates) {
601
714
  try {
715
+ resetVideoElement(video);
602
716
  this.attach(candidate);
603
717
  await candidate.start(streamId, video);
604
718
  if (await hasFirstFrame(video)) {