Skip to content

Call Quality Dashboard Guide

This guide covers building comprehensive call quality dashboards in VueSIP using the call quality scoring and network quality indicator composables, enabling you to visualize real-time quality metrics, track trends, and provide users with actionable feedback.

Overview

VueSIP provides advanced composables for building call quality dashboards that go beyond basic statistics:

  • Quality Scoring: Calculate overall quality grades (A-F) with weighted metrics
  • Trend Analysis: Track quality trends over time with confidence scoring
  • Network Indicators: Visual signal strength indicators with accessibility support
  • Quality Recommendations: Actionable suggestions based on current metrics

Why Build a Quality Dashboard?

  • User Transparency: Show users their connection quality in real-time
  • Issue Prevention: Detect degradation before calls fail
  • Support Efficiency: Provide diagnostic data for troubleshooting
  • Quality Tracking: Historical analysis for service improvement

Table of Contents


Prerequisites

Before building a quality dashboard, ensure you have:

  1. Active Call with WebRTC Stats: Use useSipWebRTCStats to collect metrics
  2. VueSIP Client: Connected and registered SIP client
  3. Modern Browser: With RTCPeerConnection.getStats() support

Quality Scoring

The useCallQualityScore composable calculates comprehensive quality scores from WebRTC metrics.

Basic Usage

typescript
import { ref, watch } from 'vue'
import { useSipClient, useSipWebRTCStats, useCallQualityScore } from 'vuesip'

// Get current call and stats
const { currentCall } = useSipClient()
const stats = useSipWebRTCStats(currentCall)

// Create quality scorer
const {
  score, // Current quality score (0-100)
  trend, // Quality trend analysis
  history, // Score history
  updateScore, // Update with new metrics
  reset, // Reset score and history
  setWeights, // Customize metric weights
} = useCallQualityScore({
  historySize: 20, // Keep last 20 scores
  enableTrendAnalysis: true, // Enable trend detection
})

// Update score when stats change
watch(
  () => stats.stats,
  (newStats) => {
    if (newStats) {
      updateScore({
        packetLoss: newStats.audio.packetsLostPercent,
        jitter: newStats.audio.jitterMs,
        rtt: newStats.rtt,
        mos: stats.mosScore.value,
      })
    }
  }
)

Quality Grades

The composable converts raw metrics into intuitive grades:

GradeScore RangeDescription
A90-100Excellent call quality
B75-89Good call quality
C60-74Fair call quality - some issues detected
D40-59Poor call quality - noticeable problems
F0-39Very poor - consider reconnecting

Score Structure

typescript
interface CallQualityScore {
  overall: number // 0-100 combined score
  audio: number // Audio quality score
  video: number | null // Video quality score (null for audio-only)
  network: number // Network conditions score
  grade: QualityGrade // A, B, C, D, or F
  description: string // Human-readable description
  timestamp: Date // When score was calculated
}

Custom Weights

Adjust metric importance based on your use case:

typescript
const { setWeights } = useCallQualityScore({
  weights: {
    packetLoss: 0.35, // Packet loss (default: 0.30)
    jitter: 0.2, // Jitter (default: 0.20)
    rtt: 0.2, // Round-trip time (default: 0.20)
    mos: 0.15, // MOS score (default: 0.15)
    bitrateStability: 0.1, // Bitrate stability (default: 0.15)
  },
})

// Update weights dynamically
setWeights({
  packetLoss: 0.4, // Prioritize packet loss for audio calls
  rtt: 0.3,
})

Network Quality Indicator

The useNetworkQualityIndicator composable provides visual signal strength indicators.

Basic Usage

typescript
import { useNetworkQualityIndicator } from 'vuesip'

const {
  indicator, // Current indicator data
  isAvailable, // Whether data is available
  update, // Update with network metrics
  reset, // Reset indicator
} = useNetworkQualityIndicator()

// Update from WebRTC stats
update({
  rtt: 50,
  jitter: 10,
  packetLoss: 0.5,
  candidateType: 'host', // ICE candidate type
})

Indicator Data

typescript
interface NetworkQualityIndicatorData {
  level: NetworkQualityLevel // excellent/good/fair/poor/critical/unknown
  bars: SignalBars // 1-5 signal bars
  color: string // Color for the indicator
  icon: NetworkQualityIcon // Icon name
  ariaLabel: string // Accessibility label
  details: NetworkDetails // Detailed metrics
}

Signal Bars Visualization

Create a visual signal strength indicator:

vue
<template>
  <div class="signal-indicator" :aria-label="indicator.ariaLabel" role="img">
    <div
      v-for="bar in 5"
      :key="bar"
      class="signal-bar"
      :class="{
        active: bar <= indicator.bars,
        inactive: bar > indicator.bars,
      }"
      :style="{ backgroundColor: bar <= indicator.bars ? indicator.color : '#ccc' }"
    />
  </div>
</template>

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

const { indicator } = useNetworkQualityIndicator()
</script>

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

.signal-bar {
  width: 4px;
  border-radius: 1px;
  transition: background-color 0.3s;
}

.signal-bar:nth-child(1) {
  height: 20%;
}
.signal-bar:nth-child(2) {
  height: 40%;
}
.signal-bar:nth-child(3) {
  height: 60%;
}
.signal-bar:nth-child(4) {
  height: 80%;
}
.signal-bar:nth-child(5) {
  height: 100%;
}
</style>

Custom Thresholds

Adjust quality level thresholds:

typescript
const indicator = useNetworkQualityIndicator({
  thresholds: {
    rtt: [50, 100, 200, 400], // Excellent/Good/Fair/Poor thresholds
    jitter: [10, 25, 50, 100],
    packetLoss: [0.5, 1, 3, 8],
  },
  colors: {
    excellent: '#22c55e', // Green
    good: '#84cc16', // Lime
    fair: '#eab308', // Yellow
    poor: '#f97316', // Orange
    critical: '#ef4444', // Red
    unknown: '#6b7280', // Gray
  },
})

Building a Dashboard

Combine composables to create a comprehensive dashboard.

Complete Dashboard Example

vue
<template>
  <div class="quality-dashboard">
    <!-- Overall Quality -->
    <div class="quality-summary">
      <div class="grade-display" :class="gradeClass">
        {{ score?.grade || '-' }}
      </div>
      <div class="quality-details">
        <span class="quality-score">{{ score?.overall?.toFixed(0) || '--' }}/100</span>
        <span class="quality-description">{{ score?.description || 'No data' }}</span>
      </div>
      <NetworkSignal :indicator="networkIndicator.indicator.value" />
    </div>

    <!-- Metric Cards -->
    <div class="metrics-grid">
      <MetricCard
        title="Packet Loss"
        :value="formatPercent(currentMetrics.packetLoss)"
        :status="getMetricStatus('packetLoss', currentMetrics.packetLoss)"
      />
      <MetricCard
        title="Jitter"
        :value="formatMs(currentMetrics.jitter)"
        :status="getMetricStatus('jitter', currentMetrics.jitter)"
      />
      <MetricCard
        title="Round Trip"
        :value="formatMs(currentMetrics.rtt)"
        :status="getMetricStatus('rtt', currentMetrics.rtt)"
      />
      <MetricCard
        title="MOS Score"
        :value="currentMetrics.mos?.toFixed(1) || '--'"
        :status="getMetricStatus('mos', currentMetrics.mos)"
      />
    </div>

    <!-- Trend Indicator -->
    <div class="trend-section" v-if="trend">
      <TrendIndicator
        :direction="trend.direction"
        :rate="trend.rate"
        :confidence="trend.confidence"
      />
    </div>

    <!-- History Chart -->
    <div class="history-chart">
      <QualityChart :history="history" />
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, watch, ref } from 'vue'
import {
  useSipClient,
  useSipWebRTCStats,
  useCallQualityScore,
  useNetworkQualityIndicator,
} from 'vuesip'

// Core composables
const { currentCall } = useSipClient()
const stats = useSipWebRTCStats(currentCall)
const { score, trend, history, updateScore } = useCallQualityScore()
const networkIndicator = useNetworkQualityIndicator()

// Current metrics
const currentMetrics = ref({
  packetLoss: 0,
  jitter: 0,
  rtt: 0,
  mos: undefined as number | undefined,
})

// Update on stats change
watch(
  () => stats.stats,
  (newStats) => {
    if (!newStats) return

    const metrics = {
      packetLoss: newStats.audio.packetsLostPercent || 0,
      jitter: newStats.audio.jitterMs || 0,
      rtt: newStats.rtt || 0,
      mos: stats.mosScore.value,
    }

    currentMetrics.value = metrics
    updateScore(metrics)
    networkIndicator.update(metrics)
  }
)

// Grade styling
const gradeClass = computed(() => ({
  'grade-a': score.value?.grade === 'A',
  'grade-b': score.value?.grade === 'B',
  'grade-c': score.value?.grade === 'C',
  'grade-d': score.value?.grade === 'D',
  'grade-f': score.value?.grade === 'F',
}))

// Metric status helpers
function getMetricStatus(metric: string, value?: number) {
  if (value === undefined) return 'unknown'

  const thresholds: Record<string, number[]> = {
    packetLoss: [0.5, 1, 3, 5],
    jitter: [10, 25, 50, 80],
    rtt: [50, 100, 200, 400],
    mos: [4.2, 4.0, 3.5, 3.0],
  }

  const t = thresholds[metric]
  if (!t) return 'unknown'

  if (metric === 'mos') {
    // Higher is better for MOS
    if (value >= t[0]) return 'excellent'
    if (value >= t[1]) return 'good'
    if (value >= t[2]) return 'fair'
    if (value >= t[3]) return 'poor'
    return 'critical'
  } else {
    // Lower is better for other metrics
    if (value <= t[0]) return 'excellent'
    if (value <= t[1]) return 'good'
    if (value <= t[2]) return 'fair'
    if (value <= t[3]) return 'poor'
    return 'critical'
  }
}

// Formatters
function formatPercent(value?: number) {
  return value !== undefined ? `${value.toFixed(2)}%` : '--'
}

function formatMs(value?: number) {
  return value !== undefined ? `${value.toFixed(0)}ms` : '--'
}
</script>

Trend Analysis

Track quality changes over time with confidence-weighted trend analysis.

typescript
interface QualityTrend {
  direction: 'improving' | 'stable' | 'degrading'
  rate: number // Change rate per measurement
  confidence: number // 0-1 confidence level
}

Trend Visualization

vue
<template>
  <div class="trend-indicator" v-if="trend">
    <span class="trend-icon" :class="trendClass">
      <ArrowUp v-if="trend.direction === 'improving'" />
      <Minus v-else-if="trend.direction === 'stable'" />
      <ArrowDown v-else />
    </span>
    <span class="trend-label">
      {{ trendLabel }}
    </span>
    <span class="trend-confidence"> ({{ confidenceLabel }} confidence) </span>
  </div>
</template>

<script setup lang="ts">
import { computed, type PropType } from 'vue'
import type { QualityTrend } from 'vuesip'

const props = defineProps<{
  trend: QualityTrend
}>()

const trendClass = computed(() => ({
  improving: props.trend.direction === 'improving',
  stable: props.trend.direction === 'stable',
  degrading: props.trend.direction === 'degrading',
}))

const trendLabel = computed(() => {
  const rate = Math.abs(props.trend.rate)
  if (props.trend.direction === 'improving') {
    return `Improving (${rate.toFixed(1)} pts/interval)`
  } else if (props.trend.direction === 'degrading') {
    return `Degrading (${rate.toFixed(1)} pts/interval)`
  }
  return 'Stable'
})

const confidenceLabel = computed(() => {
  if (props.trend.confidence >= 0.8) return 'high'
  if (props.trend.confidence >= 0.5) return 'medium'
  return 'low'
})
</script>

Quality Recommendations

Generate actionable recommendations based on current metrics.

Recommendation Logic

typescript
function getRecommendations(metrics: {
  packetLoss: number
  jitter: number
  rtt: number
  candidateType?: string
}): string[] {
  const recommendations: string[] = []

  if (metrics.packetLoss > 2) {
    recommendations.push('High packet loss detected. Check network stability.')
    if (metrics.packetLoss > 5) {
      recommendations.push('Consider switching to a wired connection.')
    }
  }

  if (metrics.jitter > 50) {
    recommendations.push('High jitter detected. Close bandwidth-heavy applications.')
  }

  if (metrics.rtt > 200) {
    recommendations.push('High latency detected. Consider using a closer server.')
    if (metrics.candidateType === 'relay') {
      recommendations.push('Connection is relayed. Try disabling VPN if using one.')
    }
  }

  if (recommendations.length === 0) {
    recommendations.push('Call quality is good. No action needed.')
  }

  return recommendations
}

Recommendations Component

vue
<template>
  <div class="recommendations" v-if="recommendations.length">
    <h4>Recommendations</h4>
    <ul>
      <li v-for="(rec, index) in recommendations" :key="index" class="recommendation">
        {{ rec }}
      </li>
    </ul>
  </div>
</template>

Advanced Usage

Persisting Quality History

Store quality data for post-call analysis:

typescript
import { watch } from 'vue'
import { useCallQualityScore } from 'vuesip'

const { history } = useCallQualityScore({
  historySize: 1000, // Store more history
})

// Export history after call
function exportQualityReport() {
  return {
    callId: currentCall.value?.id,
    startTime: history.value[0]?.timestamp,
    endTime: history.value[history.value.length - 1]?.timestamp,
    averageScore: calculateAverage(history.value.map((h) => h.overall)),
    minScore: Math.min(...history.value.map((h) => h.overall)),
    maxScore: Math.max(...history.value.map((h) => h.overall)),
    qualityTimeline: history.value.map((h) => ({
      timestamp: h.timestamp,
      score: h.overall,
      grade: h.grade,
    })),
  }
}

Multi-Call Quality Tracking

Track quality across multiple simultaneous calls:

typescript
import { reactive } from 'vue'
import { useCallQualityScore } from 'vuesip'

const qualityScorers = reactive(new Map<string, ReturnType<typeof useCallQualityScore>>())

function addCallQualityTracker(callId: string) {
  if (!qualityScorers.has(callId)) {
    qualityScorers.set(callId, useCallQualityScore())
  }
  return qualityScorers.get(callId)!
}

function removeCallQualityTracker(callId: string) {
  const tracker = qualityScorers.get(callId)
  if (tracker) {
    tracker.reset()
    qualityScorers.delete(callId)
  }
}

Quality-Based Adaptive Actions

Automatically respond to quality changes:

typescript
watch(score, (newScore) => {
  if (!newScore) return

  // Reduce video quality on poor network
  if (newScore.grade === 'D' || newScore.grade === 'F') {
    reduceVideoQuality()
  }

  // Show warning on degradation
  if (newScore.grade === 'D') {
    showQualityWarning('Connection quality is degraded')
  }

  // Suggest reconnection on critical
  if (newScore.grade === 'F') {
    suggestReconnection()
  }
})

Released under the MIT License.