@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.
Files changed (3) hide show
  1. package/dist/index.d.ts +108 -8
  2. package/dist/index.js +1027 -151
  3. 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 = 100;
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
- return new MediaStream(combinedTracks);
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 = 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
- if (this.callbacks.onTransitionEnd) {
819
- this.callbacks.onTransitionEnd();
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
- combinedTracks.push(originalAudioTrack);
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
- this.resetRecordingState();
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 result.blob;
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 = Date.now();
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.bind(this));
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
- handleMessage(event) {
9331
- const message = event.data;
9332
- logger.debug("[RecorderWorker] Received message:", {
9333
- type: message.type,
9334
- hasVideoStream: message.type === "start" ? !!message.videoStream : false,
9335
- hasAudioStream: message.type === "start" ? !!message.audioStream : false
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
- async handleStart(videoStream, audioStream, config) {
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?.close();
9956
+ if (pausedResult.value) {
9957
+ pausedResult.value.close();
9958
+ }
9488
9959
  return true;
9489
9960
  }
9490
9961
  calculateVideoFrameTimestamp(videoFrame) {
9491
- const rawTs = typeof videoFrame.timestamp === "number" ? videoFrame.timestamp / 1e6 : performance.now() / 1000;
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 - (this.baseVideoTimestamp ?? rawTs) - this.pausedDuration;
9496
- const prevTs = this.lastVideoTimestamp || 0;
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
- async processVideoFrame(videoFrame) {
9504
- if (!this.videoSource) {
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
- const sample = new VideoSample(videoFrame, {
9510
- timestamp: frameTimestamp,
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 isKeyFrame = this.frameCount % (this.config?.keyFrameInterval || 5) === 0;
9514
- try {
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
- } finally {
9522
- sample.close();
9523
- videoFrame.close();
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 (this.lastAudioTimestamp === 0) {
9571
- this.lastAudioTimestamp = 0;
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
- if (!this.audioSource) {
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
- try {
9590
- await this.audioSource.add(audioSample);
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
- } finally {
9596
- audioSample.close();
9597
- audioData.close();
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
- try {
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
- const continuationTimestamp = this.lastVideoTimestamp > 0 ? this.lastVideoTimestamp + 1 / this.frameRate : 0;
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
- this.baseVideoTimestamp = null;
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
- try {
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 = error instanceof Error ? error.message : String(error);
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 = isScreenCapture ? 15 : config.fps;
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 = isScreenCapture ? 15 : 30;
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 blob = await this.streamManager.stopRecording();
10463
- logger.info("[RecordingManager] Recording stopped, blob size:", blob.size);
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(blob);
10469
- return blob;
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;