Compare commits

..

2 Commits

Author SHA1 Message Date
lebaudantoine f20198b19a fixup! (frontend) expose media state in the DOM for the Renater gateway 2026-07-31 14:40:43 +02:00
lebaudantoine b6ee63a29f (frontend) expose media state in the DOM for the Renater gateway
Add a hidden div in the DOM that reflects the current state of the
microphone and camera, so that the Renater SIP media gateway can
observe it and keep an accurate view of the media state.
2026-07-31 14:33:48 +02:00
15 changed files with 63 additions and 138 deletions
-34
View File
@@ -198,37 +198,3 @@ class IsPresentInMeeting(permissions.BasePermission):
return False
except ParticipantsManagementException:
return False
class RoomCapabilityPermission(permissions.BasePermission):
"""Wip."""
capability = None
def has_object_permission(self, request, view, obj):
"""Wip."""
user = request.user
if not user or not user.is_authenticated:
return False
if obj.is_administrator_or_owner(user):
return True
if not obj.configuration.get(self.capability, True):
return False
try:
return ParticipantsManagement().check_if_in_meeting(
room_name=str(obj.pk), identity=str(user.sub)
)
except ParticipantNotFoundException:
return False
except ParticipantsManagementException:
return False
class CanRecord(RoomCapabilityPermission):
"""Wip."""
capability = "authenticated_can_record"
-1
View File
@@ -330,7 +330,6 @@ class RoomConfiguration(BaseModel):
can_publish_sources: list[TrackSource] | None = None
everyone_can_mute: bool | None = None
authenticated_can_record: bool | None = None
model_config = {"extra": "forbid"}
+7 -26
View File
@@ -369,7 +369,7 @@ class RoomViewSet(
methods=["post"],
url_path="start-recording",
permission_classes=[
permissions.CanRecord,
permissions.HasPrivilegesOnRoom,
],
)
@FeatureFlag.require("recording")
@@ -394,30 +394,11 @@ class RoomViewSet(
mode=mode,
options=options.model_dump(exclude_none=True) if options else {},
)
if room.is_administrator_or_owner(request.user):
models.RecordingAccess.objects.create(
user=self.request.user,
role=models.RoleChoices.OWNER,
recording=recording,
)
else:
# todo - encapsulate it in a clear method which replicate privileges role on obj
accesses = models.ResourceAccess.objects.filter(
resource=room,
role__in=[models.RoleChoices.OWNER, models.RoleChoices.ADMIN],
).values_list("user_id", "role")
models.RecordingAccess.objects.bulk_create(
[
models.RecordingAccess(
user_id=user_id,
role=role,
recording=recording,
)
for user_id, role in accesses
]
)
models.RecordingAccess.objects.create(
user=self.request.user,
role=models.RoleChoices.OWNER,
recording=recording,
)
except (DjangoValidationError, IntegrityError):
# DjangoValidationError covers the Python-level check (full_clean);
@@ -461,7 +442,7 @@ class RoomViewSet(
methods=["post"],
url_path="stop-recording",
permission_classes=[
permissions.CanRecord,
permissions.HasPrivilegesOnRoom,
],
)
@FeatureFlag.require("recording")
@@ -98,7 +98,6 @@ export const NoAccessView = ({
)}
</Text>
</VStack>
{/*todo - update the message here to adapt to room configuration */}
{!isLoggedIn && (
<LoginPrompt
heading={t(`${i18nKey}.login.heading`)}
@@ -29,7 +29,6 @@ import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
import { FeatureFlags } from '@/features/analytics/enums'
import { LimitDescription } from './LimitDescription'
import { useCanRecord } from '@/features/recording/hooks/useCanRecord'
export const ScreenRecordingSidePanel = () => {
const { data } = useConfig()
@@ -39,7 +38,6 @@ export const ScreenRecordingSidePanel = () => {
const [includeTranscript, setIncludeTranscript] = useState(false)
const canRecord = useCanRecord()
const isAdminOrOwner = useIsAdminOrOwner()
const hasScreenRecordingAccess = useHasRecordingAccess(
@@ -112,7 +110,7 @@ export const ScreenRecordingSidePanel = () => {
}
}
if (!canRecord) {
if (!isAdminOrOwner) {
return (
<NoAccessView
i18nKeyPrefix={keyPrefix}
@@ -6,7 +6,7 @@ import { useRoomContext } from '@livekit/components-react'
import {
RecordingMode,
useHasRecordingAccess,
useHasFeatureWithoutRecordingRights,
useHasFeatureWithoutAdminRights,
useRecordingStatuses,
} from '../index'
import { useState } from 'react'
@@ -53,7 +53,7 @@ export const TranscriptSidePanel = () => {
FeatureFlags.Transcript
)
const hasFeatureWithoutAdminRights = useHasFeatureWithoutRecordingRights(
const hasFeatureWithoutAdminRights = useHasFeatureWithoutAdminRights(
RecordingMode.Transcript,
FeatureFlags.Transcript
)
@@ -1,15 +0,0 @@
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
import { useUser } from '@/features/auth/api/useUser'
export const useCanRecord = () => {
const apiRoomData = useRoomData()
const isAdminOrOwner = useIsAdminOrOwner()
const { isLoggedIn } = useUser()
return (
isAdminOrOwner ||
(!!isLoggedIn &&
apiRoomData?.configuration?.authenticated_can_record !== false)
)
}
@@ -2,21 +2,21 @@ import { useFeatureFlagEnabled } from 'posthog-js/react'
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
import type { RecordingMode } from '../types'
import { useIsRecordingModeEnabled } from './useIsRecordingModeEnabled'
import { useCanRecord } from './useCanRecord'
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
import type { FeatureFlags } from '@/features/analytics/enums'
export const useHasFeatureWithoutRecordingRights = (
export const useHasFeatureWithoutAdminRights = (
mode: RecordingMode,
featureFlag: FeatureFlags
) => {
const featureEnabled = useFeatureFlagEnabled(featureFlag)
const isAnalyticsEnabled = useIsAnalyticsEnabled()
const isRecordingModeEnabled = useIsRecordingModeEnabled(mode)
const canRecord = useCanRecord()
const isAdminOrOwner = useIsAdminOrOwner()
return (
(featureEnabled || !isAnalyticsEnabled) &&
isRecordingModeEnabled &&
!canRecord
!isAdminOrOwner
)
}
@@ -2,8 +2,8 @@ import { useFeatureFlagEnabled } from 'posthog-js/react'
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
import type { RecordingMode } from '../types'
import { useIsRecordingModeEnabled } from './useIsRecordingModeEnabled'
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
import type { FeatureFlags } from '@/features/analytics/enums'
import { useCanRecord } from './useCanRecord'
export const useHasRecordingAccess = (
mode: RecordingMode,
@@ -12,11 +12,11 @@ export const useHasRecordingAccess = (
const featureEnabled = useFeatureFlagEnabled(featureFlag)
const isAnalyticsEnabled = useIsAnalyticsEnabled()
const isRecordingModeEnabled = useIsRecordingModeEnabled(mode)
const canRecord = useCanRecord()
const isAdminOrOwner = useIsAdminOrOwner()
return (
(featureEnabled || !isAnalyticsEnabled) &&
canRecord &&
isAdminOrOwner &&
isRecordingModeEnabled
)
}
+1 -1
View File
@@ -1,7 +1,7 @@
// hooks
export { useIsRecordingModeEnabled } from './hooks/useIsRecordingModeEnabled'
export { useHasRecordingAccess } from './hooks/useHasRecordingAccess'
export { useHasFeatureWithoutRecordingRights } from './hooks/useHasFeatureWithoutRecordingRights.ts'
export { useHasFeatureWithoutAdminRights } from './hooks/useHasFeatureWithoutAdminRights'
export { useRecordingStatuses } from './hooks/useRecordingStatuses'
// api
@@ -16,7 +16,6 @@ export enum ApiAccessLevel {
export type RoomConfiguration = {
can_publish_sources?: Source[] | null
everyone_can_mute?: boolean | null
authenticated_can_record?: boolean | null
}
export type ApiRoom = {
@@ -50,12 +50,7 @@ export const Admin = () => {
isScreenShareEnabled,
} = usePublishSourcesManager()
const {
toggleMuting,
isMutingEnabled,
isAuthenticatedCanRecordEnabled,
toggleAuthenticatedCanRecord,
} = usePermissionsManager()
const { toggleMuting, isMutingEnabled } = usePermissionsManager()
return (
<Div
@@ -160,17 +155,6 @@ export const Admin = () => {
fullWidth: true,
}}
/>
<Field
type="switch"
label={'Authenticated can record'}
description={'Authenticated can record'}
isSelected={isAuthenticatedCanRecordEnabled}
onChange={toggleAuthenticatedCanRecord}
wrapperProps={{
noMargin: true,
fullWidth: true,
}}
/>
</div>
</div>
<div
@@ -0,0 +1,42 @@
import { useLocalParticipant } from '@livekit/components-react'
import { useEffect } from 'react'
export const MEDIA_STATE_ELEMENT_ID = 'media-state'
export const MEDIA_STATE_CHANGED_EVENT = 'media-state-changed'
export type MediaStateChangedDetail = {
microphoneEnabled: boolean
cameraEnabled: boolean
}
/**
* Exposes the local participant's media state in the DOM so external tools
* (e.g. bots automating the frontend) can reliably read the microphone and
* camera state, and watch for changes with a MutationObserver:
*
* const el = document.getElementById('media-state')
* new MutationObserver(...).observe(el, { attributes: true })
*/
export const MediaStateObserver = () => {
const { isMicrophoneEnabled, isCameraEnabled } = useLocalParticipant()
useEffect(() => {
window.dispatchEvent(
new CustomEvent<MediaStateChangedDetail>(MEDIA_STATE_CHANGED_EVENT, {
detail: {
microphoneEnabled: isMicrophoneEnabled,
cameraEnabled: isCameraEnabled,
},
})
)
}, [isMicrophoneEnabled, isCameraEnabled])
return (
<div
id={MEDIA_STATE_ELEMENT_ID}
style={{ display: 'none' }}
data-microphone-enabled={isMicrophoneEnabled ? 'true' : 'false'}
data-camera-enabled={isCameraEnabled ? 'true' : 'false'}
/>
)
}
@@ -12,8 +12,6 @@ export const usePermissionsManager = () => {
const roomId = data?.slug
const isMutingEnabled = configuration?.everyone_can_mute ?? true
const isAuthenticatedCanRecordEnabled =
configuration?.authenticated_can_record ?? true
const toggleMuting = useCallback(
async (enabled: boolean) => {
@@ -41,36 +39,8 @@ export const usePermissionsManager = () => {
[configuration, roomId, patchRoom]
)
const toggleAuthenticatedCanRecord = useCallback(
async (enabled: boolean) => {
if (!roomId) return
try {
const newConfiguration = {
...configuration,
authenticated_can_record: enabled,
}
const room = await patchRoom({
roomId,
room: { configuration: newConfiguration },
})
queryClient.setQueryData([keys.room, roomId], room)
return { configuration: newConfiguration }
} catch (error) {
console.error('Failed to update recording permission:', error)
return { success: false, error }
}
},
[configuration, roomId, patchRoom]
)
return {
toggleMuting,
isMutingEnabled,
isAuthenticatedCanRecordEnabled,
toggleAuthenticatedCanRecord,
}
}
@@ -11,6 +11,7 @@ import { SidePanel } from '../components/SidePanel'
import { RecordingProvider } from '@/features/recording'
import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal'
import { ConnectionObserver } from '../components/ConnectionObserver'
import { MediaStateObserver } from '../components/MediaStateObserver'
import { RoomMetadataSynchronizer } from '../components/RoomMetadataSynchronizer'
import { useRoomPageTitle } from '../hooks/useRoomPageTitle'
import { useNoiseReduction } from '../hooks/useNoiseReduction'
@@ -63,6 +64,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
<>
<RoomMetadataSynchronizer />
<ConnectionObserver />
<MediaStateObserver />
<ChatProvider />
<VideoResolutionSubscription />
<div