diff --git a/website/config/pluginsConfig/index.ts b/website/config/pluginsConfig/index.ts index 2dff86d8..bee64d46 100644 --- a/website/config/pluginsConfig/index.ts +++ b/website/config/pluginsConfig/index.ts @@ -33,7 +33,7 @@ const plugins: PluginConfig[] = [ llmsTxtPlugin, realtimeSdkPlugin, browserSdkPlugin, - // signalwireSdkPlugin, + signalwireSdkPlugin, ogImagesPlugin, ]; diff --git a/website/docs/signalwire-sdk/tech-ref/CallSession/audioMute.mdx b/website/docs/signalwire-sdk/tech-ref/CallSession/audioMute.mdx deleted file mode 100644 index c3483df1..00000000 --- a/website/docs/signalwire-sdk/tech-ref/CallSession/audioMute.mdx +++ /dev/null @@ -1,7 +0,0 @@ -# audioMute() - -The `audioMute()` method allows you to mute the audio of a call. - -```javascript -const callSession = await callSession.audioMute(); -``` \ No newline at end of file diff --git a/website/docs/signalwire-sdk/tech-ref/CallSession/hangup.mdx b/website/docs/signalwire-sdk/tech-ref/CallSession/hangup.mdx deleted file mode 100644 index 8a9a8f3e..00000000 --- a/website/docs/signalwire-sdk/tech-ref/CallSession/hangup.mdx +++ /dev/null @@ -1,11 +0,0 @@ -# hangup() - -The `hangup()` method allows you to hang up a call. - -```javascript -const callSession = await callSession.hangup(); -``` - -## Parameters - -- `reason`: The reason for hanging up the call. \ No newline at end of file diff --git a/website/docs/signalwire-sdk/tech-ref/CallSession/index.mdx b/website/docs/signalwire-sdk/tech-ref/CallSession/index.mdx deleted file mode 100644 index 0b8f514d..00000000 --- a/website/docs/signalwire-sdk/tech-ref/CallSession/index.mdx +++ /dev/null @@ -1,3 +0,0 @@ -# CallSession - -To create a `CallSession` you need to use the `client.dial()` method. \ No newline at end of file diff --git a/website/docs/signalwire-sdk/tech-ref/Types/index.mdx b/website/docs/signalwire-sdk/tech-ref/Types/index.mdx index c0a9422d..d1967388 100644 --- a/website/docs/signalwire-sdk/tech-ref/Types/index.mdx +++ b/website/docs/signalwire-sdk/tech-ref/Types/index.mdx @@ -1,4 +1,8 @@ -# API Types Documentation +--- +title: API Types +slug: /types +--- + Generated from TypeDoc JSON for @signalwire/client diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/events.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/events.mdx new file mode 100644 index 00000000..b1188483 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/events.mdx @@ -0,0 +1,1118 @@ +--- +title: CallSession Events +slug: /call-session/events +--- + +import APIField from '@site/src/components/APIField'; + +Events emitted by a CallSession object during call lifecycle, member actions, and room updates. + +## Core Call Events + +### `call.joined` + +Fired when you successfully join a call. + + + Complete room session object. + + + + The room ID. + + + + The room session ID. + + + + The call ID. + + + + Your member ID. + + + + Your permissions and capabilities for this call session. + + + + The original call ID. + + + + Node ID (may be undefined). + + +```typescript +call.on('call.joined', (event) => { + console.log('Joined call:', event.call_id) + console.log('Your member ID:', event.member_id) + console.log('Capabilities:', event.capabilities) +}) +``` + +### `call.updated` + +Fired when call properties are updated. + + + Updated room session object. + + + + The room ID. + + + + The room session ID. + + +```typescript +call.on('call.updated', (event) => { + console.log('Call updated:', event.room_session) +}) +``` + +### `call.left` + +Fired when you leave the call. + + + Room session object. + + + + The room ID. + + + + The room session ID. + + + + The call ID. + + + + Your member ID. + + + + The original call ID. + + + + Reason for leaving (e.g., "hangup", "kicked"). + + +```typescript +call.on('call.left', (event) => { + console.log('Left call:', event.reason) +}) +``` + +### `call.state` + +Fired when the call state changes. + + + The call ID. + + + + The node ID. + + + + The segment ID. + + + + Current call state. + + + + Call direction (inbound/outbound). + + + + Device information. + + + + Call start timestamp. + + + + Call answer timestamp. + + + + Call end timestamp. + + + + The room session ID. + + +```typescript +call.on('call.state', (event) => { + console.log('Call state:', event.call_state) + console.log('Direction:', event.direction) +}) +``` + +### `call.play` + +Fired when audio/video playback starts in the call. + + + Playback control ID. + + + + The call ID. + + + + The node ID. + + + + Playback state. + + + + The room session ID. + + +```typescript +call.on('call.play', (event) => { + console.log('Playback state:', event.state) +}) +``` + +### `call.connect` + +Fired when a call connection is established. + + + Connection state. + + + + The call ID. + + + + The node ID. + + + + The segment ID. + + + + The room session ID. + + + + Peer connection information (optional). + + + + The peer's call ID. + + + + The peer's node ID. + + + + The peer's device information. + + +```typescript +call.on('call.connect', (event) => { + console.log('Connect state:', event.connect_state) + if (event.peer) { + console.log('Peer call ID:', event.peer.call_id) + } +}) +``` + +### `call.room` + +Fired for room-level call events. + + + Join status. + + + + The call ID. + + + + The node ID. + + + + The segment ID. + + + + The room session ID. + + +```typescript +call.on('call.room', (event) => { + console.log('Joined status:', event.joined_status) +}) +``` + +## Room Events + +### `room.joined` + +Fired when the room is joined. + + + Complete room session object. + + + + The room ID. + + + + The room session ID. + + + + The call ID. + + + + Your member ID. + + + + Node ID (may be undefined). + + + + The original call ID. + + + + Raw capabilities array. + + +```typescript +call.on('room.joined', (event) => { + console.log('Room joined:', event.room_id) + console.log('Member ID:', event.member_id) +}) +``` + +### `room.subscribed` + +Fired when successfully subscribed to room events. This is the event that `start()` waits for. + + + Complete room session object. + + + + The room ID. + + + + The room session ID. + + + + The call ID. + + + + Your member ID. + + + + Node ID (may be undefined). + + + + The original call ID. + + + + Raw capabilities array. + + +```typescript +call.on('room.subscribed', (event) => { + console.log('Subscribed to room:', event.call_id) +}) +``` + +### `room.updated` + +Fired when room properties change (e.g., lock/unlock). + + + Updated room session object. + + + + The room ID. + + + + The room session ID. + + +```typescript +call.on('room.updated', (event) => { + console.log('Room locked:', event.room_session.locked) +}) +``` + +### `room.left` + +Fired when you leave the room. + + + Reason for leaving (all parameters are optional). + + +```typescript +call.on('room.left', (event) => { + if (event) { + console.log('Left room:', event.reason) + } +}) +``` + +## Member Events + +### `member.joined` + +Fired when a new member joins the call. + + + The member object with all properties. + + + + The room ID. + + + + The room session ID. + + +```typescript +call.on('member.joined', (event) => { + console.log(`${event.member.name} joined (ID: ${event.member.id})`) +}) +``` + +### `member.updated` + +General event fired when any member property changes. + + + Updated member object with changed properties. + + + + The room ID. + + + + The room session ID. + + +```typescript +call.on('member.updated', (event) => { + console.log('Member updated:', event.member) +}) +``` + +### `member.left` + +Fired when a member leaves the call. + + + The member who left. + + + + The room ID. + + + + The room session ID. + + + + Reason for leaving (e.g., "hangup", "kicked"). + + +```typescript +call.on('member.left', (event) => { + console.log(`${event.member.name} left: ${event.reason}`) +}) +``` + +### `member.talking` + +Fired when a member starts or stops talking (voice activity detection). + + + The room ID. + + + + The room session ID. + + + + The member ID. + + + + The node ID. + + + + Whether member is currently talking. + + +```typescript +call.on('member.talking', (event) => { + if (event.member.talking) { + console.log(`Member ${event.member.member_id} is talking`) + } +}) +``` + +### `memberList.updated` + +Fired when the member list changes (members join or leave). + + + Complete array of all current members. + + +```typescript +call.on('memberList.updated', (event) => { + console.log(`Total members: ${event.members.length}`) +}) +``` + +## Member State Events + +These specific `member.updated` events fire when particular member properties change. + +### `member.updated.audioMuted` + +Fired when a member's audio mute state changes. + +```typescript +call.on('member.updated.audioMuted', (event) => { + const status = event.member.audio_muted ? 'muted' : 'unmuted' + console.log(`${event.member.name} audio ${status}`) +}) +``` + +### `member.updated.videoMuted` + +Fired when a member's video mute state changes. + +```typescript +call.on('member.updated.videoMuted', (event) => { + const status = event.member.video_muted ? 'muted' : 'unmuted' + console.log(`${event.member.name} video ${status}`) +}) +``` + +### `member.updated.deaf` + +Fired when a member's deaf state changes. + +```typescript +call.on('member.updated.deaf', (event) => { + const status = event.member.deaf ? 'deafened' : 'undeafened' + console.log(`${event.member.name} ${status}`) +}) +``` + +### `member.updated.visible` + +Fired when a member's visibility changes. + +```typescript +call.on('member.updated.visible', (event) => { + console.log(`${event.member.name} visible:`, event.member.visible) +}) +``` + +### `member.updated.onHold` + +Fired when a member's hold state changes. + +```typescript +call.on('member.updated.onHold', (event) => { + const status = event.member.on_hold ? 'on hold' : 'off hold' + console.log(`${event.member.name} is ${status}`) +}) +``` + +### `member.updated.inputVolume` + +Fired when a member's input (microphone) volume changes. + +```typescript +call.on('member.updated.inputVolume', (event) => { + console.log(`${event.member.name} input volume:`, event.member.input_volume) +}) +``` + +### `member.updated.outputVolume` + +Fired when a member's output (speaker) volume changes. + +```typescript +call.on('member.updated.outputVolume', (event) => { + console.log(`${event.member.name} output volume:`, event.member.output_volume) +}) +``` + +### `member.updated.inputSensitivity` + +Fired when a member's input sensitivity changes. + +```typescript +call.on('member.updated.inputSensitivity', (event) => { + console.log(`${event.member.name} sensitivity:`, event.member.input_sensitivity) +}) +``` + +### `member.updated.handraised` + +Fired when a member raises or lowers their hand. + +```typescript +call.on('member.updated.handraised', (event) => { + const status = event.member.handraised ? 'raised' : 'lowered' + console.log(`${event.member.name} ${status} hand`) +}) +``` + +### `member.updated.echoCancellation` + +Fired when a member's echo cancellation setting changes. + +```typescript +call.on('member.updated.echoCancellation', (event) => { + console.log(`Echo cancellation:`, event.member.echo_cancellation) +}) +``` + +### `member.updated.autoGain` + +Fired when a member's automatic gain control setting changes. + +```typescript +call.on('member.updated.autoGain', (event) => { + console.log(`Auto gain:`, event.member.auto_gain) +}) +``` + +### `member.updated.noiseCancellation` + +Fired when a member's noise cancellation setting changes. + +```typescript +call.on('member.updated.noiseCancellation', (event) => { + console.log(`Noise cancellation:`, event.member.noise_cancellation) +}) +``` + +### `member.updated.noiseSuppression` + +Fired when a member's noise suppression setting changes. + +```typescript +call.on('member.updated.noiseSuppression', (event) => { + console.log(`Noise suppression:`, event.member.noise_suppression) +}) +``` + +## Layout Events + +### `layout.changed` + +Fired when the video layout changes. + + + Layout object with name and layers. + + + + Layout name (e.g., "grid-responsive", "2x2"). + + + + Array of layer objects with member positions. + + + + The room ID. + + + + The room session ID. + + +```typescript +call.on('layout.changed', (event) => { + console.log('Layout changed to:', event.layout.name) + + // Find your position + const myLayer = event.layout.layers.find( + layer => layer.member_id === call.memberId + ) + if (myLayer) { + console.log('Your position:', myLayer.position) + } +}) +``` + +## Media Events + +### `media.connected` + +Fired when media connection is established. + +```typescript +call.on('media.connected', () => { + console.log('Media connected') +}) +``` + +### `media.reconnecting` + +Fired when media is reconnecting after a disruption. + +```typescript +call.on('media.reconnecting', () => { + console.log('Media reconnecting...') +}) +``` + +### `media.disconnected` + +Fired when media connection is lost. + +```typescript +call.on('media.disconnected', () => { + console.log('Media disconnected') +}) +``` + +## Connection State Events + +These events pass the CallSession object as a parameter and represent internal call state changes. + +### `connecting` + +Connection is being established. + +```typescript +call.on('connecting', (callSession) => { + console.log('Connecting...') +}) +``` + +### `connected` + +Connection has been successfully established. + +```typescript +call.on('connected', (callSession) => { + console.log('Connected!') +}) +``` + +### `disconnected` + +Connection has been terminated. + +```typescript +call.on('disconnected', (callSession) => { + console.log('Disconnected') +}) +``` + +### `disconnecting` + +Connection is being torn down. + +```typescript +call.on('disconnecting', (callSession) => { + console.log('Disconnecting...') +}) +``` + +### `reconnecting` + +Connection is attempting to reconnect after being lost. + +```typescript +call.on('reconnecting', (callSession) => { + console.log('Reconnecting...') +}) +``` + +### `reconnected` + +Connection has been successfully re-established. + +```typescript +call.on('reconnected', (callSession) => { + console.log('Reconnected!') +}) +``` + +### `active` + +Connection is active and media is flowing. + +```typescript +call.on('active', (callSession) => { + console.log('Call is active') +}) +``` + +### `answering` + +Call is being answered. + +```typescript +call.on('answering', (callSession) => { + console.log('Answering call...') +}) +``` + +### `early` + +Early media state (before call is fully answered). + +```typescript +call.on('early', (callSession) => { + console.log('Early media') +}) +``` + +### `hangup` + +Call is hanging up. + +```typescript +call.on('hangup', (callSession) => { + console.log('Call hanging up') +}) +``` + +### `destroy` + +Call is fully destroyed and all resources are released. + +```typescript +call.on('destroy', () => { + console.log('Call destroyed') +}) +``` + +### `held` + +Call is on hold. + +```typescript +call.on('held', (callSession) => { + console.log('Call on hold') +}) +``` + +### `new` + +New connection being established. + +```typescript +call.on('new', (callSession) => { + console.log('New connection') +}) +``` + +### `ringing` + +Call is ringing. + +```typescript +call.on('ringing', (callSession) => { + console.log('Ringing...') +}) +``` + +### `trying` + +Attempting to establish connection. + +```typescript +call.on('trying', (callSession) => { + console.log('Trying to connect...') +}) +``` + +### `requesting` + +Connection request is being initiated. + +```typescript +call.on('requesting', (callSession) => { + console.log('Requesting connection...') +}) +``` + +### `recovering` + +Connection is in recovery mode, attempting to restore state. + +```typescript +call.on('recovering', (callSession) => { + console.log('Recovering connection...') +}) +``` + +### `purge` + +Connection resources are being purged/cleaned up. + +```typescript +call.on('purge', (callSession) => { + console.log('Purging connection resources') +}) +``` + +## Device Events + +### `camera.updated` + +Fired when the camera device changes. + + + The previous camera device. + + + + The current camera device. + + +```typescript +call.on('camera.updated', (event) => { + console.log('Camera updated from:', event.previous.deviceId) + console.log('Camera updated to:', event.current.deviceId) +}) +``` + +### `camera.disconnected` + +Fired when the camera is disconnected. + + + The device ID of the disconnected camera. + + + + The label of the disconnected camera. + + + + The kind of device (videoinput). + + +```typescript +call.on('camera.disconnected', (event) => { + console.log('Camera disconnected:', event.label) +}) +``` + +### `microphone.updated` + +Fired when the microphone device changes. + + + The previous microphone device. + + + + The current microphone device. + + +```typescript +call.on('microphone.updated', (event) => { + console.log('Microphone updated from:', event.previous.deviceId) + console.log('Microphone updated to:', event.current.deviceId) +}) +``` + +### `microphone.disconnected` + +Fired when the microphone is disconnected. + + + The device ID of the disconnected microphone. + + + + The label of the disconnected microphone. + + + + The kind of device (audioinput). + + +```typescript +call.on('microphone.disconnected', (event) => { + console.log('Microphone disconnected:', event.label) +}) +``` + +### `speaker.updated` + +Fired when the speaker device changes. + + + The previous speaker device. + + + + The current speaker device. + + +```typescript +call.on('speaker.updated', (event) => { + console.log('Speaker updated from:', event.previous.deviceId) + console.log('Speaker updated to:', event.current.deviceId) +}) +``` + +### `speaker.disconnected` + +Fired when the speaker is disconnected. + + + The device ID of the disconnected speaker. + + + + The label of the disconnected speaker. + + + + The kind of device (audiooutput). + + +```typescript +call.on('speaker.disconnected', (event) => { + console.log('Speaker disconnected:', event.label) +}) +``` + +## WebRTC Events + +### `track` + +Fired when a new WebRTC media track is added. + +```typescript +call.on('track', (event) => { + console.log('New track:', event.track.kind) + console.log('Streams:', event.streams) +}) +``` + +## Common Patterns + +### Managing Event Listeners + +```typescript +// Add listener +const handler = (params) => { + console.log('Event fired:', params) +} +call.on('member.joined', handler) + +// Remove listener +call.off('member.joined', handler) + +// One-time listener +call.once('call.joined', (params) => { + console.log('Joined - fires only once') +}) +``` + +### Track Member Count + +```typescript +let memberCount = 0 + +call.on('member.joined', () => { + memberCount++ + updateUI({ memberCount }) +}) + +call.on('member.left', () => { + memberCount-- + updateUI({ memberCount }) +}) +``` + +### Monitor Active Speakers + +```typescript +const activeSpeakers = new Set() + +call.on('member.talking', (event) => { + if (event.member.talking) { + activeSpeakers.add(event.member.member_id) + } else { + activeSpeakers.delete(event.member.member_id) + } + + console.log(`Active speakers: ${activeSpeakers.size}`) +}) +``` + +### Handle Connection Issues + +```typescript +call.on('media.disconnected', () => { + showNotification('Connection lost') +}) + +call.on('media.reconnecting', () => { + showNotification('Reconnecting...') +}) + +call.on('media.connected', () => { + showNotification('Connection restored') +}) +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/index.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/index.mdx new file mode 100644 index 00000000..1c183361 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/index.mdx @@ -0,0 +1,368 @@ +--- +slug: /call-session +title: CallSession +--- + +import APIField from '@site/src/components/APIField'; + +# CallSession + +A `CallSession` object represents an active voice or video call. You get instances of a CallSession from the SignalWire Client by dialing, answering, or reattaching to calls. + +## How It Works + +The CallSession provides real-time control over an active call through both method calls and events. When you call methods like `audioMute()` or `setLayout()`, the SDK sends commands over the WebRTC connection and returns promises with the results. Simultaneously, you can listen for events like member updates or layout changes. + +### Getting a CallSession + +You receive a `CallSession` object when you dial, answer, or reattach to a call: + +```typescript +// Outbound call +const call = await client.dial({ to: 'user@example.com' }) + +// Answer incoming call +client.on('call.received', async (incomingCall) => { + const call = await incomingCall.answer() +}) + +// Reattach to existing call +const call = await client.reattach({ call_id: 'existing-call-id' }) +``` + +## Key Concepts + +### Member Targeting + +Many CallSession methods accept an optional `memberId` parameter: + +- **No `memberId`** - Action applies to yourself +- **Specific ID** - Action applies to that member (requires permission) + +```typescript +// Mute yourself +await call.audioMute() + +// Mute another member +await call.audioMute({ memberId: 'member-123' }) +``` + +### Capabilities and Permissions + +Before calling control methods that affect other members, check your `capabilities` to ensure you have permission. + +```typescript +if (call.capabilities?.member.remove) { + await call.removeMember({ memberId: 'member-123' }) +} +``` + +### Lifecycle + +Always call `hangup()` when done with a call to properly clean up resources. Listen to the `destroy` event to know when cleanup is complete. + +```typescript +await call.hangup() + +call.on('destroy', () => { + console.log('Call cleaned up') +}) +``` + +## Properties + + + Your unique member ID within this call session. Set when you successfully join the call. + + ```typescript + call.on('call.joined', () => { + console.log('My member ID:', call.memberId) + }) + ``` + + + + The full layout changed event object containing the current layout and all member positions. Updated automatically when the room layout changes. + + ```typescript + const layoutEvent = call.currentLayoutEvent + console.log('Current layout:', layoutEvent.layout.name) + console.log('Layout layers:', layoutEvent.layout.layers) + ``` + + + + The current video layout object (convenience accessor from `currentLayoutEvent.layout`). Updated automatically when the room layout changes. + + ```typescript + const layout = call.currentLayout + console.log('Current layout:', layout.name) + ``` + + + + Your current position within the video layout, or `undefined` if not found in any layer. + + ```typescript + const position = call.currentPosition + if (position) { + console.log('My position:', position) + } + ``` + + + + Defines what actions are allowed for this call session based on your role and permissions. + + ```typescript + // Check before performing actions + if (call.capabilities?.self.muteAudio.on) { + await call.audioMute() + } + + if (call.capabilities?.member.remove) { + await call.removeMember({ memberId: 'member-id' }) + } + ``` + + + + Array of active screen sharing sessions in this call. + + ```typescript + console.log('Active screen shares:', call.screenShareList.length) + ``` + + + + The underlying connection ID for this call. + + ```typescript + console.log('Call ID:', call.callId) + ``` + + + + The unique identifier for the room. + + ```typescript + console.log('Room ID:', call.roomId) + ``` + + + + The unique identifier for the room session. + + ```typescript + console.log('Room Session ID:', call.roomSessionId) + ``` + + + + Unique id for this room session. + + ```typescript + console.log('Session ID:', call.id) + ``` + + + + Whether the connection is currently active. + + ```typescript + if (call.active) { + console.log('Call is active') + } + ``` + + + + Provides access to the local MediaStream. + + ```typescript + const stream = call.localStream + if (stream) { + console.log('Local stream tracks:', stream.getTracks()) + } + ``` + + + + Provides access to the remote MediaStream. + + ```typescript + const stream = call.remoteStream + if (stream) { + console.log('Remote stream tracks:', stream.getTracks()) + } + ``` + + + + Provides access to the local audio MediaStreamTrack. + + ```typescript + const audioTrack = call.localAudioTrack + if (audioTrack) { + console.log('Audio enabled:', audioTrack.enabled) + } + ``` + + + + Provides access to the local video MediaStreamTrack. + + ```typescript + const videoTrack = call.localVideoTrack + if (videoTrack) { + console.log('Video enabled:', videoTrack.enabled) + } + ``` + + + + The id of the video device, or null if not available. + + ```typescript + console.log('Camera ID:', call.cameraId) + ``` + + + + The label of the video device, or null if not available. + + ```typescript + console.log('Camera:', call.cameraLabel) + ``` + + + + The constraints applied to the video device, or null if not available. + + ```typescript + console.log('Camera constraints:', call.cameraConstraints) + ``` + + + + The id of the audio input device, or null if not available. + + ```typescript + console.log('Microphone ID:', call.microphoneId) + ``` + + + + The label of the audio input device, or null if not available. + + ```typescript + console.log('Microphone:', call.microphoneLabel) + ``` + + + + The constraints applied to the audio input device, or null if not available. + + ```typescript + console.log('Microphone constraints:', call.microphoneConstraints) + ``` + + + + Indicates if there is any receiving audio. + + ```typescript + if (call.withAudio) { + console.log('Receiving audio') + } + ``` + + + + Indicates if there is any receiving video. + + ```typescript + if (call.withVideo) { + console.log('Receiving video') + } + ``` + + + + The preview URL for the room. Only available with `enable_room_previews: true` on room configuration. + + ```typescript + if (call.previewUrl) { + console.log('Preview URL:', call.previewUrl) + } + ``` + + +## Examples + +### Muting Audio and Video + +```typescript +// Mute your own audio +await call.audioMute() + +// Unmute your video +await call.videoUnmute() + +// Mute a specific member (requires permission) +await call.audioMute({ memberId: 'member-123' }) +``` + +### Managing Call Members + +```typescript +// Get all members in the call +const { members } = await call.getMembers() +members.forEach(member => { + console.log(`${member.name}: ${member.audio_muted ? 'muted' : 'unmuted'}`) +}) + +// Remove a member (requires permission) +await call.removeMember({ memberId: 'member-123' }) +``` + +### Changing Video Layouts + +```typescript +// Change the video layout +await call.setLayout({ name: 'grid-responsive' }) + +// Get available layouts +const { layouts } = await call.getLayouts() + +// Position members in the layout +await call.setPositions({ + positions: { + 'member-1': { layer: 0, position: 'standard-1' }, + 'member-2': { layer: 0, position: 'standard-2' } + } +}) +``` + +### Listening to Call Events + +```typescript +// Listen for member updates +call.on('member.updated', (event) => { + console.log('Member updated:', event.member) +}) + +// Listen for layout changes +call.on('layout.changed', (event) => { + console.log('New layout:', event.layout.name) +}) + +// Track call lifecycle +call.on('call.joined', () => { + console.log('Successfully joined the call') +}) + +call.on('destroy', () => { + console.log('Call ended and cleaned up') +}) +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/answer.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/answer.mdx new file mode 100644 index 00000000..560dc104 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/answer.mdx @@ -0,0 +1,124 @@ +--- +title: answer +slug: /call-session/answer +--- + +Answers an incoming call and establishes the WebRTC connection. + +## Signature + +```typescript +answer(): Promise +``` + +## Parameters + +None + +## Returns + +**Type:** `Promise` + +## Important Notes + +**Use invite.accept() Instead:** For incoming calls in the Call/Unified API, use the `invite.accept()` method provided in the call notification rather than calling `answer()` directly. + +**Correct approach:** +```typescript +client.on('call.received', (notification) => { + const call = notification.invite.accept({ audio: true, video: true }) +}) +``` + +**Not typically used:** +```typescript +// Direct answer() calls are handled internally by the SDK +await call.answer() +``` + +**Lifecycle Events:** When answering a call, listen for these events: +- `call.joined` - Call successfully answered and joined +- `room.subscribed` - Subscribed to call room events +- `destroy` - Call failed to answer or was terminated + +**Inbound Call Direction:** The `answer()` method is only relevant for inbound calls (calls you receive). For outbound calls initiated with `client.dial()`, use `start()` instead. + +## Events + +When answering a call, the following events are emitted: +- `call.joined` - Successfully joined the call +- `room.subscribed` - Subscribed to call events +- Connection state events: `connecting`, `connected`, etc. +- `destroy` - Call failed or ended + +## Examples + +### Standard Pattern (Recommended) + +Use `invite.accept()` when handling incoming calls: + +```typescript +// Set up incoming call handler +client.on('call.received', (notification) => { + console.log('Incoming call from:', notification.invite.details.caller_id_name) + + // Accept the incoming call - this internally handles the answer process + const call = notification.invite.accept({ + audio: true, + video: true + }) + + // Call is now answered and connecting + call.on('call.joined', () => { + console.log('Call answered and joined successfully') + }) +}) +``` + +### With Call Parameters + +```typescript +client.on('call.received', (notification) => { + const call = notification.invite.accept({ + audio: true, + video: false, // Audio-only call + rootElement: document.getElementById('video-container') + }) + + call.on('call.joined', () => { + console.log('Audio call answered') + }) +}) +``` + +### Rejecting Instead of Answering + +```typescript +client.on('call.received', (notification) => { + const callerName = notification.invite.details.caller_id_name + + // Reject the call + await notification.invite.reject() + console.log(`Rejected call from ${callerName}`) +}) +``` + +### Error Handling + +```typescript +client.on('call.received', (notification) => { + try { + const call = notification.invite.accept({ audio: true, video: true }) + + call.on('call.joined', () => { + console.log('Call answered successfully') + }) + + call.on('destroy', () => { + console.error('Call failed to connect') + }) + } catch (error) { + console.error('Failed to accept call:', error) + } +}) +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/audio-mute.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/audio-mute.mdx new file mode 100644 index 00000000..7b595205 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/audio-mute.mdx @@ -0,0 +1,97 @@ +--- +title: audioMute +slug: /call-session/audiomute +--- + +import APIField from '@site/src/components/APIField'; + +Mutes the microphone for yourself or another participant in the call. + +## Signature + +```typescript +audioMute(params?: MemberCommandParams): Promise +``` + +## Parameters + + + Options for the mute operation. + + + + ID of the member to mute. If omitted, mutes your own microphone. + + +## Returns + +**Type:** `Promise` + +A Promise that resolves when the mute operation completes successfully. + +## Permissions + +- **Muting yourself:** Requires `call.capabilities.self.muteAudio.on` +- **Muting another member:** Requires `call.capabilities.member.muteAudio.on` + +## Events + +- `member.updated` - General member update event +- `member.updated.audioMuted` - Specific audio mute state change + +## Examples + +### Mute your own microphone + +```typescript +await call.audioMute() +``` + +### Mute another participant + +```typescript +const { members } = await call.getMembers() +const member = members.find(m => m.name === 'John Doe') + +if (member) { + await call.audioMute({ memberId: member.id }) +} +``` + +### Check capabilities before muting + +```typescript +if (call.capabilities?.self.muteAudio.on) { + await call.audioMute() +} +``` + +### Listen for mute events + +```typescript +call.on('member.updated.audioMuted', (event) => { + console.log(`Member ${event.member.id} is now ${event.member.audio_muted ? 'muted' : 'unmuted'}`) +}) + +await call.audioMute() +``` + +### Toggle mute with state tracking + +```typescript +let isMuted = false + +call.on('member.updated.audioMuted', (event) => { + if (event.member.id === call.memberId) { + isMuted = event.member.audio_muted + } +}) + +async function toggleMute() { + if (isMuted) { + await call.audioUnmute() + } else { + await call.audioMute() + } +} +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/audio-unmute.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/audio-unmute.mdx new file mode 100644 index 00000000..81024430 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/audio-unmute.mdx @@ -0,0 +1,91 @@ +--- +title: audioUnmute +slug: /call-session/audiounmute +--- + +import APIField from '@site/src/components/APIField'; + +Unmutes the microphone for yourself or another participant in the call. + +## Signature + +```typescript +audioUnmute(params?: MemberCommandParams): Promise +``` + +## Parameters + + + Options for the unmute operation. + + + + ID of the member to unmute. If omitted, unmutes your own microphone. + + +## Permissions + +- **Unmuting yourself:** Requires `call.capabilities.self.muteAudio.off` +- **Unmuting another member:** Requires `call.capabilities.member.muteAudio.off` + +## Events + +- `member.updated` - General member update event +- `member.updated.audioMuted` - Specific audio mute state change + +## Examples + +### Unmute your own microphone + +```typescript +await call.audioUnmute() +``` + +### Unmute another participant + +```typescript +const { members } = await call.getMembers() +const member = members.find(m => m.name === 'John Doe') + +if (member) { + await call.audioUnmute({ memberId: member.id }) +} +``` + +### Check capabilities before unmuting + +```typescript +if (call.capabilities?.self.muteAudio.off) { + await call.audioUnmute() +} +``` + +### Listen for unmute events + +```typescript +call.on('member.updated.audioMuted', (event) => { + console.log(`Member ${event.member.id} is now ${event.member.audio_muted ? 'muted' : 'unmuted'}`) +}) + +await call.audioUnmute() +``` + +### Toggle mute with state tracking + +```typescript +let isMuted = false + +call.on('member.updated.audioMuted', (event) => { + if (event.member.id === call.memberId) { + isMuted = event.member.audio_muted + } +}) + +async function toggleMute() { + if (isMuted) { + await call.audioUnmute() + } else { + await call.audioMute() + } +} +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/deaf.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/deaf.mdx new file mode 100644 index 00000000..d348c509 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/deaf.mdx @@ -0,0 +1,105 @@ +--- +title: deaf +slug: /call-session/deaf +--- + +import APIField from '@site/src/components/APIField'; + +Mutes incoming audio so you or another participant cannot hear others in the call. + +## Parameters + + + Options for the deaf operation. + + + + ID of the member to make deaf. If omitted, makes yourself deaf. + + +## Signature + +```typescript +deaf(params?: MemberCommandParams): Promise +``` + +## Permissions + +- **Making yourself deaf:** Requires `call.capabilities.self.deaf.on` +- **Making another member deaf:** Requires `call.capabilities.member.deaf.on` + +## Events + +- `member.updated` - General member update event +- `member.updated.deaf` - Specific deaf state change +- `member.updated.audioMuted` - Also emitted because microphone is auto-muted + +## Important Notes + +**Automatic Microphone Mute:** When you make a participant deaf, their microphone is automatically muted as well, even if you don't have the `muteAudio` permission. + +```typescript +// Making deaf automatically mutes the microphone too +await call.deaf() + +// You can manually unmute the microphone if needed +await call.audioUnmute() + +// Now you can speak but still cannot hear others +``` + +This ensures that deaf participants don't accidentally broadcast audio when they can't hear the conversation. + +**deaf() vs audioMute():** + +| Method | Effect on You | Effect on Others | +|--------|---------------|------------------| +| `deaf()` | You can't hear others (and your mic is muted) | Others can't hear you (mic auto-muted) | +| `audioMute()` | You can still hear others | Others can't hear you | + +```typescript +// Scenario 1: You want to listen but not speak +await call.audioMute() // You can hear, they can't hear you + +// Scenario 2: You want to completely disconnect from audio +await call.deaf() // You can't hear, they can't hear you (mic auto-muted) +``` + +## Examples + +### Make yourself deaf + +```typescript +await call.deaf() +console.log('You can no longer hear other participants') +``` + +### Make another participant deaf + +```typescript +const { members } = await call.getMembers() +const member = members.find(m => m.name === 'John Doe') + +if (member) { + await call.deaf({ memberId: member.id }) +} +``` + +### Check capabilities before making deaf + +```typescript +if (call.capabilities?.self.deaf.on) { + await call.deaf() +} +``` + +### Listen for deaf state changes + +```typescript +call.on('member.updated.deaf', (event) => { + const status = event.member.deaf ? 'deaf' : 'can hear' + console.log(`Member ${event.member.id} is now ${status}`) +}) + +await call.deaf() +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/destroy.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/destroy.mdx new file mode 100644 index 00000000..cc5b332b --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/destroy.mdx @@ -0,0 +1,131 @@ +--- +title: destroy +slug: /call-session/destroy +--- + +Destroys the CallSession JavaScript object and cleans up local resources. This does not affect the server-side call or other participants. + +## Signature + +```typescript +call.destroy(): void +``` + +## Parameters + +None + +## Returns + +**Type:** `void` + +## Examples + +### Basic Destroy + +```typescript +call.destroy() +console.log('CallSession object destroyed') +``` + +### Cleanup After Hangup + +```typescript +// End the call +await call.hangup() + +// Clean up the local object +call.destroy() +``` + +### React useEffect Cleanup + +```typescript +function CallComponent({ call }) { + useEffect(() => { + // Component mounted + + // Cleanup on unmount + return () => { + if (call) { + call.destroy() + } + } + }, [call]) + + return
Call interface
+} +``` + +### Single Page Application Cleanup + +```typescript +// When navigating away from call page +function navigateAway() { + if (currentCall) { + currentCall.hangup() + currentCall.destroy() + currentCall = null + } + + router.navigate('/home') +} +``` + +### Multiple Call Management + +```typescript +class CallManager { + constructor() { + this.calls = [] + } + + addCall(call) { + this.calls.push(call) + } + + async endCall(call) { + // End the call + await call.hangup() + + // Clean up the object + call.destroy() + + // Remove from list + this.calls = this.calls.filter(c => c !== call) + } + + async destroyAll() { + for (const call of this.calls) { + await call.hangup() + call.destroy() + } + this.calls = [] + } +} +``` + +## Important Notes + +**Local Only**: This method only destroys the JavaScript object locally. It does not: +- End the call on the server +- Remove you from the call +- Affect other participants +- Stop WebRTC connections + +To end the call properly, use `hangup()` before calling `destroy()`. + +**Memory Cleanup**: Use this method to: +- Clean up event listeners +- Release local resources +- Prevent memory leaks in long-running applications +- Free up browser resources + +**Object Unusable After Destroy**: Once `destroy()` is called, the CallSession object cannot be used anymore. Any subsequent method calls will likely fail. + +**Automatic Cleanup**: In most cases, you don't need to manually call `destroy()`. The SDK automatically cleans up when: +- The call ends via `hangup()` +- The page unloads +- The component unmounts (in framework integrations) + +Only call `destroy()` manually when you need explicit control over resource cleanup. diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/end.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/end.mdx new file mode 100644 index 00000000..55299044 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/end.mdx @@ -0,0 +1,140 @@ +--- +title: end +slug: /call-session/end +--- + +import APIField from '@site/src/components/APIField'; + +Ends the call for yourself or another participant, disconnecting them completely. + +## Signature + +```typescript +end(params?: MemberCommandParams): Promise +``` + +## Parameters + + + Configuration object. + + + + Member to end the call for. Omit to end your own call, provide specific ID to end another member's call. + + +## Returns + +**Type:** `Promise` + +## Important Notes + +**end() vs hangup() vs leave():** + +| Method | Effect | Use Case | +|--------|--------|----------| +| `end()` | Ends call for yourself or another member | General disconnect, can target specific members | +| `hangup()` | Ends your own call | Explicitly disconnect yourself | +| `leave()` | Alias for `hangup()` | Same as hangup, more semantic name | + +**When to use each:** + +```typescript +// End your own call +await call.end() // ✅ Works +await call.hangup() // ✅ Works (more explicit) +await call.leave() // ✅ Works (semantic alias) + +// End another member's call +await call.end({ memberId: 'member-123' }) // ✅ Works +await call.removeMember({ memberId: 'member-123' }) // ✅ Alternative method +``` + +**end() vs removeMember():** + +Both methods can disconnect a participant: + +| Method | Purpose | Semantic Meaning | +|--------|---------|------------------| +| `end()` | End the call for a member | Terminate their call session | +| `removeMember()` | Remove a member from the call | Kick them out / moderate | + +**Recommendation:** Use `removeMember()` for moderation actions (kicking users), and `end()` for general disconnection. + +**Cleanup Behavior:** When you end your own call: +- WebRTC connection is closed +- All media streams are stopped +- Screen shares are automatically stopped +- Event listeners remain active (clean them up manually if needed) + +## Events + +When a call is ended, the following events may fire: +- `call.left` - Participant has left the call +- `member.left` - Member has been removed from the call + +```typescript +call.on('member.left', (event) => { + console.log(`Member ${event.member.id} left the call`) +}) +``` + +## Examples + +### End your own call + +```typescript +await call.end() +console.log('You have left the call') +``` + +### End another participant's call + +```typescript +await call.end({ memberId: 'member-123' }) +console.log('Member removed from call') +``` + +### Get member ID and end their call + +```typescript +const { members } = await call.getMembers() +const memberToRemove = members.find(m => m.name === 'John Doe') + +if (memberToRemove) { + await call.end({ memberId: memberToRemove.id }) + console.log(`${memberToRemove.name} has been removed from the call`) +} +``` + +### Graceful disconnect + +```typescript +async function leaveCallGracefully() { + // Mute audio/video before leaving + await call.audioMute() + await call.videoMute() + + // End the call + await call.end() + + console.log('Left call gracefully') +} +``` + +### End call with confirmation + +```typescript +async function endCallWithConfirmation() { + const confirmed = confirm('Are you sure you want to end the call?') + + if (confirmed) { + try { + await call.end() + console.log('Call ended by user') + } catch (error) { + console.error('Failed to end call:', error) + } + } +} +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/get-layouts.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/get-layouts.mdx new file mode 100644 index 00000000..5cedfc96 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/get-layouts.mdx @@ -0,0 +1,138 @@ +--- +title: getLayouts +slug: /call-session/getlayouts +--- + +Retrieves a list of available video layouts for the call. + +## Signature + +```typescript +getLayouts(): Promise<{ layouts: string[] }> +``` + +## Parameters + +None + +## Returns + +**Type:** `Promise<{ layouts: string[] }>` + +An object containing an array of layout name strings. + +**Common layout names include:** +- `"grid-responsive"` - Responsive grid layout +- `"2x1"`, `"3x3"`, `"4x4"`, `"5x5"`, `"6x6"` - Fixed grid layouts +- `"1x1"` - Single participant layout +- `"2x2"` - Four participant grid + +## Important Notes + +**Layout Availability:** The list of available layouts depends on your server configuration and call settings. Common layouts include: + +| Layout Name | Description | +|-------------|-------------| +| `grid-responsive` | Automatically adjusts grid based on number of participants | +| `1x1` | Single participant (full screen) | +| `2x1` | Two participants side by side | +| `2x2` | Four participants in a 2x2 grid | +| `3x3` | Nine participants in a 3x3 grid | +| `4x4` | Sixteen participants in a 4x4 grid | +| `5x5` | Twenty-five participants in a 5x5 grid | +| `6x6` | Thirty-six participants in a 6x6 grid | + +**Tip:** Use `grid-responsive` for most use cases as it automatically adapts to the number of participants. + +**Layout vs Position:** +- **Layout** - The overall arrangement template (e.g., `3x3` grid) +- **Position** - Where a specific participant appears within that layout + +```typescript +// Set the layout template +await call.setLayout({ name: '3x3' }) + +// Optionally set specific member positions within the layout +await call.setPositions({ + positions: { + [memberId1]: { x: 0, y: 0 }, + [memberId2]: { x: 1, y: 0 } + } +}) +``` + +## Events + +While `getLayouts()` doesn't emit events, layout changes emit: + +- `layout.changed` - Fired when the call layout changes + +```typescript +call.on('layout.changed', (event) => { + console.log('New layout:', event.layout.name) + console.log('Layout layers:', event.layout.layers) +}) +``` + +## Examples + +### Get available layouts + +```typescript +const { layouts } = await call.getLayouts() +console.log('Available layouts:', layouts) +// Example output: ["grid-responsive", "2x2", "3x3", "4x4", "6x6"] +``` + +### Choose and set a layout + +```typescript +const { layouts } = await call.getLayouts() + +if (layouts.includes('grid-responsive')) { + await call.setLayout({ name: 'grid-responsive' }) + console.log('Layout set to grid-responsive') +} +``` + +### Display layout options to user + +```typescript +async function showLayoutOptions() { + const { layouts } = await call.getLayouts() + + const select = document.getElementById('layout-select') + + layouts.forEach(layout => { + const option = document.createElement('option') + option.value = layout + option.textContent = layout + select.appendChild(option) + }) + + select.addEventListener('change', async (e) => { + const selectedLayout = e.target.value + await call.setLayout({ name: selectedLayout }) + }) +} + +showLayoutOptions() +``` + +### Validate layout before setting + +```typescript +async function setLayoutSafely(layoutName: string) { + const { layouts } = await call.getLayouts() + + if (layouts.includes(layoutName)) { + await call.setLayout({ name: layoutName }) + console.log(`Successfully set layout to: ${layoutName}`) + } else { + console.error(`Layout "${layoutName}" is not available`) + console.log('Available layouts:', layouts) + } +} + +setLayoutSafely('4x4') +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/get-member-overlay.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/get-member-overlay.mdx new file mode 100644 index 00000000..d5173e9c --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/get-member-overlay.mdx @@ -0,0 +1,90 @@ +--- +title: getMemberOverlay +slug: /call-session/getmemberoverlay +--- + +import APIField from '@site/src/components/APIField'; + +Returns the member overlay element on top of the root element for a specific member. + +## Signature + +```typescript +getMemberOverlay(memberId: string): UserOverlay | undefined +``` + +## Parameters + + + The ID of the member whose overlay to retrieve. + + +## Returns + +**Type:** `UserOverlay | undefined` + +Returns the overlay object for the specified member, or `undefined` if not found. + +## Important Notes + +**Root Element Required:** + +Member overlays are only available when a `rootElement` is provided during call setup: + +```typescript +const call = await client.dial({ + to: 'user@example.com', + rootElement: document.getElementById('video-container') +}) + +// Now overlays are available +const overlay = call.getMemberOverlay(memberId) +``` + +**Overlay Availability:** + +Overlays are created when members join and have active video. If a member doesn't have an overlay, `getMemberOverlay()` returns `undefined`. + +**Use Cases:** + +- Custom UI elements on top of member video +- Adding badges or indicators to member tiles +- Implementing custom controls per member +- Accessing DOM elements for advanced manipulation + +## Examples + +### Get member overlay + +```typescript +const overlay = call.getMemberOverlay('member-123') +if (overlay) { + console.log('Found overlay for member') +} +``` + +### Manipulate member overlay + +```typescript +const { members } = await call.getMembers() + +members.forEach(member => { + const overlay = call.getMemberOverlay(member.id) + if (overlay) { + // Access or manipulate the overlay element + console.log(`Overlay for ${member.name}:`, overlay) + } +}) +``` + +### Check if member has overlay + +```typescript +function hasMemberOverlay(memberId: string): boolean { + return call.getMemberOverlay(memberId) !== undefined +} + +if (hasMemberOverlay('member-123')) { + console.log('Member has an active overlay') +} +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/get-members.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/get-members.mdx new file mode 100644 index 00000000..efc1af36 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/get-members.mdx @@ -0,0 +1,148 @@ +--- +title: getMembers +slug: /call-session/getmembers +--- + +Retrieves a list of all participants currently in the call. + +## Signature + +```typescript +getMembers(): Promise<{ members: VideoMemberEntity[] }> +``` + +## Parameters + +None + +## Returns + +**Type:** `Promise<{ members: VideoMemberEntity[] }>` + +An object containing an array of member objects with properties including: +- `id` - Unique member ID +- `name` - Member display name +- `audioMuted` - Whether audio is muted +- `videoMuted` - Whether video is muted +- `deaf` - Whether incoming audio is muted +- `talking` - Whether member is currently talking +- `handraised` - Whether hand is raised +- `visible` - Whether member is visible in layout +- `inputVolume`, `outputVolume`, `inputSensitivity` - Audio settings +- And more + +## Important Notes + +**Real-time Updates:** `getMembers()` returns a snapshot of members at the time it's called. For real-time updates, listen to member events: + +```typescript +call.on('member.joined', (event) => { + console.log('Member joined:', event.member.name) +}) + +call.on('member.left', (event) => { + console.log('Member left:', event.member.id) +}) + +call.on('member.updated', (event) => { + console.log('Member updated:', event.member.name) +}) +``` + +**Getting Your Own Member:** Your own member is included in the members array: + +```typescript +const { members } = await call.getMembers() +const me = members.find(m => m.id === call.memberId) + +if (me) { + console.log('My name:', me.name) + console.log('Am I muted?', me.audioMuted) +} +``` + +## Events + +Several events involve member information: +- `member.joined` - New member joined the call +- `member.left` - Member left the call +- `member.updated` - Member state changed +- `member.updated.audioMuted` - Audio mute state changed +- `member.updated.videoMuted` - Video mute state changed +- `member.talking` - Member started or stopped talking +- `memberList.updated` - Batch update of multiple members + +## Examples + +### Get all members + +```typescript +const { members } = await call.getMembers() +console.log(`There are ${members.length} participants in the call`) + +members.forEach(member => { + console.log(`Member: ${member.name} (ID: ${member.id})`) +}) +``` + +### Find a specific member + +```typescript +const { members} = await call.getMembers() + +const john = members.find(m => m.name === 'John Doe') + +if (john) { + console.log('John is in the call') + console.log('Audio muted:', john.audioMuted) + console.log('Video muted:', john.videoMuted) +} +``` + +### Control another member + +```typescript +const { members } = await call.getMembers() +const targetMember = members.find(m => m.name === 'Jane Smith') + +if (targetMember) { + await call.audioMute({ memberId: targetMember.id }) + console.log(`Muted ${targetMember.name}`) +} +``` + +### Filter members by state + +```typescript +const { members } = await call.getMembers() + +// Find who's talking +const talking = members.filter(m => m.talking) +console.log('Currently talking:', talking.map(m => m.name)) + +// Find who has video muted +const videoMuted = members.filter(m => m.videoMuted) +console.log('Video muted:', videoMuted.map(m => m.name)) + +// Find who has hand raised +const handsRaised = members.filter(m => m.handraised) +console.log('Hands raised:', handsRaised.map(m => m.name)) +``` + +### Track member count + +```typescript +async function updateMemberCount() { + const { members } = await call.getMembers() + const count = members.length + + document.getElementById('member-count').textContent = `${count} participant${count !== 1 ? 's' : ''}` +} + +// Update on member changes +call.on('member.joined', updateMemberCount) +call.on('member.left', updateMemberCount) + +// Initial update +updateMemberCount() +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/hangup.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/hangup.mdx new file mode 100644 index 00000000..4809b952 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/hangup.mdx @@ -0,0 +1,121 @@ +--- +title: hangup +slug: /call-session/hangup +--- + +import APIField from '@site/src/components/APIField'; + +Ends the call and disconnects the WebRTC connection. + +## Signature + +```typescript +hangup(id?: string): Promise +``` + +## Parameters + + + RTC Peer ID to disconnect. If provided, only that specific peer is disconnected; otherwise, all peers are destroyed. + + +## Returns + +**Type:** `Promise` + +## Important Notes + +**Automatic Cleanup:** When you call `hangup()`, the SDK automatically: +1. Ends all active screen shares in the call +2. Sends a disconnect signal to the server +3. Closes the WebRTC peer connection +4. Sets the call state to `hangup` +5. Emits the `destroy` event when cleanup is complete + +**Lifecycle Events:** The hangup process triggers these events in order: +1. **`hangup`** - Call state changed to hangup +2. **`destroy`** - Call has been fully cleaned up + +```typescript +call.on('hangup', (callSession) => { + console.log('Call is hanging up') +}) + +call.on('destroy', () => { + console.log('Call destroyed - resources released') + // Safe to navigate away or start a new call +}) + +await call.hangup() +``` + +**leave() vs hangup():** Both methods do the same thing - `leave()` is simply an alias that calls `hangup()` internally: + +```typescript +// These are functionally identical +await call.hangup() +await call.leave() +``` + +Use whichever name makes more sense in your context: +- `hangup()` - Traditional telephony terminology +- `leave()` - Modern "leaving a room" terminology + +**Optional ID Parameter:** The optional `id` parameter is for advanced scenarios where you need to disconnect a specific RTC peer. Most users should call `hangup()` without any parameters to end the entire call session. + +## Events + +When you call `hangup()`, the following events are emitted: +- `hangup` - Call state changed to hangup (emits the CallSession object) +- `destroy` - Call has been fully destroyed and cleaned up +- Connection state transitions: `disconnecting`, `disconnected` + +## Examples + +### Basic usage + +```typescript +const call = await client.dial({ to: 'user@example.com' }) + +// Later, when you want to end the call +await call.hangup() +console.log('Call ended') +``` + +### With event listeners + +```typescript +call.on('hangup', () => { + console.log('Hangup state entered') +}) + +call.on('destroy', () => { + console.log('Call fully cleaned up') +}) + +await call.hangup() +``` + +### Using leave() alias + +```typescript +// These are equivalent +await call.hangup() +await call.leave() +``` + +### Error handling + +```typescript +try { + await call.hangup() + console.log('Call ended successfully') +} catch (error) { + console.error('Error hanging up call:', error) +} + +// Always listen for the destroy event +call.on('destroy', () => { + console.log('Call resources released') +}) +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/hold.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/hold.mdx new file mode 100644 index 00000000..ed3462f1 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/hold.mdx @@ -0,0 +1,108 @@ +--- +title: hold +slug: /call-session/hold +--- + +Puts the call on hold by stopping outbound audio and video from the local participant. + +## Signature + +```typescript +call.hold(): Promise +``` + +## Parameters + +None + +## Returns + +**Type:** `Promise` + +## Examples + +### Basic Hold + +```typescript +await call.hold() +console.log('Call is now on hold') +``` + +### Hold/Unhold Toggle + +```typescript +let isOnHold = false + +async function toggleHold() { + if (isOnHold) { + await call.unhold() + console.log('Call resumed') + } else { + await call.hold() + console.log('Call on hold') + } + isOnHold = !isOnHold +} + +await toggleHold() +``` + +### Hold with UI Feedback + +```typescript +const holdButton = document.getElementById('hold-button') + +holdButton.addEventListener('click', async () => { + try { + await call.hold() + holdButton.textContent = 'Resume' + holdButton.classList.add('on-hold') + } catch (error) { + console.error('Failed to hold call:', error) + } +}) +``` + +### Multi-Call Management + +```typescript +class CallManager { + constructor() { + this.calls = [] + } + + async switchToCall(targetCall) { + // Hold all other calls + for (const call of this.calls) { + if (call !== targetCall) { + await call.hold() + } + } + + // Unhold the target call + await targetCall.unhold() + } +} +``` + +## Important Notes + +**What Happens on Hold:** +- Your outbound audio and video streams are stopped +- You continue to receive audio and video from other participants +- The WebRTC connection remains active +- Other participants will see/hear that you're on hold (muted) + +**Hold vs Mute:** +- **Hold**: Stops both audio AND video outbound streams +- **Mute**: Stops only audio (`audioMute()`) or only video (`videoMute()`) + +Use `hold()` when you want to completely pause your participation in the call but stay connected. + +**Still Connected:** While on hold, you remain connected to the call: +- You can still receive audio/video from others +- You can still listen to the conversation +- Other participants remain in the call +- You can use `unhold()` to resume + +**Not a Pause:** Hold does not pause the call for other participants. They continue their conversation - you just stop sending audio/video to them. diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/leave.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/leave.mdx new file mode 100644 index 00000000..fcff2be9 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/leave.mdx @@ -0,0 +1,83 @@ +--- +title: leave +slug: /call-session/leave +--- + +Leaves the call and disconnects from the WebRTC session. This is an alias for `hangup()`. + +## Signature + +```typescript +leave(): Promise +``` + +## Parameters + +None + +## Returns + +**Type:** `Promise` + +## Important Notes + +**Alias for hangup():** + +`leave()` is functionally identical to `hangup()`. Both methods: +- End the call for the local participant +- Disconnect the WebRTC connection +- Clean up all resources +- Trigger the `destroy` event + +```typescript +// These are equivalent +await call.leave() +await call.hangup() +``` + +**Cleanup:** + +Always listen to the `destroy` event to know when cleanup is complete: + +```typescript +call.on('destroy', () => { + console.log('Call resources cleaned up') + // Perform any additional cleanup +}) + +await call.leave() +``` + +## Examples + +### Basic usage + +```typescript +// Leave the call +await call.leave() +console.log('Left the call') +``` + +### With cleanup listener + +```typescript +call.on('destroy', () => { + console.log('Call has been cleaned up') +}) + +await call.leave() +``` + +### Leave and navigate away + +```typescript +async function endCallAndRedirect() { + try { + await call.leave() + console.log('Successfully left the call') + window.location.href = '/dashboard' + } catch (error) { + console.error('Failed to leave call:', error) + } +} +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/lock.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/lock.mdx new file mode 100644 index 00000000..bbdc4c02 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/lock.mdx @@ -0,0 +1,108 @@ +--- +title: lock +slug: /call-session/lock +--- + +Locks the call to prevent new participants from joining. + +## Signature + +```typescript +lock(): Promise +``` + +## Parameters + +None + +## Returns + +**Type:** `Promise` + +## Important Notes + +**Lock Behavior:** When a call is locked: +- New participants **cannot** join the call +- Existing participants remain in the call +- Participants who left cannot rejoin until the call is unlocked +- The lock state persists until explicitly unlocked + +**Lock does NOT affect:** +- Existing participants (they stay in the call) +- Ability to invite existing members back +- Screen sharing or other call features +- Media streams or audio/video + +## Events + +Lock/unlock operations may trigger room update events: + +```typescript +call.on('room.updated', (event) => { + console.log('Room updated, locked state may have changed') +}) +``` + +## Examples + +### Basic usage + +```typescript +await call.lock() +console.log('Call is now locked') +``` + +### Lock after meeting starts + +```typescript +async function startMeeting() { + const { members } = await call.getMembers() + + if (members.length >= expectedParticipantCount) { + await call.lock() + console.log('Meeting started - call is now locked') + } +} +``` + +### UI toggle button + +```typescript +let isLocked = false + +async function toggleLock() { + try { + if (isLocked) { + await call.unlock() + isLocked = false + console.log('Call unlocked') + } else { + await call.lock() + isLocked = true + console.log('Call locked') + } + + updateLockButtonUI(isLocked) + } catch (error) { + console.error('Failed to toggle lock:', error) + } +} +``` + +### Auto-lock after grace period + +```typescript +const GRACE_PERIOD_MS = 5 * 60 * 1000 // 5 minutes + +call.on('member.joined', async (event) => { + const { members } = await call.getMembers() + + if (members.length === 1) { + // First participant joined, start grace period + setTimeout(async () => { + await call.lock() + console.log('Grace period ended - call locked') + }, GRACE_PERIOD_MS) + } +}) +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/off.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/off.mdx new file mode 100644 index 00000000..c68a305c --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/off.mdx @@ -0,0 +1,131 @@ +--- +title: off +slug: /call-session/off +--- + +import APIField from '@site/src/components/APIField'; + +Removes an event listener that was previously registered with `on()` or `once()`. + +## Signature + +```typescript +call.off( + event: T, + handler?: EventListener +): CallSession +``` + +## Parameters + + + The event name to remove listeners from. + + + + The specific handler function to remove. If not provided, all listeners for this event are removed. + + +## Returns + +**Type:** `CallSession` + +Returns the CallSession instance for method chaining. + +## Important Notes + +**Handler Reference Required:** To remove a specific handler, you must pass the exact same function reference that was used with `on()`: + +```typescript +// ❌ This won't work - different function instances +call.on('member.joined', (params) => console.log(params)) +call.off('member.joined', (params) => console.log(params)) // Different function! + +// ✅ This works - same function reference +const handler = (params) => console.log(params) +call.on('member.joined', handler) +call.off('member.joined', handler) +``` + +**Remove All Listeners:** Calling `off()` without a handler removes ALL listeners for that event: + +```typescript +call.off('member.joined') // Removes ALL member.joined listeners +``` + +**No Error on Missing Handler:** Calling `off()` with a handler that was never registered (or already removed) does nothing and throws no error. + +**Automatic Cleanup:** When a CallSession is destroyed, all event listeners are automatically cleaned up. You don't need to manually call `off()` before calling `hangup()`. + +## Examples + +### Remove Specific Handler + +```typescript +const memberJoinedHandler = (params) => { + console.log('Member joined:', params.member.name) +} + +// Register the handler +call.on('member.joined', memberJoinedHandler) + +// Later, remove the specific handler +call.off('member.joined', memberJoinedHandler) +``` + +### Remove All Handlers for an Event + +```typescript +// Register multiple handlers +call.on('member.joined', handler1) +call.on('member.joined', handler2) +call.on('member.joined', handler3) + +// Remove ALL member.joined handlers +call.off('member.joined') +``` + +### Cleanup Pattern + +```typescript +function setupCallHandlers(call) { + const handlers = { + onMemberJoined: (params) => { + console.log('Member joined:', params.member.name) + }, + onMemberLeft: (params) => { + console.log('Member left:', params.member.name) + }, + onLayoutChanged: (params) => { + console.log('Layout changed:', params.layout.name) + } + } + + // Register handlers + call.on('member.joined', handlers.onMemberJoined) + call.on('member.left', handlers.onMemberLeft) + call.on('layout.changed', handlers.onLayoutChanged) + + // Return cleanup function + return () => { + call.off('member.joined', handlers.onMemberJoined) + call.off('member.left', handlers.onMemberLeft) + call.off('layout.changed', handlers.onLayoutChanged) + } +} + +// Usage +const cleanup = setupCallHandlers(call) + +// Later, when done +cleanup() +``` + +### Method Chaining + +```typescript +call + .off('member.joined', handler1) + .off('member.left', handler2) + .off('layout.changed', handler3) +``` \ No newline at end of file diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/on.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/on.mdx new file mode 100644 index 00000000..ca9392fe --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/on.mdx @@ -0,0 +1,125 @@ +--- +title: on +slug: /call-session/on +--- + +import APIField from '@site/src/components/APIField'; + +Registers an event listener for the specified event. The listener will be called every time the event is emitted. + +## Signature + +```typescript +call.on( + event: T, + handler: EventListener +): CallSession +``` + +## Parameters + + + The event name to listen for. + + + + Callback function to execute when the event fires. + + +## Returns + +**Type:** `CallSession` + +Returns the CallSession instance for method chaining. + +## Important Notes + +**Multiple Listeners:** You can register multiple listeners for the same event. They will all be called in the order they were registered. + +```typescript +call.on('member.joined', (params) => { + console.log('First handler') +}) + +call.on('member.joined', (params) => { + console.log('Second handler') +}) +// Both handlers will be called when member.joined fires +``` + +**Removing Listeners:** To remove a specific listener, you must keep a reference to the handler function and use `off()`: + +```typescript +const handler = (params) => { + console.log('Member joined:', params.member.name) +} + +call.on('member.joined', handler) + +// Later, remove the listener +call.off('member.joined', handler) +``` + +**Listener Lifecycle:** Event listeners remain active until either: +- You explicitly remove them with `off()` +- The CallSession is destroyed + +**One-Time Listeners:** If you need a listener that only fires once, use `once()` instead. + +## Examples + +### Basic Event Listener + +```typescript +call.on('call.joined', (params) => { + console.log('Call joined:', params.call_id) + console.log('Member ID:', params.member_id) +}) +``` + +### Member Events + +```typescript +call.on('member.joined', (params) => { + console.log('Member joined:', params.member.name) +}) + +call.on('member.left', (params) => { + console.log('Member left:', params.member.name) +}) +``` + +### Connection State Events + +```typescript +call.on('connected', (callSession) => { + console.log('Call connected') +}) + +call.on('disconnected', (callSession) => { + console.log('Call disconnected') +}) +``` + +### Layout Events + +```typescript +call.on('layout.changed', (params) => { + console.log('Layout changed to:', params.layout.name) +}) +``` + +### Method Chaining + +```typescript +call + .on('call.joined', (params) => { + console.log('Joined call:', params.call_id) + }) + .on('member.joined', (params) => { + console.log('Member joined:', params.member.name) + }) + .on('layout.changed', (params) => { + console.log('Layout changed:', params.layout.name) + }) +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/once.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/once.mdx new file mode 100644 index 00000000..444d7a84 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/once.mdx @@ -0,0 +1,165 @@ +--- +title: once +slug: /call-session/once +--- + +import APIField from '@site/src/components/APIField'; + +Registers a one-time event listener for the specified event. The listener will be automatically removed after it fires once. + +## Signature + +```typescript +call.once( + event: T, + handler: EventListener +): CallSession +``` + +## Parameters + + + The event name to listen for. + + + + Callback function to execute when the event fires (will only fire once). + + +## Returns + +**Type:** `CallSession` + +Returns the CallSession instance for method chaining. + +## Important Notes + +**Automatic Removal:** The listener is automatically removed after it fires once. You don't need to call `off()`. + +**Can Be Removed Early:** You can still manually remove a `once()` listener before it fires by using `off()`: + +```typescript +const handler = (params) => { + console.log('This might never run') +} + +call.once('member.joined', handler) + +// Changed your mind? Remove it before it fires +call.off('member.joined', handler) +``` + +**Multiple once() Listeners:** You can register multiple `once()` listeners for the same event. They will all fire once and then be removed: + +```typescript +call.once('call.joined', (params) => { + console.log('First handler') +}) + +call.once('call.joined', (params) => { + console.log('Second handler') +}) + +// Both will fire once when call.joined occurs, then both are removed +``` + +**Initialization Pattern:** Commonly used to detect initial state or setup: + +```typescript +call.once('call.joined', (params) => { + // Initialize UI with call details + initializeCallUI(params) +}) + +call.once('layout.changed', (params) => { + // Set initial layout in UI + setInitialLayout(params.layout) +}) +``` + +## Examples + +### Basic One-Time Listener + +```typescript +call.once('call.joined', (params) => { + console.log('Call joined (this will only log once):', params.call_id) +}) +``` + +### Wait for First Member to Join + +```typescript +call.once('member.joined', (params) => { + console.log('First member joined:', params.member.name) + // This will NOT fire again when other members join +}) +``` + +### Promise-Based Pattern + +```typescript +function waitForCallJoined(call) { + return new Promise((resolve) => { + call.once('call.joined', (params) => { + resolve(params) + }) + }) +} + +// Usage +const params = await waitForCallJoined(call) +console.log('Call joined:', params.call_id) +``` + +### Combining with Regular Listeners + +```typescript +// First member only +call.once('member.joined', (params) => { + console.log('First member:', params.member.name) +}) + +// All members +call.on('member.joined', (params) => { + console.log('Any member:', params.member.name) +}) + +// When a member joins, BOTH handlers fire +// But the once() handler is then removed +``` + +### Method Chaining + +```typescript +call + .once('call.joined', (params) => { + console.log('Joined:', params.call_id) + }) + .once('layout.changed', (params) => { + console.log('First layout:', params.layout.name) + }) + .on('member.joined', (params) => { + console.log('Member joined:', params.member.name) + }) +``` + +### Async/Await Pattern + +```typescript +async function handleCall(call) { + // Wait for call to be joined + await new Promise((resolve) => { + call.once('call.joined', resolve) + }) + + console.log('Call is now active') + + // Wait for first member + const memberParams = await new Promise((resolve) => { + call.once('member.joined', resolve) + }) + + console.log('First member joined:', memberParams.member.name) +} +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/remove-all-listeners.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/remove-all-listeners.mdx new file mode 100644 index 00000000..ac86a65c --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/remove-all-listeners.mdx @@ -0,0 +1,137 @@ +--- +title: removeAllListeners +slug: /call-session/removealllisteners +--- + +import APIField from '@site/src/components/APIField'; + +Removes all event listeners, either for a specific event or for all events. + +## Signature + +```typescript +call.removeAllListeners( + event?: T +): CallSession +``` + +## Parameters + + + The event name to remove all listeners from. If not provided, removes ALL listeners from ALL events. + + +## Returns + +**Type:** `CallSession` + +Returns the CallSession instance for method chaining. + +## Important Notes + +**Nuclear Option:** Calling `removeAllListeners()` without parameters removes ALL listeners from ALL events. Use with caution. + +```typescript +call.removeAllListeners() // Removes EVERYTHING +``` + +**Event-Specific Cleanup:** To remove all listeners for just one event, pass the event name: + +```typescript +call.removeAllListeners('member.joined') // Only removes member.joined listeners +``` + +**Automatic Cleanup:** When a CallSession is destroyed (via `hangup()`), all listeners are automatically cleaned up. You typically don't need to call this manually before ending a call. + +**No Error on Empty:** Calling `removeAllListeners()` when there are no listeners does nothing and throws no error. + +**Prefer Specific Cleanup:** For better code maintainability, prefer using `off()` with specific handler references when possible: + +```typescript +// ✅ Better - explicit and targeted +call.off('member.joined', specificHandler) + +// ⚠️ Use sparingly - removes all handlers +call.removeAllListeners('member.joined') + +// ❌ Use very carefully - removes everything +call.removeAllListeners() +``` + +## Examples + +### Remove All Listeners for a Specific Event + +```typescript +// Register multiple handlers +call.on('member.joined', handler1) +call.on('member.joined', handler2) +call.once('member.joined', handler3) + +// Remove all member.joined listeners +call.removeAllListeners('member.joined') +``` + +### Remove All Listeners from All Events + +```typescript +// Register various handlers +call.on('member.joined', handler1) +call.on('member.left', handler2) +call.on('layout.changed', handler3) +call.on('call.state', handler4) + +// Remove ALL listeners from ALL events +call.removeAllListeners() +``` + +### Cleanup Pattern + +```typescript +function attachCallListeners(call) { + call.on('member.joined', (params) => { + console.log('Member joined:', params.member.name) + }) + + call.on('member.left', (params) => { + console.log('Member left:', params.member.name) + }) + + call.on('layout.changed', (params) => { + console.log('Layout changed:', params.layout.name) + }) + + // Return cleanup function + return () => { + call.removeAllListeners() // Clean up everything + } +} + +// Usage +const cleanup = attachCallListeners(call) + +// Later, when done +cleanup() +``` + +### Component Unmount Cleanup + +```typescript +class CallComponent { + constructor(call) { + this.call = call + this.setupListeners() + } + + setupListeners() { + this.call.on('member.joined', this.onMemberJoined) + this.call.on('member.left', this.onMemberLeft) + this.call.on('layout.changed', this.onLayoutChanged) + } + + unmount() { + // Clean up all listeners when component is destroyed + this.call.removeAllListeners() + } +} +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/remove-member.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/remove-member.mdx new file mode 100644 index 00000000..87e7eee0 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/remove-member.mdx @@ -0,0 +1,128 @@ +--- +title: removeMember +slug: /call-session/removemember +--- + +import APIField from '@site/src/components/APIField'; + +Removes a specific participant from the call. + +## Signature + +```typescript +removeMember(params: Required): Promise +``` + +## Parameters + + + Parameters for the remove operation. + + + + ID of the member to remove. + + +## Returns + +**Type:** `Promise` + +## Important Notes + +**Permissions Required:** Requires `call.capabilities.member.remove` capability to remove other participants. + +**Cannot Remove Yourself:** You cannot use `removeMember()` to remove yourself from the call. Use `hangup()` or `leave()` instead: + +```typescript +// ❌ Wrong - will fail +await call.removeMember({ memberId: call.memberId }) + +// ✅ Correct - use hangup() to leave +await call.hangup() +``` + +**Member Left Event:** When a member is removed, the `member.left` event is emitted: + +```typescript +call.on('member.left', (event) => { + console.log(`Member ${event.member.id} left the call`) +}) + +await call.removeMember({ memberId: 'member-id' }) +``` + +## Events + +When you call `removeMember()`, the following event is emitted: + +- `member.left` - Fired when the member is removed from the call + +```typescript +call.on('member.left', (event) => { + console.log('Member left:', event.member.id) + console.log('Member name:', event.member.name) +}) +``` + +## Examples + +### Remove a specific member + +```typescript +const { members } = await call.getMembers() +const memberToRemove = members.find(m => m.name === 'Disruptive User') + +if (memberToRemove) { + await call.removeMember({ memberId: memberToRemove.id }) + console.log(`Removed ${memberToRemove.name} from the call`) +} +``` + +### Remove with member ID + +```typescript +const memberId = 'de550c0c-3fac-4efd-b06f-b5b8614b8966' + +try { + await call.removeMember({ memberId }) + console.log('Member removed successfully') +} catch (error) { + console.error('Failed to remove member:', error) +} +``` + +### Remove with confirmation + +```typescript +async function removeWithConfirmation(memberId: string, memberName: string) { + const confirmed = confirm(`Are you sure you want to remove ${memberName}?`) + + if (confirmed) { + await call.removeMember({ memberId }) + console.log(`${memberName} has been removed`) + } +} + +const { members } = await call.getMembers() +const targetMember = members.find(m => m.name === 'John Doe') + +if (targetMember) { + await removeWithConfirmation(targetMember.id, targetMember.name) +} +``` + +### Check permissions before removing + +```typescript +const { members } = await call.getMembers() +const memberToRemove = members.find(m => m.name === 'Problem User') + +if (memberToRemove) { + if (call.capabilities?.member.remove) { + await call.removeMember({ memberId: memberToRemove.id }) + console.log('Member removed') + } else { + console.log('You do not have permission to remove members') + } +} +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/send-digits.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/send-digits.mdx new file mode 100644 index 00000000..cb0611d0 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/send-digits.mdx @@ -0,0 +1,101 @@ +--- +title: sendDigits +slug: /call-session/senddigits +--- + +import APIField from '@site/src/components/APIField'; + +Sends DTMF (Dual-Tone Multi-Frequency) tones during an active call. + +## Signature + +```typescript +call.sendDigits(dtmf: string): Promise +``` + +## Parameters + + + The DTMF digits to send. Valid characters: `0-9`, `A-D`, `*`, `#` + + +## Returns + +**Type:** `Promise` + +## DTMF Reference + +DTMF tones are generated by combining two frequencies: + +| Digit | Description | +|-------|-------------| +| 0-9 | Standard numeric digits | +| * | Star/asterisk key | +| # | Pound/hash key | +| A-D | Extended DTMF tones (rarely used in telephony) | + +## Important Notes + +**Valid Characters:** DTMF tones support the following characters: +- Digits: `0`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`, `9` +- Special: `*`, `#` +- Letters (rarely used): `A`, `B`, `C`, `D` + +**Timing:** DTMF tones are sent with standard timing intervals. If you need to send multiple digits with delays between them, add explicit delays using `setTimeout()` or similar timing mechanisms. + +**Active Call Required:** DTMF tones can only be sent during an active call. Attempting to send digits before the call is connected or after it has ended will fail. + +**Common Use Cases:** +- Navigating IVR (Interactive Voice Response) menus +- Entering PINs or passwords +- Accessing voicemail +- Controlling call forwarding or conferencing features +- Remote control of phone systems + +**No Visual Feedback:** This method sends tones but does not provide visual feedback. If you want to show digit input to the user, you must implement that separately in your UI. + +## Examples + +### Send Single Digit + +```typescript +await call.sendDigits('1') +``` + +### Send Multiple Digits + +```typescript +// Send a sequence of digits +await call.sendDigits('12345') +``` + +### Interactive Menu Navigation + +```typescript +// Navigate an IVR (Interactive Voice Response) menu +await call.sendDigits('1') // Press 1 for English +await new Promise(resolve => setTimeout(resolve, 2000)) // Wait 2 seconds +await call.sendDigits('2') // Press 2 for sales +``` + +### Complete IVR Flow + +```typescript +async function navigateIVR(call) { + // Wait for greeting + await new Promise(resolve => setTimeout(resolve, 3000)) + + // Select language + await call.sendDigits('1') + await new Promise(resolve => setTimeout(resolve, 1000)) + + // Select department + await call.sendDigits('2') + await new Promise(resolve => setTimeout(resolve, 1000)) + + // Enter account number + await call.sendDigits('123456789#') +} + +await navigateIVR(call) +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-audio-direction.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-audio-direction.mdx new file mode 100644 index 00000000..e863678c --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-audio-direction.mdx @@ -0,0 +1,110 @@ +--- +title: setAudioDirection +slug: /call-session/setaudiodirection +--- + +import APIField from '@site/src/components/APIField'; + +Sets the direction of the audio track in the WebRTC connection by performing RTC peer renegotiation. + +## Signature + +```typescript +call.setAudioDirection(direction: UpdateMediaDirection): Promise +``` + +## Parameters + + + The desired audio direction. Valid values: `'sendrecv'`, `'sendonly'`, `'recvonly'`, `'inactive'` + + +**UpdateMediaDirection** values: +- `'sendrecv'` - Send and receive audio (default, two-way audio) +- `'sendonly'` - Only send audio, don't receive +- `'recvonly'` - Only receive audio, don't send +- `'inactive'` - Neither send nor receive audio + +## Returns + +**Type:** `Promise` + +## Important Notes + +**Peer Renegotiation:** This method triggers WebRTC peer renegotiation to update the audio track direction. The connection will temporarily adjust while renegotiating. + +**Direction vs Mute:** +- **Mute** (`audioMute()`): Temporarily stops sending audio, but the track remains active +- **Direction**: Changes the fundamental audio flow in the WebRTC connection + +Use `setAudioDirection()` when you need to: +- Permanently disable audio sending or receiving +- Optimize bandwidth by not receiving audio when unnecessary +- Implement broadcast or listen-only modes + +Use `audioMute()`/`audioUnmute()` when you just need to temporarily stop sending audio. + +**Video Unaffected:** This method only changes the audio direction. Video direction remains unchanged. To update both, use `updateMedia()`. + +## Examples + +### Enable Two-Way Audio + +```typescript +await call.setAudioDirection('sendrecv') +``` + +### Listen-Only Mode + +```typescript +// Receive audio but don't send +await call.setAudioDirection('recvonly') +``` + +### Broadcast Mode + +```typescript +// Send audio but don't receive +await call.setAudioDirection('sendonly') +``` + +### Disable Audio Completely + +```typescript +// Neither send nor receive audio +await call.setAudioDirection('inactive') +``` + +### Dynamic Audio Control + +```typescript +// Start in listen-only mode +await call.setAudioDirection('recvonly') + +// When user clicks "Speak" button, enable sending +button.addEventListener('click', async () => { + await call.setAudioDirection('sendrecv') + console.log('Audio now two-way') +}) +``` + +### Push-to-Talk + +```typescript +let isPushed = false + +// Default: listen only +await call.setAudioDirection('recvonly') + +// When button is pressed +button.addEventListener('mousedown', async () => { + isPushed = true + await call.setAudioDirection('sendrecv') +}) + +// When button is released +button.addEventListener('mouseup', async () => { + isPushed = false + await call.setAudioDirection('recvonly') +}) +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-audio-flags.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-audio-flags.mdx new file mode 100644 index 00000000..0c80b28d --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-audio-flags.mdx @@ -0,0 +1,166 @@ +--- +title: setAudioFlags +slug: /call-session/setaudioflags +--- + +import APIField from '@site/src/components/APIField'; + +Configures audio processing flags (echo cancellation, automatic gain control, noise suppression) for yourself or another participant. + +## Signature + +```typescript +setAudioFlags(params: SetAudioFlagsParams): Promise +``` + +## Parameters + + + Audio processing configuration. + + + + Enable/disable echo cancellation. + + + + Enable/disable automatic gain control (AGC). + + + + Enable/disable noise suppression. + + + + Member to control. Omit to control yourself. + + +## Returns + +**Type:** `Promise` + +## Important Notes + +**Audio Processing Features:** + +**Echo Cancellation** (`echoCancellation`) +- Removes echo and feedback from audio +- Essential for speakers/headphones that may cause audio feedback +- **Recommended:** Enable in most scenarios +- **Disable when:** Using headphones with no echo issues, or when audio quality degradation occurs + +**Automatic Gain Control** (`autoGain` / AGC) +- Automatically adjusts microphone volume to maintain consistent levels +- Normalizes quiet and loud speech +- **Recommended:** Enable for users with varying speech volume +- **Disable when:** Manual control of microphone volume is preferred, or in professional audio setups + +**Noise Suppression** (`noiseSuppression`) +- Filters out background noise (keyboard typing, traffic, etc.) +- Focuses on speech frequencies +- **Recommended:** Enable in noisy environments +- **Disable when:** In quiet environments or when high-fidelity audio is needed (music, etc.) + +**At Least One Flag Required:** You must specify at least one audio flag: + +```typescript +// ✅ Valid - at least one flag specified +await call.setAudioFlags({ echoCancellation: true }) + +// ❌ Invalid - no flags specified +await call.setAudioFlags({}) // Will fail + +// ✅ Valid - multiple flags +await call.setAudioFlags({ + echoCancellation: true, + noiseSuppression: false +}) +``` + +**Audio Quality vs Processing:** +- **More processing** (all enabled): Better noise/echo handling, may introduce slight audio latency, potential audio quality reduction in some cases +- **Less processing** (all disabled): Higher audio fidelity, lower latency, more susceptible to noise and echo + +**Real-time Updates:** +- Changes take effect immediately +- No need to restart audio streams +- Can be adjusted dynamically during the call + +## Examples + +### Enable all audio processing features + +```typescript +await call.setAudioFlags({ + echoCancellation: true, + autoGain: true, + noiseSuppression: true +}) +``` + +### Configure individual features + +```typescript +// Enable only echo cancellation +await call.setAudioFlags({ + echoCancellation: true +}) + +// Enable only noise suppression +await call.setAudioFlags({ + noiseSuppression: true +}) + +// Disable automatic gain control +await call.setAudioFlags({ + autoGain: false +}) +``` + +### Mix enabled and disabled features + +```typescript +await call.setAudioFlags({ + echoCancellation: true, + autoGain: false, + noiseSuppression: true +}) +``` + +### Configure another member's audio + +```typescript +await call.setAudioFlags({ + memberId: 'member-123', + autoGain: false +}) +``` + +### Environment-based audio configuration + +```typescript +async function configureAudioForEnvironment(environment: 'quiet' | 'noisy' | 'echoey') { + if (environment === 'quiet') { + // Minimal processing for quiet environments + await call.setAudioFlags({ + echoCancellation: false, + autoGain: false, + noiseSuppression: false + }) + } else if (environment === 'noisy') { + // Aggressive noise suppression for noisy environments + await call.setAudioFlags({ + echoCancellation: true, + autoGain: true, + noiseSuppression: true + }) + } else if (environment === 'echoey') { + // Focus on echo cancellation + await call.setAudioFlags({ + echoCancellation: true, + autoGain: false, + noiseSuppression: true + }) + } +} +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-input-sensitivity.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-input-sensitivity.mdx new file mode 100644 index 00000000..49bf8b98 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-input-sensitivity.mdx @@ -0,0 +1,162 @@ +--- +title: setInputSensitivity +slug: /call-session/setinputsensitivity +--- + +import APIField from '@site/src/components/APIField'; + +Adjusts the microphone sensitivity threshold for detecting when a participant is speaking. + +## Signature + +```typescript +setInputSensitivity(params: MemberCommandWithValueParams): Promise +``` + +## Parameters + + + Sensitivity configuration object. + + + + Sensitivity level from 0 to 100. Default is 30. + + + + Member to control. Omit to control your own microphone, provide specific ID to control another member. + + +## Returns + +**Type:** `Promise` + +## Important Notes + +**Sensitivity Scale (0 to 100):** + +| Value | Behavior | Use Case | +|-------|----------|----------| +| `0` | Lowest sensitivity (essentially muted) | Completely suppress voice detection | +| `1-20` | Very low sensitivity | Very noisy environments, only detect loud speech | +| `21-40` | Low to moderate sensitivity | Noisy environments, normal speech levels | +| `30` | **Default** | Balanced for most environments | +| `41-60` | Moderate to high sensitivity | Quiet environments | +| `61-80` | High sensitivity | Very quiet environments, soft-spoken users | +| `81-100` | Very high sensitivity | Detect very quiet speech, may pick up noise | + +**Sensitivity vs Volume:** + +| Setting | Controls | Purpose | +|---------|----------|---------| +| **Input Sensitivity** | Speech detection threshold | When the `member.talking` event fires | +| **Input Volume** | Audio loudness | How loud the microphone sounds to others | + +```typescript +// High sensitivity = detect quieter speech (lower threshold for talking detection) +await call.setInputSensitivity({ value: 80 }) + +// High volume = louder microphone output (others hear you louder) +await call.setInputVolume({ volume: 10 }) +``` + +**Effect on member.talking Event:** +- Sensitivity controls when the `member.talking` event is triggered +- **Higher sensitivity** = Quieter speech triggers the event +- **Lower sensitivity** = Only louder speech triggers the event +- Does NOT affect the actual audio transmitted to other participants + +```typescript +// With high sensitivity (80), even whispers may trigger talking event +await call.setInputSensitivity({ value: 80 }) + +call.on('member.talking', (event) => { + console.log(`Member ${event.member.id} is talking:`, event.member.talking) +}) +``` + +**Environment Considerations:** + +**Noisy Environments:** +- Use **lower sensitivity** (10-20) to avoid false positives +- Prevents background noise from triggering talking detection +- User must speak louder to be detected as talking + +**Quiet Environments:** +- Use **higher sensitivity** (60-80) to detect softer speech +- Allows natural conversation without raising voice +- May pick up background sounds as false positives + +**Real-time Adjustment:** +- Sensitivity changes take effect immediately +- No need to restart microphone or re-join call +- Can be adjusted dynamically based on detected noise levels + +## Examples + +### Adjust your own microphone sensitivity + +```typescript +// Increase sensitivity (detect quieter speech) +await call.setInputSensitivity({ value: 80 }) + +// Decrease sensitivity (only detect louder speech) +await call.setInputSensitivity({ value: 15 }) + +// Reset to default sensitivity +await call.setInputSensitivity({ value: 30 }) + +// Maximum sensitivity (most sensitive) +await call.setInputSensitivity({ value: 100 }) + +// Minimum sensitivity (essentially muted) +await call.setInputSensitivity({ value: 0 }) +``` + +### Adjust another member's microphone sensitivity + +```typescript +await call.setInputSensitivity({ + memberId: 'member-123', + value: 80 +}) +``` + +### Sensitivity control UI + +```typescript +// HTML: + +const sensitivitySlider = document.getElementById('micSensitivity') as HTMLInputElement + +sensitivitySlider.addEventListener('change', async (e) => { + const value = parseInt(e.target.value) + + try { + await call.setInputSensitivity({ value }) + console.log(`Microphone sensitivity set to ${value}`) + } catch (error) { + console.error('Failed to set sensitivity:', error) + } +}) +``` + +### Automatic sensitivity based on environment + +```typescript +async function setOptimalSensitivity() { + const noiseLevel = await detectEnvironmentNoise() + + let sensitivity: number + + if (noiseLevel === 'quiet') { + sensitivity = 80 // High sensitivity for quiet environments + } else if (noiseLevel === 'moderate') { + sensitivity = 30 // Default sensitivity + } else { + sensitivity = 10 // Low sensitivity for noisy environments + } + + await call.setInputSensitivity({ value: sensitivity }) +} +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-input-volume.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-input-volume.mdx new file mode 100644 index 00000000..923209d7 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-input-volume.mdx @@ -0,0 +1,137 @@ +--- +title: setInputVolume +slug: /call-session/setinputvolume +--- + +import APIField from '@site/src/components/APIField'; + +Adjusts the microphone (input) volume level for yourself or another participant. + +## Signature + +```typescript +setInputVolume(params: MemberCommandWithVolumeParams): Promise +``` + +## Parameters + + + Volume configuration object. + + + + Volume level in decibels (dB). Typically ranges from -50 to 50. + + + + Member to control. Omit to control your own microphone, provide specific ID to control another member. + + +## Returns + +**Type:** `Promise` + +## Important Notes + +**Volume Range:** +- Volume is specified in **decibels (dB)** +- Typical range: **-50dB to +50dB** +- `0dB` = default/neutral volume (no adjustment) +- Negative values = reduce volume +- Positive values = increase volume +- Extreme values may cause audio distortion or clipping + +**Recommended ranges:** +- For reduction: `-20dB to 0dB` +- For boost: `0dB to +20dB` + +**Input vs Output Volume:** +- **setInputVolume()** - Adjusts the microphone/input volume (how loud the member sounds to others) +- **setOutputVolume()** - Adjusts the speaker/output volume (how loud others sound to the member) + +```typescript +// Make your microphone quieter (others hear you softer) +await call.setInputVolume({ volume: -10 }) + +// Make others quieter for you (you hear them softer) +await call.setOutputVolume({ volume: -10 }) +``` + +**Volume Persistence:** +- Volume settings persist for the duration of the call +- Settings do not carry over to new calls +- Each member's volume can be adjusted independently + +**Real-time Adjustment:** +- Volume changes take effect immediately +- No need to mute/unmute or restart audio streams +- Can be called multiple times to fine-tune levels + +## Examples + +### Adjust your own microphone volume + +```typescript +// Decrease your microphone volume by 10dB +await call.setInputVolume({ volume: -10 }) + +// Increase your microphone volume by 10dB +await call.setInputVolume({ volume: 10 }) + +// Reset to default volume (0dB) +await call.setInputVolume({ volume: 0 }) +``` + +### Adjust another member's microphone volume + +```typescript +// Decrease another member's microphone volume +await call.setInputVolume({ + memberId: 'member-123', + volume: -10 +}) + +// Increase another member's microphone volume +await call.setInputVolume({ + memberId: 'member-123', + volume: 15 +}) +``` + +### Volume control UI + +```typescript +// HTML: + +const volumeSlider = document.getElementById('micVolume') as HTMLInputElement + +volumeSlider.addEventListener('change', async (e) => { + const volume = parseInt(e.target.value) + + try { + await call.setInputVolume({ volume }) + console.log(`Microphone volume set to ${volume}dB`) + } catch (error) { + console.error('Failed to set volume:', error) + } +}) +``` + +### User volume preferences + +```typescript +// Save and restore user volume preferences +const VOLUME_PREF_KEY = 'micVolume' + +// Load saved preference +const savedVolume = localStorage.getItem(VOLUME_PREF_KEY) +if (savedVolume) { + await call.setInputVolume({ volume: parseInt(savedVolume) }) +} + +// Save preference when changed +async function setAndSaveVolume(volume: number) { + await call.setInputVolume({ volume }) + localStorage.setItem(VOLUME_PREF_KEY, volume.toString()) +} +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-layout.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-layout.mdx new file mode 100644 index 00000000..eab45413 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-layout.mdx @@ -0,0 +1,142 @@ +--- +title: setLayout +slug: /call-session/setlayout +--- + +import APIField from '@site/src/components/APIField'; + +Changes the video layout for the call, optionally positioning specific members. + +## Signature + +```typescript +setLayout(params: SetLayoutParams): Promise +``` + +## Parameters + + + Layout configuration object. + + + + Name of the layout to apply (e.g., "grid-responsive", "2x2", "6x6"). + + + + Map of member IDs to specific positions in the layout. + + +## Returns + +**Type:** `Promise` + +## Important Notes + +**Layout Availability:** +- Available layouts depend on your SignalWire project configuration +- Use `getLayouts()` to retrieve the list of available layouts +- Common layouts: `grid-responsive`, `2x2`, `3x3`, `4x4`, `5x5`, `6x6`, `8x8` + +**Position Types:** + +**Reserved Positions** (`reserved-1`, `reserved-2`, etc.) +- Pin important participants to specific spots +- Remain fixed even as other members join/leave + +**Standard Positions** (`standard-1`, `standard-2`, etc.) +- Dynamic positioning for regular participants + +**Special Positions:** +- `auto` - SDK automatically positions the member +- `off-canvas` - Member is in the call but not visible +- `full-screen` - Member takes up the entire screen +- `playback` - For media playback positioning + +**Positions vs setPositions():** +- `setLayout()` can optionally set positions while changing the layout +- Use `setPositions()` if you only want to reposition members without changing the layout + +## Events + +- `layout.changed` - Layout has been updated + +```typescript +call.on('layout.changed', (event) => { + console.log('New layout:', event.layout.name) +}) +``` + +## Examples + +### Set a basic layout + +```typescript +await call.setLayout({ name: '6x6' }) +await call.setLayout({ name: '2x2' }) +await call.setLayout({ name: 'grid-responsive' }) +``` + +### Set layout with specific member positions + +```typescript +await call.setLayout({ + name: '6x6', + positions: { + 'member-id-1': 'reserved-1', + 'member-id-2': 'reserved-2', + 'member-id-3': 'auto' + } +}) +``` + +### Full workflow: Get available layouts, then set + +```typescript +const { layouts } = await call.getLayouts() + +const desiredLayout = '6x6' +if (layouts.includes(desiredLayout)) { + await call.setLayout({ name: desiredLayout }) +} +``` + +### Position yourself in a layout + +```typescript +// Use "self" keyword if you don't know your member ID yet +await call.setLayout({ + name: '4x4', + positions: { + 'self': 'reserved-1' + } +}) + +// If you know your member ID +await call.setLayout({ + name: '4x4', + positions: { + [call.memberId]: 'reserved-1' + } +}) +``` + +### Dynamic layout based on participant count + +```typescript +const { members } = await call.getMembers() +const memberCount = members.length + +let layoutName: string +if (memberCount <= 4) { + layoutName = '2x2' +} else if (memberCount <= 9) { + layoutName = '3x3' +} else if (memberCount <= 16) { + layoutName = '4x4' +} else { + layoutName = '6x6' +} + +await call.setLayout({ name: layoutName }) +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-local-stream.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-local-stream.mdx new file mode 100644 index 00000000..5f5bc60c --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-local-stream.mdx @@ -0,0 +1,138 @@ +--- +title: setLocalStream +slug: /call-session/setlocalstream +--- + +import APIField from '@site/src/components/APIField'; + +Replaces the current local media stream with a custom MediaStream. + +## Parameters + + + The MediaStream to use as the new local stream. + + +## Signature + +```typescript +call.setLocalStream(stream: MediaStream): Promise +``` + +## Returns + +**Type:** `Promise` + +Returns the MediaStream that was set. + +## Examples + +### Replace with Custom MediaStream + +```typescript +// Create a custom media stream +const customStream = await navigator.mediaDevices.getUserMedia({ + audio: true, + video: { width: 1920, height: 1080 } +}) + +// Replace the call's local stream +await call.setLocalStream(customStream) +``` + +### Use Canvas as Video Source + +```typescript +// Create a canvas element +const canvas = document.createElement('canvas') +canvas.width = 640 +canvas.height = 480 +const ctx = canvas.getContext('2d') + +// Draw something on the canvas +ctx.fillStyle = 'blue' +ctx.fillRect(0, 0, canvas.width, canvas.height) + +// Capture canvas as stream +const canvasStream = canvas.captureStream(30) // 30 fps + +// Get audio from microphone +const audioStream = await navigator.mediaDevices.getUserMedia({ audio: true }) + +// Combine canvas video with microphone audio +const combinedStream = new MediaStream([ + ...canvasStream.getVideoTracks(), + ...audioStream.getAudioTracks() +]) + +// Replace call stream with canvas + audio +await call.setLocalStream(combinedStream) +``` + +### Use Pre-processed Audio/Video + +```typescript +// Get media with constraints +const stream = await navigator.mediaDevices.getUserMedia({ + audio: { + echoCancellation: true, + noiseSuppression: true + }, + video: { + width: { ideal: 1280 }, + height: { ideal: 720 } + } +}) + +// Apply custom processing (e.g., filters, effects) +// ... custom processing logic ... + +// Set the processed stream +await call.setLocalStream(stream) +``` + +### Switch Between Multiple Streams + +```typescript +let currentStreamIndex = 0 +const streams = [stream1, stream2, stream3] + +async function switchToNextStream() { + currentStreamIndex = (currentStreamIndex + 1) % streams.length + await call.setLocalStream(streams[currentStreamIndex]) + console.log(`Switched to stream ${currentStreamIndex}`) +} + +// Call this to cycle through streams +await switchToNextStream() +``` + +## Related Properties + +Access the current local media stream: + +- `call.localStream` - Current local MediaStream +- `call.localAudioTrack` - Current local audio track +- `call.localVideoTrack` - Current local video track + +## Important Notes + +**Full Stream Replacement:** This method replaces the entire local media stream (both audio and video tracks). If you only want to replace the camera or microphone, use `updateCamera()` or `updateMicrophone()` instead. + +**Advanced Use Case:** This method is primarily for advanced scenarios where you need complete control over the media stream, such as: +- Using canvas or WebGL as video source +- Applying custom audio/video processing +- Using pre-recorded media files +- Implementing virtual backgrounds or filters + +**Track Management:** You are responsible for managing the lifecycle of tracks in the custom stream. Make sure to properly stop old tracks when replacing streams to avoid resource leaks. + +**Stream Requirements:** The MediaStream can contain: +- Audio tracks, video tracks, or both +- **If the stream has no audio tracks:** Audio remains unaffected (existing audio continues) +- **If the stream has no video tracks:** Video remains unaffected (existing video continues) +- Tracks must be in the correct state (not ended/stopped) + +This allows you to replace only audio or only video by providing a stream with just the desired track type. + +**Mute State Preserved:** The current mute state (audio/video) is preserved when replacing the stream. If the call was muted, the new stream will also be muted. diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-output-volume.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-output-volume.mdx new file mode 100644 index 00000000..c7d5117b --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-output-volume.mdx @@ -0,0 +1,126 @@ +--- +title: setOutputVolume +slug: /call-session/setoutputvolume +--- + +import APIField from '@site/src/components/APIField'; + +Adjusts the speaker (output) volume level for yourself or another participant. + +## Signature + +```typescript +setOutputVolume(params: MemberCommandWithVolumeParams): Promise +``` + +## Parameters + + + Volume configuration object. + + + + Volume level in decibels (dB). Typically ranges from -50 to 50. + + + + Member to control. Omit to control your own speakers, provide specific ID to control another member. + + +## Returns + +**Type:** `Promise` + +## Important Notes + +**Volume Range:** +- Volume is specified in **decibels (dB)** +- Typical range: **-50dB to +50dB** +- `0dB` = default/neutral volume (no adjustment) +- Negative values = reduce volume +- Positive values = increase volume +- Extreme values may cause audio distortion + +**Recommended ranges:** +- For reduction: `-20dB to 0dB` +- For boost: `0dB to +20dB` + +**Input vs Output Volume:** + +| Method | Effect | Use Case | +|--------|--------|----------| +| `setInputVolume()` | Adjusts **microphone volume** (how loud you sound to others) | Control how loud your microphone is | +| `setOutputVolume()` | Adjusts **speaker volume** (how loud others sound to you) | Control how loud you hear others | + +```typescript +// Make yourself quieter to others +await call.setInputVolume({ volume: -10 }) + +// Make others quieter to you +await call.setOutputVolume({ volume: -10 }) +``` + +**Volume Persistence:** +- Volume settings persist for the duration of the call +- Settings do not carry over to new calls +- Each member's volume can be adjusted independently + +**Real-time Adjustment:** +- Volume changes take effect immediately +- No need to mute/unmute or restart audio streams +- Can be called multiple times to fine-tune levels + +## Examples + +### Adjust your own speaker volume + +```typescript +// Decrease your speaker volume by 10dB (others sound quieter to you) +await call.setOutputVolume({ volume: -10 }) + +// Increase your speaker volume by 10dB (others sound louder to you) +await call.setOutputVolume({ volume: 10 }) + +// Reset to default volume (0dB) +await call.setOutputVolume({ volume: 0 }) +``` + +### Adjust another member's speaker volume + +```typescript +await call.setOutputVolume({ + memberId: 'member-123', + volume: -10 +}) +``` + +### Volume control UI + +```typescript +// HTML: + +const volumeSlider = document.getElementById('speakerVolume') as HTMLInputElement + +volumeSlider.addEventListener('change', async (e) => { + const volume = parseInt(e.target.value) + await call.setOutputVolume({ volume }) +}) +``` + +### User volume preferences + +```typescript +const SPEAKER_VOLUME_KEY = 'speakerVolume' + +// Load saved preference +const savedVolume = localStorage.getItem(SPEAKER_VOLUME_KEY) +if (savedVolume) { + await call.setOutputVolume({ volume: parseInt(savedVolume) }) +} + +// Save preference when user adjusts volume +async function setAndSaveVolume(volume: number) { + await call.setOutputVolume({ volume }) + localStorage.setItem(SPEAKER_VOLUME_KEY, volume.toString()) +} +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-positions.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-positions.mdx new file mode 100644 index 00000000..a13385ad --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-positions.mdx @@ -0,0 +1,148 @@ +--- +title: setPositions +slug: /call-session/setpositions +--- + +import APIField from '@site/src/components/APIField'; + +Repositions multiple members within the current video layout without changing the layout itself. + +## Signature + +```typescript +setPositions(params: SetPositionsParams): Promise +``` + +## Parameters + + + Positioning configuration object. + + + + Map of member IDs to their desired positions in the layout. + + +## Returns + +**Type:** `Promise` + +## Important Notes + +**setPositions() vs setLayout():** + +| Method | Purpose | Layout Change | +|--------|---------|---------------| +| `setLayout()` | Change layout **and** optionally position members | ✅ Changes layout | +| `setPositions()` | Reposition members **within** current layout | ❌ Keeps current layout | + +**When to use each:** + +```typescript +// Change from 2x2 to 6x6 layout AND position members +await call.setLayout({ + name: '6x6', + positions: { + 'member-1': 'reserved-1' + } +}) + +// Keep current layout (6x6), just move members around +await call.setPositions({ + positions: { + 'member-1': 'reserved-2', + 'member-2': 'reserved-1' + } +}) +``` + +**Position Types:** + +**Reserved Positions** (`reserved-1`, `reserved-2`, etc.) +- Pin important participants to specific spots +- Remain fixed even as other members join/leave + +**Standard Positions** (`standard-1`, `standard-2`, etc.) +- Dynamic positioning for regular participants + +**Special Positions:** +- `auto` - SDK automatically positions the member +- `off-canvas` - Member is in the call but not visible +- `full-screen` - Member takes up the entire screen +- `playback` - For media playback positioning + +**Using "self" Keyword:** +- Use `"self"` as a key when you don't know your own member ID yet +- Useful when joining a call and positioning yourself immediately +- Can be mixed with explicit member IDs + +**Persistence:** +- Position settings persist until explicitly changed +- New members joining will not automatically take reserved positions +- Positions remain even if members temporarily leave and rejoin + +## Examples + +### Reposition multiple members + +```typescript +await call.setPositions({ + positions: { + 'member-id-1': 'reserved-1', + 'member-id-2': 'reserved-2', + 'member-id-3': 'auto' + } +}) +``` + +### Use "self" keyword + +```typescript +// Position yourself without knowing your member ID +await call.setPositions({ + positions: { + 'self': 'reserved-1' + } +}) + +// Mix self and other member IDs +await call.setPositions({ + positions: { + 'self': 'reserved-1', + 'member-id-2': 'reserved-2' + } +}) +``` + +### Hide multiple members + +```typescript +await call.setPositions({ + positions: { + 'member-id-1': 'off-canvas', + 'member-id-2': 'off-canvas' + } +}) +``` + +### Promote/demote members + +```typescript +// Promote a member to a reserved position +async function promoteMember(memberId: string) { + await call.setPositions({ + positions: { + [memberId]: 'reserved-1' + } + }) +} + +// Demote a member back to auto positioning +async function demoteMember(memberId: string) { + await call.setPositions({ + positions: { + [memberId]: 'auto' + } + }) +} +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-raised-hand.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-raised-hand.mdx new file mode 100644 index 00000000..4200436a --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-raised-hand.mdx @@ -0,0 +1,119 @@ +--- +title: setRaisedHand +slug: /call-session/setraisedhand +--- + +import APIField from '@site/src/components/APIField'; + +Raises or lowers the hand for yourself or another member in the call. + +## Parameters + + + Configuration for raising/lowering hand. + + + + `true` to raise hand, `false` to lower hand. + + + + Member to control. Omit to control yourself, provide specific ID to control another member. + + +## Signature + +```typescript +setRaisedHand(params?: SetRaisedHandRoomParams): Promise +``` + +## Returns + +**Type:** `Promise` + +## Permissions + +- **Raise your own hand:** Requires `call.capabilities.self.raisehand.on` +- **Lower your own hand:** Requires `call.capabilities.self.raisehand.off` +- **Control other members:** Requires `call.capabilities.member.raisehand.on` (to raise) or `call.capabilities.member.raisehand.off` (to lower) + +## Events + +- `member.updated` - General member update event +- `member.updated.handraised` - Specific event for hand state changes + +## Examples + +### Raise your own hand + +```typescript +// Raise your hand (default behavior) +await call.setRaisedHand() + +// Explicitly raise your hand +await call.setRaisedHand({ raised: true }) +``` + +### Lower your own hand + +```typescript +await call.setRaisedHand({ raised: false }) +``` + +### Control another member's hand + +```typescript +// Raise another member's hand (requires permission) +await call.setRaisedHand({ + raised: true, + memberId: 'member-123' +}) + +// Lower another member's hand (requires permission) +await call.setRaisedHand({ + raised: false, + memberId: 'member-123' +}) +``` + +### With capability check + +```typescript +if (call.capabilities?.self.raisehand.on) { + await call.setRaisedHand({ raised: true }) +} else { + console.log('You do not have permission to raise your hand') +} +``` + +### Track members with raised hands + +```typescript +const membersWithRaisedHands: string[] = [] + +call.on('member.updated.handraised', (event) => { + if (event.member.handraised) { + membersWithRaisedHands.push(event.member.id) + } else { + const index = membersWithRaisedHands.indexOf(event.member.id) + if (index > -1) { + membersWithRaisedHands.splice(index, 1) + } + } + + console.log(`${membersWithRaisedHands.length} members have raised hands`) +}) +``` + +### Virtual classroom + +```typescript +// Student raises hand to ask a question +await call.setRaisedHand({ raised: true }) + +// Teacher acknowledges and lowers student's hand +await call.setRaisedHand({ + raised: false, + memberId: studentMemberId +}) +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-video-direction.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-video-direction.mdx new file mode 100644 index 00000000..ef4615b7 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/set-video-direction.mdx @@ -0,0 +1,95 @@ +--- +title: setVideoDirection +slug: /call-session/setvideodirection +--- + +import APIField from '@site/src/components/APIField'; + +Sets the direction of the video track in the WebRTC connection by performing RTC peer renegotiation. + +## Parameters + + + The desired video direction. Valid values: `'sendrecv'`, `'sendonly'`, `'recvonly'`, `'inactive'` + + +**UpdateMediaDirection** values: +- `'sendrecv'` - Send and receive video (default, two-way video) +- `'sendonly'` - Only send video, don't receive +- `'recvonly'` - Only receive video, don't send +- `'inactive'` - Neither send nor receive video + +## Signature + +```typescript +call.setVideoDirection(direction: UpdateMediaDirection): Promise +``` + +## Returns + +**Type:** `Promise` + +## Examples + +### Enable Two-Way Video + +```typescript +await call.setVideoDirection('sendrecv') +``` + +### Watch-Only Mode + +```typescript +// Receive video but don't send +await call.setVideoDirection('recvonly') +``` + +### Broadcast Mode + +```typescript +// Send video but don't receive +await call.setVideoDirection('sendonly') +``` + +### Disable Video Completely + +```typescript +// Neither send nor receive video +await call.setVideoDirection('inactive') +``` + +### Upgrade Audio-Only Call to Video + +```typescript +// Start with audio only (video inactive) +await call.setVideoDirection('inactive') + +// Later, enable two-way video +await call.setVideoDirection('sendrecv') +``` + +### Large Conference (Bandwidth Savings) + +```typescript +// In a 50-person call, only send your video +// Don't receive 49 other video streams +await call.setVideoDirection('sendonly') +``` + +## Important Notes + +**Peer Renegotiation:** This method triggers WebRTC peer renegotiation to update the video track direction. The connection will temporarily adjust while renegotiating. + +**Direction vs Mute:** +- **Mute** (`videoMute()`): Temporarily stops sending video, but the track remains active +- **Direction**: Changes the fundamental video flow in the WebRTC connection + +Use `setVideoDirection()` when you need to: +- Permanently disable video sending or receiving +- Optimize bandwidth by not receiving video when unnecessary +- Upgrade from audio-only to video call +- Implement broadcast or watch-only modes + +Use `videoMute()`/`videoUnmute()` when you just need to temporarily stop sending video. + +**Audio Unaffected:** This method only changes the video direction. Audio direction remains unchanged. To update both, use `updateMedia()`. diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/start-screen-share.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/start-screen-share.mdx new file mode 100644 index 00000000..7aed58c7 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/start-screen-share.mdx @@ -0,0 +1,198 @@ +--- +title: startScreenShare +slug: /call-session/startscreenshare +--- + +import APIField from '@site/src/components/APIField'; + +Starts screen sharing within the call, allowing you to share your screen, application window, or browser tab with other participants. + +## Parameters + + + Screen share configuration options. + + + + Whether to automatically join the screen share to the call. + + + + Whether to capture system audio with the screen share. + + + + Whether to capture video (the screen content). + + + + Layout name to use for the screen share. + + + + Position configuration for the screen share in the layout. + + +## Signature + +```typescript +call.startScreenShare(opts?: StartScreenShareOptions): Promise +``` + +## Returns + +**Type:** `Promise` + +A Promise that resolves with a `RoomSessionScreenShare` object representing the active screen share session. + +## Examples + +### Basic Screen Share + +```typescript +// Start screen sharing (user will be prompted to select screen/window/tab) +const screenShare = await call.startScreenShare() +console.log('Screen sharing started') +``` + +### Screen Share with Audio + +```typescript +// Share screen with system audio (if supported by browser) +const screenShare = await call.startScreenShare({ + audio: true, + video: true +}) +``` + +### Manual Join Control + +```typescript +// Create screen share but don't join automatically +const screenShare = await call.startScreenShare({ + autoJoin: false +}) + +// Join manually later +await screenShare.join() +``` + +### Screen Share with Layout + +```typescript +// Start screen share with specific layout +const screenShare = await call.startScreenShare({ + layout: '1x1', + positions: { + self: 'standard' + } +}) +``` + +### Stop Screen Share + +```typescript +const screenShare = await call.startScreenShare() + +// Later, stop sharing +await screenShare.leave() +``` + +### Handle Screen Share Lifecycle + +```typescript +const screenShare = await call.startScreenShare() + +// Listen for when screen share ends +screenShare.on('room.left', () => { + console.log('Screen sharing stopped') + // Update UI +}) + +// User clicks stop button in browser or your app +await screenShare.leave() +``` + +### Presentation Mode + +```typescript +async function startPresentation() { + try { + const screenShare = await call.startScreenShare({ + audio: true // Include system audio for video playback + }) + + // Optional: Mute your camera while presenting + await call.videoMute() + + return screenShare + } catch (error) { + console.error('Failed to start presentation:', error) + } +} +``` + +### Screen Share Toggle + +```typescript +let activeScreenShare = null + +async function toggleScreenShare() { + if (activeScreenShare) { + // Stop existing screen share + await activeScreenShare.leave() + activeScreenShare = null + } else { + // Start new screen share + activeScreenShare = await call.startScreenShare() + + activeScreenShare.on('room.left', () => { + activeScreenShare = null + updateUIButton('Share Screen') + }) + + updateUIButton('Stop Sharing') + } +} +``` + +## Important Notes + +**Browser Prompt:** When you call this method, the browser will display a native dialog asking the user to select what to share: +- **Entire screen** - Share the full desktop +- **Application window** - Share a specific application +- **Browser tab** - Share a specific tab + +**System Audio:** The `audio: true` option allows capturing system audio along with screen content. This is useful for: +- Sharing videos with sound +- Presenting applications with audio +- Broadcasting computer audio + +**Note:** System audio capture is only supported in Chrome/Edge and requires specific permissions. + +**Automatic Stop:** The screen share will automatically end when: +- User clicks the browser's "Stop sharing" button +- The video track ends +- You call `screenShare.leave()` + +**Multiple Screen Shares:** You can have multiple simultaneous screen shares. Access them via `call.screenShareList`: + +```typescript +// Start first screen share +const screenShare1 = await call.startScreenShare() + +// Start second screen share +const screenShare2 = await call.startScreenShare() + +// Both are tracked +console.log(call.screenShareList) // [screenShare1, screenShare2] +``` + +**RoomSessionScreenShare Object:** The returned object has its own methods and events: +- `screenShare.leave()` - Stop the screen share +- `screenShare.on('room.left', handler)` - Listen for end events + +**Browser Support:** Screen sharing requires: +- Modern browsers (Chrome, Edge, Firefox, Safari) +- Secure context (HTTPS or localhost) +- User permission (granted via browser prompt) diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/start.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/start.mdx new file mode 100644 index 00000000..66146f64 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/start.mdx @@ -0,0 +1,127 @@ +--- +title: start +slug: /call-session/start +--- + +Initiates the WebRTC connection and joins the call session. + +## Signature + +```typescript +start(): Promise +``` + +## Parameters + +None + +## Returns + +**Type:** `Promise` + +**Resolves:** When the call has successfully joined (after `room.subscribed` event fires) + +**Rejects:** If the call fails to start or is destroyed before joining + +## Important Notes + +**Automatic Invocation:** + +In most use cases, you don't need to call `start()` manually. The SDK automatically calls it when: +- You provide `listen` parameter to `client.dial()` +- You provide `listen` parameter to `client.reattach()` + +**What it does:** +1. Initiates the WebRTC connection +2. Sends the appropriate signaling message (invite for outbound, answer for inbound) +3. Waits for the `room.subscribed` event confirming you've joined +4. Resolves when the call is successfully established + +**Lifecycle Events:** + +`start()` waits for specific events to complete: +- **Success:** Resolves when `room.subscribed` event fires +- **Failure:** Rejects if `destroy` event fires before joining + +**Call Only Once:** + +Do not call `start()` multiple times on the same `CallSession` object. It should only be invoked once per call session. + +## Events + +During the `start()` process, the following events may be emitted: + +- `room.subscribed` - Fires when successfully joined (causes `start()` to resolve) +- `destroy` - Fires if the call fails to start (causes `start()` to reject) +- Connection state events: `connecting`, `connected`, etc. + +## Examples + +### Automatic usage (recommended) + +When you provide event listeners to `dial()`, the SDK automatically calls `start()`: + +```typescript +// The SDK calls start() automatically when you provide listeners +const call = await client.dial({ + to: 'user@example.com', + listen: { + onCallJoined: () => console.log('Call joined!') + } +}) + +// Call is already started and joined at this point +console.log('Call is active:', call.memberId) +``` + +### Manual usage (advanced) + +In advanced scenarios where you need explicit control: + +```typescript +// Get the call session without starting it +const callSession = buildOutboundCall({ to: 'user@example.com' }) + +// Set up event listeners first +callSession.on('room.subscribed', () => { + console.log('Successfully joined the call') +}) + +callSession.on('destroy', () => { + console.log('Call failed to start') +}) + +// Now manually start the call +try { + await callSession.start() + console.log('Call started successfully') +} catch (error) { + console.error('Failed to start call:', error) +} +``` + +### With error handling + +```typescript +try { + await call.start() + console.log('Call connected successfully') +} catch (error) { + console.error('Call failed to start:', error.message) + // Handle failure - call may have been destroyed +} +``` + +### Listening for lifecycle events + +```typescript +call.on('room.subscribed', (params) => { + console.log('Joined call:', params.call_id) +}) + +call.on('destroy', () => { + console.log('Call was destroyed before joining') +}) + +await call.start() +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/undeaf.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/undeaf.mdx new file mode 100644 index 00000000..f745cd3e --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/undeaf.mdx @@ -0,0 +1,164 @@ +--- +title: undeaf +slug: /call-session/undeaf +--- + +import APIField from '@site/src/components/APIField'; + +Unmutes incoming audio so you or another participant can hear others in the call again. + +## Signature + +```typescript +undeaf(params?: MemberCommandParams): Promise +``` + +## Parameters + + + Configuration for the undeaf operation. + + + + Member to undeaf. Omit to undeaf yourself, provide specific ID to undeaf another member. + + +## Returns + +**Type:** `Promise` + +## Permissions + +- **Undeaf yourself:** Requires `call.capabilities.self.deaf.off` +- **Undeaf another member:** Requires `call.capabilities.member.deaf.off` + +## Events + +- `member.updated` - General member update event +- `member.updated.deaf` - Specific event for deaf state change + +## Important Notes + +**Microphone Remains Muted:** + +When you call `undeaf()`, the microphone remains muted. You must manually call `audioUnmute()` to speak again. + +```typescript +// Step 1: Resume hearing others +await call.undeaf() +console.log('You can hear others now') + +// Step 2: Manually unmute microphone to speak +await call.audioUnmute() +console.log('You can now hear and speak') +``` + +This is intentional - it gives you control over when you want to start speaking again. + +**Member Targeting:** + +The `memberId` parameter follows the standard member targeting pattern: +- **No parameter** - Undeafs yourself +- **Specific member ID** - Undeafs that member (requires permission) + +```typescript +// Undeaf yourself +await call.undeaf() + +// Undeaf a specific member +await call.undeaf({ memberId: 'de550c0c-3fac-4efd-b06f-b5b8614b8966' }) +``` + +**Resume Participation Workflow:** + +```typescript +async function resumeFullParticipation() { + try { + // Step 1: Resume hearing + await call.undeaf() + console.log('Can hear others') + + // Step 2: Resume speaking + await call.audioUnmute() + console.log('Can speak') + + console.log('Fully rejoined the conversation') + } catch (error) { + console.error('Failed to resume participation:', error) + } +} + +resumeFullParticipation() +``` + +## Examples + +### Undeaf yourself + +```typescript +// Resume hearing other participants +await call.undeaf() +console.log('You can now hear other participants again') +``` + +### Undeaf another participant + +```typescript +// Get the list of members +const { members } = await call.getMembers() + +// Find a specific member +const member = members.find(m => m.name === 'John Doe') + +if (member) { + // Undeaf that member (requires permission) + await call.undeaf({ memberId: member.id }) + console.log(`${member.name} can now hear other participants`) +} +``` + +### Resume full participation after being deaf + +```typescript +// You were made deaf, now resume full participation +await call.undeaf() // Resume hearing others + +// Microphone is still muted from when you were made deaf +await call.audioUnmute() // Resume speaking + +console.log('You can now hear and speak in the call') +``` + +### With event listeners + +```typescript +// Listen for undeaf events +call.on('member.updated.deaf', (event) => { + if (!event.member.deaf) { + console.log(`Member ${event.member.id} can now hear others`) + } +}) + +// Undeaf yourself +await call.undeaf() +``` + +### Check capabilities before undeafing + +```typescript +// Check if you can undeaf yourself +if (call.capabilities?.self.deaf.off) { + await call.undeaf() + console.log('Successfully undeaf yourself') +} else { + console.log('You do not have permission to undeaf yourself') +} + +// Check if you can undeaf other members +if (call.capabilities?.member.deaf.off) { + await call.undeaf({ memberId: 'member-id' }) + console.log('Successfully undeaf member') +} else { + console.log('You do not have permission to undeaf other members') +} +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/unhold.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/unhold.mdx new file mode 100644 index 00000000..4cbb4cee --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/unhold.mdx @@ -0,0 +1,108 @@ +--- +title: unhold +slug: /call-session/unhold +--- + +Resumes the call from hold by restoring outbound audio and video from the local participant. + +## Signature + +```typescript +call.unhold(): Promise +``` + +## Parameters + +None + +## Returns + +**Type:** `Promise` + +## Examples + +### Basic Unhold + +```typescript +await call.unhold() +console.log('Call resumed') +``` + +### Hold/Unhold Toggle + +```typescript +let isOnHold = false + +async function toggleHold() { + if (isOnHold) { + await call.unhold() + console.log('Call resumed') + } else { + await call.hold() + console.log('Call on hold') + } + isOnHold = !isOnHold +} + +await toggleHold() +``` + +### Unhold with UI Feedback + +```typescript +const resumeButton = document.getElementById('resume-button') + +resumeButton.addEventListener('click', async () => { + try { + await call.unhold() + resumeButton.textContent = 'Hold' + resumeButton.classList.remove('on-hold') + } catch (error) { + console.error('Failed to resume call:', error) + } +}) +``` + +### Timed Hold + +```typescript +// Put call on hold for 2 minutes +await call.hold() +console.log('Call on hold for 2 minutes...') + +setTimeout(async () => { + await call.unhold() + console.log('Call automatically resumed') +}, 2 * 60 * 1000) +``` + +### Multi-Call Switching + +```typescript +async function switchBetweenCalls(fromCall, toCall) { + // Put current call on hold + await fromCall.hold() + + // Resume the other call + await toCall.unhold() + + console.log('Switched calls') +} +``` + +## Important Notes + +**What Happens on Unhold:** +- Your outbound audio and video streams are restored +- You resume sending audio/video to other participants +- Other participants can now see and hear you again +- The call returns to its normal active state + +**Restores Previous State:** The call is restored to the state it was in before being put on hold. If audio or video were muted before hold, they will remain muted after unhold. + +**Must Be On Hold:** This method should only be called when the call is currently on hold (after calling `hold()`). Calling it on an already active call has no effect. + +**Automatic Media Resumption:** The SDK automatically: +- Restores audio/video tracks +- Re-enables media streaming +- Updates the UI state (if using framework integration) diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/unlock.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/unlock.mdx new file mode 100644 index 00000000..3814cb68 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/unlock.mdx @@ -0,0 +1,174 @@ +--- +title: unlock +slug: /call-session/unlock +--- + +Unlocks the call to allow new participants to join. + +## Signature + +```typescript +unlock(): Promise +``` + +## Parameters + +None + +## Returns + +**Type:** `Promise` + +## Important Notes + +**Permissions:** + +Permission requirements vary based on your call configuration. +- May require `room.unlock` capability + +**Unlock Behavior:** + +When a call is unlocked: +- New participants **can** join the call +- Participants who previously left can rejoin +- The unlock state persists until explicitly locked again + +Unlock does NOT affect: +- Existing participants (they remain in the call) +- Screen sharing or other call features +- Media streams or audio/video + +**Checking Lock State:** + +Track lock state manually: + +```typescript +let isCallLocked = false + +async function lockCall() { + await call.lock() + isCallLocked = true +} + +async function unlockCall() { + await call.unlock() + isCallLocked = false +} + +// Check state +if (!isCallLocked) { + console.log('Call is unlocked - new participants can join') +} +``` + +## Events + +Lock/unlock operations may trigger room update events: + +```typescript +call.on('room.updated', (event) => { + console.log('Room updated, locked state may have changed') +}) +``` + +## Examples + +### Basic usage + +```typescript +// Unlock the call to allow new participants to join +await call.unlock() +console.log('Call is now unlocked') +``` + +### Unlock to let someone in + +```typescript +// Temporarily unlock to allow a late participant to join +async function letParticipantIn() { + console.log('Unlocking call for late participant...') + await call.unlock() + + // Wait for participant to join (or timeout after 2 minutes) + await waitForParticipant(120000) + + // Lock again + await call.lock() + console.log('Call locked again') +} +``` + +### With error handling + +```typescript +try { + await call.unlock() + console.log('Call unlocked successfully') +} catch (error) { + console.error('Failed to unlock call:', error) +} +``` + +### UI toggle button + +```typescript +let isLocked = false + +async function toggleLock() { + try { + if (isLocked) { + await call.unlock() + isLocked = false + console.log('Call unlocked') + } else { + await call.lock() + isLocked = true + console.log('Call locked') + } + + updateLockButtonUI(isLocked) + } catch (error) { + console.error('Failed to toggle lock:', error) + } +} + +function updateLockButtonUI(locked: boolean) { + const button = document.getElementById('lock-button') + if (button) { + button.textContent = locked ? 'Unlock' : 'Lock' + button.className = locked ? 'locked' : 'unlocked' + } +} +``` + +### Temporary unlock for late arrivals + +```typescript +// Unlock for 5 minutes to allow late arrivals, then lock again +async function openForLateArrivals() { + await call.unlock() + console.log('Call unlocked for late arrivals') + + setTimeout(async () => { + await call.lock() + console.log('Call locked again after grace period') + }, 5 * 60 * 1000) // 5 minutes +} +``` + +### Conditional unlock based on capacity + +```typescript +// Unlock if capacity drops below threshold +const MAX_PARTICIPANTS = 10 +const UNLOCK_THRESHOLD = 7 + +call.on('member.left', async () => { + const { members } = await call.getMembers() + + if (members.length <= UNLOCK_THRESHOLD) { + await call.unlock() + console.log(`Capacity at ${members.length}/${MAX_PARTICIPANTS} - call unlocked`) + } +}) +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/update-camera.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/update-camera.mdx new file mode 100644 index 00000000..48fea0ed --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/update-camera.mdx @@ -0,0 +1,99 @@ +--- +title: updateCamera +slug: /call-session/updatecamera +--- + +import APIField from '@site/src/components/APIField'; + +Replaces the current camera stream with one from a different video device. + +## Parameters + + + Constraints that the new camera device should satisfy. + + +## Signature + +```typescript +call.updateCamera(constraints: MediaTrackConstraints): Promise +``` + +## Returns + +**Type:** `Promise` + +## Examples + +### Switch to Specific Camera Device + +```typescript +// Get available video devices +const devices = await navigator.mediaDevices.enumerateDevices() +const cameras = devices.filter(d => d.kind === 'videoinput') + +// Switch to a specific camera +await call.updateCamera({ + deviceId: cameras[1].deviceId +}) +``` + +### Switch with Exact Device ID + +```typescript +await call.updateCamera({ + deviceId: { exact: "/o4ZeWzroh+8q0Ds/CFfmn9XpqaHzmW3L/5ZBC22CRg=" } +}) +``` + +### Switch Camera with Resolution Constraints + +```typescript +// Switch camera and request specific resolution +await call.updateCamera({ + deviceId: cameraId, + width: { ideal: 1280 }, + height: { ideal: 720 } +}) +``` + +### Switch to Front/Rear Camera (Mobile) + +```typescript +// Switch to front camera +await call.updateCamera({ + facingMode: 'user' +}) + +// Switch to rear camera +await call.updateCamera({ + facingMode: 'environment' +}) +``` + +## Related Properties + +You can access information about the current camera using these CallSession properties: + +- `call.cameraId` - Current camera device ID +- `call.cameraLabel` - Current camera device label/name +- `call.cameraConstraints` - Current camera constraints +- `call.localVideoTrack` - Current local video MediaStreamTrack + +## Important Notes + +**Active Stream Replacement:** This method replaces the active video track in the ongoing call. The video stream will switch seamlessly without ending the call. + +**MediaTrackConstraints:** The `constraints` parameter follows the [MediaTrackConstraints](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints) web standard. Common properties include: +- `deviceId` - Specific device identifier +- `facingMode` - Camera direction ('user' or 'environment') +- `width` / `height` - Resolution constraints +- `frameRate` - Frame rate constraints +- `aspectRatio` - Aspect ratio (SDK automatically includes `16/9` as default) + +**Device Permissions:** The browser must have camera permissions granted. If switching to a new device requires additional permissions, the user will be prompted. + +**Device Availability:** The call will throw an error if: +- The specified device doesn't exist +- The device is already in use by another application +- The constraints cannot be satisfied diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/update-media.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/update-media.mdx new file mode 100644 index 00000000..1fed0950 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/update-media.mdx @@ -0,0 +1,174 @@ +--- +title: updateMedia +slug: /call-session/updatemedia +--- + +import APIField from '@site/src/components/APIField'; + +Upgrades or downgrades the media tracks in the WebRTC connection by performing RTC peer renegotiation. + +## Parameters + + + Media update configuration. + + + + Audio track configuration. + + + + Audio direction. Valid values: `'sendrecv'`, `'sendonly'`, `'recvonly'`, `'inactive'` + + + + Audio constraints (only when direction is 'sendonly' or 'sendrecv'). + + + + Video track configuration. + + + + Video direction. Valid values: `'sendrecv'`, `'sendonly'`, `'recvonly'`, `'inactive'` + + + + Video constraints (only when direction is 'sendonly' or 'sendrecv'). + + +**UpdateMediaDirection** values: +- `'sendrecv'` - Send and receive media (default) +- `'sendonly'` - Only send media, don't receive +- `'recvonly'` - Only receive media, don't send +- `'inactive'` - Neither send nor receive media + +## Signature + +```typescript +call.updateMedia(params: UpdateMediaParams): Promise +``` + +## Returns + +**Type:** `Promise` + +## Examples + +### Enable Two-Way Video + +```typescript +// Upgrade to full two-way video +await call.updateMedia({ + video: { direction: 'sendrecv' } +}) +``` + +### Enable Video with Constraints + +```typescript +// Enable two-way video with specific resolution +await call.updateMedia({ + video: { + direction: 'sendrecv', + constraints: { + width: { ideal: 1280 }, + height: { ideal: 720 } + } + } +}) +``` + +### Listen-Only Mode + +```typescript +// Set both audio and video to receive-only +await call.updateMedia({ + audio: { direction: 'recvonly' }, + video: { direction: 'recvonly' } +}) +``` + +### Broadcast Mode + +```typescript +// Send audio and video but don't receive +await call.updateMedia({ + audio: { direction: 'sendonly' }, + video: { direction: 'sendonly' } +}) +``` + +### Audio-Only Call + +```typescript +// Enable two-way audio, disable video +await call.updateMedia({ + audio: { direction: 'sendrecv' }, + video: { direction: 'inactive' } +}) +``` + +### Upgrade from Audio to Video + +```typescript +// Start with audio only +await call.updateMedia({ + audio: { direction: 'sendrecv' }, + video: { direction: 'inactive' } +}) + +// Later, upgrade to include video +await call.updateMedia({ + audio: { direction: 'sendrecv' }, + video: { direction: 'sendrecv' } +}) +``` + +### Webinar/Broadcast Scenario + +```typescript +// Host: Send audio/video, receive only audio +await call.updateMedia({ + audio: { direction: 'sendrecv' }, + video: { direction: 'sendonly' } +}) + +// Attendee: Receive everything, send only audio +await call.updateMedia({ + audio: { direction: 'sendrecv' }, + video: { direction: 'recvonly' } +}) +``` + +## Important Notes + +**Peer Renegotiation:** This method triggers WebRTC peer renegotiation, which means: +- New media constraints are negotiated with the remote peer +- The connection temporarily adjusts while renegotiating +- This is a more fundamental change than muting/unmuting + +**Direction vs Mute:** +- **Mute** (`audioMute()`/`videoMute()`): Stops sending media but the track remains active +- **Direction**: Changes the fundamental media flow in the connection + +Use `updateMedia()` when you need to: +- Add or remove media tracks entirely +- Change fundamental media flow (send/receive/both/neither) +- Optimize bandwidth by disabling receiving when not needed + +Use mute/unmute when you just need to temporarily stop sending media. + +**Partial Updates:** You can update just audio or video independently: + +```typescript +// Update only video direction +await call.updateMedia({ + video: { direction: 'recvonly' } +}) +// Audio direction remains unchanged +``` + +**Use Specific Methods:** For common cases, prefer using the specific direction methods: +- `setAudioDirection()` - Update only audio direction +- `setVideoDirection()` - Update only video direction diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/update-microphone.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/update-microphone.mdx new file mode 100644 index 00000000..8b508a5e --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/update-microphone.mdx @@ -0,0 +1,105 @@ +--- +title: updateMicrophone +slug: /call-session/updatemicrophone +--- + +import APIField from '@site/src/components/APIField'; + +Replaces the current microphone stream with one from a different audio input device. + +## Parameters + + + Constraints that the new microphone device should satisfy. + + +## Signature + +```typescript +call.updateMicrophone(constraints: MediaTrackConstraints): Promise +``` + +## Returns + +**Type:** `Promise` + +## Examples + +### Switch to Specific Microphone + +```typescript +// Get available audio devices +const devices = await navigator.mediaDevices.enumerateDevices() +const microphones = devices.filter(d => d.kind === 'audioinput') + +// Switch to a specific microphone +await call.updateMicrophone({ + deviceId: microphones[1].deviceId +}) +``` + +### Switch with Exact Device ID + +```typescript +await call.updateMicrophone({ + deviceId: { exact: "/o4ZeWzroh+8q0Ds/CFfmn9XpqaHzmW3L/5ZBC22CRg=" } +}) +``` + +### Switch Microphone with Audio Constraints + +```typescript +// Switch microphone with specific constraints +await call.updateMicrophone({ + deviceId: micId, + echoCancellation: true, + noiseSuppression: true, + autoGainControl: true +}) +``` + +### Dynamic Device Switching + +```typescript +// Listen for device changes and switch automatically +navigator.mediaDevices.addEventListener('devicechange', async () => { + const devices = await navigator.mediaDevices.enumerateDevices() + const defaultMic = devices.find(d => + d.kind === 'audioinput' && d.deviceId === 'default' + ) + + if (defaultMic) { + await call.updateMicrophone({ deviceId: defaultMic.deviceId }) + } +}) +``` + +## Related Properties + +You can access information about the current microphone using these CallSession properties: + +- `call.microphoneId` - Current microphone device ID +- `call.microphoneLabel` - Current microphone device label/name +- `call.microphoneConstraints` - Current microphone constraints +- `call.localAudioTrack` - Current local audio MediaStreamTrack + +## Important Notes + +**Active Stream Replacement:** This method replaces the active audio track in the ongoing call. The audio stream will switch seamlessly without ending the call. + +**MediaTrackConstraints:** The `constraints` parameter follows the [MediaTrackConstraints](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints) web standard. Common audio properties include: +- `deviceId` - Specific device identifier +- `echoCancellation` - Enable/disable echo cancellation +- `noiseSuppression` - Enable/disable noise suppression +- `autoGainControl` - Enable/disable automatic gain control +- `sampleRate` - Audio sample rate +- `channelCount` - Number of audio channels + +**Audio Processing:** When switching microphones, audio processing settings (echo cancellation, noise suppression, etc.) can be reconfigured. For runtime changes to audio processing without switching devices, use `setAudioFlags()`. + +**Device Permissions:** The browser must have microphone permissions granted. If switching to a new device requires additional permissions, the user will be prompted. + +**Device Availability:** The call will throw an error if: +- The specified device doesn't exist +- The device is already in use by another application +- The constraints cannot be satisfied diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/update-speaker.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/update-speaker.mdx new file mode 100644 index 00000000..555a4c36 --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/update-speaker.mdx @@ -0,0 +1,218 @@ +--- +title: updateSpeaker +slug: /call-session/updatespeaker +--- + +import APIField from '@site/src/components/APIField'; + +Updates the audio output device (speaker) used for playing remote audio from the call. + +## Parameters + + + The ID of the speaker device to use. + + +## Signature + +```typescript +call.updateSpeaker({ deviceId }: { deviceId: string }): Promise +``` + +## Returns + +**Type:** `Promise` + +## Examples + +### Switch to Specific Speaker + +```typescript +// Get available audio output devices +const devices = await navigator.mediaDevices.enumerateDevices() +const speakers = devices.filter(d => d.kind === 'audiooutput') + +// Switch to a specific speaker +await call.updateSpeaker({ + deviceId: speakers[1].deviceId +}) +``` + +### Switch to Default Speaker + +```typescript +await call.updateSpeaker({ + deviceId: 'default' +}) +``` + +### Dynamic Speaker Selection + +```typescript +async function selectSpeaker() { + const devices = await navigator.mediaDevices.enumerateDevices() + const speakers = devices.filter(d => d.kind === 'audiooutput') + + // Show dropdown to user + const selectedId = await showSpeakerDropdown(speakers) + + // Update to selected speaker + await call.updateSpeaker({ deviceId: selectedId }) +} +``` + +### Listen for Speaker Changes + +```typescript +// The updateSpeaker method triggers a 'speaker.updated' event +call.on('speaker.updated', (data) => { + console.log('Speaker changed from:', data.previous) + console.log('Speaker changed to:', data.current) + console.log('New device ID:', data.current.deviceId) + console.log('New device label:', data.current.label) +}) + +// Update speaker +await call.updateSpeaker({ deviceId: newDeviceId }) +``` + +### Speaker Selection UI + +```typescript +async function buildSpeakerSelector() { + const devices = await navigator.mediaDevices.enumerateDevices() + const speakers = devices.filter(d => d.kind === 'audiooutput') + + const select = document.createElement('select') + + speakers.forEach(speaker => { + const option = document.createElement('option') + option.value = speaker.deviceId + option.text = speaker.label || `Speaker ${speaker.deviceId.substring(0, 5)}` + select.appendChild(option) + }) + + select.addEventListener('change', async (e) => { + await call.updateSpeaker({ deviceId: e.target.value }) + }) + + return select +} +``` + +### React Speaker Selector + +```typescript +function SpeakerSelector({ call }) { + const [speakers, setSpeakers] = useState([]) + const [currentSpeaker, setCurrentSpeaker] = useState('') + + useEffect(() => { + async function loadSpeakers() { + const devices = await navigator.mediaDevices.enumerateDevices() + const outputs = devices.filter(d => d.kind === 'audiooutput') + setSpeakers(outputs) + } + + loadSpeakers() + + call.on('speaker.updated', (data) => { + setCurrentSpeaker(data.current.deviceId) + }) + }, [call]) + + async function handleChange(e) { + await call.updateSpeaker({ deviceId: e.target.value }) + } + + return ( + + ) +} +``` + +## Events + +### speaker.updated + +Fired when the speaker device changes. + +```typescript +call.on('speaker.updated', (params) => { + console.log('Previous speaker:', params.previous.deviceId, params.previous.label) + console.log('Current speaker:', params.current.deviceId, params.current.label) +}) +``` + +**Parameters:** + + + Previous speaker device ID. + + + + Previous speaker device label. + + + + New speaker device ID. + + + + New speaker device label. + + +### speaker.disconnected + +Fired when the current speaker is physically disconnected. + +```typescript +call.on('speaker.disconnected', (params) => { + console.log('Speaker disconnected:', params.deviceId, params.label) +}) +``` + +**Parameters:** + + + Disconnected speaker device ID. + + + + Disconnected speaker device label. + + +## Important Notes + +**Browser Support:** Changing audio output device requires: +- Modern browsers with `setSinkId()` support (Chrome, Edge, Opera) +- Firefox and Safari have limited support +- The method may not work in all browsers + +Check support with: +```typescript +const supportsSetSinkId = 'setSinkId' in HTMLMediaElement.prototype +if (!supportsSetSinkId) { + console.warn('Browser does not support speaker selection') +} +``` + +**Events Emitted:** Calling this method triggers events: +- `speaker.updated` - When speaker successfully changes +- `speaker.disconnected` - When current speaker is physically disconnected + +**Automatic Fallback:** If the currently selected speaker is disconnected (unplugged), the OS and SDK automatically fall back to the default speaker, and a `speaker.updated` event is emitted. + +**Device IDs:** Speaker device IDs can be obtained from: +```typescript +const devices = await navigator.mediaDevices.enumerateDevices() +const audioOutputs = devices.filter(d => d.kind === 'audiooutput') +``` + +**Permissions:** Unlike camera and microphone, speaker selection typically doesn't require explicit user permission. However, device enumeration (getting device labels) may require media permissions. diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/video-mute.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/video-mute.mdx new file mode 100644 index 00000000..179270ea --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/video-mute.mdx @@ -0,0 +1,200 @@ +--- +title: videoMute +slug: /call-session/videomute +--- + +import APIField from '@site/src/components/APIField'; + +Mutes the video for yourself or another participant in the call. + +## Parameters + + + Configuration for the mute operation. + + + + Member to mute. Omit to mute your own video, provide specific ID to mute another member. + + +## Signature + +```typescript +videoMute(params?: MemberCommandParams): Promise +``` + +## Returns + +**Type:** `Promise` + +## Permissions + +- **Mute yourself:** Requires `call.capabilities.self.muteVideo.on` +- **Mute another member:** Requires `call.capabilities.member.muteVideo.on` + +## Events + +- `member.updated` - General member update event +- `member.updated.videoMuted` - Specific event for video mute state change + +## Important Notes + +**Member Targeting:** + +The `memberId` parameter follows the standard member targeting pattern: +- **No parameter** - Mutes your own video +- **Specific member ID** - Mutes that member's video (requires permission) + +```typescript +// Mute your own video +await call.videoMute() + +// Mute a specific member's video +await call.videoMute({ memberId: 'de550c0c-3fac-4efd-b06f-b5b8614b8966' }) +``` + +**Visual Effect:** + +When video is muted, participants will see a placeholder image instead of the video stream. + +## Examples + +### Mute your own video + +```typescript +// Mute your own video +await call.videoMute() +console.log('Your video is now muted') +``` + +### Mute another participant's video + +```typescript +// Get the list of members +const { members } = await call.getMembers() + +// Find a specific member +const member = members.find(m => m.name === 'John Doe') + +if (member) { + // Mute that member's video (requires permission) + await call.videoMute({ memberId: member.id }) + console.log(`Muted ${member.name}'s video`) +} +``` + +### Toggle video on/off + +```typescript +let isVideoMuted = false + +async function toggleVideo() { + if (isVideoMuted) { + await call.videoUnmute() + console.log('Video enabled') + } else { + await call.videoMute() + console.log('Video disabled') + } + isVideoMuted = !isVideoMuted +} + +// Toggle video +toggleVideo() +``` + +### With event listeners + +```typescript +// Listen for video mute events +call.on('member.updated.videoMuted', (event) => { + const status = event.member.video_muted ? 'muted' : 'unmuted' + console.log(`Member ${event.member.id} video is ${status}`) +}) + +// Mute your video +await call.videoMute() +``` + +### Check capabilities before muting + +```typescript +// Check if you can mute your own video +if (call.capabilities?.self.muteVideo.on) { + await call.videoMute() + console.log('Successfully muted your video') +} else { + console.log('You do not have permission to mute your video') +} + +// Check if you can mute other members' video +if (call.capabilities?.member.muteVideo.on) { + await call.videoMute({ memberId: 'member-id' }) + console.log('Successfully muted member video') +} else { + console.log('You do not have permission to mute other members') +} +``` + +### Provide UI feedback + +```typescript +const videoButton = document.getElementById('video-btn') + +videoButton.addEventListener('click', async () => { + const isVideoMuted = videoButton.classList.contains('muted') + + try { + if (isVideoMuted) { + await call.videoUnmute() + videoButton.textContent = 'Stop Video' + videoButton.classList.remove('muted') + } else { + await call.videoMute() + videoButton.textContent = 'Start Video' + videoButton.classList.add('muted') + } + } catch (error) { + console.error('Failed to toggle video:', error) + } +}) +``` + +### Track video state + +```typescript +let isVideoMuted = false + +call.on('member.updated.videoMuted', (event) => { + if (event.member.id === call.memberId) { + isVideoMuted = event.member.video_muted + updateVideoUI(isVideoMuted) + } +}) + +function updateVideoUI(muted) { + const button = document.getElementById('video-btn') + button.textContent = muted ? 'Start Video' : 'Stop Video' + button.classList.toggle('muted', muted) +} +``` + +### Handle video element visibility + +```typescript +call.on('member.updated.videoMuted', (event) => { + const videoElement = document.getElementById(`video-${event.member.id}`) + + if (videoElement) { + if (event.member.video_muted) { + // Show placeholder when video is muted + videoElement.style.display = 'none' + showPlaceholder(event.member.id) + } else { + // Show video when unmuted + videoElement.style.display = 'block' + hidePlaceholder(event.member.id) + } + } +}) +``` diff --git a/website/docs/signalwire-sdk/tech-ref/call-session/methods/video-unmute.mdx b/website/docs/signalwire-sdk/tech-ref/call-session/methods/video-unmute.mdx new file mode 100644 index 00000000..0985dc3e --- /dev/null +++ b/website/docs/signalwire-sdk/tech-ref/call-session/methods/video-unmute.mdx @@ -0,0 +1,221 @@ +--- +title: videoUnmute +slug: /call-session/videounmute +--- + +import APIField from '@site/src/components/APIField'; + +Unmutes the video for yourself or another participant in the call. + +## Parameters + + + Configuration for the unmute operation. + + + + Member to unmute. Omit to unmute your own video, provide specific ID to unmute another member. + + +## Signature + +```typescript +videoUnmute(params?: MemberCommandParams): Promise +``` + +## Returns + +**Type:** `Promise` + +## Permissions + +- **Unmute yourself:** Requires `call.capabilities.self.muteVideo.off` +- **Unmute another member:** Requires `call.capabilities.member.muteVideo.off` + +## Events + +- `member.updated` - General member update event +- `member.updated.videoMuted` - Specific event for video mute state change + +## Important Notes + +**Member Targeting:** + +The `memberId` parameter follows the standard member targeting pattern: +- **No parameter** - Unmutes your own video +- **Specific member ID** - Unmutes that member's video (requires permission) + +```typescript +// Unmute your own video +await call.videoUnmute() + +// Unmute a specific member's video +await call.videoUnmute({ memberId: 'de550c0c-3fac-4efd-b06f-b5b8614b8966' }) +``` + +**Visual Effect:** + +When video is unmuted, the live video stream replaces the placeholder image. + +## Examples + +### Unmute your own video + +```typescript +// Unmute your own video +await call.videoUnmute() +console.log('Your video is now unmuted') +``` + +### Unmute another participant's video + +```typescript +// Get the list of members +const { members } = await call.getMembers() + +// Find a specific member +const member = members.find(m => m.name === 'John Doe') + +if (member) { + // Unmute that member's video (requires permission) + await call.videoUnmute({ memberId: member.id }) + console.log(`Unmuted ${member.name}'s video`) +} +``` + +### Toggle video on/off + +```typescript +let isVideoMuted = false + +async function toggleVideo() { + if (isVideoMuted) { + await call.videoUnmute() + console.log('Video enabled') + } else { + await call.videoMute() + console.log('Video disabled') + } + isVideoMuted = !isVideoMuted +} + +// Toggle video +toggleVideo() +``` + +### With event listeners + +```typescript +// Listen for video unmute events +call.on('member.updated.videoMuted', (event) => { + if (!event.member.video_muted) { + console.log(`Member ${event.member.id} turned on their video`) + } +}) + +// Unmute your video +await call.videoUnmute() +``` + +### Check capabilities before unmuting + +```typescript +// Check if you can unmute your own video +if (call.capabilities?.self.muteVideo.off) { + await call.videoUnmute() + console.log('Successfully unmuted your video') +} else { + console.log('You do not have permission to unmute your video') +} + +// Check if you can unmute other members' video +if (call.capabilities?.member.muteVideo.off) { + await call.videoUnmute({ memberId: 'member-id' }) + console.log('Successfully unmuted member video') +} else { + console.log('You do not have permission to unmute other members') +} +``` + +### Provide UI feedback + +```typescript +const videoButton = document.getElementById('video-btn') + +videoButton.addEventListener('click', async () => { + const isVideoMuted = videoButton.classList.contains('muted') + + try { + if (isVideoMuted) { + await call.videoUnmute() + videoButton.textContent = 'Stop Video' + videoButton.classList.remove('muted') + } else { + await call.videoMute() + videoButton.textContent = 'Start Video' + videoButton.classList.add('muted') + } + } catch (error) { + console.error('Failed to toggle video:', error) + } +}) +``` + +### Track video state with events + +```typescript +let isVideoMuted = false + +call.on('member.updated.videoMuted', (event) => { + if (event.member.id === call.memberId) { + isVideoMuted = event.member.video_muted + updateVideoUI(isVideoMuted) + } +}) + +function updateVideoUI(muted) { + const button = document.getElementById('video-btn') + button.textContent = muted ? 'Start Video' : 'Stop Video' + button.classList.toggle('muted', muted) +} +``` + +### Request camera permissions when unmuting + +```typescript +async function unmuteVideo() { + try { + // Ensure camera access before unmuting + const stream = await navigator.mediaDevices.getUserMedia({ video: true }) + + // Unmute video + await call.videoUnmute() + console.log('Video unmuted') + } catch (error) { + console.error('Camera access denied or unmute failed:', error) + } +} +``` + +### Handle video element display + +```typescript +call.on('member.updated.videoMuted', (event) => { + const videoElement = document.getElementById(`video-${event.member.id}`) + + if (videoElement) { + if (event.member.video_muted) { + // Show placeholder when video is muted + videoElement.style.display = 'none' + showPlaceholder(event.member.id) + } else { + // Show video stream when unmuted + videoElement.style.display = 'block' + hidePlaceholder(event.member.id) + } + } +}) + +// Unmute video +await call.videoUnmute() +``` diff --git a/website/docs/signalwire-sdk/tech-ref/notifications.mdx b/website/docs/signalwire-sdk/tech-ref/notifications.mdx index f4f530a3..71361c35 100644 --- a/website/docs/signalwire-sdk/tech-ref/notifications.mdx +++ b/website/docs/signalwire-sdk/tech-ref/notifications.mdx @@ -29,10 +29,10 @@ client.online({ When an incoming call arrives, the `websocket` handler receives a `callInvite` object containing: - `callInvite.invite.details` - caller information (name, number) -- `callInvite.invite.accept()` - accepts the call and returns a [`CallSession`](./CallSession) +- `callInvite.invite.accept()` - accepts the call and returns a [`CallSession`](/sdks/signalwire-sdk/call-session) - `callInvite.invite.reject()` - rejects the call -[`incomingCallHandlers`]: ./Types#incomingcallhandlers +[`incomingCallHandlers`]: /sdks/signalwire-sdk/types#incomingcallhandlers ## Push Notifications @@ -42,7 +42,7 @@ The SignalWire platform can be configured to send push notifications to a subscr When the subscriber's device receives a push notification, it will make the device ring and forward the notification to your application. Your application should set up an audio/video link if the user accepts the call using the -[`handlePushNotification`](./Types#handlepushnotificationresult) method. +[`handlePushNotification`](/sdks/signalwire-sdk/types#handlepushnotificationresult) method. The push notification is only sent if there is no active WebSocket connection to the subscriber at that moment. ## Setup diff --git a/website/src/components/APIField/index.tsx b/website/src/components/APIField/index.tsx new file mode 100644 index 00000000..e52b0604 --- /dev/null +++ b/website/src/components/APIField/index.tsx @@ -0,0 +1,124 @@ +import React, { ReactNode } from 'react'; +import clsx from 'clsx'; +import Link from '@docusaurus/Link'; +import styles from './styles.module.css'; + +export interface APIFieldProps { + /** + * The name of the parameter/field + */ + name: string; + /** + * The type of the parameter (e.g., 'string', 'number', 'boolean', 'Promise') + * Supports markdown-style links: [TypeName](/path/to/type#anchor) + */ + type: string; + /** + * Whether the parameter is required + */ + required?: boolean; + /** + * Default value for the parameter + */ + default?: string; + /** + * Whether the parameter is deprecated + */ + deprecated?: boolean; + /** + * Description of the parameter (can include markdown/JSX) + */ + children?: ReactNode; +} + +export const APIField: React.FC = ({ + name, + type, + required = false, + default: defaultValue, + deprecated = false, + children +}: APIFieldProps) => { + // Create a URL-friendly ID from the field name + const fieldId = `field-${name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`; + + const handleAnchorClick = (e: React.MouseEvent) => { + e.preventDefault(); + + // Update URL and jump to location (like header anchors) + window.location.hash = fieldId; + + // Copy to clipboard + navigator.clipboard.writeText(window.location.href).then(() => { + // Optional: Show a toast/notification that link was copied + console.log('Link copied to clipboard'); + }).catch(err => { + console.error('Failed to copy link:', err); + }); + }; + + // Parse markdown-style links in type string: [text](url) + const parseTypeLinks = (typeString: string) => { + const parts: (string | React.ReactElement)[] = []; + let lastIndex = 0; + const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; + let match; + + while ((match = linkRegex.exec(typeString)) !== null) { + // Add text before the link + if (match.index > lastIndex) { + parts.push(typeString.substring(lastIndex, match.index)); + } + + // Add the link + parts.push( + + {match[1]} + + ); + + lastIndex = match.index + match[0].length; + } + + // Add remaining text + if (lastIndex < typeString.length) { + parts.push(typeString.substring(lastIndex)); + } + + return parts.length > 0 ? parts : typeString; + }; + + return ( +
+
+
+ + + + + + + {name} + +
+ + {parseTypeLinks(type)} + + {required && required} + {deprecated && deprecated} +
+ {defaultValue && ( +
+ Default: {defaultValue} +
+ )} + {children && ( +
+ {children} +
+ )} +
+ ); +}; + +export default APIField; diff --git a/website/src/components/APIField/styles.module.css b/website/src/components/APIField/styles.module.css new file mode 100644 index 00000000..2bce65c1 --- /dev/null +++ b/website/src/components/APIField/styles.module.css @@ -0,0 +1,152 @@ +/** + * APIField component styles + * Inspired by Mintlify's ResponseField component + */ + +.apiField { + padding: 1rem 0; + border-bottom: 1px solid var(--ifm-color-emphasis-200); +} + +.apiField:last-child { + border-bottom: none; +} + +.deprecatedField { + opacity: 0.7; +} + +/* Field Header - contains name, type, and badges */ +.fieldHeader { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.5rem; + flex-wrap: wrap; + position: relative; +} + +/* Field Name Container with anchor */ +.fieldNameContainer { + display: flex; + align-items: center; + gap: 0.5rem; + position: relative; +} + +/* Anchor Link */ +.fieldAnchor { + position: absolute; + left: -1.75rem; + display: flex; + align-items: center; + justify-content: center; + width: 1.25rem; + height: 1.25rem; + opacity: 0; + color: var(--ifm-color-primary); + transition: opacity 0.2s ease; + text-decoration: none; +} + +.fieldNameContainer:hover .fieldAnchor, +.fieldAnchor:focus { + opacity: 1; +} + +/* Field Name */ +.fieldName { + font-weight: 600; + font-size: 1rem; + color: var(--ifm-color-success); + font-family: var(--ifm-font-family-monospace); + cursor: pointer; +} + +/* Deprecated name with strikethrough */ +.deprecatedName { + text-decoration: line-through; + color: var(--ifm-color-emphasis-600); +} + +/* Field Type */ +.fieldType { + font-family: var(--ifm-font-family-monospace); + font-size: 0.875rem; + color: var(--ifm-color-emphasis-700); + background-color: var(--ifm-color-emphasis-100); + padding: 0.125rem 0.5rem; + border-radius: 4px; +} + +/* Badges (required, deprecated) */ +.fieldBadge { + font-size: 0.75rem; + font-weight: 600; + padding: 0.125rem 0.5rem; + border-radius: 4px; + text-transform: lowercase; + background-color: var(--ifm-color-danger-contrast-background); + color: var(--ifm-color-danger-contrast-foreground); +} + +.deprecatedBadge { + background-color: var(--ifm-color-warning-contrast-background); + color: var(--ifm-color-warning-contrast-foreground); +} + +/* Default Value */ +.fieldDefault { + margin-bottom: 0.5rem; + font-size: 0.875rem; + color: var(--ifm-color-emphasis-700); +} + +.defaultLabel { + font-weight: 600; + margin-right: 0.25rem; +} + +.fieldDefault code { + background-color: var(--ifm-color-emphasis-100); + padding: 0.125rem 0.375rem; + border-radius: 3px; + font-size: 0.875rem; +} + +/* Field Description */ +.fieldDescription { + color: var(--ifm-color-emphasis-800); + font-size: 0.9375rem; + line-height: 1.6; +} + +.fieldDescription p:first-child { + margin-top: 0; +} + +.fieldDescription p:last-child { + margin-bottom: 0; +} + +/* Dark mode adjustments */ +[data-theme='dark'] .fieldName { + color: var(--ifm-color-success-light); +} + +[data-theme='dark'] .fieldAnchor { + color: var(--ifm-color-primary); +} + +[data-theme='dark'] .fieldType { + background-color: var(--ifm-color-emphasis-200); + color: var(--ifm-color-emphasis-900); +} + +[data-theme='dark'] .fieldDefault code { + background-color: var(--ifm-color-emphasis-200); +} + +[data-theme='dark'] .deprecatedName { + color: var(--ifm-color-emphasis-500); +}