ECV Video Call Android SDK [EN]
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:
-
The SDK: Provided as a pre-compiled Android Library (
.aarfile). You will add this as a 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.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.
-
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
-
-
Copy all three
.aarfiles into your project'slibsdirectory (create it under theappmodule if it doesn't exist). -
Open your app-level
build.gradle(orbuild.gradle.kts) file. -
Add the
libsdirectory 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 } -
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) } -
Add Lifecycle Runtime Dependency (If Needed):
The ECV SDK uses theandroidx.lifecycle:lifecycle-runtime-ktxlibrary internally (implementationdependency). Because it's not anapidependency, 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.), 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.
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.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 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",
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: 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.
-
-
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.
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)
// 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
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"
-
-
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"
-
-
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"
-
-
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(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 to3000milliseconds): Only used ifuseQueueSocketisfalse. Specifies the delay between HTTP polling requests. -
waitForRoomIntervalEndpoint: String(Optional, defaults to"/api/waitforroominterval?rtcId="): Only used ifuseQueueSocketisfalse. The relative API endpoint (appended toserviceUrl) used for HTTP polling. The SDK automatically appends thertcId.
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 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")
-
-
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")
-
-
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"
-
-
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 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 inWebRtcConnection). -
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. 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 tofalsefor audio-only calls. -
disableVideo: Boolean = false: High-level flag to completely disable video sending/receiving if needed (usuallyvideoCallEnabledis 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 (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 used for commands like FLASH_ON/OFF, HOLD/PICKUP, etc. Usually left at default. SeeDataChannelConfigclass 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:
-
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 (
.aarfiles,lifecycle-runtime-ktx) in your environment as described inREADME.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) {
// ... (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
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: 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:
-
Permissions: Use Android's standard permission request mechanism (
ActivityCompat.requestPermissions/registerForActivityResult). Check permissions before enabling call initiation UI. -
Data Collection: Use
EditText,Spinner, or other standard Android UI elements to collectcallerNameand any necessaryattributes. -
Initiating the Call: (e.g., inside an
OnClickListenerfor 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:
-
Retrieve
VideoCallInstance: Get the instance using thertcIdpassed 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; -
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 } }); } }); -
UI: Show a
ProgressBarand waiting text. Add a HangupButton. -
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
onDestroyor FragmentonDestroyView):@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:
-
Retrieve
VideoCall&WebRtcConnection: Similar to QueueScreen, getvideoCallviartcId. CallvideoCall.connect()andwebRtcConnection.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 -
Event Handling & UI State: Subscribe to
videoCall.eventFlow(Kotlin) or usevideoCall.subscribe(Java) to update UI elements (mute icons, hold overlay visibility, etc.) based on SDK events. Store UI state in member variables orViewModel. -
Call Controls: Wire up
Buttonclicks 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(); -
Video Rendering: This is significantly different between Compose and traditional Views.
-
Compose: Uses
AndroidViewwithfactory,update, andonReleasefor lifecycle management as detailed inSDK_Usage.mdand shown in theVideoRenderercomposable. -
Java/XML:
-
Include
<org.webrtc.SurfaceViewRenderer ... />in 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 the connection is ready (e.g., afterWebRtcConnectedor when the view becomes available). -
Detachment & Release: Crucially, in
onDestroyoronDestroyView, callwebRtcConnection.detachLocalView(...),webRtcConnection.detachRemoteView(...), andsurfaceViewRenderer.release()for both local and remote renderers. Failure to do this will cause leaks and crashes.
-
-
-
Immersive Mode: Use
WindowInsetsControllerCompatto hide/show system bars inonResume/onPauseor relevant lifecycle events. -
Cleanup: In
onDestroy, ensurevideoCall.hangup()is called if the call is still active. Release renderers as mentioned above. Stop any event subscriptions implicitly handled byvideoCall.subscribe'sLifecycleOwner.
Conclusion
The demo application provides a practical structure for using the ECV SDK. Key integration points involve:
-
Initializing and managing the
ECVSdkinstance. -
Creating and managing
VideoCallinstances. -
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 withLifecycleOwner). -
Correctly managing the lifecycle of
SurfaceViewRenderers within your UI framework (especiallyinit,attach/detach, andrelease). -
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:
-
Kotlin (Flow): Access the
videoCall.eventFlow(aSharedFlow<VideoCallEvent>) and collect events using Kotlin Coroutines, typically within aLaunchedEffectorlifecycleScope. -
Java (Callback): Use the
videoCall.subscribe(lifecycleOwner, eventCallback)method, providing an AndroidLifecycleOwner(like an Activity or Fragment) and anEventCallbackimplementation. The subscription is automatically managed by theLifecycleOwner.
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., thertcIdforGotRtcId, an interaction ID forDialSuccess, a reason forCallEnded). -
error: ECVError?: (Optional) Included only forErrorevents. Contains anECVErrorobject with anErrorTypesenum and a descriptive message. -
objData: Any?: (Optional) Can contain related objects, such as theWebRtcConnectionduringSignalingConnectedor an HTTP response object on certain errors. Use type checking (isorinstanceof) 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 theVideoCallinstance that emitted the event. -
rtcId: String: (Set internally) 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-
When: Fired after
sdkInstance.createCall()successfully retrieves a unique call identifier from the backend. -
Data:
event.detailcontains thertcId(String). -
Response: Store the
rtcId. You need it to retrieve theVideoCallinstance 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.detailmay contain the Interaction ID.event.objDatamight 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
rtcIdfails,call.start()parameters are invalid, the backend rejects the dial request, or network issues occur. -
Data:
event.errorcontains theECVError.event.detailmay have more context.event.objDatamight hold an HTTP response. Checkerror.errorfor specificErrorTypeslikeCALL_NOT_INITIALIZED,DIAL_FAILED. -
Response: Display an error message to the user. Prevent further call progress. Potentially clean up the
VideoCallinstance 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:
-
Call
videoCall.connect()to get theWebRtcConnection. -
Call
webRtcConnection.startRoom(context)to begin WebRTC setup. -
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_FAILEDif using polling). -
Data:
event.errorcontains theECVError. -
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 triggerCallEnded.
-
-
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.detailmight 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/joinrequest successful, received initial parameters (like ICE servers, initiator status).event.objDatacontainsJoinRoomResponse. -
RtcPeerConnectionCreated: The underlying WebRTCPeerConnectionobject has been created.event.objDataholds thePeerConnectionClient. -
GotOffer/SentOffer: SDP Offer received from / sent to the peer.event.objDatahas the JSON message. -
GotAnswer/SentAnswer: SDP Answer received from / sent to the peer.event.objDatahas 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.objDataholds theDataChannel. -
WebRtcDisconnected/WebRtcFailed: The WebRTC connection dropped or failed unexpectedly. Usually leads to aCallEndedevent shortly after.
Phase 4: Active Call & Media
-
GotLocalStream: Local camera capture has started.event.objDatamight contain theVideoCapturer. -
GotRemoteStream: Remote media stream is available (often triggered aroundWebRtcConnected).-
Response: Ensure remote
SurfaceViewRendereris attached.
-
-
AudioMuted/AudioUnmuted: Local audio sending state changed (triggered bysetAudioEnabled).-
Response: Update the mute button UI state.
-
-
VideoMuted/VideoUnmuted: Local video sending state changed (triggered bysetVideoEnabled).-
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
autoHandleHoldSideEffectsis 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
autoHandleCameraSwitchis true.
-
-
TextChannelMessage: A command or message received over the text data channel.-
Data:
event.detailcontains 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
CallEndedis 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.detailoften contains a reason (e.g., "LOCAL_HANGUP", "REMOTE_HANGUP", "PEER_DISCONNECTED", "CONNECTION_TIMEOUT", error description). -
Response: This is the definitive signal to:
-
Clean up all call-related UI (renderers, controls).
-
Navigate away from the call screen.
-
Release any resources tied to the specific
VideoCall. The SDK typically removes its internal reference to theVideoCallinstance 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 afterGotRtcId). -
agentConnectedState: Boolean: True if theAgentConnectedevent has occurred. -
didHangup: Boolean: True ifhangup()has been called locally. -
webRtcConnection: WebRtcConnection?: The associated WebRTC connection (available afterconnect()is called).
-
-
On
WebRtcConnection:-
audioEnabled: Boolean: Current state of local audio sending (reflects lastsetAudioEnabledcall). -
videoEnabled: Boolean: Current state of local video sending (reflects lastsetVideoEnabledcall). -
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.SurfaceViewRendererinstances 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, ActivityonDestroy). Failure to release correctly is a common source of crashes and memory leaks. (SeeSDK_Usage.mdandDemo_App_Guide.mdfor 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:
-
Fixed Configuration: If your app only ever connects to one backend environment (e.g., production), you can hardcode the
ECVSdkConfigurationvalues when initializing theECVSdk.// 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 -
Dynamic Configuration: Fetch configuration details (like
serviceUrl,webrtcUrl) from your own application backend before initializing theECVSdk. 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:
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
TextChannelMessageevent and parseevent.detailto implement custom application behavior. -
ConnectParametersTuning: For specific network environments or quality requirements, you might experiment with adjusting parameters inConnectParameters, such asvideoMaxBitrate, codecs, or ICE filtering modes. Consult WebRTC documentation for details on these advanced options. -
Audio Management: The SDK uses
AppRTCAudioManagerinternally. While defaults (useSpeakerPhone,autoSwitchToEarPiece) are provided inConnectParameters, 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).
as
7.1 Call Transfer
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
-
Error:
Could not find :ECV-175-SDK-vX.Y.Z:(or other AARs)-
Cause: Gradle cannot locate the required
.aarfiles. -
Solution:
-
Ensure all three required
.aarfiles (ECV SDK,M134-libwebrtc,autobahn-android-21.7.1) are copied directly into theapp/libsdirectory of your project. -
Verify that your app-level
build.gradle/.ktsincludes theflatDir { dirs 'libs' }entry within therepositories { ... }block. -
Confirm the
implementation(name: '...', ext: 'aar')lines in your dependencies block exactly match the filenames (without the.aarextension) in thelibsdirectory. -
Perform a Gradle Sync (
File > Sync Project with Gradle Files). TryBuild > Clean ProjectandBuild > Rebuild Project.
-
-
-
Error:
Unresolved reference: lifecycleor similar related toandroidx.lifecycle-
Cause: The required
androidx.lifecycle:lifecycle-runtime-ktxdependency is missing from your app's build file. The SDK uses it internally but doesn't expose it viaapi. -
Solution: Add the
implementation("androidx.lifecycle:lifecycle-runtime-ktx:VERSION")dependency to your app-levelbuild.gradle/.ktsfile, replacingVERSIONwith a recent stable version compatible with your project (e.g.,2.7.0). SeeREADME.mdfor details. Sync Gradle.
-
-
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).
-
-
-
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.xmlcorrectly declares necessary permissions likeCAMERA,RECORD_AUDIO,INTERNET,MODIFY_AUDIO_SETTINGS, and potentiallyBLUETOOTH_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
-
Crash:
UnsatisfiedLinkError(related toorg.webrtcor native methods)-
Cause: The native WebRTC libraries (
.sofiles inside theM134-libwebrtc.aar) could not be loaded. This is often due to architecture mismatches or installation issues. -
Solution:
-
Ensure the
M134-libwebrtc.aaris correctly included and processed by Gradle. -
If using ABI splits in your build, ensure the correct architecture's
.sofiles 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.
-
-
-
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 accesssdkInstance(e.g.,MyApp.getSdkInstance()). Check for initialization errors in logs. -
getVideoCallReturns Null: ThertcIdmight be invalid, the call might have already ended (CallEndedevent), or the SDK instance might have been re-initialized. Always check the return value ofgetVideoCall(rtcId)for null before using theVideoCallobject, especially when resuming an Activity/Fragment or navigating. -
WebRtcConnectionis Null: EnsurevideoCall.connect()was called successfully after theAgentConnectedevent and before accessingwebRtcConnection. Check for errors during theconnect()orstartRoom()process. -
Context Null: Ensure valid
Contextis passed where required (e.g.,startRoom).
-
-
-
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 (onReleasein ComposeAndroidView,onDestroyView/onDestroyfor Fragments/Activities). -
Detach Before Release: Crucially, call
webRtcConnection.detachLocalView(...)andwebRtcConnection.detachRemoteView(...)on the renderer before callingrelease(). -
Threading: Ensure
init,attach/detach, andreleaseare called from the correct thread (usually the main UI thread). -
Double Release: Ensure
release()isn't called multiple times on the same renderer instance.
-
-
-
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()orwebRtcConnection.startRoom(). -
Check the permission status before enabling call functionality.
-
Handle cases where the user denies permissions gracefully. Refer to the demo's
MainScreen.ktfor an example.
-
-
Media Quality Issues
-
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 loweringvideoWidth/videoHeight/videoFpsor setting avideoMaxBitrateif bandwidth is limited. EnsurevideoCodecHwAccelerationis enabled if possible. -
WebRTC Stats: Use
peerConnectionClient.enableStatsEventsto gather detailed WebRTC statistics (though interpreting them requires expertise).
-
-
Specific Feature Issues
-
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).
-
-
-
Hold/Pickup Doesn't Mute/Unmute Automatically:
-
Debugging:
-
Ensure
autoHandleHoldSideEffects = trueinConnectParameters(it's the default). -
Check if
StartHold/EndHoldevents are being received correctly. -
Check if
TextChannelMessageevents related to "HOLD|" or "PICKUP|" are received. Verify the text channel is connected (TextChannelConnectedevent).
-
-
General Debugging Steps
-
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). -
Verify Configuration: Meticulously double-check
ECVSdkConfiguration(URLs,useQueueSocket),DialCallParameters(attributes, queue), and any customConnectParametersagainst your target environment and requirements. -
Check Network Connectivity: Can the device reach the
serviceUrl,webrtcUrl, andsignalingUrl? Use browser/ping/network tools. Check Wi-Fi/Cellular connection status. Test different networks. Ask about corporate firewalls if applicable. -
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. -
Isolate the Phase: Determine when the failure occurs: SDK init? Dialing? Queue? WebRTC connection? Media rendering? This helps narrow down the search.
-
Consult Demo App: Carefully compare your implementation logic, especially around event handling, navigation, and
SurfaceViewRendererlifecycle management, with the provided demo source code. -
Log All Events: Add logging inside your event handling callback/collector to print every
VideoCallEventreceived. 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, relevantDialCallParameters, and any customConnectParameters. -
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.

