diff --git a/.changeset/fresh-bears-wait.md b/.changeset/fresh-bears-wait.md new file mode 100644 index 000000000..852efce96 --- /dev/null +++ b/.changeset/fresh-bears-wait.md @@ -0,0 +1,6 @@ +--- +"@whereby.com/browser-sdk": minor +"@whereby.com/core": minor +--- + +Add camera, mic, and connection error subscribers to RoomConnectionClient diff --git a/.changeset/khaki-eagles-warn.md b/.changeset/khaki-eagles-warn.md new file mode 100644 index 000000000..94f2f3e82 --- /dev/null +++ b/.changeset/khaki-eagles-warn.md @@ -0,0 +1,5 @@ +--- +"@whereby.com/core": minor +--- + +Expose roomSessionId on RoomConnectionClient diff --git a/packages/browser-sdk/src/lib/react/useRoomConnection/initialState.ts b/packages/browser-sdk/src/lib/react/useRoomConnection/initialState.ts index 5ffa2fd57..40713baed 100644 --- a/packages/browser-sdk/src/lib/react/useRoomConnection/initialState.ts +++ b/packages/browser-sdk/src/lib/react/useRoomConnection/initialState.ts @@ -9,10 +9,14 @@ export const initialState: RoomConnectionState = { groupedParticipants: [], participantsInCurrentGroup: [], }, + isCameraEnabled: false, + isMicrophoneEnabled: false, localParticipant: undefined, remoteParticipants: [], screenshares: [], connectionStatus: "ready", + connectionError: null, waitingParticipants: [], spotlightedParticipants: [], + roomSessionId: null, }; diff --git a/packages/core/src/client/RoomConnection/__tests__/RoomConnectionClient.spec.ts b/packages/core/src/client/RoomConnection/__tests__/RoomConnectionClient.spec.ts new file mode 100644 index 000000000..d085b627d --- /dev/null +++ b/packages/core/src/client/RoomConnection/__tests__/RoomConnectionClient.spec.ts @@ -0,0 +1,413 @@ +import { RoomConnectionClient } from "../"; +import { Store, RootState } from "../../../redux"; +import { createStore } from "../../../redux/tests/store.setup"; + +describe("RoomConnectionClient", () => { + let client: RoomConnectionClient; + let mockStore: Store; + let initialState: RootState; + let storeSubscriber: () => void; + + beforeEach(() => { + mockStore = createStore(); + jest.spyOn(mockStore, "subscribe"); + jest.spyOn(mockStore, "dispatch"); + jest.spyOn(mockStore, "getState"); + initialState = mockStore.getState(); + client = new RoomConnectionClient(mockStore); + // the function the client uses to listen for store changes + storeSubscriber = (mockStore.subscribe as jest.Mock).mock.calls[0][0]; + }); + + describe("subscribeToChatMessages", () => { + it("triggers the chat messages lisneter", () => { + const callback = jest.fn(); + const unsubscribe = client.subscribeToChatMessages(callback); + + // not called for no changes + storeSubscriber(); + expect(callback).not.toHaveBeenCalled(); + + let chatState = { chatMessages: [{ text: "Some message" }] }; + (mockStore.getState as jest.Mock).mockReturnValue({ ...initialState, chat: chatState }); + storeSubscriber(); + expect(callback).toHaveBeenCalledWith(chatState.chatMessages); + + unsubscribe(); + + chatState = { chatMessages: [{ text: "another message" }] }; + (mockStore.getState as jest.Mock).mockReturnValue({ ...initialState, chat: chatState }); + // not called after unsubscribe + storeSubscriber(); + expect(callback).toHaveBeenCalledTimes(1); + }); + }); + + describe("subscribeToCloudRecording", () => { + it("triggers the cloud recording listener", () => { + const callback = jest.fn(); + const unsubscribe = client.subscribeToCloudRecording(callback); + + storeSubscriber(); + expect(callback).not.toHaveBeenCalled(); + + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + cloudRecording: { status: "recording" }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledWith({ status: "recording" }); + + unsubscribe(); + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + cloudRecording: { status: "stopped" }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledTimes(1); + }); + }); + + describe("subscribeToLiveTranscription", () => { + it("triggers the live transcription listener", () => { + const callback = jest.fn(); + const unsubscribe = client.subscribeToLiveTranscription(callback); + + storeSubscriber(); + expect(callback).not.toHaveBeenCalled(); + + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + liveTranscription: { status: "requested" }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledWith({ status: "requested" }); + + unsubscribe(); + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + liveTranscription: { status: "transcribing" }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledTimes(1); + }); + }); + + describe("subscribeToBreakoutConfig", () => { + it("triggers the breakout config listener", () => { + const callback = jest.fn(); + const unsubscribe = client.subscribeToBreakoutConfig(callback); + + storeSubscriber(); + expect(callback).not.toHaveBeenCalled(); + + (mockStore.getState as jest.Mock).mockImplementation(() => ({ + ...initialState, + breakout: { startedAt: new Date() }, + })); + storeSubscriber(); + expect(callback).toHaveBeenCalledWith(expect.objectContaining({ isActive: true })); + + unsubscribe(); + (mockStore.getState as jest.Mock).mockReturnValue({ ...initialState, breakout: { isActive: false } }); + storeSubscriber(); + expect(callback).toHaveBeenCalledTimes(1); + }); + }); + + describe("subscribeToConnectionStatus", () => { + it("triggers the connection status listener", () => { + const callback = jest.fn(); + const unsubscribe = client.subscribeToConnectionStatus(callback); + + storeSubscriber(); + expect(callback).not.toHaveBeenCalled(); + + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + roomConnection: { status: "connected" }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledWith("connected"); + + unsubscribe(); + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + roomConnection: { connectionStatus: "disconnected" }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledTimes(1); + }); + }); + + describe("subscribeToConnectionError", () => { + it("triggers the connection error listener", () => { + const callback = jest.fn(); + const unsubscribe = client.subscribeToConnectionError(callback); + + storeSubscriber(); + expect(callback).not.toHaveBeenCalled(); + + const roomConnection = { error: new Error() }; + (mockStore.getState as jest.Mock).mockReturnValue({ ...initialState, roomConnection }); + storeSubscriber(); + expect(callback).toHaveBeenCalledWith(roomConnection.error); + + unsubscribe(); + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + roomConnection: { connectionError: "other" }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledTimes(1); + }); + }); + + describe("subscribeToLiveStream", () => { + it("triggers the live stream listener", () => { + const callback = jest.fn(); + const unsubscribe = client.subscribeToLiveStream(callback); + + storeSubscriber(); + expect(callback).not.toHaveBeenCalled(); + + const startedAt = new Date(); + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + streaming: { isStreaming: true, startedAt }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledWith({ status: "streaming", startedAt: startedAt }); + + unsubscribe(); + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + streaming: { liveStream: undefined }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledTimes(1); + }); + }); + + describe("subscribeToLocalScreenshareStatus", () => { + it("triggers the local screenshare status listener", () => { + const callback = jest.fn(); + const unsubscribe = client.subscribeToLocalScreenshareStatus(callback); + + storeSubscriber(); + expect(callback).not.toHaveBeenCalled(); + + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + localParticipant: { isScreenSharing: true }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledWith("active"); + + unsubscribe(); + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + localParticipant: { isScreenSharing: false }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledTimes(1); + }); + }); + + describe("subscribeToLocalParticipant", () => { + it("triggers the local participant listener", () => { + const callback = jest.fn(); + const unsubscribe = client.subscribeToLocalParticipant(callback); + + storeSubscriber(); + expect(callback).not.toHaveBeenCalled(); + + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + localParticipant: { id: "1", displayName: "Me" }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledWith({ id: "1", displayName: "Me" }); + + unsubscribe(); + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + localParticipant: { id: "1", displayName: "Updated" }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledTimes(1); + }); + }); + + describe("subscribeToRemoteParticipants", () => { + it("triggers the remote participants listener", () => { + const callback = jest.fn(); + const unsubscribe = client.subscribeToRemoteParticipants(callback); + + storeSubscriber(); + expect(callback).not.toHaveBeenCalled(); + + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + remoteParticipants: { remoteParticipants: [{ id: "1" }] }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledWith([{ id: "1" }]); + + unsubscribe(); + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + remoteParticipants: { remoteParticipants: [{ id: "2" }] }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledTimes(1); + }); + }); + + describe("subscribeToScreenshares", () => { + it("triggers the screenshares listener", () => { + const callback = jest.fn(); + const unsubscribe = client.subscribeToScreenshares(callback); + + storeSubscriber(); + expect(callback).not.toHaveBeenCalled(); + + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + remoteParticipants: { + remoteParticipants: [{ presentationStream: { id: "id", getTracks: () => [] }, id: "pid" }], + }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledWith([expect.objectContaining({ id: "id", participantId: "pid" })]); + + unsubscribe(); + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + remoteParticipants: { remoteParticipants: [] }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledTimes(1); + }); + }); + + describe("subscribeToWaitingParticipants", () => { + it("triggers the waiting participants listener", () => { + const callback = jest.fn(); + const unsubscribe = client.subscribeToWaitingParticipants(callback); + + storeSubscriber(); + expect(callback).not.toHaveBeenCalled(); + + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + waitingParticipants: { waitingParticipants: [{ id: "1" }] }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledWith([{ id: "1" }]); + + unsubscribe(); + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + waitingParticipants: { waitingParticipants: [] }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledTimes(1); + }); + }); + + describe("subscribeToSpotlightedParticipants", () => { + it("triggers the spotlighted participants listener", () => { + const callback = jest.fn(); + const unsubscribe = client.subscribeToSpotlightedParticipants(callback); + + storeSubscriber(); + expect(callback).not.toHaveBeenCalled(); + + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + spotlights: { sorted: [{ clientId: "1", streamId: "0" }] }, + remoteParticipants: { remoteParticipants: [{ id: "1" }] }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledWith([expect.objectContaining({ id: "1", clientId: "1" })]); + + unsubscribe(); + (mockStore.getState as jest.Mock).mockReturnValue({ ...initialState, spotlights: { sorted: [] } }); + storeSubscriber(); + expect(callback).toHaveBeenCalledTimes(1); + }); + }); + + describe("subscribeToCameraState", () => { + it("triggers the camera state listener", () => { + const callback = jest.fn(); + const unsubscribe = client.subscribeToCameraState(callback); + + storeSubscriber(); + expect(callback).not.toHaveBeenCalled(); + + (mockStore.getState as jest.Mock).mockReturnValue({ ...initialState, localMedia: { cameraEnabled: true } }); + storeSubscriber(); + expect(callback).toHaveBeenCalledWith(true); + + unsubscribe(); + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + localMedia: { cameraEnabled: false }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledTimes(1); + }); + }); + + describe("subscribeToMicrophoneState", () => { + it("triggers the microphone state listener", () => { + const callback = jest.fn(); + const unsubscribe = client.subscribeToMicrophoneState(callback); + + storeSubscriber(); + expect(callback).not.toHaveBeenCalled(); + + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + localMedia: { microphoneEnabled: true }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledWith(true); + + unsubscribe(); + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + localMedia: { microphoneEnabled: false }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledTimes(1); + }); + }); + + describe("subscribeToRoomSessionId", () => { + it("triggers the room session ID listener", () => { + const callback = jest.fn(); + const unsubscribe = client.subscribeToRoomSessionId(callback); + + storeSubscriber(); + expect(callback).not.toHaveBeenCalled(); + + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + roomConnection: { session: { id: "some ID" } }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledWith("some ID"); + + unsubscribe(); + (mockStore.getState as jest.Mock).mockReturnValue({ + ...initialState, + roomConnection: { session: { id: "some other ID" } }, + }); + storeSubscriber(); + expect(callback).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/core/src/client/RoomConnection/events.ts b/packages/core/src/client/RoomConnection/events.ts index 08f315e66..464162e93 100644 --- a/packages/core/src/client/RoomConnection/events.ts +++ b/packages/core/src/client/RoomConnection/events.ts @@ -21,15 +21,22 @@ export const CLOUD_RECORDING_STATUS_CHANGED = "cloud-recording:status-changed"; export const CONNECTION_STATUS_CHANGED = "connection:status-changed"; /* Live Transcription Events */ export const LIVE_TRANSCRIPTION_STATUS_CHANGED = "live-transcription:status-changed"; +/* Connection Error Events */ +export const CONNECTION_ERROR_CHANGED = "connection:error-changed"; /* Local participant events */ export const LOCAL_PARTICIPANT_CHANGED = "local-participant:changed"; export const LOCAL_SCREENSHARE_STATUS_CHANGED = "local-screenshare:status-changed"; /* Remote participant events */ export const REMOTE_PARTICIPANTS_CHANGED = "remote-participants:changed"; +/* Room Session ID events */ +export const ROOM_SESSION_ID_CHANGED = "room-session-id:changed"; /* Screen share events */ export const SCREENSHARE_STARTED = "screenshare:started"; export const SCREENSHARE_STOPPED = "screenshare:stopped"; +export const CAMERA_STATE_CHANGED = "camera:state-changed"; +export const MICROPHONE_STATE_CHANGED = "microphone:state-changed"; + /* Streaming events */ export const STREAMING_STARTED = "streaming:started"; export const STREAMING_STOPPED = "streaming:stopped"; @@ -54,17 +61,21 @@ type RoomJoinedEvent = { /* Event types for RoomConnection client */ export type RoomConnectionEvents = { [BREAKOUT_CONFIG_CHANGED]: [config: BreakoutState]; + [CAMERA_STATE_CHANGED]: [isCameraEnabled: boolean]; [CHAT_NEW_MESSAGE]: [message: ChatMessage]; [CLOUD_RECORDING_STATUS_CHANGED]: [status: CloudRecordingState | undefined]; [CONNECTION_STATUS_CHANGED]: [status: ConnectionStatus]; [LIVE_TRANSCRIPTION_STATUS_CHANGED]: [status: LiveTranscriptionState | undefined]; + [CONNECTION_ERROR_CHANGED]: [error: string | null]; [LOCAL_PARTICIPANT_CHANGED]: [participant?: LocalParticipantState]; [LOCAL_SCREENSHARE_STATUS_CHANGED]: [status?: LocalScreenshareStatus]; + [MICROPHONE_STATE_CHANGED]: [isMicrophoneEnabled: boolean]; [REMOTE_PARTICIPANTS_CHANGED]: [participants: RemoteParticipantState[]]; [SCREENSHARE_STARTED]: [screenshare: Screenshare]; [SCREENSHARE_STOPPED]: [screenshareId: string]; [ROOM_JOINED]: [room: RoomJoinedEvent]; [ROOM_JOINED_ERROR]: [error: RoomJoinedErrors | string]; + [ROOM_SESSION_ID_CHANGED]: [roomSessionId: string | null]; [WAITING_PARTICIPANT_JOINED]: [participant: WaitingParticipant]; [WAITING_PARTICIPANT_LEFT]: [participantId: string]; [SPOTLIGHT_PARTICIPANT_ADDED]: [participant: ClientView]; diff --git a/packages/core/src/client/RoomConnection/index.ts b/packages/core/src/client/RoomConnection/index.ts index e7fca87a9..25c3c5bfe 100644 --- a/packages/core/src/client/RoomConnection/index.ts +++ b/packages/core/src/client/RoomConnection/index.ts @@ -52,15 +52,19 @@ import type { } from "./types"; import { BREAKOUT_CONFIG_CHANGED, + CAMERA_STATE_CHANGED, CHAT_NEW_MESSAGE, CLOUD_RECORDING_STATUS_CHANGED, + CONNECTION_ERROR_CHANGED, CONNECTION_STATUS_CHANGED, LIVE_TRANSCRIPTION_STATUS_CHANGED, LOCAL_PARTICIPANT_CHANGED, LOCAL_SCREENSHARE_STATUS_CHANGED, + MICROPHONE_STATE_CHANGED, REMOTE_PARTICIPANTS_CHANGED, ROOM_JOINED, ROOM_JOINED_ERROR, + ROOM_SESSION_ID_CHANGED, SCREENSHARE_STARTED, SCREENSHARE_STOPPED, SPOTLIGHT_PARTICIPANT_ADDED, @@ -72,7 +76,6 @@ import { type RoomConnectionEvents, } from "./events"; import { selectRoomConnectionState } from "./selector"; -import { coreVersion } from "../../version"; import { BaseClient } from "../BaseClient"; import { doCameraEffectsSwitchPreset } from "../../redux/slices/cameraEffects"; @@ -80,18 +83,22 @@ export class RoomConnectionClient extends BaseClient; private selfId: string | null = null; - private chatMessageSubscribers = new Set<(messages: ChatMessage[]) => void>(); - private cloudRecordingSubscribers = new Set<(status: CloudRecordingState | undefined) => void>(); - private liveTranscriptionSubscribers = new Set<(status: LiveTranscriptionState | undefined) => void>(); private breakoutSubscribers = new Set<(config: BreakoutState) => void>(); + private cameraStateSubscribers = new Set<(isCameraEnabled: boolean) => void>(); + private chatMessageSubscribers = new Set<(messages: ChatMessage[]) => void>(); + private cloudRecordingSubscribers = new Set<(status: CloudRecordingState | undefined | undefined) => void>(); + private connectionErrorSubscribers = new Set<(status: string | null) => void>(); private connectionStatusSubscribers = new Set<(status: ConnectionStatus) => void>(); private liveStreamSubscribers = new Set<(status: { status: "streaming" } | undefined) => void>(); - private localScreenshareStatusSubscribers = new Set<(status?: LocalScreenshareStatus) => void>(); + private liveTranscriptionSubscribers = new Set<(status: LiveTranscriptionState | undefined) => void>(); private localParticipantSubscribers = new Set<(participant?: LocalParticipantState) => void>(); + private localScreenshareStatusSubscribers = new Set<(status?: LocalScreenshareStatus) => void>(); + private microphoneStateSubscribers = new Set<(isMicrophoneEnabled: boolean) => void>(); private remoteParticipantsSubscribers = new Set<(participants: RemoteParticipantState[]) => void>(); + private roomSessionIdSubscribers = new Set<(roomSessionId: string | null) => void>(); private screenshareSubscribers = new Set<(screenshares: ScreenshareState[]) => void>(); - private waitingParticipantsSubscribers = new Set<(participants: WaitingParticipantState[]) => void>(); private spotlightedParticipantsSubscribers = new Set<(participants: ClientView[]) => void>(); + private waitingParticipantsSubscribers = new Set<(participants: WaitingParticipantState[]) => void>(); constructor(store: AppStore) { super(store); @@ -124,6 +131,21 @@ export class RoomConnectionClient extends BaseClient cb(state.connectionError)); + this.emit(CONNECTION_ERROR_CHANGED, state.connectionError); + } + + if (state.isCameraEnabled !== previousState.isCameraEnabled) { + this.cameraStateSubscribers.forEach((cb) => cb(state.isCameraEnabled)); + this.emit(CAMERA_STATE_CHANGED, state.isCameraEnabled); + } + + if (state.isMicrophoneEnabled !== previousState.isMicrophoneEnabled) { + this.microphoneStateSubscribers.forEach((cb) => cb(state.isMicrophoneEnabled)); + this.emit(MICROPHONE_STATE_CHANGED, state.isMicrophoneEnabled); + } + if (state.liveStream !== previousState.liveStream) { this.liveStreamSubscribers.forEach((cb) => cb(state.liveStream)); if (state.liveStream?.status === "streaming") { @@ -148,6 +170,11 @@ export class RoomConnectionClient extends BaseClient cb(state.roomSessionId)); + this.emit(ROOM_SESSION_ID_CHANGED, state.roomSessionId); + } + if (state.screenshares !== previousState.screenshares) { this.screenshareSubscribers.forEach((cb) => cb(state.screenshares)); @@ -251,6 +278,11 @@ export class RoomConnectionClient extends BaseClient this.connectionStatusSubscribers.delete(callback); } + public subscribeToConnectionError(callback: (error: string | null) => void): () => void { + this.connectionErrorSubscribers.add(callback); + return () => this.connectionErrorSubscribers.delete(callback); + } + public subscribeToLiveStream(callback: (status: { status: "streaming" } | undefined) => void): () => void { this.liveStreamSubscribers.add(callback); return () => this.liveStreamSubscribers.delete(callback); @@ -286,6 +318,20 @@ export class RoomConnectionClient extends BaseClient this.spotlightedParticipantsSubscribers.delete(callback); } + public subscribeToMicrophoneState(callback: (isMicrophoneEnabled: boolean) => void): () => void { + this.microphoneStateSubscribers.add(callback); + return () => this.microphoneStateSubscribers.delete(callback); + } + + public subscribeToCameraState(callback: (isCameraEnabled: boolean) => void): () => void { + this.cameraStateSubscribers.add(callback); + return () => this.cameraStateSubscribers.delete(callback); + } + + public subscribeToRoomSessionId(callback: (roomSessionId: string | null) => void): () => void { + this.roomSessionIdSubscribers.add(callback); + return () => this.roomSessionIdSubscribers.delete(callback); + } /** * Get the current state of the Whereby client. * @return {object} - The current state of the client. @@ -322,16 +368,16 @@ export class RoomConnectionClient extends BaseClient { const state: RoomConnectionState = { chatMessages, @@ -71,7 +83,10 @@ export const selectRoomConnectionState = createSelector( participantsInCurrentGroup: clientViewsInCurrentGroup, }, connectionStatus, + connectionError, events: notificationsEmitter, + isCameraEnabled, + isMicrophoneEnabled, liveStream: streaming.isStreaming ? { status: "streaming", @@ -83,6 +98,7 @@ export const selectRoomConnectionState = createSelector( error: liveTranscription.error, startedAt: liveTranscription.startedAt, status: liveTranscription.status, + transcriptionId: liveTranscription.transcriptionId, } : undefined, localScreenshareStatus: localParticipant.isScreenSharing ? "active" : undefined, @@ -91,6 +107,7 @@ export const selectRoomConnectionState = createSelector( screenshares, waitingParticipants, spotlightedParticipants: spotlightedClientViews, + roomSessionId: roomSessionId ?? null, }; return state; diff --git a/packages/core/src/client/RoomConnection/types.ts b/packages/core/src/client/RoomConnection/types.ts index 2fe0363ac..1926b0fc6 100644 --- a/packages/core/src/client/RoomConnection/types.ts +++ b/packages/core/src/client/RoomConnection/types.ts @@ -49,6 +49,7 @@ export type LiveTranscriptionState = { error?: string; status: "transcribing" | "requested" | "error"; startedAt?: number; + transcriptionId?: string; }; export type LiveStreamState = { @@ -74,15 +75,19 @@ export type BreakoutState = { export interface RoomConnectionState { connectionStatus: ConnectionStatus; + connectionError: string | null; chatMessages: ChatMessage[]; cloudRecording?: CloudRecordingState; breakout: BreakoutState; events?: NotificationsEventEmitter; + isCameraEnabled: boolean; + isMicrophoneEnabled: boolean; liveStream?: LiveStreamState; liveTranscription?: LiveTranscriptionState; localScreenshareStatus?: LocalScreenshareStatus; localParticipant?: LocalParticipantState; remoteParticipants: RemoteParticipantState[]; + roomSessionId: string | null; screenshares: Screenshare[]; waitingParticipants: WaitingParticipantState[]; spotlightedParticipants: ClientView[]; diff --git a/packages/core/src/redux/slices/liveTranscription.ts b/packages/core/src/redux/slices/liveTranscription.ts index 1d1241086..87c538380 100644 --- a/packages/core/src/redux/slices/liveTranscription.ts +++ b/packages/core/src/redux/slices/liveTranscription.ts @@ -13,6 +13,7 @@ export interface LiveTranscriptionState { error?: string; status?: "transcribing" | "requested" | "error"; startedAt?: number; + transcriptionId?: string; } export const initialLiveTranscriptionState: LiveTranscriptionState = { @@ -55,6 +56,7 @@ export const liveTranscriptionSlice = createSlice({ isTranscribing: false, status: "error", error: payload.error, + transcriptionId: payload.transcriptionId, }; });