@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/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
- if (!backendUrl) {
309
- throw new Error("backendUrl is required");
310
- }
311
- this.configService = ConfigService.getInstance({ apiKey, backendUrl });
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 = 100;
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
- return new MediaStream(combinedTracks);
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 = 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
- 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) {
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
- combinedTracks.push(originalAudioTrack);
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
- this.resetRecordingState();
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 result.blob;
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 = Date.now();
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.bind(this));
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
- 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
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
- async handleStart(videoStream, audioStream, config) {
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?.close();
9958
+ if (pausedResult.value) {
9959
+ pausedResult.value.close();
9960
+ }
9488
9961
  return true;
9489
9962
  }
9490
9963
  calculateVideoFrameTimestamp(videoFrame) {
9491
- const rawTs = typeof videoFrame.timestamp === "number" ? videoFrame.timestamp / 1e6 : performance.now() / 1000;
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 - (this.baseVideoTimestamp ?? rawTs) - this.pausedDuration;
9496
- const prevTs = this.lastVideoTimestamp || 0;
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
- async processVideoFrame(videoFrame) {
9504
- if (!this.videoSource) {
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
- const sample = new VideoSample(videoFrame, {
9510
- timestamp: frameTimestamp,
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 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);
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
- } finally {
9522
- sample.close();
9523
- videoFrame.close();
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 (this.lastAudioTimestamp === 0) {
9571
- this.lastAudioTimestamp = 0;
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
- if (!this.audioSource) {
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
- try {
9590
- await this.audioSource.add(audioSample);
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
- } finally {
9596
- audioSample.close();
9597
- audioData.close();
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
- try {
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
- const continuationTimestamp = this.lastVideoTimestamp > 0 ? this.lastVideoTimestamp + 1 / this.frameRate : 0;
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
- this.baseVideoTimestamp = null;
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
- try {
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 = error instanceof Error ? error.message : String(error);
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 = isScreenCapture ? 15 : config.fps;
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 = isScreenCapture ? 15 : 30;
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 blob = await this.streamManager.stopRecording();
10463
- logger.info("[RecordingManager] Recording stopped, blob size:", blob.size);
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(blob);
10469
- return blob;
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
- if (!config.apiUrl) {
11009
- throw new Error("apiUrl is required");
11010
- }
11011
- this.config = config;
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;