Skip to main content

ECV Video Call Android SDK [EN]

Introduction

Welcome to the ECV Video Call Android SDK! This SDK allows you to easily integrate real-time video call functionality into your Android applications. It handles the complexities of signaling, WebRTC peer connections, media streams, and call state management, letting you focus on your application's user interface and business logic.

This repository contains:

  1. The SDK: An Android Library hosted on Artifactory. You will add this as a Maven dependency to your project.
  2. Demo Application: Full source code for a demonstration app (ecv175_android) showcasing how to use the SDK features. This demo is intended as a reference and example, not production-ready UI code.

Key Features

  • Establishes connection to ECV backend services (v1.75+).
  • Handles WebRTC setup and peer-to-peer connection.
  • Manages local and remote video/audio streams.
  • Provides controls for muting/unmuting audio and video.
  • Supports camera switching.
  • Manages call lifecycle events (dialing, queuing, connected, hold, ended).
  • Configurable environments and call parameters.

Getting Started

Prerequisites

  • Android Studio (Latest Stable Version Recommended)
  • Basic knowledge of Android development (Kotlin or Java).
  • Minimum Android SDK Version: 21 (Confirm this matches your SDK's minSdk)
  • Target Android SDK Version: 35 (Confirm this matches your SDK's targetSdk)

1. Add Required Libraries

Your application needs to include the ECV SDK library and its required dependencies by fetching them from your Artifactory repository. This is a two-step process: adding the repository and then declaring the dependencies.

  1. Add the Artifactory Repository to Your Project

    In modern Android projects, the best practice is to add repositories to the project-level settings.gradle (or settings.gradle.kts) file.

    settings.gradle.kts (Kotlin DSL):

    dependencyResolutionManagement {
        repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
        repositories {
            google()
            mavenCentral()
            // --- Add the ECV Artifactory Repository Here ---
            maven {
                url = uri("https://repo.easyconnect.cloud/repository/android-maven-public/")
            }
            // --- End ---
        }
    }
    

    settings.gradle (Groovy):

    dependencyResolutionManagement {
        repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
        repositories {
            google()
            mavenCentral()
            // --- Add the ECV Artifactory Repository Here ---
            maven {
                url 'https://repo.easyconnect.cloud/repository/android-maven-public/'
            }
            // --- End ---
        }
    }
    
  2. Add the SDK Dependencies

    Next, open your app-level build.gradle (or build.gradle.kts) file and add the SDK and its required libraries to the dependencies block.

    build.gradle.kts (Kotlin):

    dependencies {
        // ... other dependencies
    
        // --- ECV SDK and Dependencies ---
        // Note: Check Artifactory for the latest version numbers.
        implementation("group.ccr.ecv:ecv-175-sdk:<LATEST_VERSION>")
        implementation("org.webrtc:libwebrtc:M134") // This version may change based on SDK requirements
        implementation("group.ccr.ecv.external:autobahn-android-legacy:20.2.1")
    
        // for android 21 compatibility
        implementation("net.sourceforge.streamsupport:streamsupport-cfuture:1.7.4")
        // if streamsupport-cfuture is not available, then instead
        // `coreLibraryDesugaringEnabled(true)` should be added in compileOptions
        // and the following line should be uncommented
        // coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
    
        // --- End ECV SDK Dependencies ---
    
        // You might also need lifecycle-runtime-ktx (see below)
    }
    

    build.gradle (Groovy):

    dependencies {
        // ... other dependencies
    
        // --- ECV SDK and Dependencies ---
        // Note: Check Artifactory for the latest version numbers.
        implementation 'group.ccr.ecv:ecv-175-sdk:<LATEST_VERSION>'
        implementation 'org.webrtc:libwebrtc:M134' // This version may change based on SDK requirements
        implementation 'group.ccr.ecv.external:autobahn-android-legacy:20.2.1'
    
        // for android 21 compatibility
        implementation 'net.sourceforge.streamsupport:streamsupport-cfuture:1.7.4'
        // if streamsupport-cfuture is not available, then instead
        // `coreLibraryDesugaringEnabled true` should be added in compileOptions
        // and the following line should be uncommented
        // coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
    
        // --- End ECV SDK Dependencies ---
    
        // You might also need lifecycle-runtime-ktx (see below)
    }
    
  3. Add Lifecycle Runtime Dependency (If Needed): The ECV SDK uses the androidx.lifecycle:lifecycle-runtime-ktx library internally. Because it's not exposed as an api dependency, your app needs to provide it if it's not already included by other libraries you use (like Compose, Navigation, etc.).

    Ensure this dependency is present in your app-level build.gradle/build.gradle.kts:

    build.gradle (Groovy):

    dependencies {
        // ... other dependencies
        implementation libs.androidx.lifecycle.runtime.ktx // Or the specific version string, e.g., "androidx.lifecycle:lifecycle-runtime-ktx:2.7.0"
    
        // ECV SDK implementation lines...
    }
    

    build.gradle.kts (Kotlin):

    dependencies {
        // ... other dependencies
        implementation(libs.androidx.lifecycle.runtime.ktx) // Or the specific version string, e.g., "androidx.lifecycle:lifecycle-runtime-ktx:2.7.0"
    
        // ECV SDK implementation lines...
    }
    

    Note: If you are not using version catalogs (libs.), replace libs.androidx.lifecycle.runtime.ktx with the explicit coordinate string like "androidx.lifecycle:lifecycle-runtime-ktx:2.7.0" (use the latest stable version compatible with your project).

  4. Sync Your Gradle Project Click File > Sync Project with Gradle Files. Gradle will download the required libraries from your specified Artifactory repository.

2. Explore the Demo Application

The included demo application (ecv175_android) provides a practical example of SDK integration.

  1. Open the ecv175_android project in Android Studio.
  2. Build and run the demo app on an emulator or physical device.
  3. Examine the source code, particularly the files in the ui package (EnvironmentSelectorScreen.kt, MainScreen.kt, QueueScreen.kt, CallScreen.kt) and the AppState.kt object to understand how SDK components are initialized and used.

3. Basic SDK Initialization

// Somewhere in your Application class or a central management object
import group.ccr.ecv.sdk.ECVSdk
import group.ccr.ecv.sdk.ECVSdkConfiguration

// 1. Define the SDK Configuration (See Configuration.md for details)
val sdkConfig = ECVSdkConfiguration(
    serviceUrl = "YOUR_SERVICE_URL" // Replace with your actual service URL
    // Optionally override webrtcUrl, signalingUrl, etc.
)

// 2. Initialize the SDK Instance
// It's recommended to have a single instance managed centrally
val ecvSdk = ECVSdk(instanceName = "MyAppSdkInstance", config = sdkConfig)

// Store this instance for access throughout your app (e.g., in Application class, DI, Singleton)
// Example: MyApp.sdkInstance = ecvSdk
// Somewhere in your Application class or a central management object
import group.ccr.ecv.sdk.ECVSdk;
import group.ccr.ecv.sdk.ECVSdkConfiguration;

// 1. Define the SDK Configuration (See Configuration.md for details)
ECVSdkConfiguration sdkConfig = new ECVSdkConfiguration(
    /* serviceUrl */ "YOUR_SERVICE_URL" // Replace with your actual service URL
);

// 2. Initialize the SDK Instance
// It's recommended to have a single instance managed centrally
ECVSdk ecvSdk = new ECVSdk("MyAppSdkInstance", sdkConfig);

// Store this instance for access throughout your app (e.g., in Application class, DI, Singleton)
// Example: MyApp.sdkInstance = ecvSdk;

Core Concepts

  • ECVSdk: The main entry point for interacting with the SDK. Used to create calls and access global settings/modules.
  • ECVSdkConfiguration: Defines the backend URLs and connection parameters for the SDK.
  • VideoCall: Represents a single video call session. Manages the call state, events, and connection details.
  • WebRtcConnection: Handles the underlying WebRTC peer connection, media streams, and renderers. Accessed via VideoCall.connect().
  • Events: The SDK uses a flow/callback system (VideoCall.eventFlow / VideoCall.subscribe) to notify your application about call state changes, errors, and media events (CallEventTypes).
  • Renderers (SurfaceViewRenderer): Standard WebRTC views used to display local and remote video. You need to manage their lifecycle and attach/detach them using WebRtcConnection.

Demo Application Flow

The demo app follows this basic flow:

  1. EnvironmentSelectorScreen: Allows selecting or defining backend connection configurations (Service URL, etc.). This is primarily for demonstration purposes; your app will likely have a fixed configuration or fetch it dynamically.
  2. MainScreen: Collects necessary call information (e.g., caller name, attributes), requests permissions (Camera, Mic), and initiates the call using sdk.createCall() and call.start().
  3. QueueScreen: Displays a waiting state while the call connects to an agent. Listens for AgentConnected or Error/Hangup events.
  4. CallScreen: The main video call interface. Manages SurfaceViewRenderers for local and remote video, displays call controls (mute, switch camera, hangup), and reacts to SDK events to update the UI.

Documentation Structure

  • README.md: (This file) Overview and setup.
  • SDK_Usage.md: Detailed guide on using SDK classes and methods.
  • Configuration.md: Explains SDK and call configuration options.
  • Demo_App_Guide.md: Walkthrough of the demo application's code and SDK integration points.
  • Events_and_State.md: Reference for SDK events and state management.
  • Customization.md: Guidance on adapting the SDK for your specific application needs.
  • Troubleshooting.md: Common issues and solutions.

Next Steps

  • Follow the steps in Getting Started to set up the SDK and demo app.
  • Read SDK_Usage.md for a detailed understanding of the SDK's core components.
  • Consult Demo_App_Guide.md while examining the demo app source code.

Usage Guide

This guide provides detailed information on using the core components of the ECV Android SDK.

1. Initialization (ECVSdk)

The ECVSdk class is the primary entry point for the SDK. You typically create a single instance for your application's lifecycle.

Creating an Instance:

import group.ccr.ecv.sdk.ECVSdk
import group.ccr.ecv.sdk.ECVSdkConfiguration

// Configuration (See Configuration.md for details)
val sdkConfig = ECVSdkConfiguration(
    serviceUrl = "YOUR_SERVICE_URL"
    // ... other optional parameters
)

// Create SDK Instance
// Use a descriptive name for logging/debugging purposes
val sdkInstance = ECVSdk(instanceName = "MyAppSdkInstance", config = sdkConfig)

// --- Accessing the SDK Instance ---
// It's crucial to manage this instance appropriately. Common patterns:
// 1. Store in your Application subclass:
// class MyApp : Application() {
//     lateinit var sdkInstance: ECVSdk
//     override fun onCreate() {
//         super.onCreate()
//         sdkInstance = ECVSdk(...)
//     }
// }
// Access via: (applicationContext as MyApp).sdkInstance

// 2. Dependency Injection (Hilt, Koin, etc.): Provide ECVSdk as a singleton.

// 3. Simple Singleton Object (Kotlin):
// object SdkManager {
//     lateinit var instance: ECVSdk
//     fun initialize(config: ECVSdkConfiguration) {
//         instance = ECVSdk("MyAppSdkInstance", config)
//     }
// }
// Access via: SdkManager.instance
import group.ccr.ecv.sdk.ECVSdk;
import group.ccr.ecv.sdk.ECVSdkConfiguration;
import android.app.Application;

// Configuration (See Configuration.md for details)
ECVSdkConfiguration sdkConfig = new ECVSdkConfiguration(
    /* serviceUrl */ "YOUR_SERVICE_URL"
    // ... other parameters like webrtcUrl, signalingUrl ...
);

// Create SDK Instance
// Use a descriptive name for logging/debugging purposes
ECVSdk sdkInstance = new ECVSdk("MyAppSdkInstance", sdkConfig);

// --- Accessing the SDK Instance ---
// It's crucial to manage this instance appropriately. Common patterns:
// 1. Store in your Application subclass:
// public class MyApp extends Application {
//     private ECVSdk sdkInstance;
//     @Override
//     public void onCreate() {
//         super.onCreate();
//         ECVSdkConfiguration config = new ECVSdkConfiguration(...);
//         sdkInstance = new ECVSdk("MyAppSdkInstance", config);
//     }
//     public ECVSdk getSdkInstance() {
//         return sdkInstance;
//     }
// }
// Access via: ((MyApp) getApplicationContext()).getSdkInstance();

// 2. Dependency Injection (Dagger, Hilt): Provide ECVSdk as a singleton.

// 3. Simple Singleton Class (Java):
// public class SdkManager {
//     private static ECVSdk instance;
//     public static void initialize(ECVSdkConfiguration config) {
//         if (instance == null) {
//             instance = new ECVSdk("MyAppSdkInstance", config);
//         }
//     }
//     public static ECVSdk getInstance() {
//         if (instance == null) {
//             throw new IllegalStateException("SdkManager not initialized");
//         }
//         return instance;
//     }
// }
// Access via: SdkManager.getInstance();

Key Components:

  • config: The ECVSdkConfiguration passed during initialization.
  • logger: An internal logger instance. Logs can typically be viewed in Android's Logcat.
  • modules: Provides access to SDK modules (currently focused on event subscription).

2. Managing Calls (VideoCall)

A VideoCall object represents a single call session from initiation to termination.

Creating a Call:

// Assumes sdkInstance is available
val videoCall: VideoCall = sdkInstance.createCall() // Generates rtcId via API call

// OR for testing/joining with a known rtcId:
// val testVideoCall: VideoCall = sdkInstance.createTestCall() // Doesn't generate rtcId automatically
// testVideoCall.setRtcId("EXISTING_RTC_ID")
// Assumes sdkInstance is available
VideoCall videoCall = sdkInstance.createCall(); // Generates rtcId via API call

// OR for testing/joining with a known rtcId:
// VideoCall testVideoCall = sdkInstance.createTestCall(); // Doesn't generate rtcId automatically
// testVideoCall.setRtcId("EXISTING_RTC_ID");

Call Lifecycle & Key Methods:

  1. Initialization: When createCall() is called, the SDK asynchronously fetches an rtcId from the backend. You should listen for the GotRtcId event. The rtcId is essential for identifying the call.
    • videoCall.rtcId: Access the unique ID (available after GotRtcId event).
  2. Dialing (start): Initiates the call process by contacting the backend with call parameters. This usually puts the caller in a queue.
    • videoCall.start(dialParameters: DialCallParameters): See Configuration.md for DialCallParameters.
    • Listen for DialSuccess or Error events.
    • OnQueue event indicates the call is waiting for an agent.
  3. Agent Connection:
    • The AgentConnected event fires when an agent accepts the call.
    • videoCall.agentConnectedState: Boolean flag indicating if the agent is connected.
  4. WebRTC Connection (connect): After AgentConnected, you establish the peer-to-peer media connection.
    • videoCall.connect(parameters: ConnectParameters? = null): WebRtcConnection: Creates or retrieves the WebRtcConnection object. See Configuration.md for ConnectParameters.
  5. Starting Media (startRoom): Once you have the WebRtcConnection, you need to start the WebRTC setup process.
    • webRtcConnection.startRoom(context: Context): Initializes PeerConnection, sets up media tracks, and starts the signaling process.
  6. Media Control: Use methods on the WebRtcConnection instance (see next section).
  7. Ending the Call (hangup): Terminates the call session and cleans up resources.
    • videoCall.hangup(): Initiates the hangup process. This should be called when the user hangs up or the call ends for other reasons (e.g., remote hangup, error).
    • Listen for the Hangup event (triggered by calling hangup) or CallEnded event (often triggered by remote party or errors).

Finding an Existing Call:

If you navigate away from the call screen and need to retrieve the VideoCall instance later (e.g., in QueueScreen or CallScreen), use the rtcId:

val rtcId: String = // ... obtain the rtcId for the current call ...
val existingCall = sdkInstance.getVideoCall(rtcId)
if (existingCall != null) {
    // Use the call instance
} else {
    // Call not found, handle appropriately (e.g., navigate back)
}
String rtcId = // ... obtain the rtcId for the current call ...
VideoCall existingCall = sdkInstance.getVideoCall(rtcId);
if (existingCall != null) {
    // Use the call instance
} else {
    // Call not found, handle appropriately (e.g., navigate back)
}

Important: The SDK keeps a reference to VideoCall instances internally using the rtcId. Calling hangup() will eventually lead to the cleanup and potential removal of this reference.

3. WebRTC Connection (WebRtcConnection)

This class manages the low-level WebRTC details, including the PeerConnection, media tracks, and renderers. You obtain it via videoCall.connect().

Key Methods:

  • startRoom(context: Context): (As mentioned above) Kicks off the WebRTC connection setup after AgentConnected. Requires Context.
  • eglBase: EglBase: Provides the EGL context needed for initializing SurfaceViewRenderers.
  • attachLocalView(renderer: SurfaceViewRenderer): Attaches a renderer to display the local camera feed.
  • attachRemoteView(renderer: SurfaceViewRenderer): Attaches a renderer to display the remote participant's video feed.
  • detachLocalView(renderer: SurfaceViewRenderer): Detaches a renderer from the local feed.
  • detachRemoteView(renderer: SurfaceViewRenderer): Detaches a renderer from the remote feed.
  • setAudioEnabled(state: Boolean): Mutes or unmutes the local audio output stream. Triggers AudioMuted/AudioUnmuted events.
  • setVideoEnabled(state: Boolean): Stops or starts sending the local video stream. Triggers VideoMuted/VideoUnmuted events.
  • switchCamera(targetCamera: String? = null): Switches between front ("f") and back ("b") cameras. If targetCamera is null, it toggles.
  • changeFlashState(state: Boolean) v1.0.18.45+: Activates the flash when true is passed, and deactivates it when false is passed. Only works with the rear camera.
  • toggleFlashState() v1.0.18.45+: Toggles the flash state between on and off. Only works with the rear camera.

Renderer Lifecycle Management (Crucial):

SurfaceViewRenderer requires careful lifecycle management, especially in Jetpack Compose using AndroidView.

  1. Initialization: When the AndroidView's factory lambda runs, create the SurfaceViewRenderer and initialize it using webRtcConnection.eglBase.eglBaseContext.
  2. Attachment: Attach the renderer to the local or remote stream using webRtcConnection.attachLocalView() or webRtcConnection.attachRemoteView() within the factory or update lambda.
  3. Detachment: Detach the renderer in the update lambda if the target stream changes (e.g., switching between local/remote in PiP).
  4. Release: Crucially, detach the renderer (detachLocalView/detachRemoteView) and call surfaceViewRenderer.release() in the AndroidView's onRelease lambda to free up resources and prevent leaks.

Refer to the CallScreen.kt -> VideoRenderer composable in the demo app for a concrete example.

// Simplified Example within an AndroidView block
import androidx.compose.runtime.*
import androidx.compose.ui.viewinterop.AndroidView
import org.webrtc.RendererCommon
import org.webrtc.SurfaceViewRenderer

// Assume webRtcConnection and isLocalView are available in the composable scope
@Composable
fun VideoDisplay(webRtcConnection: WebRtcConnection?, isLocalView: Boolean, modifier: Modifier = Modifier) {
    if (webRtcConnection == null) {
        // Handle case where connection is not ready
        Box(modifier.background(Color.Gray)) { Text("Connecting...") }
        return
    }

    AndroidView(
        modifier = modifier,
        factory = { ctx ->
            SurfaceViewRenderer(ctx).apply {
                init(webRtcConnection.eglBase.eglBaseContext, null)
                setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL)
            }
        },
        update = { surfaceViewRenderer ->
            // Logic to decide whether to attach local or remote
            if (isLocalView) {
                webRtcConnection.detachRemoteView(surfaceViewRenderer) // Ensure cleanup
                webRtcConnection.attachLocalView(surfaceViewRenderer)
            } else {
                webRtcConnection.detachLocalView(surfaceViewRenderer) // Ensure cleanup
                webRtcConnection.attachRemoteView(surfaceViewRenderer)
            }
             surfaceViewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL) // Re-apply if needed
        },
        onRelease = { surfaceViewRenderer ->
            // DETACH IS ESSENTIAL before release
            webRtcConnection.detachLocalView(surfaceViewRenderer)
            webRtcConnection.detachRemoteView(surfaceViewRenderer)
            surfaceViewRenderer.release() // Release resources
            Log.d("VideoRenderer", "SurfaceViewRenderer released (isLocal: $isLocalView)")
        }
    )
}
// Simplified Example within an Android Fragment or Activity
// Assume webRtcConnection and surfaceViewRenderer are member variables initialized appropriately

// In onCreateView or onViewCreated:
// SurfaceViewRenderer surfaceViewRenderer = binding.surfaceView; // From ViewBinding
// if (webRtcConnection != null) {
//     surfaceViewRenderer.init(webRtcConnection.getEglBase().getEglBaseContext(), null);
//     surfaceViewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
//     if (isLocalView) { // isLocalView determined by UI logic
//          webRtcConnection.attachLocalView(surfaceViewRenderer);
//     } else {
//          webRtcConnection.attachRemoteView(surfaceViewRenderer);
//     }
// }

// In onDestroyView:
// if (webRtcConnection != null && surfaceViewRenderer != null) {
//     // It's safe to call detach for both, even if only one was attached
//     webRtcConnection.detachLocalView(surfaceViewRenderer);
//     webRtcConnection.detachRemoteView(surfaceViewRenderer);
//     surfaceViewRenderer.release();
// }

4. Handling Events

The SDK communicates state changes and occurrences asynchronously via events.

Subscribing to Events:

  • Kotlin (Flow): Use coroutines and collect on videoCall.eventFlow. This is lifecycle-aware when used with LaunchedEffect or lifecycleScope.launch/repeatOnLifecycle.
    import androidx.compose.runtime.LaunchedEffect
    import androidx.lifecycle.Lifecycle
    import androidx.lifecycle.lifecycleScope
    import androidx.lifecycle.repeatOnLifecycle
    import group.ccr.ecv.sdk.videoCall.VideoCall // Import your VideoCall class
    import group.ccr.ecv.sdk.videoCall.events.CallEventTypes
    import kotlinx.coroutines.flow.collect
    
    // Inside a Composable (using LaunchedEffect)
    @Composable
    fun MyCallScreen(videoCall: VideoCall?) { // Pass the VideoCall instance
        LaunchedEffect(videoCall) { // Key on videoCall instance
            videoCall?.eventFlow?.collect { event ->
                Log.d("MyApp", "Received SDK Event: ${event.type} - ${event.detail}")
                when (event.type) {
                    CallEventTypes.GotRtcId -> { /* Store rtcId: event.detail */ }
                    CallEventTypes.DialSuccess -> { /* Call is dialing */ }
                    CallEventTypes.OnQueue -> { /* Waiting for agent */ }
                    CallEventTypes.AgentConnected -> { /* Navigate to call screen, call videoCall.connect() */ }
                    CallEventTypes.WebRtcConnected -> { /* Media connection established */ }
                    CallEventTypes.VideoMuted -> { /* Update local video mute UI state */ }
                    CallEventTypes.RemoteVideoMuted -> { /* Update remote video mute UI state */ }
                    CallEventTypes.StartHold -> { /* Show "On Hold" UI */ }
                    CallEventTypes.CallEnded -> { /* Navigate back, cleanup. Detail: event.detail */ }
                    CallEventTypes.Error -> { /* Show error message: event.error / event.detail */ }
                    // ... handle other relevant events
                    else -> { /* Optional: Log unhandled events */ }
                }
            }
        }
        // ... Rest of the Composable UI ...
    }
    
    
    // Inside an Activity/Fragment (using lifecycleScope)
    // override fun onCreate(savedInstanceState: Bundle?) {
    //     super.onCreate(savedInstanceState)
    //     // ... setup ...
    //     val videoCall = // ... get the VideoCall instance ...
    //
    //     lifecycleScope.launch {
    //         repeatOnLifecycle(Lifecycle.State.STARTED) {
    //             videoCall?.eventFlow?.collect { event ->
    //                 // Handle event as above
    //             }
    //         }
    //     }
    // }
    
  • Java (Callback): Use the subscribe method with a LifecycleOwner and an EventCallback.
    import androidx.lifecycle.LifecycleOwner; // e.g., your Activity or Fragment
    import group.ccr.ecv.sdk.EventCallback;
    import group.ccr.ecv.sdk.videoCall.VideoCall;
    import group.ccr.ecv.sdk.videoCall.events.CallEventTypes;
    import group.ccr.ecv.sdk.videoCall.events.VideoCallEvent;
    import android.util.Log;
    
    // In your Activity/Fragment onCreate or onViewCreated
    LifecycleOwner lifecycleOwner = this; // Your Activity/Fragment implements LifecycleOwner
    VideoCall videoCall = // ... get videoCall instance ...
    
    if (videoCall != null) {
        videoCall.subscribe(lifecycleOwner, new EventCallback() {
            @Override
            public void onEvent(VideoCallEvent event) {
                // It's often safer to post UI updates to the main thread
                runOnUiThread(() -> {
                     Log.d("MyApp", "Received SDK Event: " + event.getType() + " - " + event.getDetail());
                    // Use switch statement or if-else if on event.getType()
                    if (event.getType() == CallEventTypes.GotRtcId) {
                        // Store rtcId: event.getDetail()
                    } else if (event.getType() == CallEventTypes.AgentConnected) {
                        // Navigate to call screen, call videoCall.connect()
                    } else if (event.getType() == CallEventTypes.CallEnded) {
                        // Navigate back, cleanup. Detail: event.getDetail()
                    } else if (event.getType() == CallEventTypes.Error) {
                        // Show error message: event.getError() / event.getDetail()
                    }
                    // ... handle other relevant events
                });
            }
        });
    }
    
    // Alternatively, subscribe to global events via ECVSdkModules
    // sdkInstance.getModules().subscribe(lifecycleOwner, new EventCallback() { ... });
    

Key Event Types (CallEventTypes):

Refer to Events_and_State.md for a more comprehensive list, but some critical ones include:

  • GotRtcId: SDK has obtained the unique call identifier.
  • DialSuccess: Backend has acknowledged the call request.
  • Error: An error occurred (check event.error and event.detail).
  • OnQueue: Caller is waiting for an agent.
  • AgentConnected: Agent has joined, ready to establish media.
  • WebRtcConnected: Peer-to-peer media connection is fully established.
  • AudioMuted/AudioUnmuted: Local audio send state changed.
  • VideoMuted/VideoUnmuted: Local video send state changed.
  • RemoteAudioMuted/RemoteAudioUnmuted: Remote participant's audio state changed.
  • RemoteVideoMuted/RemoteVideoUnmuted: Remote participant's video state changed.
  • StartHold/EndHold: Call hold status changed.
  • Hangup: Local hangup initiated.
  • CallEnded: The call session has terminated (locally or remotely).

5. Error Handling

Errors are typically reported via the Error event (CallEventTypes.Error). The VideoCallEvent object will contain:

  • event.error: An ECVError object with an ErrorTypes enum and a message.
  • event.detail: Additional string details about the error, if available.
  • event.objData: Sometimes contains related data (e.g., HTTP response).

Always check for Error events and handle them appropriately (e.g., show a message to the user, clean up the call, navigate back).

Configuration Guide

This guide explains the various configuration options available for the ECV SDK and individual calls.

1. SDK Initialization Configuration (ECVSdkConfiguration)

This configuration is provided when you create the main ECVSdk instance and defines the core connection parameters for the SDK to communicate with the backend infrastructure.

Class: group.ccr.ecv.sdk.ECVSdkConfiguration

Parameters:

  • serviceUrl: String (Required): The base URL for the main ECV backend API services (e.g., obtaining rtcId, dialing, hangup notifications).
    • Example: "https://your-company.ecv.backend.com"
  • webrtcUrl: String (Optional, defaults to serviceUrl): The base URL used specifically for WebRTC signaling-related HTTP requests (like the /join endpoint). If your WebRTC signaling infrastructure is hosted separately, specify it here.
    • Example: "https://your-company.ecv.webrtc.com"
  • signalingUrl: String (Optional, defaults to serviceUrl): The base URL for the WebSocket signaling server. The SDK will replace http/https with ws/wss. If your WebSocket server is hosted separately, specify it here.
    • Example: "https://your-company.ecv.websocket.com"
  • useQueueSocket: Boolean (Optional, defaults to true): Determines the mechanism for receiving queue status updates (like AgentConnected).
    • true (Recommended for ECV v1.75+): Uses a dedicated WebSocket (QueueSocket) for real-time updates. This requires your ECV backend infrastructure to be version 1.75 or higher. Provides faster notifications compared to polling.
    • false: Falls back to periodically polling an HTTP endpoint to check the call status.

Example Usage:

import group.ccr.ecv.sdk.ECVSdkConfiguration

// Configuration for ECV v1.75+ backend (using preferred WebSocket)
val configV175 = ECVSdkConfiguration(
    serviceUrl = "https://prod-v175.ecv.example.com"
)

// Configuration with separate WebRTC/Signaling URLs
val configSeparate = ECVSdkConfiguration(
    serviceUrl = "https://api.example.com",
    webrtcUrl = "https://webrtc.example.com",
    signalingUrl = "https://websocket.example.com"
)
import group.ccr.ecv.sdk.ECVSdkConfiguration;

// Simple configuration
ECVSdkConfiguration config = new ECVSdkConfiguration(
    /* serviceUrl */ "https://prod-v175.ecv.example.com"
);

2. Call Initiation Parameters (DialCallParameters)

These parameters are provided when you call videoCall.start() to initiate a specific call session.

Class: group.ccr.ecv.sdk.videoCall.api.DialCallParameters

Parameters:

  • callerName: String (Optional, defaults to ""): A display name for the caller, potentially shown to the agent.
  • attributes: Map<String, String> (Optional, defaults to emptyMap()): A map of key-value pairs representing general call attributes or customer data. This information is sent to the backend during the dial process and can be used for routing, screen pops, or context.
    • Example: mapOf("customerId" to "12345", "product" to "Premium Account")
  • acdAttributes: Map<String, String> (Optional, defaults to emptyMap()): Specific attributes intended for ACD (Automatic Call Distributor) routing logic on the backend.
    • Example: mapOf("skill" to "Billing", "language" to "en-US")
  • queue: String? (Optional, defaults to null): The specific queue or workgroup name to route the call to. If null or empty, default routing rules on the backend apply.
    • Example: "SalesQueue"
  • customPayload: Map<String, String> (Optional, defaults to emptyMap()): A flexible map for sending any other custom data required by your specific backend integration.
  • additionalHeaders: Map<String, String> (Optional, defaults to emptyMap()): Allows adding custom HTTP headers to the /api/dialex request. This can be used for authentication tokens or other metadata.
    • Example: mapOf("Authorization" to "Bearer YOUR_TOKEN", "X-Tenant-ID" to "TenantA")

Example Usage:

import group.ccr.ecv.sdk.videoCall.api.DialCallParameters

val params = DialCallParameters(
    callerName = "Jane Doe",
    attributes = mapOf(
        "accountNumber" to "ACC9876",
        "lastInteraction" to "2023-10-26"
    ),
    queue = "SupportQueue",
    additionalHeaders = mapOf("X-Auth-Token" to "user_session_token")
)

// Assuming videoCall is an instance of VideoCall
videoCall.start(params)
import group.ccr.ecv.sdk.videoCall.api.DialCallParameters;
import java.util.HashMap;
import java.util.Map;

Map<String, String> attributes = new HashMap<>();
attributes.put("accountNumber", "ACC9876");
attributes.put("lastInteraction", "2023-10-26");

Map<String, String> headers = new HashMap<>();
headers.put("X-Auth-Token", "user_session_token");

DialCallParameters params = new DialCallParameters(
    /* callerName */ "Jane Doe",
    /* attributes */ attributes,
    /* acdAttributes */ new HashMap<>(), // Empty map
    /* queue */ "SupportQueue",
    /* customPayload */ new HashMap<>(), // Empty map
    /* additionalHeaders */ headers
);

// Assuming videoCall is an instance of VideoCall
videoCall.start(params);

3. WebRTC Connection Parameters (ConnectParameters)

These parameters are optionally provided when you call videoCall.connect() and fine-tune the WebRTC peer connection behavior. Most parameters have sensible defaults.

Class: group.ccr.ecv.sdk.webRtc.ConnectParameters

Key Parameters (Defaults shown):

  • Connection & Signaling:
    • candidateFilteringMode: CandidateFilteringMode = CandidateFilteringMode.ALL: Controls which ICE candidates are used (All, Relay only, Host only).
    • connectionTimeout: Int = 10000: Timeout (ms) for the overall WebRTC connection attempt.
    • joinHttpConnectTimeout: Int = 3000: HTTP connect timeout (ms) for the /join request.
    • joinHttpReadTimeout: Int = 3000: HTTP read timeout (ms) for the /join request.
    • additionalHeaders: Map<String, String> = emptyMap(): Custom HTTP headers added to the /join request and WebSocket signaling connection handshake.
  • Media Control & Behavior:
    • videoCallEnabled: Boolean = true: Whether video tracks should be set up.
    • disableVideo: Boolean = false: High-level flag to completely disable video sending/receiving.
    • disableAudio: Boolean = false: High-level flag to completely disable audio sending/receiving.
    • useSpeakerPhone: Boolean = true: Start the call using the speakerphone.
    • autoSwitchToEarPiece: Boolean = false: Automatically switch to earpiece when the proximity sensor is triggered.
    • autoHandleCameraSwitch: Boolean = true: Allows the SDK to automatically switch cameras on certain events.
    • autoHandleHoldSideEffects: Boolean = true: Automatically mutes/unmutes local media when the agent holds/resumes the call.
  • Video Configuration:
    • videoWidth: Int = 480 / videoHeight: Int = 640: Requested video resolution.
    • videoFps: Int = 0 (Defaults to ~30): Requested video frame rate.
    • videoMaxBitrate: Int = 0 (WebRTC default): Maximum bitrate (kbps) for video encoding.
    • videoCodec: String = "VP8": Preferred video codec ("VP8", "VP9", "H264").
    • videoCodecHwAcceleration: Boolean = true: Enable hardware acceleration if available.
  • Audio Configuration:
    • audioCodec: String = "OPUS": Preferred audio codec ("OPUS", "ISAC").
    • audioStartBitrate: Int = 0 (WebRTC default): Initial audio bitrate (kbps).
    • noAudioProcessing: Boolean = false: Disable platform audio processing (AEC, AGC, NS).
    • useOpenSLES: Boolean = false: Use OpenSL ES audio backend (Android specific).
    • disableBuiltInAEC: Boolean = false / disableBuiltInNS: Boolean = false: Disable hardware AEC/NS if available.
  • Debugging:
    • tracing: Boolean = false: Enable WebRTC internal tracing.
    • aecDump: Boolean = false: Save AEC diagnostic data.
    • saveInputAudioToFile: Boolean = false: Save raw input audio (requires useOpenSLES = false).
    • enableRtcEventLog: Boolean = false: Log WebRTC events to a file.
  • Data Channel (textChannelConfig):
    • textChannelConfig: DataChannelConfig? = DataChannelConfig(...): Configuration for the built-in text data channel. Usually left at default.

Example Usage:

import group.ccr.ecv.sdk.webRtc.ConnectParameters
import group.ccr.ecv.sdk.webRtc.CandidateFilteringMode

// Only override specific parameters.
val customConnectParams = ConnectParameters(
    videoWidth = 1280,
    videoHeight = 720,
    videoCodec = "H264",
    candidateFilteringMode = CandidateFilteringMode.RELAY, // Example: Only use TURN servers
    additionalHeaders = mapOf("X-WebRTC-Auth" to "webrtc_token")
)

// Get the WebRtcConnection using the custom parameters
val webRtcConnection = videoCall.connect(parameters = customConnectParams)
webRtcConnection.startRoom(context)

Demo Application Guide

Introduction

This guide walks you through the provided Android Demo Application (ecv175_android). Its primary purpose is to demonstrate how to integrate and utilize the ECV Video Call SDK in a functional, albeit simplified, application.

The demo uses Jetpack Compose for its UI and follows a common navigation pattern. While the UI itself might not be production-ready, the way it interacts with the SDK provides valuable examples. Java code snippets are provided conceptually to illustrate SDK usage, but UI implementation in a traditional Java/XML app will differ significantly.

We will examine the key screens and how they leverage the SDK's capabilities:

  1. Global State Management (AppState.kt)
  2. Environment Selection (EnvironmentSelectorScreen.kt)
  3. Call Initiation (MainScreen.kt)
  4. Waiting Queue (QueueScreen.kt)
  5. Active Call (CallScreen.kt)

Prerequisites

  • Successfully set up the SDK and its dependencies in your environment as described in README.md.
  • Be able to build and run the ecv175_android demo project in Android Studio.
  • Familiarity with either Kotlin/Compose or Java/XML Android development.

1. Global State Management (AppState.kt)

In a real application, you need a way to manage the initialized ECVSdk instance and potentially the currently active VideoCall instance so they can be accessed from different screens (Activities/Fragments/Composables).

The demo application uses a simple Kotlin object named AppState.

Kotlin Example (AppState.kt concept):

// Simplified concept from AppState.kt
object AppState {
    var sdkInstance: ECVSdk? = null
    // ... other demo-specific state ...

    fun initializeSdk(context: Context, environment: Environment) {
        // ...
        sdkInstance = ECVSdk(instanceName = "DemoSdk-${environment.name}", config = sdkConfig)
        // ...
    }
}
// Access via: AppState.sdkInstance

Java Conceptual Equivalent (e.g., in Application class or Singleton):

import android.app.Application;
import android.content.Context;
import group.ccr.ecv.sdk.ECVSdk;
import group.ccr.ecv.sdk.ECVSdkConfiguration;

public class MyApp extends Application {
    private static ECVSdk sdkInstance;

    // Method to initialize SDK (example)
    public static synchronized void initializeSdk(Context context, ECVSdkConfiguration config) {
         if (sdkInstance == null) {
            sdkInstance = new ECVSdk("MyAppSdkInstance", config);
         }
    }

    // Static getter for easy access
    public static ECVSdk getSdkInstance() {
        if (sdkInstance == null) {
            throw new IllegalStateException("SDK not initialized. Call initializeSdk first.");
        }
        return sdkInstance;
    }
}
// Access via: MyApp.getSdkInstance()

Key Takeaways:

  • Central Instance: Storing sdkInstance globally makes it accessible across the app. Choose a pattern suitable for your architecture (Application class, Singleton, DI).
  • Initialization: Initialize the SDK once, typically during application startup, with the appropriate ECVSdkConfiguration.

2. Environment Selection (EnvironmentSelectorScreen.kt)

Purpose: A demo-specific screen for testing different backend configurations (like Staging, Production, etc.). It shows how to load/save configurations.

Production Relevance: Low. Your app will typically have a fixed configuration or fetch it from your own backend, not present this choice to end-users.

3. Call Initiation (MainScreen.kt / Equivalent Activity/Fragment)

Purpose: Serves as the entry point to start a video call. It handles permissions, collects basic call data, and kicks off the call process.

SDK Interaction & Code Highlights:

  1. Permissions: The demo uses rememberLauncherForActivityResult to request permissions. In a traditional app, you would use ActivityCompat.requestPermissions and handle the result in onRequestPermissionsResult.

  2. Data Collection: The demo uses OutlinedTextField and custom dropdowns. In a traditional app, you would use EditText, Spinner, etc.

  3. Initiating the Call: (Inside an onClick listener)

    • Create Call: Get a VideoCall instance from the SDK.
      val call = AppState.sdkInstance?.createCall()
      
    • Wait for GotRtcId: Robust code should wait for the GotRtcId event before proceeding.
      // In a coroutine scope:
      val initEvent = call.eventFlow.first { it.type == CallEventTypes.GotRtcId || it.type == CallEventTypes.Error }
      // ... handle error or get rtcId from initEvent.detail
      
    • Create DialCallParameters: Populate with data from the UI.
    • Call start(): call.start(dialParams) sends the request to the backend.
    • Navigate: On successful initiation, navigate to the queue screen, passing the rtcId.

    Java Conceptual Equivalent (in an Activity onClick):

    ECVSdk sdk = MyApp.getSdkInstance();
    VideoCall videoCall = sdk.createCall();
    
    // UI data
    String callerName = editTextCallerName.getText().toString();
    Map<String, String> callerAttributes = getAttributesFromUI();
    
    // The robust way to handle this is to subscribe to events.
    // In the onEvent callback for GotRtcId:
    //   String rtcId = event.getDetail();
    //   DialCallParameters dialParams = new DialCallParameters(callerName, callerAttributes, ...);
    //   videoCall.start(dialParams);
    //   // Navigate to Queue Activity
    //   Intent intent = new Intent(this, QueueActivity.class);
    //   intent.putExtra("RTC_ID", rtcId);
    //   startActivity(intent);
    

4. Waiting Queue (QueueScreen.kt / QueueActivity or QueueFragment)

Purpose: Displays a waiting indicator while the call is in the queue. Manages navigation based on SDK events.

SDK Interaction & Code Highlights:

  1. Retrieve VideoCall Instance: Get the VideoCall object associated with the rtcId passed during navigation. Handle the case where the call may no longer exist.
    // In Activity onCreate or Fragment onViewCreated
    String rtcId = getIntent().getStringExtra("RTC_ID");
    VideoCall videoCall = MyApp.getSdkInstance().getVideoCall(rtcId);
    if (videoCall == null) {
        finish(); // Call not found, close this screen
        return;
    }
    this.videoCall = videoCall; // Store as member variable
    
  2. Event Handling: Subscribe to events for the specific videoCall.
    // In Activity onCreate or Fragment onViewCreated
    LifecycleOwner lifecycleOwner = this;
    videoCall.subscribe(lifecycleOwner, event -> {
        runOnUiThread(() -> {
            if (event.getType() == CallEventTypes.AgentConnected) {
                navigateToCallScreen(rtcId);
            } else if (event.getType() == CallEventTypes.Error || event.getType() == CallEventTypes.CallEnded) {
                navigateBackToMainScreen();
            }
        });
    });
    
  3. Cleanup: Hang up the call if the user leaves the screen prematurely.
    // In Activity onDestroy or Fragment onDestroyView
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (videoCall != null && !videoCall.getDidHangup() && !videoCall.getAgentConnectedState()) {
             new Thread(() -> videoCall.hangup()).start();
        }
    }
    

5. Active Call (CallScreen.kt / CallActivity or CallFragment)

Purpose: The main interface for the ongoing video call. Manages renderers, controls, and call state.

SDK Interaction & Code Highlights:

  1. Retrieve VideoCall & WebRtcConnection: Similar to the queue screen, get videoCall via rtcId. Then call videoCall.connect() and webRtcConnection.startRoom(context).
    // In Activity onCreate or Fragment onViewCreated
    VideoCall videoCall = MyApp.getSdkInstance().getVideoCall(rtcId);
    // ... null check ...
    this.videoCall = videoCall;
    
    WebRtcConnection connection = videoCall.connect(null); // Pass ConnectParameters if needed
    // ... null check ...
    this.webRtcConnection = connection;
    
    webRtcConnection.startRoom(this); // Pass context
    
  2. Event Handling & UI State: Subscribe to videoCall events to update UI elements (mute icons, hold overlays, etc.).
  3. Call Controls: Connect Button clicks to SDK methods.
    // In OnClickListener for the mute button
    if (webRtcConnection != null) {
        webRtcConnection.setAudioEnabled(!currentAudioState);
    }
    
    // In OnClickListener for the hangup button
    if (videoCall != null) {
         new Thread(() -> videoCall.hangup()).start();
    }
    
  4. Video Rendering (Java/XML):
    1. Add <org.webrtc.SurfaceViewRenderer ... /> to your XML layout.
    2. Get references to the renderers (e.g., using ViewBinding).
    3. Initialization: In onCreate or onViewCreated, call surfaceViewRenderer.init(webRtcConnection.getEglBase().getEglBaseContext(), null).
    4. Attachment: Call webRtcConnection.attachLocalView(...) and webRtcConnection.attachRemoteView(...) when ready.
    5. Detachment & Release: Crucially, in onDestroy or onDestroyView, call webRtcConnection.detach...View(...) and surfaceViewRenderer.release() for all renderers. Failure to do so will cause leaks and crashes.
  5. Cleanup: In onDestroy, ensure videoCall.hangup() is called if the call is still active. Release renderers as described above.

Conclusion

The demo application provides a practical, yet simplified, structure for using the ECV SDK. The key integration points involve initializing and managing SDK instances, handling permissions, starting calls with correct parameters, subscribing to events to drive the UI, and correctly managing the SurfaceViewRenderer lifecycle.

Events and State Management

Introduction

The ECV Android SDK operates asynchronously. It uses an event-driven architecture to communicate status changes, errors, and other important occurrences to your application. Understanding these events is crucial for building a responsive and robust UI.

Subscribing to Events

You listen for events on a VideoCall instance. The SDK provides two primary mechanisms:

  1. Kotlin (Flow): Access the videoCall.eventFlow and collect events using Kotlin Coroutines.
  2. Java (Callback): Use the videoCall.subscribe(lifecycleOwner, eventCallback) method, providing an Android LifecycleOwner (like an Activity or Fragment).

Refer to the "Handling Events" section in SDK_Usage.md for detailed code examples.

Event Structure (VideoCallEvent)

All SDK events are instances of the group.ccr.ecv.sdk.videoCall.events.VideoCallEvent data class.

Key Fields:

  • type: CallEventTypes: (Required) An enum indicating the kind of event.
  • detail: String?: (Optional) Additional string information (e.g., the rtcId).
  • error: ECVError?: (Optional) Included for Error events.
  • objData: Any?: (Optional) Can contain related objects, like the WebRtcConnection.
  • rtcId: String: The rtcId of the call associated with the event.

Event Type Reference (CallEventTypes)

Here's a breakdown of common event types, grouped by the typical phase of a call:


Phase 1: Initialization & Dialing

  • GotRtcId: Fired after createCall() successfully retrieves a unique call identifier. Response: Store the rtcId and proceed to dial.
  • DialSuccess: Fired after start() successfully sends the dial request. Response: Move to the waiting/queue UI.
  • Error: Can occur if fetching rtcId fails or the dial is rejected. Response: Show an error and stop call progress.

Phase 2: Queue & Agent Connection

  • OnQueue: Fired after a successful dial, indicating the caller is waiting. Response: Show a waiting UI.
  • AgentConnected: Fired when an agent accepts the call. Response: This is the trigger to call videoCall.connect(), then webRtcConnection.startRoom(), and navigate to the main call screen.
  • Error: Can occur due to queue timeouts or network issues. Response: Inform the user and navigate back.
  • CallEnded: If the user hangs up or the call is terminated while in queue. Response: Clean up and navigate back.

Phase 3: WebRTC Connection Establishment

  • SignalingConnected: WebSocket connection to the signaling server is established.
  • JoinedRoom: HTTP /join request was successful.
  • WebRtcConnected: The peer-to-peer media connection is fully established. Response: You can hide loading indicators; media can now flow.
  • WebRtcFailed: The connection failed. Usually leads to a CallEnded event.

Phase 4: Active Call & Media

  • AudioMuted / AudioUnmuted: Local audio state changed. Response: Update your mute button UI.
  • VideoMuted / VideoUnmuted: Local video state changed. Response: Update your video toggle button UI.
  • RemoteAudioMuted / RemoteAudioUnmuted: The remote participant's audio state changed. Response: Optionally show a "remote muted" indicator.
  • RemoteVideoMuted / RemoteVideoUnmuted: The remote participant's video state changed. Response: Optionally show an overlay on the remote video.
  • StartHold / EndHold: The call was put on hold by the agent. Response: Show/hide a "Call on Hold" overlay.
  • TextChannelMessage: A command received over the data channel (e.g., "FLASH_ON").

Phase 5: Call Termination

  • Hangup: Fired immediately when videoCall.hangup() is called locally.
  • CallEnded: The final event indicating the session is over. Response: This is the definitive signal to clean up all call UI, navigate away from the call screen, and release resources.

State Properties

While events signal changes, you can also check some current state properties:

  • On VideoCall:
    • rtcId: String
    • agentConnectedState: Boolean
    • didHangup: Boolean
  • On WebRtcConnection:
    • audioEnabled: Boolean
    • videoEnabled: Boolean

It's best practice to react to events to update your UI rather than polling these properties.

Customization Guide

Introduction

The provided Demo Application showcases the core functionalities of the ECV SDK. This guide provides advice on adapting the SDK and demo concepts to your specific application.

1. UI Implementation

The SDK is UI-agnostic and can be integrated into apps built with Jetpack Compose, traditional XML Views, or a combination.

Key Considerations for Your UI:

  • Build Your Own UI: Design screens that match your application's look and feel.
  • Core SDK Interactions Remain the Same:
    • Permissions: You must handle Camera and Microphone permissions.
    • Video Renderers: You must provide org.webrtc.SurfaceViewRenderer instances to display video.
    • Renderer Lifecycle: This is critical. You MUST correctly manage the lifecycle of SurfaceViewRenderers: initialize, attach, detach, and release. Failure to release() is a common source of crashes and memory leaks.
    • Call Controls: Implement UI for mute, video toggle, camera switch, and hangup.
    • Event Handling: Your UI must listen to SDK events to update state and navigate.

2. Configuration (ECVSdkConfiguration)

The demo's EnvironmentSelectorScreen is for testing. You will likely not need this screen in production.

Production Configuration Strategies:

  1. Fixed Configuration: If your app only connects to one backend, you can hardcode the ECVSdkConfiguration values when initializing the ECVSdk.
  2. Dynamic Configuration: Fetch configuration details (like serviceUrl) from your own application backend before initializing the ECVSdk. This allows you to manage endpoints centrally.

3. Call Data (DialCallParameters)

In your application, you should populate DialCallParameters with relevant contextual data just before calling videoCall.start().

Example:

// User and call context
val userId = "user123"
val targetQueue = "BillingSupport"

val dialParams = DialCallParameters(
    callerName = "User $userId",
    attributes = mapOf("userId" to userId),
    queue = targetQueue,
    additionalHeaders = mapOf("Authorization" to "Bearer ${getUserAuthToken()}")
)

videoCall.start(dialParams)
Map<String, String> attributes = new HashMap<>();
attributes.put("userId", "user123");

DialCallParameters dialParams = new DialCallParameters(
    /* callerName */ "User 123",
    /* attributes */ attributes,
    /* acdAttributes */ new HashMap<>(),
    /* queue */ "BillingSupport",
    /* customPayload */ new HashMap<>(),
    /* additionalHeaders */ new HashMap<>()
);

videoCall.start(dialParams);

4. Event Handling Logic

While the demo shows how to subscribe to events, what you do in response is application-specific. Ensure your implementation correctly handles key events for:

  • Navigation: Trigger screen changes on AgentConnected, CallEnded, and Error.
  • UI Updates: Visually reflect mute status, hold status, etc.
  • Error Display: Show user-friendly messages for Error events.
  • Resource Cleanup: Use CallEnded as the definitive signal to clean up.

5. Advanced Customization (Optional)

  • Custom Data Channel Messages: If your backend sends custom commands, listen for the TextChannelMessage event and parse event.detail.
  • ConnectParameters Tuning: For specific network environments, you might experiment with adjusting parameters like videoMaxBitrate or codecs.
  • Audio Management: For complex audio routing, you might need deeper integration beyond the basic SDK usage.

6. Permissions

Always follow Android's best practices for requesting permissions: request them contextually, explain why they are needed, and handle denial gracefully.

7. Additional Modules

7.1 Call Transfer v1.0.18.45+

This module allows an active call to be transferred to another agent or queue. The lifecycle is as follows:

  1. The source call is connected.
  2. The agent initiates a transfer.
  3. A TransferPending event is received via videoCall.eventFlow.
    • At this point, display a "Please wait, your call is being transferred" message to the user.
    • The transfer can be canceled, which will trigger a TransferCanceled event. If this occurs, hide the message.
  4. When the transfer is complete, a TransferCompleted event is received. event.detail will contain the new RtcId for the transferred call.
    • Destroy the current call view and all related resources.
    • Navigate the user back to the queue screen, using the new RtcId.
    • The flow continues as if a new call was initiated, waiting for an AgentConnected event for the new agent.

An implementation example for this scenario is available in the demo application.

Troubleshooting Guide

This guide provides solutions and debugging steps for common issues encountered while integrating or using the ECV Android SDK.

Common Issues & Solutions

Build & Dependency Errors

  1. Error: Failed to resolve: group.ccr.ecv:ecv-175-sdk:...
    • Cause: Gradle cannot download the required dependencies from Artifactory.
    • Solution:
      • Check Repository URL: Ensure the maven { url "..." } block is correctly added to your settings.gradle(.kts). Check for typos.
      • Network Connection: Ensure you have an internet connection and are connected to any required VPN for the Artifactory repository.
      • Dependency Coordinates: Double-check that the group, artifact name, and version in your build.gradle are correct.
      • Clear Gradle Cache: Try Build > Clean Project, then Build > Rebuild Project. If the issue persists, try File > Invalidate Caches / Restart....
  2. Error: Unresolved reference: lifecycle
    • Cause: The required androidx.lifecycle:lifecycle-runtime-ktx dependency is missing.
    • Solution: Add the implementation("androidx.lifecycle:lifecycle-runtime-ktx:VERSION") dependency to your app-level build.gradle.
  3. Error: Duplicate Class Found...
    • Cause: Your project or another library includes conflicting versions of dependencies (e.g., a different version of WebRTC).
    • Solution: Analyze the build error to identify the conflict. Use Gradle's dependency resolution strategies to exclude the transitive dependency.
  4. Error: Manifest Merger Failed
    • Cause: The SDK's manifest might conflict with your app's manifest (e.g., regarding permissions).
    • Solution: Ensure your app's AndroidManifest.xml correctly declares CAMERA, RECORD_AUDIO, INTERNET, MODIFY_AUDIO_SETTINGS, and BLUETOOTH_CONNECT (for API 31+).

Runtime Crashes

  1. Crash: UnsatisfiedLinkError (related to org.webrtc)
    • Cause: The native WebRTC libraries could not be loaded.
    • Solution: Ensure the org.webrtc:libwebrtc dependency is correctly included and downloaded by Gradle. Clean and rebuild the project.
  2. Crash: NullPointerException
    • Cause: Accessing an SDK object before it's initialized or after it's been cleaned up.
    • Solution:
      • SDK Not Initialized: Ensure ECVSdk(...) is called before you try to access the instance.
      • getVideoCall Returns Null: Always check the return value of getVideoCall(rtcId) for null before using it, especially when resuming a screen. The call may have ended.
      • WebRtcConnection is Null: Ensure videoCall.connect() was called successfully after the AgentConnected event.
  3. Crash: Related to SurfaceViewRenderer.release()
    • Cause: Improper lifecycle management of SurfaceViewRenderer.
    • Solution:
      • release() MUST be called in the appropriate lifecycle callback (onRelease in Compose, onDestroyView/onDestroy for Fragments/Activities).
      • Crucially, call detachLocalView(...) and detachRemoteView(...) before calling release().
  4. Crash: SecurityException (Permission Denial)
    • Cause: Attempting to start the camera or microphone without the required runtime permissions.
    • Solution: Implement robust permission request logic before starting a call.

Media Quality Issues

  1. Poor Video/Audio Quality:
    • Debugging:
      • Network: This is the most common cause. Check bandwidth, latency, and packet loss.
      • Device Performance: Lower-end devices may struggle with encoding/decoding.
      • ConnectParameters: Consider lowering video resolution (videoWidth/videoHeight) or setting a videoMaxBitrate.

Specific Feature Issues

  1. Camera Switch Doesn't Work:
    • Debugging: Ensure webRtcConnection.switchCamera() is being called and check logs for errors. Verify the device has multiple cameras.
  2. Hold/Pickup Doesn't Mute/Unmute Automatically:
    • Debugging: Ensure autoHandleHoldSideEffects = true in ConnectParameters. Check if StartHold/EndHold events are being received.

General Debugging Steps

  1. Check Logcat: This is your most important tool. Filter by SDK tags (ECVSdk, VideoCall, WebRtcConnection, PeerConnectionClient, etc.) and your own app tags.
  2. Verify Configuration: Double-check all ECVSdkConfiguration, DialCallParameters, and ConnectParameters.
  3. Check Network Connectivity: Can the device reach the configured URLs?
  4. Simplify: Temporarily remove complex UI and custom logic. Does a basic call work?
  5. Isolate the Phase: Determine when the failure occurs: Init? Dialing? Queue? WebRTC connection?
  6. Consult Demo App: Compare your logic, especially for event handling and renderer lifecycle, with the demo code.
  7. Log All Events: Add logging to print every VideoCallEvent you receive to trace the call flow.

Contacting Support

If you continue to face issues, please prepare the following information:

  • SDK Version: (e.g., ecv-175-sdk:1.2.3)
  • Device(s) Tested: Manufacturer, Model, Android OS Version.
  • Backend Version: v1.75+
  • Configuration: Your ECVSdkConfiguration, DialCallParameters, and any custom ConnectParameters.
  • Detailed Problem Description: What are the symptoms? What are the steps to reproduce?
  • Relevant Logcat Output: Capture logs during the time the issue occurs.
  • Screenshots/Videos: If helpful to illustrate the problem.