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