@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 +1111 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +136 -1
- package/dist/index.d.ts +136 -1
- package/dist/index.js +1110 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,7 @@ __export(index_exports, {
|
|
|
31
31
|
StreamNotFoundError: () => StreamNotFoundError,
|
|
32
32
|
UnauthorizedError: () => UnauthorizedError,
|
|
33
33
|
createBroadcast: () => createBroadcast2,
|
|
34
|
+
createCompositor: () => createCompositor,
|
|
34
35
|
createPlayer: () => createPlayer2,
|
|
35
36
|
livepeerResponseHandler: () => livepeerResponseHandler
|
|
36
37
|
});
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
597
|
+
pc.oniceconnectionstatechange = () => {
|
|
578
598
|
if (this.state === "ended") return;
|
|
579
|
-
const
|
|
580
|
-
if (
|
|
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 (
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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 ??
|
|
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 ??
|
|
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
|
});
|