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
- Overview
- Architecture
- State Management
- Frontend Implementation
- UI Components
- Backend Integration
- Error Handling
- 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
| Feature | Description |
|---|---|
| State Machine | Type-safe state transitions with loading, waiting, connected, reconnecting, disconnected, failed, ended states |
| Token Refresh | Refs-based token refresh to prevent stale closures |
| Device Selection | Camera and microphone selection UI |
| Network Monitoring | Real-time connection quality indicators |
| Session Timeout | 30-minute warning notification |
| Accessibility | ARIA 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.OTSDK 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
| Hook | Purpose |
|---|---|
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:
| Level | Packet Loss | Jitter | Latency | Color |
|---|---|---|---|---|
| Excellent | < 1% | < 20ms | < 100ms | Green |
| Good | < 5% | < 50ms | < 200ms | Blue |
| Poor | < 10% | < 100ms | < 500ms | Yellow |
| Bad | > 10% | > 100ms | > 500ms | Red |
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
| Method | Path | Description |
|---|---|---|
| GET | /clinical/encounters/:id/video_session | Get session status |
| POST | /clinical/encounters/:id/video_session | Create session |
| POST | /clinical/encounters/:id/video_session/join | Join session |
| POST | /clinical/encounters/:id/video_session/token | Get provider token |
| POST | /clinical/encounters/:id/video_session/start | Start session |
| POST | /clinical/encounters/:id/video_session/complete | End 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
endError 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
endError Handling
Frontend Error States
| State | Trigger | Recovery |
|---|---|---|
error | Connection failed, auth error | Show error UI, allow retry |
reconnecting | Lost connection | Auto-reconnect with backoff |
call_ended | Session completed | Show summary |
Navigation Guards
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
| Area | What to Test |
|---|---|
| State Machine | All state transitions, computed properties |
| Reconnection | Backoff logic, max attempts |
| Token Refresh | Timing, stale closure prevention |
| Device Selection | Enumeration, switching |
| Network Quality | Stats polling, quality calculation |
| Cleanup | All resources released on unmount |
E2E Test Scenarios
- Happy Path: Create session → Join → Start → End
- Reconnection: Simulate disconnect, verify auto-reconnect
- Device Switching: Change camera/mic mid-call
- Network Quality: Simulate poor network, verify indicator
- Timeout: Wait 30 minutes, verify timeout handling
Rollback Plan
If issues arise:
- Disable via feature flag
- Revert to previous VideoSessionPage
- Keep backend changes (backwards compatible)
Success Criteria
| Metric | Target |
|---|---|
| Session success rate | > 95% |
| Reconnection success | > 90% |
| Error rate | < 5% |
| Page refresh recovery | > 90% |