Compare commits

...

2 Commits

Author SHA1 Message Date
lebaudantoine 09aeec3285 🐛(frontend) generalize screen-share error modal beyond macOS
The screen-share error modal was tailored to macOS and did not work
correctly on other operating systems.

Make it OS-aware so it also handles Windows properly, showing the
right guidance for each platform.

Also open the OS settings link in a new tab, so the user is not
disconnected from the ongoing meeting when following it.
2026-08-14 10:59:34 +02:00
lebaudantoine 3f07885204 🐛(frontend) stop reporting screen-share denials as errors
Add a small helper that classifies a `getDisplayMedia` failure as a
user, browser, or OS permission denial, or returns null when it is
a genuine error.

Chromium reports denials with explicit, non-localized messages:

* "Permission denied by user" when the user cancels or dismisses
  the source picker.
* "Permission denied by system" when the OS blocks capture (e.g.
  the macOS Screen Recording privacy setting).
* Plain "Permission denied" for browser-level blocks (site
  settings, enterprise policy, permissions-policy).

Firefox and Safari use generic `NotAllowedError` messages, which
fall into the "browser" bucket.

Firefox additionally does not map macOS Screen Recording (TCC)
blocks to `NotAllowedError`: the OS silently returns no capturable
sources, so `getDisplayMedia` rejects with `NotFoundError` ("The
object can not be found here."). Same quirk as the mic/cam OS blocks
handled in `useWatchMediaDeviceErrors` via `isLikelySystemNotFound`.

Behavior on a denied screen-share permission:

* Denials are expected outcomes (picker cancelled by the user, OS
  privacy settings, enterprise policy…) and no longer surface as
  exceptions in error tracking; capture an analytics event instead.
* Only OS-level blocks get the modal, since it explains how to
  unblock them.
2026-08-14 10:59:11 +02:00
8 changed files with 84 additions and 30 deletions
+2
View File
@@ -21,6 +21,8 @@ and this project adheres to
- 🐛(frontend) implement hysteresis band for the control bar layout - 🐛(frontend) implement hysteresis band for the control bar layout
- 🐛(frontend) fix toolbar ResizeObserver loop and alignment drift - 🐛(frontend) fix toolbar ResizeObserver loop and alignment drift
- 🐛(analytics) filter benign ResizeObserver loop error in Sentry/PostHog - 🐛(analytics) filter benign ResizeObserver loop error in Sentry/PostHog
- 🐛(frontend) stop reporting screen-share denials as errors
- 🐛(frontend) generalize screen-share error modal beyond macOS
## [1.26.0] - 2026-08-12 ## [1.26.0] - 2026-08-12
@@ -137,6 +137,7 @@ export const captureMediaEvent = async (
| 'media-device-success' | 'media-device-success'
| 'device-not-found' | 'device-not-found'
| 'permissions-denied' | 'permissions-denied'
| 'screen-share-permission-denied'
| 'silent-mic-detected' | 'silent-mic-detected'
| 'silent-mic-analyser-unavailable' | 'silent-mic-analyser-unavailable'
| 'silent-mic-recovered' | 'silent-mic-recovered'
@@ -1,6 +1,13 @@
import { A, Button, Dialog, P } from '@/primitives' import { A, Button, Dialog, P } from '@/primitives'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { css } from '@/styled-system/css' import { css } from '@/styled-system/css'
import { getOS, type OS } from '@/utils/os'
const SCREEN_CAPTURE_SETTINGS_LINKS: Partial<Record<OS, string>> = {
macos:
'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture',
windows: 'ms-settings:privacy-graphicscaptureprogrammatic',
}
// todo - refactor it into a generic system // todo - refactor it into a generic system
export const ScreenShareErrorModal = ({ export const ScreenShareErrorModal = ({
@@ -11,7 +18,8 @@ export const ScreenShareErrorModal = ({
onClose: () => void onClose: () => void
}) => { }) => {
const { t } = useTranslation('rooms', { keyPrefix: 'error.screenShare' }) const { t } = useTranslation('rooms', { keyPrefix: 'error.screenShare' })
const isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1 const os = getOS()
const settingsHref = SCREEN_CAPTURE_SETTINGS_LINKS[os]
return ( return (
<Dialog <Dialog
@@ -26,15 +34,16 @@ export const ScreenShareErrorModal = ({
<> <>
<P> <P>
{t('message')}{' '} {t('message')}{' '}
{isMac && ( {settingsHref && (
<> <>
{t('macInstructions')}{' '} {t('settingsInstructions')}{' '}
<A <A
href="x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture" href={settingsHref}
target="_blank"
color="primary" color="primary"
aria-label={t('macSystemPreferences') + '-' + t('newTab')} aria-label={t(`settingsLabel.${os}`) + '-' + t('newTab')}
> >
{t('macSystemPreferences')} {t(`settingsLabel.${os}`)}
</A> </A>
.{' '} .{' '}
</> </>
@@ -11,7 +11,9 @@ import { SidePanel } from '../components/SidePanel'
import { RecordingProvider } from '@/features/recording' import { RecordingProvider } from '@/features/recording'
import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal' import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal'
import { ConnectionObserver } from '../components/ConnectionObserver' import { ConnectionObserver } from '../components/ConnectionObserver'
import { reportError } from '@/features/analytics/telemetry' import { captureMediaEvent, reportError } from '@/features/analytics/telemetry'
import { getOS } from '@/utils/os'
import { isFireFox } from '@/utils/livekit'
import { MediaStateObserver } from '../components/MediaStateObserver' import { MediaStateObserver } from '../components/MediaStateObserver'
import { RoomMetadataSynchronizer } from '../components/RoomMetadataSynchronizer' import { RoomMetadataSynchronizer } from '../components/RoomMetadataSynchronizer'
import { useNoiseReduction } from '../hooks/useNoiseReduction' import { useNoiseReduction } from '../hooks/useNoiseReduction'
@@ -36,6 +38,20 @@ export interface VideoConferenceProps extends React.HTMLAttributes<HTMLDivElemen
SettingsComponent?: React.ComponentType SettingsComponent?: React.ComponentType
} }
const getScreenSharePermissionDeniedScope = (
error: Error
): 'system' | 'user' | 'browser' | null => {
if (error.name === 'NotAllowedError') {
if (/by system/i.test(error.message)) return 'system'
if (/by user/i.test(error.message)) return 'user'
return 'browser'
}
if (error.name === 'NotFoundError' && isFireFox() && getOS() === 'macos') {
return 'system'
}
return null
}
/** /**
* The `VideoConference` ready-made component is your drop-in solution for a classic video conferencing application. * The `VideoConference` ready-made component is your drop-in solution for a classic video conferencing application.
* It provides functionality such as focusing on one participant, grid view with pagination to handle large numbers * It provides functionality such as focusing on one participant, grid view with pagination to handle large numbers
@@ -61,6 +77,34 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
const [isShareErrorVisible, setIsShareErrorVisible] = useState(false) const [isShareErrorVisible, setIsShareErrorVisible] = useState(false)
const handleDeviceError = ({
source,
error,
}: {
source: Track.Source
error: Error
}) => {
if (source === Track.Source.ScreenShare) {
const scope = getScreenSharePermissionDeniedScope(error)
if (scope) {
if (scope === 'system') setIsShareErrorVisible(true)
void captureMediaEvent('screen-share-permission-denied', {
at: 'ControlBar.onDeviceError',
source,
error_name: error.name,
error_message: error.message,
denied_scope: scope,
os: getOS(),
})
return
}
}
reportError('device_switch_failure', error, {
at: 'ControlBar.onDeviceError',
source,
})
}
return ( return (
<> <>
<RoomMetadataSynchronizer /> <RoomMetadataSynchronizer />
@@ -92,21 +136,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
<StageLayout /> <StageLayout />
)} )}
</RoomContentArea> </RoomContentArea>
<ControlBar <ControlBar onDeviceError={handleDeviceError} />
onDeviceError={(e) => {
reportError('device_switch_failure', e.error, {
at: 'ControlBar.onDeviceError',
source: e.source,
})
if (
e.source == Track.Source.ScreenShare &&
e.error.toString() ==
'NotAllowedError: Permission denied by system'
) {
setIsShareErrorVisible(true)
}
}}
/>
<SidePanel /> <SidePanel />
</> </>
)} )}
+5 -2
View File
@@ -214,8 +214,11 @@
"title": "Bildschirmfreigabe nicht möglich", "title": "Bildschirmfreigabe nicht möglich",
"ariaLabel": "Bildschirmfreigabe nicht möglich", "ariaLabel": "Bildschirmfreigabe nicht möglich",
"message": "Deinem Browser fehlt möglicherweise die Berechtigung, den Bildschirm deines Geräts abzugreifen.", "message": "Deinem Browser fehlt möglicherweise die Berechtigung, den Bildschirm deines Geräts abzugreifen.",
"macInstructions": "Gehe zu den", "settingsInstructions": "Gehe zu den",
"macSystemPreferences": "Systemeinstellungen", "settingsLabel": {
"macos": "Systemeinstellungen",
"windows": "Windows-Datenschutzeinstellungen"
},
"helpLinkText": "Weitere Informationen findest du unter", "helpLinkText": "Weitere Informationen findest du unter",
"helpLinkLabel": "Bildschirmfreigabeproblem", "helpLinkLabel": "Bildschirmfreigabeproblem",
"closeButton": "Schließen", "closeButton": "Schließen",
+5 -2
View File
@@ -214,8 +214,11 @@
"title": "Unable to share your screen", "title": "Unable to share your screen",
"ariaLabel": "Unable to share your screen", "ariaLabel": "Unable to share your screen",
"message": "Your browser may not be allowed to record the screen on your computer.", "message": "Your browser may not be allowed to record the screen on your computer.",
"macInstructions": "Go to your", "settingsInstructions": "Go to your",
"macSystemPreferences": "System Preferences", "settingsLabel": {
"macos": "System Preferences",
"windows": "Windows privacy settings"
},
"helpLinkText": "To learn more, see", "helpLinkText": "To learn more, see",
"helpLinkLabel": "Presentation issue", "helpLinkLabel": "Presentation issue",
"closeButton": "Dismiss", "closeButton": "Dismiss",
+5 -2
View File
@@ -214,8 +214,11 @@
"title": "Impossible de partager votre écran", "title": "Impossible de partager votre écran",
"ariaLabel": "Impossible de partager votre écran", "ariaLabel": "Impossible de partager votre écran",
"message": "Il se peut que votre navigateur ne soit pas autorisé à enregistrer l'écran sur votre ordinateur.", "message": "Il se peut que votre navigateur ne soit pas autorisé à enregistrer l'écran sur votre ordinateur.",
"macInstructions": "Accèdez à vos", "settingsInstructions": "Accédez à vos",
"macSystemPreferences": "Préférences système", "settingsLabel": {
"macos": "Préférences système",
"windows": "paramètres de confidentialité Windows"
},
"helpLinkText": "Pour en savoir plus, consulter", "helpLinkText": "Pour en savoir plus, consulter",
"helpLinkLabel": "Problème de présentation", "helpLinkLabel": "Problème de présentation",
"closeButton": "Ignorer", "closeButton": "Ignorer",
+5 -2
View File
@@ -214,8 +214,11 @@
"title": "Kan uw scherm niet delen", "title": "Kan uw scherm niet delen",
"ariaLabel": "Kan uw scherm niet delen", "ariaLabel": "Kan uw scherm niet delen",
"message": "Het is mogelijk dat uw browser geen toestemming heeft om het scherm op uw computer op te nemen.", "message": "Het is mogelijk dat uw browser geen toestemming heeft om het scherm op uw computer op te nemen.",
"macInstructions": "Ga naar uw", "settingsInstructions": "Ga naar uw",
"macSystemPreferences": "Systeemvoorkeuren", "settingsLabel": {
"macos": "Systeemvoorkeuren",
"windows": "Windows-privacyinstellingen"
},
"helpLinkText": "Meer informatie, zie", "helpLinkText": "Meer informatie, zie",
"helpLinkLabel": "Presentatieprobleem", "helpLinkLabel": "Presentatieprobleem",
"closeButton": "Negeren", "closeButton": "Negeren",