diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md index dd772c6383..c3ee0723c6 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -2,6 +2,13 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +## [1.14.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.13.1...@stream-io/video-client-1.14.0) (2025-01-02) + + +### Features + +* **closed captions:** Integration in the SDKs ([#1508](https://github.com/GetStream/stream-video-js/issues/1508)) ([bcb8589](https://github.com/GetStream/stream-video-js/commit/bcb85892c0dafcb03f9debf8d2fd361622224166)) + ## [1.13.1](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.13.0...@stream-io/video-client-1.13.1) (2024-12-20) diff --git a/packages/client/openapitools.json b/packages/client/openapitools.json index 0f02b59fc3..3a7a5b4db4 100644 --- a/packages/client/openapitools.json +++ b/packages/client/openapitools.json @@ -2,6 +2,6 @@ "$schema": "../../node_modules/@openapitools/openapi-generator-cli/config.schema.json", "spaces": 2, "generator-cli": { - "version": "7.5.0" + "version": "7.8.0" } } diff --git a/packages/client/package.json b/packages/client/package.json index fbfb251701..6f6d787fb6 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@stream-io/video-client", - "version": "1.13.1", + "version": "1.14.0", "packageManager": "yarn@3.2.4", "main": "dist/index.cjs.js", "module": "dist/index.es.js", diff --git a/packages/client/src/Call.ts b/packages/client/src/Call.ts index ef9c4ee3ec..05829cdd7c 100644 --- a/packages/client/src/Call.ts +++ b/packages/client/src/Call.ts @@ -56,12 +56,16 @@ import type { SendCallEventResponse, SendReactionRequest, SendReactionResponse, + StartClosedCaptionsRequest, + StartClosedCaptionsResponse, StartHLSBroadcastingResponse, StartRecordingRequest, StartRecordingResponse, StartTranscriptionRequest, StartTranscriptionResponse, StatsOptions, + StopClosedCaptionsRequest, + StopClosedCaptionsResponse, StopHLSBroadcastingResponse, StopLiveResponse, StopRecordingResponse, @@ -76,7 +80,7 @@ import type { UpdateCallResponse, UpdateUserPermissionsRequest, UpdateUserPermissionsResponse, - VideoResolution, + VideoDimension, } from './gen/coordinator'; import { OwnCapability } from './gen/coordinator'; import { @@ -84,6 +88,7 @@ import { CallConstructor, CallLeaveOptions, ClientPublishOptions, + ClosedCaptionsSettings, JoinCallData, TrackMuteType, VideoTrackType, @@ -563,6 +568,7 @@ export class Call { this.dynascaleManager.setSfuClient(undefined); this.state.setCallingState(CallingState.LEFT); + this.state.dispose(); // Call all leave call hooks, e.g. to clean up global event handlers this.leaveCallHooks.forEach((hook) => hook()); @@ -1854,7 +1860,54 @@ export class Call { }; /** - * Sends a `call.permission_request` event to all users connected to the call. The call settings object contains infomration about which permissions can be requested during a call (for example a user might be allowed to request permission to publish audio, but not video). + * Starts the closed captions of the call. + */ + startClosedCaptions = async ( + options?: StartClosedCaptionsRequest, + ): Promise => { + const trx = this.state.setCaptioning(true); // optimistic update + try { + return await this.streamClient.post< + StartClosedCaptionsResponse, + StartClosedCaptionsRequest + >(`${this.streamClientBasePath}/start_closed_captions`, options); + } catch (err) { + trx.rollback(); // revert the optimistic update + throw err; + } + }; + + /** + * Stops the closed captions of the call. + */ + stopClosedCaptions = async ( + options?: StopClosedCaptionsRequest, + ): Promise => { + const trx = this.state.setCaptioning(false); // optimistic update + try { + return await this.streamClient.post< + StopClosedCaptionsResponse, + StopClosedCaptionsRequest + >(`${this.streamClientBasePath}/stop_closed_captions`, options); + } catch (err) { + trx.rollback(); // revert the optimistic update + throw err; + } + }; + + /** + * Updates the closed caption settings. + * + * @param config the closed caption settings to apply + */ + updateClosedCaptionSettings = (config: Partial) => { + this.state.updateClosedCaptionSettings(config); + }; + + /** + * Sends a `call.permission_request` event to all users connected to the call. + * The call settings object contains information about which permissions can be requested during a call + * (for example, a user might be allowed to request permission to publish audio, but not video). */ requestPermissions = async ( data: RequestPermissionRequest, @@ -2460,7 +2513,7 @@ export class Call { * preference has effect on. Affects all participants by default. */ setPreferredIncomingVideoResolution = ( - resolution: VideoResolution | undefined, + resolution: VideoDimension | undefined, sessionIds?: string[], ) => { this.dynascaleManager.setVideoTrackSubscriptionOverrides( diff --git a/packages/client/src/gen/coordinator/index.ts b/packages/client/src/gen/coordinator/index.ts index 64288fbfa6..f10ed50ab8 100644 --- a/packages/client/src/gen/coordinator/index.ts +++ b/packages/client/src/gen/coordinator/index.ts @@ -51,15 +51,21 @@ export interface APIError { * @memberof APIError */ more_info: string; + /** + * Flag that indicates if the error is unrecoverable, requests that return unrecoverable errors should not be retried, this error only applies to the request that caused it + * @type {boolean} + * @memberof APIError + */ + unrecoverable?: boolean; } /** - * + * AcceptCallResponse is the payload for accepting a call. * @export * @interface AcceptCallResponse */ export interface AcceptCallResponse { /** - * + * Duration of the request in milliseconds * @type {string} * @memberof AcceptCallResponse */ @@ -255,7 +261,7 @@ export type BlockListOptionsBehaviorEnum = (typeof BlockListOptionsBehaviorEnum)[keyof typeof BlockListOptionsBehaviorEnum]; /** - * + * BlockUserRequest is the payload for blocking a user. * @export * @interface BlockUserRequest */ @@ -268,7 +274,7 @@ export interface BlockUserRequest { user_id: string; } /** - * + * BlockUserResponse is the payload for blocking a user. * @export * @interface BlockUserResponse */ @@ -338,7 +344,7 @@ export interface BroadcastSettingsRequest { hls?: HLSSettingsRequest; } /** - * + * BroadcastSettingsResponse is the payload for broadcasting settings * @export * @interface BroadcastSettingsResponse */ @@ -423,6 +429,87 @@ export interface CallClosedCaption { * @memberof CallClosedCaption */ text: string; + /** + * + * @type {UserResponse} + * @memberof CallClosedCaption + */ + user: UserResponse; +} +/** + * This event is sent when call closed captions has failed + * @export + * @interface CallClosedCaptionsFailedEvent + */ +export interface CallClosedCaptionsFailedEvent { + /** + * + * @type {string} + * @memberof CallClosedCaptionsFailedEvent + */ + call_cid: string; + /** + * + * @type {string} + * @memberof CallClosedCaptionsFailedEvent + */ + created_at: string; + /** + * The type of event: "call.closed_captions_failed" in this case + * @type {string} + * @memberof CallClosedCaptionsFailedEvent + */ + type: string; +} +/** + * This event is sent when call closed caption has started + * @export + * @interface CallClosedCaptionsStartedEvent + */ +export interface CallClosedCaptionsStartedEvent { + /** + * + * @type {string} + * @memberof CallClosedCaptionsStartedEvent + */ + call_cid: string; + /** + * + * @type {string} + * @memberof CallClosedCaptionsStartedEvent + */ + created_at: string; + /** + * The type of event: "call.closed_captions_started" in this case + * @type {string} + * @memberof CallClosedCaptionsStartedEvent + */ + type: string; +} +/** + * This event is sent when call closed captions has stopped + * @export + * @interface CallClosedCaptionsStoppedEvent + */ +export interface CallClosedCaptionsStoppedEvent { + /** + * + * @type {string} + * @memberof CallClosedCaptionsStoppedEvent + */ + call_cid: string; + /** + * + * @type {string} + * @memberof CallClosedCaptionsStoppedEvent + */ + created_at: string; + /** + * The type of event: "call.transcription_stopped" in this case + * @type {string} + * @memberof CallClosedCaptionsStoppedEvent + */ + type: string; } /** * This event is sent when a call is created. Clients receiving this event should check if the ringing @@ -562,6 +649,18 @@ export interface CallEndedEvent { * @interface CallEvent */ export interface CallEvent { + /** + * + * @type {string} + * @memberof CallEvent + */ + category?: string; + /** + * + * @type {string} + * @memberof CallEvent + */ + component?: string; /** * * @type {string} @@ -574,6 +673,24 @@ export interface CallEvent { * @memberof CallEvent */ end_timestamp: number; + /** + * + * @type {boolean} + * @memberof CallEvent + */ + internal: boolean; + /** + * + * @type {Array} + * @memberof CallEvent + */ + issue_tags?: Array; + /** + * + * @type {string} + * @memberof CallEvent + */ + kind: string; /** * * @type {number} @@ -675,7 +792,7 @@ export interface CallHLSBroadcastingStoppedEvent { type: string; } /** - * + * CallIngressResponse is the payload for ingress settings * @export * @interface CallIngressResponse */ @@ -1245,7 +1362,7 @@ export interface CallRejectedEvent { user: UserResponse; } /** - * + * CallRequest is the payload for creating a call. * @export * @interface CallRequest */ @@ -1305,6 +1422,12 @@ export interface CallResponse { * @memberof CallResponse */ blocked_user_ids: Array; + /** + * + * @type {boolean} + * @memberof CallResponse + */ + captioning: boolean; /** * The unique identifier for a call (:) * @type {string} @@ -1512,7 +1635,6 @@ export interface CallSessionEndedEvent { */ type: string; } - /** * This event is sent when the participant counts in a call session are updated * @export @@ -2333,10 +2455,10 @@ export interface ChannelConfigWithInfo { partition_size?: number; /** * - * @type {number} + * @type {string} * @memberof ChannelConfigWithInfo */ - partition_ttl?: number; + partition_ttl?: string | null; /** * * @type {boolean} @@ -2385,6 +2507,12 @@ export interface ChannelConfigWithInfo { * @memberof ChannelConfigWithInfo */ search: boolean; + /** + * + * @type {boolean} + * @memberof ChannelConfigWithInfo + */ + skip_last_msg_update_for_system_msgs: boolean; /** * * @type {boolean} @@ -2451,29 +2579,41 @@ export type ChannelConfigWithInfoBlocklistBehaviorEnum = */ export interface ChannelMember { /** - * Expiration date of the ban + * + * @type {string} + * @memberof ChannelMember + */ + archived_at?: string; + /** + * * @type {string} * @memberof ChannelMember */ ban_expires?: string; /** - * Whether member is banned this channel or not + * * @type {boolean} * @memberof ChannelMember */ banned: boolean; /** - * Role of the member in the channel + * * @type {string} * @memberof ChannelMember */ channel_role: string; /** - * Date/time of creation + * * @type {string} * @memberof ChannelMember */ created_at: string; + /** + * + * @type {{ [key: string]: any; }} + * @memberof ChannelMember + */ + custom: { [key: string]: any }; /** * * @type {string} @@ -2481,25 +2621,25 @@ export interface ChannelMember { */ deleted_at?: string; /** - * Date when invite was accepted + * * @type {string} * @memberof ChannelMember */ invite_accepted_at?: string; /** - * Date when invite was rejected + * * @type {string} * @memberof ChannelMember */ invite_rejected_at?: string; /** - * Whether member was invited or not + * * @type {boolean} * @memberof ChannelMember */ invited?: boolean; /** - * Whether member is channel moderator or not + * * @type {boolean} * @memberof ChannelMember */ @@ -2511,7 +2651,13 @@ export interface ChannelMember { */ notifications_muted: boolean; /** - * Whether member is shadow banned in this channel or not + * + * @type {string} + * @memberof ChannelMember + */ + pinned_at?: string; + /** + * * @type {boolean} * @memberof ChannelMember */ @@ -2523,7 +2669,7 @@ export interface ChannelMember { */ status?: string; /** - * Date/time of the last update + * * @type {string} * @memberof ChannelMember */ @@ -2573,11 +2719,57 @@ export interface ChannelMute { updated_at: string; /** * - * @type {UserObject} + * @type {UserResponse} * @memberof ChannelMute */ - user?: UserObject; + user?: UserResponse; } + +/** + * All possibility of string to use + * @export + */ +export const ChannelOwnCapability = { + BAN_CHANNEL_MEMBERS: 'ban-channel-members', + CAST_POLL_VOTE: 'cast-poll-vote', + CONNECT_EVENTS: 'connect-events', + CREATE_ATTACHMENT: 'create-attachment', + CREATE_CALL: 'create-call', + DELETE_ANY_MESSAGE: 'delete-any-message', + DELETE_CHANNEL: 'delete-channel', + DELETE_OWN_MESSAGE: 'delete-own-message', + FLAG_MESSAGE: 'flag-message', + FREEZE_CHANNEL: 'freeze-channel', + JOIN_CALL: 'join-call', + JOIN_CHANNEL: 'join-channel', + LEAVE_CHANNEL: 'leave-channel', + MUTE_CHANNEL: 'mute-channel', + PIN_MESSAGE: 'pin-message', + QUERY_POLL_VOTES: 'query-poll-votes', + QUOTE_MESSAGE: 'quote-message', + READ_EVENTS: 'read-events', + SEARCH_MESSAGES: 'search-messages', + SEND_CUSTOM_EVENTS: 'send-custom-events', + SEND_LINKS: 'send-links', + SEND_MESSAGE: 'send-message', + SEND_POLL: 'send-poll', + SEND_REACTION: 'send-reaction', + SEND_REPLY: 'send-reply', + SEND_TYPING_EVENTS: 'send-typing-events', + SET_CHANNEL_COOLDOWN: 'set-channel-cooldown', + SKIP_SLOW_MODE: 'skip-slow-mode', + SLOW_MODE: 'slow-mode', + TYPING_EVENTS: 'typing-events', + UPDATE_ANY_MESSAGE: 'update-any-message', + UPDATE_CHANNEL: 'update-channel', + UPDATE_CHANNEL_MEMBERS: 'update-channel-members', + UPDATE_OWN_MESSAGE: 'update-own-message', + UPDATE_THREAD: 'update-thread', + UPLOAD_FILE: 'upload-file', +} as const; +export type ChannelOwnCapability = + (typeof ChannelOwnCapability)[keyof typeof ChannelOwnCapability]; + /** * Represents channel in chat * @export @@ -2596,6 +2788,12 @@ export interface ChannelResponse { * @memberof ChannelResponse */ auto_translation_language?: string; + /** + * Whether this channel is blocked by current user or not + * @type {boolean} + * @memberof ChannelResponse + */ + blocked?: boolean; /** * Channel CID (:) * @type {string} @@ -2622,12 +2820,12 @@ export interface ChannelResponse { created_at: string; /** * - * @type {UserObject} + * @type {UserResponse} * @memberof ChannelResponse */ - created_by?: UserObject; + created_by?: UserResponse; /** - * + * Custom data for this object * @type {{ [key: string]: any; }} * @memberof ChannelResponse */ @@ -2700,10 +2898,10 @@ export interface ChannelResponse { muted?: boolean; /** * List of channel capabilities of authenticated user - * @type {Array} + * @type {Array} * @memberof ChannelResponse */ - own_capabilities?: Array; + own_capabilities?: Array; /** * Team the channel belongs to (multi-tenant only) * @type {string} @@ -2718,10 +2916,10 @@ export interface ChannelResponse { truncated_at?: string; /** * - * @type {UserObject} + * @type {UserResponse} * @memberof ChannelResponse */ - truncated_by?: UserObject; + truncated_by?: UserResponse; /** * Type of the channel * @type {string} @@ -2810,7 +3008,7 @@ export interface CollectUserFeedbackRequest { user_session_id: string; } /** - * + * Basic response information * @export * @interface CollectUserFeedbackResponse */ @@ -2909,10 +3107,10 @@ export interface ConnectUserDetailsRequest { name?: string; /** * - * @type {PrivacySettings} + * @type {PrivacySettingsResponse} * @memberof ConnectUserDetailsRequest */ - privacy_settings?: PrivacySettings; + privacy_settings?: PrivacySettingsResponse; /** * * @type {PushNotificationSettingsInput} @@ -3002,7 +3200,7 @@ export interface Coordinates { longitude: number; } /** - * + * Create device request * @export * @interface CreateDeviceRequest */ @@ -3146,83 +3344,121 @@ export interface CustomVideoEvent { user: UserResponse; } /** - * + * DeleteCallRequest is the payload for deleting a call. + * @export + * @interface DeleteCallRequest + */ +export interface DeleteCallRequest { + /** + * if true the call will be hard deleted along with all related data + * @type {boolean} + * @memberof DeleteCallRequest + */ + hard?: boolean; +} +/** + * DeleteCallResponse is the payload for deleting a call. + * @export + * @interface DeleteCallResponse + */ +export interface DeleteCallResponse { + /** + * + * @type {CallResponse} + * @memberof DeleteCallResponse + */ + call: CallResponse; + /** + * + * @type {string} + * @memberof DeleteCallResponse + */ + duration: string; + /** + * + * @type {string} + * @memberof DeleteCallResponse + */ + task_id?: string; +} +/** + * Response for DeleteRecording * @export * @interface DeleteRecordingResponse */ export interface DeleteRecordingResponse { /** - * + * Duration of the request in milliseconds * @type {string} * @memberof DeleteRecordingResponse */ duration: string; } /** - * + * DeleteTranscriptionResponse is the payload for deleting a transcription. * @export * @interface DeleteTranscriptionResponse */ export interface DeleteTranscriptionResponse { /** - * + * Duration of the request in milliseconds * @type {string} * @memberof DeleteTranscriptionResponse */ duration: string; } /** - * + * Response for Device * @export - * @interface Device + * @interface DeviceResponse */ -export interface Device { +export interface DeviceResponse { /** * Date/time of creation * @type {string} - * @memberof Device + * @memberof DeviceResponse */ created_at: string; /** * Whether device is disabled or not * @type {boolean} - * @memberof Device + * @memberof DeviceResponse */ disabled?: boolean; /** * Reason explaining why device had been disabled * @type {string} - * @memberof Device + * @memberof DeviceResponse */ disabled_reason?: string; /** * Device ID * @type {string} - * @memberof Device + * @memberof DeviceResponse */ id: string; /** * Push provider * @type {string} - * @memberof Device + * @memberof DeviceResponse */ push_provider: string; /** * Push provider name * @type {string} - * @memberof Device + * @memberof DeviceResponse */ push_provider_name?: string; /** * User ID * @type {string} - * @memberof Device + * @memberof DeviceResponse */ user_id: string; /** * When true the token is for Apple VoIP push notifications * @type {boolean} - * @memberof Device + * @memberof DeviceResponse */ voip?: boolean; } @@ -3323,13 +3559,19 @@ export interface EgressRTMPResponse { * @type {string} * @memberof EgressRTMPResponse */ - stream_key: string; + started_at: string; /** * * @type {string} * @memberof EgressRTMPResponse */ - url: string; + stream_key?: string; + /** + * + * @type {string} + * @memberof EgressRTMPResponse + */ + stream_url?: string; } /** * @@ -3357,13 +3599,13 @@ export interface EgressResponse { rtmps: Array; } /** - * + * Response for ending a call * @export * @interface EndCallResponse */ export interface EndCallResponse { /** - * + * Duration of the request in milliseconds * @type {string} * @memberof EndCallResponse */ @@ -3500,7 +3742,7 @@ export interface GetCallResponse { own_capabilities: Array; } /** - * + * Basic response information * @export * @interface GetCallStatsResponse */ @@ -3537,16 +3779,16 @@ export interface GetCallStatsResponse { duration: string; /** * - * @type {Stats} + * @type {TimeStats} * @memberof GetCallStatsResponse */ - jitter?: Stats; + jitter?: TimeStats; /** * - * @type {Stats} + * @type {TimeStats} * @memberof GetCallStatsResponse */ - latency?: Stats; + latency?: TimeStats; /** * * @type {number} @@ -3597,7 +3839,7 @@ export interface GetCallStatsResponse { sfus: Array; } /** - * + * Basic response information * @export * @interface GetEdgesResponse */ @@ -3707,6 +3949,12 @@ export interface GoLiveRequest { * @memberof GoLiveRequest */ recording_storage_name?: string; + /** + * + * @type {boolean} + * @memberof GoLiveRequest + */ + start_closed_caption?: boolean; /** * * @type {boolean} @@ -3719,6 +3967,12 @@ export interface GoLiveRequest { * @memberof GoLiveRequest */ start_recording?: boolean; + /** + * + * @type {boolean} + * @memberof GoLiveRequest + */ + start_rtmp_broadcasts?: boolean; /** * * @type {boolean} @@ -3733,7 +3987,7 @@ export interface GoLiveRequest { transcription_storage_name?: string; } /** - * + * Basic response information * @export * @interface GoLiveResponse */ @@ -3777,7 +4031,7 @@ export interface HLSSettingsRequest { quality_tracks: Array; } /** - * + * HLSSettings is the payload for HLS settings * @export * @interface HLSSettingsResponse */ @@ -3812,7 +4066,7 @@ export interface HealthCheckEvent { * @type {string} * @memberof HealthCheckEvent */ - cid: string; + cid?: string; /** * * @type {string} @@ -3827,10 +4081,16 @@ export interface HealthCheckEvent { created_at: string; /** * - * @type {OwnUser} + * @type {OwnUserResponse} + * @memberof HealthCheckEvent + */ + me?: OwnUserResponse; + /** + * + * @type {string} * @memberof HealthCheckEvent */ - me?: OwnUser; + received_at?: string; /** * * @type {string} @@ -3980,13 +4240,13 @@ export interface JoinCallResponse { */ export interface LabelThresholds { /** - * Threshold for automatic message block + * * @type {number} * @memberof LabelThresholds */ block?: number; /** - * Threshold for automatic message flag + * * @type {number} * @memberof LabelThresholds */ @@ -4031,17 +4291,17 @@ export interface LimitsSettingsResponse { max_participants?: number; } /** - * + * List devices response * @export * @interface ListDevicesResponse */ export interface ListDevicesResponse { /** * List of devices - * @type {Array} + * @type {Array} * @memberof ListDevicesResponse */ - devices: Array; + devices: Array; /** * * @type {string} @@ -4050,7 +4310,7 @@ export interface ListDevicesResponse { duration: string; } /** - * + * Response for listing recordings * @export * @interface ListRecordingsResponse */ @@ -4081,7 +4341,7 @@ export interface ListTranscriptionsResponse { */ duration: string; /** - * + * List of transcriptions for the call * @type {Array} * @memberof ListTranscriptionsResponse */ @@ -4115,58 +4375,27 @@ export interface Location { /** * * @export - * @interface MOSStats + * @interface MediaPubSubHint */ -export interface MOSStats { +export interface MediaPubSubHint { /** * - * @type {number} - * @memberof MOSStats + * @type {boolean} + * @memberof MediaPubSubHint */ - average_score: number; + audio_published: boolean; /** * - * @type {Array} - * @memberof MOSStats + * @type {boolean} + * @memberof MediaPubSubHint */ - histogram_duration_seconds: Array; + audio_subscribed: boolean; /** * - * @type {number} - * @memberof MOSStats + * @type {boolean} + * @memberof MediaPubSubHint */ - max_score: number; - /** - * - * @type {number} - * @memberof MOSStats - */ - min_score: number; -} -/** - * - * @export - * @interface MediaPubSubHint - */ -export interface MediaPubSubHint { - /** - * - * @type {boolean} - * @memberof MediaPubSubHint - */ - audio_published: boolean; - /** - * - * @type {boolean} - * @memberof MediaPubSubHint - */ - audio_subscribed: boolean; - /** - * - * @type {boolean} - * @memberof MediaPubSubHint - */ - video_published: boolean; + video_published: boolean; /** * * @type {boolean} @@ -4175,7 +4404,7 @@ export interface MediaPubSubHint { video_subscribed: boolean; } /** - * + * MemberRequest is the payload for adding a member to a call. * @export * @interface MemberRequest */ @@ -4200,7 +4429,7 @@ export interface MemberRequest { user_id: string; } /** - * + * MemberResponse is the payload for a member of a call. * @export * @interface MemberResponse */ @@ -4292,7 +4521,7 @@ export interface MuteUsersRequest { video?: boolean; } /** - * + * MuteUsersResponse is the response payload for the mute users endpoint. * @export * @interface MuteUsersResponse */ @@ -4421,9 +4650,11 @@ export const OwnCapability = { SEND_AUDIO: 'send-audio', SEND_VIDEO: 'send-video', START_BROADCAST_CALL: 'start-broadcast-call', + START_CLOSED_CAPTIONS_CALL: 'start-closed-captions-call', START_RECORD_CALL: 'start-record-call', START_TRANSCRIPTION_CALL: 'start-transcription-call', STOP_BROADCAST_CALL: 'stop-broadcast-call', + STOP_CLOSED_CAPTIONS_CALL: 'stop-closed-captions-call', STOP_RECORD_CALL: 'stop-record-call', STOP_TRANSCRIPTION_CALL: 'stop-transcription-call', UPDATE_CALL: 'update-call', @@ -4433,157 +4664,6 @@ export const OwnCapability = { } as const; export type OwnCapability = (typeof OwnCapability)[keyof typeof OwnCapability]; -/** - * - * @export - * @interface OwnUser - */ -export interface OwnUser { - /** - * - * @type {boolean} - * @memberof OwnUser - */ - banned: boolean; - /** - * - * @type {Array} - * @memberof OwnUser - */ - blocked_user_ids?: Array; - /** - * - * @type {Array} - * @memberof OwnUser - */ - channel_mutes: Array; - /** - * - * @type {string} - * @memberof OwnUser - */ - created_at: string; - /** - * - * @type {{ [key: string]: any; }} - * @memberof OwnUser - */ - custom: { [key: string]: any }; - /** - * - * @type {string} - * @memberof OwnUser - */ - deactivated_at?: string; - /** - * - * @type {string} - * @memberof OwnUser - */ - deleted_at?: string; - /** - * - * @type {Array} - * @memberof OwnUser - */ - devices: Array; - /** - * - * @type {string} - * @memberof OwnUser - */ - id: string; - /** - * - * @type {boolean} - * @memberof OwnUser - */ - invisible?: boolean; - /** - * - * @type {string} - * @memberof OwnUser - */ - language: string; - /** - * - * @type {string} - * @memberof OwnUser - */ - last_active?: string; - /** - * - * @type {Array} - * @memberof OwnUser - */ - latest_hidden_channels?: Array; - /** - * - * @type {Array} - * @memberof OwnUser - */ - mutes: Array; - /** - * - * @type {boolean} - * @memberof OwnUser - */ - online: boolean; - /** - * - * @type {PrivacySettings} - * @memberof OwnUser - */ - privacy_settings?: PrivacySettings; - /** - * - * @type {PushNotificationSettings} - * @memberof OwnUser - */ - push_notifications?: PushNotificationSettings; - /** - * - * @type {string} - * @memberof OwnUser - */ - role: string; - /** - * - * @type {Array} - * @memberof OwnUser - */ - teams?: Array; - /** - * - * @type {number} - * @memberof OwnUser - */ - total_unread_count: number; - /** - * - * @type {number} - * @memberof OwnUser - */ - unread_channels: number; - /** - * - * @type {number} - * @memberof OwnUser - */ - unread_count: number; - /** - * - * @type {number} - * @memberof OwnUser - */ - unread_threads: number; - /** - * - * @type {string} - * @memberof OwnUser - */ - updated_at: string; -} /** * * @export @@ -4634,10 +4714,10 @@ export interface OwnUserResponse { deleted_at?: string; /** * - * @type {Array} + * @type {Array} * @memberof OwnUserResponse */ - devices: Array; + devices: Array; /** * * @type {string} @@ -4734,6 +4814,12 @@ export interface OwnUserResponse { * @memberof OwnUserResponse */ unread_channels: number; + /** + * + * @type {number} + * @memberof OwnUserResponse + */ + unread_count: number; /** * * @type {number} @@ -4824,7 +4910,7 @@ export interface PinRequest { user_id: string; } /** - * + * Basic response information * @export * @interface PinResponse */ @@ -4855,7 +4941,6 @@ export interface PrivacySettings { */ typing_indicators?: TypingIndicators; } - /** * * @export @@ -4864,16 +4949,16 @@ export interface PrivacySettings { export interface PrivacySettingsResponse { /** * - * @type {ReadReceipts} + * @type {ReadReceiptsResponse} * @memberof PrivacySettingsResponse */ - read_receipts?: ReadReceipts; + read_receipts?: ReadReceiptsResponse; /** * - * @type {TypingIndicators} + * @type {TypingIndicatorsResponse} * @memberof PrivacySettingsResponse */ - typing_indicators?: TypingIndicators; + typing_indicators?: TypingIndicatorsResponse; } /** * @@ -5113,7 +5198,7 @@ export interface QueryCallMembersRequest { type: string; } /** - * + * Basic response information * @export * @interface QueryCallMembersResponse */ @@ -5181,7 +5266,7 @@ export interface QueryCallStatsRequest { sort?: Array; } /** - * + * Basic response information * @export * @interface QueryCallStatsResponse */ @@ -5242,7 +5327,7 @@ export interface QueryCallsRequest { */ prev?: string; /** - * + * Array of sort parameters * @type {Array} * @memberof QueryCallsRequest */ @@ -5340,6 +5425,19 @@ export interface ReadReceipts { * @type {boolean} * @memberof ReadReceipts */ + enabled: boolean; +} +/** + * + * @export + * @interface ReadReceiptsResponse + */ +export interface ReadReceiptsResponse { + /** + * + * @type {boolean} + * @memberof ReadReceiptsResponse + */ enabled?: boolean; } /** @@ -5398,7 +5496,7 @@ export type RecordSettingsRequestQualityEnum = (typeof RecordSettingsRequestQualityEnum)[keyof typeof RecordSettingsRequestQualityEnum]; /** - * + * RecordSettings is the payload for recording settings * @export * @interface RecordSettingsResponse */ @@ -5442,7 +5540,7 @@ export interface RejectCallRequest { */ export interface RejectCallResponse { /** - * + * Duration of the request in milliseconds * @type {string} * @memberof RejectCallResponse */ @@ -5511,14 +5609,14 @@ export interface RequestPermissionRequest { */ export interface RequestPermissionResponse { /** - * + * Duration of the request in milliseconds * @type {string} * @memberof RequestPermissionResponse */ duration: string; } /** - * + * Basic response information * @export * @interface Response */ @@ -5732,7 +5830,7 @@ export interface SendCallEventRequest { */ export interface SendCallEventResponse { /** - * + * Duration of the request in milliseconds * @type {string} * @memberof SendCallEventResponse */ @@ -5764,7 +5862,7 @@ export interface SendReactionRequest { type: string; } /** - * + * Basic response information * @export * @interface SendReactionResponse */ @@ -5804,17 +5902,55 @@ export interface SortParamRequest { /** * * @export + * @interface StartClosedCaptionsRequest + */ +export interface StartClosedCaptionsRequest { + /** + * Enable transcriptions along with closed captions + * @type {boolean} + * @memberof StartClosedCaptionsRequest + */ + enable_transcription?: boolean; + /** + * Which external storage to use for transcriptions (only applicable if enable_transcription is true) + * @type {string} + * @memberof StartClosedCaptionsRequest + */ + external_storage?: string; + /** + * The spoken language in the call, if not provided the language defined in the transcription settings will be used + * @type {string} + * @memberof StartClosedCaptionsRequest + */ + language?: string; +} +/** + * + * @export + * @interface StartClosedCaptionsResponse + */ +export interface StartClosedCaptionsResponse { + /** + * + * @type {string} + * @memberof StartClosedCaptionsResponse + */ + duration: string; +} +/** + * StartHLSBroadcastingResponse is the payload for starting an HLS broadcasting. + * @export * @interface StartHLSBroadcastingResponse */ export interface StartHLSBroadcastingResponse { /** - * Duration of the request in milliseconds + * * @type {string} * @memberof StartHLSBroadcastingResponse */ duration: string; /** - * + * the URL of the HLS playlist * @type {string} * @memberof StartHLSBroadcastingResponse */ @@ -5834,13 +5970,13 @@ export interface StartRecordingRequest { recording_external_storage?: string; } /** - * + * StartRecordingResponse is the response payload for the start recording endpoint. * @export * @interface StartRecordingResponse */ export interface StartRecordingResponse { /** - * + * Duration of the request in milliseconds * @type {string} * @memberof StartRecordingResponse */ @@ -5853,7 +5989,19 @@ export interface StartRecordingResponse { */ export interface StartTranscriptionRequest { /** - * + * Enable closed captions along with transcriptions + * @type {boolean} + * @memberof StartTranscriptionRequest + */ + enable_closed_captions?: boolean; + /** + * The spoken language in the call, if not provided the language defined in the transcription settings will be used + * @type {string} + * @memberof StartTranscriptionRequest + */ + language?: string; + /** + * Store transcriptions in this external storage * @type {string} * @memberof StartTranscriptionRequest */ @@ -5866,7 +6014,7 @@ export interface StartTranscriptionRequest { */ export interface StartTranscriptionResponse { /** - * + * Duration of the request in milliseconds * @type {string} * @memberof StartTranscriptionResponse */ @@ -5875,50 +6023,57 @@ export interface StartTranscriptionResponse { /** * * @export - * @interface Stats + * @interface StatsOptions */ -export interface Stats { - /** - * - * @type {number} - * @memberof Stats - */ - average_seconds: number; +export interface StatsOptions { /** * * @type {number} - * @memberof Stats + * @memberof StatsOptions */ - max_seconds: number; + reporting_interval_ms: number; } /** * * @export - * @interface StatsOptions + * @interface StopClosedCaptionsRequest */ -export interface StatsOptions { +export interface StopClosedCaptionsRequest { /** * - * @type {number} - * @memberof StatsOptions + * @type {boolean} + * @memberof StopClosedCaptionsRequest */ - reporting_interval_ms: number; + stop_transcription?: boolean; } /** - * + * Basic response information * @export - * @interface StopHLSBroadcastingResponse + * @interface StopClosedCaptionsResponse */ -export interface StopHLSBroadcastingResponse { +export interface StopClosedCaptionsResponse { /** * Duration of the request in milliseconds * @type {string} - * @memberof StopHLSBroadcastingResponse + * @memberof StopClosedCaptionsResponse */ duration: string; } /** - * + * Basic response information + * @export + * @interface StopHLSBroadcastingResponse + */ +export interface StopHLSBroadcastingResponse { + /** + * Duration of the request in milliseconds + * @type {string} + * @memberof StopHLSBroadcastingResponse + */ + duration: string; +} +/** + * * @export * @interface StopLiveResponse */ @@ -5930,14 +6085,14 @@ export interface StopLiveResponse { */ call: CallResponse; /** - * Duration of the request in milliseconds + * * @type {string} * @memberof StopLiveResponse */ duration: string; } /** - * + * Basic response information * @export * @interface StopRecordingResponse */ @@ -5952,6 +6107,19 @@ export interface StopRecordingResponse { /** * * @export + * @interface StopTranscriptionRequest + */ +export interface StopTranscriptionRequest { + /** + * + * @type {boolean} + * @memberof StopTranscriptionRequest + */ + stop_closed_captions?: boolean; +} +/** + * Basic response information + * @export * @interface StopTranscriptionResponse */ export interface StopTranscriptionResponse { @@ -6019,7 +6187,7 @@ export interface TargetResolution { width: number; } /** - * Sets thresholds for AI moderation + * * @export * @interface Thresholds */ @@ -6082,6 +6250,25 @@ export interface ThumbnailsSettingsResponse { */ enabled: boolean; } +/** + * + * @export + * @interface TimeStats + */ +export interface TimeStats { + /** + * + * @type {number} + * @memberof TimeStats + */ + average_seconds: number; + /** + * + * @type {number} + * @memberof TimeStats + */ + max_seconds: number; +} /** * * @export @@ -6093,13 +6280,13 @@ export interface TranscriptionSettingsRequest { * @type {string} * @memberof TranscriptionSettingsRequest */ - closed_caption_mode?: string; + closed_caption_mode?: TranscriptionSettingsRequestClosedCaptionModeEnum; /** * - * @type {Array} + * @type {string} * @memberof TranscriptionSettingsRequest */ - languages?: Array; + language?: string; /** * * @type {string} @@ -6108,6 +6295,17 @@ export interface TranscriptionSettingsRequest { mode: TranscriptionSettingsRequestModeEnum; } +/** + * @export + */ +export const TranscriptionSettingsRequestClosedCaptionModeEnum = { + AVAILABLE: 'available', + DISABLED: 'disabled', + AUTO_ON: 'auto-on', +} as const; +export type TranscriptionSettingsRequestClosedCaptionModeEnum = + (typeof TranscriptionSettingsRequestClosedCaptionModeEnum)[keyof typeof TranscriptionSettingsRequestClosedCaptionModeEnum]; + /** * @export */ @@ -6130,13 +6328,13 @@ export interface TranscriptionSettingsResponse { * @type {string} * @memberof TranscriptionSettingsResponse */ - closed_caption_mode: string; + closed_caption_mode: TranscriptionSettingsResponseClosedCaptionModeEnum; /** * - * @type {Array} + * @type {string} * @memberof TranscriptionSettingsResponse */ - languages: Array; + language: string; /** * * @type {string} @@ -6145,6 +6343,17 @@ export interface TranscriptionSettingsResponse { mode: TranscriptionSettingsResponseModeEnum; } +/** + * @export + */ +export const TranscriptionSettingsResponseClosedCaptionModeEnum = { + AVAILABLE: 'available', + DISABLED: 'disabled', + AUTO_ON: 'auto-on', +} as const; +export type TranscriptionSettingsResponseClosedCaptionModeEnum = + (typeof TranscriptionSettingsResponseClosedCaptionModeEnum)[keyof typeof TranscriptionSettingsResponseClosedCaptionModeEnum]; + /** * @export */ @@ -6167,11 +6376,24 @@ export interface TypingIndicators { * @type {boolean} * @memberof TypingIndicators */ - enabled?: boolean; + enabled: boolean; } /** * * @export + * @interface TypingIndicatorsResponse + */ +export interface TypingIndicatorsResponse { + /** + * + * @type {boolean} + * @memberof TypingIndicatorsResponse + */ + enabled?: boolean; +} +/** + * UnblockUserRequest is the payload for unblocking a user. + * @export * @interface UnblockUserRequest */ export interface UnblockUserRequest { @@ -6183,7 +6405,7 @@ export interface UnblockUserRequest { user_id: string; } /** - * + * UnblockUserResponse is the payload for unblocking a user. * @export * @interface UnblockUserResponse */ @@ -6228,7 +6450,7 @@ export interface UnblockedUserEvent { user: UserResponse; } /** - * + * UnpinRequest is the payload for unpinning a message. * @export * @interface UnpinRequest */ @@ -6247,7 +6469,7 @@ export interface UnpinRequest { user_id: string; } /** - * + * UnpinResponse is the payload for unpinning a message. * @export * @interface UnpinResponse */ @@ -6260,7 +6482,7 @@ export interface UnpinResponse { duration: string; } /** - * + * Update call members * @export * @interface UpdateCallMembersRequest */ @@ -6279,7 +6501,7 @@ export interface UpdateCallMembersRequest { update_members?: Array; } /** - * + * Basic response information * @export * @interface UpdateCallMembersResponse */ @@ -6298,7 +6520,7 @@ export interface UpdateCallMembersResponse { members: Array; } /** - * + * Request for updating a call * @export * @interface UpdateCallRequest */ @@ -6323,7 +6545,7 @@ export interface UpdateCallRequest { starts_at?: string; } /** - * Represents a call + * Response for updating a call * @export * @interface UpdateCallResponse */ @@ -6385,7 +6607,7 @@ export interface UpdateUserPermissionsRequest { user_id: string; } /** - * + * Basic response information * @export * @interface UpdateUserPermissionsResponse */ @@ -6434,6 +6656,121 @@ export interface UpdatedCallPermissionsEvent { */ user: UserResponse; } +/** + * + * @export + * @interface User + */ +export interface UserObject { + /** + * + * @type {string} + * @memberof User + */ + ban_expires?: string; + /** + * + * @type {boolean} + * @memberof User + */ + banned: boolean; + /** + * + * @type {string} + * @memberof User + */ + readonly created_at?: string; + /** + * + * @type {{ [key: string]: any; }} + * @memberof User + */ + custom: { [key: string]: any }; + /** + * + * @type {string} + * @memberof User + */ + readonly deactivated_at?: string; + /** + * + * @type {string} + * @memberof User + */ + readonly deleted_at?: string; + /** + * + * @type {string} + * @memberof User + */ + id: string; + /** + * + * @type {boolean} + * @memberof User + */ + invisible?: boolean; + /** + * + * @type {string} + * @memberof User + */ + language?: string; + /** + * + * @type {string} + * @memberof User + */ + readonly last_active?: string; + /** + * + * @type {string} + * @memberof User + */ + readonly last_engaged_at?: string; + /** + * + * @type {boolean} + * @memberof User + */ + readonly online: boolean; + /** + * + * @type {PrivacySettings} + * @memberof User + */ + privacy_settings?: PrivacySettings; + /** + * + * @type {PushNotificationSettings} + * @memberof User + */ + push_notifications?: PushNotificationSettings; + /** + * + * @type {string} + * @memberof User + */ + revoke_tokens_issued_before?: string; + /** + * + * @type {string} + * @memberof User + */ + role: string; + /** + * + * @type {Array} + * @memberof User + */ + teams?: Array; + /** + * + * @type {string} + * @memberof User + */ + readonly updated_at?: string; +} /** * * @export @@ -6584,286 +6921,297 @@ export interface UserDeletedEvent { /** * * @export - * @interface UserFeedbackReportResponse + * @interface UserEventPayload */ -export interface UserFeedbackReportResponse { +export interface UserEventPayload { /** * - * @type {Array>} - * @memberof UserFeedbackReportResponse + * @type {boolean} + * @memberof UserEventPayload */ - daily: Array>; -} -/** - * - * @export - * @interface UserFeedbackReport - */ -export interface UserFeedbackReport { + banned: boolean; /** * - * @type {{ [key: string]: number; }} - * @memberof UserFeedbackReport + * @type {Array} + * @memberof UserEventPayload */ - count_by_rating: { [key: string]: number }; + blocked_user_ids: Array; /** * - * @type {number} - * @memberof UserFeedbackReport + * @type {string} + * @memberof UserEventPayload */ - unreported_count: number; -} -/** - * - * @export - * @interface UserInfoResponse - */ -export interface UserInfoResponse { + created_at: string; /** * * @type {{ [key: string]: any; }} - * @memberof UserInfoResponse + * @memberof UserEventPayload */ custom: { [key: string]: any }; /** * * @type {string} - * @memberof UserInfoResponse + * @memberof UserEventPayload */ - image: string; + deactivated_at?: string; /** * * @type {string} - * @memberof UserInfoResponse + * @memberof UserEventPayload */ - name: string; + deleted_at?: string; /** * - * @type {Array} - * @memberof UserInfoResponse + * @type {string} + * @memberof UserEventPayload */ - roles: Array; -} -/** - * - * @export - * @interface UserMute - */ -export interface UserMute { + id: string; /** - * Date/time of creation + * * @type {string} - * @memberof UserMute + * @memberof UserEventPayload */ - created_at: string; + image?: string; /** - * Date/time of mute expiration + * + * @type {boolean} + * @memberof UserEventPayload + */ + invisible?: boolean; + /** + * * @type {string} - * @memberof UserMute + * @memberof UserEventPayload */ - expires?: string; + language: string; /** * - * @type {UserObject} - * @memberof UserMute + * @type {string} + * @memberof UserEventPayload */ - target?: UserObject; + last_active?: string; /** - * Date/time of the last update + * * @type {string} - * @memberof UserMute + * @memberof UserEventPayload */ - updated_at: string; + name?: string; /** * - * @type {UserObject} - * @memberof UserMute + * @type {boolean} + * @memberof UserEventPayload */ - user?: UserObject; -} -/** - * - * @export - * @interface UserMuteResponse - */ -export interface UserMuteResponse { + online: boolean; /** * - * @type {string} - * @memberof UserMuteResponse + * @type {PrivacySettingsResponse} + * @memberof UserEventPayload */ - created_at: string; + privacy_settings?: PrivacySettingsResponse; /** * * @type {string} - * @memberof UserMuteResponse + * @memberof UserEventPayload */ - expires?: string; + revoke_tokens_issued_before?: string; /** * - * @type {UserResponse} - * @memberof UserMuteResponse + * @type {string} + * @memberof UserEventPayload */ - target?: UserResponse; + role: string; /** * - * @type {string} - * @memberof UserMuteResponse + * @type {Array} + * @memberof UserEventPayload */ - updated_at: string; + teams: Array; /** * - * @type {UserResponse} - * @memberof UserMuteResponse + * @type {string} + * @memberof UserEventPayload */ - user?: UserResponse; + updated_at: string; } - /** * * @export - * @interface UserMutedEvent + * @interface UserFlaggedEvent */ -export interface UserMutedEvent { +export interface UserFlaggedEvent { /** * * @type {string} - * @memberof UserMutedEvent + * @memberof UserFlaggedEvent */ created_at: string; /** * * @type {string} - * @memberof UserMutedEvent + * @memberof UserFlaggedEvent */ target_user?: string; /** * * @type {Array} - * @memberof UserMutedEvent + * @memberof UserFlaggedEvent */ target_users?: Array; /** * * @type {string} - * @memberof UserMutedEvent + * @memberof UserFlaggedEvent */ type: string; /** * * @type {UserObject} - * @memberof UserMutedEvent + * @memberof UserFlaggedEvent */ user?: UserObject; } /** - * Represents chat user + * * @export - * @interface UserObject + * @interface UserFeedbackReportResponse */ -export interface UserObject { +export interface UserFeedbackReportResponse { /** - * Expiration date of the ban - * @type {string} - * @memberof UserObject + * + * @type {Array>} + * @memberof UserFeedbackReportResponse */ - ban_expires?: string; + daily: Array>; +} +/** + * + * @export + * @interface UserFeedbackReport + */ +export interface UserFeedbackReport { /** - * Whether a user is banned or not - * @type {boolean} - * @memberof UserObject + * + * @type {{ [key: string]: number; }} + * @memberof UserFeedbackReport */ - banned: boolean; + count_by_rating: { [key: string]: number }; /** - * Date/time of creation - * @type {string} - * @memberof UserObject + * + * @type {number} + * @memberof UserFeedbackReport */ - readonly created_at?: string; + unreported_count: number; +} +/** + * + * @export + * @interface UserInfoResponse + */ +export interface UserInfoResponse { /** * * @type {{ [key: string]: any; }} - * @memberof UserObject + * @memberof UserInfoResponse */ custom: { [key: string]: any }; /** - * Date of deactivation + * * @type {string} - * @memberof UserObject + * @memberof UserInfoResponse */ - readonly deactivated_at?: string; + id: string; /** - * Date/time of deletion + * * @type {string} - * @memberof UserObject + * @memberof UserInfoResponse */ - readonly deleted_at?: string; + image: string; /** - * Unique user identifier + * * @type {string} - * @memberof UserObject + * @memberof UserInfoResponse */ - id: string; + name: string; /** * - * @type {boolean} - * @memberof UserObject + * @type {Array} + * @memberof UserInfoResponse */ - invisible?: boolean; + roles: Array; +} +/** + * + * @export + * @interface UserMuteResponse + */ +export interface UserMuteResponse { /** - * Preferred language of a user + * * @type {string} - * @memberof UserObject + * @memberof UserMuteResponse */ - language?: string; + created_at: string; /** - * Date of last activity + * * @type {string} - * @memberof UserObject + * @memberof UserMuteResponse */ - readonly last_active?: string; + expires?: string; /** - * Whether a user online or not - * @type {boolean} - * @memberof UserObject + * + * @type {UserResponse} + * @memberof UserMuteResponse */ - readonly online: boolean; + target?: UserResponse; /** * - * @type {PrivacySettings} - * @memberof UserObject + * @type {string} + * @memberof UserMuteResponse */ - privacy_settings?: PrivacySettings; + updated_at: string; /** * - * @type {PushNotificationSettings} - * @memberof UserObject + * @type {UserResponse} + * @memberof UserMuteResponse */ - push_notifications?: PushNotificationSettings; + user?: UserResponse; +} +/** + * + * @export + * @interface UserMutedEvent + */ +export interface UserMutedEvent { /** - * Revocation date for tokens + * * @type {string} - * @memberof UserObject + * @memberof UserMutedEvent */ - revoke_tokens_issued_before?: string; + created_at: string; /** - * Determines the set of user permissions + * * @type {string} - * @memberof UserObject + * @memberof UserMutedEvent */ - role: string; + target_user?: string; /** - * List of teams user is a part of + * * @type {Array} - * @memberof UserObject + * @memberof UserMutedEvent */ - teams?: Array; + target_users?: Array; /** - * Date/time of the last update + * * @type {string} - * @memberof UserObject + * @memberof UserMutedEvent */ - readonly updated_at?: string; + type: string; + /** + * + * @type {UserObject} + * @memberof UserMutedEvent + */ + user?: UserObject; } /** * @@ -6916,7 +7264,7 @@ export interface UserReactivatedEvent { user?: UserObject; } /** - * + * User request object * @export * @interface UserRequest */ @@ -6959,10 +7307,10 @@ export interface UserRequest { name?: string; /** * - * @type {PrivacySettings} + * @type {PrivacySettingsResponse} * @memberof UserRequest */ - privacy_settings?: PrivacySettings; + privacy_settings?: PrivacySettingsResponse; /** * * @type {PushNotificationSettingsInput} @@ -6971,7 +7319,7 @@ export interface UserRequest { push_notifications?: PushNotificationSettingsInput; } /** - * + * User response object * @export * @interface UserResponse */ @@ -7141,16 +7489,22 @@ export interface UserSessionStats { geolocation?: GeolocationResult; /** * - * @type {Stats} + * @type {string} * @memberof UserSessionStats */ - jitter?: Stats; + group: string; /** * - * @type {Stats} + * @type {TimeStats} * @memberof UserSessionStats */ - latency?: Stats; + jitter?: TimeStats; + /** + * + * @type {TimeStats} + * @memberof UserSessionStats + */ + latency?: TimeStats; /** * * @type {number} @@ -7199,6 +7553,12 @@ export interface UserSessionStats { * @memberof UserSessionStats */ max_receiving_video_quality?: VideoQuality; + /** + * + * @type {number} + * @memberof UserSessionStats + */ + min_event_ts: number; /** * * @type {string} @@ -7231,22 +7591,16 @@ export interface UserSessionStats { published_tracks?: Array; /** * - * @type {MOSStats} + * @type {TimeStats} * @memberof UserSessionStats */ - publisher_audio_mos?: MOSStats; + publisher_jitter?: TimeStats; /** * - * @type {Stats} + * @type {TimeStats} * @memberof UserSessionStats */ - publisher_jitter?: Stats; - /** - * - * @type {Stats} - * @memberof UserSessionStats - */ - publisher_latency?: Stats; + publisher_latency?: TimeStats; /** * * @type {number} @@ -7335,22 +7689,16 @@ export interface UserSessionStats { session_id: string; /** * - * @type {MOSStats} - * @memberof UserSessionStats - */ - subscriber_audio_mos?: MOSStats; - /** - * - * @type {Stats} + * @type {TimeStats} * @memberof UserSessionStats */ - subscriber_jitter?: Stats; + subscriber_jitter?: TimeStats; /** * - * @type {Stats} + * @type {TimeStats} * @memberof UserSessionStats */ - subscriber_latency?: Stats; + subscriber_latency?: TimeStats; /** * * @type {number} @@ -7483,72 +7831,109 @@ export interface UserUnbannedEvent { /** * * @export - * @interface UserUpdatedEvent + * @interface UserUnmutedEvent */ -export interface UserUpdatedEvent { +export interface UserUnmutedEvent { /** * * @type {string} - * @memberof UserUpdatedEvent + * @memberof UserUnmutedEvent */ created_at: string; /** * * @type {string} - * @memberof UserUpdatedEvent + * @memberof UserUnmutedEvent */ - received_at?: string; + target_user?: string; + /** + * + * @type {Array} + * @memberof UserUnmutedEvent + */ + target_users?: Array; /** * * @type {string} - * @memberof UserUpdatedEvent + * @memberof UserUnmutedEvent */ type: string; /** * * @type {UserObject} - * @memberof UserUpdatedEvent + * @memberof UserUnmutedEvent */ user?: UserObject; } /** * * @export - * @interface VideoQuality + * @interface UserUpdatedEvent */ -export interface VideoQuality { +export interface UserUpdatedEvent { /** * - * @type {VideoResolution} - * @memberof VideoQuality + * @type {string} + * @memberof UserUpdatedEvent */ - resolution?: VideoResolution; + created_at: string; /** * * @type {string} - * @memberof VideoQuality + * @memberof UserUpdatedEvent */ - usage_type?: string; + received_at?: string; + /** + * + * @type {string} + * @memberof UserUpdatedEvent + */ + type: string; + /** + * + * @type {UserEventPayload} + * @memberof UserUpdatedEvent + */ + user: UserEventPayload; } /** * * @export - * @interface VideoResolution + * @interface VideoDimension */ -export interface VideoResolution { +export interface VideoDimension { /** * * @type {number} - * @memberof VideoResolution + * @memberof VideoDimension */ height: number; /** * * @type {number} - * @memberof VideoResolution + * @memberof VideoDimension */ width: number; } +/** + * + * @export + * @interface VideoQuality + */ +export interface VideoQuality { + /** + * + * @type {VideoDimension} + * @memberof VideoQuality + */ + resolution?: VideoDimension; + /** + * + * @type {string} + * @memberof VideoQuality + */ + usage_type?: string; +} /** * * @export @@ -7648,7 +8033,7 @@ export type VideoSettingsResponseCameraFacingEnum = (typeof VideoSettingsResponseCameraFacingEnum)[keyof typeof VideoSettingsResponseCameraFacingEnum]; /** - * + * Websocket auth message * @export * @interface WSAuthMessage */ @@ -7681,6 +8066,9 @@ export type WSEvent = | ({ type: 'call.accepted' } & CallAcceptedEvent) | ({ type: 'call.blocked_user' } & BlockedUserEvent) | ({ type: 'call.closed_caption' } & ClosedCaptionEvent) + | ({ type: 'call.closed_captions_failed' } & CallClosedCaptionsFailedEvent) + | ({ type: 'call.closed_captions_started' } & CallClosedCaptionsStartedEvent) + | ({ type: 'call.closed_captions_stopped' } & CallClosedCaptionsStoppedEvent) | ({ type: 'call.created' } & CallCreatedEvent) | ({ type: 'call.deleted' } & CallDeletedEvent) | ({ type: 'call.ended' } & CallEndedEvent) diff --git a/packages/client/src/store/CallState.ts b/packages/client/src/store/CallState.ts index 5a379387f8..cefd4f84de 100644 --- a/packages/client/src/store/CallState.ts +++ b/packages/client/src/store/CallState.ts @@ -9,6 +9,7 @@ import type { Patch } from './rxUtils'; import * as RxUtils from './rxUtils'; import { CallingState } from './CallingState'; import { + type ClosedCaptionsSettings, type StreamVideoParticipant, type StreamVideoParticipantPatch, type StreamVideoParticipantPatches, @@ -19,6 +20,7 @@ import { import { CallStatsReport } from '../stats'; import { BlockedUserEvent, + CallClosedCaption, CallHLSBroadcastingStartedEvent, CallIngressResponse, CallMemberAddedEvent, @@ -32,6 +34,7 @@ import { CallSessionParticipantLeftEvent, CallSessionResponse, CallSettingsResponse, + ClosedCaptionEvent, EgressResponse, MemberResponse, OwnCapability, @@ -97,6 +100,7 @@ export class CallState { CallSettingsResponse | undefined >(undefined); private transcribingSubject = new BehaviorSubject(false); + private captioningSubject = new BehaviorSubject(false); private endedBySubject = new BehaviorSubject( undefined, ); @@ -117,6 +121,7 @@ export class CallState { private callStatsReportSubject = new BehaviorSubject< CallStatsReport | undefined >(undefined); + private closedCaptionsSubject = new BehaviorSubject([]); // These are tracks that were delivered to the Subscriber's onTrack event // that we couldn't associate with a participant yet. @@ -275,6 +280,11 @@ export class CallState { */ transcribing$: Observable; + /** + * Will provide the closed captioning state of this call. + */ + captioning$: Observable; + /** * Will provide the user who ended this call. */ @@ -285,15 +295,24 @@ export class CallState { */ thumbnails$: Observable; + /** + * The queue of closed captions. + */ + closedCaptions$: Observable; + readonly logger = getLogger(['CallState']); /** * A list of comparators that are used to sort the participants. - * - * @private */ private sortParticipantsBy = defaultSortPreset; + /** + * The closed captions configuration. + */ + private closedCaptionsSettings: ClosedCaptionsSettings | undefined; + private closedCaptionsTasks = new Map(); + private readonly eventHandlers: { [EventType in WSEvent['type']]: | ((event: Extract) => void) @@ -357,6 +376,7 @@ export class CallState { this.settings$ = this.settingsSubject.asObservable(); this.endedBy$ = this.endedBySubject.asObservable(); this.thumbnails$ = this.thumbnailsSubject.asObservable(); + this.closedCaptions$ = this.closedCaptionsSubject.asObservable(); /** * Performs shallow comparison of two arrays. @@ -390,10 +410,10 @@ export class CallState { this.participantCount$ = duc(this.participantCountSubject); this.recording$ = duc(this.recordingSubject); this.transcribing$ = duc(this.transcribingSubject); + this.captioning$ = duc(this.captioningSubject); this.eventHandlers = { // these events are not updating the call state: - 'call.closed_caption': undefined, 'call.deleted': undefined, 'call.permission_request': undefined, 'call.recording_ready': undefined, @@ -415,6 +435,16 @@ export class CallState { // events that update call state: 'call.accepted': (e) => this.updateFromCallResponse(e.call), 'call.blocked_user': this.blockUser, + 'call.closed_caption': this.updateFromClosedCaptions, + 'call.closed_captions_failed': () => { + this.setCurrentValue(this.captioningSubject, false); + }, + 'call.closed_captions_started': () => { + this.setCurrentValue(this.captioningSubject, true); + }, + 'call.closed_captions_stopped': () => { + this.setCurrentValue(this.captioningSubject, false); + }, 'call.created': (e) => this.updateFromCallResponse(e.call), 'call.ended': (e) => { this.updateFromCallResponse(e.call); @@ -464,6 +494,16 @@ export class CallState { }; } + /** + * Runs the cleanup tasks. + */ + dispose = () => { + for (const [ccKey, taskId] of this.closedCaptionsTasks.entries()) { + clearTimeout(taskId); + this.closedCaptionsTasks.delete(ccKey); + } + }; + /** * Sets the list of criteria that are used to sort the participants. * To disable sorting, you can pass `noopComparator()`. @@ -533,6 +573,23 @@ export class CallState { return this.setCurrentValue(this.startedAtSubject, startedAt); }; + /** + * Returns whether closed captions are enabled in the current call. + */ + get captioning() { + return this.getCurrentValue(this.captioning$); + } + + /** + * Sets the closed captioning state of the current call. + * + * @internal + * @param captioning the closed captioning state. + */ + setCaptioning = (captioning: boolean) => { + return RxUtils.updateValue(this.captioningSubject, captioning); + }; + /** * The server-side counted number of anonymous participants connected to the current call. * This number includes the anonymous participants as well. @@ -792,6 +849,13 @@ export class CallState { return this.getCurrentValue(this.thumbnails$); } + /** + * Returns the current queue of closed captions. + */ + get closedCaptions() { + return this.getCurrentValue(this.closedCaptions$); + } + /** * Will try to find the participant with the given sessionId in the current call. * @@ -840,7 +904,6 @@ export class CallState { const thePatch = typeof patch === 'function' ? patch(participant) : patch; const updatedParticipant: StreamVideoParticipant = { - // FIXME OL: this is not a deep merge, we might want to revisit this ...participant, ...thePatch, }; @@ -912,7 +975,6 @@ export class CallState { * * @param trackType the kind of subscription to update. * @param changes the list of subscription changes to do. - * @param type the debounce type to use for the update. */ updateParticipantTracks = ( trackType: VideoTrackType, @@ -1040,6 +1102,15 @@ export class CallState { return orphans; }; + /** + * Updates the closed captions settings. + * + * @param config the new closed captions settings. + */ + updateClosedCaptionSettings = (config: Partial) => { + this.closedCaptionsSettings = { ...this.closedCaptionsSettings, ...config }; + }; + /** * Updates the call state with the data received from the server. * @@ -1066,6 +1137,7 @@ export class CallState { this.updateParticipantCountFromSession(s); this.setCurrentValue(this.settingsSubject, call.settings); this.setCurrentValue(this.transcribingSubject, call.transcribing); + this.setCurrentValue(this.captioningSubject, call.captioning); this.setCurrentValue(this.thumbnailsSubject, call.thumbnails); }; @@ -1299,4 +1371,42 @@ export class CallState { this.setCurrentValue(this.ownCapabilitiesSubject, event.own_capabilities); } }; + + private updateFromClosedCaptions = (event: ClosedCaptionEvent) => { + this.setCurrentValue(this.closedCaptionsSubject, (queue) => { + const { closed_caption } = event; + + const keyOf = (c: CallClosedCaption) => `${c.speaker_id}/${c.start_time}`; + const currentKey = keyOf(closed_caption); + + const duplicate = queue.some((caption) => keyOf(caption) === currentKey); + if (duplicate) return queue; + + const nextQueue = [...queue, closed_caption]; + + const { visibilityDurationMs = 2700, maxVisibleCaptions = 2 } = + this.closedCaptionsSettings || {}; + // schedule the removal of the closed caption after the retention time + if (visibilityDurationMs > 0) { + const taskId = setTimeout(() => { + this.setCurrentValue(this.closedCaptionsSubject, (captions) => + captions.filter((caption) => caption !== closed_caption), + ); + this.closedCaptionsTasks.delete(currentKey); + }, visibilityDurationMs); + this.closedCaptionsTasks.set(currentKey, taskId); + + // cancel the cleanup tasks for the closed captions that are no longer in the queue + for (let i = 0; i < nextQueue.length - maxVisibleCaptions; i++) { + const key = keyOf(nextQueue[i]); + const task = this.closedCaptionsTasks.get(key); + clearTimeout(task); + this.closedCaptionsTasks.delete(key); + } + } + + // trim the queue + return nextQueue.slice(-maxVisibleCaptions); + }); + }; } diff --git a/packages/client/src/store/__tests__/CallState.test.ts b/packages/client/src/store/__tests__/CallState.test.ts index 6d52a5ec39..6403e48689 100644 --- a/packages/client/src/store/__tests__/CallState.test.ts +++ b/packages/client/src/store/__tests__/CallState.test.ts @@ -967,4 +967,105 @@ describe('CallState', () => { expect(state['orphanedTracks'].length).toBe(0); }); }); + + describe('closed captions', () => { + it('should add closed captions to the queue', () => { + const state = new CallState(); + state.updateFromEvent({ + type: 'call.closed_caption', + // @ts-expect-error incomplete data + closed_caption: { + speaker_id: '123', + text: 'Hello world', + start_time: '2021-01-01T00:00:00.000Z', + end_time: '2021-01-01T00:02:00.000Z', + }, + }); + expect(state.closedCaptions.length).toBe(1); + }); + + it('should maintain predefined queue size', () => { + const state = new CallState(); + state.updateClosedCaptionSettings({ maxVisibleCaptions: 2 }); + for (let i = 0; i < 5; i++) { + state.updateFromEvent({ + type: 'call.closed_caption', + // @ts-expect-error incomplete data + closed_caption: { + speaker_id: `123-${i}`, + text: `Hello world ${i}`, + start_time: '2021-01-01T00:00:00.000Z', + end_time: '2021-01-01T00:02:00.000Z', + }, + }); + } + expect(state.closedCaptions.length).toBe(2); + expect(state['closedCaptionsTasks'].size).toBe(2); + expect(state.closedCaptions.map((cc) => cc.text)).toEqual([ + 'Hello world 3', + 'Hello world 4', + ]); + }); + + it('should remove stale captions from the queue', () => { + const state = new CallState(); + vi.useFakeTimers(); + state.updateFromEvent({ + type: 'call.closed_caption', + // @ts-expect-error incomplete data + closed_caption: { + speaker_id: `123`, + text: `Hello world`, + start_time: '2021-01-01T00:00:00.000Z', + end_time: '2021-01-01T00:02:00.000Z', + }, + }); + expect(state.closedCaptions.length).toBe(1); + expect(state['closedCaptionsTasks'].size).toBe(1); + + vi.runAllTimers(); + expect(state.closedCaptions.length).toBe(0); + expect(state['closedCaptionsTasks'].size).toBe(0); + }); + + it('should remove stale captions from the queue after timer runs', () => { + const state = new CallState(); + state.updateClosedCaptionSettings({ visibilityDurationMs: 100 }); + vi.useFakeTimers(); + state.updateFromEvent({ + type: 'call.closed_caption', + // @ts-expect-error incomplete data + closed_caption: { + speaker_id: `123`, + text: `Hello world`, + start_time: '2021-01-01T00:00:00.000Z', + end_time: '2021-01-01T00:02:00.000Z', + }, + }); + expect(state.closedCaptions.length).toBe(1); + expect(state['closedCaptionsTasks'].size).toBe(1); + + vi.advanceTimersByTime(101); + expect(state.closedCaptions.length).toBe(0); + expect(state['closedCaptionsTasks'].size).toBe(0); + }); + + it('dispose cancels all cleanup tasks', () => { + const state = new CallState(); + state.updateFromEvent({ + type: 'call.closed_caption', + // @ts-expect-error incomplete data + closed_caption: { + speaker_id: `123`, + text: `Hello world`, + start_time: '2021-01-01T00:00:00.000Z', + }, + }); + expect(state.closedCaptions.length).toBe(1); + expect(state['closedCaptionsTasks'].size).toBe(1); + + state.dispose(); + expect(state['closedCaptionsTasks'].size).toBe(0); + }); + }); }); diff --git a/packages/client/src/store/rxUtils.ts b/packages/client/src/store/rxUtils.ts index 9f9d7f638e..3b647b1a85 100644 --- a/packages/client/src/store/rxUtils.ts +++ b/packages/client/src/store/rxUtils.ts @@ -1,4 +1,4 @@ -import { combineLatest, Observable, Subject } from 'rxjs'; +import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs'; import { withoutConcurrency } from '../helpers/concurrency'; import { getLogger } from '../logger'; @@ -59,6 +59,28 @@ export const setCurrentValue = (subject: Subject, update: Patch) => { return next; }; +/** + * Updates the value of the provided Subject and returns the previous value + * and a function to roll back the update. + * This is useful when you want to optimistically update a value + * and roll back the update if an error occurs. + * + * @param subject the subject to update. + * @param update the update to apply to the subject. + */ +export const updateValue = ( + subject: BehaviorSubject, + update: Patch, +) => { + const lastValue = subject.getValue(); + const value = setCurrentValue(subject, update); + return { + lastValue, + value, + rollback: () => setCurrentValue(subject, lastValue), + }; +}; + /** * Creates a subscription and returns a function to unsubscribe. * diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts index 7133678018..f84c4f75df 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -122,6 +122,21 @@ export type ParticipantPin = { pinnedAt: number; }; +export type ClosedCaptionsSettings = { + /** + * The time in milliseconds to keep a closed caption in the state (visible). + * Default is 2700 ms. + */ + visibilityDurationMs?: number; + /** + * The maximum number of closed captions to keep in the state (visible). + * When the maximum number is reached, the oldest closed caption is removed. + * + * Default is 2. + */ + maxVisibleCaptions?: number; +}; + /** * A partial representation of the StreamVideoParticipant. */ diff --git a/packages/react-bindings/CHANGELOG.md b/packages/react-bindings/CHANGELOG.md index 8dddb70da3..769c136daa 100644 --- a/packages/react-bindings/CHANGELOG.md +++ b/packages/react-bindings/CHANGELOG.md @@ -2,6 +2,16 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +## [1.3.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-bindings-1.2.16...@stream-io/video-react-bindings-1.3.0) (2025-01-02) + +### Dependency Updates + +* `@stream-io/video-client` updated to version `1.14.0` + +### Features + +* **closed captions:** Integration in the SDKs ([#1508](https://github.com/GetStream/stream-video-js/issues/1508)) ([bcb8589](https://github.com/GetStream/stream-video-js/commit/bcb85892c0dafcb03f9debf8d2fd361622224166)) + ## [1.2.16](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-bindings-1.2.15...@stream-io/video-react-bindings-1.2.16) (2024-12-20) ### Dependency Updates diff --git a/packages/react-bindings/package.json b/packages/react-bindings/package.json index 34be82ab35..89e8405df6 100644 --- a/packages/react-bindings/package.json +++ b/packages/react-bindings/package.json @@ -1,6 +1,6 @@ { "name": "@stream-io/video-react-bindings", - "version": "1.2.16", + "version": "1.3.0", "packageManager": "yarn@3.2.4", "main": "./dist/index.cjs.js", "module": "./dist/index.es.js", diff --git a/packages/react-bindings/src/hooks/callStateHooks.ts b/packages/react-bindings/src/hooks/callStateHooks.ts index e4e7a825ae..7e8473eb05 100644 --- a/packages/react-bindings/src/hooks/callStateHooks.ts +++ b/packages/react-bindings/src/hooks/callStateHooks.ts @@ -1,6 +1,7 @@ import { useMemo } from 'react'; import { Call, + CallClosedCaption, CallIngressResponse, CallSessionResponse, CallSettingsResponse, @@ -479,3 +480,19 @@ export const useIncomingVideoSettings = () => { ); return settings; }; + +/** + * Returns the current call's closed captions queue. + */ +export const useCallClosedCaptions = (): CallClosedCaption[] => { + const { closedCaptions$ } = useCallState(); + return useObservableValue(closedCaptions$); +}; + +/** + * Returns the current call's closed captions queue. + */ +export const useIsCallCaptioningInProgress = (): boolean => { + const { captioning$ } = useCallState(); + return useObservableValue(captioning$); +}; diff --git a/packages/react-bindings/src/hooks/callUtilHooks.ts b/packages/react-bindings/src/hooks/callUtilHooks.ts index e4e1a74233..90be48b6d8 100644 --- a/packages/react-bindings/src/hooks/callUtilHooks.ts +++ b/packages/react-bindings/src/hooks/callUtilHooks.ts @@ -36,6 +36,7 @@ export const useToggleCallRecording = () => { } } catch (e) { console.error(`Failed start recording`, e); + throw e; } }, [call, isCallRecordingInProgress]); diff --git a/packages/react-native-sdk/CHANGELOG.md b/packages/react-native-sdk/CHANGELOG.md index 762900c5dc..09accfa40a 100644 --- a/packages/react-native-sdk/CHANGELOG.md +++ b/packages/react-native-sdk/CHANGELOG.md @@ -2,6 +2,17 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +## [1.6.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-native-sdk-1.5.2...@stream-io/video-react-native-sdk-1.6.0) (2025-01-02) + +### Dependency Updates + +* `@stream-io/video-client` updated to version `1.14.0` +* `@stream-io/video-react-bindings` updated to version `1.3.0` + +### Features + +* **closed captions:** Integration in the SDKs ([#1508](https://github.com/GetStream/stream-video-js/issues/1508)) ([bcb8589](https://github.com/GetStream/stream-video-js/commit/bcb85892c0dafcb03f9debf8d2fd361622224166)) + ## [1.5.2](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-native-sdk-1.5.1...@stream-io/video-react-native-sdk-1.5.2) (2024-12-31) diff --git a/packages/react-native-sdk/README.md b/packages/react-native-sdk/README.md index 97f119c020..ea3b01b9a8 100644 --- a/packages/react-native-sdk/README.md +++ b/packages/react-native-sdk/README.md @@ -136,6 +136,7 @@ Stream's video roadmap and changelog are available [here](https://github.com/Get - [x] Speaking while muted - [x] Demo app on play-store and app-store - [x] Transcriptions +- [x] Closed Captions - [x] Speaker management - [x] PiP on iOS - [x] Video filters diff --git a/packages/react-native-sdk/package.json b/packages/react-native-sdk/package.json index 2bec210d99..8b0d0197f4 100644 --- a/packages/react-native-sdk/package.json +++ b/packages/react-native-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@stream-io/video-react-native-sdk", - "version": "1.5.2", + "version": "1.6.0", "packageManager": "yarn@3.2.4", "main": "dist/commonjs/index.js", "module": "dist/module/index.js", diff --git a/packages/react-sdk/CHANGELOG.md b/packages/react-sdk/CHANGELOG.md index 68f5afa318..a4fb70313b 100644 --- a/packages/react-sdk/CHANGELOG.md +++ b/packages/react-sdk/CHANGELOG.md @@ -2,6 +2,17 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +## [1.9.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-sdk-1.8.7...@stream-io/video-react-sdk-1.9.0) (2025-01-02) + +### Dependency Updates + +* `@stream-io/video-client` updated to version `1.14.0` +* `@stream-io/video-react-bindings` updated to version `1.3.0` + +### Features + +* **closed captions:** Integration in the SDKs ([#1508](https://github.com/GetStream/stream-video-js/issues/1508)) ([bcb8589](https://github.com/GetStream/stream-video-js/commit/bcb85892c0dafcb03f9debf8d2fd361622224166)) + ## [1.8.7](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-sdk-1.8.6...@stream-io/video-react-sdk-1.8.7) (2024-12-20) ### Dependency Updates diff --git a/packages/react-sdk/README.md b/packages/react-sdk/README.md index e055bec564..cfb5c90f61 100644 --- a/packages/react-sdk/README.md +++ b/packages/react-sdk/README.md @@ -86,7 +86,7 @@ Here are some of the features we support: - [ ] Break-out rooms - [ ] Waiting rooms -- [ ] Closed captions +- [x] Closed captions - [ ] Query call session endpoint - [ ] Logging 2.0 - [x] Hardware-accelerated video encoding on supported platforms diff --git a/packages/react-sdk/package.json b/packages/react-sdk/package.json index d4e2a3edc2..39edd094c0 100644 --- a/packages/react-sdk/package.json +++ b/packages/react-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@stream-io/video-react-sdk", - "version": "1.8.7", + "version": "1.9.0", "packageManager": "yarn@3.2.4", "main": "./dist/index.cjs.js", "module": "./dist/index.es.js", diff --git a/sample-apps/client/ts-quickstart/index.html b/sample-apps/client/ts-quickstart/index.html index d9eec53e42..baf13beb05 100644 --- a/sample-apps/client/ts-quickstart/index.html +++ b/sample-apps/client/ts-quickstart/index.html @@ -1,4 +1,4 @@ - + @@ -10,6 +10,7 @@
+
diff --git a/sample-apps/client/ts-quickstart/package.json b/sample-apps/client/ts-quickstart/package.json index 21430dcb77..e08a12d6e2 100644 --- a/sample-apps/client/ts-quickstart/package.json +++ b/sample-apps/client/ts-quickstart/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite --host 0.0.0.0 --https", + "dev": "https=1 vite --host 0.0.0.0", "build": "tsc && vite build", "preview": "vite preview" }, diff --git a/sample-apps/client/ts-quickstart/src/closed-captions.ts b/sample-apps/client/ts-quickstart/src/closed-captions.ts new file mode 100644 index 0000000000..5557515252 --- /dev/null +++ b/sample-apps/client/ts-quickstart/src/closed-captions.ts @@ -0,0 +1,64 @@ +import { Call, CallClosedCaption } from '@stream-io/video-client'; + +export class ClosedCaptionManager { + status: 'on' | 'off' = 'off'; + private unsubscribe?: () => void; + private captionContainer?: HTMLElement; + + constructor(private call: Call) {} + + renderToggleElement() { + const button = document.createElement('button'); + button.textContent = + this.status === 'on' + ? 'Turn off closed captions' + : 'Turn on closed captions'; + + button.addEventListener('click', async () => { + this.status === 'on' ? this.hideCaptions() : this.showCaptions(); + button.textContent = + this.status === 'on' + ? 'Turn off closed captions' + : 'Turn on closed captions'; + }); + + return button; + } + + renderCaptionContainer() { + this.captionContainer = document.createElement('div'); + + return this.captionContainer; + } + + async showCaptions() { + this.status = 'on'; + await this.call.startClosedCaptions(); + this.unsubscribe = this.call.state.closedCaptions$.subscribe((captions) => { + this.updateDisplayedCaptions(captions); + }).unsubscribe; + } + + async hideCaptions() { + this.status = 'off'; + await this.call.stopClosedCaptions(); + this.cleanup(); + } + + cleanup() { + this.unsubscribe?.(); + } + + private updateDisplayedCaptions(captions: CallClosedCaption[]) { + if (!this.captionContainer) { + console.warn( + 'Render caption container before turning on closed captions', + ); + return; + } + + this.captionContainer.innerHTML = captions + .map((caption) => `${caption.user.name}: ${caption.text}`) + .join('
'); + } +} diff --git a/sample-apps/client/ts-quickstart/src/main.ts b/sample-apps/client/ts-quickstart/src/main.ts index 8a23d74f56..81fa3f03bd 100644 --- a/sample-apps/client/ts-quickstart/src/main.ts +++ b/sample-apps/client/ts-quickstart/src/main.ts @@ -10,6 +10,7 @@ import { renderVolumeControl, } from './device-selector'; import { isMobile } from './mobile'; +import { ClosedCaptionManager } from './closed-captions'; const searchParams = new URLSearchParams(window.location.search); const extractPayloadFromToken = (token: string) => { @@ -50,32 +51,42 @@ call.screenShare.setSettings({ maxBitrate: 1500000, }); -call.join({ create: true }).then(async () => { - // render mic and camera controls - const controls = renderControls(call); - const container = document.getElementById('call-controls')!; - container.appendChild(controls.audioButton); - container.appendChild(controls.videoButton); - container.appendChild(controls.screenShareButton); - - container.appendChild(renderAudioDeviceSelector(call)); - - // render device selectors - if (isMobile.any()) { - container.appendChild(controls.flipButton); - } else { - container.appendChild(renderVideoDeviceSelector(call)); - } - - const audioOutputSelector = renderAudioOutputSelector(call); - if (audioOutputSelector) { - container.appendChild(audioOutputSelector); - } - - container.appendChild(renderVolumeControl(call)); -}); +const container = document.getElementById('call-controls')!; + +// render mic and camera controls +const controls = renderControls(call); +container.appendChild(controls.audioButton); +container.appendChild(controls.videoButton); +container.appendChild(controls.screenShareButton); + +container.appendChild(renderAudioDeviceSelector(call)); + +// render device selectors +if (isMobile.any()) { + container.appendChild(controls.flipButton); +} else { + container.appendChild(renderVideoDeviceSelector(call)); +} + +const audioOutputSelector = renderAudioOutputSelector(call); +if (audioOutputSelector) { + container.appendChild(audioOutputSelector); +} + +container.appendChild(renderVolumeControl(call)); + +// Closed caption controls +const closedCaptionManager = new ClosedCaptionManager(call); +container.appendChild(closedCaptionManager.renderToggleElement()); + +const captionContainer = document.getElementById('closed-captions'); +captionContainer?.appendChild(closedCaptionManager.renderCaptionContainer()); + +call.join({ create: true }); window.addEventListener('beforeunload', () => { + // Make sure to remove your event listeners when you leave a call + closedCaptionManager?.cleanup(); call.leave(); }); diff --git a/sample-apps/react-native/dogfood/package.json b/sample-apps/react-native/dogfood/package.json index 234b78ba81..ae3be22bec 100644 --- a/sample-apps/react-native/dogfood/package.json +++ b/sample-apps/react-native/dogfood/package.json @@ -1,6 +1,6 @@ { "name": "@stream-io/video-react-native-dogfood", - "version": "4.10.2", + "version": "4.11.0", "private": true, "scripts": { "android": "react-native run-android", diff --git a/sample-apps/react-native/dogfood/src/components/CallControlsComponent.tsx b/sample-apps/react-native/dogfood/src/components/CallControlsComponent.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sample-apps/react-native/dogfood/src/components/ClosedCaptions.tsx b/sample-apps/react-native/dogfood/src/components/ClosedCaptions.tsx new file mode 100644 index 0000000000..f693329875 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/components/ClosedCaptions.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { StyleSheet, Text, View } from 'react-native'; +import { useCallStateHooks } from '@stream-io/video-react-native-sdk'; +import { appTheme } from '../theme'; + +export const ClosedCaptions = () => { + const { useCallClosedCaptions } = useCallStateHooks(); + const closedCaptions = useCallClosedCaptions(); + return ( + + {closedCaptions.map(({ user, start_time, text }) => ( + + {user.name}: + {text} + + ))} + + ); +}; + +const styles = StyleSheet.create({ + rootContainer: { + backgroundColor: appTheme.colors.static_overlay, + paddingVertical: 6, + paddingHorizontal: 6, + height: 50, + }, + closedCaptionItem: { + flexDirection: 'row', + gap: 8, + }, + speakerName: { + color: appTheme.colors.light_gray, + }, + closedCaption: { + color: appTheme.colors.static_white, + }, +}); diff --git a/sample-apps/react/react-dogfood/components/ActiveCall.tsx b/sample-apps/react/react-dogfood/components/ActiveCall.tsx index eed08f78f1..73bed30ad5 100644 --- a/sample-apps/react/react-dogfood/components/ActiveCall.tsx +++ b/sample-apps/react/react-dogfood/components/ActiveCall.tsx @@ -27,7 +27,11 @@ import { InvitePanel, InvitePopup } from './InvitePanel/InvitePanel'; import { ChatWrapper } from './ChatWrapper'; import { ChatUI } from './ChatUI'; import { CallStatsSidebar, ToggleStatsButton } from './CallStatsWrapper'; -import { ClosedCaptions, ClosedCaptionsSidebar } from './ClosedCaptions'; +import { + ClosedCaptions, + ClosedCaptionsSidebar, + ToggleClosedCaptionsButton, +} from './ClosedCaptions'; import { ToggleSettingsTabModal } from './Settings/SettingsTabModal'; import { IncomingVideoSettingsButton } from './IncomingVideoSettings'; import { ToggleEffectsButton } from './ToggleEffectsButton'; @@ -246,23 +250,9 @@ export const ActiveCall = (props: ActiveCallProps) => {
- {isPronto && ( -
- - { - setSidebarContent( - showClosedCaptions ? null : 'closed-captions', - ); - }} - > - - - -
- )} +
+ +
@@ -280,6 +270,23 @@ export const ActiveCall = (props: ActiveCallProps) => {
+ {isPronto && ( +
+ + { + setSidebarContent( + showClosedCaptions ? null : 'closed-captions', + ); + }} + > + + + +
+ )}
{ - const [queue, setQueue] = useState(initialQueue); - - const addToQueue = useCallback((newCaption: CallClosedCaption) => { - setQueue((prevQueue) => { - const key = `${newCaption.speaker_id}-${newCaption.start_time}`; - const isDuplicate = prevQueue.some( - (caption) => `${caption.speaker_id}-${caption.start_time}` === key, - ); - - if (isDuplicate) { - return prevQueue; - } - - return [...prevQueue, newCaption]; - }); - }, []); - - return [queue, addToQueue, setQueue] as const; +export const ToggleClosedCaptionsButton = () => { + const call = useCall(); + const { useIsCallCaptioningInProgress, useHasPermissions } = + useCallStateHooks(); + const isCaptioned = useIsCallCaptioningInProgress(); + const canToggle = useHasPermissions( + OwnCapability.START_CLOSED_CAPTIONS_CALL, + OwnCapability.STOP_CLOSED_CAPTIONS_CALL, + ); + return ( + + { + if (!call) return; + try { + if (isCaptioned) { + await call.stopClosedCaptions(); + } else { + await call.startClosedCaptions(); + } + } catch (e) { + console.error('Failed to toggle closed captions', e); + } + }} + > + + + + ); }; export const ClosedCaptions = () => { - const call = useCall(); - const [queue, addToQueue, setQueue] = useDeduplicatedQueue(); - - useEffect(() => { - if (!call) return; - return call.on('call.closed_caption', (e) => { - if (e.type !== 'call.closed_caption') return; - if (e.closed_caption.text.trim() === '') return; - addToQueue(e.closed_caption); - }); - }, [call, addToQueue]); - - useEffect(() => { - const id = setTimeout(() => { - setQueue((prevQueue) => - prevQueue.length !== 0 ? prevQueue.slice(1) : prevQueue, - ); - }, 2700); - return () => clearTimeout(id); - }, [queue, setQueue]); - - const userNameMapping = useUserIdToUserNameMapping(); - + const { useCallClosedCaptions } = useCallStateHooks(); + const closedCaptions = useCallClosedCaptions(); return (
- {queue.slice(-2).map(({ speaker_id, text, start_time }) => ( -

- - {userNameMapping[speaker_id] || speaker_id}: - - {text} -

- ))} +
); }; export const ClosedCaptionsSidebar = () => { const call = useCall(); - const [queue, addToQueue] = useDeduplicatedQueue(); - + const [queue, addToQueue] = useState([]); useEffect(() => { if (!call) return; return call.on('call.closed_caption', (e) => { - if (e.type !== 'call.closed_caption') return; - if (e.closed_caption.text.trim() === '') return; - addToQueue(e.closed_caption); + addToQueue((q) => [...q, e.closed_caption]); }); - }, [call, addToQueue]); - - const userNameMapping = useUserIdToUserNameMapping(); - + }, [call]); return (

Closed Captions

- {queue.map(({ speaker_id, text, start_time }) => ( -

- - {userNameMapping[speaker_id] || speaker_id}: - - {text} -

- ))} +
); }; -const useUserIdToUserNameMapping = () => { - const { useCallSession } = useCallStateHooks(); - const session = useCallSession(); - return useMemo(() => { - if (!session) return {}; - return session.participants.reduce>( - (result, participant) => { - result[participant.user.id] = participant.user.name; - return result; - }, - {}, - ); - }, [session]); +const ClosedCaptionList = (props: { queue: CallClosedCaption[] }) => { + const { queue } = props; + return queue.map(({ user, text, start_time }) => ( +

+ {user.name}: + {text} +

+ )); }; diff --git a/sample-apps/react/react-dogfood/components/Settings/Transcriptions.tsx b/sample-apps/react/react-dogfood/components/Settings/Transcriptions.tsx index bfa0efdcaf..272c7d7d47 100644 --- a/sample-apps/react/react-dogfood/components/Settings/Transcriptions.tsx +++ b/sample-apps/react/react-dogfood/components/Settings/Transcriptions.tsx @@ -1,13 +1,16 @@ -import { Fragment, useEffect, useState } from 'react'; +import { Fragment, ReactNode, useEffect, useState } from 'react'; import { DropDownSelect, DropDownSelectOption, TranscriptionSettingsRequestModeEnum, useCall, + useCallStateHooks, + useI18n, } from '@stream-io/video-react-sdk'; +import clsx from 'clsx'; const languages = [ - { code: null, label: 'None' }, + { code: undefined, label: 'None' }, { code: 'en', label: 'English' }, { code: 'fr', label: 'French' }, { code: 'es', label: 'Spanish' }, @@ -44,8 +47,10 @@ const languages = [ export const TranscriptionSettings = () => { const call = useCall(); - const [firstLanguage, setFirstLanguage] = useState('en'); - const [secondLanguage, setSecondLanguage] = useState(null); + const [transcriptionLanguage, setTranscriptionLanguage] = useState< + string | undefined + >('en'); + useEffect(() => { if (!call) return; call @@ -54,25 +59,32 @@ export const TranscriptionSettings = () => { transcription: { ...call.state.settings?.transcription, mode: TranscriptionSettingsRequestModeEnum.AUTO_ON, - languages: [firstLanguage, secondLanguage].filter( - Boolean, - ) as string[], + language: transcriptionLanguage, }, }, }) .catch((err) => { console.error('Error updating call settings:', err); }); - }, [call, firstLanguage, secondLanguage]); + }, [call, transcriptionLanguage]); return (
-

Primary language

+
+
+ + +
+
+ +

Language

setFirstLanguage(languages[index + 1].code)} + handleSelect={(index) => + setTranscriptionLanguage(languages[index + 1].code) + } > {languages.map((language) => language.code ? ( @@ -86,22 +98,74 @@ export const TranscriptionSettings = () => { ), )} +
+ ); +}; -

Secondary language

- setSecondLanguage(languages[index].code)} - > - {languages.map((language) => ( - - ))} - +const ClosedCaptionStatus = () => { + const { t } = useI18n(); + const { useCallSettings, useIsCallCaptioningInProgress } = + useCallStateHooks(); + const settings = useCallSettings(); + const inProgress = useIsCallCaptioningInProgress(); + + return ( + + ); +}; + +const TranscriptionStatus = () => { + const { t } = useI18n(); + const { useCallSettings, useIsCallTranscribingInProgress } = + useCallStateHooks(); + const settings = useCallSettings(); + const inProgress = useIsCallTranscribingInProgress(); + + return ( + + ); +}; + +const StatusCard = (props: { + label: string; + value: string | ReactNode; + status?: 'on' | 'off'; +}) => { + const { t } = useI18n(); + const { label, value, status } = props; + + return ( +
+
+
{label}
+
{value}
+
+ {status && {t(status)}} +
+ ); +}; + +const StatusIndicator = (props: { + children: ReactNode; + status: 'on' | 'off'; +}) => { + const { children, status } = props; + return ( +
+
{children}
); }; diff --git a/sample-apps/react/react-dogfood/style/Transcriptions.scss b/sample-apps/react/react-dogfood/style/Transcriptions.scss index be7a42913b..1c5e64f8d5 100644 --- a/sample-apps/react/react-dogfood/style/Transcriptions.scss +++ b/sample-apps/react/react-dogfood/style/Transcriptions.scss @@ -1,2 +1,9 @@ .rd__transcriptions { + .str-video__call-stats { + padding: 0; + + .str-video__call-stats__card { + background-color: var(--str-video__background-color2); + } + } }