@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.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 = 2e6;
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
- 1e4
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
- 1e4
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
  };