/*---------------------------------------------------------------------------*/
/* appclient_media.cpp                                                       */
/* copyright (c) innovaphone 2017                                            */
/*                                                                           */
/*---------------------------------------------------------------------------*/

#include "platform/platform.h"
#include "common/os/iomux.h"

#include "common/interface/time.h"
#include "common/interface/socket.h"
#include "common/interface/dns.h"
#include "common/interface/guuid.h"
#include "common/interface/random.h"
#include "common/interface/task.h"
#include "common/interface/stask.h"
#include "common/interface/audio.h"
#include "common/interface/media.h"
#include "common/interface/media_tones.h"
#include "common/interface/dtls.h"
#include "common/interface/certificates.h"
#include "common/interface/webserver_plugin.h"
#include "common/interface/json_api.h"
#include "common/interface/url_handler.h"
#include "common/interface/notifications-if.h"
#include "common/interface/launcherapi.h"
#include "common/interface/call_kit.h"
#include "common/interface/local_storage.h"
#include "common/ilib/json.h"
#include "common/ilib/http_query_args.h"
#include "common/lib/appservice.h"
#include "common/lib/appwebsocket.h"

#include "common/service/openssl_shared/ssl.h"
#include "common/service/dtls/dtls.h"
#include "common/service/media/ice.h"
#include "common/service/media/video_endpoint.h"
#include "common/service/media/appsharing_endpoint.h"

#include "common/lib/language.h"
#include "client/lang/myapps_languages.h"

#include "appclient_media.h"

#if TARGET_OS_IPHONE
extern volatile bool connectivity_lan;
#define MediaRtpOverTcp(connType) (!connectivity_lan && (((connType) == ConnectionType::LOCAL_VIDEO_STREAM) || ((connType) == ConnectionType::REMOTE_VIDEO_STREAM) || ((connType) == ConnectionType::LOCAL_SHARING_STREAM) || ((connType) == ConnectionType::LOCAL_WEBCAM_STREAM)))
#else
#define MediaRtpOverTcp(connType) false
#endif

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

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

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

static enum AppSharingCoder AvailableAppSharingCoders[] =
{
    AppSharingCoder::JRFB,
    AppSharingCoder::JRFB2,
    AppSharingCoder::VIDEO_CODER,
};

#define AVAILABLE_APPSHARING_CODERS sizeof(AvailableAppSharingCoders)/sizeof(AvailableAppSharingCoders[0])

static MediaIdNameItem IdNameDeviceType[] =
{
    { AudioDeviceType::DeviceTypeUnknown,   "Unknown" },
    { AudioDeviceType::HandsetDevice,       "Handset" },
    { AudioDeviceType::HeadsetDevice,       "Headset" },
    { AudioDeviceType::HandsfreeDevice,     "Handsfree" },
    { VideoDeviceType::Webcam,              "Webcam" },
    { VideoDeviceType::VideoScreen,         "VideoScreen" },
    { AppSharingDeviceType::FrameBuffer,    "FrameBuffer" },
    { AppSharingDeviceType::Canvas,         "Canvas" },
    { 0,                                    NULL },
};

static MediaIdNameItem IdSetupRole[] =
{
    { SetupRole::SETUP_ROLE_NONE,     "none" },
    { SetupRole::SETUP_ROLE_ACTIVE,   "active" },
    { SetupRole::SETUP_ROLE_PASSIVE,  "passive" },
    { SetupRole::SETUP_ROLE_ACTPASS,  "actpass" },
    { SetupRole::SETUP_ROLE_HOLDCONN, "holdconn" },
    { 0,                              NULL },
};

static MediaIdNameItem IdNameDeviceMode[] =
{
    { AudioDeviceMode::AudioModeRecord,        "Record" },
    { AudioDeviceMode::AudioModePlayback,      "Playback" },
    { AudioDeviceMode::AudioModeCommunication, "Communication" },
    { 0,                                       NULL },
};

static MediaIdNameItem OldIdNameCoder[] =
{
    { AudioCoder::G711_A,       "G711_A" },
    { AudioCoder::G711_U,       "G711_u" },
    { AudioCoder::G723_53,      "G723_53" },
    { AudioCoder::G723_63,      "G723_63" },
    { AudioCoder::G729,         "G729" },
    { AudioCoder::G729A,        "G729A" },
    { AudioCoder::G729B,        "G729B" },
    { AudioCoder::G729AB,       "G729AB" },
    { AudioCoder::G726_40,      "G726_40" },
    { AudioCoder::G726_32,      "G726_32" },
    { AudioCoder::G726_24,      "G726_24" },
    { AudioCoder::G726_16,      "G726_16" },
    { AudioCoder::GSM,          "GSM" },
    { AudioCoder::DVI4,         "DIV4" },
    { AudioCoder::LPC,          "LPC" },
    { AudioCoder::L16,          "L16" },
    { AudioCoder::ILBC,         "ILBC" },
    { AudioCoder::SPEEX,        "SPEEX" },
    { AudioCoder::DTMF,         "DTMF" },
    { AudioCoder::CN,           "CN" },
    { AudioCoder::RED,          "RED" },
    { AudioCoder::G722,         "G722" },
    { AudioCoder::G7221,        "G7221" },
    { AudioCoder::CLEARCHANNEL, "XPARENT" },
    { AudioCoder::OPUS_NB,      "OPUS_NB" },
    { AudioCoder::OPUS_WB,      "OPUS_WB" },
    { AudioCoder::AMR_WB,       "AMR_WB" },
    { VideoCoder::H264,         "H264" },
    { VideoCoder::VP8,          "VP8" },
    { VideoCoder::VP9,          "VP9" },
    { VideoCoder::AV1,          "AV1" },
    { AppSharingCoder::JRFB,    "JRFB" },
    { AppSharingCoder::JRFB2,   "JRFB2" },
    { AppSharingCoder::VIDEO_CODER,    "VIDEO_CODER" },
    { 0,                        NULL }
};

static MediaIdNameItem IdNameCoder[] =
{
    { AudioCoder::G711_A,       "G711A" },
    { AudioCoder::G711_U,       "G711u" },
    { AudioCoder::G723_53,      "G723-53" },
    { AudioCoder::G723_63,      "G723-63" },
    { AudioCoder::G729,         "G729" },
    { AudioCoder::G729A,        "G729A" },
    { AudioCoder::G729B,        "G729B" },
    { AudioCoder::G729AB,       "G729AB" },
    { AudioCoder::G726_40,      "G726-40" },
    { AudioCoder::G726_32,      "G726-32" },
    { AudioCoder::G726_24,      "G726-24" },
    { AudioCoder::G726_16,      "G726-16" },
    { AudioCoder::GSM,          "GSM" },
    { AudioCoder::DVI4,         "DIV4" },
    { AudioCoder::LPC,          "LPC" },
    { AudioCoder::L16,          "L16" },
    { AudioCoder::ILBC,         "ILBC" },
    { AudioCoder::SPEEX,        "SPEEX" },
    { AudioCoder::DTMF,         "DTMF" },
    { AudioCoder::CN,           "CN" },
    { AudioCoder::RED,          "RED" },
    { AudioCoder::G722,         "G722" },
    { AudioCoder::G7221,        "G7221" },
    { AudioCoder::CLEARCHANNEL, "XPARENT" },
    { AudioCoder::OPUS_NB,      "OPUS-NB" },
    { AudioCoder::OPUS_WB,      "OPUS-WB" },
    { AudioCoder::AMR_WB,       "AMR-WB" },
    { VideoCoder::H264,         "H264" },
    { VideoCoder::VP8,          "VP8" },
    { VideoCoder::VP9,          "VP9" },
    { VideoCoder::AV1,          "AV1" },
    { AppSharingCoder::JRFB,    "JRFB" },
    { 0,                        NULL }
};

static MediaIdNameItem IdNameIceCandidateType[] =
{
    { CHANNEL_CANDIDATE_HOST,              "HOST" },
    { CHANNEL_CANDIDATE_SRFLX,             "SRFLX" },
    { CHANNEL_CANDIDATE_RELAY,             "RELAY" },
    { CHANNEL_CANDIDATE_HOST_TCP_PASSIVE,  "HOST-TCP-PASSIVE" },
    { CHANNEL_CANDIDATE_HOST_TCP_ACTIVE,   "HOST-TCP-ACTIVE" },
    { CHANNEL_CANDIDATE_HOST_TCP_SO,       "HOST-TCP-SO" },
    { CHANNEL_CANDIDATE_SRFLX_TCP_PASSIVE, "SRFLX-TCP-PASSIVE" },
    { CHANNEL_CANDIDATE_SRFLX_TCP_ACTIVE,  "SRFLX-TCP-ACTIVE" },
    { CHANNEL_CANDIDATE_SRFLX_TCP_SO,      "SRFLX-TCP-SO" },
    { CHANNEL_CANDIDATE_RELAY_TCP_PASSIVE, "RELAY-TCP-PASSIVE" },
    { CHANNEL_CANDIDATE_RELAY_TCP_ACTIVE,  "RELAY-TCP-ACTIVE" },
    { CHANNEL_CANDIDATE_RELAY_TCP_SO,      "RELAY-TCP-SO" },
    { -1,                                  NULL },
};

static MediaIdNameItem IdNameIceType[] =
{
    { MediaType::AUDIO,         "audio_ice" },
    { MediaType::VIDEO,         "video_ice" },
    { MediaType::APPSHARING,    "collab_ice" },
    { 0,                        NULL },
};

static MediaIdNameItem IdNameMediaType[] =
{
    { MediaType::AUDIO,          "Audio" },
    { MediaType::VIDEO,          "Video" },
    { MediaType::VIDEO_APPSHARING, "VideoAppSharing" },
    { MediaType::APPSHARING,     "AppSharing" },
    { MediaType::MP4_CONTAINER,  "MP4Container" },
    { MediaType::WEBM_VP8_CONTAINER, "WebMVP8Container" },
    { MediaType::WEBM_VP9_CONTAINER, "WebMVP9Container" },
    { MediaType::APPSHARING_CONTAINER, "AppSharingContainer" },
    { 0,                         NULL },
};

static MediaIdNameItem IdNameHookKey[] =
{
    { KEY_EHS_TALK,              "EHS_TALK" },
    { KEY_EHS_FLASH,             "EHS_FLASH" },
    { KEY_EHS_REDIAL,            "EHS_REDIAL" },
    { KEY_OFFHOOK,               "OFFHOOK" },
    { KEY_ONHOOK,                "ONHOOK" },
    { KEY_DISC,                  "DISC" },
    { KEY_INCALL,                "INCALL" },
    { KEY_MIC,                   "MUTE" },
    { KEY_HOLD,                  "HOLD" },
    { KEY_RETRIEVE,              "RETRIEVE" },
    { 0,                         NULL },
};

AppClientMedia::AppClientMedia(IIoMux * const iomux, IWebserverPluginProvider * webserverPluginProvider, ISocketProvider * localSocketProvider,
                               ISocketProvider * socketUdpProvider, ISocketProvider * socketTcpProvider, ISocketProvider* socketTlsProvider, AppInstanceArgs * args, class MediaConfiguration * mediaConfiguration)
{
    iomux->RegisterShutdownHandler(this);
    this->logFlags = args->logFlags;
    stopping = false;
    stoppingAudio = false;
    stoppingVideo = false;
    stoppingAppSharing = false;
    pwd = _strdup(args->appPassword);
    this->iomux = iomux;
    this->appName = _strdup(args->appName);
    this->appDomain = _strdup(args->appDomain);
    if (args->webserver) {
        this->webserverPlugin = webserverPluginProvider->CreateWebserverPlugin(iomux, localSocketProvider, this, args->webserver, args->webserverPath, this);
        this->webserverPlugin->WebsocketListen();
    }
    else this->webserverPlugin = NULL;
    this->mediaProvider = CreateMediaProvider();
    this->socketUdpProvider = socketUdpProvider;
    this->socketTcpProvider = socketTcpProvider;
    this->socketTlsProvider = socketTlsProvider;
    this->context = ISSLProvider::SharedInstance()->CreateSSLSocketContext(this);
    this->context->EnableDTLS(false);
    this->contextSrtp = ISSLProvider::SharedInstance()->CreateSSLSocketContext(this);
    this->contextSrtp->EnableDTLS(true);
    byte * cert;
    size_t certLen;
    if(mediaConfiguration->cert) {
        cert = (byte *)(malloc(mediaConfiguration->certLen));
        memcpy(cert, mediaConfiguration->cert, mediaConfiguration->certLen);
        certLen = mediaConfiguration->certLen;
    }
    else {
        cert = ISSLProvider::SharedInstance()->CreateCertificate(this, &certLen);
    }
    if(cert && certLen) {
        this->context->SetServerCertificate(cert, certLen);
        this->contextSrtp->SetServerCertificate(cert, certLen);
        this->context->SetClientCertificate(cert, certLen);
        this->contextSrtp->SetClientCertificate(cert, certLen);
        ISSLProvider::SharedInstance()->ValidateCertificate(this, cert, certLen);
    }
    if(cert) free(cert);
    this->mediaConfiguration = mediaConfiguration;

#if !defined(__NO_AUDIO_IO__)
    if (mediaConfiguration->isRunningTS) audioPhone = NULL;
    else audioPhone = IAudioPhone::Create(iomux, this, mediaConfiguration->execInterval, mediaConfiguration);
    audioIo = IAudioIo::Create(iomux, audioPhone, this, mediaConfiguration->execInterval, mediaConfiguration);
#else
    audioPhone = NULL;
    audioIo = NULL;
#endif
#if !defined(__NO_VIDEO_IO__)
    videoIo = IVideoIo::Create(iomux, this, mediaConfiguration);
#else
    videoIo = NULL;
#endif
    appSharingIo = IAppSharingIo::Create(iomux, this, mediaConfiguration);
    audioIoInitialized = false;
    videoIoInitialized = false;
    appSharingIoInitialized = false;
    this->Log(LOG_APP, "AppClientMedia(%p)::AppClientMedia started flags=%d", this, this->logFlags);
}

AppClientMedia::~AppClientMedia()
{
    this->Log(LOG_APP, "AppClientMedia(%p)::~AppClientMedia %p,%p,%p,%p,%p", this, audioPhone, audioIo, videoIo, appSharingIo, webserverPlugin);

    if(audioPhone) delete audioPhone;
    if(audioIo) delete audioIo;
    if(videoIo) delete videoIo;
    if(appSharingIo) delete appSharingIo;
    if(webserverPlugin) delete webserverPlugin;
    if(mediaProvider) delete mediaProvider;
    if(context) delete context;
    if(contextSrtp) delete contextSrtp;
    if(appName) free(appName);
    if(appDomain) free(appDomain);
    if(pwd) free(pwd);
}

void AppClientMedia::TryStop()
{
    if (!stopping) {
        this->Log(LOG_APP, "AppClientMedia(%p)::Stop() stopping", this);
        stopping = true;
        if (webserverPlugin) webserverPlugin->Close();
    }
    bool appSharingSession = false;
    bool videoSession = false;
    bool audioSession = false;
    if(!sessions.empty()) {
        AppClientMediaSession *s;
        for(s = sessions.front(); s; s = s->goNext()) {
            if(s->GetMediaType() == MediaType::APPSHARING || s->GetMediaType() == MediaType::APPSHARING_CONTAINER) {
                appSharingSession = true;
                this->Log(LOG_APP, "AppClientMedia(%p)::Stop() APPSHARING s=%p(%d)", this, s, s->GetMediaType());
            }
            else if((s->GetMediaType() == MediaType::VIDEO) || (s->GetMediaType() == MediaType::VIDEO_APPSHARING)) {
                videoSession = true;
                this->Log(LOG_APP, "AppClientMedia(%p)::Stop() VIDEO s=%p(%d)", this, s, s->GetMediaType());
            }
            else if(s->GetMediaType() == MediaType::AUDIO) {
                audioSession = true;
                this->Log(LOG_APP, "AppClientMedia(%p)::Stop() AUDIO s=%p", this, s);
            }
        }
    }

    this->Log(LOG_APP, "AppClientMedia(%p)::Stop() sessions appS=%d,v=%d,a=%d (%d,%d,%d) (%p,%p,%p)", this, appSharingSession, videoSession, audioSession, stoppingAppSharing, stoppingVideo, stoppingAudio, appSharingIo, videoIo, audioIo);

    if(appSharingSession || videoSession || audioSession) return;

    if(!stoppingAppSharing && appSharingIo && !appSharingSession) {
        stoppingAppSharing = true;
        if (!appSharingIoInitialized) appSharingIo->Initialize(this, this);
        appSharingIo->Close();
    }
    if(!stoppingVideo && videoIo && !videoSession) {
        stoppingVideo = true;
        if(!videoIoInitialized) videoIo->Initialize(this, this, this);
        videoIo->Close();
    }
    if(!stoppingAudio && audioIo && !audioSession) {
        stoppingAudio = true;
        // audioIo needs to close AudioPhone
        audioIo->Close(this);
    } 
}

void AppClientMedia::Shutdown()
{
    this->Log(LOG_APP, "AppClientMedia(%p)::Shutdown %d sessions=%d", this, stopping, !sessions.empty());

    TryStop();
    if (!webserverPlugin && !audioIo && !videoIo && !appSharingIo) {
        this->iomux->ShutdownComplete(this);
    }
}

void AppClientMedia::ShutdownTimeout()
{
    bool appSharingSession = false;
    bool videoSession = false;
    bool audioSession = false;
    if(!sessions.empty()) {
        AppClientMediaSession *s;
        for(s = sessions.front(); s; s = s->goNext()) {
            if(s->GetMediaType() == MediaType::APPSHARING || s->GetMediaType() == MediaType::APPSHARING_CONTAINER) {
                appSharingSession = true;
            }
            else if((s->GetMediaType() == MediaType::VIDEO) || (s->GetMediaType() == MediaType::VIDEO_APPSHARING)) {
                videoSession = true;
            }
            else if(s->GetMediaType() == MediaType::AUDIO) {
                audioSession = true;
            }
        }
    }
    debug->printf("AppClientMedia(%p)::ShutdownTimeout %d  %d %d %d  %d %d %d  %d %d %d", this,
        (webserverPlugin != NULL), appSharingSession, videoSession, audioSession,
        stoppingAppSharing, stoppingVideo, stoppingAudio,
        (appSharingIo != NULL), (videoIo != NULL), (audioIo != NULL));
}

void AppClientMedia::SetExcludeInterfaces(const char* excludeInf)
{
    if (this->mediaConfiguration->excludeInterfaces) free(this->mediaConfiguration->excludeInterfaces);
    this->mediaConfiguration->excludeInterfaces = _strdup(excludeInf);
}

void AppClientMedia::SetNoVpnAddresses(bool noVpnAddresses)
{
    this->mediaConfiguration->noVpnAddresses = noVpnAddresses;
}

void AppClientMedia::SetCurrentUserSip(const char * sip)
{
    if (this->mediaConfiguration->currentUserSip) free(this->mediaConfiguration->currentUserSip);
    this->mediaConfiguration->currentUserSip = _strdup(sip);
}

void AppClientMedia::SetRecordingUrl(const char * url, bool force)
{
    if (force) {
        if (this->mediaConfiguration->recordingUrl) free(this->mediaConfiguration->recordingUrl);
        this->mediaConfiguration->recordingUrl = _strdup(url);
    }
    else {
        // If not force, leave the setting if set
        if (this->mediaConfiguration->recordingUrl == NULL && url) this->mediaConfiguration->recordingUrl = _strdup(url);
    }
}

void AppClientMedia::SetLangId(const char * langId)
{
    this->mediaConfiguration->langId = Language::GetLanguageId(langId, myappsLanguageTable, MYAPPS_NUM_LANGUAGES, MYAPPS_LANGUAGE_EN);
}

void AppClientMedia::QueryDevices(bool cleanDevices)
{
    if (audioIo) {
        if(cleanDevices) audioIo->CleanRemoteDevices();
        audioIo->QueryDevices(NULL);
    }
    if (videoIo) {
        if (cleanDevices) videoIo->CleanRemoteDevices();
        videoIo->QueryDevices(NULL);
    }
}

void AppClientMedia::RemoveSession(class AppClientMediaSession * session)
{
    if(!sessions.empty()) {
        AppClientMediaSession *s;
        for(s = sessions.front(); s; s = s->goNext()) {
            if(s == session) {
                this->Log(LOG_APP, "AppClientMedia(%p)::RemoveSession %p", this, session);
                s->remove();
                break;  
            }
        }
        bool appSharingSession = false;
        bool videoSession = false;
        bool audioSession = false;
        for(s = sessions.front(); s; s = s->goNext()) {
            if(s->GetMediaType() == MediaType::APPSHARING) {
                appSharingSession = true;
            }
            else if(s->GetMediaType() == MediaType::VIDEO) {
                videoSession = true;
            }
            else if(s->GetMediaType() == MediaType::AUDIO) {
                audioSession = true;
            }
        }
        if(session->GetMediaType() == MediaType::APPSHARING && !appSharingSession) {
            appSharingIo->UnshareAllApplications(session);
            appSharingIo->UnsubscribeApplications();
        }
        else if(session->GetMediaType() == MediaType::VIDEO && !videoSession) {
            // Need to close something?
        }
        else if(session->GetMediaType() == MediaType::AUDIO && !audioSession) {
            // Need to close something?
        }
    }
    if(stopping) TryStop();
}

void AppClientMedia::CloseAppSharingIoComplete()
{
    this->Log(LOG_APP, "AppClientMedia(%p)::CloseAppSharingIoComplete %d", this, stopping);

    if(appSharingIo) delete appSharingIo;
    appSharingIo = NULL;
    Shutdown();
}

void AppClientMedia::CloseVideoIoComplete()
{
    this->Log(LOG_APP, "AppClientMedia(%p)::CloseVideoIoComplete %d", this, stopping);

    if(videoIo) delete videoIo;
    videoIo = NULL;
    Shutdown();
}

void AppClientMedia::CloseAudioIoComplete()
{
    this->Log(LOG_APP, "AppClientMedia(%p)::CloseAudioIoComplete %d", this, stopping);

    if(audioIo) delete audioIo;
    audioIo = NULL;
    Shutdown();
}

const char *AppClientMedia::IdName(MediaIdNameItem *map, int id)
{
    unsigned i = 0;
    while (map[i].name && (map[i].id != id)) {
        i++;
    }
    return map[i].name;
}

int AppClientMedia::NameId(MediaIdNameItem *map, const char *name)
{
    unsigned i = 0;
    if (!name) {
        while (map[i].name) {
            i++;
        }
    }
    else {
        while (map[i].name && (strcmp(map[i].name, name) != 0)) {
            i++;
        }
    }
    return map[i].id;
}

class AppClientMediaSession * AppClientMedia::FindSessionByContext(const void * context, enum MediaType media, bool local)
{
    Log(LOG_MEDIA_CHANNEL, "AppClientMediaSession(%p)::FindSessionByContext %p local=%d media=%d", this, context, local, media);

    if(!sessions.empty()) {
        class AppClientMediaSession * s;
        for(s = sessions.front(); s; s = s->goNext()) {
            if(s->GetMediaType() == media && s->GetLocal() == local && s == context) return s;
        }
    }
    return NULL;
}

class AppClientMediaSession * AppClientMedia::FindSessionByChannelId(const char * channelId, enum MediaType media)
{
    Log(LOG_MEDIA, "AppClientMedia(%p)::FindSessionByChannelId %s media %d", this, channelId, media);

    if(!sessions.empty()) {
        class AppClientMediaSession * s;
        for(s = sessions.front(); s; s = s->goNext()) {
            Log(LOG_MEDIA, "AppClientMedia(%p)::FindSessionByChannelId session=%p found=%p", this, s, s->FindSessionByChannelId(channelId, media));

            if (s->FindSessionByChannelId(channelId, media)) return s;
        }
    }
    return NULL;
}

void AppClientMedia::MediaIoAppSharingContainerSend(const void * buf, size_t len, dword timestamp, const void * context, bool local, enum MediaType type)
{
    Log(LOG_MEDIA_CHANNEL, "AppClientMedia(%p)::MediaIoAppSharingContainerSend ctx=%p len=%d", this, context, len);

    if(!sessions.empty()) {
        class AppClientMediaSession * s = FindSessionByContext(context, type, local);
        if(s) s->GetMediaContainer()->PutSample((char *)buf, len, timestamp);
    }
}

void AppClientMedia::RequestAppSharingControl(unsigned int userId)
{
    AppClientMediaSession * session;

    Log(LOG_APP, "AppClientMediaSession(%p)::RequestAppSharingControl id=%d", this, userId);
    for(session = sessions.front(); session; session = session->goNext()) {
        if(session->GetMediaType() == MediaType::APPSHARING) {
            char temp[128];
            char * tmp = temp;
            char cfg[128];
            class json_io send(cfg);

            word sendBase = 0xffff;
            sendBase = send.add_object(0xffff, 0);
            send.add_string(sendBase, "mt", "RequestAppSharingControl");
            send.add_int(sendBase, "userId", userId, tmp);
            session->AppWebsocketMessageSend(send, cfg);
        }
    }
}

void AppClientMedia::AddAppSharingApplication(unsigned int id, const char* name, const char* desc, const char* hwId, const char* icon, const char* thumbnail, bool isDesktop)
{
    AppClientMediaSession * session;

    Log(LOG_APP, "AppClientMediaSession(%p)::AddAppSharingApplication id=%d name=%s hwId=%s", this, id, name, hwId ? hwId : "");
    for(session = sessions.front(); session; session = session->goNext()) {
        if(session->GetMediaType() == MediaType::APPSHARING || session->GetMediaType() == MediaType::VIDEO) {
            char temp[128];
            char * tmp = temp;
            char * cfg = (char *)malloc((name ? (strlen(name)<<1) : 0) + (desc ? (strlen(desc)<<1) : 0) + (icon ? strlen(icon) : 0) + (thumbnail ? strlen(thumbnail) : 0) + 1024);
            class json_io send(cfg);

            word sendBase = 0xffff;
            sendBase = send.add_object(0xffff, 0);
            send.add_string(sendBase, "mt", "AddAppSharingApplication");
            send.add_int(sendBase, "id", id, tmp);

            char nameSeed[17];
            IRandom::GetChars(nameSeed, 16, random_chars_url); nameSeed[16] = 0;
            char * nameEncrypted = (char *)malloc((strlen(name) << 1) + 1);
            session->AppWebsocketEncrypt(nameSeed, name, nameEncrypted, (strlen(name) << 1) + 1);
            send.add_string(sendBase, "nameSeed", nameSeed);
            send.add_string(sendBase, "nameEncrypted", nameEncrypted);

            char descSeed[17];
            IRandom::GetChars(descSeed, 16, random_chars_url); descSeed[16] = 0;
            char * descEncrypted = (char *)malloc((strlen(desc) << 1) + 1);
            session->AppWebsocketEncrypt(descSeed, desc, descEncrypted, (strlen(desc) << 1) + 1);
            send.add_string(sendBase, "descSeed", descSeed);
            send.add_string(sendBase, "descEncrypted", descEncrypted);

            send.add_string(sendBase, "hwId", hwId);
            send.add_string(sendBase, "icon", icon);
            send.add_string(sendBase, "thumbnail", thumbnail);
            send.add_bool(sendBase, "desktop", isDesktop);
            session->AppWebsocketMessageSend(send, cfg);

            free(nameEncrypted);
            free(descEncrypted);
            free(cfg);
        }
    }
}

void AppClientMedia::RemoveAppSharingApplication(unsigned int id)
{
    AppClientMediaSession * session;

    Log(LOG_APP, "AppClientMediaSession(%p)::RemoveAppSharingApplication id=%d", this, id);
    for (session = sessions.front(); session; session = session->goNext()) {
        if(session->GetMediaType() == MediaType::APPSHARING || session->GetMediaType() == MediaType::VIDEO) {
            char temp[128];
            char * tmp = temp;
            char cfg[2000];
            class json_io send(cfg);

            word sendBase = 0xffff;
            sendBase = send.add_object(0xffff, 0);
            send.add_string(sendBase, "mt", "RemoveAppSharingApplication");
            send.add_int(sendBase, "id", id, tmp);
            session->AppWebsocketMessageSend(send, cfg);
        }
    }
}

void AppClientMedia::UpdateAppSharingApplication(unsigned int id, const char * name, const char * desc, const char * hwId, const char * icon,  const char * thumbnail)
{
    AppClientMediaSession * session;

    Log(LOG_APPSHARING, "AppClientMediaSession(%p)::UpdateAppSharingApplication id=%d name=%s hwId=%s", this, id, name, hwId ? hwId : "");
    for(session = sessions.front(); session; session = session->goNext()) {
        if(session->GetMediaType() == MediaType::APPSHARING || session->GetMediaType() == MediaType::VIDEO) {
            char temp[128];
            char * tmp = temp;
            char * cfg = (char *)malloc((name ? (strlen(name)<<1) : 0) + (desc ? (strlen(desc)<<1) : 0) + (icon ? strlen(icon) : 0) + (thumbnail ? strlen(thumbnail) : 0) + 1024);
            class json_io send(cfg);

            word sendBase = 0xffff;
            sendBase = send.add_object(0xffff, 0);
            send.add_string(sendBase, "mt", "UpdateAppSharingApplication");
            send.add_int(sendBase, "id", id, tmp);

            char nameSeed[17];
            IRandom::GetChars(nameSeed, 16, random_chars_url); nameSeed[16] = 0;
            char * nameEncrypted = (char *)malloc((strlen(name) << 1) + 1);
            session->AppWebsocketEncrypt(nameSeed, name, nameEncrypted, (strlen(name) << 1) + 1);
            send.add_string(sendBase, "nameSeed", nameSeed);
            send.add_string(sendBase, "nameEncrypted", nameEncrypted);

            char descSeed[17];
            IRandom::GetChars(descSeed, 16, random_chars_url); descSeed[16] = 0;
            char * descEncrypted = (char *)malloc((strlen(desc) << 1) + 1);
            session->AppWebsocketEncrypt(descSeed, desc, descEncrypted, (strlen(desc) << 1) + 1);
            send.add_string(sendBase, "descSeed", descSeed);
            send.add_string(sendBase, "descEncrypted", descEncrypted);

            send.add_string(sendBase, "hwId", hwId);
            send.add_string(sendBase, "icon", icon);
            send.add_string(sendBase, "thumbnail", thumbnail);
            session->AppWebsocketMessageSend(send, cfg);

            free(nameEncrypted);
            free(descEncrypted);
            free(cfg);
        }
    }
}

void AppClientMedia::MediaIoDeviceAdded(IDeviceIo * deviceIo, void * context, const char *deviceId, int deviceType, unsigned deviceCapabilities, const char *deviceName, bool selected)
{
    class AppClientMediaSrc * src = (class AppClientMediaSrc *) context;

    Log(LOG_APP, "AppClientMediaSession(%p)::MediaIoDeviceAdded id=%s type=%d capabilities=%x name=%s src=%p session=%p selected=%s", this, deviceId, deviceType, deviceCapabilities, deviceName, src, src ? src->session : NULL, selected ? "true" : "false");
    if(!sessions.empty()) {
        AppClientMediaSession* session;
        for(session = sessions.front(); session; session = session->goNext()) {
            if((session->deviceIo == deviceIo) && (!src || (session == src->session))) {
                char temp[128];
                char * tmp = temp;
                char cfg[2000];
                class json_io send(cfg);

                word sendBase = 0xffff;
                sendBase = send.add_object(0xffff, 0);
                send.add_string(sendBase, "mt", "AddMediaDevice");
                if (src && src->src) {
                    send.add_string(sendBase, "src", src->src);
                }
                send.add_string(sendBase, "device", deviceId);
                send.add_string(sendBase, "type", AppClientMedia::IdName(IdNameDeviceType, deviceType));
                send.add_unsigned(sendBase, "capabilities", deviceCapabilities, tmp);
                send.add_string(sendBase, "name", deviceName);
                if(selected) send.add_bool(sendBase, "selected", selected);
                session->AppWebsocketMessageSend(send, cfg);
            }
        }
    }
}

void AppClientMedia::MediaIoDeviceFailed(IDeviceIo* deviceIo, void * context, const char * deviceId)
{
    class AppClientMediaSrc * src = (class AppClientMediaSrc*) context;

    Log(LOG_APP, "AppClientMedia(%p)::MediaIoDeviceFailed id=%s sessions=%d context=%p session=%p", this, deviceId, !sessions.empty(), context, src ? src->session : NULL);
    if (!sessions.empty()) {
        AppClientMediaSession* session;
        for (session = sessions.front(); session; session = session->goNext()) {
            if ((session->deviceIo == deviceIo) && (!src || (session == src->session))) {
                char cfg[2000];
                class json_io send(cfg);

                word sendBase = 0xffff;
                sendBase = send.add_object(0xffff, 0);
                send.add_string(sendBase, "mt", "MediaDeviceFailed");
                if (src && src->src) {
                    send.add_string(sendBase, "src", src->src);
                }
                send.add_string(sendBase, "device", deviceId);
                session->AppWebsocketMessageSend(send, cfg);
            }
        }
    }
}

void AppClientMedia::MediaIoDeviceRemoved(IDeviceIo * deviceIo, void * context, const char * deviceId)
{
    class AppClientMediaSrc * src = (class AppClientMediaSrc *) context;
    AppClientMediaSession * session;

    Log(LOG_APP, "AppClientMediaSession(%p)::MediaIoDeviceRemoved id=%s", this, deviceId);
    if(!sessions.empty()) {
        for (session = sessions.front(); session; session = session->goNext()) {
            if((session->deviceIo == deviceIo) && (!src || (session == src->session))) {
                char cfg[2000];
                class json_io send(cfg);

                word sendBase = 0xffff;
                sendBase = send.add_object(0xffff, 0);
                send.add_string(sendBase, "mt", "RemoveMediaDevice");
                if (src && src->src) {
                    send.add_string(sendBase, "src", src->src);
                }
                send.add_string(sendBase, "device", deviceId);
                session->AppWebsocketMessageSend(send, cfg);
            }
        }
    }
}

void AppClientMedia::MediaIoQueryDevicesResult(IDeviceIo * deviceIo, void * context)
{
    class AppClientMediaSrc * src = (class AppClientMediaSrc *) context;
    AppClientMediaSession * session;

    Log(LOG_APP, "AppClientMediaSession(%p)::MediaIoQueryDevicesResult src=%p", this, context);
    if(!sessions.empty()) {
        for(session = sessions.front(); session; session = session->goNext()) {
            if(session->deviceIo == deviceIo) {
                char cfg[2000];
                class json_io send(cfg);

                word sendBase = 0xffff;
                sendBase = send.add_object(0xffff, 0);
                send.add_string(sendBase, "mt", "QueryDevicesResult");
                if (src && src->src) {
                    send.add_string(sendBase, "src", src->src);
                }
                session->AppWebsocketMessageSend(send, cfg);
            }
        }
    }
}

void AppClientMedia::HookDeviceMsg(IAudioIo* audioIo, const char* deviceId, const char* msg)
{
    AppClientMediaSession* session;
    Log(LOG_APP, "AppClientMediaSession(%p)::HookDeviceMsg id=%s msg=%s", this, deviceId, msg);
    if (!sessions.empty()) {
        for (session = sessions.front(); session; session = session->goNext()) {
            if (session->deviceIo == audioIo) {
                char cfg[2000];
                class json_io send(cfg);
                word sendBase = 0xffff;
                sendBase = send.add_object(0xffff, 0);
                send.add_string(sendBase, "mt", msg);
                send.add_string(sendBase, "device", deviceId);
                session->AppWebsocketMessageSend(send, cfg);
            }
        }
    }
}

void AppClientMedia::MediaIoDeviceStarted(IDeviceIo * deviceIo, const char * deviceId)
{
    Log(LOG_APP, "AppClientMediaSession(%p)::MediaIoDeviceStarted deviceId=%s", this, deviceId);

    for (AppClientMediaSession * session = sessions.front(); session; session = session->goNext()) {
        if (session->deviceIo == deviceIo) {
            char buf[2000];
            class json_io send(buf);
            word base = send.add_object(0xffff, 0);
            send.add_string(base, "mt", "DeviceStarted");
            send.add_string(base, "device", deviceId);
            session->AppWebsocketMessageSend(send, buf);
        }
    }
}

void AppClientMedia::MediaIoDeviceStopped(IDeviceIo * deviceIo, const char * deviceId)
{
    Log(LOG_APP, "AppClientMediaSession(%p)::MediaIoDeviceStopped deviceId=%s", this, deviceId);

    for (AppClientMediaSession * session = sessions.front(); session; session = session->goNext()) {
        if (session->deviceIo == deviceIo) {
            char buf[2000];
            class json_io send(buf);
            word base = send.add_object(0xffff, 0);
            send.add_string(base, "mt", "DeviceStopped");
            send.add_string(base, "device", deviceId);
            session->AppWebsocketMessageSend(send, buf);
        }
    }
}

void AppClientMedia::HookKeyReceived(IAudioIo * audioIo, const char *deviceId, byte key)
{
    AppClientMediaSession * session;

    Log(LOG_APP, "AppClientMediaSession(%p)::HookKeyReceived id=%s key=%x", this, deviceId, key);
    if(!sessions.empty()) {
        for (session = sessions.front(); session; session = session->goNext()) {
            if(session->deviceIo == audioIo) {
                char cfg[2000];
                class json_io send(cfg);

                word sendBase = 0xffff;
                sendBase = send.add_object(0xffff, 0);
                send.add_string(sendBase, "mt", "HookKeyReceived");
                send.add_string(sendBase, "device", deviceId);
                send.add_string(sendBase, "key", AppClientMedia::IdName(IdNameHookKey, key));
                session->AppWebsocketMessageSend(send, cfg);
            }
        }
    }
}

// ******************** Webserver Plugin ******************** //

void AppClientMedia::WebserverPluginHttpListenResult(IWebserverPlugin * plugin, ws_request_type_t requestType, char * resourceName, const char * registeredPathForRequest, ulong64 dataSize)
{
    this->Log(LOG_APP, "AppClientMedia(%p)::WebserverPluginHttpListenResult (%s) (%s)", this, (requestType == WS_REQUEST_GET ? "GET" : (requestType == WS_REQUEST_POST ? "POST" : "Unknown Request")), (resourceName ? resourceName : "empty!"));

    plugin->Cancel(WSP_CANCEL_NOT_FOUND);
}

void AppClientMedia::WebserverPluginClose(IWebserverPlugin * plugin, wsp_close_reason_t reason, bool lastUser)
{
    this->Log(LOG_APP, "AppClientMedia(%p)::WebserverPluginClose %d", this, lastUser);
    if(lastUser) {
        delete this->webserverPlugin;
        this->webserverPlugin = NULL;
        Shutdown();
    }
}

void AppClientMedia::WebserverPluginWebsocketListenResult(IWebserverPlugin * plugin, const char * path, const char * registeredPathForRequest, const char * host)
{
    AppClientMediaSession *session;
    this->Log(LOG_APP, "AppClientMedia(%p)::WebserverWebsocketListenResult %s %s (%s) %p,%p,%p", this, path, registeredPathForRequest, host, strstr(path, "audio"), strstr(path, "video"), strstr(path, "appsharing"));

    if(!strcmp(path, "/audio")) {
        if(audioIo) {
            session = new AppClientMediaSession(this, iomux, socketUdpProvider, socketTcpProvider, socketTlsProvider, this->contextSrtp, mediaProvider, audioIo, MediaType::AUDIO, NULL, false, mediaConfiguration);
            sessions.push_back(session);
            audioIo->Initialize(this, this);
            audioIoInitialized = true;
        }
        else plugin->Cancel(WSP_CANCEL_UNAVAILABLE);
    }
    else if(!strcmp(path, "/video")) {
        if(videoIo) {
            session = new AppClientMediaSession(this, iomux, socketUdpProvider, socketTcpProvider, socketTlsProvider, this->contextSrtp, mediaProvider, videoIo, MediaType::VIDEO, NULL, false, mediaConfiguration);
            sessions.push_back(session);
            videoIo->Initialize(this, this, this);
            videoIoInitialized = true;
        }
        else plugin->Cancel(WSP_CANCEL_UNAVAILABLE);
    }
    else if(!strcmp(path, "/appsharing")) {
        if(appSharingIo) {
            session = new AppClientMediaSession(this, iomux, socketUdpProvider, socketTcpProvider, socketTlsProvider, this->context, mediaProvider, appSharingIo, MediaType::APPSHARING, NULL, false, mediaConfiguration);
            sessions.push_back(session);
            appSharingIo->Initialize(this, this);
            appSharingIoInitialized = true;
        }
        else plugin->Cancel(WSP_CANCEL_UNAVAILABLE);
    }
    else if(strstr(path, "/appsharing/")) {  // appsharing/channelId
        if(appSharingIo) {
            const char * pStr = path + 12; // /appsharing/ (12)
            const char * p1Str = strstr(pStr, "/");
        
            char * chId = NULL;
            if(p1Str) {
                chId = (char *)malloc(p1Str - pStr + 1);
                memcpy(chId, pStr, p1Str - pStr);
                chId[p1Str - pStr] = 0;
            }

            if(chId) {
                class AppClientMediaSession * remoteContainerSession = new AppClientMediaSession(this, iomux, socketUdpProvider, socketTcpProvider, socketTlsProvider, this->contextSrtp, mediaProvider, appSharingIo, MediaType::APPSHARING_CONTAINER, chId, false, mediaConfiguration);
                sessions.push_back(remoteContainerSession);
                appSharingIo->AddRemoteContainer(FindSessionByChannelId(chId, MediaType::APPSHARING), remoteContainerSession, chId, MediaType::APPSHARING_CONTAINER);
                free(chId);
            }
            else {
                plugin->Cancel(WSP_CANCEL_UNAVAILABLE);
            }
        }
        else {
            plugin->Cancel(WSP_CANCEL_UNAVAILABLE);
        }
    }
}

// ******************** AppClientSession ******************** //

AppClientMediaSession::AppClientMediaSession(class AppClientMedia * appclient, IIoMux * const iomux, class ISocketProvider * socketUdpProvider, class ISocketProvider * socketTcpProvider, class ISocketProvider * socketTlsProvider, class ISocketContext * context, class IMediaProvider * mediaProvider, class IDeviceIo * deviceIo, MediaType mediaType, char * id, bool local, class MediaConfiguration * mediaConfiguration) :
AppWebsocket(appclient->webserverPlugin, appclient)
{
    this->iomux = iomux;
    this->appclient = appclient;
    this->deviceIo = deviceIo;
    this->mediaProvider = mediaProvider;
    this->socketUdpProvider = socketUdpProvider;
    this->socketTcpProvider = socketTcpProvider;
    this->socketTlsProvider = socketTlsProvider;
    this->mediaType = mediaType;
    this->log = appclient;
    this->socketContext = context;
    this->closed = false;
    this->callCount = 0;
    this->allocatedTones = 16;
    this->tones = (struct AudioIoDualTone *)(malloc(this->allocatedTones * sizeof(struct AudioIoDualTone)));
    this->id = _strdup(id);
    this->local = local;
    this->mediaConfiguration = mediaConfiguration;
    this->hostPbx = NULL;
    this->mediaSessionContext = NULL;
    if (mediaType == MediaType::APPSHARING_CONTAINER) this->container = new class AppSharingContainer(this, log);
#if !defined(__REMOTE_CONTROL_CLIENT__)
    else if(mediaType == MediaType::MP4_CONTAINER) this->container = new class MP4Container(this, log);
    else if(mediaType == MediaType::WEBM_VP8_CONTAINER) this->container = new class WebMContainer(this, log, false);
    else if(mediaType == MediaType::WEBM_VP9_CONTAINER) this->container = new class WebMContainer(this, log, true);
#endif
    else this->container = NULL;

    log->Log(LOG_APP, "AppClientMediaSession(%p)::AppClientMediaSession %s (%p) container %p", this, AppClientMedia::IdName(IdNameMediaType, mediaType), deviceIo, container);
}

AppClientMediaSession::~AppClientMediaSession()
{
    log->Log(LOG_APP, "AppClientMediaSession(%p)::~AppClientMediaSession media %d container %p connections %d local %d", this, mediaType, container, !connections.empty(), local);

    if(mediaType == MediaType::APPSHARING_CONTAINER) {
        (static_cast<class IAppSharingIo *>(deviceIo))->RemoveRemoteContainer(this, mediaType);
    }
    while(!devices.empty()) {
        class SelectedDevices * d = devices.pop_front();
#ifdef OLD_JS_SOFTPHONE
        // Devices were handled by the Javascript in the past and this was needed if the SoftphoneApp was closed during a call
        // With the new Softphone in C++, the devices are handled by the C++ module
        if(d->device) deviceIo->StopDevice(this, d->device);
#endif
        delete d;
    }
    while(!hookDevices.empty()) {
        class SelectedDevices * d = hookDevices.pop_front();
#ifdef OLD_JS_SOFTPHONE
        if(d->device) deviceIo->StopDevice(this, d->device);
#endif
        delete d;
    }
#ifdef OLD_JS_SOFTPHONE
    if (mediaType == MediaType::AUDIO) {
        unsigned i;
        for(i = 0; i < callCount; i++) {
            (static_cast<class IAudioIo *>(deviceIo))->SendHookKey("", KEY_ONHOOK, callIds[i], NULL, NULL);
        }
    }
#endif
    if(tones) free(tones);
    if(container) delete container;
    if(id) free(id);
    if(hostPbx) free(hostPbx);
    if(mediaSessionContext) delete mediaSessionContext;

    appclient->RemoveSession(this);
}

bool AppClientMediaSession::AppWebsocketConnectComplete(class json_io & msg, word info)
{
    return true;
}

enum ConnectionType getConnectionType(const char * type) {
    char firstChar = type ? type[0] : 0;
    if (firstChar == 'R') {
        if (!strcmp(type, "RemoteRtp")) return ConnectionType::REMOTE_RTP;
        if (!strcmp(type, "RemoteVideoStream")) return ConnectionType::REMOTE_VIDEO_STREAM;
    }
    if (firstChar == 'L') {
        if (!strcmp(type, "LocalVideoStream")) return ConnectionType::LOCAL_VIDEO_STREAM;
        if (!strcmp(type, "LocalSharingStream")) return ConnectionType::LOCAL_SHARING_STREAM;
        if (!strcmp(type, "LocalWebcamStream")) return ConnectionType::LOCAL_WEBCAM_STREAM;
    }
    return ConnectionType::REMOTE_RTP;
}

void AppClientMediaSession::AppWebsocketMessage(class json_io & msg, word base, const char * mt, const char * src)
{
    const char * channel = msg.get_string(base, "channel");
    const char * connType = msg.get_string(base, "type");

    log->Log(LOG_APP, "AppClientMediaSession(%p)::AppWebsocketMessage msg=%s app=%s type=(%s) src=%s sip=%s ch=(%s)", this, mt, app, connType ? connType : "", src, GetSip(), channel ? channel : "");

    class MediaConnection * conn = NULL;
    if(!strcmp(mt, "AllocChannel")) {
        if(!channel) {
            AppWebsocketMessageComplete();
            return;
        }
        conn = new class MediaConnection(this, iomux, log, socketUdpProvider, socketTcpProvider, socketTlsProvider, socketContext, mediaProvider, deviceIo, mediaType, channel, getConnectionType(connType), msg.get_string(base, "dn"), mediaConfiguration, hostPbx);
        if(conn) {
            connections.push_back(conn);
            conn->WebsocketMessage(msg, base, mt, src);
        }
    }
    else if(!strcmp(mt, "ConnectChannel") || !strcmp(mt, "ReleaseChannel")) {
        if(!channel) {
            AppWebsocketMessageComplete();
            return;
        }
        conn = GetConnectionByChannelID(channel, getConnectionType(connType), this->mediaType);
        if(conn) {
            conn->WebsocketMessage(msg, base, mt, src);
            // in case no stop device is sent!
            //if(!strcmp(mt, "ReleaseChannel") && selectedInputDevice) mediaIo->StopDevice(inputDeviceType, selectedInputDevice);
        }
        else debug->printf("AppClientSession(%p)::AppWebsocketMessage No connection for %s found!", this, channel);
    }
    else if(!strcmp(mt, "QueryDevices")) {
        if(mediaSessionContext && ((src && !mediaSessionContext->src) || (!src && mediaSessionContext->src) || (src && mediaSessionContext->src && (strcmp(src, mediaSessionContext->src) != 0)))) {
            delete mediaSessionContext;
            mediaSessionContext = NULL;
        }
        if(!mediaSessionContext) {
            mediaSessionContext = new class AppClientMediaSrc(this, src, msg.get_string(base, "app"));
        }
        deviceIo->QueryDevices(mediaSessionContext);
    }
    else if(!strcmp(mt, "Init")) {
        if(hostPbx) free(hostPbx);
        hostPbx = _strdup(msg.get_string(base, "pbx"));
    }
    else if (!strcmp(mt, "AcquireHookDevice")) {
        const char* selectedDevice = msg.get_string(base, "device");
        (static_cast<class IAudioIo*>(deviceIo))->AcquireHookDevice(selectedDevice);
    }
    else if(!strcmp(mt, "SelectDevice")) {
        deviceIo->SelectDevice(msg.get_string(base, "app"), msg.get_string(base, "device"));
    }
    else if(!strcmp(mt, "ChangeDevice")) {
        deviceIo->ChangeDevice(msg.get_string(base, "app"), msg.get_string(base, "device"));
    }
    else if(!strcmp(mt, "StartDevice")) {
        const char* selectedDevice;
        if (mediaType == MediaType::VIDEO) {
            bool wPresent = false, hPresent = false;
            int w = msg.get_int(base, "width", &wPresent);
            if (wPresent == false || w == 0) w = 352;
            int h = msg.get_int(base, "height", &hPresent);
            if (hPresent == false || h == 0) h = 288;
            selectedDevice = (static_cast<class IVideoIo*>(deviceIo))->StartVideoDevice((void*)this, (void*)mediaSessionContext, msg.get_string(base, "device"), w, h,
                AppClientMedia::NameId(IdNameDeviceMode, msg.get_string(base, "mode")));
        }
        else selectedDevice = deviceIo->StartDevice((void*)this, (void*)mediaSessionContext, msg.get_string(base, "device"),
            AppClientMedia::NameId(IdNameDeviceMode, msg.get_string(base, "mode")));
        class SelectedDevices * d = NULL;
        if(!devices.empty()) {
            for(d = devices.front(); d; d = d->goNext()) {
                if(d->device && selectedDevice && !strcmp(d->device, selectedDevice) && d->context == this) {
                    d->IncCounter();
                    break;
                }
            }
        }
        if(!d && selectedDevice) {
            log->Log(LOG_APP, "AppClientMediaSession(%p)::AppWebsocketMessage Started device (%s)", this, selectedDevice);
            d = new class SelectedDevices((void *)this, (const char *)selectedDevice);
            devices.push_back(d);
        }
    }
    else if(!strcmp(mt, "StopDevice")) {
        const char * selectedDevice = msg.get_string(base, "device");
        class SelectedDevices * d = NULL;
        if(!devices.empty()) {
            for(d = devices.front(); d; d = d->goNext()) {
                if(d->device && selectedDevice && !strcmp(d->device, selectedDevice) && d->context == this) {
                    break;
                }
            }
        }
        if(!d) {
            debug->printf("AppClientMediaSession(%p)::AppWebsocketMessage StopDevice not found %s", this, selectedDevice);
        } 
        else {
            deviceIo->StopDevice(this, d->device);
            if(d->GetCounter() == 0) {
                d->remove();
                delete d;
            }
            else d->DecCounter();
        }
    }
    else if(!strcmp(mt, "SubscribeApplications")) {
        deviceIo->SubscribeApplications();
    }
    else if(!strcmp(mt, "UnsubscribeApplications")) {
        deviceIo->UnsubscribeApplications();
    }
#if 0
    else if(!strcmp(mt, "GiveControlToUser")) {
        (static_cast<class IAppSharingIo *>(deviceIo))->GiveControlToUser((void *)this, msg.get_int(base, "sessionId"));
    }
    else if(!strcmp(mt, "RemoveControlFromUser")) {
        (static_cast<class IAppSharingIo *>(deviceIo))->RemoveControlFromUser((void *)this, msg.get_int(base, "sessionId"));
    }
    else if(!strcmp(mt, "RequestControlFromUser")) {
        (static_cast<class IAppSharingIo *>(deviceIo))->RequestControlFromUser((void *)this, msg.get_int(base, "sessionId"));
    }
#endif
    else if(!strcmp(mt, "ShareApplication")) {
        deviceIo->ShareApplication((void *)this, msg.get_int(base, "id"), msg.get_bool(base, "admin"));
    }
    else if(!strcmp(mt, "UnshareApplication")) {
        deviceIo->UnshareApplication((void *)this, msg.get_int(base, "id"));
    }
    else if (!strcmp(mt, "RemoteVideoPriority")) {
        deviceIo->RemoteVideoPriority((void*)this, msg.get_string(base, "channel"), msg.get_bool(base, "on"));
    }
    else if(!strcmp(mt, "StartMonitoring")) {
        (static_cast<class IAudioIo *>(deviceIo))->StartMonitoring(msg.get_string(base, "device"),
            AppClientMedia::NameId(IdNameDeviceMode, msg.get_string(base, "mode")));
    }
    else if(!strcmp(mt, "StopMonitoring")) {
        (static_cast<class IAudioIo *>(deviceIo))->StopMonitoring(msg.get_string(base, "device"));
    }
#if !defined(__NO_AUDIO_IO__)
    else if(!strcmp(mt, "StartHookDevice")) {
        const char* selectedDevice = msg.get_string(base, "device");
        (static_cast<class IAudioIo *>(deviceIo))->StartHookDevice(selectedDevice);
        class SelectedDevices * d = NULL;
        if(!hookDevices.empty()) {
            for(d = hookDevices.front(); d; d = d->goNext()) {
                if(d->device && selectedDevice && !strcmp(d->device, selectedDevice) && d->context == this) {
                    d->IncCounter();
                    break;
                }
            }
        }
        if(!d && selectedDevice) {
            log->Log(LOG_APP, "AppClientMediaSession(%p)::AppWebsocketMessage Started hook device (%s)", this, selectedDevice);
            d = new class SelectedDevices((void *)this, (const char *)selectedDevice);
            hookDevices.push_back(d);
        }
    }
    else if(!strcmp(mt, "StopHookDevice")) {
        const char * selectedDevice = msg.get_string(base, "device");
        class SelectedDevices * d = NULL;
        if(!hookDevices.empty()) {
            for(d = hookDevices.front(); d; d = d->goNext()) {
                if(d->device && selectedDevice && !strcmp(d->device, selectedDevice) && d->context == this) {
                    break;
                }
            }
        }
        if(!d) {
            debug->printf("AppClientMediaSession(%p)::AppWebsocketMessage StopHookDevice not found %s", this, selectedDevice);
        } 
        else {
            (static_cast<class IAudioIo *>(deviceIo))->StopHookDevice(selectedDevice);
            if(d->GetCounter() == 0) {
                d->remove();
                delete d;
            }
            else d->DecCounter();
        }
    }
    else if(!strcmp(mt, "SendHookKey")) {
        int key = AppClientMedia::NameId(IdNameHookKey, msg.get_string(base, "key"));
        int callId = msg.get_int(base, "callId");
        (static_cast<class IAudioIo *>(deviceIo))->SendHookKey(msg.get_string(base, "device"), key, callId, msg.get_string(base, "number"), msg.get_string(base, "name"));
        unsigned i;
        i = 0;
        while((i < callCount) && (callIds[i] != callId)) {
            i++;
        }
        if(key == KEY_ONHOOK) {
            if(i < callCount) {
                callCount--;
                while(i < callCount) {
                    callIds[i] = callIds[i+1];
                    i++;
                }
            }
        }
        else {
            if(i == callCount) {
                if(i == APPCLIENT_MEDIA_MAX_CALLS) {
                    callCount = 0;
                }
                callIds[callCount++] = callId;
            }
        }
    }
    else if(!strcmp(mt, "Mute")) {
        (static_cast<class IAudioIo *>(deviceIo))->Mute(msg.get_bool(base, "microphone"), msg.get_bool(base, "speaker"));
    }
    else if(!strcmp(mt, "StartDualTones")) {
        struct AudioIoDualTone * t;
        word baseTones, baseTone, lastTone;
        unsigned count;
        int power;
        dword flags = 0;
        if (msg.get_bool(base, "modulate")) {
            flags |= AUDIO_IO_DUAL_TONE_MODULATE;
        }
        if (msg.get_bool(base, "loop")) {
            flags |= AUDIO_IO_DUAL_TONE_LOOP;
        }
        if (msg.get_bool(base, "autooff")) {
            flags |= AUDIO_IO_DUAL_TONE_AUTOOFF;
        }
        if (msg.get_bool(base, "passthrough")) {
            flags |= AUDIO_IO_DUAL_TONE_PASSTHROUGH;
        }
        if (msg.get_bool(base, "peer")) {
            flags |= AUDIO_IO_DUAL_TONE_PEER;
        }
        if (msg.get_bool(base, "both")) {
            flags |= AUDIO_IO_DUAL_TONE_BOTH;
        }
        baseTones = msg.get_array(base, "tones");
        count = 0;
        lastTone = 0;
        baseTone = msg.get_object(baseTones, lastTone);
        while (baseTone != JSON_ID_NONE) {
            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;
            }
            tones[count].onTimeMs = (word)(msg.get_int(baseTone, "on"));
            tones[count].offTimeMs = (word)(msg.get_int(baseTone, "off"));
            tones[count].frequency0 = (word)(msg.get_int(baseTone, "f0"));
            power = msg.get_int(baseTone, "p0");
            tones[count].level0 = (word)((power < -128) ? -32768 : ((power > 127) ? 32767 : power << 8));
            tones[count].frequency1 = (word)(msg.get_int(baseTone, "f1"));
            power = msg.get_int(baseTone, "p1");
            tones[count].level1 = (word)((power < -128) ? -32768 : ((power > 127) ? 32767 : power << 8));
            count++;
            baseTone = msg.get_object(baseTones, lastTone);
        }
        (static_cast<class IAudioIo *>(deviceIo))->StartDualTones(flags, count, tones);
    }
    else if(!strcmp(mt, "StopDualTones")) {
        (static_cast<class IAudioIo *>(deviceIo))->StopDualTones();
    }
    else if(!strcmp(mt, "StartTone")) {
        dword flags;
        unsigned count = MediaTones::PrepareToneMsg(msg.get_string(base, "type"), msg.get_string(base, "country"), &flags, tones);
        (static_cast<class IAudioIo *>(deviceIo))->StartDualTones(flags, count, tones);
    }
    else if(!strcmp(mt, "StopTone")) {
        (static_cast<class IAudioIo *>(deviceIo))->StopDualTones();
    }
    else if(!strcmp(mt, "SendDTMF")) {
        struct AudioIoDualTone * t;
        unsigned count, digit;
        bool payloadTypePresent;
        dword flags = 0;
        const char *digits = msg.get_string(base, "digits");
        bool feedback = msg.get_bool(base, "feedback");
        int payloadType = msg.get_int(base, "pt", &payloadTypePresent);
        if (payloadTypePresent) {
            if(!connections.empty()) {
                for (conn = connections.front(); conn; conn = conn->goNext()) {
                    if ((conn->GetConnectionType() == ConnectionType::REMOTE_RTP) && (conn->GetMediaType() == this->mediaType)) {
                        for (digit = 0; digits[digit] != '\0'; digit++) {
                            conn->RtpDtmf(digits[digit], payloadType);
                        }
                    }
                }
            }
        }
        if (feedback || !payloadTypePresent) {
            flags |= feedback ? AUDIO_IO_DUAL_TONE_BOTH | AUDIO_IO_DUAL_TONE_PEER : AUDIO_IO_DUAL_TONE_PEER;
            count = 0;
            for (digit = 0; digits[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(digits[digit], &tones[count]);
            }
            if (count != 0) {
                (static_cast<class IAudioIo *>(deviceIo))->StartDualTones(flags, count, tones);
            }
        }
    }
    else if(!strcmp(mt, "LoopTest")) {
        (static_cast<class IAudioIo *>(deviceIo))->LoopTest(msg.get_bool(base, "on"));
    }
#endif
    AppWebsocketMessageComplete();
    return;
}

void AppClientMediaSession::AppWebsocketBinaryMessage(void * buffer, int len)
{
    log->Log(LOG_APPSHARING, "AppClientMediaSession(%p)::AppWebsocketBinaryMessage(%s) len=%d", this, (mediaType == MediaType::VIDEO ? "video" : mediaType == MediaType::AUDIO ? "audio" : "appSharing"), len);
    if(mediaType == MediaType::APPSHARING_CONTAINER) {
        (static_cast<class IAppSharingIo *>(deviceIo))->SignalingMessage(this, buffer, len);
    }
    AppWebsocketMessageComplete();
}

void AppClientMediaSession::AppWebsocketSendResult()
{
    //log->Log(LOG_APP, "AppClientMediaSession(%p)::AppWebsocketSendResult", this);
}

void AppClientMediaSession::AppWebsocketClosed()
{
    log->Log(LOG_APP, "AppClientMediaSession(%p)::AppWebsocketClosed connections=%d media=%d", this, !connections.empty(), mediaType);

    closed = true;

    // websocket connection was closed unexpectedly
    if(!connections.empty()) {
        class MediaConnection * conn = connections.front();
        while(conn) {
            conn->CloseMediaConnection();  
            conn = conn->goNext();
        }
    }
    else delete this;
}

class AppClientMediaSession * AppClientMediaSession::FindSessionByChannelId(const char * channelId, enum MediaType type)
{
    log->Log(LOG_MEDIA, "AppClientMediaSession(%p)::FindSessionByChannelId channelId=%s", this, channelId ? channelId : "");

    if (!connections.empty()) {
        class MediaConnection * conn = connections.front();
        while(conn) {
            if(conn->GetChannelId() && channelId && !strcmp(conn->GetChannelId(), channelId) && type == this->mediaType) return this;
            conn = conn->goNext();
        }
    }
    return NULL;
}

void AppClientMediaSession::ContainerSample(char * buf, int len)
{
    log->Log(LOG_VIDEO, "AppClientMediaSession(%p)::ContainerSample %d", this, len);

    AppWebsocketMessageSendBinary(buf, len);
}

class MediaConnection * AppClientMediaSession::GetConnectionByChannelID(const char * ch, enum ConnectionType connType, MediaType media)
{
    log->Log(LOG_MEDIA, "AppClientMediaSession(%p)::GetConnectionByChannelID %s (%d)", this, ch, media);

    if(!connections.empty()) {
        class MediaConnection * m;
        for(m = connections.front(); m; m = m->goNext()) {
            log->Log(LOG_APP, "AppClientMediaSession(%p)::GetConnectionByChannelID (%s,%d,%d) != (%s,%d,%d) closing=%d", this, ch, media, connType, m->GetChannelId(), m->GetMediaType(), m->GetConnectionType(), m->IsClosing());
            if(m->IsClosing() == false && !strcmp(ch, m->GetChannelId()) && m->GetConnectionType() == connType && m->GetMediaType() == media) return m;
        }
    }
    return NULL;
}

void AppClientMediaSession::RemoveConnection(class MediaConnection * connection)
{
    if(!connections.empty()) {
        class MediaConnection * m;
        for(m = connections.front(); m; m = m->goNext()) {
            if(m == connection) {
                log->Log(LOG_VIDEO, "AppClientMediaSession(%p)::RemoveConnection %p", this, connection);
                m->remove();
                break;
            }
        }
    }
    if(closed && connections.empty()) delete this;
}

// ******************** MediaConnection ******************** //

int MediaConnection::DefaultPayloadType(unsigned coder)
{
    switch(coder) {
    case AudioCoder::G711_A:     return DEFAULT_PAYLOAD_TYPE_G711_A;
    case AudioCoder::G711_U:     return DEFAULT_PAYLOAD_TYPE_G711_U;
    case AudioCoder::G723_53:
    case AudioCoder::G723_63:    return DEFAULT_PAYLOAD_TYPE_G723;
    case AudioCoder::G729:
    case AudioCoder::G729A:
    case AudioCoder::G729B:
    case AudioCoder::G729AB:     return DEFAULT_PAYLOAD_TYPE_G729;
    case AudioCoder::G726_40:
    case AudioCoder::G726_32:
    case AudioCoder::G726_24:
    case AudioCoder::G726_16:    return DEFAULT_PAYLOAD_TYPE_G726;
    case AudioCoder::GSM:        return DEFAULT_PAYLOAD_TYPE_GSM;
    case AudioCoder::DVI4:       return DEFAULT_PAYLOAD_TYPE_DVI4;
    case AudioCoder::LPC:        return DEFAULT_PAYLOAD_TYPE_LPC;
    case AudioCoder::L16:        return DEFAULT_PAYLOAD_TYPE_L16;
    case AudioCoder::ILBC:       return 0;
    case AudioCoder::SPEEX:      return 0;
    case AudioCoder::DTMF:       return DEFAULT_PAYLOAD_TYPE_DTMF;
    case AudioCoder::CN:         return DEFAULT_PAYLOAD_TYPE_CN;
    case AudioCoder::RED:        return 0;
    case AudioCoder::G722:       return DEFAULT_PAYLOAD_TYPE_G722;
    case AudioCoder::G7221:      return 0;
    case AudioCoder::CLEARCHANNEL: return 0;
    case AudioCoder::OPUS_NB:
    case AudioCoder::OPUS_WB:    return DEFAULT_PAYLOAD_TYPE_OPUS;
    case AudioCoder::AMR_WB:     return DEFAULT_PAYLOAD_TYPE_AMR;
    case VideoCoder::H264:       return DEFAULT_PAYLOAD_TYPE_H264;
    case VideoCoder::VP8:        return DEFAULT_PAYLOAD_TYPE_VP8;
    case VideoCoder::VP9:        return DEFAULT_PAYLOAD_TYPE_VP9;
    case VideoCoder::AV1:        return DEFAULT_PAYLOAD_TYPE_AV1;
    case AppSharingCoder::JRFB:  return DEFAULT_SCTP_CHANNEL_NUMBER;
    case AppSharingCoder::JRFB2: return DEFAULT_SCTP_CHANNEL_NUMBER;
    case AppSharingCoder::VIDEO_CODER: return DEFAULT_SCTP_CHANNEL_NUMBER;
    }
    return 0;
}

MediaConnection::MediaConnection(class AppClientMediaSession * mediaSession, IIoMux * const iomux, class IInstanceLog * log, class ISocketProvider * socketUdpProvider, class ISocketProvider * socketTcpProvider, class ISocketProvider * socketTlsProvider, class ISocketContext * context, class IMediaProvider * mediaProvider, class IDeviceIo * deviceIo, MediaType mediaType, const char * channelId, enum ConnectionType connType, const char * dn, class MediaConfiguration * mediaConfiguration, char * hostPbx) :
    log(log),
    iomux(iomux)
{
    this->closed = false;
    this->closing = false;
    this->container = NULL;
    this->mediaSession = mediaSession;
    this->socketUdpProvider = socketUdpProvider;
    this->socketTcpProvider = socketTcpProvider;
    this->socketTlsProvider = socketTlsProvider;
    this->media = mediaProvider->CreateMedia(iomux, this, log);
    this->deviceIo = deviceIo;
    this->mediaType = mediaType;
    this->config = NULL;
    this->socketContext = context;
    this->dn = _strdup(dn);
    this->hostPbx = _strdup(hostPbx);
    this->mediaConfiguration = mediaConfiguration;
    if(channelId) { 
        this->ch = _strdup(channelId);
    }
    else {
        this->ch = NULL;
    }
    this->connType = connType;
    switch(mediaType) {
        case MediaType::AUDIO:
#if !defined(__NO_AUDIO_IO__)
            this->mediaIoChannel = (static_cast<class IAudioIo *>(deviceIo))->CreateChannel(this);
#else
            this->mediaIoChannel = NULL;
#endif
            break;
        case MediaType::VIDEO:
        case MediaType::VIDEO_APPSHARING:
#if !defined(__NO_VIDEO_IO__)
            this->mediaIoChannel = IVideoIoChannel::Create(iomux, static_cast<class IVideoIo *>(deviceIo), this, log);
#else
            this->mediaIoChannel = NULL;
#endif
            break;
        case MediaType::APPSHARING:
            this->mediaIoChannel = IAppSharingIoChannel::Create(iomux, static_cast<class IAppSharingIo *>(deviceIo), this, log, dn, mediaConfiguration);
            break;
        case MediaType::MP4_CONTAINER:
        case MediaType::WEBM_VP8_CONTAINER:
        case MediaType::WEBM_VP9_CONTAINER:
        case MediaType::APPSHARING_CONTAINER:
            break;
    }
    this->mediaCodec = AudioCoder::CODER_UNDEFINED;
    this->selectedInputDevice = NULL;

    log->Log(LOG_APP, "MediaConnection(%p,%p)::MediaConnection (%s,%s)", this, mediaIoChannel, ch, AppClientMedia::IdName(IdNameMediaType, mediaType));
}

MediaConnection::~MediaConnection()
{
    log->Log(LOG_APP, "MediaConnection(%p)::~MediaConnection channel=%p closed=%d (%s)", this, mediaIoChannel, closed, (mediaType == MediaType::VIDEO ? "Video" : mediaType == MediaType::AUDIO ? "Audio" : "AppSharing"));

    if(config) delete config;
    if(media) delete media;
    if(ch) free(ch);
    if(dn) free(dn);
    if(hostPbx) free(hostPbx);
    if(mediaIoChannel) delete mediaIoChannel;
    if(selectedInputDevice) free(selectedInputDevice);
    while(!assignmentsList.empty()) {
        delete assignmentsList.front();
    }

    mediaSession->RemoveConnection(this);
}

void MediaConnection::WebsocketMessage(class json_io & msg, word base, const char * mt, const char * src)
{ 
    log->Log(LOG_APP, "MediaConnection(%p)::WebsocketMessage %s src=%s ch=%s media=%s", this, mt, src, (ch ? ch : msg.get_string(base, "channel")), AppClientMedia::IdName(IdNameMediaType, mediaType));

    if(!strcmp(mt, "AllocChannel")) {
        char * stunServer = NULL;
        char * turnServer = NULL;
        const char * turnUsername = NULL;
        const char * turnPassword = NULL;
        word sLast = 0, t, end = 0xFFFF;
        word iceServers = msg.get_array(base, "iceServers");
        while ((t = msg.get_object(iceServers, sLast)) != end) {
            bool turnUrls = false;
            word urlsArray = msg.get_array(t, "urls");
            if (urlsArray != end) {
                word urlsLast = 0;
                const char * url;
                while ((url = msg.get_string(urlsArray, urlsLast)) != NULL) {
                    const char * p;
                    unsigned n;
                    if((p = strstr(url, "stun:")) != NULL) {
                        p += 5;
                        if (stunServer) {
                            n = strlen(stunServer);
                            char * q = (char *)malloc(n + 1 + strlen(p)+1);
                            memcpy(q, stunServer, n);
                            free(stunServer);
                            stunServer = q;
                            stunServer[n++] = ',';
                        }
                        else {
                            n = 0;
                            stunServer = (char *)malloc(strlen(p)+1);
                        }
                        strcpy(&stunServer[n], p);
                    }
                    else if((p = strstr(url, "turn:")) != NULL) {
                        p += 5;
                        if (turnServer) {
                            n = strlen(turnServer);
                            char * q = (char *)malloc(n + 1 + strlen(p)+1);
                            memcpy(q, turnServer, n);
                            free(turnServer);
                            turnServer = q;
                            turnServer[n++] = ',';
                        }
                        else {
                            n = 0;
                            turnServer = (char *)malloc(strlen(p)+1);
                        }
                        strcpy(&turnServer[n], p);
                        turnUrls = true;
                    }
                }
            }
            else {
                const char * urls = msg.get_string(t, "urls");
                if (urls) {
                    const char * p;
                    if((p = strstr(urls, "stun:")) != NULL) {
                        p += 5;
                        stunServer = (char *)malloc(strlen(p)+1);
                        strcpy(stunServer, p);
                    }
                    else if((p = strstr(urls, "turn:")) != NULL) {
                        p += 5;
                        turnServer = (char *)malloc(strlen(p)+1);
                        strcpy(turnServer, p);
                        turnUrls = true;
                    }
                }
            }
            if (turnUrls) {
                turnUsername = msg.get_string(t, "username");
                turnPassword = msg.get_string(t, "credential");
            }
            if(stunServer && turnServer) break;
        }
        word assignments = msg.get_array(base, "assignments");
        if (assignments != end) {
            sLast = 0;
            while ((t = msg.get_object(assignments, sLast)) != end) {
                const char* codec = msg.get_string(t, "coder");
                bool ptPresent = false;
                int pt = msg.get_int(t, "pt", &ptPresent);
                if (ptPresent && codec) {
                    log->Log(LOG_MEDIA, "MediaConnection::WebsocketMessage assignments %s(%d):%d", codec, AppClientMedia::NameId(IdNameCoder, codec), pt);
                    assignmentsList.push_back(new class CodecAssignments(AppClientMedia::NameId(IdNameCoder, codec), pt));
                }
            }
        }
        switch(mediaType) {
            case MediaType::AUDIO:
                media->Initialize(socketUdpProvider, socketTcpProvider, socketContext, 50000, 50099, stunServer, turnServer, turnUsername, turnPassword, (MediaType)this->mediaType, msg.get_bool(base, "stunSlow"), mediaConfiguration ? mediaConfiguration->turnOnly : false, mediaConfiguration ? mediaConfiguration->iceNoHost : false, false, mediaConfiguration ? mediaConfiguration->mediaDropPacketsTx : 0, mediaConfiguration ? mediaConfiguration->mediaDropPacketsRx : 0, hostPbx, mediaConfiguration ? mediaConfiguration->noVpnAddresses : false, &assignmentsList, true, socketTlsProvider, mediaConfiguration ? mediaConfiguration->excludeInterfaces : NULL, NULL, NULL);
                break;
            case MediaType::VIDEO:
            case MediaType::VIDEO_APPSHARING:
                media->Initialize(socketUdpProvider, socketTcpProvider, socketContext, 50100, 50199, stunServer, turnServer, turnUsername, turnPassword, (MediaType)this->mediaType, msg.get_bool(base, "stunSlow"), (connType == ConnectionType::REMOTE_RTP ? (mediaConfiguration ? mediaConfiguration->turnOnly : false) : false), (connType == ConnectionType::REMOTE_RTP ? (mediaConfiguration ? mediaConfiguration->iceNoHost : false) : false), MediaRtpOverTcp(connType), mediaConfiguration ? mediaConfiguration->mediaDropPacketsTx : 0, mediaConfiguration ? mediaConfiguration->mediaDropPacketsRx : 0, hostPbx, mediaConfiguration ? mediaConfiguration->noVpnAddresses : false, &assignmentsList, true, socketTlsProvider, mediaConfiguration ? mediaConfiguration->excludeInterfaces : NULL, NULL, NULL);
                break;
            case MediaType::APPSHARING:
                media->Initialize(socketUdpProvider, socketTcpProvider, socketContext, 50200, 50299, stunServer, turnServer, turnUsername, turnPassword, (MediaType)this->mediaType, msg.get_bool(base, "stunSlow"), mediaConfiguration ? mediaConfiguration->turnOnly : false, mediaConfiguration ? mediaConfiguration->iceNoHost : false, false, mediaConfiguration ? mediaConfiguration->mediaDropPacketsTx : 0, mediaConfiguration ? mediaConfiguration->mediaDropPacketsRx : 0, hostPbx, mediaConfiguration ? mediaConfiguration->noVpnAddresses : false, &assignmentsList, true, socketTlsProvider, mediaConfiguration ? mediaConfiguration->excludeInterfaces : NULL, NULL, NULL);
                break;
            case MediaType::MP4_CONTAINER:
            case MediaType::WEBM_VP8_CONTAINER:
            case MediaType::WEBM_VP9_CONTAINER:
            case MediaType::APPSHARING_CONTAINER:
                break;
        }
        if(stunServer) free(stunServer);
        if(turnServer) free(turnServer);
    }
    else if(!strcmp(mt, "ConnectChannel")) {
        bool msgOk = false;
        class MediaConfig * remoteCfg = new class MediaConfig();
        remoteCfg->type = this->mediaType;
        word baseChannels = msg.get_object(base, "channels");
        if(baseChannels != 0xffff) {
            word baseCh = msg.get_array(baseChannels, "ch");
            if(baseCh != 0xFFFF) {
                word c_last = 0, t, end = 0xFFFF;
                while ((t = msg.get_object(baseCh, c_last)) != end) {
                    class Codec * codec = new class Codec();
                    codec->coder = AppClientMedia::NameId(IdNameCoder, msg.get_string(t, "coder"));
                    if(codec->coder == 0) codec->coder = AppClientMedia::NameId(OldIdNameCoder, msg.get_string(t, "coder"));
                    codec->xmitPacket = msg.get_int(t, "xmit"); 
                    codec->recvPacket = msg.get_int(t, "recv"); 
                    codec->addr = _strdup(msg.get_string(t, "addr"));
                    codec->port = msg.get_int(t, "port");
                    codec->mcAddr = _strdup(msg.get_string(t, "mc_addr"));
                    codec->mcPort = msg.get_int(t, "mc_port");
                    if(codec->mcAddr && codec->mcPort && !remoteCfg->mcAddr) {
                        remoteCfg->mcAddr = _strdup(codec->mcAddr);
                        remoteCfg->mcPort = codec->mcPort;
                    }
                    codec->flags = msg.get_int(t, "flags");
                    bool present;
                    codec->pt = msg.get_int(t, "pt", &present);
                    if(present == false) {
                        codec->pt = DefaultPayloadType(codec->coder);
                    } 
                    if(remoteCfg->defAddr == NULL && codec->addr) { 
                        remoteCfg->defAddr = _strdup(codec->addr);
                        remoteCfg->defPort = codec->port;
                    }
                    remoteCfg->codecList.push_back(codec);
                }
            }
            word baseIce = msg.get_object(baseChannels, AppClientMedia::IdName(IdNameIceType, mediaType));
            if(baseIce != 0xFFFF) {
                word c_last = 0, t, end = 0xFFFF;
                remoteCfg->ice = new class IceCandidates();
                remoteCfg->ice->iceUfrag = _strdup(msg.get_string(baseIce, "usr"));
                remoteCfg->ice->icePwd = _strdup(msg.get_string(baseIce, "pwd"));
                remoteCfg->ice->fingerprint = _strdup(msg.get_string(baseIce, "fingerprint"));
                remoteCfg->ice->rtcpMux = this->mediaType == MediaType::AUDIO ? msg.get_bool(baseChannels, "audio_rtcp_mux") : this->mediaType == MediaType::VIDEO  ? msg.get_bool(baseChannels, "video_rtcp_mux") : this->mediaType == MediaType::APPSHARING ? msg.get_bool(baseChannels, "collab_rtcp_mux") : false;
                word iceArray = msg.get_array(baseIce, "candidate");
                c_last = 0;
                end = 0xFFFF;
                while ((t = msg.get_object(iceArray, c_last)) != end) {
                    int type = AppClientMedia::NameId(IdNameIceCandidateType, msg.get_string(t, "type"));
                    if (type != -1) {
                        class IceCandidate * cand = new class IceCandidate();
                        cand->addr = _strdup(msg.get_string(t, "addr"));
                        cand->relatedAddr = _strdup(msg.get_string(t, "r_addr"));
                        cand->foundation = _strdup(msg.get_string(t, "foundation"));
                        cand->rtpPort = msg.get_int(t, "rtp");
                        cand->rtcpPort = msg.get_int(t, "rtcp");
                        cand->relatedRtpPort = msg.get_int(t, "r_rtp");
                        cand->relatedRtcpPort = msg.get_int(t, "r_rtcp");
                        cand->rtpPriority = msg.get_int(t, "rtp_prio");
                        cand->rtcpPriority = msg.get_int(t, "rtcp_prio");
                        cand->type = type;
                        remoteCfg->ice->candidateList.push_back(cand);
                    }
                }
            }
            const char* type = msg.get_string(base, "type");
            const char* role = msg.get_string(base, "role");
            const char* remoteIceRole = msg.get_string(base, "remoteIceRole");
            const char* remoteDtlsRole = msg.get_string(base, "remoteDtlsRole");
            if((remoteIceRole && remoteDtlsRole) || role) {
                msgOk = true;
                class Codec * c = remoteCfg->codecList.front();
                if (c && c->coder) {
                    mediaCodec = c->coder;
                    if(mediaType == MediaType::VIDEO) {
                        if (type) {
                            if (!strcmp(type, "RemoteRtp"))
                                (static_cast<class IVideoIo*>(this->deviceIo))->StartRemoteVideoEncoder(mediaSession, ch, mediaCodec);
                            else if (!strcmp(type, "LocalVideoStream"))
                                (static_cast<class IVideoIo*>(this->deviceIo))->StartLocalVideoEncoder(mediaSession, ch, mediaCodec);
                        }
                    }
                }
                if(remoteIceRole && remoteDtlsRole)
                    media->Connect(remoteCfg, (!strcmp(remoteIceRole, "controlling") ? true : false), (enum SetupRole)AppClientMedia::NameId(IdSetupRole, remoteDtlsRole));
                else
                    media->Connect(remoteCfg, (!strcmp(role, "controlling") ? false : true), SetupRole::SETUP_ROLE_NONE);
            }
        }
        if(msgOk == false) {
            delete remoteCfg;
            MediaConnectResult(media, "MessageError");
        }
    }
    else if(!strcmp(mt, "ReleaseChannel")) {
        if (mediaType == MediaType::VIDEO) {
            if (msg.get_string(base, "type")) {
                if (!strcmp(msg.get_string(base, "type"), "LocalVideoStream")) (static_cast<class IVideoIo*>(this->deviceIo))->StopLocalVideoEncoder(mediaSession, ch, mediaCodec);
            }
        }
        media->Close();
        closing = true;
    }
}

enum AudioCoder MediaConnection::AvailableAudioCoder(unsigned coderNumber)
{
    return AvailableAudioCoders[coderNumber];
}

enum VideoCoder MediaConnection::AvailableVideoCoder(unsigned coderNumber)
{
    return AvailableVideoCoders[coderNumber];
}

enum AppSharingCoder MediaConnection::AvailableAppSharingCoder(unsigned coderNumber)
{
    return AvailableAppSharingCoders[coderNumber];
}

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

unsigned MediaConnection::AvailableVideoCoderCount()
{
    return AVAILABLE_VIDEO_CODERS;
}

unsigned MediaConnection::AvailableAppSharingCoderCount()
{
    return AVAILABLE_APPSHARING_CODERS;
}

static const char * strConnectionType(enum ConnectionType connectionType) {
    if (connectionType == REMOTE_RTP) return "RemoteRtp";
    if (connectionType == REMOTE_VIDEO_STREAM) return "RemoteVideoStream";
    if (connectionType == LOCAL_VIDEO_STREAM) return "LocalVideoStream";
    if (connectionType == LOCAL_SHARING_STREAM) return "LocalSharingStream";
    if (connectionType == LOCAL_WEBCAM_STREAM) return "LocalWebcamStream";
    return "UnknownConnectionType";
}

void MediaConnection::MediaInitializeComplete(IMedia * const media, class MediaConfig *mediaConfig)
{
    log->Log(LOG_MEDIA, "MediaConnection(%p)::MediaInitializeComplete mediaConfig=%p", this, mediaConfig);

    char cfg[16384];
    class json_io send(cfg);
    char b[8192];
    char * tmp = b;
    word sendBase = 0xffff;

    sendBase = send.add_object(0xffff, 0);
    send.add_string(sendBase, "mt", "AllocChannelResult");
    send.add_string(sendBase, "channel", ch);
    send.add_string(sendBase, "type", strConnectionType(connType));
    word channel = send.add_object(sendBase, "channels");
    send.add_string(channel, "source", "REMOTE");

    if(mediaConfig) {
        this->config = mediaConfig;

        debug->printf("MediaConnection(%p)::MediaInitializeComplete (%s) %s:%d", this, (mediaConfig->type == MediaType::AUDIO ? "AUDIO" : mediaConfig->type == MediaType::VIDEO ? "VIDEO" : "APPSHARING"), (mediaConfig->defAddr ? mediaConfig->defAddr : ""), mediaConfig->defPort);
        class Codec * c;
        for(c = mediaConfig->codecList.front(); c; c = c->goNext()) {
            debug->printf("MediaConnection(%p)::MediaInitializeComplete codec (%s) %s:%d pt=%d", this, AppClientMedia::IdName(OldIdNameCoder, c->coder) ? AppClientMedia::IdName(OldIdNameCoder, c->coder) : "", (c->addr ? c->addr : ""), c->port, c->pt);
        }
        if(mediaConfig->ice) {
            debug->printf("MediaConnection(%p)::MediaInitializeComplete RtcpMux(%d) ICE(%s:%s) Count=%d", this, mediaConfig->ice->rtcpMux,
                ((mediaConfig->ice && mediaConfig->ice->iceUfrag) ? mediaConfig->ice->iceUfrag : ""), ((mediaConfig->ice && mediaConfig->ice->icePwd) ? mediaConfig->ice->icePwd : ""), mediaConfig->ice->count);
            class IceCandidate * c;
            c = mediaConfig->ice->candidateList.front();
            while (c) {
                class IceCandidate * n = c->goNext();
                if ((strcmp(c->addr, "127.0.0.1") == 0) && ((c->type == CHANNEL_CANDIDATE_HOST) || (c->type == CHANNEL_CANDIDATE_SRFLX) || (c->type == CHANNEL_CANDIDATE_RELAY))) {
                    delete c;
                }
                c = n;
            }
            // Filter ICE candidates if media module delivers more than CHANNEL_MAX_CANDIDATE
            if(mediaConfig->ice->count > CHANNEL_MAX_CANDIDATE) {
                istd::list<class IceCandidate> newCandList;
                int totCnt = 0;
                int srflxCnt = 0;
                for(c = mediaConfig->ice->candidateList.front(); c; c = c->goNext()) {
                    if(c->type == CHANNEL_CANDIDATE_RELAY) {
                        // Search for the related HOST and SRFLX candidates
                        class IceCandidate * hC = mediaConfig->ice->candidateList.front();
                        while(hC) {
                            class IceCandidate * nC = hC->goNext();
                            // Host candidate for this relay candidate
                            if(hC->type == CHANNEL_CANDIDATE_HOST && !strcmp(c->relatedAddr, hC->addr) && c->relatedRtpPort == hC->rtpPort && c->relatedRtcpPort == hC->rtcpPort) {
                                hC->remove();
                                newCandList.push_back(hC);
                                totCnt++;
                            }
                            // SRFLX candidate for this relay candidate
                            else if(hC->type == CHANNEL_CANDIDATE_SRFLX && !strcmp(c->relatedAddr, hC->relatedAddr) && c->relatedRtpPort == hC->relatedRtpPort && c->relatedRtcpPort == hC->relatedRtcpPort) {
                                hC->remove();
                                newCandList.push_back(hC);
                                totCnt++;
                                srflxCnt++;
                            }
                            hC = nC;
                        }
                        // Only 1 RELAY candidate
                        c->remove();
                        newCandList.push_back(c);
                        totCnt++;
                        break;
                    }
                }
                c = mediaConfig->ice->candidateList.front();
                while (c) {
                    class IceCandidate * n = c->goNext();
                    if(c->type == CHANNEL_CANDIDATE_SRFLX) {
                        class IceCandidate * hC = mediaConfig->ice->candidateList.front();
                        while (hC) {
                            class IceCandidate * nC = hC->goNext();
                            // Host candidate for this SRFLX candidate
                            if (hC->type == CHANNEL_CANDIDATE_HOST && !strcmp(c->relatedAddr, hC->addr) && c->relatedRtpPort == hC->rtpPort && c->relatedRtcpPort == hC->rtcpPort) {
                                hC->remove();
                                newCandList.push_back(hC);
                                totCnt++;
                                break;
                            }
                            hC = nC;
                        }
                        c->remove();
                        newCandList.push_back(c);
                        totCnt++;
                        srflxCnt++;
                        // Only 2 SRFLX candidates
                        if (srflxCnt == 2) break;
                    }
                    c = n;
                }
                c = mediaConfig->ice->candidateList.front();
                while (c && totCnt < CHANNEL_MAX_CANDIDATE) {
                    class IceCandidate * n = c->goNext();
                    if ((c->type != CHANNEL_CANDIDATE_HOST) && (c->type != CHANNEL_CANDIDATE_SRFLX) && (c->type != CHANNEL_CANDIDATE_RELAY)) {
                        c->remove();
                        newCandList.push_back(c);
                        totCnt++;
                    }
                    c = n;
                }
                c = mediaConfig->ice->candidateList.front();
                while (c && totCnt < CHANNEL_MAX_CANDIDATE) {
                    class IceCandidate * n = c->goNext();
                    if (c->type == CHANNEL_CANDIDATE_HOST) {
                        c->remove();
                        newCandList.push_back(c);
                        totCnt++;
                    }
                    c = n;
                }
                // Remove remaining candidates
                while (mediaConfig->ice->candidateList.front()) delete mediaConfig->ice->candidateList.front();
                // Copy list
                c = newCandList.front();
                while (c) {
                    class IceCandidate * n = c->goNext();
                    c->remove();
                    mediaConfig->ice->candidateList.push_back(c);
                    c = n;
                }
            }
            for(c = mediaConfig->ice->candidateList.front(); c; c = c->goNext()) {
                if(c->type == 0)
                    debug->printf("MediaConnection(%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("MediaConnection(%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);
            }
        }
        switch(mediaConfig->type) {
            case MediaType::AUDIO:
                send.add_bool(channel, "audio_rtcp_mux", mediaConfig->ice ? mediaConfig->ice->rtcpMux : false);
                break;
            case MediaType::VIDEO:
            case MediaType::VIDEO_APPSHARING:
                send.add_bool(channel, "video_rtcp_mux", mediaConfig->ice ? mediaConfig->ice->rtcpMux : false);
                break;
            case MediaType::APPSHARING:
                break;
            case MediaType::MP4_CONTAINER:
            case MediaType::WEBM_VP8_CONTAINER:
            case MediaType::WEBM_VP9_CONTAINER:
            case MediaType::APPSHARING_CONTAINER:
                break;
        }
        word audioIce = send.add_object(channel, AppClientMedia::IdName(IdNameIceType, mediaConfig->type));
        if(mediaConfig->ice) {
            send.add_string(audioIce, "fingerprint", mediaConfig->ice->fingerprint);
            send.add_string(audioIce, "usr", mediaConfig->ice->iceUfrag);
            send.add_string(audioIce, "pwd", mediaConfig->ice->icePwd);
            word baseIceArray = send.add_array(audioIce, "candidate");
            IceCandidate * cd = NULL;
            for(cd = mediaConfig->ice->candidateList.front(); cd; cd = cd->goNext()) {
                word jsonCodec = send.add_object(baseIceArray, 0);
                send.add_string(jsonCodec, "addr", cd->addr);
                send.add_string(jsonCodec, "r_addr", cd->relatedAddr);
                send.add_int(jsonCodec, "rtp", cd->rtpPort, tmp);
                send.add_int(jsonCodec, "rtcp", cd->rtcpPort, tmp);
                send.add_int(jsonCodec, "r_rtp", cd->relatedRtpPort, tmp);
                send.add_int(jsonCodec, "r_rtcp", cd->relatedRtcpPort, tmp);
                send.add_int(jsonCodec, "rtp_prio", cd->rtpPriority, tmp);
                send.add_int(jsonCodec, "rtcp_prio", cd->rtcpPriority, tmp);
                send.add_string(jsonCodec, "type", AppClientMedia::IdName(IdNameIceCandidateType, cd->type));
                send.add_string(jsonCodec, "foundation", cd->foundation);
            }
        }
        word baseCodecArray = send.add_array(channel, "ch");
        for(c = mediaConfig->codecList.front(); c; c = c->goNext()) {
#if 0 // activate h264 or av1 for remote RTP
            if ((connType == ConnectionType::REMOTE_RTP) && (mediaConfig->type == MediaType::VIDEO)) {
                if (c->coder == VP8 || c->coder == H264) continue;
            }
#endif
            word jsonCodec = send.add_object(baseCodecArray, 0);
            send.add_string(jsonCodec, "coder", AppClientMedia::IdName(OldIdNameCoder, c->coder));
            send.add_int(jsonCodec, "number", c->number, tmp);
            send.add_int(jsonCodec, "xmit", c->xmitPacket, tmp);
            send.add_int(jsonCodec, "recv", c->recvPacket, tmp);
            send.add_int(jsonCodec, "rate", c->rate, tmp);
            send.add_string(jsonCodec, "addr", c->addr);
            send.add_int(jsonCodec, "port", c->port, tmp);
            send.add_string(jsonCodec, "mcAddr", c->mcAddr);
            send.add_int(jsonCodec, "mcPort", c->mcPort, tmp);
            send.add_int(jsonCodec, "flags", c->flags, tmp);
            send.add_int(jsonCodec, "pt", c->pt != 0 ? c->pt : DefaultPayloadType(c->coder), tmp);
        }
    }
    mediaSession->AppWebsocketMessageSend(send, cfg);
}

void MediaConnection::MediaConnectResult(IMedia * const media, const char * error)
{
    char cfg[2000];
    class json_io send(cfg);

    debug->printf("MediaConnection(%p)::MediaConnectResult error=%d mediaChannel=%p mediaCodec=%d", this, error, mediaIoChannel, mediaCodec);

    word sendBase = 0xffff;
    sendBase = send.add_object(0xffff, 0);
    send.add_string(sendBase, "mt", "ConnectChannelResult");
    send.add_string(sendBase, "channel", ch);
    send.add_string(sendBase, "type", strConnectionType(connType));
    if (error) {
        send.add_string(sendBase, "state", "failed");
        send.add_string(sendBase, "error", error);
    }
    else {
        send.add_string(sendBase, "state", "connected");
        if(mediaIoChannel) {
            switch(mediaType) {
                case MediaType::AUDIO:
                    (static_cast<class IAudioIo *>(deviceIo))->InitializeChannel(static_cast<class IAudioIoChannel *>(mediaIoChannel),
                        (enum AudioCoder) mediaCodec, false, 20);
                    break;
                case MediaType::VIDEO:
                case MediaType::VIDEO_APPSHARING:
                    (static_cast<class IVideoIoChannel *>(this->mediaIoChannel))->Initialize((void *)mediaSession, (const char *)ch, (enum VideoCoder)mediaCodec, connType);
                    break;
                case MediaType::APPSHARING:
                    (static_cast<class IAppSharingIoChannel *>(this->mediaIoChannel))->Initialize((void *)mediaSession, (const char *)ch, (enum AppSharingCoder)mediaCodec);
                    break;
                case MediaType::MP4_CONTAINER:
                case MediaType::WEBM_VP8_CONTAINER:
                case MediaType::WEBM_VP9_CONTAINER:
                case MediaType::APPSHARING_CONTAINER:
                    break;
            }
            mediaIoChannel->Open();
        }
    }
    mediaSession->AppWebsocketMessageSend(send, cfg);
}

void MediaConnection::MediaRtpSendResult(IMedia * const media)
{
}

void MediaConnection::MediaSctpSendResult(IMedia * const media)
{
    if(mediaIoChannel) mediaIoChannel->SctpSendResult();
}

void MediaConnection::MediaRtpRecvResult(IMedia * const media, void * buf, size_t len, dword timestamp, short sequenceNumberDiff, bool marker)
{
    log->Log(LOG_MEDIA_CHANNEL, "MediaConnection(%p,ch=%p,%p)::MediaRtpRecvResult len=%d timestamp=%x media=%s", this, mediaIoChannel, container, len, timestamp, AppClientMedia::IdName(IdNameMediaType, mediaType));
    // Received RTP goes to container or to mediaIoChannel
    if(mediaIoChannel && !mediaConfiguration->phoneLoad) mediaIoChannel->RtpRecv(buf, len, timestamp, sequenceNumberDiff, marker);
    //if(container) container->PutSample((char *)buf, len, timestamp);
}

void MediaConnection::MediaRtpRecv(IMedia * const media, const char * src_addr, word src_port, dword ssrc, word pt)
{
    char cfg[2000];
    char b[128];
    char * tmp = b;
    class json_io send(cfg);

    if(!closed) {
        word sendBase = 0xffff;
        sendBase = send.add_object(0xffff, 0);
        send.add_string(sendBase, "mt", "RtpRecv");
        send.add_string(sendBase, "channel", ch);
        send.add_string(sendBase, "type", (mediaType == MediaType::VIDEO ? "video" : mediaType == MediaType::AUDIO ? "audio" : "appsharing"));
        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);

        mediaSession->AppWebsocketMessageSend(send, cfg);
    }
}

void MediaConnection::MediaRtpDtmfNearStart(char digit)
{
    char digitString[2];
    char cfg[2000];
    class json_io send(cfg);

    log->Log(LOG_MEDIA, "MediaConnection(%p)::MediaRtpDtmfNearStart ch=%p closed=%d (%s) %c", this, mediaIoChannel, closed, (mediaType == MediaType::VIDEO ? "Video" : mediaType == MediaType::AUDIO ? "Audio" : "AppSharing"), digit);

    if(!closed) {
        word sendBase = 0xffff;
        sendBase = send.add_object(0xffff, 0);
        send.add_string(sendBase, "mt", "RtpDtmfNearStart");
        send.add_string(sendBase, "channel", ch);
        send.add_string(sendBase, "type", (mediaType == MediaType::VIDEO ? "video" : mediaType == MediaType::AUDIO ? "audio" : "appsharing"));
        digitString[0] = digit;
        digitString[1] = '\0';
        send.add_string(sendBase, "digit", digitString);

        mediaSession->AppWebsocketMessageSend(send, cfg);
    }
}

void MediaConnection::MediaRtpDtmfNear(char digit)
{
    char digitString[2];
    char cfg[2000];
    class json_io send(cfg);

    log->Log(LOG_MEDIA, "MediaConnection(%p)::MediaRtpDtmfNear ch=%p closed=%d (%s) %c", this, mediaIoChannel, closed, (mediaType == MediaType::VIDEO ? "Video" : mediaType == MediaType::AUDIO ? "Audio" : "AppSharing"), digit);

    if(!closed) {
        word sendBase = 0xffff;
        sendBase = send.add_object(0xffff, 0);
        send.add_string(sendBase, "mt", "RtpDtmfNear");
        send.add_string(sendBase, "channel", ch);
        send.add_string(sendBase, "type", (mediaType == MediaType::VIDEO ? "video" : mediaType == MediaType::AUDIO ? "audio" : "appsharing"));
        digitString[0] = digit;
        digitString[1] = '\0';
        send.add_string(sendBase, "digit", digitString);

        mediaSession->AppWebsocketMessageSend(send, cfg);
    }
}

void MediaConnection::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(connType == ConnectionType::REMOTE_RTP) {
                    char temp[128];
                    char* tmp = temp;
                    char cfg[1024];
                    class json_io send(cfg);

                    log->Log(LOG_MEDIA, "MediaConnection(%p)::MediaEventReceived CHANNEL_MEDIA_STATISTICS ch=%p (%s)", this, mediaIoChannel, (mediaType == MediaType::VIDEO ? "Video" : mediaType == MediaType::AUDIO ? "Audio" : "AppSharing"));

                    word sendBase = 0xffff;
                    sendBase = send.add_object(0xffff, 0);
                    send.add_string(sendBase, "mt", "MediaStatistics");
                    send.add_string(sendBase, "channel", ch);
                    send.add_string(sendBase, "type", strConnectionType(connType));
                    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);
                    mediaSession->AppWebsocketMessageSend(send, cfg);
                }
                delete s;
            }
        }
    }
    else if(event == MediaEndpointEvent::CHANNEL_MEDIA_ERROR) {
        if (ctx) {
            class EventMediaError * e = (class EventMediaError *)ctx;
            if (e) {
                if (connType == ConnectionType::REMOTE_RTP) {
                    char temp[128];
                    char* tmp = temp;
                    char cfg[1024];
                    class json_io send(cfg);

                    log->Log(LOG_MEDIA, "MediaConnection(%p)::MediaEventReceived CHANNEL_MEDIA_ERROR ch=%p (%s)", this, mediaIoChannel, (mediaType == MediaType::VIDEO ? "Video" : mediaType == MediaType::AUDIO ? "Audio" : "AppSharing"));

                    word sendBase = 0xffff;
                    sendBase = send.add_object(0xffff, 0);
                    send.add_string(sendBase, "mt", "MediaError");
                    send.add_string(sendBase, "channel", ch);
                    send.add_string(sendBase, "type", strConnectionType(connType));
                    send.add_unsigned(sendBase, "code", e->code, tmp);
                    send.add_string(sendBase, "errorText", e->errorText);
                    send.add_string(sendBase, "src", e->src);
                    mediaSession->AppWebsocketMessageSend(send, cfg);
                }
                delete e;
            }
        }
    }
    else if (mediaIoChannel) mediaIoChannel->EventRecv(event, ctx);
}

void MediaConnection::MediaSctpRecvResult(IMedia * const media, void * buf, size_t len)
{
    log->Log(LOG_MEDIA_CHANNEL, "MediaConnection(%p,%p,%p)::MediaSctpRecvResult len=%d media=%s", this, mediaIoChannel, container, len, AppClientMedia::IdName(IdNameMediaType, mediaType));

    if(mediaIoChannel) mediaIoChannel->SctpRecv(buf, len);
    //if (container) container->PutSample((char *)buf, len, 0);
}

void MediaConnection::MediaSctpRecvAck(IMedia * const media, unsigned num)
{
    log->Log(LOG_MEDIA_CHANNEL, "MediaConnection(%p,ch=%p,%p)::MediaSctpRecvAck num=%x media=%s", this, mediaIoChannel, container, num, AppClientMedia::IdName(IdNameMediaType, mediaType));

    if(mediaIoChannel) mediaIoChannel->SctpRecvAck(num);
}

void MediaConnection::MediaCloseComplete(IMedia * const media)
{
    char cfg[2000];
    class json_io send(cfg);

    log->Log(LOG_MEDIA, "MediaConnection(%p)::MediaCloseComplete ch=%p closed=%d (%s)", this, mediaIoChannel, closed, (mediaType == MediaType::VIDEO ? "Video" : mediaType == MediaType::AUDIO ? "Audio" : "AppSharing"));

    if(!closed) {
        word sendBase = 0xffff;
        sendBase = send.add_object(0xffff, 0);
        send.add_string(sendBase, "mt", "ReleaseChannelResult");
        send.add_string(sendBase, "channel", ch);
        send.add_string(sendBase, "type", strConnectionType(connType));
        mediaSession->AppWebsocketMessageSend(send, cfg);
    }
    
    if(mediaIoChannel) {
        mediaIoChannel->Close();
        if(mediaType == MediaType::AUDIO) MediaIoCloseComplete(mediaIoChannel);
    }
    else {
        debug->printf("MediaConnection(%p)::MediaCloseComplete mediaIoChannel is NULL!", this);
        MediaIoCloseComplete(mediaIoChannel);
    }
}

void MediaConnection::MediaIoRtpForward(const void * buf, size_t len, dword timestamp, short sequenceNumberDiff, bool marker, enum MediaType mediaType)
{
    log->Log(LOG_MEDIA, "MediaConnection(%p)::MediaIoRtpForward len=%d old=%u", this, len, timestamp);

    if(!mediaConfiguration->phoneLoad) media->RtpForward(buf, len, (dword)timestamp, sequenceNumberDiff, marker);
}

void MediaConnection::MediaIoSendFIR()
{
    log->Log(LOG_MEDIA, "MediaConnection(%p)::MediaIoSendFIR", this);

    media->SendFIR();
}

void MediaConnection::MediaIoRtcpSend(const void * buf, size_t len, enum MediaType mediaType)
{
    //log->Log(LOG_MEDIA, "MediaConnection(%p)::MediaIoRtcpSend len=%d", this, len);

    if(!mediaConfiguration->phoneLoad) media->RtcpSend(buf, len);
}

void MediaConnection::MediaIoRtpSend(const void * buf, size_t len, dword timestamp, enum MediaType mediaType, bool isConference)
{
    //log->Log(LOG_MEDIA, "MediaConnection(%p)::MediaIoRtpSend len=%d", this, len);

    if(!mediaConfiguration->phoneLoad) media->RtpSend(buf, len, timestamp, mediaType, isConference);
}

void MediaConnection::MediaIoSctpSend(const void * buf, size_t len, unsigned num)
{
    media->SctpSend(buf, len, num);
}

void MediaConnection::CloseMediaConnection()
{
    log->Log(LOG_MEDIA, "MediaConnection(%p)::CloseMediaConnection media=%p closed=%d", this, media, closed);

    if(media && !closed) media->Close(); 
    closed = true; 
}

void MediaConnection::MediaIoCloseComplete(class IMediaIoChannel * const mediaIoChannel)
{
    log->Log(LOG_MEDIA, "MediaConnection(%p)::MediaIoCloseComplete ch=%p", this, mediaIoChannel);

    delete this;
}


AppClientMediaSrc::AppClientMediaSrc(class AppClientMediaSession * session, const char * src, const char * app)
{
    this->session = (void *)session;
    this->src = _strdup(src);
    this->app = _strdup(app);
}

AppClientMediaSrc::~AppClientMediaSrc()
{
    if (src) free(src);
    if (app) free(app);
}
