
/*-----------------------------------------------------------------------------------------------*/
/* signaling.cpp                                                                                 */
/* copyright (c) innovaphone 2020                                                                */
/*                                                                                               */
/*-----------------------------------------------------------------------------------------------*/

#include <platform.h>

#include "sdk/common/os/iomux.h"
#include "sdk/common/interface/time.h"
#include "sdk/common/interface/random.h"
#include "sdk/common/interface/task.h"
#include "sdk/common/interface/stask.h"
#include "sdk/common/interface/socket.h"
#include "sdk/common/interface/webserver_plugin.h"
#include "sdk/common/interface/json_api.h"
#include "sdk/common/interface/appwebsocket_client.h"
#include "sdk/common/interface/dns.h"
#include "sdk/common/interface/media.h"
#include "sdk/common/interface/media_tones.h"
#include "sdk/common/interface/audio.h"
#include "sdk/common/interface/ringer.h"
#include "sdk/common/interface/remote_services.h"

#include "sdk/common/lib/appservice.h"
#include "sdk/common/lib/appwebsocket.h"
#include "sdk/common/lib/app_updates.h"
#include "sdk/common/interface/guuid.h"
#include "sdk/common/interface/file.h"
#include "sdk/common/lib/tasks_file.h"

#include "sdk/common/interface/local_storage.h"
#include "common/ilib/json.h"
#include "common/ilib/uri.h"
#include "common/ilib/str.h"

#include "sdk/common/interface/http_client.h"

#include "signaling_api.h"
#include "signaling.h"

#define TIMEOUT_MIN 1000
#define TIMEOUT_MAX 25000    // what myApps Client needs to connect to standby after master is gone

#define DELETE_OBJ(obj) { if (obj) { delete obj; obj = 0; } }

enum {
    presenceSubscriptionCallId = 1,
    presencePublishCallId = 2,
    diversionCallId = 3,
    firstCallId = 4
};

static const char * CoderName(int type);
static int CoderPt(int type);
static int CoderType(const char * name);
static const char * CandidateName(int type);
static int CandidateType(const char * name);
static const char * ChannelsCmdName(unsigned type);
static int ChannelsCmdType(const char * name);

static const char* CountryCodes[] = { "1", "20", "210", "211", "212", "213", "214", "215", "216", "217", "218", "219", "220", "221", "222", "223", "224", "225", "226", "227", "228", "229", "230", "231", "232", "233", "234", "235", "236", "237", "238", "239", "240", "241", "242", "243", "244", "245", "246", "247", "248", "249", "250", "251", "252", "253", "254", "255", "256", "257", "258", "259", "260", "261", "262", "263", "264", "265", "266", "267", "268", "269", "27", "280", "281", "282", "283", "284", "285", "286", "287", "288", "289", "290", "291", "292", "293", "294", "295", "296", "297", "298", "299", "30", "31", "32", "33", "34", "350", "351", "352", "353", "354", "355", "356", "357", "358", "359", "36", "370", "371", "372", "373", "374", "375", "376", "377", "378", "379", "380", "381", "382", "383", "384", "385", "386", "387", "388", "389", "39", "40", "41", "420", "421", "422", "423", "424", "425", "426", "427", "428", "429", "43", "44", "45", "46", "47", "48", "49", "500", "501", "502", "503", "504", "505", "506", "507", "508", "509", "51", "52", "53", "54", "55", "56", "57", "58", "590", "591", "592", "593", "594", "595", "596", "597", "598", "599", "599", "60", "61", "62", "63", "64", "65", "66", "670", "671", "672", "673", "674", "675", "676", "677", "678", "679", "680", "681", "682", "683", "684", "685", "686", "687", "688", "689", "690", "691", "692", "693", "694", "695", "696", "697", "698", "699", "7", "800", "801", "802", "803", "804", "805", "806", "807", "808", "809", "81", "82", "830", "831", "832", "833", "834", "835", "836", "837", "838", "839", "84", "850", "851", "852", "853", "854", "855", "856", "857", "858", "859", "86", "870", "871", "872", "873", "874", "875", "876", "877", "878", "879", "880", "881", "882", "883", "884", "885", "886", "887", "888", "889", "890", "891", "892", "893", "894", "895", "896", "897", "898", "899", "90", "91", "92", "93", "94", "95", "960", "961", "962", "963", "964", "965", "966", "967", "968", "969", "970", "971", "972", "973", "974", "975", "976", "977", "978", "979", "98", "990", "991", "992", "993", "994", "995", "996", "997", "998", "999", nullptr };

static char charPool[] = {   // 64
        0x41, 0x42, 0x43, 0x44, 0x45, 0x46,
        0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c,
        0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x51,
        0x52, 0x53, 0x54, 0x55, 0x56, 0x57,
        0x58, 0x59, 0x5a, 0x61, 0x62, 0x63,
        0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
        0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f,
        0x70, 0x71, 0x71, 0x72, 0x73, 0x74,
        0x75, 0x76, 0x77, 0x78, 0x79, 0x7a,
        0x30, 0x31, 0x32, 0x33, 0x34, 0x35,
        0x36, 0x37, 0x38, 0x39
};

static struct AudioIoDualTone mediaLostTone[] =
{
    { 100, 100, 620, -12 * 256, 0, 0 },
    { 100, 100, 620, -12 * 256, 0, 0 },
};

SignalingMediaConfig::SignalingMediaConfig(
    ISocketProvider * udpSocketProvider,
    ISocketProvider * tcpSocketProvider,
    ISocketProvider * tlsSocketProvider,
    class ISocketContext * socketContext,
    word minPort,
    word maxPort,
    word minVideoPort,
    word maxVideoPort,
    const char * stunServers,
    const char * turnServers,
    const char * turnUsername,
    const char * turnPassword,
    enum MediaType media,
    bool stunSlow,
    bool turnOnly,
    bool iceNoHost,
    bool recordRtpStream,
    int dropMediaTx,
    int dropMediaRx,
    const char * hostPbx,
    bool noVpnAddresses,
    const char * currentUserSip,
    const char * recordingUrl,
    bool isRecordingByDefaultOn,
    bool recordExternalOnly,
    const char * recordingUsername,
    const char * recordingPassword,
    const char * excludeInterfaces,
    bool signalingTestMode)
{
    this->udpSocketProvider = udpSocketProvider;
    this->tcpSocketProvider = tcpSocketProvider;
    this->tlsSocketProvider = tlsSocketProvider;
    this->socketContext = socketContext;
    this->minPort = minPort;
    this->maxPort = maxPort;
    this->minVideoPort = minVideoPort;
    this->maxVideoPort = maxVideoPort;
    this->stunServers = _strdup(stunServers);
    this->turnServers = _strdup(turnServers);
    this->turnUsername = _strdup(turnUsername);
    this->turnPassword = _strdup(turnPassword);
    this->media = media;
    this->stunSlow = stunSlow;
    this->turnOnly = turnOnly;
    this->iceNoHost = iceNoHost;
    this->dropMediaTx = dropMediaTx;
    this->dropMediaRx = dropMediaRx;
    this->hostPbx = _strdup(hostPbx);
    this->noVpnAddresses = noVpnAddresses;
    this->currentUserSip = _strdup(currentUserSip);
    this->recordingUrl = _strdup(recordingUrl);
    this->isRecordingByDefaultOn = isRecordingByDefaultOn;
    this->recordExternalOnly = recordExternalOnly;
    this->recordingUsername = _strdup(recordingUsername);
    this->recordingPassword = _strdup(recordingPassword);
    this->recordRtpStream = recordRtpStream;
    this->excludeInterfaces = _strdup(excludeInterfaces);
    this->signalingTestMode = signalingTestMode;
}

/*-----------------------------------------------------------------------------------------------*/
/* class SignalingService                                                                        */
/*-----------------------------------------------------------------------------------------------*/

class ISignalingService * ISignalingService::CreateSignaling(class IIoMux * const iomux, class ISocketProvider * localSocketProvider, class ISocketProvider * tcpSocketProvider, class ISocketProvider * tlsSocketProvider, IWebserverPluginProvider * const webserverPluginProvider, IAppWebsocketClientProvider * appWebsocketClientProvider, IMediaProvider * mediaProvider, SignalingMediaConfig * mediaConfig, IAudioIo * audioIo, IRingerIo * ringerIo, IVideoIo * videoIo, IAudioIo* remoteAudioIo, IRingerIo* remoteRingerIo, IVideoIo* remoteVideoIo, IMediaProvider* remoteMediaProvider, ISignalingRemote* remoteSignaling, AppServiceArgs * args)
{
    return new SignalingService(iomux, localSocketProvider, tcpSocketProvider, tlsSocketProvider, webserverPluginProvider, appWebsocketClientProvider, mediaProvider, mediaConfig, audioIo, ringerIo, videoIo, remoteAudioIo, remoteRingerIo, remoteVideoIo, remoteMediaProvider, remoteSignaling, args);
}

SignalingService::SignalingService(class IIoMux * const iomux, class ISocketProvider * localSocketProvider, class ISocketProvider * tcpSocketProvider, class ISocketProvider * tlsSocketProvider, class IWebserverPluginProvider * const webserverPluginProvider, class IAppWebsocketClientProvider * appWebsocketClientProvider, IMediaProvider * mediaProvider, SignalingMediaConfig * mediaConfig, IAudioIo * audioIo, IRingerIo * ringerIo, IVideoIo * videoIo, IAudioIo* remoteAudioIo, IRingerIo* remoteRingerIo, IVideoIo* remoteVideoIo, IMediaProvider* remoteMediaProvider, ISignalingRemote* remoteSignaling, AppServiceArgs * args) :
        AppService(iomux, localSocketProvider, args)
{
    this->iomux = iomux;
    this->localSocketProvider = localSocketProvider;
    this->tcpSocketProvider = tcpSocketProvider;
    this->tlsSocketProvider = tlsSocketProvider;
    this->webserverPluginProvider = webserverPluginProvider;
    this->appWebsocketClientProvider = appWebsocketClientProvider;
    this->mediaProvider = mediaProvider;
    this->mediaConfig = mediaConfig;
    this->audioIo = audioIo;
    this->ringerIo = ringerIo;
    this->videoIo = videoIo;
    this->remoteAudioIo = remoteAudioIo;
    this->remoteRingerIo = remoteRingerIo;
    this->remoteVideoIo = remoteVideoIo;
    this->remoteMediaProvider = remoteMediaProvider;
    this->remoteSignaling = remoteSignaling;
}

SignalingService::~SignalingService()
{

}

void SignalingService::IAppStart(AppInstanceArgs * args)
{
    AppStart(args);
}

class AppInstance * SignalingService::CreateInstance(AppInstanceArgs * args)
{
    //debug->printf("%x:SignalingService::CreateInstance()", this );
	return new Signaling(iomux, 0, this, args);
}

class ISignaling * SignalingService::ICreateInstance(class USignaling * user, AppInstanceArgs * args)
{
    //debug->printf("%x:SignalingService::ICreateInstance()", this );
	return new Signaling(iomux, user, this, args);
}

void SignalingService::AppServiceApps(istd::list<AppServiceApp> * appList)
{

}

/*-----------------------------------------------------------------------------------------------*/
/* class Signaling                                                                               */
/*-----------------------------------------------------------------------------------------------*/

ISignaling::ISignaling(class USignaling * user, class AppService * appService, AppInstanceArgs * args) : AppInstance(appService, args)
{
}

Signaling::Signaling(IIoMux * const iomux, class USignaling * user, class SignalingService * service, AppInstanceArgs * args) : ISignaling(user, service, args), AppUpdates(iomux), SignalingRcc(this, this)
{
    this->Log("%x:Signaling::Signaling user is: %s", this, user ? "true" : "false ");
    this->user = user;
    this->service = service;
    this->log = this;
    if (args->webserver) {
        webserverPlugin = service->webserverPluginProvider->CreateWebserverPlugin(iomux, service->localSocketProvider, this, args->webserver, args->webserverPath, this);
        webserverPlugin->HttpListen(0, 0, 0, 0, _BUILD_STRING_);
        webserverPlugin->WebsocketListen();
        // keys for innovaphone.test.exe
        webserverPluginKeys = service->webserverPluginProvider->CreateWebserverPlugin(iomux, service->localSocketProvider, this, args->webserver, "/keys", this);
        webserverPluginKeys->HttpListen(0, 0, 0, 0, _BUILD_STRING_);
        webserverPluginKeys->WebsocketListen();
    }
    else webserverPlugin = NULL, webserverPluginKeys = NULL;
    pwd = _strdup(args->appPassword);
    stopping = false;
    client = 0;
    registration = 0;
    rtptp4 = 0;
    rtptp6 = 0;
    videoLicense = false;
    recordingLicense = false;
    startVideo = false;
    doNotStartCamera = false;
    callWaiting = CallWaitingOff;
    onhookTransfer = false;
    dndRejectCall = false;
    clir = false;
    uiRunning = 0;
    autoHangup = AUTOHANGUP_OFF;
    lastRkeyPress = 0;
    hookDeviceState = 0;
    defaultAudioDeviceId = 0;
    changedAudioDeviceId = 0;
    videoDeviceId = 0;
    ringerDeviceId = 0;
    ringtoneId = 0;
    testAudioDeviceId = 0;
    testRingerDeviceId = 0;
    testVideoDeviceId = 0;
    localIpAddr = 0;
    recordingUrlSet = false;
    lastStartCallDest = 0;

    if (service->videoIo) {
        service->videoIo->SetUSelectDeviceIoUser(this);
        service->videoIo->SubscribeApplications();
    }
    class IVideoIo* videoIo = GetVideoIo();
    if (videoIo) {
        videoIo->QueryDevices(NULL);
        videoIo->SubscribeApplications();
    }

    if (service->ringerIo) service->ringerIo->SetUSelectDeviceIoUser(this);
    class IRingerIo* ringerIo = GetRingerIo();
    if (ringerIo) {
        ringerIo->QueryDevices(NULL);
        ringerIo->QueryRingtones(NULL);
    }

    if (service->audioIo) {
        service->audioIo->SetUSelectDeviceIoUser(this);
        service->audioIo->AddUHookDeviceUser(this);
    }
    class IAudioIo* audioIo = GetAudioIo();
    if (audioIo) audioIo->QueryDevices(NULL);

    service->signaling = this;
    service->mediaConfig->recordingUrl = ILocalStorage::GetItem("recordingUrl");
    if (service->mediaConfig->recordingUrl && (strlen(service->mediaConfig->recordingUrl) <= 1)) {
        free(service->mediaConfig->recordingUrl);
        service->mediaConfig->recordingUrl = NULL;
    }
    this->workingPath = nullptr;
    this->url = NULL;
    this->sec = NULL;
    this->phys = NULL;
    this->app = NULL;
    this->usr = NULL;
    this->userPwd = NULL;
    this->domain = NULL;
    this->appdomain = NULL;
    this->localInit = false;
    this->Log("%x:App instance started %s", this, service->GetAppServiceId());
}

Signaling::~Signaling()
{
    this->log->Log(LOG_SIGNALING, "Signaling(%p)::~Signaling", this);

    while (!hookKeyQueue.empty()) {
        delete hookKeyQueue.pop_front();
    }

    if (service->mediaConfig->recordingUrl) {
        free(service->mediaConfig->recordingUrl);
        service->mediaConfig->recordingUrl = NULL;
    }
    if (webserverPlugin) delete webserverPlugin;
    if (webserverPluginKeys) delete webserverPluginKeys;
    free(pwd);
    if(defaultAudioDeviceId) free(defaultAudioDeviceId);
    if(videoDeviceId) free(videoDeviceId);
    if(ringerDeviceId) free(ringerDeviceId);
    if(ringtoneId) free(ringtoneId);
    if(testAudioDeviceId) free(testAudioDeviceId);
    if(testRingerDeviceId) free(testRingerDeviceId);
    if(testVideoDeviceId) free(testVideoDeviceId);
    if(localIpAddr) free(localIpAddr);
    if(lastStartCallDest) delete lastStartCallDest;
    if(this->url) free(this->url);
    if(this->sec) free(this->sec);
    if(this->phys) free(this->phys);
    if(this->app) free(this->app);
    if(this->usr) free(this->usr);
    if(this->userPwd) free(this->userPwd);
    if(this->domain) free(this->domain);
    if(this->appdomain) free(this->appdomain);
    if (this->workingPath) free(this->workingPath);
}

void Signaling::SetSignalingUser(class USignaling* user)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::SetSignalingUser user:%x", this, user);
    this->user = user;
}

void Signaling::RegisterToAltPbx()
{
    log->Log(LOG_SIGNALING, "%x:Signaling::RegisterToAltPbx", this);
    if(client) client->RegisterToAltPbx();
}

void Signaling::RegisterToRedirectUrl(const char* url)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::RegisterToRedirectUrl url=%s", this, url ? url : "");
    if (client) client->RegisterToRedirectUrl(url);
}

void Signaling::InnovaphoneData(class json_io & msg, word base)
{
    int cmd = msg.get_int(base, "cmd");
    switch (cmd) {
    case PBX_INNOVAPHONE_DATA_ATTACH:
        log->Log(LOG_SIGNALING, "%x:Signaling::InnovaphoneData(cmd=%u) Phone-App attached", this, cmd);
        videoLicenseBackup = videoLicense;
        videoLicense = false; // disable all video stuff (phone-app will do video)
        if (Sessions()) StartUpdate(new SignalingUpdate(SignalingRegistrationInfo, "PhoneAppAttached", 0, filters));
        break;
    case PBX_INNOVAPHONE_DATA_DETACH:
        log->Log(LOG_SIGNALING, "%x:Signaling::InnovaphoneData(cmd=%u) Phone-App detached", this, cmd);
        videoLicense = videoLicenseBackup;// allow video stuff if license
        if (Sessions()) StartUpdate(new SignalingUpdate(SignalingRegistrationInfo, "PhoneAppDetached", 0, filters));
        break;
    default:
        log->Log(LOG_SIGNALING, "%x:Signaling::InnovaphoneData(cmd=%u) unhandled", this, cmd);
    }
}

void Signaling::SetClient(const char * url, const char * sec, const char * phys, const char * app, const char * usr, const char * pwd, const char * domain, const char * appdomain, unsigned timeout, bool localInit)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::SetClient app: %s", this, app ? app : "");
    if (!client) client = new SignalingClient(this);
    client->Update(url, sec, phys, app, usr, pwd, domain, appdomain, timeout, localInit);
}

void Signaling::CloseClient()
{
    log->Log(LOG_SIGNALING, "%x:Signaling::CloseClient client=%p registration=%p", this, client, registration);
    if (client) {
        client->Close();
        if (registration) registration->Update(NULL, NULL, 0);
        RegistrationState(false);
    }
}

int Signaling::NumActiveCalls()
{
    if (registration && registration->calls) return registration->calls->get_count();
    return 0;
}

void Signaling::SetRegistration(const char * hw, const char * phys, unsigned timeout)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::SetRegistration hw: %s registration: %p phys: %s this->phys: %s", this, hw ? hw : "", registration, phys && phys[0] ? phys : "-", this->phys ? this->phys : "-");
    if (!registration) registration = new SignalingRegistration(this, this->phys);
    registration->Update(hw, phys, timeout);
    class IRingerIo* ringerIo = GetRingerIo();
    if (ringerIo && registration->GetHwId()) {
        if (user) user->SelectRingtone(ringerIo->GetRingtone(registration->GetHwId()));
    }
}

void Signaling::RegistrationCheck()
{
    if (client) client->RegistrationCheck();
}

void Signaling::LookupInfo(const char* src, class json_io& msg, word base)
{    
    class SignalingCall * call = FindCallByRemoteNum(src);

    if(call) call->LookupInfo(msg, base);
}

void Signaling::StartRinging(const char* uuid)
{
    class SignalingCall* call = FindCallByConfId(uuid);

    if (call) call->StartRinging();
}

void Signaling::SaveCredentials(const char* url, const char* sec, const char* phys, const char* app, const char* usr, const char* pwd, const char* domain, const char* appdomain, unsigned timeout)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::SaveCredentials user=%x", this, user);
    if (this->user) this->user->SaveCredentials(url, sec, phys, app, usr, pwd, domain, appdomain, timeout);
    if (this->url) free(this->url);
    this->url = _strdup(url);
    if (this->sec) free(this->sec);
    this->sec = _strdup(sec);
    if (this->phys) free(this->phys);
    this->phys = _strdup(phys);
    if (this->app) free(this->app);
    this->app = _strdup(app);
    if (this->usr) free(this->usr);
    this->usr = _strdup(usr);
    if (this->userPwd) free(this->userPwd);
    this->userPwd = _strdup(pwd);
    if (this->domain) free(this->domain);
    this->domain = _strdup(domain);
    if (this->appdomain) free(this->appdomain);
    this->appdomain = _strdup(appdomain);
}

void Signaling::SaveRegistration(const char* hw, const char* phys, unsigned timeout)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::SaveRegistration user=%x", this, user);
    if (this->user) this->user->SaveRegistration(hw, phys, timeout);
}

void Signaling::KeysSessionClosed(class KeysSession * keysSession)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::KeysSessionClosed keysSession=%x", this, keysSession);
    delete keysSession;
}

void Signaling::RkeyHandling(char key)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::RkeyHandling R-%c registration=%p count=%u", this, key, registration, registration && registration->calls ? registration->calls->get_count() : 0);
    
    if (!registration) return;

    unsigned count = registration->calls ? registration->calls->get_count() : 0;
    if (count != 2) return;
    
    class SignalingCall * activeCall = registration->activePhoneCall;
    if (!activeCall) return;
    word state = activeCall->state;
    
    class SignalingCall * otherCall = FindOtherCall(activeCall);
    if (!otherCall) return;
    
    log->Log(LOG_SIGNALING, "%x:Signaling::RkeyHandling state=%x otherCall->state=%x", this, state, otherCall->state);

    if (key == '0') {
        // R-0: reject knocking call
        if (otherCall->state == (SIG_CALL_STATE_CALL_WAITING | SIG_CALL_STATE_OUTG | SIG_CALL_STATE_ALERT)) {
            otherCall->ClearCall();
        }
    }
    else if (key == '1') {
        // R-1: disconnect active call and retrieve call on hold
        if ((state & 0xF) == SIG_CALL_STATE_CONN && !(state & SIG_CALL_STATE_HOLD)) {
            if (otherCall->state & SIG_CALL_STATE_HOLD) {
                activeCall->ClearCall();
                otherCall->Retrieve();
            }
        }
    }
    else if (key == '2' || key == 'R') {
        // R-2: put active call on hold and activate held/waiting call
        if ((state & 0xF) == SIG_CALL_STATE_CONN && !(state & SIG_CALL_STATE_HOLD)) {
            if (otherCall->state & SIG_CALL_STATE_HOLD) {
                activeCall->Hold();
                otherCall->Retrieve();
            }
            if (otherCall->state == (SIG_CALL_STATE_CALL_WAITING | SIG_CALL_STATE_OUTG | SIG_CALL_STATE_ALERT)) {
                activeCall->Hold();
                otherCall->ConnectCall(GetAudioIo(), GetVideoIo());
            }
        }
    }
    else if (key == '3') {
        // R-3: toggle conference
        if ((state & 0xF) == SIG_CALL_STATE_CONN) {
            if (otherCall->state & SIG_CALL_STATE_HOLD) otherCall->Retrieve();
            else otherCall->Hold();
        }
    }
    else if (key == '4') {
        // R-4: initiate call transfer
        if ((state & 0xF) == SIG_CALL_STATE_CONN) {
            if ((otherCall->state & 0xF) == SIG_CALL_STATE_CONN) {
                activeCall->StartTransfer(otherCall);
            }
        }
    }
}

void Signaling::IncomingCall()
{
    log->Log(LOG_SIGNALING, "%x:Signaling::IncomingCall close test devices audio=%s, ringer=%s, video=%s", this, testAudioDeviceId, testRingerDeviceId, testVideoDeviceId);

    class IAudioIo* audioIo = GetAudioIo();
    if (audioIo && testAudioDeviceId) {
        audioIo->StopDevice((void*)this, testAudioDeviceId);
        audioIo->LoopTest(false);
        audioIo->StopDualTones();
        free(testAudioDeviceId);
        testAudioDeviceId = NULL;
    }
    class IRingerIo* ringerIo = GetRingerIo();
    if (ringerIo && testRingerDeviceId) {
        ringerIo->RingtoneStop(testRingerDeviceId);
        free(testRingerDeviceId);
        testRingerDeviceId = NULL;
    }
    class IVideoIo* videoIo = GetVideoIo();
    if (videoIo && testVideoDeviceId) {
        videoIo->StopDevice((void*)this, testVideoDeviceId);
        free(testVideoDeviceId);
        testVideoDeviceId = NULL;
    }
}

void Signaling::VideoDeviceList()
{
    if (registration && registration->registered) {
        if (videoDeviceId) {
            if (service->videoIo->GetDeviceIo(registration->GetHwId()) && !strcmp(videoDeviceId, service->videoIo->GetDeviceIo(registration->GetHwId()))) return;
            free(videoDeviceId);
        }
        videoDeviceId = _strdup(service->videoIo->GetDeviceIo(registration->GetHwId()));
        log->Log(LOG_SIGNALING, "%x:Signaling::VideoDeviceList hw=%s videoDeviceId=%s", this, registration->GetHwId() ? registration->GetHwId() : "-", videoDeviceId ? videoDeviceId : "-");
    }
}

void Signaling::SelectVideoDevice(const char* app, const char* deviceId)
{
    log->Log(LOG_SIGNALING, "Signaling::SelectVideoDevice app=%s deviceId=%s", app ? app : "-", deviceId ? deviceId : "-");
    if (registration && registration->GetHwId() && app && !strcmp(registration->GetHwId(), app) && deviceId && (videoDeviceId == NULL || strcmp(deviceId, videoDeviceId))) {
        if (videoDeviceId) free(videoDeviceId);
        videoDeviceId = _strdup(deviceId);
    }
}

void Signaling::ChangeVideoDevice(const char* app, const char* deviceId)
{
    log->Log(LOG_SIGNALING, "Signaling::ChangeVideoDevice app=%s deviceId=%s", app ? app : "-", deviceId ? deviceId : "-");
    if (registration && registration->calls && registration->calls->get_count() > 0) {
        SignalingCall* call = nullptr;
        while ((call = (class SignalingCall*)registration->calls->btree_find_next_right(call ? (void*)(intp)call->call : 0))) {
            call->ChangeVideoDevice(deviceId);
        }
    }
}

void Signaling::VideoDeviceRemoved(const char* deviceId)
{
    log->Log(LOG_SIGNALING, "Signaling::VideoDeviceRemoved deviceId=%s defaultAudioDeviceId=%s", deviceId ? deviceId : "-", videoDeviceId ? videoDeviceId : "-");

    if (deviceId && videoDeviceId && !strcmp(deviceId, videoDeviceId)) {
        free(videoDeviceId);
        videoDeviceId = NULL;
    }
}

void Signaling::RingtoneList()
{
    if (registration) {
        if (ringtoneId) {
            if (service->ringerIo->GetRingtone(registration->GetHwId()) && !strcmp(ringtoneId, service->ringerIo->GetRingtone(registration->GetHwId()))) return;
            free(ringtoneId);
        }
        ringtoneId = _strdup(service->ringerIo->GetRingtone(registration->GetHwId()));
        log->Log(LOG_SIGNALING, "%x:Signaling::RingerDeviceList hw=%s ringtoneId=%s", this, registration->GetHwId() ? registration->GetHwId() : "-", ringtoneId ? ringtoneId : "-");
    }
}

void Signaling::RingerDeviceList()
{
    if (service->ringerIo && registration && registration->registered) {
        if (ringerDeviceId) {
            if (service->ringerIo->GetDeviceIo(registration->GetHwId()) && !strcmp(ringerDeviceId, service->ringerIo->GetDeviceIo(registration->GetHwId()))) return;
            free(ringerDeviceId);
        }
        ringerDeviceId = _strdup(service->ringerIo->GetDeviceIo(registration->GetHwId()));
        log->Log(LOG_SIGNALING, "%x:Signaling::RingerDeviceList hw=%s ringerDeviceId=%s", this, registration->GetHwId() ? registration->GetHwId() : "-", ringerDeviceId ? ringerDeviceId : "-");
    }
}

void Signaling::SelectRingerDevice(class IRingerIo* ringerIo, const char* app, const char* deviceId)
{
    log->Log(LOG_SIGNALING, "Signaling::SelectRingerDevice app=%s deviceId=%s audioDeviceId=%s", app ? app : "-", deviceId ? deviceId : "-", ringerDeviceId ? ringerDeviceId : "-");
    if (registration && registration->GetHwId() && app && !strcmp(registration->GetHwId(), app) && deviceId && (ringerDeviceId == NULL || strcmp(deviceId, ringerDeviceId))) {
        if (ringerDeviceId) free(ringerDeviceId);
        ringerDeviceId = _strdup(deviceId);
    }
}

void Signaling::RingerDeviceRemoved(const char* deviceId)
{
    log->Log(LOG_SIGNALING, "Signaling::RingerDeviceRemoved deviceId=%s defaultAudioDeviceId=%s", deviceId ? deviceId : "-", ringerDeviceId ? ringerDeviceId : "-");

    if (deviceId && ringerDeviceId && !strcmp(deviceId, ringerDeviceId)) {
        free(ringerDeviceId);
        ringerDeviceId = NULL;
    }
}

void Signaling::SelectRingtone(class IRingerIo* ringerIo, const char* app, const char* ringtoneId)
{
    log->Log(LOG_SIGNALING, "Signaling::SelectRingtone app=%s ringtoneId=%s", app ? app : "-", ringtoneId ? ringtoneId : "-");
    if (registration && registration->GetHwId() && app && !strcmp(registration->GetHwId(), app)) {
        if (user) user->SelectRingtone(ringerIo->GetRingtone(registration->GetHwId()));
    }
}

void Signaling::AudioDeviceList()
{
    if (registration && registration->registered && !registration->calls) {
        class IAudioIo* a = GetAudioIo();

        const char* adev = service->audioIo->GetDeviceIo(registration->GetHwId());
        if (defaultAudioDeviceId) {
            if (adev && !strcmp(defaultAudioDeviceId, adev)) return;
            free(defaultAudioDeviceId);
        }
        defaultAudioDeviceId = _strdup(adev);
        log->Log(LOG_SIGNALING, "%x:Signaling::AudioDeviceList hw=%s audioDeviceId=%s", this, registration->GetHwId() ? registration->GetHwId() : "-", defaultAudioDeviceId ? defaultAudioDeviceId : "-");
        if (defaultAudioDeviceId && (a->DeviceCapabilities(defaultAudioDeviceId) & AUDIO_CAPABILITY_HOOK_DEVICE)) {
            a->StartHookDevice(defaultAudioDeviceId);
            a->SendHookKey(defaultAudioDeviceId, KEY_ONHOOK, 0, 0, 0);
        }
    }
}

void Signaling::SelectAudioDevice(class IAudioIo * audioIo, const char * app, const char * deviceId, int type)
{
    class IAudioIo* a = GetAudioIo();

    log->Log(LOG_SIGNALING, "Signaling::SelectAudioDevice reg=%s app=%s deviceId=%s type=%d audioDeviceId=%s type=%d capabilities=%x", registration ? "ok" : "nok", app ? app : "-", deviceId ? deviceId : "-", type, defaultAudioDeviceId ? defaultAudioDeviceId : "-", defaultAudioDeviceId ? a->GetDeviceIoType(defaultAudioDeviceId) : 0, defaultAudioDeviceId ? a->DeviceCapabilities(defaultAudioDeviceId) : 0);
    if (registration && registration->GetHwId() && app && !strcmp(registration->GetHwId(), app) && deviceId && (defaultAudioDeviceId == NULL || strcmp(deviceId, defaultAudioDeviceId))) {

        if (defaultAudioDeviceId) {
            if(a->DeviceCapabilities(defaultAudioDeviceId) & AUDIO_CAPABILITY_HOOK_DEVICE) a->StopHookDevice(defaultAudioDeviceId);
            free(defaultAudioDeviceId);
        }
        defaultAudioDeviceId = _strdup(deviceId);
        if (a->DeviceCapabilities(deviceId) & AUDIO_CAPABILITY_HOOK_DEVICE) {
            a->StartHookDevice(deviceId);
            a->SendHookKey(deviceId, KEY_ONHOOK, 0, 0, 0);
        }
    }
}

void Signaling::ChangeAudioDevice(class IAudioIo * audioIo, const char * app, const char * deviceId, int type)
{
    log->Log(LOG_SIGNALING, "Signaling::ChangeAudioDevice reg=%s app=%s deviceId=%s audioDeviceId=%s type=%d changedAudioDeviceId=%s", registration ? "ok" : "nok", app ? app : "-", deviceId ? deviceId : "-", defaultAudioDeviceId ? defaultAudioDeviceId : "-", type, changedAudioDeviceId ? changedAudioDeviceId : "");
    const char * checkDeviceId = changedAudioDeviceId ? changedAudioDeviceId : defaultAudioDeviceId;
    if (deviceId && checkDeviceId && strcmp(deviceId, checkDeviceId) && registration && registration->calls && registration->calls->get_count() > 0) {
        audioIo->StartHookDevice(deviceId);
        SignalingCall* call = nullptr;
        while ((call = (class SignalingCall*)registration->calls->btree_find_next_right(call ? (void*)(intp)call->call : 0))) {
            call->ChangeAudioDevice(deviceId);
        }
        if (changedAudioDeviceId) free(changedAudioDeviceId);
        changedAudioDeviceId = _strdup(deviceId);
    }
}

void Signaling::AudioDeviceRemoved(const char* deviceId)
{
    log->Log(LOG_SIGNALING, "Signaling::AudioDeviceRemoved deviceId=%s defaultAudioDeviceId=%s", deviceId ? deviceId : "-", defaultAudioDeviceId ? defaultAudioDeviceId : "-");

    if (deviceId && defaultAudioDeviceId && !strcmp(deviceId, defaultAudioDeviceId)) {
        free(defaultAudioDeviceId);
        defaultAudioDeviceId = NULL;
    }
}

void Signaling::StartHookDevice(const char* deviceId, byte key, byte callId, const char* number, const char* name)
{
    class IAudioIo* audioIo = GetAudioIo();

    log->Log(LOG_SIGNALING, "Signaling::StartHookDevice deviceId=%s", deviceId ? deviceId : "-");

    audioIo->StartHookDevice(deviceId);
    audioIo->SendHookKey(deviceId, key, callId, number, name);
}

void Signaling::SendHookKey(const char* deviceId, byte key, byte callId, const char* number, const char* name)
{
    class IAudioIo* a = GetAudioIo();
    if (a->DeviceCapabilities(deviceId) & AUDIO_CAPABILITY_HOOK_DEVICE) a->SendHookKey(deviceId, key, callId, number, name);
}

void Signaling::SetExcludeInterfaces(const char * excludeInf)
{
    if(excludeInf) debug->printf("Signaling(%p)::SetExcludeInterfaces excludeInf=%s", this, excludeInf);

    if (service->mediaConfig->excludeInterfaces) free(service->mediaConfig->excludeInterfaces);
    service->mediaConfig->excludeInterfaces = _strdup(excludeInf);
}

void Signaling::SetNoVpnAddresses(bool noVpnAddresses)
{
    if(service->mediaConfig->noVpnAddresses != noVpnAddresses) log->Log(LOG_SIGNALING, "%x:Signaling::SetNoVpnAddresses noVpnAddresses: %d", this, noVpnAddresses);
    service->mediaConfig->noVpnAddresses = noVpnAddresses;
}

void Signaling::SetCurrentUserSip(const char * sip)
{
    if(service->mediaConfig->currentUserSip) free(service->mediaConfig->currentUserSip);
    service->mediaConfig->currentUserSip = _strdup(sip);
}

void Signaling::SetRecordingParameters(const char * url, bool pbx, bool forceUrl, bool isRecordingByDefaultOn, bool forceDefaultOn, bool recordExternalOnly, bool forceExternalOnly)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::SetRecordingParameters url: %s pbx: %d force: %d recordingUrlSet: %d isRecordingByDefaultOn: %d forceDefaultOn: %d recordExternalOnly: %d forceExternalOnly: %d"
             , this, url ? url : "-", pbx, forceUrl, recordingUrlSet, isRecordingByDefaultOn, forceDefaultOn, recordExternalOnly, forceExternalOnly);
    // force == true means, url comes from the PBX and it will store in LocalStorage outside signaling
    if (forceUrl) {
        if (service->mediaConfig->recordingUrl) free(service->mediaConfig->recordingUrl);
        service->mediaConfig->recordingUrl = _strdup(url);
    }
    if (forceDefaultOn) service->mediaConfig->isRecordingByDefaultOn = isRecordingByDefaultOn;
    if (forceExternalOnly) service->mediaConfig->recordExternalOnly = recordExternalOnly;
    // PBX setting, force false. Apply if Url not set over Softphone2 Configuration
    // Also stored outside signaling
    else if (pbx) {
        if (service->mediaConfig->recordingUrl == nullptr && url) service->mediaConfig->recordingUrl = _strdup(url);

        const char * isRecordingByDefaultOnString = ILocalStorage::GetItem("isRecordingByDefaultOn");
        if(!isRecordingByDefaultOnString) service->mediaConfig->isRecordingByDefaultOn = isRecordingByDefaultOn;
        else if(!strcmp(isRecordingByDefaultOnString, "true")) service->mediaConfig->isRecordingByDefaultOn = true;
        else service->mediaConfig->isRecordingByDefaultOn = false;

        const char * recordExternalOnlyString = ILocalStorage::GetItem("recordExternalOnly");
        if(!recordExternalOnlyString) service->mediaConfig->recordExternalOnly = recordExternalOnly;
        else if(!strcmp(recordExternalOnlyString, "true")) service->mediaConfig->recordExternalOnly = true;
        else service->mediaConfig->recordExternalOnly = false;
    }
    // Softphone2 Config, not possible if pbx-force is set
    // Apply even the PBX setting is set without force
    // url may be null if Softphone2 config value was removed
    else if (url || recordingUrlSet) {
        if (service->mediaConfig->recordingUrl) free(service->mediaConfig->recordingUrl);
        service->mediaConfig->recordingUrl = _strdup(url);
        service->mediaConfig->isRecordingByDefaultOn = isRecordingByDefaultOn;
        service->mediaConfig->recordExternalOnly = recordExternalOnly;
        recordingUrlSet = url != nullptr;
        ILocalStorage::SetItem("recordingUrl", url ? url : "");
        ILocalStorage::SetItem("isRecordingByDefaultOn", isRecordingByDefaultOn ? "true" : "false");
        ILocalStorage::SetItem("recordExternalOnly", recordExternalOnly ? "true" : "false");
    }
    else if (url == nullptr) ILocalStorage::SetItem("recordingUrl", "");
}

void Signaling::TestCloseRegistration()
{
    if (client) client->TestCloseRegistration();
}

void Signaling::HookDeviceMsg(IAudioIo* audioIo, const char* deviceId, const char* msg)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::HookDeviceMsg deviceId: %s msg: %s", this, deviceId, msg);
    if (registration && msg) {
        if (!strcmp(msg, "HookDeviceAcquired")) {
            hookDeviceState = HOOK_DEVICE_ACQUIRE;
            SignalingCall* call = nullptr;
            while ((call = (class SignalingCall*)registration->calls->btree_find_next_right(call ? (void*)(intp)call->call : 0))) {
                call->HookDeviceAcquired();
            }
        }
        else if (!strcmp(msg, "HookDeviceDenied")) {
            hookDeviceState = HOOK_DEVICE_DENIED;
            SignalingCall* call = nullptr;
            while ((call = (class SignalingCall*)registration->calls->btree_find_next_right(call ? (void*)(intp)call->call : 0))) {
                call->HookDeviceDenied();
            }
        }
        else if (!strcmp(msg, "HookDeviceNotFound")) {
            hookDeviceState = HOOK_DEVICE_NOT_FOUND;
            SignalingCall* call = nullptr;
            while ((call = (class SignalingCall*)registration->calls->btree_find_next_right(call ? (void*)(intp)call->call : 0))) {
                call->HookDeviceNotFound();
            }
        }
    }
}

void Signaling::AudioDeviceDelayed(IAudioIo* audioIo, const char* deviceIn, const char* deviceOut)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::AudioDeviceDelayed deviceId: %s %s", this, deviceIn ? deviceIn : "", deviceOut ? deviceOut : "");

    if (!sessions.empty()) {
        class SignalingMessage offhook("AudioDeviceDelayed", 0, alloca(2000), alloca(1000));
        offhook.add_string(offhook.base, "deviceIn", deviceIn);
        offhook.add_string(offhook.base, "deviceOut", deviceOut);
        class SignalingSessionHelper* helper = sessions.front();
        while (helper) {
            offhook.Send(helper->session);
            helper = helper->goNext();
        }
    }
}

void Signaling::AudioDeviceOpened(IAudioIo* audioIo, const char* deviceIn, const char* deviceOut)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::AudioDeviceOpened deviceId: %s %s", this, deviceIn ? deviceIn : "", deviceOut ? deviceOut : "");

    if (!sessions.empty()) {
        class SignalingMessage offhook("AudioDeviceOpened", 0, alloca(2000), alloca(1000));
        offhook.add_string(offhook.base, "deviceIn", deviceIn);
        offhook.add_string(offhook.base, "deviceOut", deviceOut);
        class SignalingSessionHelper* helper = sessions.front();
        while (helper) {
            offhook.Send(helper->session);
            helper = helper->goNext();
        }
    }
}

void Signaling::SendHookKeyReceived(const char * device, const char * key)
{
    if (!sessions.empty()) {
        class SignalingMessage msg("HookKeyReceived", 0, alloca(1000), alloca(100));
        msg.add_string(msg.base, "device", device);
        msg.add_string(msg.base, "key", key);
        class SignalingSessionHelper * helper = sessions.front();
        while (helper) {
            msg.Send(helper->session);
            helper = helper->goNext();
        }
    }
    else {
        hookKeyQueue.push_back(new SignalingHookKey(device, key));
        if (user) user->StartSoftphoneApp();
    }
}

void Signaling::HookKeyReceived(IAudioIo* audioIo, const char* deviceId, byte key)
{
    class SignalingCall * activeCall = registration ? registration->activePhoneCall : nullptr;
    if (!deviceId) {
        deviceId = activeCall ? activeCall->getAudioDeviceId() : defaultAudioDeviceId;
    }

    log->Log(LOG_SIGNALING, "%x:Signaling::HookKeyReceived deviceId:\"%s\" key:0x%x activeCall:%d activeCallState:%x Sessions():%d callWating:%d onhookTransfer:%d", this, deviceId, key, registration && activeCall ? 1 : 0, registration && activeCall ? activeCall->state : 0, !sessions.empty(), callWaiting, onhookTransfer);
    
    time_t now = time(0);
    if (lastRkeyPress) {
        // forget R key press if more than 3 seconds ago
        if ((now - lastRkeyPress) > 3) lastRkeyPress = 0;
    }

    switch(key) {
        case KEY_EHS_TALK:
            if (registration) {
                if (!activeCall) {
                    SendHookKeyReceived(deviceId, "OFFHOOK");
                }
                else if ((activeCall->state & 0x8F) == (SIG_CALL_STATE_ALERT | SIG_CALL_STATE_OUTG)) {
                    // accept inbound call
                    if (registration->signaling->videoLicense) {
                        activeCall->videoAllowed = registration->signaling->startVideo;
                    }
                    if (registration->signaling->user) registration->signaling->user->BringWindowToForeground();
                    activeCall->ConnectCall(GetAudioIo(), GetVideoIo());
                }
                else {
                    SignalingCall* queuedCall = nullptr;
                    SignalingCall* call = nullptr;
                    while ((call = (class SignalingCall*)registration->calls->btree_find_next_right(call ? (void*)(intp)call->call : 0))) {
                        log->Log(LOG_SIGNALING, "%x:Signaling::HookKeyReceived KEY_EHS_TALK call:%d callState:%x", this, call->call, call->state);
                        if (call == activeCall) continue;
                        else if (call->state & SIG_CALL_STATE_CALL_WAITING) {
                            queuedCall = call;
                        }
                    }
                    if (queuedCall) {
                        if (activeCall && (activeCall != queuedCall)) {
                            if (activeCall->state & SIG_CALL_STATE_HOLD) {} // already on HOLD
                            else if (activeCall->state & SIG_CALL_STATE_HELD) {} // already on HOLD
                            else if ((activeCall->state & 0xF) == SIG_CALL_STATE_CONN) activeCall->Hold(); // put this on HOLD
                            else activeCall->ClearCall(); // abort this call
                        }
                        if (registration->signaling->user) registration->signaling->user->BringWindowToForeground();
                        queuedCall->ConnectCall(GetAudioIo(), GetVideoIo());
                    }
                    else {
                        activeCall->ClearCall();
                    }
                }
            }
            break;
        case KEY_OFFHOOK:
        case KEY_LOUD:
        case KEY_HEADSET:
            if (registration) {
                if (!activeCall) {
                    SendHookKeyReceived(deviceId, "OFFHOOK");
                }
                else if ((activeCall->state & 0x8F) == (SIG_CALL_STATE_ALERT | SIG_CALL_STATE_OUTG)) {
                    // accept inbound call
                    if (registration->signaling->videoLicense) {
                        activeCall->videoAllowed = registration->signaling->startVideo;
                    }
                    if (registration->signaling->user) registration->signaling->user->BringWindowToForeground();
#ifdef IP270
                    if (registration->signaling->changedAudioDeviceId) free(registration->signaling->changedAudioDeviceId);
                    registration->signaling->changedAudioDeviceId = _strdup(deviceId);
#endif
                    activeCall->ConnectCall(GetAudioIo(), GetVideoIo());
                }
                else if (((activeCall->state & 0xF) == SIG_CALL_STATE_CONN || (activeCall->state & 0xF) == SIG_CALL_STATE_ALERT) &&
                    activeCall->getAudioDeviceId() &&
                    (strcmp(deviceId, activeCall->getAudioDeviceId()) != 0) &&
                    !strstr(activeCall->getAudioDeviceId(), deviceId) &&
                    !strstr(deviceId, activeCall->getAudioDeviceId())) {
                    activeCall->ChangeAudioDevice(deviceId);
                }
                else {
                    SignalingCall* call = nullptr;
                    while ((call = (class SignalingCall*)registration->calls->btree_find_next_right(call ? (void*)(intp)call->call : 0))) {
                        log->Log(LOG_SIGNALING, "%x:Signaling::HookKeyReceived KEY_OFFHOOK call:%d callState:%x", this, call->call, call->state);
                        if (call == activeCall) continue;
                        else if (call->state & SIG_CALL_STATE_CALL_WAITING) {
                            if (activeCall) {
                                if (activeCall->state & SIG_CALL_STATE_HOLD) {} // already on HOLD
                                else if (activeCall->state & SIG_CALL_STATE_HELD) {} // already on HOLD
                                else if ((activeCall->state & 0xF) == SIG_CALL_STATE_CONN) activeCall->Hold(); // put this on HOLD
                                else activeCall->ClearCall(); // abort this call
                            }
                            if (registration->signaling->user) registration->signaling->user->BringWindowToForeground();
                            call->ConnectCall(GetAudioIo(), GetVideoIo());
                            break;
                        }
                    }
                    if (!call) {
                        if (((activeCall->state & 0xF) == SIG_CALL_STATE_CONN) && ((key == KEY_LOUD) || (key == KEY_HEADSET))) {
                            bool initiateTransfer = false;
                            class SignalingCall * heldCall = nullptr;
                            if (onhookTransfer) {
                                SignalingCall* call = nullptr;
                                while ((call = (class SignalingCall*)registration->calls->btree_find_next_right(call ? (void*)(intp)call->call : 0))) {
                                    if (call == activeCall) continue;
                                    else if ((call->state & SIG_CALL_STATE_HOLD) && !heldCall) {
                                        heldCall = call;
                                        initiateTransfer = true;
                                    }
                                    else initiateTransfer = false;
                                }
                            }
                            if (initiateTransfer && heldCall) {

                                //TEST if activeCall is in Ringback

                                activeCall->StartTransfer(heldCall);
                            }
                            else {
                                activeCall->ClearCall();
                            }
                        } else if(key != KEY_OFFHOOK) {
                            activeCall->ClearCall();
                        }
                    }
                }
            }
            break;
        case KEY_ONHOOK:
            if (registration) {
                if (activeCall) {
                    bool initiateTransfer = false;
                    class SignalingCall* heldCall = nullptr;
                    if (onhookTransfer && activeCall->state != 0) {
                        SignalingCall* call = nullptr;
                        while ((call = (class SignalingCall*)registration->calls->btree_find_next_right(call ? (void*)(intp)call->call : 0))) {
                            if (call == activeCall) continue;
                            else if ((call->state & SIG_CALL_STATE_HOLD) && !heldCall) {
                                heldCall = call;
                                initiateTransfer = true;
                            }
                            else initiateTransfer = false;
                        }
                    }
                    if (initiateTransfer && heldCall) {

                        //TEST if activeCall is in Ringback

                        activeCall->StartTransfer(heldCall);
                    }
#ifdef IP270
                    else if (!strcmp(activeCall->getAudioDeviceId(), deviceId))
#else
                    else
#endif
                    {
                        activeCall->ClearCall();
                    }
                }
                else if(registration->calls && registration->calls->get_count() == 1) {  // No active phone call but call is held but remote peer
                    SignalingCall* call = nullptr;
                    while ((call = (class SignalingCall*)registration->calls->btree_find_next_right(call ? (void*)(intp)call->call : 0))) {
                        if (call->state & SIG_CALL_STATE_HELD) {
                            call->ClearCall();
                            break;
                        }
                    }
                }
            }
            break;
        case KEY_MIC:
            if (activeCall) {
                activeCall->muted = !activeCall->muted;
                class IAudioIo* audioIo = registration->signaling->GetAudioIo();
                if (audioIo) audioIo->Mute(activeCall->muted, false);
                activeCall->Mute();
            }
            else if (registration) {
#ifdef IP270
                registration->toggleDnd();
#endif
            }
            break;
        case KEY_DISC:
            if (registration && activeCall) {
                activeCall->ClearCall();
            }
            break;
        case KEY_SIG:
            log->Log(LOG_SIGNALING, "%x:Signaling::HookKeyReceived KEY_SIG", this);
            if (activeCall) {
                unsigned count = registration->calls->get_count();
                word state = activeCall->state;
                log->Log(LOG_SIGNALING, "%x:Signaling::HookKeyReceived state=%x count=%u", this, state, count);
                if (count == 1) {
                    if ((state & 0xF) == SIG_CALL_STATE_CONN) {
                        if (state & SIG_CALL_STATE_HOLD) activeCall->Retrieve();
                        else {
                            // put active call on hold and open a new call for overlap dialing
                            activeCall->Hold();
                            class SignalingCall * call = new SignalingCall(registration, CallTypeVoice, 0, 0, 0, 0, false);
                            call->videoAllowed = activeCall->videoAllowed;
                            class IAudioIo * audioIo = GetAudioIo();
                            class IVideoIo * videoIo = GetVideoIo();
                            if (audioIo && call) call->StartCall(audioIo, videoIo, 0, 0);
                        }
                    }
                }
                else if (count == 2) {
                    class SignalingCall * otherCall = FindOtherCall(activeCall);
                    if (lastRkeyPress) {
                        lastRkeyPress = 0; // R-R
                        RkeyHandling('R');
                    }
                    else if ((state & 0xF) < SIG_CALL_STATE_CONN && (!(state & SIG_CALL_STATE_OUTG))) {
                        // cancel new outbound overlap dialing call and auto-retrieve held call
                        activeCall->ClearCall();
                        if (otherCall->state & SIG_CALL_STATE_HOLD) otherCall->Retrieve();
                    }
                    else lastRkeyPress = now; // wait for next key press (0|1|2|3|4|R)
                }
                else {
                }
            }
            else {
                SendHookKeyReceived(deviceId, "RKEY");
            }
            break;
        case KEY_REDIAL:
            log->Log(LOG_SIGNALING, "%x:Signaling::HookKeyReceived KEY_REDIAL", this);
            if (!activeCall) {
                SendHookKeyReceived(deviceId, "REDIAL");
            }
            break;
        case KEY_BS:
            log->Log(LOG_SIGNALING, "%x:Signaling::HookKeyReceived KEY_BS", this);
            if (!activeCall) {
                SendHookKeyReceived(deviceId, "BACKSPACE");
            }
            break;
        case KEY_EHS_REDIAL:
            if (registration && lastStartCallDest) {
                class SignalingCall * call = new SignalingCall(registration, CallTypeVoice, 0, lastStartCallDest->num, lastStartCallDest->sip, lastStartCallDest->dn, true);
                if (registration->signaling->videoLicense) {
                    call->videoAllowed = registration->signaling->startVideo;
                }
                class IAudioIo * audioIo = GetAudioIo();
                class IVideoIo * videoIo = GetVideoIo();
                if (audioIo && call) call->StartCall(audioIo, videoIo, 0, 0);
                else debug->printf("%x:Signaling::EHS_REDIAL Oooops There is either no audioIo or no call object ", this);
            }
            break;
        case KEY_EHS_FLASH:
            if (registration && activeCall) {
                // toggle between active call and held call
                SignalingCall* heldCall = nullptr;
                SignalingCall* call = nullptr;
                while ((call = (class SignalingCall*)registration->calls->btree_find_next_right(call ? (void*)(intp)call->call : 0))) {
                    if (call == activeCall) continue;
                    else if ((call->state & SIG_CALL_STATE_HOLD) && !heldCall) {
                        heldCall = call;
                    }
                }
                if (!(activeCall->state & SIG_CALL_STATE_HOLD)) {
                    activeCall->Hold();
                    if (heldCall) {
                        heldCall->Retrieve();
                    }
                }
                else if (activeCall->state & SIG_CALL_STATE_HOLD) {
                    activeCall->Retrieve();
                }
            }
            break;
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
        case '*':
        case '#':
            if (lastRkeyPress) {
                lastRkeyPress = 0;
                RkeyHandling(key); // R-x
                return;
            }
            if (registration) {
                char digit[2];
                digit[0] = (char)key;
                digit[1] = '\0';
                if (!activeCall) {
                    SendHookKeyReceived(deviceId, digit);
                }
                else if (activeCall->state == SIG_CALL_STATE_SETUP_ACK) {
                    activeCall->Info(digit, nullptr);
                }
                else if ((activeCall->state & 0xF) == SIG_CALL_STATE_CONN) {
                    activeCall->SendDTMF(digit, true);
                }
            }
            break;
        case KEY_DEL:
            if (registration) {
                if (!activeCall) {
                    SendHookKeyReceived(deviceId, "DEL");
                }
            }
            break;
    }
}

void Signaling::AudioRouteChanged(const char * deviceId)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::AudioRouteChanged new deviceId: %s ", this, deviceId);

    class IAudioIo* audioIo = GetAudioIo();
    if (audioIo && registration && registration->GetHwId() && deviceId) audioIo->ChangeDevice(registration->GetHwId(), deviceId);

    if(registration && registration->activePhoneCall){
        registration->activePhoneCall->AudioRouteChanged();
    } else {
        debug->printf("%x:Signaling::AudioRouteChanged no active phone call", this);
    }
}

static unsigned NormalizeE164(const char *number, char * buffer, unsigned length)
{
    const char * n;
    unsigned i = 0;

    if (number) {
        n = number;;
        while (*n != '\0') {
            if ((*n >= 0) && (*n != ' ') && (*n != '(') && (*n != ')') && (*n != '-') && (*n != '/') && (*n != '.') && (*n != ':') && (*n != ';') && (*n != '\r') && (*n != '\n')) {
                if (i+1 < length) {
                    buffer[i] = *n;
                    i++;
                }
            }
            n++;
        }
    }
    buffer[i] = '\0';
    return i;
}

static bool IsE164(const char * string)
{
    const char * s;

    s = string;
    while ((*s != '\0') && (((*s >= '0') && (*s <= '9')) || (*s == '*') || (*s == '#') || ((*s == '+') && (s == string)) || ((*s == ',') && (s != string)))) {
        s++;
    }
    return (*s == '\0');
}

const char * Signaling::AppendSipDomain(const char * sip, char * buffer, unsigned length)
{
    if (sip && !strchr(sip, '@') && client && client->domain) {
        _snprintf(buffer, length, "%s@%s", sip, client->domain);
        return buffer;
    }
    else {
        return sip;
    }
}

const char * Signaling::RemoveSipDomain(const char * sip, char * buffer, unsigned length)
{
    if (sip && client && client->domain) {
        unsigned lenSip = strlen(sip);
        unsigned lenDomain = strlen(client->domain);
        if ((lenSip > lenDomain) && (sip[lenSip - lenDomain - 1] == '@') && (memcmp(&sip[lenSip - lenDomain], client->domain, lenDomain) == 0)) {
            unsigned l = lenSip - lenDomain - 1;
            if (l >= length) l = length ? length - 1 : 0;
            memcpy(buffer, sip, l);
            buffer[l] = '\0';
            return buffer;
        }
        else {
            return sip;
        }
    }
    else {
        return sip;
    }
}

void Signaling::PhoneStartRinging(const char* uuid)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::PhoneStartRinging uuid: %s", this, uuid);
    class SignalingCall* call = FindCallByConfId(uuid);
    if (call) {
        if (((call->state & 0xF) == SIG_CALL_STATE_ALERT) || call->state & SIG_CALL_STATE_CALL_WAITING) call->StartRinging();
    }
    else debug->printf("%x:Signaling::PhoneStartRinging Oooops could not find a call object for uuid %s ", this, uuid);
}

void Signaling::PhoneCheckIncomingCall(const char* uuid)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::PhoneCheckIncomingCall uuid: %s", this, uuid);
    class SignalingCall* call = FindCallByConfId(uuid);
    if (!call && registration) {
        class SignalingRegMessage reg("CheckCall", registration->src, alloca(1000));
        reg.add_string(reg.base, "confID", uuid);
        reg.Send(registration);
    }
}

void Signaling::PhoneStartCallAction(const char * uuid, const char * number, const char * name, bool video, bool sendingComplete)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::PhoneStartCallAction uuid: %s number: %s name: %s video: %d", this, uuid, number, name, video);
    class SignalingCall *call = FindCallByConfId(uuid);
    if (call) {
        char normalizedNumber[1024];
        NormalizeE164(number, normalizedNumber, sizeof(normalizedNumber));
        if (normalizedNumber[0] && IsE164(normalizedNumber)) {
            if (!call->remote.num) {
                char * dtmf = strchr(normalizedNumber, ',');
                if (dtmf) {
                    *dtmf = '\0';
                    call->SendDTMF(dtmf + 1, false);
                }
                char prefixedNumber[1024];
                call->remote.SetNum(registration->AddExternalLinePrefix(normalizedNumber, prefixedNumber, sizeof(prefixedNumber)));
            }
            if (registration->RemoveDeppenNull(call->remote.num, normalizedNumber, sizeof(normalizedNumber))) call->remote.SetNum(normalizedNumber);
        }
        else {
            char remoteSip[1024];
            const char * sipWithoutDomain = registration->signaling->RemoveSipDomain(number, remoteSip, sizeof(remoteSip));
            if (!sipWithoutDomain || !call->remote.sip || (strcmp(sipWithoutDomain, call->remote.sip) != 0)) {
                call->remote.SetSip(sipWithoutDomain);
            }
        }
        if (!name || !call->remote.dn || (strcmp(name, call->remote.dn) != 0)) {
            call->remote.SetDn(name);
        }
        call->sendingComplete = sendingComplete;
        call->DoStartCall();
    }
    else {
        debug->printf("%x:Signaling::PhoneStartCallAction Oooops could not find a call object for uuid %s ", this, uuid);
        if (user) {
            user->ReportCallEnded(uuid, CALL_ENDED_REASON_FAILED);
        }
    }
}

void Signaling::PhoneAnswerCallAction(const char * uuid)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::PhoneAnswerCallAction uuid: %s", this, uuid);
    class SignalingCall *call = FindCallByConfId(uuid);
    if (call) {
        if (registration->signaling->videoLicense) {
            call->videoAllowed = registration->signaling->startVideo;
        }
        call->ConnectCall(GetAudioIo(), GetVideoIo());
    }
    else {
        debug->printf("%x:Signaling::PhoneAnswerCallAction Oooops could not find a call object for uuid %s ", this, uuid);
        if (user) {
            user->ReportCallEnded(uuid, CALL_ENDED_REASON_FAILED);
        }
    }
}

void Signaling::PhoneEndCallAction(const char *uuid, int callError)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::PhoneEndCallAction uuid: %s error: %d", this, uuid, callError);
    class SignalingCall *call = FindCallByConfId(uuid);
    if (call) call->ClearCall();
    else debug->printf("%x:Signaling::PhoneEndCallAction Oooops could not find a call object for uuid %s ", this, uuid);
}

void Signaling::PhoneDialNumber(const char * number, const char * name, const char * dn, bool videoMode, bool sendingComplete, bool numOffset)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::PhoneDialNumber number: %s(%d) name: %s dn: %s video_mode: %d", this, number, numOffset, name, dn, videoMode);
    if (registration) {
        char prefixedNumber[1024];
        class SignalingCall * call = new SignalingCall(registration, CallTypeVoice, 0, numOffset ? registration->AddExternalLinePrefix(number, prefixedNumber, sizeof(prefixedNumber)) : number, name, dn, sendingComplete);
        if (registration->signaling->videoLicense) {
            call->videoAllowed = registration->signaling->startVideo || videoMode;
        }
        class IAudioIo * audioIo = GetAudioIo();
        class IVideoIo * videoIo = GetVideoIo();
        if (audioIo && call) call->StartCall(audioIo, videoIo, 0, 0);
        else debug->printf("%x:Signaling::PhoneDialNumber Oooops There is either no audioIo or no call object ", this);
    }
    else {
        debug->printf("%x:Signaling::PhoneDialNumber Oooops no registration", this);
    }
}

void Signaling::PhoneHoldCallAction(const char * uuid)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::PhoneHoldCallAction uuid: %s", this, uuid);
    class SignalingCall *call = FindCallByConfId(uuid);

    if (call) {
        call->Hold();
    } 
    else {
        debug->printf("%x:Signaling::PhoneHoldCallAction Oooops could not find a call object for uuid %s ", this, uuid);
        if (user) {
            user->ReportCallEnded(uuid, CALL_ENDED_REASON_FAILED);
        }
    }
}

void Signaling::PhoneRetrieveCallAction(const char * uuid)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::PhoneRetrieveCallAction uuid: %s", this, uuid);
    class SignalingCall *call = FindCallByConfId(uuid);
    if (call) {
        call->Retrieve();
    }
    else {
        debug->printf("%x:Signaling::PhoneRetrieveCallAction Oooops could not find a call object for uuid %s ", this, uuid);
        if (user) {
            user->ReportCallEnded(uuid, CALL_ENDED_REASON_FAILED);
        }
    }
}

void Signaling::PhoneSetMutedCallAction(const char * uuid, bool muted)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::PhoneSetMutedCallAction uuid: %s muted is: %s", this, uuid, muted ? "true" : "false");
    class SignalingCall *call = FindCallByConfId(uuid);

    if(call && registration){
        class IAudioIo* audioIo = registration->signaling->GetAudioIo();
        call->muted = muted;
        if(audioIo) audioIo->Mute(muted, false);
        call->Mute();
    } else {
        debug->printf("%x:Signaling::PhoneSetMutedCallAction Oooops could not find a call object for uuid %s ", this, uuid);
    }
}

void Signaling::PhoneAudioRouteChanged(const char * uuid, const char * deviceId)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::PhoneAudioRouteChanged uuid: %s new deviceId: %s ", this, uuid, deviceId);

    class IAudioIo* audioIo = GetAudioIo();
    if (audioIo && registration && registration->GetHwId() && deviceId) audioIo->ChangeDevice(registration->GetHwId(), deviceId);

    class SignalingCall *call = FindCallByConfId(uuid);
    if(call && registration){
        call->AudioRouteChanged();
    } else {
        debug->printf("%x:Signaling::PhoneAudioRouteChanged Oooops could not find a call object for uuid %s ", this, uuid);
    }
}

void Signaling::PhonePlayDtmfCallAction(const char * uuid, const char * digits)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::PhonePlayDtmfCallAction uuid: %s digits: %s", this, uuid, digits);
    class SignalingCall *call = FindCallByConfId(uuid);

    if (call) {
        call->SendDTMF(digits, false);
    }
    else {
        debug->printf("%x:Signaling::PhonePlayDtmfCallAction Oooops could not find a call object for uuid %s ", this, uuid);
        if (user) {
            user->ReportCallEnded(uuid, CALL_ENDED_REASON_FAILED);
        }
    }
}

SignalingCall * Signaling::FindCallByConfId(const char * uuid)
{
    if (registration && uuid) {
        SignalingCall *call = nullptr;
        while ((call = (class SignalingCall *) registration->calls->btree_find_next_right(call ? (void*)(intp)call->call : 0))) {
            if (call->confId && !strcmp(uuid, call->confId)) {
                return call;
            }
        }
    }
    return NULL;
}

SignalingCall* Signaling::FindCallByRemoteNum(const char* num)
{
    if (registration && num) {
        SignalingCall* call = nullptr;
        while ((call = (class SignalingCall*)registration->calls->btree_find_next_right(call ? (void*)(intp)call->call : 0))) {
            if (call->remote.num && !strcmp(call->remote.num, num)) {
                return call;
            }
        }
    }
    return NULL;
}

SignalingCall* Signaling::FindOtherCall(class SignalingCall * call)
{
    class btree * b = registration->calls->btree_find_next_right(0);
    while (b) {
        class SignalingCall * c = (class SignalingCall *)b;
        if (c != call) return c;
        b = registration->calls->btree_find_next_right((void *)(intp)c->call);
    }
    return nullptr;
}

IRTPTP * Signaling::SetRTPTP4(URTPTP * user, const char * sip, unsigned timeout)
{
    if (!rtptp4) rtptp4 = new SignalingRTPTP(this);
    rtptp4 = rtptp4->Update(user, sip, timeout);
    return rtptp4;
}

IRTPTP * Signaling::SetRTPTP6(URTPTP * user, const char * sip, unsigned timeout)
{
    if (!rtptp6) rtptp6 = new SignalingRTPTP(this);
    rtptp6 = rtptp6->Update(user, sip, timeout);
    return rtptp6;
}

void Signaling::WebserverPluginHttpListenResult(class IWebserverPlugin * plugin, ws_request_type_t requestType, char * resourceName, const char * registeredPathForRequest, ulong64 dataSize)
{
    if (requestType == WS_REQUEST_GET) {
        if (plugin->BuildRedirect(resourceName, _BUILD_STRING_, strlen(_BUILD_STRING_))) {
            return;
        }
    }
    plugin->Cancel(WSP_CANCEL_NOT_FOUND);
}

void Signaling::WebserverPluginWebsocketListenResult(IWebserverPlugin * plugin, const char * path, const char * registeredPathForRequest, const char * host)
{
    if (plugin == webserverPlugin) {
        new SignalingSession(this);
    }
    if (plugin == webserverPluginKeys) {
        new KeysSession(this);
    }
}

void Signaling::WebserverPluginClose(IWebserverPlugin * plugin, wsp_close_reason_t reason, bool lastUser)
{
    if (plugin == webserverPlugin) {
        delete webserverPlugin;
        webserverPlugin = NULL;
    }
    if (plugin == webserverPluginKeys) {
        delete webserverPluginKeys;
        webserverPluginKeys = NULL;
    }
    if (stopping) Stop();
}

void Signaling::Stop()
{
    log->Log(LOG_SIGNALING, "%x:Signaling::Stop client:%p registration:%p", this, client, registration);

    if (!stopping) {
        stopping = true;
        if (user) {
            user->SignalingClosing(this);
        }
        if (service->videoIo) service->videoIo->SetUSelectDeviceIoUser(NULL);
        if (service->ringerIo) service->ringerIo->SetUSelectDeviceIoUser(NULL);
        if (service->audioIo) {
            service->audioIo->SetUSelectDeviceIoUser(NULL);
            service->audioIo->RemoveUHookDeviceUser(this);
        }
    }
    if (client) {
        client->Close();
        client = NULL;
    }
    if (registration) {
        registration->Update(NULL, NULL, 0);
        registration = NULL;
    }
    if (rtptp4) {
        rtptp4->Update(NULL, NULL, 0);
        rtptp4 = NULL;
    }
    if (rtptp6) {
        rtptp6->Update(NULL, NULL, 0);
        rtptp6 = NULL;
    }
    if (webserverPlugin) webserverPlugin->Close();
    else if (webserverPluginKeys) webserverPluginKeys->Close();
    else this->appService->AppStopped(this);
}

void Signaling::AppInstanceShutdownTimeout()
{

}

class IAudioIo* Signaling::GetAudioIo() 
{ 
    if (user && user->UseRemoteClientServices()) return service->remoteAudioIo;
    else return service->audioIo;
}

class IRingerIo* Signaling::GetRingerIo() 
{ 
    if (user && user->UseRemoteClientServices()) return service->remoteRingerIo;
    else return service->ringerIo;
}

class IVideoIo* Signaling::GetVideoIo() 
{ 
#if 0
    if (user && user->UseRemoteClientServices()) return service->remoteVideoIo;
    else
#else
        return service->videoIo;
#endif
}

class IMediaProvider* Signaling::GetMediaProvider() 
{ 
    if (user && user->UseRemoteClientServices()) return service->remoteMediaProvider;
    else return service->mediaProvider;
};

class ISignalingRemote* Signaling::GetSignalingRemote()
{
    if (user && user->UseRemoteClientServices()) return service->remoteSignaling;
    else return NULL;
};

void Signaling::AddClientMonitor(class SignalingClientMonitor * monitor)
{
    clientMonitors.push_back(monitor);
    if (client && client->websocket) monitor->ClientState(true);
}

void Signaling::RemoveClientMonitor(class SignalingClientMonitor* monitor)
{
    monitor->remove();
}

void Signaling::ClientState(bool up, bool localInit)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::ClientState up: %s sessions: %d", this, up ? "true" : "false", Sessions());
    if (Sessions()) {
        StartUpdate(new SignalingUpdate(SignalingClientInfo, up ? "ClientUp" : "ClientDown", 0, filters));
    }
    for (class SignalingClientMonitor * m = clientMonitors.front(); m; m = m->goNext()) {
        m->ClientState(up);
    }
    if (user) user->ClientState(up, localInit);
    this->localInit = localInit;
}

void Signaling::UiSessionOpened()
{
    if (user) user->UserSession(true);
    uiRunning++;
}

void Signaling::UiSessionClosed()
{
    if (user) user->UserSession(false);
    if (uiRunning) uiRunning--;
    if (!uiRunning) {
        class IAudioIo* audioIo = GetAudioIo();
        if (audioIo && testAudioDeviceId) {
            audioIo->StopDevice((void*)this, testAudioDeviceId);
            audioIo->LoopTest(false);
            audioIo->StopDualTones();
            free(testAudioDeviceId);
            testAudioDeviceId = NULL;
        }
        class IRingerIo* ringerIo = GetRingerIo();
        if (ringerIo && testRingerDeviceId) {
            ringerIo->RingtoneStop(testRingerDeviceId);
            free(testRingerDeviceId);
            testRingerDeviceId = NULL;
        }
        class IVideoIo* videoIo = GetVideoIo();
        if (videoIo && testVideoDeviceId) {
            videoIo->StopDevice((void*)this, testVideoDeviceId);
            free(testVideoDeviceId);
            testVideoDeviceId = NULL;
        }
    }
}

void Signaling::AddSignalingSession(class SignalingSession* session)
{
    if (!sessions.empty()) {
        class SignalingSessionHelper* helper = sessions.front();
        while (helper) {
            if (helper->session == session) return;
            helper = helper->goNext();
        }
    }
    UiSessionOpened();
    sessions.push_back(new SignalingSessionHelper(session));

    if (!hookKeyQueue.empty()) {
        class SignalingHookKey* hookKey = hookKeyQueue.pop_front();
        while (hookKey) {
            SendHookKeyReceived(hookKey->DeviceId(), hookKey->Key());
            delete hookKey;
            hookKey = hookKeyQueue.pop_front();
        }
    }
}

void Signaling::RemoveSignalingSession(class SignalingSession* session)
{
    if (!sessions.empty()) {
        class SignalingSessionHelper* helper = sessions.front();
        while (helper) {
            if (helper->session == session) {
                helper->remove();
                delete helper;
                UiSessionClosed();
                return;
            }
            helper = helper->goNext();
        }
    }
}

void Signaling::AddRegistrationMonitor(class SignalingRegistrationMonitor * monitor)
{
    registrationMonitors.push_back(monitor);
    if (registration && registration->clientUp && (registration->registered || registration->err)) monitor->RegistrationState(true);
}

void Signaling::RemoveRegistrationMonitor(class SignalingRegistrationMonitor * monitor)
{
    monitor->remove();
}

void Signaling::MonitorSync(class SignalingRegistrationMonitor * monitor)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::MonitorSync", this);
    if (registration) {
        SignalingCall *call = nullptr;
        while ((call = (class SignalingCall *) registration->calls->btree_find_next_right(call ? (void*)(intp)call->call : 0))) {
            class SignalingCallMonitor* m = monitor->CallAdded(call);
            if (m) m->MonitorSync();
        }
    }
}

void Signaling::RegistrationState(bool up)
{
    log->Log(LOG_SIGNALING, "%x:Signaling::RegistrationState reg: %x up: %s user : %s ", this, registration, up ? "true" : "false", user ? "true" : "false");
    for (class SignalingRegistrationMonitor * m = registrationMonitors.front(); m; m = m->goNext()) {
        m->RegistrationState(up);
    }
    if (registration) {
        SignalingCall *call = nullptr;
        while ((call = (class SignalingCall *) registration->calls->btree_find_next_right(call ? (void*)(intp)call->call : 0))) {
            call->RegistrationState(up);
        }
    }
    if (user) user->RegistrationState(up);
    if (client) client->RegistrationState(up);
    // Default devices may not be still initialized if the registration with the PBX took longer
    if (up) {
        AudioDeviceList();
        VideoDeviceList();
        RingerDeviceList();
        char* t = ILocalStorage::GetItem("callWaiting");
        if (t) {
            if (!strcmp(t, "on") || !strcmp(t, "true")) this->callWaiting = CallWaitingOn;
            else if (!strcmp(t, "silent")) this->callWaiting = CallWaitingSilent;
            else this->callWaiting = CallWaitingOff;
            free(t);
        }
        else this->callWaiting = CallWaitingOff;
        t = ILocalStorage::GetItem("onhookTransfer");
        if (t) {
            if (!strcmp(t, "true")) this->onhookTransfer = true;
            else this->onhookTransfer = false;
            free(t);
        }
        else this->onhookTransfer = false;
        t = ILocalStorage::GetItem("dndRejectCall");
        if (t) {
            if (!strcmp(t, "true")) this->dndRejectCall = true;
            else this->dndRejectCall = false;
            free(t);
        }
        else this->dndRejectCall = false;
        t = ILocalStorage::GetItem("clir");
        if (t) {
            if (!strcmp(t, "true")) this->clir = true;
            else this->clir = false;
            free(t);
        }
        else this->clir = false;
        t = ILocalStorage::GetItem("autoHangup");
        if (t) {
            if (!strcmp(t, "allCalls")) autoHangup = AUTOHANGUP_ALLCALLS;
            else if (!strcmp(t, "connectedCalls")) autoHangup = AUTOHANGUP_CONNECTEDCALLS;
            else this->autoHangup = AUTOHANGUP_OFF;
            free(t);
        }
        else this->autoHangup = AUTOHANGUP_OFF;
    }
}

void Signaling::CallAdded(class SignalingCall * call, class SignalingRegistrationMonitor * exclude)
{
    for (class SignalingRegistrationMonitor * m = registrationMonitors.front(); m; m = m->goNext()) {
        if (m != exclude && call->callType == m->callType) m->CallAdded(call);
    }
}

void Signaling::UnknownCallMessage(const char * type, class json_io & msg, word base)
{
    for (class SignalingRegistrationMonitor * m = registrationMonitors.front(); m; m = m->goNext()) {
        m->UnknownCallMessage(type, msg, base);
    }
}

void Signaling::RTPTPState(class IRTPTP * rtptp, bool up)
{
    if (Sessions()) {
        if (rtptp == rtptp4) {
            StartUpdate(new SignalingUpdate(SignalingRTPTP4Info, up ? "RTPTP4Up" : "RTPTP4Down", 0, filters));
        }
        else {
            StartUpdate(new SignalingUpdate(SignalingRTPTP6Info, up ? "RTPTP6Up" : "RTPTP6Down", 0, filters));
        }
    }
}

void SignalingDialingLocation::zeroize()
{
    prefixIntl = prefixNtl = prefixSubs = country = area = subscriber = 0;
    maxIntern = 0;
    tones = MEDIA_TONES_EUROPE_PBX;
}

void SignalingDialingLocation::cleanup()
{
    if (prefixIntl) free(prefixIntl);
    if (prefixNtl) free(prefixNtl);
    if (prefixSubs) free(prefixSubs);
    if (country) free(country);
    if (area) free(area);
    if (subscriber) free(subscriber);
    zeroize();
}

void SignalingDialingLocation::read(class json_io & msg, word base)
{
    cleanup();
    prefixIntl = _strdup(msg.get_string(base, "prefixIntl"));
    prefixNtl = _strdup(msg.get_string(base, "prefixNtl"));
    prefixSubs = _strdup(msg.get_string(base, "prefixSubs"));
    country = _strdup(msg.get_string(base, "country"));
    area = _strdup(msg.get_string(base, "area"));
    subscriber = _strdup(msg.get_string(base, "subscriber"));
    maxIntern = msg.get_unsigned(base, "maxIntern");
    tones = msg.get_unsigned(base, "tones");
}

void SignalingDialingLocation::write(class SignalingRCCMessage * msg)
{
    msg->add_string(msg->base, "prefixIntl", prefixIntl);
    msg->add_string(msg->base, "prefixNtl", prefixNtl);
    msg->add_string(msg->base, "prefixSubs", prefixSubs);
    msg->add_string(msg->base, "country", country);
    msg->add_string(msg->base, "area", area);
    msg->add_string(msg->base, "subscriber", subscriber);
    msg->add_unsigned(msg->base, "maxIntern", maxIntern, msg->tmp);
    msg->add_unsigned(msg->base, "tones", tones, msg->tmp);
}

KeysSession::KeysSession(class Signaling * signaling) :
    signaling(signaling), websocket(0), closing(false)
{
    signaling->log->Log(LOG_SIGNALING, "%x:KeysSession()", this);
    signaling->webserverPluginKeys->WebsocketAccept(this);
}

KeysSession::~KeysSession()
{
    signaling->log->Log(LOG_SIGNALING, "%x:~KeysSession()", this);
}

void KeysSession::Close()
{
    signaling->log->Log(LOG_SIGNALING, "%x:KeysSession::Close()", this);
    if (!closing) {
        closing = true;
        if (websocket) websocket->Close();
    }
}

void KeysSession::WebsocketAcceptComplete(class IWebsocket * websocket)
{
    signaling->log->Log(LOG_SIGNALING, "%x:KeysSession::WebsocketAcceptComplete()", this);
    this->websocket = websocket;
    closing ? websocket->Close() : websocket->Recv();
}

void * KeysSession::WebsocketRecvBuffer(size_t len)
{
    signaling->log->Log(LOG_SIGNALING, "%x:KeysSession::WebsocketRecvBuffer() len=%d", this, len);
    char * buffer = (char *)malloc(len + 1);
    buffer[len] = 0;
    return buffer;
}

void KeysSession::WebsocketRecvResult(class IWebsocket * websocket, void * buffer, size_t len, bool text, bool isFragmented)
{
    signaling->log->Log(LOG_SIGNALING, "%x:KeysSession::WebsocketRecvResult() len=%d buffer=%x", this, len, buffer);
    if (text && !isFragmented) {
        class json_io recv((char *)buffer);
        if (recv.decode()) {
            word base = recv.get_object(JSON_ID_ROOT, NULL);
            const char * mt = recv.get_string(base, "mt");
            if (!strcmp(mt, "Key")) {
                const char * code = recv.get_string(base, "code");
                unsigned code_len = code ? strlen(code) : 0;
                byte key = 0;
                signaling->log->Log(LOG_SIGNALING, "%x:KeysSession::WebsocketRecvResult() key-code=%s", this, code);
                if (code_len == 1 && strchr("1234567890*#", code[0])) key = code[0];
                else if (!strcmp(code, "mic")) key = KEY_MIC;
                else if (!strcmp(code, "rkey")) key = KEY_SIG;
                else if (!strcmp(code, "redial")) key = KEY_REDIAL;
                else if (!strcmp(code, "speaker")) key = KEY_LOUD;
                else if (!strcmp(code, "backspace")) key = KEY_BS;
                class IAudioIo *audioIo = signaling->GetAudioIo();
                const char * deviceId = nullptr; // find HandsfreeDevice
                for (unsigned i = 0; true; i++) {
                    deviceId = audioIo->GetDeviceId(i);
                    if (!deviceId || !strcmp(deviceId, "Handsfree | Handsfree")) break;
                }
                if (key) signaling->HookKeyReceived(audioIo, deviceId, key);
            }
            else {
                signaling->log->Log(LOG_SIGNALING, "%x:KeysSession::WebsocketRecvResult() mt=%s", this, mt);
            }
        }
    }
    free(buffer);
    this->websocket->Recv();
}

void KeysSession::WebsocketRecvCanceled(class IWebsocket * websocket, void * buffer)
{
    signaling->log->Log(LOG_SIGNALING, "%x:KeysSession::WebsocketRecvCanceled()", this);
    free(buffer);
}

void KeysSession::WebsocketSendResult(class IWebsocket * websocket)
{
    signaling->log->Log(LOG_SIGNALING, "%x:KeysSession::WebsocketSendResult()", this);
}

void KeysSession::WebsocketCloseComplete(class IWebsocket * websocket, ws_close_reason_t reason)
{
    signaling->log->Log(LOG_SIGNALING, "%x:KeysSession::WebsocketCloseComplete() reason=%x", this, reason);
    delete websocket;
    this->websocket = 0;
    signaling->KeysSessionClosed(this);
}

/*-----------------------------------------------------------------------------------------------*/
/* class SignalingSession                                                                        */
/*-----------------------------------------------------------------------------------------------*/

SignalingSession::SignalingSession(class Signaling * signaling) : AppUpdatesSession(signaling, signaling->webserverPlugin, signaling, signaling)
{
    signaling->log->Log(LOG_SIGNALING, "%x:SignalingSession()", this);
    this->signaling = signaling;
    currentSrc = 0;
    clientInfo = nullptr;
    mediaTest = nullptr;
    closing = false;
    if (false) ChannelsCmdName(0);
}

void SignalingSession::AppWebsocketClosed()
{
    signaling->log->Log(LOG_SIGNALING, "%x:SignalingSession::AppWebsocketClosed() mediaTest=%p", this, mediaTest);
    signaling->RemoveSignalingSession(this);
    if (mediaTest) {
        closing = true;
        mediaTest->Close();
    }
    else delete this;
}

void SignalingSession::ResponseSent()
{

}

bool SignalingSession::AppWebsocketConnectComplete(class json_io & msg, word info)
{
    signaling->log->Log(LOG_SIGNALING, "%x:SignalingSession::AppWebsocketConnectComplete()", this);
    signaling->AddSignalingSession(this);
    return true;
}

void SignalingSession::AppWebsocketMessage(class json_io & msg, word base, const char * mt, const char * src)
{
    signaling->log->Log(LOG_SIGNALING, "%x:SignalingSession::AppWebsocketMessage(%s)", this, mt);
    if (currentSrc) free(currentSrc);
    currentSrc = _strdup(src);
    if (!strcmp(mt, "SetClient")) {
        ((class ISignaling *)signaling)->SetClient(msg.get_string(base, "url"), msg.get_string(base, "sec"), msg.get_string(base, "phys"), msg.get_string(base, "sip"), msg.get_string(base, "usr"), msg.get_string(base, "pwd"), msg.get_string(base, "domain"), msg.get_string(base, "appdomain"), msg.get_unsigned(base, "timeout"), false);

        class SignalingMessage result("SetClientResult", src, alloca(1000), alloca(100));
        if (signaling->client) signaling->client->Sync(result, base);
        result.Send(this);
        AppWebsocketMessageComplete();
        if(!clientInfo) clientInfo = new SignalingUpdatesFilter(SignalingClientInfo, signaling->filters, this, src, sip);
    }
    else if (!strcmp(mt, "ConnInfoNotAvailable")) {
        class SignalingMessage result("ConnInfoNotAvailableResult", src, alloca(1000), alloca(100));
        if (signaling->user) result.add_bool(result.base, "credentials", signaling->user->HasCredentials());
        if (signaling->client) {
            signaling->client->Sync(result, base);
            // softphone app is standard app but UI is connected to standby PBX -> no SetClient will arrive -> create the filter
            if (!clientInfo) clientInfo = new SignalingUpdatesFilter(SignalingClientInfo, signaling->filters, this, currentSrc, sip);
        }
        result.Send(this);
        AppWebsocketMessageComplete();
    }
    else if (!strcmp(mt, "SetRegistration")) {
        // Signaling already registered and a new SetRegistration arrives for a different device
        if (signaling->registration && msg.get_string(base, "hw") && strcmp(msg.get_string(base, "hw"), signaling->registration->GetHwId()) && signaling->registration->registered) {
            class SignalingMessage result("SetRegistrationResult", src, alloca(1000), alloca(100));
            word handle = result.add_object(result.base, "sync");
            result.add_string(handle, "mt", "RegistrationBusy");
            result.Send(this);
        }
        else {
            ((class ISignaling *)signaling)->SetRegistration(msg.get_string(base, "hw"), msg.get_string(base, "phys"), msg.get_unsigned(base, "timeout"));

            class SignalingMessage result("SetRegistrationResult", src, alloca(1000), alloca(100));
            if (signaling->registration) signaling->registration->Sync(result, base);
            result.Send(this);
        }
        AppWebsocketMessageComplete();
        new SignalingUpdatesFilter(SignalingRegistrationInfo, signaling->filters, this, src, sip);
   }
    else if (!strcmp(mt, "SetRTPTP4")) {
        ((class ISignaling *)signaling)->SetRTPTP4(signaling->rtptp4 ? signaling->rtptp4->user : 0, msg.get_string(base, "sip"), msg.get_unsigned(base, "timeout"));

        class SignalingMessage result("SetRTPTP4Result", src, alloca(1000), alloca(100));
        if (signaling->rtptp4) signaling->rtptp4->Sync(result, base);
        result.Send(this);
        AppWebsocketMessageComplete();
        new SignalingUpdatesFilter(SignalingRTPTP4Info, signaling->filters, this, src, sip);
    }
    else if (!strcmp(mt, "SetRTPTP6")) {
        ((class ISignaling *)signaling)->SetRTPTP6(signaling->rtptp6 ? signaling->rtptp6->user : 0, msg.get_string(base, "sip"), msg.get_unsigned(base, "timeout"));

        class SignalingMessage result("SetRTPTP6Result", src, alloca(1000), alloca(100));
        if (signaling->rtptp6) signaling->rtptp6->Sync(result, base);
        result.Send(this);
        AppWebsocketMessageComplete();
        new SignalingUpdatesFilter(SignalingRTPTP6Info, signaling->filters, this, src, sip);
    }
    else if (!strcmp(mt, "TestAudioDevice")) {
        class IAudioIo* audioIo = signaling->GetAudioIo();
        if (audioIo) {
            if (signaling->testAudioDeviceId) {
                audioIo->StopDevice((void*)this, signaling->testAudioDeviceId);
                audioIo->LoopTest(false);
                audioIo->StopDualTones();
                free(signaling->testAudioDeviceId);
                signaling->testAudioDeviceId = NULL;
            }
            if (msg.get_string(base, "id")) {
                signaling->testAudioDeviceId = _strdup(msg.get_string(base, "id"));
                audioIo->StartDevice((void*)this, (void*)this, signaling->testAudioDeviceId, AudioDeviceMode::AudioModeCommunication);
                audioIo->LoopTest(true);
                const char* digits = "#";
                struct AudioIoDualTone tone;
                MediaTones::PrepareDtmf(digits[0], &tone);
                audioIo->StartDualTones(AUDIO_IO_DUAL_TONE_BOTH | AUDIO_IO_DUAL_TONE_PEER, 1, &tone);
            }
        }
        AppWebsocketMessageComplete();
    }
    else if (!strcmp(mt, "TestRingerDevice")) {
        class IRingerIo* ringerIo = signaling->GetRingerIo();
        if (ringerIo && signaling->registration) {
            if (signaling->testRingerDeviceId) {
                ringerIo->RingtoneStop(signaling->testRingerDeviceId);
                free(signaling->testRingerDeviceId);
                signaling->testRingerDeviceId = NULL;
            }
            if (msg.get_string(base, "id")) {
                signaling->testRingerDeviceId = _strdup(msg.get_string(base, "id"));
                ringerIo->RingtoneStart(signaling->testRingerDeviceId, 0, ringerIo->GetRingtone(signaling->registration->GetHwId()));
            }
        }
        AppWebsocketMessageComplete();
    }
    else if (!strcmp(mt, "TestVideoDevice")) {
        class IVideoIo* videoIo = signaling->GetVideoIo();
        if (videoIo) {
            if (signaling->testVideoDeviceId) {
                videoIo->StopDevice((void*)this, signaling->testVideoDeviceId);
                free(signaling->testVideoDeviceId);
                signaling->testVideoDeviceId = NULL;
            }
            if (msg.get_string(base, "id")) {
                signaling->testVideoDeviceId = _strdup(msg.get_string(base, "id"));
                videoIo->StartVideoDevice((void*)this, (void*)this, signaling->testVideoDeviceId, 1280, 720, 1);
            }
        }
        AppWebsocketMessageComplete();
    }
    else if (!strcmp(mt, "TestIceGathering")) {
        SignalingMediaConfig* c = signaling->service->mediaConfig;

        if (!mediaTest) {
            if (signaling->user && signaling->user->UseRemoteClientServices()) signaling->GetSignalingRemote()->CreateCall(this);
            mediaTest = signaling->GetMediaProvider()->CreateMedia(signaling->GetIoMux(), this, signaling, MediaType::VIDEO);
            mediaTest->Initialize(
                c->udpSocketProvider, c->tcpSocketProvider, c->socketContext, c->minVideoPort, c->maxVideoPort,
                c->stunServers, c->turnServers, c->turnUsername, c->turnPassword,
                AUDIO, c->stunSlow, c->turnOnly, c->iceNoHost, false,
                c->dropMediaTx, c->dropMediaRx, c->hostPbx, c->noVpnAddresses, NULL, true, c->tlsSocketProvider, c->excludeInterfaces, c->recordingUrl, NULL, false, c->recordRtpStream, c->currentUserSip, c->recordingUsername, c->recordingPassword);
        }

        AppWebsocketMessageComplete();
    }
    else if (!strcmp(mt, "WindowTitle")) {
        if (signaling->user) signaling->user->SoftphoneAppWindowTitle(msg.get_string(base, "title"));
        AppWebsocketMessageComplete();
    }
    else {
        signaling->log->Log(LOG_SIGNALING, "%x:SignalingSession::AppWebsocketMessage unknown message %s", this, mt);
        AppWebsocketMessageComplete();
    }
}

void SignalingSession::MediaInitializeComplete(IMedia* const media, class MediaConfig* localMediaConfig)
{
    class SignalingMessage testIceGathering("TestIceGatheringResult", 0, alloca(2000), alloca(1000));
    word baseIceArray = testIceGathering.add_array(testIceGathering.base, "candidate");

    debug->printf("%x:SignalingSession::MediaInitializeComplete(%p,%p) config=%p", this, media, mediaTest, localMediaConfig);

    if (localMediaConfig) {
        if (localMediaConfig->ice) {
            signaling->log->Log(LOG_SIGNALING, "SignalingSession(%p)::MediaInitializeComplete RtcpMux(%d) ICE(%s:%s) Count=%d", this, localMediaConfig->ice->rtcpMux,
                ((localMediaConfig->ice && localMediaConfig->ice->iceUfrag) ? localMediaConfig->ice->iceUfrag : ""), ((localMediaConfig->ice && localMediaConfig->ice->icePwd) ? localMediaConfig->ice->icePwd : ""), localMediaConfig->ice->count);

            class IceCandidate* c = localMediaConfig->ice->candidateList.front();
            if (c) {
                for (; c; c = c->goNext()) {
                    word jsonCandidate = testIceGathering.add_object(baseIceArray, 0);
                    testIceGathering.add_string(jsonCandidate, "addr", c->addr);
                    testIceGathering.add_string(jsonCandidate, "r_addr", c->relatedAddr);
                    testIceGathering.add_int(jsonCandidate, "rtp", c->rtpPort, testIceGathering.tmp);
                    testIceGathering.add_int(jsonCandidate, "rtcp", c->rtcpPort, testIceGathering.tmp);
                    testIceGathering.add_int(jsonCandidate, "r_rtp", c->relatedRtpPort, testIceGathering.tmp);
                    testIceGathering.add_int(jsonCandidate, "r_rtcp", c->relatedRtcpPort, testIceGathering.tmp);
                    testIceGathering.add_int(jsonCandidate, "rtp_prio", c->rtpPriority, testIceGathering.tmp);
                    testIceGathering.add_int(jsonCandidate, "rtcp_prio", c->rtcpPriority, testIceGathering.tmp);
                    testIceGathering.add_string(jsonCandidate, "type", c->type == CHANNEL_CANDIDATE_HOST ? "HOST" : c->type == CHANNEL_CANDIDATE_SRFLX ? "SRFLX" : "RELAY");
                    testIceGathering.add_string(jsonCandidate, "foundation", c->foundation);
                }
            }
        }
    }

    testIceGathering.Send(this);

    mediaTest->Close();
}

void SignalingSession::MediaCloseComplete(IMedia* const media)
{
    delete media;
    this->mediaTest = nullptr;
    if (signaling->user && signaling->user->UseRemoteClientServices()) signaling->GetSignalingRemote()->DeleteCall(this);
    if (closing) delete this;
}

SignalingMessage::SignalingMessage(const char * mt, const char * src, void * sb, void * tmp) : JsonIo((char *)sb)
{
    this->sb = (char *)sb;
    this->tmp = (char *)tmp;
    base = add_object(0xffff, 0);
    add_string(base, "mt", mt);
    add_string(base, "src", src);
}

void SignalingMessage::Send(class SignalingSession * session)
{
    session->SendResponse(*this, sb);
}

/*-----------------------------------------------------------------------------------------------*/
/* SignalingUpdate                                                                               */
/*-----------------------------------------------------------------------------------------------*/

SignalingUpdate::SignalingUpdate(enum SignalingUpdateType type, const char * mt, const char * info, class AppUpdatesFilters & filters, const char * sip) : AppUpdate(filters, sip)
{
    this->type = type;
    this->mt = mt;
    this->info = _strdup(info);
    debug->printf("%x(%u):SignalingUpdate(mt=%s)", this, type, mt);
}

SignalingUpdate::~SignalingUpdate()
{
    free(info);
}

SignalingUpdatesFilter::SignalingUpdatesFilter(enum SignalingUpdateType type, AppUpdatesFilters & filters, class AppUpdatesSession * session, const char * src, const char * sip) : AppUpdatesFilter(filters, session, src, sip)
{
    this->type = type;
    this->src = _strdup(src);
}

SignalingUpdatesFilter::~SignalingUpdatesFilter()
{
    if (this->src) free(this->src);
};

bool SignalingUpdatesFilter::Test(SignalingUpdate * update)
{
    return update->type==type;
}

void SignalingUpdatesFilter::Send(SignalingUpdate * update)
{
    char sb[16000];
    class json_io send(sb);
    word base = send.add_object(0xffff, 0);
    if (GetSrc()) send.add_string(base, "src", GetSrc());
    send.add_string(base, "mt", update->mt);

    send.add_json(base, "info", update->info);
    SendUpdate(send, sb);
}

/*-----------------------------------------------------------------------------------------------*/
/* class SignalingClient                                                                         */
/*-----------------------------------------------------------------------------------------------*/

SignalingClient::SignalingClient(class Signaling * signaling) : retry(signaling->service->iomux, this), tryAltPbx(signaling->service->iomux, this)
{
    signaling->log->Log(LOG_SIGNALING, "%x:SignalingClient()", this);
    this->signaling = signaling;
    signaling->client = this;
    websocket = 0;
    usePrimaryUrl = true;
    url = 0;
    sec = 0;
    redirectUrl = 0;
    curPbx = 0;
    phys = 0;
    app = 0;
    usr = 0;
    pwd = 0;
    domain = 0;
    appdomain = 0;
    timeout = TIMEOUT_MIN;
    up = false;
    localInit = false;
    clearRegistration = false;
    updateCredentials = false;
    fastConnect = false;
}

SignalingClient::~SignalingClient()
{
    if (signaling && signaling->client == this) signaling->client = 0;
    free(url);
    if (sec) free(sec);
    if (curPbx) free(curPbx);
    if (phys) free(phys);
    free(app);
    free(usr);
    free(pwd);
    free(domain);
    if (appdomain) free(appdomain);
}

void SignalingClient::TestCloseRegistration()
{
    signaling->log->Log(LOG_SIGNALING, "%x:SignalingClient()::TestCloseRegistration", this);
    if (websocket) websocket->Close(); 
}

void SignalingClient::Close()
{
    if (websocket) {
        websocket->Close();
        DeleteCalls();
        if (signaling && signaling->client == this) signaling->client = 0;
        signaling = 0;
        retry.Cancel();
        tryAltPbx.Cancel();
    }
    else delete this;
}

void SignalingClient::Update(const char * url, const char * sec, const char * phys, const char * app, const char * usr, const char * pwd, const char * domain, const char * appdomain, unsigned timeout, bool localInit)
{
    uri_dissector uriDiss;
    if (url && uriDiss.dissect_uri((char *)url) && (uriDiss.ip || uriDiss.fqdn)) {
        SignalingMediaConfig* c = signaling->service->mediaConfig;
        const char *host = (uriDiss.ip) ? uriDiss.ip : uriDiss.fqdn;
        const char *hostEnd = (uriDiss.port) ? uriDiss.e_port : ((uriDiss.ip) ? uriDiss.e_ip : uriDiss.e_fqdn);
        if (c->hostPbx) free(c->hostPbx);
        c->hostPbx = (char *)(malloc(hostEnd - host + 1));
        memcpy(c->hostPbx, host, hostEnd - host);
        c->hostPbx[hostEnd - host] = '\0';
    }
    if (url && app && usr && pwd) {
        if (timeout) this->timeout = timeout;
        if (!websocket) {
            this->url = _strdup(url);
            this->sec = sec && sec[0] ? _strdup(sec) : NULL;
            if (domain && phys && phys[0]) {
                if (!this->phys) {
                    if (strstr(phys, domain)) {
                        this->phys = _strdup(phys);
                    }
                    else {
                        this->phys = (char*)malloc(strlen(domain) + 1 + strlen(phys) + 1);
                        _sprintf(this->phys, "%s@%s", domain, phys);
                    }
                }
                // New physical Location(can it really happen?)
                else if (!strstr(this->phys, phys) && !strstr(phys, this->phys)) {
                    free(this->phys);
                    if (strstr(phys, domain)) {
                        this->phys = _strdup(phys);
                    }
                    else {
                        this->phys = (char*)malloc(strlen(domain) + 1 + strlen(phys) + 1);
                        _sprintf(this->phys, "%s@%s", domain, phys);
                    }
                }
            }
            this->app = _strdup(app);
            this->usr = _strdup(usr);
            this->pwd = _strdup(pwd);
            this->domain = _strdup(domain);
            this->appdomain = appdomain && appdomain[0] ? _strdup(appdomain) : NULL;
            this->clientTimeout = timeout ? timeout : TIMEOUT_MIN;
            this->localInit = localInit;
            if (signaling) {
                websocket = signaling->service->appWebsocketClientProvider->Create(this, signaling->service);
                websocket->Connect(url, pwd, usr, "-", app, "-", "-");
                if (curPbx) free(curPbx);
                curPbx = _strdup(this->url);
                if (!localInit) updateCredentials = true;
            }
        }
        else if (strcmp(this->url, url) || strcmp(this->app, app) || strcmp(this->usr, usr) || strcmp(this->pwd, pwd)) {
            if (signaling) signaling->ClientState(false, localInit);
            websocket->Close();
            DeleteCalls();
            if (signaling) {
                class SignalingClient * s = new SignalingClient(signaling);
                s->Update(url, sec, phys, app, usr, pwd, domain, appdomain, timeout ? timeout : TIMEOUT_MIN, localInit);
                signaling->client = s;
                signaling = 0;
                // Clear registration if for instance a user connects to a different PBX
                // Clear when the app connects to the new PBX. Otherwise myApps could crash if a call is present, registration pointer is used.
                s->ClearRegistration();
            }
        }
        else {
            bool updateSecPhysCredentials = false;
            if (!this->sec && sec && sec[0]) {
                this->sec = _strdup(sec);
                updateSecPhysCredentials = true;
            }
            else if (this->sec && (!sec || !sec[0])) {
                if (this->sec) free(this->sec);
                this->sec = NULL;
                updateSecPhysCredentials = true;
            }
            if (domain && phys && phys[0]) {
                if (!this->phys) {
                    if (strstr(phys, domain)) {
                        this->phys = _strdup(phys);
                    }
                    else {
                        this->phys = (char*)malloc(strlen(domain) + 1 + strlen(phys) + 1);
                        _sprintf(this->phys, "%s@%s", domain, phys);
                    }
                    updateSecPhysCredentials = true;
                }
                // New physical Location(can it really happen?)
                else if (!strstr(this->phys, phys) && !strstr(phys, this->phys)) {
                    free(this->phys);
                    if (strstr(phys, domain)) {
                        this->phys = _strdup(phys);
                    }
                    else {
                        this->phys = (char*)malloc(strlen(domain) + 1 + strlen(phys) + 1);
                        _sprintf(this->phys, "%s@%s", domain, phys);
                    }
                    updateSecPhysCredentials = true;
                }
            }
#if 0 // Keep the physical Location(!) as it arrived from the PBX
            else if (this->phys && (!phys || !phys[0])) {
                if (this->phys) free(this->phys);
                this->phys = NULL;
                updateSecPhysCredentials = true;
            }
#endif
            if(updateSecPhysCredentials) signaling->SaveCredentials(this->url, this->sec, this->phys, this->app, this->usr, this->pwd, this->domain, this->appdomain, this->clientTimeout);
        }
    }
    else {
        if (signaling && up) signaling->ClientState(false, localInit);
        if (websocket) {
            websocket->Close();
            DeleteCalls();
            if(signaling && signaling->client == this) signaling->client = 0;
            signaling = 0;
        }
        else {
            delete this;
        }
    }
}

void SignalingClient::RegisterToAltPbx()
{
    if (signaling) signaling->log->Log(LOG_SIGNALING, "%x:SignalingClient::RegisterToAltPbx() localInit:%d", this, localInit);

    localInit = false;
    updateCredentials = false;

    if (usePrimaryUrl && sec) usePrimaryUrl = false;
    else if (!usePrimaryUrl && url) usePrimaryUrl = true;

    websocket->Close();
}

void SignalingClient::RegisterToRedirectUrl(const char* url)
{
    if (signaling) signaling->log->Log(LOG_SIGNALING, "%x:SignalingClient::RegisterToRedirectUrl()", this);

    localInit = false;
    if (this->redirectUrl) free(this->redirectUrl);
    this->redirectUrl = (url && url[0]) ? _strdup(url) : NULL;

    updateCredentials = false;

    websocket->Close();
}

void SignalingClient::RegistrationState(bool up)
{
    if(up) tryAltPbx.Cancel();
}

void SignalingClient::Sync(class json_io & msg, word base)
{
    if(signaling) signaling->log->Log(LOG_SIGNALING, "%x:SignalingClient::Sync() up: %d", this, up);
    if (up) {
        word handle = msg.add_object(base, "sync");
        msg.add_string(handle, "mt", "ClientUp");
    }
}

void SignalingClient::AppWebsocketClientConnectComplete(class IAppWebsocketClient * appWebsocketClient)
{
    if(signaling) signaling->log->Log(LOG_SIGNALING, "%x:SignalingClient::AppWebsocketClientConnectComplete websocket: %x pbx: %s clearRegistration: %d timeout: %d", this, websocket, curPbx ? curPbx : "", clearRegistration, this->clientTimeout);
    up = true;
    timeout = TIMEOUT_MIN;
    websocket->MessageComplete();
    if (signaling) {
        if (clearRegistration) {
            if (signaling->registration) signaling->registration->Update(NULL, NULL, 0);
            clearRegistration = false;
        }
        signaling->ClientState(true, localInit);
        if(updateCredentials) signaling->SaveCredentials(this->url, this->sec, this->phys, this->app, this->usr, this->pwd, this->domain, this->appdomain, this->clientTimeout);
    }
}

void SignalingClient::AppWebsocketClientMessage(class IAppWebsocketClient * appWebsocketClient, class json_io & msg, word base, const char * mt, const char * src)
{
    if (signaling) {
        if (signaling->log->LogFlagSet(LOG_SIGNALING)) {
            char buffer[WS_MAX_DATA_SIZE];
            memset(buffer, 0x00, WS_MAX_DATA_SIZE);
            msg.encode(base, buffer);
            signaling->log->Log(LOG_SIGNALING,
                "%x:SignalingClient::AppWebsocketClientMessage(mt=%s,src=%s) websocket=%x buffer=%s",
                this, mt, src, websocket, buffer);
        }
        if (src) {
            for (class SignalingClientMonitor* m = signaling->clientMonitors.front(); m; m = m->goNext()) {
                if (!strcmp(m->src, src)) m->ClientMessage(msg, base, mt, src);
            }
        }
    }
    websocket->MessageComplete();
}

void SignalingClient::AppWebsocketClientSendResult(class IAppWebsocketClient * appWebsocketClient)
{
    if(signaling) signaling->log->Log(LOG_SIGNALING, "%x:SignalingClient::AppWebsocketClientSendResult", this);
}

void SignalingClient::DeleteCalls()
{
    if(signaling) signaling->log->Log(LOG_SIGNALING, "%x:SignalingClient::DeleteCalls reg=%p", this, signaling->registration);

    if (signaling && signaling->registration) {
        SignalingCall * call = nullptr;

        while ((call = (class SignalingCall *)signaling->registration->calls->btree_find_next_right(call ? (void*)(intp)call->call : 0))) {
            call->ClearCall();
        }

        if (signaling->registration->presenceCall) {
            SignalingSigMessage msg(presenceSubscriptionCallId, "rel", alloca(1000), alloca(100));
            msg.Send(signaling->registration);
            signaling->registration->presenceCall = false;
        }
    }
}

void SignalingClient::AppWebsocketClientClosed(class IAppWebsocketClient * appWebsocketClient)
{
    if(signaling) signaling->log->Log(LOG_SIGNALING, "%x:SignalingClient::AppWebsocketClientClosed timeout: %d websocket: %x signaling: %x fastConnect: %d", this, timeout, websocket, signaling, fastConnect);
    delete websocket;
    websocket = 0;
    up = false;
    if (signaling) {
        if (signaling->registration) {
            if (signaling->registration->presenceCall) {
                SignalingSigMessage msg(presenceSubscriptionCallId, "rel", alloca(1000), alloca(100));
                msg.Send(signaling->registration);
                signaling->registration->presenceCall = false;
            }
            SignalingCall* call = nullptr;
            while ((call = (class SignalingCall*)signaling->registration->calls->btree_find_next_right(call ? (void*)(intp)call->call : 0))) {
                if (call->IsReconnectedCall()) {
                    call->failedReconnect = true;
                    call->ClearCall(Q931_CAUSE_NonSelectedUserClearing);
                }
                else if (call->GetReconnectRole() == nullptr) {
                    call->ClearCall();
                }
                // Non connected and non onHold
                else if((!((call->state & 0xF) == SIG_CALL_STATE_CONN)) && (!(call->state & 0x300))) {
                    call->ClearCall();
                }
            }
        }
        signaling->ClientState(false, localInit);
        if (fastConnect) {
            fastConnect = false;
            retry.Start(500);
        }
        else retry.Start(timeout);
        if(tryAltPbx.IsRunning() == false) tryAltPbx.Start(TIMEOUT_MAX);
    }
    else {
        delete this;
    }
}

void SignalingClient::TimerOnTimeout(ITimer * timer)
{
    if (timer == &retry) {
        if (!signaling || websocket) return;
        websocket = signaling->service->appWebsocketClientProvider->Create(this, signaling->service);
        if (redirectUrl) {
            websocket->Connect(redirectUrl, pwd, usr, "-", app, "-", "-");
            if (curPbx) free(curPbx);
            curPbx = _strdup(redirectUrl);
            free(redirectUrl);
            redirectUrl = NULL;
        }
        else if (usePrimaryUrl && url) {
            websocket->Connect(url, pwd, usr, "-", app, "-", "-");
            if (curPbx) free(curPbx);
            curPbx = _strdup(url);
        }
        else if (!usePrimaryUrl && sec) {
            websocket->Connect(sec, pwd, usr, "-", app, "-", "-");
            if (curPbx) free(curPbx);
            curPbx = _strdup(sec);
        }
        if (timeout < TIMEOUT_MAX) timeout *= 2;
    }
    else if (timer == &tryAltPbx) {
        if (usePrimaryUrl && sec) usePrimaryUrl = false;
        else if (!usePrimaryUrl && url) usePrimaryUrl = true;
        if (websocket) fastConnect = true;
        else retry.Start(500);
        tryAltPbx.Start(TIMEOUT_MAX);
    }
}

void SignalingClient::RegistrationCheck()
{
    timeout = TIMEOUT_MIN;
    if (retry.IsRunning()) {
        retry.Cancel();
        retry.Start(1);
    }
}

/*-----------------------------------------------------------------------------------------------*/

SignalingClientMonitor::SignalingClientMonitor(class Signaling * signaling, const char * src)
{
    this->signaling = signaling;
    this->src = _strdup(src);
}

SignalingClientMonitor::~SignalingClientMonitor()
{
    if(this->src) free(this->src);
}

void SignalingClientMonitor::MessageSend(class json_io & msg, word base, char * buffer)
{
    msg.add_string(base, "api", "PbxSignal");
    msg.add_string(base, "src", src);

    if (signaling->log->LogFlagSet(LOG_SIGNALING)) {
        word len = msg.encode();
        signaling->log->Log(LOG_SIGNALING, "%x:SignalingClientMonitor::MessageSend len:%d buffer: %s", this,
                            len, buffer);
    }
    // The websocket connection must be connected before data is sent. Otherwiese ASSERT in websocketclient.cpp due to wrong state.
    if (signaling->client && signaling->client->IsClientUp()) signaling->client->websocket->MessageSend(msg, buffer);
}

/*-----------------------------------------------------------------------------------------------*/
/* class SignalingRegistration                                                                   */
/*-----------------------------------------------------------------------------------------------*/

SignalingRegistration::SignalingRegistration(class Signaling * signaling, const char * phys) : SignalingClientMonitor(signaling, "registration"), retry(signaling->service->iomux, this)
{
    signaling->log->Log(LOG_SIGNALING, "%x:SignalingRegistration() signaling:%x phys:%s", this, signaling, phys ? phys : "-");
    this->signaling = signaling;
    signaling->registration = this;
    hw = 0;
    this->phys = _strdup(phys);
    timeout = TIMEOUT_MIN;
    clientUp = false;
    err = 0;
    registered = false;
    calls = 0;
    activePhoneCall = 0;
    reconnectingCallDelete = 0;
    pendingReconnectCall = 0;
    presenceCall = false;
    dnd = false;
    diversionSession = NULL;
    callId = 1;
}

SignalingRegistration::~SignalingRegistration()
{
    signaling->log->Log(LOG_SIGNALING, "%x:~SignalingRegistration()", this);
    free(hw);
    if (phys) free(phys);
    if (err) free(err);
    signaling->RemoveClientMonitor(this);
    signaling->RegistrationDeleted(this);
}

void SignalingRegistration::ClientState(bool up)
{
    signaling->log->Log(LOG_SIGNALING, "%x:SignalingRegistration::ClientState(%u) registered:%u", this, up, registered);
    timeout = TIMEOUT_MIN;
    retry.Cancel();
    this->registered = false;
    if (up) {
        Register();
        if (this->err) {
            free(this->err);
            this->err = 0;
        }
    }
    else {
        if (signaling->Sessions()) signaling->StartUpdate(new SignalingUpdate(SignalingRegistrationInfo, "RegistrationDown", 0, signaling->filters));
        signaling->RegistrationState(false);
    }
    this->clientUp = up;
}

word find_fty(class json_io & msg, word base, const char * name, unsigned index = 0)
{
    word fty = msg.get_array(base, "fty");
    if (fty != JSON_ID_NONE) {
        word object = msg.get_object(fty, 0);
        for (unsigned idx = 0; object != JSON_ID_NONE;) {
            const char * type = msg.get_string(object, "type");
            if (type && !strcmp(type, name) && idx++ == index) return object;
            object = msg.get_object(fty, object);
        }
    }
    return JSON_ID_NONE;
}

void SignalingRegistration::Register()
{
    class SignalingRegMessage reg("Register", src, alloca(1000));
    reg.add_string(reg.base, "hw", hw);
    if (phys) reg.add_string(reg.base, "gk", phys);
    char versionId[256];
    _sprintf(versionId, "%s %s [%s]", _VERSION_STR_, _RELEASE_STATE_, _BUILD_STRING_);
    reg.add_string(reg.base, "versionId", versionId);
    reg.add_string(reg.base, "productId", _PRODUCT_NAME_);
    reg.add_string(reg.base, "flags", "RECONNECT");
    reg.Send(this);
}

void SignalingRegistration::ClientMessage(class json_io & msg, word base, const char * mt, const char * src)
{
    signaling->log->Log(LOG_SIGNALING, "%x:SignalingRegistration::ClientMessage(mt: %s) phys=%s", this, mt, phys ? phys : "-");
    if (!strcmp(mt, "RegisterResult")) {
        const char * err = msg.get_string(base, "err");
        if (err) {
            retry.Start(timeout);
            if (this->err) free(this->err);
            this->err = _strdup(err);
            if (this->err) {
                const char* altGk = msg.get_string(base, "alt-gk");
                const char* altIp = msg.get_string(base, "alt-ip");
                const char* altUrl = msg.get_string(base, "url");
                if(!strcmp(this->err, "standingby")) {
                    if (altIp && altUrl) {
                        signaling->RegisterToRedirectUrl(altUrl);
                        if (altGk) {
                            if (this->phys) free(this->phys);
                            this->phys = _strdup(altGk);
                        }
                    }
                    else signaling->RegisterToAltPbx();
                }
                else if (!strcmp(this->err, "wrong pbx")) {
                    signaling->RegisterToRedirectUrl(altUrl);
                    if (this->phys) free(this->phys);
                    this->phys = _strdup(altGk);
                }
                else if (!strcmp(this->err, "hw unknown")) {
                }
            }
        }
        else {
            if (signaling->localIpAddr) free(signaling->localIpAddr);
            signaling->localIpAddr = _strdup(msg.get_string(base, "addr"));
            signaling->videoLicense = msg.get_bool(base, "video");
            signaling->recordingLicense = msg.get_bool(base, "recording");
            dialingLocation.read(msg, base);
            signaling->SaveRegistration(hw, phys, hwTimeout);
            registered = true;
            if (this->err) {
                free(this->err);
                this->err = 0;
            }

            class SignalingMediaConfig * c = signaling->service->mediaConfig;
            if (c->stunServers) free(c->stunServers);
            c->stunServers = _strdup(msg.get_string(base, "stun"));
            if (c->turnServers) free(c->turnServers);
            c->turnServers = _strdup(msg.get_string(base, "turn"));
            if (c->turnUsername) free(c->turnUsername);
            c->turnUsername = _strdup(msg.get_string(base, "turnUsr"));
            if (c->turnPassword) free(c->turnPassword);
            c->turnPassword = _strdup(msg.get_string(base, "turnPwd"));
            c->stunSlow = msg.get_bool(base, "stunSlow");

            const char * recordingUsrEncrypted = msg.get_string(base, "recordingUsrEncrypted");
            const char * recordingPwdEncrypted = msg.get_string(base, "recordingPwdEncrypted");

            if (recordingUsrEncrypted && recordingPwdEncrypted) {
                size_t usrLen = strlen(recordingUsrEncrypted) / 2 + 1;
                if (c->recordingUsername) free(c->recordingUsername);
                c->recordingUsername = (char*)malloc(usrLen);
                signaling->client->websocket->Decrypt(msg.get_string(base, "recordingUsrSeed"), recordingUsrEncrypted, c->recordingUsername, usrLen);

                size_t pwdLen = strlen(recordingPwdEncrypted) / 2 + 1;
                if (c->recordingPassword) free( c->recordingPassword);
                c->recordingPassword = (char*)malloc(pwdLen);
                signaling->client->websocket->Decrypt(msg.get_string(base, "recordingPwdSeed"), recordingPwdEncrypted, c->recordingPassword, pwdLen);
            }

            if (!presenceCall) {
                presenceCall = true;
                SignalingSigMessage msg(presenceSubscriptionCallId, "setup", alloca(1000), alloca(100));

                word ftys = msg.add_array(msg.sig, "fty");
                word fty = msg.add_object(ftys, 0);
                msg.add_string(fty, "type", "presence_subscribe");

                msg.add_int(msg.sig, "channel", 0, msg.tmp);

                word cd = msg.add_object(msg.sig, "cd");
                msg.add_string(cd, "sip", signaling->service->mediaConfig->currentUserSip);

                msg.Send(this);
            }
        }
        if (signaling->Sessions()) signaling->StartUpdate(new SignalingUpdate(SignalingRegistrationInfo, err ? "RegistrationDown" : "RegistrationUp", 0, signaling->filters));
        signaling->RegistrationState(err == 0);
    }
    else if (!strcmp(mt, "RegisterSync")) {
        if (this->calls) {
            class btree* b = this->calls->btree_find_next_right(0);
            while (b) {
                class SignalingCall* c = (class SignalingCall*)b;
                c->ignoreAddVideoReconnecting = true;
                c->ReconnectCall(true);
                b = this->calls->btree_find_next_right((void*)(intp)c->call);
            }
        }
    }
    else if (!strcmp(mt, "Unregister")) {
        signaling->RegisterToAltPbx();
    }
    else if (!strcmp(mt, "InnovaphoneData")) {
        signaling->InnovaphoneData(msg, base);
    }
    else if (!strcmp(mt, "Signaling")) {
        if (signaling->user && signaling->user->UseRemoteServices()) return;
        int call = msg.get_int(base, "call");
        word sig = msg.get_object(base, "sig");
        const char * type = msg.get_string(sig, "type");
        if (type) {
            if (call == presenceSubscriptionCallId) {
                if (!strcmp(type, "facility")) {
                    word fty = msg.get_array(sig, "fty");
                    if (fty != JSON_ID_NONE) {
                        word object = msg.get_object(fty, 0);
                        for (unsigned idx = 0; object != JSON_ID_NONE; idx++) {
                            const char* type = msg.get_string(object, "type");
                            if (type && !strcmp(type, "presence_notify")) {
                                const char* activity = msg.get_string(object, "activity");
                                if (activity && !strcmp(activity, "on-the-phone")) {
                                    signaling->log->Log(LOG_SIGNALING, "%x:SignalingRegistration::ClientMessage presence_notify on-the-phone", this);
                                    object = msg.get_object(fty, object);
                                    continue;
                                }
                                if (activity && !strcmp(activity, "dnd")) {
                                    signaling->log->Log(LOG_SIGNALING, "%x:SignalingRegistration::ClientMessage presence_notify dnd", this);
                                    dnd = true;
                                    if (signaling->user) signaling->user->SetDnd(dnd);
                                }
                                else {
                                    signaling->log->Log(LOG_SIGNALING, "%x:SignalingRegistration::ClientMessage presence_notify", this);
                                    dnd = false;
                                    if (signaling->user) signaling->user->SetDnd(dnd);
                                }
                                break;
                            }
                            object = msg.get_object(fty, object);
                        }
                    }
                }
                else if (!strcmp(type, "rel")) {
                    signaling->log->Log(LOG_SIGNALING, "%x:SignalingRegistration::Presence subscribe call was removed", this);
                }
            }
            if (call == diversionCallId) {
                word diversion_interrogate_result = find_fty(msg, sig, "diversion_interrogate_result");
                word diversion_deactivate_result = find_fty(msg, sig, "diversion_deactivate_result");
                word diversion_activate_result = find_fty(msg, sig, "diversion_activate_result");
                if (diversion_interrogate_result != JSON_ID_NONE && diversionSession) {
                    class SignalingRCCMessage * diversionInterrogateResult = new class SignalingRCCMessage("DiversionInterrogateResult", 0, diversionSession, alloca(1000), alloca(100));
                    for (unsigned index = 0; true; index++) {
                        word fty = find_fty(msg, sig, "diversion_interrogate_result", index);
                        if (fty == JSON_ID_NONE) break;
                        word list = msg.get_array(fty, "list");
                        if (list == JSON_ID_NONE) continue;
                        const char * procedure = 0, *num = 0, *sip = 0;
                        for (word object = msg.get_object(list, 0); object != JSON_ID_NONE; object = msg.get_object(fty, object)) {
                            procedure = msg.get_string(object, "procedure");
                            word diverted_to = msg.get_object(object, "diverted_to");
                            if (diverted_to != JSON_ID_NONE) {
                                num = msg.get_string(diverted_to, "num");
                                sip = msg.get_string(diverted_to, "sip");
                            }
                        }
                        if (procedure && (num || sip)) {
                            word obj = diversionInterrogateResult->add_object(diversionInterrogateResult->base, procedure);
                            if (num) diversionInterrogateResult->add_string(obj, "num", num);
                            if (sip) diversionInterrogateResult->add_string(obj, "sip", sip);
                        }
                    }
                    diversionInterrogateResult->Send();
                    delete diversionInterrogateResult;
                }
                else if (diversion_deactivate_result != JSON_ID_NONE && diversionSession) {
                    class SignalingRCCMessage * diversionDeactivateResult = new class SignalingRCCMessage("DiversionDeactivateResult", 0, diversionSession, alloca(1000), alloca(100));
                    diversionDeactivateResult->Send();
                    delete diversionDeactivateResult;
                }
                else if (diversion_activate_result != JSON_ID_NONE && diversionSession) {
                    class SignalingRCCMessage * diversionActivateResult = new class SignalingRCCMessage("DiversionActivateResult", 0, diversionSession, alloca(1000), alloca(100));
                    const char * procedure = msg.get_string(diversion_activate_result, "procedure");
                    diversionActivateResult->add_string(diversionActivateResult->base, "procedure", procedure);
                    int error = msg.get_int(diversion_activate_result, "error");
                    if (error) diversionActivateResult->add_int(diversionActivateResult->base, "error", error, diversionActivateResult->tmp);
                    diversionActivateResult->Send();
                    delete diversionActivateResult;
                }
                if (strcmp(type, "rel")) {
                    // release signaling connection
                    SignalingSigMessage msg(call, "rel", alloca(1000), alloca(100));
                    msg.Send(this);
                }
                return;
            }
            if (!strcmp(type, "setup")) {
                if (msg.get_int(sig, "channel") == -1) {
                    while (SignalingCall* signalingCall = FindCallByState(0)) {
                        signalingCall->AbortTone();
                        signalingCall->Delete();
                    }
                    int localCallId = 0;
                    const char* hwId = nullptr;
                    word ftyReconnectCall = find_fty(msg, sig, "reconnect_setup");
                    if (ftyReconnectCall != JSON_ID_NONE) {
                        localCallId = msg.get_unsigned(ftyReconnectCall, "id");
                        hwId = msg.get_string(ftyReconnectCall, "hw");
                    }
                    // after getting new registration we may get a call with same "call", already in the btree
                    class btree* b = calls->btree_find((void*)(intp)call);
                    if (b) {
                        class SignalingCall* c = (class SignalingCall*)b;
                        if (localCallId) {
                            debug->printf("%x:SignalingRegistration::ClientMessage(mt: %s) setup with same callId(!) %i reconnect_setup:%d==%d originalCall=%p", this, mt, call, localCallId, c->GetReconnectCallId(), c);
                            if (c->GetReconnectCallId() == localCallId) {
                                reconnectingCallDelete = c;
                                c->callNotInBtree = true;
                                calls = calls->btree_get(c);
                            }
                            else {
                                debug->printf("%x:SignalingRegistration::ClientMessage(mt:%s) reconnect call for an unknown id, release (%d,%d) (%s,%s)", this, mt, call, localCallId, this->hw ? this->hw : "", hwId ? hwId : "");
                                class SignalingSigMessage send(call, "rel", alloca(1000), alloca(100));
                                word cau = send.add_object(send.sig, "cau");
                                send.add_int(cau, "num", Q931_CAUSE_ServiceOrOptionNotAvailable_Unspecified, send.tmp);
                                send.Send(this);
                                return;
                            }
                        }
                        else { // New non-reconnect call, we leave the original pending, reconnect may arrive. 
                            class SignalingCall* c = (class SignalingCall*)b;
                            if (c->GetReconnectInitRecv() || c->GetReconnectSetupRecv()) {
                                pendingReconnectCall = c;
                                c->callNotInBtree = true;
                                calls = calls->btree_get(c);
                                c->StartReconnectRequestTimer(90000);
                            }
                            else {  // This can not happen, active reconnect with call == x and new incoming call with call == x too. 
                                debug->printf("%x:SignalingRegistration::ClientMessage(mt:%s) call found but no reconnect call, release (%d,%d) (%s,%s)", this, mt, call, localCallId, this->hw ? this->hw : "", hwId ? hwId : "");
                                class SignalingSigMessage send(call, "rel", alloca(1000), alloca(100));
                                word cau = send.add_object(send.sig, "cau");
                                send.add_int(cau, "num", Q931_CAUSE_UserBusy, send.tmp);
                                send.Send(this);
                                return;
                            }
                        }
                    }
                    // We had for instance "call":-2
                    // Reconnect Call arrives now with "call":-1 but localCallId may be the same
                    else if(localCallId) {
                        bool recCallFound = false;
                        class btree* bt = calls->btree_find_next_right(0);
                        while (bt) {
                            class SignalingCall* c = (class SignalingCall*)bt;
                            if (c->recCallId == localCallId) {
                                recCallFound = true;
                                break;
                            }
                            bt = calls->btree_find_next_right((void*)(intp)c->call);
                        }
                        if (!recCallFound && pendingReconnectCall && pendingReconnectCall->recCallId == localCallId) recCallFound = true;
                        if (!recCallFound || !hwId || !this->hw || strcmp(hwId, this->hw)) {
                            debug->printf("%x:SignalingRegistration::ClientMessage(mt:%s) call not found recCallFound:%d (%d,%d) (%s,%s)", this, mt, recCallFound, call, localCallId, this->hw ? this->hw : "", hwId ? hwId : "");
                            class SignalingSigMessage send(call, "rel", alloca(1000), alloca(100));
                            word cau = send.add_object(send.sig, "cau");
                            send.add_int(cau, "num", Q931_CAUSE_ServiceOrOptionNotAvailable_Unspecified, send.tmp);
                            send.Send(this);
                            return;
                        }
                    }
                }
            }
            class btree * b = calls->btree_find((void *)(intp)call);
            if (!b) {
                if (!strcmp(type, "setup")) {
                    bool ctSetupFty = false;
                    const char* confId = msg.get_string(sig, "confId");
                    SignalingCall* callWithConfId = confId ? signaling->FindCallByConfId(confId) : nullptr;
                    if (callWithConfId) {
                        word ftyCtSetup = find_fty(msg, sig, "ct_setup");
                        if (ftyCtSetup != JSON_ID_NONE) {
                            callWithConfId = nullptr;
                            ctSetupFty = true;
                        }
                    }
                    bool autoConnect = false;
                    word innoRemoteControl = find_fty(msg, sig, "innovaphone_remote_control");
                    if (innoRemoteControl != JSON_ID_NONE) {
                        const char* control = msg.get_string(innoRemoteControl, "control");
                        if (control && !strcmp(control, "CONNECT")) autoConnect = true;
                    }
                    int localCallId = 0;
                    word ftyReconnectCall = find_fty(msg, sig, "reconnect_setup");
                    if (ftyReconnectCall != JSON_ID_NONE) {
                        localCallId = msg.get_unsigned(ftyReconnectCall, "id");
                    }
                    // new inbound call
                    word fty_video_setup = find_fty(msg, sig, "video_setup");
                    if (fty_video_setup != JSON_ID_NONE) {
                        // keep this call id in mind as pass-through signaling
                        const char * mode = msg.get_string(fty_video_setup, "mode");
                        inboundVideoCalls.push_back(new SignalingInboundVideoCall(call, mode));
                        // pass-through to frontend for handling
                        signaling->UnknownCallMessage(type, msg, base);
                    }
                    else if (msg.get_int(sig, "channel") == 0) {
                        ControlCall(msg, base, mt, src);
                    }
                    else if(!localCallId && (((calls && calls->get_count() > 0) && !signaling->callWaiting && !autoConnect && !ctSetupFty) || (dnd && signaling->dndRejectCall) || callWithConfId)) {
                        class SignalingSigMessage send(call, "rel", alloca(1000), alloca(100));
                        word cau = send.add_object(send.sig, "cau");
                        send.add_int(cau, "num", Q931_CAUSE_UserBusy, send.tmp);
                        send.Send(this);
                        if ((calls && calls->get_count() > 0) && !signaling->callWaiting && !autoConnect) {
                            word fty = msg.get_array(sig, "fty");
                            word cg = msg.get_object(sig, "cg");
                            const char* num = nullptr;
                            const char* sip = nullptr;
                            const char* dn = nullptr;
                            if (cg != 0xffff) {
                                num = msg.get_string(cg, "num");
                                sip = msg.get_string(cg, "sip");
                            }
                            if (fty != 0xffff) {
                                for (word last = 0, f = msg.get_object(fty, last); f != 0xffff; f = msg.get_object(fty, last)) {
                                    const char* ftyType = msg.get_string(f, "type");
                                    if (ftyType) {
                                        if (!strcmp(ftyType, "name_identification")) {
                                            if (msg.get_string(f, "name")) {
                                                dn = msg.get_string(f, "name");
                                            }
                                        }
                                    }
                                }
                            }
                            unsigned offset = ExternalLinePrefixOffset(num);
                            signaling->user->ReportMissedCall(confId, num + offset, offset, sip, dn, CALL_ENDED_REASON_USER_BUSY);
                        }
                    }
                    else {
                        // Close Test Devices if opened!
                        signaling->IncomingCall();
                        class SignalingCall* c = new SignalingCall(this, CallTypeVoice, call, 0, 0, 0, false);
                        signaling->CallAdded(c, 0);
                        b = c;
                    }
                }
                else {
                    class SignalingInboundVideoCall * inboundVideoCall = inboundVideoCalls.front();
                    while (inboundVideoCall) {
                        if (inboundVideoCall->id == call) break;
                        inboundVideoCall = inboundVideoCall->goNext();
                    }
                    if (inboundVideoCall) {
                        if (!strcmp(type, "rel")) {
                            inboundVideoCall->remove();
                            delete inboundVideoCall;
                        }
                    }
                    signaling->UnknownCallMessage(type, msg, base);
                    return;
                }
            }
            if (b) {
                // "call":-1 is being disconnected and waits for MediaCloseComplete
                // A ControlCall then arrives with "call":-1 and channel == 0
                // Call should be processed separately, nothing to do with the audio call
                if (!strcmp(type, "setup") && msg.get_int(sig, "channel") == 0) {
                    ControlCall(msg, base, mt, src);
                }
                else {
                    const char* cmd = msg.get_string(sig, "channels_cmd");
                    if (cmd && (((class SignalingCall*)b)->IsCloseState() || ((class SignalingCall*)b)->IsConnectingState()) && !strcmp(cmd, "SELECT")) {
                        debug->printf("%x:SignalingRegistration::ClientMessage(mt: %s) ignore SELECT in CLOSE(%d) or connecting(%d) state", this, mt, ((class SignalingCall*)b)->IsCloseState(), ((class SignalingCall*)b)->IsConnectingState());
                    }
                    else ((class SignalingCall *)b)->Message(type, msg, sig);
                }
            }
        }
    }
    else if (!strcmp(mt, "CheckCallResult")) {
        const char * uuid = msg.get_string(base, "confID");
        bool ok = msg.get_bool(base, "ok");
        if (!ok && signaling->user) {
            signaling->user->ReportCallEnded(uuid, CALL_ENDED_REASON_ANSWERED_ELSEWHERE);
        }
    }
}

void SignalingRegistration::ControlCall(class json_io& msg, word base, const char* mt, const char* src)
{
    int call = msg.get_int(base, "call");
    bool sendRel = true;

    signaling->log->Log(LOG_SIGNALING, "%x:SignalingRegistration::ControlCall reg=%x (hw=%s) this->hw=%s", this, signaling->registration, hw ? hw : "-", this->hw ? this->hw : "-");

    word sig = msg.get_object(base, "sig");
    if (sig != 0xffff) {
        word ftys = msg.get_array(sig, "fty");
        if (ftys != 0xffff) {
            word fty = msg.get_object(ftys, 0);
            while (fty) {
                if (const char* type = msg.get_string(fty, "type")) {
                    signaling->log->Log(LOG_SIGNALING, "%x:SignalingRegistration::ControlCall type=%s", this, type);
                    if (!strcmp(type, "mwi_activate")) {
                        // show MWI in UI
                    }
                    if (!strcmp(type, "mwi_deactivate")) {
                        // hide MWI in UI
                    }
                    if (!strcmp(type, "diverting_leg4")) {
                        const char * reason = msg.get_string(fty, "reason");
                        if (diversionSession) {
                            // notify frontend so it will start another interrogation
                            class SignalingRccSession * session = diversionSession;
                            class SignalingRCCMessage * diversionsChanged = new class SignalingRCCMessage("DiversionsChanged", 0, session, alloca(1000), alloca(100));
                            diversionsChanged->add_string(diversionsChanged->base, "procedure", reason);
                            diversionsChanged->Send();
                        }
                    }
                }
                fty = msg.get_object(ftys, fty);
            }
        }
    }
    if (sendRel) {
        class SignalingSigMessage send(call, "rel", alloca(1000), alloca(100));
        send.Send(this);
    }
}

void SignalingRegistration::Update(const char * hw, const char* phys, unsigned timeout)
{
    signaling->log->Log(LOG_SIGNALING, "%x:SignalingRegistration::Update reg=%x (hw=%s) this->hw=%s timeout=%d phys=%s this->phys=%s", this, signaling->registration, hw ? hw : "-", this->hw ? this->hw : "-", timeout, phys && phys[0] ? phys : "-", this->phys ? this->phys : "-");
    if (hw) {
        if (timeout) this->timeout = timeout;
        if (signaling->client && signaling->client->domain && phys && phys[0]) {
            if (this->phys) free(this->phys);
            if (strstr(phys, signaling->client->domain)) {
                this->phys = _strdup(phys);
            }
            else {
                this->phys = (char*)malloc(strlen(signaling->client->domain) + 1 + strlen(phys) + 1);
                _sprintf(this->phys, "%s@%s", signaling->client->domain, phys);
            }
        }
        if (!this->hw) {
            this->hw = _strdup(hw);
            this->hwTimeout = this->timeout;
            signaling->AddClientMonitor(this);
        }
        else if (strcmp(this->hw, hw)) {
            UnRegister();
            class SignalingRegistration* r = new SignalingRegistration(signaling, this->phys);
            r->Update(hw, phys, this->timeout);
            signaling->registration = r;
            // Update registration pointer in actual calls
            if (this->calls) {
                class btree* b = this->calls->btree_find_next_right(0);
                while (b) {
                    class SignalingCall* c = (class SignalingCall*)b;
                    debug->printf("%x:SignalingRegistration::Update call:%x", this, c);
                    c->ResetRegistration(r);
                    b = this->calls->btree_find_next_right((void*)(intp)c->call);
                }
                r->calls = this->calls;
                this->calls = 0;
            }
            if (this->activePhoneCall) {
                r->activePhoneCall = this->activePhoneCall;
                this->activePhoneCall = 0;
            }
            delete this;
        }
        else if(registered) signaling->SaveRegistration(this->hw, this->phys, timeout);
    }
    else {
        delete this;
    }
}

void SignalingRegistration::Sync(class json_io & msg, word base)
{
    if (registered) {
        word handle = msg.add_object(base, "sync");
        msg.add_string(handle, "mt", "RegistrationUp");
    }
}

void SignalingRegistration::UnRegister()
{
    signaling->log->Log(LOG_SIGNALING, "%x:SignalingRegistration::UnRegister()", this);
    if (registered) {
        class SignalingRegMessage reg("UnRegister", src, alloca(1000));
        reg.Send(this);
    }
}

void SignalingRegistration::TimerOnTimeout(ITimer * timer)
{
    Register();
    if (timeout < TIMEOUT_MAX) timeout *= 2;
}

SignalingCall * SignalingRegistration::FindCallByState(word state)
{
    SignalingCall *call = nullptr;
    while ((call = (class SignalingCall *)calls->btree_find_next_right(call ? (void*)(intp)call->call : 0))) {
        if (call->state == state) {
            return call;
        }
    }
    return NULL;
}

void SignalingRegistration::StateChanged(SignalingCall* call)
{
    static int inStateChanged = 0;
    if (inStateChanged == call->call) debug->printf("DEBUG keep from recursion on call %i", call->call);
    if (inStateChanged == call->call) return; // keep from recursive calling
    else inStateChanged = call->call;

    signaling->log->Log(LOG_SIGNALING, "SignalingRegistration::StateChanged() call=%i state=%x", call->call, call->state);

    class SignalingCall* calling = 0, * connected = 0, * holding = 0, * alerting = 0, * waiting = 0, * terminated = 0;

    class btree* b = this->calls->btree_find_next_right(0);
    while (b) {
        class SignalingCall* c = (class SignalingCall*)b;
        if (c->state && (c->state & 0xFF) <= SIG_CALL_STATE_ALERT) calling = c;
        if (c->state == SIG_CALL_STATE_CONN) connected = c;
        if (c->state == (SIG_CALL_STATE_CONN | SIG_CALL_STATE_OUTG)) connected = c;
        if (c->state == (SIG_CALL_STATE_ALERT | SIG_CALL_STATE_OUTG)) alerting = c;
        if (c->state == (SIG_CALL_STATE_ALERT | SIG_CALL_STATE_OUTG | SIG_CALL_STATE_CALL_WAITING) && !waiting) waiting = c;
        if (c->state & SIG_CALL_STATE_HOLD) holding = c;
        if (!c->state) terminated = c;
        b = this->calls->btree_find_next_right((void*)(intp)c->call);
    }
    signaling->log->Log(LOG_SIGNALING, "DEBUG calling=%i connected=%i holding=%i alerting=%i waiting=%i dnd=%i", calling ? calling->call : 0, connected ? connected->call : 0, holding ? holding->call : 0, alerting ? alerting->call : 0, waiting ? waiting->call : 0, dnd);

    if (connected) activePhoneCall = connected;
    else if (terminated) activePhoneCall = terminated;
    else if (calling) activePhoneCall = calling;
    else if (alerting) activePhoneCall = alerting;
    else if (holding) activePhoneCall = holding;
    else activePhoneCall = 0;

    if (calling || connected) {
        if (alerting) {
            debug->printf("DEBUG Turn alerting call %i into waiting call", alerting->call);
            alerting->callWaiting = true;
            alerting->SendAlert();
            alerting->StopRinging();
            if (signaling->callWaiting != CallWaitingSilent) {
                class IAudioIo * audioIo = alerting->GetAudioIo();
                if (audioIo) MediaTones::StartTone(MEDIA_TONES_CALLWAITING, dialingLocation.tones, audioIo);
            }
        }
        else if (waiting && connected) {
            // not for calling as we would terminate its ringback tone
            if (signaling->callWaiting != CallWaitingSilent) {
                class IAudioIo * audioIo = waiting->GetAudioIo();
                if (audioIo) MediaTones::StartTone(MEDIA_TONES_CALLWAITING, dialingLocation.tones, audioIo);
            }
        }
    }
    else if (alerting) {
        if (!dnd && signaling->user) signaling->user->RequestStartRinging(alerting->GetConfId());
    }
    else if (holding) {
        // no auto-retrieve
    }
    else if (waiting) {
        debug->printf("DEBUG Turn waiting call %i into alerting call", waiting->call);
        waiting->callWaiting = false;
        waiting->SendAlert();
        if (waiting->GetAudioIo()) MediaTones::StopTone(waiting->GetAudioIo());
        if (!dnd && signaling->user) signaling->user->RequestStartRinging(waiting->GetConfId());
    }

    inStateChanged = 0;
}

const char * SignalingRegistration::AddExternalLinePrefix(const char * number, char * buffer, unsigned length)
{
    if(!number) return number;
    // dialingLocation: "000", "00", "0"
    // dialingLocation: "900", "90", "9"

    const char * prefixIntl = dialingLocation.prefixIntl ? dialingLocation.prefixIntl : "";
    const char * prefixNtl = dialingLocation.prefixNtl ? dialingLocation.prefixNtl : "";
    const char * prefixSubs = dialingLocation.prefixSubs ? dialingLocation.prefixSubs : "";
    unsigned maxIntern = dialingLocation.maxIntern ? dialingLocation.maxIntern : 6;

    if (!strncmp(number, "+", strlen("+")) ) {
        // fully qualified international number
        return number;
    }
    if (strlen(prefixIntl) > 0 && !strncmp(number, prefixIntl, strlen(prefixIntl)) ) {
        // international number with external line prefix
        return number;
    }

    if (strlen(prefixIntl) > 0 && strlen(prefixSubs) > 0 && !strncmp(prefixIntl, prefixSubs, strlen(prefixSubs))) {
        prefixIntl = prefixIntl + strlen(prefixSubs);
    }

    if (strlen(prefixNtl) > 0 && strlen(prefixSubs) > 0 && !strncmp(prefixNtl, prefixSubs, strlen(prefixSubs))) {
        prefixNtl = prefixNtl + strlen(prefixSubs);
    }

    if (strlen(prefixIntl) > 0 &&  !strncmp(number, prefixIntl, strlen(prefixIntl)) ) {
        // international number without external line prefix
        // national number with external line prefix
        _snprintf(buffer,length,"%s%s",prefixSubs, number);
        return buffer;
    }
    else if (strlen(prefixNtl) > 0 && !strncmp(number, prefixNtl, strlen(prefixNtl)) ) {
        // national number without external line prefix
        // subscriber number with external line prefix
        _snprintf(buffer,length,"%s%s",prefixSubs, number);
        return buffer;
    }
    else if (strlen(number) > maxIntern) {
        // subscriber number without external line prefix
        _snprintf(buffer,length,"%s%s",prefixSubs, number);
        return buffer;
    }
    return number;
}

int SignalingRegistration::CheckCountryCode(const char* num)
{
    for (int i = 0; CountryCodes[i] != nullptr; i++) {
        if (!strncmp(num, CountryCodes[i], strlen(CountryCodes[i]))) return strlen(CountryCodes[i]);
    }
    return 0;
}

// buffer contains normalized number 00 instead of 000
bool SignalingRegistration::RemoveDeppenNull(const char * number, char * buffer, unsigned length)
{
    const char * prefixIntl = dialingLocation.prefixIntl ? dialingLocation.prefixIntl : "";
    const char * country = dialingLocation.country ? dialingLocation.country : "";
    int prefixIntlLen = strlen(prefixIntl);
    int countrylLen = strlen(country);
    int numberLen = strlen(number);
    int prefixLen = 0;

    if (!prefixIntlLen || !countrylLen || ((prefixIntlLen + countrylLen) >= numberLen)) {
        return false;
    }

    if (number[0] == '+') prefixLen = 1;
    else if (prefixIntl && !strncmp(number, prefixIntl, prefixIntlLen)) prefixLen = prefixIntlLen;

    if (prefixLen) {
        if ((!strncmp(&number[prefixLen], "39", 2) || !strncmp(&number[prefixLen], "242", 3) || !strncmp(&number[prefixLen], "800", 3) || !strncmp(&number[prefixLen], "808", 3) || !strncmp(&number[prefixLen], "992", 3))) {
            return false;
        }
        buffer[0] = 0;
        int i;
        if (number[0] == '+') i = _sprintf(buffer, "+");
        else if (!strncmp(number, prefixIntl, prefixIntlLen)) i = _sprintf(buffer, "%s", prefixIntl);
        else i = 0;
        int ccLen = CheckCountryCode(&number[prefixLen]);
        memcpy(&buffer[i], &number[i], ccLen);
        i += ccLen;
        if (number[i] == 0x30) _sprintf(&buffer[i], "%s", &number[i + 1]);
        else _sprintf(&buffer[i], "%s", &number[i]);
        debug->printf("SignalingRegistration::RemoveDeppenNull RemoveDeppenNull number=%s intPrefix=%d ccLen=%d", buffer, prefixLen, ccLen);
        return true;
    }
    return false;
}

unsigned SignalingRegistration::ExternalLinePrefixOffset(const char * number)
{
    if( (dialingLocation.prefixSubs &&
        strlen(dialingLocation.prefixSubs) > 0 &&
        number &&
        !strncmp(number, dialingLocation.prefixSubs, strlen(dialingLocation.prefixSubs)) &&
        (strchr(number, ',') ? strchr(number, ',') - number : strlen(number)) >= strlen(dialingLocation.prefixSubs) + 7) ||
        ( number && dialingLocation.prefixNtl && !strncmp(number, dialingLocation.prefixNtl, strlen(dialingLocation.prefixNtl)) ) ||
        ( number && dialingLocation.prefixIntl && !strncmp(number, dialingLocation.prefixIntl, strlen(dialingLocation.prefixIntl)) ) )
    {
        return dialingLocation.prefixSubs ? strlen(dialingLocation.prefixSubs) : 0;
    }
    return 0;
}

bool SignalingRegistration::IsExternalCall(const char * number)
{
    if(!number) return false;
    // dialingLocation: "000", "00", "0"
    // dialingLocation: "900", "90", "9"

    const char * prefixIntl = dialingLocation.prefixIntl ? dialingLocation.prefixIntl : "";
    const char * prefixNtl = dialingLocation.prefixNtl ? dialingLocation.prefixNtl : "";
    const char * prefixSubs = dialingLocation.prefixSubs ? dialingLocation.prefixSubs : "";
    unsigned maxIntern = dialingLocation.maxIntern ? dialingLocation.maxIntern : 6;

    if (!strncmp(number, "+", strlen("+")) ) {
        // fully qualified international number
        return true;
    }
    if (strlen(prefixIntl) > 0 && !strncmp(number, prefixIntl, strlen(prefixIntl)) ) {
        // international number with external line prefix
        return true;
    }

    if (strlen(prefixIntl) > 0 && strlen(prefixSubs) > 0 && !strncmp(prefixIntl, prefixSubs, strlen(prefixSubs))) {
        prefixIntl = prefixIntl + strlen(prefixSubs);
    }

    if (strlen(prefixNtl) > 0 && strlen(prefixSubs) > 0 && !strncmp(prefixNtl, prefixSubs, strlen(prefixSubs))) {
        prefixNtl = prefixNtl + strlen(prefixSubs);
    }

    if (strlen(prefixIntl) > 0 &&  !strncmp(number, prefixIntl, strlen(prefixIntl)) ) {
        // international number without external line prefix
        // national number with external line prefix
        return true;
    }
    else if (strlen(prefixNtl) > 0 && !strncmp(number, prefixNtl, strlen(prefixNtl)) ) {
        // national number without external line prefix
        // subscriber number with external line prefix
        return true;
    }
    else if (strlen(number) > maxIntern) {
        // subscriber number without external line prefix
        return true;
    }
    return false;
}

void SignalingRegistration::toggleDnd()
{
    // publish DND presence
    SignalingSigMessage msg(presencePublishCallId, "setup", alloca(1000), alloca(100));
    word cd = msg.add_object(msg.sig, "cd");
    msg.add_string(cd, "sip", signaling->service->mediaConfig->currentUserSip);
    msg.add_int(msg.sig, "channel", 0, msg.tmp);
    word ftys = msg.add_array(msg.sig, "fty");
    word fty = msg.add_object(ftys, 0);
    msg.add_string(fty, "type", "presence_publish");
    msg.add_string(fty, "contact", "tel:");
    msg.add_string(fty, "activity", dnd ? nullptr : "dnd");
    msg.Send(this);

    // will receive "presence_notify" on presenceSubscriptionCallId in return
}

SignalingRegMessage::SignalingRegMessage(const char * mt, const char * src, void * sb) : JsonIo((char *)sb)
{
    this->sb = (char *)sb;
    base = add_object(0xffff, 0);
    add_string(base, "mt", mt);
    add_string(base, "src", src);
}

void SignalingRegMessage::Send(class SignalingRegistration * registration)
{
    registration->MessageSend(*this, base, sb);
}

/*-----------------------------------------------------------------------------------------------*/

static istd::list<class CodecAssignments> assignmentsList;

SignalingCall::SignalingCall(class SignalingRegistration * registration, enum CallType callType, int call, const char * num, const char * sip, const char * dn, bool sendingComplete) :
    remote(num, sip, dn), diverting(0, 0, 0), originalCalled(0, 0, 0), transfering(0, 0, 0),
    toneEndTimer(registration->signaling->service->iomux, this),
    autoConnectTimer(registration->signaling->service->iomux, this),
    mediaInfoInterval(registration->signaling->service->iomux, this),
    queuedDTMFTimer(registration->signaling->service->iomux, this),
    watingCallToneTimer(registration->signaling->service->iomux, this),
    hookDeviceTimer(registration->signaling->service->iomux, this),
    reconnectCallTimer(registration->signaling->service->iomux, this),
    audioRtpReceived(registration->signaling->service->iomux, this),
    reconnectCallResponse(registration->signaling->service->iomux, this),
    reconnectCallRequest(registration->signaling->service->iomux, this),
    mediaLostToneTimer(registration->signaling->service->iomux, this),
    signalingTestTimer(registration->signaling->service->iomux, this)
{
    if (registration->signaling->user && registration->signaling->user->UseRemoteClientServices()) registration->signaling->GetSignalingRemote()->CreateCall(this);
    this->registration = registration;
    this->audioIo = registration && registration->signaling ? registration->signaling->GetAudioIo() : NULL;
    this->videoIo = registration && registration->signaling ? registration->signaling->GetVideoIo() : NULL;
    this->ringerIo = registration && registration->signaling ? registration->signaling->GetRingerIo() : NULL;
    this->mediaProvider = registration && registration->signaling ? registration->signaling->GetMediaProvider() : NULL;
    this->mediaProviderVideo = registration && registration->signaling ? registration->signaling->service->mediaProvider : NULL;
    this->audioCoder = CODER_UNDEFINED;
    this->videoCoder = CODER_UNDEFINED;
    this->videoCoderBeforeHold = CODER_UNDEFINED;
    this->videoDeviceIdBeforeHold = NULL;
    this->callType = callType;
    this->sendingComplete = sendingComplete;
    if (!call) {
        call = firstCallId;
        for (class btree * b = registration->calls->btree_find((void *)(intp)call); b; call++, b = registration->calls->btree_find((void *)(intp)call));
    }
    this->call = call;
    registration->calls = registration->calls->btree_put(this);
    state = 0;
    cause = 0;
    media = 0; // for AUDIO
    media2 = 0; // for VIDEO
    localAudioConfig = 0;
    localVideoConfig = 0;
    remoteAudioConfig = 0;
    remoteVideoConfig = 0;
    remoteMediaCmd = MEDIA_CMD_REQUEST;
    localDTMF = DEFAULT_PAYLOAD_TYPE_DTMF;
    remoteDTMF = 0;
    connPostponed = false;
    alertPostponedOnHookDevice = false;
    connPostponedOnHookDevice = false;
    updateHookDevice = false;
    connReceived = false;
    selectProposalByConn = false;
    videoAllowed = registration->signaling->videoLicense ? registration->signaling->startVideo : false;
    videoDisabled = registration->signaling->videoLicense ? false : true;
    sendVideoProposal = false;
    callWaiting = false;
    initializing = 0;
    isMediaInitialized = false;
    connecting = 0;
    audioIoChannel = 0;
    videoIoChannel = 0;
    media2Closing = 0;
    mediaClosing = 0;
    videoIoChannelClosing = 0;
    rtptp = 0;
    fty_ct_initiate.zerioze();
    ctIdentify = 0;
    ctInitiate = 0;
    alertPresence = NULL;
    deleting = false;
    hookAudioDeviceId = 0;
    audioDeviceId = 0;
    videoDeviceId = 0;
    ringerDeviceId = 0;
    ringertoneIdent = 0;
    conferenceGuid = 0;
    conferenceType = 0;
    multiVideoSupport = false;
    remoteVideoPriority = false;
    announce = false;
    muted = false;
    hookState = 0;
    tx.jitter = tx.loss = tx.roundTrip = 0;
    rx.jitter = rx.loss = rx.roundTrip = 0;
    ct_setup_id = 0;
    confId = (char *)malloc(2 * sizeof(this->conf) + 1);
    confId[0] = 0;
    reconnectConfId = nullptr;
    numAudioPackets = numVideoPackets = 0;
    lastNumAudioPackets = lastNumVideoPackets = lastNumRtpAudioPackets = 0;
    audioDataOff = videoDataOff = false;
    numNoRtpPackets = 0;
    this->allocatedTones = 16;
    this->tones = (struct AudioIoDualTone*)(malloc(this->allocatedTones * sizeof(struct AudioIoDualTone)));
    this->duration = 0;
    this->audioMediaConnected = false;
    replacedCall = 0;
    replacementCall = 0;
    this->recordingInfo.initialize();
    this->noAudioIceCandidates = false;
    this->noVideoIceCandidates = false;
    this->closeState = false;
    this->connectingState = false;
    this->isRccCall = false;
    this->recCallId = registration ? registration->GetNextCallId() : 0;
    this->remoteCallId = 0;
    this->remoteHwId = nullptr;
    this->remoteSipId = nullptr;
    this->localCallId = 0;
    this->localHwId = nullptr;
    this->reconnectingCall = nullptr;
    this->reconnectedCall = nullptr;
    this->reconnectInitRecv = false;
    this->reconnectSetupRecv = false;
    this->sigRelSent = false;
    this->callNotInBtree = false;
    this->reconnectTimeout = 1250;
    this->failedReconnect = false;
    this->ignoreAddVideoReconnecting = false;

    if (assignmentsList.empty()) {
        assignmentsList.push_back(new class CodecAssignments(G711_A, DEFAULT_PAYLOAD_TYPE_G711_A));
        assignmentsList.push_back(new class CodecAssignments(G711_U, DEFAULT_PAYLOAD_TYPE_G711_U));
        assignmentsList.push_back(new class CodecAssignments(G722, DEFAULT_PAYLOAD_TYPE_G722));
        assignmentsList.push_back(new class CodecAssignments(OPUS_NB, DEFAULT_PAYLOAD_TYPE_OPUS));
        assignmentsList.push_back(new class CodecAssignments(OPUS_WB, DEFAULT_PAYLOAD_TYPE_OPUS));
        assignmentsList.push_back(new class CodecAssignments(G729, DEFAULT_PAYLOAD_TYPE_G729));
        assignmentsList.push_back(new class CodecAssignments(G729A, DEFAULT_PAYLOAD_TYPE_G729));
        assignmentsList.push_back(new class CodecAssignments(G729B, DEFAULT_PAYLOAD_TYPE_G729));
        assignmentsList.push_back(new class CodecAssignments(G729AB, DEFAULT_PAYLOAD_TYPE_G729));
        assignmentsList.push_back(new class CodecAssignments(VP8, DEFAULT_PAYLOAD_TYPE_VP8));
        assignmentsList.push_back(new class CodecAssignments(VP9, DEFAULT_PAYLOAD_TYPE_VP9));
        assignmentsList.push_back(new class CodecAssignments(AV1, DEFAULT_PAYLOAD_TYPE_AV1));
        assignmentsList.push_back(new class CodecAssignments(H264, DEFAULT_PAYLOAD_TYPE_H264));
    }
    debug->printf("%x/%i:SignalingCall()", this, call);
}

SignalingCall::~SignalingCall()
{
    debug->printf("%x/%i:~SignalingCall()", this, call);
    if (ctInitiate) ctInitiate->ctIdentify = 0;
    if (ctIdentify) ctIdentify->ctInitiate = 0;
    if (conferenceGuid) free(conferenceGuid);
    if (hookAudioDeviceId) free(hookAudioDeviceId);
    if (audioDeviceId) free(audioDeviceId);
    if (videoDeviceId) free(videoDeviceId);
    if (ringerDeviceId) free(ringerDeviceId);
    if (ringertoneIdent) free(ringertoneIdent);
    if (tones) free(tones);
    if (confId) free(confId);
    if (reconnectConfId) free(reconnectConfId);
    if (remoteHwId) free(remoteHwId);
    if (remoteSipId) free(remoteSipId);
    if (localHwId) free(localHwId);
    reconnectingCall = nullptr;
    reconnectedCall = nullptr;
    toneEndTimer.Cancel();
    autoConnectTimer.Cancel();
    mediaInfoInterval.Cancel();
    queuedDTMFTimer.Cancel();
    watingCallToneTimer.Cancel();
    hookDeviceTimer.Cancel();
    reconnectCallTimer.Cancel();
    audioRtpReceived.Cancel();
    reconnectCallResponse.Cancel();
    reconnectCallRequest.Cancel();
    mediaLostToneTimer.Cancel();
    signalingTestTimer.Cancel();
    while (dtmfs.front()) delete dtmfs.front();
    conferenceGuid = 0;
    recordingInfo.cleanup();
    if (alertPresence) delete alertPresence;
}

void SignalingCall::RccSessionOpened()
{
    debug->printf("%x/%i:SignalingCall::RccSessionOpened() reg=%x sig=%x videoAllowed=%d startVideo=%d calls=%d", this, call, registration, registration ? registration->signaling : 0, videoAllowed, registration && registration->signaling ? registration->signaling->startVideo : 0, registration && registration->calls ? registration->calls->get_count() : 0);
}

void SignalingCall::RccSessionClosed()
{
    debug->printf("%x/%i:SignalingCall::RccSessionClosed() active calls=%d", this, call, registration->calls->get_count());
    if (registration) {
        if ((registration->calls->get_count() < 2) || (!registration->signaling->uiRunning)) {
            if (videoIo && videoDeviceId) {
                videoIo->StopDevice((void*)this, videoDeviceId);
                free(videoDeviceId);
                videoDeviceId = NULL;
                videoIo->UnshareAllApplications((void*)this);
            }
        }
        if (registration->calls->get_count() < 2) {
            StopRinging();
        }
    }
}

void SignalingCall::HookDeviceAcquired()
{
    debug->printf("%x/%i:SignalingCall::HookDeviceAcquired media=%p postponed=%d,%d updateHookDevice=%d state=%x", this, call, media, alertPostponedOnHookDevice, connPostponedOnHookDevice, updateHookDevice, state);
    if (updateHookDevice) {
        if (audioDeviceId) {
            registration->signaling->SendHookKey(audioDeviceId, KEY_OFFHOOK, call, remote.num ? remote.num : remote.sip, remote.dn);
            if(hookState != KEY_OFFHOOK) registration->signaling->SendHookKey(audioDeviceId, hookState, call, remote.num ? remote.num : remote.sip, remote.dn);
        }
        updateHookDevice = false;
    }
    if (alertPostponedOnHookDevice) {
        if (media) MediaInitialized();
        alertPostponedOnHookDevice = false;
    }
    if (connPostponedOnHookDevice) {
        SendConn();
        connPostponedOnHookDevice = false;
    }
}

void SignalingCall::HookDeviceDenied()
{
    debug->printf("%x/%i:SignalingCall::HookDeviceDenied postponed=%d,%d", this, call, alertPostponedOnHookDevice, connPostponedOnHookDevice);
    if(audioDeviceId) free(audioDeviceId);
    audioDeviceId = 0;
    if (alertPostponedOnHookDevice) {
        if (media) MediaInitialized();
        alertPostponedOnHookDevice = false;
    }
    if (isMediaInitialized) {
        char confIdString[2 * sizeof(conf) + 1];
        str::from_hexmem(conf.b, sizeof(conf), confIdString);

        char cfg[1024];
        class json_io send(cfg);
        word sendBase = send.add_object(0xffff, 0);
        send.add_string(sendBase, "channel", confIdString);
        send.add_string(sendBase, "errorText", "Access denied to hook device");
        send.add_string(sendBase, "errorId", "hook_device_denied");

        MonitorXmit("hook_device_denied", send, sendBase);
    }
    if (connPostponedOnHookDevice) {
        SendRel();
        connPostponedOnHookDevice = false;
    }
}

void SignalingCall::HookDeviceNotFound()
{
    debug->printf("%x/%i:SignalingCall::HookDeviceNotFound postponed=%d,%d", this, call, alertPostponedOnHookDevice, connPostponedOnHookDevice);
    if (audioDeviceId) free(audioDeviceId);
    audioDeviceId = 0;
    if (alertPostponedOnHookDevice) {
        if (media) MediaInitialized();
        alertPostponedOnHookDevice = false;
    }
    if (isMediaInitialized) {
        char confIdString[2 * sizeof(conf) + 1];
        str::from_hexmem(conf.b, sizeof(conf), confIdString);

        char cfg[1024];
        class json_io send(cfg);
        word sendBase = send.add_object(0xffff, 0);
        send.add_string(sendBase, "channel", confIdString);
        send.add_string(sendBase, "errorText", "Hook device not found");

        MonitorXmit("hook_device_not_found", send, sendBase);
    }
    if (connPostponedOnHookDevice) {
        SendRel();
        connPostponedOnHookDevice = false;
    }
}

void SignalingCall::LookupInfo(class json_io& msg, word base)
{
    const char * dn = msg.get_string(base, "dn");
    word contact = msg.get_object(base, "contact");
    const char * company = (contact != 0xffff) ? msg.get_string(contact, "company") : 0;

    int lookUpLen = dn ? strlen(dn) : 0;
    if(company) lookUpLen += strlen(company);

    if (!lookUpLen) {
        debug->printf("%x/%i:SignalingCall::LookupInfo no result provided dn:%p company:%p", this, call, dn, company);
        return;
    }

    char* lookupDn = (char*)malloc(lookUpLen + 2);
    if (company) _sprintf(lookupDn, "%s %s", dn, company);
    else _sprintf(lookupDn, "%s", dn);

    debug->printf("%x/%i:SignalingCall::LookupInfo dn:%s last:%s", this, call, lookupDn ? lookupDn : "", remote.dn ? remote.dn : "");

    if (lookupDn && (!remote.dn || strcmp(remote.dn, lookupDn))) {
        remote.SetDn(lookupDn);

        if (registration && registration->signaling->user) {
            unsigned offset = registration->ExternalLinePrefixOffset(remote.num);
            char remoteSip[1024];
            registration->signaling->user->ReportNameIdentification(confId, remote.num + offset, registration->signaling->AppendSipDomain(remote.sip, remoteSip, sizeof(remoteSip)), lookupDn,
                this->diverting.dn ? this->diverting.dn : this->diverting.sip ? this->diverting.sip : this->diverting.num ? this->diverting.num : NULL,
                this->originalCalled.dn ? this->originalCalled.dn : this->originalCalled.sip ? this->originalCalled.sip : this->originalCalled.num ? this->originalCalled.num : NULL,
                videoAllowed, true, true, true, true, reconnectedCall ? true : false);
        }
        MonitorXmit("contact", msg, base);
    }

    free(lookupDn);
}

void SignalingCall::MonitorSync()
{
    debug->printf("%x/%i:SignalingCall::MonitorSync", this, call);
    for (class SignalingCallMonitor* m = monitors.front(); m; m = m->goNext()) {
        m->MonitorSync();
    }
}

void SignalingCall::StartReconnectRequestTimer(int timeout, bool restart)
{
    if(restart || reconnectCallRequest.IsRunning() == false) reconnectCallRequest.Start(timeout);
}

void SignalingCall::StartReconnectTimer()
{
    reconnectCallTimer.Start(reconnectTimeout);
    reconnectTimeout = (reconnectTimeout >= 2000) ? reconnectTimeout : (reconnectTimeout + 250);
}

void SignalingCall::CreateReconnectCall()
{
    if (registration && registration->signaling && registration->clientUp && registration->registered && !this->reconnectingCall) {
        const char* e164 = remote.num;
        const char* h323 = remote.sip;
        const char* dn = remote.dn;

        debug->printf("%x/%i:SignalingCall::CreateReconnectCall reconnect call to num=%s sip=%s", this, call, e164 ? e164 : "", h323 ? h323 : "");

        class SignalingCall* newCall = new SignalingCall(registration, CallTypeVoice, 0, e164, h323, dn, sendingComplete);
        if (registration->signaling->videoLicense) {
            newCall->videoAllowed = videoAllowed;
        }
        registration->signaling->CallAdded(newCall, 0);
        this->reconnectingCall = newCall;
        newCall->reconnectedCall = this;
        newCall->StartReconnectCall(audioIo, videoIo, 0, exclude);
    }
    else StartReconnectTimer();
}

void SignalingCall::StartReconnectCall(class IAudioIo* audioIo, class IVideoIo* videoIo, class URTPTP* rtptp, class SignalingRegistrationMonitor* exclude)
{
    IGuuid::GenerateBinaryUUID(&conf);
    str::from_hexmem(conf.b, sizeof(conf), this->confId);

    registration->signaling->log->Log(LOG_SIGNALING, "%x/%i:SignalingCall::StartReconnectCall() videoAllowed=%u confId=%s", this, call, videoAllowed, this->confId);

    this->audioIo = audioIo;
    this->videoIo = videoIo;
    this->rtptp = rtptp;
    this->exclude = exclude;

    MediaInitialize(MediaType::AUDIO);

    if (videoIo && videoAllowed) {
        videoIoChannel = videoIo->CreateChannel(this);
        MediaInitialize(MediaType::VIDEO);
    }
}

void SignalingCall::StartCall(class IAudioIo * audioIo, class IVideoIo * videoIo, class URTPTP * rtptp, class SignalingRegistrationMonitor * exclude)
{
    IGuuid::GenerateBinaryUUID(&conf);
    str::from_hexmem(conf.b, sizeof(conf), this->confId);

    registration->signaling->log->Log(LOG_SIGNALING, "%x/%i:SignalingCall::StartCall() videoAllowed=%u confId=%s", this, call, videoAllowed, this->confId);

    if (registration->signaling->lastStartCallDest) delete registration->signaling->lastStartCallDest;
    registration->signaling->lastStartCallDest = new VoipEndpoint(remote.num, remote.sip, remote.dn);
    this->audioIo = audioIo;
    this->videoIo = videoIo;
    this->rtptp = rtptp;
    this->exclude = exclude;
    if (registration->clientUp && (registration->registered || registration->err)) {
        this->recordingInfo.isExternal = registration->IsExternalCall(remote.num);
        this->recordingInfo.isRecordingByDefaultOn = registration->signaling->service->mediaConfig->isRecordingByDefaultOn;
        if (registration->signaling->user) {
            RequestStartCall();
        }
        else {
            DoStartCall();
        }
    }
}

void SignalingCall::RegistrationState(bool up)
{
    if (up && state == 0 && !media) {
        if (registration && registration->signaling->user) {
            RequestStartCall();
        }
        else {
            DoStartCall();
        }
    }
    else if (registration) registration->signaling->log->Log(LOG_SIGNALING, "%x/%i:SignalingCall::RegistrationState() up=%u state=%d media=%p", this, call, up, state, media);
}

void SignalingCall::RequestStartCall()
{
    if (registration->signaling->user) {
        unsigned offset = registration->ExternalLinePrefixOffset(remote.num);
        char remoteSip[1024];
        registration->signaling->user->RequestStartCall(confId, remote.num ? remote.num + offset : registration->signaling->AppendSipDomain(remote.sip, remoteSip, sizeof(remoteSip)), remote.dn, videoIo && videoAllowed, sendingComplete);
    }
    else {
        DoStartCall();
    }
}

const char* SignalingCall::GetConfId()
{
    return (const char *)this->confId;
}

const char* SignalingCall::GetReconnectRole()
{
    if (reconnectInitRecv || reconnectSetupRecv) return "passive";
    if (remoteCallId) return "active";
    return nullptr;
}

void SignalingCall::DoStartCall()
{
    // Other calls to Hold if connected
    class btree* b = registration->calls->btree_find_next_right(0);
    while (b) {
        class SignalingCall* c = (class SignalingCall*)b;
        if (c != this && ((c->state == SIG_CALL_STATE_CONN) || (c->state == (SIG_CALL_STATE_CONN | SIG_CALL_STATE_OUTG)))) {
            if (c->replacementCall == this) { /* no need to put this replaced call on hold */ }
            else c->Hold();
        }
        b = registration->calls->btree_find_next_right((void*)(intp)c->call);
    }

    MediaInitialize(MediaType::AUDIO);

    if (audioIo) {
        audioIoChannel = audioIo->CreateChannel(this);
        if (audioDeviceId) free(audioDeviceId);
        if (registration->signaling->changedAudioDeviceId) audioDeviceId = _strdup(registration->signaling->changedAudioDeviceId);
        else audioDeviceId = _strdup(audioIo->GetDeviceIo(registration->GetHwId()));
        if (audioDeviceId) {
            audioIo->StartDevice((void*)this, (void*)this, audioDeviceId, AudioDeviceMode::AudioModeCommunication);
            if (audioIo->DeviceCapabilities(audioDeviceId) & AUDIO_CAPABILITY_HOOK_DEVICE_SUPPORTS_LOCKING) {
                registration->signaling->hookDeviceState = HOOK_DEVICE_WAITING;
                audioIo->AcquireHookDevice(audioDeviceId);
            }
            else registration->signaling->hookDeviceState = HOOK_DEVICE_ACQUIRE;
        }
        else {
            // No audio device available, what to do?
            registration->signaling->log->Log(LOG_SIGNALING, "%x/%i:SignalingCall::StartCall() no audio device available", this, call);
            return;
        }
    }
    else registration->signaling->hookDeviceState = HOOK_DEVICE_ACQUIRE;

    if (videoIo && videoAllowed) {
        videoIoChannel = videoIo->CreateChannel(this);
        if (!registration->signaling->doNotStartCamera) {
            if (videoDeviceId) free(videoDeviceId);
            videoDeviceId = _strdup(videoIo->GetDeviceIo(registration->GetHwId()));
            if (videoDeviceId) videoIo->StartVideoDevice((void*)this, (void*)this, videoDeviceId, 1280, 720, 0);
        }
        MediaInitialize(MediaType::VIDEO);
    }

    registration->signaling->CallAdded(this, exclude);

    if (registration->signaling->hookDeviceState == HOOK_DEVICE_WAITING) alertPostponedOnHookDevice = true;
}

void SignalingCall::ReconnectCall(bool sync)
{
    debug->printf("%x/%i:SignalingCall::ReconnectCall remoteCallId = %d", this, call, remoteCallId);

    // called after registration is back. Therefore small timeout
    if (remoteCallId) {
        if (reconnectCallTimer.IsRunning() == false) reconnectCallTimer.Start(100);
    }
    if(sync) MonitorSync();
}

void SignalingCall::ClearCall(word cause)
{
    debug->printf("%x/%i:SignalingCall::ClearCall() state:%x hookState:%d pendingReconnectCall:%p", this, call, state, hookState, registration ? registration->pendingReconnectCall : nullptr);

    if (registration) {
        if (audioIo) MediaTones::StopTone(audioIo);
        if (audioDeviceId) {
            if (hookAudioDeviceId) free(hookAudioDeviceId);
            hookAudioDeviceId = _strdup(audioDeviceId);
        }
        if (registration->calls->get_count() < 2) {
            if (audioIo && audioDeviceId) {
                audioIo->StopDevice((void *) this, audioDeviceId);
                free(audioDeviceId);
                audioDeviceId = NULL;
                registration->signaling->hookDeviceState = 0;
            }
            if (videoIo && videoDeviceId) {
                videoIo->StopDevice((void *)this, videoDeviceId);
                free(videoDeviceId);
                videoDeviceId = NULL;
                videoIo->UnshareAllApplications((void*)this);
            }
            StopRinging();
        }
        if (registration->signaling->user) {
            registration->signaling->user->ReportCallEnded(confId, (cause == Q931_CAUSE_NonSelectedUserClearing) ? CALL_ENDED_REASON_ANSWERED_ELSEWHERE : CALL_ENDED_REASON_REMOTE_ENDED, reconnectedCall ? true : false, reconnectConfId);
        }
    }
    this->cause = cause;
    if (registration) {
        if (!registration->pendingReconnectCall || this != registration->pendingReconnectCall) SendRel();
        else {
            registration->pendingReconnectCall = nullptr;
            Delete();
        }
    }
}

void SignalingCall::ClearReconnectedCall(word cause)
{
    debug->printf("%x/%i:SignalingCall::ClearReconnectedCall() state:%x callNotInBtree:%d reconnectConfId:%p", this, call, state, callNotInBtree, reconnectConfId);

    if (registration && registration->signaling->user) {
        registration->signaling->user->ReportCallEnded(confId, (cause == Q931_CAUSE_NonSelectedUserClearing) ? CALL_ENDED_REASON_ANSWERED_ELSEWHERE : CALL_ENDED_REASON_REMOTE_ENDED, true, reconnectConfId);
    }
    this->cause = cause;
    if(callNotInBtree == false) SendRel();
    else {
        class SignalingSigMessage send(call, "rel", alloca(1000), alloca(100));
        if (cause) {
            word cau = send.add_object(send.sig, "cau");
            send.add_int(cau, "num", cause, send.tmp);
        }
        ChangeState(0);
        MonitorXmit("rel", send, send.sig);
        debug->printf("%x/%i:SignalingCall::ClearReconnectedCall() deleting=%d muted=%d registration=%p hookAudioDeviceId=%p hookState=%d", this, call, deleting, muted, registration, hookAudioDeviceId, hookState);

        if (deleting) return;

        deleting = true;
        while (monitors.front()) delete monitors.front();
        if (registration) {
            registration->StateChanged(this);
            registration = 0;
        }

        this->state = 0;
        mediaInfoInterval.Cancel();
        audioRtpReceived.Cancel();
        hookDeviceTimer.Cancel();

        DELETE_OBJ(localAudioConfig);
        DELETE_OBJ(localVideoConfig);
        if (media) {
            media->Close();
        }
        if (media2) {
            media2->Close();
        }
        if (media == NULL && mediaClosing == NULL && media2 == NULL && media2Closing == NULL && audioIoChannel == NULL && videoIoChannel == NULL && videoIoChannelClosing == NULL) delete this;
    }
}

void SignalingCall::ConnectCall(class IAudioIo * audioIo, class IVideoIo * videoIo)
{
    debug->printf("%x/%i:SignalingCall::ConnectCall() videoAllowed=%u remoteVideoConfig=%p coder=%d IoChannels=%p,%p registration=%p", this, call, videoAllowed, remoteVideoConfig, remoteVideoConfig && remoteVideoConfig->codecList.front() ? remoteVideoConfig->codecList.front()->coder : 0, audioIoChannel, videoIoChannel, registration);

    if (!registration) {
        ClearCall();
        return;
    }

    selectProposalByConn = false;

    StopRinging();

    if (audioIo) {
        if (!audioIoChannel) audioIoChannel = audioIo->CreateChannel(this);
        if (audioDeviceId) free(audioDeviceId);
        if (registration->signaling->changedAudioDeviceId) audioDeviceId = _strdup(registration->signaling->changedAudioDeviceId);
        else audioDeviceId = _strdup(audioIo->GetDeviceIo(registration->GetHwId()));
        if (audioDeviceId) {
            audioIo->StartDevice((void*)this, (void*)this, audioDeviceId, AudioDeviceMode::AudioModeCommunication);
            if (audioIo->DeviceCapabilities(audioDeviceId) & AUDIO_CAPABILITY_HOOK_DEVICE_SUPPORTS_LOCKING) {
                registration->signaling->hookDeviceState = HOOK_DEVICE_WAITING;
                audioIo->AcquireHookDevice(audioDeviceId);
            }
            else registration->signaling->hookDeviceState = HOOK_DEVICE_ACQUIRE;
        }
        else {
            debug->printf("%x/%i:SignalingCall::ConnectCall() no audio device available", this, call);
            ClearCall();
            return;
        }
    }
    // We got a PROPOSAL with Video, (remoteVideoConfig && remoteVideoConfig->codecList.front()) ||
    // nothing (empty setup) received so far, (remoteMediaCmd == MEDIA_CMD_REQUEST), then we add Video to our Proposal if Video is active
    if (videoIo && !videoDisabled && ((videoAllowed && (remoteMediaCmd == MEDIA_CMD_REQUEST)) || (remoteVideoConfig && remoteVideoConfig->codecList.front()))) {
        // Webcam must only be started if videoAllowed
        if (videoAllowed && !registration->signaling->doNotStartCamera) {
            if (videoDeviceId) free(videoDeviceId);
            videoDeviceId = _strdup(videoIo->GetDeviceIo(registration->GetHwId()));
            if (videoDeviceId) videoIo->StartVideoDevice((void*)this, (void*)this, videoDeviceId, 1280, 720, 0);
        }
        if (!videoIoChannel) videoIoChannel = videoIo->CreateChannel(this);
        if (!media2) {
            MediaInitialize(MediaType::VIDEO);
        }
    }

    if (initializing) connPostponed = true;
    else if (registration->signaling->hookDeviceState == HOOK_DEVICE_ACQUIRE) SendConn();
    else if (registration->signaling->hookDeviceState == HOOK_DEVICE_WAITING) connPostponedOnHookDevice = true;
}

void SignalingCall::TimerOnTimeout(ITimer* iTimer)
{
    //debug->printf("%x:SignalingCall::TimerOnTimeout state=%x", this, state);

    if (iTimer == &autoConnectTimer) {
        if (state == (SIG_CALL_STATE_ALERT | SIG_CALL_STATE_OUTG)) {
            // auto-answer announce call with audio-only and mute microphone
            if (audioIo) {
                ConnectCall(audioIo, 0);
                if (announce) {
                    muted = true;
                    audioIo->Mute(true, false);
                }
            }
        }
        return;
    }
    else if (iTimer == &audioRtpReceived) {
        audioRtpReceived.Start(1500);
        if (numAudioPackets && (lastNumRtpAudioPackets == numAudioPackets)) {
            if (++numNoRtpPackets == 2) {
                debug->printf("%x%i:SignalingCall::TimerOnTimeout no RTP packets remoteCallId=%d reconnectInitRecv=%d(%d) sigRelSent=%d registration(%p)=%d,%d", this, call, remoteCallId, reconnectInitRecv, reconnectSetupRecv, sigRelSent, registration, registration ? registration->clientUp : 0, registration ? registration->registered : 0);

                if (remoteCallId) {
                    mediaLostToneTimer.Start(0);
                    CreateReconnectCall();
                }
                else if ((reconnectInitRecv || reconnectSetupRecv) && registration && registration->clientUp && registration->registered && !sigRelSent) {
                    mediaLostToneTimer.Start(0);
                    class SignalingSigMessage send(call, "rel", alloca(1000), alloca(100));
                    word cau = send.add_object(send.sig, "cau");
                    send.add_int(cau, "num", Q931_CAUSE_TemporaryFailure, send.tmp);
                    send.Send(registration);
                    sigRelSent = true;
                }
            }
        }
        else numNoRtpPackets = 0;
        lastNumRtpAudioPackets = numAudioPackets;
        return;
    }
    else if (iTimer == &signalingTestTimer) {
        if (registration) registration->signaling->TestCloseRegistration();
        return;
    }
    else if (iTimer == &mediaInfoInterval) {
        class SignalingSigMessage send(call, "facility", alloca(10000), alloca(1000));
        word ftys = send.add_array(send.sig, "fty");
        word fty = send.add_object(ftys, 0);
        send.add_string(fty, "type", "media_info");
        word rxBase = send.add_object(fty, "receive");
        send.add_string(rxBase, "audio", CoderName(audioCoder));
        send.add_bool(rxBase, "srtp", true);
        send.add_unsigned(rxBase, "loss", rx.loss, send.tmp);
        send.add_unsigned(rxBase, "jitter", rx.jitter, send.tmp);
        send.add_unsigned(rxBase, "roundTrip", rx.roundTrip, send.tmp);
        send.add_string(rxBase, "video", CoderName(videoCoder));
        word txBase = send.add_object(fty, "transmit");
        send.add_string(txBase, "audio", CoderName(audioCoder));
        send.add_bool(txBase, "srtp", true);
        send.add_unsigned(txBase, "loss", rx.loss, send.tmp);
        send.add_unsigned(txBase, "jitter", rx.jitter, send.tmp);
        send.add_unsigned(txBase, "roundTrip", rx.roundTrip, send.tmp);
        send.add_string(txBase, "video", CoderName(videoCoder));
        send.Send(registration);
        mediaInfoInterval.Start(5000);
        if (!videoDataOff && numVideoPackets && (lastNumVideoPackets == numVideoPackets)) {
            char cfg[1024];
            class json_io send(cfg);

            debug->printf("%x/%i:SignalingCall::TimerOnTimeout no video packets received", this, call);

            word sendBase = 0xffff;
            sendBase = send.add_object(0xffff, 0);
            send.add_string(sendBase, "channel", confId);
            send.add_string(sendBase, "type", "video");

            MonitorXmit("media_data_off", send, sendBase);
            videoDataOff = true;
        }
        lastNumVideoPackets = numVideoPackets;
        if (!audioDataOff && numAudioPackets && (lastNumAudioPackets == numAudioPackets)) {
            char cfg[1024];
            class json_io send(cfg);

            debug->printf("%x/%i:SignalingCall::TimerOnTimeout no audio packets received", this, call);

            word sendBase = 0xffff;
            sendBase = send.add_object(0xffff, 0);
            send.add_string(sendBase, "channel", confId);
            send.add_string(sendBase, "type", "audio");

            MonitorXmit("media_data_off", send, sendBase);
            audioDataOff = true;
        }
        lastNumAudioPackets = numAudioPackets;
        return;
    }
    else if (iTimer == &queuedDTMFTimer) {
        if (!dtmfs.empty()) {
            class QueuedDTMF * dtmfHelper = dtmfs.pop_front();
            DTMF(dtmfHelper->dtmf, dtmfHelper->feedback);
            delete dtmfHelper;
            if (!dtmfs.empty()) queuedDTMFTimer.Start(500);
        }
        return;
    }
    else if (iTimer == &watingCallToneTimer) {
        // call still connected and we have a waiting call 
        if (registration && audioIo && ((state == SIG_CALL_STATE_CONN) || (state == (SIG_CALL_STATE_CONN | SIG_CALL_STATE_OUTG)))) {
            class SignalingCall* waiting = 0;
            class btree* b = registration->calls->btree_find_next_right(0);
            while (b) {
                class SignalingCall* c = (class SignalingCall*)b;
                if (c != this) {
                    if (c->state == (SIG_CALL_STATE_ALERT | SIG_CALL_STATE_OUTG | SIG_CALL_STATE_CALL_WAITING)) {
                        waiting = c;
                        break;
                    }
                }
                b = registration->calls->btree_find_next_right((void*)(intp)c->call);
            }
            if (waiting) {
                debug->printf("%x/%i:SignalingCall::TimerOnTimeout waiting=%i", this, call, waiting ? waiting->call : 0);
                MediaTones::StartTone(MEDIA_TONES_CALLWAITING, registration->dialingLocation.tones, audioIo);
            }
        }
        return;
    }
    else if (iTimer == &toneEndTimer) {
        if ((state & 0xF) == SIG_CALL_STATE_DISC_RCVD) {
            if (hookAudioDeviceId) free(hookAudioDeviceId);
            hookAudioDeviceId = _strdup(audioDeviceId);
            AbortTone();
            ClearCall();
            return;
        }
        else {
            if (registration && registration->signaling->user) {
                registration->signaling->user->ReportCallEnded(confId, (this->cause == Q931_CAUSE_NonSelectedUserClearing) ? CALL_ENDED_REASON_ANSWERED_ELSEWHERE : CALL_ENDED_REASON_REMOTE_ENDED, false, reconnectConfId);
            }
        }
    }
    else if (iTimer == &hookDeviceTimer) {
        ClearCall();
        return;
    }
    else if (iTimer == &reconnectCallTimer) {
        if (remoteCallId) {
            debug->printf("%x/%i:SignalingCall::TimerOnTimeout reconnect timer", this, call);

            CreateReconnectCall();
        }
        return;
    }
    else if (iTimer == &reconnectCallResponse) {
        debug->printf("%x/%i:SignalingCall::reconnectCallResponse timer, no answer received! reconnectedCall=%p", this, call, reconnectedCall);
        if (reconnectedCall) {
            reconnectedCall->reconnectingCall = nullptr;
            reconnectedCall->StartReconnectTimer();
        }
        failedReconnect = true;
        ClearCall(Q931_CAUSE_NonSelectedUserClearing);
        return;
    }
    else if (iTimer == &reconnectCallRequest) {
        debug->printf("%x/%i:SignalingCall::reconnectCallRequest timer, no setup received! pendingReconnectCall=%p", this, call, registration ? registration->pendingReconnectCall : nullptr);
        ClearCall();
        if (registration && registration->pendingReconnectCall == this) {
            registration->pendingReconnectCall = nullptr;
        }
        return;
    }
    else if (iTimer == &mediaLostToneTimer) {
        debug->printf("%x/%i:SignalingCall::mediaLostToneTimer", this, call);
        if (audioIo) audioIo->StartDualTones(AUDIO_IO_DUAL_TONE_BOTH | AUDIO_IO_DUAL_TONE_PASSTHROUGH | AUDIO_IO_DUAL_TONE_AUTOOFF, sizeof(mediaLostTone) / sizeof(mediaLostTone[0]), mediaLostTone);
        mediaLostToneTimer.Start(3000);
        class SignalingSigMessage send(call, "reconnecting", alloca(10000), alloca(1000));
        MonitorXmit("reconnecting", send, send.sig);
        return;
    }

    // toneEndTimer
    AbortTone();
    Delete();
}

void SignalingCall::ChangeAudioDevice(const char* deviceId)
{
    debug->printf("%x:SignalingCall::ChangeAudioDevice %s -> %s", this, audioDeviceId ? audioDeviceId : "-", deviceId ? deviceId : "-");

    if (audioIo && audioDeviceId && deviceId && strcmp(audioDeviceId, deviceId)) {
        // StopDevice stops the audio channel and should not be called!
        audioIo->StopDevice((void*)this, audioDeviceId);
        free(audioDeviceId);
        audioDeviceId = _strdup(deviceId);
        audioIo->StartDevice((void*)this, (void*)this, audioDeviceId, AudioDeviceMode::AudioModeCommunication);
        if (audioIo->DeviceCapabilities(audioDeviceId) & AUDIO_CAPABILITY_HOOK_DEVICE_SUPPORTS_LOCKING) {
            registration->signaling->hookDeviceState = HOOK_DEVICE_WAITING;
            updateHookDevice = true;
            audioIo->AcquireHookDevice(audioDeviceId);
        }
        else {
            registration->signaling->hookDeviceState = HOOK_DEVICE_ACQUIRE;
            registration->signaling->SendHookKey(audioDeviceId, KEY_OFFHOOK, call, remote.num ? remote.num : remote.sip, remote.dn);
            if (hookState != KEY_OFFHOOK) registration->signaling->SendHookKey(audioDeviceId, hookState, call, remote.num ? remote.num : remote.sip, remote.dn);
        }
    }
}

void SignalingCall::ChangeVideoDevice(const char* deviceId)
{
    debug->printf("%x:SignalingCall::ChangeVideoDevice %s -> %s", this, videoDeviceId ? videoDeviceId : "-", deviceId ? deviceId : "-");

    if (videoIo && videoDeviceId && deviceId && strcmp(videoDeviceId, deviceId)) {
        videoIo->StopDevice((void *)this, videoDeviceId);
        if (!registration->signaling->doNotStartCamera) {
            free(videoDeviceId);
            videoDeviceId = _strdup(deviceId);
            videoIo->StartVideoDevice((void *)this, (void *)this, videoDeviceId, 1280, 720, 0);
        }
    }
}

void SignalingCall::ChangeState(word state)
{
    debug->printf("%x/%i:SignalingCall::ChangeState %x->%x", this, this->call, this->state, state);

    if (this->state != state) {
        this->state = state;
        if(registration) registration->StateChanged(this);
    }
    if ((state == SIG_CALL_STATE_CONN) || (state == (SIG_CALL_STATE_CONN | SIG_CALL_STATE_OUTG))) {
        mediaInfoInterval.Start(5000);
        hookDeviceTimer.Cancel();
        reconnectCallResponse.Cancel();
    }
    else {
        mediaInfoInterval.Cancel();
        audioRtpReceived.Cancel();
    }
}

void SignalingCall::AbortTone() 
{
    debug->printf("%x/%i:SignalingCall::AbortTone", this, call);
    if (audioIo) MediaTones::StopTone(audioIo);

    bool stopDevice = true;
    if (registration && registration->calls && registration->calls->get_count() > 1) stopDevice = false;
    if (audioIo && audioDeviceId && stopDevice) {
        audioIo->StopDevice((void*)this, audioDeviceId);
        if (hookAudioDeviceId) free(hookAudioDeviceId);
        hookAudioDeviceId = _strdup(audioDeviceId);
        free(audioDeviceId);
        audioDeviceId = NULL;
    }

    if (toneEndTimer.IsRunning()) {
        if (registration->signaling->user) {
            registration->signaling->user->ReportCallEnded(confId, (this->cause == Q931_CAUSE_NonSelectedUserClearing) ? CALL_ENDED_REASON_ANSWERED_ELSEWHERE : CALL_ENDED_REASON_REMOTE_ENDED, false, reconnectConfId);
        }
        toneEndTimer.Cancel();
    }
}

void SignalingCall::Rc(unsigned rc)
{
    debug->printf("%x/%i:SignalingCall::Rc() uuid %s rc: %d reg=%x", this, call, confId, rc, registration);

    switch (rc) {
        case 1:
            SendConn();
            break;
        case FTY_REMOTE_CONTROL_REC_ON:
            if (!recordingInfo.recordingError && (!recordingInfo.isTrunkRecording || recordingInfo.isRecordingByDefaultOn) ) media->RecordingOn(true);
            break;
        case FTY_REMOTE_CONTROL_REC_OFF:
            if (!recordingInfo.recordingError && (!recordingInfo.isTrunkRecording || recordingInfo.isRecordingByDefaultOn) ) media->RecordingOn(false);
            break;
        case FTY_REMOTE_CONTROL_3PTY_ON:
            Retrieve();
            break;
        case FTY_REMOTE_CONTROL_MIC_ON:
            if(audioIo){
                muted = false;
                audioIo->Mute(false,false);
            }
            break;
        case FTY_REMOTE_CONTROL_MIC_OFF:
            if(audioIo){
                muted = true;
                audioIo->Mute(true,false);
            }
            break;
        case FTY_REMOTE_CONTROL_START_CAMERA:
            debug->printf("%x/%i:SignalingCall::Rc(FTY_REMOTE_CONTROL_START_CAMERA)", this, call);
            if (videoIo && !registration->signaling->doNotStartCamera) {
                if (videoDeviceId) free(videoDeviceId);
                videoDeviceId = _strdup(videoIo->GetDeviceIo(registration->GetHwId()));
                if (videoDeviceId) videoIo->StartVideoDevice((void*)this, (void*)this, videoDeviceId, 1280, 720, 0);
            }
            break;
        case FTY_REMOTE_CONTROL_STOP_CAMERA:
            debug->printf("%x/%i:SignalingCall::Rc(FTY_REMOTE_CONTROL_STOP_CAMERA)", this, call);
            if (videoIo && videoDeviceId) {
                videoIo->StopDevice((void*)this, videoDeviceId);
                free(videoDeviceId);
                videoDeviceId = NULL;
            }
            break;
        case FTY_REMOTE_CONTROL_ADD_VIDEO:
            debug->printf("%x/%i:SignalingCall::Rc(FTY_REMOTE_CONTROL_ADD_VIDEO) videoIo=%x videoIoChannel=%x videoCoder=%u media2=%x localVideoConfig=%p initializing=%x connecting=%x ignoreAddVideo=%d", this, call, videoIo, videoIoChannel, videoCoder, media2, localVideoConfig, initializing, connecting, ignoreAddVideoReconnecting);
            if (ignoreAddVideoReconnecting) break;
            videoDisabled = false;
            if (registration && registration->signaling && videoIo) {
                if (!videoIoChannel) videoIoChannel = videoIo->CreateChannel(this);
                if (!media2) {
                    MediaInitialize(MediaType::VIDEO);
                    if (connecting & (1 << AUDIO)) {
                        connecting &= ~(1 << AUDIO);
                        media->Close();
                        mediaClosing = media;
                        DELETE_OBJ(localAudioConfig);
                        MediaInitialize(MediaType::AUDIO);
                    }
                }
                else if(localVideoConfig) {
                    GenerateIceCredentials(localVideoConfig->ice);
                    media2->RewriteIceCredentials(localVideoConfig->ice->iceUfrag, localVideoConfig->ice->icePwd);
                    if (connecting & (1 << AUDIO)) {
                        connecting &= ~(1 << AUDIO);
                        media->Close();
                        mediaClosing = media;
                        DELETE_OBJ(localAudioConfig);
                        MediaInitialize(MediaType::AUDIO);
                    }
                    else SendChannels();
                }
                else {
                    debug->printf("%x/%i:SignalingCall::Rc localVideoConfig not set. Video being initialized media2:%x", this, call, media2);
                }
                sendVideoProposal = true;
            }
            break;
        case FTY_REMOTE_CONTROL_ALLOW_VIDEO:
            debug->printf("SignalingCall::Rc(FTY_REMOTE_CONTROL_ALLOW_VIDEO) videoIo=%x videoIoChannel=%x videoCoder=%u media2=%x initializing=%x connecting=%x videoDisabled=%x", videoIo, videoIoChannel, videoCoder, media2, initializing, connecting, videoDisabled);
            videoDisabled = false;
            break;
        case FTY_REMOTE_CONTROL_REM_VIDEO:
            debug->printf("SignalingCall::Rc(FTY_REMOTE_CONTROL_REM_VIDEO) videoIo=%x videoIoChannel=%x videoCoder=%u media2=%x initializing=%x connecting=%x", videoIo, videoIoChannel, videoCoder, media2, initializing, connecting);
            videoDisabled = true;
            if (videoIo && videoDeviceId) {
                videoIo->StopDevice((void*)this, videoDeviceId);
                free(videoDeviceId);
                videoDeviceId = NULL;
            }
            if (videoIoChannel) {
                videoIoChannel->Close();
                videoIoChannelClosing = videoIoChannel;
                videoIoChannel = NULL;
            }
            if (media2) {
                media2->Close();
                media2Closing = media2;
                media2 = NULL;
                DELETE_OBJ(localVideoConfig);
                if (initializing & (1 << VIDEO)) {
                    initializing &= ~(1 << VIDEO);
                    MediaInitialized();
                }
                else if (videoCoder) {
                    if (connecting & (1 << AUDIO)) {
                        connecting &= ~(1 << AUDIO);
                        media->Close();
                        mediaClosing = media;
                        DELETE_OBJ(localAudioConfig);
                        MediaInitialize(MediaType::AUDIO);
                    }
                    else SendChannels();
                }
            }
            break;
        case FTY_REMOTE_CONTROL_ADD_APPSHARING:
            {
                class SignalingSigMessage send(call, "facility", alloca(10000), alloca(1000));
                word ftys = send.add_array(send.sig, "fty");
                word fty = send.add_object(ftys, 0);
                send.add_string(fty, "type", "video_setup");
                send.add_string(fty, "mode", "sendonly");
                send.add_string(fty, "guid", conferenceGuid);
                send.add_unsigned(fty, "bitrate", 1024000, send.tmp);
                send.Send(registration);
            }
            break;
        case FTY_REMOTE_CONTROL_REM_APPSHARING:
            {
                class SignalingSigMessage send(call, "facility", alloca(10000), alloca(1000));
                word ftys = send.add_array(send.sig, "fty");
                word fty = send.add_object(ftys, 0);
                send.add_string(fty, "type", "video_setup");
                send.add_string(fty, "mode", "sendonly");
                send.add_string(fty, "guid", conferenceGuid);
                send.add_unsigned(fty, "bitrate", 0, send.tmp);
                send.Send(registration);
            }
            break;
    }
}

void SignalingCall::Info(const char * num, const char * sip)
{
    if (audioIo) MediaTones::StopTone(audioIo);
    remote.AddtoNum(num);
    remote.SetSip(sip);
    SendInfo(num, sip);
}

void SignalingCall::DisplayName(class json_io& json, word base, const char* src)
{
    const char * dn = json.get_string(base, "dn");

    debug->printf("%x/%i:SignalingCall::DisplayName() dn=%s", this, call, dn ? dn : "-");

    if (dn && (!remote.dn || strcmp(remote.dn, dn))) {
        remote.SetDn(dn);

        if (registration && registration->signaling->user) {
            unsigned offset = registration->ExternalLinePrefixOffset(remote.num);
            char remoteSip[1024];
            registration->signaling->user->ReportNameIdentification(confId, remote.num + offset, registration->signaling->AppendSipDomain(remote.sip, remoteSip, sizeof(remoteSip)), dn,
                this->diverting.dn ? this->diverting.dn : this->diverting.sip ? this->diverting.sip : this->diverting.num ? this->diverting.num : NULL,
                this->originalCalled.dn ? this->originalCalled.dn : this->originalCalled.sip ? this->originalCalled.sip : this->originalCalled.num ? this->originalCalled.num : NULL,
                videoAllowed, true, true, true, true, reconnectedCall ? true : false);
        }
    }
}

void SignalingCall::SendDTMF(const char* dtmf, bool feedback)
{
    if (!dtmfs.empty() || audioMediaConnected == false) dtmfs.push_back(new class QueuedDTMF(dtmf, feedback));
    else DTMF(dtmf, feedback);
}

void SignalingCall::DTMF(const char * dtmf, bool feedback)
{
    debug->printf("SignalingCall::DTMF (%s) feedback=%d", dtmf, feedback);

    if (remoteDTMF && media && dtmf) {
        for (int i = 0; dtmf[i]; i++) {
            media->RtpDtmf(dtmf[i], remoteDTMF);
        }
    }
    struct AudioIoDualTone* t;
    unsigned count, digit;
    unsigned int flags = 0;
    if (feedback || !remoteDTMF) {
        flags |= feedback ? AUDIO_IO_DUAL_TONE_BOTH | AUDIO_IO_DUAL_TONE_PEER : AUDIO_IO_DUAL_TONE_PEER;
        count = 0;
        for (digit = 0; dtmf[digit] != '\0'; digit++) {
            if (count == allocatedTones) {
                t = (struct AudioIoDualTone*)(malloc(2 * allocatedTones * sizeof(struct AudioIoDualTone)));
                memcpy(t, tones, allocatedTones * sizeof(struct AudioIoDualTone));
                free(tones);
                tones = t;
                allocatedTones *= 2;
            }
            count += MediaTones::PrepareDtmf(dtmf[digit], &tones[count]);
        }
        if (count != 0) {
            audioIo->StartDualTones(flags, count, tones);
        }
    }
}

void SignalingCall::UUI(unsigned pd, const char * uui)
{
    SignalingSigMessage msg(call, "user_info", alloca(10000), alloca(100));
    if (pd) msg.add_unsigned(msg.sig, "pd", pd, msg.tmp);
    msg.add_string(msg.sig, uui, uui);
    MonitorXmit("user_info", msg, msg.sig);
    msg.Send(registration);
}

void SignalingCall::StartRinging()
{
    if (ringerDeviceId) {
        // ringer already started
    }
    else if (ringerIo) {
        ringerDeviceId = _strdup(ringerIo->GetDeviceIo(registration->GetHwId()));
        ringertoneIdent = _strdup(ringerIo->GetRingtone(registration->GetHwId()));
        if(ringerDeviceId) ringerIo->RingtoneStart(ringerDeviceId, 0, ringertoneIdent);
    }
}

void SignalingCall::StopRinging()
{
    if (ringerIo && ringerDeviceId) {
        ringerIo->RingtoneStop(ringerDeviceId);
        free(ringerDeviceId);
        ringerDeviceId = 0;
        if(ringertoneIdent) free(ringertoneIdent);
        ringertoneIdent = 0;
    }
}

void SignalingCall::StartTransfer(class SignalingCall * from)
{
    if (!ctIdentify && !ctInitiate && !from->ctIdentify && !from->ctInitiate) {
        ctInitiate = from;
        from->ctIdentify = this;
        SignalingSigMessage msg(call, "facility", alloca(1000), alloca(100));
        word ftys = msg.add_array(msg.sig, "fty");
        word fty = msg.add_object(ftys, 0);
        msg.add_string(fty, "type", "ct_identify");
        msg.Send(registration);
    }
}

void SignalingCall::StartRedirect(const char * num, const char * sip)
{
    SignalingSigMessage msg(call, "facility", alloca(1000), alloca(100));
    word ftys = msg.add_array(msg.sig, "fty");
    word fty = msg.add_object(ftys, 0);
    msg.add_string(fty, "type", "ct_initiate");
    word dst = msg.add_object(fty, "dst");
    msg.add_string(dst, "num", num);
    msg.add_string(dst, "sip", sip);
    msg.Send(registration);
}

void SignalingCall::StartReroute(const char * num, const char * sip)
{
    SignalingSigMessage msg(call, "facility", alloca(1000), alloca(100));
    word ftys = msg.add_array(msg.sig, "fty");
    word fty = msg.add_object(ftys, 0);
    msg.add_string(fty, "type", "diversion_callreroute");
    msg.add_string(fty, "reason", "CFU");
    word called = msg.add_object(fty, "called");
    msg.add_string(called, "num", num);
    msg.add_string(called, "sip", sip);
    msg.Send(registration);
}

void SignalingCall::Hold()
{
    ChangeState(state | SIG_CALL_STATE_HOLD);

    SignalingSigMessage msg(call, "facility", alloca(1000), alloca(100));
    MonitorXmit("facility_hold", msg, msg.sig);
    word ftys = msg.add_array(msg.sig, "fty");
    word fty = msg.add_object(ftys, 0);
    msg.add_string(fty, "type", "remote_hold");
    msg.Send(registration);
    if (registration->signaling->user) {
        registration->signaling->user->ReportCallHold(confId);
    }

    videoCoderBeforeHold = videoCoder;
    videoDeviceIdBeforeHold = _strdup(videoDeviceId);
    if (registration) {
        if (audioDeviceId) {
            registration->signaling->SendHookKey(audioDeviceId, KEY_HOLD, call, remote.num ? remote.num : remote.sip, remote.dn);
            hookState = KEY_HOLD;
        }
        if (registration->calls->get_count() < 2) {
            if (videoIo && videoDeviceId) {
                videoIo->StopDevice((void*)this, videoDeviceId);
                free(videoDeviceId);
                videoDeviceId = NULL;
                videoIo->UnshareAllApplications((void*)this);
            }
        }
    }
}

void SignalingCall::Retrieve()
{
    ChangeState(state & ~SIG_CALL_STATE_HOLD);

    SignalingSigMessage msg(call, "facility", alloca(1000), alloca(100));
    MonitorXmit("facility_retrieve", msg, msg.sig);
    word ftys = msg.add_array(msg.sig, "fty");
    word fty = msg.add_object(ftys, 0);
    msg.add_string(fty, "type", "remote_retrieve");
    msg.Send(registration);
    if (registration->signaling->user) {
        registration->signaling->user->ReportCallRetrieve(confId);
    }

    // always re-start audio device on RETRIEVE
    if (audioIo) {
        audioIo->Mute(muted, false);
        if (!audioIoChannel) audioIoChannel = audioIo->CreateChannel(this);
        if (!audioDeviceId) {
            if (registration->signaling->changedAudioDeviceId) audioDeviceId = _strdup(registration->signaling->changedAudioDeviceId);
            else audioDeviceId = _strdup(audioIo->GetDeviceIo(registration->GetHwId()));
        }
        if (audioDeviceId) {
            registration->signaling->SendHookKey(audioDeviceId, KEY_RETRIEVE, call, remote.num ? remote.num : remote.sip, remote.dn);
            registration->signaling->SendHookKey(audioDeviceId, KEY_OFFHOOK, call, remote.num ? remote.num : remote.sip, remote.dn);
            hookState = KEY_OFFHOOK;
        }
    }

    // re-start video device only if video was negotiated before HOLD and the webcam was activated, could have been stopped
    if(videoDeviceIdBeforeHold) {
        if (videoIo && videoCoderBeforeHold) {
            if (!videoIoChannel) videoIoChannel = videoIo->CreateChannel(this);
            if (!registration->signaling->doNotStartCamera) {
                videoIo->StartVideoDevice((void*)this, (void*)this, videoDeviceIdBeforeHold, 1280, 720, 0);
                if (videoDeviceId) free(videoDeviceId);
                videoDeviceId = videoDeviceIdBeforeHold;
            }
        }
        else free(videoDeviceIdBeforeHold);
        videoDeviceIdBeforeHold = NULL;
    }
}

void SignalingCall::Park(const char * parked_to_num, const char * parked_to_sip)
{
    SignalingSigMessage msg(call, "facility", alloca(1000), alloca(100));
    MonitorXmit("facility_park", msg, msg.sig);
    word ftys = msg.add_array(msg.sig, "fty");
    word fty = msg.add_object(ftys, 0);
    msg.add_string(fty, "type", "cp_park");
    if (parked_to_num || parked_to_sip) {
        word parked_to = msg.add_object(fty, "parked_to");
        msg.add_string(parked_to, "num", parked_to_num);
        msg.add_string(parked_to, "sip", parked_to_sip);
    }
    msg.Send(registration);
}

void SignalingCall::Mute()
{
    SignalingSigMessage msg(call, "facility", alloca(1000), alloca(100));
    MonitorXmit("facility_mute", msg, msg.sig);
}

void SignalingCall::AudioRouteChanged()
{
    SignalingSigMessage msg(call, "facility", alloca(1000), alloca(100));
    MonitorXmit("facility_audio_route_changed", msg, msg.sig);
}

const char * SignalingCall::GetNameIdentification(class json_io & msg, word base)
{
    if (base != 0xffff) {
        for (word last = 0, f = msg.get_object(base, last); f != 0xffff; f = msg.get_object(base, last)) {
            const char * ftyType = msg.get_string(f, "type");
            if (ftyType && !strcmp(ftyType, "name_identification")) {
                return msg.get_string(f, "name");
            }
        }
    }
    return NULL;
}

void SignalingCall::Message(const char * type, class json_io & msg, word base)
{
    if (registration && registration->signaling && registration->signaling->log) registration->signaling->log->Log(LOG_SIGNALING, "%x/%i:SignalingCall::Message(%s) state:%x hookState:%d reconnectedCall:%p", this, call, type, state, hookState, reconnectedCall);

    if (reconnectedCall && remoteCallId && !(state & SIG_CALL_STATE_CONN)) reconnectCallResponse.Start(15000);

    bool isPendingReconnect = false;
    word fty = msg.get_array(base, "fty");
    word cg = msg.get_object(base, "cg");
    if (cg != 0xffff) {
        const char* nameIdentification = GetNameIdentification(msg, fty);
        const char* flags = msg.get_string(cg, "flags");
        bool restricted = flags && strchr(flags, 'R') ? true : false;
        const char* num = msg.get_string(cg, "num");
        const char* sip = msg.get_string(cg, "sip");
        remote.SetRestricted(restricted);
        remote.SetNum(num);
        remote.SetSip(sip);
        if (!restricted && !nameIdentification && num && registration && registration->signaling->user) {
            registration->signaling->user->Lookup(num, num, registration->dialingLocation.prefixIntl, registration->dialingLocation.prefixNtl, registration->dialingLocation.prefixSubs, registration->dialingLocation.country, registration->dialingLocation.area, registration->dialingLocation.subscriber);
        }
        this->recordingInfo.isExternal = flags && strchr(flags, 'P') ? false : true;
        this->recordingInfo.isRecordingByDefaultOn = registration->signaling->service->mediaConfig->isRecordingByDefaultOn;
        if (registration && registration->pendingReconnectCall && 
            ((registration->pendingReconnectCall->remote.num && num && !strcmp(registration->pendingReconnectCall->remote.num, num)) || (registration->pendingReconnectCall->remote.sip && sip && !strcmp(registration->pendingReconnectCall->remote.sip, sip)))) {
            isPendingReconnect = true;
        }
    }
    if (fty != 0xffff) {
        bool reportNameIdentification = false;
        bool reconnectSetupFty = false;
        for (word last = 0, f = msg.get_object(fty, last); f != 0xffff; f = msg.get_object(fty, last)) {
            const char * ftyType = msg.get_string(f, "type");
            if (ftyType) {
                if (!strcmp(ftyType, "name_identification")) {
                    if (msg.get_string(f, "name") && (!remote.dn || !remote.dn[0])) {
                        remote.SetDn(msg.get_string(f, "name"));
                        reportNameIdentification = true;
                    }
                }
                else if (!strcmp(ftyType, "diverting_leg1")) {
                    remote.Clear(); // clear all including DN
                    word nominated = msg.get_object(f, "nominated");
                    if (nominated != 0xffff) {
                        remote.SetNum(msg.get_string(nominated, "num"));
                        remote.SetSip(msg.get_string(nominated, "sip"));
                        reportNameIdentification = true;
                    }
                }
                else if (!strcmp(ftyType, "diverting_leg2")) {
                    word diverting = msg.get_object(f, "diverting");
                    if (diverting != 0xffff) {
                        this->diverting.SetNum(msg.get_string(diverting, "num"));
                        this->diverting.SetSip(msg.get_string(diverting, "sip"));
                        this->diverting.SetDn(msg.get_string(f, "diverting_name"));
                        reportNameIdentification = true;
                    }
                    word originalCalled = msg.get_object(f, "original_called");
                    if (originalCalled != 0xffff) {
                        this->originalCalled.SetNum(msg.get_string(originalCalled, "num"));
                        this->originalCalled.SetSip(msg.get_string(originalCalled, "sip"));
                        this->originalCalled.SetDn(msg.get_string(f, "original_called_name"));
                        reportNameIdentification = true;
                    }
                }
                else if (!strcmp(ftyType, "diverting_leg3")) {
                    remote.Clear(); // clear all including DN
                    word redirection = msg.get_object(f, "redirection");
                    if (redirection != 0xffff) {
                        remote.SetNum(msg.get_string(redirection, "num"));
                        remote.SetSip(msg.get_string(redirection, "sip"));
                        reportNameIdentification = true;
                    }
                }
                else if (!strcmp(ftyType, "ct_setup")) {
                    word dst = msg.get_object(f, "dst");
                    if (dst != 0xffff) {
                        this->transfering.SetNum(msg.get_string(dst, "num"));
                        this->transfering.SetSip(msg.get_string(dst, "sip"));
                        reportNameIdentification = true;
                    }
                }
                else if (!strcmp(ftyType, "ct_initiate")) {
                    // {"type":"ct_initiate","id":960051513,"dst":{"num":"70532"}}
                    // {"type":"ct_initiate","dst":{"flags":"I","num":"4970317300924"}}
                    this->fty_ct_initiate.id = msg.get_unsigned(f, "id");
                    word dst = msg.get_object(f, "dst");
                    if (dst != 0xffff) {
                        const char* num = msg.get_string(dst, "num");
                        const char* flags = msg.get_string(dst, "flags");
                        if (num && flags && strchr(flags, 'I')) {
                            char buf [256];
                            isnprintf(buf, sizeof(buf), "+%s", num);
                            this->fty_ct_initiate.num = _strdup(buf);
                        }
                        else {
                            this->fty_ct_initiate.num = _strdup(num);
                        }
                        this->fty_ct_initiate.sip = _strdup(msg.get_string(dst, "sip"));
                        if (this->remote.dn && num && this->remote.num && !strcmp(num, this->remote.num)) {
                            this->fty_ct_initiate.dn = _strdup(this->remote.dn);
                        }
                    }
                }
                else if (!strcmp(ftyType, "ct_complete")) {
                    // {"type":"ct_complete","remote":{"num":"00049703173009670"},"remote_name":""}
                    // {"type":"ct_complete","end":1,"remote_name":""}
                    word remote = msg.get_object(f, "remote");
                    if (remote != 0xffff) {
                        const char* flags = msg.get_string(remote, "flags");
                        bool restricted = flags && strchr(flags, 'R') ? true : false;
                        const char* num = msg.get_string(remote, "num");
                        const char* sip = msg.get_string(remote, "sip");
                        const char* remoteName = msg.get_string(f, "remote_name");
                        const char* nameIdentification = GetNameIdentification(msg, fty);
                        this->remote.SetRestricted(restricted);
                        this->remote.SetNum(num);
                        this->remote.SetSip(sip);
                        this->remote.SetDn(remoteName);
                        if (nameIdentification) this->remote.SetDn(nameIdentification);
                        reportNameIdentification = true;

                        if (!restricted && !nameIdentification && num && !sip && !remoteName && registration && registration->signaling->user) {
                            registration->signaling->user->Lookup(num, num, registration->dialingLocation.prefixIntl, registration->dialingLocation.prefixNtl, registration->dialingLocation.prefixSubs, registration->dialingLocation.country, registration->dialingLocation.area, registration->dialingLocation.subscriber);
                        }
                    }
                    else {
                        this->remote.SetRestricted(true);
                        this->remote.SetNum(nullptr);
                        this->remote.SetSip(nullptr);
                        this->remote.SetDn(nullptr);
                    }
                }
                else if (!strcmp(ftyType, "presence_notify")) {
                    const char * contact = msg.get_string(f, "contact");
                    const char * status = msg.get_string(f, "status");
                    const char * activity = msg.get_string(f, "activity");
                    const char * note = msg.get_string(f, "note");
                    if (!alertPresence) {
                        alertPresence = new class PresenceInfo(contact, status, activity, note);
                    }
                }
                else if (!strcmp(ftyType, "video_conn")) {
                    const char * guid = msg.get_string(f, "guid");
                    bool passive = msg.get_bool(f, "passive");
                    if (conferenceGuid) free(conferenceGuid);
                    conferenceGuid = guid ? _strdup(guid) : 0;
                    conferenceType = guid ? (passive ? "3pty" : "central") : 0;
                }
                else if (!strcmp(ftyType, "video_capabilities")) {
                    multiVideoSupport = msg.get_bool(f, "multivideo");
                }
                else if (!strcmp(ftyType, "conference_info")) {
                    conferenceInfo.read(msg, f);
                }
                else if (!strcmp(ftyType, "im_message")) {
                    chatMessage.read(msg, f);
                }
                else if (!strcmp(ftyType, "innovaphone_remote_control")) {
                    const char* control = msg.get_string(f, "control");
                    if (!strcmp(control, "ANNOUNCE")) {
                        // auto-connect after 2 seconds alerting and auto-mute
                        autoConnectTimer.Start(2000);
                        announce = true;
                    }
                    else if (!strcmp(control, "CONNECT")) {
                        // NONE means nothing to be done and call is connected.
                        // Mantis 409545. Some kind of SOAP application doing something strange...
                        if ((remoteMediaCmd == MEDIA_CMD_NONE) && ((this->state == SIG_CALL_STATE_CONN) || (this->state == (SIG_CALL_STATE_CONN | SIG_CALL_STATE_OUTG)))) {
                            if (registration && registration->signaling && registration->signaling->log) registration->signaling->log->Log(LOG_SIGNALING, "%x/%i:SignalingCall::Message ignore RCC Connect state=%x", this, call, this->state);
                            return;
                        }
                        StopRinging();
                        ChangeState(SIG_CALL_STATE_CONN | SIG_CALL_STATE_OUTG);
                        if (const char* confIdString = msg.get_string(base, "confId")) {
                            str::to_hexmem(confIdString, conf.b, sizeof(conf.b), true);
                            if (this->confId) free(this->confId);
                            this->confId = _strdup(confIdString);
                        }
                        isRccCall = true;
                        // Other calls to Hold if connected
                        class btree* b = registration->calls->btree_find_next_right(0);
                        while (b) {
                            class SignalingCall* c = (class SignalingCall*)b;
                            if (c != this && ((c->state == SIG_CALL_STATE_CONN) || (c->state == (SIG_CALL_STATE_CONN | SIG_CALL_STATE_OUTG)))) c->Hold();
                            b = registration->calls->btree_find_next_right((void*)(intp)c->call);
                        }
                        if (!closeState) {
                            class SignalingSigMessage send(call, "conn", alloca(10000), alloca(1000));
                            if (localAudioConfig) SendLocalMedia(send, send.sig, send.tmp);
                            MonitorXmit("conn", send, send.sig);
                            send.Send(registration);
                            if (audioDeviceId) {
                                registration->signaling->SendHookKey(audioDeviceId, KEY_OFFHOOK, call, remote.num ? remote.num : remote.sip, remote.dn);
                                hookState = KEY_OFFHOOK;
                            }
                        }
                        else Retrieve();
                        return; // no default "setup" handling
                    }
                    else if (!strcmp(control, "MIC_OFF")) {
                        if (audioIo) {
                            muted = true;
                            audioIo->Mute(true, false);
                            Mute();
                        }
                    }
                    else if (!strcmp(control, "MIC_ON")) {
                        if (audioIo) {
                            muted = false;
                            audioIo->Mute(false, false);
                            Mute();
                        }
                    }
                    else if (!strcmp(control, "GET_AUDIO_MODE")) {
                        enum AudioDeviceType type = (enum AudioDeviceType)audioIo->GetDeviceIoType(audioDeviceId);
                        const char * headsetDeviceId = audioIo->GetDeviceByType(HeadsetDevice);
                        // availabe modes in upper half
                        // current mode in lower half
                        enum { HANDSET = 0x1, HANDSFREE = 0x2, HEADSET = 0x4 };
                        unsigned char available = (HANDSET | HANDSFREE | (headsetDeviceId ? HEADSET : 0)), current = 0;
                        if (type == HandsetDevice) current |= HANDSET;
                        else if (type == HandsfreeDevice) current |= HANDSFREE;
                        else if (type == HeadsetDevice) current |= HEADSET;
                        unsigned mode = (available << 4) | current;
                        // return "innovaphone_remote_control" with "AUDIO_MODE"
                        SignalingSigMessage send(call, "facility", alloca(1000), alloca(100));
                        word ftys = send.add_array(send.sig, "fty");
                        word fty = send.add_object(ftys, 0);
                        send.add_string(fty, "type", "innovaphone_remote_control");
                        send.add_string(fty, "control", "AUDIO_MODE"); 
                        send.add_unsigned(fty, "mode", mode, send.tmp);
                        send.Send(registration);
                    }
                    else if (!strcmp(control, "HANDSET")) {
                        ChangeAudioDevice(audioIo->GetDeviceByType(HandsetDevice));
                    }
                    else if (!strcmp(control, "HEADSET")) {
                        ChangeAudioDevice(audioIo->GetDeviceByType(HeadsetDevice));
                    }
                    else if (!strcmp(control, "HANDSFREE")) {
                        ChangeAudioDevice(audioIo->GetDeviceByType(HandsfreeDevice));
                    }
                    else if (!strcmp(control, "DTMF_DIGIT")) {
                        SendDTMF(msg.get_string(f, "digit"), true);
                    }
                    else if (!strcmp(control, "INFO_REMOTE_HOLD")) {
                        // PBX performed remote_hold on behalf of us
                        if (!(state & SIG_CALL_STATE_HOLD)) {
                            ChangeState(state | SIG_CALL_STATE_HOLD);
                            if (registration->signaling->user) {
                                registration->signaling->user->ReportCallHold(confId);
                            }
                        }
                    }
                    else if (!strcmp(control, "INFO_REMOTE_RETRIEVE")) {
                        // PBX performed remote_hold on behalf of us
                        if (state & SIG_CALL_STATE_HOLD) {
                            ChangeState(state & ~SIG_CALL_STATE_HOLD);
                            if (registration->signaling->user) {
                                //registration->signaling->user->ReportCallRetrieve(confId);
                            }
                        }
                        // Other calls to Hold if connected
                        class btree* b = registration->calls->btree_find_next_right(0);
                        while (b) {
                            class SignalingCall* c = (class SignalingCall*)b;
                            if (c != this && ((c->state == SIG_CALL_STATE_CONN) || (c->state == (SIG_CALL_STATE_CONN | SIG_CALL_STATE_OUTG)))) c->Hold();
                            b = registration->calls->btree_find_next_right((void*)(intp)c->call);
                        }
                    }
                    else if (!strcmp(control, "HOLD")) {
                        Hold();
                    }
                    else if (!strcmp(control, "RETRIEVE")) {
                        // Other calls to Hold if connected
                        class btree* b = registration->calls->btree_find_next_right(0);
                        while (b) {
                            class SignalingCall* c = (class SignalingCall*)b;
                            if (c != this && ((c->state == SIG_CALL_STATE_CONN) || (c->state == (SIG_CALL_STATE_CONN | SIG_CALL_STATE_OUTG)))) c->Hold();
                            b = registration->calls->btree_find_next_right((void*)(intp)c->call);
                        }
                        Retrieve();
                    }
                    else if (!strcmp(control, "3PTY_ON")) {
                        Retrieve();
                    }
                }
                else if (!strcmp(ftyType, "reconnect_init")) {
                    debug->printf("%x/%i:SignalingCall::Message(%s) reconnect_init FTY hw:%s id:%d", this, call, type, registration->GetHwId() ? registration->GetHwId() : "", this->recCallId);
                    reconnectInitRecv = true;
                }
                else if (!strcmp(ftyType, "reconnect_info")) {
                    remoteCallId = msg.get_unsigned(f, "id");
                    if (remoteHwId) free(remoteHwId);
                    remoteHwId = _strdup(msg.get_string(f, "hw"));
                    if (remoteSipId) free(remoteSipId);
                    remoteSipId = _strdup(msg.get_string(f, "sip"));
                    debug->printf("%x/%i:SignalingCall::Message(%s) reconnect_info FTY hw:%s sip:%s id:%d", this, call, type, remoteHwId ? remoteHwId : "", remoteSipId ? remoteSipId : "", remoteCallId);
                }
                else if (!strcmp(ftyType, "reconnect_setup")) {
                    localCallId = msg.get_unsigned(f, "id");
                    if (localHwId) free(localHwId);
                    localHwId = _strdup(msg.get_string(f, "hw"));
                    this->reconnectSetupRecv = true;
                    reconnectSetupFty = true;
                }
            }
        }
        if (reportNameIdentification && registration && registration->signaling && registration->signaling->user) {
            unsigned offset = registration->ExternalLinePrefixOffset(remote.num);
            char remoteSip[1024];
            registration->signaling->user->ReportNameIdentification(confId, remote.num + offset, registration->signaling->AppendSipDomain(remote.sip, remoteSip, sizeof(remoteSip)), remote.dn,
                this->diverting.dn ? this->diverting.dn : this->diverting.sip ? this->diverting.sip : this->diverting.num ? this->diverting.num : NULL,
                this->originalCalled.dn ? this->originalCalled.dn : this->originalCalled.sip ? this->originalCalled.sip : this->originalCalled.num ? this->originalCalled.num : NULL,
                videoAllowed, true, true, true, true, reconnectedCall ? true : false);
        }
        if (reconnectSetupFty || isPendingReconnect) {
            if (const char* confIdString = msg.get_string(base, "confId")) {
                str::to_hexmem(confIdString, this->conf.b, sizeof(this->conf.b), true);
                if (this->confId) free(this->confId);
                this->confId = _strdup(confIdString);
            }
            debug->printf("%x/%i:SignalingCall::Message(%s) reconnect_setup FTY hw:%s id:%d(pending=%d) (%s) reconnectingCallDelete=%p pendingReconnectCall=%p", this, call, type, localHwId ? localHwId : "", localCallId, isPendingReconnect, this->confId ? this->confId : "-", registration->reconnectingCallDelete, registration->pendingReconnectCall);
            class SignalingCall* c = nullptr;
            if (registration && registration->reconnectingCallDelete && registration->reconnectingCallDelete->recCallId == localCallId) {
                c = registration->reconnectingCallDelete;
            }
            else if (registration && registration->pendingReconnectCall && ((registration->pendingReconnectCall->recCallId == localCallId) || isPendingReconnect)) {
                c = registration->pendingReconnectCall;
                c->reconnectCallRequest.Cancel();
            }
            else if (registration && registration->calls) {
                class btree* b = registration->calls->btree_find_next_right(0);
                while (b) {
                    c = (class SignalingCall*)b;
                    if (c->recCallId == localCallId) break;
                    b = registration->calls->btree_find_next_right((void*)(intp)c->call);
                }
            }
            if (!c) {
                debug->printf("%x/%i:SignalingCall::Message(%s) original call not found!", this, call, type);
                class SignalingSigMessage send(call, "rel", alloca(1000), alloca(100));
                send.Send(registration);
                return;
            }

            this->remote.SetNum(c->remote.num);
            this->remote.SetSip(c->remote.sip);
            this->remote.SetDn(c->remote.dn);
            if (registration->signaling->videoLicense) {
                this->videoAllowed = c->videoAllowed;
            }

            RecvRemoteMedia(msg, base);

            MediaInitialize(MediaType::AUDIO);

            if (videoIo && !videoDisabled && ((videoAllowed && (remoteMediaCmd == MEDIA_CMD_REQUEST)) || (remoteVideoConfig && remoteVideoConfig->codecList.front()))) {
                if (!videoIoChannel) videoIoChannel = videoIo->CreateChannel(this);
                if (!registration->signaling->doNotStartCamera) {
                    if (videoDeviceId) free(videoDeviceId);
                    videoDeviceId = _strdup(c->videoDeviceId);
                }
                if (!media2) {
                    MediaInitialize(MediaType::VIDEO);
                }
            }

            SendConn();

            c->reconnectingCall = this;
            this->reconnectedCall = c;

            // Do not inform UI about this call
            //MonitorRecv(type, msg, base);
            return;
        }
    }

    if (!strcmp(type, "setup")) {
        if (toneEndTimer.IsRunning()) AbortTone();

        callWaiting = (registration->calls->get_count() > 1); // already have other call(s)
        if (!callWaiting) {
            if (registration) {
                if (audioDeviceId) free(audioDeviceId);
                audioDeviceId = _strdup(audioIo->GetDeviceIo(registration->GetHwId()));
                if (audioDeviceId) {
                    if (audioIo->DeviceCapabilities(audioDeviceId) & AUDIO_CAPABILITY_HOOK_DEVICE_SUPPORTS_LOCKING) {
                        registration->signaling->hookDeviceState = HOOK_DEVICE_WAITING;
                        audioIo->AcquireHookDevice(audioDeviceId);
                    }
                    else registration->signaling->hookDeviceState = HOOK_DEVICE_ACQUIRE;
                }
            }
        }

        if (const char * confIdString = msg.get_string(base, "confId")) {
            str::to_hexmem(confIdString, conf.b, sizeof(conf.b), true);
            if (this->confId) free(this->confId);
            this->confId = _strdup(confIdString);
        }

        MediaInitialize(MediaType::AUDIO);

        if (registration && registration->signaling->user) {
            unsigned offset = registration->ExternalLinePrefixOffset(remote.num);
            char remoteSip[1024];
            registration->signaling->user->ReportNewIncomingCall(
                    this->confId, remote.num + offset, offset, registration->signaling->AppendSipDomain(remote.sip, remoteSip, sizeof(remoteSip)), remote.dn,
                    this->diverting.dn ? this->diverting.dn : this->diverting.sip ? this->diverting.sip : this->diverting.num ? this->diverting.num : NULL,
                    this->originalCalled.dn ? this->originalCalled.dn : this->originalCalled.sip ? this->originalCalled.sip : this->originalCalled.num ? this->originalCalled.num : NULL,
                    videoAllowed, true, true, true, true);
        }

        ChangeState(SIG_CALL_STATE_SETUP | SIG_CALL_STATE_OUTG);

        if (!remoteMediaCmd && (msg.get_int(base, "channel") == -1)) {
            remoteMediaCmd = MEDIA_CMD_REQUEST; // fixing undefined remoteMediaCmd
        }

        if (registration->signaling->hookDeviceState == HOOK_DEVICE_WAITING) alertPostponedOnHookDevice = true;
    }
    else if (!strcmp(type, "setup_ack")) {
        ChangeState(SIG_CALL_STATE_SETUP_ACK);
    }
    else if (!strcmp(type, "call_proc")) {
        if (state & SIG_CALL_STATE_CONN) {
            debug->printf("%x/%i:SignalingCall::Message call already connected, ignore call proceeding message");
            return;
        }
        ChangeState(SIG_CALL_STATE_CALL_PROC);
    }
    else if (!strcmp(type, "alert")) {
        ChangeState(SIG_CALL_STATE_ALERT);
        if(audioIo) MediaTones::StartTone(MEDIA_TONES_RINGBACK, registration->dialingLocation.tones, audioIo);
        word diverting_leg1 = find_fty(msg, base, "diverting_leg1");
        word ag = msg.get_object(base, "ag");
        if (ag != 0xffff && diverting_leg1 == 0xffff) {
            remote.SetNum(msg.get_string(ag, "num"));
            remote.SetSip(msg.get_string(ag, "sip"));
        }
        if (registration->signaling->user) {
            unsigned offset = registration->ExternalLinePrefixOffset(remote.num);
            char remoteSip[1024];
            registration->signaling->user->ReportOutgoingCallAlerting(confId, remote.dn, registration->signaling->AppendSipDomain(remote.sip, remoteSip, sizeof(remoteSip)), remote.num + offset, offset, reconnectedCall ? true : false);
        }
    }
    else if (!strcmp(type, "conn")) {
        if (connReceived) {
            debug->printf("%x/%i:SignalingCall::Message ignore additional conn message", this, call);
            return;
        }
        word cn = msg.get_object(base, "cn");
        if (cn != 0xffff) {
            remote.SetNum(msg.get_string(cn, "num"));
            remote.SetSip(msg.get_string(cn, "sip"));
        }
        connReceived = true;
        ChangeState(SIG_CALL_STATE_CONN);
        if(audioIo) MediaTones::StopTone(audioIo);
    }
    else if (!strcmp(type, "disc")) {
        word cau = msg.get_object(base, "cau");
        if (cau != 0xffff) this->cause = msg.get_int(cau, "num");
        bool inband_info = msg.get_bool(base, "inband_info");

        if (registration) {
            unsigned delay = 0;
            int autoHangup = registration->signaling->autoHangup;
            if (autoHangup == AUTOHANGUP_ALLCALLS) delay = 0;
            else if (this->cause == Q931_CAUSE_NonSelectedUserClearing) delay = 0; // remove this call immediately
            else if ((state & SIG_CALL_STATE_OUTG) && (state & 0xF) < SIG_CALL_STATE_CONN) delay = 0; // inbound un-answered calls
            else if ((state & 0xF) == SIG_CALL_STATE_CONN) delay = (autoHangup == AUTOHANGUP_CONNECTEDCALLS) ? 0 : 2500;
            // disc arrives before rel
            else if ((state & 0xF) == SIG_CALL_STATE_DISC_RCVD) delay = 2500;
            else if (this->cause == Q931_CAUSE_NormalCallClearing) delay = 2500;
            else delay = 10000; // keep failed outbound calls for even longer time and wait for user to hangup
            if (delay && !inband_info) {
                MediaTones::StartTone(MEDIA_TONES_BUSY, registration->dialingLocation.tones, audioIo);
                if (audioDeviceId) free(audioDeviceId);
                if (registration->signaling->changedAudioDeviceId) audioDeviceId = _strdup(registration->signaling->changedAudioDeviceId);
                else audioDeviceId = _strdup(audioIo->GetDeviceIo(registration->GetHwId()));
                if (audioDeviceId) audioIo->StartDevice((void*)this, (void*)this, audioDeviceId, AudioDeviceMode::AudioModeCommunication);
            }
            toneEndTimer.Start(delay); // delete call after delay
        }
        ChangeState(SIG_CALL_STATE_DISC_RCVD);
    }
    else if (!strcmp(type, "progress")) {
        if (registration) {
            word cau = msg.get_object(base, "cau");
            if (cau != 0xffff) this->cause = msg.get_int(cau, "num");
            if (this->cause == Q931_CAUSE_TemporaryFailure) {
                debug->printf("%x/%i:SignalingCall::Message(%s) reconnectInitRecv=%d(%d) remoteCallId=%d reconnectingCall=%p reconnectedCall=%p", this, call, type, reconnectInitRecv, reconnectSetupRecv, remoteCallId, reconnectingCall, reconnectedCall);
                if (!reconnectInitRecv && !reconnectSetupRecv) {
                    if (remoteCallId && !reconnectingCall && !reconnectedCall) {
                        CreateReconnectCall();                                    // progress for original call
                    }
                    else if (!reconnectingCall && reconnectedCall) {
                        reconnectedCall->ReconnectCall();                         // progress for reconnecting call, release but start a new reconnecting call
                        this->cause = 0;
                        SendRel();
                        return;
                    }
                }
            }
        }
    }
    else if (!strcmp(type, "rel")) {
        word cau = msg.get_object(base, "cau");
        if (cau != 0xffff) this->cause = msg.get_int(cau, "num");
        if (this->cause == Q931_CAUSE_TemporaryFailure) {
            debug->printf("%x/%i:SignalingCall::Message(%s) reconnectInitRecv=%d(%d) remoteCallId=%d reconnectingCall=%p reconnectedCall=%p", this, call, type, reconnectInitRecv, reconnectSetupRecv, remoteCallId, reconnectingCall, reconnectedCall);
            if (reconnectInitRecv || reconnectSetupRecv) {
                reconnectCallRequest.Start(30000);
                return;
            }
            else {
                if (remoteCallId && !reconnectingCall && !reconnectedCall) {
                    CreateReconnectCall();                                    // rel for original call which did not start a reconnected call since !reconnectingCall
                    return;
                }
                else if (reconnectingCall && !reconnectedCall) {
                    return;                                                   // rel for original call with a reconnected call running
                }
                else if (!reconnectingCall && reconnectedCall) {
                    failedReconnect = true;
                    reconnectedCall->ReconnectCall();                         // rel for reconnecting call, release but start a new reconnecting call
                }
            }
        }
        else if (reconnectedCall) {
            debug->printf("%x/%i:SignalingCall::Message(%s) this->cause=%d reconnectedCall=%p", this, call, type, this->cause, reconnectedCall);
            if (this->cause == Q931_CAUSE_ServiceOrOptionNotAvailable_Unspecified) {
                reconnectedCall->reconnectingCall = nullptr;
                reconnectedCall->ClearReconnectedCall();
                reconnectedCall = nullptr;
            }
            else {
                reconnectedCall->reconnectingCall = nullptr;
                reconnectedCall->StartReconnectTimer();
            }
            Delete();
            return;
        }
        if (registration) {
            if (hookAudioDeviceId) free(hookAudioDeviceId);
            hookAudioDeviceId = _strdup(audioDeviceId);
            if (registration->calls->get_count() < 2) {
                if (videoIo && videoDeviceId) {
                    videoIo->StopDevice((void*)this, videoDeviceId);
                    free(videoDeviceId);
                    videoDeviceId = NULL;
                    videoIo->UnshareAllApplications((void*)this);
                }
            }
            StopRinging();

            word cp_park_result = find_fty(msg, base, "cp_park_result");

            if (replacementCall) {
                replacementCall->replacedCall = nullptr;
                replacementCall = nullptr;
            }

            if (reconnectingCall) {
                if (registration->activePhoneCall == this) registration->activePhoneCall = reconnectingCall;
                reconnectingCall->reconnectedCall = nullptr;
                reconnectingCall = nullptr;
            }

            bool inbound = (this->state & SIG_CALL_STATE_OUTG) ? true : false;
            byte state = (this->state & 0xF);
            unsigned delay = 0;
            int autoHangup = registration->signaling->autoHangup;
            if (autoHangup == AUTOHANGUP_ALLCALLS) delay = 0;
            else if (cp_park_result != 0xffff) delay = 0; // remove this call immediately
            else if (cause == Q931_CAUSE_NonSelectedUserClearing) delay = 0; // remove this call immediately
            else if (inbound && state < SIG_CALL_STATE_CONN) delay = 0; // inbound un-answered calls
            else if (!inbound && state < SIG_CALL_STATE_CONN && cause == Q931_CAUSE_NormalCallClearing) delay = 0; // outbound call cancelled via RCC
            else if (reconnectedCall) delay = 0;
            else if (state == SIG_CALL_STATE_CONN) delay = (autoHangup == AUTOHANGUP_CONNECTEDCALLS) ? 0 : 2500;
            // disc arrives before rel
            else if (state == SIG_CALL_STATE_DISC_RCVD) delay = 2500;
            else if (cause == Q931_CAUSE_NormalCallClearing) delay = 2500;
            else delay = 10000; // keep failed outbound calls for even longer time and wait for user to hangup
            {   // no busy-tone if there's another call active
                class SignalingCall * c = (class SignalingCall *)registration->calls->btree_find_next_right(0);
                while (c) {
                    if ((c != this) && (c->state & SIG_CALL_STATE_CONN) && !(c->state & SIG_CALL_STATE_HOLD)) delay = 0;
                    c = (class SignalingCall *)registration->calls->btree_find_next_right((void *)(intp)c->call);
                }
            }
            if (delay) {
                MediaTones::StartTone(MEDIA_TONES_BUSY, registration->dialingLocation.tones, audioIo);
            }
            else if (audioIo && audioDeviceId && registration->calls->get_count() < 2) {
                audioIo->StopDevice((void *)this, audioDeviceId);
                free(audioDeviceId);
                audioDeviceId = NULL;
            }

            if (reconnectedCall) {
                reconnectedCall->reconnectingCall = nullptr;
                reconnectedCall = nullptr;
            }

            toneEndTimer.Start(delay); // delete call after delay
        }
        ChangeState(0);
    }

    // RecvRemoteMedia must be processed before the code below is executed in case a PROPOSAL arrives with Video and remoteVideoConfig is then set.
    if (ChannelsCmdType(msg.get_string(base, "channels_cmd"))) RecvRemoteMedia(msg, base);

    if (!strcmp(type, "channels") && msg.get_string(base, "channels_cmd") && !strcmp(msg.get_string(base, "channels_cmd"), "PROPOSAL")) {       // This should be called after a RETRIEVE - PROPOSAL
        if (!media) {
            MediaInitialize(MediaType::AUDIO);
        }
        debug->printf("%x/%i:SignalingCall::Message(%s) PROPOSAL video=%d videoDisabled=%d media2=%p remoteVideoConfig=%x", this, call, type, videoAllowed, videoDisabled, media2, remoteVideoConfig);
        if (!media2 && (!videoDisabled) && (remoteVideoConfig && remoteVideoConfig->codecList.front())) {
            // Remote side has proposed video
            // videoAllowed tells us if we should start the webcam but we can receive remote video. 
            // Therefore we should allocate the media channel to receive remote video stream
            if (videoIo && videoAllowed && !videoDeviceId && !registration->signaling->doNotStartCamera) {
                videoDeviceId = _strdup(videoIo->GetDeviceIo(registration->GetHwId()));
                videoIo->StartVideoDevice((void*)this, (void*)this, videoDeviceId, 1280, 720, 0);
            }
            MediaInitialize(MediaType::VIDEO);
            // Since localAudioConfig is set, it means signaling got a new PROPOSAL with Video.
            // media is initialized above if this is a complete new PROPOSAL with audio and video. If this case localAudioConfig would be unset
            if (localAudioConfig && localAudioConfig->ice) {
                GenerateIceCredentials(localAudioConfig->ice);
                media->RewriteIceCredentials(localAudioConfig->ice->iceUfrag, localAudioConfig->ice->icePwd);
            }
        }
        // leave state unchanged
    }
    else if (!strcmp(type, "channels") && msg.get_string(base, "channels_cmd") && !strcmp(msg.get_string(base, "channels_cmd"), "REQUEST") && (!media2 || (initializing & (1 << VIDEO))) && videoAllowed && !videoDisabled) {
        debug->printf("%x/%i:SignalingCall::Message(%s) REQUEST add video", this, call, type);
        // REQUEST from the PBX
        // videoAllowed tells us if we should start the webcam but we can receive remote video. 
        // Therefore we should allocate the media channel to receive remote video stream
        if (videoIo && !registration->signaling->doNotStartCamera) {
            videoDeviceId = _strdup(videoIo->GetDeviceIo(registration->GetHwId()));
            videoIo->StartVideoDevice((void*)this, (void*)this, videoDeviceId, 1280, 720, 0);
        }
        if (!media2) {
            MediaInitialize(MediaType::VIDEO);
        }
    }
    // Should we initialize Video with setup? Two options:   
    else if (!strcmp(type, "setup") && !media2 && !videoDisabled) {
        bool initializeVideo = false;
        // PROPOSAL in setup with Video
        if (msg.get_string(base, "channels_cmd")) {
            if (!strcmp(msg.get_string(base, "channels_cmd"), "PROPOSAL") && (remoteVideoConfig && remoteVideoConfig->codecList.front())) initializeVideo = true;
        }
        // Empty setup, no channels and auto-video is on
        else if((msg.get_object(base, "channels") == JSON_ID_NONE) && videoAllowed) initializeVideo = true;
        if (initializeVideo) {
            MediaInitialize(MediaType::VIDEO);
        }
    }

    fty = msg.get_array(base, "fty");
    if (fty != 0xffff) {
        for (word last = 0, f = msg.get_object(fty, last); f != 0xffff; f = msg.get_object(fty, last)) {
            const char * type = msg.get_string(f, "type");
            if (type) {
                if (!strcmp(type, "ct_identify_result")) {
                    if (ctInitiate) {
                        SignalingSigMessage send(ctInitiate->call, "facility", alloca(1000), alloca(100));
                        word ftys = send.add_array(send.sig, "fty");
                        word fty = send.add_object(ftys, 0);
                        send.add_string(fty, "type", "ct_initiate");
                        send.add_unsigned(fty, "id", msg.get_unsigned(f, "id"), send.tmp);
                        send.Send(ctInitiate->registration);
                        ctInitiate->ctIdentify = 0;
                        ctInitiate = 0;
                    }
                }
                else if (!strcmp(type, "call_waiting")) {
                    ChangeState(state | SIG_CALL_STATE_CALL_WAITING);
                }
                else if (!strcmp(type, "co_alerting")) {
                    ChangeState(state & ~SIG_CALL_STATE_CALL_WAITING);
                }
                else if (!strcmp(type, "hold_notify")) {
                    ChangeState(state | SIG_CALL_STATE_HELD);
                }
                else if (!strcmp(type, "retrieve_notify")) {
                    ChangeState(state & ~SIG_CALL_STATE_HELD);
                }
                else if (!strcmp(type, "video_priority")) {
                    remoteVideoPriority = msg.get_bool(f, "on");
                    if(videoIo) videoIo->RemoteVideoPriority((void*)this, (const char*)confId, remoteVideoPriority);
                }
                if (!strcmp(type, "innovaphone_recording_state")) {
                    this->recordingInfo.isTrunkRecording = true;
                    SignalingSigMessage message(call, "facility", alloca(1000), alloca(100));
                    if (msg.get_bool(f, "on")) MonitorXmit("facility_recording_started", message, message.sig);
                    else MonitorXmit("facility_recording_stopped", message, message.sig);
                }
            }
        }
    }

    if (replacedCall) {
        class SignalingCall * c = replacedCall;
        replacedCall->replacementCall = nullptr;
        replacedCall = nullptr;
        c->SendRel();
    }

    if(!reconnectedCall && !failedReconnect) MonitorRecv(type, msg, base);
    conferenceInfo.cleanup();
    chatMessage.cleanup();
}

void SignalingCall::GenerateIceCredentials(class IceCandidates* ice)
{
    if (ice) {
        if (ice->icePwd) free(ice->icePwd);
        ice->icePwd = GenerateRandomString(22);
        if (ice->iceUfrag) free(ice->iceUfrag);
        ice->iceUfrag = GenerateRandomString(4);
    }
}

char* SignalingCall::GenerateRandomString(int len)
{
    char* result = (char*)malloc((len + 1) * sizeof(char));
    if (result) {
        for (int i = 0; i < len; i++) {
            int u = (IRandom::GetRandom() % 64);
            result[i] = charPool[u];
        }
        result[len] = 0x0;
    }
    return result;
}

void SignalingCall::MediaInitializeComplete(IMedia* const media, class MediaConfig* localMediaConfig)
{
    enum MediaType mediaType = localMediaConfig->type;
    const char* type = (mediaType == MediaType::AUDIO) ? "AUDIO" : (mediaType == MediaType::VIDEO) ? "VIDEO" : "APPSHARING";
    initializing &= ~(1 << mediaType);

    debug->printf("%x/%i:SignalingCall::MediaInitializeComplete(%p) addr=%s type=%s state=%x connPostponed=%u selectProposalByConn=%u", this, call, localMediaConfig, localMediaConfig->defAddr, type, state, connPostponed, selectProposalByConn);

    if (localMediaConfig) {
        debug->printf("SignalingCall(%p)::MediaInitializeComplete (%s) %s:%d", this, (localMediaConfig->type == MediaType::AUDIO ? "AUDIO" : localMediaConfig->type == MediaType::VIDEO ? "VIDEO" : "APPSHARING"), (localMediaConfig->defAddr ? localMediaConfig->defAddr : ""), localMediaConfig->defPort);
        class Codec * c;
        for (c = localMediaConfig->codecList.front(); c; c = c->goNext()) {
            debug->printf("SignalingCall(%p)::MediaInitializeComplete codec (%d) %s:%d pt=%d", this, c->coder, (c->addr ? c->addr : ""), c->port, c->pt);
            class MediaConfig* remoteMediaConfig = mediaType == MediaType::AUDIO ? remoteAudioConfig : remoteVideoConfig;
            if (remoteMediaConfig) {
                class Codec * cr;
                for (cr = remoteMediaConfig->codecList.front(); cr; cr = cr->goNext()) {
                    if (cr->coder == c->coder) {
                        c->pt = cr->pt;
                        break;
                    }
                }
            }
        }
        if (localMediaConfig->ice) {
            debug->printf("SignalingCall(%p)::MediaInitializeComplete RtcpMux(%d) ICE(%s:%s) Count=%d", this, localMediaConfig->ice->rtcpMux,
                ((localMediaConfig->ice && localMediaConfig->ice->iceUfrag) ? localMediaConfig->ice->iceUfrag : ""), ((localMediaConfig->ice && localMediaConfig->ice->icePwd) ? localMediaConfig->ice->icePwd : ""), localMediaConfig->ice->count);
            class IceCandidate * c = localMediaConfig->ice->candidateList.front();
            if (c) {
                for (; c; c = c->goNext()) {
                    if (c->type == 0)
                        debug->printf("SignalingCall(%p)::MediaInitializeComplete candidate(%d) %s:%d,%d (%s) (%d,%d)", this, c->type, c->addr, c->rtpPort, c->rtcpPort, (c->foundation ? c->foundation : ""), c->rtpPriority, c->rtcpPriority);
                    else
                        debug->printf("SignalingCall(%p)::MediaInitializeComplete candidate(%d) %s:%d,%d (%s:%d,%d) (%s) (%d,%d)", this, c->type, c->addr, c->rtpPort, c->rtcpPort, c->relatedAddr, c->relatedRtpPort, c->relatedRtcpPort, (c->foundation ? c->foundation : ""), c->rtpPriority, c->rtcpPriority);
                }
            }
            else if (this->media == media) noAudioIceCandidates = true;
            else if (this->media2 == media) noVideoIceCandidates = true;
        }
    }
    debug->printf("DEBUG SignalingCall::MediaInitializeComplete() [1] initializing=%x deleting=%x media=%x this->media=%x this->media2=%x state=%d localMediaConfig=%p,%p hookDevice=%d state=%x", initializing, deleting, media, this->media, this->media2, state, this->localAudioConfig, this->localVideoConfig, registration && registration->signaling ? registration->signaling->hookDeviceState : 0, state);

    if (mediaType == MediaType::AUDIO) {
        this->localAudioConfig = localMediaConfig;
        if(remoteAudioConfig && remoteAudioConfig->ice) this->localAudioConfig->ice->rtcpMux = remoteAudioConfig->ice->rtcpMux;
    }
    else if (mediaType == MediaType::VIDEO || mediaType == MediaType::VIDEO_APPSHARING) {
        this->localVideoConfig = localMediaConfig;
        if (remoteVideoConfig && remoteVideoConfig->ice) this->localVideoConfig->ice->rtcpMux = remoteVideoConfig->ice->rtcpMux;
    }

    MediaInitialized();
}

void SignalingCall::MediaInitialized()
{
    if (!initializing) {
        isMediaInitialized = true;
        if (!deleting) {
            if (registration->signaling->hookDeviceState == HOOK_DEVICE_ACQUIRE) {
                switch (state) {
                case 0:
                    SendSetup();
                    if (audioDeviceId && !reconnectedCall) {
                        registration->signaling->SendHookKey(audioDeviceId, KEY_OFFHOOK, call, remote.num ? remote.num : remote.sip, remote.dn);
                        hookState = KEY_OFFHOOK;
                    }
                    break;
                case SIG_CALL_STATE_SETUP | SIG_CALL_STATE_OUTG:
                    if (connPostponed) SendConn();
                    else {
                        SendAlert();
                        if (audioIo) {
                            // Second incoming alert 
                            if (!audioDeviceId) {
                                if (registration->signaling->changedAudioDeviceId) audioDeviceId = _strdup(registration->signaling->changedAudioDeviceId);
                                else audioDeviceId = _strdup(audioIo->GetDeviceIo(registration->GetHwId()));
                            }
                            if (audioDeviceId) {
                                registration->signaling->SendHookKey(audioDeviceId, KEY_INCALL, call, remote.num ? remote.num : remote.sip, remote.dn);
                                hookState = KEY_INCALL;
                            }
                        }
                    }
                    break;
                case SIG_CALL_STATE_ALERT | SIG_CALL_STATE_OUTG:
                    if (!selectProposalByConn) SendConn();
                    else selectProposalByConn = false;
                    break;
                default:
                    if (connPostponed) SendConn();
                    else SendChannels();
                    break;
                }
                if (noAudioIceCandidates) {
                    char cfg[1024];
                    class json_io send(cfg);

                    debug->printf("%x/%i:SignalingCall::MediaInitialized missing ice candidates for audio", this, call);

                    word sendBase = 0xffff;
                    sendBase = send.add_object(0xffff, 0);
                    send.add_string(sendBase, "channel", confId);
                    send.add_string(sendBase, "type", "RemoteRtp");
                    send.add_string(sendBase, "kind", "audio");
                    send.add_string(sendBase, "errorText", "Could not open RTP port for audio");

                    if(!reconnectedCall) MonitorXmit("no_ice_candidates", send, sendBase);
                    noAudioIceCandidates = false;
                }
                if (noVideoIceCandidates) {
                    char cfg[1024];
                    class json_io send(cfg);

                    debug->printf("%x/%i:SignalingCall::MediaInitialized missing ice candidates for video", this, call);

                    word sendBase = 0xffff;
                    sendBase = send.add_object(0xffff, 0);
                    send.add_string(sendBase, "channel", confId);
                    send.add_string(sendBase, "type", "RemoteRtp");
                    send.add_string(sendBase, "kind", "video");
                    send.add_string(sendBase, "errorText", "Could not open RTP port for video");

                    if (!reconnectedCall) MonitorXmit("no_ice_candidates", send, sendBase);
                    noVideoIceCandidates = false;
                }
            }
            else if (registration->signaling->hookDeviceState == HOOK_DEVICE_WAITING) {
                debug->printf("DEBUG SignalingCall::MediaInitializeComplete() waiting on hookDevice ... ");
            }
            else {
                if (registration->signaling->hookDeviceState == HOOK_DEVICE_DENIED) {
                    char cfg[1024];
                    class json_io send(cfg);
                    word sendBase = send.add_object(0xffff, 0);
                    send.add_string(sendBase, "channel", confId);
                    send.add_string(sendBase, "errorText", "Access denied to hook device");
                    send.add_string(sendBase, "errorId", "hook_device_denied");

                    if (!reconnectedCall) MonitorXmit("hook_device_denied", send, sendBase);
                }
                if (registration->signaling->hookDeviceState == HOOK_DEVICE_NOT_FOUND) {
                    char cfg[1024];
                    class json_io send(cfg);
                    word sendBase = send.add_object(0xffff, 0);
                    send.add_string(sendBase, "channel", confId);
                    send.add_string(sendBase, "errorText", "Hook device not found");
                    send.add_string(sendBase, "errorId", "hook_device_not_found");

                    if (!reconnectedCall) MonitorXmit("hook_device_not_found", send, sendBase);
                }
                hookDeviceTimer.Start(20000);
            }
        }
    }
}

void SignalingCall::MediaInitialize(enum MediaType mediaType)
{
    SignalingMediaConfig* c = registration->signaling->service->mediaConfig;

    if (mediaType == MediaType::AUDIO) {
        bool recordOn = registration->signaling->recordingLicense && c->recordingUrl && c->isRecordingByDefaultOn && (!c->recordExternalOnly || (c->recordExternalOnly && this->recordingInfo.isExternal));
        bool rtcpMux = (remoteAudioConfig && remoteAudioConfig->ice) ? remoteAudioConfig->ice->rtcpMux : true;
        media = mediaProvider->CreateMedia(registration->signaling->GetIoMux(), this, registration->signaling, MediaType::AUDIO);
        media->Initialize(
            c->udpSocketProvider, c->tcpSocketProvider, c->socketContext, c->minPort, c->maxPort,
            c->stunServers, c->turnServers, c->turnUsername, c->turnPassword,
            AUDIO, c->stunSlow, c->turnOnly, c->iceNoHost, false,
            c->dropMediaTx, c->dropMediaRx, c->hostPbx, c->noVpnAddresses, &assignmentsList, rtcpMux, c->tlsSocketProvider, c->excludeInterfaces, c->recordingUrl, &conf, recordOn, c->recordRtpStream, c->currentUserSip, c->recordingUsername, c->recordingPassword);
        initializing |= (1 << AUDIO);
        if (c->recordRtpStream) audioIo->RecordWaves();
    }
    else {
        bool rtcpMux = (remoteVideoConfig && remoteVideoConfig->ice) ? remoteVideoConfig->ice->rtcpMux : true;
        media2 = mediaProviderVideo->CreateMedia(registration->signaling->GetIoMux(), this, registration->signaling, MediaType::VIDEO);
        media2->Initialize(
            c->udpSocketProvider, c->tcpSocketProvider, c->socketContext, c->minVideoPort, c->maxVideoPort,
            c->stunServers, c->turnServers, c->turnUsername, c->turnPassword,
            VIDEO, c->stunSlow, c->turnOnly, c->iceNoHost, false,
            c->dropMediaTx, c->dropMediaRx, c->hostPbx, c->noVpnAddresses, &assignmentsList, rtcpMux, c->tlsSocketProvider, c->excludeInterfaces, c->recordingUrl, &conf);
        initializing |= (1 << VIDEO);
    }
    isMediaInitialized = false;
}

void SignalingCall::MediaConnectResult(IMedia * const media, const char * error)
{
    enum MediaType mediaType = AUDIO;
    if (media == this->media) mediaType = AUDIO;
    if (media == this->media2) mediaType = VIDEO;
    connecting &= ~(1 << mediaType);

    debug->printf("%x/%i:SignalingCall::MediaConnectResult(error=%s) mediaType=%u audioCoder=%u videoCoder=%u connecting=%x reconnectedCall=%p", this, call, error, mediaType, audioCoder, videoCoder, connecting, reconnectedCall);

    if (error) {
        debug->printf("%x/%i:SignalingCall::MediaConnectResult(error=%s) mediaType=%s", this, call, error ? error : "", (media == this->media) ? "Audio" : "Video");
        if (media == this->media) {
            if(!reconnectedCall) ClearCall(Q931_CAUSE_NetworkOutOfOrder);
            else {
                reconnectedCall->reconnectingCall = nullptr;
                if (remoteCallId) {
                    debug->printf("%x/%i:SignalingCall::MediaConnectResult restart reconnecting call", this, call);
                    reconnectedCall->StartReconnectTimer();
                }
                else debug->printf("%x/%i:SignalingCall::MediaConnectResult wait for new reconnecting call", this, call);

                cause = Q931_CAUSE_NonSelectedUserClearing;
                failedReconnect = true;
                SendRel();
                return;
            }
        }
        // Should the call be cleared if problem with Video?
        // audio failed too and call may be in deleting state
        else if (!deleting && registration->signaling->user) {
            unsigned offset = registration->ExternalLinePrefixOffset(remote.num);
            char remoteSip[1024];
            registration->signaling->user->ReportCallConnected(confId, error == nullptr, remote.num + offset, offset, registration->signaling->AppendSipDomain(remote.sip, remoteSip, sizeof(remoteSip)), remote.dn,
                this->diverting.dn ? this->diverting.dn : this->diverting.sip ? this->diverting.sip : this->diverting.num ? this->diverting.num : NULL,
                this->originalCalled.dn ? this->originalCalled.dn : this->originalCalled.sip ? this->originalCalled.sip : this->originalCalled.num ? this->originalCalled.num : NULL,
                videoAllowed, true, true, true, true, reconnectedCall ? true : false);
        }
    } 
    else {
        if (mediaType == AUDIO) {
            // only once, a renegotation would reconnect audio
            if(!duration) duration = ITime::TimeStampMilliseconds() / 1000;
            if (audioIoChannel && registration) {
                if (audioIo) audioIo->InitializeChannel(audioIoChannel, (enum AudioCoder)audioCoder, false, 20);
                audioIoChannel->Open();
            }
            if (registration->signaling->user) {
                unsigned offset = registration->ExternalLinePrefixOffset(remote.num);
                char remoteSip[1024];
                registration->signaling->user->ReportCallConnected(confId, error == nullptr, remote.num + offset, offset, registration->signaling->AppendSipDomain(remote.sip, remoteSip, sizeof(remoteSip)), remote.dn,
                    this->diverting.dn ? this->diverting.dn : this->diverting.sip ? this->diverting.sip : this->diverting.num ? this->diverting.num : NULL,
                    this->originalCalled.dn ? this->originalCalled.dn : this->originalCalled.sip ? this->originalCalled.sip : this->originalCalled.num ? this->originalCalled.num : NULL,
                    videoAllowed, true, true, true, true, reconnectedCall ? true : false);
            }
            audioMediaConnected = true;
            if (!dtmfs.empty()) queuedDTMFTimer.Start(1000);
            watingCallToneTimer.Start(500);
            if (reconnectedCall) {
                if (registration && registration->activePhoneCall == reconnectedCall) registration->activePhoneCall = this;
                if (reconnectedCall->reconnectConfId) this->reconnectConfId = _strdup(reconnectedCall->reconnectConfId);
                else this->reconnectConfId = _strdup(reconnectedCall->confId);
                reconnectedCall->reconnectingCall = nullptr;
                if (registration && registration->reconnectingCallDelete == reconnectedCall) {
                    registration->reconnectingCallDelete = nullptr;
                }
                if (registration && registration->pendingReconnectCall == reconnectedCall) {
                    registration->pendingReconnectCall = nullptr;
                }
                duration = reconnectedCall->duration;
                if (reconnectedCall->state & SIG_CALL_STATE_HOLD) this->Hold();
                MonitorSync();
                reconnectedCall->ClearReconnectedCall();
                reconnectedCall = nullptr;
            }
            else if (registration && registration->signaling->service->mediaConfig->signalingTestMode) {
                signalingTestTimer.Start(6000);
            }
            audioRtpReceived.Start(5000);
        }
        else if (mediaType == VIDEO) {
            if (!videoIoChannel) {
                videoIoChannel = videoIo->CreateChannel(this);
            }
            videoIoChannel->Initialize(NULL, (const char *)confId, (enum VideoCoder)videoCoder, ConnectionType::REMOTE_RTP);
            videoIoChannel->Open();
        }
    }
}

void SignalingCall::MediaEventReceived(IMedia * const media, enum MediaEndpointEvent event, void * ctx)
{
    if (event == MediaEndpointEvent::CHANNEL_MEDIA_STATISTICS) {
        if (ctx) {
            class EventMediaStatistics * s = (class EventMediaStatistics*)ctx;
            if (s) {
#if 1
                if (s->xmit) {
                    tx.jitter = s->jitter;
                    tx.loss = s->loss;
                    tx.roundTrip = s->roundTrip;
                }
                else {
                    rx.jitter = s->jitter;
                    rx.loss = s->loss;
                    rx.roundTrip = s->roundTrip;
                }
#else
                char temp[128];
                char* tmp = temp;
                char cfg[1024];
                class json_io send(cfg);

                debug->printf("%x/%i:SignalingCall::MediaEventReceived CHANNEL_MEDIA_STATISTICS conf=%s (%s)", this, call, confId, (media == this->media2 ? "Video" : media == this->media ? "Audio" : "AppSharing"));

                word sendBase = 0xffff;
                sendBase = send.add_object(0xffff, 0);
                send.add_string(sendBase, "mt", "MediaStatistics");
                send.add_string(sendBase, "channel", confId);
                send.add_string(sendBase, "type", "RemoteRtp");
                send.add_bool(sendBase, "xmit", s->xmit);
                send.add_unsigned(sendBase, "loss", s->loss, tmp);
                send.add_unsigned(sendBase, "jitter", s->jitter, tmp);
                send.add_unsigned(sendBase, "roundTrip", s->roundTrip, tmp);

                if (registration && registration->signaling && registration->signaling->session) registration->signaling->session->SendResponse(send, cfg);

                delete s;
#endif
            }
        }
    }
    else if (event == MediaEndpointEvent::CHANNEL_MEDIA_ERROR) {
        if (ctx) {
            class EventMediaError * e = (class EventMediaError *)ctx;
            if (e) {
                char temp[128];
                char* tmp = temp;
                char cfg[1024];
                class json_io send(cfg);

                debug->printf("%x/%i:SignalingCall::MediaEventReceived CHANNEL_MEDIA_ERROR code=%x(%x) error=%s conf=%s (%s) remoteCallId=%d reconnectedCall=%p", this, call, e->code, FAULT_CODE_RTP_RECONNECT_NEEDED, e->errorText ? e->errorText : "", confId, (media == this->media2 ? "video" : media == this->media ? "audio" : "unknown"), remoteCallId, reconnectedCall);

                word sendBase = 0xffff;
                sendBase = send.add_object(0xffff, 0);
                send.add_string(sendBase, "channel", confId);
                send.add_string(sendBase, "type", "RemoteRtp");
                send.add_string(sendBase, "kind", this->media == media ? "audio" : "video");
                send.add_unsigned(sendBase, "code", e->code, tmp);
                send.add_string(sendBase, "errorText", e->errorText);
                send.add_string(sendBase, "src", e->src);

                if (!reconnectedCall) MonitorXmit("media_error", send, sendBase);

                if (this->media == media && e->code == FAULT_CODE_RTP_RECONNECT_NEEDED) {
                    debug->printf("%x/%i:SignalingCall::MediaEventReceived FAULT_CODE_RTP_RECONNECT_NEEDED start new call registration=%p(%d,%d) remoteCallId=%d reconnectInitRecv=%d(%d) sigRelSent=%d", this, call, registration, registration ? registration->clientUp : 0, registration ? registration->registered : 0, remoteCallId, reconnectInitRecv, reconnectSetupRecv, sigRelSent);
                    if (registration) {
                        if (remoteCallId) {
                            if (mediaLostToneTimer.IsRunning() == false) mediaLostToneTimer.Start(0);
                            CreateReconnectCall();
                        }
                        else if (reconnectInitRecv || reconnectSetupRecv) {
                            if (registration->clientUp && registration->registered && !sigRelSent) {
                                if (reconnectedCall) {
                                    cause = Q931_CAUSE_NonSelectedUserClearing;
                                    failedReconnect = true;
                                    SendRel();
                                }
                                else {
                                    if (mediaLostToneTimer.IsRunning() == false) mediaLostToneTimer.Start(0);
                                    class SignalingSigMessage send(call, "rel", alloca(1000), alloca(100));
                                    word cau = send.add_object(send.sig, "cau");
                                    send.add_int(cau, "num", Q931_CAUSE_TemporaryFailure, send.tmp);
                                    send.Send(registration);
                                    sigRelSent = true;
                                }
                            }
                        }
                    }
                    else debug->printf("%x/%i:SignalingCall::MediaEventReceived FAULT_CODE_RTP_RECONNECT_NEEDED no registration available", this, call);
                }
                else if (registration && registration->signaling->user && !reconnectedCall) registration->signaling->user->SendEvent(e->code, e->errorText, e->src);
                else if (reconnectedCall) {
                    debug->printf("%x/%i:SignalingCall::MediaEventReceived remoteCallId=%d", this, call, remoteCallId);
                    reconnectedCall->reconnectingCall = nullptr;
                    if (remoteCallId) reconnectedCall->StartReconnectTimer();
                    else reconnectedCall->sigRelSent = false;
                    cause = Q931_CAUSE_NonSelectedUserClearing;
                    failedReconnect = true;
                    SendRel();
                }

                delete e;
            }
        }
    }
    else if (event == MediaEndpointEvent::CHANNEL_MEDIA_WARNING) {
        if (ctx) {
            class EventMediaError* e = (class EventMediaError*)ctx;
            if (e) {
                char temp[128];
                char* tmp = temp;
                char cfg[1024];
                class json_io send(cfg);

                debug->printf("%x/%i:SignalingCall::MediaEventReceived CHANNEL_MEDIA_WARNING code=%d error=%s conf=%s (%s)", this, call, e->code, e->errorText ? e->errorText : "", confId, (media == this->media2 ? "video" : media == this->media ? "audio" : "unknown"));

                word sendBase = 0xffff;
                sendBase = send.add_object(0xffff, 0);
                send.add_string(sendBase, "channel", confId);
                send.add_string(sendBase, "type", "RemoteRtp");
                send.add_string(sendBase, "kind", this->media == media ? "audio" : "video");
                send.add_unsigned(sendBase, "code", e->code, tmp);
                send.add_string(sendBase, "warningText", e->errorText);
                send.add_string(sendBase, "src", e->src);

                if (!reconnectedCall) MonitorXmit("media_warning", send, sendBase);

                delete e;
            }
        }
    }
    else if (videoIoChannel) videoIoChannel->EventRecv(event, ctx);
}

void SignalingCall::MediaRtpRecvResult(IMedia * const media, void * buf, size_t len, dword timestamp, short sequenceNumberDiff, bool marker)
{
    enum MediaType mediaType = AUDIO;
    if (media == this->media) mediaType = AUDIO;
    if (media == this->media2) mediaType = VIDEO;

    //debug->printf("%x/%i:SignalingCall::MediaRtpRecvResult(%u) mediaType=%d audioIoChannel=%x videoIoChannel=%x rtptp=%x", this, call, len, mediaType, audioIoChannel, videoIoChannel, rtptp);

    if (mediaType == AUDIO) {
        if (audioDataOff) {
            char cfg[1024];
            class json_io send(cfg);

            debug->printf("%x/%i:SignalingCall::MediaRtpRecvResult audio packets received again", this, call);

            word sendBase = 0xffff;
            sendBase = send.add_object(0xffff, 0);
            send.add_string(sendBase, "channel", confId);
            send.add_string(sendBase, "type", "audio");

            MonitorXmit("media_data_on", send, sendBase);

            audioDataOff = false;
        }
        numAudioPackets++;
        if (audioIoChannel) audioIoChannel->RtpRecv(buf, len, timestamp, sequenceNumberDiff, marker);
        else if (rtptp) rtptp->RTPTPRecv(buf, len);
    }
    else if (mediaType == VIDEO) {
        if (videoDataOff) {
            char cfg[1024];
            class json_io send(cfg);

            debug->printf("%x/%i:SignalingCall::MediaRtpRecvResult video packets received again", this, call);

            word sendBase = 0xffff;
            sendBase = send.add_object(0xffff, 0);
            send.add_string(sendBase, "channel", confId);
            send.add_string(sendBase, "type", "video");

            MonitorXmit("media_data_on", send, sendBase);

            videoDataOff = false;
        }
        numVideoPackets++;
        if (videoIoChannel) videoIoChannel->RtpRecv(buf, len, timestamp, sequenceNumberDiff, marker);
    }
}

void SignalingCall::MediaIoSendFIR()
{
    if (registration && registration->signaling && registration->signaling->log) registration->signaling->log->Log(LOG_SIGNALING, "%x/%i:SignalingCall::MediaIoSendFIR media2=%p", this, call, media2);

    if (media2) media2->SendFIR();
}

void SignalingCall::MediaRtpRecv(IMedia* const media, const char* src_addr, word src_port, dword ssrc, word pt)
{
    if (registration && registration->signaling && registration->signaling->log) registration->signaling->log->Log(LOG_SIGNALING, "%x/%i:SignalingCall::MediaRtpRecv conf=%s (%s)", this, call, confId, (media == this->media2 ? "video" : media == this->media ? "audio" : "unknown"));

#if 0
    char cfg[2000];
    char b[128];
    char* tmp = b;
    class json_io send(cfg);

    word sendBase = 0xffff;
    sendBase = send.add_object(0xffff, 0);
    send.add_string(sendBase, "mt", "RtpRecv");
    send.add_string(sendBase, "channel", confId);
    send.add_string(sendBase, "type", (media == this->media2 ? "video" : media == this->media ? "audio" : "unknown"));
    send.add_string(sendBase, "src_addr", src_addr);
    send.add_int(sendBase, "src_port", src_port, tmp);
    send.add_unsigned(sendBase, "ssrc", ssrc, tmp);
    send.add_int(sendBase, "pt", pt, tmp);

    //if (registration && registration->signaling && registration->signaling->session) registration->signaling->session->SendResponse(send, cfg);
#else
    if (registration && registration->signaling) {
        if (audioIo) MediaTones::StopTone(audioIo);
    }
#endif
}

void SignalingCall::MediaRtpDtmfNearStart(char digit)
{
#if 0
    debug->printf("%x/%i:SignalingCall::MediaRtpDtmfNearStart conf=%s (%s) %c", this, call, confId, "audio", digit);

    char digitString[2];
    char cfg[2000];
    class json_io send(cfg);

    word sendBase = 0xffff;
    sendBase = send.add_object(0xffff, 0);
    send.add_string(sendBase, "mt", "RtpDtmfNearStart");
    send.add_string(sendBase, "channel", confId);
    send.add_string(sendBase, "type", "audio"));
    digitString[0] = digit;
    digitString[1] = '\0';
    send.add_string(sendBase, "digit", digitString);

    // UI does not process this message 
    //if (registration && registration->signaling && registration->signaling->session) registration->signaling->session->SendResponse(send, cfg);
#endif
}

void SignalingCall::MediaRtpDtmfNear(char digit)
{
#if 0
    debug->printf(("%x/%i:SignalingCall::MediaRtpDtmfNear conf=%s (%s) %c", this, call, confId, "audio", digit));

    char digitString[2];
    char cfg[2000];
    class json_io send(cfg);

    word sendBase = 0xffff;
    sendBase = send.add_object(0xffff, 0);
    send.add_string(sendBase, "mt", "RtpDtmfNear");
    send.add_string(sendBase, "channel", confId);
    send.add_string(sendBase, "type", "audio"));
    digitString[0] = digit;
    digitString[1] = '\0';
    send.add_string(sendBase, "digit", digitString);

    // UI does not process this message 
    //if (registration && registration->signaling && registration->signaling->session) registration->signaling->session->SendResponse(send, cfg);
#endif
}

void SignalingCall::MediaCloseComplete(IMedia * const media)
{
    debug->printf("%x/%i:SignalingCall::MediaCloseComplete() deleting=%d media=%x audio=%x,%x video=%x,%x audioIoChannel=%x videoIoChannel=%x initializing=%x connecting=%x", this, call, deleting, media, this->media, this->mediaClosing, this->media2, this->media2Closing, audioIoChannel, videoIoChannel, initializing, connecting);
    if (media == this->media) {
        if (audioIoChannel) {
            audioIoChannel->Close();
            MediaIoCloseComplete(audioIoChannel);
        }
        delete this->media;
        this->media = NULL;
    }
    if (media == this->mediaClosing) {
        delete this->mediaClosing;
        this->mediaClosing = NULL;
    }
    if (media == this->media2) {
        if (videoIoChannel) {
            videoIoChannel->Close();
            // MediaIoCloseComplete(videoIoChannel) will be called by class VideoIoChannel
        }
        delete this->media2;
        this->media2 = NULL;
    }
    if (media == this->media2Closing) {
        delete this->media2Closing;
        this->media2Closing = NULL;
    }
    if (deleting && this->media == NULL && this->mediaClosing == NULL && this->media2 == NULL && this->media2Closing == NULL && audioIoChannel == NULL && videoIoChannel == NULL && videoIoChannelClosing == NULL) delete this;
}

void SignalingCall::RecordingStopped(const char * error)
{
    if (registration && registration->signaling && registration->signaling->log) registration->signaling->log->Log(LOG_SIGNALING, "%x/%i:SignalingCall::RecordingStopped reason: %s",this, call, error);
    if (error) this->recordingInfo.recordingError = _strdup(error);
    SignalingSigMessage msg(call, "facility", alloca(1000), alloca(100));
    MonitorXmit("facility_recording_stopped", msg, msg.sig);
}

void SignalingCall::RecordingStarted()
{
    if (registration && registration->signaling && registration->signaling->log) registration->signaling->log->Log(LOG_SIGNALING, "%x/%i:SignalingCall::RecordingStarted",this, call);
    SignalingSigMessage msg(call, "facility", alloca(1000), alloca(100));
    MonitorXmit("facility_recording_started", msg, msg.sig);
}

static enum AudioCoder AvailableAudioCoders[] =
{
    AudioCoder::OPUS_WB,
    AudioCoder::G722,
    AudioCoder::OPUS_NB,
    AudioCoder::G711_A,
    AudioCoder::G711_U,
    //AudioCoder::G729,
    //AudioCoder::G729A,
    //AudioCoder::G729B,
    //AudioCoder::G729AB,
    AudioCoder::DTMF,
};

#define AVAILABLE_AUDIO_CODERS sizeof(AvailableAudioCoders)/sizeof(AvailableAudioCoders[0])

enum AudioCoder SignalingCall::AvailableAudioCoder(unsigned coderNumber)
{
    if (callType == CallTypeVoice) {
        ASSERT(coderNumber < AVAILABLE_AUDIO_CODERS, "Out of range!");
        return AvailableAudioCoders[coderNumber];
    }
    if (callType == CallTypeData) {
        return CLEARCHANNEL;
    }
    return CODER_UNDEFINED;
}

unsigned SignalingCall::AvailableAudioCoderCount()
{
    return AVAILABLE_AUDIO_CODERS;
}

static enum VideoCoder AvailableVideoCoders[] =
{
    VideoCoder::VP8,
#if !defined __ANDROID__ && !defined _IOS_
    //VideoCoder::VP9,
#endif
#if defined __ANDROID__ || defined _IOS_
    VideoCoder::H264,
#elif defined _WINDOWS
    VideoCoder::H264,
    VideoCoder::AV1,
#endif
};

#define AVAILABLE_VIDEO_CODERS sizeof(AvailableVideoCoders)/sizeof(AvailableVideoCoders[0])

enum VideoCoder SignalingCall::AvailableVideoCoder(unsigned coderNumber)
{
    if (callType == CallTypeVoice) {
        ASSERT(coderNumber < AVAILABLE_VIDEO_CODERS, "Out of range!");
        return AvailableVideoCoders[coderNumber];
    }
    return AvailableVideoCoders[0];
}

unsigned SignalingCall::AvailableVideoCoderCount()
{
    if (callType == CallTypeVoice) {
        return AVAILABLE_VIDEO_CODERS;
    }
    return 0;
}

void SignalingCall::MonitorRecv(const char * type, class json_io & msg, word base)
{
    if (!strcmp(type, "facility") && monitors.empty()) {
        debug->printf("%x/%u:SignalingCall::MonitorRecv(type=%s) no monitor available", this, call, type);

        if (fty_ct_initiate.num || fty_ct_initiate.sip || fty_ct_initiate.id) {
            // must replace this SignalingCall from PBX by a new outbound SignalingCall to PBX

            class SignalingCall* newCall = new SignalingCall(registration, CallTypeVoice, 0, fty_ct_initiate.num, fty_ct_initiate.sip, fty_ct_initiate.dn, true);
            newCall->videoAllowed = videoAllowed;
            newCall->videoDisabled = videoDisabled;
            newCall->ct_setup_id = fty_ct_initiate.id;

            // detach replacedCall from this SignalingRccCall
            class SignalingCall* replacedCall = this;

            replacedCall->replacementCall = newCall;
            newCall->replacedCall = replacedCall;
            newCall->StartCall(audioIo, videoIo, 0, 0);

            // release replacedCall on any received message on newCall
            replacedCall->fty_ct_initiate.cleanup();
        }
    }

    for (class SignalingCallMonitor * m = monitors.front(); m; m = m->goNext()) {
        m->MonitorRecv(type, msg, base);
    }
}

void SignalingCall::MonitorXmit(const char * type, class json_io & msg, word base)
{
    for (class SignalingCallMonitor * m = monitors.front(); m; m = m->goNext()) {
        m->MonitorXmit(type, msg, base);
    }
}

void SignalingCall::MediaIoRtpSend(const void * buf, size_t len, dword timestamp, enum MediaType mediaType, bool isConference)
{
    if (mediaType == MediaType::AUDIO && media) {
        media->RtpSend(buf, len, timestamp, mediaType);
    }
    else if ((mediaType == MediaType::VIDEO || mediaType == MediaType::VIDEO_APPSHARING) && media2) {
        media2->RtpSend(buf, len, timestamp, mediaType);
    }
}

void SignalingCall::MediaIoRtpForward(const void* buf, size_t len, dword timestamp, short sequenceNumberDiff, bool marker, enum MediaType mediaType)
{
    if (mediaType == MediaType::AUDIO && media) {
        media->RtpForward(buf, len, (dword)timestamp, sequenceNumberDiff, marker);
    }
    else if ((mediaType == MediaType::VIDEO || mediaType == MediaType::VIDEO_APPSHARING) && media2) {
        media2->RtpForward(buf, len, (dword)timestamp, sequenceNumberDiff, marker);
    }
}

void SignalingCall::MediaIoRtcpSend(const void* buf, size_t len, enum MediaType mediaType)
{
    if (mediaType == MediaType::AUDIO && media) {
        media->RtcpSend(buf, len);
    }
    else if ((mediaType == MediaType::VIDEO || mediaType == MediaType::VIDEO_APPSHARING) && media2) {
        media2->RtcpSend(buf, len);
    }
}

void SignalingCall::MediaIoSctpSend(const void * buf, size_t len, unsigned num)
{

}

void SignalingCall::MediaIoCloseComplete(class IMediaIoChannel * const mediaIoChannel)
{
    debug->printf("%x/%i:SignalingCall::MediaIoCloseComplete del=%d media=%p media2=%p,%p mediaIoChannel=%x audioIoChannel=%x videoIoChannel=%x,%x", this, call, deleting, this->media, this->media2, this->media2Closing, mediaIoChannel, audioIoChannel, videoIoChannel, videoIoChannelClosing);

    if (audioIoChannel == mediaIoChannel) {
        DELETE_OBJ(audioIoChannel);
    }
    if (videoIoChannel == mediaIoChannel) {
        DELETE_OBJ(videoIoChannel);
    }
    if (videoIoChannelClosing == mediaIoChannel) {
        DELETE_OBJ(videoIoChannelClosing);
    }
    if (deleting && this->media == NULL && this->mediaClosing == NULL && this->media2 == NULL && this->media2Closing == NULL && audioIoChannel == NULL && videoIoChannel == NULL && videoIoChannelClosing == NULL) delete this;
}

void SignalingCall::SendSetup()
{
    debug->printf("%x/%i:SignalingCall::SendSetup()", this, call);
    class SignalingSigMessage send(call, "setup", alloca(10000), alloca(4000));

    if (registration->signaling->clir) {
        word cg = send.add_object(send.sig, "cg");
        send.add_string(cg, "flags", "R");
    }

    word cd = send.add_object(send.sig, "cd");
    send.add_string(cd, "num", remote.num);
    send.add_string(cd, "sip", remote.sip);
    send.add_int(send.sig, "channel", -1, send.tmp);

    if(sendingComplete) send.add_bool(send.sig, "complete", sendingComplete);

    SendLocalMedia(send, send.sig, send.tmp);

    word ftys = send.add_array(send.sig, "fty");
    if (true) {
        word fty = send.add_object(ftys, 0);
        send.add_string(fty, "type", "video_capabilities");
        send.add_bool(fty, "multivideo", true);
    }
    if (ct_setup_id) {
        word fty = send.add_object(ftys, 0);
        send.add_string(fty, "type", "ct_setup");
        send.add_unsigned(fty, "id", ct_setup_id, send.tmp);
    }
    if (reconnectedCall) {
        word fty = send.add_object(ftys, 0);
        send.add_string(fty, "type", "reconnect_setup");
        send.add_string(fty, "sip", reconnectedCall->remoteSipId);
        send.add_string(fty, "hw", reconnectedCall->remoteHwId);
        send.add_unsigned(fty, "id", reconnectedCall->remoteCallId, send.tmp);
        reconnectCallResponse.Start(15000);
    }
    if (true) {
        word fty = send.add_object(ftys, 0);
        send.add_string(fty, "type", "reconnect_init");
    }

    send.add_hexstring(send.sig, "confId", conf.b, sizeof(conf.b), send.tmp);
    send.add_printf(send.sig, "sourceInfo", send.tmp, "%s %s", _PRODUCT_NAME_, _BUILD_STRING_);

    ChangeState(SIG_CALL_STATE_SETUP);
    if(!reconnectedCall) MonitorXmit("setup", send, send.sig);
    send.Send(registration);

    if ((!remote.num || (remote.num && !remote.num[0])) && (!remote.sip || (remote.sip && !remote.sip[0])) && audioIo) MediaTones::StartTone(MEDIA_TONES_DIALING, registration->dialingLocation.tones, audioIo);
}

void SignalingCall::SendInfo(const char * num, const char * sip)
{
    debug->printf("%x/%i:SignalingCall::SendInfo(%s:%s)", this, call, num ? num : "", sip ? sip : "");
    class SignalingSigMessage send(call, "info", alloca(1000), alloca(100));
    word cd = send.add_object(send.sig, "cd");
    send.add_string(cd, "num", num);
    send.add_string(cd, "sip", sip);

    MonitorXmit("info", send, send.sig);
    send.Send(registration);
}

void SignalingCall::SendAlert()
{
    debug->printf("%x/%i:SignalingCall::SendAlert() state=%x callWaiting=%u", this, call, state, callWaiting);
    class SignalingSigMessage send(call, "alert", alloca(1000), alloca(100));
    if (state & SIG_CALL_STATE_OUTG) {
        if (callWaiting && !(state & SIG_CALL_STATE_CALL_WAITING)) {
            ChangeState(SIG_CALL_STATE_ALERT | SIG_CALL_STATE_OUTG | SIG_CALL_STATE_CALL_WAITING);
            word ftys = send.add_array(send.sig, "fty");
            word fty = send.add_object(ftys, 0);
            send.add_string(fty, "type", "call_waiting");
        }
        else if (!callWaiting && (state & SIG_CALL_STATE_CALL_WAITING)) {
            ChangeState(SIG_CALL_STATE_ALERT | SIG_CALL_STATE_OUTG);
            word ftys = send.add_array(send.sig, "fty");
            word fty = send.add_object(ftys, 0);
            send.add_string(fty, "type", "co_alerting");
        }
        else if (registration->dnd) {
            word ftys = send.add_array(send.sig, "fty");
            word fty = send.add_object(ftys, 0);
            send.add_string(fty, "type", "call_waiting");
        }
        else {
            ChangeState(SIG_CALL_STATE_ALERT | SIG_CALL_STATE_OUTG);
        }
    }
    if (!reconnectedCall) MonitorXmit("alert", send, send.sig);
    send.Send(registration);
}

void SignalingCall::SendConn()
{
    debug->printf("%x/%i:SignalingCall::SendConn() localAudioConfig=%x", this, call, localAudioConfig);
    ChangeState(SIG_CALL_STATE_CONN | SIG_CALL_STATE_OUTG);
    if (localAudioConfig) {
        class SignalingSigMessage send(call, "conn", alloca(10000), alloca(4000));
        SendLocalMedia(send, send.sig, send.tmp);
        if (reconnectInitRecv || reconnectSetupRecv) {
            word ftys = send.get_array(send.base, "fty");
            if (ftys == JSON_ID_NONE) ftys = send.add_array(send.sig, "fty");
            word fty = send.add_object(ftys, 0);
            send.add_string(fty, "type", "reconnect_info");
            send.add_unsigned(fty, "id", this->recCallId, send.tmp);
            send.add_string(fty, "hw", registration->GetHwId());
        }
        // SendLocalMedia may start media->Connect()
        // If there is something wrong with the message, no ICE, no candidates, no coders, ...
        // MediaConnectResult will come right away with an error and the call will be deleted before calling send.Send(registration)
        // and registration is resetted to null -> crash.
        if (!deleting) {
            if (!reconnectedCall) MonitorXmit("conn", send, send.sig);
            send.Send(registration);
            if (audioDeviceId) {
                registration->signaling->SendHookKey(audioDeviceId, KEY_OFFHOOK, call, remote.num ? remote.num : remote.sip, remote.dn);
                hookState = KEY_OFFHOOK;
            }
        }
        connPostponed = false;
    }
    else {
        connPostponed = true; // do SendConn() when localAudioConfig is available
    }
}

void SignalingCall::SendChannels()
{
    debug->printf("%x/%i:SignalingCall::SendChannels() localAudioConfig=%x", this, call, localAudioConfig);
    if (localAudioConfig) {
        class SignalingSigMessage send(call, "channels", alloca(10000), alloca(4000));
        SendLocalMedia(send, send.sig, send.tmp);
        if (!reconnectedCall) MonitorXmit("channels", send, send.sig);
        send.Send(registration);
    }
}

void SignalingCall::SendRel()
{
    class SignalingSigMessage send(call, "rel", alloca(1000), alloca(100));
    if (cause) {
        word cau = send.add_object(send.sig, "cau");
        send.add_int(cau, "num", cause, send.tmp);
    }
    ChangeState(0);
    if(!failedReconnect) MonitorXmit("rel", send, send.sig);
    if (registration) send.Send(registration);
    Delete();
}

int SignalingCall::SelectCoder(class MediaConfig * remoteMediaConfig, class MediaConfig * localMediaConfig)
{
    Codec * remote = 0;
    for (remote = remoteMediaConfig->codecList.front(); remote; remote = remote->goNext()) {
        Codec * local = 0;
        for (local = localMediaConfig->codecList.front(); local; local = local->goNext()) {
            if (remote->coder == local->coder) break;
        }
        if (local) break;
    }
    if (remote) {
        remote->remove();
        while (remoteMediaConfig->codecList.front()) delete remoteMediaConfig->codecList.front();
        remoteMediaConfig->codecList.push_back(remote);
        return remote->coder;
    }
    return 0;
}

static word default_coder_rate(word coder)
{
    switch (coder) {
    case AudioCoder::OPUS_WB:
    case AudioCoder::OPUS_NB:
        return 48000;
    default:
        return 8000;
    }
}

void SignalingCall::SendLocalMedia(class json_io & json, word base, char * & tmp)
{
    int localMediaCmd = 0;

    debug->printf("%x/%i:SignalingCall::SendLocalMedia() localAudioConfig=%x remoteAudioConfig=%x localVideoConfig=%x remoteVideoConfig=%x remoteMediaCmd=%s audioCoder=%d videoCoder=%d", this, call, localAudioConfig, remoteAudioConfig, localVideoConfig, remoteVideoConfig, ChannelsCmdName(remoteMediaCmd) ? ChannelsCmdName(remoteMediaCmd) : "NONE", audioCoder, videoCoder);

    switch (remoteMediaCmd) {

    case MEDIA_CMD_PROPOSAL:
        localMediaCmd = MEDIA_CMD_SELECT;
        json.add_string(base, "channels_cmd", "SELECT");
        if (remoteAudioConfig && localAudioConfig) audioCoder = SelectCoder(remoteAudioConfig, localAudioConfig);
        if (remoteVideoConfig && localVideoConfig) videoCoder = SelectCoder(remoteVideoConfig, localVideoConfig);
        if(!isRccCall) connectingState = true;
        break;

    case 0:
    case MEDIA_CMD_REQUEST:
        audioCoder = CODER_UNDEFINED;
        videoCoder = CODER_UNDEFINED;
        localMediaCmd = MEDIA_CMD_PROPOSAL;
        json.add_string(base, "channels_cmd", "PROPOSAL");
        connectingState = false;
        break;
    }

    word channels = json.add_object(base, "channels");
    json.add_string(channels, "source", "REMOTE");
    word chs = json.add_array(channels, "ch");
    for (Codec * codec = localAudioConfig->codecList.front(); codec; codec = codec->goNext()) {
        if (audioCoder == codec->coder || localMediaCmd == MEDIA_CMD_PROPOSAL) {
            word rate = codec->rate ? codec->rate : default_coder_rate(codec->coder);
            word pt = (codec->coder == AudioCoder::DTMF) ? localDTMF : codec->pt;
            word ch = json.add_object(chs, 0);
            json.add_string(ch, "coder", CoderName(codec->coder));
            json.add_unsigned(ch, "rate", rate, tmp);
            json.add_unsigned(ch, "pt", pt, tmp);
            json.add_unsigned(ch, "xmit", 20, tmp);
            json.add_unsigned(ch, "recv", 20, tmp);
            json.add_string(ch, "addr", codec->addr);
            json.add_unsigned(ch, "port", codec->port, tmp);
        }
    }
    if (localMediaCmd == MEDIA_CMD_SELECT && remoteAudioConfig->remoteDTMF) {
        for (Codec* codec = localAudioConfig->codecList.front(); codec; codec = codec->goNext()) {
            if (codec->coder == AudioCoder::DTMF) {
                word rate = codec->rate ? codec->rate : default_coder_rate(codec->coder);
                word pt = remoteAudioConfig->remoteDTMF;
                word ch = json.add_object(chs, 0);
                json.add_string(ch, "coder", CoderName(codec->coder));
                json.add_unsigned(ch, "rate", rate, tmp);
                json.add_unsigned(ch, "pt", pt, tmp);
                json.add_unsigned(ch, "xmit", 20, tmp);
                json.add_unsigned(ch, "recv", 20, tmp);
                json.add_string(ch, "addr", codec->addr);
                json.add_unsigned(ch, "port", codec->port, tmp);
                break;
            }
        }
    }
    if (localVideoConfig) {
        // add video codecs
        for (Codec* codec = localVideoConfig->codecList.front(); codec; codec = codec->goNext()) {
            if (videoCoder == codec->coder || localMediaCmd == MEDIA_CMD_PROPOSAL) {
                word ch = json.add_object(chs, 0);
                json.add_string(ch, "coder", CoderName(codec->coder));
                json.add_unsigned(ch, "pt", codec->pt, tmp);
                json.add_string(ch, "addr", codec->addr);
                json.add_unsigned(ch, "port", codec->port, tmp);
            }
        }
    }

    if (localAudioConfig && localAudioConfig->ice && (audioCoder || localMediaCmd == MEDIA_CMD_PROPOSAL)) {
        bool addIce = !remoteAudioConfig ? true : remoteAudioConfig->ice != NULL;
        if (addIce) {
            if (localAudioConfig->ice->rtcpMux) json.add_bool(channels, "audio_rtcp_mux", true);
            word ice = json.add_object(channels, "audio_ice");
            json.add_string(ice, "usr", localAudioConfig->ice->iceUfrag);
            json.add_string(ice, "pwd", localAudioConfig->ice->icePwd);
            json.add_string(ice, "fingerprint", localAudioConfig->ice->fingerprint);
            if (localAudioConfig->ice->candidateList.front()) {
                word candidates = json.add_array(ice, "candidate");
                for (IceCandidate* c = localAudioConfig->ice->candidateList.front(); c; c = c->goNext()) {
                    word candidate = json.add_object(candidates, 0);
                    json.add_string(candidate, "foundation", c->foundation);
                    json.add_string(candidate, "addr", c->addr);
                    json.add_unsigned(candidate, "rtp", c->rtpPort, tmp);
                    json.add_unsigned(candidate, "rtp_prio", c->rtcpPriority, tmp);
                    if (!localAudioConfig->ice->rtcpMux) {
                        json.add_unsigned(candidate, "rtcp", c->rtcpPort, tmp);
                        json.add_unsigned(candidate, "rtcp_prio", c->rtcpPriority, tmp);
                    }
                    json.add_string(candidate, "type", CandidateName(c->type));
                }
            }
        }
    }
    if (localVideoConfig && localVideoConfig->ice && (videoCoder || localMediaCmd == MEDIA_CMD_PROPOSAL)) {
        bool addIce = !remoteVideoConfig ? true : remoteVideoConfig->ice != NULL;
        if (addIce) {
            if (localVideoConfig->ice->rtcpMux) json.add_bool(channels, "video_rtcp_mux", true);
            word ice = json.add_object(channels, "video_ice");
            json.add_string(ice, "usr", localVideoConfig->ice->iceUfrag);
            json.add_string(ice, "pwd", localVideoConfig->ice->icePwd);
            json.add_string(ice, "fingerprint", localVideoConfig->ice->fingerprint);
            if (localVideoConfig->ice->candidateList.front()) {
                word candidates = json.add_array(ice, "candidate");
                for (IceCandidate* c = localVideoConfig->ice->candidateList.front(); c; c = c->goNext()) {
                    word candidate = json.add_object(candidates, 0);
                    json.add_string(candidate, "foundation", c->foundation);
                    json.add_string(candidate, "addr", c->addr);
                    json.add_unsigned(candidate, "rtp", c->rtpPort, tmp);
                    json.add_unsigned(candidate, "rtp_prio", c->rtcpPriority, tmp);
                    if (!localVideoConfig->ice->rtcpMux) {
                        json.add_unsigned(candidate, "rtcp", c->rtcpPort, tmp);
                        json.add_unsigned(candidate, "rtcp_prio", c->rtcpPriority, tmp);
                    }
                    json.add_string(candidate, "type", CandidateName(c->type));
                }
            }
        }
    }

    if (remoteAudioConfig) {
        // if we send SELECT, remote is ICE controlling and we are active on DTLS
        bool iceRemoteRole = true;
        enum SetupRole remoteDtlsRole = SETUP_ROLE_NONE;
        connecting |= (1 << AUDIO);
        media->Connect(remoteAudioConfig, iceRemoteRole, remoteDtlsRole);
        remoteAudioConfig = 0;
        // remoteVideoConfig maybe not null from a previous negotiation where video came with a PROPOSAL but we just SELECT audio.
        // It means remoteAudioConfig would be null but remoteVideoConfig was set if we for example add now video.
        // there is no situation where remoteVideoConfig is allocated but remoteAudioConfig no.
        if (media2 && remoteVideoConfig) {
            bool iceRemoteRole = true;
            enum SetupRole remoteDtlsRole = SETUP_ROLE_NONE;
            connecting |= (1 << VIDEO);
            media2->Connect(remoteVideoConfig, iceRemoteRole, remoteDtlsRole);
            remoteVideoConfig = 0;
        }
    }

    remoteMediaCmd = MEDIA_CMD_NONE;
}

enum SetupRole decode(const char * str)
{
    if (str) {
        if (!strcmp(str, "active")) return SETUP_ROLE_ACTIVE;
        if (!strcmp(str, "passive")) return SETUP_ROLE_PASSIVE;
        if (!strcmp(str, "actpass")) return SETUP_ROLE_ACTPASS;
        if (!strcmp(str, "holdconn")) return SETUP_ROLE_HOLDCONN;
    }
    return SETUP_ROLE_NONE;
}

void SignalingCall::RecvRemoteMedia(class json_io & json, word base)
{
    // Every incoming message except "media_info" resets the remoteMediaCmd
    // This fix is added because between media->Initialize(..) and MediaInitializeComplete(..)
    // there might be these kind of message which reset the remoteMediaCmd to null and disturb
    // the logic flow. Especially after an client RETRIEVE followed by a PBX REQUEST

    unsigned int oldRemoteMediaCmd = remoteMediaCmd;
    remoteMediaCmd = ChannelsCmdType( json.get_string(base, "channels_cmd") );

    word media_info = find_fty(json, base, "media_info");
    if (media_info != JSON_ID_NONE) {
        remoteMediaCmd = oldRemoteMediaCmd;
        return;
    }

    word channels = json.get_object(base, "channels");
    if (channels != 0xffff) {
        delete remoteAudioConfig;
        remoteAudioConfig = new MediaConfig();
        remoteAudioConfig->type = AUDIO;
        delete remoteVideoConfig;
        remoteVideoConfig = new MediaConfig();
        remoteVideoConfig->type = VIDEO;
        word chs = json.get_array(channels, "ch");
        word ch = 0;
        for (ch = chs != 0xffff ? json.get_object(chs, ch) : 0xffff; ch != 0xffff; ch = json.get_object(chs, ch)) {
            const char * coderName = json.get_string(ch, "coder");
            if (coderName && !strcmp(coderName, "DTMF")) {
                remoteDTMF = json.get_unsigned(ch, "pt");
                if (remoteMediaCmd == MEDIA_CMD_PROPOSAL) localDTMF = remoteDTMF;
                remoteAudioConfig->localDTMF = localDTMF;
                remoteAudioConfig->remoteDTMF = remoteDTMF;
                continue;
            }
            unsigned coder = CoderType(coderName);
            if (coder) {
                enum MediaType mediaType = CoderMediaType(coder);
                class Codec * codec = new class Codec();
                codec->coder = coder;
                codec->number = json.get_unsigned(ch, "number");
                codec->xmitPacket = json.get_unsigned(ch, "xmit");
                codec->recvPacket = json.get_unsigned(ch, "recv");
                codec->rate = json.get_unsigned(ch, "rate");
                codec->pt = json.get_unsigned(ch, "pt");
                if (!codec->pt) codec->pt = CoderPt(codec->coder);
                codec->addr = _strdup(json.get_string(ch, "addr"));
                codec->port = json.get_unsigned(ch, "port");
                codec->mcAddr = _strdup(json.get_string(ch, "mc_addr"));
                codec->mcPort = json.get_int(ch, "mc_port");
                if (codec->mcAddr && codec->mcPort && !remoteAudioConfig->mcAddr) {
                    remoteAudioConfig->mcAddr = _strdup(codec->mcAddr);
                    remoteAudioConfig->mcPort = codec->mcPort;
                }
                codec->flags = json.get_int(ch, "flags");
                if (mediaType == AUDIO) {
                    remoteAudioConfig->codecList.push_back(codec);
                    if (remoteMediaCmd == MEDIA_CMD_SELECT) audioCoder = coder;
                    if (localAudioConfig) {
                        class Codec * cl;
                        for (cl = localAudioConfig->codecList.front(); cl; cl = cl->goNext()) {
                            if (cl->coder == codec->coder) {
                                cl->pt = codec->pt;
                                break;
                            }
                        }
                    }
                }
                if (mediaType == VIDEO) {
                    remoteVideoConfig->codecList.push_back(codec);
                    if (remoteMediaCmd == MEDIA_CMD_SELECT) videoCoder = coder;
                    if (localVideoConfig) {
                        class Codec * cl;
                        for (cl = localVideoConfig->codecList.front(); cl; cl = cl->goNext()) {
                            if (cl->coder == codec->coder) {
                                cl->pt = codec->pt;
                                break;
                            }
                        }
                    }
                }
            }
        }
        word ice = json.get_object(channels, "audio_ice");
        if (ice != 0xffff) {
            remoteAudioConfig->ice = new IceCandidates();
            remoteAudioConfig->ice->rtcpMux = json.get_bool(channels, "audio_rtcp_mux");
            if (localAudioConfig && localAudioConfig->ice && !remoteAudioConfig->ice->rtcpMux) localAudioConfig->ice->rtcpMux = false;
            remoteAudioConfig->ice->dtlsSetupRole = decode(json.get_string(channels, "audio_setup_role"));
            remoteAudioConfig->ice->iceUfrag = _strdup(json.get_string(ice, "usr"));
            remoteAudioConfig->ice->icePwd = _strdup(json.get_string(ice, "pwd"));
            remoteAudioConfig->ice->fingerprint = _strdup(json.get_string(ice, "fingerprint"));
            word candidates = json.get_array(ice, "candidate");
            if(candidates != 0xffff) {
                word candidate = 0;
                for (candidate = json.get_object(candidates, candidate); candidate != 0xffff; candidate = json.get_object(candidates, candidate)) {
                    class IceCandidate * c = new IceCandidate();
                    c->foundation = _strdup(json.get_string(candidate, "foundation"));
                    c->addr = _strdup(json.get_string(candidate, "addr"));
                    c->rtpPort = json.get_unsigned(candidate, "rtp");
                    c->rtpPriority = json.get_unsigned(candidate, "rtp_prio");
                    c->rtcpPort = json.get_unsigned(candidate, "rtcp");
                    c->rtcpPriority = json.get_unsigned(candidate, "rtcp_prio");
                    c->type = CandidateType(json.get_string(candidate, "type"));
                    remoteAudioConfig->ice->candidateList.push_back(c);
                }
            }
        }
        ice = json.get_object(channels, "video_ice");
        if (ice != 0xffff) {
            remoteVideoConfig->ice = new IceCandidates();
            remoteVideoConfig->ice->rtcpMux = json.get_bool(channels, "video_rtcp_mux");
            if (localVideoConfig && localVideoConfig->ice && !remoteVideoConfig->ice->rtcpMux) localVideoConfig->ice->rtcpMux = false;
            remoteVideoConfig->ice->dtlsSetupRole = decode(json.get_string(channels, "video_setup_role"));
            remoteVideoConfig->ice->iceUfrag = _strdup(json.get_string(ice, "usr"));
            remoteVideoConfig->ice->icePwd = _strdup(json.get_string(ice, "pwd"));
            remoteVideoConfig->ice->fingerprint = _strdup(json.get_string(ice, "fingerprint"));
            word candidates = json.get_array(ice, "candidate");
            if (candidates != 0xffff) {
                word candidate = 0;
                for (candidate = json.get_object(candidates, candidate); candidate != 0xffff; candidate = json.get_object(candidates, candidate)) {
                    class IceCandidate* c = new IceCandidate();
                    c->foundation = _strdup(json.get_string(candidate, "foundation"));
                    c->addr = _strdup(json.get_string(candidate, "addr"));
                    c->rtpPort = json.get_unsigned(candidate, "rtp");
                    c->rtpPriority = json.get_unsigned(candidate, "rtp_prio");
                    c->rtcpPort = json.get_unsigned(candidate, "rtcp");
                    c->rtcpPriority = json.get_unsigned(candidate, "rtcp_prio");
                    c->type = CandidateType(json.get_string(candidate, "type"));
                    remoteVideoConfig->ice->candidateList.push_back(c);
                }
            }
        }
    }
    if (remoteAudioConfig && !remoteAudioConfig->codecList.front()) {
        delete remoteAudioConfig;
        remoteAudioConfig = 0;
    }
    if (remoteVideoConfig && !remoteVideoConfig->codecList.front()) {
        delete remoteVideoConfig;
        remoteVideoConfig = 0;
    }

    closeState = false;
    connectingState = false;
    switch (remoteMediaCmd) {
    case 0:
        if (remoteAudioConfig) remoteMediaCmd = MEDIA_CMD_SELECT;
        break;
    case MEDIA_CMD_PROPOSAL:
    case MEDIA_CMD_SELECT:
        if (!remoteAudioConfig) remoteMediaCmd = MEDIA_CMD_NONE;
        if (audioIo) {
            if (!audioIoChannel) audioIoChannel = audioIo->CreateChannel(this);
        }
        break;
    case MEDIA_CMD_REQUEST:
        if (audioIo) {
            if (!audioIoChannel) audioIoChannel = audioIo->CreateChannel(this);
            if (!media) {
                MediaInitialize(MediaType::AUDIO);
            }
            else if(localAudioConfig) {
                GenerateIceCredentials(localAudioConfig->ice);
                media->RewriteIceCredentials(localAudioConfig->ice->iceUfrag, localAudioConfig->ice->icePwd);
            }
            else {
                debug->printf("%x/%i:SignalingCall::RecvRemoteMedia localAudioConfig not set. Audio being initialized media:%x", this, call, media);
            }
        }
        if (videoIo && videoAllowed && !videoDisabled) {
            if (!videoIoChannel) videoIoChannel = videoIo->CreateChannel(this);
            // UI not opened and a call comes in, autostart the softphone, no webcam was started
            if (!videoDeviceId && remoteVideoConfig && !registration->signaling->doNotStartCamera) {
                videoDeviceId = _strdup(videoIo->GetDeviceIo(registration->GetHwId()));
                if (videoDeviceId) videoIo->StartVideoDevice((void*)this, (void*)this, videoDeviceId, 1280, 720, 0);
            }
            if (!media2) {
                MediaInitialize(MediaType::VIDEO);
            }
            else if (localVideoConfig) {
                GenerateIceCredentials(localVideoConfig->ice);
                media2->RewriteIceCredentials(localVideoConfig->ice->iceUfrag, localVideoConfig->ice->icePwd);
            }
            else {
                debug->printf("%x/%i:SignalingCall::RecvRemoteMedia localVideoConfig not set. Video being initialized media2:%x", this, call, media2);
            }
        }
        // We received a REQUEST after an empty SETUP
        // This REQUEST triggered a SendConn which was false as the user did not connect the call
        // Do nothing with this REQUEST and wait for the user to connect the call
        if ((state & SIG_CALL_STATE_OUTG) && ((state & 0xF) < SIG_CALL_STATE_CONN)) selectProposalByConn = true;
        MediaInitialized();
        break;
    case MEDIA_CMD_CLOSE:
        if (audioIoChannel) {
            audioIoChannel->Close();
            MediaIoCloseComplete(audioIoChannel);
        }
        if (videoIoChannel) {
            videoIoChannel->Close();
            videoIoChannelClosing = videoIoChannel;
            videoIoChannel = NULL;
        }
        audioCoder = CODER_UNDEFINED;
        videoCoder = CODER_UNDEFINED;
        connReceived = false;
        if (media && localAudioConfig) media->CleanSrtpKeys();
        if (media2 && localVideoConfig) media2->CleanSrtpKeys();
        closeState = true;
        break;
    default:
        DELETE_OBJ(remoteAudioConfig);
        DELETE_OBJ(remoteVideoConfig);
    }

    debug->printf("%x/%i:SignalingCall::RecvRemoteMedia() local=%x remote=%x localVideo=%x remoteVideo=%x cmd=%s video=%d,%d media2=%p", this, call, localAudioConfig, remoteAudioConfig, localVideoConfig, remoteVideoConfig, ChannelsCmdName(remoteMediaCmd), videoAllowed, videoDisabled, media2);

    if (localAudioConfig && remoteMediaCmd) {
        if ((state & SIG_CALL_STATE_OUTG) && ((state & 0xF) < SIG_CALL_STATE_CONN)) {
            // Signaling gets first an empty Setup und a PROPOSAL afterwards
            // This had triggered a SendLocalMedia with Channels
            debug->printf("%x/%i:SignalingCall::RecvRemoteMedia() incoming call receiving command before connect state=%x", this, call, state);
            selectProposalByConn = true;
        }
        else if (remoteMediaCmd == MEDIA_CMD_SELECT) {
            // if we send PROPOSAL, remote is ICE controlled and remote is active on DTLS (by default)
            enum SetupRole remoteDtlsRole = SETUP_ROLE_NONE; 
            if (remoteAudioConfig->ice && remoteAudioConfig->ice->dtlsSetupRole == SETUP_ROLE_PASSIVE) remoteDtlsRole = SETUP_ROLE_PASSIVE;
            connecting |= (1 << AUDIO);
            media->Connect(remoteAudioConfig, false, remoteDtlsRole);
            remoteAudioConfig = 0;
            if (media2 && remoteVideoConfig) {
                enum SetupRole remoteDtlsRole = SETUP_ROLE_NONE;
                if (remoteVideoConfig->ice && remoteVideoConfig->ice->dtlsSetupRole == SETUP_ROLE_PASSIVE) remoteDtlsRole = SETUP_ROLE_PASSIVE;
                connecting |= (1 << VIDEO);
                media2->Connect(remoteVideoConfig, false, remoteDtlsRole);
            }
            else {
                // SELECT with Audio only
                if (videoIo && videoDeviceId) {
                    videoIo->StopDevice((void*)this, videoDeviceId);
                    free(videoDeviceId);
                    videoDeviceId = NULL;
                }
                if (videoIoChannel) {
                    videoIoChannel->Close();
                    videoIoChannelClosing = videoIoChannel;
                    videoIoChannel = NULL;
                }
                if (media2) {
                    media2->Close();
                    media2Closing = media2;
                    media2 = NULL;
                    DELETE_OBJ(localVideoConfig);
                }
                delete remoteVideoConfig;
            }
            remoteVideoConfig = 0;
            remoteMediaCmd = MEDIA_CMD_NONE;
        }
        // We got a PROPOSAL with Video but we had no video allocated. It means we should first allocate Video if enabled.
        // This will be done outside this function after RecvRemoteMedia
        else if ((remoteMediaCmd == MEDIA_CMD_PROPOSAL) && !videoDisabled && (remoteVideoConfig && remoteVideoConfig->codecList.front()) && !media2) {
            // Ignore this case, outside following will be executed:
            // if (!media2 && !videoDisabled && (remoteVideoConfig && remoteVideoConfig->codecList.front())) {
        }
        // We got a REQUEST from the PBX, remote did not offer anything yet but we support Video, transfer?
        else if ((remoteMediaCmd == MEDIA_CMD_REQUEST) && videoAllowed && !videoDisabled && (!media2 || (initializing & (1<<VIDEO)))) {

        }
        else if (remoteMediaCmd == MEDIA_CMD_PROPOSAL || remoteMediaCmd == MEDIA_CMD_REQUEST) {
            // This is a new proposal without Video
            if (media2 && !remoteVideoConfig) {
                // PROPOSAL/REQUEST without previous CLOSE, videoIoChannel still opened and would not be deleted
                if (videoIoChannel) {
                    videoIoChannel->Close();
                    videoIoChannelClosing = videoIoChannel;
                    videoIoChannel = NULL;
                }
                media2->Close();
                media2Closing = media2;
                media2 = NULL;
                DELETE_OBJ(localVideoConfig);
                videoCoder = CODER_UNDEFINED;
                GenerateIceCredentials(localAudioConfig->ice);
                media->RewriteIceCredentials(localAudioConfig->ice->iceUfrag, localAudioConfig->ice->icePwd);
            }
            class SignalingSigMessage send(call, "channels", alloca(10000), alloca(4000));
            SendLocalMedia(send, send.sig, send.tmp);
            send.Send(registration);
        }
    }
}

void SignalingCall::Delete()
{
    debug->printf("%x/%i:SignalingCall::Delete() deleting=%d muted=%d registration=%p hookAudioDeviceId=%p hookState=%d reconnectedCall=%p reconnectingCall=%p reconnectingCallDelete:%p pendingReconnectCall:%p", this, call, deleting, muted, registration, hookAudioDeviceId, hookState, reconnectedCall, reconnectingCall, registration ? registration->reconnectingCallDelete : nullptr, registration ? registration->pendingReconnectCall : nullptr);
    
    if (deleting) return;

    if (registration) {
        if (hookAudioDeviceId) {
            if (hookState == KEY_HOLD) registration->signaling->SendHookKey(hookAudioDeviceId, KEY_RETRIEVE, call, remote.num ? remote.num : remote.sip, remote.dn);
            if (hookState != KEY_ONHOOK) registration->signaling->SendHookKey(hookAudioDeviceId, KEY_ONHOOK, call, remote.num ? remote.num : remote.sip, remote.dn);
            hookState = KEY_ONHOOK;
        }
        if (registration->signaling->user && registration->signaling->user->UseRemoteClientServices()) registration->signaling->GetSignalingRemote()->DeleteCall(this);
    }

    class SignalingCall* tmpReconnectedCall = reconnectedCall;
    if (reconnectedCall) {
        reconnectedCall->reconnectingCall = nullptr;
        reconnectedCall = nullptr;
    }

    if (reconnectingCall) {
        if (registration && registration->activePhoneCall == this) registration->activePhoneCall = reconnectingCall;
        reconnectingCall->reconnectedCall = nullptr;
        reconnectingCall = nullptr;
    }

    deleting = true;
    while (monitors.front()) delete monitors.front();
    if (registration) {
        if (callNotInBtree == false) registration->calls = registration->calls->btree_get(this);
        if (registration->reconnectingCallDelete && registration->reconnectingCallDelete == tmpReconnectedCall) {
            registration->calls = registration->calls->btree_put(registration->reconnectingCallDelete);
            registration->reconnectingCallDelete->callNotInBtree = false;
            registration->reconnectingCallDelete = nullptr;
        }
        if (registration->pendingReconnectCall && registration->pendingReconnectCall == tmpReconnectedCall) {
            class btree* b = registration->calls->btree_find((void*)(intp)registration->pendingReconnectCall->call);
            if (!b) {
                registration->calls = registration->calls->btree_put(registration->pendingReconnectCall);
                registration->pendingReconnectCall->callNotInBtree = false;
                registration->pendingReconnectCall = nullptr;
            }
            else registration->pendingReconnectCall->reconnectCallRequest.Start(30000);
        }
        if (registration->calls == nullptr) {
            registration->signaling->hookDeviceState = 0;
            // call is muted when terminating the call and no more calls
            if (muted) audioIo->Mute(false, false);
            if (registration->signaling->changedAudioDeviceId) {
                free(registration->signaling->changedAudioDeviceId);
                registration->signaling->changedAudioDeviceId = 0;
                audioIo->StartHookDevice(registration->signaling->defaultAudioDeviceId);
            }
        }

        // ChangeState funcion call cannot be called due to this->state != state
        // ChangeState(0) is called with "rel" and activeCall should still be this one (terminated = c)
        // ChangeState(0) is called again after btree_get so that another call is set as activeCall
        this->state = 0;
        registration->StateChanged(this);
        mediaInfoInterval.Cancel();
        audioRtpReceived.Cancel();
        hookDeviceTimer.Cancel();
        reconnectCallTimer.Cancel();
        reconnectCallResponse.Cancel();
        mediaLostToneTimer.Cancel();

        if (registration->activePhoneCall == this) registration->activePhoneCall = 0;
        registration = 0;
    }
    DELETE_OBJ(localAudioConfig);
    DELETE_OBJ(localVideoConfig);
    if (media) {
        media->Close();
    }
    if (media2) {
        media2->Close();
    }
    if (media == NULL && mediaClosing == NULL && media2 == NULL && media2Closing == NULL && audioIoChannel == NULL && videoIoChannel == NULL && videoIoChannelClosing == NULL) delete this;
}

SignalingCallMonitor::SignalingCallMonitor(class SignalingCall * call)
{
    this->call = call;
    call->monitors.push_back(this);
}

SignalingCallMonitor::~SignalingCallMonitor()
{
}

SignalingSigMessage::SignalingSigMessage(int call, const char * type, void * sb, void * tmp) : JsonIo((char *)sb)
{
    this->sb = (char *)sb;
    this->tmp = (char *)tmp;
    base = add_object(0xffff, 0);
    add_string(base, "mt", "Signaling");
    add_int(base, "call", call, this->tmp);
    sig = add_object(base, "sig");
    add_string(sig, "type", type);
}

void SignalingSigMessage::Send(class SignalingRegistration * registration)
{
    registration->MessageSend(*this, base, sb);
}

/*-----------------------------------------------------------------------------------------------*/

VoipEndpoint::VoipEndpoint(const char * num, const char * sip, const char * dn)
{
    this->num = _strdup(num);
    this->sip = _strdup(sip);
    this->dn = _strdup(dn);
    restricted = false;
}

void VoipEndpoint::SetNum(const char * num)
{
    free(this->num);
    this->num = _strdup(num);
}

void VoipEndpoint::AddtoNum(const char * num)
{
    if (this->num) {
        if (num) {
            char * s = (char *)alloca(strlen(num) + strlen(this->num) + 1);
            strcpy(s, this->num);
            strcpy(&s[strlen(this->num)], num);
            SetNum(s);
        }
    }
    else {
        SetNum(num);
    }
}

void VoipEndpoint::SetSip(const char * sip)
{
    free(this->sip);
    this->sip = _strdup(sip);
}

void VoipEndpoint::SetDn(const char * dn)
{
    free(this->dn);
    this->dn = _strdup(dn);
}

void VoipEndpoint::Clear()
{
    free(num), free(sip), free(dn);
    num = sip = dn = nullptr;
}

/*-----------------------------------------------------------------------------------------------*/

SignalingRegistrationMonitor::SignalingRegistrationMonitor(enum CallType callType)
{
    this->callType = callType;
}

/*-----------------------------------------------------------------------------------------------*/
/* SignalingRcc                                                                                  */
/*-----------------------------------------------------------------------------------------------*/

SignalingRcc::SignalingRcc(class JsonApiContext * context, class IInstanceLog * log)
{
    this->log = log;
    if(log) log->Log(LOG_SIGNALING, "%x:SignalingRcc()", this);
    context->RegisterJsonApi(this);
}

class JsonApi * SignalingRcc::CreateJsonApi(class IJsonApiConnection * connection, class json_io & msg, word base)
{
    debug->printf("%x:SignalingRcc::CreateJsonApi()", this);
    return 0;
}

class JsonApi * SignalingRcc::JsonApiRequested(class IJsonApiConnection * connection)
{
    if (log) log->Log(LOG_SIGNALING, "%x:SignalingRcc::JsonApiRequested()", this);
    return new SignalingRccSession(this, connection, log);
}

/*-----------------------------------------------------------------------------------------------*/

SignalingRccSession::SignalingRccSession(class SignalingRcc * rcc, class IJsonApiConnection * connection, class IInstanceLog * log) : SignalingRegistrationMonitor(CallTypeVoice)
{
    log->Log(LOG_SIGNALING, "%x:SignalingRccSession() rcc: %x", this, rcc);
    this->rcc = rcc;
    this->log = log;
    this->connection = connection;
    
    user = 0;
    userInitializeSrc = 0;
    userInitializeRx = false;
    lastSessionHandle = 0;
    handles = 0;
    stopping = false;
    httpClient = nullptr;

    rcc->AddRegistrationMonitor(this);
    connection->RegisterJsonApi(this);
}

SignalingRccSession::~SignalingRccSession()
{
    ASSERT(httpClient == nullptr, "HTTP client still running...");

    class SignalingRegistration* registration = rcc->GetRegistration();

    log->Log(LOG_SIGNALING, "%x:~SignalingRccSession() registration=%x", this, registration);

    // Clear video calls
    if (registration && !videoConfCalls.empty()) {
        class SignalingRccVideoConfCall* c = videoConfCalls.front();
        while (c) {
            class SignalingRccVideoConfCall* n = c->goNext();

            char* buf = (char*)alloca(10000), * tmp = (char*)alloca(1000);
            class json_io out(buf);

            word base = out.add_object(0xffff, 0);
            out.add_string(base, "mt", "Signaling");
            out.add_int(base, "call", c->id, tmp);
            word sig = out.add_object(base, "sig");
            out.add_string(sig, "type", "rel");

            word len = out.encode();
            registration->MessageSend(out, base, buf);
            (void)len;

            c->remove();
            delete c;
            c = n;
        }
    }
    rcc->RemoveRegistrationMonitor(this);
    while (users.front()) delete users.front();
}

void SignalingRccSession::JsonApiStart()
{
    log->Log(LOG_APP, "%x:SignalingRccSession::JsonApiStart()", this);
}

void SignalingRccSession::Message(class json_io & msg, word base, const char * mt, const char * src)
{
    log->Log(LOG_APP, "%x:SignalingRccSession::Message(mt=%s)", this, mt);
    if (!strcmp(mt, "Version")) Version(msg, base, src);
    else if (!strcmp(mt, "DoNotStartCamera")) DoNotStartCamera(msg, base, src);
    else if (!strcmp(mt, "DiversionInterrogate")) DiversionInterrogate(msg, base, src);
    else if (!strcmp(mt, "DiversionDeactivate")) DiversionActivateDeactivate(msg, base, src);
    else if (!strcmp(mt, "DiversionActivate")) DiversionActivateDeactivate(msg, base, src);
    else if (!strcmp(mt, "UserInitialize")) UserInitialize(msg, base, src);
    else if (!strcmp(mt, "UserCall")) UserCall(msg, base, src);
    else if (!strcmp(mt, "UserClear")) UserClear(msg, base, src);
    else if (!strcmp(mt, "UserConnect")) UserConnect(msg, base, src);
    else if (!strcmp(mt, "UserRc")) UserRc(msg, base, src);
    else if (!strcmp(mt, "UserInfo")) UserInfo(msg, base, src);
    else if (!strcmp(mt, "UserDTMF")) UserDTMF(msg, base, src);
    else if (!strcmp(mt, "UserUUI")) UserUUI(msg, base, src);
    // UserTransfer, one call on hold, one connected.
    // One connected call (A), one outgoing alerting call (B).
    else if (!strcmp(mt, "UserTransfer")) UserTransfer(msg, base, src);
    // UserRedirect, only one connected call. Blind Transfer to another peer.
    else if (!strcmp(mt, "UserRedirect")) UserRedirect(msg, base, src);
    // UserReroute, only one alerting call. Call forwarding to another peer.
    else if (!strcmp(mt, "UserReroute")) UserReroute(msg, base, src);
    else if (!strcmp(mt, "UserHold")) UserHold(msg, base, src);
    else if (!strcmp(mt, "UserRetrieve")) UserRetrieve(msg, base, src);
    else if (!strcmp(mt, "UserPark")) UserPark(msg, base, src);
    else if (!strcmp(mt, "UserOptions")) UserOptions(msg, base, src);
    else if (!strcmp(mt, "PbxSignal")) PbxSignal(msg, base, src);
    else if (!strcmp(mt, "PlaySound")) PlaySound(msg, base, src);
    else if (!strcmp(mt, "DisplayName")) DisplayName(msg, base, src);

    connection->JsonApiMessageComplete();
}

void SignalingRccSession::JsonApiConnectionClosed()
{
    stopping = true;

    log->Log(LOG_APP, "%x:SignalingRccSession::JsonApiConnectionClosed() registration=%x httpClient=%x", this, rcc->GetRegistration(), httpClient);
    class SignalingRegistration* registration = rcc->GetRegistration();
    if (registration) {
        if (registration->signaling) {
            registration->signaling->startVideo = false;
        }
        if (registration->diversionSession == this) registration->diversionSession = NULL;
    }
    if(!httpClient) delete this;
}

void SignalingRccSession::Version(class json_io & json, word base, const char * src)
{
    SignalingRCCMessage send("VersionResult", src, this, alloca(1000), alloca(100));
    send.add_string(send.base, "ver", _VERSION_STR_);
    send.add_string(send.base, "build", _BUILD_STRING_);
    send.Send();
}

void SignalingRccSession::UserInitializeResult()
{
    class SignalingRegistration * registration = rcc->GetRegistration();
    user = new SignalingRccUser(this, userInitializeSrc);
    class SignalingRCCMessage * userInitializeMsg = new class SignalingRCCMessage("UserInitializeResult", userInitializeSrc, this, alloca(1000), alloca(100));
    if (registration) {
        if (registration->registered) {
            userInitializeMsg->add_unsigned(userInitializeMsg->base, "user", user->handle, userInitializeMsg->tmp);
            userInitializeMsg->add_string(userInitializeMsg->base, "addr", registration->signaling->localIpAddr);
            userInitializeMsg->add_bool(userInitializeMsg->base, "video", registration->signaling->videoLicense);
            userInitializeMsg->add_bool(userInitializeMsg->base, "recording", registration->signaling->recordingLicense);
            registration->dialingLocation.write(userInitializeMsg);
            class SignalingMediaConfig* c = registration->signaling->service->mediaConfig;
            userInitializeMsg->add_string(userInitializeMsg->base, "stun", c->stunServers);
            userInitializeMsg->add_string(userInitializeMsg->base, "turn", c->turnServers);
            userInitializeMsg->add_string(userInitializeMsg->base, "turnUsr", c->turnUsername);
            userInitializeMsg->add_string(userInitializeMsg->base, "turnPwd", c->turnPassword);
            if (c->turnOnly) userInitializeMsg->add_bool(userInitializeMsg->base, "turnOnly", c->turnOnly);
            if (c->stunSlow) userInitializeMsg->add_bool(userInitializeMsg->base, "stunSlow", c->stunSlow);
        }
        else if (registration->err) {
            userInitializeMsg->add_string(userInitializeMsg->base, "error", registration->err);
        }
    }
    userInitializeMsg->Send();
    rcc->MonitorSync(user);
    user = 0;
    delete userInitializeMsg;
    if(userInitializeSrc) free(userInitializeSrc);
    userInitializeSrc = 0;
    userInitializeRx = false;
}

void SignalingRccSession::DoNotStartCamera(class json_io & json, word base, const char * src)
{
    class SignalingRegistration * registration = rcc->GetRegistration();
    if (registration && registration->signaling) {
        registration->signaling->doNotStartCamera = true;
        // send result to confirm this mode
        SignalingRCCMessage result("DoNotStartCameraResult", src, this, alloca(1000), alloca(100));
        result.Send();
    }
}

const char * diversion_types[] = { "CFU", "CFB", "CFNR" };

void SignalingRccSession::DiversionInterrogate(class json_io & json, word base, const char * src)
{
    class SignalingRegistration * registration = rcc->GetRegistration();
    if (!registration) {
        log->Log(LOG_SIGNALING, "%x:SignalingRccSession::DiversionInterrogate no registration available", this);
        return;
    }
    const char * procedure = json.get_string(base, "procedure");

    SignalingSigMessage setup(diversionCallId, "setup", alloca(1000), alloca(100));
    setup.add_int(setup.sig, "channel", 0, setup.tmp);
    word ftys = setup.add_array(setup.sig, "fty");
    for (unsigned i = 0; i < 3; i++) {
        const char * type = diversion_types[i];
        if (!procedure || !strcmp(procedure, type)) {
            word fty = setup.add_object(ftys, 0);
            setup.add_string(fty, "type", "diversion_interrogate");
            setup.add_string(fty, "procedure", type);
        }
    }
    registration->diversionSession = this;
    setup.Send(registration);
}

void SignalingRccSession::DiversionActivateDeactivate(class json_io & json, word base, const char * src)
{
    class SignalingRegistration * registration = rcc->GetRegistration();
    if (!registration) {
        log->Log(LOG_SIGNALING, "%x:SignalingRccSession::DiversionActivateDeactivate no registration available", this);
        return;
    }
    const char * mt = json.get_string(base, "mt");
    const char * procedure = json.get_string(base, "procedure");
    const char * num = json.get_string(base, "num");
    const char * sip = json.get_string(base, "sip");

    const char * type = 0;
    if (mt && !strcmp(mt, "DiversionActivate")) type = "diversion_activate";
    if (mt && !strcmp(mt, "DiversionDeactivate")) type = "diversion_deactivate";

    SignalingSigMessage setup(diversionCallId, "setup", alloca(1000), alloca(100));
    setup.add_int(setup.sig, "channel", 0, setup.tmp);
    word ftys = setup.add_array(setup.sig, "fty");
    word fty = setup.add_object(ftys, 0);
    setup.add_string(fty, "type", type);
    setup.add_string(fty, "procedure", procedure);
    word diverted_to = setup.add_object(fty, "diverted_to");
    if (num) setup.add_string(diverted_to, "num", num);
    if (sip) setup.add_string(diverted_to, "sip", sip);
    //word served = setup.add_object(fty, "served");
    //if (myNum) setup.add_string(served, "num", myNum);
    //if (mySip) setup.add_string(served, "sip", mySip);

    registration->diversionSession = this;
    setup.Send(registration);
}

void SignalingRccSession::UserInitialize(class json_io & json, word base, const char * src)
{
    class SignalingRegistration * registration = rcc->GetRegistration();
    if (user) {
        delete user;
        user = 0;
    }

    log->Log(LOG_SIGNALING, "%x:SignalingRccSession::UserInitialize() reg=%p up=%d err=%s videoLicense=%d", this, registration, registration ? registration->registered : 0, (registration && registration->err) ? registration->err : "-", registration ? registration->signaling->videoLicense : false);

    if (userInitializeSrc) {
        free(userInitializeSrc);
        userInitializeSrc = NULL;
    }
    userInitializeSrc = _strdup(src);
    userInitializeRx = true;
    if (registration && registration->clientUp && (registration->registered || registration->err)) {
        UserInitializeResult();
    }
}

void SignalingRccSession::RegistrationState(bool up)
{
    class SignalingRegistration* registration = rcc->GetRegistration();

    log->Log(LOG_SIGNALING, "%x:SignalingRccSession::RegistrationState() err=%s user=%p userInitializeRx=%d reg=%p up=%d clientUp=%d", this, (registration && registration->err) ? registration->err : "-", user, userInitializeRx, registration, registration ? registration->registered : 0, registration ? registration->clientUp : 0);
    if (userInitializeRx && (registration && registration->clientUp && (registration->registered || registration->err))) {
        UserInitializeResult();
    }
}

void SignalingRccSession::UserCall(class json_io & json, word base, const char * src)
{
    class SignalingRegistration * registration = rcc->GetRegistration();
    class SignalingRccUser * user = FindUser(json.get_unsigned(base, "user"));
    class SignalingRccCall * rccCall = 0;
    const char* e164 = json.get_string(base, "e164");
    const char* h323 = json.get_string(base, "h323");
    const char* dn = json.get_string(base, "dn");
    bool sendingCompletePresent;
    bool sendingComplete = json.get_bool(base, "complete", &sendingCompletePresent);

    log->Log(LOG_SIGNALING, "%x:SignalingRccSession::UserCall() reg: %x sig: %x user: %x e164: %s h323: %s", this, registration, registration ? registration->signaling : 0, user, e164 ? e164 : "-", h323 ? h323 : "-");

    SignalingRCCMessage send("UserCallResult", src, this, alloca(1000), alloca(100));
    if (registration) {
        if (user) {
            bool canStartCall = true;
            unsigned calls = registration->calls ? registration->calls->get_count() : 0;
            if (calls > 0) {
                SignalingCall* call = nullptr;
                while ((call = (class SignalingCall*)registration->calls->btree_find_next_right(call ? (void*)(intp)call->call : 0))) {
                    if (!(call->state & SIG_CALL_STATE_OUTG) && ((call->state & 0xF) < SIG_CALL_STATE_CONN)) {
                        canStartCall = false; // outgoing call in setup or alert state
                        break;
                    }
                }
            }
            if (canStartCall) {
                class SignalingCall* call = new SignalingCall(registration, CallTypeVoice, 0, e164, h323, dn, sendingCompletePresent ? sendingComplete : false);
                if (registration->signaling->videoLicense) {
                    call->videoAllowed = json.get_bool(base, "video");
                }
                rccCall = new SignalingRccCall(this, user, call);
                class IAudioIo* audioIo = rcc->GetAudioIo();
                class IVideoIo* videoIo = rcc->GetVideoIo();
                if (!calls) {
                    const char * audioDeviceId = json.get_string(base, "device");
#ifdef IP270
                    // prefer external USB device (if plugged) or handsfree
                    if (!audioDeviceId) audioDeviceId = audioIo->GetDeviceByType(HeadsetDevice);
                    if (!audioDeviceId) audioDeviceId = audioIo->GetDeviceByType(HandsfreeDevice);
#endif
                    debug->printf("%x:SignalingRccSession::UserCall() audioDeviceId=%s", this, audioDeviceId);
                    if (audioDeviceId) {
                        if (registration->signaling->changedAudioDeviceId) free(registration->signaling->changedAudioDeviceId);
                        registration->signaling->changedAudioDeviceId = _strdup(audioDeviceId);
                    }
                }
                if (audioIo) call->StartCall(audioIo, videoIo, 0, user);
                send.add_unsigned(send.base, "call", rccCall ? rccCall->handle : 0, send.tmp);
                if (!dn && e164 && strlen(e164) > 5) {
                    registration->signaling->user->Lookup(e164, e164, registration->dialingLocation.prefixIntl, registration->dialingLocation.prefixNtl, registration->dialingLocation.prefixSubs, registration->dialingLocation.country, registration->dialingLocation.area, registration->dialingLocation.subscriber);
                }
            }
        }
        else send.add_string(send.base, "error", "No user available");
    }
    else send.add_string(send.base, "error", "No registration available");
    send.Send();
}

void SignalingRccSession::UserClear(class json_io & json, word base, const char * src)
{
    log->Log(LOG_SIGNALING, "%x:SignalingRccSession::UserClear()", this);
    class SignalingRccCall * call = FindCall(json.get_unsigned(base, "call"));
    SignalingRCCMessage send("UserClearResult", src, this, alloca(1000), alloca(100));
    send.Send();
    if (call) {
        if (call->call->GetReconnectingCall()) {
            call->call->GetReconnectingCall()->failedReconnect = true;
            call->call->GetReconnectingCall()->ClearCall(Q931_CAUSE_NonSelectedUserClearing);
        }
        call->call->ClearCall(json.get_int(base, "cause"));
    }
}

void SignalingRccSession::UserConnect(class json_io & json, word base, const char * src)
{
    log->Log(LOG_SIGNALING, "%x:SignalingRccSession::UserConnect()", this);
    class SignalingRegistration * registration = rcc->GetRegistration();
    class SignalingRccCall * call = FindCall(json.get_unsigned(base, "call"));
    SignalingRCCMessage send("UserConnectResult", src, this, alloca(1000), alloca(100));
    send.Send();
    
    if (registration && call && call->call) {
        if (registration->signaling->videoLicense) {
            call->call->videoAllowed = json.get_bool(base, "video");
        }
        class IAudioIo * audioIo = rcc->GetAudioIo();
        class IVideoIo * videoIo = rcc->GetVideoIo();
        const char * audioDeviceId = json.get_string(base, "device");
#ifdef IP270
        // prefer external USB device (if plugged) or handsfree
        if (!audioDeviceId) audioDeviceId = audioIo->GetDeviceByType(HeadsetDevice);
        if (!audioDeviceId) audioDeviceId = audioIo->GetDeviceByType(HandsfreeDevice);
#endif
        debug->printf("%x:SignalingRccSession::UserConnect() audioDeviceId=%s", this, audioDeviceId);
        if (audioDeviceId) {
            if (registration->signaling->changedAudioDeviceId) free(registration->signaling->changedAudioDeviceId);
            registration->signaling->changedAudioDeviceId = _strdup(audioDeviceId);
        }
        if (audioIo) call->call->ConnectCall(audioIo, videoIo);
    }
}

static struct { const char * str;  unsigned code; } rc_codes[] = {
    { "FTY_REMOTE_CONTROL_REC_ON", FTY_REMOTE_CONTROL_REC_ON },
    { "FTY_REMOTE_CONTROL_REC_OFF", FTY_REMOTE_CONTROL_REC_OFF },
    { "FTY_REMOTE_CONTROL_3PTY_ON", FTY_REMOTE_CONTROL_3PTY_ON },
    { "FTY_REMOTE_CONTROL_MIC_ON", FTY_REMOTE_CONTROL_MIC_ON },
    { "FTY_REMOTE_CONTROL_MIC_OFF", FTY_REMOTE_CONTROL_MIC_OFF },
    { "FTY_REMOTE_CONTROL_ADD_VIDEO", FTY_REMOTE_CONTROL_ADD_VIDEO },
    { "FTY_REMOTE_CONTROL_REM_VIDEO", FTY_REMOTE_CONTROL_REM_VIDEO },
    { "FTY_REMOTE_CONTROL_START_CAMERA", FTY_REMOTE_CONTROL_START_CAMERA },
    { "FTY_REMOTE_CONTROL_STOP_CAMERA", FTY_REMOTE_CONTROL_STOP_CAMERA },
    { "FTY_REMOTE_CONTROL_ADD_APPSHARING", FTY_REMOTE_CONTROL_ADD_APPSHARING },
    { "FTY_REMOTE_CONTROL_REM_APPSHARING", FTY_REMOTE_CONTROL_REM_APPSHARING },
    { "FTY_REMOTE_CONTROL_ALLOW_VIDEO", FTY_REMOTE_CONTROL_ALLOW_VIDEO },
};

void SignalingRccSession::UserRc(class json_io & json, word base, const char * src)
{
    debug->printf("%x:SignalingRccSession::UserRc()", this);
    class SignalingRccCall * call = FindCall(json.get_unsigned(base, "call"));
    SignalingRCCMessage send("UserRcResult", src, this, alloca(1000), alloca(100));
    send.Send();
    if (call) {
        // #130651 - Allow "UserRc" with textual command
        // (e.g. 10 or "FTY_REMOTE_CONTROL_3PTY_ON")
        unsigned rc = 0;
        bool present = false;
        const char * str = json.get_string(base, "rc", &present);
        if (present && str) {
            const unsigned max = sizeof(rc_codes) / sizeof(*rc_codes);
            for (unsigned i = 0; i < max; i++) {
                if (!strcmp(str, rc_codes[i].str)) {
                    rc = rc_codes[i].code;
                    break;
                }
            }
        }
        else {
            rc = json.get_unsigned(base, "rc", &present);
        }
        call->call->Rc(rc);
    }
}

void SignalingRccSession::UserInfo(class json_io & json, word base, const char * src)
{
    log->Log(LOG_SIGNALING, "%x:SignalingRccSession::UserInfo()", this);
    class SignalingRccCall * call = FindCall(json.get_unsigned(base, "call"));
    SignalingRCCMessage send("UserInfoResult", src, this, alloca(1000), alloca(100));
    send.Send();
    const char * e164 = json.get_string(base, "e164");
    const char * h323 = json.get_string(base, "h323");
    if (call && (e164 || h323)) {
        call->call->Info(e164, h323);
    }
}

void SignalingRccSession::UserDTMF(class json_io & json, word base, const char * src)
{
    log->Log(LOG_SIGNALING, "%x:SignalingRccSession::UserDTMF()", this);
    class SignalingRccCall * call = FindCall(json.get_unsigned(base, "call"));
    SignalingRCCMessage send("UserDTMFResult", src, this, alloca(1000), alloca(100));
    send.Send();
    if (call) {
        call->call->SendDTMF(json.get_string(base, "dtmf"), true);
    }
}

void SignalingRccSession::UserUUI(class json_io & json, word base, const char * src)
{
    log->Log(LOG_SIGNALING, "%x:SignalingRccSession::UserUUI()", this);
    class SignalingRccCall * call = FindCall(json.get_unsigned(base, "call"));
    SignalingRCCMessage send("UserUUIResult", src, this, alloca(1000), alloca(100));
    send.Send();
    if (call) {
        call->call->UUI(json.get_unsigned(base, "pd"), json.get_string(base, "uui"));
    }
}

void SignalingRccSession::UserTransfer(class json_io & json, word base, const char * src)
{
    log->Log(LOG_SIGNALING, "%x:SignalingRccSession::UserTransfer()", this);
    class SignalingRccCall * a = FindCall(json.get_unsigned(base, "a"));
    class SignalingRccCall * b = FindCall(json.get_unsigned(base, "b"));
    SignalingRCCMessage msg("UserTransferResult", src, this, alloca(1000), alloca(100));
    msg.Send();
    if (a && b) {
        a->call->StartTransfer(b->call);
    }
}

void SignalingRccSession::UserRedirect(class json_io & json, word base, const char * src)
{
    log->Log(LOG_SIGNALING, "%x:SignalingRccSession::UserRedirect()", this);
    class SignalingRccCall * call = FindCall(json.get_unsigned(base, "call"));
    const char * e164 = json.get_string(base, "e164");
    const char * h323 = json.get_string(base, "h323");
    SignalingRCCMessage msg("UserRedirectResult", src, this, alloca(1000), alloca(100));
    msg.Send();
    if (call && (e164 || h323)) {
        call->call->StartRedirect(e164, h323);
    }
}

void SignalingRccSession::UserReroute(class json_io & json, word base, const char * src)
{
    log->Log(LOG_SIGNALING, "%x:SignalingRccSession::UserReroute()", this);
    class SignalingRccCall * call = FindCall(json.get_unsigned(base, "call"));
    const char * e164 = json.get_string(base, "e164");
    const char * h323 = json.get_string(base, "h323");
    SignalingRCCMessage msg("UserRerouteResult", src, this, alloca(1000), alloca(100));
    msg.Send();
    if (call && (e164 || h323)) {
        call->call->StartReroute(e164, h323);
    }
}

void SignalingRccSession::UserHold(class json_io & json, word base, const char * src)
{
    log->Log(LOG_SIGNALING, "%x:SignalingRccSession::UserHold()", this);
    class SignalingRccCall * call = FindCall(json.get_unsigned(base, "call"));
    SignalingRCCMessage msg("UserHoldResult", src, this, alloca(1000), alloca(100));
    msg.Send();
    if (call && call->call) {
        call->call->Hold();
    }
}

void SignalingRccSession::UserRetrieve(class json_io & json, word base, const char * src)
{
    log->Log(LOG_SIGNALING, "%x:SignalingRccSession::UserRetrieve()", this);
    class SignalingRccCall * call = FindCall(json.get_unsigned(base, "call"));
    SignalingRCCMessage msg("UserRetrieveResult", src, this, alloca(1000), alloca(100));
    msg.Send();
    if (call && call->call) {
        call->call->Retrieve();
    }
}

void SignalingRccSession::UserPark(class json_io& json, word base, const char* src)
{
    log->Log(LOG_SIGNALING, "%x:SignalingRccSession::UserPark()", this);
    class SignalingRccCall* call = FindCall(json.get_unsigned(base, "call"));
    SignalingRCCMessage msg("UserParkResult", src, this, alloca(1000), alloca(100));
    msg.Send();

    const char* parked_to_num = 0, * parked_to_sip = 0;
    word parked_to = json.get_object(base, "parked_to");
    if (parked_to != 0xffff) {
        parked_to_num = json.get_string(parked_to, "num");
        parked_to_sip = json.get_string(parked_to, "sip");
    }
    if (call && call->call) {
        call->call->Park(parked_to_num, parked_to_sip);
    }
}

void SignalingRccSession::UserOptions(class json_io& json, word base, const char* src)
{
    log->Log(LOG_SIGNALING, "%x:SignalingRccSession::UserOptions()", this);
    class SignalingRegistration* registration = rcc->GetRegistration();
    if (registration && registration->signaling) {
        registration->signaling->startVideo = json.get_bool(base, "startVideo");
        const char * callWaiting = json.get_string(base, "callWaiting");
        if (callWaiting) {
            if (!strcmp(callWaiting, "off")) registration->signaling->callWaiting = CallWaitingOff;
            else if (!strcmp(callWaiting, "on")) registration->signaling->callWaiting = CallWaitingOn;
            else if (!strcmp(callWaiting, "silent")) registration->signaling->callWaiting = CallWaitingSilent;
            ILocalStorage::SetItem("callWaiting", callWaiting);
        }
        else {
            // legacy boolean attribute
            bool callWaiting = json.get_bool(base, "callWaiting");
            registration->signaling->callWaiting = callWaiting ? CallWaitingOn : CallWaitingOff;
            ILocalStorage::SetItem("callWaiting", callWaiting ? "on" : "off");
        }
        registration->signaling->onhookTransfer = json.get_bool(base, "onhookTransfer");
        if (registration->signaling->onhookTransfer) ILocalStorage::SetItem("onhookTransfer", "true");
        else ILocalStorage::SetItem("onhookTransfer", "false");
        registration->signaling->dndRejectCall = json.get_string(base, "dndReaction") ? !strcmp(json.get_string(base, "dndReaction"), "reject") : false;
        if (registration->signaling->dndRejectCall) ILocalStorage::SetItem("dndRejectCall", "true");
        else ILocalStorage::SetItem("dndRejectCall", "false");
        registration->signaling->clir = json.get_bool(base, "clir");
        if (registration->signaling->clir) ILocalStorage::SetItem("clir", "true");
        else ILocalStorage::SetItem("clir", "false");
        if (const char* autoHangup = json.get_string(base, "autoHangup")) {
            if (!strcmp(autoHangup, "off")) registration->signaling->autoHangup = AUTOHANGUP_OFF;
            else if (!strcmp(autoHangup, "allCalls")) registration->signaling->autoHangup = AUTOHANGUP_ALLCALLS;
            else if (!strcmp(autoHangup, "connectedCalls")) registration->signaling->autoHangup = AUTOHANGUP_CONNECTEDCALLS;
            ILocalStorage::SetItem("autoHangup", autoHangup);
        }
        else ILocalStorage::SetItem("autoHangup", "off");
        // URL could be removed, call the function even if url is null
        registration->signaling->SetRecordingParameters(json.get_string(base, "recordingUrl"), false, false, json.get_bool(base, "isRecordingByDefaultOn"), false, json.get_bool(base, "recordExternalOnly"), false);
    }
}

void SignalingRccSession::PbxSignal(class json_io& json, word base, const char* src)
{
    class SignalingRegistration* registration = rcc->GetRegistration();

    int call = json.get_int(base, "call");
    class SignalingRccCall* rccCall = FindCall(call);
    int callId = rccCall && rccCall->call ? rccCall->call->call : call;

    log->Log(LOG_SIGNALING, "%x:SignalingRccSession::PbxSignal() call=%d rccCall=%p(%d)", this, call, rccCall, callId);

    word sig = json.get_object(base, "sig");
    if (sig != 0xffff) {
        char* sig_buf = (char*)alloca(10000);
        word sig_len = json.encode(sig, sig_buf);

        char* buf = (char*)alloca(10000), * tmp = (char*)alloca(1000);
        class json_io out(buf);
        word base = out.add_object(0xffff, 0);
        out.add_string(base, "mt", "Signaling");
        out.add_int(base, "call", callId, tmp);
        out.add_json(base, "sig", sig_buf, sig_len);

        // Keep track of video conference calls
        // If UI is closed, a rel message will be sent to the PBX
        const char * msg = json.get_string(sig, "type");
        if (msg && !strcmp(msg, "setup")) {
            // Search voice call
            if (registration && registration->signaling && registration->signaling->FindCallByConfId(json.get_string(sig, "confId"))) {
                debug->printf("%x:SignalingRccSession::PbxSignal() add video conference call=%d", this, callId);

                videoConfCalls.push_back(new SignalingRccVideoConfCall(json.get_string(sig, "confId"), callId));
            }
        }
        else if (msg && !strcmp(msg, "rel")) {
            if (!videoConfCalls.empty()) {
                class SignalingRccVideoConfCall * c = videoConfCalls.front();
                while(c) {
                    if (c->id == callId) {
                        debug->printf("%x:SignalingRccSession::PbxSignal() remove video conference call=%d", this, callId);

                        c->remove();
                        delete c;
                        break;
                    }
                    c = c->goNext();
                }
            }
        }
        word len = out.encode();
        if(registration) registration->MessageSend(out, base, buf);
        (void)len;
    }
}

void SignalingRccSession::PlaySound(class json_io& json, word base, const char* src)
{
    log->Log(LOG_SIGNALING, "%x:SignalingRccSession::PlaySound() url=%s", this, json.get_string(base, "url") ? json.get_string(base, "url") : "");

    class SignalingRegistration * registration = rcc->GetRegistration();

    _snprintf(webaccessTone, 4096, "%swebaccess.entrance.mp3", registration->signaling->GetWorkingPath());
    if (!FileSystem::FileExists(webaccessTone, registration->signaling)) {
        if(!httpClient) httpClient = new class SignalingRccSessionHttpClient(registration->signaling, this, json.get_string(base, "url"));
    }
    else {
        const char * toneFilename = "webaccess.entrance.mp3";
#if defined __ANDROID__ || defined _IOS_
        char toneId[4096];
        _snprintf(toneId, sizeof(toneId), "%s%s%s", "file://", registration->signaling->GetWorkingPath(), toneFilename);
#else
        const char * toneId = toneFilename;
#endif
        registration->signaling->GetRingerIo()->NotificationTonePlay(registration->signaling->GetAudioDevice(), 0, toneId);
    }
}

void SignalingRccSession::DisplayName(class json_io& json, word base, const char* src)
{
    log->Log(LOG_SIGNALING, "%x:SignalingRccSession::DisplayName()", this);
    class SignalingRccCall* call = FindCall(json.get_unsigned(base, "call"));
    SignalingRCCMessage msg("DisplayNameResult", src, this, alloca(1000), alloca(100));
    msg.Send();
    if (call && call->call) {
        call->call->DisplayName(json, base, src);
    }
}

void SignalingRccSession::SignalingRccSessionHttpClientClosed(class SignalingRccSessionHttpClient* httpClient)
{
    log->Log(LOG_SIGNALING, "%x:SignalingRccSession::SignalingRccSessionHttpClientClosed() stopping:%d Success:%d", this, stopping, httpClient->Success());

    if (this->stopping) {
        delete httpClient;
        this->httpClient = nullptr;
        delete this;
    }
    else {
        if (httpClient->Success()) {
            class SignalingRegistration* registration = rcc->GetRegistration();

            const char * toneFilename = "webaccess.entrance.mp3";
#if defined __ANDROID__ || defined _IOS_
            char toneId[4096];
            _snprintf(toneId, sizeof(toneId), "%s%s%s", "file://", registration->signaling->GetWorkingPath(), toneFilename);
#else
            const char * toneId = toneFilename;
#endif
            registration->signaling->GetRingerIo()->NotificationTonePlay(registration->signaling->GetAudioDevice(), 0, toneId);
        }
        delete httpClient;
        this->httpClient = nullptr;
    }
}

class SignalingRccUser * SignalingRccSession::FindUser(word handle)
{
    class btree * b = handles->btree_find((void *)(uintp)handle);
    if (b) {
        if (((class SignalingRccHandle *)b)->type == RCC_TYPE_USER) {
            return (class SignalingRccUser *)(class SignalingRccHandle *)b;
        }
    }
    return 0;
}

class SignalingRccCall * SignalingRccSession::FindCall(word handle)
{
    class btree * b = handles->btree_find((void *)(uintp)handle);
    if (b) {
        if (((class SignalingRccHandle *)b)->type == RCC_TYPE_CALL) {
            return (class SignalingRccCall *)(class SignalingRccHandle *)b;
        }
    }
    return 0;
}

SignalingRccHandle::SignalingRccHandle(class SignalingRccSession * session, word type)
{
    this->session = session;
    this->type = type;

    do {
        session->lastSessionHandle++;
    } while(!session->lastSessionHandle || session->handles->btree_find((void *)(uintp)session->lastSessionHandle));
    handle = session->lastSessionHandle;
    session->handles = session->handles->btree_put(this);
}

SignalingRccHandle::~SignalingRccHandle()
{
    session->handles = session->handles->btree_get(this);
}

int SignalingRccHandle::btree_compare(void * key)
{
    return (word)(uintp)key-handle;
}

int SignalingRccHandle::btree_compare(class btree * b)
{
    return ((class SignalingRccHandle *)b)->handle-handle;
}

SignalingRCCMessage::SignalingRCCMessage(const char * mt, const char * src, class SignalingRccSession * session, void * sb, void * tmp) : JsonIo((char *)sb)
{
    this->session = session;
    this->sb = (char *)sb;
    this->tmp = (char *)tmp;
    base = add_object(0xffff, 0);
    add_string(base, "mt", mt);
    add_string(base, "src", src);
    add_string(base, "api", session->Name());
}

void SignalingRCCMessage::Send()
{
    session->connection->JsonApiMessage(*this, sb);
}

/*-----------------------------------------------------------------------------------------------*/
/* SignalingRccUser                                                                              */
/*-----------------------------------------------------------------------------------------------*/

SignalingRccUser::SignalingRccUser(class SignalingRccSession * session, const char * src) : SignalingRccHandle(session, RCC_TYPE_USER), SignalingRegistrationMonitor(CallTypeVoice)
{
    this->log = session->GetLog();
    if(this->log) this->log->Log(LOG_SIGNALING, "%x/%u:SignalingRccUser()", this, handle);
    this->src = _strdup(src);
    
    session->rcc->AddRegistrationMonitor(this);
    session->users.push_back(this);
}

SignalingRccUser::~SignalingRccUser()
{
    if (log) log->Log(LOG_SIGNALING, "%x/%u:~SignalingRccUser()", this, handle);
    while (calls.front()) delete calls.front();
}

void SignalingRccUser::RegistrationState(bool up)
{
    if(log) log->Log(LOG_SIGNALING, "%x/%u:SignalingRccUser::RegistrationState(up=%u)", this, handle, up);
}

class SignalingCallMonitor * SignalingRccUser::CallAdded(class SignalingCall * call)
{
    if (log) log->Log(LOG_SIGNALING, "%x/%u:SignalingRccUser::CallAdded(%i)", this, handle, call->call);
    call->RccSessionOpened();
    return new SignalingRccCall(session, this, call);
}

void SignalingRccUser::UnknownCallMessage(const char * type, class json_io & msg, word base)
{
    word sig = msg.get_object(base, "sig");
    if (sig != 0xffff) {
        int call = msg.get_int(base, "call");

        char* sig_buf = (char*)alloca(10000);
        word sig_len = msg.encode(sig, sig_buf);

        char* buf = (char*)alloca(10000), * tmp = (char*)alloca(1000);
        class json_io out(buf);
        word base = out.add_object(0xffff, 0);
        out.add_string(base, "api", "RCC");
        out.add_int(base, "user", handle, tmp);
        out.add_string(base, "mt", "PbxSignal");
        out.add_int(base, "call", call, tmp);
        out.add_json(base, "sig", sig_buf, sig_len);

        word len = out.encode();
        session->JsonApiMessage(out, buf);
        (void)len;
    }
}

/*-----------------------------------------------------------------------------------------------*/
/* SignalingRccCall                                                                              */
/*-----------------------------------------------------------------------------------------------*/

SignalingRccCall::SignalingRccCall(SignalingRccSession * session, SignalingRccUser * user, class SignalingCall * call) :
    SignalingRccHandle(session, RCC_TYPE_CALL), SignalingCallMonitor(call)
{
    debug->printf("%x/%u:SignalingRccCall(user=%u)", this, handle, user->handle);
    this->session = session;
    this->user = user;
    user->calls.push_back(this);
}

SignalingRccCall::~SignalingRccCall()
{
    debug->printf("%x/%u:~SignalingRccCall(user=%u) reconnect=%d", this, handle, user->handle, call->failedReconnect);
    if (!call->failedReconnect) {
        class SignalingRCCMessage send("CallInfo", user->src, session, alloca(10000), alloca(4000));
        SendCallInfo(send, send.base, send.tmp);
        send.add_bool(send.base, "del", true);
        send.add_string(send.base, "msg", "del");
        if (call->cause) {
            word info = send.add_array(send.base, "info");
            info = send.add_object(info, 0);
            send.add_string(info, "type", "cause");
            send.add_int(info, "vali", call->cause, send.tmp);
        }
        send.Send();
    }
    call->RccSessionClosed();
}

void SignalingRccCall::MonitorXmit(const char * type, class json_io & msg, word base)
{
    class SignalingRegistration* registration = session->rcc ? session->rcc->GetRegistration() : NULL;
    if (registration && registration->signaling && registration->signaling->log) registration->signaling->log->Log(LOG_SIGNALING, "%x/%u:SignalingRccCall::MonitorXmit(type=%s)", this, handle, type);

    class SignalingRCCMessage send("CallInfo", user->src, session, alloca(10000), alloca(4000));

    if (!strcmp(type, "setup")) {
        SendCallInfo(send, send.base, send.tmp);
        send.add_string(send.base, "msg", "x-setup");
        send.Send();
    }
    else if (!strcmp(type, "info")) {
        SendCallInfo(send, send.base, send.tmp);
        send.add_string(send.base, "msg", "x-info");
        send.Send();
    }
    else if (!strcmp(type, "alert")) {
        SendCallInfo(send, send.base, send.tmp);
        send.add_string(send.base, "msg", "x-alert");
        send.Send();
    }
    else if (!strcmp(type, "conn")) {
        SendCallInfo(send, send.base, send.tmp);
        send.add_string(send.base, "msg", "x-conn");
        send.Send();
    }
    else if (!strcmp(type, "channels")) {
        SendCallInfo(send, send.base, send.tmp);
        send.add_string(send.base, "msg", "x-channels");
        send.Send();
    }
    else if (!strcmp(type, "rel")) {
        SendCallInfo(send, send.base, send.tmp);
        send.add_string(send.base, "msg", "x-rel");
        send.Send();
    }
    else if (!strcmp(type, "facility_hold")) {
        SendCallInfo(send, send.base, send.tmp);
        send.add_string(send.base, "msg", "x-facility");
        word ftys = send.add_array(send.base, "info");
        word fty = send.add_object(ftys, 0);
        send.add_string(fty, "type", "remote_hold");
        send.add_bool(fty, "muted", this->call->muted);
        send.Send();
    }
    else if (!strcmp(type, "facility_retrieve")) {
        SendCallInfo(send, send.base, send.tmp);
        send.add_string(send.base, "msg", "x-facility");
        word ftys = send.add_array(send.base, "info");
        word fty = send.add_object(ftys, 0);
        send.add_string(fty, "type", "remote_retrieve");
        send.add_bool(fty,"muted",this->call->muted);
        send.Send();
    }
    else if (!strcmp(type, "facility_mute")) {
        SendCallInfo(send, send.base, send.tmp);
        send.add_string(send.base, "msg", "x-facility");
        word ftys = send.add_array(send.base, "info");
        word fty = send.add_object(ftys, 0);
        send.add_bool(fty,"muted",this->call->muted);
        send.Send();
    }
    else if (!strcmp(type, "facility_audio_route_changed")) {
        SendCallInfo(send, send.base, send.tmp);
        send.add_string(send.base, "msg", "x-facility");
        word ftys = send.add_array(send.base, "info");
        word fty = send.add_object(ftys, 0);
        send.add_string(fty, "changedAudioRoute", this->call->getAudioDeviceId());
        send.Send();
    }
    else if (!strcmp(type, "media_data_on")) {
        class SignalingRCCMessage send("MediaDataOn", user->src, session, alloca(5000), alloca(1000));
        send.add_string(send.base, "channel", msg.get_string(base, "channel"));
        send.add_string(send.base, "type", msg.get_string(base, "type"));
        send.Send();
    }
    else if (!strcmp(type, "media_data_off")) {
        class SignalingRCCMessage send("MediaDataOff", user->src, session, alloca(5000), alloca(1000));
        send.add_string(send.base, "channel", msg.get_string(base, "channel"));
        send.add_string(send.base, "type", msg.get_string(base, "type"));
        send.Send();
    }
    else if (!strcmp(type, "media_error")) {
        class SignalingRCCMessage send("MediaError", user->src, session, alloca(5000), alloca(1000));
        send.add_unsigned(send.base, "call", handle, send.tmp);
        send.add_string(send.base, "channel", msg.get_string(base, "channel"));
        send.add_string(send.base, "type", msg.get_string(base, "type"));
        send.add_string(send.base, "kind", msg.get_string(base, "kind"));
        send.add_unsigned(send.base, "code", msg.get_unsigned(base, "code"), send.tmp);
        send.add_string(send.base, "errorText", msg.get_string(base, "errorText"));
        send.add_string(send.base, "src", msg.get_string(base, "src"));
        send.Send();
    }
    else if (!strcmp(type, "media_warning")) {
        class SignalingRCCMessage send("MediaWarning", user->src, session, alloca(5000), alloca(1000));
        send.add_unsigned(send.base, "call", handle, send.tmp);
        send.add_string(send.base, "channel", msg.get_string(base, "channel"));
        send.add_string(send.base, "type", msg.get_string(base, "type"));
        send.add_string(send.base, "kind", msg.get_string(base, "kind"));
        send.add_unsigned(send.base, "code", msg.get_unsigned(base, "code"), send.tmp);
        send.add_string(send.base, "warningText", msg.get_string(base, "warningText"));
        send.add_string(send.base, "src", msg.get_string(base, "src"));
        send.Send();
    }
    else if (!strcmp(type, "hook_device_denied")) {
        class SignalingRCCMessage send("HookDeviceError", user->src, session, alloca(5000), alloca(1000));
        send.add_unsigned(send.base, "call", handle, send.tmp);
        send.add_string(send.base, "channel", msg.get_string(base, "channel"));
        send.add_string(send.base, "errorText", msg.get_string(base, "errorText"));
        send.add_string(send.base, "errorId", "hook_device_denied");
        send.add_string(send.base, "src", msg.get_string(base, "src"));
        send.Send();
    }
    else if (!strcmp(type, "hook_device_not_found")) {
        class SignalingRCCMessage send("HookDeviceError", user->src, session, alloca(5000), alloca(1000));
        send.add_unsigned(send.base, "call", handle, send.tmp);
        send.add_string(send.base, "channel", msg.get_string(base, "channel"));
        send.add_string(send.base, "errorText", msg.get_string(base, "errorText"));
        send.add_string(send.base, "errorId", "hook_device_not_found");
        send.add_string(send.base, "src", msg.get_string(base, "src"));
        send.Send();
    }
    else if (!strcmp(type, "no_ice_candidates")) {
        class SignalingRCCMessage send("NoIceCandidates", user->src, session, alloca(5000), alloca(1000));
        send.add_unsigned(send.base, "call", handle, send.tmp);
        send.add_string(send.base, "channel", msg.get_string(base, "channel"));
        send.add_string(send.base, "type", msg.get_string(base, "type"));
        send.add_string(send.base, "kind", msg.get_string(base, "kind"));
        send.add_string(send.base, "errorText", msg.get_string(base, "errorText"));
        send.add_string(send.base, "errorId", msg.get_string(base, "no_ice_candidates"));
        send.Send();
    }
    else if (!strcmp(type, "facility_recording_started")) {
        SendCallInfo(send, send.base, send.tmp);
        send.add_string(send.base, "msg", "x-facility");
        word ftys = send.add_array(send.base, "info");
        word fty = send.add_object(ftys, 0);
        send.add_bool(fty,"recording",true);
        send.add_bool(fty,"isTrunkRecording",this->call->recordingInfo.isTrunkRecording);
        send.add_string(fty,"recordingError",this->call->recordingInfo.recordingError);
        send.Send();
    }
    else if (!strcmp(type, "facility_recording_stopped")) {
        SendCallInfo(send, send.base, send.tmp);
        send.add_string(send.base, "msg", "x-facility");
        word ftys = send.add_array(send.base, "info");
        word fty = send.add_object(ftys, 0);
        send.add_bool(fty,"recording",false);
        send.add_string(fty,"recordingError",this->call->recordingInfo.recordingError);
        send.Send();
    }
    else if (!strcmp(type, "contact")) {
        SendCallInfo(send, send.base, send.tmp);
        char contactBuffer[16 * 1024];
        word contactObject = msg.get_object(base, "contact");
        word contactLength = msg.encode(contactObject, contactBuffer);
        send.add_json(send.base, "contact", contactBuffer, contactLength);
        send.Send();
    }
    else if (!strcmp(type, "reconnecting")) {
        SendCallInfo(send, send.base, send.tmp);
        send.add_string(send.base, "msg", "reconnecting");
        send.Send();
    }
}

void SignalingRccCall::MonitorRecv(const char * type, class json_io & msg, word base)
{
    class SignalingRegistration* registration = session->rcc ? session->rcc->GetRegistration() : NULL;
    if (registration && registration->signaling && registration->signaling->log) registration->signaling->log->Log(LOG_SIGNALING, "%x/%u:SignalingRccCall::MonitorRecv(type=%s)", this, handle, type);
    class SignalingRCCMessage send("CallInfo", user->src, session, alloca(10000), alloca(4000));

    if (!strcmp(type, "setup")) {
        word no = 0xffff;
        SendCallInfo(send, send.base, send.tmp);
        if (call->diverting.num || call->diverting.sip || call->diverting.dn) {
            if (no == 0xffff) no = send.add_array(send.base, "no");
            word diverting = send.add_object(no, 0);
            send.add_string(diverting, "type", "leg2");
            send.add_string(diverting, "e164", call->diverting.num);
            send.add_string(diverting, "h323", call->diverting.sip);
            send.add_string(diverting, "dn", call->diverting.dn);
        }
        if (call->originalCalled.num || call->originalCalled.sip || call->originalCalled.dn) {
            if (no == 0xffff) no = send.add_array(send.base, "no");
            word originalCalled = send.add_object(no, 0);
            send.add_string(originalCalled, "type", "leg2orig");
            send.add_string(originalCalled, "e164", call->originalCalled.num);
            send.add_string(originalCalled, "h323", call->originalCalled.sip);
            send.add_string(originalCalled, "dn", call->originalCalled.dn);
        }
        if (call->transfering.num || call->transfering.sip || call->transfering.dn) {
            if (no == 0xffff) no = send.add_array(send.base, "no");
            word transfering = send.add_object(no, 0);
            send.add_string(transfering, "type", "ct");
            send.add_string(transfering, "e164", call->transfering.num);
            send.add_string(transfering, "h323", call->transfering.sip);
            send.add_string(transfering, "dn", call->transfering.dn);
        }
        send.add_string(send.base, "reconnect", call->GetReconnectRole());
        send.add_string(send.base, "msg", "r-setup");
        send.Send();
    }
    else if (!strcmp(type, "setup_ack")) {
        SendCallInfo(send, send.base, send.tmp);
        send.add_string(send.base, "msg", "r-setup-ack");
        send.Send();
    }
    else if (!strcmp(type, "call_proc")) {
        SendCallInfo(send, send.base, send.tmp);
        send.add_string(send.base, "msg", "r-call-proc");
        send.Send();
    }
    else if (!strcmp(type, "alert")) {
        SendCallInfo(send, send.base, send.tmp);
        send.add_string(send.base, "msg", "r-alert");
        if (call->alertPresence) {
            word presence = send.add_object(send.base, "presence");
            send.add_string(presence, "contact", call->alertPresence->contact);
            send.add_string(presence, "status", call->alertPresence->status);
            send.add_string(presence, "activity", call->alertPresence->activity);
            send.add_string(presence, "note", call->alertPresence->note);
        }
        send.Send();
    }
    else if (!strcmp(type, "conn")) {
        SendCallInfo(send, send.base, send.tmp);
        send.add_string(send.base, "reconnect", call->GetReconnectRole());
        send.add_string(send.base, "msg", "r-conn");
        send.Send();
    }
    else if (!strcmp(type, "channels")) {
        SendCallInfo(send, send.base, send.tmp);
        send.add_string(send.base, "msg", "r-channels");
        if (call->sendVideoProposal) {
            if(CoderName(call->videoCoder) == 0) send.add_bool(send.base, "videoRejected", true);
            call->sendVideoProposal = false;
        }
        send.Send();
    }
    else if (!strcmp(type, "facility")) {
        if (call->fty_ct_initiate.num || call->fty_ct_initiate.sip || call->fty_ct_initiate.id) {
            // must replace this SignalingCall from PBX by a new outbound SignalingCall to PBX
            // the SignalingRccCall with the frontend can stay the same, it will only change its direction attibute

            const char* num = call->fty_ct_initiate.num;
            const char* sip = call->fty_ct_initiate.sip;
            const char* dn = call->fty_ct_initiate.dn;

            class SignalingRcc* rcc = session->rcc;
            class SignalingRegistration* registration = rcc->GetRegistration();
            class SignalingCall* newCall = new SignalingCall(registration, CallTypeVoice, 0, num, sip, dn, true);
            newCall->videoAllowed = call->videoAllowed;
            newCall->videoDisabled = call->videoDisabled;
            newCall->ct_setup_id = call->fty_ct_initiate.id;

            // detach replacedCall from this SignalingRccCall
            class SignalingCall* replacedCall = call;
            this->SignalingCallMonitor::remove();

            // attach newCall to this SignalingRccCall
            call = newCall;
            call->monitors.push_back(this);

            replacedCall->replacementCall = newCall;
            newCall->replacedCall = replacedCall;

            class IAudioIo* audioIo = rcc->GetAudioIo();
            class IVideoIo* videoIo = rcc->GetVideoIo();
            if (audioIo) call->StartCall(audioIo, videoIo, 0, user);

            // release replacedCall on any received message on newCall
            replacedCall->fty_ct_initiate.cleanup();
        }
        SendCallInfo(send, send.base, send.tmp);
        send.add_string(send.base, "msg", "r-facility");
        send.Send();
    }
    else if (!strcmp(type, "disc") || !strcmp(type, "rel")) {
        SendCallInfo(send, send.base, send.tmp);
        if (call->cause) {
            word info = send.add_array(send.base, "info");
            info = send.add_object(info, 0);
            send.add_string(info, "type", "cause");
            send.add_int(info, "vali", call->cause, send.tmp);
        }
        send.add_printf(send.base, "msg", send.tmp, "r-%s", type);
        send.Send();
    }
}

void SignalingRccCall::MonitorSync()
{
    debug->printf("%x/%u:SignalingRccCall::MonitorSync()", this, handle);
    class SignalingRCCMessage send("CallInfo", user->src, session, alloca(10000), alloca(4000));

    SendCallInfo(send, send.base, send.tmp);
    word no = JSON_ID_NONE;
    if (call->diverting.num || call->diverting.sip || call->diverting.dn) {
        if (no == JSON_ID_NONE) no = send.add_array(send.base, "no");
        word diverting = send.add_object(no, 0);
        send.add_string(diverting, "type", "leg2");
        send.add_string(diverting, "e164", call->diverting.num);
        send.add_string(diverting, "h323", call->diverting.sip);
        send.add_string(diverting, "dn", call->diverting.dn);
    }
    if (call->originalCalled.num || call->originalCalled.sip || call->originalCalled.dn) {
        if (no == JSON_ID_NONE) no = send.add_array(send.base, "no");
        word originalCalled = send.add_object(no, 0);
        send.add_string(originalCalled, "type", "leg2orig");
        send.add_string(originalCalled, "e164", call->originalCalled.num);
        send.add_string(originalCalled, "h323", call->originalCalled.sip);
        send.add_string(originalCalled, "dn", call->originalCalled.dn);
    }
    if (call->transfering.num || call->transfering.sip || call->transfering.dn) {
        if (no == JSON_ID_NONE) no = send.add_array(send.base, "no");
        word transfering = send.add_object(no, 0);
        send.add_string(transfering, "type", "ct");
        send.add_string(transfering, "e164", call->transfering.num);
        send.add_string(transfering, "h323", call->transfering.sip);
        send.add_string(transfering, "dn", call->transfering.dn);
    }
    send.add_string(send.base, "msg", "sync");
    send.add_int(send.base, "duration", call->duration ? (int)((ITime::TimeStampMilliseconds()/1000) - call->duration) : 0, send.tmp);
    send.Send();
}

void SignalingRccCall::SendCallInfo(class json_io & send, word base, char * & tmp)
{
    send.add_unsigned(base, "user", user->handle, tmp);
    send.add_unsigned(base, "call", handle, tmp);
    send.add_unsigned(base, "state", call->state, tmp);
    send.add_printf(base, "conf", tmp, "%.*H", sizeof(call->conf), call->conf.b);
    send.add_string(base, "conferenceGuid", call->conferenceGuid);
    send.add_string(base, "conferenceType", call->conferenceType);
    if (call->multiVideoSupport) send.add_bool(base, "multiVideoSupport", call->multiVideoSupport);
    word peer = send.add_object(base, "peer");
    if (call->remote.restricted) {
        send.add_bool(peer, "restricted", true);
    }
    else {
        send.add_string(peer, "e164", call->remote.num);
        send.add_string(peer, "h323", call->remote.sip);
        send.add_string(peer, "dn", call->remote.dn);
    }
    word codecs = send.add_object(base, "codecs");
    send.add_string(codecs, "audio", CoderName(call->audioCoder));
    send.add_string(codecs, "video", CoderName(call->videoCoder));
    send.add_string(base, "camera", call->IsCameraOn() ? "on" : "off");
    if (call->muted) send.add_bool(base, "muted", true);
    if (call->conferenceInfo.reason) {
        word conference_info = send.add_object(base, "conference_info");
        call->conferenceInfo.write(send, conference_info, tmp);
    }
    if (call->chatMessage.data) {
        word im_message = send.add_object(base, "im_message");
        call->chatMessage.write(send, im_message, tmp);
    }
    if (call->remoteVideoPriority) {
        send.add_bool(base, "videoPriority", true);
    }
}

/*-----------------------------------------------------------------------------------------------*/
/* SignalingRTPTP                                                                                */
/*-----------------------------------------------------------------------------------------------*/

SignalingRTPTP::SignalingRTPTP(class Signaling * signaling) : SignalingRegistrationMonitor(CallTypeData), retry(signaling->service->iomux, this)
{
    debug->printf("%x:SignalingRTPTP()", this);
    this->signaling = signaling;
    user = 0;
    sip = 0;
    timeout = TIMEOUT_MIN;
    up = false;
    call = 0;
}

SignalingRTPTP::~SignalingRTPTP()
{

}

class SignalingRTPTP * SignalingRTPTP::Update(class URTPTP * user, const char * sip, unsigned timeout)
{
    debug->printf("%x:SignalingRTPTP::Update(sip=%s) timeout=%d", this, sip, timeout);
    this->user = user;
    if (sip) {
        if (timeout) this->timeout = timeout;
        if (!this->sip) {
            this->sip = _strdup(sip);
            signaling->AddRegistrationMonitor(this);
        }
        else if (strcmp(this->sip, sip)) {
            if (call) {
                call->call->ClearCall();
                call = 0;
            }
            free(this->sip);
            this->sip = _strdup(sip);
            signaling->RemoveRegistrationMonitor(this);
            signaling->AddRegistrationMonitor(this);
        }
    }
    else {
        free(this->sip);
        this->sip = 0;
        call->call->ClearCall();
        call = 0;
        retry.Cancel();
        signaling->RemoveRegistrationMonitor(this);
        if (!user) {
            delete this;
            return 0;
        }
    }
    return this;
}

void SignalingRTPTP::Sync(class json_io & msg, word base)
{
    debug->printf("%x:SignalingRTPTP::Sync()", this);
}

void SignalingRTPTP::RegistrationState(bool up)
{
    debug->printf("%x:SignalingRTPTP::RegistrationState(up=%x)", this, up);
    if (up) {
        class SignalingCall * c = new SignalingCall(signaling->registration, CallTypeData, 0, 0, sip, 0, true);
        call = new SignalingRTPTPCall(this, c);
        c->StartCall(0, 0, user, this);
    }
}

void SignalingRTPTP::State(bool up)
{
    signaling->RTPTPState(this, up);
    if (user) user->RTPTPState(up);
}

void SignalingRTPTP::TimerOnTimeout(ITimer * timer)
{

}

void SignalingRTPTP::Send(void * buf, size_t len)
{
}

/*-----------------------------------------------------------------------------------------------*/

SignalingRTPTPCall::SignalingRTPTPCall(class SignalingRTPTP * rtptp, class SignalingCall * call) : SignalingCallMonitor(call)
{
    debug->printf("%x:SignalingRTPTPCall()", this);
    this->rtptp = rtptp;
}

SignalingRTPTPCall::~SignalingRTPTPCall()
{
    debug->printf("%x:~SignalingRTPTPCall()", this);
}

void SignalingRTPTPCall::MonitorXmit(const char * type, class json_io & msg, word base)
{
    debug->printf("%x:SignalingRTPTPCall::MonitorXmit(%s)", this, type);
    if (!strcmp(type, "rel")) {
        rtptp->State(false);
    }
}

void SignalingRTPTPCall::MonitorRecv(const char * type, class json_io & msg, word base)
{
    debug->printf("%x:SignalingRTPTPCall::MonitorRecv(%s)", this, type);
    if (!strcmp(type, "conn")) {
        rtptp->State(true);
    }
    else if (!strcmp(type, "rel")) {
        rtptp->State(false);
    }
}

void SignalingRTPTPCall::MonitorSync()
{
    debug->printf("%x:SignalingRTPTPCall::MonitorSync()", this);

}

/*-----------------------------------------------------------------------------------------------*/

void ConferenceInfo::zeroize()
{
    reason = 0;
    memset(&participant, 0, sizeof(participant));
}

void ConferenceInfo::cleanup()
{
    if (reason) free(reason);
    if (participant.name) free(participant.name);
    if (participant.inRoomState) free(participant.inRoomState);
    if (participant.activeSpeaker) free(participant.activeSpeaker);
    if (participant.muted) free(participant.muted);
    if (participant.mutedByMod) free(participant.mutedByMod);
    if (participant.mutedByUser) free(participant.mutedByUser);
    if (participant.videoOffByMod) free(participant.videoOffByMod);
    if (participant.videoOffByUser) free(participant.videoOffByUser);
    if (participant.appSharing) free(participant.appSharing);
    if (participant.transcribing) free(participant.transcribing);
    if (participant.hand) free(participant.hand);
    zeroize();
}

void ConferenceInfo::read(class json_io& msg, word base)
{
    cleanup();

    this->reason = _strdup(msg.get_string(base, "reason"));
    word participant = msg.get_object(base, "participant");
    if (participant != 0xffff) {
        this->participant.id = msg.get_unsigned(participant, "id");
        this->participant.name = _strdup(msg.get_string(participant, "name"));
        this->participant.inRoomState = _strdup(msg.get_string(participant, "inRoomState"));

        // get boolean values as strings ("true" or "false" or null)
        this->participant.activeSpeaker = _strdup(msg.get_value(participant, 0, "activeSpeaker"));
        this->participant.muted = _strdup(msg.get_value(participant, 0, "muted"));
        this->participant.mutedByMod = _strdup(msg.get_value(participant, 0, "mutedByMod"));
        this->participant.mutedByUser = _strdup(msg.get_value(participant, 0, "mutedByUser"));
        this->participant.videoOffByMod = _strdup(msg.get_value(participant, 0, "videoOffByMod"));
        this->participant.videoOffByUser = _strdup(msg.get_value(participant, 0, "videoOffByUser"));
        this->participant.appSharing = _strdup(msg.get_value(participant, 0, "appSharing"));
        this->participant.transcribing = _strdup(msg.get_value(participant, 0, "transcribing"));
        this->participant.hand = _strdup(msg.get_value(participant, 0, "hand"));
        this->participant.handOnTs = _strdup(msg.get_value(participant, 0, "handOnTs"));
        this->participant.handOffTs = _strdup(msg.get_value(participant, 0, "handOffTs"));

        word remote = msg.get_object(participant, "remote");
        if (remote != 0xffff) {
            this->participant.remote.flags = _strdup(msg.get_string(remote, "flags"));
            this->participant.remote.num = _strdup(msg.get_string(remote, "num"));
            this->participant.remote.sip = _strdup(msg.get_string(remote, "sip"));
        }

        word videoChannels = msg.get_array(participant, "videoChannels");
        if (videoChannels != 0xffff) {
            word ch = msg.get_object(videoChannels, 0);
            word max = sizeof(this->participant.videoChannels) / sizeof(this->participant.videoChannels[0]);
            for (word idx = 0; idx < max; idx++) {
                if (ch != 0xffff) {
                    this->participant.videoChannels[idx].id = msg.get_unsigned(ch, "id");
                    this->participant.videoChannels[idx].bitrate = msg.get_unsigned(ch, "bitrate");
                    ch = msg.get_object(videoChannels, ch);
                }
            }
        }
    }
}

void ConferenceInfo::write(class json_io& msg, word base, char*& tmp)
{
    msg.add_string(base, "reason", this->reason);
    word participant = msg.add_object(base, "participant");
    msg.add_unsigned(participant, "id", this->participant.id, tmp);
    msg.add_string(participant, "name", this->participant.name);
    msg.add_string(participant, "inRoomState", this->participant.inRoomState);

    // write boolean values as unquoted strings (true or false)
    msg.add(JSON_TYPE_VALUE, 0, participant, "activeSpeaker", this->participant.activeSpeaker);
    msg.add(JSON_TYPE_VALUE, 0, participant, "muted", this->participant.muted);
    msg.add(JSON_TYPE_VALUE, 0, participant, "mutedByMod", this->participant.mutedByMod);
    msg.add(JSON_TYPE_VALUE, 0, participant, "mutedByUser", this->participant.mutedByUser);
    msg.add(JSON_TYPE_VALUE, 0, participant, "videoOffByMod", this->participant.videoOffByMod);
    msg.add(JSON_TYPE_VALUE, 0, participant, "videoOffByUser", this->participant.videoOffByUser);
    msg.add(JSON_TYPE_VALUE, 0, participant, "appSharing", this->participant.appSharing);
    msg.add(JSON_TYPE_VALUE, 0, participant, "transcribing", this->participant.transcribing);
    msg.add(JSON_TYPE_VALUE, 0, participant, "hand", this->participant.hand);
    msg.add(JSON_TYPE_VALUE, 0, participant, "handOnTs", this->participant.handOnTs);
    msg.add(JSON_TYPE_VALUE, 0, participant, "handOffTs", this->participant.handOffTs);

    if (this->participant.remote.num || this->participant.remote.sip) {
        word remote = msg.add_object(participant, "remote");
        msg.add_string(remote, "flags", this->participant.remote.flags);
        msg.add_string(remote, "num", this->participant.remote.num);
        msg.add_string(remote, "sip", this->participant.remote.sip);
    }

    if (this->participant.videoChannels[0].id) {
        word videoChannels = msg.add_array(participant, "videoChannels");
        word max = sizeof(this->participant.videoChannels) / sizeof(this->participant.videoChannels[0]);
        for (word idx = 0; idx < max; idx++) {
            if (this->participant.videoChannels[idx].id) {
                word ch = msg.add_object(videoChannels, 0);
                msg.add_unsigned(ch, "id", this->participant.videoChannels[idx].id, tmp);
                msg.add_unsigned(ch, "bitrate", this->participant.videoChannels[idx].bitrate, tmp);
            }
        }
    }
}

/*-----------------------------------------------------------------------------------------------*/

void ChatMessage::zeroize()
{
    type = sender = sender_dn = data = guid = mime = 0;
    time = 0;
}

void ChatMessage::cleanup()
{
    if (type) free(type);
    if (sender) free(sender);
    if (sender_dn) free(sender_dn);
    if (data) free(data);
    if (guid) free(guid);
    if (mime) free(mime);
    zeroize();
}

void ChatMessage::read(class json_io& msg, word base)
{
    /*
    {
        "type": "im_message",
        "sender": "endeavour",
        "sender_dn": "Endeavour",
        "data": "abcdefg",
        "guid": "8256ff9f332f620133c30090334113d8",
        "time": 1647260575,
        "mime": "text/html"
    }
    */
    cleanup();
    type = _strdup(msg.get_string(base, "type"));
    sender = _strdup(msg.get_string(base, "sender"));
    sender_dn = _strdup(msg.get_string(base, "sender_dn"));
    data = _strdup(msg.get_string(base, "data"));
    guid = _strdup(msg.get_string(base, "guid"));
    time = msg.get_unsigned(base, "time");
    mime = _strdup(msg.get_string(base, "mime"));
}

void ChatMessage::write(class json_io& msg, word base, char*& tmp)
{
    msg.add_string(base, "type", type);
    msg.add_string(base, "sender", sender);
    msg.add_string(base, "sender_dn", sender_dn);
    msg.add_string(base, "data", data);
    msg.add_string(base, "guid", guid);
    msg.add_unsigned(base, "time", time, tmp);
    msg.add_string(base, "mime", mime);
}

/*---------------------------------------------------------------------------*/
/*                          SignalingRccSessionHttpClient                    */
/*---------------------------------------------------------------------------*/

SignalingRccSessionHttpClient::SignalingRccSessionHttpClient(class Signaling * signaling, class SignalingRccSession * rccSession, const char * url) :
    STaskContext(signaling->GetIoMux())
{
    debug->printf("SignalingRccSessionHttpClient::SignalingRccSessionHttpClient");
    this->signaling = signaling;
    this->rccSession = rccSession;
    this->recvBuffer = nullptr;
    this->httpOk = false;
    this->fileOk = false;
    this->closing = false;
    this->httpClient = nullptr;
    this->url = _strdup(url);
    this->fWrite = new TaskFileWrite(this, this->signaling->GetWorkingPath(), "webaccess.entrance.mp3", false);
    this->fWrite->Start(this);
}

SignalingRccSessionHttpClient::~SignalingRccSessionHttpClient()
{
    ASSERT(this->httpClient == nullptr, "SignalingRccSessionHttpClient::~SignalingRccSessionHttpClient httpClient!=nullptr");
    ASSERT(this->fWrite == nullptr, "SignalingRccSessionHttpClient::~SignalingRccSessionHttpClient fWrite!=nullptr");

    free(this->recvBuffer);
    free(this->url);
}

void SignalingRccSessionHttpClient::HTTPClientConnectComplete(IHTTPClient* const httpClient)
{
    debug->printf("SignalingRccSessionHttpClient::HTTPClientConnectComplete");

    uri_dissector uri;
    size_t len = strlen(this->url);
    char* resourceName = (char*)alloca(len);
    *resourceName = '\0';
    if (uri.dissect_uri((char*)this->url)) {
        _snprintf(resourceName, len, "%.*s%.*s%s%.*s", (dword)(uri.e_path - uri.path), uri.path,
            (dword)(uri.e_file - uri.file), uri.file,
            uri.query ? "?" : "",
            (dword)(uri.e_query - uri.query), uri.query);
    }
    if (!*resourceName) {
        this->TryClose();
        return;
    }

    debug->printf("SignalingRccSessionHttpClient::HTTPClientConnectComplete resourceName:%s", resourceName);

    this->recvBuffer = (byte*)malloc(sizeof(byte) * 65536);
    httpClient->SetRequestType(HTTP_GET, resourceName);
    httpClient->Send();
}

void SignalingRccSessionHttpClient::HTTPClientShutdown(IHTTPClient* const httpClient, http_shutdown_reason_t reason)
{
    debug->printf("SignalingRccSessionHttpClient::HTTPClientShutdown reason:%d", reason);

    delete httpClient;
    this->httpClient = nullptr;
    TryClose();
}

void SignalingRccSessionHttpClient::HTTPClientSendResult(IHTTPClient* const httpClient)
{

}

void SignalingRccSessionHttpClient::HTTPClientRecvResult(IHTTPClient* const httpClient, byte* buffer, size_t len, bool transferComplete)
{
    debug->printf("SignalingRccSessionHttpClient::HTTPClientRecvResult len:%d transferComplete:%d", len, transferComplete);

    fWrite->write(buffer, len, transferComplete);
    if (transferComplete) {
        httpOk = true;
    }
}

void SignalingRccSessionHttpClient::HTTPClientRecvCanceled(IHTTPClient* const httpClient, byte* buffer)
{
    debug->printf("SignalingRccSessionHttpClient::HTTPClientRecvCanceled");

    // freed in destructor
}

void SignalingRccSessionHttpClient::HTTPClientRequestComplete(IHTTPClient* const httpClient)
{
    debug->printf("SignalingRccSessionHttpClient::HTTPClientRequestComplete");

    httpClient->Recv(this->recvBuffer, sizeof(byte) * 65536);
}

void SignalingRccSessionHttpClient::TaskComplete(class ITask* const task)
{
    debug->printf("SignalingRccSessionHttpClient::TaskComplete");

    fileOk = true;
    delete task;
    this->fWrite = nullptr;
    TryClose();
}

void SignalingRccSessionHttpClient::TaskFailed(class ITask* const task)
{
    debug->printf("SignalingRccSessionHttpClient::TaskFailed");

    delete task;
    this->fWrite = nullptr;
    TryClose();
}

void SignalingRccSessionHttpClient::TaskProgress(class ITask* const task, dword progress)
{
    debug->printf("SignalingRccSessionHttpClient::TaskProgress closing:%d this->httpClient:%p", closing, this->httpClient);

    if (this->closing) {
        TryClose();
    }
    else {
        if (this->httpClient == nullptr) {  // there is a first TaskProgress, so we establish the HTTP client connection now
            this->httpClient = IHTTPClient::Create(this->signaling->GetIoMux(), this->signaling->GetTcpSocketProvider(), this->signaling->GetTlsSocketProvider(), this, this->signaling);
            this->httpClient->Connect(this->url);
        }
        else {
            this->httpClient->Recv(this->recvBuffer, sizeof(byte) * 65536);
        }
    }
}

void SignalingRccSessionHttpClient::TryClose()
{
    debug->printf("SignalingRccSessionHttpClient::TryClose closing:%d httpClient:%p fWrite:%p", closing, httpClient, fWrite);

    if (!this->closing) {
        this->closing = true;
        if (this->httpClient) {
            this->httpClient->Shutdown();
        }
        if (this->fWrite) { // fWrite doesn't support Stop :(
            this->fWrite->write(nullptr, 0, true);
            //this->fWrite->Stop();
        }
    }
    if (this->httpClient) {
        return;
    }
    if (this->fWrite) {
        return;
    }
    this->rccSession->SignalingRccSessionHttpClientClosed(this);
}

void SignalingRccSessionHttpClient::Close()
{
    this->TryClose();
}

bool SignalingRccSessionHttpClient::Success()
{
    return this->httpOk && this->fileOk;
}

/*-----------------------------------------------------------------------------------------------*/

static const char* CoderName(int type)
{
    switch (type) {
        // audio codecs
    case G711_A:        return "G711A";
    case G711_U:        return "G711u";
    case G723_53:       return "G723-53";
    case G723_63:       return "G723-63";
    case G729:          return "G729";
    case G729A:         return "G729A";
    case G729B:         return "G729B";
    case G729AB:        return "G729AB";
    case G726_40:       return "G726-40";
    case G726_32:       return "G726-32";
    case G726_24:       return "G726-24";
    case G726_16:       return "G726-16";
    case GSM:           return "GSM";
    case DVI4:          return "DVI4";
    case LPC:           return "LPC";
    case L16:           return "L16";
    case ILBC:          return "ILBC";
    case SPEEX:         return "SPEEX";
    case DTMF:          return "DTMF";
    case CN:            return "CN";
    case RED:           return "RED";
    case G722:          return "G722";
    case G7221:         return "G7221";
    case CLEARCHANNEL:  return "XPARENT";
    case OPUS_NB:       return "OPUS-NB";
    case OPUS_WB:       return "OPUS-WB";
    case AMR_WB:        return "AMR-WB";
        // video codecs
    case H264:          return "H264";
    case VP8:           return "VP8";
    case VP9:           return "VP9";
    case AV1:           return "AV1";
    case JRFB:          return "JRFB";
    }
    return 0;
}

static int CoderPt(int type)
{
    switch (type) {
        // audio codecs
    case G711_A:        return DEFAULT_PAYLOAD_TYPE_G711_A;
    case G711_U:        return DEFAULT_PAYLOAD_TYPE_G711_U;
    case G722:          return DEFAULT_PAYLOAD_TYPE_G722;
    case G729:          return DEFAULT_PAYLOAD_TYPE_G729;
    case G729A:         return DEFAULT_PAYLOAD_TYPE_G729;
    case G729B:         return DEFAULT_PAYLOAD_TYPE_G729;
    case G729AB:        return DEFAULT_PAYLOAD_TYPE_G729;
    case OPUS_NB:       return DEFAULT_PAYLOAD_TYPE_OPUS;
    case OPUS_WB:       return DEFAULT_PAYLOAD_TYPE_OPUS;
    case DTMF:          return DEFAULT_PAYLOAD_TYPE_DTMF;
    case CLEARCHANNEL:  return 97;
        // video codecs
    case H264:          return DEFAULT_PAYLOAD_TYPE_H264;
    case VP8:           return DEFAULT_PAYLOAD_TYPE_VP8;
    case VP9:           return DEFAULT_PAYLOAD_TYPE_VP9;
    case AV1:           return DEFAULT_PAYLOAD_TYPE_AV1;
    }
    return 0;
}

static int CoderType(const char * name)
{
    if (name) {
        // audio codecs
        if (!str::casecmp(name, "G711A")) return G711_A;
        if (!str::casecmp(name, "G711u")) return G711_U;
        if (!str::casecmp(name, "G722")) return G722;
        if (!str::casecmp(name, "G729")) return G729;
        if (!str::casecmp(name, "G729A")) return G729A;
        if (!str::casecmp(name, "G729B")) return G729B;
        if (!str::casecmp(name, "G729AB")) return G729AB;
        if (!str::casecmp(name, "OPUS-NB")) return OPUS_NB;
        if (!str::casecmp(name, "OPUS-WB")) return OPUS_WB;
        if (!str::casecmp(name, "XPARENT")) return CLEARCHANNEL;
        // video codecs
        if (!str::casecmp(name, "H264")) return H264;
        if (!str::casecmp(name, "VP8")) return VP8;
        if (!str::casecmp(name, "VP9")) return VP9;
        if (!str::casecmp(name, "AV1")) return AV1;
    }
    return CODER_UNDEFINED;
}

static const char * candidate_name[] = { "HOST", "SRFLX", "PRFLX", "RELAY" };

static const char * CandidateName(int type)
{
    return type <= 3 ? candidate_name[type] : 0;
}

static int CandidateType(const char * name)
{
    int type = 0;
    if (name) for (type = 0; type <= 3 && strcmp(name, candidate_name[type]); type++);
    return type <= 3 ? type : 0;
}

static const char * channels_cmd[] = { 0, "SELECT", "PROPOSAL", "REQUEST", "CLOSE", "INFO" };
static const char * ChannelsCmdName(unsigned type)
{
    return type && type <= 5 ? channels_cmd[type] : 0;
}

static int ChannelsCmdType(const char * name)
{
    int type = 0;
    if (name) for (type = 1; type <= 5 && strcmp(name, channels_cmd[type]); type++);
    return type <= 5 ? type : 0;
}
