πŸ“š Telemedicine Platforms 24 min read

How to Build an Enterprise Telemedicine Platform with WebRTC Video, EHR Integration, and Multi-Provider Scheduling

Complete guide to building production-ready telemedicine platforms supporting thousands of concurrent video visits, EHR integration via FHIR, intelligent scheduling, provider availability management, and HIPAA compliance. Includes WebRTC implementation, signaling server architecture, recording, and quality monitoring for 99.9% uptime virtual care delivery.

✍️
Dr. Sarah Chen

Enterprise telemedicine platforms must support thousands of concurrent high-quality video visits, seamlessly integrate with existing EHR systems, manage complex multi-provider scheduling, maintain HIPAA compliance with encrypted video streams, and provide 99.9% uptime reliabilityβ€”yet 71% of custom implementations fail to scale beyond 50 concurrent sessions due to poor WebRTC architecture, inadequate server infrastructure, or lack of quality monitoring. Production-ready telemedicine platforms require WebRTC mesh or SFU topology for video delivery, TURN/STUN servers for NAT traversal, Redis-based signaling for real-time coordination, FHIR R4 integration for EHR context, intelligent load balancing across media servers, and comprehensive quality monitoringβ€”all while encrypting video streams end-to-end and maintaining detailed audit logs.

JustCopy.ai’s 10 specialized AI agents can build complete enterprise telemedicine platforms, automatically generating WebRTC infrastructure, EHR integration, scheduling systems, and compliance frameworks.

System Architecture

Enterprise telemedicine platforms integrate multiple layers:

  1. WebRTC Video Infrastructure: Peer connections, media servers
  2. Signaling Server: WebSocket coordination for session setup
  3. TURN/STUN Servers: NAT traversal for connectivity
  4. Media Recording: Encrypted visit recording storage
  5. EHR Integration: FHIR R4 APIs for patient context
  6. Scheduling Engine: Multi-provider availability management
  7. Quality Monitoring: Real-time video quality metrics
  8. Load Balancing: Geographic distribution of media servers
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Client Applications                   β”‚
β”‚  Provider Web App β”‚ Patient Mobile App       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                β”‚
                β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         API Gateway (Load Balanced)          β”‚
β”‚  - Authentication                            β”‚
β”‚  - Rate limiting                             β”‚
β”‚  - Request routing                           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β–Ό                  β–Ό                  β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Signaling  β”‚  β”‚   Media Servers  β”‚  β”‚    EHR     β”‚
β”‚   Server    β”‚  β”‚  (SFU Topology)  β”‚  β”‚Integration β”‚
β”‚ (WebSocket) β”‚  β”‚  - TURN/STUN     β”‚  β”‚   (FHIR)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  - Recording     β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                 β”‚  - Transcoding   β”‚
                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
                          β–Ό
                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                 β”‚  CDN/Storage     β”‚
                 β”‚ - Visit recordingsβ”‚
                 β”‚ - Encrypted at restβ”‚
                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Database Schema

-- Telemedicine appointments
CREATE TABLE telemedicine_appointments (
    appointment_id BIGSERIAL PRIMARY KEY,

    -- Participants
    patient_id BIGINT NOT NULL,
    provider_id BIGINT NOT NULL,

    -- Scheduling
    scheduled_start TIMESTAMP NOT NULL,
    scheduled_duration_minutes INTEGER DEFAULT 30,

    -- Visit details
    visit_type VARCHAR(50) NOT NULL, -- routine, urgent, follow_up
    visit_reason TEXT,
    specialty VARCHAR(100),

    -- Status
    appointment_status VARCHAR(30) DEFAULT 'scheduled',
    -- scheduled, in_waiting_room, in_progress, completed, cancelled, no_show

    -- Visit session details
    actual_start_time TIMESTAMP,
    actual_end_time TIMESTAMP,
    actual_duration_minutes INTEGER,

    -- Video session
    session_id UUID,
    video_room_url VARCHAR(500),

    -- EHR integration
    encounter_id VARCHAR(100), -- EHR encounter reference
    encounter_created BOOLEAN DEFAULT FALSE,

    -- Recording
    recording_enabled BOOLEAN DEFAULT TRUE,
    recording_url VARCHAR(500),
    recording_duration_seconds INTEGER,

    -- Quality metrics
    video_quality_score DECIMAL(3,2), -- 0-5 score
    audio_quality_score DECIMAL(3,2),
    connection_quality VARCHAR(20), -- excellent, good, fair, poor

    -- Patient experience
    patient_satisfaction_rating INTEGER, -- 1-5
    patient_feedback TEXT,

    -- Provider notes
    visit_completed BOOLEAN DEFAULT FALSE,
    documentation_completed BOOLEAN DEFAULT FALSE,

    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_tele_appt_patient ON telemedicine_appointments(patient_id);
CREATE INDEX idx_tele_appt_provider ON telemedicine_appointments(provider_id);
CREATE INDEX idx_tele_appt_scheduled ON telemedicine_appointments(scheduled_start);
CREATE INDEX idx_tele_appt_status ON telemedicine_appointments(appointment_status);

-- Video session management
CREATE TABLE video_sessions (
    session_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    appointment_id BIGINT REFERENCES telemedicine_appointments(appointment_id),

    -- Session configuration
    session_type VARCHAR(20), -- peer_to_peer, sfu, mesh
    max_participants INTEGER DEFAULT 2,

    -- Media server assignment
    assigned_media_server VARCHAR(200),
    media_server_region VARCHAR(50),

    -- Connection details
    ice_servers JSONB, -- STUN/TURN server configuration
    signaling_server_url VARCHAR(500),

    -- Session lifecycle
    session_status VARCHAR(30) DEFAULT 'initializing',
    -- initializing, active, ended, failed
    session_started TIMESTAMP,
    session_ended TIMESTAMP,

    -- Participants
    participants_joined JSONB,
    -- [{participant_id, participant_type, join_time, leave_time}]

    -- Quality monitoring
    avg_bitrate_kbps INTEGER,
    packet_loss_percent DECIMAL(5,2),
    avg_latency_ms INTEGER,
    resolution_height INTEGER,
    resolution_width INTEGER,
    frames_per_second INTEGER,

    -- Encryption
    encryption_enabled BOOLEAN DEFAULT TRUE,
    dtls_fingerprint VARCHAR(200),

    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_video_sessions_appointment ON video_sessions(appointment_id);
CREATE INDEX idx_video_sessions_status ON video_sessions(session_status);

-- Provider virtual availability
CREATE TABLE provider_virtual_availability (
    availability_id SERIAL PRIMARY KEY,
    provider_id BIGINT NOT NULL,

    -- Schedule
    day_of_week INTEGER NOT NULL, -- 0-6 (Sunday-Saturday)
    start_time TIME NOT NULL,
    end_time TIME NOT NULL,

    -- Availability type
    availability_type VARCHAR(30) DEFAULT 'virtual_only',
    -- virtual_only, hybrid, in_person_only

    -- Slot configuration
    slot_duration_minutes INTEGER DEFAULT 30,
    buffer_between_slots_minutes INTEGER DEFAULT 5,
    max_concurrent_virtual_visits INTEGER DEFAULT 1,

    -- Override dates (exceptions)
    exception_dates DATE[],

    -- Status
    is_active BOOLEAN DEFAULT TRUE,

    effective_start_date DATE NOT NULL,
    effective_end_date DATE,

    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_provider_avail_provider ON provider_virtual_availability(provider_id);
CREATE INDEX idx_provider_avail_dow ON provider_virtual_availability(day_of_week);

-- Virtual waiting room
CREATE TABLE virtual_waiting_room (
    waiting_room_entry_id BIGSERIAL PRIMARY KEY,
    appointment_id BIGINT REFERENCES telemedicine_appointments(appointment_id),

    -- Patient details
    patient_id BIGINT NOT NULL,

    -- Entry time
    entered_waiting_room TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    estimated_wait_minutes INTEGER,

    -- Status
    waiting_room_status VARCHAR(30) DEFAULT 'waiting',
    -- waiting, called_by_provider, joined_visit, left

    -- Provider notification
    provider_notified BOOLEAN DEFAULT FALSE,
    provider_notified_timestamp TIMESTAMP,

    -- Technical readiness check
    camera_working BOOLEAN,
    microphone_working BOOLEAN,
    connection_quality VARCHAR(20),

    left_waiting_room TIMESTAMP
);

CREATE INDEX idx_waiting_room_appointment ON virtual_waiting_room(appointment_id);
CREATE INDEX idx_waiting_room_patient ON virtual_waiting_room(patient_id);
CREATE INDEX idx_waiting_room_status ON virtual_waiting_room(waiting_room_status);

-- Quality metrics log
CREATE TABLE video_quality_metrics (
    metric_id BIGSERIAL PRIMARY KEY,
    session_id UUID REFERENCES video_sessions(session_id),

    -- Participant
    participant_id BIGINT NOT NULL,
    participant_type VARCHAR(20), -- provider, patient

    -- Timestamp
    metric_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    -- Video metrics
    video_bitrate_kbps INTEGER,
    video_resolution_width INTEGER,
    video_resolution_height INTEGER,
    video_fps INTEGER,
    video_packet_loss_percent DECIMAL(5,2),

    -- Audio metrics
    audio_bitrate_kbps INTEGER,
    audio_packet_loss_percent DECIMAL(5,2),

    -- Network metrics
    rtt_ms INTEGER, -- Round-trip time
    jitter_ms INTEGER,

    -- Browser/device info
    browser_type VARCHAR(50),
    device_type VARCHAR(50), -- desktop, mobile, tablet

    -- Connection type
    connection_type VARCHAR(50), -- wifi, ethernet, cellular

    -- CPU usage
    cpu_usage_percent INTEGER
);

CREATE INDEX idx_quality_metrics_session ON video_quality_metrics(session_id);
CREATE INDEX idx_quality_metrics_timestamp ON video_quality_metrics(metric_timestamp DESC);

-- Visit recordings
CREATE TABLE visit_recordings (
    recording_id BIGSERIAL PRIMARY KEY,
    session_id UUID REFERENCES video_sessions(session_id),
    appointment_id BIGINT REFERENCES telemedicine_appointments(appointment_id),

    -- Recording details
    recording_start_time TIMESTAMP NOT NULL,
    recording_end_time TIMESTAMP,
    recording_duration_seconds INTEGER,

    -- Storage
    storage_path VARCHAR(500) NOT NULL, -- S3/cloud storage path
    file_size_bytes BIGINT,
    video_codec VARCHAR(50),
    audio_codec VARCHAR(50),

    -- Encryption
    encryption_key_id VARCHAR(200), -- KMS key ID
    encrypted_at_rest BOOLEAN DEFAULT TRUE,

    -- Access control
    accessible_by_provider BOOLEAN DEFAULT TRUE,
    accessible_by_patient BOOLEAN DEFAULT FALSE,
    retention_days INTEGER DEFAULT 2555, -- 7 years default
    auto_delete_date DATE,

    -- Processing status
    processing_status VARCHAR(30) DEFAULT 'recording',
    -- recording, processing, available, archived, deleted

    -- Compliance
    consent_obtained BOOLEAN DEFAULT FALSE,
    hipaa_compliant BOOLEAN DEFAULT TRUE,

    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_recordings_session ON visit_recordings(session_id);
CREATE INDEX idx_recordings_appointment ON visit_recordings(appointment_id);
CREATE INDEX idx_recordings_auto_delete ON visit_recordings(auto_delete_date);

JustCopy.ai generates this comprehensive schema optimized for enterprise telemedicine with WebRTC, quality monitoring, and compliance.

WebRTC Implementation

// Enterprise Telemedicine WebRTC Infrastructure
// Scalable SFU architecture with quality monitoring
// Built with JustCopy.ai's video and infrastructure agents

import { EventEmitter } from 'events';

interface MediaServerConfig {
  region: string;
  stunServers: string[];
  turnServers: TURNServer[];
}

interface TURNServer {
  urls: string;
  username: string;
  credential: string;
}

class EnterpriseTelemedicineVideo extends EventEmitter {
  private peerConnection: RTCPeerConnection | null = null;
  private localStream: MediaStream | null = null;
  private remoteStream: MediaStream | null = null;
  private dataChannel: RTCDataChannel | null = null;
  private signalingClient: SignalingClient;
  private qualityMonitor: VideoQualityMonitor;
  private sessionId: string;

  constructor(sessionId: string, signalingServerUrl: string) {
    super();
    this.sessionId = sessionId;
    this.signalingClient = new SignalingClient(signalingServerUrl);
    this.qualityMonitor = new VideoQualityMonitor();
  }

  async initialize(mediaServerConfig: MediaServerConfig): Promise<void> {
    try {
      // Get optimal media server based on region
      const iceServers = this.buildICEServers(mediaServerConfig);

      // Create RTCPeerConnection with optimal configuration
      this.peerConnection = new RTCPeerConnection({
        iceServers: iceServers,
        iceTransportPolicy: 'all',
        bundlePolicy: 'max-bundle',
        rtcpMuxPolicy: 'require',
        iceCandidatePoolSize: 10
      });

      // Setup event handlers
      this.setupPeerConnectionHandlers();

      // Create data channel for signaling
      this.dataChannel = this.peerConnection.createDataChannel('visit-data', {
        ordered: true
      });

      this.setupDataChannelHandlers();

      // Connect to signaling server
      await this.signalingClient.connect(this.sessionId);

      this.emit('initialized');

    } catch (error) {
      this.emit('error', { type: 'initialization', error });
      throw error;
    }
  }

  private buildICEServers(config: MediaServerConfig): RTCIceServer[] {
    const iceServers: RTCIceServer[] = [];

    // STUN servers
    config.stunServers.forEach(url => {
      iceServers.push({ urls: url });
    });

    // TURN servers (for NAT traversal)
    config.turnServers.forEach(turn => {
      iceServers.push({
        urls: turn.urls,
        username: turn.username,
        credential: turn.credential,
        credentialType: 'password'
      });
    });

    return iceServers;
  }

  private setupPeerConnectionHandlers(): void {
    if (!this.peerConnection) return;

    // ICE candidate handler
    this.peerConnection.onicecandidate = (event) => {
      if (event.candidate) {
        this.signalingClient.sendICECandidate(event.candidate);
      }
    };

    // ICE connection state monitoring
    this.peerConnection.oniceconnectionstatechange = () => {
      const state = this.peerConnection!.iceConnectionState;
      this.emit('connection-state-change', state);

      if (state === 'failed') {
        this.handleConnectionFailure();
      } else if (state === 'connected') {
        this.startQualityMonitoring();
      }
    };

    // Remote stream handler
    this.peerConnection.ontrack = (event) => {
      this.remoteStream = event.streams[0];
      this.emit('remote-stream', this.remoteStream);
    };

    // Negotiation needed
    this.peerConnection.onnegotiationneeded = async () => {
      try {
        await this.createAndSendOffer();
      } catch (error) {
        this.emit('error', { type: 'negotiation', error });
      }
    };
  }

  private setupDataChannelHandlers(): void {
    if (!this.dataChannel) return;

    this.dataChannel.onopen = () => {
      this.emit('data-channel-open');
    };

    this.dataChannel.onmessage = (event) => {
      const data = JSON.parse(event.data);
      this.handleDataChannelMessage(data);
    };
  }

  async startLocalMedia(constraints?: MediaStreamConstraints): Promise<MediaStream> {
    try {
      // High-quality video/audio constraints
      const mediaConstraints: MediaStreamConstraints = constraints || {
        video: {
          width: { ideal: 1280 },
          height: { ideal: 720 },
          frameRate: { ideal: 30 },
          facingMode: 'user'
        },
        audio: {
          echoCancellation: true,
          noiseSuppression: true,
          autoGainControl: true,
          sampleRate: 48000
        }
      };

      this.localStream = await navigator.mediaDevices.getUserMedia(mediaConstraints);

      // Add tracks to peer connection
      if (this.peerConnection) {
        this.localStream.getTracks().forEach(track => {
          this.peerConnection!.addTrack(track, this.localStream!);
        });
      }

      this.emit('local-stream', this.localStream);

      return this.localStream;

    } catch (error) {
      this.emit('error', { type: 'media-access', error });
      throw error;
    }
  }

  async createAndSendOffer(): Promise<void> {
    if (!this.peerConnection) return;

    try {
      const offer = await this.peerConnection.createOffer({
        offerToReceiveAudio: true,
        offerToReceiveVideo: true
      });

      await this.peerConnection.setLocalDescription(offer);

      // Send offer via signaling
      await this.signalingClient.sendOffer({
        type: 'offer',
        sdp: offer.sdp!,
        sessionId: this.sessionId
      });

    } catch (error) {
      this.emit('error', { type: 'offer-creation', error });
      throw error;
    }
  }

  async handleRemoteOffer(offer: RTCSessionDescriptionInit): Promise<void> {
    if (!this.peerConnection) return;

    try {
      await this.peerConnection.setRemoteDescription(offer);

      const answer = await this.peerConnection.createAnswer();
      await this.peerConnection.setLocalDescription(answer);

      await this.signalingClient.sendAnswer({
        type: 'answer',
        sdp: answer.sdp!,
        sessionId: this.sessionId
      });

    } catch (error) {
      this.emit('error', { type: 'answer-creation', error });
      throw error;
    }
  }

  async handleRemoteAnswer(answer: RTCSessionDescriptionInit): Promise<void> {
    if (!this.peerConnection) return;

    try {
      await this.peerConnection.setRemoteDescription(answer);
    } catch (error) {
      this.emit('error', { type: 'remote-description', error });
      throw error;
    }
  }

  async addICECandidate(candidate: RTCIceCandidateInit): Promise<void> {
    if (!this.peerConnection) return;

    try {
      await this.peerConnection.addIceCandidate(candidate);
    } catch (error) {
      this.emit('error', { type: 'ice-candidate', error });
    }
  }

  private startQualityMonitoring(): void {
    if (!this.peerConnection) return;

    // Monitor quality every 2 seconds
    this.qualityMonitor.start(this.peerConnection, (metrics) => {
      this.emit('quality-metrics', metrics);

      // Send to backend for storage
      this.reportQualityMetrics(metrics);

      // Check for quality issues
      if (metrics.videoPacketLoss > 5 || metrics.rttMs > 300) {
        this.emit('quality-warning', {
          type: 'degraded-quality',
          metrics
        });
      }
    });
  }

  private async reportQualityMetrics(metrics: any): Promise<void> {
    try {
      await fetch('/api/telemedicine/quality-metrics', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          session_id: this.sessionId,
          metrics: metrics,
          timestamp: new Date().toISOString()
        })
      });
    } catch (error) {
      // Silent fail - don't disrupt video for metrics reporting
    }
  }

  private async handleConnectionFailure(): Promise<void> {
    this.emit('connection-failed');

    // Attempt ICE restart
    try {
      if (this.peerConnection) {
        const offer = await this.peerConnection.createOffer({
          iceRestart: true
        });

        await this.peerConnection.setLocalDescription(offer);
        await this.signalingClient.sendOffer({
          type: 'offer',
          sdp: offer.sdp!,
          sessionId: this.sessionId,
          iceRestart: true
        });

        this.emit('reconnecting');
      }
    } catch (error) {
      this.emit('error', { type: 'reconnection-failed', error });
    }
  }

  private handleDataChannelMessage(data: any): void {
    switch (data.type) {
      case 'chat':
        this.emit('chat-message', data.message);
        break;
      case 'vital-sign':
        this.emit('vital-sign-data', data.vitalSign);
        break;
      case 'device-data':
        this.emit('device-data', data.deviceData);
        break;
    }
  }

  sendDataChannelMessage(type: string, payload: any): void {
    if (this.dataChannel && this.dataChannel.readyState === 'open') {
      this.dataChannel.send(JSON.stringify({ type, ...payload }));
    }
  }

  async toggleVideo(enabled: boolean): Promise<void> {
    if (this.localStream) {
      this.localStream.getVideoTracks().forEach(track => {
        track.enabled = enabled;
      });
      this.emit('video-toggled', enabled);
    }
  }

  async toggleAudio(enabled: boolean): Promise<void> {
    if (this.localStream) {
      this.localStream.getAudioTracks().forEach(track => {
        track.enabled = enabled;
      });
      this.emit('audio-toggled', enabled);
    }
  }

  async switchCamera(): Promise<void> {
    if (!this.localStream) return;

    try {
      // Stop current video track
      const videoTrack = this.localStream.getVideoTracks()[0];
      videoTrack.stop();

      // Get new video stream with opposite facing mode
      const currentFacingMode = videoTrack.getSettings().facingMode;
      const newFacingMode = currentFacingMode === 'user' ? 'environment' : 'user';

      const newStream = await navigator.mediaDevices.getUserMedia({
        video: { facingMode: newFacingMode }
      });

      const newVideoTrack = newStream.getVideoTracks()[0];

      // Replace track in peer connection
      const sender = this.peerConnection!.getSenders().find(
        s => s.track?.kind === 'video'
      );

      if (sender) {
        await sender.replaceTrack(newVideoTrack);
      }

      // Update local stream
      this.localStream.removeTrack(videoTrack);
      this.localStream.addTrack(newVideoTrack);

      this.emit('camera-switched', newFacingMode);

    } catch (error) {
      this.emit('error', { type: 'camera-switch', error });
    }
  }

  disconnect(): void {
    // Stop quality monitoring
    this.qualityMonitor.stop();

    // Close data channel
    if (this.dataChannel) {
      this.dataChannel.close();
    }

    // Close peer connection
    if (this.peerConnection) {
      this.peerConnection.close();
    }

    // Stop local media
    if (this.localStream) {
      this.localStream.getTracks().forEach(track => track.stop());
    }

    // Disconnect signaling
    this.signalingClient.disconnect();

    this.emit('disconnected');
  }
}

// Signaling client for WebSocket communication
class SignalingClient {
  private ws: WebSocket | null = null;
  private serverUrl: string;

  constructor(serverUrl: string) {
    this.serverUrl = serverUrl;
  }

  async connect(sessionId: string): Promise<void> {
    return new Promise((resolve, reject) => {
      this.ws = new WebSocket(`${this.serverUrl}?sessionId=${sessionId}`);

      this.ws.onopen = () => {
        resolve();
      };

      this.ws.onerror = (error) => {
        reject(error);
      };

      this.ws.onmessage = (event) => {
        this.handleSignalingMessage(JSON.parse(event.data));
      };
    });
  }

  private handleSignalingMessage(message: any): void {
    // Emit to video engine
    // [Would be handled by parent class]
  }

  sendOffer(offer: any): Promise<void> {
    return this.send({ type: 'offer', ...offer });
  }

  sendAnswer(answer: any): Promise<void> {
    return this.send({ type: 'answer', ...answer });
  }

  sendICECandidate(candidate: RTCIceCandidate): Promise<void> {
    return this.send({
      type: 'ice-candidate',
      candidate: candidate.toJSON()
    });
  }

  private async send(message: any): Promise<void> {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(message));
    }
  }

  disconnect(): void {
    if (this.ws) {
      this.ws.close();
    }
  }
}

// Video quality monitoring
class VideoQualityMonitor {
  private intervalId: any = null;
  private peerConnection: RTCPeerConnection | null = null;

  start(peerConnection: RTCPeerConnection, callback: (metrics: any) => void): void {
    this.peerConnection = peerConnection;

    this.intervalId = setInterval(async () => {
      const metrics = await this.collectMetrics();
      callback(metrics);
    }, 2000);
  }

  private async collectMetrics(): Promise<any> {
    if (!this.peerConnection) return {};

    const stats = await this.peerConnection.getStats();
    const metrics: any = {
      timestamp: new Date().toISOString(),
      videoBitrate: 0,
      audioBitrate: 0,
      videoPacketLoss: 0,
      audioPacketLoss: 0,
      rttMs: 0,
      jitterMs: 0,
      resolution: { width: 0, height: 0 },
      fps: 0
    };

    stats.forEach(report => {
      if (report.type === 'inbound-rtp' && report.kind === 'video') {
        metrics.videoBitrate = report.bytesReceived * 8 / 1000; // kbps
        metrics.videoPacketLoss = report.packetsLost / (report.packetsReceived + report.packetsLost) * 100;
        metrics.jitterMs = report.jitter * 1000;
        metrics.fps = report.framesPerSecond;
      }

      if (report.type === 'inbound-rtp' && report.kind === 'audio') {
        metrics.audioBitrate = report.bytesReceived * 8 / 1000;
        metrics.audioPacketLoss = report.packetsLost / (report.packetsReceived + report.packetsLost) * 100;
      }

      if (report.type === 'candidate-pair' && report.state === 'succeeded') {
        metrics.rttMs = report.currentRoundTripTime * 1000;
      }

      if (report.type === 'track' && report.kind === 'video') {
        metrics.resolution = {
          width: report.frameWidth,
          height: report.frameHeight
        };
      }
    });

    return metrics;
  }

  stop(): void {
    if (this.intervalId) {
      clearInterval(this.intervalId);
    }
  }
}

export { EnterpriseTelemedicineVideo, MediaServerConfig };

JustCopy.ai generates complete WebRTC infrastructure with quality monitoring, reconnection handling, and enterprise-grade reliability.

Implementation Timeline

22-Week Implementation:

  • Weeks 1-4: Architecture, WebRTC infrastructure
  • Weeks 5-8: Signaling server, TURN/STUN setup
  • Weeks 9-12: EHR integration (FHIR)
  • Weeks 13-16: Scheduling engine, provider availability
  • Weeks 17-18: Recording infrastructure
  • Weeks 19-20: Quality monitoring, analytics
  • Weeks 21-22: Load testing, optimization, launch

Using JustCopy.ai, this reduces to 8-10 weeks.

ROI Calculation

Large Health System (500 providers, 1.2M patients):

Benefits:

  • Virtual visit revenue (38% of visits): $18,400,000/year
  • Reduced no-shows: $4,200,000/year
  • Provider productivity gains: $3,800,000/year
  • Expanded access (new patients): $2,600,000/year
  • Total annual benefit: $29,000,000

3-Year ROI: 8,700%

JustCopy.ai makes enterprise telemedicine accessible, automatically generating WebRTC infrastructure, EHR integration, and quality monitoring systems that enable scalable virtual care delivery.

πŸš€

Build This with JustCopy.ai

Skip months of development with 10 specialized AI agents. JustCopy.ai can copy, customize, and deploy this application instantly. Our AI agents write code, run tests, handle deployment, and monitor your applicationβ€”all following healthcare industry best practices and HIPAA compliance standards.