Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1bb414cb4 | |||
| 513f3c588a | |||
| b407bfda07 | |||
| 3de4cc01dc | |||
| 1fe1a557ad | |||
| fb92e5b79f | |||
| e71bc093bd | |||
| 43df855461 |
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "meet"
|
||||
version = "0.1.25"
|
||||
version = "0.1.26"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
Generated
+8
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"version": "0.1.25",
|
||||
"version": "0.1.26",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "meet",
|
||||
"version": "0.1.25",
|
||||
"version": "0.1.26",
|
||||
"dependencies": {
|
||||
"@livekit/components-react": "2.9.12",
|
||||
"@livekit/components-styles": "1.1.6",
|
||||
@@ -15,6 +15,7 @@
|
||||
"@react-aria/toast": "3.0.2",
|
||||
"@remixicon/react": "4.6.0",
|
||||
"@tanstack/react-query": "5.76.0",
|
||||
"@timephy/rnnoise-wasm": "1.0.0",
|
||||
"crisp-sdk-web": "1.0.25",
|
||||
"hoofd": "1.7.3",
|
||||
"i18next": "25.1.2",
|
||||
@@ -3888,6 +3889,11 @@
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@timephy/rnnoise-wasm": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@timephy/rnnoise-wasm/-/rnnoise-wasm-1.0.0.tgz",
|
||||
"integrity": "sha512-zzRdFyALbhaNIuEo3LKazPWxabatbPxkaBrHzRjGDlVHX2dFklh68HUENHlvHkKsq+OOQSC1ag5sGPDMDgg2CA=="
|
||||
},
|
||||
"node_modules/@ts-morph/common": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.25.0.tgz",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"private": true,
|
||||
"version": "0.1.25",
|
||||
"version": "0.1.26",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "panda codegen && vite",
|
||||
@@ -20,6 +20,7 @@
|
||||
"@react-aria/toast": "3.0.2",
|
||||
"@remixicon/react": "4.6.0",
|
||||
"@tanstack/react-query": "5.76.0",
|
||||
"@timephy/rnnoise-wasm": "1.0.0",
|
||||
"crisp-sdk-web": "1.0.25",
|
||||
"hoofd": "1.7.3",
|
||||
"i18next": "25.1.2",
|
||||
|
||||
@@ -2,4 +2,5 @@ export enum FeatureFlags {
|
||||
Transcript = 'transcription-summary',
|
||||
ScreenRecording = 'screen-recording',
|
||||
faceLandmarks = 'face-landmarks',
|
||||
noiseReduction = 'noise-reduction',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useEffect } from 'react'
|
||||
import { Track } from 'livekit-client'
|
||||
import { useRoomContext } from '@livekit/components-react'
|
||||
import { RnnNoiseProcessor } from '../processors/RnnNoiseProcessor'
|
||||
import { usePersistentUserChoices } from './usePersistentUserChoices'
|
||||
import { useNoiseReductionAvailable } from '@/features/rooms/livekit/hooks/useNoiseReductionAvailable'
|
||||
|
||||
export const useNoiseReduction = () => {
|
||||
const room = useRoomContext()
|
||||
const noiseReductionAvailable = useNoiseReductionAvailable()
|
||||
|
||||
const {
|
||||
userChoices: { noiseReductionEnabled },
|
||||
} = usePersistentUserChoices()
|
||||
|
||||
const audioTrack = room.localParticipant.getTrackPublication(
|
||||
Track.Source.Microphone
|
||||
)?.audioTrack
|
||||
|
||||
useEffect(() => {
|
||||
if (!audioTrack || !noiseReductionAvailable) return
|
||||
|
||||
const processor = audioTrack?.getProcessor()
|
||||
|
||||
if (noiseReductionEnabled && !processor) {
|
||||
const rnnNoiseProcessor = new RnnNoiseProcessor()
|
||||
audioTrack.setProcessor(rnnNoiseProcessor)
|
||||
} else if (!noiseReductionEnabled && processor) {
|
||||
audioTrack.stopProcessor()
|
||||
}
|
||||
}, [audioTrack, noiseReductionEnabled, noiseReductionAvailable])
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useIsMobile } from '@/utils/useIsMobile'
|
||||
import { useFeatureFlagEnabled } from 'posthog-js/react'
|
||||
import { FeatureFlags } from '@/features/analytics/enums'
|
||||
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
|
||||
|
||||
export const useNoiseReductionAvailable = () => {
|
||||
const featureEnabled = useFeatureFlagEnabled(FeatureFlags.faceLandmarks)
|
||||
const isAnalyticsEnabled = useIsAnalyticsEnabled()
|
||||
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
return !isMobile && (!isAnalyticsEnabled || featureEnabled)
|
||||
}
|
||||
@@ -22,6 +22,9 @@ export function usePersistentUserChoices() {
|
||||
saveUsername: (username: string) => {
|
||||
userChoicesStore.username = username
|
||||
},
|
||||
saveNoiseReductionEnabled: (enabled: boolean) => {
|
||||
userChoicesStore.noiseReductionEnabled = enabled
|
||||
},
|
||||
saveProcessorSerialized: (
|
||||
processorSerialized: ProcessorSerialized | undefined
|
||||
) => {
|
||||
|
||||
@@ -31,6 +31,7 @@ import { useSidePanel } from '../hooks/useSidePanel'
|
||||
import { RecordingStateToast } from '@/features/recording'
|
||||
import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal'
|
||||
import { useConnectionObserver } from '../hooks/useConnectionObserver'
|
||||
import { useNoiseReduction } from '../hooks/useNoiseReduction'
|
||||
|
||||
const LayoutWrapper = styled(
|
||||
'div',
|
||||
@@ -87,6 +88,8 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
||||
|
||||
const layoutContext = useCreateLayoutContext()
|
||||
|
||||
useNoiseReduction()
|
||||
|
||||
const screenShareTracks = tracks
|
||||
.filter(isTrackReference)
|
||||
.filter((track) => track.publication.source === Track.Source.ScreenShare)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Track, TrackProcessor, ProcessorOptions } from 'livekit-client'
|
||||
import { NoiseSuppressorWorklet_Name } from '@timephy/rnnoise-wasm'
|
||||
|
||||
// This is an example how to get the script path using Vite, may be different when using other build tools
|
||||
// NOTE: `?worker&url` is important (`worker` to generate a working script, `url` to get its url to load it)
|
||||
import NoiseSuppressorWorklet from '@timephy/rnnoise-wasm/NoiseSuppressorWorklet?worker&url'
|
||||
|
||||
// Use Jitsi's approach: maintain a global AudioContext variable
|
||||
// and suspend/resume it as needed to manage audio state
|
||||
let audioContext: AudioContext
|
||||
|
||||
export interface AudioProcessorInterface
|
||||
extends TrackProcessor<Track.Kind.Audio> {
|
||||
name: string
|
||||
}
|
||||
|
||||
export class RnnNoiseProcessor implements AudioProcessorInterface {
|
||||
name: string = 'noise-reduction'
|
||||
processedTrack?: MediaStreamTrack
|
||||
|
||||
private source?: MediaStreamTrack
|
||||
private sourceNode?: MediaStreamAudioSourceNode
|
||||
private destinationNode?: MediaStreamAudioDestinationNode
|
||||
private noiseSuppressionNode?: AudioWorkletNode
|
||||
|
||||
constructor() {}
|
||||
|
||||
async init(opts: ProcessorOptions<Track.Kind.Audio>) {
|
||||
if (!opts.track) {
|
||||
throw new Error('Track is required for audio processing')
|
||||
}
|
||||
|
||||
this.source = opts.track as MediaStreamTrack
|
||||
|
||||
if (!audioContext) {
|
||||
audioContext = new AudioContext()
|
||||
} else {
|
||||
await audioContext.resume()
|
||||
}
|
||||
|
||||
await audioContext.audioWorklet.addModule(NoiseSuppressorWorklet)
|
||||
|
||||
this.sourceNode = audioContext.createMediaStreamSource(
|
||||
new MediaStream([this.source])
|
||||
)
|
||||
|
||||
this.noiseSuppressionNode = new AudioWorkletNode(
|
||||
audioContext,
|
||||
NoiseSuppressorWorklet_Name
|
||||
)
|
||||
|
||||
this.destinationNode = audioContext.createMediaStreamDestination()
|
||||
|
||||
// Connect the audio processing chain
|
||||
this.sourceNode
|
||||
.connect(this.noiseSuppressionNode)
|
||||
.connect(this.destinationNode)
|
||||
|
||||
// Get the processed track
|
||||
const tracks = this.destinationNode.stream.getAudioTracks()
|
||||
if (tracks.length === 0) {
|
||||
throw new Error('No audio tracks found for processing')
|
||||
}
|
||||
|
||||
this.processedTrack = tracks[0]
|
||||
}
|
||||
|
||||
async restart(opts: ProcessorOptions<Track.Kind.Audio>) {
|
||||
await this.destroy()
|
||||
return this.init(opts)
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
// Clean up audio nodes and context
|
||||
this.sourceNode?.disconnect()
|
||||
this.noiseSuppressionNode?.disconnect()
|
||||
this.destinationNode?.disconnect()
|
||||
|
||||
/**
|
||||
* Audio Context Lifecycle Management
|
||||
*
|
||||
* We prefer suspending the audio context rather than destroying and recreating it
|
||||
* to avoid memory leaks in WebAssembly-based audio processing.
|
||||
*
|
||||
* Issue: When an AudioContext containing WebAssembly modules is destroyed,
|
||||
* the WASM resources are not properly garbage collected. This causes:
|
||||
* - Retained JavaScript VM instances
|
||||
* - Growing memory consumption over multiple create/destroy cycles
|
||||
* - Potential performance degradation
|
||||
*
|
||||
* Solution: Use suspend() and resume() methods instead of close() to maintain
|
||||
* the same context instance while controlling audio processing state.
|
||||
*/
|
||||
await audioContext.suspend()
|
||||
|
||||
this.sourceNode = undefined
|
||||
this.destinationNode = undefined
|
||||
this.source = undefined
|
||||
this.processedTrack = undefined
|
||||
this.noiseSuppressionNode = undefined
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DialogProps, Field, H } from '@/primitives'
|
||||
import { DialogProps, Field, H, Switch } from '@/primitives'
|
||||
|
||||
import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
|
||||
import {
|
||||
@@ -13,16 +13,46 @@ import { HStack } from '@/styled-system/jsx'
|
||||
import { ActiveSpeaker } from '@/features/rooms/components/ActiveSpeaker'
|
||||
import { usePersistentUserChoices } from '@/features/rooms/livekit/hooks/usePersistentUserChoices'
|
||||
import { ReactNode } from 'react'
|
||||
import { css } from '@/styled-system/css'
|
||||
import posthog from 'posthog-js'
|
||||
import { useNoiseReductionAvailable } from '@/features/rooms/livekit/hooks/useNoiseReductionAvailable'
|
||||
|
||||
type RowWrapperProps = {
|
||||
heading: string
|
||||
children: ReactNode[]
|
||||
beta?: boolean
|
||||
}
|
||||
|
||||
const RowWrapper = ({ heading, children }: RowWrapperProps) => {
|
||||
const BetaBadge = () => (
|
||||
<span
|
||||
className={css({
|
||||
content: '"Beta"',
|
||||
display: 'block',
|
||||
letterSpacing: '-0.02rem',
|
||||
padding: '0 0.25rem',
|
||||
backgroundColor: '#E8EDFF',
|
||||
color: '#0063CB',
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
margin: '0 0 0.9375rem 0.3125rem',
|
||||
lineHeight: '1rem',
|
||||
borderRadius: '4px',
|
||||
width: 'fit-content',
|
||||
height: 'fit-content',
|
||||
marginTop: { base: '10px', sm: '5px' },
|
||||
})}
|
||||
>
|
||||
Beta
|
||||
</span>
|
||||
)
|
||||
|
||||
const RowWrapper = ({ heading, children, beta }: RowWrapperProps) => {
|
||||
return (
|
||||
<>
|
||||
<H lvl={2}>{heading}</H>
|
||||
<HStack>
|
||||
<H lvl={2}>{heading}</H>
|
||||
{beta && <BetaBadge />}
|
||||
</HStack>
|
||||
<HStack
|
||||
gap={0}
|
||||
style={{
|
||||
@@ -61,7 +91,11 @@ export const AudioTab = ({ id }: AudioTabProps) => {
|
||||
const { t } = useTranslation('settings')
|
||||
const { localParticipant } = useRoomContext()
|
||||
|
||||
const { saveAudioInputDeviceId } = usePersistentUserChoices()
|
||||
const {
|
||||
userChoices: { noiseReductionEnabled },
|
||||
saveAudioInputDeviceId,
|
||||
saveNoiseReductionEnabled,
|
||||
} = usePersistentUserChoices()
|
||||
|
||||
const isSpeaking = useIsSpeaking(localParticipant)
|
||||
|
||||
@@ -109,6 +143,8 @@ export const AudioTab = ({ id }: AudioTabProps) => {
|
||||
return defaultItem.value
|
||||
}
|
||||
|
||||
const noiseReductionAvailable = useNoiseReductionAvailable()
|
||||
|
||||
return (
|
||||
<TabPanel padding={'md'} flex id={id}>
|
||||
<RowWrapper heading={t('audio.microphone.heading')}>
|
||||
@@ -158,6 +194,23 @@ export const AudioTab = ({ id }: AudioTabProps) => {
|
||||
<SoundTester />
|
||||
</RowWrapper>
|
||||
)}
|
||||
{noiseReductionAvailable && (
|
||||
<RowWrapper heading={t('audio.noiseReduction.heading')} beta>
|
||||
<Switch
|
||||
aria-label={t(
|
||||
`audio.noiseReduction.ariaLabel.${noiseReductionEnabled ? 'disable' : 'enable'}`
|
||||
)}
|
||||
isSelected={noiseReductionEnabled}
|
||||
onChange={(v) => {
|
||||
saveNoiseReductionEnabled(v)
|
||||
if (v) posthog.capture('noise-reduction-init')
|
||||
}}
|
||||
>
|
||||
{t('audio.noiseReduction.label')}
|
||||
</Switch>
|
||||
<div />
|
||||
</RowWrapper>
|
||||
)}
|
||||
</TabPanel>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
"label": "Wählen Sie Ihre Audioeingabe",
|
||||
"disabled": "Mikrofon deaktiviert"
|
||||
},
|
||||
"noiseReduction": {
|
||||
"label": "Rauschunterdrückung",
|
||||
"heading": "Rauschunterdrückung",
|
||||
"ariaLabel": {
|
||||
"enable": "Rauschunterdrückung aktivieren",
|
||||
"disable": "Rauschunterdrückung deaktivieren"
|
||||
}
|
||||
},
|
||||
"speakers": {
|
||||
"heading": "Lautsprecher",
|
||||
"label": "Wählen Sie Ihre Audioausgabe",
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
"label": "Select your audio input",
|
||||
"disabled": "Microphone disabled"
|
||||
},
|
||||
"noiseReduction": {
|
||||
"label": "Noise reduction",
|
||||
"heading": "Noise reduction",
|
||||
"ariaLabel": {
|
||||
"enable": "Enable noise reduction",
|
||||
"disable": "Disable noise reduction"
|
||||
}
|
||||
},
|
||||
"speakers": {
|
||||
"heading": "Speakers",
|
||||
"label": "Select your audio output",
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
"label": "Sélectionner votre entrée audio",
|
||||
"disabled": "Micro désactivé"
|
||||
},
|
||||
"noiseReduction": {
|
||||
"label": "Réduction de bruit",
|
||||
"heading": "Réduction de bruit",
|
||||
"ariaLabel": {
|
||||
"enable": "Activer la réduction du bruit",
|
||||
"disable": "Désactiver la réduction du bruit"
|
||||
}
|
||||
},
|
||||
"speakers": {
|
||||
"heading": "Haut-parleurs",
|
||||
"label": "Sélectionner votre sortie audio",
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
"label": "Selecteer uw audioinvoer",
|
||||
"disabled": "Microfoon uitgeschakeld"
|
||||
},
|
||||
"noiseReduction": {
|
||||
"label": "Ruisonderdrukking",
|
||||
"heading": "Ruisonderdrukking",
|
||||
"ariaLabel": {
|
||||
"enable": "Ruisonderdrukking inschakelen",
|
||||
"disable": "Ruisonderdrukking uitschakelen"
|
||||
}
|
||||
},
|
||||
"speakers": {
|
||||
"heading": "Luidsprekers",
|
||||
"label": "Selecteer uw audio-uitvoer",
|
||||
|
||||
@@ -8,10 +8,14 @@ import {
|
||||
|
||||
export type LocalUserChoices = LocalUserChoicesLK & {
|
||||
processorSerialized?: ProcessorSerialized
|
||||
noiseReductionEnabled?: boolean
|
||||
}
|
||||
|
||||
function getUserChoicesState(): LocalUserChoices {
|
||||
return loadUserChoices()
|
||||
return {
|
||||
noiseReductionEnabled: false,
|
||||
...loadUserChoices(),
|
||||
}
|
||||
}
|
||||
|
||||
export const userChoicesStore = proxy<LocalUserChoices>(getUserChoicesState())
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "0.1.25",
|
||||
"version": "0.1.26",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mail_mjml",
|
||||
"version": "0.1.25",
|
||||
"version": "0.1.26",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@html-to/text-cli": "0.5.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "0.1.25",
|
||||
"version": "0.1.26",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "0.1.25",
|
||||
"version": "0.1.26",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sdk",
|
||||
"version": "0.1.25",
|
||||
"version": "0.1.26",
|
||||
"license": "ISC",
|
||||
"workspaces": [
|
||||
"./library",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "0.1.25",
|
||||
"version": "0.1.26",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "summary"
|
||||
version = "0.1.25"
|
||||
version = "0.1.26"
|
||||
dependencies = [
|
||||
"fastapi[standard]>=0.105.0",
|
||||
"uvicorn>=0.24.0",
|
||||
|
||||
Reference in New Issue
Block a user