Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion -

This file was deleted.

66 changes: 32 additions & 34 deletions src/hooks/useScreenRecorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ const DEFAULT_HEIGHT = 1080;
const CODEC_ALIGNMENT = 2;
const RECORDER_TIMESLICE_MS = 1000;
const BITS_PER_MEGABIT = 1_000_000;
const MIN_FRAME_RATE = 30;
const CHROME_MEDIA_SOURCE = "desktop";
const RECORDING_FILE_PREFIX = "recording-";
const VIDEO_FILE_EXTENSION = ".webm";
const AUDIO_BITRATE_VOICE = 128_000;
Expand Down Expand Up @@ -57,6 +55,11 @@ type UseScreenRecorderReturn = {
setCountdownDelay: (delay: number) => void;
};

type ExtendedDisplayMediaStreamOptions = DisplayMediaStreamOptions & {
selfBrowserSurface?: "exclude" | "include";
surfaceSwitching?: "exclude" | "include";
};

export function useScreenRecorder(): UseScreenRecorderReturn {
const [recording, setRecording] = useState(false);
const [paused, setPaused] = useState(false);
Expand Down Expand Up @@ -571,45 +574,47 @@ export function useScreenRecorder(): UseScreenRecorderReturn {

let videoTrack: MediaStreamTrack | undefined;
let systemAudioIncluded = false;
const browserScreenVideoConstraints = {
mandatory: {
chromeMediaSource: CHROME_MEDIA_SOURCE,
chromeMediaSourceId: selectedSource.id,
maxWidth: TARGET_WIDTH,
maxHeight: TARGET_HEIGHT,
maxFrameRate: TARGET_FRAME_RATE,
minFrameRate: MIN_FRAME_RATE,
googCaptureCursor: false,
},
const displayMediaVideoConstraints = {
displaySurface: selectedSource.id?.startsWith("window:") ? "window" : "monitor",
width: { ideal: TARGET_WIDTH, max: TARGET_WIDTH },
height: { ideal: TARGET_HEIGHT, max: TARGET_HEIGHT },
frameRate: { ideal: TARGET_FRAME_RATE, max: TARGET_FRAME_RATE },
cursor: "never" as const,
};
Comment on lines +577 to 583
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd src/hooks && wc -l useScreenRecorder.ts

Repository: webadderall/Recordly

Length of output: 88


🏁 Script executed:

cd src/hooks && sed -n '690,710p' useScreenRecorder.ts

Repository: webadderall/Recordly

Length of output: 690


🏁 Script executed:

cd src/hooks && sed -n '577,600p' useScreenRecorder.ts

Repository: webadderall/Recordly

Length of output: 1016


🏁 Script executed:

cd src/hooks && grep -n "applyConstraints" useScreenRecorder.ts

Repository: webadderall/Recordly

Length of output: 111


🏁 Script executed:

cd src/hooks && sed -n '577,583p' useScreenRecorder.ts && echo "---" && sed -n '697,710p' useScreenRecorder.ts

Repository: webadderall/Recordly

Length of output: 932


Include cursor: "never" in the applyConstraints() call at line 697.

The cursor: "never" constraint set during initialization (line 577) will be cleared when applyConstraints() is invoked because it replaces the constraint set and restores omitted properties to defaults. Reapply the constraint:

Code change needed
        await videoTrack.applyConstraints({
          frameRate: { ideal: TARGET_FRAME_RATE, max: TARGET_FRAME_RATE },
          width: { ideal: TARGET_WIDTH, max: TARGET_WIDTH },
          height: { ideal: TARGET_HEIGHT, max: TARGET_HEIGHT },
          cursor: "never",
        } as MediaTrackConstraints);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useScreenRecorder.ts` around lines 577 - 583, The cursor:"never"
constraint set in displayMediaVideoConstraints gets lost when applyConstraints()
is called; update the applyConstraints() call on the videoTrack (in
useScreenRecorder.ts) to include cursor: "never" along with frameRate, width,
and height (use the existing TARGET_FRAME_RATE, TARGET_WIDTH, TARGET_HEIGHT
constants) and cast to MediaTrackConstraints so the cursor constraint persists.

const displayMediaOptions: ExtendedDisplayMediaStreamOptions = {
selfBrowserSurface: "exclude",
surfaceSwitching: "exclude",
};

if (wantsAudioCapture) {
let screenMediaStream: MediaStream;

if (systemAudioEnabled) {
try {
screenMediaStream = await (navigator.mediaDevices as any).getUserMedia({
audio: {
mandatory: {
chromeMediaSource: CHROME_MEDIA_SOURCE,
chromeMediaSourceId: selectedSource.id,
},
},
video: browserScreenVideoConstraints,
screenMediaStream = await navigator.mediaDevices.getDisplayMedia({
audio: true,
video: displayMediaVideoConstraints,
...displayMediaOptions,
});
if (screenMediaStream.getAudioTracks().length === 0) {
alert(
"System audio is not available for this source. Recording will continue without system audio.",
);
}
} catch (audioError) {
console.warn("System audio capture failed, falling back to video-only:", audioError);
alert("System audio is not available for this source. Recording will continue without system audio.");
screenMediaStream = await (navigator.mediaDevices as any).getUserMedia({
screenMediaStream = await navigator.mediaDevices.getDisplayMedia({
audio: false,
video: browserScreenVideoConstraints,
video: displayMediaVideoConstraints,
...displayMediaOptions,
});
Comment on lines 604 to 611
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check if the file exists and examine the code around lines 604-611
wc -l src/hooks/useScreenRecorder.ts

Repository: webadderall/Recordly

Length of output: 98


🏁 Script executed:

# Read the relevant section with more context around lines 604-611
sed -n '580,630p' src/hooks/useScreenRecorder.ts

Repository: webadderall/Recordly

Length of output: 1965


🌐 Web query:

MDN getDisplayMedia exceptions AbortError NotAllowedError

💡 Result:

For navigator.mediaDevices.getDisplayMedia(), MDN documents these two DOMException rejections:

  • NotAllowedError: The user denied the screen-share permission prompt, or the page isn’t allowed to use screen capture in the current context (for example, blocked by a Permissions Policy). [1]
  • AbortError: A generic failure—used when the capture fails for a reason that doesn’t match the other, more specific exceptions. [1]

MDN also notes a closely related case that’s often confused with NotAllowedError:

  • InvalidStateError: Thrown if getDisplayMedia() wasn’t called from a transient user activation (e.g., not directly in a click handler), or if the document isn’t fully active/focused, etc. [1]

Typical handling pattern:

try {
  const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
} catch (e) {
  if (e.name === "NotAllowedError") {
    // user denied OR blocked by policy/context
  } else if (e.name === "AbortError") {
    // unexpected failure not covered by other exceptions
  } else if (e.name === "InvalidStateError") {
    // not triggered by a user gesture / not focused / etc.
  }
}

Sources: MDN getDisplayMedia() exceptions list and definitions. [1]


Don't reprompt after a cancelled or denied share dialog.

MDN confirms NotAllowedError (user denied) and AbortError (generic failure) are thrown by getDisplayMedia(). Line 604 currently retries with audio: false after every failure, causing user cancellations to trigger a second picker instead of aborting cleanly. Only downgrade to video-only for cases that don't indicate user denial or unrecoverable failures.

Suggested guard
           } catch (audioError) {
+            if (
+              audioError instanceof DOMException &&
+              (audioError.name === "AbortError" || audioError.name === "NotAllowedError")
+            ) {
+              throw audioError;
+            }
             console.warn("System audio capture failed, falling back to video-only:", audioError);
             alert("System audio is not available for this source. Recording will continue without system audio.");
             screenMediaStream = await navigator.mediaDevices.getDisplayMedia({
               audio: false,
               video: displayMediaVideoConstraints,
               ...displayMediaOptions,
             });
           }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (audioError) {
console.warn("System audio capture failed, falling back to video-only:", audioError);
alert("System audio is not available for this source. Recording will continue without system audio.");
screenMediaStream = await (navigator.mediaDevices as any).getUserMedia({
screenMediaStream = await navigator.mediaDevices.getDisplayMedia({
audio: false,
video: browserScreenVideoConstraints,
video: displayMediaVideoConstraints,
...displayMediaOptions,
});
} catch (audioError) {
if (
audioError instanceof DOMException &&
(audioError.name === "AbortError" || audioError.name === "NotAllowedError")
) {
throw audioError;
}
console.warn("System audio capture failed, falling back to video-only:", audioError);
alert("System audio is not available for this source. Recording will continue without system audio.");
screenMediaStream = await navigator.mediaDevices.getDisplayMedia({
audio: false,
video: displayMediaVideoConstraints,
...displayMediaOptions,
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useScreenRecorder.ts` around lines 604 - 611, The catch block that
handles audio capture errors in useScreenRecorder (around the getDisplayMedia
call producing screenMediaStream) must not retry with audio: false when the user
cancelled or denied the share dialog; modify the catch for audioError to inspect
audioError.name (or error.code) and if it is "NotAllowedError" or "AbortError"
(or clearly indicates user denial/abort) then rethrow or return/exit instead of
calling navigator.mediaDevices.getDisplayMedia again; only perform the fallback
to video-only for other error names (non-user-denial/unrecoverable) so that user
cancellations don't trigger a second picker.

}
} else {
screenMediaStream = await (navigator.mediaDevices as any).getUserMedia({
screenMediaStream = await navigator.mediaDevices.getDisplayMedia({
audio: false,
video: browserScreenVideoConstraints,
video: displayMediaVideoConstraints,
...displayMediaOptions,
});
}

Expand Down Expand Up @@ -676,16 +681,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
} else {
const mediaStream = await navigator.mediaDevices.getDisplayMedia({
audio: false,
video: {
displaySurface: selectedSource.id?.startsWith("window:") ? "window" : "monitor",
width: { ideal: TARGET_WIDTH, max: TARGET_WIDTH },
height: { ideal: TARGET_HEIGHT, max: TARGET_HEIGHT },
frameRate: { ideal: TARGET_FRAME_RATE, max: TARGET_FRAME_RATE },
cursor: "never",
},
selfBrowserSurface: "exclude",
surfaceSwitching: "exclude",
} as any);
video: displayMediaVideoConstraints,
...displayMediaOptions,
});

stream.current = mediaStream;
videoTrack = mediaStream.getVideoTracks()[0];
Expand Down