@@ -11,12 +11,15 @@ import {
1111 TokenSourceFetchOptions ,
1212 RoomConnectOptions ,
1313 decodeTokenPayload ,
14+ BaseKeyProvider ,
15+ RoomOptions ,
16+ ExternalE2EEKeyProvider ,
1417} from 'livekit-client' ;
1518import { EventEmitter } from 'events' ;
1619
1720import { useMaybeRoomContext } from '../context' ;
1821import { AgentState , useAgent , useAgentTimeoutIdStore } from './useAgent' ;
19- import { TrackReference } from '@livekit/components-core' ;
22+ import { TrackReference , log } from '@livekit/components-core' ;
2023import { useLocalParticipant } from './useLocalParticipant' ;
2124
2225/** @beta */
@@ -129,9 +132,9 @@ type SessionStateDisconnected = SessionStateCommon & {
129132
130133type SessionActions = {
131134 /** Returns a promise that resolves once the room connects. */
132- waitUntilConnected : ( signal ?: AbortSignal ) => void ;
135+ waitUntilConnected : ( signal ?: AbortSignal ) => Promise < void > ;
133136 /** Returns a promise that resolves once the room disconnects */
134- waitUntilDisconnected : ( signal ?: AbortSignal ) => void ;
137+ waitUntilDisconnected : ( signal ?: AbortSignal ) => Promise < void > ;
135138
136139 prepareConnection : ( ) => Promise < void > ;
137140
@@ -140,6 +143,9 @@ type SessionActions = {
140143
141144 /** Disconnect from the underlying room */
142145 end : ( ) => Promise < void > ;
146+
147+ /** Enable or disable E2EE. */
148+ setEncryptionEnabled : ( enabled : boolean ) => Promise < void > ;
143149} ;
144150
145151/** @beta */
@@ -162,17 +168,53 @@ export function isUseSessionReturn(value: unknown): value is UseSessionReturn {
162168}
163169
164170type UseSessionCommonOptions = {
165- room ?: Room ;
166-
167171 /**
168172 * Amount of time in milliseonds the system will wait for an agent to join the room, before
169173 * transitioning to the "failure" state.
170174 */
171175 agentConnectTimeoutMilliseconds ?: number ;
172176} ;
173177
174- type UseSessionConfigurableOptions = UseSessionCommonOptions & TokenSourceFetchOptions ;
175- type UseSessionFixedOptions = UseSessionCommonOptions ;
178+ type UseSessionWithRoomOptions = {
179+ room : Room ;
180+ encryption ?: never ;
181+ } ;
182+
183+ type UseSessionEncryptionOptions =
184+ | {
185+ /**
186+ * Accepts a passphrase that's used to create the crypto keys.
187+ * When passing in a string, PBKDF2 is used. (recommended for maximum compatibility across SDKs)
188+ * When passing in an ArrayBuffer of cryptographically random numbers, HKDF is used.
189+ *
190+ * Note: Not all client SDKs support HKDF.
191+ */
192+ key : string | ArrayBuffer | BaseKeyProvider ;
193+
194+ /** An instance of the E2EE webworker, which must be constructed using your js build tool's
195+ * webworker construction mechanism. */
196+ worker : Worker ;
197+ }
198+ | {
199+ key ?: undefined ;
200+ worker ?: undefined ;
201+ } ;
202+
203+ type UseSessionWithoutRoomOptions = {
204+ // NOTE: This must be here to make typescript go down this discriminated union branch when
205+ // "room" is omitted.
206+ room ?: never ;
207+
208+ /** Configuration for room-level E2EE */
209+ encryption ?: UseSessionEncryptionOptions ;
210+ } ;
211+
212+ type UseSessionRoomOptions = UseSessionWithRoomOptions | UseSessionWithoutRoomOptions ;
213+
214+ type UseSessionConfigurableOptions = UseSessionCommonOptions &
215+ UseSessionRoomOptions &
216+ TokenSourceFetchOptions ;
217+ type UseSessionFixedOptions = UseSessionCommonOptions & UseSessionRoomOptions ;
176218
177219/**
178220 * Given two TokenSourceFetchOptions values, check to see if they are deep equal.
@@ -323,13 +365,63 @@ export function useSession(
323365 tokenSource : TokenSourceConfigurable | TokenSourceFixed ,
324366 options : UseSessionConfigurableOptions | UseSessionFixedOptions = { } ,
325367) : UseSessionReturn {
326- const { room : optionsRoom , agentConnectTimeoutMilliseconds, ...restOptions } = options ;
368+ const {
369+ room : optionsRoom ,
370+ agentConnectTimeoutMilliseconds,
371+ encryption : unstableEncryption ,
372+ ...unstableRestOptions
373+ } = options ;
374+
375+ const encryptionKey = unstableEncryption ?. key ?? null ;
376+ const encryptionWorker = unstableEncryption ?. worker ?? null ;
327377
328378 const roomFromContext = useMaybeRoomContext ( ) ;
329- const room = React . useMemo (
330- ( ) => roomFromContext ?? optionsRoom ?? new Room ( ) ,
331- [ roomFromContext , optionsRoom ] ,
332- ) ;
379+
380+ const externalKeyProviderRef = React . useRef < ExternalE2EEKeyProvider | null > ( null ) ;
381+
382+ const keyProvider = React . useMemo ( ( ) => {
383+ if ( typeof encryptionKey === 'string' || encryptionKey instanceof ArrayBuffer ) {
384+ if ( externalKeyProviderRef . current === null ) {
385+ externalKeyProviderRef . current = new ExternalE2EEKeyProvider ( ) ;
386+ }
387+ externalKeyProviderRef . current . setKey ( encryptionKey ) . catch ( ( e ) => log . error ( e ) ) ;
388+ return externalKeyProviderRef . current ;
389+ } else {
390+ return encryptionKey ;
391+ }
392+ } , [ encryptionKey ] ) ;
393+
394+ const room = React . useMemo ( ( ) => {
395+ const preGeneratedRoom = roomFromContext ?? optionsRoom ;
396+ if ( preGeneratedRoom ) {
397+ return preGeneratedRoom ;
398+ }
399+
400+ const encryptionEnabled = ! ! ( keyProvider && encryptionWorker ) ;
401+
402+ const roomOptions : RoomOptions = { } ;
403+ if ( encryptionEnabled ) {
404+ roomOptions . encryption = {
405+ keyProvider,
406+ worker : encryptionWorker ,
407+ } ;
408+ } else {
409+ log . warn (
410+ 'useSession options encryption was set, but required keys encryption.key and encryption.worker were omitted.' ,
411+ ) ;
412+ }
413+ const room = new Room ( roomOptions ) ;
414+ if ( encryptionEnabled ) {
415+ room . setE2EEEnabled ( true ) ;
416+ }
417+ return room ;
418+ } , [ roomFromContext , optionsRoom , keyProvider , encryptionWorker ] ) ;
419+
420+ React . useEffect ( ( ) => {
421+ return ( ) => {
422+ room . disconnect ( ) ;
423+ } ;
424+ } , [ room ] ) ;
333425
334426 const emitter = React . useMemo (
335427 ( ) => new EventEmitter ( ) as TypedEventEmitter < SessionCallbacks > ,
@@ -546,6 +638,11 @@ export function useSession(
546638 [ waitUntilConnectionState ] ,
547639 ) ;
548640
641+ const setEncryptionEnabled = React . useCallback (
642+ async ( enabled : boolean ) => room . setE2EEEnabled ( enabled ) ,
643+ [ room ] ,
644+ ) ;
645+
549646 const agent = useAgent (
550647 React . useMemo (
551648 ( ) => ( {
@@ -557,9 +654,9 @@ export function useSession(
557654 ) ,
558655 ) ;
559656
560- const tokenSourceFetch = useSessionTokenSourceFetch ( tokenSource , restOptions ) ;
657+ const tokenSourceFetch = useSessionTokenSourceFetch ( tokenSource , unstableRestOptions ) ;
561658
562- const [ wasSessionEndCalled , setWasSessionEndCalled ] = React . useState ( false ) ;
659+ const wasSessionEndCalledRef = React . useRef ( false ) ;
563660
564661 const start = React . useCallback (
565662 async ( connectOptions : SessionConnectOptions = { } ) => {
@@ -570,7 +667,7 @@ export function useSession(
570667 } = connectOptions ;
571668
572669 await waitUntilDisconnected ( signal ) ;
573- setWasSessionEndCalled ( false ) ;
670+ wasSessionEndCalledRef . current = false ;
574671
575672 const onSignalAbort = ( ) => {
576673 room . disconnect ( ) ;
@@ -581,7 +678,7 @@ export function useSession(
581678 // on disconnection force a new token to be fetched in order to avoid reusing the same room right after
582679 // this works around the fact that agents won't rejoin a room that existed previously
583680 // and depends on the assumption that the endpoint will return a token for a different room
584- if ( ! wasSessionEndCalled ) {
681+ if ( ! wasSessionEndCalledRef . current ) {
585682 tokenSourceFetch ( true ) ;
586683 }
587684 } ;
@@ -629,18 +726,11 @@ export function useSession(
629726
630727 signal ?. removeEventListener ( 'abort' , onSignalAbort ) ;
631728 } ,
632- [
633- room ,
634- waitUntilDisconnected ,
635- tokenSourceFetch ,
636- waitUntilConnected ,
637- agent . waitUntilConnected ,
638- wasSessionEndCalled ,
639- ] ,
729+ [ room , waitUntilDisconnected , tokenSourceFetch , waitUntilConnected , agent . waitUntilConnected ] ,
640730 ) ;
641731
642732 const end = React . useCallback ( async ( ) => {
643- setWasSessionEndCalled ( true ) ;
733+ wasSessionEndCalledRef . current = true ;
644734 tokenSourceFetch ( true ) ;
645735 await room . disconnect ( ) ;
646736 } , [ room , tokenSourceFetch ] ) ;
@@ -671,7 +761,17 @@ export function useSession(
671761 prepareConnection,
672762 start,
673763 end,
764+
765+ setEncryptionEnabled,
674766 } ) ,
675- [ conversationState , waitUntilConnected , waitUntilDisconnected , prepareConnection , start , end ] ,
767+ [
768+ conversationState ,
769+ waitUntilConnected ,
770+ waitUntilDisconnected ,
771+ prepareConnection ,
772+ start ,
773+ end ,
774+ setEncryptionEnabled ,
775+ ] ,
676776 ) ;
677777}
0 commit comments