Kulcare Docs
DeveloperTeleconsultation

Video Session Production Readiness

Purpose: Technical guide for the production-ready video session implementation with Vonage integration, state management, and UI components.


Table of Contents

  1. Overview
  2. Architecture
  3. State Management
  4. Frontend Implementation
  5. UI Components
  6. Backend Integration
  7. Error Handling
  8. Testing

Overview

The video session feature has been enhanced for production readiness with robust state management, reconnection handling, device selection, and network quality monitoring.

Key Features Implemented

FeatureDescription
State MachineType-safe state transitions with loading, waiting, connected, reconnecting, disconnected, failed, ended states
Token RefreshRefs-based token refresh to prevent stale closures
Device SelectionCamera and microphone selection UI
Network MonitoringReal-time connection quality indicators
Session Timeout30-minute warning notification
AccessibilityARIA live region announcements

Architecture

┌─────────────────────────────────────────────────────────────┐
│                      VideoSessionPage                        │
│                     (pages/VideoSessionPage.tsx)             │
└──────────────────────────┬──────────────────────────────────┘
                           │
        ┌──────────────────┼──────────────────┐
        ▼                  ▼                  ▼
┌───────────────┐  ┌─────────────────┐  ┌──────────────┐
│useVideoSession│  │  useVideoSession│  │ PreCallSetup │
│    State      │  │  (React Query)  │  │  Component   │
│  (SDK + State │  │  + mutations    │  │              │
│   Machine)    │  │                 │  │              │
└───────────────┘  └─────────────────┘  └──────────────┘
        │                  │
        ▼                  ▼
┌─────────────────────────────────────────────────────────────┐
│              Vonage SDK (window.OT)                          │
│    Loaded via CDN in apps/cms/index.html                    │
└─────────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                    Backend API                              │
│          /clinical/encounters/:id/video_session             │
└─────────────────────────────────────────────────────────────┘

State Management

State Machine Hook

Location: frontend/apps/cms/src/features/video-sessions/hooks/useVideoSessionState.ts

States

loading → no_session / pre_call_setup
pre_call_setup → joining
joining → waiting_for_participants / error
waiting_for_participants → call_active / reconnecting / call_ended
call_active → reconnecting / call_ended
reconnecting → call_active / error / call_ended
call_ended → (terminal)
error → (terminal)

Configuration

interface VideoSessionStateOptions {
  maxReconnectAttempts?: number;  // Default: 3
  reconnectBaseDelayMs?: number;  // Default: 1000
  reconnectMaxDelayMs?: number;   // Default: 10000
  tokenRefreshIntervalMs?: number; // Default: 23 hours
}

Usage

const {
  state,
  canJoin,
  canStart,
  canEnd,
  error,
  initiateJoin,
  initiateStart,
  initiateEnd,
} = useVideoSessionState({
  maxReconnectAttempts: 3,
  onStateChange: (newState) => {
    console.log('State changed to:', newState);
  },
});

Frontend Implementation

Hooks

useVideoSessionState

Location: frontend/apps/cms/src/features/video-sessions/hooks/useVideoSessionState.ts

State machine hook that manages the entire Vonage session lifecycle: SDK loading, session initialization, event handling, reconnection, and state transitions.

Key Features:

  • Waits for window.OT SDK to load (polls with 15s timeout)
  • Creates session via OT.initSession(apiKey, sessionId)
  • Connects with token via session.connect(token)
  • Sets up all Vonage event handlers (streams, connections, reconnection)
  • Exposes state machine: idle -> initializing -> waiting -> connected -> disconnected/ended
  • Exponential backoff reconnection (configurable max attempts)

Configuration:

const {
  state,            // Current session state
  isConnected,      // Whether connected to Vonage
  hasRemoteParticipant, // Whether remote stream exists
  error,            // Error message if failed
  connect,          // (apiKey, sessionId, token) => Promise<Session>
  disconnect,       // () => void
  session,          // Current OT.Session reference
  reconnectAttempts,// Ref tracking reconnect count
} = useVideoSessionState({
  maxReconnectAttempts: 3,  // Default: 3
});

React Query API Hooks

Location: frontend/packages/shared-services/src/domains/clinical/hooks.ts

HookPurpose
useVideoSession(encounterId)GET session status (skips retry on 404)
useCreateVideoSession()POST create session
useJoinVideoSession()POST join session (returns token_data)
useStartVideoSession()POST mark session active
useCompleteVideoSession()POST end session
useDeclineVideoSession()POST decline invitation
useRefreshVideoSessionToken()POST get new token (no cache invalidation)

Token Refresh Pattern

Token refresh in VideoSessionPage.tsx uses refs to avoid stale closures in the interval callback:

const tokenExpiryRef = useRef<number | null>(null);
const refreshTokenMutationRef = useRef<ReturnType<typeof useRefreshVideoSessionToken> | null>(null);
const encounterIdRef = useRef(encounterId);

const refreshInterval = setInterval(() => {
  const now = Date.now();
  const timeUntilExpiry = (tokenExpiryRef.current ?? 0) - now;
  if (timeUntilExpiry < 5 * 60 * 1000) {
    refreshTokenMutationRef.current?.mutate(encounterIdRef.current!, {
      onSuccess: (data: { token: string; token_expires_at: string }) => {
        tokenExpiryRef.current = new Date(data.token_expires_at).getTime();
      }
    });
  }
}, 60000);

UI Components

PreCallSetup

Location: frontend/apps/cms/src/features/video-sessions/components/PreCallSetup.tsx

Device selection before joining a session.

Features:

  • Camera enumeration
  • Microphone enumeration
  • Device switching
const [videoDevices, setVideoDevices] = useState<MediaDevice[]>([]);
const [selectedVideoDevice, setSelectedVideoDevice] = useState<string>('');
const [selectedAudioDevice, setSelectedAudioDevice] = useState<string>('');

const switchCamera = async (deviceId: string) => {
  const newStream = await navigator.mediaDevices.getUserMedia({
    video: { deviceId: { exact: deviceId } }
  });
  // Replace track in existing stream...
};

const switchMicrophone = async (deviceId: string) => {
  // Similar implementation for audio
};

Network Quality Monitoring

Location: Inline in VideoSessionPage.tsx (not a separate component)

Polls Vonage stats every 10 seconds and displays connection quality in the header.

Quality Levels:

LevelPacket LossJitterLatencyColor
Excellent< 1%< 20ms< 100msGreen
Good< 5%< 50ms< 200msBlue
Poor< 10%< 100ms< 500msYellow
Bad> 10%> 100ms> 500msRed
const statsInterval = setInterval(() => {
  session.getStats((stats) => {
    setConnectionStats({
      packetsLost: stats.packetsLost,
      rtt: stats.rtt,
      jitter: stats.jitter,
    });
  });
}, 10000);

Call Duration Timer

Location: Inline in VideoSessionPage.tsx (not a separate component)

Displays elapsed call time in the header using a 1-second interval. Format: MM:SS or HH:MM:SS if over 1 hour. Includes 30-minute timeout warning toast when in waiting state.

WaitingRoom / SessionSummary

Status: Not yet implemented as separate components. Currently handled inline in VideoSessionPage.tsx:

  • Waiting state shows a blue banner: "Waiting for participant to join..."
  • Session end navigates back to encounters list

Backend Integration

API Endpoints

MethodPathDescription
GET/clinical/encounters/:id/video_sessionGet session status
POST/clinical/encounters/:id/video_sessionCreate session
POST/clinical/encounters/:id/video_session/joinJoin session
POST/clinical/encounters/:id/video_session/tokenGet provider token
POST/clinical/encounters/:id/video_session/startStart session
POST/clinical/encounters/:id/video_session/completeEnd session

Create Service Enhancement

Location: backend/domains/clinical/services/video_sessions/create_service.rb

Returns existing session instead of error on duplicate creation:

def call
  existing = Clinical::VideoSession
    .for_encounter(@encounter.id)
    .where(status: %w[created active])
    .first

  if existing
    Rails.logger.info("[VideoSession] Returning existing session #{existing.id}")
    return success(existing)
  end

  # ... rest of create logic
end

Error Response Structure

ERROR_CODES = {
  SESSION_NOT_FOUND: 'VIDEO_SESSION_NOT_FOUND',
  SESSION_NOT_JOINABLE: 'VIDEO_SESSION_NOT_JOINABLE',
  SESSION_COMPLETED: 'VIDEO_SESSION_COMPLETED',
}.freeze

def render_error(code:, message:, status: :unprocessable_entity)
  render json: {
    error: {
      code: code,
      message: message
    }
  }, status: status
end

Error Handling

Frontend Error States

StateTriggerRecovery
errorConnection failed, auth errorShow error UI, allow retry
reconnectingLost connectionAuto-reconnect with backoff
call_endedSession completedShow summary

Prevents accidental navigation away from active session:

useEffect(() => {
  const handleBeforeUnload = (e: BeforeUnloadEvent) => {
    if (state === 'call_active' || state === 'reconnecting') {
      e.preventDefault();
      e.returnValue = 'You are in an active call. Are you sure you want to leave?';
    }
  };

  window.addEventListener('beforeunload', handleBeforeUnload);
  return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [state]);

Cleanup

Proper cleanup with destroy() calls:

useEffect(() => {
  return () => {
    // Clean up publisher
    if (publisherRef.current) {
      publisherRef.current.destroy();
    }
    // Clean up subscriber
    if (subscriberRef.current) {
      subscriberRef.current.destroy();
    }
    // Clear intervals
    clearInterval(refreshInterval);
    clearInterval(statsInterval);
  };
}, []);

Testing

Test Coverage Areas

AreaWhat to Test
State MachineAll state transitions, computed properties
ReconnectionBackoff logic, max attempts
Token RefreshTiming, stale closure prevention
Device SelectionEnumeration, switching
Network QualityStats polling, quality calculation
CleanupAll resources released on unmount

E2E Test Scenarios

  1. Happy Path: Create session → Join → Start → End
  2. Reconnection: Simulate disconnect, verify auto-reconnect
  3. Device Switching: Change camera/mic mid-call
  4. Network Quality: Simulate poor network, verify indicator
  5. Timeout: Wait 30 minutes, verify timeout handling

Rollback Plan

If issues arise:

  1. Disable via feature flag
  2. Revert to previous VideoSessionPage
  3. Keep backend changes (backwards compatible)

Success Criteria

MetricTarget
Session success rate> 95%
Reconnection success> 90%
Error rate< 5%
Page refresh recovery> 90%

On this page