diff --git a/src/lib/RoomConnection.ts b/src/lib/RoomConnection.ts index 0a1a96f5..1d89bf54 100644 --- a/src/lib/RoomConnection.ts +++ b/src/lib/RoomConnection.ts @@ -21,6 +21,7 @@ import ServerSocket, { ChatMessage as SignalChatMessage, ClientLeftEvent, ClientMetadataReceivedEvent, + CloudRecordingStartedEvent, KnockerLeftEvent, KnockAcceptedEvent, KnockRejectedEvent, @@ -58,8 +59,9 @@ export type ConnectionStatus = | "knock_rejected"; export type CloudRecordingState = { - status: "recording"; - startedAt: number; + error?: string; + status: "recording" | "requested" | "error"; + startedAt?: number; }; export type LiveStreamState = { @@ -138,7 +140,9 @@ export type LocalMicrophoneEnabledEvent = { export interface RoomEventsMap { chat_message: (e: CustomEvent) => void; + cloud_recording_request_started: (e: CustomEvent) => void; cloud_recording_started: (e: CustomEvent) => void; + cloud_recording_started_error: (e: CustomEvent) => void; cloud_recording_stopped: (e: CustomEvent) => void; local_camera_enabled: (e: CustomEvent) => void; local_microphone_enabled: (e: CustomEvent) => void; @@ -351,6 +355,7 @@ export default class RoomConnection extends TypedEventTarget { this.signalSocket.on("knocker_left", this._handleKnockerLeft.bind(this)); this.signalSocket.on("room_joined", this._handleRoomJoined.bind(this)); this.signalSocket.on("room_knocked", this._handleRoomKnocked.bind(this)); + this.signalSocket.on("cloud_recording_started", this._handleCloudRecordingStarted.bind(this)); this.signalSocket.on("cloud_recording_stopped", this._handleCloudRecordingStopped.bind(this)); this.signalSocket.on("screenshare_started", this._handleScreenshareStarted.bind(this)); this.signalSocket.on("screenshare_stopped", this._handleScreenshareStopped.bind(this)); @@ -408,7 +413,19 @@ export default class RoomConnection extends TypedEventTarget { this.dispatchEvent(new RoomConnectionEvent("chat_message", { detail: message })); } - private _handleCloudRecordingStarted({ client }: { client: SignalClient }) { + private _handleCloudRecordingStarted(event: CloudRecordingStartedEvent) { + // Only handle the start failure event here. The recording is + // considered started when the recorder client joins. + if (event.error) { + this.dispatchEvent( + new RoomConnectionEvent("cloud_recording_started_error", { + detail: { error: event.error, status: "error" }, + }) + ); + } + } + + private _handleRecorderClientJoined({ client }: { client: SignalClient }) { this.dispatchEvent( new RoomConnectionEvent("cloud_recording_started", { detail: { @@ -438,7 +455,7 @@ export default class RoomConnection extends TypedEventTarget { private _handleNewClient({ client }: NewClientEvent) { if (client.role.roleName === "recorder") { - this._handleCloudRecordingStarted({ client }); + this._handleRecorderClientJoined({ client }); } if (client.role.roleName === "streamer") { this._handleStreamingStarted(); @@ -568,7 +585,7 @@ export default class RoomConnection extends TypedEventTarget { const recorderClient = clients.find((c) => c.role.roleName === "recorder"); if (recorderClient) { - this._handleCloudRecordingStarted({ client: recorderClient }); + this._handleRecorderClientJoined({ client: recorderClient }); } const streamerClient = clients.find((c) => c.role.roleName === "streamer"); @@ -971,6 +988,7 @@ export default class RoomConnection extends TypedEventTarget { }) ); } + public stopScreenshare() { if (this.localMedia.screenshareStream) { const { id } = this.localMedia.screenshareStream; @@ -983,4 +1001,15 @@ export default class RoomConnection extends TypedEventTarget { this.localMedia.stopScreenshare(); } } + + public startCloudRecording() { + this.signalSocket.emit("start_recording", { + recording: "cloud", + }); + this.dispatchEvent(new RoomConnectionEvent("cloud_recording_request_started")); + } + + public stopCloudRecording() { + this.signalSocket.emit("stop_recording"); + } } diff --git a/src/lib/react/useRoomConnection.ts b/src/lib/react/useRoomConnection.ts index 2d0a6b53..e12d8ee6 100644 --- a/src/lib/react/useRoomConnection.ts +++ b/src/lib/react/useRoomConnection.ts @@ -58,6 +58,13 @@ type RoomConnectionEvent = type: "CLOUD_RECORDING_STARTED"; payload: CloudRecordingState; } + | { + type: "CLOUD_RECORDING_REQUEST_STARTED"; + } + | { + type: "CLOUD_RECORDING_STARTED_ERROR"; + payload: CloudRecordingState; + } | { type: "CLOUD_RECORDING_STOPPED"; } @@ -225,6 +232,13 @@ function reducer(state: RoomConnectionState, action: RoomConnectionEvent): RoomC ...state, chatMessages: [...state.chatMessages, action.payload], }; + case "CLOUD_RECORDING_REQUEST_STARTED": + return { + ...state, + cloudRecording: { + status: "requested", + }, + }; case "CLOUD_RECORDING_STARTED": return { ...state, @@ -233,6 +247,14 @@ function reducer(state: RoomConnectionState, action: RoomConnectionEvent): RoomC startedAt: action.payload.startedAt, }, }; + case "CLOUD_RECORDING_STARTED_ERROR": + return { + ...state, + cloudRecording: { + status: action.payload.status, + error: action.payload.error, + }, + }; case "CLOUD_RECORDING_STOPPED": delete state.cloudRecording; return { @@ -389,7 +411,9 @@ interface RoomConnectionActions { toggleMicrophone(enabled?: boolean): void; acceptWaitingParticipant(participantId: string): void; rejectWaitingParticipant(participantId: string): void; + startCloudRecording(): void; startScreenshare(): void; + stopCloudRecording(): void; stopScreenshare(): void; } @@ -449,10 +473,16 @@ export function useRoomConnection( createEventListener("chat_message", (e) => { dispatch({ type: "CHAT_MESSAGE", payload: e.detail }); }), + createEventListener("cloud_recording_request_started", () => { + dispatch({ type: "CLOUD_RECORDING_REQUEST_STARTED" }); + }), createEventListener("cloud_recording_started", (e) => { const { status, startedAt } = e.detail; dispatch({ type: "CLOUD_RECORDING_STARTED", payload: { status, startedAt } }); }), + createEventListener("cloud_recording_started_error", (e) => { + dispatch({ type: "CLOUD_RECORDING_STARTED_ERROR", payload: e.detail }); + }), createEventListener("cloud_recording_stopped", () => { dispatch({ type: "CLOUD_RECORDING_STOPPED" }); }), @@ -596,6 +626,16 @@ export function useRoomConnection( rejectWaitingParticipant: (participantId) => { roomConnection.rejectWaitingParticipant(participantId); }, + startCloudRecording: () => { + // don't start recording if it's already started or requested + if (state.cloudRecording && ["recording", "requested"].includes(state.cloudRecording?.status)) { + return; + } + roomConnection.startCloudRecording(); + }, + stopCloudRecording: () => { + roomConnection.stopCloudRecording(); + }, startScreenshare: async () => { dispatch({ type: "LOCAL_SCREENSHARE_STARTING" }); diff --git a/src/types.d.ts b/src/types.d.ts index c43ac1e0..0c5d617b 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -149,6 +149,11 @@ declare module "@whereby/jslib-media/src/utils/ServerSocket" { userId: string; } + interface CloudRecordingStartedEvent { + error?: string; + startedAt?: string; + } + interface ClientLeftEvent { clientId: string; } @@ -217,6 +222,7 @@ declare module "@whereby/jslib-media/src/utils/ServerSocket" { chat_message: ChatMessage; client_left: ClientLeftEvent; client_metadata_received: ClientMetadataReceivedEvent; + cloud_recording_started: CloudRecordingStartedEvent; cloud_recording_stopped: void; chat_message: ChatMessage; connect: void; @@ -266,6 +272,8 @@ declare module "@whereby/jslib-media/src/utils/ServerSocket" { knock_room: KnockRoomRequest; leave_room: void; send_client_metadata: { type: string; payload: { displayName?: string } }; + start_recording: { recording: string }; + stop_recording: void; } export default class ServerSocket {