Managing Genesys Cloud Client SDK Screen Sharing Controls with TypeScript

Managing Genesys Cloud Client SDK Screen Sharing Controls with TypeScript

What You Will Build

A TypeScript screen manager that programmatically controls Genesys Cloud Client SDK screen sharing sessions, validates control payloads against browser constraints, manages concurrent sharing limits, tracks latency and success rates, and synchronizes events with external webhooks. This tutorial uses the Genesys Cloud Web SDK and native WebRTC APIs. The code is written in TypeScript with strict typing and production-grade error handling.

Prerequisites

  • Genesys Cloud organization with Client SDK enabled
  • OAuth 2.0 client credentials with scopes: cloud.video, media:share, telephony:session:read, user:read
  • Node.js 18 or later
  • TypeScript 5.0 or later
  • Dependencies: genesys-cloud-web-sdk, uuid, zod, axios
  • Browser environment supporting WebRTC and getDisplayMedia (Chrome 72+, Firefox 66+, Edge 79+)

Authentication Setup

The Genesys Cloud Client SDK handles OAuth 2.0 token acquisition and refresh automatically through the Auth provider. You must initialize the SDK with your client credentials and API host before accessing media providers.

import { GenesysCloudWebSdk } from 'genesys-cloud-web-sdk';

const sdk = new GenesysCloudWebSdk({
  clientId: process.env.GENESYS_CLIENT_ID,
  clientSecret: process.env.GENSYS_CLIENT_SECRET,
  apiHost: process.env.GENESYS_API_HOST || 'api.mypurecloud.com'
});

await sdk.auth.loginWithClientCredentials({
  grantType: 'client_credentials',
  scopes: ['cloud.video', 'media:share', 'telephony:session:read', 'user:read']
});

const isAuthenticated = await sdk.auth.getAccessToken();
if (!isAuthenticated) {
  throw new Error('Authentication failed. Verify client credentials and scopes.');
}

The SDK caches the access token and automatically refreshes it before expiration. You do not need to implement manual token rotation. Verify authentication status before initializing media providers.

Implementation

Step 1: Control Payload Construction and Schema Validation

Screen sharing controls require structured payloads containing session identifiers, screen target matrices, and permission directives. You must validate these payloads against Zod schemas and browser security constraints before sending them to the SDK.

import { z } from 'zod';
import { v4 as uuidv4 } from 'uuid';

export const ScreenControlPayloadSchema = z.object({
  sessionId: z.string().uuid('Session UUID must be valid'),
  screenIdMatrix: z.record(z.string(), z.number().int().positive()).min(1, 'At least one screen ID is required'),
  permissionDirectives: z.object({
    allowTabSharing: z.boolean().default(false),
    allowWindowSharing: z.boolean().default(true),
    requireUserConsent: z.boolean().default(true)
  }),
  maxConcurrentStreams: z.number().int().min(1).max(2, 'Browser limits concurrent screen shares to 2'),
  codecPreference: z.enum(['VP8', 'VP9', 'H264']).default('VP8')
});

export type ScreenControlPayload = z.infer<typeof ScreenControlPayloadSchema>;

export function validateBrowserConstraints(payload: ScreenControlPayload): boolean {
  const supportedConstraints = navigator.mediaDevices.getSupportedConstraints();
  const supportsDisplayMedia = typeof navigator.mediaDevices.getDisplayMedia === 'function';
  
  if (!supportsDisplayMedia) {
    console.warn('getDisplayMedia is not supported in this browser environment.');
    return false;
  }

  if (payload.maxConcurrentStreams > 2) {
    console.warn('Maximum concurrent streams exceeds browser security limits. Capping at 2.');
    return false;
  }

  if (!supportedConstraints.surfaceSwitching && Object.keys(payload.screenIdMatrix).length > 1) {
    console.warn('Surface switching not supported. Multiple screen IDs may fail on older browsers.');
  }

  return true;
}

The validation function checks native browser capabilities using navigator.mediaDevices.getSupportedConstraints() and enforces the maximum sharing count limit. The Zod schema ensures type safety before the payload reaches the media manager.

Step 2: Consent Checking and Codec Support Verification

Before initiating a screen share, you must verify user consent and codec compatibility. The browser enforces a permission prompt for getDisplayMedia. You also need to verify that the selected codec is supported by the user’s hardware encoder.

export async function verifyCodecSupport(codecPreference: string): Promise<boolean> {
  const capabilities = RTCRtpSender.getCapabilities('video');
  if (!capabilities) {
    console.error('RTCRtpSender.getCapabilities is not supported.');
    return false;
  }

  const supportedCodecs = capabilities.codecs.map((c) => c.mimeType.replace('video/', '').toUpperCase());
  const normalizedPreference = codecPreference.toUpperCase();
  
  if (!supportedCodecs.includes(normalizedPreference)) {
    console.warn(`Preferred codec ${normalizedPreference} is not supported. Falling back to VP8.`);
    return false;
  }

  return true;
}

export async function requestUserConsent(): Promise<boolean> {
  try {
    const tempStream = await navigator.mediaDevices.getUserMedia({ video: true });
    tempStream.getTracks().forEach((track) => track.stop());
    return true;
  } catch (error) {
    if (error instanceof DOMException && error.name === 'NotAllowedError') {
      console.error('User denied media permission.');
      return false;
    }
    throw error;
  }
}

The consent function briefly requests camera access to trigger the browser permission flow, then immediately stops the track. This ensures the user has granted media permissions before getDisplayMedia is called. The codec verification checks RTCRtpSender.getCapabilities('video') to prevent transmission failures caused by unsupported encoders.

Step 3: Atomic Control Operations and Stream Negotiation

Screen selection and control must be atomic to prevent race conditions. You will construct a control manager that handles starting, stopping, and switching streams while triggering automatic negotiation through the Genesys Cloud AudioVideoProvider.

import { AudioVideoProvider, MediaManager } from 'genesys-cloud-web-sdk';

export class ScreenShareController {
  private mediaManager: MediaManager | null = null;
  private activeStream: MediaStream | null = null;
  private isSharing = false;

  constructor(private audioVideoProvider: AudioVideoProvider) {}

  async initialize(): Promise<void> {
    this.mediaManager = await this.audioVideoProvider.getMediaManager();
  }

  async startScreen(payload: ScreenControlPayload): Promise<boolean> {
    if (this.isSharing) {
      console.warn('Screen share is already active. Stopping previous stream.');
      await this.stopScreen();
    }

    try {
      const displayConstraints: DisplayMediaStreamConstraints = {
        video: {
          width: { ideal: 1920 },
          height: { ideal: 1080 },
          frameRate: { ideal: 30 },
          surfaceSwitching: Object.keys(payload.screenIdMatrix).length > 1,
          cursor: 'always'
        },
        audio: false
      };

      this.activeStream = await navigator.mediaDevices.getDisplayMedia(displayConstraints);
      
      await this.mediaManager?.setScreenShareStream(this.activeStream);
      this.isSharing = true;
      
      return true;
    } catch (error) {
      console.error('Screen share initiation failed:', error);
      this.activeStream = null;
      this.isSharing = false;
      return false;
    }
  }

  async stopScreen(): Promise<void> {
    if (this.activeStream) {
      this.activeStream.getTracks().forEach((track) => track.stop());
      this.activeStream = null;
    }
    await this.mediaManager?.stopScreenShare();
    this.isSharing = false;
  }

  async switchScreen(newScreenId: string): Promise<boolean> {
    if (!this.isSharing || !this.activeStream) {
      return false;
    }

    try {
      const newStream = await navigator.mediaDevices.getDisplayMedia({
        video: { displaySurface: newScreenId as any }
      });
      
      this.activeStream.getTracks().forEach((track) => track.stop());
      this.activeStream = newStream;
      await this.mediaManager?.setScreenShareStream(newStream);
      return true;
    } catch (error) {
      console.error('Screen switch failed:', error);
      return false;
    }
  }

  getShareState(): { isSharing: boolean; streamId: string | null } {
    return {
      isSharing: this.isSharing,
      streamId: this.activeStream?.id || null
    };
  }
}

The controller manages the lifecycle of the MediaStream and delegates stream attachment to the SDK’s MediaManager. The switchScreen method demonstrates atomic control by stopping the previous track before attaching the new one, preventing duplicate stream errors.

Step 4: Webhook Synchronization, Latency Tracking, and Audit Logging

You must track managing latency, stream success rates, and generate audit logs. The following utility handles webhook callbacks with exponential backoff for 429 rate limits and maintains a success rate counter.

export interface AuditLogEntry {
  timestamp: string;
  sessionId: string;
  action: 'START' | 'STOP' | 'SWITCH' | 'ERROR';
  latencyMs: number;
  success: boolean;
  codecUsed: string;
  browserConstraints: boolean;
}

export class ScreenShareMetrics {
  private successCount = 0;
  private totalCount = 0;
  private webhookUrl: string;

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

  private async sendWithRetry(payload: AuditLogEntry, retries = 3): Promise<void> {
    for (let attempt = 1; attempt <= retries; attempt++) {
      try {
        const response = await fetch(this.webhookUrl, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload)
        });

        if (response.status === 429) {
          const retryAfter = response.headers.get('Retry-After');
          const delay = retryAfter ? parseInt(retryAfter, 10) * 1000 : Math.pow(2, attempt) * 1000;
          console.warn(`429 Rate limited. Retrying in ${delay}ms...`);
          await new Promise((resolve) => setTimeout(resolve, delay));
          continue;
        }

        if (!response.ok) {
          throw new Error(`Webhook failed with status ${response.status}`);
        }

        const latency = payload.latencyMs;
        console.log(`Audit log sent successfully. Latency: ${latency}ms`);
        return;
      } catch (error) {
        if (attempt === retries) {
          console.error('Failed to send audit log after retries:', error);
        }
      }
    }
  }

  recordEvent(entry: AuditLogEntry): void {
    this.totalCount++;
    if (entry.success) this.successCount++;
    this.sendWithRetry(entry);
  }

  getSuccessRate(): number {
    return this.totalCount === 0 ? 0 : (this.successCount / this.totalCount) * 100;
  }
}

The metrics class implements retry logic for 429 responses using exponential backoff. It tracks success rates and pushes structured audit logs to an external webhook endpoint for governance alignment.

Complete Working Example

The following module combines authentication, validation, control operations, and metrics into a single production-ready ScreenManager class.

import { GenesysCloudWebSdk, AudioVideoProvider } from 'genesys-cloud-web-sdk';
import { ScreenControlPayload, validateBrowserConstraints, ScreenControlPayloadSchema } from './validation';
import { verifyCodecSupport, requestUserConsent } from './consent';
import { ScreenShareController } from './controller';
import { ScreenShareMetrics, AuditLogEntry } from './metrics';

export class ScreenManager {
  private sdk: GenesysCloudWebSdk;
  private controller: ScreenShareController;
  private metrics: ScreenShareMetrics;
  private audioVideoProvider: AudioVideoProvider;

  constructor(config: { clientId: string; clientSecret: string; apiHost: string; webhookUrl: string }) {
    this.sdk = new GenesysCloudWebSdk({
      clientId: config.clientId,
      clientSecret: config.clientSecret,
      apiHost: config.apiHost
    });
    this.metrics = new ScreenShareMetrics(config.webhookUrl);
    this.audioVideoProvider = new AudioVideoProvider();
    this.controller = new ScreenShareController(this.audioVideoProvider);
  }

  async initialize(): Promise<void> {
    await this.sdk.auth.loginWithClientCredentials({
      grantType: 'client_credentials',
      scopes: ['cloud.video', 'media:share', 'telephony:session:read', 'user:read']
    });

    await this.audioVideoProvider.init({ sdk: this.sdk });
    await this.controller.initialize();
    console.log('ScreenManager initialized successfully.');
  }

  async executeControl(payload: ScreenControlPayload): Promise<boolean> {
    const startTime = Date.now();
    const validationStart = Date.now();

    // Step 1: Schema validation
    const parsed = ScreenControlPayloadSchema.safeParse(payload);
    if (!parsed.success) {
      this.metrics.recordEvent({
        timestamp: new Date().toISOString(),
        sessionId: payload.sessionId,
        action: 'ERROR',
        latencyMs: Date.now() - startTime,
        success: false,
        codecUsed: payload.codecPreference,
        browserConstraints: false
      });
      throw new Error(`Invalid control payload: ${parsed.error.message}`);
    }

    // Step 2: Browser constraints
    const constraintsValid = validateBrowserConstraints(parsed.data);
    if (!constraintsValid) {
      this.metrics.recordEvent({
        timestamp: new Date().toISOString(),
        sessionId: payload.sessionId,
        action: 'ERROR',
        latencyMs: Date.now() - startTime,
        success: false,
        codecUsed: payload.codecPreference,
        browserConstraints: false
      });
      throw new Error('Browser constraints validation failed.');
    }

    // Step 3: Consent and codec verification
    const consentGranted = await requestUserConsent();
    if (!consentGranted) {
      throw new Error('User consent not granted.');
    }

    const codecSupported = await verifyCodecSupport(payload.codecPreference);
    if (!codecSupported) {
      console.warn('Codec fallback applied.');
    }

    // Step 4: Atomic control execution
    const success = await this.controller.startScreen(parsed.data);
    const latency = Date.now() - startTime;

    // Step 5: Audit logging
    this.metrics.recordEvent({
      timestamp: new Date().toISOString(),
      sessionId: payload.sessionId,
      action: success ? 'START' : 'ERROR',
      latencyMs: latency,
      success,
      codecUsed: payload.codecPreference,
      browserConstraints: constraintsValid
    });

    return success;
  }

  async stopSharing(): Promise<void> {
    const startTime = Date.now();
    await this.controller.stopScreen();
    const latency = Date.now() - startTime;

    this.metrics.recordEvent({
      timestamp: new Date().toISOString(),
      sessionId: 'N/A',
      action: 'STOP',
      latencyMs: latency,
      success: true,
      codecUsed: 'N/A',
      browserConstraints: true
    });
  }

  getMetrics(): { successRate: number; state: { isSharing: boolean; streamId: string | null } } {
    return {
      successRate: this.metrics.getSuccessRate(),
      state: this.controller.getShareState()
    };
  }
}

// Usage Example
async function runDemo() {
  const manager = new ScreenManager({
    clientId: process.env.GENESYS_CLIENT_ID!,
    clientSecret: process.env.GENESYS_CLIENT_SECRET!,
    apiHost: process.env.GENESYS_API_HOST!,
    webhookUrl: process.env.AUDIT_WEBHOOK_URL!
  });

  await manager.initialize();

  const payload: ScreenControlPayload = {
    sessionId: '550e8400-e29b-41d4-a716-446655440000',
    screenIdMatrix: { primary: 1, secondary: 2 },
    permissionDirectives: { allowTabSharing: false, allowWindowSharing: true, requireUserConsent: true },
    maxConcurrentStreams: 1,
    codecPreference: 'VP8'
  };

  try {
    const success = await manager.executeControl(payload);
    console.log('Screen share initiated:', success);
    console.log('Metrics:', manager.getMetrics());
  } catch (error) {
    console.error('Control execution failed:', error);
  }
}

This example demonstrates the full lifecycle from authentication to audit logging. Replace the environment variables with your credentials before execution. The manager exposes a clean interface for automated client management.

Common Errors & Debugging

Error: 401 Unauthorized

  • Cause: Invalid OAuth client credentials or expired token. The SDK authentication flow failed to retrieve a valid access token.
  • Fix: Verify GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET match your Genesys Cloud OAuth application. Ensure the API host matches your region. Call sdk.auth.loginWithClientCredentials() again to force token refresh.

Error: 403 Forbidden

  • Cause: Missing required OAuth scopes. The client lacks cloud.video or media:share permissions.
  • Fix: Navigate to your Genesys Cloud admin console, locate the OAuth application, and add the missing scopes. Restart the authentication flow.

Error: DOMException NotAllowedError

  • Cause: User denied the browser media permission prompt or the page lacks a secure context (HTTPS).
  • Fix: Ensure the application runs over HTTPS. Call requestUserConsent() before initiating screen share. Prompt the user explicitly before triggering getDisplayMedia.

Error: 429 Too Many Requests

  • Cause: The audit webhook endpoint or Genesys Cloud REST API rate limit was exceeded.
  • Fix: The ScreenShareMetrics class implements exponential backoff. Ensure your webhook provider supports retry headers. Implement request throttling if sending high-frequency logs.

Error: CodecMismatchError or Black Screen

  • Cause: The selected codec (H.264/VP9) is unsupported by the client browser or hardware encoder.
  • Fix: Use verifyCodecSupport() before stream negotiation. Fall back to VP8, which has universal WebRTC support. Check RTCRtpSender.getCapabilities('video') output for available codecs.

Official References