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