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