@daydreamlive/browser 0.1.1 → 0.3.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
  });
@@ -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;
@@ -193,7 +196,6 @@ var WHIPClient = class {
193
196
  await this.audioSender.replaceTrack(audioTrack);
194
197
  }
195
198
  this.setCodecPreferences();
196
- await this.applyBitrateConstraints();
197
199
  const offer = await this.pc.createOffer({
198
200
  offerToReceiveAudio: false,
199
201
  offerToReceiveVideo: false
@@ -206,7 +208,7 @@ var WHIPClient = class {
206
208
  this.abortController = new AbortController();
207
209
  const timeoutId = this.timers.setTimeout(
208
210
  () => this.abortController?.abort(),
209
- 1e4
211
+ this.connectionTimeout
210
212
  );
211
213
  try {
212
214
  const fetchUrl = this.getUrlWithCachedRedirect();
@@ -365,6 +367,9 @@ var WHIPClient = class {
365
367
  this.abortController = null;
366
368
  }
367
369
  if (this.pc) {
370
+ this.pc.oniceconnectionstatechange = null;
371
+ this.pc.onconnectionstatechange = null;
372
+ this.pc.ontrack = null;
368
373
  try {
369
374
  this.pc.getTransceivers().forEach((t) => {
370
375
  try {
@@ -407,7 +412,9 @@ var WHIPClient = class {
407
412
  }
408
413
  }
409
414
  isConnected() {
410
- return this.pc !== null && this.pc.connectionState === "connected";
415
+ if (!this.pc) return false;
416
+ const iceState = this.pc.iceConnectionState;
417
+ return iceState === "connected" || iceState === "completed";
411
418
  }
412
419
  getUrlWithCachedRedirect() {
413
420
  const originalUrl = new URL(this.url);
@@ -530,6 +537,16 @@ var Broadcast = class extends TypedEventEmitter {
530
537
  get stream() {
531
538
  return this.currentStream;
532
539
  }
540
+ get reconnectInfo() {
541
+ if (this.state !== "reconnecting") return null;
542
+ const baseDelay = this.reconnectConfig.baseDelayMs ?? 1e3;
543
+ const delay = baseDelay * Math.pow(2, this.reconnectAttempts - 1);
544
+ return {
545
+ attempt: this.reconnectAttempts,
546
+ maxAttempts: this.reconnectConfig.maxAttempts ?? 5,
547
+ delayMs: delay
548
+ };
549
+ }
533
550
  async connect() {
534
551
  try {
535
552
  const result = await this.whipClient.connect(this.currentStream);
@@ -551,6 +568,9 @@ var Broadcast = class extends TypedEventEmitter {
551
568
  await this.whipClient.disconnect();
552
569
  this.clearListeners();
553
570
  }
571
+ setMaxFramerate(fps) {
572
+ this.whipClient.setMaxFramerate(fps);
573
+ }
554
574
  async replaceStream(newStream) {
555
575
  if (!this.whipClient.isConnected()) {
556
576
  this.currentStream = newStream;
@@ -574,10 +594,10 @@ var Broadcast = class extends TypedEventEmitter {
574
594
  setupConnectionMonitoring() {
575
595
  const pc = this.whipClient.getPeerConnection();
576
596
  if (!pc) return;
577
- pc.onconnectionstatechange = () => {
597
+ pc.oniceconnectionstatechange = () => {
578
598
  if (this.state === "ended") return;
579
- const connState = pc.connectionState;
580
- if (connState === "connected") {
599
+ const iceState = pc.iceConnectionState;
600
+ if (iceState === "connected" || iceState === "completed") {
581
601
  this.clearGraceTimeout();
582
602
  if (this.state === "reconnecting") {
583
603
  this.stateMachine.transition("live");
@@ -585,19 +605,19 @@ var Broadcast = class extends TypedEventEmitter {
585
605
  }
586
606
  return;
587
607
  }
588
- if (connState === "disconnected") {
608
+ if (iceState === "disconnected") {
589
609
  this.clearGraceTimeout();
590
610
  this.whipClient.restartIce();
591
611
  this.disconnectedGraceTimeout = setTimeout(() => {
592
612
  if (this.state === "ended") return;
593
- const currentState = pc.connectionState;
613
+ const currentState = pc.iceConnectionState;
594
614
  if (currentState === "disconnected") {
595
615
  this.scheduleReconnect();
596
616
  }
597
617
  }, 2e3);
598
618
  return;
599
619
  }
600
- if (connState === "failed" || connState === "closed") {
620
+ if (iceState === "failed" || iceState === "closed") {
601
621
  this.clearGraceTimeout();
602
622
  this.scheduleReconnect();
603
623
  }
@@ -635,6 +655,11 @@ var Broadcast = class extends TypedEventEmitter {
635
655
  const baseDelay = this.reconnectConfig.baseDelayMs ?? 1e3;
636
656
  const delay = baseDelay * Math.pow(2, this.reconnectAttempts);
637
657
  this.reconnectAttempts++;
658
+ this.emit("reconnect", {
659
+ attempt: this.reconnectAttempts,
660
+ maxAttempts: this.reconnectConfig.maxAttempts ?? 5,
661
+ delayMs: delay
662
+ });
638
663
  this.reconnectTimeout = setTimeout(async () => {
639
664
  if (this.state === "ended") return;
640
665
  try {
@@ -658,6 +683,9 @@ function createBroadcast(options) {
658
683
  stream,
659
684
  reconnect,
660
685
  video,
686
+ audio,
687
+ iceServers,
688
+ connectionTimeout,
661
689
  onStats,
662
690
  statsIntervalMs,
663
691
  onResponse
@@ -667,8 +695,11 @@ function createBroadcast(options) {
667
695
  stream,
668
696
  reconnect,
669
697
  whipConfig: {
698
+ iceServers,
670
699
  videoBitrate: video?.bitrate,
700
+ audioBitrate: audio?.bitrate,
671
701
  maxFramerate: video?.maxFramerate,
702
+ connectionTimeout,
672
703
  onStats,
673
704
  statsIntervalMs,
674
705
  onResponse
@@ -677,6 +708,7 @@ function createBroadcast(options) {
677
708
  }
678
709
 
679
710
  // src/internal/WHEPClient.ts
711
+ var DEFAULT_CONNECTION_TIMEOUT2 = 1e4;
680
712
  var WHEPClient = class {
681
713
  constructor(config) {
682
714
  this.pc = null;
@@ -687,6 +719,8 @@ var WHEPClient = class {
687
719
  this.iceGatheringTimer = null;
688
720
  this.url = config.url;
689
721
  this.iceServers = config.iceServers ?? DEFAULT_ICE_SERVERS;
722
+ this.connectionTimeout = config.connectionTimeout ?? DEFAULT_CONNECTION_TIMEOUT2;
723
+ this.skipIceGathering = config.skipIceGathering ?? true;
690
724
  this.onStats = config.onStats;
691
725
  this.statsIntervalMs = config.statsIntervalMs ?? 5e3;
692
726
  this.pcFactory = config.peerConnectionFactory ?? defaultPeerConnectionFactory;
@@ -712,11 +746,13 @@ var WHEPClient = class {
712
746
  };
713
747
  const offer = await this.pc.createOffer();
714
748
  await this.pc.setLocalDescription(offer);
715
- await this.waitForIceGathering();
749
+ if (!this.skipIceGathering) {
750
+ await this.waitForIceGathering();
751
+ }
716
752
  this.abortController = new AbortController();
717
753
  const timeoutId = this.timers.setTimeout(
718
754
  () => this.abortController?.abort(),
719
- 1e4
755
+ this.connectionTimeout
720
756
  );
721
757
  try {
722
758
  const response = await this.fetch(this.url, {
@@ -811,6 +847,9 @@ var WHEPClient = class {
811
847
  this.abortController = null;
812
848
  }
813
849
  if (this.pc) {
850
+ this.pc.oniceconnectionstatechange = null;
851
+ this.pc.onconnectionstatechange = null;
852
+ this.pc.ontrack = null;
814
853
  try {
815
854
  this.pc.getTransceivers().forEach((t) => {
816
855
  try {
@@ -892,6 +931,19 @@ var Player = class extends TypedEventEmitter {
892
931
  get stream() {
893
932
  return this._stream;
894
933
  }
934
+ get reconnectInfo() {
935
+ if (this.state !== "buffering") return null;
936
+ const baseDelay = this.reconnectConfig.baseDelayMs ?? 200;
937
+ const delay = this.calculateReconnectDelay(
938
+ this.reconnectAttempts - 1,
939
+ baseDelay
940
+ );
941
+ return {
942
+ attempt: this.reconnectAttempts,
943
+ maxAttempts: this.reconnectConfig.maxAttempts ?? 30,
944
+ delayMs: delay
945
+ };
946
+ }
895
947
  async connect() {
896
948
  try {
897
949
  this._stream = await this.whepClient.connect();
@@ -975,19 +1027,24 @@ var Player = class extends TypedEventEmitter {
975
1027
  this.stateMachine.transition("ended");
976
1028
  return;
977
1029
  }
978
- const maxAttempts = this.reconnectConfig.maxAttempts ?? 10;
1030
+ const maxAttempts = this.reconnectConfig.maxAttempts ?? 30;
979
1031
  if (this.reconnectAttempts >= maxAttempts) {
980
1032
  this.stateMachine.transition("ended");
981
1033
  return;
982
1034
  }
983
1035
  this.clearReconnectTimeout();
984
1036
  this.stateMachine.transition("buffering");
985
- const baseDelay = this.reconnectConfig.baseDelayMs ?? 300;
1037
+ const baseDelay = this.reconnectConfig.baseDelayMs ?? 200;
986
1038
  const delay = this.calculateReconnectDelay(
987
1039
  this.reconnectAttempts,
988
1040
  baseDelay
989
1041
  );
990
1042
  this.reconnectAttempts++;
1043
+ this.emit("reconnect", {
1044
+ attempt: this.reconnectAttempts,
1045
+ maxAttempts: this.reconnectConfig.maxAttempts ?? 30,
1046
+ delayMs: delay
1047
+ });
991
1048
  this.reconnectTimeout = setTimeout(async () => {
992
1049
  if (this.state === "ended") return;
993
1050
  try {
@@ -1020,12 +1077,1052 @@ function createPlayer(whepUrl, options) {
1020
1077
  whepUrl,
1021
1078
  reconnect: options?.reconnect,
1022
1079
  whepConfig: {
1080
+ iceServers: options?.iceServers,
1081
+ connectionTimeout: options?.connectionTimeout,
1082
+ skipIceGathering: options?.skipIceGathering,
1023
1083
  onStats: options?.onStats,
1024
1084
  statsIntervalMs: options?.statsIntervalMs
1025
1085
  }
1026
1086
  });
1027
1087
  }
1028
1088
 
1089
+ // src/internal/compositor/Registry.ts
1090
+ function createRegistry(events) {
1091
+ const sources = /* @__PURE__ */ new Map();
1092
+ return {
1093
+ register(id, source) {
1094
+ if (!id) throw new Error("Source id is required");
1095
+ if (!source) throw new Error("Source is required");
1096
+ sources.set(id, {
1097
+ id,
1098
+ source,
1099
+ registeredAt: Date.now()
1100
+ });
1101
+ events?.onRegister?.(id, source);
1102
+ },
1103
+ unregister(id) {
1104
+ const entry = sources.get(id);
1105
+ if (!entry) return void 0;
1106
+ sources.delete(id);
1107
+ events?.onUnregister?.(id);
1108
+ return entry.source;
1109
+ },
1110
+ get(id) {
1111
+ return sources.get(id)?.source;
1112
+ },
1113
+ has(id) {
1114
+ return sources.has(id);
1115
+ },
1116
+ list() {
1117
+ return Array.from(sources.values()).map((entry) => ({
1118
+ id: entry.id,
1119
+ source: entry.source
1120
+ }));
1121
+ },
1122
+ clear() {
1123
+ const ids = Array.from(sources.keys());
1124
+ sources.clear();
1125
+ ids.forEach((id) => events?.onUnregister?.(id));
1126
+ }
1127
+ };
1128
+ }
1129
+
1130
+ // src/internal/compositor/Renderer.ts
1131
+ function createRenderer(options) {
1132
+ let size = {
1133
+ width: options.width,
1134
+ height: options.height,
1135
+ dpr: Math.min(2, options.dpr)
1136
+ };
1137
+ let keepalive = options.keepalive;
1138
+ let captureCanvas = null;
1139
+ let captureCtx = null;
1140
+ let offscreen = null;
1141
+ let offscreenCtx = null;
1142
+ let currentSource = null;
1143
+ let frameIndex = 0;
1144
+ let rectCache = /* @__PURE__ */ new WeakMap();
1145
+ function initCanvas() {
1146
+ const canvas = document.createElement("canvas");
1147
+ canvas.style.display = "none";
1148
+ const pxW = Math.round(size.width * size.dpr);
1149
+ const pxH = Math.round(size.height * size.dpr);
1150
+ const outW = Math.round(size.width);
1151
+ const outH = Math.round(size.height);
1152
+ canvas.width = outW;
1153
+ canvas.height = outH;
1154
+ const ctx = canvas.getContext("2d", {
1155
+ alpha: false,
1156
+ desynchronized: true
1157
+ });
1158
+ if (!ctx) throw new Error("2D context not available");
1159
+ captureCanvas = canvas;
1160
+ captureCtx = ctx;
1161
+ try {
1162
+ const off = new OffscreenCanvas(pxW, pxH);
1163
+ offscreen = off;
1164
+ const offCtx = off.getContext("2d", { alpha: false });
1165
+ if (!offCtx) throw new Error("2D context not available for Offscreen");
1166
+ offCtx.imageSmoothingEnabled = true;
1167
+ offscreenCtx = offCtx;
1168
+ } catch {
1169
+ const off = document.createElement("canvas");
1170
+ off.width = pxW;
1171
+ off.height = pxH;
1172
+ const offCtx = off.getContext("2d", { alpha: false });
1173
+ if (!offCtx)
1174
+ throw new Error("2D context not available for Offscreen fallback");
1175
+ offCtx.imageSmoothingEnabled = true;
1176
+ offscreen = off;
1177
+ offscreenCtx = offCtx;
1178
+ }
1179
+ offscreenCtx.fillStyle = "#111";
1180
+ offscreenCtx.fillRect(0, 0, pxW, pxH);
1181
+ captureCtx.drawImage(
1182
+ offscreen,
1183
+ 0,
1184
+ 0,
1185
+ pxW,
1186
+ pxH,
1187
+ 0,
1188
+ 0,
1189
+ outW,
1190
+ outH
1191
+ );
1192
+ }
1193
+ function isSourceReady(source) {
1194
+ if (source.kind === "video") {
1195
+ const v = source.element;
1196
+ return typeof v.readyState === "number" && v.readyState >= 2 && (v.videoWidth || 0) > 0 && (v.videoHeight || 0) > 0;
1197
+ }
1198
+ const c = source.element;
1199
+ return (c.width || 0) > 0 && (c.height || 0) > 0;
1200
+ }
1201
+ function getDrawRect(el, fit) {
1202
+ const canvas = offscreenCtx?.canvas;
1203
+ if (!canvas) return null;
1204
+ const canvasW = canvas.width;
1205
+ const canvasH = canvas.height;
1206
+ const sourceW = el.videoWidth ?? el.width;
1207
+ const sourceH = el.videoHeight ?? el.height;
1208
+ if (!sourceW || !sourceH) return null;
1209
+ const cached = rectCache.get(el);
1210
+ if (cached && cached.canvasW === canvasW && cached.canvasH === canvasH && cached.sourceW === sourceW && cached.sourceH === sourceH && cached.fit === fit) {
1211
+ return { dx: cached.dx, dy: cached.dy, dw: cached.dw, dh: cached.dh };
1212
+ }
1213
+ const scale = fit === "cover" ? Math.max(canvasW / sourceW, canvasH / sourceH) : Math.min(canvasW / sourceW, canvasH / sourceH);
1214
+ const dw = Math.floor(sourceW * scale);
1215
+ const dh = Math.floor(sourceH * scale);
1216
+ const dx = Math.floor((canvasW - dw) / 2);
1217
+ const dy = Math.floor((canvasH - dh) / 2);
1218
+ rectCache.set(el, {
1219
+ canvasW,
1220
+ canvasH,
1221
+ sourceW,
1222
+ sourceH,
1223
+ dx,
1224
+ dy,
1225
+ dw,
1226
+ dh,
1227
+ fit
1228
+ });
1229
+ return { dx, dy, dw, dh };
1230
+ }
1231
+ function blitSource(source) {
1232
+ if (!offscreenCtx) return;
1233
+ const ctx = offscreenCtx;
1234
+ const el = source.element;
1235
+ const rect = getDrawRect(el, source.fit ?? "contain");
1236
+ if (!rect) return;
1237
+ ctx.drawImage(
1238
+ el,
1239
+ rect.dx,
1240
+ rect.dy,
1241
+ rect.dw,
1242
+ rect.dh
1243
+ );
1244
+ }
1245
+ initCanvas();
1246
+ return {
1247
+ get captureCanvas() {
1248
+ return captureCanvas;
1249
+ },
1250
+ get offscreenCtx() {
1251
+ return offscreenCtx;
1252
+ },
1253
+ get size() {
1254
+ return { ...size };
1255
+ },
1256
+ isSourceReady,
1257
+ setActiveSource(source) {
1258
+ currentSource = source;
1259
+ },
1260
+ renderFrame(_timestamp) {
1261
+ const off = offscreenCtx;
1262
+ const cap = captureCtx;
1263
+ const capCanvas = captureCanvas;
1264
+ if (!off || !cap || !capCanvas) return;
1265
+ off.globalCompositeOperation = "source-over";
1266
+ if (currentSource && isSourceReady(currentSource)) {
1267
+ off.fillStyle = "#000";
1268
+ off.fillRect(0, 0, off.canvas.width, off.canvas.height);
1269
+ blitSource(currentSource);
1270
+ }
1271
+ if (keepalive) {
1272
+ const w = off.canvas.width;
1273
+ const h = off.canvas.height;
1274
+ const prevAlpha = off.globalAlpha;
1275
+ const prevFill = off.fillStyle;
1276
+ try {
1277
+ off.globalAlpha = 0.08;
1278
+ off.fillStyle = frameIndex % 2 ? "#101010" : "#0e0e0e";
1279
+ off.fillRect(w - 16, h - 16, 16, 16);
1280
+ } finally {
1281
+ off.globalAlpha = prevAlpha;
1282
+ off.fillStyle = prevFill;
1283
+ }
1284
+ }
1285
+ frameIndex++;
1286
+ cap.drawImage(
1287
+ off.canvas,
1288
+ 0,
1289
+ 0,
1290
+ off.canvas.width,
1291
+ off.canvas.height,
1292
+ 0,
1293
+ 0,
1294
+ capCanvas.width,
1295
+ capCanvas.height
1296
+ );
1297
+ },
1298
+ resize(width, height, dpr) {
1299
+ const nextDpr = Math.min(2, dpr);
1300
+ if (size.width === width && size.height === height && size.dpr === nextDpr) {
1301
+ return;
1302
+ }
1303
+ size = { width, height, dpr: nextDpr };
1304
+ const pxW = Math.round(width * nextDpr);
1305
+ const pxH = Math.round(height * nextDpr);
1306
+ const outW = Math.round(width);
1307
+ const outH = Math.round(height);
1308
+ if (captureCanvas) {
1309
+ captureCanvas.width = outW;
1310
+ captureCanvas.height = outH;
1311
+ }
1312
+ if (offscreen instanceof HTMLCanvasElement) {
1313
+ offscreen.width = pxW;
1314
+ offscreen.height = pxH;
1315
+ } else if (offscreen instanceof OffscreenCanvas) {
1316
+ offscreen.width = pxW;
1317
+ offscreen.height = pxH;
1318
+ }
1319
+ rectCache = /* @__PURE__ */ new WeakMap();
1320
+ },
1321
+ setKeepalive(enabled) {
1322
+ keepalive = enabled;
1323
+ },
1324
+ destroy() {
1325
+ currentSource = null;
1326
+ captureCanvas = null;
1327
+ captureCtx = null;
1328
+ offscreen = null;
1329
+ offscreenCtx = null;
1330
+ }
1331
+ };
1332
+ }
1333
+
1334
+ // src/internal/compositor/Scheduler.ts
1335
+ function createScheduler(options) {
1336
+ let fps = Math.max(1, options.fps);
1337
+ let sendFps = Math.max(1, options.sendFps);
1338
+ const onFrame = options.onFrame;
1339
+ const onSendFpsChange = options.onSendFpsChange;
1340
+ let isRunning = false;
1341
+ let lastFrameAt = 0;
1342
+ let rafId = null;
1343
+ let rafFallbackActive = false;
1344
+ let videoFrameRequestId = null;
1345
+ let videoFrameSource = null;
1346
+ function getTimestamp() {
1347
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
1348
+ }
1349
+ function shouldRenderFrame() {
1350
+ const now = getTimestamp();
1351
+ const minIntervalMs = 1e3 / Math.max(1, sendFps);
1352
+ if (lastFrameAt !== 0 && now - lastFrameAt < minIntervalMs) {
1353
+ return false;
1354
+ }
1355
+ return true;
1356
+ }
1357
+ function renderIfNeeded() {
1358
+ if (!shouldRenderFrame()) return;
1359
+ const timestamp = getTimestamp();
1360
+ onFrame(timestamp);
1361
+ lastFrameAt = timestamp;
1362
+ }
1363
+ function scheduleWithRaf(isFallback) {
1364
+ if (isFallback) {
1365
+ if (rafFallbackActive) return;
1366
+ rafFallbackActive = true;
1367
+ }
1368
+ const loop = () => {
1369
+ renderIfNeeded();
1370
+ rafId = requestAnimationFrame(loop);
1371
+ };
1372
+ rafId = requestAnimationFrame(loop);
1373
+ }
1374
+ function scheduleWithVideoFrame(videoEl) {
1375
+ if (typeof videoEl.requestVideoFrameCallback !== "function") {
1376
+ scheduleWithRaf(false);
1377
+ return;
1378
+ }
1379
+ videoFrameSource = videoEl;
1380
+ const cb = () => {
1381
+ renderIfNeeded();
1382
+ if (videoFrameSource === videoEl) {
1383
+ try {
1384
+ videoFrameRequestId = videoEl.requestVideoFrameCallback(cb);
1385
+ } catch {
1386
+ }
1387
+ }
1388
+ };
1389
+ try {
1390
+ videoFrameRequestId = videoEl.requestVideoFrameCallback(cb);
1391
+ } catch {
1392
+ }
1393
+ scheduleWithRaf(true);
1394
+ }
1395
+ function cancelSchedulers() {
1396
+ if (rafId != null) {
1397
+ cancelAnimationFrame(rafId);
1398
+ rafId = null;
1399
+ }
1400
+ rafFallbackActive = false;
1401
+ if (videoFrameRequestId && videoFrameSource) {
1402
+ try {
1403
+ if (typeof videoFrameSource.cancelVideoFrameCallback === "function") {
1404
+ videoFrameSource.cancelVideoFrameCallback(videoFrameRequestId);
1405
+ }
1406
+ } catch {
1407
+ }
1408
+ }
1409
+ videoFrameRequestId = null;
1410
+ videoFrameSource = null;
1411
+ }
1412
+ return {
1413
+ get isRunning() {
1414
+ return isRunning;
1415
+ },
1416
+ get fps() {
1417
+ return fps;
1418
+ },
1419
+ get sendFps() {
1420
+ return sendFps;
1421
+ },
1422
+ start(videoElement) {
1423
+ if (isRunning) {
1424
+ cancelSchedulers();
1425
+ }
1426
+ isRunning = true;
1427
+ lastFrameAt = 0;
1428
+ if (videoElement && typeof videoElement.requestVideoFrameCallback === "function") {
1429
+ scheduleWithVideoFrame(videoElement);
1430
+ } else {
1431
+ scheduleWithRaf(false);
1432
+ }
1433
+ },
1434
+ stop() {
1435
+ isRunning = false;
1436
+ cancelSchedulers();
1437
+ },
1438
+ setFps(newFps) {
1439
+ fps = Math.max(1, newFps);
1440
+ },
1441
+ setSendFps(newSendFps) {
1442
+ const next = Math.max(1, newSendFps);
1443
+ if (sendFps === next) return;
1444
+ sendFps = next;
1445
+ onSendFpsChange?.(sendFps);
1446
+ }
1447
+ };
1448
+ }
1449
+
1450
+ // src/internal/compositor/AudioManager.ts
1451
+ function createAudioManager(options) {
1452
+ let outputStream = null;
1453
+ let audioCtx = null;
1454
+ let silentOsc = null;
1455
+ let silentGain = null;
1456
+ let audioDst = null;
1457
+ let silentAudioTrack = null;
1458
+ const externalAudioTrackIds = /* @__PURE__ */ new Set();
1459
+ const externalAudioEndHandlers = /* @__PURE__ */ new Map();
1460
+ let audioUnlockHandler = null;
1461
+ let audioUnlockAttached = false;
1462
+ let audioStateListenerAttached = false;
1463
+ function ensureSilentAudioTrack() {
1464
+ if (options.disableSilentAudio) return;
1465
+ if (!outputStream) return;
1466
+ const alreadyHasAudio = outputStream.getAudioTracks().length > 0;
1467
+ if (alreadyHasAudio) return;
1468
+ if (silentAudioTrack && silentAudioTrack.readyState === "live") {
1469
+ try {
1470
+ outputStream.addTrack(silentAudioTrack);
1471
+ } catch {
1472
+ }
1473
+ return;
1474
+ }
1475
+ if (!audioCtx) {
1476
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
1477
+ if (!AudioContextClass) return;
1478
+ audioCtx = new AudioContextClass({
1479
+ sampleRate: 48e3
1480
+ });
1481
+ try {
1482
+ audioCtx.resume().catch(() => {
1483
+ });
1484
+ } catch {
1485
+ }
1486
+ attachAudioCtxStateListener();
1487
+ }
1488
+ const ac = audioCtx;
1489
+ if (!ac) return;
1490
+ silentOsc = ac.createOscillator();
1491
+ silentGain = ac.createGain();
1492
+ audioDst = ac.createMediaStreamDestination();
1493
+ silentGain.gain.setValueAtTime(1e-4, ac.currentTime);
1494
+ silentOsc.frequency.setValueAtTime(440, ac.currentTime);
1495
+ silentOsc.type = "sine";
1496
+ silentOsc.connect(silentGain);
1497
+ silentGain.connect(audioDst);
1498
+ silentOsc.start();
1499
+ const track = audioDst.stream.getAudioTracks()[0];
1500
+ if (track) {
1501
+ silentAudioTrack = track;
1502
+ try {
1503
+ outputStream.addTrack(track);
1504
+ } catch {
1505
+ }
1506
+ }
1507
+ }
1508
+ function removeSilentAudioTrack() {
1509
+ try {
1510
+ if (outputStream && silentAudioTrack) {
1511
+ try {
1512
+ outputStream.removeTrack(silentAudioTrack);
1513
+ } catch {
1514
+ }
1515
+ }
1516
+ if (silentOsc) {
1517
+ try {
1518
+ silentOsc.stop();
1519
+ } catch {
1520
+ }
1521
+ try {
1522
+ silentOsc.disconnect();
1523
+ } catch {
1524
+ }
1525
+ }
1526
+ if (silentGain) {
1527
+ try {
1528
+ silentGain.disconnect();
1529
+ } catch {
1530
+ }
1531
+ }
1532
+ silentOsc = null;
1533
+ silentGain = null;
1534
+ audioDst = null;
1535
+ silentAudioTrack = null;
1536
+ if (audioCtx) {
1537
+ try {
1538
+ audioCtx.close();
1539
+ } catch {
1540
+ }
1541
+ }
1542
+ audioCtx = null;
1543
+ } catch {
1544
+ }
1545
+ }
1546
+ function rebuildSilentAudioTrack() {
1547
+ if (options.disableSilentAudio) return;
1548
+ if (!outputStream) return;
1549
+ if (externalAudioTrackIds.size > 0) return;
1550
+ if (silentAudioTrack) {
1551
+ try {
1552
+ outputStream.removeTrack(silentAudioTrack);
1553
+ } catch {
1554
+ }
1555
+ }
1556
+ if (silentOsc) {
1557
+ try {
1558
+ silentOsc.stop();
1559
+ } catch {
1560
+ }
1561
+ try {
1562
+ silentOsc.disconnect();
1563
+ } catch {
1564
+ }
1565
+ }
1566
+ if (silentGain) {
1567
+ try {
1568
+ silentGain.disconnect();
1569
+ } catch {
1570
+ }
1571
+ }
1572
+ silentOsc = null;
1573
+ silentGain = null;
1574
+ audioDst = null;
1575
+ silentAudioTrack = null;
1576
+ const ac = audioCtx;
1577
+ if (!ac || ac.state !== "running") return;
1578
+ attachAudioCtxStateListener();
1579
+ silentOsc = ac.createOscillator();
1580
+ silentGain = ac.createGain();
1581
+ audioDst = ac.createMediaStreamDestination();
1582
+ silentGain.gain.setValueAtTime(1e-4, ac.currentTime);
1583
+ silentOsc.frequency.setValueAtTime(440, ac.currentTime);
1584
+ silentOsc.type = "sine";
1585
+ silentOsc.connect(silentGain);
1586
+ silentGain.connect(audioDst);
1587
+ silentOsc.start();
1588
+ const track = audioDst.stream.getAudioTracks()[0];
1589
+ if (track) {
1590
+ silentAudioTrack = track;
1591
+ try {
1592
+ outputStream.addTrack(track);
1593
+ } catch {
1594
+ }
1595
+ }
1596
+ }
1597
+ function attachAudioCtxStateListener() {
1598
+ const ac = audioCtx;
1599
+ if (!ac || audioStateListenerAttached) return;
1600
+ const onStateChange = () => {
1601
+ try {
1602
+ if (audioCtx && audioCtx.state === "running") {
1603
+ rebuildSilentAudioTrack();
1604
+ cleanupAudioAutoUnlock();
1605
+ }
1606
+ } catch {
1607
+ }
1608
+ };
1609
+ try {
1610
+ ac.onstatechange = onStateChange;
1611
+ audioStateListenerAttached = true;
1612
+ } catch {
1613
+ }
1614
+ }
1615
+ function setupAudioAutoUnlock() {
1616
+ if (!options.autoUnlock) return;
1617
+ if (typeof document === "undefined") return;
1618
+ if (audioUnlockAttached) return;
1619
+ const handler = () => {
1620
+ unlock();
1621
+ };
1622
+ audioUnlockHandler = handler;
1623
+ options.unlockEvents.forEach((evt) => {
1624
+ try {
1625
+ document.addEventListener(evt, handler, { capture: true });
1626
+ } catch {
1627
+ }
1628
+ });
1629
+ audioUnlockAttached = true;
1630
+ }
1631
+ function cleanupAudioAutoUnlock() {
1632
+ if (!audioUnlockAttached) return;
1633
+ if (typeof document !== "undefined" && audioUnlockHandler) {
1634
+ options.unlockEvents.forEach((evt) => {
1635
+ try {
1636
+ document.removeEventListener(evt, audioUnlockHandler, {
1637
+ capture: true
1638
+ });
1639
+ } catch {
1640
+ }
1641
+ });
1642
+ }
1643
+ audioUnlockAttached = false;
1644
+ audioUnlockHandler = null;
1645
+ }
1646
+ async function unlock() {
1647
+ try {
1648
+ if (typeof window === "undefined") return false;
1649
+ if (!audioCtx || audioCtx.state === "closed") {
1650
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
1651
+ if (!AudioContextClass) return false;
1652
+ audioCtx = new AudioContextClass({
1653
+ sampleRate: 48e3
1654
+ });
1655
+ }
1656
+ const ac = audioCtx;
1657
+ if (!ac) return false;
1658
+ try {
1659
+ await ac.resume();
1660
+ } catch {
1661
+ }
1662
+ attachAudioCtxStateListener();
1663
+ if (ac.state === "running") {
1664
+ rebuildSilentAudioTrack();
1665
+ cleanupAudioAutoUnlock();
1666
+ return true;
1667
+ }
1668
+ return false;
1669
+ } catch {
1670
+ return false;
1671
+ }
1672
+ }
1673
+ setupAudioAutoUnlock();
1674
+ return {
1675
+ setOutputStream(stream) {
1676
+ outputStream = stream;
1677
+ ensureSilentAudioTrack();
1678
+ },
1679
+ addTrack(track) {
1680
+ if (!outputStream) return;
1681
+ try {
1682
+ if (silentAudioTrack) {
1683
+ try {
1684
+ outputStream.removeTrack(silentAudioTrack);
1685
+ } catch {
1686
+ }
1687
+ }
1688
+ const exists = outputStream.getAudioTracks().some((t) => t.id === track.id);
1689
+ if (!exists) {
1690
+ outputStream.addTrack(track);
1691
+ }
1692
+ externalAudioTrackIds.add(track.id);
1693
+ const onEnded = () => {
1694
+ try {
1695
+ if (!outputStream) return;
1696
+ outputStream.getAudioTracks().forEach((t) => {
1697
+ if (t.id === track.id) {
1698
+ try {
1699
+ outputStream.removeTrack(t);
1700
+ } catch {
1701
+ }
1702
+ }
1703
+ });
1704
+ externalAudioTrackIds.delete(track.id);
1705
+ externalAudioEndHandlers.delete(track.id);
1706
+ if (outputStream.getAudioTracks().length === 0) {
1707
+ ensureSilentAudioTrack();
1708
+ }
1709
+ } catch {
1710
+ }
1711
+ try {
1712
+ track.removeEventListener("ended", onEnded);
1713
+ } catch {
1714
+ }
1715
+ };
1716
+ track.addEventListener("ended", onEnded);
1717
+ externalAudioEndHandlers.set(track.id, onEnded);
1718
+ } catch {
1719
+ }
1720
+ },
1721
+ removeTrack(trackId) {
1722
+ if (!outputStream) return;
1723
+ outputStream.getAudioTracks().forEach((t) => {
1724
+ if (t.id === trackId) {
1725
+ outputStream.removeTrack(t);
1726
+ }
1727
+ });
1728
+ externalAudioTrackIds.delete(trackId);
1729
+ const handler = externalAudioEndHandlers.get(trackId);
1730
+ const tracks = outputStream.getAudioTracks();
1731
+ const tr = tracks.find((t) => t.id === trackId);
1732
+ if (tr && handler) {
1733
+ try {
1734
+ tr.removeEventListener("ended", handler);
1735
+ } catch {
1736
+ }
1737
+ }
1738
+ externalAudioEndHandlers.delete(trackId);
1739
+ if (outputStream.getAudioTracks().length === 0) {
1740
+ ensureSilentAudioTrack();
1741
+ }
1742
+ },
1743
+ unlock,
1744
+ destroy() {
1745
+ cleanupAudioAutoUnlock();
1746
+ try {
1747
+ if (audioCtx && audioCtx.onstatechange) {
1748
+ audioCtx.onstatechange = null;
1749
+ }
1750
+ } catch {
1751
+ }
1752
+ audioStateListenerAttached = false;
1753
+ externalAudioEndHandlers.forEach((handler, id) => {
1754
+ try {
1755
+ const tr = outputStream?.getAudioTracks().find((t) => t.id === id);
1756
+ if (tr) tr.removeEventListener("ended", handler);
1757
+ } catch {
1758
+ }
1759
+ });
1760
+ externalAudioEndHandlers.clear();
1761
+ externalAudioTrackIds.clear();
1762
+ removeSilentAudioTrack();
1763
+ outputStream = null;
1764
+ }
1765
+ };
1766
+ }
1767
+
1768
+ // src/internal/compositor/VisibilityHandler.ts
1769
+ function createVisibilityHandler(options) {
1770
+ let isHidden = false;
1771
+ let backgroundIntervalId = null;
1772
+ let visibilityListener = null;
1773
+ let started = false;
1774
+ function onVisibilityChange() {
1775
+ if (typeof document === "undefined") return;
1776
+ const hidden = document.visibilityState === "hidden";
1777
+ if (hidden && !isHidden) {
1778
+ isHidden = true;
1779
+ options.onHidden();
1780
+ if (backgroundIntervalId == null) {
1781
+ backgroundIntervalId = setInterval(() => {
1782
+ options.backgroundRenderFn();
1783
+ }, 1e3);
1784
+ }
1785
+ } else if (!hidden && isHidden) {
1786
+ isHidden = false;
1787
+ if (backgroundIntervalId != null) {
1788
+ clearInterval(backgroundIntervalId);
1789
+ backgroundIntervalId = null;
1790
+ }
1791
+ options.onVisible();
1792
+ }
1793
+ }
1794
+ return {
1795
+ get isHidden() {
1796
+ return isHidden;
1797
+ },
1798
+ start() {
1799
+ if (started) return;
1800
+ if (typeof document === "undefined") return;
1801
+ started = true;
1802
+ visibilityListener = onVisibilityChange;
1803
+ document.addEventListener("visibilitychange", visibilityListener);
1804
+ onVisibilityChange();
1805
+ },
1806
+ stop() {
1807
+ if (!started) return;
1808
+ started = false;
1809
+ if (typeof document !== "undefined" && visibilityListener) {
1810
+ try {
1811
+ document.removeEventListener("visibilitychange", visibilityListener);
1812
+ } catch {
1813
+ }
1814
+ visibilityListener = null;
1815
+ }
1816
+ if (backgroundIntervalId != null) {
1817
+ clearInterval(backgroundIntervalId);
1818
+ backgroundIntervalId = null;
1819
+ }
1820
+ isHidden = false;
1821
+ }
1822
+ };
1823
+ }
1824
+
1825
+ // src/Compositor.ts
1826
+ var CompositorEventEmitter = class {
1827
+ constructor() {
1828
+ this.listeners = /* @__PURE__ */ new Map();
1829
+ }
1830
+ on(event, handler) {
1831
+ if (!this.listeners.has(event)) {
1832
+ this.listeners.set(event, /* @__PURE__ */ new Set());
1833
+ }
1834
+ this.listeners.get(event).add(handler);
1835
+ return () => this.off(event, handler);
1836
+ }
1837
+ off(event, handler) {
1838
+ this.listeners.get(event)?.delete(handler);
1839
+ }
1840
+ emit(event, ...args) {
1841
+ this.listeners.get(event)?.forEach((handler) => {
1842
+ handler(...args);
1843
+ });
1844
+ }
1845
+ clearListeners() {
1846
+ this.listeners.clear();
1847
+ }
1848
+ };
1849
+ var Compositor = class extends CompositorEventEmitter {
1850
+ constructor(options = {}) {
1851
+ super();
1852
+ this._activeId = null;
1853
+ this.lastVisibleSendFps = null;
1854
+ this.outputStream = null;
1855
+ this.destroyed = false;
1856
+ const width = options.width ?? 512;
1857
+ const height = options.height ?? 512;
1858
+ this._fps = Math.max(1, options.fps ?? 30);
1859
+ this._sendFps = Math.max(1, options.sendFps ?? this._fps);
1860
+ const dpr = Math.min(
1861
+ 2,
1862
+ options.dpr ?? (typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1)
1863
+ );
1864
+ const keepalive = options.keepalive ?? true;
1865
+ this.registry = createRegistry({
1866
+ onRegister: (id, source) => this.emit("registered", id, source),
1867
+ onUnregister: (id) => this.emit("unregistered", id)
1868
+ });
1869
+ this.renderer = createRenderer({
1870
+ width,
1871
+ height,
1872
+ dpr,
1873
+ keepalive
1874
+ });
1875
+ this.scheduler = createScheduler({
1876
+ fps: this._fps,
1877
+ sendFps: this._sendFps,
1878
+ onFrame: (timestamp) => this.renderer.renderFrame(timestamp),
1879
+ onSendFpsChange: (fps) => {
1880
+ this._sendFps = fps;
1881
+ options.onSendFpsChange?.(fps);
1882
+ this.applyVideoTrackConstraints();
1883
+ }
1884
+ });
1885
+ this.audioManager = createAudioManager({
1886
+ autoUnlock: options.autoUnlockAudio ?? true,
1887
+ unlockEvents: options.unlockEvents && options.unlockEvents.length > 0 ? options.unlockEvents : ["pointerdown", "click", "touchstart", "keydown"],
1888
+ disableSilentAudio: options.disableSilentAudio ?? false
1889
+ });
1890
+ this.visibilityHandler = createVisibilityHandler({
1891
+ onHidden: () => {
1892
+ if (this.lastVisibleSendFps == null) this.lastVisibleSendFps = this._sendFps;
1893
+ if (this._sendFps !== 5) {
1894
+ this.scheduler.setSendFps(5);
1895
+ this._sendFps = 5;
1896
+ }
1897
+ },
1898
+ onVisible: () => {
1899
+ if (this.lastVisibleSendFps != null && this._sendFps !== this.lastVisibleSendFps) {
1900
+ this.scheduler.setSendFps(this.lastVisibleSendFps);
1901
+ this._sendFps = this.lastVisibleSendFps;
1902
+ }
1903
+ this.lastVisibleSendFps = null;
1904
+ },
1905
+ backgroundRenderFn: () => {
1906
+ this.renderer.renderFrame(performance.now());
1907
+ this.requestVideoTrackFrame();
1908
+ }
1909
+ });
1910
+ this.outputStream = this.createOutputStream();
1911
+ this.audioManager.setOutputStream(this.outputStream);
1912
+ this.visibilityHandler.start();
1913
+ }
1914
+ // ============================================================================
1915
+ // Source Registry
1916
+ // ============================================================================
1917
+ register(id, source) {
1918
+ if (this.destroyed) return;
1919
+ this.registry.register(id, source);
1920
+ }
1921
+ unregister(id) {
1922
+ if (this.destroyed) return;
1923
+ const wasActive = this._activeId === id;
1924
+ this.registry.unregister(id);
1925
+ if (wasActive) {
1926
+ this._activeId = null;
1927
+ this.renderer.setActiveSource(null);
1928
+ this.scheduler.stop();
1929
+ this.emit("activated", null, void 0);
1930
+ }
1931
+ }
1932
+ get(id) {
1933
+ return this.registry.get(id);
1934
+ }
1935
+ has(id) {
1936
+ return this.registry.has(id);
1937
+ }
1938
+ list() {
1939
+ return this.registry.list();
1940
+ }
1941
+ // ============================================================================
1942
+ // Active Source Management
1943
+ // ============================================================================
1944
+ activate(id) {
1945
+ if (this.destroyed) return;
1946
+ const source = this.registry.get(id);
1947
+ if (!source) {
1948
+ throw new Error(`Source "${id}" not registered`);
1949
+ }
1950
+ this._activeId = id;
1951
+ this.renderer.setActiveSource(source);
1952
+ const videoEl = source.kind === "video" ? source.element : void 0;
1953
+ this.scheduler.start(videoEl);
1954
+ this.emit("activated", id, source);
1955
+ }
1956
+ deactivate() {
1957
+ if (this.destroyed) return;
1958
+ this._activeId = null;
1959
+ this.renderer.setActiveSource(null);
1960
+ this.scheduler.stop();
1961
+ this.emit("activated", null, void 0);
1962
+ }
1963
+ get activeId() {
1964
+ return this._activeId;
1965
+ }
1966
+ // ============================================================================
1967
+ // Output Stream
1968
+ // ============================================================================
1969
+ get stream() {
1970
+ return this.outputStream;
1971
+ }
1972
+ // ============================================================================
1973
+ // Settings
1974
+ // ============================================================================
1975
+ resize(width, height, dpr) {
1976
+ if (this.destroyed) return;
1977
+ const effectiveDpr = Math.min(
1978
+ 2,
1979
+ dpr ?? (typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1)
1980
+ );
1981
+ this.renderer.resize(width, height, effectiveDpr);
1982
+ this.recreateStream();
1983
+ }
1984
+ get size() {
1985
+ return this.renderer.size;
1986
+ }
1987
+ setFps(fps) {
1988
+ if (this.destroyed) return;
1989
+ const next = Math.max(1, fps);
1990
+ if (this._fps === next) return;
1991
+ this._fps = next;
1992
+ this.scheduler.setFps(next);
1993
+ this.recreateStream();
1994
+ }
1995
+ get fps() {
1996
+ return this._fps;
1997
+ }
1998
+ setSendFps(fps) {
1999
+ if (this.destroyed) return;
2000
+ const next = Math.max(1, fps);
2001
+ if (this._sendFps === next) return;
2002
+ this._sendFps = next;
2003
+ this.scheduler.setSendFps(next);
2004
+ }
2005
+ get sendFps() {
2006
+ return this._sendFps;
2007
+ }
2008
+ // ============================================================================
2009
+ // Audio
2010
+ // ============================================================================
2011
+ addAudioTrack(track) {
2012
+ if (this.destroyed) return;
2013
+ this.audioManager.addTrack(track);
2014
+ }
2015
+ removeAudioTrack(trackId) {
2016
+ if (this.destroyed) return;
2017
+ this.audioManager.removeTrack(trackId);
2018
+ }
2019
+ unlockAudio() {
2020
+ if (this.destroyed) return Promise.resolve(false);
2021
+ return this.audioManager.unlock();
2022
+ }
2023
+ // ============================================================================
2024
+ // Lifecycle
2025
+ // ============================================================================
2026
+ destroy() {
2027
+ if (this.destroyed) return;
2028
+ this.destroyed = true;
2029
+ this.scheduler.stop();
2030
+ this.visibilityHandler.stop();
2031
+ this.audioManager.destroy();
2032
+ this.renderer.destroy();
2033
+ this.registry.clear();
2034
+ if (this.outputStream) {
2035
+ try {
2036
+ this.outputStream.getVideoTracks().forEach((t) => {
2037
+ try {
2038
+ t.stop();
2039
+ } catch {
2040
+ }
2041
+ });
2042
+ } catch {
2043
+ }
2044
+ }
2045
+ this.outputStream = null;
2046
+ this.clearListeners();
2047
+ }
2048
+ // ============================================================================
2049
+ // Private Helpers
2050
+ // ============================================================================
2051
+ createOutputStream() {
2052
+ const stream = this.renderer.captureCanvas.captureStream(this._fps);
2053
+ try {
2054
+ const vtrack = stream.getVideoTracks()[0];
2055
+ if (vtrack && vtrack.contentHint !== void 0) {
2056
+ vtrack.contentHint = "detail";
2057
+ }
2058
+ } catch {
2059
+ }
2060
+ return stream;
2061
+ }
2062
+ recreateStream() {
2063
+ const newStream = this.createOutputStream();
2064
+ const prev = this.outputStream;
2065
+ if (prev && prev !== newStream) {
2066
+ try {
2067
+ prev.getAudioTracks().forEach((t) => {
2068
+ try {
2069
+ newStream.addTrack(t);
2070
+ } catch {
2071
+ }
2072
+ });
2073
+ } catch {
2074
+ }
2075
+ }
2076
+ this.outputStream = newStream;
2077
+ this.audioManager.setOutputStream(newStream);
2078
+ this.applyVideoTrackConstraints();
2079
+ if (prev && prev !== newStream) {
2080
+ try {
2081
+ prev.getVideoTracks().forEach((t) => {
2082
+ try {
2083
+ t.stop();
2084
+ } catch {
2085
+ }
2086
+ });
2087
+ } catch {
2088
+ }
2089
+ }
2090
+ }
2091
+ applyVideoTrackConstraints() {
2092
+ try {
2093
+ const track = this.outputStream?.getVideoTracks()[0];
2094
+ const canvas = this.renderer.captureCanvas;
2095
+ if (!track || !canvas) return;
2096
+ const constraints = {
2097
+ width: canvas.width,
2098
+ height: canvas.height,
2099
+ frameRate: Math.max(1, this._sendFps || this._fps)
2100
+ };
2101
+ try {
2102
+ if (track.contentHint !== void 0) {
2103
+ track.contentHint = "detail";
2104
+ }
2105
+ } catch {
2106
+ }
2107
+ track.applyConstraints(constraints).catch(() => {
2108
+ });
2109
+ } catch {
2110
+ }
2111
+ }
2112
+ requestVideoTrackFrame() {
2113
+ const track = this.outputStream?.getVideoTracks()[0];
2114
+ if (track && typeof track.requestFrame === "function") {
2115
+ try {
2116
+ track.requestFrame();
2117
+ } catch {
2118
+ }
2119
+ }
2120
+ }
2121
+ };
2122
+ function createCompositor(options = {}) {
2123
+ return new Compositor(options);
2124
+ }
2125
+
1029
2126
  // src/index.ts
1030
2127
  var livepeerResponseHandler = (response) => ({
1031
2128
  whepUrl: response.headers.get("livepeer-playback-url") ?? void 0
@@ -1052,6 +2149,7 @@ function createPlayer2(whepUrl, options) {
1052
2149
  StreamNotFoundError,
1053
2150
  UnauthorizedError,
1054
2151
  createBroadcast,
2152
+ createCompositor,
1055
2153
  createPlayer,
1056
2154
  livepeerResponseHandler
1057
2155
  });