Danamon Enhancement Mobile Development Guide - iOS
Attached data fields on the create call
Each EnvironmentCallAttribute declared for an environment becomes one input on the
create call page, and its value is sent as an entry of DialCallParameters.attributes
(the Attributes object of the dialex request).
| Key | Label | Values |
|---|---|---|
video_flow |
Video Flow | ETB, NTB |
video_channel |
Video Channel | Registration, Reactivation, Activation, Saving (2.0), Credit Card |
video_application |
Video Application | DBP 2.0 |
video_language |
Video Language | id, en |
ECV Video Call iOS SDK — Queue Position and Estimated Wait Time
The SDK can report the caller's live queue position and estimated wait time (EWT) while the call is waiting in the queue. It is off by default; enable it with one configuration flag and listen for one event.
This document also covers the attached data fields added for the Danamon flavor
(video_flow, video_channel, video_application, video_language).
What the SDK does
The SDK has three mutually exclusive ways of waiting for the queue. They are picked in a fixed order of precedence:
| # | Mode | Enabled by | Reports position / wait time |
|---|---|---|---|
| 1 | Queue socket | useQueueSocket = true |
no |
| 2 | Queue info polling | useQueueSocket = false and useQueueInfo = true |
yes |
| 3 | Legacy WaitForRoomInterval polling |
both flags false | no |
In queue info mode, VideoCall.start() sends the dial request and then polls
/api/WaitForRoomQueueInterval instead of the legacy endpoint. On every poll it emits a
QueueInfoUpdated event, and it still raises AgentConnected, CallEnded and Error
exactly as before — nothing else in your call flow changes.
Position and wait time come from the Genesys URS statistics through the service.
Backend requirement: the service must expose
GET /api/WaitForRoomQueueInterval?rtcId=…. Position and wait time are only filled in when Genesys URS returns queue statistics; otherwise they arrive asnil/-1and the UI should fall back to a plain spinner.
Enable the feature
let config = ECVSdkConfiguration(
name: "My Environment",
serviceUrl: "https://your-ecv-service",
useQueueSocket: false, // the queue socket takes precedence, so turn it off
useQueueInfo: true, // queue position + estimated wait time
queueInfoIntervalDelay: 3000 // poll interval in ms, optional (default 3000)
)
If your service exposes the endpoint under a different path, override it with
queueInfoEndpoint (default /api/WaitForRoomQueueInterval).
useQueueInfois ignored whileuseQueueSocketis true. If you leave the queue socket on, the call still works — you simply get noQueueInfoUpdatedevents.
In the demo both flags are set per environment in
ECV175_iOS_DEMO/Build.swift (see the DANAMON flavor).
Per-call override
Both flags can also be set per call, on the VideoCall object. A nil override (the
default) falls back to the SDK configuration; a non-nil value wins for that call only.
Set them after creating the call and before start() — they are read when the
queue wait begins:
let call = await sdk.createVideoCall()
call?.useQueueSocketOverride = false // this call: no socket
call?.useQueueInfoOverride = true // this call: queue info polling
await call?.start(dialCallParameters: params)
The same precedence applies to the effective values: socket > queue info > legacy. So to
get queue info mode for a single call while the global configuration uses the socket,
override both flags as above — overriding only useQueueInfoOverride is not enough,
because the socket still wins.
Listen for the event
QueueInfoUpdated carries a QueueInfo in objData:
sdk.modules.onCallEvent.append { (videoCall, newEvent, lastEvent) in
switch newEvent.type {
case .QueueInfoUpdated:
let info = newEvent.objData as? QueueInfo
// update the UI with info?.displayPosition / info?.ewtMinutes
case .AgentConnected:
// navigate to the call screen
case .Error, .Hangup, .CallEnded:
// leave the queue screen
default:
break
}
}
The last snapshot is also available at any time as videoCall.queueInfo, which is
useful when the screen appears after the first event was already emitted:
queueInfo = call?.queueInfo
QueueInfo
| Member | Type | Meaning |
|---|---|---|
state |
String | Raw wait state (wait, not found, agent_connected, …). The SDK already acts on it; you normally do not need it. |
position |
Int? | Raw position. nil if the service has no queue info, -1 if URS reported nothing for the interaction (see below). |
ewtSeconds |
Double? | Raw estimated wait time in seconds. Same nil / -1 rules. |
displayPosition |
Int? | position filtered for UI — nil unless it is a real position. |
ewtMinutes |
Int? | ewtSeconds rounded up to whole minutes, nil when unknown. |
Prefer displayPosition and ewtMinutes in the UI.
What -1 means
position = -1 together with ewtSeconds = -1 is the service's "no queue data"
answer. It is not a call state — read state (or the SDK's own events) for that.
It is returned whenever URS has nothing to report for the interaction, which covers
several different situations:
- the interaction left the queue and is already routed and ringing at an agent — URS no longer tracks it as queued, but the call is perfectly alive
- the interaction never reached URS, or the service could not resolve its connection id
- the queue statistics mode configured on the service does not match the queue type, so the URS lookup fails
- the call really did end on the Genesys side
Because these are indistinguishable from the client, never treat -1 as "the call
ended" and never end the call because of it. Termination always arrives through the
state field and the SDK's CallEnded / Hangup / Error events.
displayPosition and ewtMinutes both return nil for -1, so UI code that uses
them needs no special handling.
Render it
Queue figures are not guaranteed to arrive, so the demo keeps its original waiting
screen and only switches to the position layout once there is something real to show.
From ECV175_iOS_DEMO/Views/Screens/QueueScreen.swift:
@State private var queueInfo: QueueInfo? = nil
// Only switch layouts once the backend actually reported something to show.
private var hasQueueData: Bool {
queueInfo?.displayPosition != nil || queueInfo?.ewtMinutes != nil
}
if hasQueueData {
Text("Please Wait...").font(.title2).bold()
Text("You are in the queue")
if let position = queueInfo?.displayPosition {
Text("\(position)").font(.system(size: 64, weight: .bold))
}
if let minutes = queueInfo?.ewtMinutes {
Text("Estimated wait time about \(minutes) minutes")
}
}
else {
ProgressView()
Text("Please wait, your call will be connected shortly.")
}
Inside the position layout each figure is rendered only when it exists, since a queue
can publish a position without an EWT or the other way round. Design the screen so it
works with no figures at all: the first polls happen before the interaction reaches
Genesys, some routing strategies never publish an EWT, and a caller who gets routed
mid-wait goes back to -1 (see above).
Attached data on the create call page (Danamon)
Each EnvironmentCallAttribute declared for an environment becomes one input on the
main screen, and its value is sent as an entry of DialCallParameters.attributes
(the Attributes object of the DialEx request).
They are declared per environment in ECV175_iOS_DEMO/Build.swift:
EnvironmentCallAttribute(key: "video_flow", displayName: "Video Flow", values: ["ETB", "NTB"])
EnvironmentCallAttribute(key: "NikNumber", displayName: "Nik Number (encrypted)",
defaultValue: "…", type: "text")
| Field | Meaning |
|---|---|
key |
Attribute key sent to the backend |
displayName |
Label shown on the main screen |
values |
Dropdown values for type = "select". The first entry is the preselected one. |
defaultValue |
Prefilled value for type = "text" |
type |
select (default) or text |
The Danamon flavor declares these four attached data fields, shared by both of its
environments through the danamonCallAttributes list:
| Key | Label | Values |
|---|---|---|
video_flow |
Video Flow | ETB, NTB |
video_channel |
Video Channel | Registration, Reactivation, Activation, Saving (2.0), Credit Card |
video_application |
Video Application | DBP 2.0 |
video_language |
Video Language | id, en |
video_flow and video_channel are independent dropdowns, so make sure the selected
combination is valid for your routing: Registration / Reactivation / Activation
belong to ETB, while Saving (2.0) and Credit Card belong to NTB.
Backward compatibility. Older clients do not send these attributes at all. The
service fills in its own defaults for them (video_flow = NTB,
video_channel = Saving (1.0), video_application = DBP 1.0, video_language = id).
The 1.0 values exist only as those service side defaults, which is why they are not
offered on the create call page — a current client always sends the values explicitly.
6. Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
No QueueInfoUpdated events at all |
Feature not enabled, or the queue socket is still on | Check useQueueInfo = true and useQueueSocket = false — in the configuration, or as per-call overrides set before start() |
Events arrive with state=wait but position=-1 ewt=-1.0 for the whole call |
URS reported nothing for this interaction | Check the service log for the urs/call/… request, and whether the queue statistics mode configured on the service matches the queue type. The screen stays on the plain waiting layout, which is the intended fallback. |
position / ewtSeconds flip to -1 mid-wait |
The interaction left the queue — usually routed and now ringing at an agent | Expected, and not a call end. Keep waiting for AgentConnected. |
| Queue never progresses | rtcId mismatch or the dial failed |
Compare the rtcId in the app log with the DialEx request on the service |
ECV Video Call iOS SDK — Hold and Resume
When the agent puts the call on hold from the agent desktop, the SDK reports it to your application so you can cover the screen with a "please wait" overlay, and reports the resume so you can take it away again.
No configuration is needed — the events are always emitted.
How it works
Hold is driven entirely by the agent side. The agent desktop sends a control message over the WebRTC data channel, and the SDK turns it into an event:
| Agent action | Data channel message | SDK event |
|---|---|---|
| Puts the call on hold | HOLD|… |
.StartHold |
| Takes the call back | PICKUP|… |
.EndHold |
There is no client-side API to place a call on hold — the caller cannot hold the agent. Your job is to react to the two events.
Automatic media side effects
On top of the events, the SDK can mute the caller's own media for the duration of the hold. This is controlled by one configuration flag:
let config = ECVSdkConfiguration(
name: "My Environment",
serviceUrl: "https://your-ecv-service",
autoHandleHoldSideEffects: true // default
)
With the flag on (the default), receiving HOLD disables the local audio and video
tracks, and PICKUP enables them again — the agent neither hears nor sees the caller
while the call is on hold. With the flag off, the SDK only emits the events and leaves
the local media untouched; mute it yourself if your flow requires it.
The events are emitted either way.
Implement the overlay
Three pieces: a flag, two event branches, and a view rendered on top.
Keep a flag
In the call screen (the demo's is ECV175_iOS_DEMO/Views/Screens/CallScreen.swift):
@State private var isOnHold = false
Drive it from the events
Extend the onCallEvent handler the call screen already registers:
appState.sdk?.modules.onCallEvent.append { (videoCall, newEvent, lastEvent) in
if self.videoCall?.rtcId != videoCall.rtcId {
return // only react to the current call's events
}
switch newEvent.type {
case .StartHold:
isOnHold = true
case .EndHold:
isOnHold = false
// … other events …
default:
break
}
}
Render the overlay last
Inside the ZStack that holds the video views and the call controls, render the overlay
after everything else so it paints on top:
ZStack {
// remote video, local video preview, call controls …
// --- OVERLAYS ---
// Rendered last to appear on top of the UI elements above.
if isOnHold {
HoldOverlayView()
}
}
struct HoldOverlayView: View {
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
Image(systemName: "hourglass")
.font(.system(size: 64))
.foregroundColor(.white)
}
.zIndex(4) // above the controls
}
}
Things to get right
Make it opaque. The overlay is a solid black view, not a translucent scrim. The agent side stops sending video for the duration of the hold, so what sits behind the overlay is a dead surface — a frozen last frame or a blank view, depending on the device. An opaque overlay replaces that with a deliberate "please wait" state; a translucent scrim would just show the caller a dimmed frozen picture of the agent.
Render it above the controls. zIndex(4) puts it over the call control row, so the
caller cannot hang up while on hold. If your flow needs an escape hatch, either lower
the overlay's zIndex below the controls or place a hangup button inside the overlay
itself.
The state is per call. isOnHold is @State on the call screen, so a call that
ends while on hold simply disposes it when the screen is dismissed. There is nothing to
reset.
Don't fight the automatic mute. With autoHandleHoldSideEffects on, the local
tracks are already disabled during the hold — do not also toggle them from your event
handler, or the resume can leave the two out of sync. Either let the SDK do it (default)
or turn the flag off and own the muting entirely.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
No .StartHold / .EndHold events |
The data channel is not up, or the agent desktop does not send hold messages | Check for TextChannelConnected in the log; verify hold works from the agent desktop for other clients |
| Overlay stays after the agent resumes | .EndHold handled by a handler that was removed |
Keep the onCallEvent handler registered for as long as the call screen is shown |
| Hold does not mute/unmute the caller automatically | autoHandleHoldSideEffects is off, or the events never arrive |
Verify the flag in the configuration and check the log for HOLD received / PICKUP received |
No comments to display
No comments to display