@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/LICENSE +21 -0
- package/README.md +2 -2
- package/dist/index.cjs +132 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.global.js +132 -18
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +132 -18
- package/dist/index.js.map +1 -1
- package/package.json +11 -12
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
|
|
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
|
-
|
|
249
|
-
|
|
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
|
|
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({
|
|
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
|
|
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
|
-
|
|
313
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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.
|
|
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: "
|
|
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)) {
|