Skip to main content

ECV Video Call Android SDK [EN]

image-20240102-113213.pngimage-20240102-113321.png

ECV Video Call Android SDK

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: Provided as a pre-compiled Android Library (.aar file). You will add this as a 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.5 and 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: WebRTC and Autobahn. You will receive these as .aar files.

  1. Required AAR Files:

    • ECV SDK: (e.g., ECV-175-SDK-vX.Y.Z.aar) - Replace with your actual SDK filename.

    • WebRTC: M134-libwebrtc.aar

    • Autobahn: autobahn-android-21.7.1.aar

  2. Copy all three .aar files into your project's libs directory (create it under the app module if it doesn't exist).

  3. Open your app-level build.gradle (or build.gradle.kts) file.

  4. Add the libs directory to your repositories if it's not already there:

    // build.gradle (Groovy)
    repositories {
        google()
        mavenCentral()
        flatDir {
            dirs 'libs' // Add this line
        }
        // Add other custom repositories if needed
    }
    
    // build.gradle.kts (Kotlin)
    repositories {
        google()
        mavenCentral()
        flatDir {
            dirs("libs") // Add this line
        }
        // Add other custom repositories if needed
    }
    
  5. Add the AAR files as dependencies:

    // build.gradle (Groovy)
    dependencies {
        // ... other dependencies
    
        // --- ECV SDK and Dependencies ---
        implementation(name: 'ECV-175-SDK-vX.Y.Z', ext: 'aar') // Replace with your SDK filename
        implementation(name: 'M134-libwebrtc', ext: 'aar')
        implementation(name: 'autobahn-android-21.7.1', ext: 'aar')
        // --- End ECV SDK Dependencies ---
    
        // You might also need lifecycle-runtime-ktx (see below)
    }
    
    // build.gradle.kts (Kotlin)
    dependencies {
        // ... other dependencies
    
        // --- ECV SDK and Dependencies ---
        implementation(name = "ECV-175-SDK-vX.Y.Z", ext = "aar") // Replace with your SDK filename
        implementation(name = "M134-libwebrtc", ext = "aar")
        implementation(name = "autobahn-android-21.7.1", ext = "aar")
        // --- End ECV SDK Dependencies ---
    
         // You might also need lifecycle-runtime-ktx (see below)
    }
    
  6. Add Lifecycle Runtime Dependency (If Needed):
    The ECV SDK uses the androidx.lifecycle:lifecycle-runtime-ktx library internally (implementation dependency). Because it's not an api dependency, your app needs to provide it if it's not already included transitively 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 AARs...
    }
    
    // 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 AARs...
    }
    

    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).

  7. Sync your Gradle project.

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 (or equivalent) object to understand how SDK components are initialized and used.

3. Basic SDK Initialization (Conceptual)

// 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
    // Make sure useQueueSocket matches your backend version (true for 1.75+, false for 1.5)
    useQueueSocket = true // or false
    // 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
    // ... other optional parameters ...
    /* useQueueSocket */ true // or false, Make sure this matches your backend version (true for 1.75+, false for 1.5)
    // ... other optional parameters ...
);

// 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",
    useQueueSocket = true // Example: Set based on your backend version
    // ... 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 ...
    /* useQueueSocket */ true // Example: Set based on your backend version
    // ... other parameters ...
);


// 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)
                // Initial attach often done in update, but could be here if logic is simple
            }
        },
        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) {
//     if (isLocalView) {
//          webRtcConnection.detachLocalView(surfaceViewRenderer);
//     } else {
//          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 (Required for ECV v1.5 or if WebSockets disabled): Falls back to periodically polling an HTTP endpoint (waitForRoomIntervalEndpoint) to check the call status. Use this setting if your ECV backend is version 1.5. It can also be used with v1.75+ if WebSocket usage is intentionally disabled or unavailable in your environment.

  • waitForRoomIntervalDelay: Int (Optional, defaults to 3000 milliseconds): Only used if useQueueSocket is false. Specifies the delay between HTTP polling requests.

  • waitForRoomIntervalEndpoint: String (Optional, defaults to "/api/waitforroominterval?rtcId="): Only used if useQueueSocket is false. The relative API endpoint (appended to serviceUrl) used for HTTP polling. The SDK automatically appends the rtcId.

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",
    useQueueSocket = true // Default, but explicitly shown
)

// Configuration for ECV v1.5 backend (must use HTTP polling)
val configV15 = ECVSdkConfiguration(
    serviceUrl = "https://prod-v15.ecv.example.com",
    useQueueSocket = false // Required for v1.5
    // waitForRoomIntervalDelay and waitForRoomIntervalEndpoint use defaults unless overridden
)

// Configuration with separate WebRTC/Signaling URLs and disabled Queue Socket (e.g., for v1.5)
val configSeparateV15 = ECVSdkConfiguration(
    serviceUrl = "https://api.example.com",
    webrtcUrl = "https://webrtc-v15.example.com", // Separate endpoint for /join
    signalingUrl = "https://websocket-v15.example.com", // Separate WebSocket server (if applicable for v1.5 signaling)
    useQueueSocket = false, // Required for v1.5
    waitForRoomIntervalDelay = 5000, // Poll every 5 seconds
    waitForRoomIntervalEndpoint = "/custom/pollstatus?callid=" // Custom polling endpoint
)
import group.ccr.ecv.sdk.ECVSdkConfiguration;

// Configuration for ECV v1.75+ backend (using preferred WebSocket)
// Assumes constructor takes serviceUrl and other params are defaulted or set later.
ECVSdkConfiguration configV175 = new ECVSdkConfiguration(
    /* serviceUrl */ "https://prod-v175.ecv.example.com"
    // Assuming useQueueSocket defaults to true or can be set if needed via constructor/setter
);

// Configuration for ECV v1.5 backend (must use HTTP polling)
// Assumes a constructor or setters allow setting useQueueSocket.
ECVSdkConfiguration configV15 = new ECVSdkConfiguration(
    /* serviceUrl */ "https://prod-v15.ecv.example.com",
    /* webrtcUrl */ "https://prod-v15.ecv.example.com", // Example: same as serviceUrl
    /* signalingUrl */ "https://prod-v15.ecv.example.com", // Example: same as serviceUrl
    /* useQueueSocket */ false, // Required for v1.5
    /* waitForRoomIntervalDelay */ 3000, // Default
    /* waitForRoomIntervalEndpoint */ "/api/waitforroominterval?rtcId=" // Default
);

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 (used by a manual timer in WebRtcConnection).

    • 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. Useful for authentication tokens that need to reach the WebRTC/Signaling infrastructure.

  • Media Control & Behavior:

    • videoCallEnabled: Boolean = true: Whether video tracks should be set up. Set to false for audio-only calls.

    • disableVideo: Boolean = false: High-level flag to completely disable video sending/receiving if needed (usually videoCallEnabled is sufficient).

    • disableAudio: Boolean = false: High-level flag to completely disable audio sending/receiving if needed.

    • useSpeakerPhone: Boolean = true: Start the call using the speakerphone.

    • autoSwitchToEarPiece: Boolean = false: Automatically switch to earpiece when the proximity sensor is triggered (phone near ear).

    • autoHandleCameraSwitch: Boolean = true: Allows the SDK to automatically switch cameras when certain events occur (e.g., agent requests photo with back camera).

    • autoHandleHoldSideEffects: Boolean = true: Automatically mutes/unmutes local audio/video when the agent puts the call on hold or resumes it.

  • 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 used for commands like FLASH_ON/OFF, HOLD/PICKUP, etc. Usually left at default. See DataChannelConfig class for details.

Example Usage:

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

// Most defaults are usually fine. 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)
import group.ccr.ecv.sdk.webRtc.ConnectParameters;
import group.ccr.ecv.sdk.webRtc.CandidateFilteringMode;
import group.ccr.ecv.sdk.webRtc.DataChannelConfig; // Assuming this exists and is needed
import java.util.HashMap;
import java.util.Map;

// Create ConnectParameters using the constructor (assuming available) or defaults
ConnectParameters customConnectParams = new ConnectParameters(
    /* videoCallEnabled */ true,
    /* disableVideo */ false,
    /* disableAudio */ false,
    /* videoWidth */ 1280, // Custom width
    /* videoHeight

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 (.aar files, lifecycle-runtime-ktx) 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) {
        // ... (logic as shown before) ...
        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 your Environment/Configuration classes if needed for init logic
import group.ccr.ecv.sdk.ECVSdk;
import group.ccr.ecv.sdk.ECVSdkConfiguration;

public class MyApp extends Application {
    private static ECVSdk sdkInstance;

    @Override
    public void onCreate() {
        super.onCreate();
        // Initialize SDK here or on demand
        // initializeSdk(this, /* Pass your configuration source */);
    }

    // Method to initialize SDK (example)
    public static synchronized void initializeSdk(Context context, ECVSdkConfiguration config) {
         if (sdkInstance == null) {
            try {
                sdkInstance = new ECVSdk("MyAppSdkInstance", config);
                Log.i("MyApp", "SDK Initialized");
            } catch (Exception e) {
                 Log.e("MyApp", "SDK Initialization failed", e);
            }
         }
    }

    // Static getter for easy access
    public static ECVSdk getSdkInstance() {
        if (sdkInstance == null) {
            // Optionally initialize here or throw an error
            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: Demo-specific screen for testing different backend configurations.

SDK Interaction: Primarily involves selecting a configuration that is then used to initialize the ECVSdk instance (likely via AppState.initializeSdk or equivalent).

Production Relevance: Low. Your app will typically have a fixed or dynamically fetched configuration, not this UI.

(No specific SDK interaction code snippets here that need a direct Java equivalent, as the core logic is config management, not direct SDK calls.)

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

Purpose: Handles permissions, gathers call data, and initiates the call process.

SDK Interaction & Code Highlights:

  1. Permissions: Use Android's standard permission request mechanism (ActivityCompat.requestPermissions / registerForActivityResult). Check permissions before enabling call initiation UI.

  2. Data Collection: Use EditText, Spinner, or other standard Android UI elements to collect callerName and any necessary attributes.

  3. Initiating the Call: (e.g., inside an OnClickListener for a "Start Call" button)

    Kotlin (Demo Snippet):

    // Inside Composable onClick scope.launch
    val call = AppState.sdkInstance?.createCall()
    if (call == null) { /* Handle SDK not init */ return@launch }
    
    // Recommended: Wait for GotRtcId explicitly
    var rtcId: String? = null
    try {
        val initEvent = call.eventFlow.first { /* ... check for GotRtcId or Error ... */ }
        if (initEvent.type == CallEventTypes.GotRtcId) rtcId = initEvent.detail
        else throw Exception("Error getting rtcId: ...")
    } catch (e: Exception) { /* Handle error */ return@launch }
    
    if (rtcId == null) return@launch
    
    val dialParams = DialCallParameters(callerName = callerName, attributes = callerAttributes)
    call.start(dialParams)
    navController.navigate("queue/$rtcId")
    

    Java Conceptual Equivalent (e.g., in an Activity onClick):

    // Get SDK instance (e.g., from Application class)
    ECVSdk sdk = MyApp.getSdkInstance();
    if (sdk == null) {
        Toast.makeText(this, "SDK not initialized", Toast.LENGTH_SHORT).show();
        return;
    }
    
    // Create the call
    VideoCall videoCall = sdk.createCall();
    
    // Get UI data
    String callerName = editTextCallerName.getText().toString();
    Map<String, String> callerAttributes = getAttributesFromSpinners(); // Your logic here
    
    // Note: Waiting for GotRtcId in Java requires a callback mechanism or
    // careful state management, as you can't directly 'await' a flow like in Kotlin.
    // A simplified approach (less robust) might skip explicit waiting if the delay is acceptable.
    // A more robust way involves using the subscribe mechanism (shown later)
    // or potentially a Future/CompletableFuture if the SDK provided one.
    // --- Simplified Dialing (assuming rtcId available quickly) ---
     Handler().postDelayed(() -> { // Example delay, NOT recommended for production
         if (videoCall.getRtcId().isEmpty()) {
             Toast.makeText(this, "Failed to get call ID", Toast.LENGTH_SHORT).show();
             return;
         }
         DialCallParameters dialParams = new DialCallParameters(callerName, callerAttributes, ...);
         videoCall.start(dialParams);
    
         //Navigate to Queue Activity/Fragment, passing the rtcId
         Intent intent = new Intent(this, QueueActivity.class);
         intent.putExtra("RTC_ID", videoCall.getRtcId());
         startActivity(intent);
     }, 1000); // Arbitrary delay - BAD PRACTICE
    
    
    // --- Better approach: Subscribe to events ---
    // Subscribe within the Activity's lifecycle scope
    // (See event handling section below for the subscribe pattern)
    // In the onEvent callback for GotRtcId:
       String rtcId = event.getDetail();
       DialCallParameters dialParams = new DialCallParameters(callerName,callerAttributes, ...);
       videoCall.start(dialParams);
       // Navigate to Queue Activity/Fragment
       Intent intent = new Intent(this, QueueActivity.class);
       intent.putExtra("RTC_ID", rtcId);
       startActivity(intent);
    
    
    // --- For this conceptual example, let's assume GotRtcId happens somehow ---
    // --- and then you dial and navigate ---
     DialCallParameters dialParams = new DialCallParameters(callerName, callerAttributes, ...);
     videoCall.start(dialParams);
     // Assuming rtcId is available now (which might not be true without proper event handling)
     Intent intent = new Intent(this, QueueActivity.class);
     intent.putExtra("RTC_ID", videoCall.getRtcId());  // May be empty initially!
     startActivity(intent);
    

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

Purpose: Displays a waiting indicator and handles navigation based on SDK events while in the queue.

SDK Interaction & Code Highlights:

  1. Retrieve VideoCall Instance: Get the instance using the rtcId passed via Intent extras or Fragment arguments.

    Kotlin (Demo):

    val rtcId: String = // from navigation args
    var videoCall = AppState.sdkInstance?.getVideoCall(rtcId)
    // Handle if null
    

    Java Conceptual Equivalent:

    // In Activity onCreate or Fragment onViewCreated
    String rtcId = getIntent().getStringExtra("RTC_ID"); // Or getArguments().getString(...)
    VideoCall videoCall = MyApp.getSdkInstance().getVideoCall(rtcId);
    
    if (videoCall == null) {
        Toast.makeText(this, "Call not found", Toast.LENGTH_SHORT).show();
        finish(); // Close this activity
        return;
    }
    // Store videoCall as a member variable
    this.videoCall = videoCall;
    
  2. Event Handling: Subscribe to events for the specific videoCall.

    Kotlin (Demo - LaunchedEffect):

    LaunchedEffect(videoCall) {
        if (videoCall.agentConnectedState) { /* Navigate */ return@LaunchedEffect }
        videoCall.eventFlow.collect { event ->
            when (event.type) {
                CallEventTypes.AgentConnected -> { /* Navigate to CallActivity */ }
                CallEventTypes.Error, CallEventTypes.Hangup, CallEventTypes.CallEnded -> { /* Navigate back to Main */ }
                else -> {}
            }
        }
    }
    

    Java Conceptual Equivalent (using subscribe):

    // In Activity onCreate or Fragment onViewCreated, after getting videoCall
    LifecycleOwner lifecycleOwner = this; // Activity/Fragment is a LifecycleOwner
    
    // Check initial state
     if (videoCall.getAgentConnectedState()) {
         navigateToCallScreen(rtcId); // Your navigation method
         return; // Don't subscribe if already connected
     }
    
    videoCall.subscribe(lifecycleOwner, new EventCallback() {
        @Override
        public void onEvent(VideoCallEvent event) {
            runOnUiThread(() -> { // Ensure UI updates on main thread
                if (event.getType() == CallEventTypes.AgentConnected) {
                    navigateToCallScreen(rtcId); // Your navigation method
                } else if (event.getType() == CallEventTypes.Error
                        || event.getType() == CallEventTypes.Hangup
                        || event.getType() == CallEventTypes.CallEnded) {
                    String message = event.getDetail();
                    if (message == null && event.getError() != null) {
                         message = event.getError().getMessage();
                    }
                    Toast.makeText(QueueActivity.this, "Exiting Queue: " + message, Toast.LENGTH_LONG).show();
                    navigateBackToMainScreen(); // Your navigation method
                }
            });
        }
    });
    
  3. UI: Show a ProgressBar and waiting text. Add a Hangup Button.

  4. Cleanup: Hang up the call if the user leaves the screen prematurely.

    Kotlin (Demo - DisposableEffect):

    DisposableEffect(videoCall) {
        onDispose {
            if (!videoCall.didHangup && !videoCall.agentConnectedState) {
                 videoCall.hangup()
            }
        }
    }
    

    Java Conceptual Equivalent (in Activity onDestroy or Fragment onDestroyView):

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (videoCall != null && !videoCall.getDidHangup() && !videoCall.getAgentConnectedState()) {
            Log.d("QueueActivity", "onDestroy, hanging up call " + videoCall.getRtcId());
            // Run hangup potentially off the main thread if it involves network calls
            // Be cautious about performing long operations in onDestroy
             new Thread(() -> videoCall.hangup()).start(); // Simple threading example
        }
    }
    

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

Purpose: Main call interface, managing renderers, controls, and call state.

SDK Interaction & Code Highlights:

  1. Retrieve VideoCall & WebRtcConnection: Similar to QueueScreen, get videoCall via rtcId. Call videoCall.connect() and webRtcConnection.startRoom(context).

    Kotlin (Demo):

    LaunchedEffect(videoCall) {
        if (videoCall == null) { /* Handle error */ return@LaunchedEffect }
        val connection = videoCall.connect()
        if (connection == null) { /* Handle error */ return@LaunchedEffect }
        webRtcConnection = connection
        connection.startRoom(context)
    }
    

    Java Conceptual Equivalent:

    // In Activity onCreate or Fragment onViewCreated
    String rtcId = getIntent().getStringExtra("RTC_ID");
    VideoCall videoCall = MyApp.getSdkInstance().getVideoCall(rtcId);
    if (videoCall == null) { /* handle error, finish */ return; }
    this.videoCall = videoCall;
    
    WebRtcConnection connection = videoCall.connect(null); // Pass ConnectParameters if needed
    if (connection == null) { /* handle error, finish */ return; }
    this.webRtcConnection = connection;
    
    // Start WebRTC - consider timing, might need to wait for view setup
    webRtcConnection.startRoom(this); // Pass context
    
  2. Event Handling & UI State: Subscribe to videoCall.eventFlow (Kotlin) or use videoCall.subscribe (Java) to update UI elements (mute icons, hold overlay visibility, etc.) based on SDK events. Store UI state in member variables or ViewModel.

  3. Call Controls: Wire up Button clicks to SDK methods.

    Kotlin (Demo):

    onToggleAudioMuteClick = { webRtcConnection?.setAudioEnabled(!audioState) }
    onSwitchCameraClick = { webRtcConnection?.switchCamera() }
    onHangupClick = { /* Show dialog */ videoCall?.hangup() }
    

    Java Conceptual Equivalent:

    // In OnClickListener for the mute button
    boolean currentAudioState = /* get current state, e.g., from a boolean member */;
    if (webRtcConnection != null) {
        webRtcConnection.setAudioEnabled(!currentAudioState);
        // Update button icon and currentAudioState variable based on AudioMuted/Unmuted events
    }
    
    // In OnClickListener for switch camera button
    if (webRtcConnection != null) {
        webRtcConnection.switchCamera();
    }
    
    // In OnClickListener for hangup button
    new AlertDialog.Builder(this)
        .setTitle("Confirm Hangup")
        // ... message, buttons ...
        .setPositiveButton("Hangup", (dialog, which) -> {
            if (videoCall != null) {
                 new Thread(() -> videoCall.hangup()).start(); // Hangup off main thread
                 // UI update/navigation often driven by CallEnded event
            }
        })
        .show();
    
  4. Video Rendering: This is significantly different between Compose and traditional Views.

    • Compose: Uses AndroidView with factory, update, and onRelease for lifecycle management as detailed in SDK_Usage.md and shown in the VideoRenderer composable.

    • Java/XML:

      1. Include <org.webrtc.SurfaceViewRenderer ... /> in 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 the connection is ready (e.g., after WebRtcConnected or when the view becomes available).

      5. Detachment & Release: Crucially, in onDestroy or onDestroyView, call webRtcConnection.detachLocalView(...), webRtcConnection.detachRemoteView(...), and surfaceViewRenderer.release() for both local and remote renderers. Failure to do this will cause leaks and crashes.

  5. Immersive Mode: Use WindowInsetsControllerCompat to hide/show system bars in onResume/onPause or relevant lifecycle events.

  6. Cleanup: In onDestroy, ensure videoCall.hangup() is called if the call is still active. Release renderers as mentioned above. Stop any event subscriptions implicitly handled by videoCall.subscribe's LifecycleOwner.

Conclusion

The demo application provides a practical structure for using the ECV SDK. Key integration points involve:

  • Initializing and managing the ECVSdk instance.

  • Creating and managing VideoCall instances.

  • Handling permissions.

  • Starting calls with appropriate parameters.

  • Subscribing to and reacting to VideoCallEvents to drive UI state and navigation (using Kotlin Flows or Java Callbacks with LifecycleOwner).

  • Correctly managing the lifecycle of SurfaceViewRenderers within your UI framework (especially init, attach/detach, and release).

  • Calling SDK methods (hangup, setAudioEnabled, etc.) in response to user actions.

Refer back to the demo code while reading the other documentation files (SDK_Usage.md, Configuration.md, Events_and_State.md) for a complete understanding. Adapt the concepts shown here to your application's specific architecture (Compose or View-based).

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.

This guide details the structure of SDK events, lists the key event types, and mentions relevant state properties.

Subscribing to Events

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

  1. Kotlin (Flow): Access the videoCall.eventFlow (a SharedFlow<VideoCallEvent>) and collect events using Kotlin Coroutines, typically within a LaunchedEffect or lifecycleScope.

  2. Java (Callback): Use the videoCall.subscribe(lifecycleOwner, eventCallback) method, providing an Android LifecycleOwner (like an Activity or Fragment) and an EventCallback implementation. The subscription is automatically managed by the LifecycleOwner.

Refer to the "Handling Events" section in SDK_Usage.md for detailed code examples of both subscription methods.

Event Structure (VideoCallEvent)

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

Key Fields:

  • type: CallEventTypes: (Required) An enum value indicating the kind of event that occurred. This is the primary field you'll check to determine how to react.

  • detail: String?: (Optional) Provides additional string information specific to the event (e.g., the rtcId for GotRtcId, an interaction ID for DialSuccess, a reason for CallEnded).

  • error: ECVError?: (Optional) Included only for Error events. Contains an ECVError object with an ErrorTypes enum and a descriptive message.

  • objData: Any?: (Optional) Can contain related objects, such as the WebRtcConnection during SignalingConnected or an HTTP response object on certain errors. Use type checking (is or instanceof) if accessing this data.

  • data: Map<String, String>: (Optional, often empty) A generic map for potential key-value data associated with the event.

  • createdAtMillis: Long: Timestamp (epoch milliseconds) when the event was created within the SDK.

  • videoCall: VideoCall: (Set internally) A reference back to the VideoCall instance that emitted the event.

  • rtcId: String: (Set internally) 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

    • When: Fired after sdkInstance.createCall() successfully retrieves a unique call identifier from the backend.

    • Data: event.detail contains the rtcId (String).

    • Response: Store the rtcId. You need it to retrieve the VideoCall instance later and to initiate dialing (call.start()).

  • DialSuccess

    • When: Fired after call.start() successfully sends the dial request to the backend and receives confirmation (often includes an Interaction ID).

    • Data: event.detail may contain the Interaction ID. event.objData might hold the raw JSON response.

    • Response: Usually indicates you can proceed to the waiting/queue state.

  • Error (During Init/Dial)

    • When: Can occur if fetching rtcId fails, call.start() parameters are invalid, the backend rejects the dial request, or network issues occur.

    • Data: event.error contains the ECVError. event.detail may have more context. event.objData might hold an HTTP response. Check error.error for specific ErrorTypes like CALL_NOT_INITIALIZED, DIAL_FAILED.

    • Response: Display an error message to the user. Prevent further call progress. Potentially clean up the VideoCall instance if it's unusable.


Phase 2: Queue & Agent Connection

  • OnQueue

    • When: Fired after a successful dial, indicating the caller is now waiting for an agent.

    • Data: Typically no specific data needed.

    • Response: Show a waiting UI (like QueueScreen).

  • AgentConnected

    • When: Fired when an agent accepts the call. This is the trigger to establish the peer-to-peer media connection.

    • Data: None specific.

    • Response: Initiate the WebRTC connection:

      1. Call videoCall.connect() to get the WebRtcConnection.

      2. Call webRtcConnection.startRoom(context) to begin WebRTC setup.

      3. Navigate to the main call screen (CallScreen).

  • Error (During Queue)

    • When: Can occur due to queue timeouts (QUEUE_SOCKET_TIMEOUT), network issues with the queue mechanism (QUEUE_SOCKET_ERROR, QUEUE_SOCKET_CALL_DISCONNECTED), or backend problems (WAIT_FOR_ROOM_FAILED if using polling).

    • Data: event.error contains the ECVError.

    • Response: Inform the user the call couldn't connect. Navigate back from the queue screen. Call videoCall.hangup() to ensure cleanup if the SDK doesn't automatically trigger CallEnded.

  • Hangup / CallEnded (During Queue)

    • When: If the user explicitly hangs up (videoCall.hangup()), or the call is terminated by the backend before an agent connects.

    • Data: event.detail might contain a reason ("LOCAL_HANGUP", etc.).

    • Response: Navigate back from the queue screen. Ensure UI is cleaned up.


Phase 3: WebRTC Connection Establishment

These events are typically handled internally by WebRtcConnection but can be observed for debugging or advanced use cases. The key event signaling success is WebRtcConnected.

  • SignalingConnected: WebSocket connection to the signaling server established.

  • SignalingRegistered: Client successfully registered with the signaling server for the room (rtcId).

  • JoinedRoom: HTTP /join request successful, received initial parameters (like ICE servers, initiator status). event.objData contains JoinRoomResponse.

  • RtcPeerConnectionCreated: The underlying WebRTC PeerConnection object has been created. event.objData holds the PeerConnectionClient.

  • GotOffer / SentOffer: SDP Offer received from / sent to the peer. event.objData has the JSON message.

  • GotAnswer / SentAnswer: SDP Answer received from / sent to the peer. event.objData has the JSON message.

  • WebRtcConnected

    • When: The WebRTC PeerConnection (ICE and DTLS) is fully established and media can start flowing.

    • Data: None specific.

    • Response: Often used as a trigger to know the media path is ready. You might hide initial loading indicators on the CallScreen.

  • TextChannelConnected: The specific data channel used for text commands (hold, flash, etc.) is open. event.objData holds the DataChannel.

  • WebRtcDisconnected / WebRtcFailed: The WebRTC connection dropped or failed unexpectedly. Usually leads to a CallEnded event shortly after.


Phase 4: Active Call & Media

  • GotLocalStream: Local camera capture has started. event.objData might contain the VideoCapturer.

  • GotRemoteStream: Remote media stream is available (often triggered around WebRtcConnected).

    • Response: Ensure remote SurfaceViewRenderer is attached.

  • AudioMuted / AudioUnmuted: Local audio sending state changed (triggered by setAudioEnabled).

    • Response: Update the mute button UI state.

  • VideoMuted / VideoUnmuted: Local video sending state changed (triggered by setVideoEnabled).

    • Response: Update the video mute button UI state. Potentially show/hide the local video preview overlay.

  • RemoteAudioMuted / RemoteAudioUnmuted: The remote participant muted/unmuted their audio.

    • Response: Optionally display an indicator that the remote party is muted.

  • RemoteVideoMuted / RemoteVideoUnmuted: The remote participant muted/unmuted their video.

    • Response: Show/hide an overlay or indicator on the remote video renderer.

  • StartHold / EndHold: The call was put on hold or taken off hold by the agent (usually triggered via the text data channel).

    • Response: Show/hide a "Call on Hold" overlay. The SDK (if autoHandleHoldSideEffects is true) might automatically mute/unmute local media in response.

  • TakePhotoOn / TakePhotoOff: Signals related to agent-initiated photo capture features.

    • Response: Adjust UI if needed (e.g., potentially force fullscreen local video). The SDK might automatically switch cameras if autoHandleCameraSwitch is true.

  • TextChannelMessage: A command or message received over the text data channel.

    • Data: event.detail contains the raw message string (e.g., "FLASH_ON", "STATUS|VIDEO|MUTED|rtcId").

    • Response: SDK handles many standard messages internally (like HOLD, PICKUP, MUTE statuses, FLASH commands). You might react to custom messages specific to your implementation.


Phase 5: Call Termination

  • Hangup

    • When: Fired immediately when videoCall.hangup() is called locally.

    • Data: None specific.

    • Response: Can be used to initiate UI cleanup before the backend confirms the call end. Note that CallEnded is the definitive end event.

  • CallEnded

    • When: The final event indicating the call session is completely terminated, either locally (hangup() completion), remotely (agent/peer hangs up), or due to an unrecoverable error.

    • Data: event.detail often contains a reason (e.g., "LOCAL_HANGUP", "REMOTE_HANGUP", "PEER_DISCONNECTED", "CONNECTION_TIMEOUT", error description).

    • Response: This is the definitive signal to:

      1. Clean up all call-related UI (renderers, controls).

      2. Navigate away from the call screen.

      3. Release any resources tied to the specific VideoCall. The SDK typically removes its internal reference to the VideoCall instance after this event.

State Properties

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

  • On VideoCall:

    • rtcId: String: The unique call identifier (available after GotRtcId).

    • agentConnectedState: Boolean: True if the AgentConnected event has occurred.

    • didHangup: Boolean: True if hangup() has been called locally.

    • webRtcConnection: WebRtcConnection?: The associated WebRTC connection (available after connect() is called).

  • On WebRtcConnection:

    • audioEnabled: Boolean: Current state of local audio sending (reflects last setAudioEnabled call).

    • videoEnabled: Boolean: Current state of local video sending (reflects last setVideoEnabled call).

    • isClosing: Boolean: True if the cleanup process for this connection has started.

It's generally best practice to react to events to update your UI state rather than constantly polling these properties. Use the properties for initial state checks or decisions where needed (e.g., checking agentConnectedState before calling connect()).

Customization Guide

Introduction

The provided Demo Application (ecv175_android) showcases the core functionalities of the ECV SDK. However, your production application will undoubtedly have different UI/UX requirements, configuration methods, and data needs. This guide provides advice on adapting the SDK and demo concepts to your specific application.

1. UI Implementation

The demo uses Jetpack Compose, but the SDK itself is UI-agnostic. You can integrate it into applications built with:

  • Jetpack Compose

  • Traditional Android Views (XML layouts with Activities/Fragments)

  • Any combination thereof.

Key Considerations for Your UI:

  • Build Your Own UI: Do not simply copy the demo UI screens. Design screens that match your application's look, feel, and user flow.

  • Core SDK Interactions Remain the Same: Regardless of your UI toolkit, the fundamental interactions with the SDK persist:

    • Permissions: You must request and handle Camera, Microphone (and Bluetooth Connect for S+) permissions before initiating a call.

    • Video Renderers: You need to provide org.webrtc.SurfaceViewRenderer instances in your layout to display video.

    • Renderer Lifecycle: This is critical. You MUST correctly manage the lifecycle of SurfaceViewRenderers:

      • Initialize them using webRtcConnection.eglBase.eglBaseContext.

      • Attach them to local/remote streams using webRtcConnection.attachLocalView/attachRemoteView.

      • Detach them using webRtcConnection.detachLocalView/detachRemoteView.

      • Release them using surfaceViewRenderer.release() when they are no longer needed (e.g., onDestroyView, onDispose, Activity onDestroy). Failure to release correctly is a common source of crashes and memory leaks. (See SDK_Usage.md and Demo_App_Guide.md for examples).

    • Call State Display: Your UI must reflect the current call status (e.g., "Connecting...", "Waiting for agent...", "Connected", "On Hold") based on SDK events.

    • Call Controls: Implement UI elements (Buttons, Icons) for:

      • Muting/Unmuting Audio (webRtcConnection.setAudioEnabled(...))

      • Toggling Video (webRtcConnection.setVideoEnabled(...))

      • Switching Camera (webRtcConnection.switchCamera(...))

      • Hanging Up (videoCall.hangup())

    • Event Handling: Your UI layer must listen to SDK events (see Events_and_State.md) to update its state, navigate between screens, and handle errors.

2. Configuration (ECVSdkConfiguration)

The demo's EnvironmentSelectorScreen is designed for easily testing different backend environments. You will likely not need this screen in your production application.

Production Configuration Strategies:

  1. Fixed Configuration: If your app only ever connects to one backend environment (e.g., production), you can hardcode the ECVSdkConfiguration values when initializing the ECVSdk.

    // Example: Inside Application.onCreate or DI module
    val prodConfig = ECVSdkConfiguration(
        serviceUrl = "https://your-prod.ecv.example.com",
        useQueueSocket = true // Assuming backend is v1.75+
        // ... other params if needed ...
    )
    val sdkInstance = ECVSdk("MyAppProdInstance", prodConfig)
    
    // Example: Inside Application onCreate or DI module
    ECVSdkConfiguration prodConfig = new ECVSdkConfiguration(
        /* serviceUrl */ "https://your-prod.ecv.example.com",
        // ... other constructor args ...
        /* useQueueSocket */ true // Assuming backend is v1.75+
        // ...
    );
    ECVSdk sdkInstance = new ECVSdk("MyAppProdInstance", prodConfig);
    // Store sdkInstance globally
    
  2. Dynamic Configuration: Fetch configuration details (like serviceUrl, webrtcUrl) from your own application backend before initializing the ECVSdk. This allows you to manage endpoints centrally without requiring app updates.

    • Your app fetches the config from your server.

    • Uses the fetched URLs to create the ECVSdkConfiguration.

    • Initializes ECVSdk.

Important: Ensure the useQueueSocket parameter in your chosen ECVSdkConfiguration matches the capabilities of your target ECV backend (false for v1.5, true recommended for v1.75+).

3. Call Data (DialCallParameters)

The demo uses EnvironmentCallAttribute to dynamically add dropdowns for call data based on the selected environment. This is specific to the demo's multi-environment setup.

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

Example:

// User profile data
val userId = "user123"
val accountType = "Premium"
val intent = "Billing Inquiry"

// Target queue/skill
val targetQueue = "BillingSupport"

// Get SDK instance and create call
val videoCall = sdkInstance.createCall()
// ... wait for GotRtcId ...

// Build parameters with app-specific data
val dialParams = DialCallParameters(
    callerName = "User $userId", // Or actual user name
    attributes = mapOf(
        "userId" to userId,
        "accountType" to accountType,
        "callIntent" to intent
    ),
    acdAttributes = mapOf(
        "skill" to "Billing" // Example ACD attribute
    ),
    queue = targetQueue, // Explicitly set the target queue
    additionalHeaders = mapOf("Authorization" to "Bearer ${getUserAuthToken()}") // Example auth
)

// Start the call
videoCall.start(dialParams)

// Navigate to Queue/Waiting screen
// User profile data
String userId = "user123";
String accountType = "Premium";
String intent = "Billing Inquiry";

// Target queue/skill
String targetQueue = "BillingSupport";

// Get SDK instance and create call
VideoCall videoCall = MyApp.getSdkInstance().createCall();
// ... Handle GotRtcId event using subscribe ...

// Assume GotRtcId event has fired and you are ready to dial:
// Build parameters with app-specific data
Map<String, String> attributes = new HashMap<>();
attributes.put("userId", userId);
attributes.put("accountType", accountType);
attributes.put("callIntent", intent);

Map<String, String> acdAttributes = new HashMap<>();
acdAttributes.put("skill", "Billing");

Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + getUserAuthToken()); // Example auth

DialCallParameters dialParams = new DialCallParameters(
    /* callerName */ "User " + userId, // Or actual user name
    /* attributes */ attributes,
    /* acdAttributes */ acdAttributes,
    /* queue */ targetQueue, // Explicitly set the target queue
    /* customPayload */ new HashMap<>(),
    /* additionalHeaders */ headers
);

// Start the call
videoCall.start(dialParams);

// Navigate to Queue/Waiting screen

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, Error.

  • UI Updates: Reflect mute status, hold status, connection state changes visually.

  • Error Display: Show user-friendly messages when Error events occur.

  • Resource Cleanup: Use CallEnded as the definitive signal to clean up UI and potentially release call-specific resources.

5. Advanced Customization (Optional)

  • Custom Data Channel Messages: If your backend sends custom commands via the text data channel (beyond HOLD, FLASH, etc.), you can listen for the TextChannelMessage event and parse event.detail to implement custom application behavior.

  • ConnectParameters Tuning: For specific network environments or quality requirements, you might experiment with adjusting parameters in ConnectParameters, such as videoMaxBitrate, codecs, or ICE filtering modes. Consult WebRTC documentation for details on these advanced options.

  • Audio Management: The SDK uses AppRTCAudioManager internally. While defaults (useSpeakerPhone, autoSwitchToEarPiece) are provided in ConnectParameters, more complex audio routing might require deeper integration or customization beyond the scope of the basic SDK usage.

6. Permissions

Always follow Android's best practices for requesting permissions:

  • Request permissions only when needed (just before initiating the call).

  • Clearly explain why the permissions (Camera, Microphone) are required.

  • Handle cases where the user denies permissions gracefully (e.g., disable calling functionality, provide instructions on how to enable them in settings).

7. Additional Modules

7.1 Call Transfer v1.0.18.45+

This module allows an active, ongoing call to be directed to a different agent/queue. The lifecycle of a transferred call is as follows:

  • The source call is dialed (call creation).

  • The source call waits in the queue.

  • The source call is connected.

  • The agent initiates the transfer.

  • The transfer can be to a queue or directly to a different agent.

  • A direct transfer to an agent can be canceled or rejected by the agent.

  • A "TransferPending" event is received via videoCall.eventFlow.

    • When this event is received, a message such as "Please wait, your call will be transferred shortly" can be displayed to the user in the interface.

    • At any point after this event, a "TransferCanceled" event may arrive. When the cancel event is received, the displayed warning must be hidden. The call will continue as normal.

  • The agent may have placed the call on HOLD before the transfer, so the transfer message must be displayed in front of the HOLD interface.

  • When the transfer is complete, a "TransferCompleted" event will be received via eventFlow. The event.detail will contain the new RtcId value assigned to the transferred call.

  • The current call view and all associated resources are destroyed.

  • The user is sent back to the queue page, this time with the new RtcId.

  • The user waits in the queue as if a new call was initiated and is connected via an "AgentConnected" event when an agent is ready.

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: Could not find :ECV-175-SDK-vX.Y.Z: (or other AARs)

    • Cause: Gradle cannot locate the required .aar files.

    • Solution:

      • Ensure all three required .aar files (ECV SDK, M134-libwebrtc, autobahn-android-21.7.1) are copied directly into the app/libs directory of your project.

      • Verify that your app-level build.gradle/.kts includes the flatDir { dirs 'libs' } entry within the repositories { ... } block.

      • Confirm the implementation(name: '...', ext: 'aar') lines in your dependencies block exactly match the filenames (without the .aar extension) in the libs directory.

      • Perform a Gradle Sync (File > Sync Project with Gradle Files). Try Build > Clean Project and Build > Rebuild Project.

  2. Error: Unresolved reference: lifecycle or similar related to androidx.lifecycle

    • Cause: The required androidx.lifecycle:lifecycle-runtime-ktx dependency is missing from your app's build file. The SDK uses it internally but doesn't expose it via api.

    • Solution: Add the implementation("androidx.lifecycle:lifecycle-runtime-ktx:VERSION") dependency to your app-level build.gradle/.kts file, replacing VERSION with a recent stable version compatible with your project (e.g., 2.7.0). See README.md for details. Sync Gradle.

  3. Error: Duplicate Class Found...

    • Cause: Your project or another library includes conflicting versions of dependencies also used internally by the SDK's AARs (e.g., different WebRTC or Autobahn versions, potentially Kotlin standard library issues).

    • Solution:

      • Analyze the build error message to identify the conflicting libraries.

      • Use Gradle's dependency resolution strategies to exclude transitive dependencies or force specific versions. Example (Groovy):

        implementation('some.other.library:1.0') {
            // Exclude a conflicting module provided by ECV SDK's deps
            exclude group: 'org.webrtc', module: 'google-webrtc'
        }
        
      • Ensure your project uses compatible versions of core libraries (like Kotlin).

  4. Error: Manifest Merger Failed (Permissions)

    • Cause: The SDK's manifest might declare permissions or features that conflict with your app's manifest.

    • Solution:

      • Examine the specific manifest merger error message in the Build output.

      • Ensure your app's AndroidManifest.xml correctly declares necessary permissions like CAMERA, RECORD_AUDIO, INTERNET, MODIFY_AUDIO_SETTINGS, and potentially BLUETOOTH_CONNECT (for API 31+).

      • Use manifest merger override rules (tools:overrideLibrary, tools:node="replace") carefully if needed, but understand the implications. Usually, ensuring your app declares the required permissions is sufficient.


Runtime Crashes

  1. Crash: UnsatisfiedLinkError (related to org.webrtc or native methods)

    • Cause: The native WebRTC libraries (.so files inside the M134-libwebrtc.aar) could not be loaded. This is often due to architecture mismatches or installation issues.

    • Solution:

      • Ensure the M134-libwebrtc.aar is correctly included and processed by Gradle.

      • If using ABI splits in your build, ensure the correct architecture's .so files are included for the target device/emulator.

      • Clean and rebuild the project. Test on different devices/emulators if possible.

      • Verify no other library is interfering with native library loading.

  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 trying to access sdkInstance (e.g., MyApp.getSdkInstance()). Check for initialization errors in logs.

      • getVideoCall Returns Null: The rtcId might be invalid, the call might have already ended (CallEnded event), or the SDK instance might have been re-initialized. Always check the return value of getVideoCall(rtcId) for null before using the VideoCall object, especially when resuming an Activity/Fragment or navigating.

      • WebRtcConnection is Null: Ensure videoCall.connect() was called successfully after the AgentConnected event and before accessing webRtcConnection. Check for errors during the connect() or startRoom() process.

      • Context Null: Ensure valid Context is passed where required (e.g., startRoom).

  3. Crash: Related to SurfaceViewRenderer.release() or EGL errors

    • Cause: Improper lifecycle management of SurfaceViewRenderer.

    • Solution:

      • Release MUST be called: Ensure surfaceViewRenderer.release() is called in the appropriate lifecycle callback (onRelease in Compose AndroidView, onDestroyView/onDestroy for Fragments/Activities).

      • Detach Before Release: Crucially, call webRtcConnection.detachLocalView(...) and webRtcConnection.detachRemoteView(...) on the renderer before calling release().

      • Threading: Ensure init, attach/detach, and release are called from the correct thread (usually the main UI thread).

      • Double Release: Ensure release() isn't called multiple times on the same renderer instance.

  4. Crash: SecurityException (Permission Denial)

    • Cause: Attempting to start the camera or microphone without the required runtime permissions granted by the user.

    • Solution:

      • Implement robust permission request logic before calling videoCall.start() or webRtcConnection.startRoom().

      • Check the permission status before enabling call functionality.

      • Handle cases where the user denies permissions gracefully. Refer to the demo's MainScreen.kt for an example.


Media Quality Issues

  1. Poor Video/Audio Quality (Lag, Freezing, Pixelation, Garbled Audio):

    • Debugging:

      • Network: This is the most common cause. Check network bandwidth, latency, and packet loss on the device (e.g., using speed test apps, ping). Test on different networks (Wi-Fi vs Cellular).

      • Device Performance: Older or lower-end devices might struggle with encoding/decoding, especially at higher resolutions.

      • ConnectParameters: Consider lowering videoWidth/videoHeight/videoFps or setting a videoMaxBitrate if bandwidth is limited. Ensure videoCodecHwAcceleration is enabled if possible.

      • WebRTC Stats: Use peerConnectionClient.enableStatsEvents to gather detailed WebRTC statistics (though interpreting them requires expertise).


Specific Feature Issues

  1. Camera Switch Doesn't Work:

    • Debugging:

      • Ensure webRtcConnection.switchCamera() is being called.

      • Check logs for errors during the switch attempt.

      • Verify the device actually has multiple cameras (front/back).

  2. Hold/Pickup Doesn't Mute/Unmute Automatically:

    • Debugging:

      • Ensure autoHandleHoldSideEffects = true in ConnectParameters (it's the default).

      • Check if StartHold / EndHold events are being received correctly.

      • Check if TextChannelMessage events related to "HOLD|" or "PICKUP|" are received. Verify the text channel is connected (TextChannelConnected event).

General Debugging Steps

  1. Check Logcat: This is your most important tool. Filter by tags used in the SDK (ECVSdk, VideoCall, WebRtcConnection, Signaling, QueueSocket, PeerConnectionClient, AppRTCAudioManager) and your own application tags (AppState, MainScreen, CallScreen, etc.). Look for warnings and errors. Increase logging verbosity if needed (though the SDK uses standard Android logging levels).

  2. Verify Configuration: Meticulously double-check ECVSdkConfiguration (URLs, useQueueSocket), DialCallParameters (attributes, queue), and any custom ConnectParameters against your target environment and requirements.

  3. Check Network Connectivity: Can the device reach the serviceUrl, webrtcUrl, and signalingUrl? Use browser/ping/network tools. Check Wi-Fi/Cellular connection status. Test different networks. Ask about corporate firewalls if applicable.

  4. Simplify: Temporarily remove complex UI, custom logic, or optional SDK features. Use the most basic, default configurations (ECVSdkConfiguration, DialCallParameters, ConnectParameters). Does a simple call work? Gradually reintroduce complexity.

  5. Isolate the Phase: Determine when the failure occurs: SDK init? Dialing? Queue? WebRTC connection? Media rendering? This helps narrow down the search.

  6. Consult Demo App: Carefully compare your implementation logic, especially around event handling, navigation, and SurfaceViewRenderer lifecycle management, with the provided demo source code.

  7. Log All Events: Add logging inside your event handling callback/collector to print every VideoCallEvent received. This helps trace the call flow and see where it might be going wrong or which expected events are missing.

Contacting Support

If you continue to face issues after following these steps, please prepare the following information when contacting support:

  • SDK Version: (e.g., ECV-175-SDK-v1.2.3)

  • Device(s) Tested: Manufacturer, Model, Android OS Version.

  • Backend Version: ECV v1.5 or v1.75+?

  • Configuration: Your ECVSdkConfiguration, relevant DialCallParameters, and any custom ConnectParameters.

  • Detailed Problem Description: What are the symptoms? What steps reproduce the issue? What is the expected vs actual behavior?

  • Relevant Logcat Output: Capture logs during the time the issue occurs, including SDK tags and your application tags. Filter appropriately if possible, but provide context.

  • Screenshots/Videos: If helpful to illustrate the problem.