@vidtreo/recorder 0.8.5 → 0.8.6
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.d.ts +108 -8
- package/dist/index.js +1027 -151
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -694,7 +694,7 @@ var logger = {
|
|
|
694
694
|
};
|
|
695
695
|
|
|
696
696
|
// src/core/stream/source-switch-manager.ts
|
|
697
|
-
var SCREEN_SHARE_TRANSITION_DELAY =
|
|
697
|
+
var SCREEN_SHARE_TRANSITION_DELAY = 0;
|
|
698
698
|
var TRACK_READY_STATE_LIVE = "live";
|
|
699
699
|
|
|
700
700
|
class SourceSwitchManager {
|
|
@@ -774,11 +774,76 @@ class SourceSwitchManager {
|
|
|
774
774
|
}
|
|
775
775
|
combineScreenShareWithOriginalAudio(screenVideoTrack) {
|
|
776
776
|
const originalAudioTrack = this.originalCameraStream ? this.originalCameraStream.getAudioTracks()[0] : undefined;
|
|
777
|
+
logger.debug("[SourceSwitchManager] combineScreenShareWithOriginalAudio", {
|
|
778
|
+
hasOriginalCameraStream: !!this.originalCameraStream,
|
|
779
|
+
originalCameraStreamId: this.originalCameraStream?.id,
|
|
780
|
+
hasOriginalAudioTrack: !!originalAudioTrack,
|
|
781
|
+
originalAudioTrackId: originalAudioTrack?.id,
|
|
782
|
+
originalAudioTrackReadyState: originalAudioTrack?.readyState,
|
|
783
|
+
originalAudioTrackEnabled: originalAudioTrack?.enabled,
|
|
784
|
+
originalAudioTrackMuted: originalAudioTrack?.muted,
|
|
785
|
+
originalAudioTrackLabel: originalAudioTrack?.label,
|
|
786
|
+
isTrackLive: this.isTrackLive(originalAudioTrack),
|
|
787
|
+
screenVideoTrackId: screenVideoTrack.id,
|
|
788
|
+
screenVideoTrackReadyState: screenVideoTrack.readyState
|
|
789
|
+
});
|
|
777
790
|
const combinedTracks = [screenVideoTrack];
|
|
778
|
-
if (this.isTrackLive(originalAudioTrack)) {
|
|
791
|
+
if (this.isTrackLive(originalAudioTrack) && originalAudioTrack) {
|
|
779
792
|
combinedTracks.push(originalAudioTrack);
|
|
793
|
+
logger.debug("[SourceSwitchManager] Added original audio track to combined stream", {
|
|
794
|
+
audioTrackId: originalAudioTrack.id,
|
|
795
|
+
combinedTracksCount: combinedTracks.length
|
|
796
|
+
});
|
|
797
|
+
} else {
|
|
798
|
+
logger.warn("[SourceSwitchManager] Original audio track is not live, not adding to combined stream", {
|
|
799
|
+
audioTrackId: originalAudioTrack?.id,
|
|
800
|
+
audioTrackReadyState: originalAudioTrack?.readyState,
|
|
801
|
+
combinedTracksCount: combinedTracks.length
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
const combinedStream = new MediaStream(combinedTracks);
|
|
805
|
+
logger.debug("[SourceSwitchManager] Combined stream created", {
|
|
806
|
+
combinedStreamId: combinedStream.id,
|
|
807
|
+
combinedStreamVideoTracksCount: combinedStream.getVideoTracks().length,
|
|
808
|
+
combinedStreamAudioTracksCount: combinedStream.getAudioTracks().length,
|
|
809
|
+
combinedStreamAudioTrackId: combinedStream.getAudioTracks()[0]?.id,
|
|
810
|
+
combinedStreamAudioTrackReadyState: combinedStream.getAudioTracks()[0]?.readyState
|
|
811
|
+
});
|
|
812
|
+
return combinedStream;
|
|
813
|
+
}
|
|
814
|
+
handleScreenSelectionError() {
|
|
815
|
+
if (this.callbacks.onScreenSelectionEnd) {
|
|
816
|
+
this.callbacks.onScreenSelectionEnd();
|
|
817
|
+
}
|
|
818
|
+
if (this.callbacks.onTransitionEnd) {
|
|
819
|
+
this.callbacks.onTransitionEnd();
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
isPermissionDeniedError(error) {
|
|
823
|
+
const errorMessage = extractErrorMessage(error);
|
|
824
|
+
return errorMessage.includes("NotAllowedError") || errorMessage.includes("AbortError") || errorMessage.toLowerCase().includes("permission denied") || errorMessage.toLowerCase().includes("user denied");
|
|
825
|
+
}
|
|
826
|
+
async processScreenShareStream(screenShareStream, currentStream) {
|
|
827
|
+
this.screenShareStream = screenShareStream;
|
|
828
|
+
const screenVideoTrack = screenShareStream.getVideoTracks()[0];
|
|
829
|
+
if (!screenVideoTrack) {
|
|
830
|
+
this.stopStreamTracks(screenShareStream);
|
|
831
|
+
throw new Error("No video track found in screen share stream");
|
|
832
|
+
}
|
|
833
|
+
const combinedStream = this.combineScreenShareWithOriginalAudio(screenVideoTrack);
|
|
834
|
+
if (currentStream && currentStream !== this.originalCameraStream) {
|
|
835
|
+
this.stopStreamVideoTracks(currentStream);
|
|
836
|
+
}
|
|
837
|
+
const screenAudioTracks = screenShareStream.getAudioTracks();
|
|
838
|
+
for (const track of screenAudioTracks) {
|
|
839
|
+
track.stop();
|
|
840
|
+
}
|
|
841
|
+
this.currentSourceType = "screen";
|
|
842
|
+
if (this.callbacks.onSourceChange) {
|
|
843
|
+
await this.callbacks.onSourceChange(this.currentSourceType);
|
|
780
844
|
}
|
|
781
|
-
|
|
845
|
+
this.setupScreenShareTrackHandler(combinedStream);
|
|
846
|
+
return combinedStream;
|
|
782
847
|
}
|
|
783
848
|
async switchToScreenCapture() {
|
|
784
849
|
const currentStream = this.streamManager.getStream();
|
|
@@ -789,38 +854,18 @@ class SourceSwitchManager {
|
|
|
789
854
|
if (this.callbacks.onTransitionStart) {
|
|
790
855
|
this.callbacks.onTransitionStart("Select screen to share...");
|
|
791
856
|
}
|
|
857
|
+
if (this.callbacks.onScreenSelectionStart) {
|
|
858
|
+
this.callbacks.onScreenSelectionStart();
|
|
859
|
+
}
|
|
792
860
|
try {
|
|
793
861
|
const screenShareStream = await navigator.mediaDevices.getDisplayMedia({
|
|
794
862
|
video: true,
|
|
795
863
|
audio: true
|
|
796
864
|
});
|
|
797
|
-
this.screenShareStream
|
|
798
|
-
const screenVideoTrack = screenShareStream.getVideoTracks()[0];
|
|
799
|
-
if (!screenVideoTrack) {
|
|
800
|
-
this.stopStreamTracks(screenShareStream);
|
|
801
|
-
throw new Error("No video track found in screen share stream");
|
|
802
|
-
}
|
|
803
|
-
const combinedStream = this.combineScreenShareWithOriginalAudio(screenVideoTrack);
|
|
804
|
-
if (currentStream && currentStream !== this.originalCameraStream) {
|
|
805
|
-
this.stopStreamVideoTracks(currentStream);
|
|
806
|
-
}
|
|
807
|
-
const screenAudioTracks = screenShareStream.getAudioTracks();
|
|
808
|
-
for (const track of screenAudioTracks) {
|
|
809
|
-
track.stop();
|
|
810
|
-
}
|
|
811
|
-
this.currentSourceType = "screen";
|
|
812
|
-
if (this.callbacks.onSourceChange) {
|
|
813
|
-
await this.callbacks.onSourceChange(this.currentSourceType);
|
|
814
|
-
}
|
|
815
|
-
this.setupScreenShareTrackHandler(combinedStream);
|
|
816
|
-
return combinedStream;
|
|
865
|
+
return await this.processScreenShareStream(screenShareStream, currentStream);
|
|
817
866
|
} catch (error) {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
}
|
|
821
|
-
const errorMessage = extractErrorMessage(error);
|
|
822
|
-
const isPermissionDenied = errorMessage.includes("NotAllowedError") || errorMessage.includes("AbortError") || errorMessage.toLowerCase().includes("permission denied") || errorMessage.toLowerCase().includes("user denied");
|
|
823
|
-
if (isPermissionDenied) {
|
|
867
|
+
this.handleScreenSelectionError();
|
|
868
|
+
if (this.isPermissionDeniedError(error)) {
|
|
824
869
|
return null;
|
|
825
870
|
}
|
|
826
871
|
throw error;
|
|
@@ -938,7 +983,23 @@ class SourceSwitchManager {
|
|
|
938
983
|
}
|
|
939
984
|
async createCameraStreamWithOriginalAudio(cameraDeviceId) {
|
|
940
985
|
const originalAudioTrack = this.originalCameraStream ? this.originalCameraStream.getAudioTracks()[0] : undefined;
|
|
986
|
+
logger.debug("[SourceSwitchManager] createCameraStreamWithOriginalAudio", {
|
|
987
|
+
hasOriginalCameraStream: !!this.originalCameraStream,
|
|
988
|
+
originalCameraStreamId: this.originalCameraStream?.id,
|
|
989
|
+
hasOriginalAudioTrack: !!originalAudioTrack,
|
|
990
|
+
originalAudioTrackId: originalAudioTrack?.id,
|
|
991
|
+
originalAudioTrackReadyState: originalAudioTrack?.readyState,
|
|
992
|
+
originalAudioTrackEnabled: originalAudioTrack?.enabled,
|
|
993
|
+
originalAudioTrackMuted: originalAudioTrack?.muted,
|
|
994
|
+
originalAudioTrackLabel: originalAudioTrack?.label,
|
|
995
|
+
isTrackLive: this.isTrackLive(originalAudioTrack),
|
|
996
|
+
cameraDeviceId
|
|
997
|
+
});
|
|
941
998
|
if (!this.isTrackLive(originalAudioTrack)) {
|
|
999
|
+
logger.warn("[SourceSwitchManager] Original audio track is not live, cannot reuse", {
|
|
1000
|
+
originalAudioTrackId: originalAudioTrack?.id,
|
|
1001
|
+
originalAudioTrackReadyState: originalAudioTrack?.readyState
|
|
1002
|
+
});
|
|
942
1003
|
return null;
|
|
943
1004
|
}
|
|
944
1005
|
const videoConstraints = this.buildVideoConstraints(cameraDeviceId);
|
|
@@ -946,14 +1007,45 @@ class SourceSwitchManager {
|
|
|
946
1007
|
video: Object.keys(videoConstraints).length > 0 ? videoConstraints : true,
|
|
947
1008
|
audio: false
|
|
948
1009
|
};
|
|
1010
|
+
logger.debug("[SourceSwitchManager] Requesting new video stream", {
|
|
1011
|
+
constraints,
|
|
1012
|
+
cameraDeviceId
|
|
1013
|
+
});
|
|
949
1014
|
const newStream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
950
1015
|
const videoTrack = newStream.getVideoTracks()[0];
|
|
951
1016
|
this.validateTrack(videoTrack, "video", newStream);
|
|
1017
|
+
logger.debug("[SourceSwitchManager] New video stream obtained", {
|
|
1018
|
+
newStreamId: newStream.id,
|
|
1019
|
+
videoTrackId: videoTrack.id,
|
|
1020
|
+
videoTrackReadyState: videoTrack.readyState,
|
|
1021
|
+
newStreamAudioTracksCount: newStream.getAudioTracks().length
|
|
1022
|
+
});
|
|
952
1023
|
const combinedTracks = [videoTrack];
|
|
953
|
-
|
|
1024
|
+
if (originalAudioTrack) {
|
|
1025
|
+
combinedTracks.push(originalAudioTrack);
|
|
1026
|
+
}
|
|
1027
|
+
logger.debug("[SourceSwitchManager] Creating combined stream with original audio", {
|
|
1028
|
+
videoTrackId: videoTrack.id,
|
|
1029
|
+
audioTrackId: originalAudioTrack?.id,
|
|
1030
|
+
audioTrackReadyState: originalAudioTrack?.readyState,
|
|
1031
|
+
audioTrackEnabled: originalAudioTrack?.enabled,
|
|
1032
|
+
audioTrackMuted: originalAudioTrack?.muted,
|
|
1033
|
+
combinedTracksCount: combinedTracks.length
|
|
1034
|
+
});
|
|
954
1035
|
const combinedStream = new MediaStream(combinedTracks);
|
|
955
1036
|
this.stopLiveTracks(newStream.getAudioTracks());
|
|
1037
|
+
const previousOriginalCameraStreamId = this.originalCameraStream?.id;
|
|
956
1038
|
this.originalCameraStream = combinedStream;
|
|
1039
|
+
logger.debug("[SourceSwitchManager] Combined stream created and assigned", {
|
|
1040
|
+
combinedStreamId: combinedStream.id,
|
|
1041
|
+
previousOriginalCameraStreamId,
|
|
1042
|
+
newOriginalCameraStreamId: this.originalCameraStream.id,
|
|
1043
|
+
combinedStreamVideoTracksCount: combinedStream.getVideoTracks().length,
|
|
1044
|
+
combinedStreamAudioTracksCount: combinedStream.getAudioTracks().length,
|
|
1045
|
+
combinedStreamAudioTrackId: combinedStream.getAudioTracks()[0]?.id,
|
|
1046
|
+
combinedStreamAudioTrackReadyState: combinedStream.getAudioTracks()[0]?.readyState,
|
|
1047
|
+
audioTrackStillSame: combinedStream.getAudioTracks()[0] === originalAudioTrack
|
|
1048
|
+
});
|
|
957
1049
|
return combinedStream;
|
|
958
1050
|
}
|
|
959
1051
|
async createCameraStreamWithNewAudio(cameraDeviceId) {
|
|
@@ -999,7 +1091,7 @@ class SourceSwitchManager {
|
|
|
999
1091
|
}
|
|
1000
1092
|
return stream;
|
|
1001
1093
|
}
|
|
1002
|
-
if (this.originalCameraStream) {
|
|
1094
|
+
if (!isRecording && this.originalCameraStream) {
|
|
1003
1095
|
this.originalCameraStream = null;
|
|
1004
1096
|
}
|
|
1005
1097
|
const managerStream = this.streamManager.getStream();
|
|
@@ -1092,28 +1184,88 @@ class SourceSwitchManager {
|
|
|
1092
1184
|
}
|
|
1093
1185
|
logger.debug("[SourceSwitchManager] handleScreenShareStop invoked", {
|
|
1094
1186
|
currentSourceType: this.currentSourceType,
|
|
1095
|
-
hasScreenShareStream: !!this.screenShareStream
|
|
1187
|
+
hasScreenShareStream: !!this.screenShareStream,
|
|
1188
|
+
screenShareStreamId: this.screenShareStream?.id,
|
|
1189
|
+
hasOriginalCameraStream: !!this.originalCameraStream,
|
|
1190
|
+
originalCameraStreamId: this.originalCameraStream?.id
|
|
1096
1191
|
});
|
|
1097
1192
|
const screenShareStreamToStop = this.screenShareStream;
|
|
1098
1193
|
const currentStream = this.streamManager.getStream();
|
|
1194
|
+
logger.debug("[SourceSwitchManager] Current stream state before stop", {
|
|
1195
|
+
currentStreamId: currentStream?.id,
|
|
1196
|
+
currentStreamVideoTracksCount: currentStream?.getVideoTracks().length,
|
|
1197
|
+
currentStreamAudioTracksCount: currentStream?.getAudioTracks().length,
|
|
1198
|
+
currentStreamAudioTrackId: currentStream?.getAudioTracks()[0]?.id,
|
|
1199
|
+
currentStreamAudioTrackReadyState: currentStream?.getAudioTracks()[0]?.readyState,
|
|
1200
|
+
originalCameraStreamAudioTrackId: this.originalCameraStream?.getAudioTracks()[0]?.id,
|
|
1201
|
+
originalCameraStreamAudioTrackReadyState: this.originalCameraStream?.getAudioTracks()[0]?.readyState
|
|
1202
|
+
});
|
|
1099
1203
|
if (screenShareStreamToStop) {
|
|
1204
|
+
const screenShareAudioTracks = screenShareStreamToStop.getAudioTracks();
|
|
1205
|
+
logger.debug("[SourceSwitchManager] Screen share stream audio tracks before stop", {
|
|
1206
|
+
screenShareStreamId: screenShareStreamToStop.id,
|
|
1207
|
+
screenShareAudioTracksCount: screenShareAudioTracks.length,
|
|
1208
|
+
screenShareAudioTrackIds: screenShareAudioTracks.map((t) => ({
|
|
1209
|
+
id: t.id,
|
|
1210
|
+
readyState: t.readyState,
|
|
1211
|
+
enabled: t.enabled
|
|
1212
|
+
}))
|
|
1213
|
+
});
|
|
1100
1214
|
this.removeScreenShareTrackHandler(screenShareStreamToStop);
|
|
1101
1215
|
this.stopScreenShareStreamTracks(screenShareStreamToStop);
|
|
1102
1216
|
this.stopDisplayTracks(screenShareStreamToStop);
|
|
1103
1217
|
this.screenShareStream = null;
|
|
1104
1218
|
await this.waitForTracksToEnd(SCREEN_SHARE_TRANSITION_DELAY);
|
|
1219
|
+
logger.debug("[SourceSwitchManager] Screen share stream stopped", {
|
|
1220
|
+
screenShareAudioTracksAfterStop: screenShareAudioTracks.map((t) => ({
|
|
1221
|
+
id: t.id,
|
|
1222
|
+
readyState: t.readyState
|
|
1223
|
+
}))
|
|
1224
|
+
});
|
|
1105
1225
|
}
|
|
1106
1226
|
if (currentStream) {
|
|
1227
|
+
const currentStreamAudioTracks = currentStream.getAudioTracks();
|
|
1228
|
+
logger.debug("[SourceSwitchManager] Current stream audio tracks before video stop", {
|
|
1229
|
+
currentStreamId: currentStream.id,
|
|
1230
|
+
currentStreamAudioTracksCount: currentStreamAudioTracks.length,
|
|
1231
|
+
currentStreamAudioTrackIds: currentStreamAudioTracks.map((t) => ({
|
|
1232
|
+
id: t.id,
|
|
1233
|
+
readyState: t.readyState,
|
|
1234
|
+
enabled: t.enabled
|
|
1235
|
+
}))
|
|
1236
|
+
});
|
|
1107
1237
|
this.stopStreamVideoTracks(currentStream);
|
|
1108
1238
|
this.stopDisplayTracks(currentStream);
|
|
1239
|
+
logger.debug("[SourceSwitchManager] Current stream audio tracks after video stop", {
|
|
1240
|
+
currentStreamId: currentStream.id,
|
|
1241
|
+
currentStreamAudioTracksCount: currentStream.getAudioTracks().length,
|
|
1242
|
+
currentStreamAudioTrackIds: currentStream.getAudioTracks().map((t) => ({
|
|
1243
|
+
id: t.id,
|
|
1244
|
+
readyState: t.readyState,
|
|
1245
|
+
enabled: t.enabled
|
|
1246
|
+
}))
|
|
1247
|
+
});
|
|
1109
1248
|
}
|
|
1249
|
+
logger.debug("[SourceSwitchManager] Original camera stream state before source change", {
|
|
1250
|
+
hasOriginalCameraStream: !!this.originalCameraStream,
|
|
1251
|
+
originalCameraStreamId: this.originalCameraStream?.id,
|
|
1252
|
+
originalCameraStreamAudioTracksCount: this.originalCameraStream?.getAudioTracks().length,
|
|
1253
|
+
originalCameraStreamAudioTrackId: this.originalCameraStream?.getAudioTracks()[0]?.id,
|
|
1254
|
+
originalCameraStreamAudioTrackReadyState: this.originalCameraStream?.getAudioTracks()[0]?.readyState,
|
|
1255
|
+
originalCameraStreamAudioTrackEnabled: this.originalCameraStream?.getAudioTracks()[0]?.enabled
|
|
1256
|
+
});
|
|
1110
1257
|
this.currentSourceType = "camera";
|
|
1111
1258
|
if (this.callbacks.onSourceChange) {
|
|
1112
1259
|
await this.callbacks.onSourceChange(this.currentSourceType);
|
|
1113
1260
|
}
|
|
1114
1261
|
logger.debug("[SourceSwitchManager] handleScreenShareStop completed", {
|
|
1115
1262
|
hasScreenShareStream: !!this.screenShareStream,
|
|
1116
|
-
currentSourceType: this.currentSourceType
|
|
1263
|
+
currentSourceType: this.currentSourceType,
|
|
1264
|
+
hasOriginalCameraStream: !!this.originalCameraStream,
|
|
1265
|
+
originalCameraStreamId: this.originalCameraStream?.id,
|
|
1266
|
+
originalCameraStreamAudioTracksCount: this.originalCameraStream?.getAudioTracks().length,
|
|
1267
|
+
originalCameraStreamAudioTrackId: this.originalCameraStream?.getAudioTracks()[0]?.id,
|
|
1268
|
+
originalCameraStreamAudioTrackReadyState: this.originalCameraStream?.getAudioTracks()[0]?.readyState
|
|
1117
1269
|
});
|
|
1118
1270
|
}
|
|
1119
1271
|
async applyCameraStream(newStream, isRecording) {
|
|
@@ -1547,6 +1699,135 @@ function formatTime(totalSeconds) {
|
|
|
1547
1699
|
return `${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
|
1548
1700
|
}
|
|
1549
1701
|
|
|
1702
|
+
// src/core/utils/tab-visibility-tracker.ts
|
|
1703
|
+
class TabVisibilityTracker {
|
|
1704
|
+
recordingStartTime = 0;
|
|
1705
|
+
totalPausedTime = 0;
|
|
1706
|
+
pauseStartTime = null;
|
|
1707
|
+
intervals = [];
|
|
1708
|
+
currentIntervalStart = null;
|
|
1709
|
+
isTracking = false;
|
|
1710
|
+
visibilityChangeHandler;
|
|
1711
|
+
blurHandler;
|
|
1712
|
+
focusHandler;
|
|
1713
|
+
constructor() {
|
|
1714
|
+
this.visibilityChangeHandler = this.handleVisibilityChange.bind(this);
|
|
1715
|
+
this.blurHandler = this.handleBlur.bind(this);
|
|
1716
|
+
this.focusHandler = this.handleFocus.bind(this);
|
|
1717
|
+
}
|
|
1718
|
+
start(recordingStartTime) {
|
|
1719
|
+
if (this.isTracking) {
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
this.recordingStartTime = recordingStartTime;
|
|
1723
|
+
this.totalPausedTime = 0;
|
|
1724
|
+
this.pauseStartTime = null;
|
|
1725
|
+
this.intervals = [];
|
|
1726
|
+
this.currentIntervalStart = null;
|
|
1727
|
+
this.isTracking = true;
|
|
1728
|
+
if (typeof document !== "undefined") {
|
|
1729
|
+
document.addEventListener("visibilitychange", this.visibilityChangeHandler);
|
|
1730
|
+
}
|
|
1731
|
+
if (typeof window !== "undefined") {
|
|
1732
|
+
window.addEventListener("blur", this.blurHandler);
|
|
1733
|
+
window.addEventListener("focus", this.focusHandler);
|
|
1734
|
+
}
|
|
1735
|
+
this.checkInitialState();
|
|
1736
|
+
}
|
|
1737
|
+
pause() {
|
|
1738
|
+
if (!this.isTracking || this.pauseStartTime !== null) {
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
this.pauseStartTime = Date.now();
|
|
1742
|
+
this.endCurrentIntervalIfActive();
|
|
1743
|
+
}
|
|
1744
|
+
resume() {
|
|
1745
|
+
if (!this.isTracking || this.pauseStartTime === null) {
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
const pausedDuration = Date.now() - this.pauseStartTime;
|
|
1749
|
+
this.totalPausedTime += pausedDuration;
|
|
1750
|
+
this.pauseStartTime = null;
|
|
1751
|
+
}
|
|
1752
|
+
getIntervals() {
|
|
1753
|
+
this.endCurrentIntervalIfActive();
|
|
1754
|
+
return this.intervals.map((interval) => ({
|
|
1755
|
+
start: this.normalizeTimestamp(interval.start),
|
|
1756
|
+
end: this.normalizeTimestamp(interval.end)
|
|
1757
|
+
}));
|
|
1758
|
+
}
|
|
1759
|
+
reset() {
|
|
1760
|
+
this.intervals = [];
|
|
1761
|
+
this.currentIntervalStart = null;
|
|
1762
|
+
this.totalPausedTime = 0;
|
|
1763
|
+
this.pauseStartTime = null;
|
|
1764
|
+
}
|
|
1765
|
+
cleanup() {
|
|
1766
|
+
this.isTracking = false;
|
|
1767
|
+
this.endCurrentIntervalIfActive();
|
|
1768
|
+
if (typeof document !== "undefined") {
|
|
1769
|
+
document.removeEventListener("visibilitychange", this.visibilityChangeHandler);
|
|
1770
|
+
}
|
|
1771
|
+
if (typeof window !== "undefined") {
|
|
1772
|
+
window.removeEventListener("blur", this.blurHandler);
|
|
1773
|
+
window.removeEventListener("focus", this.focusHandler);
|
|
1774
|
+
}
|
|
1775
|
+
this.reset();
|
|
1776
|
+
}
|
|
1777
|
+
checkInitialState() {
|
|
1778
|
+
if (typeof document === "undefined") {
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
if (document.visibilityState === "hidden") {
|
|
1782
|
+
this.startInterval();
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
handleVisibilityChange() {
|
|
1786
|
+
if (typeof document === "undefined") {
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
if (document.visibilityState === "hidden") {
|
|
1790
|
+
this.startInterval();
|
|
1791
|
+
} else {
|
|
1792
|
+
this.endCurrentIntervalIfActive();
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
handleBlur() {
|
|
1796
|
+
this.startInterval();
|
|
1797
|
+
}
|
|
1798
|
+
handleFocus() {
|
|
1799
|
+
this.endCurrentIntervalIfActive();
|
|
1800
|
+
}
|
|
1801
|
+
startInterval() {
|
|
1802
|
+
if (this.currentIntervalStart !== null || !this.isTracking) {
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
if (this.pauseStartTime !== null) {
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
this.currentIntervalStart = Date.now();
|
|
1809
|
+
}
|
|
1810
|
+
endCurrentIntervalIfActive() {
|
|
1811
|
+
if (this.currentIntervalStart === null) {
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
const endTime = Date.now();
|
|
1815
|
+
const startTime = this.currentIntervalStart;
|
|
1816
|
+
if (endTime > startTime) {
|
|
1817
|
+
this.intervals.push({
|
|
1818
|
+
start: startTime,
|
|
1819
|
+
end: endTime
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
this.currentIntervalStart = null;
|
|
1823
|
+
}
|
|
1824
|
+
normalizeTimestamp(absoluteTime) {
|
|
1825
|
+
const elapsed = absoluteTime - this.recordingStartTime;
|
|
1826
|
+
const normalized = (elapsed - this.totalPausedTime) / 1000;
|
|
1827
|
+
return Math.max(0, normalized);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1550
1831
|
// src/core/utils/validation.ts
|
|
1551
1832
|
function requireNonNull(value, message) {
|
|
1552
1833
|
if (value === null || value === undefined) {
|
|
@@ -1596,6 +1877,10 @@ class StreamRecordingState {
|
|
|
1596
1877
|
totalPausedTime = 0;
|
|
1597
1878
|
streamProcessor = null;
|
|
1598
1879
|
bufferSizeUpdateInterval = null;
|
|
1880
|
+
tabVisibilityTracker = null;
|
|
1881
|
+
visibilityChangeHandler = null;
|
|
1882
|
+
blurHandler = null;
|
|
1883
|
+
focusHandler = null;
|
|
1599
1884
|
streamManager;
|
|
1600
1885
|
constructor(streamManager) {
|
|
1601
1886
|
this.streamManager = streamManager;
|
|
@@ -1615,7 +1900,7 @@ class StreamRecordingState {
|
|
|
1615
1900
|
}
|
|
1616
1901
|
return this.streamManager.getAudioStreamForAnalysis();
|
|
1617
1902
|
}
|
|
1618
|
-
async startRecording(processor, config) {
|
|
1903
|
+
async startRecording(processor, config, enableTabVisibilityOverlay, tabVisibilityOverlayText) {
|
|
1619
1904
|
const mediaStream = this.streamManager.getStream();
|
|
1620
1905
|
logger.debug("[StreamRecordingState] startRecording called", {
|
|
1621
1906
|
hasMediaStream: !!mediaStream,
|
|
@@ -1646,10 +1931,29 @@ class StreamRecordingState {
|
|
|
1646
1931
|
const formatted = formatFileSize(size);
|
|
1647
1932
|
this.streamManager.emit("recordingbufferupdate", { size, formatted });
|
|
1648
1933
|
}, TIMER_INTERVAL);
|
|
1934
|
+
this.resetRecordingState();
|
|
1935
|
+
const overlayConfig = enableTabVisibilityOverlay && tabVisibilityOverlayText ? {
|
|
1936
|
+
enabled: true,
|
|
1937
|
+
text: tabVisibilityOverlayText,
|
|
1938
|
+
recordingStartTime: this.recordingStartTime
|
|
1939
|
+
} : undefined;
|
|
1940
|
+
logger.debug("[StreamRecordingState] Overlay config", {
|
|
1941
|
+
enableTabVisibilityOverlay,
|
|
1942
|
+
hasOverlayText: !!tabVisibilityOverlayText,
|
|
1943
|
+
overlayText: tabVisibilityOverlayText,
|
|
1944
|
+
overlayConfig
|
|
1945
|
+
});
|
|
1649
1946
|
logger.debug("[StreamRecordingState] Starting processing");
|
|
1650
|
-
await processor.startProcessing(mediaStream, config);
|
|
1947
|
+
await processor.startProcessing(mediaStream, config, overlayConfig);
|
|
1651
1948
|
logger.info("[StreamRecordingState] Processing started and worker ready");
|
|
1652
|
-
|
|
1949
|
+
if (enableTabVisibilityOverlay) {
|
|
1950
|
+
logger.debug("[StreamRecordingState] Setting up tab visibility tracking", {
|
|
1951
|
+
recordingStartTime: this.recordingStartTime
|
|
1952
|
+
});
|
|
1953
|
+
this.tabVisibilityTracker = new TabVisibilityTracker;
|
|
1954
|
+
this.tabVisibilityTracker.start(this.recordingStartTime);
|
|
1955
|
+
this.setupVisibilityUpdates(processor);
|
|
1956
|
+
}
|
|
1653
1957
|
this.streamManager.setState("recording");
|
|
1654
1958
|
this.streamManager.emit("recordingstart", { recorder: null });
|
|
1655
1959
|
this.startRecordingTimer();
|
|
@@ -1666,6 +1970,19 @@ class StreamRecordingState {
|
|
|
1666
1970
|
this.clearRecordingTimer();
|
|
1667
1971
|
this.clearBufferSizeInterval();
|
|
1668
1972
|
this.resetPauseState();
|
|
1973
|
+
this.cleanupVisibilityUpdates();
|
|
1974
|
+
let tabVisibilityIntervals = [];
|
|
1975
|
+
if (this.tabVisibilityTracker) {
|
|
1976
|
+
tabVisibilityIntervals = this.tabVisibilityTracker.getIntervals();
|
|
1977
|
+
logger.debug("[StreamRecordingState] Tab visibility intervals collected", {
|
|
1978
|
+
intervalsCount: tabVisibilityIntervals.length,
|
|
1979
|
+
intervals: tabVisibilityIntervals
|
|
1980
|
+
});
|
|
1981
|
+
this.tabVisibilityTracker.cleanup();
|
|
1982
|
+
this.tabVisibilityTracker = null;
|
|
1983
|
+
} else {
|
|
1984
|
+
logger.debug("[StreamRecordingState] No tab visibility tracker was active");
|
|
1985
|
+
}
|
|
1669
1986
|
logger.debug("[StreamRecordingState] Finalizing stream processor");
|
|
1670
1987
|
const result = await this.streamProcessor.finalize();
|
|
1671
1988
|
logger.info("[StreamRecordingState] Stream processor finalized", {
|
|
@@ -1679,13 +1996,19 @@ class StreamRecordingState {
|
|
|
1679
1996
|
});
|
|
1680
1997
|
this.streamProcessor = null;
|
|
1681
1998
|
logger.debug("[StreamRecordingState] StreamProcessor cleared");
|
|
1682
|
-
return
|
|
1999
|
+
return {
|
|
2000
|
+
blob: result.blob,
|
|
2001
|
+
tabVisibilityIntervals
|
|
2002
|
+
};
|
|
1683
2003
|
}
|
|
1684
2004
|
pauseRecording() {
|
|
1685
2005
|
this.clearRecordingTimer();
|
|
1686
2006
|
if (this.pauseStartTime === null) {
|
|
1687
2007
|
this.pauseStartTime = Date.now();
|
|
1688
2008
|
}
|
|
2009
|
+
if (this.tabVisibilityTracker) {
|
|
2010
|
+
this.tabVisibilityTracker.pause();
|
|
2011
|
+
}
|
|
1689
2012
|
if (this.streamProcessor && this.isRecording()) {
|
|
1690
2013
|
this.streamProcessor.pause();
|
|
1691
2014
|
}
|
|
@@ -1696,6 +2019,9 @@ class StreamRecordingState {
|
|
|
1696
2019
|
this.totalPausedTime += pausedDuration;
|
|
1697
2020
|
this.pauseStartTime = null;
|
|
1698
2021
|
}
|
|
2022
|
+
if (this.tabVisibilityTracker) {
|
|
2023
|
+
this.tabVisibilityTracker.resume();
|
|
2024
|
+
}
|
|
1699
2025
|
this.startRecordingTimer();
|
|
1700
2026
|
if (this.streamProcessor && this.isRecording()) {
|
|
1701
2027
|
this.streamProcessor.resume();
|
|
@@ -1778,7 +2104,7 @@ class StreamRecordingState {
|
|
|
1778
2104
|
}
|
|
1779
2105
|
}
|
|
1780
2106
|
resetRecordingState() {
|
|
1781
|
-
this.recordingStartTime =
|
|
2107
|
+
this.recordingStartTime = performance.now();
|
|
1782
2108
|
this.totalPausedTime = 0;
|
|
1783
2109
|
this.pauseStartTime = null;
|
|
1784
2110
|
}
|
|
@@ -1786,11 +2112,72 @@ class StreamRecordingState {
|
|
|
1786
2112
|
this.totalPausedTime = 0;
|
|
1787
2113
|
this.pauseStartTime = null;
|
|
1788
2114
|
}
|
|
2115
|
+
setupVisibilityUpdates(processor) {
|
|
2116
|
+
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
2117
|
+
logger.warn("[StreamRecordingState] Cannot setup visibility updates - document/window not available");
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
2120
|
+
this.visibilityChangeHandler = () => {
|
|
2121
|
+
if (typeof document === "undefined") {
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
const isHidden = document.visibilityState === "hidden";
|
|
2125
|
+
const timestamp = performance.now();
|
|
2126
|
+
logger.debug("[StreamRecordingState] Visibility change", {
|
|
2127
|
+
isHidden,
|
|
2128
|
+
timestamp,
|
|
2129
|
+
visibilityState: document.visibilityState
|
|
2130
|
+
});
|
|
2131
|
+
processor.updateTabVisibility(isHidden, timestamp);
|
|
2132
|
+
};
|
|
2133
|
+
this.blurHandler = () => {
|
|
2134
|
+
const timestamp = performance.now();
|
|
2135
|
+
logger.debug("[StreamRecordingState] Window blur", { timestamp });
|
|
2136
|
+
processor.updateTabVisibility(true, timestamp);
|
|
2137
|
+
};
|
|
2138
|
+
this.focusHandler = () => {
|
|
2139
|
+
const timestamp = performance.now();
|
|
2140
|
+
logger.debug("[StreamRecordingState] Window focus", { timestamp });
|
|
2141
|
+
processor.updateTabVisibility(false, timestamp);
|
|
2142
|
+
};
|
|
2143
|
+
document.addEventListener("visibilitychange", this.visibilityChangeHandler);
|
|
2144
|
+
window.addEventListener("blur", this.blurHandler);
|
|
2145
|
+
window.addEventListener("focus", this.focusHandler);
|
|
2146
|
+
const initialHidden = document.visibilityState === "hidden";
|
|
2147
|
+
if (initialHidden) {
|
|
2148
|
+
const timestamp = performance.now();
|
|
2149
|
+
logger.debug("[StreamRecordingState] Initial state is hidden", {
|
|
2150
|
+
timestamp
|
|
2151
|
+
});
|
|
2152
|
+
processor.updateTabVisibility(true, timestamp);
|
|
2153
|
+
} else {
|
|
2154
|
+
logger.debug("[StreamRecordingState] Initial state is visible");
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
cleanupVisibilityUpdates() {
|
|
2158
|
+
if (this.visibilityChangeHandler && typeof document !== "undefined") {
|
|
2159
|
+
document.removeEventListener("visibilitychange", this.visibilityChangeHandler);
|
|
2160
|
+
this.visibilityChangeHandler = null;
|
|
2161
|
+
}
|
|
2162
|
+
if (this.blurHandler && typeof window !== "undefined") {
|
|
2163
|
+
window.removeEventListener("blur", this.blurHandler);
|
|
2164
|
+
this.blurHandler = null;
|
|
2165
|
+
}
|
|
2166
|
+
if (this.focusHandler && typeof window !== "undefined") {
|
|
2167
|
+
window.removeEventListener("focus", this.focusHandler);
|
|
2168
|
+
this.focusHandler = null;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
1789
2171
|
destroy() {
|
|
1790
2172
|
if (this.streamProcessor) {
|
|
1791
2173
|
this.streamProcessor.cancel().catch(() => {});
|
|
1792
2174
|
this.streamProcessor = null;
|
|
1793
2175
|
}
|
|
2176
|
+
this.cleanupVisibilityUpdates();
|
|
2177
|
+
if (this.tabVisibilityTracker) {
|
|
2178
|
+
this.tabVisibilityTracker.cleanup();
|
|
2179
|
+
this.tabVisibilityTracker = null;
|
|
2180
|
+
}
|
|
1794
2181
|
this.clearRecordingTimer();
|
|
1795
2182
|
this.clearBufferSizeInterval();
|
|
1796
2183
|
}
|
|
@@ -1855,8 +2242,8 @@ class CameraStreamManager {
|
|
|
1855
2242
|
switchAudioDevice(deviceId) {
|
|
1856
2243
|
return this.streamManager.switchAudioDevice(deviceId);
|
|
1857
2244
|
}
|
|
1858
|
-
async startRecording(processor, config) {
|
|
1859
|
-
return await this.recordingState.startRecording(processor, config);
|
|
2245
|
+
async startRecording(processor, config, enableTabVisibilityOverlay, tabVisibilityOverlayText) {
|
|
2246
|
+
return await this.recordingState.startRecording(processor, config, enableTabVisibilityOverlay, tabVisibilityOverlayText);
|
|
1860
2247
|
}
|
|
1861
2248
|
async stopRecording() {
|
|
1862
2249
|
return await this.recordingState.stopRecording();
|
|
@@ -9195,6 +9582,14 @@ class Output {
|
|
|
9195
9582
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
9196
9583
|
*/
|
|
9197
9584
|
|
|
9585
|
+
// src/core/utils/error-handler.ts
|
|
9586
|
+
function extractErrorMessage(error) {
|
|
9587
|
+
if (error instanceof Error) {
|
|
9588
|
+
return error.message;
|
|
9589
|
+
}
|
|
9590
|
+
return String(error);
|
|
9591
|
+
}
|
|
9592
|
+
|
|
9198
9593
|
// src/core/utils/logger.ts
|
|
9199
9594
|
function isDebugEnabled() {
|
|
9200
9595
|
const globalAny = globalThis;
|
|
@@ -9290,8 +9685,35 @@ var logger = {
|
|
|
9290
9685
|
}
|
|
9291
9686
|
};
|
|
9292
9687
|
|
|
9688
|
+
// src/core/utils/validation.ts
|
|
9689
|
+
function requireNonNull(value, message) {
|
|
9690
|
+
if (value === null || value === undefined) {
|
|
9691
|
+
throw new Error(message);
|
|
9692
|
+
}
|
|
9693
|
+
return value;
|
|
9694
|
+
}
|
|
9695
|
+
function requireDefined(value, message) {
|
|
9696
|
+
if (value === undefined) {
|
|
9697
|
+
throw new Error(message);
|
|
9698
|
+
}
|
|
9699
|
+
return value;
|
|
9700
|
+
}
|
|
9701
|
+
function requireInitialized(value, componentName) {
|
|
9702
|
+
if (value === null || value === undefined) {
|
|
9703
|
+
throw new Error(\`\${componentName} is not initialized\`);
|
|
9704
|
+
}
|
|
9705
|
+
return value;
|
|
9706
|
+
}
|
|
9707
|
+
|
|
9293
9708
|
// src/core/processor/worker/recorder-worker.ts
|
|
9294
9709
|
var CHUNK_SIZE = 16 * 1024 * 1024;
|
|
9710
|
+
var OVERLAY_BACKGROUND_OPACITY = 0.6;
|
|
9711
|
+
var OVERLAY_PADDING = 16;
|
|
9712
|
+
var OVERLAY_TEXT_COLOR = "#ffffff";
|
|
9713
|
+
var OVERLAY_FONT_SIZE = 16;
|
|
9714
|
+
var OVERLAY_FONT_FAMILY = "Arial, sans-serif";
|
|
9715
|
+
var OVERLAY_MIN_WIDTH = 200;
|
|
9716
|
+
var OVERLAY_MIN_HEIGHT = 50;
|
|
9295
9717
|
|
|
9296
9718
|
class RecorderWorker {
|
|
9297
9719
|
output = null;
|
|
@@ -9314,8 +9736,18 @@ class RecorderWorker {
|
|
|
9314
9736
|
bufferUpdateInterval = null;
|
|
9315
9737
|
pausedDuration = 0;
|
|
9316
9738
|
pauseStartedAt = null;
|
|
9739
|
+
overlayConfig = null;
|
|
9740
|
+
overlayCanvas = null;
|
|
9741
|
+
compositionCanvas = null;
|
|
9742
|
+
compositionCtx = null;
|
|
9743
|
+
hiddenIntervals = [];
|
|
9744
|
+
currentHiddenIntervalStart = null;
|
|
9745
|
+
recordingStartTime = 0;
|
|
9746
|
+
pendingVisibilityUpdates = [];
|
|
9747
|
+
isScreenCapture = false;
|
|
9748
|
+
driftOffset = 0;
|
|
9317
9749
|
constructor() {
|
|
9318
|
-
self.addEventListener("message", this.handleMessage
|
|
9750
|
+
self.addEventListener("message", this.handleMessage);
|
|
9319
9751
|
}
|
|
9320
9752
|
formatFileSize(bytes2) {
|
|
9321
9753
|
if (bytes2 === 0) {
|
|
@@ -9327,59 +9759,78 @@ class RecorderWorker {
|
|
|
9327
9759
|
const size = Math.round(bytes2 / base ** index * 100) / 100;
|
|
9328
9760
|
return \`\${size} \${units[index]}\`;
|
|
9329
9761
|
}
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9762
|
+
shouldIgnoreMessage() {
|
|
9763
|
+
return this.isStopping || this.isFinalized;
|
|
9764
|
+
}
|
|
9765
|
+
handleAsyncOperation(operation, context) {
|
|
9766
|
+
operation.catch((error) => {
|
|
9767
|
+
logger.error(\`[RecorderWorker] Error in \${context}:\`, error);
|
|
9768
|
+
this.sendError(error);
|
|
9336
9769
|
});
|
|
9337
|
-
switch (message.type) {
|
|
9338
|
-
case "start":
|
|
9339
|
-
if (this.isStopping || this.isFinalized) {
|
|
9340
|
-
logger.debug("[RecorderWorker] start ignored (stopping/finalized)");
|
|
9341
|
-
return;
|
|
9342
|
-
}
|
|
9343
|
-
logger.debug("[RecorderWorker] Starting recording", {
|
|
9344
|
-
hasVideoStream: !!message.videoStream,
|
|
9345
|
-
hasAudioStream: !!message.audioStream
|
|
9346
|
-
});
|
|
9347
|
-
this.handleStart(message.videoStream, message.audioStream, message.config).catch((error) => {
|
|
9348
|
-
logger.error("[RecorderWorker] Error in handleStart:", error);
|
|
9349
|
-
this.sendError(error);
|
|
9350
|
-
});
|
|
9351
|
-
break;
|
|
9352
|
-
case "pause":
|
|
9353
|
-
this.handlePause();
|
|
9354
|
-
break;
|
|
9355
|
-
case "resume":
|
|
9356
|
-
this.handleResume();
|
|
9357
|
-
break;
|
|
9358
|
-
case "stop":
|
|
9359
|
-
if (this.isStopping || this.isFinalized) {
|
|
9360
|
-
logger.debug("[RecorderWorker] stop ignored (stopping/finalized)");
|
|
9361
|
-
return;
|
|
9362
|
-
}
|
|
9363
|
-
this.handleStop().catch((error) => {
|
|
9364
|
-
this.sendError(error);
|
|
9365
|
-
});
|
|
9366
|
-
break;
|
|
9367
|
-
case "toggleMute":
|
|
9368
|
-
this.handleToggleMute();
|
|
9369
|
-
break;
|
|
9370
|
-
case "switchSource":
|
|
9371
|
-
this.handleSwitchSource(message.videoStream).catch((error) => {
|
|
9372
|
-
this.sendError(error);
|
|
9373
|
-
});
|
|
9374
|
-
break;
|
|
9375
|
-
case "updateFps":
|
|
9376
|
-
this.handleUpdateFps(message.fps);
|
|
9377
|
-
break;
|
|
9378
|
-
default:
|
|
9379
|
-
this.sendError(new Error(\`Unknown message type: \${message.type}\`));
|
|
9380
|
-
}
|
|
9381
9770
|
}
|
|
9382
|
-
|
|
9771
|
+
handleMessage = (event) => {
|
|
9772
|
+
const message = event.data;
|
|
9773
|
+
logger.debug("[RecorderWorker] Received message:", { type: message.type });
|
|
9774
|
+
if (message.type === "start") {
|
|
9775
|
+
if (this.shouldIgnoreMessage()) {
|
|
9776
|
+
logger.debug("[RecorderWorker] start ignored (stopping/finalized)");
|
|
9777
|
+
return;
|
|
9778
|
+
}
|
|
9779
|
+
this.handleAsyncOperation(this.handleStart(message.videoStream, message.audioStream, message.config, message.overlayConfig), "handleStart");
|
|
9780
|
+
return;
|
|
9781
|
+
}
|
|
9782
|
+
if (message.type === "pause") {
|
|
9783
|
+
this.handlePause();
|
|
9784
|
+
return;
|
|
9785
|
+
}
|
|
9786
|
+
if (message.type === "resume") {
|
|
9787
|
+
this.handleResume();
|
|
9788
|
+
return;
|
|
9789
|
+
}
|
|
9790
|
+
if (message.type === "stop") {
|
|
9791
|
+
if (this.shouldIgnoreMessage()) {
|
|
9792
|
+
logger.debug("[RecorderWorker] stop ignored (stopping/finalized)");
|
|
9793
|
+
return;
|
|
9794
|
+
}
|
|
9795
|
+
this.handleAsyncOperation(this.handleStop(), "handleStop");
|
|
9796
|
+
return;
|
|
9797
|
+
}
|
|
9798
|
+
if (message.type === "toggleMute") {
|
|
9799
|
+
this.handleToggleMute();
|
|
9800
|
+
return;
|
|
9801
|
+
}
|
|
9802
|
+
if (message.type === "switchSource") {
|
|
9803
|
+
this.handleAsyncOperation(this.handleSwitchSource(message.videoStream), "handleSwitchSource");
|
|
9804
|
+
return;
|
|
9805
|
+
}
|
|
9806
|
+
if (message.type === "updateFps") {
|
|
9807
|
+
this.handleUpdateFps(message.fps);
|
|
9808
|
+
return;
|
|
9809
|
+
}
|
|
9810
|
+
if (message.type === "updateVisibility") {
|
|
9811
|
+
this.handleUpdateVisibility(message.isHidden, message.timestamp);
|
|
9812
|
+
return;
|
|
9813
|
+
}
|
|
9814
|
+
if (message.type === "updateSourceType") {
|
|
9815
|
+
this.handleUpdateSourceType(message.isScreenCapture);
|
|
9816
|
+
return;
|
|
9817
|
+
}
|
|
9818
|
+
this.sendError(new Error(\`Unknown message type: \${message.type}\`));
|
|
9819
|
+
};
|
|
9820
|
+
async handleStart(videoStream, audioStream, config, overlayConfig) {
|
|
9821
|
+
requireDefined(config, "Transcode config is required");
|
|
9822
|
+
if (config.width <= 0 || config.height <= 0) {
|
|
9823
|
+
throw new Error("Video dimensions must be greater than zero");
|
|
9824
|
+
}
|
|
9825
|
+
if (config.fps <= 0) {
|
|
9826
|
+
throw new Error("Frame rate must be greater than zero");
|
|
9827
|
+
}
|
|
9828
|
+
if (config.bitrate <= 0) {
|
|
9829
|
+
throw new Error("Bitrate must be greater than zero");
|
|
9830
|
+
}
|
|
9831
|
+
if (config.keyFrameInterval <= 0) {
|
|
9832
|
+
throw new Error("Key frame interval must be greater than zero");
|
|
9833
|
+
}
|
|
9383
9834
|
logger.debug("[RecorderWorker] handleStart called", {
|
|
9384
9835
|
hasVideoStream: !!videoStream,
|
|
9385
9836
|
hasAudioStream: !!audioStream,
|
|
@@ -9388,7 +9839,9 @@ class RecorderWorker {
|
|
|
9388
9839
|
height: config.height,
|
|
9389
9840
|
fps: config.fps,
|
|
9390
9841
|
bitrate: config.bitrate
|
|
9391
|
-
}
|
|
9842
|
+
},
|
|
9843
|
+
hasOverlayConfig: !!overlayConfig,
|
|
9844
|
+
overlayConfig
|
|
9392
9845
|
});
|
|
9393
9846
|
this.isStopping = false;
|
|
9394
9847
|
this.isFinalized = false;
|
|
@@ -9406,6 +9859,19 @@ class RecorderWorker {
|
|
|
9406
9859
|
this.frameCount = 0;
|
|
9407
9860
|
this.pausedDuration = 0;
|
|
9408
9861
|
this.pauseStartedAt = null;
|
|
9862
|
+
this.overlayConfig = overlayConfig ? { enabled: overlayConfig.enabled, text: overlayConfig.text } : null;
|
|
9863
|
+
this.overlayCanvas = null;
|
|
9864
|
+
this.hiddenIntervals = [];
|
|
9865
|
+
this.currentHiddenIntervalStart = null;
|
|
9866
|
+
this.recordingStartTime = overlayConfig?.recordingStartTime !== undefined ? overlayConfig.recordingStartTime / 1000 : performance.now() / 1000;
|
|
9867
|
+
this.pendingVisibilityUpdates = [];
|
|
9868
|
+
const logData = {
|
|
9869
|
+
hasOverlayConfig: !!this.overlayConfig,
|
|
9870
|
+
overlayEnabled: this.overlayConfig?.enabled,
|
|
9871
|
+
overlayText: this.overlayConfig?.text,
|
|
9872
|
+
recordingStartTime: this.recordingStartTime
|
|
9873
|
+
};
|
|
9874
|
+
logger.debug("[RecorderWorker] Overlay config initialized", logData);
|
|
9409
9875
|
const writable = new WritableStream({
|
|
9410
9876
|
write: (chunk) => {
|
|
9411
9877
|
this.sendChunk(chunk.data, chunk.position);
|
|
@@ -9434,6 +9900,9 @@ class RecorderWorker {
|
|
|
9434
9900
|
this.setupVideoProcessing(videoStream);
|
|
9435
9901
|
}
|
|
9436
9902
|
if (audioStream && config.audioBitrate && config.audioCodec) {
|
|
9903
|
+
if (config.audioBitrate <= 0) {
|
|
9904
|
+
throw new Error("Audio bitrate must be greater than zero");
|
|
9905
|
+
}
|
|
9437
9906
|
this.audioSource = new AudioSampleSource({
|
|
9438
9907
|
codec: config.audioCodec,
|
|
9439
9908
|
bitrate: config.audioBitrate
|
|
@@ -9484,50 +9953,260 @@ class RecorderWorker {
|
|
|
9484
9953
|
if (pausedResult.done) {
|
|
9485
9954
|
return false;
|
|
9486
9955
|
}
|
|
9487
|
-
pausedResult.value
|
|
9956
|
+
if (pausedResult.value) {
|
|
9957
|
+
pausedResult.value.close();
|
|
9958
|
+
}
|
|
9488
9959
|
return true;
|
|
9489
9960
|
}
|
|
9490
9961
|
calculateVideoFrameTimestamp(videoFrame) {
|
|
9491
|
-
|
|
9962
|
+
requireDefined(this.frameRate, "Frame rate must be set");
|
|
9963
|
+
if (this.frameRate <= 0) {
|
|
9964
|
+
throw new Error("Frame rate must be greater than zero");
|
|
9965
|
+
}
|
|
9966
|
+
const rawTs = typeof videoFrame.timestamp === "number" && videoFrame.timestamp !== null ? videoFrame.timestamp / 1e6 : performance.now() / 1000;
|
|
9492
9967
|
if (this.baseVideoTimestamp === null) {
|
|
9493
9968
|
this.baseVideoTimestamp = rawTs;
|
|
9969
|
+
const logData = {
|
|
9970
|
+
baseVideoTimestamp: this.baseVideoTimestamp,
|
|
9971
|
+
recordingStartTime: this.recordingStartTime,
|
|
9972
|
+
difference: this.baseVideoTimestamp - this.recordingStartTime,
|
|
9973
|
+
pendingUpdates: this.pendingVisibilityUpdates.length
|
|
9974
|
+
};
|
|
9975
|
+
logger.debug("[RecorderWorker] baseVideoTimestamp set", logData);
|
|
9976
|
+
for (const update of this.pendingVisibilityUpdates) {
|
|
9977
|
+
this.processVisibilityUpdate(update.isHidden, update.timestamp);
|
|
9978
|
+
}
|
|
9979
|
+
this.pendingVisibilityUpdates = [];
|
|
9980
|
+
}
|
|
9981
|
+
requireNonNull(this.baseVideoTimestamp, "Base video timestamp must be set");
|
|
9982
|
+
if (this.frameCount === 0 && this.lastVideoTimestamp > 0) {
|
|
9983
|
+
const originalBase = this.baseVideoTimestamp;
|
|
9984
|
+
const offset = rawTs - originalBase;
|
|
9985
|
+
this.baseVideoTimestamp = rawTs - this.lastVideoTimestamp;
|
|
9986
|
+
const frameTimestamp2 = this.lastVideoTimestamp;
|
|
9987
|
+
logger.debug("[RecorderWorker] First frame after source switch", {
|
|
9988
|
+
rawTs,
|
|
9989
|
+
originalBase,
|
|
9990
|
+
offset,
|
|
9991
|
+
adjustedBaseVideoTimestamp: this.baseVideoTimestamp,
|
|
9992
|
+
continuationTimestamp: this.lastVideoTimestamp,
|
|
9993
|
+
frameTimestamp: frameTimestamp2,
|
|
9994
|
+
isScreenCapture: this.isScreenCapture
|
|
9995
|
+
});
|
|
9996
|
+
return frameTimestamp2;
|
|
9494
9997
|
}
|
|
9495
|
-
const normalizedTs = rawTs -
|
|
9496
|
-
const prevTs = this.lastVideoTimestamp
|
|
9998
|
+
const normalizedTs = rawTs - this.baseVideoTimestamp - this.pausedDuration;
|
|
9999
|
+
const prevTs = this.lastVideoTimestamp > 0 ? this.lastVideoTimestamp : 0;
|
|
9497
10000
|
const frameTimestamp = normalizedTs >= prevTs ? normalizedTs : prevTs + 1 / this.frameRate;
|
|
10001
|
+
if (frameTimestamp < 0) {
|
|
10002
|
+
logger.warn("[RecorderWorker] Negative frame timestamp detected, clamping to zero", { frameTimestamp, normalizedTs, prevTs });
|
|
10003
|
+
return 0;
|
|
10004
|
+
}
|
|
9498
10005
|
if (this.lastVideoTimestamp === 0) {
|
|
9499
10006
|
this.lastVideoTimestamp = frameTimestamp;
|
|
9500
10007
|
}
|
|
10008
|
+
logger.debug("[RecorderWorker] Frame timestamp calculation", {
|
|
10009
|
+
rawTs,
|
|
10010
|
+
baseVideoTimestamp: this.baseVideoTimestamp,
|
|
10011
|
+
normalizedTs,
|
|
10012
|
+
prevTs,
|
|
10013
|
+
frameTimestamp,
|
|
10014
|
+
lastVideoTimestamp: this.lastVideoTimestamp,
|
|
10015
|
+
isScreenCapture: this.isScreenCapture,
|
|
10016
|
+
frameCount: this.frameCount
|
|
10017
|
+
});
|
|
9501
10018
|
return frameTimestamp;
|
|
9502
10019
|
}
|
|
9503
|
-
|
|
9504
|
-
|
|
10020
|
+
createOverlayCanvas(text) {
|
|
10021
|
+
requireDefined(text, "Overlay text is required");
|
|
10022
|
+
const canvas = new OffscreenCanvas(1, 1);
|
|
10023
|
+
const ctx = requireNonNull(canvas.getContext("2d"), "Failed to get OffscreenCanvas context");
|
|
10024
|
+
ctx.font = \`\${OVERLAY_FONT_SIZE}px \${OVERLAY_FONT_FAMILY}\`;
|
|
10025
|
+
const textMetrics = ctx.measureText(text);
|
|
10026
|
+
const textWidth = textMetrics.width;
|
|
10027
|
+
const textHeight = OVERLAY_FONT_SIZE;
|
|
10028
|
+
const overlayWidth = Math.max(OVERLAY_MIN_WIDTH, textWidth + OVERLAY_PADDING * 2);
|
|
10029
|
+
const overlayHeight = Math.max(OVERLAY_MIN_HEIGHT, textHeight + OVERLAY_PADDING * 2);
|
|
10030
|
+
canvas.width = overlayWidth;
|
|
10031
|
+
canvas.height = overlayHeight;
|
|
10032
|
+
const r = 20;
|
|
10033
|
+
const g = 20;
|
|
10034
|
+
const b = 20;
|
|
10035
|
+
const borderRadius = 50;
|
|
10036
|
+
ctx.fillStyle = \`rgba(\${r}, \${g}, \${b}, \${OVERLAY_BACKGROUND_OPACITY})\`;
|
|
10037
|
+
ctx.beginPath();
|
|
10038
|
+
ctx.roundRect(0, 0, overlayWidth, overlayHeight, borderRadius);
|
|
10039
|
+
ctx.fill();
|
|
10040
|
+
ctx.fillStyle = OVERLAY_TEXT_COLOR;
|
|
10041
|
+
ctx.font = \`\${OVERLAY_FONT_SIZE}px \${OVERLAY_FONT_FAMILY}\`;
|
|
10042
|
+
ctx.textBaseline = "middle";
|
|
10043
|
+
ctx.textAlign = "center";
|
|
10044
|
+
const textX = overlayWidth / 2;
|
|
10045
|
+
const textY = overlayHeight / 2;
|
|
10046
|
+
ctx.fillText(text, textX, textY);
|
|
10047
|
+
return canvas;
|
|
10048
|
+
}
|
|
10049
|
+
getOverlayPosition(overlayWidth, videoWidth) {
|
|
10050
|
+
const padding = OVERLAY_PADDING;
|
|
10051
|
+
return {
|
|
10052
|
+
x: videoWidth - overlayWidth - padding,
|
|
10053
|
+
y: padding
|
|
10054
|
+
};
|
|
10055
|
+
}
|
|
10056
|
+
shouldApplyOverlay(timestamp) {
|
|
10057
|
+
if (!this.overlayConfig?.enabled) {
|
|
10058
|
+
return false;
|
|
10059
|
+
}
|
|
10060
|
+
if (this.isScreenCapture) {
|
|
10061
|
+
return false;
|
|
10062
|
+
}
|
|
10063
|
+
const completedIntervalMatch = this.hiddenIntervals.some((interval) => timestamp >= interval.start && timestamp <= interval.end);
|
|
10064
|
+
const currentIntervalMatch = this.currentHiddenIntervalStart !== null && timestamp >= this.currentHiddenIntervalStart;
|
|
10065
|
+
const shouldApply = completedIntervalMatch || currentIntervalMatch;
|
|
10066
|
+
if (this.frameCount % 90 === 0) {
|
|
10067
|
+
logger.debug("[RecorderWorker] Overlay check", {
|
|
10068
|
+
timestamp,
|
|
10069
|
+
shouldApply,
|
|
10070
|
+
frameCount: this.frameCount,
|
|
10071
|
+
intervalsCount: this.hiddenIntervals.length
|
|
10072
|
+
});
|
|
10073
|
+
}
|
|
10074
|
+
return shouldApply;
|
|
10075
|
+
}
|
|
10076
|
+
handleUpdateVisibility(isHidden, timestamp) {
|
|
10077
|
+
if (this.baseVideoTimestamp === null) {
|
|
10078
|
+
this.pendingVisibilityUpdates.push({ isHidden, timestamp });
|
|
9505
10079
|
return;
|
|
9506
10080
|
}
|
|
10081
|
+
this.processVisibilityUpdate(isHidden, timestamp);
|
|
10082
|
+
}
|
|
10083
|
+
processVisibilityUpdate(isHidden, timestamp) {
|
|
10084
|
+
const timestampSeconds = timestamp / 1000;
|
|
10085
|
+
const normalizedTimestamp = timestampSeconds - this.recordingStartTime - this.pausedDuration;
|
|
10086
|
+
if (isHidden) {
|
|
10087
|
+
if (this.currentHiddenIntervalStart === null) {
|
|
10088
|
+
this.currentHiddenIntervalStart = Math.max(0, normalizedTimestamp);
|
|
10089
|
+
logger.debug("[RecorderWorker] Started hidden interval", {
|
|
10090
|
+
start: this.currentHiddenIntervalStart
|
|
10091
|
+
});
|
|
10092
|
+
}
|
|
10093
|
+
} else if (this.currentHiddenIntervalStart !== null) {
|
|
10094
|
+
const endTimestamp = Math.max(0, normalizedTimestamp);
|
|
10095
|
+
if (endTimestamp > this.currentHiddenIntervalStart) {
|
|
10096
|
+
const interval = {
|
|
10097
|
+
start: this.currentHiddenIntervalStart,
|
|
10098
|
+
end: endTimestamp
|
|
10099
|
+
};
|
|
10100
|
+
this.hiddenIntervals.push(interval);
|
|
10101
|
+
logger.debug("[RecorderWorker] Completed hidden interval", {
|
|
10102
|
+
interval,
|
|
10103
|
+
duration: endTimestamp - this.currentHiddenIntervalStart,
|
|
10104
|
+
totalIntervals: this.hiddenIntervals.length
|
|
10105
|
+
});
|
|
10106
|
+
} else {
|
|
10107
|
+
logger.warn("[RecorderWorker] Invalid interval (end <= start), discarding");
|
|
10108
|
+
}
|
|
10109
|
+
this.currentHiddenIntervalStart = null;
|
|
10110
|
+
}
|
|
10111
|
+
}
|
|
10112
|
+
async processVideoFrame(videoFrame) {
|
|
10113
|
+
const videoSource = requireInitialized(this.videoSource, "Video source");
|
|
10114
|
+
const config = requireInitialized(this.config, "Transcode config");
|
|
9507
10115
|
const frameTimestamp = this.calculateVideoFrameTimestamp(videoFrame);
|
|
10116
|
+
requireDefined(this.frameRate, "Frame rate must be set");
|
|
10117
|
+
if (this.frameRate <= 0) {
|
|
10118
|
+
throw new Error("Frame rate must be greater than zero");
|
|
10119
|
+
}
|
|
9508
10120
|
const frameDuration = 1 / this.frameRate;
|
|
9509
|
-
|
|
9510
|
-
|
|
10121
|
+
let frameToProcess = videoFrame;
|
|
10122
|
+
let imageBitmap = null;
|
|
10123
|
+
if (this.shouldApplyOverlay(frameTimestamp)) {
|
|
10124
|
+
const width = videoFrame.displayWidth;
|
|
10125
|
+
const height = videoFrame.displayHeight;
|
|
10126
|
+
if (width <= 0 || height <= 0) {
|
|
10127
|
+
logger.warn("[RecorderWorker] Invalid video frame dimensions, skipping overlay", { width, height });
|
|
10128
|
+
} else if (this.overlayConfig) {
|
|
10129
|
+
if (!this.overlayCanvas) {
|
|
10130
|
+
this.overlayCanvas = this.createOverlayCanvas(this.overlayConfig.text);
|
|
10131
|
+
logger.debug("[RecorderWorker] Overlay canvas created", {
|
|
10132
|
+
overlayWidth: this.overlayCanvas.width,
|
|
10133
|
+
overlayHeight: this.overlayCanvas.height
|
|
10134
|
+
});
|
|
10135
|
+
}
|
|
10136
|
+
if (this.overlayCanvas) {
|
|
10137
|
+
if (!(this.compositionCanvas && this.compositionCtx) || this.compositionCanvas.width !== width || this.compositionCanvas.height !== height) {
|
|
10138
|
+
this.compositionCanvas = new OffscreenCanvas(width, height);
|
|
10139
|
+
this.compositionCtx = requireNonNull(this.compositionCanvas.getContext("2d"), "Failed to get composition canvas context");
|
|
10140
|
+
}
|
|
10141
|
+
requireNonNull(this.compositionCtx, "Composition context must be available");
|
|
10142
|
+
this.compositionCtx.clearRect(0, 0, width, height);
|
|
10143
|
+
this.compositionCtx.drawImage(videoFrame, 0, 0, width, height);
|
|
10144
|
+
const position = this.getOverlayPosition(this.overlayCanvas.width, width);
|
|
10145
|
+
this.compositionCtx.drawImage(this.overlayCanvas, position.x, position.y);
|
|
10146
|
+
imageBitmap = this.compositionCanvas.transferToImageBitmap();
|
|
10147
|
+
const frameInit = {};
|
|
10148
|
+
if (typeof videoFrame.timestamp === "number" && videoFrame.timestamp !== null) {
|
|
10149
|
+
frameInit.timestamp = videoFrame.timestamp;
|
|
10150
|
+
}
|
|
10151
|
+
if (typeof videoFrame.duration === "number" && videoFrame.duration !== null) {
|
|
10152
|
+
frameInit.duration = videoFrame.duration;
|
|
10153
|
+
}
|
|
10154
|
+
frameToProcess = new VideoFrame(imageBitmap, frameInit);
|
|
10155
|
+
}
|
|
10156
|
+
}
|
|
10157
|
+
}
|
|
10158
|
+
const keyFrameInterval = config.keyFrameInterval > 0 ? config.keyFrameInterval : 5;
|
|
10159
|
+
const isKeyFrame = this.frameCount % keyFrameInterval === 0;
|
|
10160
|
+
const maxLead = 0.05;
|
|
10161
|
+
const maxLag = 0.1;
|
|
10162
|
+
const targetAudio = this.lastAudioTimestamp;
|
|
10163
|
+
let adjustedTimestamp = frameTimestamp + this.driftOffset;
|
|
10164
|
+
if (adjustedTimestamp - targetAudio > maxLead) {
|
|
10165
|
+
adjustedTimestamp = targetAudio + maxLead;
|
|
10166
|
+
} else if (targetAudio - adjustedTimestamp > maxLag) {
|
|
10167
|
+
adjustedTimestamp = targetAudio - maxLag;
|
|
10168
|
+
}
|
|
10169
|
+
const monotonicTimestamp = this.lastVideoTimestamp + frameDuration;
|
|
10170
|
+
const finalTimestamp = adjustedTimestamp >= monotonicTimestamp ? adjustedTimestamp : monotonicTimestamp;
|
|
10171
|
+
this.driftOffset *= 0.5;
|
|
10172
|
+
const sample = new VideoSample(frameToProcess, {
|
|
10173
|
+
timestamp: finalTimestamp,
|
|
9511
10174
|
duration: frameDuration
|
|
9512
10175
|
});
|
|
9513
|
-
const
|
|
9514
|
-
|
|
9515
|
-
await this.videoSource.add(sample, isKeyFrame ? { keyFrame: true } : undefined);
|
|
9516
|
-
this.frameCount += 1;
|
|
9517
|
-
this.lastVideoTimestamp = frameTimestamp;
|
|
9518
|
-
} catch (error) {
|
|
9519
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
10176
|
+
const addError = await videoSource.add(sample, isKeyFrame ? { keyFrame: true } : undefined).then(() => null).catch((error) => {
|
|
10177
|
+
const errorMessage = extractErrorMessage(error);
|
|
9520
10178
|
this.sendError(new Error(\`Failed to add video frame: \${errorMessage}\`));
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
10179
|
+
return error;
|
|
10180
|
+
});
|
|
10181
|
+
sample.close();
|
|
10182
|
+
if (!addError) {
|
|
10183
|
+
this.frameCount += 1;
|
|
10184
|
+
this.lastVideoTimestamp = finalTimestamp;
|
|
10185
|
+
if (this.frameCount % 90 === 0 && this.audioProcessingActive) {
|
|
10186
|
+
const avDrift = this.lastAudioTimestamp - this.lastVideoTimestamp;
|
|
10187
|
+
logger.debug("[RecorderWorker] AV drift metrics", {
|
|
10188
|
+
frameCount: this.frameCount,
|
|
10189
|
+
lastAudioTimestamp: this.lastAudioTimestamp,
|
|
10190
|
+
lastVideoTimestamp: this.lastVideoTimestamp,
|
|
10191
|
+
avDrift,
|
|
10192
|
+
isScreenCapture: this.isScreenCapture
|
|
10193
|
+
});
|
|
10194
|
+
}
|
|
9524
10195
|
}
|
|
10196
|
+
if (imageBitmap) {
|
|
10197
|
+
imageBitmap.close();
|
|
10198
|
+
imageBitmap = null;
|
|
10199
|
+
}
|
|
10200
|
+
if (frameToProcess !== videoFrame) {
|
|
10201
|
+
frameToProcess.close();
|
|
10202
|
+
}
|
|
10203
|
+
videoFrame.close();
|
|
9525
10204
|
}
|
|
9526
10205
|
async processVideoFrames() {
|
|
9527
10206
|
if (!(this.videoProcessor && this.videoSource)) {
|
|
9528
10207
|
return;
|
|
9529
10208
|
}
|
|
9530
|
-
while (this.videoProcessingActive) {
|
|
10209
|
+
while (this.videoProcessingActive && !this.isStopping) {
|
|
9531
10210
|
if (this.isPaused) {
|
|
9532
10211
|
const shouldContinue = await this.handlePausedVideoFrame();
|
|
9533
10212
|
if (!shouldContinue) {
|
|
@@ -9543,15 +10222,30 @@ class RecorderWorker {
|
|
|
9543
10222
|
if (!videoFrame) {
|
|
9544
10223
|
continue;
|
|
9545
10224
|
}
|
|
9546
|
-
await this.processVideoFrame(videoFrame)
|
|
10225
|
+
await this.processVideoFrame(videoFrame).catch((error) => {
|
|
10226
|
+
const errorMessage = extractErrorMessage(error);
|
|
10227
|
+
logger.error("[RecorderWorker] Error processing video frame", errorMessage);
|
|
10228
|
+
videoFrame.close();
|
|
10229
|
+
});
|
|
9547
10230
|
}
|
|
9548
10231
|
}
|
|
9549
10232
|
setupAudioProcessing(audioStream) {
|
|
9550
10233
|
if (!this.audioSource) {
|
|
10234
|
+
logger.warn("[RecorderWorker] setupAudioProcessing called but audioSource is null");
|
|
9551
10235
|
return;
|
|
9552
10236
|
}
|
|
10237
|
+
logger.debug("[RecorderWorker] setupAudioProcessing", {
|
|
10238
|
+
hasAudioSource: !!this.audioSource,
|
|
10239
|
+
hasAudioStream: !!audioStream,
|
|
10240
|
+
audioProcessingActive: this.audioProcessingActive,
|
|
10241
|
+
lastAudioTimestamp: this.lastAudioTimestamp
|
|
10242
|
+
});
|
|
9553
10243
|
this.audioProcessor = audioStream.getReader();
|
|
9554
10244
|
this.audioProcessingActive = true;
|
|
10245
|
+
logger.debug("[RecorderWorker] Audio processing started", {
|
|
10246
|
+
hasAudioProcessor: !!this.audioProcessor,
|
|
10247
|
+
audioProcessingActive: this.audioProcessingActive
|
|
10248
|
+
});
|
|
9555
10249
|
this.processAudioData();
|
|
9556
10250
|
}
|
|
9557
10251
|
handlePausedAudioData(audioData) {
|
|
@@ -9559,16 +10253,25 @@ class RecorderWorker {
|
|
|
9559
10253
|
}
|
|
9560
10254
|
createAudioBuffer(audioData) {
|
|
9561
10255
|
const numberOfFrames = audioData.numberOfFrames;
|
|
10256
|
+
if (numberOfFrames <= 0) {
|
|
10257
|
+
throw new Error("Number of frames must be greater than zero");
|
|
10258
|
+
}
|
|
9562
10259
|
const numberOfChannels = audioData.numberOfChannels;
|
|
10260
|
+
if (numberOfChannels <= 0) {
|
|
10261
|
+
throw new Error("Number of channels must be greater than zero");
|
|
10262
|
+
}
|
|
9563
10263
|
const audioBuffer = new Float32Array(numberOfFrames * numberOfChannels);
|
|
9564
10264
|
audioData.copyTo(audioBuffer, { planeIndex: 0 });
|
|
9565
10265
|
return audioBuffer;
|
|
9566
10266
|
}
|
|
9567
10267
|
createAudioSample(audioData, audioBuffer) {
|
|
9568
10268
|
const sampleRate = audioData.sampleRate;
|
|
10269
|
+
if (sampleRate <= 0) {
|
|
10270
|
+
throw new Error("Sample rate must be greater than zero");
|
|
10271
|
+
}
|
|
9569
10272
|
const numberOfChannels = audioData.numberOfChannels;
|
|
9570
|
-
if (
|
|
9571
|
-
|
|
10273
|
+
if (numberOfChannels <= 0) {
|
|
10274
|
+
throw new Error("Number of channels must be greater than zero");
|
|
9572
10275
|
}
|
|
9573
10276
|
const shouldWriteSilence = this.isMuted;
|
|
9574
10277
|
return new AudioSample({
|
|
@@ -9580,37 +10283,73 @@ class RecorderWorker {
|
|
|
9580
10283
|
});
|
|
9581
10284
|
}
|
|
9582
10285
|
async processAudioSample(audioData, audioSample) {
|
|
9583
|
-
|
|
9584
|
-
return;
|
|
9585
|
-
}
|
|
10286
|
+
const audioSource = requireInitialized(this.audioSource, "Audio source");
|
|
9586
10287
|
const sampleRate = audioData.sampleRate;
|
|
10288
|
+
if (sampleRate <= 0) {
|
|
10289
|
+
throw new Error("Sample rate must be greater than zero");
|
|
10290
|
+
}
|
|
9587
10291
|
const numberOfFrames = audioData.numberOfFrames;
|
|
9588
10292
|
const duration = numberOfFrames / sampleRate;
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
this.lastAudioTimestamp += duration;
|
|
9592
|
-
} catch (error) {
|
|
9593
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
10293
|
+
await audioSource.add(audioSample).catch((error) => {
|
|
10294
|
+
const errorMessage = extractErrorMessage(error);
|
|
9594
10295
|
this.sendError(new Error(\`Failed to add audio sample: \${errorMessage}\`));
|
|
9595
|
-
}
|
|
9596
|
-
|
|
9597
|
-
|
|
9598
|
-
|
|
10296
|
+
});
|
|
10297
|
+
logger.debug("[RecorderWorker] Audio sample processed", {
|
|
10298
|
+
lastAudioTimestamp: this.lastAudioTimestamp,
|
|
10299
|
+
duration,
|
|
10300
|
+
newLastAudioTimestamp: this.lastAudioTimestamp + duration,
|
|
10301
|
+
sampleRate: audioData.sampleRate,
|
|
10302
|
+
numberOfFrames: audioData.numberOfFrames
|
|
10303
|
+
});
|
|
10304
|
+
this.lastAudioTimestamp += duration;
|
|
10305
|
+
audioSample.close();
|
|
10306
|
+
audioData.close();
|
|
9599
10307
|
}
|
|
9600
10308
|
async processAudioData() {
|
|
9601
10309
|
if (!(this.audioProcessor && this.audioSource)) {
|
|
10310
|
+
logger.warn("[RecorderWorker] processAudioData called but processor or source is null", {
|
|
10311
|
+
hasAudioProcessor: !!this.audioProcessor,
|
|
10312
|
+
hasAudioSource: !!this.audioSource
|
|
10313
|
+
});
|
|
9602
10314
|
return;
|
|
9603
10315
|
}
|
|
10316
|
+
logger.debug("[RecorderWorker] processAudioData loop started", {
|
|
10317
|
+
hasAudioProcessor: !!this.audioProcessor,
|
|
10318
|
+
hasAudioSource: !!this.audioSource,
|
|
10319
|
+
audioProcessingActive: this.audioProcessingActive,
|
|
10320
|
+
isPaused: this.isPaused,
|
|
10321
|
+
isMuted: this.isMuted,
|
|
10322
|
+
lastAudioTimestamp: this.lastAudioTimestamp
|
|
10323
|
+
});
|
|
10324
|
+
let audioSampleCount = 0;
|
|
9604
10325
|
while (this.audioProcessingActive) {
|
|
9605
10326
|
const result = await this.audioProcessor.read();
|
|
9606
10327
|
if (result.done) {
|
|
10328
|
+
logger.debug("[RecorderWorker] Audio processor stream ended", {
|
|
10329
|
+
audioSampleCount,
|
|
10330
|
+
lastAudioTimestamp: this.lastAudioTimestamp,
|
|
10331
|
+
audioProcessingActive: this.audioProcessingActive
|
|
10332
|
+
});
|
|
9607
10333
|
this.audioProcessingActive = false;
|
|
9608
10334
|
break;
|
|
9609
10335
|
}
|
|
9610
10336
|
const audioData = result.value;
|
|
9611
10337
|
if (!audioData) {
|
|
10338
|
+
logger.warn("[RecorderWorker] Received null audioData from processor");
|
|
9612
10339
|
continue;
|
|
9613
10340
|
}
|
|
10341
|
+
audioSampleCount += 1;
|
|
10342
|
+
if (audioSampleCount % 100 === 0) {
|
|
10343
|
+
logger.debug("[RecorderWorker] Processing audio sample", {
|
|
10344
|
+
sampleCount: audioSampleCount,
|
|
10345
|
+
numberOfFrames: audioData.numberOfFrames,
|
|
10346
|
+
sampleRate: audioData.sampleRate,
|
|
10347
|
+
numberOfChannels: audioData.numberOfChannels,
|
|
10348
|
+
lastAudioTimestamp: this.lastAudioTimestamp,
|
|
10349
|
+
isPaused: this.isPaused,
|
|
10350
|
+
isMuted: this.isMuted
|
|
10351
|
+
});
|
|
10352
|
+
}
|
|
9614
10353
|
if (this.isPaused) {
|
|
9615
10354
|
this.handlePausedAudioData(audioData);
|
|
9616
10355
|
continue;
|
|
@@ -9619,6 +10358,11 @@ class RecorderWorker {
|
|
|
9619
10358
|
const audioSample = this.createAudioSample(audioData, audioBuffer);
|
|
9620
10359
|
await this.processAudioSample(audioData, audioSample);
|
|
9621
10360
|
}
|
|
10361
|
+
logger.debug("[RecorderWorker] processAudioData loop ended", {
|
|
10362
|
+
audioSampleCount,
|
|
10363
|
+
lastAudioTimestamp: this.lastAudioTimestamp,
|
|
10364
|
+
audioProcessingActive: this.audioProcessingActive
|
|
10365
|
+
});
|
|
9622
10366
|
}
|
|
9623
10367
|
handlePause() {
|
|
9624
10368
|
if (this.isPaused) {
|
|
@@ -9658,11 +10402,9 @@ class RecorderWorker {
|
|
|
9658
10402
|
this.audioProcessor = null;
|
|
9659
10403
|
}
|
|
9660
10404
|
if (this.output) {
|
|
9661
|
-
|
|
9662
|
-
await this.output.finalize();
|
|
9663
|
-
} catch (error) {
|
|
10405
|
+
await this.output.finalize().catch((error) => {
|
|
9664
10406
|
logger.warn("[RecorderWorker] finalize failed (ignored, already finalized?)", error);
|
|
9665
|
-
}
|
|
10407
|
+
});
|
|
9666
10408
|
}
|
|
9667
10409
|
await this.cleanup();
|
|
9668
10410
|
this.sendStateChange("stopped");
|
|
@@ -9672,6 +10414,9 @@ class RecorderWorker {
|
|
|
9672
10414
|
this.isMuted = !this.isMuted;
|
|
9673
10415
|
}
|
|
9674
10416
|
handleUpdateFps(fps) {
|
|
10417
|
+
if (fps <= 0) {
|
|
10418
|
+
throw new Error("Frame rate must be greater than zero");
|
|
10419
|
+
}
|
|
9675
10420
|
logger.debug("[RecorderWorker] Updating FPS", {
|
|
9676
10421
|
fps,
|
|
9677
10422
|
previousFps: this.frameRate
|
|
@@ -9681,16 +10426,51 @@ class RecorderWorker {
|
|
|
9681
10426
|
this.config.fps = fps;
|
|
9682
10427
|
}
|
|
9683
10428
|
}
|
|
10429
|
+
handleUpdateSourceType(isScreenCapture) {
|
|
10430
|
+
logger.debug("[RecorderWorker] Updating source type", {
|
|
10431
|
+
isScreenCapture,
|
|
10432
|
+
previousIsScreenCapture: this.isScreenCapture
|
|
10433
|
+
});
|
|
10434
|
+
this.isScreenCapture = isScreenCapture;
|
|
10435
|
+
}
|
|
9684
10436
|
async handleSwitchSource(videoStream) {
|
|
10437
|
+
requireDefined(videoStream, "Video stream is required");
|
|
10438
|
+
requireDefined(this.frameRate, "Frame rate must be set");
|
|
10439
|
+
if (this.frameRate <= 0) {
|
|
10440
|
+
throw new Error("Frame rate must be greater than zero");
|
|
10441
|
+
}
|
|
9685
10442
|
if (this.videoProcessor) {
|
|
9686
10443
|
this.videoProcessingActive = false;
|
|
9687
10444
|
await this.videoProcessor.cancel();
|
|
10445
|
+
let drainResult = await this.videoProcessor.read().catch(() => ({ done: true }));
|
|
10446
|
+
while (!drainResult.done) {
|
|
10447
|
+
drainResult.value?.close();
|
|
10448
|
+
drainResult = await this.videoProcessor.read().catch(() => ({ done: true }));
|
|
10449
|
+
}
|
|
9688
10450
|
this.videoProcessor = null;
|
|
9689
10451
|
}
|
|
9690
|
-
|
|
10452
|
+
requireNonNull(this.baseVideoTimestamp, "Base video timestamp must be set for source switch");
|
|
10453
|
+
const minFrameDuration = 1 / this.frameRate;
|
|
10454
|
+
const rawDrift = this.lastAudioTimestamp - this.lastVideoTimestamp;
|
|
10455
|
+
const maxDriftCorrection = 0.1;
|
|
10456
|
+
this.driftOffset = Math.max(-maxDriftCorrection, Math.min(maxDriftCorrection, rawDrift));
|
|
10457
|
+
const continuationTimestamp = Math.max(this.lastAudioTimestamp, this.lastVideoTimestamp) + minFrameDuration;
|
|
10458
|
+
const previousVideoTimestamp = this.lastVideoTimestamp;
|
|
9691
10459
|
this.lastVideoTimestamp = continuationTimestamp;
|
|
9692
10460
|
this.frameCount = 0;
|
|
9693
|
-
|
|
10461
|
+
logger.debug("[RecorderWorker] handleSwitchSource - preserving baseVideoTimestamp", {
|
|
10462
|
+
continuationTimestamp,
|
|
10463
|
+
lastVideoTimestamp: this.lastVideoTimestamp,
|
|
10464
|
+
frameRate: this.frameRate,
|
|
10465
|
+
isScreenCapture: this.isScreenCapture,
|
|
10466
|
+
baseVideoTimestamp: this.baseVideoTimestamp,
|
|
10467
|
+
recordingStartTime: this.recordingStartTime,
|
|
10468
|
+
lastAudioTimestamp: this.lastAudioTimestamp,
|
|
10469
|
+
previousVideoTimestamp,
|
|
10470
|
+
minFrameDuration,
|
|
10471
|
+
rawDrift,
|
|
10472
|
+
driftOffset: this.driftOffset
|
|
10473
|
+
});
|
|
9694
10474
|
this.setupVideoProcessing(videoStream);
|
|
9695
10475
|
}
|
|
9696
10476
|
async cleanup() {
|
|
@@ -9719,11 +10499,9 @@ class RecorderWorker {
|
|
|
9719
10499
|
}
|
|
9720
10500
|
if (this.output) {
|
|
9721
10501
|
if (!this.isFinalized) {
|
|
9722
|
-
|
|
9723
|
-
await this.output.cancel();
|
|
9724
|
-
} catch (error) {
|
|
10502
|
+
await this.output.cancel().catch((error) => {
|
|
9725
10503
|
logger.warn("[RecorderWorker] cancel failed (ignored, possibly finalized)", error);
|
|
9726
|
-
}
|
|
10504
|
+
});
|
|
9727
10505
|
this.isFinalized = true;
|
|
9728
10506
|
}
|
|
9729
10507
|
this.output = null;
|
|
@@ -9735,13 +10513,20 @@ class RecorderWorker {
|
|
|
9735
10513
|
this.totalSize = 0;
|
|
9736
10514
|
this.pausedDuration = 0;
|
|
9737
10515
|
this.pauseStartedAt = null;
|
|
10516
|
+
this.overlayCanvas = null;
|
|
10517
|
+
this.overlayConfig = null;
|
|
10518
|
+
this.hiddenIntervals = [];
|
|
10519
|
+
this.currentHiddenIntervalStart = null;
|
|
10520
|
+
this.recordingStartTime = 0;
|
|
10521
|
+
this.pendingVisibilityUpdates = [];
|
|
10522
|
+
this.isScreenCapture = false;
|
|
9738
10523
|
}
|
|
9739
10524
|
sendReady() {
|
|
9740
10525
|
const response = { type: "ready" };
|
|
9741
10526
|
self.postMessage(response);
|
|
9742
10527
|
}
|
|
9743
10528
|
sendError(error) {
|
|
9744
|
-
const errorMessage =
|
|
10529
|
+
const errorMessage = extractErrorMessage(error);
|
|
9745
10530
|
const response = {
|
|
9746
10531
|
type: "error",
|
|
9747
10532
|
error: errorMessage
|
|
@@ -9808,6 +10593,7 @@ class WorkerProcessor {
|
|
|
9808
10593
|
isMuted = false;
|
|
9809
10594
|
currentVideoTrack = null;
|
|
9810
10595
|
isPaused = false;
|
|
10596
|
+
overlayConfig = null;
|
|
9811
10597
|
constructor() {
|
|
9812
10598
|
this.setupWorker();
|
|
9813
10599
|
}
|
|
@@ -9883,7 +10669,7 @@ class WorkerProcessor {
|
|
|
9883
10669
|
this.onError(new Error(error.message || "Unknown worker error"));
|
|
9884
10670
|
}
|
|
9885
10671
|
}
|
|
9886
|
-
async startProcessing(stream, config) {
|
|
10672
|
+
async startProcessing(stream, config, overlayConfig) {
|
|
9887
10673
|
if (!this.worker) {
|
|
9888
10674
|
throw new Error("Worker not initialized");
|
|
9889
10675
|
}
|
|
@@ -9895,10 +10681,11 @@ class WorkerProcessor {
|
|
|
9895
10681
|
this.isPaused = false;
|
|
9896
10682
|
this.chunks = [];
|
|
9897
10683
|
this.totalSize = 0;
|
|
10684
|
+
this.overlayConfig = overlayConfig || null;
|
|
9898
10685
|
const format = config.format || "mp4";
|
|
9899
10686
|
const audioCodec = config.audioCodec || getDefaultAudioCodecForFormat(format);
|
|
9900
10687
|
const isScreenCapture = isScreenCaptureStream(stream);
|
|
9901
|
-
const targetFps =
|
|
10688
|
+
const targetFps = config.fps;
|
|
9902
10689
|
logger.debug("[WorkerProcessor] Starting processing", {
|
|
9903
10690
|
isScreenCapture,
|
|
9904
10691
|
targetFps,
|
|
@@ -9950,7 +10737,8 @@ class WorkerProcessor {
|
|
|
9950
10737
|
type: "start",
|
|
9951
10738
|
videoStream,
|
|
9952
10739
|
audioStream,
|
|
9953
|
-
config: workerConfig
|
|
10740
|
+
config: workerConfig,
|
|
10741
|
+
overlayConfig: this.overlayConfig || undefined
|
|
9954
10742
|
};
|
|
9955
10743
|
const transferables = [];
|
|
9956
10744
|
if (videoStream) {
|
|
@@ -10021,7 +10809,7 @@ class WorkerProcessor {
|
|
|
10021
10809
|
return Promise.resolve();
|
|
10022
10810
|
}
|
|
10023
10811
|
const isScreenCapture = isScreenCaptureStream(newStream);
|
|
10024
|
-
const targetFps =
|
|
10812
|
+
const targetFps = 30;
|
|
10025
10813
|
logger.debug("[WorkerProcessor] Source type detected", {
|
|
10026
10814
|
isScreenCapture,
|
|
10027
10815
|
targetFps
|
|
@@ -10031,6 +10819,11 @@ class WorkerProcessor {
|
|
|
10031
10819
|
fps: targetFps
|
|
10032
10820
|
};
|
|
10033
10821
|
this.worker.postMessage(fpsUpdateMessage);
|
|
10822
|
+
const sourceTypeMessage = {
|
|
10823
|
+
type: "updateSourceType",
|
|
10824
|
+
isScreenCapture
|
|
10825
|
+
};
|
|
10826
|
+
this.worker.postMessage(sourceTypeMessage);
|
|
10034
10827
|
const originalTrack = videoTracks[0];
|
|
10035
10828
|
this.stopCurrentVideoTrack();
|
|
10036
10829
|
const videoTrack = this.cloneVideoTrack(originalTrack);
|
|
@@ -10126,6 +10919,43 @@ class WorkerProcessor {
|
|
|
10126
10919
|
getMutedState() {
|
|
10127
10920
|
return this.isMuted;
|
|
10128
10921
|
}
|
|
10922
|
+
updateTabVisibility(isHidden, timestamp) {
|
|
10923
|
+
if (!(this.isWorkerActive() && this.worker)) {
|
|
10924
|
+
logger.warn("[WorkerProcessor] Cannot update visibility - worker not active", {
|
|
10925
|
+
isActive: this.isActive,
|
|
10926
|
+
hasWorker: !!this.worker
|
|
10927
|
+
});
|
|
10928
|
+
return;
|
|
10929
|
+
}
|
|
10930
|
+
logger.debug("[WorkerProcessor] Sending visibility update to worker", {
|
|
10931
|
+
isHidden,
|
|
10932
|
+
timestamp,
|
|
10933
|
+
timestampSeconds: timestamp / 1000
|
|
10934
|
+
});
|
|
10935
|
+
const message = {
|
|
10936
|
+
type: "updateVisibility",
|
|
10937
|
+
isHidden,
|
|
10938
|
+
timestamp
|
|
10939
|
+
};
|
|
10940
|
+
this.worker.postMessage(message);
|
|
10941
|
+
}
|
|
10942
|
+
updateSourceType(isScreenCapture) {
|
|
10943
|
+
if (!(this.isWorkerActive() && this.worker)) {
|
|
10944
|
+
logger.warn("[WorkerProcessor] Cannot update source type - worker not active", {
|
|
10945
|
+
isActive: this.isActive,
|
|
10946
|
+
hasWorker: !!this.worker
|
|
10947
|
+
});
|
|
10948
|
+
return;
|
|
10949
|
+
}
|
|
10950
|
+
logger.debug("[WorkerProcessor] Sending source type update to worker", {
|
|
10951
|
+
isScreenCapture
|
|
10952
|
+
});
|
|
10953
|
+
const sourceTypeMessage = {
|
|
10954
|
+
type: "updateSourceType",
|
|
10955
|
+
isScreenCapture
|
|
10956
|
+
};
|
|
10957
|
+
this.worker.postMessage(sourceTypeMessage);
|
|
10958
|
+
}
|
|
10129
10959
|
isPausedState() {
|
|
10130
10960
|
return this.isPaused;
|
|
10131
10961
|
}
|
|
@@ -10220,7 +11050,7 @@ class WorkerProcessor {
|
|
|
10220
11050
|
this.totalSize = 0;
|
|
10221
11051
|
}
|
|
10222
11052
|
static isSupported() {
|
|
10223
|
-
return typeof Worker !== "undefined" && typeof MediaStreamTrackProcessor !== "undefined" && typeof VideoFrame !== "undefined" && typeof AudioData !== "undefined";
|
|
11053
|
+
return typeof Worker !== "undefined" && typeof MediaStreamTrackProcessor !== "undefined" && typeof VideoFrame !== "undefined" && typeof AudioData !== "undefined" && typeof OffscreenCanvas !== "undefined";
|
|
10224
11054
|
}
|
|
10225
11055
|
}
|
|
10226
11056
|
|
|
@@ -10241,7 +11071,7 @@ class StreamProcessor {
|
|
|
10241
11071
|
throw new Error(`Failed to initialize worker: ${errorMessage}. Web Workers are required for video processing.`);
|
|
10242
11072
|
}
|
|
10243
11073
|
}
|
|
10244
|
-
async startProcessing(stream, config) {
|
|
11074
|
+
async startProcessing(stream, config, overlayConfig) {
|
|
10245
11075
|
this.workerProcessor.setOnBufferUpdate((size, formatted) => {
|
|
10246
11076
|
logger.debug("[StreamProcessor] Buffer update:", { size, formatted });
|
|
10247
11077
|
});
|
|
@@ -10249,7 +11079,13 @@ class StreamProcessor {
|
|
|
10249
11079
|
logger.error("[StreamProcessor] Worker error:", error);
|
|
10250
11080
|
});
|
|
10251
11081
|
this.currentVideoStream = stream;
|
|
10252
|
-
await this.workerProcessor.startProcessing(stream, config);
|
|
11082
|
+
await this.workerProcessor.startProcessing(stream, config, overlayConfig);
|
|
11083
|
+
}
|
|
11084
|
+
updateTabVisibility(isHidden, timestamp) {
|
|
11085
|
+
this.workerProcessor.updateTabVisibility(isHidden, timestamp);
|
|
11086
|
+
}
|
|
11087
|
+
updateSourceType(isScreenCapture) {
|
|
11088
|
+
this.workerProcessor.updateSourceType(isScreenCapture);
|
|
10253
11089
|
}
|
|
10254
11090
|
pause() {
|
|
10255
11091
|
this.workerProcessor.pause();
|
|
@@ -10329,6 +11165,8 @@ class RecordingManager {
|
|
|
10329
11165
|
callbacks;
|
|
10330
11166
|
streamProcessor = null;
|
|
10331
11167
|
originalCameraStream = null;
|
|
11168
|
+
enableTabVisibilityOverlay = false;
|
|
11169
|
+
tabVisibilityOverlayText;
|
|
10332
11170
|
constructor(streamManager, callbacks) {
|
|
10333
11171
|
this.streamManager = streamManager;
|
|
10334
11172
|
this.callbacks = callbacks;
|
|
@@ -10339,6 +11177,10 @@ class RecordingManager {
|
|
|
10339
11177
|
setMaxRecordingTime(maxTime) {
|
|
10340
11178
|
this.maxRecordingTime = maxTime;
|
|
10341
11179
|
}
|
|
11180
|
+
setTabVisibilityOverlayConfig(enabled, text) {
|
|
11181
|
+
this.enableTabVisibilityOverlay = enabled;
|
|
11182
|
+
this.tabVisibilityOverlayText = text;
|
|
11183
|
+
}
|
|
10342
11184
|
getRecordingState() {
|
|
10343
11185
|
return this.recordingState;
|
|
10344
11186
|
}
|
|
@@ -10351,6 +11193,12 @@ class RecordingManager {
|
|
|
10351
11193
|
getStreamProcessor() {
|
|
10352
11194
|
return this.streamProcessor;
|
|
10353
11195
|
}
|
|
11196
|
+
updateSourceType(isScreenCapture) {
|
|
11197
|
+
if (this.recordingState !== RECORDING_STATE_RECORDING || !this.streamProcessor) {
|
|
11198
|
+
return;
|
|
11199
|
+
}
|
|
11200
|
+
this.streamProcessor.updateSourceType(isScreenCapture);
|
|
11201
|
+
}
|
|
10354
11202
|
setOriginalCameraStream(stream) {
|
|
10355
11203
|
this.originalCameraStream = stream;
|
|
10356
11204
|
}
|
|
@@ -10426,7 +11274,7 @@ class RecordingManager {
|
|
|
10426
11274
|
return;
|
|
10427
11275
|
}
|
|
10428
11276
|
logger.debug("[RecordingManager] Starting recording with stream manager");
|
|
10429
|
-
const recordingError = await this.streamManager.startRecording(this.streamProcessor, configResult.config).then(() => {
|
|
11277
|
+
const recordingError = await this.streamManager.startRecording(this.streamProcessor, configResult.config, this.enableTabVisibilityOverlay, this.tabVisibilityOverlayText).then(() => {
|
|
10430
11278
|
logger.info("[RecordingManager] Recording started successfully");
|
|
10431
11279
|
return null;
|
|
10432
11280
|
}).catch((error) => {
|
|
@@ -10459,14 +11307,15 @@ class RecordingManager {
|
|
|
10459
11307
|
this.resetPauseState();
|
|
10460
11308
|
this.callbacks.onStopAudioTracking();
|
|
10461
11309
|
logger.debug("[RecordingManager] Stopping recording in stream manager");
|
|
10462
|
-
const
|
|
10463
|
-
|
|
11310
|
+
const stopResult = await this.streamManager.stopRecording();
|
|
11311
|
+
const finalBlob = stopResult.blob;
|
|
11312
|
+
logger.info("[RecordingManager] Recording stopped, blob size:", finalBlob.size);
|
|
10464
11313
|
this.recordingState = RECORDING_STATE_IDLE;
|
|
10465
11314
|
this.callbacks.onStateChange(this.recordingState);
|
|
10466
11315
|
this.recordingSeconds = 0;
|
|
10467
11316
|
this.streamProcessor = null;
|
|
10468
|
-
this.callbacks.onRecordingComplete(
|
|
10469
|
-
return
|
|
11317
|
+
this.callbacks.onRecordingComplete(finalBlob);
|
|
11318
|
+
return finalBlob;
|
|
10470
11319
|
} catch (error) {
|
|
10471
11320
|
this.handleError(error);
|
|
10472
11321
|
this.recordingState = RECORDING_STATE_IDLE;
|
|
@@ -10583,6 +11432,8 @@ class RecorderController {
|
|
|
10583
11432
|
callbacks;
|
|
10584
11433
|
uploadQueueManager = null;
|
|
10585
11434
|
isInitialized = false;
|
|
11435
|
+
enableTabVisibilityOverlay = false;
|
|
11436
|
+
tabVisibilityOverlayText;
|
|
10586
11437
|
constructor(callbacks = {}) {
|
|
10587
11438
|
this.callbacks = callbacks;
|
|
10588
11439
|
this.streamManager = new CameraStreamManager;
|
|
@@ -10630,6 +11481,13 @@ class RecorderController {
|
|
|
10630
11481
|
if (config.maxRecordingTime !== undefined) {
|
|
10631
11482
|
this.recordingManager.setMaxRecordingTime(config.maxRecordingTime);
|
|
10632
11483
|
}
|
|
11484
|
+
if (config.enableTabVisibilityOverlay !== undefined) {
|
|
11485
|
+
this.enableTabVisibilityOverlay = config.enableTabVisibilityOverlay;
|
|
11486
|
+
}
|
|
11487
|
+
if (config.tabVisibilityOverlayText !== undefined) {
|
|
11488
|
+
this.tabVisibilityOverlayText = config.tabVisibilityOverlayText;
|
|
11489
|
+
}
|
|
11490
|
+
this.recordingManager.setTabVisibilityOverlayConfig(this.enableTabVisibilityOverlay, this.tabVisibilityOverlayText);
|
|
10633
11491
|
const onStorageCleanupError = this.callbacks.onStorageCleanupError ?? NOOP_STORAGE_CLEANUP_ERROR;
|
|
10634
11492
|
await this.storageManager.initialize(onStorageCleanupError);
|
|
10635
11493
|
const storageService = this.storageManager.getStorageService();
|
|
@@ -10673,6 +11531,12 @@ class RecorderController {
|
|
|
10673
11531
|
});
|
|
10674
11532
|
return blob;
|
|
10675
11533
|
}
|
|
11534
|
+
getTabVisibilityOverlayConfig() {
|
|
11535
|
+
return {
|
|
11536
|
+
enabled: this.enableTabVisibilityOverlay,
|
|
11537
|
+
text: this.tabVisibilityOverlayText
|
|
11538
|
+
};
|
|
11539
|
+
}
|
|
10676
11540
|
pauseRecording() {
|
|
10677
11541
|
this.recordingManager.pauseRecording();
|
|
10678
11542
|
}
|
|
@@ -10798,6 +11662,16 @@ class RecorderController {
|
|
|
10798
11662
|
onError: sourceSwitch?.onError,
|
|
10799
11663
|
onTransitionStart: sourceSwitch?.onTransitionStart,
|
|
10800
11664
|
onTransitionEnd: sourceSwitch?.onTransitionEnd,
|
|
11665
|
+
onScreenSelectionStart: () => {
|
|
11666
|
+
if (this.isRecording()) {
|
|
11667
|
+
this.recordingManager.updateSourceType(true);
|
|
11668
|
+
}
|
|
11669
|
+
},
|
|
11670
|
+
onScreenSelectionEnd: () => {
|
|
11671
|
+
if (this.isRecording()) {
|
|
11672
|
+
this.recordingManager.updateSourceType(false);
|
|
11673
|
+
}
|
|
11674
|
+
},
|
|
10801
11675
|
getSelectedCameraDeviceId: () => this.deviceManager.getSelectedCameraDeviceId(),
|
|
10802
11676
|
getSelectedMicDeviceId: () => this.deviceManager.getSelectedMicDeviceId()
|
|
10803
11677
|
};
|
|
@@ -11065,7 +11939,9 @@ class VidtreoRecorder {
|
|
|
11065
11939
|
enableSourceSwitching: this.config.enableSourceSwitching,
|
|
11066
11940
|
enableMute: this.config.enableMute,
|
|
11067
11941
|
enablePause: this.config.enablePause,
|
|
11068
|
-
enableDeviceChange: this.config.enableDeviceChange
|
|
11942
|
+
enableDeviceChange: this.config.enableDeviceChange,
|
|
11943
|
+
enableTabVisibilityOverlay: this.config.enableTabVisibilityOverlay,
|
|
11944
|
+
tabVisibilityOverlayText: this.config.tabVisibilityOverlayText
|
|
11069
11945
|
};
|
|
11070
11946
|
await this.controller.initialize(recorderConfig);
|
|
11071
11947
|
this.isInitialized = true;
|