-
Notifications
You must be signed in to change notification settings - Fork 164
fix(windows): hide native desktop cursor during browser capture #79
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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.tsRepository: 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.tsRepository: webadderall/Recordly Length of output: 1965 🌐 Web query:
💡 Result: For
MDN also notes a closely related case that’s often confused with
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 Don't reprompt after a cancelled or denied share dialog. MDN confirms 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| screenMediaStream = await (navigator.mediaDevices as any).getUserMedia({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| screenMediaStream = await navigator.mediaDevices.getDisplayMedia({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| audio: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| video: browserScreenVideoConstraints, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| video: displayMediaVideoConstraints, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...displayMediaOptions, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: webadderall/Recordly
Length of output: 88
🏁 Script executed:
Repository: webadderall/Recordly
Length of output: 690
🏁 Script executed:
Repository: webadderall/Recordly
Length of output: 1016
🏁 Script executed:
Repository: webadderall/Recordly
Length of output: 111
🏁 Script executed:
Repository: webadderall/Recordly
Length of output: 932
Include
cursor: "never"in theapplyConstraints()call at line 697.The
cursor: "never"constraint set during initialization (line 577) will be cleared whenapplyConstraints()is invoked because it replaces the constraint set and restores omitted properties to defaults. Reapply the constraint:Code change needed
🤖 Prompt for AI Agents