Skip to content

Composables API Reference

Complete reference for all VueSip composables providing reactive SIP functionality.

Table of Contents


Core Composables

useSipClient

Provides a Vue composable interface for managing SIP client connections, registration, and configuration with reactive state management.

Source: src/composables/useSipClient.ts

Signature

typescript
function useSipClient(
  initialConfig?: SipClientConfig,
  options?: {
    eventBus?: EventBus
    autoConnect?: boolean
    autoCleanup?: boolean
    reconnectDelay?: number
    connectionTimeout?: number
  }
): UseSipClientReturn

Parameters

ParameterTypeDefaultDescription
initialConfigSipClientConfigundefinedOptional initial SIP client configuration
options.eventBusEventBusnew instanceShared event bus instance
options.autoConnectbooleanfalseAuto-connect on mount
options.autoCleanupbooleantrueAuto-cleanup on unmount
options.reconnectDelaynumber1000Reconnect delay in milliseconds
options.connectionTimeoutnumber30000Connection timeout in milliseconds

Returns: UseSipClientReturn

Reactive State
PropertyTypeDescription
isConnectedComputedRef<boolean>Whether client is connected to SIP server
isRegisteredComputedRef<boolean>Whether client is registered with SIP server
connectionStateComputedRef<ConnectionState>Current connection state
registrationStateComputedRef<RegistrationState>Current registration state
registeredUriComputedRef<string | null>Registered SIP URI
errorRef<Error | null>Current error message
isConnectingComputedRef<boolean>Whether client is connecting
isDisconnectingRef<boolean>Whether client is disconnecting
isStartedComputedRef<boolean>Whether client is started
Methods
MethodSignatureDescription
connect() => Promise<void>Start the SIP client and connect to server
disconnect() => Promise<void>Disconnect from SIP server and stop the client
register() => Promise<void>Register with SIP server
unregister() => Promise<void>Unregister from SIP server
updateConfig(config: Partial<SipClientConfig>) => ValidationResultUpdate SIP client configuration
reconnect() => Promise<void>Reconnect to SIP server
getClient() => SipClient | nullGet the underlying SIP client instance
getEventBus() => EventBusGet the event bus instance

Usage Example

typescript
import { useSipClient } from '@/composables/useSipClient'

const { isConnected, isRegistered, connect, disconnect, register } = useSipClient(
  {
    uri: 'sip:alice@domain.com',
    password: 'secret',
    wsServers: ['wss://sip.domain.com:7443'],
  },
  {
    autoConnect: true,
    autoCleanup: true,
  }
)

// Connect and register
await connect()
await register()

// Check status
if (isConnected.value && isRegistered.value) {
  console.log('Ready to make calls')
}

useSipRegistration

Provides reactive SIP registration state management with automatic refresh, retry logic, and expiry tracking.

Source: src/composables/useSipRegistration.ts

Signature

typescript
function useSipRegistration(
  sipClient: Ref<SipClient | null>,
  options?: RegistrationOptions
): UseSipRegistrationReturn

Parameters

ParameterTypeDefaultDescription
sipClientRef<SipClient | null>-SIP client instance
options.expiresnumber600Registration expiry time in seconds
options.maxRetriesnumber3Maximum retry attempts before giving up
options.autoRefreshbooleantrueEnable automatic re-registration before expiry
options.userAgentstringundefinedCustom User-Agent header

Returns: UseSipRegistrationReturn

Reactive State
PropertyTypeDescription
stateRef<RegistrationState>Current registration state
registeredUriRef<string | null>Registered SIP URI
isRegisteredComputedRef<boolean>Whether currently registered
isRegisteringComputedRef<boolean>Whether registration is in progress
isUnregisteringComputedRef<boolean>Whether unregistration is in progress
hasRegistrationFailedComputedRef<boolean>Whether registration has failed
expiresRef<number>Registration expiry time in seconds
lastRegistrationTimeRef<Date | null>Timestamp when registration was last successful
expiryTimeRef<Date | null>Timestamp when registration will expire
secondsUntilExpiryComputedRef<number>Seconds remaining until registration expires
isExpiringSoonComputedRef<boolean>Whether registration is about to expire (less than 30 seconds)
hasExpiredComputedRef<boolean>Whether registration has expired
retryCountRef<number>Number of registration retry attempts
lastErrorRef<string | null>Last registration error message
Methods
MethodSignatureDescription
register() => Promise<void>Register with SIP server
unregister() => Promise<void>Unregister from SIP server
refresh() => Promise<void>Manually refresh registration
resetRetries() => voidReset retry count
getStatistics() => RegistrationStatisticsGet registration statistics

Usage Example

typescript
import { useSipRegistration } from '@/composables/useSipRegistration'

const { isRegistered, register, unregister, secondsUntilExpiry } = useSipRegistration(sipClient, {
  expires: 600,
  maxRetries: 3,
  autoRefresh: true,
})

// Register
await register()

// Check status
if (isRegistered.value) {
  console.log(`Registered, expires in ${secondsUntilExpiry.value}s`)
}

// Unregister
await unregister()

useCallSession

Provides reactive call session management with support for outgoing/incoming calls, call controls (hold, mute, DTMF), media streams, and call statistics.

Source: src/composables/useCallSession.ts

Signature

typescript
function useCallSession(
  sipClient: Ref<SipClient | null>,
  mediaManager?: Ref<MediaManager | null>
): UseCallSessionReturn

Parameters

ParameterTypeDescription
sipClientRef<SipClient | null>SIP client instance
mediaManagerRef<MediaManager | null>Optional media manager instance

Returns: UseCallSessionReturn

Reactive State
PropertyTypeDescription
sessionRef<CallSession | null>Active call session
stateComputedRef<CallState>Call state
callIdComputedRef<string | null>Call ID
directionComputedRef<CallDirection | null>Call direction (incoming/outgoing)
localUriComputedRef<string | null>Local SIP URI
remoteUriComputedRef<string | null>Remote SIP URI
remoteDisplayNameComputedRef<string | null>Remote display name
isActiveComputedRef<boolean>Is call active
isOnHoldComputedRef<boolean>Is on hold
isMutedComputedRef<boolean>Is muted
hasRemoteVideoComputedRef<boolean>Has remote video
hasLocalVideoComputedRef<boolean>Has local video
localStreamComputedRef<MediaStream | null>Local media stream
remoteStreamComputedRef<MediaStream | null>Remote media stream
timingComputedRef<CallTimingInfo>Call timing information
durationComputedRef<number>Call duration in seconds (if active)
terminationCauseComputedRef<TerminationCause | undefined>Termination cause (if ended)
Methods
MethodSignatureDescription
makeCall(target: string, options?: CallSessionOptions) => Promise<void>Make an outgoing call
answer(options?: AnswerOptions) => Promise<void>Answer an incoming call
reject(statusCode?: number) => Promise<void>Reject an incoming call
hangup() => Promise<void>Hangup the call
hold() => Promise<void>Put call on hold
unhold() => Promise<void>Resume call from hold
toggleHold() => Promise<void>Toggle hold state
mute() => voidMute audio
unmute() => voidUnmute audio
toggleMute() => voidToggle mute state
sendDTMF(tone: string, options?: DTMFOptions) => Promise<void>Send DTMF tone
getStats() => Promise<CallStatistics | null>Get call statistics
clearSession() => voidClear current session

Usage Example

typescript
import { useCallSession } from '@/composables/useCallSession'

const { state, makeCall, answer, hangup, hold, mute, sendDTMF } = useCallSession(
  sipClient,
  mediaManager
)

// Make a call
await makeCall('sip:bob@domain.com', { audio: true, video: false })

// Answer incoming call
await answer()

// Put on hold
await hold()

// Mute
mute()

// Send DTMF
await sendDTMF('1')

// Hangup
await hangup()

useMediaDevices

Provides reactive media device management with device enumeration, selection, permission handling, and device testing capabilities.

Source: src/composables/useMediaDevices.ts

Signature

typescript
function useMediaDevices(
  mediaManager?: Ref<MediaManager | null>,
  options?: {
    autoEnumerate?: boolean
    autoMonitor?: boolean
  }
): UseMediaDevicesReturn

Parameters

ParameterTypeDefaultDescription
mediaManagerRef<MediaManager | null>undefinedOptional media manager instance
options.autoEnumeratebooleantrueAuto-enumerate devices on mount
options.autoMonitorbooleantrueAuto-monitor device changes

Returns: UseMediaDevicesReturn

Reactive State
PropertyTypeDescription
audioInputDevicesComputedRef<readonly MediaDevice[]>Audio input devices
audioOutputDevicesComputedRef<readonly MediaDevice[]>Audio output devices
videoInputDevicesComputedRef<readonly MediaDevice[]>Video input devices
allDevicesComputedRef<readonly MediaDevice[]>All devices
selectedAudioInputIdRef<string | null>Selected audio input device ID
selectedAudioOutputIdRef<string | null>Selected audio output device ID
selectedVideoInputIdRef<string | null>Selected video input device ID
selectedAudioInputDeviceComputedRef<MediaDevice | undefined>Selected audio input device
selectedAudioOutputDeviceComputedRef<MediaDevice | undefined>Selected audio output device
selectedVideoInputDeviceComputedRef<MediaDevice | undefined>Selected video input device
audioPermissionComputedRef<PermissionStatus>Audio permission status
videoPermissionComputedRef<PermissionStatus>Video permission status
hasAudioPermissionComputedRef<boolean>Has audio permission
hasVideoPermissionComputedRef<boolean>Has video permission
hasAudioInputDevicesComputedRef<boolean>Has audio input devices
hasAudioOutputDevicesComputedRef<boolean>Has audio output devices
hasVideoInputDevicesComputedRef<boolean>Has video input devices
totalDevicesComputedRef<number>Total device count
isEnumeratingRef<boolean>Is enumerating devices
lastErrorRef<Error | null>Last error
Methods
MethodSignatureDescription
enumerateDevices() => Promise<MediaDevice[]>Enumerate all media devices
requestAudioPermission() => Promise<boolean>Request audio permission
requestVideoPermission() => Promise<boolean>Request video permission
requestPermissions(audio?: boolean, video?: boolean) => Promise<void>Request permissions
selectAudioInput(deviceId: string) => voidSelect audio input device
selectAudioOutput(deviceId: string) => voidSelect audio output device
selectVideoInput(deviceId: string) => voidSelect video input device
testAudioInput(deviceId?: string, options?: DeviceTestOptions) => Promise<boolean>Test audio input device
testAudioOutput(deviceId?: string) => Promise<boolean>Test audio output device
getDeviceById(deviceId: string) => MediaDevice | undefinedGet device by ID
getDevicesByKind(kind: MediaDeviceKind) => readonly MediaDevice[]Get devices by kind
startDeviceChangeMonitoring() => voidStart device change monitoring
stopDeviceChangeMonitoring() => voidStop device change monitoring

Usage Example

typescript
import { useMediaDevices } from '@/composables/useMediaDevices'

const {
  audioInputDevices,
  selectedAudioInputId,
  enumerateDevices,
  requestPermissions,
  selectAudioInput,
  testAudioInput,
} = useMediaDevices(mediaManager)

// Request permissions
await requestPermissions(true, false)

// Enumerate devices
await enumerateDevices()

// Select device
if (audioInputDevices.value.length > 0) {
  selectAudioInput(audioInputDevices.value[0].deviceId)
}

// Test device
const success = await testAudioInput()

useDTMF

Provides DTMF (Dual-Tone Multi-Frequency) tone sending functionality for active call sessions with support for tone sequences and queue management.

Source: src/composables/useDTMF.ts

Signature

typescript
function useDTMF(session: Ref<CallSession | null>): UseDTMFReturn

Parameters

ParameterTypeDescription
sessionRef<CallSession | null>Call session instance

Returns: UseDTMFReturn

Reactive State
PropertyTypeDescription
isSendingRef<boolean>Is currently sending DTMF
queuedTonesRef<string[]>Queued tones
lastSentToneRef<string | null>Last sent tone
lastResultRef<DTMFSendResult | null>Last send result
tonesSentCountRef<number>Total tones sent
queueSizeComputedRef<number>Queue size
isQueueEmptyComputedRef<boolean>Is queue empty
Methods
MethodSignatureDescription
sendTone(tone: string, options?: DTMFOptions) => Promise<void>Send a single DTMF tone
sendToneSequence(tones: string, options?: DTMFSequenceOptions) => Promise<void>Send a sequence of DTMF tones
queueTone(tone: string) => voidQueue a tone for sending
queueToneSequence(tones: string) => voidQueue multiple tones for sending
processQueue(options?: DTMFSequenceOptions) => Promise<void>Process the tone queue
clearQueue() => voidClear the tone queue
stopSending() => voidStop sending (clear queue and cancel current)
resetStats() => voidReset statistics

Usage Example

typescript
import { useDTMF } from '@/composables/useDTMF'

const { sendTone, sendToneSequence, isSending, queuedTones, queueToneSequence, processQueue } =
  useDTMF(session)

// Send single tone
await sendTone('1')

// Send sequence
await sendToneSequence('1234#', {
  duration: 100,
  interToneGap: 70,
  onToneSent: (tone) => console.log(`Sent: ${tone}`),
})

// Queue tones
queueToneSequence('5678')
await processQueue()

Advanced Composables

useCallHistory

Provides reactive call history management with filtering, searching, export functionality, and persistence to IndexedDB.

Source: src/composables/useCallHistory.ts

Signature

typescript
function useCallHistory(): UseCallHistoryReturn

Returns: UseCallHistoryReturn

Reactive State
PropertyTypeDescription
historyComputedRef<readonly CallHistoryEntry[]>All call history entries
filteredHistoryComputedRef<readonly CallHistoryEntry[]>Filtered history entries
totalCallsComputedRef<number>Total number of calls in history
missedCallsCountComputedRef<number>Total number of missed calls
currentFilterRef<HistoryFilter | null>Current filter
Methods
MethodSignatureDescription
getHistory(filter?: HistoryFilter) => HistorySearchResultGet history with optional filter
searchHistory(query: string, filter?: HistoryFilter) => HistorySearchResultSearch history by query
clearHistory() => Promise<void>Clear all history
deleteEntry(entryId: string) => Promise<void>Delete a specific entry
exportHistory(options: HistoryExportOptions) => Promise<void>Export history to file
getStatistics(filter?: HistoryFilter) => HistoryStatisticsGet history statistics
setFilter(filter: HistoryFilter | null) => voidSet current filter
getMissedCalls() => readonly CallHistoryEntry[]Get missed calls only
getRecentCalls(limit?: number) => readonly CallHistoryEntry[]Get recent calls (last N)

Usage Example

typescript
import { useCallHistory } from '@/composables/useCallHistory'

const { history, filteredHistory, searchHistory, exportHistory, getStatistics } = useCallHistory()

// Search history
const results = searchHistory('john')

// Get statistics
const stats = getStatistics()
console.log(`Total calls: ${stats.totalCalls}`)

// Export to CSV
await exportHistory({ format: 'csv', filename: 'my-calls' })

useCallControls

Provides advanced call control features including blind/attended transfers, call forwarding, and basic conference management.

Source: src/composables/useCallControls.ts

Signature

typescript
function useCallControls(sipClient: Ref<SipClient | null>): UseCallControlsReturn

Parameters

ParameterTypeDescription
sipClientRef<SipClient | null>SIP client instance

Returns: UseCallControlsReturn

Reactive State
PropertyTypeDescription
activeTransferRef<ActiveTransfer | null>Active transfer (if any)
transferStateComputedRef<TransferState>Transfer state
isTransferringComputedRef<boolean>Whether a transfer is in progress
consultationCallRef<CallSession | null>Consultation call for attended transfer
Methods
MethodSignatureDescription
blindTransfer(callId: string, targetUri: string, extraHeaders?: string[]) => Promise<void>Perform blind transfer
initiateAttendedTransfer(callId: string, targetUri: string) => Promise<string>Initiate attended transfer (creates consultation call)
completeAttendedTransfer() => Promise<void>Complete attended transfer (connect call to consultation call)
cancelTransfer() => Promise<void>Cancel active transfer
forwardCall(callId: string, targetUri: string) => Promise<void>Forward call to target URI
getTransferProgress() => TransferProgress | nullGet transfer progress
onTransferEvent(callback: (event: TransferEvent) => void) => () => voidListen for transfer events

Usage Example

typescript
import { useCallControls } from '@/composables/useCallControls'

const { blindTransfer, initiateAttendedTransfer, completeAttendedTransfer, isTransferring } =
  useCallControls(sipClient)

// Blind transfer
await blindTransfer('call-123', 'sip:transfer-target@domain.com')

// Attended transfer
const consultationCallId = await initiateAttendedTransfer('call-123', 'sip:consult@domain.com')
// ... talk to consultation target ...
await completeAttendedTransfer()

usePresence

Provides SIP presence (SUBSCRIBE/NOTIFY) functionality for tracking user status (available, away, busy, offline) and watching other users' presence.

Source: src/composables/usePresence.ts

Signature

typescript
function usePresence(sipClient: Ref<SipClient | null>): UsePresenceReturn

Parameters

ParameterTypeDescription
sipClientRef<SipClient | null>SIP client instance

Returns: UsePresenceReturn

Reactive State
PropertyTypeDescription
currentStatusRef<PresenceStatus | null>Current user's presence status
watchedUsersRef<Map<string, PresenceStatus>>Map of watched users and their presence status
subscriptionsRef<Map<string, PresenceSubscription>>Active subscriptions
currentStateComputedRef<PresenceState>Current presence state
subscriptionCountComputedRef<number>Number of active subscriptions
Methods
MethodSignatureDescription
setStatus(state: PresenceState, options?: PresencePublishOptions) => Promise<void>Set own presence status
subscribe(uri: string, options?: PresenceSubscriptionOptions) => Promise<string>Subscribe to user's presence
unsubscribe(uri: string) => Promise<void>Unsubscribe from user's presence
getStatus(uri: string) => PresenceStatus | nullGet presence status for a specific user
unsubscribeAll() => Promise<void>Unsubscribe from all watched users
onPresenceEvent(callback: (event: PresenceEvent) => void) => () => voidListen for presence events

Usage Example

typescript
import { usePresence } from '@/composables/usePresence'
import { PresenceState } from '@/types/presence.types'

const { setStatus, subscribe, watchedUsers } = usePresence(sipClient)

// Set own status
await setStatus(PresenceState.Available, {
  statusMessage: 'Working on project',
})

// Watch another user
await subscribe('sip:alice@domain.com')

// Check their status
watchedUsers.value.forEach((status, uri) => {
  console.log(`${uri}: ${status.state}`)
})

useMessaging

Provides SIP MESSAGE functionality for sending and receiving instant messages via SIP protocol, with support for delivery notifications and composing indicators.

Source: src/composables/useMessaging.ts

Signature

typescript
function useMessaging(sipClient: Ref<SipClient | null>): UseMessagingReturn

Parameters

ParameterTypeDescription
sipClientRef<SipClient | null>SIP client instance

Returns: UseMessagingReturn

Reactive State
PropertyTypeDescription
messagesRef<Message[]>All messages
conversationsComputedRef<Map<string, Conversation>>Conversations grouped by URI
unreadCountComputedRef<number>Total unread message count
composingIndicatorsRef<Map<string, ComposingIndicator>>Composing indicators
Methods
MethodSignatureDescription
sendMessage(to: string, content: string, options?: MessageSendOptions) => Promise<string>Send a message
markAsRead(messageId: string) => voidMark message as read
markAllAsRead(uri?: string) => voidMark all messages from a URI as read
deleteMessage(messageId: string) => voidDelete a message
clearMessages(uri?: string) => voidClear all messages
getMessagesForUri(uri: string) => Message[]Get messages for a specific URI
getFilteredMessages(filter: MessageFilter) => Message[]Get filtered messages
sendComposingIndicator(to: string, isComposing: boolean) => Promise<void>Send composing indicator
onMessagingEvent(callback: (event: MessagingEvent) => void) => () => voidListen for messaging events

Usage Example

typescript
import { useMessaging } from '@/composables/useMessaging'

const { sendMessage, messages, unreadCount } = useMessaging(sipClient)

// Send a message
const messageId = await sendMessage('sip:alice@domain.com', 'Hello!')

// Check unread count
console.log(`Unread messages: ${unreadCount.value}`)

// Mark as read
markAllAsRead('sip:alice@domain.com')

useConference

Provides conference call functionality for managing multi-party calls with participant management, audio level monitoring, and conference controls.

Source: src/composables/useConference.ts

Signature

typescript
function useConference(sipClient: Ref<SipClient | null>): UseConferenceReturn

Parameters

ParameterTypeDescription
sipClientRef<SipClient | null>SIP client instance

Returns: UseConferenceReturn

Reactive State
PropertyTypeDescription
conferenceRef<ConferenceStateInterface | null>Current conference state
stateComputedRef<ConferenceState>Current state of the conference
participantsComputedRef<Participant[]>Array of all participants
localParticipantComputedRef<Participant | null>The local participant (self)
participantCountComputedRef<number>Total number of participants
isActiveComputedRef<boolean>Whether the conference is active
isLockedComputedRef<boolean>Whether the conference is locked
isRecordingComputedRef<boolean>Whether the conference is being recorded
Methods
MethodSignatureDescription
createConference(options?: ConferenceOptions) => Promise<string>Create a new conference
joinConference(conferenceUri: string, options?: ConferenceOptions) => Promise<void>Join an existing conference
addParticipant(uri: string, displayName?: string) => Promise<string>Add a participant to the conference
removeParticipant(participantId: string, reason?: string) => Promise<void>Remove a participant from the conference
muteParticipant(participantId: string) => Promise<void>Mute a participant's audio
unmuteParticipant(participantId: string) => Promise<void>Unmute a participant's audio
endConference() => Promise<void>End the conference for all participants
lockConference() => Promise<void>Lock the conference
unlockConference() => Promise<void>Unlock the conference
startRecording() => Promise<void>Start recording the conference
stopRecording() => Promise<void>Stop recording the conference
getParticipant(participantId: string) => Participant | nullGet a specific participant by ID
onConferenceEvent(callback: (event: ConferenceEvent) => void) => () => voidListen for conference events

Usage Example

typescript
import { useConference } from '@/composables/useConference'

const { createConference, addParticipant, participants, participantCount, endConference } =
  useConference(sipClient)

// Create a new conference
const confId = await createConference({
  maxParticipants: 10,
  locked: false,
})

// Add participants
await addParticipant('sip:alice@domain.com', 'Alice')
await addParticipant('sip:bob@domain.com', 'Bob')

// Monitor participants
console.log(`Active participants: ${participantCount.value}`)

// End conference when done
await endConference()

Call Quality Composables

Composables for monitoring and adapting to call quality in real-time. These provide quality scoring, network indicators, and bandwidth adaptation recommendations for WebRTC calls.

useCallQualityScore

Provides comprehensive call quality scoring with A-F grading, trend analysis, and weighted metric evaluation.

Source: src/composables/useCallQualityScore.ts

Signature

typescript
function useCallQualityScore(options?: CallQualityScoreOptions): UseCallQualityScoreReturn

Parameters

ParameterTypeDefaultDescription
options.weightsPartial<QualityScoreWeights>See defaultsCustom weight configuration
options.historySizenumber10Number of samples for trend analysis
options.updateIntervalnumber1000Update interval in milliseconds
options.enableTrendAnalysisbooleantrueEnable quality trend calculation
Default Weights
typescript
{
  packetLoss: 0.25,
  jitter: 0.15,
  rtt: 0.2,
  mos: 0.25,
  bitrateStability: 0.15
}

Returns: UseCallQualityScoreReturn

Reactive State
PropertyTypeDescription
scoreRef<CallQualityScore | null>Current quality score (null if no data)
trendRef<QualityTrend | null>Quality trend (null if insufficient history)
historyRef<CallQualityScore[]>Score history for charting
weightsRef<QualityScoreWeights>Current weight configuration
Score Object
typescript
interface CallQualityScore {
  overall: number // 0-100
  audio: number // 0-100
  video: number | null // 0-100 (null for audio-only)
  network: number // 0-100
  grade: QualityGrade // 'A' | 'B' | 'C' | 'D' | 'F'
  description: string // Human-readable description
  timestamp: number
}
Trend Object
typescript
interface QualityTrend {
  direction: 'improving' | 'stable' | 'degrading'
  rate: number // -100 to 100 (negative = degrading)
  confidence: number // 0-1 (higher = more certain)
}
Methods
MethodSignatureDescription
updateScore(input: QualityScoreInput) => voidUpdate score with new WebRTC stats
reset() => voidClear history and reset to initial state

Usage Example

typescript
import { useCallQualityScore } from 'vuesip'

const { score, trend, history, updateScore, reset } = useCallQualityScore({
  historySize: 20,
  enableTrendAnalysis: true,
  weights: {
    packetLoss: 0.3, // Prioritize packet loss
    mos: 0.3,
  },
})

// Update with WebRTC stats (typically in a stats polling interval)
updateScore({
  packetLoss: 0.5,
  jitter: 15,
  rtt: 80,
  mos: 4.2,
  bitrate: 1500,
  previousBitrate: 1480,
  audioPacketLoss: 0.3,
  videoPacketLoss: 0.7,
  framerate: 28,
  targetFramerate: 30,
})

// Check quality
if (score.value) {
  console.log(`Quality: ${score.value.grade} (${score.value.overall}/100)`)
  console.log(score.value.description)
}

// Monitor trend
if (trend.value?.direction === 'degrading') {
  console.warn('Call quality is degrading!')
}

Grade Thresholds

GradeScore RangeDescription
A90-100Excellent quality
B75-89Good quality
C60-74Fair quality
D40-59Poor quality
F0-39Very poor quality

useNetworkQualityIndicator

Provides visual network quality indicators with signal bars, colors, and accessibility labels for displaying connection quality in the UI.

Source: src/composables/useNetworkQualityIndicator.ts

Signature

typescript
function useNetworkQualityIndicator(
  options?: NetworkQualityIndicatorOptions
): UseNetworkQualityIndicatorReturn

Parameters

ParameterTypeDefaultDescription
options.updateIntervalnumber1000Update interval in ms
options.colorsPartial<NetworkQualityColors>See defaultsCustom color scheme
options.estimateBandwidthbooleantrueEnable bandwidth estimation
options.thresholdsPartial<NetworkQualityThresholds>See defaultsCustom quality thresholds
Default Colors
typescript
{
  excellent: '#22c55e', // green-500
  good: '#22c55e',      // green-500
  fair: '#eab308',      // yellow-500
  poor: '#f97316',      // orange-500
  critical: '#ef4444',  // red-500
  unknown: '#9ca3af'    // gray-400
}
Default Thresholds
typescript
{
  rtt: [50, 100, 200, 400],           // ms [excellent, good, fair, poor]
  packetLoss: [0.5, 1, 2, 5],         // % [excellent, good, fair, poor]
  jitter: [10, 20, 40, 80]            // ms [excellent, good, fair, poor]
}

Returns: UseNetworkQualityIndicatorReturn

Reactive State
PropertyTypeDescription
indicatorRef<NetworkQualityIndicatorData>Current indicator data
isAvailableRef<boolean>Whether network data is available
Indicator Object
typescript
interface NetworkQualityIndicatorData {
  level: NetworkQualityLevel // 'excellent' | 'good' | 'fair' | 'poor' | 'critical' | 'unknown'
  bars: SignalBars // 1 | 2 | 3 | 4 | 5
  color: string // CSS color value
  icon: NetworkQualityIcon // Icon name suggestion
  ariaLabel: string // Accessibility label
  details: NetworkDetails // Detailed metrics
}
Methods
MethodSignatureDescription
update(input: NetworkQualityInput) => voidUpdate with new network stats
reset() => voidReset to unknown state

Usage Example

vue
<template>
  <div class="network-indicator">
    <!-- Signal Bars -->
    <div class="signal-bars" :style="{ color: indicator.color }" :aria-label="indicator.ariaLabel">
      <div v-for="bar in 5" :key="bar" class="bar" :class="{ active: bar <= indicator.bars }" />
    </div>

    <!-- Tooltip with details -->
    <div v-if="isAvailable" class="tooltip">
      <p>RTT: {{ indicator.details.rtt }}ms</p>
      <p>Jitter: {{ indicator.details.jitter }}ms</p>
      <p>Packet Loss: {{ indicator.details.packetLoss }}%</p>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useNetworkQualityIndicator, useSipWebRTCStats } from 'vuesip'

const { stats } = useSipWebRTCStats(session)
const { indicator, isAvailable, update } = useNetworkQualityIndicator({
  colors: {
    excellent: '#10b981',
    critical: '#dc2626',
  },
})

// Update indicator when stats change
watch(stats, (newStats) => {
  if (newStats) {
    update({
      rtt: newStats.rtt,
      jitter: newStats.jitter,
      packetLoss: newStats.packetLoss,
      candidateType: newStats.candidateType,
    })
  }
})
</script>

<style scoped>
.signal-bars {
  display: flex;
  gap: 2px;
  align-items: flex-end;
}

.bar {
  width: 4px;
  background: currentColor;
  opacity: 0.2;
}

.bar.active {
  opacity: 1;
}

.bar:nth-child(1) {
  height: 4px;
}
.bar:nth-child(2) {
  height: 8px;
}
.bar:nth-child(3) {
  height: 12px;
}
.bar:nth-child(4) {
  height: 16px;
}
.bar:nth-child(5) {
  height: 20px;
}
</style>

Quality Levels

LevelBarsTypical Conditions
excellent5RTT < 50ms, jitter < 10ms, packet loss < 0.5%
good4RTT < 100ms, jitter < 20ms, packet loss < 1%
fair3RTT < 200ms, jitter < 40ms, packet loss < 2%
poor2RTT < 400ms, jitter < 80ms, packet loss < 5%
critical1RTT ≥ 400ms or jitter ≥ 80ms or packet loss ≥ 5%
unknown1No data available

useBandwidthAdaptation

Provides intelligent bandwidth adaptation recommendations based on network conditions. Analyzes available bandwidth, packet loss, and RTT to suggest resolution, framerate, and bitrate adjustments.

Source: src/composables/useBandwidthAdaptation.ts

Signature

typescript
function useBandwidthAdaptation(options?: BandwidthAdaptationOptions): UseBandwidthAdaptationReturn

Parameters

ParameterTypeDefaultDescription
options.constraintsBandwidthConstraintsSee defaultsBandwidth/quality constraints
options.sensitivitynumber0.5Adaptation sensitivity (0-1, higher = more reactive)
options.autoAdaptbooleanfalseEnable automatic adaptation
options.onRecommendation(rec: BandwidthRecommendation) => void-Callback on recommendation change
options.historySizenumber5History size for smoothing
Default Constraints
typescript
{
  minVideoBitrate: 100,       // kbps
  maxVideoBitrate: 2500,      // kbps
  minAudioBitrate: 16,        // kbps
  maxAudioBitrate: 128,       // kbps
  targetFramerate: 30,
  minFramerate: 15,
  minResolution: { width: 426, height: 240, label: '240p' },
  preferredResolution: { width: 1280, height: 720, label: '720p' }
}

Returns: UseBandwidthAdaptationReturn

Reactive State
PropertyTypeDescription
recommendationRef<BandwidthRecommendation>Current recommendation
isAutoAdaptingRef<boolean>Whether auto-adaptation is enabled
constraintsRef<Required<BandwidthConstraints>>Current constraints
Recommendation Object
typescript
interface BandwidthRecommendation {
  action: BandwidthAction // 'upgrade' | 'maintain' | 'downgrade' | 'critical'
  suggestions: AdaptationSuggestion[]
  priority: RecommendationPriority // 'low' | 'medium' | 'high' | 'critical'
  estimatedImprovement: number // 0-100
  timestamp: number
}
Suggestion Object
typescript
interface AdaptationSuggestion {
  type: SuggestionType // 'video' | 'audio' | 'network' | 'codec'
  message: string // Human-readable suggestion
  current: string // Current value description
  recommended: string // Recommended value description
  impact: number // Estimated impact 0-100
}
Methods
MethodSignatureDescription
update(input: BandwidthAdaptationInput) => voidUpdate with new stats
setAutoAdapt(enabled: boolean) => voidEnable/disable auto-adaptation
setConstraints(constraints: Partial<BandwidthConstraints>) => voidUpdate constraints
reset() => voidReset to default state
applySuggestion(suggestion: AdaptationSuggestion) => voidApply a suggestion (placeholder)

Usage Example

typescript
import { useBandwidthAdaptation } from 'vuesip'

const { recommendation, update, setConstraints, setAutoAdapt } = useBandwidthAdaptation({
  sensitivity: 0.6,
  constraints: {
    minVideoBitrate: 150,
    preferredResolution: { width: 1280, height: 720, label: '720p' },
  },
  onRecommendation: (rec) => {
    if (rec.priority === 'critical') {
      console.warn('Critical bandwidth issues detected!')
    }
  },
})

// Update with WebRTC stats
update({
  availableBitrate: 1500,
  currentBitrate: 1200,
  packetLoss: 2.5,
  rtt: 120,
  currentResolution: { width: 1280, height: 720, label: '720p' },
  currentFramerate: 25,
  videoEnabled: true,
  degradationEvents: 2,
})

// React to recommendations
if (recommendation.value.action === 'downgrade') {
  console.log('Consider these optimizations:')
  for (const suggestion of recommendation.value.suggestions) {
    console.log(`- ${suggestion.message} (Impact: ${suggestion.impact}%)`)
  }
}

// Example UI integration
const getActionColor = (action: BandwidthAction) => {
  switch (action) {
    case 'upgrade':
      return '#22c55e'
    case 'maintain':
      return '#3b82f6'
    case 'downgrade':
      return '#f97316'
    case 'critical':
      return '#ef4444'
  }
}

Action Types

ActionDescriptionTypical Trigger
upgradeCan increase qualityAvailable bandwidth > 2x current usage
maintainCurrent quality is optimalStable conditions
downgradeShould reduce qualityBandwidth constrained or high packet loss
criticalSevere issues detectedVery low bandwidth or extreme conditions

Video Resolutions

The composable supports standard video resolutions for suggestions:

ResolutionLabelUse Case
1920×10801080pHigh bandwidth
1280×720720pStandard quality
854×480480pMedium bandwidth
640×360360pLow bandwidth
426×240240pMinimum quality

Connection Recovery Composables

useConnectionRecovery

Provides connection recovery with ICE restart handling for WebRTC connections. Monitors peer connection health, detects failures, and automatically or manually triggers recovery attempts.

Source: src/composables/useConnectionRecovery.ts

Signature

typescript
function useConnectionRecovery(options?: ConnectionRecoveryOptions): UseConnectionRecoveryReturn

Parameters

ParameterTypeDefaultDescription
options.autoRecoverbooleantrueEnable automatic recovery on ICE failure
options.maxAttemptsnumber3Maximum recovery attempts before giving up
options.attemptDelaynumber2000Base delay between attempts in milliseconds
options.iceRestartTimeoutnumber10000Timeout for ICE restart in milliseconds
options.strategyRecoveryStrategy'ice-restart'Recovery strategy to use
options.exponentialBackoffbooleanfalseUse exponential backoff for retry delays
options.maxBackoffDelaynumber30000Maximum delay when using exponential backoff (ms)
options.autoReconnectOnNetworkChangebooleanfalseTrigger ICE restart on network changes
options.networkChangeDelaynumber500Delay before triggering recovery after network change
options.onReconnect() => Promise<boolean>-Custom reconnect handler for 'reconnect' strategy
options.onRecoveryStart() => void-Callback when recovery starts
options.onRecoverySuccess(attempt: RecoveryAttempt) => void-Callback on successful recovery
options.onRecoveryFailed(attempts: RecoveryAttempt[]) => void-Callback when all attempts fail
options.onNetworkChange(info: NetworkInfo) => void-Callback when network conditions change

Type Definitions

typescript
type RecoveryState = 'stable' | 'monitoring' | 'recovering' | 'failed'
type RecoveryStrategy = 'ice-restart' | 'reconnect' | 'none'

interface NetworkInfo {
  type: string // Connection type (wifi, cellular, ethernet, etc.)
  effectiveType: string // Effective connection type (4g, 3g, 2g, slow-2g)
  downlink: number // Downlink speed in Mbps
  rtt: number // Round-trip time in ms
  isOnline: boolean // Whether browser is online
}

interface IceHealthStatus {
  iceState: RTCIceConnectionState
  stateAge: number // Time since last state change (ms)
  recoveryAttempts: number
  isHealthy: boolean
}

interface RecoveryAttempt {
  attempt: number
  strategy: RecoveryStrategy
  success: boolean
  duration: number // Duration of attempt (ms)
  error?: string
  timestamp: number
}

Returns: UseConnectionRecoveryReturn

Reactive State
PropertyTypeDescription
stateComputedRef<RecoveryState>Current recovery state
iceHealthComputedRef<IceHealthStatus>Current ICE health status
attemptsComputedRef<RecoveryAttempt[]>History of recovery attempts
isRecoveringComputedRef<boolean>Whether recovery is in progress
isHealthyComputedRef<boolean>Whether connection is healthy
errorComputedRef<string | null>Last error message
networkInfoComputedRef<NetworkInfo>Current network connection info
Methods
MethodSignatureDescription
monitor(pc: RTCPeerConnection) => voidStart monitoring a peer connection
stopMonitoring() => voidStop monitoring the peer connection
recover() => Promise<boolean>Manually trigger recovery
reset() => voidReset recovery state

Usage Example

typescript
import { useConnectionRecovery } from 'vuesip'

const { state, iceHealth, isRecovering, isHealthy, attempts, monitor, recover, reset } =
  useConnectionRecovery({
    maxAttempts: 3,
    attemptDelay: 2000,
    autoRecover: true,
    onRecoveryStart: () => console.log('Recovery started...'),
    onRecoverySuccess: (attempt) => console.log(`Recovered on attempt ${attempt.attempt}`),
    onRecoveryFailed: (attempts) =>
      console.error('Recovery failed after', attempts.length, 'attempts'),
  })

// Start monitoring a peer connection
const peerConnection = new RTCPeerConnection()
monitor(peerConnection)

// Check connection health
watch(isHealthy, (healthy) => {
  if (!healthy) {
    console.warn('Connection health degraded')
  }
})

// Monitor recovery state
watch(state, (newState) => {
  switch (newState) {
    case 'stable':
      console.log('Connection is stable')
      break
    case 'monitoring':
      console.log('Detected issue, monitoring...')
      break
    case 'recovering':
      console.log('Attempting recovery...')
      break
    case 'failed':
      console.error('Recovery failed')
      break
  }
})

// Manual recovery (if autoRecover is false)
const success = await recover()

// Reset state after call ends
reset()

Integration with Call Session

vue
<template>
  <div class="call-status">
    <div v-if="isRecovering" class="recovery-indicator">
      <span class="spinner" />
      Reconnecting...
    </div>

    <div v-if="state === 'failed'" class="error-message">
      Connection lost.
      <button @click="recover">Retry</button>
    </div>

    <div class="health-indicator" :class="healthClass">
      {{ healthLabel }}
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, watch } from 'vue'
import { useConnectionRecovery, useCallSession } from 'vuesip'

const { session } = useCallSession(sipClient)
const { state, isRecovering, isHealthy, monitor, stopMonitoring, recover } = useConnectionRecovery({
  maxAttempts: 3,
  onRecoveryFailed: () => {
    // Notify user or escalate
    console.error('Unable to recover connection')
  },
})

// Monitor peer connection when session starts
watch(session, (newSession) => {
  if (newSession?.peerConnection) {
    monitor(newSession.peerConnection)
  } else {
    stopMonitoring()
  }
})

const healthClass = computed(() => ({
  healthy: isHealthy.value,
  unhealthy: !isHealthy.value && state.value !== 'recovering',
  recovering: state.value === 'recovering',
}))

const healthLabel = computed(() => {
  if (isRecovering.value) return 'Reconnecting...'
  if (!isHealthy.value) return 'Connection issues'
  return 'Connected'
})
</script>

Recovery States

StateDescription
stableConnection is healthy, no issues detected
monitoringIssue detected, actively monitoring
recoveringActively attempting to recover
failedAll recovery attempts exhausted

Recovery Strategies

StrategyDescription
ice-restartTrigger ICE restart with offer renegotiation (default)
reconnectCustom reconnection via onReconnect callback (must return Promise<boolean>)
noneNo automatic recovery, manual recover() calls only

Exponential Backoff

Enable exponential backoff to progressively increase delay between retry attempts:

typescript
const { recover, attempts } = useConnectionRecovery({
  maxAttempts: 5,
  attemptDelay: 1000, // Base delay: 1 second
  exponentialBackoff: true,
  maxBackoffDelay: 30000, // Cap at 30 seconds
})

// Retry delays will be:
// Attempt 1: 1000ms (1s)
// Attempt 2: 2000ms (2s)
// Attempt 3: 4000ms (4s)
// Attempt 4: 8000ms (8s)
// Attempt 5: 16000ms (16s) - capped at maxBackoffDelay if exceeded

Custom Reconnect Strategy

Use the reconnect strategy with a custom handler for full control over reconnection logic:

typescript
const { monitor, state } = useConnectionRecovery({
  strategy: 'reconnect',
  maxAttempts: 3,

  // Custom reconnect handler - must return Promise<boolean>
  onReconnect: async () => {
    try {
      // Your custom reconnection logic
      await sipClient.disconnect()
      await sipClient.connect()
      return true // Return true on success
    } catch (error) {
      console.error('Reconnect failed:', error)
      return false // Return false on failure
    }
  },

  onRecoverySuccess: () => {
    console.log('Custom reconnection successful')
  },
})

Network Change Detection

Monitor network changes and automatically trigger ICE restart when the connection type changes:

typescript
const { networkInfo, monitor } = useConnectionRecovery({
  autoReconnectOnNetworkChange: true,
  networkChangeDelay: 500, // Wait 500ms before triggering recovery

  onNetworkChange: (info) => {
    console.log('Network changed:', {
      type: info.type, // 'wifi', 'cellular', 'ethernet', etc.
      effectiveType: info.effectiveType, // '4g', '3g', '2g', 'slow-2g'
      downlink: info.downlink, // Mbps
      rtt: info.rtt, // Round-trip time in ms
      isOnline: info.isOnline,
    })
  },
})

// Access current network info reactively
watch(networkInfo, (info) => {
  if (!info.isOnline) {
    showNotification('You are offline')
  } else if (info.effectiveType === 'slow-2g') {
    showNotification('Poor network connection')
  }
})

Video Enhancement Composables

usePictureInPicture

Provides Picture-in-Picture mode support for video elements, allowing video calls to be displayed in a floating window that stays on top of other applications.

Source: src/composables/usePictureInPicture.ts

Signature

typescript
function usePictureInPicture(
  videoRef: Ref<HTMLVideoElement | null>,
  options?: PictureInPictureOptions
): UsePictureInPictureReturn

Parameters

ParameterTypeDefaultDescription
videoRefRef<HTMLVideoElement | null>-Ref to the HTMLVideoElement to use for PiP
options.persistPreferencebooleanfalseWhether to persist the user's PiP preference to localStorage
options.preferenceKeystring'vuesip-pip-preference'Key to use for localStorage when persisting preference

Returns: UsePictureInPictureReturn

Reactive State
PropertyTypeDescription
isPiPSupportedRef<boolean>Whether PiP is supported by the browser
isPiPActiveRef<boolean>Whether PiP mode is currently active
pipWindowRef<PictureInPictureWindow | null>The PiP window object (null when not in PiP)
errorRef<Error | null>Current error state
Methods
MethodSignatureDescription
enterPiP() => Promise<void>Enter Picture-in-Picture mode
exitPiP() => Promise<void>Exit Picture-in-Picture mode
togglePiP() => Promise<void>Toggle Picture-in-Picture mode

Usage Example

vue
<template>
  <div class="video-call">
    <video ref="videoElement" autoplay playsinline />

    <div class="controls">
      <button v-if="isPiPSupported" @click="togglePiP" :disabled="!videoElement">
        {{ isPiPActive ? 'Exit PiP' : 'Enter PiP' }}
      </button>
      <p v-if="!isPiPSupported">Picture-in-Picture not supported in this browser</p>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue'
import { usePictureInPicture } from 'vuesip'

const videoElement = ref<HTMLVideoElement | null>(null)

const { isPiPSupported, isPiPActive, togglePiP, error } = usePictureInPicture(videoElement, {
  persistPreference: true,
})

// Handle errors
watch(error, (e) => {
  if (e) console.error('PiP Error:', e.message)
})
</script>

Browser Support

Picture-in-Picture requires browser support:

BrowserSupport
Chrome 70+✅ Full
Edge 79+✅ Full
Safari 13.1+✅ Full
Firefox❌ Uses different API
Opera 57+✅ Full

💡 Tip: Always check isPiPSupported before showing PiP controls to users.


useVideoInset

Provides a local camera overlay on a remote video stream, commonly used in video calls to show both participants in a "picture-in-picture" style layout within your application.

Source: src/composables/useVideoInset.ts

Signature

typescript
function useVideoInset(options?: VideoInsetOptions): UseVideoInsetReturn

Parameters

ParameterTypeDefaultDescription
options.initialPositionInsetPosition'bottom-right'Initial position of the inset video
options.initialSizeInsetSize'medium'Initial size preset
options.customWidthnumber160Custom width in pixels (used when size is 'custom')
options.customHeightnumber120Custom height in pixels (used when size is 'custom')
options.marginnumber16Margin from container edges in pixels
options.borderRadiusnumber8Border radius in pixels
options.draggablebooleantrueWhether the inset can be dragged
options.showInitiallybooleantrueWhether to show the inset initially
options.persistPreferencebooleanfalsePersist position/size to localStorage
options.preferenceKeystring'vuesip-video-inset'Key for localStorage persistence

Type Definitions

typescript
type InsetPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
type InsetSize = 'small' | 'medium' | 'large' | 'custom'

interface InsetDimensions {
  width: number
  height: number
}

Size Presets

SizeDimensions
small120×90 px
medium160×120 px
large240×180 px
customUser-defined

Returns: UseVideoInsetReturn

Reactive State
PropertyTypeDescription
isVisibleRef<boolean>Whether inset is currently visible
positionRef<InsetPosition>Current position of the inset
sizeRef<InsetSize>Current size preset
dimensionsRef<InsetDimensions>Current dimensions in pixels
isSwappedRef<boolean>Whether videos are swapped (local is main, remote is inset)
isDraggableRef<boolean>Whether dragging is enabled
isDraggingRef<boolean>Whether currently being dragged
insetStylesRef<CSSProperties>Computed CSS styles for the inset container
Methods
MethodSignatureDescription
show() => voidShow the inset
hide() => voidHide the inset
toggle() => voidToggle inset visibility
setPosition(pos: InsetPosition) => voidSet position
setSize(size: InsetSize) => voidSet size preset
setCustomDimensions(width: number, height: number) => voidSet custom dimensions
swapVideos() => voidSwap main and inset videos
cyclePosition() => voidCycle through positions (clockwise)
reset() => voidReset to initial settings

Usage Example

vue
<template>
  <div class="video-container">
    <!-- Main video (remote stream or local if swapped) -->
    <video
      ref="mainVideo"
      :srcObject="isSwapped ? localStream : remoteStream"
      autoplay
      playsinline
      class="main-video"
    />

    <!-- Inset video (local camera or remote if swapped) -->
    <div v-if="isVisible" :style="insetStyles" class="inset-video">
      <video
        ref="insetVideo"
        :srcObject="isSwapped ? remoteStream : localStream"
        autoplay
        muted
        playsinline
      />

      <!-- Inset Controls -->
      <div class="inset-controls">
        <button @click="swapVideos" title="Swap videos">🔄</button>
        <button @click="cyclePosition" title="Move position">📍</button>
        <button @click="hide" title="Hide self-view">👁️</button>
      </div>
    </div>

    <!-- Show button when hidden -->
    <button v-if="!isVisible" @click="show" class="show-btn">Show Self-View</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useVideoInset, useCallSession } from 'vuesip'

// Get media streams from call session
const { localStream, remoteStream } = useCallSession(sipClient)

// Initialize video inset
const { isVisible, isSwapped, insetStyles, show, hide, swapVideos, cyclePosition, setSize } =
  useVideoInset({
    initialPosition: 'bottom-right',
    initialSize: 'medium',
    persistPreference: true,
  })

// Change size based on screen size
const handleResize = () => {
  if (window.innerWidth < 768) {
    setSize('small')
  } else {
    setSize('medium')
  }
}
</script>

<style scoped>
.video-container {
  position: relative;
  width: 100%;
  height: 100%;
}

.main-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.inset-video video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.inset-controls {
  position: absolute;
  bottom: 4px;
  right: 4px;
  display: flex;
  gap: 4px;
  opacity: 0;
  transition: opacity 0.2s;
}

.inset-video:hover .inset-controls {
  opacity: 1;
}
</style>

Advanced Usage: Custom Dragging

typescript
import { useVideoInset } from 'vuesip'

const { isDraggable, isDragging, setCustomDimensions } = useVideoInset({
  draggable: true,
  initialSize: 'custom',
  customWidth: 200,
  customHeight: 150,
})

// Implement custom drag handling
const handleDragStart = () => {
  isDragging.value = true
}

const handleDragEnd = () => {
  isDragging.value = false
}

// Resize on pinch gesture
const handlePinch = (scale: number) => {
  const currentWidth = dimensions.value.width
  const currentHeight = dimensions.value.height
  setCustomDimensions(currentWidth * scale, currentHeight * scale)
}

Conference Enhancement Composables

Composables for enhancing conference call experiences with active speaker detection, gallery layouts, and participant controls.

useActiveSpeaker

Provides reactive active speaker detection based on participant audio levels. Detects the dominant speaker (highest audio level above threshold) and tracks speaker history for conference UI components.

Source: src/composables/useActiveSpeaker.ts

Signature

typescript
function useActiveSpeaker(
  participants: Ref<Participant[]>,
  options?: ActiveSpeakerOptions
): UseActiveSpeakerReturn

Parameters

ParameterTypeDefaultDescription
participantsRef<Participant[]>-Reactive reference to conference participants array
options.thresholdnumber0.15Audio level threshold to consider someone speaking (0-1)
options.debounceMsnumber300Debounce time in ms to prevent rapid speaker switching
options.historySizenumber10Number of recent speakers to track in history
options.excludeMutedbooleantrueExclude muted participants from detection
options.onSpeakerChange(speaker, previous) => void-Callback when active speaker changes

Returns: UseActiveSpeakerReturn

Reactive State
PropertyTypeDescription
activeSpeakerComputedRef<Participant | null>Current dominant speaker (highest audio level above threshold)
activeSpeakersComputedRef<Participant[]>All participants currently speaking (above threshold), sorted by level
isSomeoneSpeakingComputedRef<boolean>Is anyone currently speaking
speakerHistoryRef<SpeakerHistoryEntry[]>Recent speaker history
Speaker History Entry
typescript
interface SpeakerHistoryEntry {
  participantId: string // Participant ID
  displayName: string // Display name at time of speaking
  startedAt: number // When they started speaking (timestamp)
  endedAt: number | null // When they stopped (null if still speaking)
  peakLevel: number // Peak audio level during this speaking period
}
Methods
MethodSignatureDescription
clearHistory() => voidClear speaker history
setThreshold(threshold: number) => voidUpdate threshold dynamically (clamped to 0-1)

Usage Example

typescript
import { ref, watch } from 'vue'
import { useActiveSpeaker } from 'vuesip'
import type { Participant } from 'vuesip'

// Get participants from conference composable
const participants = ref<Participant[]>([])

const {
  activeSpeaker,
  activeSpeakers,
  isSomeoneSpeaking,
  speakerHistory,
  clearHistory,
  setThreshold,
} = useActiveSpeaker(participants, {
  threshold: 0.2,
  debounceMs: 500,
  historySize: 20,
  excludeMuted: true,
  onSpeakerChange: (current, previous) => {
    console.log(`Speaker changed from ${previous?.displayName} to ${current?.displayName}`)
  },
})

// Watch for active speaker changes
watch(activeSpeaker, (speaker) => {
  if (speaker) {
    console.log(`${speaker.displayName} is speaking at level ${speaker.audioLevel}`)
  }
})

// Check if anyone is speaking
if (isSomeoneSpeaking.value) {
  console.log(`${activeSpeakers.value.length} people are speaking`)
}

// Adjust sensitivity dynamically
setThreshold(0.1) // More sensitive

// Clear history
clearHistory()

Integration with Conference

vue
<template>
  <div class="conference">
    <div
      v-for="participant in participants"
      :key="participant.id"
      :class="{ 'is-speaking': participant.id === activeSpeaker?.id }"
    >
      <span>{{ participant.displayName }}</span>
      <span v-if="participant.id === activeSpeaker?.id" class="speaker-indicator"> Speaking </span>
    </div>

    <div class="speaker-history">
      <h4>Recent Speakers</h4>
      <ul>
        <li v-for="entry in speakerHistory" :key="entry.startedAt">
          {{ entry.displayName }} (peak: {{ (entry.peakLevel * 100).toFixed(0) }}%)
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useConference, useActiveSpeaker } from 'vuesip'

const { participants } = useConference(sipClient)
const { activeSpeaker, speakerHistory } = useActiveSpeaker(participants)
</script>

useGalleryLayout

Provides reactive gallery layout calculations for displaying conference participants. Calculates optimal grid dimensions based on participant count and supports multiple layout modes.

Source: src/composables/useGalleryLayout.ts

Signature

typescript
function useGalleryLayout(
  participantCount: Ref<number>,
  options?: GalleryLayoutOptions
): UseGalleryLayoutReturn

Parameters

ParameterTypeDefaultDescription
participantCountRef<number>-Reactive reference to number of participants
options.containerSizeRef<ContainerSize>-Container size for dimension calculations
options.gapnumber8Gap between tiles in pixels
options.activeSpeakerIdRef<string | null>-Active speaker ID for speaker-focused layouts
options.maxColsnumber4Maximum columns
options.maxRowsnumber4Maximum rows
options.aspectRationumber16/9Aspect ratio for tiles (width/height)

Type Definitions

typescript
type GalleryLayoutMode = 'grid' | 'speaker' | 'sidebar' | 'spotlight'

interface ContainerSize {
  width: number // Container width in pixels
  height: number // Container height in pixels
}

interface TileDimensions {
  width: number // Tile width in pixels
  height: number // Tile height in pixels
}

Grid Sizing Rules

ParticipantsGrid Layout
11×1
22×1
3-42×2
5-63×2
7-93×3
10-124×3
13+4×4 or sqrt-based

Returns: UseGalleryLayoutReturn

Reactive State
PropertyTypeDescription
layoutRef<GalleryLayoutMode>Current layout mode
gridColsComputedRef<number>Number of grid columns
gridRowsComputedRef<number>Number of grid rows
tileDimensionsComputedRef<TileDimensions>Calculated tile dimensions
gridStyleComputedRef<string>CSS grid style string
focusedParticipantIdComputedRef<string | null>Currently focused participant ID
pinnedParticipantIdRef<string | null>Pinned participant ID
Methods
MethodSignatureDescription
setLayout(mode: GalleryLayoutMode) => voidSet layout mode
pinParticipant(id: string) => voidPin a participant to focus
unpinParticipant() => voidUnpin the focused participant

Usage Example

typescript
import { ref, computed } from 'vue'
import { useGalleryLayout } from 'vuesip'

// Participant count from conference
const participantCount = ref(6)

// Container size from ResizeObserver
const containerSize = ref({ width: 1920, height: 1080 })

const {
  gridCols,
  gridRows,
  tileDimensions,
  gridStyle,
  focusedParticipantId,
  pinnedParticipantId,
  setLayout,
  pinParticipant,
  unpinParticipant,
} = useGalleryLayout(participantCount, {
  containerSize,
  gap: 16,
  maxCols: 4,
  maxRows: 4,
  aspectRatio: 16 / 9,
})

// gridCols.value = 3, gridRows.value = 2
// tileDimensions.value = { width: 624, height: 351 }

// Pin a specific participant
pinParticipant('participant-123')

// Switch to speaker layout
setLayout('speaker')

Vue Component Integration

vue
<template>
  <div ref="containerRef" class="gallery-container" :style="gridStyle">
    <div
      v-for="participant in participants"
      :key="participant.id"
      class="participant-tile"
      :style="tileStyle"
      :class="{
        'is-focused': participant.id === focusedParticipantId,
        'is-pinned': participant.id === pinnedParticipantId,
      }"
      @click="pinParticipant(participant.id)"
    >
      <video :srcObject="participant.videoStream" autoplay playsinline />
      <span class="name">{{ participant.displayName }}</span>
    </div>
  </div>

  <div class="layout-controls">
    <button @click="setLayout('grid')">Grid</button>
    <button @click="setLayout('speaker')">Speaker</button>
    <button @click="setLayout('spotlight')">Spotlight</button>
    <button @click="unpinParticipant" :disabled="!pinnedParticipantId">Unpin</button>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useConference, useGalleryLayout, useActiveSpeaker } from 'vuesip'

const containerRef = ref<HTMLElement | null>(null)
const containerSize = ref({ width: 0, height: 0 })

const { participants, participantCount } = useConference(sipClient)
const { activeSpeaker } = useActiveSpeaker(participants)

const activeSpeakerId = computed(() => activeSpeaker.value?.id ?? null)

const {
  gridCols,
  gridRows,
  tileDimensions,
  gridStyle,
  focusedParticipantId,
  pinnedParticipantId,
  setLayout,
  pinParticipant,
  unpinParticipant,
} = useGalleryLayout(participantCount, {
  containerSize,
  activeSpeakerId,
  gap: 8,
})

const tileStyle = computed(() => ({
  width: `${tileDimensions.value.width}px`,
  height: `${tileDimensions.value.height}px`,
}))

// Track container size
let resizeObserver: ResizeObserver | null = null

onMounted(() => {
  if (containerRef.value) {
    resizeObserver = new ResizeObserver((entries) => {
      const entry = entries[0]
      if (entry) {
        containerSize.value = {
          width: entry.contentRect.width,
          height: entry.contentRect.height,
        }
      }
    })
    resizeObserver.observe(containerRef.value)
  }
})

onUnmounted(() => {
  resizeObserver?.disconnect()
})
</script>

<style scoped>
.gallery-container {
  width: 100%;
  height: 100%;
}

.participant-tile {
  position: relative;
  background: #1a1a1a;
  border-radius: 8px;
  overflow: hidden;
}

.participant-tile.is-focused {
  outline: 2px solid #3b82f6;
}

.participant-tile.is-pinned {
  outline: 2px solid #22c55e;
}
</style>

Layout Modes

ModeDescription
gridStandard grid layout with equal-sized tiles
speakerFocus on dominant speaker with smaller tiles for others
sidebarMain content with sidebar of participant tiles
spotlightSingle participant in focus

useParticipantControls

Provides controls for individual conference participants including mute/unmute, kick, pin, and volume control with permission-based access.

Source: src/composables/useParticipantControls.ts

Signature

typescript
function useParticipantControls(
  participant: Ref<Participant | null>,
  options?: ParticipantControlsOptions
): UseParticipantControlsReturn

Parameters

ParameterTypeDefaultDescription
participantRef<Participant | null>-Reactive reference to the participant to control
options.isModeratorRef<boolean>ref(false)Whether the current user is a moderator
options.isPinnedRef<boolean>ref(false)Whether this participant is currently pinned
options.initialVolumenumber1Initial volume level (0-1)
options.onMute(participant) => void-Callback when participant is muted
options.onUnmute(participant) => void-Callback when participant is unmuted
options.onKick(participant) => void-Callback when participant is kicked
options.onPin(participant) => void-Callback when participant is pinned
options.onUnpin(participant) => void-Callback when participant is unpinned
options.onVolumeChange(participant, volume) => void-Callback when volume changes

Returns: UseParticipantControlsReturn

Reactive State
PropertyTypeDescription
canMuteComputedRef<boolean>Can mute/unmute this participant (is moderator)
canKickComputedRef<boolean>Can kick this participant (is moderator, not self)
canPinComputedRef<boolean>Can pin this participant (has participant)
volumeRef<number>Current volume level (0-1)
Methods
MethodSignatureDescription
toggleMute() => voidToggle mute state (requires moderator permission)
kickParticipant() => voidKick participant from conference (requires moderator, cannot kick self)
togglePin() => voidToggle pin state
setVolume(level: number) => voidSet volume level (clamped to 0-1)

Usage Example

typescript
import { ref } from 'vue'
import { useParticipantControls } from 'vuesip'
import type { Participant } from 'vuesip'

const participant = ref<Participant | null>({
  id: 'participant-1',
  displayName: 'John Doe',
  isMuted: false,
  isLocal: false,
  audioLevel: 0.5,
})

const isModerator = ref(true)
const isPinned = ref(false)

const { canMute, canKick, canPin, volume, toggleMute, kickParticipant, togglePin, setVolume } =
  useParticipantControls(participant, {
    isModerator,
    isPinned,
    initialVolume: 1,
    onMute: (p) => console.log(`Muted ${p.displayName}`),
    onUnmute: (p) => console.log(`Unmuted ${p.displayName}`),
    onKick: (p) => console.log(`Kicked ${p.displayName}`),
    onPin: (p) => console.log(`Pinned ${p.displayName}`),
    onUnpin: (p) => console.log(`Unpinned ${p.displayName}`),
    onVolumeChange: (p, v) => console.log(`${p.displayName} volume: ${v}`),
  })

// Check permissions before showing controls
if (canMute.value) {
  toggleMute()
}

if (canKick.value) {
  kickParticipant()
}

// Adjust volume
setVolume(0.5) // 50% volume

Vue Component Integration

vue
<template>
  <div class="participant-controls" v-if="participant">
    <span class="name">{{ participant.displayName }}</span>

    <!-- Mute Button -->
    <button v-if="canMute" @click="toggleMute" :title="participant.isMuted ? 'Unmute' : 'Mute'">
      {{ participant.isMuted ? '🔇' : '🔊' }}
    </button>

    <!-- Volume Slider -->
    <input
      type="range"
      min="0"
      max="1"
      step="0.1"
      :value="volume"
      @input="setVolume(parseFloat(($event.target as HTMLInputElement).value))"
      title="Volume"
    />

    <!-- Pin Button -->
    <button v-if="canPin" @click="togglePin" :class="{ active: isPinned }" title="Pin participant">
      📌
    </button>

    <!-- Kick Button -->
    <button v-if="canKick" @click="kickParticipant" class="kick-btn" title="Remove from conference">
      🚫
    </button>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import { useParticipantControls } from 'vuesip'
import type { Participant } from 'vuesip'

const props = defineProps<{
  participant: Participant | null
  isModerator: boolean
  isPinnedParticipant: boolean
}>()

const emit = defineEmits<{
  (e: 'pin', id: string): void
  (e: 'unpin'): void
  (e: 'kick', id: string): void
}>()

const participantRef = computed(() => props.participant)
const isModeratorRef = computed(() => props.isModerator)
const isPinnedRef = computed(() => props.isPinnedParticipant)

const { canMute, canKick, canPin, volume, toggleMute, kickParticipant, togglePin, setVolume } =
  useParticipantControls(participantRef, {
    isModerator: isModeratorRef,
    isPinned: isPinnedRef,
    onKick: (p) => emit('kick', p.id),
    onPin: (p) => emit('pin', p.id),
    onUnpin: () => emit('unpin'),
  })
</script>

<style scoped>
.participant-controls {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px;
  background: rgba(0, 0, 0, 0.5);
  border-radius: 4px;
}

button {
  padding: 4px 8px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button.active {
  background: #3b82f6;
  color: white;
}

.kick-btn {
  background: #ef4444;
  color: white;
}

input[type='range'] {
  width: 80px;
}
</style>

Permission Matrix

ActionRequires
Mute/UnmuteModerator status
KickModerator status AND not self (isLocal = false)
PinParticipant exists
VolumeAlways available

Constants

All composables use centralized constants for configuration and magic numbers. These are exported from src/composables/constants.ts.

Source: src/composables/constants.ts

Available Constants

REGISTRATION_CONSTANTS

ConstantValueDescription
DEFAULT_EXPIRES600Default registration expiry time in seconds
DEFAULT_MAX_RETRIES3Default maximum retry attempts
REFRESH_PERCENTAGE0.9Registration refresh percentage (90% of expiry)
EXPIRING_SOON_THRESHOLD30Seconds threshold for "expiring soon" warning
BASE_RETRY_DELAY1000Base retry delay in milliseconds
MAX_RETRY_DELAY30000Maximum retry delay in milliseconds

PRESENCE_CONSTANTS

ConstantValueDescription
DEFAULT_EXPIRES3600Default presence publish expiry in seconds
SUBSCRIPTION_REFRESH_PERCENTAGE0.9Subscription refresh percentage (90% of expiry)
DEFAULT_SUBSCRIPTION_EXPIRES3600Default subscription expiry in seconds

MESSAGING_CONSTANTS

ConstantValueDescription
COMPOSING_IDLE_TIMEOUT10000Composing indicator idle timeout in milliseconds
COMPOSING_TIMEOUT_SECONDS10Composing indicator timeout in seconds

CONFERENCE_CONSTANTS

ConstantValueDescription
DEFAULT_MAX_PARTICIPANTS10Default maximum participants in a conference
AUDIO_LEVEL_INTERVAL100Audio level monitoring interval in milliseconds
STATE_TRANSITION_DELAY2000Conference state transition delay in milliseconds

TRANSFER_CONSTANTS

ConstantValueDescription
COMPLETION_DELAY2000Transfer completion delay in milliseconds
CANCELLATION_DELAY1000Transfer cancellation delay in milliseconds

HISTORY_CONSTANTS

ConstantValueDescription
DEFAULT_LIMIT10Default call history limit
DEFAULT_OFFSET0Default offset for pagination
DEFAULT_SORT_ORDER'desc'Default sort order
DEFAULT_SORT_BY'startTime'Default sort field
TOP_FREQUENT_CONTACTS10Top N frequent contacts to return

CALL_CONSTANTS

ConstantValueDescription
MAX_CONCURRENT_CALLS5Maximum concurrent calls
CALL_TIMEOUT30000Call timeout in milliseconds
RING_TIMEOUT60000Ring timeout in milliseconds

MEDIA_CONSTANTS

ConstantValueDescription
ENUMERATION_RETRY_DELAY1000Device enumeration retry delay in milliseconds
DEFAULT_TEST_DURATION2000Device test duration in milliseconds
AUDIO_LEVEL_THRESHOLD0.01Audio level threshold for device test (0-1)

DTMF_CONSTANTS

ConstantValueDescription
DEFAULT_DURATION100Default DTMF tone duration in milliseconds
DEFAULT_INTER_TONE_GAP70Default inter-tone gap in milliseconds
MIN_DURATION40Minimum allowed duration in milliseconds
MAX_DURATION6000Maximum allowed duration in milliseconds
MAX_QUEUE_SIZE100Maximum DTMF queue size

TIMEOUTS

ConstantValueDescription
SHORT_DELAY1000Short delay for UI updates in milliseconds
MEDIUM_DELAY2000Medium delay for operations in milliseconds
LONG_DELAY5000Long delay for cleanup in milliseconds

RETRY_CONFIG

ConstantValueDescription
calculateBackoff(attempt, baseDelay, maxDelay)functionCalculate exponential backoff delay
BACKOFF_MULTIPLIER2Default exponential backoff multiplier

Usage Example

typescript
import {
  REGISTRATION_CONSTANTS,
  DTMF_CONSTANTS,
  CONFERENCE_CONSTANTS,
  TIMEOUTS,
  RETRY_CONFIG,
} from '@/composables/constants'

// Use in your code
const expiryTime = REGISTRATION_CONSTANTS.DEFAULT_EXPIRES
const toneDuration = DTMF_CONSTANTS.DEFAULT_DURATION
const maxParticipants = CONFERENCE_CONSTANTS.DEFAULT_MAX_PARTICIPANTS

// Timeouts
const delay = TIMEOUTS.MEDIUM_DELAY

// Retry logic
const backoffDelay = RETRY_CONFIG.calculateBackoff(2, 1000, 30000)

Type Definitions

All composables use TypeScript for type safety. Main type definitions can be found in:

  • Call Types: src/types/call.types.ts
  • Media Types: src/types/media.types.ts
  • History Types: src/types/history.types.ts
  • Presence Types: src/types/presence.types.ts
  • Messaging Types: src/types/messaging.types.ts
  • Conference Types: src/types/conference.types.ts
  • Transfer Types: src/types/transfer.types.ts
  • SIP Types: src/types/sip.types.ts
  • Config Types: src/types/config.types.ts

Best Practices

1. Lifecycle Management

Always ensure proper cleanup by using the autoCleanup option or manually cleaning up in onUnmounted:

typescript
import { onUnmounted } from 'vue'

const { disconnect } = useSipClient(config, { autoCleanup: false })

onUnmounted(async () => {
  await disconnect()
})

2. Error Handling

All async methods can throw errors. Always wrap them in try-catch blocks:

typescript
try {
  await connect()
  await register()
} catch (error) {
  console.error('Connection failed:', error)
}

3. Reactive State

Use computed properties and watch for reactive updates:

typescript
import { watch } from 'vue'

const { isRegistered, registrationState } = useSipRegistration(sipClient)

watch(registrationState, (newState) => {
  console.log('Registration state changed:', newState)
})

4. Event Listeners

Always clean up event listeners to prevent memory leaks:

typescript
const unsubscribe = onPresenceEvent((event) => {
  console.log('Presence event:', event)
})

// Later...
unsubscribe()

5. Shared Instances

When using multiple composables, share the SipClient instance:

typescript
const sipClientRef = ref(null)

const sipClient = useSipClient(config)
sipClientRef.value = sipClient.getClient()

const registration = useSipRegistration(sipClientRef)
const callSession = useCallSession(sipClientRef)
const presence = usePresence(sipClientRef)

Released under the MIT License.