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:
- The SDK: An Android Library hosted on Artifactory. You will add this as a Maven dependency to your project.
- 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.
-
Add the Artifactory Repository to Your Project
In modern Android projects, the best practice is to add repositories to the project-level
settings.gradle(orsettings.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 --- } } -
Add the SDK Dependencies
Next, open your app-level
build.gradle(orbuild.gradle.kts) file and add the SDK and its required libraries to thedependenciesblock.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) } -
Add Lifecycle Runtime Dependency (If Needed): The ECV SDK uses the
androidx.lifecycle:lifecycle-runtime-ktxlibrary internally. Because it's not exposed as anapidependency, 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.), replacelibs.androidx.lifecycle.runtime.ktxwith the explicit coordinate string like"androidx.lifecycle:lifecycle-runtime-ktx:2.7.0"(use the latest stable version compatible with your project). -
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.
- Open the
ecv175_androidproject in Android Studio. - Build and run the demo app on an emulator or physical device.
- Examine the source code, particularly the files in the
uipackage (EnvironmentSelectorScreen.kt,MainScreen.kt,QueueScreen.kt,CallScreen.kt) and theAppState.ktobject 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 viaVideoCall.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 usingWebRtcConnection.
Demo Application Flow
The demo app follows this basic flow:
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.MainScreen: Collects necessary call information (e.g., caller name, attributes), requests permissions (Camera, Mic), and initiates the call usingsdk.createCall()andcall.start().QueueScreen: Displays a waiting state while the call connects to an agent. Listens forAgentConnectedorError/Hangupevents.CallScreen: The main video call interface. ManagesSurfaceViewRenderers 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.mdfor a detailed understanding of the SDK's core components. - Consult
Demo_App_Guide.mdwhile 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: TheECVSdkConfigurationpassed 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:
- Initialization: When
createCall()is called, the SDK asynchronously fetches anrtcIdfrom the backend. You should listen for theGotRtcIdevent. ThertcIdis essential for identifying the call.videoCall.rtcId: Access the unique ID (available afterGotRtcIdevent).
- 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): SeeConfiguration.mdforDialCallParameters.- Listen for
DialSuccessorErrorevents. OnQueueevent indicates the call is waiting for an agent.
- Agent Connection:
- The
AgentConnectedevent fires when an agent accepts the call. videoCall.agentConnectedState: Boolean flag indicating if the agent is connected.
- The
- WebRTC Connection (
connect): AfterAgentConnected, you establish the peer-to-peer media connection.videoCall.connect(parameters: ConnectParameters? = null): WebRtcConnection: Creates or retrieves theWebRtcConnectionobject. SeeConfiguration.mdforConnectParameters.
- Starting Media (
startRoom): Once you have theWebRtcConnection, you need to start the WebRTC setup process.webRtcConnection.startRoom(context: Context): Initializes PeerConnection, sets up media tracks, and starts the signaling process.
- Media Control: Use methods on the
WebRtcConnectioninstance (see next section). - 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
Hangupevent (triggered by callinghangup) orCallEndedevent (often triggered by remote party or errors).
Finding an Existing Call:
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 afterAgentConnected. RequiresContext.eglBase: EglBase: Provides the EGL context needed for initializingSurfaceViewRenderers.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. TriggersAudioMuted/AudioUnmutedevents.setVideoEnabled(state: Boolean): Stops or starts sending the local video stream. TriggersVideoMuted/VideoUnmutedevents.switchCamera(targetCamera: String? = null): Switches between front ("f") and back ("b") cameras. IftargetCamerais null, it toggles.changeFlashState(state: Boolean) v1.0.18.45+: Activates the flash whentrueis passed, and deactivates it whenfalseis 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.
- Initialization: When the
AndroidView'sfactorylambda runs, create theSurfaceViewRendererand initialize it usingwebRtcConnection.eglBase.eglBaseContext. - Attachment: Attach the renderer to the local or remote stream using
webRtcConnection.attachLocalView()orwebRtcConnection.attachRemoteView()within thefactoryorupdatelambda. - Detachment: Detach the renderer in the
updatelambda if the target stream changes (e.g., switching between local/remote in PiP). - Release: Crucially, detach the renderer (
detachLocalView/detachRemoteView) and callsurfaceViewRenderer.release()in theAndroidView'sonReleaselambda 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
collectonvideoCall.eventFlow. This is lifecycle-aware when used withLaunchedEffectorlifecycleScope.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
subscribemethod with aLifecycleOwnerand anEventCallback.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 (checkevent.errorandevent.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: AnECVErrorobject with anErrorTypesenum 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., obtainingrtcId, dialing, hangup notifications).- Example:
"https://your-company.ecv.backend.com"
- Example:
webrtcUrl: String(Optional, defaults toserviceUrl): The base URL used specifically for WebRTC signaling-related HTTP requests (like the/joinendpoint). If your WebRTC signaling infrastructure is hosted separately, specify it here.- Example:
"https://your-company.ecv.webrtc.com"
- Example:
signalingUrl: String(Optional, defaults toserviceUrl): The base URL for the WebSocket signaling server. The SDK will replacehttp/httpswithws/wss. If your WebSocket server is hosted separately, specify it here.- Example:
"https://your-company.ecv.websocket.com"
- Example:
useQueueSocket: Boolean(Optional, defaults totrue): Determines the mechanism for receiving queue status updates (likeAgentConnected).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 toemptyMap()): 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")
- Example:
acdAttributes: Map<String, String>(Optional, defaults toemptyMap()): Specific attributes intended for ACD (Automatic Call Distributor) routing logic on the backend.- Example:
mapOf("skill" to "Billing", "language" to "en-US")
- Example:
queue: String?(Optional, defaults tonull): The specific queue or workgroup name to route the call to. Ifnullor empty, default routing rules on the backend apply.- Example:
"SalesQueue"
- Example:
customPayload: Map<String, String>(Optional, defaults toemptyMap()): A flexible map for sending any other custom data required by your specific backend integration.additionalHeaders: Map<String, String>(Optional, defaults toemptyMap()): Allows adding custom HTTP headers to the/api/dialexrequest. This can be used for authentication tokens or other metadata.- Example:
mapOf("Authorization" to "Bearer YOUR_TOKEN", "X-Tenant-ID" to "TenantA")
- Example:
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/joinrequest.joinHttpReadTimeout: Int = 3000: HTTP read timeout (ms) for the/joinrequest.additionalHeaders: Map<String, String> = emptyMap(): Custom HTTP headers added to the/joinrequest 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 (requiresuseOpenSLES = 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:
- Global State Management (
AppState.kt) - Environment Selection (
EnvironmentSelectorScreen.kt) - Call Initiation (
MainScreen.kt) - Waiting Queue (
QueueScreen.kt) - 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_androiddemo 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
sdkInstanceglobally 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:
-
Permissions: The demo uses
rememberLauncherForActivityResultto request permissions. In a traditional app, you would useActivityCompat.requestPermissionsand handle the result inonRequestPermissionsResult. -
Data Collection: The demo uses
OutlinedTextFieldand custom dropdowns. In a traditional app, you would useEditText,Spinner, etc. -
Initiating the Call: (Inside an
onClicklistener)- Create Call: Get a
VideoCallinstance from the SDK.val call = AppState.sdkInstance?.createCall() - Wait for
GotRtcId: Robust code should wait for theGotRtcIdevent 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); - Create Call: Get a
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:
- Retrieve
VideoCallInstance: Get theVideoCallobject associated with thertcIdpassed 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 - 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(); } }); }); - 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:
- Retrieve
VideoCall&WebRtcConnection: Similar to the queue screen, getvideoCallviartcId. Then callvideoCall.connect()andwebRtcConnection.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 - Event Handling & UI State: Subscribe to
videoCallevents to update UI elements (mute icons, hold overlays, etc.). - Call Controls: Connect
Buttonclicks 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(); } - Video Rendering (Java/XML):
- Add
<org.webrtc.SurfaceViewRenderer ... />to your XML layout. - Get references to the renderers (e.g., using ViewBinding).
- Initialization: In
onCreateoronViewCreated, callsurfaceViewRenderer.init(webRtcConnection.getEglBase().getEglBaseContext(), null). - Attachment: Call
webRtcConnection.attachLocalView(...)andwebRtcConnection.attachRemoteView(...)when ready. - Detachment & Release: Crucially, in
onDestroyoronDestroyView, callwebRtcConnection.detach...View(...)andsurfaceViewRenderer.release()for all renderers. Failure to do so will cause leaks and crashes.
- Add
- Cleanup: In
onDestroy, ensurevideoCall.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:
- Kotlin (Flow): Access the
videoCall.eventFlowand collect events using Kotlin Coroutines. - Java (Callback): Use the
videoCall.subscribe(lifecycleOwner, eventCallback)method, providing an AndroidLifecycleOwner(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., thertcId).error: ECVError?: (Optional) Included forErrorevents.objData: Any?: (Optional) Can contain related objects, like theWebRtcConnection.rtcId: String: ThertcIdof 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 aftercreateCall()successfully retrieves a unique call identifier. Response: Store thertcIdand proceed to dial.DialSuccess: Fired afterstart()successfully sends the dial request. Response: Move to the waiting/queue UI.Error: Can occur if fetchingrtcIdfails 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 callvideoCall.connect(), thenwebRtcConnection.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/joinrequest 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 aCallEndedevent.
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 whenvideoCall.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: StringagentConnectedState: BooleandidHangup: Boolean
- On
WebRtcConnection:audioEnabled: BooleanvideoEnabled: 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.SurfaceViewRendererinstances to display video. - Renderer Lifecycle: This is critical. You MUST correctly manage the lifecycle of
SurfaceViewRenderers: initialize, attach, detach, and release. Failure torelease()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:
- Fixed Configuration: If your app only connects to one backend, you can hardcode the
ECVSdkConfigurationvalues when initializing theECVSdk. - Dynamic Configuration: Fetch configuration details (like
serviceUrl) from your own application backend before initializing theECVSdk. 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:
5. Advanced Customization (Optional)
- Custom Data Channel Messages: If your backend sends custom commands, listen for the
TextChannelMessageevent and parseevent.detail. ConnectParametersTuning: For specific network environments, you might experiment with adjusting parameters likevideoMaxBitrateor 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:
- The source call is connected.
- The agent initiates a transfer.
- A
TransferPendingevent is received viavideoCall.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
TransferCanceledevent. If this occurs, hide the message.
- When the transfer is complete, a
TransferCompletedevent is received.event.detailwill contain the newRtcIdfor 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
AgentConnectedevent 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
- 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 yoursettings.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.gradleare correct. - Clear Gradle Cache: Try
Build > Clean Project, thenBuild > Rebuild Project. If the issue persists, tryFile > Invalidate Caches / Restart....
- Check Repository URL: Ensure the
- Error:
Unresolved reference: lifecycle- Cause: The required
androidx.lifecycle:lifecycle-runtime-ktxdependency is missing. - Solution: Add the
implementation("androidx.lifecycle:lifecycle-runtime-ktx:VERSION")dependency to your app-levelbuild.gradle.
- Cause: The required
- 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.
- 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.xmlcorrectly declaresCAMERA,RECORD_AUDIO,INTERNET,MODIFY_AUDIO_SETTINGS, andBLUETOOTH_CONNECT(for API 31+).
Runtime Crashes
- Crash:
UnsatisfiedLinkError(related toorg.webrtc)- Cause: The native WebRTC libraries could not be loaded.
- Solution: Ensure the
org.webrtc:libwebrtcdependency is correctly included and downloaded by Gradle. Clean and rebuild the project.
- 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. getVideoCallReturns Null: Always check the return value ofgetVideoCall(rtcId)for null before using it, especially when resuming a screen. The call may have ended.WebRtcConnectionis Null: EnsurevideoCall.connect()was called successfully after theAgentConnectedevent.
- SDK Not Initialized: Ensure
- Crash: Related to
SurfaceViewRenderer.release()- Cause: Improper lifecycle management of
SurfaceViewRenderer. - Solution:
release()MUST be called in the appropriate lifecycle callback (onReleasein Compose,onDestroyView/onDestroyfor Fragments/Activities).- Crucially, call
detachLocalView(...)anddetachRemoteView(...)before callingrelease().
- Cause: Improper lifecycle management of
- 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
- 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 avideoMaxBitrate.
- Debugging:
Specific Feature Issues
- Camera Switch Doesn't Work:
- Debugging: Ensure
webRtcConnection.switchCamera()is being called and check logs for errors. Verify the device has multiple cameras.
- Debugging: Ensure
- Hold/Pickup Doesn't Mute/Unmute Automatically:
- Debugging: Ensure
autoHandleHoldSideEffects = trueinConnectParameters. Check ifStartHold/EndHoldevents are being received.
- Debugging: Ensure
General Debugging Steps
- Check Logcat: This is your most important tool. Filter by SDK tags (
ECVSdk,VideoCall,WebRtcConnection,PeerConnectionClient, etc.) and your own app tags. - Verify Configuration: Double-check all
ECVSdkConfiguration,DialCallParameters, andConnectParameters. - Check Network Connectivity: Can the device reach the configured URLs?
- Simplify: Temporarily remove complex UI and custom logic. Does a basic call work?
- Isolate the Phase: Determine when the failure occurs: Init? Dialing? Queue? WebRTC connection?
- Consult Demo App: Compare your logic, especially for event handling and renderer lifecycle, with the demo code.
- Log All Events: Add logging to print every
VideoCallEventyou 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 customConnectParameters. - 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.
No comments to display
No comments to display