Compare commits

..

6 Commits

Author SHA1 Message Date
lebaudantoine ec7442f0e1 🐛(frontend) harmonize cache configuration for MediaPipe assets
The wasm and js files shipped by MediaPipe were served with
different cache policies, which could leave the two out of sync on
the client (fresh js with stale wasm, or vice versa).

Align the cache configuration across the MediaPipe assets so they
are always cached and invalidated together.
2026-08-06 13:16:20 +02:00
lebaudantoine e6f9b28a64 🐛(frontend) serve MediaPipe assets under a versioned path
The MediaPipe assets were served under /assets, where the cache
behavior differs between wasm and js files. As a result, clients
could end up with a fresh js loader paired with a stale wasm binary
(or vice versa), leaving MediaPipe out of sync.

Copy the assets under a versioned route so the URL changes whenever
the dependency version bumps. Clients then reload both the js and
the wasm together, keeping them in sync.
2026-08-06 13:00:21 +02:00
lebaudantoine 41e937c1f1 🔖(patch)) release 1.25.1 2026-08-06 11:31:08 +02:00
lebaudantoine d988c72208 🚑️(frontend) fix background crash from MediaPipe WASM version mismatch 2026-08-06 11:27:26 +02:00
lebaudantoine b96d591db3 🔖(minor) bump release to 1.25.0 2026-08-05 18:12:42 +02:00
lebaudantoine bffc51ac4c 🐛(frontend) add trailing slash on the fetch-room URL
The fetch-room URL was missing its trailing slash, which caused the
backend to issue a 301 redirect. Query parameters were being dropped
in the process, leading to incorrect requests.

Append the trailing slash so the request hits the correct endpoint
directly, without going through a redirect.
2026-08-05 18:04:52 +02:00
32 changed files with 85 additions and 164 deletions
+13
View File
@@ -8,6 +8,19 @@ and this project adheres to
## [Unreleased]
### Fixed
- 🐛(frontend) serve MediaPipe assets under a versioned path
- 🐛(frontend) harmonize cache configuration for MediaPipe assets
## [1.25.1] - 2026-08-06
### Fixed
- 🚑️(frontend) fix background crash from MediaPipe WASM version mismatch
## [1.25.0] - 2026-08-05
### Added
- ✨(summary) report exception type in failure analytics
+5
View File
@@ -65,6 +65,11 @@ server {
sub_filter_once off;
}
location ^~ /assets/mediapipe/wasm/ {
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}
# Serve static files with caching
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "agents"
version = "1.24.0"
version = "1.25.1"
requires-python = ">=3.12"
dependencies = [
"livekit-agents==1.6.7",
+1 -1
View File
@@ -9,7 +9,7 @@ resolution-markers = [
[[package]]
name = "agents"
version = "1.24.0"
version = "1.25.1"
source = { virtual = "." }
dependencies = [
{ name = "livekit-agents" },
-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
@@ -349,7 +349,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
@@ -396,7 +396,7 @@ class RoomViewSet(
methods=["post"],
url_path="start-recording",
permission_classes=[
permissions.CanRecord,
permissions.HasPrivilegesOnRoom,
],
)
@FeatureFlag.require("recording")
@@ -421,30 +421,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);
@@ -488,7 +469,7 @@ class RoomViewSet(
methods=["post"],
url_path="stop-recording",
permission_classes=[
permissions.CanRecord,
permissions.HasPrivilegesOnRoom,
],
)
@FeatureFlag.require("recording")
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "uv_build"
[project]
name = "meet"
version = "1.24.0"
version = "1.25.1"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
+1 -1
View File
@@ -1187,7 +1187,7 @@ wheels = [
[[package]]
name = "meet"
version = "1.24.0"
version = "1.25.1"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
+5
View File
@@ -4,6 +4,11 @@ server {
server_tokens off;
root /usr/share/nginx/html;
location ^~ /assets/mediapipe/wasm/ {
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}
# Serve static files with caching
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
+4 -10
View File
@@ -1,12 +1,12 @@
{
"name": "meet",
"version": "1.24.0",
"version": "1.25.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meet",
"version": "1.24.0",
"version": "1.25.1",
"dependencies": {
"@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
"@fontsource-variable/lexend": "5.2.11",
@@ -15,7 +15,7 @@
"@livekit/components-react": "2.9.21",
"@livekit/components-styles": "1.2.0",
"@livekit/track-processors": "0.7.2",
"@mediapipe/tasks-vision": "0.10.35",
"@mediapipe/tasks-vision": "0.10.14",
"@pandacss/preset-panda": "1.11.3",
"@react-types/overlays": "3.10.0",
"@remixicon/react": "4.9.0",
@@ -1049,18 +1049,12 @@
"livekit-client": "^1.12.0 || ^2.1.0"
}
},
"node_modules/@livekit/track-processors/node_modules/@mediapipe/tasks-vision": {
"node_modules/@mediapipe/tasks-vision": {
"version": "0.10.14",
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.14.tgz",
"integrity": "sha512-vOifgZhkndgybdvoRITzRkIueWWSiCKuEUXXK6Q4FaJsFvRJuwgg++vqFUMlL0Uox62U5aEXFhHxlhV7Ja5e3Q==",
"license": "Apache-2.0"
},
"node_modules/@mediapipe/tasks-vision": {
"version": "0.10.35",
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.35.tgz",
"integrity": "sha512-HOvadwVRE6JC+45nyYhmnywnr5h/J8KZvOeUNVOG9q/0875pZgItznFB9bRTvLc264YSJqiZ1NsIpCStJw/egg==",
"license": "Apache-2.0"
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.29.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "1.24.0",
"version": "1.25.1",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -22,7 +22,7 @@
"@livekit/components-react": "2.9.21",
"@livekit/components-styles": "1.2.0",
"@livekit/track-processors": "0.7.2",
"@mediapipe/tasks-vision": "0.10.35",
"@mediapipe/tasks-vision": "0.10.14",
"@pandacss/preset-panda": "1.11.3",
"@react-types/overlays": "3.10.0",
"@remixicon/react": "4.9.0",
@@ -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 ParticipantRole = 'member' | 'administrator' | 'owner'
@@ -10,5 +10,5 @@ export const fetchRoom = ({
}) => {
const query = username ? `?username=${encodeURIComponent(username)}` : ''
return fetchApi<ApiRoom>(`/rooms/${roomId}${query}`)
return fetchApi<ApiRoom>(`/rooms/${roomId}/${query}`)
}
@@ -49,12 +49,7 @@ export const Admin = () => {
isScreenShareEnabled,
} = usePublishSourcesManager()
const {
toggleMuting,
isMutingEnabled,
isAuthenticatedCanRecordEnabled,
toggleAuthenticatedCanRecord,
} = usePermissionsManager()
const { toggleMuting, isMutingEnabled } = usePermissionsManager()
return (
<Div
@@ -159,17 +154,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
@@ -10,7 +10,7 @@ export const SELFIE_SEGMENTER_MODEL_PATH =
export const FACE_LANDMARKS_MODEL_PATH =
'/assets/mediapipe/models/face_landmarker.task'
export const MEDIAPIPE_PATH_WASM = '/assets/mediapipe/wasm'
export const MEDIAPIPE_PATH_WASM = `/assets/mediapipe/wasm/${__MEDIAPIPE_VERSION__}`
export enum ProcessorType {
BLUR = 'blur',
@@ -10,8 +10,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) => {
@@ -37,36 +35,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,
}
}
+4
View File
@@ -1,4 +1,8 @@
/// <reference types="vite/client" />
// Version of @mediapipe/tasks-vision, injected at build time (vite.config.ts).
declare const __MEDIAPIPE_VERSION__: string
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string
readonly VITE_APP_TITLE: string
+15 -1
View File
@@ -1,13 +1,27 @@
import { readFileSync } from 'node:fs'
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
import { visualizer } from 'rollup-plugin-visualizer'
import svgr from 'vite-plugin-svgr'
import { viteStaticCopy } from 'vite-plugin-static-copy'
const mediapipeVersion: string = JSON.parse(
readFileSync(
new URL(
'./node_modules/@mediapipe/tasks-vision/package.json',
import.meta.url
),
'utf-8'
)
).version
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
return {
define: {
__MEDIAPIPE_VERSION__: JSON.stringify(mediapipeVersion),
},
plugins: [
react(),
svgr({
@@ -23,7 +37,7 @@ export default defineConfig(({ mode }) => {
targets: [
{
src: 'node_modules/@mediapipe/tasks-vision/wasm/*',
dest: 'assets/mediapipe/wasm',
dest: `assets/mediapipe/wasm/${mediapipeVersion}`,
rename: { stripBase: 4 },
},
{
@@ -73,6 +73,11 @@ data:
sub_filter_once off;
}
location ^~ /assets/mediapipe/wasm/ {
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}
# Serve static files with caching
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mail_mjml",
"version": "1.24.0",
"version": "1.25.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail_mjml",
"version": "1.24.0",
"version": "1.25.1",
"license": "MIT",
"dependencies": {
"@html-to/text-cli": "0.6.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "1.24.0",
"version": "1.25.1",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "sdk",
"version": "1.24.0",
"version": "1.25.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sdk",
"version": "1.24.0",
"version": "1.25.1",
"license": "ISC",
"workspaces": [
"./library",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sdk",
"version": "1.24.0",
"version": "1.25.1",
"author": "",
"license": "ISC",
"description": "",
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "summary"
version = "1.24.0"
version = "1.25.1"
dependencies = [
"fastapi[standard]>=0.105.0",
"uvicorn>=0.24.0",