@daydreamlive/browser 0.1.0 → 0.2.0
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.cjs +1200 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +141 -2
- package/dist/index.d.ts +141 -2
- package/dist/index.js +1199 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,7 @@ __export(index_exports, {
|
|
|
31
31
|
StreamNotFoundError: () => StreamNotFoundError,
|
|
32
32
|
UnauthorizedError: () => UnauthorizedError,
|
|
33
33
|
createBroadcast: () => createBroadcast2,
|
|
34
|
+
createCompositor: () => createCompositor,
|
|
34
35
|
createPlayer: () => createPlayer2,
|
|
35
36
|
livepeerResponseHandler: () => livepeerResponseHandler
|
|
36
37
|
});
|
|
@@ -42,7 +43,7 @@ var DEFAULT_ICE_SERVERS = [
|
|
|
42
43
|
{ urls: "stun:stun1.l.google.com:19302" },
|
|
43
44
|
{ urls: "stun:stun.cloudflare.com:3478" }
|
|
44
45
|
];
|
|
45
|
-
var DEFAULT_VIDEO_BITRATE =
|
|
46
|
+
var DEFAULT_VIDEO_BITRATE = 3e5;
|
|
46
47
|
var DEFAULT_AUDIO_BITRATE = 64e3;
|
|
47
48
|
|
|
48
49
|
// src/errors.ts
|
|
@@ -142,6 +143,7 @@ function preferH264(sdp) {
|
|
|
142
143
|
return lines.join("\r\n");
|
|
143
144
|
}
|
|
144
145
|
var sharedRedirectCache = new LRURedirectCache();
|
|
146
|
+
var DEFAULT_CONNECTION_TIMEOUT = 1e4;
|
|
145
147
|
var WHIPClient = class {
|
|
146
148
|
constructor(config) {
|
|
147
149
|
this.pc = null;
|
|
@@ -157,6 +159,7 @@ var WHIPClient = class {
|
|
|
157
159
|
this.iceServers = config.iceServers ?? DEFAULT_ICE_SERVERS;
|
|
158
160
|
this.videoBitrate = config.videoBitrate ?? DEFAULT_VIDEO_BITRATE;
|
|
159
161
|
this.audioBitrate = config.audioBitrate ?? DEFAULT_AUDIO_BITRATE;
|
|
162
|
+
this.connectionTimeout = config.connectionTimeout ?? DEFAULT_CONNECTION_TIMEOUT;
|
|
160
163
|
this.maxFramerate = config.maxFramerate;
|
|
161
164
|
this.onStats = config.onStats;
|
|
162
165
|
this.statsIntervalMs = config.statsIntervalMs ?? 5e3;
|
|
@@ -206,7 +209,7 @@ var WHIPClient = class {
|
|
|
206
209
|
this.abortController = new AbortController();
|
|
207
210
|
const timeoutId = this.timers.setTimeout(
|
|
208
211
|
() => this.abortController?.abort(),
|
|
209
|
-
|
|
212
|
+
this.connectionTimeout
|
|
210
213
|
);
|
|
211
214
|
try {
|
|
212
215
|
const fetchUrl = this.getUrlWithCachedRedirect();
|
|
@@ -530,6 +533,16 @@ var Broadcast = class extends TypedEventEmitter {
|
|
|
530
533
|
get stream() {
|
|
531
534
|
return this.currentStream;
|
|
532
535
|
}
|
|
536
|
+
get reconnectInfo() {
|
|
537
|
+
if (this.state !== "reconnecting") return null;
|
|
538
|
+
const baseDelay = this.reconnectConfig.baseDelayMs ?? 1e3;
|
|
539
|
+
const delay = baseDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
540
|
+
return {
|
|
541
|
+
attempt: this.reconnectAttempts,
|
|
542
|
+
maxAttempts: this.reconnectConfig.maxAttempts ?? 5,
|
|
543
|
+
delayMs: delay
|
|
544
|
+
};
|
|
545
|
+
}
|
|
533
546
|
async connect() {
|
|
534
547
|
try {
|
|
535
548
|
const result = await this.whipClient.connect(this.currentStream);
|
|
@@ -551,6 +564,9 @@ var Broadcast = class extends TypedEventEmitter {
|
|
|
551
564
|
await this.whipClient.disconnect();
|
|
552
565
|
this.clearListeners();
|
|
553
566
|
}
|
|
567
|
+
setMaxFramerate(fps) {
|
|
568
|
+
this.whipClient.setMaxFramerate(fps);
|
|
569
|
+
}
|
|
554
570
|
async replaceStream(newStream) {
|
|
555
571
|
if (!this.whipClient.isConnected()) {
|
|
556
572
|
this.currentStream = newStream;
|
|
@@ -635,6 +651,11 @@ var Broadcast = class extends TypedEventEmitter {
|
|
|
635
651
|
const baseDelay = this.reconnectConfig.baseDelayMs ?? 1e3;
|
|
636
652
|
const delay = baseDelay * Math.pow(2, this.reconnectAttempts);
|
|
637
653
|
this.reconnectAttempts++;
|
|
654
|
+
this.emit("reconnect", {
|
|
655
|
+
attempt: this.reconnectAttempts,
|
|
656
|
+
maxAttempts: this.reconnectConfig.maxAttempts ?? 5,
|
|
657
|
+
delayMs: delay
|
|
658
|
+
});
|
|
638
659
|
this.reconnectTimeout = setTimeout(async () => {
|
|
639
660
|
if (this.state === "ended") return;
|
|
640
661
|
try {
|
|
@@ -658,6 +679,9 @@ function createBroadcast(options) {
|
|
|
658
679
|
stream,
|
|
659
680
|
reconnect,
|
|
660
681
|
video,
|
|
682
|
+
audio,
|
|
683
|
+
iceServers,
|
|
684
|
+
connectionTimeout,
|
|
661
685
|
onStats,
|
|
662
686
|
statsIntervalMs,
|
|
663
687
|
onResponse
|
|
@@ -667,8 +691,11 @@ function createBroadcast(options) {
|
|
|
667
691
|
stream,
|
|
668
692
|
reconnect,
|
|
669
693
|
whipConfig: {
|
|
694
|
+
iceServers,
|
|
670
695
|
videoBitrate: video?.bitrate,
|
|
696
|
+
audioBitrate: audio?.bitrate,
|
|
671
697
|
maxFramerate: video?.maxFramerate,
|
|
698
|
+
connectionTimeout,
|
|
672
699
|
onStats,
|
|
673
700
|
statsIntervalMs,
|
|
674
701
|
onResponse
|
|
@@ -677,6 +704,7 @@ function createBroadcast(options) {
|
|
|
677
704
|
}
|
|
678
705
|
|
|
679
706
|
// src/internal/WHEPClient.ts
|
|
707
|
+
var DEFAULT_CONNECTION_TIMEOUT2 = 1e4;
|
|
680
708
|
var WHEPClient = class {
|
|
681
709
|
constructor(config) {
|
|
682
710
|
this.pc = null;
|
|
@@ -687,6 +715,7 @@ var WHEPClient = class {
|
|
|
687
715
|
this.iceGatheringTimer = null;
|
|
688
716
|
this.url = config.url;
|
|
689
717
|
this.iceServers = config.iceServers ?? DEFAULT_ICE_SERVERS;
|
|
718
|
+
this.connectionTimeout = config.connectionTimeout ?? DEFAULT_CONNECTION_TIMEOUT2;
|
|
690
719
|
this.onStats = config.onStats;
|
|
691
720
|
this.statsIntervalMs = config.statsIntervalMs ?? 5e3;
|
|
692
721
|
this.pcFactory = config.peerConnectionFactory ?? defaultPeerConnectionFactory;
|
|
@@ -716,7 +745,7 @@ var WHEPClient = class {
|
|
|
716
745
|
this.abortController = new AbortController();
|
|
717
746
|
const timeoutId = this.timers.setTimeout(
|
|
718
747
|
() => this.abortController?.abort(),
|
|
719
|
-
|
|
748
|
+
this.connectionTimeout
|
|
720
749
|
);
|
|
721
750
|
try {
|
|
722
751
|
const response = await this.fetch(this.url, {
|
|
@@ -892,6 +921,16 @@ var Player = class extends TypedEventEmitter {
|
|
|
892
921
|
get stream() {
|
|
893
922
|
return this._stream;
|
|
894
923
|
}
|
|
924
|
+
get reconnectInfo() {
|
|
925
|
+
if (this.state !== "buffering") return null;
|
|
926
|
+
const baseDelay = this.reconnectConfig.baseDelayMs ?? 200;
|
|
927
|
+
const delay = this.calculateReconnectDelay(this.reconnectAttempts - 1, baseDelay);
|
|
928
|
+
return {
|
|
929
|
+
attempt: this.reconnectAttempts,
|
|
930
|
+
maxAttempts: this.reconnectConfig.maxAttempts ?? 30,
|
|
931
|
+
delayMs: delay
|
|
932
|
+
};
|
|
933
|
+
}
|
|
895
934
|
async connect() {
|
|
896
935
|
try {
|
|
897
936
|
this._stream = await this.whepClient.connect();
|
|
@@ -988,6 +1027,11 @@ var Player = class extends TypedEventEmitter {
|
|
|
988
1027
|
baseDelay
|
|
989
1028
|
);
|
|
990
1029
|
this.reconnectAttempts++;
|
|
1030
|
+
this.emit("reconnect", {
|
|
1031
|
+
attempt: this.reconnectAttempts,
|
|
1032
|
+
maxAttempts: this.reconnectConfig.maxAttempts ?? 30,
|
|
1033
|
+
delayMs: delay
|
|
1034
|
+
});
|
|
991
1035
|
this.reconnectTimeout = setTimeout(async () => {
|
|
992
1036
|
if (this.state === "ended") return;
|
|
993
1037
|
try {
|
|
@@ -1020,12 +1064,1164 @@ function createPlayer(whepUrl, options) {
|
|
|
1020
1064
|
whepUrl,
|
|
1021
1065
|
reconnect: options?.reconnect,
|
|
1022
1066
|
whepConfig: {
|
|
1067
|
+
iceServers: options?.iceServers,
|
|
1068
|
+
connectionTimeout: options?.connectionTimeout,
|
|
1023
1069
|
onStats: options?.onStats,
|
|
1024
1070
|
statsIntervalMs: options?.statsIntervalMs
|
|
1025
1071
|
}
|
|
1026
1072
|
});
|
|
1027
1073
|
}
|
|
1028
1074
|
|
|
1075
|
+
// src/internal/compositor/Registry.ts
|
|
1076
|
+
function createRegistry(events) {
|
|
1077
|
+
const sources = /* @__PURE__ */ new Map();
|
|
1078
|
+
return {
|
|
1079
|
+
register(id, source) {
|
|
1080
|
+
if (!id) throw new Error("Source id is required");
|
|
1081
|
+
if (!source) throw new Error("Source is required");
|
|
1082
|
+
sources.set(id, {
|
|
1083
|
+
id,
|
|
1084
|
+
source,
|
|
1085
|
+
registeredAt: Date.now()
|
|
1086
|
+
});
|
|
1087
|
+
events?.onRegister?.(id, source);
|
|
1088
|
+
},
|
|
1089
|
+
unregister(id) {
|
|
1090
|
+
const entry = sources.get(id);
|
|
1091
|
+
if (!entry) return void 0;
|
|
1092
|
+
sources.delete(id);
|
|
1093
|
+
events?.onUnregister?.(id);
|
|
1094
|
+
return entry.source;
|
|
1095
|
+
},
|
|
1096
|
+
get(id) {
|
|
1097
|
+
return sources.get(id)?.source;
|
|
1098
|
+
},
|
|
1099
|
+
has(id) {
|
|
1100
|
+
return sources.has(id);
|
|
1101
|
+
},
|
|
1102
|
+
list() {
|
|
1103
|
+
return Array.from(sources.values()).map((entry) => ({
|
|
1104
|
+
id: entry.id,
|
|
1105
|
+
source: entry.source
|
|
1106
|
+
}));
|
|
1107
|
+
},
|
|
1108
|
+
clear() {
|
|
1109
|
+
const ids = Array.from(sources.keys());
|
|
1110
|
+
sources.clear();
|
|
1111
|
+
ids.forEach((id) => events?.onUnregister?.(id));
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// src/internal/compositor/Renderer.ts
|
|
1117
|
+
function createRenderer(options) {
|
|
1118
|
+
let size = {
|
|
1119
|
+
width: options.width,
|
|
1120
|
+
height: options.height,
|
|
1121
|
+
dpr: Math.min(2, options.dpr)
|
|
1122
|
+
};
|
|
1123
|
+
let crossfadeMs = options.crossfadeMs;
|
|
1124
|
+
let keepalive = options.keepalive;
|
|
1125
|
+
let captureCanvas = null;
|
|
1126
|
+
let captureCtx = null;
|
|
1127
|
+
let offscreen = null;
|
|
1128
|
+
let offscreenCtx = null;
|
|
1129
|
+
let crossfadeCanvas = null;
|
|
1130
|
+
let crossfadeCtx = null;
|
|
1131
|
+
let currentSource = null;
|
|
1132
|
+
let pendingSource = null;
|
|
1133
|
+
let crossfadeStart = null;
|
|
1134
|
+
let cleanupFn = void 0;
|
|
1135
|
+
let frameIndex = 0;
|
|
1136
|
+
let rectCache = /* @__PURE__ */ new WeakMap();
|
|
1137
|
+
function initCanvas() {
|
|
1138
|
+
const canvas = document.createElement("canvas");
|
|
1139
|
+
canvas.style.display = "none";
|
|
1140
|
+
const pxW = Math.round(size.width * size.dpr);
|
|
1141
|
+
const pxH = Math.round(size.height * size.dpr);
|
|
1142
|
+
const outW = Math.round(size.width);
|
|
1143
|
+
const outH = Math.round(size.height);
|
|
1144
|
+
canvas.width = outW;
|
|
1145
|
+
canvas.height = outH;
|
|
1146
|
+
const ctx = canvas.getContext("2d", {
|
|
1147
|
+
alpha: false,
|
|
1148
|
+
desynchronized: true
|
|
1149
|
+
});
|
|
1150
|
+
if (!ctx) throw new Error("2D context not available");
|
|
1151
|
+
captureCanvas = canvas;
|
|
1152
|
+
captureCtx = ctx;
|
|
1153
|
+
try {
|
|
1154
|
+
const off = new OffscreenCanvas(pxW, pxH);
|
|
1155
|
+
offscreen = off;
|
|
1156
|
+
const offCtx = off.getContext("2d", { alpha: false });
|
|
1157
|
+
if (!offCtx) throw new Error("2D context not available for Offscreen");
|
|
1158
|
+
offCtx.imageSmoothingEnabled = true;
|
|
1159
|
+
offscreenCtx = offCtx;
|
|
1160
|
+
} catch {
|
|
1161
|
+
const off = document.createElement("canvas");
|
|
1162
|
+
off.width = pxW;
|
|
1163
|
+
off.height = pxH;
|
|
1164
|
+
const offCtx = off.getContext("2d", { alpha: false });
|
|
1165
|
+
if (!offCtx)
|
|
1166
|
+
throw new Error("2D context not available for Offscreen fallback");
|
|
1167
|
+
offCtx.imageSmoothingEnabled = true;
|
|
1168
|
+
offscreen = off;
|
|
1169
|
+
offscreenCtx = offCtx;
|
|
1170
|
+
}
|
|
1171
|
+
try {
|
|
1172
|
+
const cfCanvas = new OffscreenCanvas(pxW, pxH);
|
|
1173
|
+
crossfadeCanvas = cfCanvas;
|
|
1174
|
+
const cfCtx = cfCanvas.getContext("2d", { alpha: true });
|
|
1175
|
+
if (cfCtx) {
|
|
1176
|
+
cfCtx.imageSmoothingEnabled = true;
|
|
1177
|
+
crossfadeCtx = cfCtx;
|
|
1178
|
+
}
|
|
1179
|
+
} catch {
|
|
1180
|
+
const cfCanvas = document.createElement("canvas");
|
|
1181
|
+
cfCanvas.width = pxW;
|
|
1182
|
+
cfCanvas.height = pxH;
|
|
1183
|
+
const cfCtx = cfCanvas.getContext("2d", { alpha: true });
|
|
1184
|
+
if (cfCtx) {
|
|
1185
|
+
cfCtx.imageSmoothingEnabled = true;
|
|
1186
|
+
crossfadeCanvas = cfCanvas;
|
|
1187
|
+
crossfadeCtx = cfCtx;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
offscreenCtx.fillStyle = "#111";
|
|
1191
|
+
offscreenCtx.fillRect(0, 0, pxW, pxH);
|
|
1192
|
+
captureCtx.drawImage(
|
|
1193
|
+
offscreen,
|
|
1194
|
+
0,
|
|
1195
|
+
0,
|
|
1196
|
+
pxW,
|
|
1197
|
+
pxH,
|
|
1198
|
+
0,
|
|
1199
|
+
0,
|
|
1200
|
+
outW,
|
|
1201
|
+
outH
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
function isSourceReady(source) {
|
|
1205
|
+
if (source.kind === "video") {
|
|
1206
|
+
const v = source.element;
|
|
1207
|
+
return typeof v.readyState === "number" && v.readyState >= 2 && (v.videoWidth || 0) > 0 && (v.videoHeight || 0) > 0;
|
|
1208
|
+
}
|
|
1209
|
+
if (source.kind === "canvas") {
|
|
1210
|
+
const c = source.element;
|
|
1211
|
+
return (c.width || 0) > 0 && (c.height || 0) > 0;
|
|
1212
|
+
}
|
|
1213
|
+
return true;
|
|
1214
|
+
}
|
|
1215
|
+
function getDrawRect(el, fit) {
|
|
1216
|
+
const canvas = offscreenCtx?.canvas;
|
|
1217
|
+
if (!canvas) return null;
|
|
1218
|
+
const canvasW = canvas.width;
|
|
1219
|
+
const canvasH = canvas.height;
|
|
1220
|
+
const sourceW = el.videoWidth ?? el.width;
|
|
1221
|
+
const sourceH = el.videoHeight ?? el.height;
|
|
1222
|
+
if (!sourceW || !sourceH) return null;
|
|
1223
|
+
const cached = rectCache.get(el);
|
|
1224
|
+
if (cached && cached.canvasW === canvasW && cached.canvasH === canvasH && cached.sourceW === sourceW && cached.sourceH === sourceH && cached.fit === fit) {
|
|
1225
|
+
return { dx: cached.dx, dy: cached.dy, dw: cached.dw, dh: cached.dh };
|
|
1226
|
+
}
|
|
1227
|
+
const scale = fit === "cover" ? Math.max(canvasW / sourceW, canvasH / sourceH) : Math.min(canvasW / sourceW, canvasH / sourceH);
|
|
1228
|
+
const dw = Math.floor(sourceW * scale);
|
|
1229
|
+
const dh = Math.floor(sourceH * scale);
|
|
1230
|
+
const dx = Math.floor((canvasW - dw) / 2);
|
|
1231
|
+
const dy = Math.floor((canvasH - dh) / 2);
|
|
1232
|
+
rectCache.set(el, {
|
|
1233
|
+
canvasW,
|
|
1234
|
+
canvasH,
|
|
1235
|
+
sourceW,
|
|
1236
|
+
sourceH,
|
|
1237
|
+
dx,
|
|
1238
|
+
dy,
|
|
1239
|
+
dw,
|
|
1240
|
+
dh,
|
|
1241
|
+
fit
|
|
1242
|
+
});
|
|
1243
|
+
return { dx, dy, dw, dh };
|
|
1244
|
+
}
|
|
1245
|
+
function blitSource(source, alpha, timestamp) {
|
|
1246
|
+
if (!offscreenCtx) return;
|
|
1247
|
+
const ctx = offscreenCtx;
|
|
1248
|
+
if (source.kind === "custom") {
|
|
1249
|
+
if (alpha < 1 && crossfadeCtx && crossfadeCanvas) {
|
|
1250
|
+
crossfadeCtx.clearRect(
|
|
1251
|
+
0,
|
|
1252
|
+
0,
|
|
1253
|
+
crossfadeCtx.canvas.width,
|
|
1254
|
+
crossfadeCtx.canvas.height
|
|
1255
|
+
);
|
|
1256
|
+
if (source.onFrame) source.onFrame(crossfadeCtx, timestamp);
|
|
1257
|
+
const prev2 = ctx.globalAlpha;
|
|
1258
|
+
try {
|
|
1259
|
+
ctx.globalAlpha = Math.max(0, Math.min(1, alpha));
|
|
1260
|
+
ctx.drawImage(crossfadeCanvas, 0, 0);
|
|
1261
|
+
} finally {
|
|
1262
|
+
ctx.globalAlpha = prev2;
|
|
1263
|
+
}
|
|
1264
|
+
} else {
|
|
1265
|
+
if (source.onFrame) source.onFrame(ctx, timestamp);
|
|
1266
|
+
}
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
const el = source.element;
|
|
1270
|
+
const rect = getDrawRect(el, source.fit ?? "contain");
|
|
1271
|
+
if (!rect) return;
|
|
1272
|
+
const prev = ctx.globalAlpha;
|
|
1273
|
+
try {
|
|
1274
|
+
ctx.globalAlpha = Math.max(0, Math.min(1, alpha));
|
|
1275
|
+
ctx.drawImage(
|
|
1276
|
+
el,
|
|
1277
|
+
rect.dx,
|
|
1278
|
+
rect.dy,
|
|
1279
|
+
rect.dw,
|
|
1280
|
+
rect.dh
|
|
1281
|
+
);
|
|
1282
|
+
} finally {
|
|
1283
|
+
ctx.globalAlpha = prev;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
initCanvas();
|
|
1287
|
+
return {
|
|
1288
|
+
get captureCanvas() {
|
|
1289
|
+
return captureCanvas;
|
|
1290
|
+
},
|
|
1291
|
+
get offscreenCtx() {
|
|
1292
|
+
return offscreenCtx;
|
|
1293
|
+
},
|
|
1294
|
+
get size() {
|
|
1295
|
+
return { ...size };
|
|
1296
|
+
},
|
|
1297
|
+
isSourceReady,
|
|
1298
|
+
setActiveSource(source) {
|
|
1299
|
+
if (cleanupFn) {
|
|
1300
|
+
cleanupFn();
|
|
1301
|
+
cleanupFn = void 0;
|
|
1302
|
+
}
|
|
1303
|
+
if (source === null) {
|
|
1304
|
+
currentSource = null;
|
|
1305
|
+
pendingSource = null;
|
|
1306
|
+
crossfadeStart = null;
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
pendingSource = source;
|
|
1310
|
+
crossfadeStart = null;
|
|
1311
|
+
if (source.kind === "custom" && source.onStart) {
|
|
1312
|
+
const cleanup = source.onStart(offscreenCtx);
|
|
1313
|
+
cleanupFn = cleanup || void 0;
|
|
1314
|
+
return cleanupFn;
|
|
1315
|
+
}
|
|
1316
|
+
},
|
|
1317
|
+
renderFrame(timestamp) {
|
|
1318
|
+
const off = offscreenCtx;
|
|
1319
|
+
const cap = captureCtx;
|
|
1320
|
+
const capCanvas = captureCanvas;
|
|
1321
|
+
if (!off || !cap || !capCanvas) return;
|
|
1322
|
+
if (pendingSource && isSourceReady(pendingSource)) {
|
|
1323
|
+
if (crossfadeStart === null) crossfadeStart = timestamp;
|
|
1324
|
+
}
|
|
1325
|
+
off.globalCompositeOperation = "source-over";
|
|
1326
|
+
const willDraw = !!(pendingSource && isSourceReady(pendingSource) || currentSource);
|
|
1327
|
+
if (willDraw) {
|
|
1328
|
+
off.fillStyle = "#000";
|
|
1329
|
+
off.fillRect(0, 0, off.canvas.width, off.canvas.height);
|
|
1330
|
+
}
|
|
1331
|
+
const fading = pendingSource && crossfadeStart !== null && currentSource;
|
|
1332
|
+
if (fading) {
|
|
1333
|
+
const t = Math.min(1, (timestamp - crossfadeStart) / crossfadeMs);
|
|
1334
|
+
blitSource(currentSource, 1 - t, timestamp);
|
|
1335
|
+
blitSource(pendingSource, t, timestamp);
|
|
1336
|
+
if (t >= 1) {
|
|
1337
|
+
currentSource = pendingSource;
|
|
1338
|
+
pendingSource = null;
|
|
1339
|
+
crossfadeStart = null;
|
|
1340
|
+
}
|
|
1341
|
+
} else if (pendingSource && !currentSource) {
|
|
1342
|
+
if (isSourceReady(pendingSource)) {
|
|
1343
|
+
blitSource(pendingSource, 1, timestamp);
|
|
1344
|
+
currentSource = pendingSource;
|
|
1345
|
+
pendingSource = null;
|
|
1346
|
+
crossfadeStart = null;
|
|
1347
|
+
}
|
|
1348
|
+
} else if (currentSource) {
|
|
1349
|
+
blitSource(currentSource, 1, timestamp);
|
|
1350
|
+
}
|
|
1351
|
+
if (keepalive) {
|
|
1352
|
+
const w = off.canvas.width;
|
|
1353
|
+
const h = off.canvas.height;
|
|
1354
|
+
const prevAlpha = off.globalAlpha;
|
|
1355
|
+
const prevFill = off.fillStyle;
|
|
1356
|
+
try {
|
|
1357
|
+
off.globalAlpha = 0.08;
|
|
1358
|
+
off.fillStyle = frameIndex % 2 ? "#101010" : "#0e0e0e";
|
|
1359
|
+
off.fillRect(w - 16, h - 16, 16, 16);
|
|
1360
|
+
} finally {
|
|
1361
|
+
off.globalAlpha = prevAlpha;
|
|
1362
|
+
off.fillStyle = prevFill;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
frameIndex++;
|
|
1366
|
+
cap.drawImage(
|
|
1367
|
+
off.canvas,
|
|
1368
|
+
0,
|
|
1369
|
+
0,
|
|
1370
|
+
off.canvas.width,
|
|
1371
|
+
off.canvas.height,
|
|
1372
|
+
0,
|
|
1373
|
+
0,
|
|
1374
|
+
capCanvas.width,
|
|
1375
|
+
capCanvas.height
|
|
1376
|
+
);
|
|
1377
|
+
},
|
|
1378
|
+
resize(width, height, dpr) {
|
|
1379
|
+
const nextDpr = Math.min(2, dpr);
|
|
1380
|
+
if (size.width === width && size.height === height && size.dpr === nextDpr) {
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
size = { width, height, dpr: nextDpr };
|
|
1384
|
+
const pxW = Math.round(width * nextDpr);
|
|
1385
|
+
const pxH = Math.round(height * nextDpr);
|
|
1386
|
+
const outW = Math.round(width);
|
|
1387
|
+
const outH = Math.round(height);
|
|
1388
|
+
if (captureCanvas) {
|
|
1389
|
+
captureCanvas.width = outW;
|
|
1390
|
+
captureCanvas.height = outH;
|
|
1391
|
+
}
|
|
1392
|
+
if (offscreen instanceof HTMLCanvasElement) {
|
|
1393
|
+
offscreen.width = pxW;
|
|
1394
|
+
offscreen.height = pxH;
|
|
1395
|
+
} else if (offscreen instanceof OffscreenCanvas) {
|
|
1396
|
+
offscreen.width = pxW;
|
|
1397
|
+
offscreen.height = pxH;
|
|
1398
|
+
}
|
|
1399
|
+
if (crossfadeCanvas instanceof HTMLCanvasElement) {
|
|
1400
|
+
crossfadeCanvas.width = pxW;
|
|
1401
|
+
crossfadeCanvas.height = pxH;
|
|
1402
|
+
} else if (crossfadeCanvas instanceof OffscreenCanvas) {
|
|
1403
|
+
crossfadeCanvas.width = pxW;
|
|
1404
|
+
crossfadeCanvas.height = pxH;
|
|
1405
|
+
}
|
|
1406
|
+
rectCache = /* @__PURE__ */ new WeakMap();
|
|
1407
|
+
},
|
|
1408
|
+
setCrossfadeMs(ms) {
|
|
1409
|
+
crossfadeMs = Math.max(0, ms);
|
|
1410
|
+
},
|
|
1411
|
+
setKeepalive(enabled) {
|
|
1412
|
+
keepalive = enabled;
|
|
1413
|
+
},
|
|
1414
|
+
destroy() {
|
|
1415
|
+
if (cleanupFn) {
|
|
1416
|
+
cleanupFn();
|
|
1417
|
+
cleanupFn = void 0;
|
|
1418
|
+
}
|
|
1419
|
+
currentSource = null;
|
|
1420
|
+
pendingSource = null;
|
|
1421
|
+
captureCanvas = null;
|
|
1422
|
+
captureCtx = null;
|
|
1423
|
+
offscreen = null;
|
|
1424
|
+
offscreenCtx = null;
|
|
1425
|
+
crossfadeCanvas = null;
|
|
1426
|
+
crossfadeCtx = null;
|
|
1427
|
+
}
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// src/internal/compositor/Scheduler.ts
|
|
1432
|
+
function createScheduler(options) {
|
|
1433
|
+
let fps = Math.max(1, options.fps);
|
|
1434
|
+
let sendFps = Math.max(1, options.sendFps);
|
|
1435
|
+
const onFrame = options.onFrame;
|
|
1436
|
+
const onSendFpsChange = options.onSendFpsChange;
|
|
1437
|
+
let isRunning = false;
|
|
1438
|
+
let lastFrameAt = 0;
|
|
1439
|
+
let rafId = null;
|
|
1440
|
+
let rafFallbackActive = false;
|
|
1441
|
+
let videoFrameRequestId = null;
|
|
1442
|
+
let videoFrameSource = null;
|
|
1443
|
+
function getTimestamp() {
|
|
1444
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
1445
|
+
}
|
|
1446
|
+
function shouldRenderFrame() {
|
|
1447
|
+
const now = getTimestamp();
|
|
1448
|
+
const minIntervalMs = 1e3 / Math.max(1, sendFps);
|
|
1449
|
+
if (lastFrameAt !== 0 && now - lastFrameAt < minIntervalMs) {
|
|
1450
|
+
return false;
|
|
1451
|
+
}
|
|
1452
|
+
return true;
|
|
1453
|
+
}
|
|
1454
|
+
function renderIfNeeded() {
|
|
1455
|
+
if (!shouldRenderFrame()) return;
|
|
1456
|
+
const timestamp = getTimestamp();
|
|
1457
|
+
onFrame(timestamp);
|
|
1458
|
+
lastFrameAt = timestamp;
|
|
1459
|
+
}
|
|
1460
|
+
function scheduleWithRaf(isFallback) {
|
|
1461
|
+
if (isFallback) {
|
|
1462
|
+
if (rafFallbackActive) return;
|
|
1463
|
+
rafFallbackActive = true;
|
|
1464
|
+
}
|
|
1465
|
+
const loop = () => {
|
|
1466
|
+
renderIfNeeded();
|
|
1467
|
+
rafId = requestAnimationFrame(loop);
|
|
1468
|
+
};
|
|
1469
|
+
rafId = requestAnimationFrame(loop);
|
|
1470
|
+
}
|
|
1471
|
+
function scheduleWithVideoFrame(videoEl) {
|
|
1472
|
+
if (typeof videoEl.requestVideoFrameCallback !== "function") {
|
|
1473
|
+
scheduleWithRaf(false);
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
videoFrameSource = videoEl;
|
|
1477
|
+
const cb = () => {
|
|
1478
|
+
renderIfNeeded();
|
|
1479
|
+
if (videoFrameSource === videoEl) {
|
|
1480
|
+
try {
|
|
1481
|
+
videoFrameRequestId = videoEl.requestVideoFrameCallback(cb);
|
|
1482
|
+
} catch {
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
};
|
|
1486
|
+
try {
|
|
1487
|
+
videoFrameRequestId = videoEl.requestVideoFrameCallback(cb);
|
|
1488
|
+
} catch {
|
|
1489
|
+
}
|
|
1490
|
+
scheduleWithRaf(true);
|
|
1491
|
+
}
|
|
1492
|
+
function cancelSchedulers() {
|
|
1493
|
+
if (rafId != null) {
|
|
1494
|
+
cancelAnimationFrame(rafId);
|
|
1495
|
+
rafId = null;
|
|
1496
|
+
}
|
|
1497
|
+
rafFallbackActive = false;
|
|
1498
|
+
if (videoFrameRequestId && videoFrameSource) {
|
|
1499
|
+
try {
|
|
1500
|
+
if (typeof videoFrameSource.cancelVideoFrameCallback === "function") {
|
|
1501
|
+
videoFrameSource.cancelVideoFrameCallback(videoFrameRequestId);
|
|
1502
|
+
}
|
|
1503
|
+
} catch {
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
videoFrameRequestId = null;
|
|
1507
|
+
videoFrameSource = null;
|
|
1508
|
+
}
|
|
1509
|
+
return {
|
|
1510
|
+
get isRunning() {
|
|
1511
|
+
return isRunning;
|
|
1512
|
+
},
|
|
1513
|
+
get fps() {
|
|
1514
|
+
return fps;
|
|
1515
|
+
},
|
|
1516
|
+
get sendFps() {
|
|
1517
|
+
return sendFps;
|
|
1518
|
+
},
|
|
1519
|
+
start(videoElement) {
|
|
1520
|
+
if (isRunning) {
|
|
1521
|
+
cancelSchedulers();
|
|
1522
|
+
}
|
|
1523
|
+
isRunning = true;
|
|
1524
|
+
lastFrameAt = 0;
|
|
1525
|
+
if (videoElement && typeof videoElement.requestVideoFrameCallback === "function") {
|
|
1526
|
+
scheduleWithVideoFrame(videoElement);
|
|
1527
|
+
} else {
|
|
1528
|
+
scheduleWithRaf(false);
|
|
1529
|
+
}
|
|
1530
|
+
},
|
|
1531
|
+
stop() {
|
|
1532
|
+
isRunning = false;
|
|
1533
|
+
cancelSchedulers();
|
|
1534
|
+
},
|
|
1535
|
+
setFps(newFps) {
|
|
1536
|
+
fps = Math.max(1, newFps);
|
|
1537
|
+
},
|
|
1538
|
+
setSendFps(newSendFps) {
|
|
1539
|
+
const next = Math.max(1, newSendFps);
|
|
1540
|
+
if (sendFps === next) return;
|
|
1541
|
+
sendFps = next;
|
|
1542
|
+
onSendFpsChange?.(sendFps);
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// src/internal/compositor/AudioManager.ts
|
|
1548
|
+
function createAudioManager(options) {
|
|
1549
|
+
let outputStream = null;
|
|
1550
|
+
let audioCtx = null;
|
|
1551
|
+
let silentOsc = null;
|
|
1552
|
+
let silentGain = null;
|
|
1553
|
+
let audioDst = null;
|
|
1554
|
+
let silentAudioTrack = null;
|
|
1555
|
+
const externalAudioTrackIds = /* @__PURE__ */ new Set();
|
|
1556
|
+
const externalAudioEndHandlers = /* @__PURE__ */ new Map();
|
|
1557
|
+
let audioUnlockHandler = null;
|
|
1558
|
+
let audioUnlockAttached = false;
|
|
1559
|
+
let audioStateListenerAttached = false;
|
|
1560
|
+
function ensureSilentAudioTrack() {
|
|
1561
|
+
if (options.disableSilentAudio) return;
|
|
1562
|
+
if (!outputStream) return;
|
|
1563
|
+
const alreadyHasAudio = outputStream.getAudioTracks().length > 0;
|
|
1564
|
+
if (alreadyHasAudio) return;
|
|
1565
|
+
if (silentAudioTrack && silentAudioTrack.readyState === "live") {
|
|
1566
|
+
try {
|
|
1567
|
+
outputStream.addTrack(silentAudioTrack);
|
|
1568
|
+
} catch {
|
|
1569
|
+
}
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
if (!audioCtx) {
|
|
1573
|
+
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
|
1574
|
+
if (!AudioContextClass) return;
|
|
1575
|
+
audioCtx = new AudioContextClass({
|
|
1576
|
+
sampleRate: 48e3
|
|
1577
|
+
});
|
|
1578
|
+
try {
|
|
1579
|
+
audioCtx.resume().catch(() => {
|
|
1580
|
+
});
|
|
1581
|
+
} catch {
|
|
1582
|
+
}
|
|
1583
|
+
attachAudioCtxStateListener();
|
|
1584
|
+
}
|
|
1585
|
+
const ac = audioCtx;
|
|
1586
|
+
if (!ac) return;
|
|
1587
|
+
silentOsc = ac.createOscillator();
|
|
1588
|
+
silentGain = ac.createGain();
|
|
1589
|
+
audioDst = ac.createMediaStreamDestination();
|
|
1590
|
+
silentGain.gain.setValueAtTime(1e-4, ac.currentTime);
|
|
1591
|
+
silentOsc.frequency.setValueAtTime(440, ac.currentTime);
|
|
1592
|
+
silentOsc.type = "sine";
|
|
1593
|
+
silentOsc.connect(silentGain);
|
|
1594
|
+
silentGain.connect(audioDst);
|
|
1595
|
+
silentOsc.start();
|
|
1596
|
+
const track = audioDst.stream.getAudioTracks()[0];
|
|
1597
|
+
if (track) {
|
|
1598
|
+
silentAudioTrack = track;
|
|
1599
|
+
try {
|
|
1600
|
+
outputStream.addTrack(track);
|
|
1601
|
+
} catch {
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
function removeSilentAudioTrack() {
|
|
1606
|
+
try {
|
|
1607
|
+
if (outputStream && silentAudioTrack) {
|
|
1608
|
+
try {
|
|
1609
|
+
outputStream.removeTrack(silentAudioTrack);
|
|
1610
|
+
} catch {
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
if (silentOsc) {
|
|
1614
|
+
try {
|
|
1615
|
+
silentOsc.stop();
|
|
1616
|
+
} catch {
|
|
1617
|
+
}
|
|
1618
|
+
try {
|
|
1619
|
+
silentOsc.disconnect();
|
|
1620
|
+
} catch {
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
if (silentGain) {
|
|
1624
|
+
try {
|
|
1625
|
+
silentGain.disconnect();
|
|
1626
|
+
} catch {
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
silentOsc = null;
|
|
1630
|
+
silentGain = null;
|
|
1631
|
+
audioDst = null;
|
|
1632
|
+
silentAudioTrack = null;
|
|
1633
|
+
if (audioCtx) {
|
|
1634
|
+
try {
|
|
1635
|
+
audioCtx.close();
|
|
1636
|
+
} catch {
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
audioCtx = null;
|
|
1640
|
+
} catch {
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
function rebuildSilentAudioTrack() {
|
|
1644
|
+
if (options.disableSilentAudio) return;
|
|
1645
|
+
if (!outputStream) return;
|
|
1646
|
+
if (externalAudioTrackIds.size > 0) return;
|
|
1647
|
+
if (silentAudioTrack) {
|
|
1648
|
+
try {
|
|
1649
|
+
outputStream.removeTrack(silentAudioTrack);
|
|
1650
|
+
} catch {
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
if (silentOsc) {
|
|
1654
|
+
try {
|
|
1655
|
+
silentOsc.stop();
|
|
1656
|
+
} catch {
|
|
1657
|
+
}
|
|
1658
|
+
try {
|
|
1659
|
+
silentOsc.disconnect();
|
|
1660
|
+
} catch {
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
if (silentGain) {
|
|
1664
|
+
try {
|
|
1665
|
+
silentGain.disconnect();
|
|
1666
|
+
} catch {
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
silentOsc = null;
|
|
1670
|
+
silentGain = null;
|
|
1671
|
+
audioDst = null;
|
|
1672
|
+
silentAudioTrack = null;
|
|
1673
|
+
const ac = audioCtx;
|
|
1674
|
+
if (!ac || ac.state !== "running") return;
|
|
1675
|
+
attachAudioCtxStateListener();
|
|
1676
|
+
silentOsc = ac.createOscillator();
|
|
1677
|
+
silentGain = ac.createGain();
|
|
1678
|
+
audioDst = ac.createMediaStreamDestination();
|
|
1679
|
+
silentGain.gain.setValueAtTime(1e-4, ac.currentTime);
|
|
1680
|
+
silentOsc.frequency.setValueAtTime(440, ac.currentTime);
|
|
1681
|
+
silentOsc.type = "sine";
|
|
1682
|
+
silentOsc.connect(silentGain);
|
|
1683
|
+
silentGain.connect(audioDst);
|
|
1684
|
+
silentOsc.start();
|
|
1685
|
+
const track = audioDst.stream.getAudioTracks()[0];
|
|
1686
|
+
if (track) {
|
|
1687
|
+
silentAudioTrack = track;
|
|
1688
|
+
try {
|
|
1689
|
+
outputStream.addTrack(track);
|
|
1690
|
+
} catch {
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
function attachAudioCtxStateListener() {
|
|
1695
|
+
const ac = audioCtx;
|
|
1696
|
+
if (!ac || audioStateListenerAttached) return;
|
|
1697
|
+
const onStateChange = () => {
|
|
1698
|
+
try {
|
|
1699
|
+
if (audioCtx && audioCtx.state === "running") {
|
|
1700
|
+
rebuildSilentAudioTrack();
|
|
1701
|
+
cleanupAudioAutoUnlock();
|
|
1702
|
+
}
|
|
1703
|
+
} catch {
|
|
1704
|
+
}
|
|
1705
|
+
};
|
|
1706
|
+
try {
|
|
1707
|
+
ac.onstatechange = onStateChange;
|
|
1708
|
+
audioStateListenerAttached = true;
|
|
1709
|
+
} catch {
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
function setupAudioAutoUnlock() {
|
|
1713
|
+
if (!options.autoUnlock) return;
|
|
1714
|
+
if (typeof document === "undefined") return;
|
|
1715
|
+
if (audioUnlockAttached) return;
|
|
1716
|
+
const handler = () => {
|
|
1717
|
+
unlock();
|
|
1718
|
+
};
|
|
1719
|
+
audioUnlockHandler = handler;
|
|
1720
|
+
options.unlockEvents.forEach((evt) => {
|
|
1721
|
+
try {
|
|
1722
|
+
document.addEventListener(evt, handler, { capture: true });
|
|
1723
|
+
} catch {
|
|
1724
|
+
}
|
|
1725
|
+
});
|
|
1726
|
+
audioUnlockAttached = true;
|
|
1727
|
+
}
|
|
1728
|
+
function cleanupAudioAutoUnlock() {
|
|
1729
|
+
if (!audioUnlockAttached) return;
|
|
1730
|
+
if (typeof document !== "undefined" && audioUnlockHandler) {
|
|
1731
|
+
options.unlockEvents.forEach((evt) => {
|
|
1732
|
+
try {
|
|
1733
|
+
document.removeEventListener(evt, audioUnlockHandler, {
|
|
1734
|
+
capture: true
|
|
1735
|
+
});
|
|
1736
|
+
} catch {
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
audioUnlockAttached = false;
|
|
1741
|
+
audioUnlockHandler = null;
|
|
1742
|
+
}
|
|
1743
|
+
async function unlock() {
|
|
1744
|
+
try {
|
|
1745
|
+
if (typeof window === "undefined") return false;
|
|
1746
|
+
if (!audioCtx || audioCtx.state === "closed") {
|
|
1747
|
+
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
|
1748
|
+
if (!AudioContextClass) return false;
|
|
1749
|
+
audioCtx = new AudioContextClass({
|
|
1750
|
+
sampleRate: 48e3
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
const ac = audioCtx;
|
|
1754
|
+
if (!ac) return false;
|
|
1755
|
+
try {
|
|
1756
|
+
await ac.resume();
|
|
1757
|
+
} catch {
|
|
1758
|
+
}
|
|
1759
|
+
attachAudioCtxStateListener();
|
|
1760
|
+
if (ac.state === "running") {
|
|
1761
|
+
rebuildSilentAudioTrack();
|
|
1762
|
+
cleanupAudioAutoUnlock();
|
|
1763
|
+
return true;
|
|
1764
|
+
}
|
|
1765
|
+
return false;
|
|
1766
|
+
} catch {
|
|
1767
|
+
return false;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
setupAudioAutoUnlock();
|
|
1771
|
+
return {
|
|
1772
|
+
setOutputStream(stream) {
|
|
1773
|
+
outputStream = stream;
|
|
1774
|
+
ensureSilentAudioTrack();
|
|
1775
|
+
},
|
|
1776
|
+
addTrack(track) {
|
|
1777
|
+
if (!outputStream) return;
|
|
1778
|
+
try {
|
|
1779
|
+
if (silentAudioTrack) {
|
|
1780
|
+
try {
|
|
1781
|
+
outputStream.removeTrack(silentAudioTrack);
|
|
1782
|
+
} catch {
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
const exists = outputStream.getAudioTracks().some((t) => t.id === track.id);
|
|
1786
|
+
if (!exists) {
|
|
1787
|
+
outputStream.addTrack(track);
|
|
1788
|
+
}
|
|
1789
|
+
externalAudioTrackIds.add(track.id);
|
|
1790
|
+
const onEnded = () => {
|
|
1791
|
+
try {
|
|
1792
|
+
if (!outputStream) return;
|
|
1793
|
+
outputStream.getAudioTracks().forEach((t) => {
|
|
1794
|
+
if (t.id === track.id) {
|
|
1795
|
+
try {
|
|
1796
|
+
outputStream.removeTrack(t);
|
|
1797
|
+
} catch {
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
});
|
|
1801
|
+
externalAudioTrackIds.delete(track.id);
|
|
1802
|
+
externalAudioEndHandlers.delete(track.id);
|
|
1803
|
+
if (outputStream.getAudioTracks().length === 0) {
|
|
1804
|
+
ensureSilentAudioTrack();
|
|
1805
|
+
}
|
|
1806
|
+
} catch {
|
|
1807
|
+
}
|
|
1808
|
+
try {
|
|
1809
|
+
track.removeEventListener("ended", onEnded);
|
|
1810
|
+
} catch {
|
|
1811
|
+
}
|
|
1812
|
+
};
|
|
1813
|
+
track.addEventListener("ended", onEnded);
|
|
1814
|
+
externalAudioEndHandlers.set(track.id, onEnded);
|
|
1815
|
+
} catch {
|
|
1816
|
+
}
|
|
1817
|
+
},
|
|
1818
|
+
removeTrack(trackId) {
|
|
1819
|
+
if (!outputStream) return;
|
|
1820
|
+
outputStream.getAudioTracks().forEach((t) => {
|
|
1821
|
+
if (t.id === trackId) {
|
|
1822
|
+
outputStream.removeTrack(t);
|
|
1823
|
+
}
|
|
1824
|
+
});
|
|
1825
|
+
externalAudioTrackIds.delete(trackId);
|
|
1826
|
+
const handler = externalAudioEndHandlers.get(trackId);
|
|
1827
|
+
const tracks = outputStream.getAudioTracks();
|
|
1828
|
+
const tr = tracks.find((t) => t.id === trackId);
|
|
1829
|
+
if (tr && handler) {
|
|
1830
|
+
try {
|
|
1831
|
+
tr.removeEventListener("ended", handler);
|
|
1832
|
+
} catch {
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
externalAudioEndHandlers.delete(trackId);
|
|
1836
|
+
if (outputStream.getAudioTracks().length === 0) {
|
|
1837
|
+
ensureSilentAudioTrack();
|
|
1838
|
+
}
|
|
1839
|
+
},
|
|
1840
|
+
unlock,
|
|
1841
|
+
destroy() {
|
|
1842
|
+
cleanupAudioAutoUnlock();
|
|
1843
|
+
try {
|
|
1844
|
+
if (audioCtx && audioCtx.onstatechange) {
|
|
1845
|
+
audioCtx.onstatechange = null;
|
|
1846
|
+
}
|
|
1847
|
+
} catch {
|
|
1848
|
+
}
|
|
1849
|
+
audioStateListenerAttached = false;
|
|
1850
|
+
externalAudioEndHandlers.forEach((handler, id) => {
|
|
1851
|
+
try {
|
|
1852
|
+
const tr = outputStream?.getAudioTracks().find((t) => t.id === id);
|
|
1853
|
+
if (tr) tr.removeEventListener("ended", handler);
|
|
1854
|
+
} catch {
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1857
|
+
externalAudioEndHandlers.clear();
|
|
1858
|
+
externalAudioTrackIds.clear();
|
|
1859
|
+
removeSilentAudioTrack();
|
|
1860
|
+
outputStream = null;
|
|
1861
|
+
}
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
// src/internal/compositor/VisibilityHandler.ts
|
|
1866
|
+
function createVisibilityHandler(options) {
|
|
1867
|
+
let isHidden = false;
|
|
1868
|
+
let backgroundIntervalId = null;
|
|
1869
|
+
let visibilityListener = null;
|
|
1870
|
+
let started = false;
|
|
1871
|
+
function onVisibilityChange() {
|
|
1872
|
+
if (typeof document === "undefined") return;
|
|
1873
|
+
const hidden = document.visibilityState === "hidden";
|
|
1874
|
+
if (hidden && !isHidden) {
|
|
1875
|
+
isHidden = true;
|
|
1876
|
+
options.onHidden();
|
|
1877
|
+
if (backgroundIntervalId == null) {
|
|
1878
|
+
backgroundIntervalId = setInterval(() => {
|
|
1879
|
+
options.backgroundRenderFn();
|
|
1880
|
+
}, 1e3);
|
|
1881
|
+
}
|
|
1882
|
+
} else if (!hidden && isHidden) {
|
|
1883
|
+
isHidden = false;
|
|
1884
|
+
if (backgroundIntervalId != null) {
|
|
1885
|
+
clearInterval(backgroundIntervalId);
|
|
1886
|
+
backgroundIntervalId = null;
|
|
1887
|
+
}
|
|
1888
|
+
options.onVisible();
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
return {
|
|
1892
|
+
get isHidden() {
|
|
1893
|
+
return isHidden;
|
|
1894
|
+
},
|
|
1895
|
+
start() {
|
|
1896
|
+
if (started) return;
|
|
1897
|
+
if (typeof document === "undefined") return;
|
|
1898
|
+
started = true;
|
|
1899
|
+
visibilityListener = onVisibilityChange;
|
|
1900
|
+
document.addEventListener("visibilitychange", visibilityListener);
|
|
1901
|
+
onVisibilityChange();
|
|
1902
|
+
},
|
|
1903
|
+
stop() {
|
|
1904
|
+
if (!started) return;
|
|
1905
|
+
started = false;
|
|
1906
|
+
if (typeof document !== "undefined" && visibilityListener) {
|
|
1907
|
+
try {
|
|
1908
|
+
document.removeEventListener("visibilitychange", visibilityListener);
|
|
1909
|
+
} catch {
|
|
1910
|
+
}
|
|
1911
|
+
visibilityListener = null;
|
|
1912
|
+
}
|
|
1913
|
+
if (backgroundIntervalId != null) {
|
|
1914
|
+
clearInterval(backgroundIntervalId);
|
|
1915
|
+
backgroundIntervalId = null;
|
|
1916
|
+
}
|
|
1917
|
+
isHidden = false;
|
|
1918
|
+
}
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
// src/Compositor.ts
|
|
1923
|
+
var CompositorEventEmitter = class {
|
|
1924
|
+
constructor() {
|
|
1925
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
1926
|
+
}
|
|
1927
|
+
on(event, handler) {
|
|
1928
|
+
if (!this.listeners.has(event)) {
|
|
1929
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
1930
|
+
}
|
|
1931
|
+
this.listeners.get(event).add(handler);
|
|
1932
|
+
return () => this.off(event, handler);
|
|
1933
|
+
}
|
|
1934
|
+
off(event, handler) {
|
|
1935
|
+
this.listeners.get(event)?.delete(handler);
|
|
1936
|
+
}
|
|
1937
|
+
emit(event, ...args) {
|
|
1938
|
+
this.listeners.get(event)?.forEach((handler) => {
|
|
1939
|
+
handler(...args);
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
clearListeners() {
|
|
1943
|
+
this.listeners.clear();
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1946
|
+
var Compositor = class extends CompositorEventEmitter {
|
|
1947
|
+
constructor(options = {}) {
|
|
1948
|
+
super();
|
|
1949
|
+
this._activeId = null;
|
|
1950
|
+
this.lastVisibleSendFps = null;
|
|
1951
|
+
this.outputStream = null;
|
|
1952
|
+
this.destroyed = false;
|
|
1953
|
+
const width = options.width ?? 512;
|
|
1954
|
+
const height = options.height ?? 512;
|
|
1955
|
+
this._fps = Math.max(1, options.fps ?? 30);
|
|
1956
|
+
this._sendFps = Math.max(1, options.sendFps ?? this._fps);
|
|
1957
|
+
const dpr = Math.min(
|
|
1958
|
+
2,
|
|
1959
|
+
options.dpr ?? (typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1)
|
|
1960
|
+
);
|
|
1961
|
+
const crossfadeMs = options.crossfadeMs ?? 500;
|
|
1962
|
+
const keepalive = options.keepalive ?? true;
|
|
1963
|
+
this.registry = createRegistry({
|
|
1964
|
+
onRegister: (id, source) => this.emit("registered", id, source),
|
|
1965
|
+
onUnregister: (id) => this.emit("unregistered", id)
|
|
1966
|
+
});
|
|
1967
|
+
this.renderer = createRenderer({
|
|
1968
|
+
width,
|
|
1969
|
+
height,
|
|
1970
|
+
dpr,
|
|
1971
|
+
crossfadeMs,
|
|
1972
|
+
keepalive
|
|
1973
|
+
});
|
|
1974
|
+
this.scheduler = createScheduler({
|
|
1975
|
+
fps: this._fps,
|
|
1976
|
+
sendFps: this._sendFps,
|
|
1977
|
+
onFrame: (timestamp) => this.renderer.renderFrame(timestamp),
|
|
1978
|
+
onSendFpsChange: (fps) => {
|
|
1979
|
+
this._sendFps = fps;
|
|
1980
|
+
options.onSendFpsChange?.(fps);
|
|
1981
|
+
this.applyVideoTrackConstraints();
|
|
1982
|
+
}
|
|
1983
|
+
});
|
|
1984
|
+
this.audioManager = createAudioManager({
|
|
1985
|
+
autoUnlock: options.autoUnlockAudio ?? true,
|
|
1986
|
+
unlockEvents: options.unlockEvents && options.unlockEvents.length > 0 ? options.unlockEvents : ["pointerdown", "click", "touchstart", "keydown"],
|
|
1987
|
+
disableSilentAudio: options.disableSilentAudio ?? false
|
|
1988
|
+
});
|
|
1989
|
+
this.visibilityHandler = createVisibilityHandler({
|
|
1990
|
+
onHidden: () => {
|
|
1991
|
+
if (this.lastVisibleSendFps == null) this.lastVisibleSendFps = this._sendFps;
|
|
1992
|
+
if (this._sendFps !== 5) {
|
|
1993
|
+
this.scheduler.setSendFps(5);
|
|
1994
|
+
this._sendFps = 5;
|
|
1995
|
+
}
|
|
1996
|
+
},
|
|
1997
|
+
onVisible: () => {
|
|
1998
|
+
if (this.lastVisibleSendFps != null && this._sendFps !== this.lastVisibleSendFps) {
|
|
1999
|
+
this.scheduler.setSendFps(this.lastVisibleSendFps);
|
|
2000
|
+
this._sendFps = this.lastVisibleSendFps;
|
|
2001
|
+
}
|
|
2002
|
+
this.lastVisibleSendFps = null;
|
|
2003
|
+
},
|
|
2004
|
+
backgroundRenderFn: () => {
|
|
2005
|
+
this.renderer.renderFrame(performance.now());
|
|
2006
|
+
this.requestVideoTrackFrame();
|
|
2007
|
+
}
|
|
2008
|
+
});
|
|
2009
|
+
this.outputStream = this.createOutputStream();
|
|
2010
|
+
this.audioManager.setOutputStream(this.outputStream);
|
|
2011
|
+
this.visibilityHandler.start();
|
|
2012
|
+
}
|
|
2013
|
+
// ============================================================================
|
|
2014
|
+
// Source Registry
|
|
2015
|
+
// ============================================================================
|
|
2016
|
+
register(id, source) {
|
|
2017
|
+
if (this.destroyed) return;
|
|
2018
|
+
this.registry.register(id, source);
|
|
2019
|
+
}
|
|
2020
|
+
unregister(id) {
|
|
2021
|
+
if (this.destroyed) return;
|
|
2022
|
+
const wasActive = this._activeId === id;
|
|
2023
|
+
this.registry.unregister(id);
|
|
2024
|
+
if (wasActive) {
|
|
2025
|
+
this._activeId = null;
|
|
2026
|
+
this.renderer.setActiveSource(null);
|
|
2027
|
+
this.scheduler.stop();
|
|
2028
|
+
this.emit("activated", null, void 0);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
get(id) {
|
|
2032
|
+
return this.registry.get(id);
|
|
2033
|
+
}
|
|
2034
|
+
has(id) {
|
|
2035
|
+
return this.registry.has(id);
|
|
2036
|
+
}
|
|
2037
|
+
list() {
|
|
2038
|
+
return this.registry.list();
|
|
2039
|
+
}
|
|
2040
|
+
// ============================================================================
|
|
2041
|
+
// Active Source Management
|
|
2042
|
+
// ============================================================================
|
|
2043
|
+
activate(id) {
|
|
2044
|
+
if (this.destroyed) return;
|
|
2045
|
+
const source = this.registry.get(id);
|
|
2046
|
+
if (!source) {
|
|
2047
|
+
throw new Error(`Source "${id}" not registered`);
|
|
2048
|
+
}
|
|
2049
|
+
this._activeId = id;
|
|
2050
|
+
this.renderer.setActiveSource(source);
|
|
2051
|
+
const videoEl = source.kind === "video" ? source.element : void 0;
|
|
2052
|
+
this.scheduler.start(videoEl);
|
|
2053
|
+
this.emit("activated", id, source);
|
|
2054
|
+
}
|
|
2055
|
+
deactivate() {
|
|
2056
|
+
if (this.destroyed) return;
|
|
2057
|
+
this._activeId = null;
|
|
2058
|
+
this.renderer.setActiveSource(null);
|
|
2059
|
+
this.scheduler.stop();
|
|
2060
|
+
this.emit("activated", null, void 0);
|
|
2061
|
+
}
|
|
2062
|
+
get activeId() {
|
|
2063
|
+
return this._activeId;
|
|
2064
|
+
}
|
|
2065
|
+
// ============================================================================
|
|
2066
|
+
// Output Stream
|
|
2067
|
+
// ============================================================================
|
|
2068
|
+
get stream() {
|
|
2069
|
+
return this.outputStream;
|
|
2070
|
+
}
|
|
2071
|
+
// ============================================================================
|
|
2072
|
+
// Settings
|
|
2073
|
+
// ============================================================================
|
|
2074
|
+
resize(width, height, dpr) {
|
|
2075
|
+
if (this.destroyed) return;
|
|
2076
|
+
const effectiveDpr = Math.min(
|
|
2077
|
+
2,
|
|
2078
|
+
dpr ?? (typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1)
|
|
2079
|
+
);
|
|
2080
|
+
this.renderer.resize(width, height, effectiveDpr);
|
|
2081
|
+
this.recreateStream();
|
|
2082
|
+
}
|
|
2083
|
+
get size() {
|
|
2084
|
+
return this.renderer.size;
|
|
2085
|
+
}
|
|
2086
|
+
setFps(fps) {
|
|
2087
|
+
if (this.destroyed) return;
|
|
2088
|
+
const next = Math.max(1, fps);
|
|
2089
|
+
if (this._fps === next) return;
|
|
2090
|
+
this._fps = next;
|
|
2091
|
+
this.scheduler.setFps(next);
|
|
2092
|
+
this.recreateStream();
|
|
2093
|
+
}
|
|
2094
|
+
get fps() {
|
|
2095
|
+
return this._fps;
|
|
2096
|
+
}
|
|
2097
|
+
setSendFps(fps) {
|
|
2098
|
+
if (this.destroyed) return;
|
|
2099
|
+
const next = Math.max(1, fps);
|
|
2100
|
+
if (this._sendFps === next) return;
|
|
2101
|
+
this._sendFps = next;
|
|
2102
|
+
this.scheduler.setSendFps(next);
|
|
2103
|
+
}
|
|
2104
|
+
get sendFps() {
|
|
2105
|
+
return this._sendFps;
|
|
2106
|
+
}
|
|
2107
|
+
// ============================================================================
|
|
2108
|
+
// Audio
|
|
2109
|
+
// ============================================================================
|
|
2110
|
+
addAudioTrack(track) {
|
|
2111
|
+
if (this.destroyed) return;
|
|
2112
|
+
this.audioManager.addTrack(track);
|
|
2113
|
+
}
|
|
2114
|
+
removeAudioTrack(trackId) {
|
|
2115
|
+
if (this.destroyed) return;
|
|
2116
|
+
this.audioManager.removeTrack(trackId);
|
|
2117
|
+
}
|
|
2118
|
+
unlockAudio() {
|
|
2119
|
+
if (this.destroyed) return Promise.resolve(false);
|
|
2120
|
+
return this.audioManager.unlock();
|
|
2121
|
+
}
|
|
2122
|
+
// ============================================================================
|
|
2123
|
+
// Lifecycle
|
|
2124
|
+
// ============================================================================
|
|
2125
|
+
destroy() {
|
|
2126
|
+
if (this.destroyed) return;
|
|
2127
|
+
this.destroyed = true;
|
|
2128
|
+
this.scheduler.stop();
|
|
2129
|
+
this.visibilityHandler.stop();
|
|
2130
|
+
this.audioManager.destroy();
|
|
2131
|
+
this.renderer.destroy();
|
|
2132
|
+
this.registry.clear();
|
|
2133
|
+
if (this.outputStream) {
|
|
2134
|
+
try {
|
|
2135
|
+
this.outputStream.getVideoTracks().forEach((t) => {
|
|
2136
|
+
try {
|
|
2137
|
+
t.stop();
|
|
2138
|
+
} catch {
|
|
2139
|
+
}
|
|
2140
|
+
});
|
|
2141
|
+
} catch {
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
this.outputStream = null;
|
|
2145
|
+
this.clearListeners();
|
|
2146
|
+
}
|
|
2147
|
+
// ============================================================================
|
|
2148
|
+
// Private Helpers
|
|
2149
|
+
// ============================================================================
|
|
2150
|
+
createOutputStream() {
|
|
2151
|
+
const stream = this.renderer.captureCanvas.captureStream(this._fps);
|
|
2152
|
+
try {
|
|
2153
|
+
const vtrack = stream.getVideoTracks()[0];
|
|
2154
|
+
if (vtrack && vtrack.contentHint !== void 0) {
|
|
2155
|
+
vtrack.contentHint = "detail";
|
|
2156
|
+
}
|
|
2157
|
+
} catch {
|
|
2158
|
+
}
|
|
2159
|
+
return stream;
|
|
2160
|
+
}
|
|
2161
|
+
recreateStream() {
|
|
2162
|
+
const newStream = this.createOutputStream();
|
|
2163
|
+
const prev = this.outputStream;
|
|
2164
|
+
if (prev && prev !== newStream) {
|
|
2165
|
+
try {
|
|
2166
|
+
prev.getAudioTracks().forEach((t) => {
|
|
2167
|
+
try {
|
|
2168
|
+
newStream.addTrack(t);
|
|
2169
|
+
} catch {
|
|
2170
|
+
}
|
|
2171
|
+
});
|
|
2172
|
+
} catch {
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
this.outputStream = newStream;
|
|
2176
|
+
this.audioManager.setOutputStream(newStream);
|
|
2177
|
+
this.applyVideoTrackConstraints();
|
|
2178
|
+
if (prev && prev !== newStream) {
|
|
2179
|
+
try {
|
|
2180
|
+
prev.getVideoTracks().forEach((t) => {
|
|
2181
|
+
try {
|
|
2182
|
+
t.stop();
|
|
2183
|
+
} catch {
|
|
2184
|
+
}
|
|
2185
|
+
});
|
|
2186
|
+
} catch {
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
applyVideoTrackConstraints() {
|
|
2191
|
+
try {
|
|
2192
|
+
const track = this.outputStream?.getVideoTracks()[0];
|
|
2193
|
+
const canvas = this.renderer.captureCanvas;
|
|
2194
|
+
if (!track || !canvas) return;
|
|
2195
|
+
const constraints = {
|
|
2196
|
+
width: canvas.width,
|
|
2197
|
+
height: canvas.height,
|
|
2198
|
+
frameRate: Math.max(1, this._sendFps || this._fps)
|
|
2199
|
+
};
|
|
2200
|
+
try {
|
|
2201
|
+
if (track.contentHint !== void 0) {
|
|
2202
|
+
track.contentHint = "detail";
|
|
2203
|
+
}
|
|
2204
|
+
} catch {
|
|
2205
|
+
}
|
|
2206
|
+
track.applyConstraints(constraints).catch(() => {
|
|
2207
|
+
});
|
|
2208
|
+
} catch {
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
requestVideoTrackFrame() {
|
|
2212
|
+
const track = this.outputStream?.getVideoTracks()[0];
|
|
2213
|
+
if (track && typeof track.requestFrame === "function") {
|
|
2214
|
+
try {
|
|
2215
|
+
track.requestFrame();
|
|
2216
|
+
} catch {
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
};
|
|
2221
|
+
function createCompositor(options = {}) {
|
|
2222
|
+
return new Compositor(options);
|
|
2223
|
+
}
|
|
2224
|
+
|
|
1029
2225
|
// src/index.ts
|
|
1030
2226
|
var livepeerResponseHandler = (response) => ({
|
|
1031
2227
|
whepUrl: response.headers.get("livepeer-playback-url") ?? void 0
|
|
@@ -1052,6 +2248,7 @@ function createPlayer2(whepUrl, options) {
|
|
|
1052
2248
|
StreamNotFoundError,
|
|
1053
2249
|
UnauthorizedError,
|
|
1054
2250
|
createBroadcast,
|
|
2251
|
+
createCompositor,
|
|
1055
2252
|
createPlayer,
|
|
1056
2253
|
livepeerResponseHandler
|
|
1057
2254
|
});
|