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

#include <platform.h>
#include "sdk/common/os/iomux.h"

#include "sdk/common/interface/time.h"
#include "sdk/common/interface/socket.h"
#include "sdk/common/interface/media.h"
#include "sdk/common/interface/appsharing.h"
#include "sdk/common/interface/video_format_conversion.h"
#include "sdk/common/interface/local_storage.h"
#include "common/ilib/str.h"
#include "common/ilib/json.h"

#include "video_endpoint.h"
#include "video_io.h"

static const int __VIDEO_IO_SHUTDOWN_MSG = 1;

#if defined _WINDOWS
class IVideoIoChannel* IVideoIoChannel::Create(class IIoMux* const iomux, class IVideoIo* mediaIo, class UMediaIoChannel* const user, class IInstanceLog* log)
{
    return new VideoIoChannel(iomux, mediaIo, user, log);
}
#endif

class IVideoIo * IVideoIo::Create(class IIoMux * const iomux, class IInstanceLog * log, class MediaConfiguration* mediaConfiguration)
{
    return new VideoIo(iomux, log, mediaConfiguration);
}

VideoIo::VideoIo(class IIoMux * const iomux, class IInstanceLog * log, class MediaConfiguration* mediaConfiguration) :
    log(log),
    iomux(iomux)
{
    if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::VideoIo", this);

    this->mediaConfiguration = mediaConfiguration;
    this->videoIoUser = NULL; 
    this->deviceIoUser = NULL;
    this->appSharingIoUser = NULL;
    this->selectDeviceUser = NULL;
    this->rendererWebcam = NULL;
    this->deletedRendererWebcam = NULL;
    this->remoteServices = false;
    this->ctx = NULL;
    this->execCnt = 0;
    this->shutDown = false;
    this->fullIntraFrameTimer = new ITimer(iomux, this);
    this->restartWebcamTimer = new ITimer(iomux, this);
    this->restartWebcam = NULL;
    if (mediaConfiguration->isRunningTS) this->webcamProvider = NULL;
    else this->webcamProvider = IWebcamProvider::Create(iomux, this, this, log);
    this->videoRenderProvider = IRenderVideoProvider::Create(iomux, this, log);
    this->imageBuffer = NULL;
    this->subscribe = false;
    this->appSharingFormat.height = 0;
    this->appSharingFormat.width = 0;
    this->appSharingEncoder = NULL;
    this->deletedAppSharingEncoder = NULL;
    this->isSharing = 0;
    this->remoteVideoRunning = false;
    this->remoteVideoPriority = 0;
    this->appSharingVideoCoder = VideoCoder::VP8;
    this->videoSession = NULL;
    this->softphoneApp = NULL;
    this->selectedDeviceRemoved = false;
    this->remoteMediaAppSharingUser = NULL;

    ReadSelectedDevices();
}

VideoIo::~VideoIo()
{
    log->Log(LOG_VIDEO, "VideoIo(%p)::~VideoIo count=%d,%d", this, webcams.empty());

    if (softphoneApp) free(softphoneApp);
    while(!conferenceSamples.empty()) delete conferenceSamples.front();
    while(!webcams.empty()) delete webcams.front();
    delete fullIntraFrameTimer;
    delete restartWebcamTimer;
}

class IVideoIoChannel *VideoIo::CreateChannel(class UMediaIoChannel * const user)
{
    return IVideoIoChannel::Create(iomux, static_cast<class IVideoIo *>(this), user, log);
}

void VideoIo::Initialize(class UVideoIo * const user, class UDeviceIo * const deviceIoUser, class UAppSharingIo* const appSharingIoUser)
{
    log->Log(LOG_VIDEO, "VideoIo(%p)::Initialize %p,%p", this, user, deviceIoUser);
    this->videoIoUser = user; 
    this->deviceIoUser = deviceIoUser;
    this->appSharingIoUser = appSharingIoUser;
}

void VideoIo::Close()
{
    log->Log(LOG_VIDEO, "VideoIo(%p)::Close imageBuffer=%p ioChannels:%d", this, imageBuffer, !ioChannels.empty());

    this->shutDown = true;

    WriteSelectedDevices();

    fullIntraFrameTimer->Cancel();
    if(this->webcamProvider) this->webcamProvider->Close();
    this->videoRenderProvider->Close();
    if (rendererWebcam) {
        deletedRendererWebcam = rendererWebcam;
        rendererWebcam->Close();
        rendererWebcam = NULL;
    }
    if(videoSession) {
        if(videoSession->localEncoder) {
            videoSession->localEncoder->Close();
            videoSession->deletedLocalEncoder = videoSession->localEncoder;
            videoSession->localEncoder = NULL;
        }
        if(videoSession->remoteEncoder) {
            videoSession->remoteEncoder->Close();
            videoSession->deletedRemoteEncoder = videoSession->remoteEncoder;
            videoSession->remoteEncoder = NULL;
        }
    }
    if(imageBuffer) {
        if (subscribe) imageBuffer->UnsubscribeApplications();
        subscribe = false;
        imageBuffer->CloseImageBuffer();
    }
    restartWebcamTimer->Cancel();
}

void VideoIo::CloseComplete()
{
    debug->printf("VideoIo(%p)::CloseComplete imageBuffer=%p", this, imageBuffer);

    if(imageBuffer) delete imageBuffer;
    imageBuffer = NULL;
    TryClose();
}

void VideoIo::TryClose()
{
    debug->printf("VideoIo(%p)::TryClose session=%p imageBuffer=%p channels:%d providers:%p,%p encoders:%p,%p", this, videoSession, imageBuffer, !ioChannels.empty(), webcamProvider, videoRenderProvider, appSharingEncoder, deletedAppSharingEncoder);

    if(!ioChannels.empty()) return;
    if(videoSession) {
        if(videoSession->localEncoder || videoSession->remoteEncoder || videoSession->deletedLocalEncoder || videoSession->deletedRemoteEncoder) return;
    }
    if(appSharingEncoder || deletedAppSharingEncoder) return;
    if(webcamProvider || videoRenderProvider || imageBuffer) return;
    iomux->SetExec(this, (void *)&__VIDEO_IO_SHUTDOWN_MSG);
    execCnt++;
}

void VideoIo::ReadSelectedDevices()
{
    while (selectedDevices.empty() == false) delete selectedDevices.front();

    char* videoSelectedDevicesStr = ILocalStorage::GetItem("videoSelectedDevices");
    if (videoSelectedDevicesStr) {
        if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::ReadSelectedDevices (%s)", this, videoSelectedDevicesStr);
        class json_io jsonDevices(videoSelectedDevicesStr);
        if (jsonDevices.decode()) {
            word c_last = 0, t, end = 0xFFFF;
            word base = jsonDevices.get_object(JSON_ID_ROOT, 0);
            word devsArray = jsonDevices.get_array(base, "devices");
            while ((t = jsonDevices.get_object(devsArray, c_last)) != end) {
                if (jsonDevices.get_string(t, "device") && jsonDevices.get_string(t, "app")) {
                    class SelectedDeviceIo* dev = new class SelectedDeviceIo(jsonDevices.get_string(t, "app"), jsonDevices.get_string(t, "device"), jsonDevices.get_bool(t, "remote"));
                    selectedDevices.push_back(dev);
                }
            }
        }
        else if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::ReadSelectedDevices failed to decode json", this);
        free(videoSelectedDevicesStr);
    }
}

void VideoIo::WriteSelectedDevices()
{
    int jsonSize = 128;
    int intSize = 0;
    class SelectedDeviceIo* dev = selectedDevices.front();
    while (dev) {
        jsonSize += strlen(dev->app) + strlen(dev->device) + 40;   // {"app":"","device":"","mode":1},
        intSize += 32;
        dev = dev->goNext();
    }
    char* j = (char*)malloc(jsonSize);
    class json_io jsonDevices(j);
    word base = jsonDevices.add_object(0xffff, 0);
    word a = jsonDevices.add_array(base, "devices");
    dev = selectedDevices.front();
    while (dev) {
        word e = jsonDevices.add_object(a, "selectedDevice");
        jsonDevices.add_string(e, "app", dev->app);
        jsonDevices.add_string(e, "device", dev->device);
        jsonDevices.add_bool(e, "remote", dev->isRemote);
        dev = dev->goNext();
    }
    jsonDevices.encode();
    if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::WriteSelectedDevices Devices json sizes=%d,%d (%s)", this, jsonSize, intSize, j);
    ILocalStorage::SetItem("videoSelectedDevices", j);
    free(j);
}

void VideoIo::SelectDevice(const char *app, const char *deviceId)
{
    if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::SelectDevice app=%s deviceId=%s", this, app, deviceId);

    class SelectedDeviceIo * dev = NULL;
    if (!selectedDevices.empty()) {
        // Delete device if already exists and allow a maximum number of 10 devices pro SoftphoneApp
        int numSrcDevices = 0;
        dev = selectedDevices.front();
        while(dev) {
            class SelectedDeviceIo * next = dev->goNext();
            if (dev->app && app && !strcmp(dev->app, app)) {
                if ((dev->device && deviceId && !strcmp(dev->device, deviceId)) || (numSrcDevices >= 10)) {
                    dev->remove();
                    delete dev;
                }
                else numSrcDevices++;
            }
            dev = next;
        }
    }
    // Push the new device at the front of the list
    dev = new class SelectedDeviceIo(app, deviceId);
    selectedDevices.push_front(dev);
    // Check if device is still available
    class WebcamHelper * w;
    if ((w = FindDevice(dev->device)) != NULL) {
        class WebcamHelper* temp;
        for (temp = webcams.front(); temp; temp = temp->goNext()) {
            temp->isSelected = false;
        }
        dev->available = true;
        if(selectDeviceUser) selectDeviceUser->SelectVideoDevice(app, deviceId);
        WriteSelectedDevices();
        w->isSelected = true;
    }
}

void VideoIo::ChangeDevice(const char *app, const char *deviceId)
{
    log->Log(LOG_VIDEO, "VideoIo(%p)::ChangeDevice app=%s deviceId=%s", this, app, deviceId);

    // Check if device is still available
    if (FindDevice(deviceId)) {
        if (selectDeviceUser) selectDeviceUser->ChangeVideoDevice(app, deviceId);
    }
}

const char * VideoIo::GetDeviceIo(const char * app)
{
    log->Log(LOG_VIDEO, "VideoIo(%p)::GetDeviceIo app=%s", this, app ? app : "");

    if (softphoneApp) free(softphoneApp);
    softphoneApp = _strdup(app);

    class SelectedDeviceIo * dev = NULL;
    if (!selectedDevices.empty()) {
        for (dev = selectedDevices.front(); dev; dev = dev->goNext()) {
            if (dev->available && dev->app && app && !strcmp(dev->app, app)) {
                break;
            }
        }
    }
    // Check if device still available
    if (dev && FindDevice(dev->device)) {
        return dev->device;
    }
    // Return first video device
    if (!webcams.empty()) {
        class WebcamHelper * w = webcams.front();
        for (w = webcams.front(); w; w = w->goNext()) {
            if (w->webcam && w->webcam->GetWebcamId()) {
                log->Log(LOG_VIDEO, "VideoIo(%p)::GetDeviceIo Fallback to another video device: %s", this, w->webcam->GetWebcamName());
                return w->webcam->GetWebcamId();
            }
        }
    }
    return NULL;
}

class WebcamHelper * VideoIo::FindDevice(const char * deviceId)
{
    if (webcams.empty() == false) {
        class WebcamHelper * dev = webcams.front();
        while (dev) {
            if(dev->webcam && dev->webcam->GetWebcamId() && !strcmp(deviceId, dev->webcam->GetWebcamId())) return dev;
            else if(dev->deviceId && !strcmp(deviceId, dev->deviceId)) return dev;
            dev = dev->goNext();
        }
    }
    if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::FindDevice() Not found deviceId=%s (empty=%u)", this, deviceId, webcams.empty());
    return NULL;
}

void VideoIo::WebcamProviderCloseResult()
{
    if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::WebcamProviderCloseResult webcams=%d", this, !webcams.empty());

    delete webcamProvider;
    webcamProvider = NULL;
    TryClose();
}

void VideoIo::RenderVideoProviderCloseResult()
{
    if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::RenderVideoProviderCloseResult", this);

    delete videoRenderProvider;
    videoRenderProvider = NULL;
    TryClose();
}

void VideoIo::RenderVideoStreamCloseResult(class IRenderVideoStream* renderer)
{
    log->Log(LOG_VIDEO, "VideoIo(%p)::RenderVideoStreamCloseResult %p,%p,%p shutDown=%d", this, renderer, this->rendererWebcam, this->deletedRendererWebcam, this->shutDown);

    if (renderer == deletedRendererWebcam) {
        deletedRendererWebcam = NULL;
        delete renderer;
    }
    if(this->shutDown) TryClose();
}

void VideoIo::PacketLostIndication(class VideoIoChannel* videoIoChannel)
{
    if (isSharing && appSharingEncoder) appSharingEncoder->PacketLostIndication();
}

void VideoIo::RtpNackMessage(class VideoIoChannel* videoIoChannel, void * ctx)
{
    if (isSharing && appSharingEncoder) appSharingEncoder->RtpNackMessage(ctx);
}

void VideoIo::KeyFrameSequences(class VideoIoChannel* videoIoChannel, void* ctx)
{
    if (isSharing && appSharingEncoder) appSharingEncoder->KeyFrameSequences(ctx);
}

void VideoIo::FullIntraRequest(class VideoIoChannel * videoIoChannel)
{
    log->Log(LOG_VIDEO, "VideoIo(%p)::FullIntraRequest webcams=%d appSharing=%d ch=%p(%d) videoSession=%p (%p,%p)", this, !webcams.empty(), isSharing, videoIoChannel, videoIoChannel->GetConnectionType(), videoSession, videoSession ? videoSession->localEncoder : 0, videoSession ? videoSession->remoteEncoder : 0);

    if (videoIoChannel->GetConnectionType() == REMOTE_RTP) {
        if (!ioChannels.empty()) {
            class VideoIoChannel* c = NULL;
            for (c = ioChannels.front(); c; c = c->goNext()) {
                if (c->GetConnectionType() == ConnectionType::LOCAL_WEBCAM_STREAM) {
                    c->FullIntraRequest();
                }
            }
        }
    }
    if(isSharing && appSharingEncoder) appSharingEncoder->FullIntraRequest();
    if(videoSession) {
        if(videoSession->localEncoder || videoSession->remoteEncoder) {
            if(videoSession->localEncoder) videoSession->localEncoder->FullIntraRequest();
            if(videoSession->remoteEncoder) videoSession->remoteEncoder->FullIntraRequest();
        }
        else {
            if(!webcams.empty()) {
                class WebcamHelper * w;
                for(w = webcams.front(); w; w = w->goNext()) {
                    if(w->outputFormat) w->webcam->FullIntraRequest();
                }
            }
        }
    }
}

void VideoIo::Add(class VideoIoChannel * videoIoChannel)
{
    log->Log(LOG_VIDEO, "VideoIo(%p)::Add %p channels=%d type=%d coder=%d context=%p", this, videoIoChannel, !ioChannels.empty(), videoIoChannel->GetConnectionType(), videoIoChannel->GetVideoCoder(), videoIoChannel->GetContext());

    // New channel added, Request the webcam encoders to provide a new key frame
    FullIntraRequest(videoIoChannel);
    
    bool conference = false;
    class VideoIoChannel * c = NULL;
    if(!ioChannels.empty()) {
        for(c = ioChannels.front(); c; c = c->goNext()) {
            // More channels with same context (session) and not local -> conference
            if(c->GetConnectionType() == ConnectionType::REMOTE_RTP && videoIoChannel->GetConnectionType() == ConnectionType::REMOTE_RTP) {
                conference = true;
                c->conference = true;
            }
            // same channel and context, one rtp, one remote local and different video coders.
            else if (!strcmp(c->GetChannelId(), videoIoChannel->GetChannelId()) && c->GetConnectionType() != ConnectionType::LOCAL_VIDEO_STREAM && videoIoChannel->GetConnectionType() != ConnectionType::LOCAL_VIDEO_STREAM && c->GetVideoCoder() != videoIoChannel->GetVideoCoder()) {
                if (c->GetConnectionType() == ConnectionType::REMOTE_RTP) c->decodeStream = true;
                else videoIoChannel->decodeStream = true;
            }
            // same channel or context, video from webcam, one remote local and different video coders. PhoneApp
            else if ((c->GetContext() == videoIoChannel->GetContext()) && (c->GetVideoCoder() != videoIoChannel->GetVideoCoder())) {
                if (c->GetConnectionType() == ConnectionType::REMOTE_RTP && videoIoChannel->GetConnectionType() == ConnectionType::LOCAL_WEBCAM_STREAM) {
                    videoIoChannel->decodeStream = true;
                    videoIoChannel->FullIntraRequest();
                }
                else if (c->GetConnectionType() == ConnectionType::LOCAL_WEBCAM_STREAM && videoIoChannel->GetConnectionType() == ConnectionType::REMOTE_RTP) {
                    c->decodeStream = true;
                    c->FullIntraRequest();
                }
            }
            // SoftphoneApp -> ctx is Null
            else if (c->GetVideoCoder() != videoIoChannel->GetVideoCoder()) {
                if (c->GetContext() == NULL && c->GetConnectionType() == ConnectionType::REMOTE_RTP && videoIoChannel->GetConnectionType() == ConnectionType::LOCAL_WEBCAM_STREAM) {
                    videoIoChannel->decodeStream = true;
                    videoIoChannel->FullIntraRequest();
                }
                else if (c->GetConnectionType() == ConnectionType::LOCAL_WEBCAM_STREAM && videoIoChannel->GetContext() == NULL && videoIoChannel->GetConnectionType() == ConnectionType::REMOTE_RTP) {
                    c->decodeStream = true;
                    c->FullIntraRequest();
                }
            }
            else if (!strcmp(c->GetChannelId(), videoIoChannel->GetChannelId()) && c->GetConnectionType() == ConnectionType::REMOTE_RTP && videoIoChannel->GetConnectionType() == ConnectionType::REMOTE_VIDEO_STREAM) {
                c->FullIntraRequest();
            }
        }
    }
    ioChannels.push_back(videoIoChannel);
    if(conference) {
        videoIoChannel->conference = true;
    }


    fullIntraFrameTimer->Start(1000);
}

void VideoIo::TimerOnTimeout(ITimer * timer)
{
    log->Log(LOG_VIDEO, "VideoIo(%p)::TimerOnTimeout %p", this, videoSession);

    if (timer == restartWebcamTimer) {
        if (restartWebcam) {
            class WebcamHelper* w = NULL;
            for (w = webcams.front(); w; w = w->goNext()) {
                if (w == restartWebcam) break;
            }
            if (!w) {
                if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::TimerOnTimeout webcam was removed! restartWebcam=%p", this, restartWebcam);
                restartWebcam = NULL;
                return;
            }
            if ((restartWebcam->outputFormat = restartWebcam->webcam->Start(restartWebcam->width, restartWebcam->height, mediaConfiguration->hdVideo)) == NULL) {
                if (deviceIoUser) deviceIoUser->MediaIoDeviceFailed(this, restartWebcam->webcam->GetContext(), restartWebcam->webcam->GetWebcamId());
                if (!remoteDeviceUsers.empty()) {
                    class UDeviceIoHelper* h = remoteDeviceUsers.front();
                    while (h) {
                        h->user->MediaIoDeviceFailed(this, restartWebcam->webcam->GetContext(), restartWebcam->webcam->GetWebcamId());
                        h = h->goNext();
                    }
                }
            }
        }
        return;
    }

    if (!shareApps.empty()) {
        class AppSharingApp* shareApp;
        for (shareApp = shareApps.front(); shareApp; shareApp = shareApp->goNext()) {
            if (shareApp->captureTimer == timer) {
                ulong64 currentImageBufferSample = ITime::TimeStampMilliseconds();
                imageBuffer->Capture(shareApp->id, NULL, AppSharingCoder::VIDEO_CODER);
                shareApp->captureTimer->Start(mediaConfiguration ? mediaConfiguration->appSharingCaptureTimer : 50);
                shareApp->lastImageBufferSample = currentImageBufferSample;
                return;
            }
        }
    }

    // New channel added, Request the webcam encoders to provide a new key frame
    // Video was shown locally but not sent to remote peers
    if (videoSession) {
        if (videoSession->localEncoder || videoSession->remoteEncoder) {
            if (videoSession->localEncoder) videoSession->localEncoder->FullIntraRequest();
            if (videoSession->remoteEncoder) videoSession->remoteEncoder->FullIntraRequest();
        }
        else {
            if(!webcams.empty()) {
                class WebcamHelper * w;
                for(w = webcams.front(); w; w = w->goNext()) {
                    if(w->outputFormat) w->webcam->FullIntraRequest();
                }
            }
        }
        fullIntraFrameTimer->Start(5000);
    }
}

void VideoIo::Remove(class VideoIoChannel * videoIoChannel)
{
    log->Log(LOG_VIDEO, "VideoIo(%p)::Remove=%p front=%p isSharing=%d type=%d", this, videoIoChannel, (ioChannels.empty() == false ? ioChannels.front() : NULL), isSharing, videoIoChannel->GetConnectionType());
   
    if(!ioChannels.empty()) {
        bool found = false;
        int numChannels = 0;
        class VideoIoChannel * ch = ioChannels.front();
        while(ch) {
            class VideoIoChannel * next = ch->goNext();
            if(ch == videoIoChannel) {
                if(ch->conference) {
                    ch->conference = false;
                }
                if(!conferenceSamples.empty()) {
                    for(class ConferenceSample * cs = conferenceSamples.front(); cs; cs = cs->goNext()) {
                        if(cs->videoIoChannel == ch) {
                            cs->remove();
                            delete cs;
                            break;
                        }
                    }
                }
                ch->remove();
                found = true;
            }
            else if(ch->GetConnectionType() == ConnectionType::REMOTE_RTP) numChannels++;
            ch = next;
        }
        // Stop conference in only one channel remaining in the conference
        if(found) {
            if (numChannels == 1) {
                class VideoIoChannel* ch = ioChannels.front();
                while (ch) {
                    if (ch->GetConnectionType() == ConnectionType::REMOTE_RTP) {
                        ch->conference = false;
                        // Disconnecting a channel does not send a RemoteVideoPriority JSON message
                        // remoteVideoPriority would remain on
                        if(ch->GetRemoteVideoPriority() == false) remoteVideoPriority = 0;
                        if(ch->forwardingChannel) {
                            free(ch->forwardingChannel);
                            ch->forwardingChannel = NULL;
                        }
                        break;
                    }
                    ch = ch->goNext();
                }
            }
            else if (numChannels == 0) {
                remoteVideoRunning = false;
                remoteVideoPriority = 0;
                if (isSharing) UnshareAllApplications(NULL);
            }
        }
    }
    if (this->shutDown) TryClose();
}

void VideoIo::EncodeResult(class IMediaEncoder * encoder, const void * buf, int len, dword timestamp, dword duration)
{
    log->Log(LOG_VIDEO, "VideoIo(%p)::EncodeResult encoder=%p buf=%p len=%d timestamp=%u duration=%d ts=%d", this, encoder, buf, len, timestamp, duration, mediaConfiguration->isRunningTS);

    if (isSharing) {
        if (!ioChannels.empty()) {
            class VideoIoChannel* ch;
            for (ch = ioChannels.front(); ch; ch = ch->goNext()) {
                if (ch->GetConnectionType() != ConnectionType::REMOTE_VIDEO_STREAM) {
                    ch->VideoSample(buf, len, timestamp, duration, MediaType::VIDEO_APPSHARING);
                }
            }
        }
        if (mediaConfiguration->isRunningTS && remoteMediaAppSharingUser) remoteMediaAppSharingUser->EncodeResult(encoder, buf, len, timestamp, duration);
    }
    else if(videoSession) {
        if((videoSession->localEncoder == encoder || videoSession->remoteEncoder == encoder)) {
            // Send Video to remote peer (and local WebRTC, but not local remote WebRTC!) if no conference (if conference video must be mixed) 
            if(!ioChannels.empty()) {
                class VideoIoChannel * ch;
                for(ch = ioChannels.front(); ch; ch = ch->goNext()) {
                    if(!ch->conference && (ch->GetConnectionType() != ConnectionType::REMOTE_VIDEO_STREAM)) {
                        if (videoSession->remoteEncoder == NULL) {                      // Same video encoder local and remote, REMOTE_RTP and LOCAL_VIDEO_STREAM
                            ch->VideoSample(buf, len, timestamp, duration, MediaType::VIDEO);
                        }
                        else if (videoSession->remoteEncoder == encoder && ch->GetConnectionType() == ConnectionType::REMOTE_RTP) {
                            ch->VideoSample(buf, len, timestamp, duration, MediaType::VIDEO);
                            break;
                        }
                        else if (videoSession->localEncoder == encoder && ch->GetConnectionType() == ConnectionType::LOCAL_VIDEO_STREAM) {
                            ch->VideoSample(buf, len, timestamp, duration, MediaType::VIDEO);
                            break;
                        }
                    }
                }
            }
            return;
        }
    }
}

void VideoIo::MediaEncoderCloseResult(class IMediaEncoder * encoder)
{
    log->Log(LOG_VIDEO, "VideoIo(%p)::MediaEncoderCloseResult encoder=%p deleted=%p reconfigure=%d remoteVideoRunning=%d", this, encoder, videoSession ? videoSession->deletedLocalEncoder : NULL, videoSession ? videoSession->reconfigureLocalEncoder : false, remoteVideoRunning);

    if (deletedAppSharingEncoder == encoder) {
        if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::MediaEncoderCloseResult appSharingEncoder=%p", this, deletedAppSharingEncoder);
        deletedAppSharingEncoder = NULL;
    }
    else if(videoSession) {
        if(videoSession->deletedLocalEncoder == encoder) {
            videoSession->deletedLocalEncoder = NULL;
            if(videoSession->reconfigureLocalEncoder) {
                class WebcamHelper* w = NULL;
                for(w = webcams.front(); w; w = w->goNext()) {
                    if(w->outputFormat) break;
                }
                if(w) {
                    videoSession->localEncoder = IMediaEncoder::Create(iomux, this, log, (enum VideoCoder)videoSession->localVideoCoder);
                    videoSession->encoderInit = false;
                }
            }
        }
        else if(videoSession->deletedRemoteEncoder == encoder) {
            videoSession->deletedRemoteEncoder = NULL;
            remoteVideoRunning = false;
        }
    }
    delete encoder;
    if(this->shutDown) TryClose();
}

bool VideoIo::HasConferenceSamples(class VideoIoChannel * ch)
{
    if(!conferenceSamples.empty()) {
        class ConferenceSample * cs;
        for(cs = conferenceSamples.front(); cs; cs = cs->goNext()) {
            // Samples from other channels in the same context
            if(cs->videoIoChannel != ch) return true;
        }
    }
    return false;
}

void VideoIo::ImageBufferSample(unsigned int appId, const void* buf, int len, int width, int height, dword timestamp, dword duration)
{
    log->Log(LOG_VIDEO,"VideoIo(%p,%p)::ImageBufferSample appId=%d %p len=%d timestamp=%u duration=%d", this, videoIoUser, appId, buf, len, timestamp, duration);

    if (!isSharing) return;

    if (!appSharingEncoder || width != appSharingFormat.width || height != appSharingFormat.height) {
        struct VideoRatio fr;
        appSharingFormat.captureWidth = width;
        appSharingFormat.captureHeight = height;
        appSharingFormat.width = width;
        appSharingFormat.stride = width;
        appSharingFormat.height = height;
        appSharingFormat.interlace = VIDEO_INTERLACE_Progressive;
        appSharingFormat.format = VideoFormat::VIDEO_FORMAT_IYUV;
        appSharingFormat.sampleSize = width * height + ((width * height) >> 1);
        fr.numerator = 20;
        fr.denominator = 1;
        appSharingFormat.frameRate = fr;
        fr.numerator = 1;
        fr.denominator = 1;
        appSharingFormat.aspectRatio = fr;
        appSharingFormat.averageBitrate = 1000000;
        appSharingFormat.targetBitrate = 1000;
        appSharingFormat.hdVideo = false;
        appSharingFormat.appSharing = true;
        if (!appSharingEncoder) {
            if(videoSession && videoSession->remoteEncoder) {
                videoSession->remoteEncoder->Close();
                videoSession->deletedRemoteEncoder = videoSession->remoteEncoder;
                videoSession->remoteEncoder = NULL;
            }
            appSharingEncoder = IMediaEncoder::Create(iomux, this, log, this->appSharingVideoCoder);
            appSharingEncoder->Initialize(&appSharingFormat);
        }
    }
    appSharingEncoder->Encode(buf, len, timestamp, duration, &appSharingFormat);
    if (!shareApps.empty()) {
        class AppSharingApp* shareApp;
        for (shareApp = shareApps.front(); shareApp; shareApp = shareApp->goNext()) {
            if (shareApp->id == appId) {
                ulong64 currentImageBufferSample = ITime::TimeStampMilliseconds();
                // Timer arrived but was still encoding
                if (currentImageBufferSample - shareApp->lastImageBufferSample > 50) {
                    imageBuffer->Capture(shareApp->id, NULL, AppSharingCoder::VIDEO_CODER);
                    shareApp->captureTimer->Start(mediaConfiguration ? mediaConfiguration->appSharingCaptureTimer : 50);
                }
                shareApp->lastImageBufferSample = currentImageBufferSample;
                break;
            }
        }
    }
}

void VideoIo::WebcamSample(class IWebcam * const webcam, const void * buf, int len, dword timestamp, dword duration, const struct VideoFrameFormat * format)
{
    log->Log(LOG_VIDEO, "VideoIo(%p,%p)::WebcamSample %p len=%d timestamp=%u duration=%d isSharing=%d videoSession=%p", this, videoIoUser, buf, len, timestamp, duration, isSharing, videoSession);

    if (isSharing) return;

    if(!webcams.empty()) {
        class WebcamHelper * w = webcams.front();
        for(w = webcams.front(); w; w = w->goNext()) {
            if(w->webcam == webcam) {
                if(videoSession) {
                    if(videoSession->deviceId && !strcmp(webcam->GetWebcamId(), videoSession->deviceId)) {
                        // Encode Sample for local WebRTC 
                        if(videoSession->localEncoder) {
                            if(mediaConfiguration->hdVideo && videoSession->remoteEncoder) {
                                int width = 320, height = 240;
                                if (format->format == VideoFormat::VIDEO_FORMAT_IYUV) {
                                    if (videoSession->encoderInit == false) {
                                        videoSession->localEncoder->Initialize(format);
                                        videoSession->encoderInit = true;
                                    }
                                    videoSession->localEncoder->Encode(buf, len, timestamp, duration, format);
                                    if (rendererWebcam) rendererWebcam->DisplayFrame(buf, len, (VideoFrameFormat*)format);
                                }
                                else if(format->format == VideoFormat::VIDEO_FORMAT_YUY2) {
                                    byte* newSample = (byte*)malloc((width * height) << 1);
                                    IVideoScaleSample::ScaleYUY2(newSample, width << 1, width, height, (byte*)buf, format->stride, format->width, format->height);
                                    VideoFrameFormat nf;
                                    nf.averageBitrate = format->averageBitrate;
                                    nf.targetBitrate = format->targetBitrate;
                                    nf.sampleSize = (width * height) << 1;
                                    nf.format = format->format;
                                    nf.interlace = format->interlace;
                                    nf.frameRate.denominator = format->frameRate.denominator;
                                    nf.frameRate.numerator = format->frameRate.numerator;
                                    nf.aspectRatio.denominator = format->aspectRatio.denominator;
                                    nf.aspectRatio.numerator = format->aspectRatio.numerator;
                                    nf.stride = width << 1;
                                    nf.width = width;
                                    nf.height = height;
                                    nf.captureWidth = width;
                                    nf.captureHeight = height;
                                    nf.hdVideo = format->hdVideo;
                                    nf.appSharing = format->appSharing;
                                    if(videoSession->encoderInit == false) {
                                        videoSession->localEncoder->Initialize(&nf);
                                        videoSession->encoderInit = true;
                                    }
                                    videoSession->localEncoder->Encode(newSample, nf.sampleSize, timestamp, duration, &nf);
                                    if(rendererWebcam) rendererWebcam->DisplayFrame(newSample, nf.sampleSize, &nf);
                                    free(newSample);
                                }
                                else if(format->format == VideoFormat::VIDEO_FORMAT_RGB24) {
                                    byte* newSample = (byte*)malloc((width * height) * 3);
                                    IVideoScaleSample::ScaleRGB24(newSample, width * 3, width, height, (byte*)buf, format->stride, format->width, format->height);
                                    VideoFrameFormat nf;
                                    nf.averageBitrate = format->averageBitrate;
                                    nf.targetBitrate = format->targetBitrate;
                                    nf.sampleSize = (width * height) * 3;
                                    nf.format = format->format;
                                    nf.interlace = format->interlace;
                                    nf.frameRate.denominator = format->frameRate.denominator;
                                    nf.frameRate.numerator = format->frameRate.numerator;
                                    nf.aspectRatio.denominator = format->aspectRatio.denominator;
                                    nf.aspectRatio.numerator = format->aspectRatio.numerator;
                                    nf.stride = width * 3;
                                    nf.width = width;
                                    nf.height = height;
                                    nf.captureWidth = width;
                                    nf.captureHeight = height;
                                    nf.hdVideo = format->hdVideo;
                                    nf.appSharing = format->appSharing;
                                    if(videoSession->encoderInit == false) {
                                        videoSession->localEncoder->Initialize(&nf);
                                        videoSession->encoderInit = true;
                                    }
                                    videoSession->localEncoder->Encode(newSample, nf.sampleSize, timestamp, duration, &nf);
                                    if (rendererWebcam) rendererWebcam->DisplayFrame(newSample, nf.sampleSize, &nf);
                                    free(newSample);
                                }
                            }
                            else if (w->outputFormat) {
                                if(videoSession->encoderInit == false) {
                                    videoSession->localEncoder->Initialize(w->outputFormat);
                                    videoSession->encoderInit = true;
                                }
                                videoSession->localEncoder->Encode(buf, len, timestamp, duration, format);
                                if (rendererWebcam) rendererWebcam->DisplayFrame(buf, len, (VideoFrameFormat *)format);
                            }
                        }
                        else if (w->deviceMode == 2) {
                            if (rendererWebcam) rendererWebcam->DisplayFrame(buf, len, (VideoFrameFormat*)format);
                        }
                        // Conference, dann provide unencoded sample to channel for mixing too
                        bool conference = false;
                        if(!ioChannels.empty()) {
                            class VideoIoChannel * ch = ioChannels.front();
                            for(ch = ioChannels.front(); ch; ch = ch->goNext()) {
                                if(w->outputFormat) {
                                    // If a peer is sharing, it still gets the video mix from the two other participants
                                    if(ch->conference && !ch->forwardingChannel) {
                                        if(HasConferenceSamples(ch)) {
                                            class ConferenceSample * cs; 
                                            for(cs = conferenceSamples.front(); cs; cs = cs->goNext()) {
                                                if(cs->videoIoChannel != ch) {  // Mix webcam with other channels in the same context
                                                    ch->Mix(buf, len, format, cs->buf, cs->len, cs->format, timestamp, duration);
                                                }
                                            }
                                        }
                                        else ch->Mix(buf, len, format, NULL, 0, NULL, timestamp, duration);
                                        conference = true;
                                    }
                                    // Websample is already encoded with h264 oder vpx
                                    if(!videoSession->localEncoder && (format->format == VideoFormat::VIDEO_FORMAT_ENCODED) && (ch->GetConnectionType() != ConnectionType::REMOTE_VIDEO_STREAM)) {
                                        // Same encoder localVideoStream and remoteRtp
                                        if(videoSession->remoteEncoder == NULL || ch->GetConnectionType() == ConnectionType::LOCAL_VIDEO_STREAM) {
                                            ch->VideoSample(buf, len, timestamp, duration, MediaType::VIDEO);
                                        }
                                        else {
                                            if(log) log->Log(LOG_VIDEO, "To be implemented! Webcam sample is already encoded but remote video codec is different!");
                                        }
                                    }
                                }
                            }
                        }
                        if(videoSession->remoteEncoder && !conference) videoSession->remoteEncoder->Encode(buf, len, timestamp, duration, format);
                        break;
                    }
                }
                break;
            }
        }
    }
}

void VideoIo::WebcamClosed(class IWebcam * const webcam)
{
    log->Log(LOG_VIDEO, "VideoIo(%p,%p)::WebcamClosed %p", this, videoIoUser, webcam);

    if(!webcams.empty()) {
        class WebcamHelper * w = webcams.front();
        for(w = webcams.front(); w; w = w->goNext()) {
            if(w->webcam == webcam) {
                w->remove();
                delete w;
                break;
            }
        }
    }
}

void VideoIo::WebcamLost(class IWebcam * const webcam, void * context)
{
    log->Log(LOG_VIDEO, "VideoIo(%p,%p)::WebcamLost %p ctx=%p webcams=%d", this, videoIoUser, webcam, context, !webcams.empty());

    // Report removed device to frontend
    if(!webcams.empty()) {
        for(class WebcamHelper * w = webcams.front(); w; w = w->goNext()) {
            if(w->webcam == webcam) {
                if (deviceIoUser) deviceIoUser->MediaIoDeviceRemoved(this, context, webcam->GetWebcamId());
                if (!remoteDeviceUsers.empty()) {
                    class UDeviceIoHelper* h = remoteDeviceUsers.front();
                    while (h) {
                        h->user->MediaIoDeviceRemoved(this, context, webcam->GetWebcamId());
                        h->user->MediaIoQueryDevicesResult(this, context);
                        h = h->goNext();
                    }
                }
                if (!selectedDevices.empty()) {
                    class SelectedDeviceIo * dev;
                    for (dev = selectedDevices.front(); dev; dev = dev->goNext()) {
                        if (webcam->GetWebcamId() && dev->device && !strcmp(dev->device, webcam->GetWebcamId())) {
                            dev->available = false;
                            if (w->isSelected) {
                                if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::WebcamLost selected device was removed (%s)", this, webcam->GetWebcamId());
                                selectedDeviceRemoved = true;
                            }
                        }
                    }
                }
                if (w == restartWebcam) {
                    restartWebcam = NULL;
                    if (restartWebcamTimer->IsRunning()) restartWebcamTimer->Cancel();
                }
                w->remove();
                delete w;
                break;
            }
        }
    }
}

void VideoIo::WebcamFailed(class IWebcam * const webcam, void * context)
{
    log->Log(LOG_VIDEO, "VideoIo(%p,%p)::WebcamFailed %p ctx=%p webcams=%d", this, videoIoUser, webcam, context, !webcams.empty());
    if(!webcams.empty()) {
        class WebcamHelper * w;
        for(w = webcams.front(); w; w = w->goNext()) {
            if(w->webcam == webcam) {
                if(videoSession) {
                    if (w->outputFormat) {
                        free(w->outputFormat);
                        w->outputFormat = NULL;
                        // Restart the webcam
                        if (w->failed < 3) {
                            w->failed++;
                            restartWebcam = w;
                            restartWebcamTimer->Start(w->failed*1000);
                            return;
                        }
                    }
                    if(videoSession->deviceId && webcam->GetWebcamId() && !strcmp(videoSession->deviceId, webcam->GetWebcamId())) {
                        if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::WebcamFailed clean webcam (%s)", this, videoSession->deviceId);
                        // LocalVideoStream arrived first 
                        free(videoSession->deviceId);
                        videoSession->deviceId = NULL;
                    }
                }
                if(w->outputFormat) free(w->outputFormat);
                w->outputFormat = NULL;
                break;
            }
        }
    }
    if(deviceIoUser) deviceIoUser->MediaIoDeviceFailed(this, context, webcam->GetWebcamId());
    if (!remoteDeviceUsers.empty()) {
        class UDeviceIoHelper* h = remoteDeviceUsers.front();
        while (h) {
            h->user->MediaIoDeviceFailed(this, context, webcam->GetWebcamId());
            h = h->goNext();
        }
    }
}

// WebcamQueryResult comes now always
void VideoIo::WebcamAdded(class IWebcam * const webcam, void * context)
{
    if(log) log->Log(LOG_VIDEO, "VideoIo(%p,%p)::WebcamAdded %p ctx=%p webcams=%d", this, videoIoUser, webcam, context, !webcams.empty());

    if(!webcams.empty()) {
        for(class WebcamHelper * w = webcams.front(); w; w = w->goNext()) {
            if(w->webcam == webcam) return;
        }
    }
    webcams.push_back(new class WebcamHelper(webcam));
}

void VideoIo::WebcamQueryResult(void * context)
{
    if(log) log->Log(LOG_VIDEO, "VideoIo(%p,%p)::WebcamQueryResult ctx=%p webcams=%d", this, videoIoUser, context, !webcams.empty());

    // User may have several SoftphoneApps
    class SelectedDeviceIo* selectedDeviceIoFront = NULL;
    if (selectedDevices.empty() == false) {
        MediaProviderContext* ctx = (MediaProviderContext*)context;
        if (ctx || softphoneApp) {
            class SelectedDeviceIo* dev = selectedDevices.front();
            while (dev) {
                if (dev->app && ((ctx && ctx->app && !strcmp(dev->app, ctx->app)) || (softphoneApp && !strcmp(dev->app, softphoneApp)))) {
                    selectedDeviceIoFront = dev;
                    break;
                }
                dev = dev->goNext();
            }
        }
    }
    // Check if any available device is present on the list of selected devices for an specific app
    // context contains the app that made the request
    class WebcamHelper* webcamFirstSelected = NULL;
    class WebcamHelper* webcamSelected = NULL;
    int devLocalSelected = 0;
    if (selectedDevices.empty() == false) {
        MediaProviderContext* ctx = (MediaProviderContext*)context;
        if (ctx || softphoneApp) {
            class SelectedDeviceIo* dev = selectedDevices.front();
            while (dev) {
                if (!webcams.empty()) {
                    class WebcamHelper* w = webcams.front();
                    while (w) {
                        if (!w->isRemote && dev->device && w->webcam && w->webcam->GetWebcamId() && !strcmp(dev->device, w->webcam->GetWebcamId()) && dev->app && ((ctx && ctx->app && !strcmp(dev->app, ctx->app)) || (softphoneApp && !strcmp(dev->app, softphoneApp)))) {
                            if (dev == selectedDeviceIoFront) webcamFirstSelected = w;
                            else webcamSelected = w;
                            if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::WebcamAdded native selectedDevice (%s) for (%s) selected=%p,%p", this, dev->device, dev->app, webcamFirstSelected, webcamSelected);
                            break;
                        }
                        w = w->goNext();
                    }
                }
                if (webcamFirstSelected || webcamSelected) break;
                devLocalSelected++;
                dev = dev->goNext();
            }
        }
    }
    if (webcams.empty()) {
        if (context && deviceIoUser) deviceIoUser->MediaIoQueryDevicesResult(this, context);
        if (!remoteDeviceUsers.empty()) {
            class UDeviceIoHelper* h = remoteDeviceUsers.front();
            while (h) {
                h->user->MediaIoQueryDevicesResult(this, context);
                h = h->goNext();
            }
        }
        if (selectDeviceUser) selectDeviceUser->VideoDeviceList();
        return;
    }
    // local devices
    if (!webcams.empty()) {
        class WebcamHelper* w = webcams.front();
        while (w) {
            w->isSelected = false;
            if (!w->isRemote) {
                if (w == webcamSelected || w == webcamFirstSelected) w->isSelected = true;
                if (w->isNewDevice) {
                    if (deviceIoUser) deviceIoUser->MediaIoDeviceAdded(this, context, w->webcam->GetWebcamId(), VideoDeviceType::Webcam, w->webcam->GetWebcamCapabilities(), w->webcam->GetWebcamName(), w->isSelected);
                    if (!remoteDeviceUsers.empty()) {
                        class UDeviceIoHelper* h = remoteDeviceUsers.front();
                        while (h) {
                            h->user->MediaIoDeviceAdded(this, context, w->webcam->GetWebcamId(), VideoDeviceType::Webcam, w->webcam->GetWebcamCapabilities(), w->webcam->GetWebcamName(), w->isSelected);
                            h = h->goNext();
                        }
                    }
                    w->isNewDevice = false;
                }
            }
            w = w->goNext();
        }
    }
    // remote devices
    if (deviceIoUser) {
        if (webcamFirstSelected == NULL) {
            MediaProviderContext* ctx = (MediaProviderContext*)context;
            class WebcamHelper* w;
            if (ctx || softphoneApp) {
                int devRemoteSelected = 0;
                class SelectedDeviceIo* sel = selectedDevices.front();
                while (sel) {
                    w = webcams.front();
                    while (w) {
                        if (w->isRemote) {
                            if (w->deviceId && sel->device && !strcmp(w->deviceId, sel->device) && sel->app && ((ctx && ctx->app && !strcmp(sel->app, ctx->app)) || (softphoneApp && !strcmp(sel->app, softphoneApp)))) {
                                if (sel == selectedDevices.front() || (devRemoteSelected < devLocalSelected)) {
                                    webcamFirstSelected = w;
                                    w->isSelected = true;
                                }
                                if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::WebcamQueryResult remote selectedDevice (%s) for (%s) selected=%p %d<%d", this, sel->device, sel->app, webcamFirstSelected, devRemoteSelected, devLocalSelected);
                                break;
                            }
                        }
                        w = w->goNext();
                    }
                    if (webcamFirstSelected) break;
                    devRemoteSelected++;
                    sel = sel->goNext();
                }
            }
            w = webcams.front();
            while (w) {
                if (w->isRemote) deviceIoUser->MediaIoDeviceAdded(this, context, w->deviceId, VideoDeviceType::Webcam, w->capabilities, w->name, w->isSelected);
                w = w->goNext();
            }
        }
        else {
            class WebcamHelper* w = webcams.front();
            while (w) {
                if (w->isRemote) deviceIoUser->MediaIoDeviceAdded(this, context, w->deviceId, VideoDeviceType::Webcam, w->capabilities, w->name, w->isSelected);
                w = w->goNext();
            }
        }
    }
    if (context && deviceIoUser) deviceIoUser->MediaIoQueryDevicesResult(this, context);
    if (!remoteDeviceUsers.empty()) {
        class UDeviceIoHelper* h = remoteDeviceUsers.front();
        while (h) {
            h->user->MediaIoQueryDevicesResult(this, context);
            h = h->goNext();
        }
    }
    // Check which selected devices are still available
    if (!selectedDevices.empty()) {
        class SelectedDeviceIo* dev;
        for (dev = selectedDevices.front(); dev; dev = dev->goNext()) {
            dev->available = false;
        }
        if (!webcams.empty()) {
            class WebcamHelper* w = webcams.front();
            while (w) {
                for (class SelectedDeviceIo * sel = selectedDevices.front(); sel; sel = sel->goNext()) {
                    if (sel->device && w->isRemote && w->deviceId && !strcmp(sel->device, w->deviceId)) {
                        sel->available = true;
                    }
                    else if (sel->device && w->webcam && w->webcam->GetWebcamId() && !strcmp(sel->device, w->webcam->GetWebcamId())) {
                        sel->available = true;
                    }
                }
                w = w->goNext();
            }
        }
    }
    if (selectedDeviceRemoved) {
        if (deviceIoUser && !context) {
            if (!selectedDevices.empty()) {
                for (class SelectedDeviceIo * dev = selectedDevices.front(); dev; dev = dev->goNext()) {
                    if (dev->available && dev->app && softphoneApp && !strcmp(dev->app, softphoneApp)) {
                        class WebcamHelper* devItem;
                        if ((devItem = FindDevice(dev->device)) != NULL) {
                            if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::WebcamLost selected new selected device after removal (%s)", this, devItem->webcam ? devItem->webcam->GetWebcamId() : devItem->isRemote ? devItem->deviceId : "");
                            if (deviceIoUser) {
                                if (devItem->webcam) {
                                    deviceIoUser->MediaIoDeviceRemoved(this, context, devItem->webcam->GetWebcamId());
                                    deviceIoUser->MediaIoDeviceAdded(this, context, devItem->webcam->GetWebcamId(), VideoDeviceType::Webcam, devItem->webcam->GetWebcamCapabilities(), devItem->webcam->GetWebcamName(), true);
                                }
                                else if (devItem->isRemote) {
                                    deviceIoUser->MediaIoDeviceRemoved(this, context, devItem->deviceId);
                                    deviceIoUser->MediaIoDeviceAdded(this, context, devItem->deviceId, VideoDeviceType::Webcam, devItem->capabilities, devItem->name, true);
                                }
                            }
                            devItem->isSelected = true;
                            break;
                        }
                    }
                }
            }
        }
        selectedDeviceRemoved = false;
    }
    if (selectDeviceUser) selectDeviceUser->VideoDeviceList();
}

void VideoIo::RemoteMediaIoDeviceAdded(const char* deviceId, int deviceType, unsigned deviceCapabilities, const char* deviceName, bool selected)
{
    class WebcamHelper* w = NULL;
    if (!webcams.empty()) {
        for (w = webcams.front(); w; w = w->goNext()) {
            if (w->isRemote && w->deviceId && deviceId && !strcmp(w->deviceId, deviceId)) break;
        }
    }
    if (!w) {
        if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::RemoteMediaIoDeviceAdded deviceId=%s deviceIoUser=%p", this, deviceId, deviceIoUser);

        w = new class WebcamHelper(NULL, deviceId, deviceCapabilities, deviceName);
        w->isRemote = true;
        webcams.push_back(w);
    }
    else if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::RemoteMediaIoDeviceAdded deviceId=%s already available", this, deviceId);
}

void VideoIo::RemoteMediaIoDeviceRemoved(const char* deviceId)
{
    if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::RemoteMediaIoDeviceRemoved deviceId=%s user=%p", this, deviceId, deviceIoUser);

    if (!webcams.empty()) {
        class WebcamHelper* w;
        for (w = webcams.front(); w; w = w->goNext()) {
            if (w->isRemote && w->deviceId && deviceId && !strcmp(w->deviceId, deviceId)) {
                if (deviceIoUser) deviceIoUser->MediaIoDeviceRemoved(this, NULL, deviceId);
                if (!selectedDevices.empty()) {
                    class SelectedDeviceIo* dev;
                    for (dev = selectedDevices.front(); dev; dev = dev->goNext()) {
                        if (dev->device && !strcmp(dev->device, w->deviceId)) {
                            dev->available = false;
                            if (w->isSelected) {
                                if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::RemoteMediaIoDeviceRemoved selected device was removed (%s)", this, w->deviceId);
                                selectedDeviceRemoved = true;
                            }
                            break;
                        }
                    }
                }
                w->remove();
                delete w;
                return;
            }
        }
    }
}

void VideoIo::RemoteMediaIoDeviceFailed(const char* deviceId)
{
    if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::RemoteMediaIoDeviceFailed deviceId=%s", this, deviceId);

    if (!webcams.empty()) {
        class WebcamHelper* w;
        for (w = webcams.front(); w; w = w->goNext()) {
            if (w->isRemote && w->deviceId && deviceId && !strcmp(w->deviceId, deviceId)) {
                if (deviceIoUser) deviceIoUser->MediaIoDeviceFailed(this, NULL, deviceId);
                return;
            }
        }
    }
}

void VideoIo::RemoteMediaIoQueryDevicesResult()
{
    WebcamQueryResult(NULL);
}

void VideoIo::RemoteAddAppSharingApplication(unsigned int id, const char* name, const char* desc, const char* icon, const char* thumbnail, bool isDesktop)
{
    if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::RemoteAddAppSharingApplication id=%d name=%s desc=%s isDesktop=%d", this, id, name ? name : "-", desc ? desc : "-", isDesktop);

    class AppSharingApp* app = new class AppSharingApp(id + 1024, 0, name, desc, NULL, icon, thumbnail, NULL, NULL, isDesktop, true);
    apps.push_back(app);
    if (appSharingIoUser) appSharingIoUser->AddAppSharingApplication(id + 1024, name, desc, NULL, icon, thumbnail, isDesktop);
}

void VideoIo::RemoteRemoveAppSharingApplication(unsigned int id)
{
    if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::RemoteRemoveAppSharingApplication id=%d", this, id);

    class AppSharingApp* app;
    for (app = apps.front(); app; app = app->goNext()) {
        if (app->id == id) {
            class AppSharingApp* shareApp;
            for (shareApp = shareApps.front(); shareApp; shareApp = shareApp->goNext()) {
                if (shareApp->id == id) {
                    UnshareApplication(NULL, app->id);
                    break;
                }
            }
            app->remove();
            delete app;
            if (subscribe && appSharingIoUser) appSharingIoUser->RemoveAppSharingApplication(id);
            return;
        }
    }
}

// decoded RemoteRtp that must be transcoded for RemoteVideoStream (WebRTC)
// VP8/VP9 -> H264
void VideoIo::DecodedSample(class VideoIoChannel* videoIoChannel, const void* buf, size_t len, dword timestamp, dword duration, struct VideoFrameFormat* f)
{
    if (log) log->Log(LOG_VIDEO, "VideoIo(%p)::DecodedSample videoIoChannel=%p ctx=%p type=%d", this, videoIoChannel, videoIoChannel->GetContext(), videoIoChannel->GetConnectionType());

    class VideoIoChannel* ch;
    for (ch = ioChannels.front(); ch; ch = ch->goNext()) {
        if (ch != videoIoChannel) {
            if (!strcmp(ch->GetChannelId(), videoIoChannel->GetChannelId()) && ch->GetConnectionType() == ConnectionType::REMOTE_VIDEO_STREAM) {
                ch->Transcoding(buf, len, timestamp, duration, f);
                break;
            }
            else if ((ch->GetContext() == videoIoChannel->GetContext()) && (ch->GetConnectionType() == ConnectionType::REMOTE_RTP) && (videoIoChannel->GetConnectionType() == ConnectionType::LOCAL_WEBCAM_STREAM)) {
                ch->Transcoding(buf, len, timestamp, duration, f);
                break;
            }
            else if ((ch->GetContext() == NULL) && (ch->GetConnectionType() == ConnectionType::REMOTE_RTP) && (videoIoChannel->GetConnectionType() == ConnectionType::LOCAL_WEBCAM_STREAM)) {
                ch->Transcoding(buf, len, timestamp, duration, f);
                break;
            }
        }
    }
}

class IRenderVideoStream* VideoIo::OpenVideoRenderStream(class VideoIoChannel* videoIoChannel, bool preview, enum VideoCoder decoder, bool alwaysOnTop)
{
    return videoRenderProvider->OpenVideoRenderStream(videoIoChannel, preview, decoder, alwaysOnTop);
}

// decoded RemoteRtp needed for conference, will be mixed with Webcam or forwarded if no webcam
void VideoIo::ConferenceSample(class VideoIoChannel * videoIoChannel, const void * buf, size_t len, dword timestamp, dword duration, struct VideoFrameFormat * f)
{
    if (isSharing) {
        if (!conferenceSamples.empty()) {
            class ConferenceSample* cs = conferenceSamples.front();
            while(cs) {
                class ConferenceSample* csNext = cs->goNext();
                if (cs->videoIoChannel == videoIoChannel) {
                    cs->remove();
                    delete cs;
                }
                cs = csNext;
            }
        }
        return;
    }
    class WebcamHelper * w = NULL;
    if(!webcams.empty()) {
        for(w = webcams.front(); w; w = w->goNext()) {
            if(w->outputFormat) break; 
        }
    }
    // Keep decoded sample for mixing, only the last one
    if(w) {
        class ConferenceSample * cs;
        if(!conferenceSamples.empty()) {
            for(cs = conferenceSamples.front(); cs; cs = cs->goNext()) {
                if(cs->videoIoChannel == videoIoChannel) {
                    cs->remove();
                    delete cs;
                    break;
                } 
            }
        }
        cs = new class ConferenceSample(videoIoChannel, buf, len, timestamp, duration, f);
        conferenceSamples.push_back(cs);
    }
    // Provided decoded sample for transcoding to channels having different video coder
    else {
        class VideoIoChannel * ch;
        for(ch = ioChannels.front(); ch; ch = ch->goNext()) {
            if(ch != videoIoChannel && ch->GetVideoCoder() != videoIoChannel->GetVideoCoder() && ch->GetConnectionType() == ConnectionType::REMOTE_RTP) {
                ch->Transcoding(buf, len, timestamp, duration, f);
                break;
            }
        }
    }
}

// Video Stream that will be forwarded, RemoteRtp -> RemoteVideoStream (WebRTC)
void VideoIo::RtpRecv(class VideoIoChannel * videoIoChannel, void * buf, size_t len, dword timestamp, short sequenceNumberDiff, bool marker)
{
    log->Log(LOG_VIDEO, "VideoIo(%p)::RtpRecv(%p,%d) isSharing=%d conference=%d len=%d containers=%p channels=%d remoteVideoPriority=%d sharing=%d conn=%d", this, videoIoChannel, videoIoChannel->GetConnectionType(), isSharing, videoIoChannel->conference, len, videoSession, !ioChannels.empty(), videoIoChannel->GetRemoteVideoPriority(), isSharing, videoIoChannel->GetConnectionType());

    // Send Video to Remote Peers
    if(!isSharing && videoIoChannel->conference) {
        // Search for a webcam -> mixing local webcam sample with decoded one
        class WebcamHelper * w = NULL;
        if(!webcams.empty()) {
            for(w = webcams.front(); w; w = w->goNext()) {
                if(w->outputFormat) break;
            }
        }
        // No webcam available. Otherwise streams must be mixed. Decoded samples are delivered asynchronously.
        // Remote side is sending AppSharing
        if(w == NULL || videoIoChannel->GetRemoteVideoPriority()) {
            class VideoIoChannel * ch = ioChannels.front();
            while(ch) {
                // Channels created inside same context, different channel Ids, no webcam, same video coder and remoteRtp type -> forward sample to remote peer
                if(ch != videoIoChannel && strcmp(ch->GetChannelId(), videoIoChannel->GetChannelId()) && (videoIoChannel->GetVideoCoder() == ch->GetVideoCoder()) && ch->GetConnectionType() == ConnectionType::REMOTE_RTP) {
                    ch->ForwardRtp(buf, len, timestamp, 0, sequenceNumberDiff, marker, videoIoChannel->GetVideoCoder(), videoIoChannel->GetConnectionType());  // 0, calculate last received - actual time
                }
                ch = ch->goNext();
            }
        }
    }

    if (videoIoChannel->GetConnectionType() == ConnectionType::REMOTE_RTP) {
        // forward from REMOTE_RTP to REMOTE_VIDEO_STREAM of same channel-id
        if (!ioChannels.empty()) {
            for (class VideoIoChannel * ch = ioChannels.front(); ch; ch = ch->goNext()) {
                if (ch->GetConnectionType() == ConnectionType::REMOTE_VIDEO_STREAM && !strcmp(ch->GetChannelId(), videoIoChannel->GetChannelId())) {
                    if (videoIoChannel->GetVideoCoder() == ch->GetVideoCoder()) {
                        // 25.06: if remoteVideoPriority is on, we only then forward to REMOTE_VIDEO_STREAM the AppSharing stream. 
                        // Otherwise video would also be shown and appsharing would remain unreadable
                        // 28.06: both streams still send, to be decided what to do.
                        ch->ForwardRtp(buf, len, timestamp, 0, sequenceNumberDiff, marker, videoIoChannel->GetVideoCoder(), videoIoChannel->GetConnectionType());  // 0, calculate last received - actual time
                        break;
                    }
                }
            }
        }
    }
    if (!isSharing) {
        if (videoIoChannel->GetConnectionType() == ConnectionType::LOCAL_WEBCAM_STREAM) {
            // forward from LOCAL_WEBCAM_STREAM to REMOTE_RTP (if not sharing)
            if (!ioChannels.empty()) {
                for (class VideoIoChannel * ch = ioChannels.front(); ch; ch = ch->goNext()) {
                    if (ch->GetConnectionType() == ConnectionType::REMOTE_RTP) {
                        if (videoIoChannel->GetVideoCoder() == ch->GetVideoCoder()) {
                            //log->Log(LOG_VIDEO, "VideoIo::RtpRecv() Forwarding from LOCAL_WEBCAM_STREAM to REMOTE_RTP");
                            ch->ForwardRtp(buf, len, timestamp, 0, sequenceNumberDiff, marker, videoIoChannel->GetVideoCoder(), videoIoChannel->GetConnectionType());  // 0, calculate last received - actual time
                            break; // for
                        }
                    }
                }
            }
        }
    }
    else {
        //log->Log(LOG_VIDEO, "VideoIo::RtpRecv() Sharing is ON");
    }
}

void VideoIo::IoExec(void * execContext)
{
    log->Log(LOG_VIDEO, "VideoIo(%p)::IoExec imageBuffer=%p execCnt=%d shutDown=%d remoteUsers=%d", this, imageBuffer, execCnt, shutDown, remoteDeviceUsers.empty());

    execCnt--;
    if(shutDown && !execCnt) {
        if (remoteDeviceUsers.empty()) videoIoUser->CloseVideoIoComplete();
        else {
            execCnt++;
            iomux->SetExec(this, NULL);
        }
    }    
}

void VideoIo::CleanRemoteDevices()
{
    if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::CleanRemoteDevices", this);

    if (!webcams.empty()) {
        class WebcamHelper* w = webcams.front();
        while (w) {
            class WebcamHelper * next = w->goNext();
            if (w->isRemote) {
                if (deviceIoUser) deviceIoUser->MediaIoDeviceRemoved(this, NULL, w->deviceId);
                if (!selectedDevices.empty()) {
                    class SelectedDeviceIo* sel;
                    for (sel = selectedDevices.front(); sel; sel = sel->goNext()) {
                        if (sel->device && !strcmp(sel->device, w->deviceId)) {
                            sel->available = false;
                            if (w->isSelected) {
                                if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::CleanRemoteDevices selected device was removed (%s)", this, w->deviceId);
                                selectedDeviceRemoved = true;
                                if (selectDeviceUser) selectDeviceUser->VideoDeviceRemoved(w->deviceId);
                            }
                            break;
                        }
                    }
                }
                w->remove();
                delete w;
            }
            w = next;
        }
    }
}

void VideoIo::QueryDevices(void * context)
{
    if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::QueryDevices provider=%p context=%p webcams=%d deviceIoUser=%p,%d ts=%d", this, webcamProvider, context, !webcams.empty(), deviceIoUser, !remoteDeviceUsers.empty(), mediaConfiguration->isRunningTS);

    ReadSelectedDevices();

    if (context) {
        // context not NULL means called from the Frontend
        // and only done starting the UI
        // but no need to detect devices again which takes too long...
        if (!webcams.empty()) {
            class WebcamHelper* w = webcams.front();
            while (w) {
                w->isNewDevice = true;
                w = w->goNext();
            }
        }
        WebcamQueryResult(context);
        return;
    }
    if(!webcams.empty()) {
        class WebcamHelper * w = webcams.front();
        while (w) {
            class WebcamHelper * n = w->goNext();
            if (w->isRemote == false) {
                w->remove();
                delete w;
            }
            w = n;
        }
    }
    if (webcamProvider) webcamProvider->QueryDevices(context);
    else if (mediaConfiguration->isRunningTS) WebcamQueryResult(context);
}

// StopDevice should come before this call
void VideoIo::StopLocalVideoEncoder(void * context, const char * channelId, int coder)
{
    log->Log(LOG_VIDEO, "VideoIo(%p)::StopLocalVideoEncoder context=%p channelId=%s coder=%s webcams=%d sessions=%p", this, context, channelId, (coder == VideoCoder::H264 ? "h264" : coder == VideoCoder::VP8 ? "vp8" : "vp9"), !webcams.empty(), videoSession);

    if(videoSession) {
        // StopDevice may not found a session because StartDevice failed to start
        log->Log(LOG_VIDEO, "VideoIo(%p)::StopLocalVideoEncoder context %p channelId (%s) device=(%s)", this, context, channelId, videoSession->deviceId ? videoSession->deviceId : "");
        delete videoSession;
        videoSession = NULL;
    }
    if(!webcams.empty()) {
        class WebcamHelper* w;
        for(w = webcams.front(); w; w = w->goNext()) {
            if(w->webcam && w->outputFormat) {
                if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::StopLocalVideoEncoder webcam still running but no LocalVideoStream session (%s)", this, w->webcam ? w->webcam->GetWebcamId() : "");
                w->webcam->Stop();
                free(w->outputFormat);
                w->outputFormat = NULL;
            }
        }
    }
}

void VideoIo::StartLocalVideoEncoder(void * context, const char * channelId, int coder, bool remoteServices)
{
    log->Log(LOG_VIDEO, "VideoIo(%p)::StartLocalVideoEncoder context=%p channelId=%s coder=%s w=%d", this, context, channelId, (coder == VideoCoder::H264 ? "h264" : coder == VideoCoder::VP8 ? "vp8" : "vp9"), !webcams.empty());

    if(!videoSession) {                 // No Webcam was started yet or StartDevice failed and no session was created
        videoSession = new class VideoSession(NULL, (enum VideoCoder)coder, true);
        videoSession->localChannelId = _strdup(channelId);
        videoSession->remoteServices = remoteServices;
    }
    else if(!videoSession->localEncoder) {   // StartDevice already here
        if(videoSession->localChannelId == NULL) videoSession->localChannelId = _strdup(channelId);
        videoSession->localVideoCoder = (enum VideoCoder)coder;
        videoSession->remoteServices = remoteServices;
        this->appSharingVideoCoder = (enum VideoCoder)coder;
        class WebcamHelper * w = NULL;
        for(w = webcams.front(); w; w = w->goNext()) {
            if(w->outputFormat) break;
        }

        if(w) {
            videoSession->localEncoder = IMediaEncoder::Create(iomux, this, log, (enum VideoCoder)coder);
            videoSession->encoderInit = false;
            if(videoSession->remoteServices && !rendererWebcam && !videoSession->deviceMode) rendererWebcam = videoRenderProvider->OpenVideoRenderStream(this, true);
        }
        else if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::StartVideoEncoder No webcam found, wait to start the video encoder remoteVideoRunning", this);

        // Webcam was started later, call was started/received without local video.
        if (remoteVideoRunning && !videoSession->remoteEncoder && videoSession->localVideoCoder != remoteVideoCoder) {
            videoSession->remoteVideoCoder = (enum VideoCoder)remoteVideoCoder;
            this->appSharingVideoCoder = (enum VideoCoder)remoteVideoCoder;
            if (w && w->outputFormat) {
                videoSession->remoteEncoder = IMediaEncoder::Create(iomux, this, log, (enum VideoCoder)remoteVideoCoder);
                if (videoSession->remoteEncoder) {
                    videoSession->remoteEncoder->Initialize(w->outputFormat);
                }
            }
            else if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::StartVideoEncoder No webcam found, wait to start the video encoder", this);
        }
    }
}

void VideoIo::StartRemoteVideoEncoder(void* context, const char* channelId, int coder)
{
    log->Log(LOG_VIDEO, "VideoIo(%p)::StartRemoteVideoEncoder context=%p channelId=%s coder=%s w=%d", this, context, channelId, (coder == VideoCoder::H264 ? "h264" : coder == VideoCoder::VP8 ? "vp8" : "vp9"), !webcams.empty());

    if((videoSession == NULL) || (videoSession->localEncoder == NULL)) {
        if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::StartRemoteVideoEncoder Local video not running!", this);
        remoteVideoCoder = (enum VideoCoder)coder;
        remoteVideoRunning = true;
    }
    else {
        if(videoSession->remoteChannelId == NULL) videoSession->remoteChannelId = _strdup(channelId);
        // Different video coders for the webcam preview and for remote peer (local, VP8 (Chromium), remote H264)
        // We need to start a remote video encoder
        if(!videoSession->remoteEncoder && videoSession->localVideoCoder != coder) {
            videoSession->remoteVideoCoder = (enum VideoCoder)coder;
            this->appSharingVideoCoder = (enum VideoCoder)coder;
            class WebcamHelper* w = NULL;
            for(w = webcams.front(); w; w = w->goNext()) {
                if(w->outputFormat) break;
            }
            if(w) {
                videoSession->remoteEncoder = IMediaEncoder::Create(iomux, this, log, (enum VideoCoder)coder);
                if(videoSession->remoteEncoder) {
                    videoSession->remoteEncoder->Initialize(w->outputFormat);
                }
            }
            else if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::StartVideoEncoder No webcam found, wait to start the video encoder", this);
        }
    }
}

const char * VideoIo::StartVideoDevice(void * context, void * src, const char * device, int width, int height, int deviceMode)
{
    if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::StartVideoDevice(%d) %s deviceMode=%d context=%p videoSession=%p (%s)", this, webcams.empty(), (device ? device : ""), deviceMode, context, videoSession, videoSession ? videoSession->deviceId : "-");

    if(!webcams.empty()) {
        if(device) {
            class WebcamHelper * w = webcams.front();
            while(w) {
                if(w->webcam && !strcmp(device, w->webcam->GetWebcamId())) {
                    if (restartWebcamTimer->IsRunning()) restartWebcamTimer->Cancel();
                    restartWebcam = NULL;
                    // No session having the webcam
                    if(!w->outputFormat) {
                        if((w->outputFormat = w->webcam->Start(width, height, mediaConfiguration->hdVideo)) == NULL) {
                            if(deviceIoUser) deviceIoUser->MediaIoDeviceFailed(this, src, device);
                            if (!remoteDeviceUsers.empty()) {
                                class UDeviceIoHelper* h = remoteDeviceUsers.front();
                                while (h) {
                                    h->user->MediaIoDeviceFailed(this, src, device);
                                    h = h->goNext();
                                }
                            }
                            return NULL;
                        }
                    }
                    else {
                        if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::StartVideoDevice webcam already started", this);
                        return NULL;
                    }
                    w->context = context;
                    w->deviceMode = deviceMode;
                    w->width = width;
                    w->height = height;
                    w->failed = 0;

                    if(videoSession) {
                        if (videoSession->deviceId) free(videoSession->deviceId);
                        videoSession->deviceId = _strdup(device);
                        videoSession->deviceMode = deviceMode;
                        if (videoSession->remoteServices && !rendererWebcam && !deviceMode) rendererWebcam = videoRenderProvider->OpenVideoRenderStream(this, true);
                    }

                    if(!videoSession) {
                        if(w->deviceMode != 2) videoSession = new class VideoSession(device, VideoCoder::VP8, true);
                        else {
                            videoSession = new class VideoSession(device, VideoCoder::VP8, false);
                            videoSession->deviceId = _strdup(device);
                            videoSession->deviceMode = deviceMode;
                            if(!rendererWebcam) rendererWebcam = videoRenderProvider->OpenVideoRenderStream(this, false, VideoCoder::VP8, true);
                        }
                    }
                    else if(!videoSession->localEncoder && w->outputFormat) {
                        videoSession->localEncoder = IMediaEncoder::Create(iomux, this, log, videoSession->localVideoCoder);
                        this->appSharingVideoCoder = videoSession->localVideoCoder;
                        videoSession->encoderInit = false;
                        // Webcam was started later, call was started/received without local video.
                        if (remoteVideoRunning && !videoSession->remoteEncoder && videoSession->localVideoCoder != remoteVideoCoder) {
                            videoSession->remoteVideoCoder = (enum VideoCoder)remoteVideoCoder;
                            this->appSharingVideoCoder = (enum VideoCoder)remoteVideoCoder;
                            videoSession->remoteEncoder = IMediaEncoder::Create(iomux, this, log, (enum VideoCoder)remoteVideoCoder);
                            if (videoSession->remoteEncoder) {
                                videoSession->remoteEncoder->Initialize(w->outputFormat);
                            }
                        }
                    }
                    return w->webcam->GetWebcamId();
                }
                w = w->goNext();
            }
        }
        if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::StartDevice device is null, not found or already started (%s)", this, device ? device : "");
    }
    if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::StartDevice no devices available!", this);

    return NULL;
}

void VideoIo::StopDevice(void * context, const char * device)
{
    if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::StopDevice(%d) (%s) context=%p", this, webcams.empty(), (device ? device : ""), context);

    if(!webcams.empty()) {
        if(device) {
            class WebcamHelper * w = webcams.front();
            while(w) {
                log->Log(LOG_VIDEO, "VideoIo(%p)::StopDevice(%d) (%s) webcam-context=%p", this, webcams.empty(), (w->webcam && w->webcam->GetWebcamId() ? w->webcam->GetWebcamId() : w->deviceId ? w->deviceId : ""), context);
                if(w->webcam && !strcmp(device, w->webcam->GetWebcamId())) {
                    if(restartWebcamTimer->IsRunning()) restartWebcamTimer->Cancel();
                    restartWebcam = NULL;
                    w->webcam->Stop();
                    if(w->outputFormat) free(w->outputFormat);
                    w->outputFormat = NULL;
                    w->failed = 0;
                    w->context = NULL;
                    w->deviceMode = 0;
                    if (rendererWebcam) {
                        deletedRendererWebcam = rendererWebcam;
                        rendererWebcam->Close();
                        rendererWebcam = NULL;
                    }
                    break;
                }
                w = w->goNext();
            }
        }
    }
}

void VideoIo::SubscribeApplications()
{
    if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::SubscribeApplications %d", this, subscribe);

    if (!subscribe) {
        if (!imageBuffer) {
            mediaConfiguration->appSharingVideo = true;
            imageBuffer = IImageBuffer::CreateImageBuffer(iomux, this, log, mediaConfiguration);
        }
        if (imageBuffer) {
            subscribe = true;
            imageBuffer->SubscribeApplications();
        }
    }
    else {
        class AppSharingApp* app;
        for (app = apps.front(); app; app = app->goNext()) {
            if(appSharingIoUser) appSharingIoUser->AddAppSharingApplication(app->id, (char*)app->name, (char*)app->description, (char*)app->hwId, (char*)app->icon, (char*)app->thumbnail, app->isDesktop);
            if (!remoteAppSharingUsers.empty()) {
                class UAppSharingIoHelper* h = remoteAppSharingUsers.front();
                while (h) {
                    h->user->AddAppSharingApplication(app->id, (char*)app->name, (char*)app->description, (char*)app->hwId, (char*)app->icon, (char*)app->thumbnail, app->isDesktop);
                    h = h->goNext();
                }
            }
        }
    }
}

void VideoIo::UnsubscribeApplications()
{
    if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::UnsubscribeApplications %d", this, subscribe);

    if (subscribe && imageBuffer) imageBuffer->UnsubscribeApplications();
    subscribe = false;
}

void VideoIo::ShareApplication(void* context, unsigned int id, bool admin)
{
    if(log) log->Log(LOG_VIDEO, "VideoIo(%p,%p)::ShareApplication id=%d apps=%d shareApps=%d isSharing=%d", this, imageBuffer, id, apps.empty(), shareApps.empty(), isSharing);

    if (imageBuffer) {
        if (!apps.empty()) {
            class AppSharingApp* app = NULL;
            for (app = apps.front(); app; app = app->goNext()) {
                if (app->id == id) {
                    class AppSharingApp* shareApp = NULL;
                    for (shareApp = shareApps.front(); shareApp; shareApp = shareApp->goNext()) {
                        if (shareApp->id == app->id) {
                            break;
                        }
                    }
                    if (shareApp == NULL) {
                        shareApp = new class AppSharingApp(app->id, 0, app->name, app->description, app->hwId, app->icon, app->thumbnail, app->handle, context, app->isDesktop, app->isRemote);
                        shareApps.push_back(shareApp);
                        if (app->isRemote) {
                            if (mediaConfiguration->isRunningTS && remoteMediaAppSharingUser) remoteMediaAppSharingUser->ShareApplication(shareApp->id - 1024);
                        }
                        else {
                            shareApp->captureTimer = new ITimer(iomux, this);
                            imageBuffer->Capture(shareApp->id, NULL, AppSharingCoder::VIDEO_CODER);
                            shareApp->captureTimer->Start(500); // Leave sometime to stablish the session
                            isSharing++;
                            if (mediaConfiguration->isRunningTS && remoteMediaAppSharingUser) remoteMediaAppSharingUser->RemoteShareApplication();
                        }
                    }
                    break;
                }
            }
            if (app == NULL) if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::ShareApplication App not found %d", this, id);
        }
    }
}

void VideoIo::UnshareAllApplications(void* context)
{
    log->Log(LOG_VIDEO, "VideoIo(%p)::UnshareAllApplications (%d) isSharing (%d)", this, !shareApps.empty(), isSharing);

    if (!shareApps.empty()) {
        class AppSharingApp* shareApp = shareApps.front();
        while (shareApp) {
            class AppSharingApp* shareAppNext = shareApp->goNext();
            UnshareApplication(context, shareApp->id);
            shareApp = shareAppNext;
        }
    }
    isSharing = 0;
}

void VideoIo::RemoteVideoPriority(void* context, const char * ch, bool on)
{
   if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::RemoteVideoPriority context=%p ch=(%s) on=(%d) remoteVideoPriority=(%d)", this, context, ch ? ch : "", on, remoteVideoPriority);

    if(on) remoteVideoPriority++;
    else remoteVideoPriority--;
    if (!ioChannels.empty()) {
        class VideoIoChannel* c = NULL;
        for (c = ioChannels.front(); c; c = c->goNext()) {
            if (c->GetConnectionType() == ConnectionType::REMOTE_RTP) {
                if (c->GetChannelId() && ch && !strcmp(c->GetChannelId(), ch)) {
                    c->RemoteVideoPriority(on);
                }
                else if (c->GetChannelId() && ch && strcmp(c->GetChannelId(), ch)) {
                    if (on) {
                        c->forwardingChannel = _strdup(ch);
                    }
                    else if (c->forwardingChannel) {
                        free(c->forwardingChannel);
                        c->forwardingChannel = NULL;
                    }
                }
            }
        }
    }
}

void VideoIo::UnshareApplication(void* context, unsigned int id)
{
    if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::UnshareApplication %d isSharing=%d", this, id, isSharing);

    if (!shareApps.empty()) {
        class AppSharingApp* shareApp = NULL;
        for (shareApp = shareApps.front(); shareApp; shareApp = shareApp->goNext()) {
            if (shareApp->id == id) {
                if (shareApp->isRemote) {
                    if (mediaConfiguration->isRunningTS && remoteMediaAppSharingUser) remoteMediaAppSharingUser->UnshareApplication(id - 1024);
                }
                else {
                    if (imageBuffer) imageBuffer->Clean(shareApp->id);
                    shareApp->captureTimer->Cancel();
                }
                shareApp->remove();
                delete shareApp;
                isSharing--;
                if (mediaConfiguration->isRunningTS && remoteMediaAppSharingUser) remoteMediaAppSharingUser->RemoteUnshareApplication();
                break;
            }
        }
        if (shareApp == NULL) {
            if (log) log->Log(LOG_VIDEO, "VideoIo(%p)::UnshareApplication application not found %d", this, id);
        }
        else if (!isSharing) {
            if (appSharingEncoder) {
                appSharingEncoder->Close();
                deletedAppSharingEncoder = appSharingEncoder;
                appSharingEncoder = NULL;
            }
            if (videoSession && remoteVideoRunning && !videoSession->remoteEncoder && videoSession->localVideoCoder != remoteVideoCoder) {
                class WebcamHelper* w = NULL;
                for(w = webcams.front(); w; w = w->goNext()) {
                    if(w->outputFormat) break;
                }
                if(w) {
                    videoSession->remoteEncoder = IMediaEncoder::Create(iomux, this, log, videoSession->remoteVideoCoder);
                    if(videoSession->remoteEncoder) {
                        videoSession->remoteEncoder->Initialize(w->outputFormat);
                    }
                }
                else if(log) log->Log(LOG_VIDEO, "VideoIo(%p)::StartVideoEncoder No webcam found, wait to start the video encoder", this);
            }
            if (!ioChannels.empty()) {
                class VideoIoChannel* c = NULL;
                for (c = ioChannels.front(); c; c = c->goNext()) {
                    FullIntraRequest(c);
                    c->FullIntraRequest();
                }
            }
        }
    }
#if 0
    // reset sessionId if no application is shared and no conference 
    if (shareApps.empty()) {
        int numChannels = 0;
        if (ioChannels.empty() == false) {
            class AppSharingIoChannel* ch = ioChannels.front();
            while (ch) {
                ch = ch->goNext();
                numChannels++;
            }
        }
        if (numChannels < 2) sessionId = 0;
    }
#endif
}

void VideoIo::ReloadApplicationList()
{
    while (!apps.empty()) {
        if (subscribe) {
            class AppSharingApp* app = apps.front();
            if(appSharingIoUser) appSharingIoUser->RemoveAppSharingApplication(app->id);
            if (!remoteAppSharingUsers.empty()) {
                class UAppSharingIoHelper* h = remoteAppSharingUsers.front();
                while (h) {
                    h->user->RemoveAppSharingApplication(app->id);
                    h = h->goNext();
                }
            }
        }
        delete apps.front();
    }
    UnsubscribeApplications();
    SubscribeApplications();
}

void VideoIo::Add(unsigned int id, const char* name, const char* description, const char* hwId, const char* icon, const char* thumbnail, void* handle, bool isDesktop)
{
    log->Log(LOG_VIDEO, "VideoIo(%p)::Add(%d) %d (%s,%d) hwId=%s handle=%x isDesktop=%d", this, subscribe, id, name, strlen(name), hwId ? hwId : "", handle, isDesktop);
    if (!subscribe) return;

    class AppSharingApp* app = new class AppSharingApp(id, 0, name, description, hwId,  icon, thumbnail, handle, NULL, isDesktop);
    apps.push_back(app);
    if (appSharingIoUser) appSharingIoUser->AddAppSharingApplication(id, name, description, hwId, icon, thumbnail, isDesktop);
    if (!remoteAppSharingUsers.empty()) {
        class UAppSharingIoHelper* h = remoteAppSharingUsers.front();
        while (h) {
            h->user->AddAppSharingApplication(id, name, description, hwId, icon, thumbnail, isDesktop);
            h = h->goNext();
        }
    }
}

void VideoIo::Update(unsigned int id, const char* name, const char* description, const char* hwId, const char* icon, const char* thumbnail)
{
    log->Log(LOG_VIDEO, "VideoIo(%p)::Update(%d) Application %d (%s)", this, subscribe, id, name);
    if (!subscribe) return;

    class AppSharingApp* app;
    for (app = apps.front(); app; app = app->goNext()) {
        if (app->id == id) {
            app->Update(id, name, description, hwId, icon, thumbnail);
            if(appSharingIoUser) appSharingIoUser->UpdateAppSharingApplication(id, name, description, hwId, icon, thumbnail);
            if (!remoteAppSharingUsers.empty()) {
                class UAppSharingIoHelper* h = remoteAppSharingUsers.front();
                while (h) {
                    h->user->UpdateAppSharingApplication(id, name, description, hwId, icon, thumbnail);
                    h = h->goNext();
                }
            }
            return;
        }
    }
}

void VideoIo::Remove(unsigned int id)
{
    log->Log(LOG_VIDEO, "VideoIo(%p)::Remove(%d) Application %d", this, subscribe, id);

    class AppSharingApp* app;
    for (app = apps.front(); app; app = app->goNext()) {
        if (app->id == id) {
            class AppSharingApp* shareApp;
            for (shareApp = shareApps.front(); shareApp; shareApp = shareApp->goNext()) {
                if (shareApp->id == id) {
                    UnshareApplication(NULL, app->id);
                    break;
                }
            }
            app->remove();
            delete app;
            if (subscribe && appSharingIoUser) appSharingIoUser->RemoveAppSharingApplication(id);
            if (!remoteAppSharingUsers.empty()) {
                class UAppSharingIoHelper* h = remoteAppSharingUsers.front();
                while (h) {
                    h->user->RemoveAppSharingApplication(id);
                    h = h->goNext();
                }
            }
            return;
        }
    }
}

VideoIoChannel::VideoIoChannel(class IIoMux * const iomux, class IVideoIo * mediaIo, class UMediaIoChannel * const user, class IInstanceLog * log) :
    videoIo((VideoIo *)mediaIo),
    user(user),
    log(log),
    iomux(iomux)
{
    log->Log(LOG_VIDEO, "VideoIoChannel(%p)::VideoIoChannel user=%p", this, user);
    this->encoder = NULL;
    this->decoder = NULL;
    this->deletedEncoder = NULL;
    this->deletedDecoder = NULL;
    this->renderer = NULL;
    this->deletedRenderer = NULL;
    this->width = 0;
    this->height = 0;
    memset(&this->conf, 0x0, sizeof(struct VideoFrameFormat));

    this->open = false;
    this->conference = false;
    this->decodeStream = false;
    this->channelId = NULL;
    this->connType = ConnectionType::REMOTE_RTP;
    this->type = MediaType::WEBM_VP8_CONTAINER;
    this->sameTimestamp = 0;
    this->sameTimestampSequence = 0;

    this->partialTxTimeStamp = 0;
    this->firstTxTimeStamp = 0;
    this->procTxDataPar = 0;
    this->procTxDataTot = 0;
    this->numTxProcSamples = 0;

    this->partialRxTimeStamp = 0;
    this->firstRxTimeStamp = 0;
    this->procRxDataPar = 0;
    this->procRxDataTot = 0;
    this->numRxProcSamples = 0;

    this->keyframeTs = 0;
    this->lkeyframe = 0;
    this->isPacketLost = 0;
    this->remoteVideoPriority = false;
    this->forwardingChannel = NULL;

    this->remoteServices = false;

    this->rtpSequence = 0;
    this->processRtpQueue = new class ITimer(iomux, this);
    this->lastRtpSequence = 0;
    this->lastProcSequence = 0;
    this->initLastProcSequence = false;

    this->markers = 0;
    this->oldForwardTs = 0;
    this->lastRtpMarker = true;
    this->lastSentVideoSample = false;
    this->context = nullptr;
}

VideoIoChannel::~VideoIoChannel()
{
    log->Log(LOG_VIDEO, "VideoIoChannel(%p)::~VideoIoChannel %p,%p", this, encoder, decoder);

    if(this->processRtpQueue) delete this->processRtpQueue;
    if(channelId) free(channelId);
    if(forwardingChannel) free(forwardingChannel);
}

void VideoIoChannel::RemoteVideoPriority(bool on)
{
    if (connType == ConnectionType::REMOTE_RTP) if(log) log->Log(LOG_VIDEO, "VideoIoChannel(%p)::RemoteVideoPriority (%d)", this, on);

    remoteVideoPriority = on;

    if (on) user->MediaIoSendFIR();
}

// Switching between Video und AppSharing in a 3rd party conference
// Undocking/Docking Soft-PhoneApp
void VideoIoChannel::FullIntraRequest()
{
    log->Log(LOG_VIDEO, "VideoIoChannel(%p)::FullIntraRequest %p remoteVideoPriority %d type=%d user=%p", this, encoder, remoteVideoPriority, GetConnectionType(), user);

    if (!open) return;
    if (encoder) encoder->FullIntraRequest();
    if (user) user->MediaIoSendFIR();
}

void VideoIoChannel::ForwardRtp(const void * buf, int len, dword timestamp, dword duration, short sequenceNumberDiff, bool marker, enum VideoCoder coder, ConnectionType connOrigin)
{
    //if(connType == ConnectionType::REMOTE_RTP) debug->printf("VideoIoChannel(%p)::ForwardRtp %d timestamp=%u sequenceNumberDiff=%d fwd=(%s)", this, len, timestamp, sequenceNumberDiff, forwardingChannel ? forwardingChannel : "");
    if (connOrigin == ConnectionType::LOCAL_WEBCAM_STREAM) {
        if (lastRtpMarker) {
            ulong64 tm = ITime::TimeStampMilliseconds();
            dword t = (dword)(tm * 90);
            sameTimestampSequence = (t == sameTimestamp) ? sameTimestampSequence + 1 : 0;
            oldForwardTs = t + sameTimestampSequence;
            sameTimestamp = t;
            if (lastSentVideoSample) {
                sequenceNumberDiff++; // introduce a missed rtp packet to make sure the decoder resyncs
                lastSentVideoSample = false;
            }
        }
        log->Log(LOG_VIDEO, "VideoIoChannel(%p)::ForwardRtp timestamp=%u-> %u sequenceNumberDiff=%d marker=%d,%d", this, timestamp, oldForwardTs, sequenceNumberDiff, marker, lastRtpMarker);

        user->MediaIoRtpForward(buf, len, oldForwardTs, sequenceNumberDiff, marker, MediaType::VIDEO);

        lastRtpMarker = marker;
    }
    else user->MediaIoRtpForward(buf, len, timestamp, sequenceNumberDiff, marker, MediaType::VIDEO);
}

void VideoIoChannel::RtcpSend(const void * buf, int len)
{
    user->MediaIoRtcpSend(buf, len, MediaType::VIDEO);
}

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 VideoIoChannel::VideoSample(const void * buf, int len, dword timestamp, dword duration, enum MediaType mediaType)
{
    log->Log(LOG_VIDEO, "VideoIoChannel(%p,%p)::VideoSample encoder=%p conference=%d (%s) timestamp=%u", this, user, encoder, conference, strConnectionType(connType), timestamp);

    ulong64 tm = ITime::TimeStampMilliseconds();

    //if(log) log->Log(LOG_VIDEO, "VideoSample1(%p) %d,%llu,%llu,%llu,%d,%d", this, numTxProcSamples, tm, firstTxTimeStamp, partialTxTimeStamp, procTxDataPar, procTxDataTot);

    if(connType == ConnectionType::REMOTE_RTP) {
        if(numTxProcSamples) {
            if((numTxProcSamples & 0x3ff) == 0) {
                int timeDiffPar = (int)(tm - partialTxTimeStamp);
                int timeDiffTot = (int)(tm - firstTxTimeStamp);
                int bitratePar = (int)((ulong64)(procTxDataPar * 8) / timeDiffPar);
                int bitrateTot = (int)((ulong64)(procTxDataTot * 8) / timeDiffTot);
                if(log) log->Log(LOG_VIDEO, "VideoIoChannel(%p)::VideoSample tx-samples:%d Bytes: %d-%d tDiff: %d-%d msec BitRate: %d-%d kbps", this, numTxProcSamples, procTxDataPar, procTxDataTot, timeDiffPar, timeDiffTot, bitratePar, bitrateTot);
                partialTxTimeStamp = tm;
                procTxDataPar = 0;
            }
        }
        else {
            partialTxTimeStamp = tm;
            firstTxTimeStamp = tm;
        }
        procTxDataPar += len + (ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN + RTP_HEADER_LEN);
        procTxDataTot += len + (ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN + RTP_HEADER_LEN);
        numTxProcSamples++;
    }
    //if(log) log->Log(LOG_VIDEO, "VideoSample1(%p) %d,%llu,%llu,%llu,%d,%d", this, numTxProcSamples, tm, firstTxTimeStamp, partialTxTimeStamp, procTxDataPar, procTxDataTot);

    // Avoid jump in ts between Webcam and ImageBuffer (To be removed!)
    dword t = (dword)(tm * 90);
    sameTimestampSequence = (t == sameTimestamp) ? sameTimestampSequence + 1 : 0;
    user->MediaIoRtpSend(buf, len, t + sameTimestampSequence, mediaType, this->conference);
    sameTimestamp = t;

    lastRtpMarker = true;
    lastSentVideoSample = true; 
}

void VideoIoChannel::Open()
{
    log->Log(LOG_VIDEO, "VideoIoChannel(%p)::Open user=%p opened=%d", this, user, open);
    if(open) return;
    open = true;
    videoIo->Add(this);
}

void VideoIoChannel::TimerOnTimeout(ITimer* timer)
{
    if (open == false) return;

    class RtpPacket* p = rtpPackets.front();
    if(log) log->Log(LOG_VIDEO, "VideoIoChannel::TimerOnTimeout firstSequence=%d lastProcSequence=%d lastRtpSequence=%d rtpSequence=%d markers=%d", p ? p->rtpSequence : 0, lastProcSequence, lastRtpSequence, rtpSequence, markers);
    if (!lastRtpSequence || !markers) {
        p = rtpPackets.back();
        if (p) lastRtpSequence = p->rtpSequence;
        processRtpQueue->Start(25);
    }
    else {
        if (!rtpPackets.empty()) {
            initLastProcSequence = true;

            bool endProc = false;
            do {
                word diff = 0;
                word tmpLastProcSequence = lastProcSequence;
                while (p) {
                    class RtpPacket* n = p->goNext();
                    if (p->rtpSequence == lastRtpSequence) {
                        endProc = true;
                        break;
                    }
                    diff = (p->rtpSequence - (tmpLastProcSequence + 1)) & 0xffff;
                    if(log) log->Log(LOG_VIDEO, "VideoIoChannel::TimerOnTimeout DecodeStream check frame seq=%d ts=%d difF=%d marker=%d", p->rtpSequence, p->timestamp, diff, p->marker);
                    tmpLastProcSequence = p->rtpSequence;
                    if (p->marker) break;
                    p = n;
                }
                if (endProc) break;
                p = rtpPackets.front();
                if (diff) {
                    if(log) log->Log(LOG_VIDEO, "VideoIoChannel::TimerOnTimeout DecodeStream frame not completely received");
                    while (p) {
                        bool marker = p->marker;
                        class RtpPacket* n = p->goNext();
                        if (p->rtpSequence == lastRtpSequence) {
                            endProc = true;
                            break;
                        }
                        lastProcSequence = p->rtpSequence;
                        delete p;
                        if (marker) break;
                        p = n;
                    }
                }
                else {
                    if(log) log->Log(LOG_VIDEO, "VideoIoChannel::TimerOnTimeout DecodeStream frame completely received");
                    while (p) {
                        bool marker = p->marker;
                        class RtpPacket* n = p->goNext();
                        if (p->rtpSequence == lastRtpSequence) {
                            endProc = true;
                            break;
                        }
                        word diff = (p->rtpSequence - (lastProcSequence + 1)) & 0xffff;
                        if(log) log->Log(LOG_VIDEO, "VideoIoChannel::TimerOnTimeout DecodeStream seq=%d ts=%d difF=%d marker=%d", p->rtpSequence, p->timestamp, diff, p->marker);
                        videoIo->RtpRecv(this, p->buf, p->len, p->timestamp, diff, p->marker);
                        lastProcSequence = p->rtpSequence;
                        delete p;
                        if (marker) break;
                        p = n;
                    }
                }
                p = rtpPackets.front();
                if (markers > 0) markers--;
            } while (!endProc);

#if 0
            while (p) {
                class RtpPacket* n = p->goNext();
                if (p->rtpSequence == lastRtpSequence) break;
                word diff = (p->rtpSequence - (lastProcSequence + 1)) & 0xffff;
                if(log) log->Log(LOG_VIDEO, "VideoIoChannel::TimerOnTimeout DecodeStream seq=%d ts=%d difF=%d marker=%d", p->rtpSequence, p->timestamp, diff, p->marker);
                videoIo->RtpRecv(this, p->buf, p->len, p->timestamp, diff, p->marker);
                //decoder->DecodeStream(p->buf, p->len, p->timestamp, diff, p->marker);
                lastProcSequence = p->rtpSequence;
                delete p;
                p = n;
            }
#endif
        }
        lastRtpSequence = 0;
        processRtpQueue->Start(25);
    }
}

void VideoIoChannel::JitterBuffer(void* buf, size_t len, unsigned int timestamp, short sequenceNumberDiff, bool marker)
{
    if (decoder) {
        bool reorder = false;
        if (!rtpSequence && !this->initLastProcSequence) processRtpQueue->Start(25);
        rtpSequence += sequenceNumberDiff + 1;

        if(log) log->Log(LOG_VIDEO, "VideoIoChannel::JitterBuffer sequence=%d sequenceNumberDiff=%d marker=%d", rtpSequence, sequenceNumberDiff, marker);

        if (this->initLastProcSequence) {
            // Old packet? would fail if more than 32768 packets were lost
            if (rtpSequence <= lastProcSequence && (lastProcSequence - rtpSequence < 32768)) {
                if(log) log->Log(LOG_VIDEO, "VideoIoChannel::JitterBuffer drop old sequence=%d lastProcSequence=%d ", rtpSequence, lastProcSequence);
                return;
            }
        }
        markers += (marker == true);
        if (!rtpPackets.empty()) {
            class RtpPacket* p = rtpPackets.front();
            dword recv = 65536 + rtpSequence;
            while (p != NULL) {
                if (rtpSequence == p->rtpSequence) {
                    if(log) log->Log(LOG_VIDEO, "VideoIoChannel::JitterBuffer duplicate rtpSequence=%d", rtpSequence);
                    delete p;
                    return;
                }
                word diff = (recv - p->rtpSequence) & 0xffff;
                if (diff > 32768) { // This will fail if more than 32768 packets lost or delayed
                    reorder = true;
                    if(log) log->Log(LOG_VIDEO, "VideoIoChannel::JitterBuffer reorder recv=%d p=%d diff=%d", rtpSequence, p->rtpSequence, diff);
                    break;
                }
                p = p->goNext();
            }
            if (reorder) {
                class RtpPacket* n = new RtpPacket(buf, len, timestamp, marker, rtpSequence);
                rtpPackets.insertBefore(n, p);
            }
            else rtpPackets.push_back(new RtpPacket(buf, len, timestamp, marker, rtpSequence));
        }
        else rtpPackets.push_front(new RtpPacket(buf, len, timestamp, marker, rtpSequence));
    }
}

void VideoIoChannel::RtpRecv(void * buf, size_t len, dword timestamp, short sequenceNumberDiff, bool marker)
{
    log->Log(LOG_VIDEO, "VideoIoChannel(%p)::RtpRecv conference=%d decoder=%p decodeStream=%d len=%d timestamp=%u sequenceNumberDiff=%d marker=%d coder=%s remoteVideoPriority=%d type=%d", this, conference, decoder, decodeStream, len, timestamp, sequenceNumberDiff, marker, coder == VideoCoder::H264 ? "h264" : "vpx", remoteVideoPriority, GetConnectionType());

    if (!open) return;
    ulong64 tm = ITime::TimeStampMilliseconds();

    log->Log(LOG_VIDEO, "VideoIoChannel(%p)::RtpRecv1 %d,%llu,%llu,%llu,%d,%d", this, numRxProcSamples, tm, firstRxTimeStamp, partialRxTimeStamp, procRxDataPar, procRxDataTot);
    if(connType == ConnectionType::REMOTE_RTP) {
        if(numRxProcSamples) {
            if((numRxProcSamples & 0x3ff) == 0) {
                int timeDiffPar = (int)(tm - partialRxTimeStamp);
                int timeDiffTot = (int)(tm - firstRxTimeStamp);
                int bitratePar = (int)((ulong64)(procRxDataPar * 8) / timeDiffPar);
                int bitrateTot = (int)((ulong64)(procRxDataTot * 8) / timeDiffTot);
                if(log) log->Log(LOG_VIDEO, "VideoIoChannel(%p)::VideoSample rx-samples:%d Bytes: %d-%d tDiff: %d-%d msec BitRate: %d-%d kbps", this, numRxProcSamples, procRxDataPar, procRxDataTot, timeDiffPar, timeDiffTot, bitratePar, bitrateTot);
                partialRxTimeStamp = tm;
                procRxDataPar = 0;
            }
        }
        else {
            partialRxTimeStamp = tm;
            firstRxTimeStamp = tm;
        }
        procRxDataPar += len + (ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN + RTP_HEADER_LEN);
        procRxDataTot += len + (ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN + RTP_HEADER_LEN);
        numRxProcSamples++;
    }

    //if(log) log->Log(LOG_VIDEO, "RtpRecv2 %d,%llu,%llu,%llu,%d,%d", numRxProcSamples, tm, firstRxTimeStamp, partialRxTimeStamp, procRxDataPar, procRxDataTot);

    // Conference or different video coder between RemoteRtp and RemoteVideoStream means rtp video must be decoded
    // If conference but AppSharing is received, do not decode for mixing. Stream will be forwarded
    if (renderer) {
        renderer->RtpRecv(buf, len, timestamp, sequenceNumberDiff, marker);
        return;
    }
    if (decoder && ((conference && !remoteVideoPriority) || decodeStream)) {
        decoder->DecodeStream(buf, len, timestamp, sequenceNumberDiff, marker);
    }
    // If same video coder rtp stream can be forwarded to remotevideostream (webrtc)
    if(decodeStream == false) {
#if 0
        if(this->coder == VideoCoder::VP8) {
            char* b = (char*)buf;
            int offHdr = 1;
            if(b[0] & 0x80) {                       // Check payload descriptor for Extension header
                offHdr++;
                if(b[1] & 0x80) {                   // Picture ID present
                    if(b[2] & 0x80) {               // Long Picture ID
                        offHdr += 2;
                    }
                    else {
                        offHdr++;
                    }
                }
                if(b[1] & 0x40) offHdr++;           // TL0PICIDX byte present
                if(b[1] & 0x20) offHdr++;           // TID byte present, TID/KEYIDX octet
                else if(b[1] & 0x10) offHdr++;      // KEYIDX byte present, TID/KEYIDX octet
            }
            int fragSb = (b[0] & 0x10) ? 1 : 0;
            if(fragSb) {
                if(!(b[0] & 0x07) && !(b[offHdr] & 0x01)) {
                    //if(log) log->Log(LOG_VIDEO, "VideoChannel(%p)::RtpRecv vp8 keyframe", this);
                    keyframeTs = timestamp;
                    isPacketLost = 0;
                }
            }
        }
#endif
#if 0
        JitterBuffer(buf, len, timestamp, sequenceNumberDiff, marker);
#else
        videoIo->RtpRecv(this, buf, len, timestamp, sequenceNumberDiff, marker);
#endif
    }
#if 0
    if (keyframeTs == timestamp) {
        lkeyframe += len;
        numKfPackets++;
    }
    else {
        if (lkeyframe) {
            if(log) log->Log(LOG_VIDEO, "VideoChannel(%p)::RtpRecv vp8 keyframe size=%d ts=%d num=%d isPacketLost=%d", this, lkeyframe, keyframeTs, numKfPackets, isPacketLost);
            keyframeTs = 0;
            lkeyframe = 0;
            numKfPackets = 0;
            isPacketLost = 0;
        }
    }
    if (sequenceNumberDiff) {
        isPacketLost++;
        //if(log) log->Log(LOG_VIDEO, "VideoChannel(%p)::RtpRecv packet lost!", this, sequenceNumberDiff);
    }
#endif
}

void VideoIoChannel::Transcoding(const void * buf, size_t len, dword timestamp, dword duration, const struct VideoFrameFormat * f)
{
    log->Log(LOG_VIDEO, "VideoIoChannel(%p)::Transcoding %p timestamp=%u duration=%u coder=%d", this, encoder, timestamp, duration, this->coder);

    if (!open) return;
    if(!encoder || f->width != width || f->height != height) {
        if (!encoder) {
            if(this->coder == VideoCoder::H264) {
                encoder = IMediaEncoder::Create(iomux, this, log, VideoCoder::H264);
            }
            else if(this->coder == VideoCoder::AV1) {
                encoder = IMediaEncoder::Create(iomux, this, log, VideoCoder::AV1);
            }
            else if(this->coder == VideoCoder::VP8) {
                encoder = IMediaEncoder::Create(iomux, this, log, VideoCoder::VP8);
            }
            else {
                encoder = IMediaEncoder::Create(iomux, this, log, VideoCoder::VP9);
            }
            encoder->Initialize(f);
        }
        width = f->width;
        height = f->height;
    }
    encoder->Encode(buf, len, timestamp, duration, f);
}

void VideoIoChannel::Mix(const void * wcBuf, size_t wcLen, const struct VideoFrameFormat * wcFormat, const void * rBuf, size_t rLen, const struct VideoFrameFormat * rFormat, dword timestamp, dword duration)
{
    if (!open) return;

    int maxWidth;
    if (rFormat) {
        maxWidth = wcFormat->width + rFormat->width;
    }
    else {
        maxWidth = wcFormat->width << 1;
    }
    int halfW = wcFormat->width;

    int maxHeight = MAX(wcFormat->height, rFormat ? rFormat->height : 0);

    log->Log(LOG_VIDEO, "VideoIoChannel(%p)::Mix %p (%d,%d) -> (%d,%d) timestamp=%u", this, encoder, maxWidth, maxHeight, width, height, timestamp);

    if(!encoder || maxWidth != width || maxHeight != height) { 
        memcpy(&conf, wcFormat, sizeof(struct VideoFrameFormat));
        conf.width = maxWidth;
        conf.height = maxHeight;
        conf.captureHeight = conf.height;
        conf.captureWidth = conf.width;
        conf.stride = maxWidth;
        conf.format = VideoFormat::VIDEO_FORMAT_IYUV;
        conf.sampleSize = conf.stride * conf.height + (conf.stride >> 1) * conf.height;
        if (!encoder) {
            if(this->coder == VideoCoder::H264) {
                encoder = IMediaEncoder::Create(iomux, this, log, VideoCoder::H264);
            }
            else if(this->coder == VideoCoder::AV1) {
                encoder = IMediaEncoder::Create(iomux, this, log, VideoCoder::AV1);
            }
            else if(this->coder == VideoCoder::VP8) {
                encoder = IMediaEncoder::Create(iomux, this, log, VideoCoder::VP8);
            }
            else {
                encoder = IMediaEncoder::Create(iomux, this, log, VideoCoder::VP9);
            }
            if(!encoder) {
                return;
            }
            if(encoder->Initialize(&conf) == false) {
                return;
            }
        }
        width = maxWidth;
        height = maxHeight;

        if(log) log->Log(LOG_VIDEO, "VideoIoChannel(%p)::Mix %p (w=%d,h=%d) (%d,%d)-(%d,%d)", this, encoder, maxWidth, maxHeight, wcFormat->width, wcFormat->height, rFormat ? rFormat->width : 0, rFormat ? rFormat->height : 0);
    }
    if(wcFormat->format != VideoFormat::VIDEO_FORMAT_IYUV || (rFormat && rFormat->format != VideoFormat::VIDEO_FORMAT_IYUV)) {
        if(log) log->Log(LOG_VIDEO, "Wrong format (expected YUV) (%d,%d != %d)", wcFormat->format, rFormat ? rFormat->format : 0, VideoFormat::VIDEO_FORMAT_IYUV);
        return;
    }

    int len = width*height*2;
    byte * data = (byte *)malloc(len);
    memset(data, 0x32, width * height);
    memset(&data[width*height], 0x7f, width * height);

    int offX, offY, offWebcamY;

    // copy first sample
    if(wcFormat->width < halfW) offX = (halfW-wcFormat->width)>>1;
    else offX = 0;
    if(wcFormat->height < height) offY = (height-wcFormat->height)>>1;
    else offY = 0;
    if (wcFormat->height < wcFormat->captureHeight) offWebcamY = (wcFormat->captureHeight - wcFormat->height) >> 1;
    else offWebcamY = 0;

    // Y 
    byte* pConfData = data + offY*width + offX;
    const byte* pSample = (const byte *)wcBuf + offWebcamY*wcFormat->stride;
    for(int i=0; i<wcFormat->height; i++) {
        memcpy(pConfData, pSample, wcFormat->width);

        pConfData += width;
        pSample += wcFormat->stride;
    }
    // U
    pConfData = data + height*width + (offY>>1)*(width>>1) + (offX>>1);
    pSample = (const byte*)wcBuf + wcFormat->captureHeight*wcFormat->stride + ((offWebcamY>>1)*(wcFormat->stride>>1));
    for (int i = 0; i < wcFormat->height>>1; i++) {
        memcpy(pConfData, pSample, wcFormat->width >> 1);

        pConfData += (width >> 1);
        pSample += (wcFormat->stride >> 1);
    }
    // V
    pConfData = data + height*width + (height>>1)*(width>>1) + (offY>>1)*(width>>1) + (offX>>1);
    pSample = (const byte*)wcBuf + wcFormat->captureHeight*wcFormat->stride + ((wcFormat->captureHeight*wcFormat->stride)>>2) + ((offWebcamY>>1)*(wcFormat->stride>>1));
    for (int i = 0; i < wcFormat->height>>1; i++) {
        memcpy(pConfData, pSample, wcFormat->width >> 1);

        pConfData += (width >> 1);
        pSample += (wcFormat->stride >> 1);
    }
    // copy second sample
    if (rFormat) {
        offX = 0;
        if (rFormat->height < height) offY = (height - rFormat->height) >> 1;
        else offY = 0;

        // Y 
        pConfData = data + offY*width + halfW + offX;
        pSample = (const byte *)rBuf;
        for (int i = 0; i < rFormat->height; i++) {
            memcpy(pConfData, pSample, rFormat->width);

            pConfData += width;
            pSample += rFormat->stride;
        }
        // U
        pConfData = data + height*width + (halfW>>1) + (offY>>1)*(width>>1) + (offX>>1);
        for (int i = 0; i < rFormat->height>>1; i++) {
            memcpy(pConfData, pSample, rFormat->width >> 1);

            pConfData += (width >> 1);
            pSample += (rFormat->stride >> 1);
        }
        // V
        pConfData = data + height*width + (height>>1)*(width>>1) + (halfW>>1) + (offY>>1)*(width>>1) + (offX>>1);
        for (int i = 0; i < rFormat->height>>1; i++) {
            memcpy(pConfData, pSample, rFormat->width >> 1);

            pConfData += (width >> 1);
            pSample += (rFormat->stride >> 1);
        }
    }

    encoder->Encode(data, len, timestamp, duration, &conf);

    free(data);
}

void VideoIoChannel::EventRecv(enum MediaEndpointEvent event, void* ctx)
{
    log->Log(LOG_VIDEO, "VideoIoChannel(%p)::EventRecv (event=%d) ctx=%p connType=%d", this, event, ctx, this->connType);

    switch(event) {
        case CHANNEL_FULL_INTRA_REQUEST:
            if (this->connType == ConnectionType::REMOTE_RTP) videoIo->FullIntraRequest(this);
            break;
        case CHANNEL_NACK_MESSAGE:
            if (this->connType == ConnectionType::REMOTE_RTP) videoIo->RtpNackMessage(this, ctx);
            break;
        case CHANNEL_KEYFRAME_SEQUENCES:
            if(this->connType == ConnectionType::REMOTE_RTP) videoIo->KeyFrameSequences(this, ctx);
            break;
        case CHANNEL_PACKET_LOST_INDICATION:
            if (this->connType == ConnectionType::REMOTE_RTP) videoIo->PacketLostIndication(this);
            break;
        default: break;
    }
}

void VideoIoChannel::Close()
{
    log->Log(LOG_VIDEO, "VideoIoChannel(%p)::Close encoder=%p decoder=%p", this, encoder, decoder);
    open = false;
    videoIo->Remove(this);
    if(encoder) {
        deletedEncoder = encoder;
        encoder->Close();
        encoder = NULL;
    }
    if(decoder) {
        deletedDecoder = decoder;
        decoder->Close();
        decoder = NULL;
    }
    if(renderer) {
        deletedRenderer = renderer;
        renderer->Close();
        renderer = NULL;
    }

    if(processRtpQueue->IsRunning()) processRtpQueue->Cancel();
    while (!rtpPackets.empty()) delete rtpPackets.pop_front();

    TryClose();
}

void VideoIoChannel::TryClose()
{
    if(!encoder && !decoder && !renderer && !deletedEncoder && !deletedDecoder && !deletedRenderer) {
        iomux->SetExec(this, NULL);
    }
}

void VideoIoChannel::IoExec(void* execContext)
{
    user->MediaIoCloseComplete(this);
}

void VideoIoChannel::EncodeResult(class IMediaEncoder * encoder, const void * buf, int len, dword timestamp, dword duration)
{
    log->Log(LOG_VIDEO, "VideoIoChannel(%p)::EncodeResult (CONF=%d) %p len=%d ts=%d duration=%d type=%s coder=%d", this, conference, encoder, len, timestamp, duration, strConnectionType(connType), this->coder);

    VideoSample(buf, len, timestamp, duration, MediaType::VIDEO);
}

void VideoIoChannel::DecodeResult(class IMediaDecoder * decoder, const void * buf, int len, dword timestamp, dword duration, struct VideoFrameFormat * f)
{
    log->Log(LOG_VIDEO, "VideoIoChannel(%p)::DecodeResult (CONF=%d) (decodeStream=%d) len=%d ts=%d d=%d f=%d type=%s coder=%d", this, conference, decodeStream, len, timestamp, duration, f->format, strConnectionType(connType), this->coder);

    if (decoder == this->decoder) {
        // Sample needed for conference
        if(conference) videoIo->ConferenceSample(this, buf, len, timestamp, duration, f);
        if(decodeStream) videoIo->DecodedSample(this, buf, len, timestamp, duration, f);
    }
}

void VideoIoChannel::Initialize(void * context, const char * channelId, enum VideoCoder coder, ConnectionType connType, bool remoteServices)
{
    if(log) log->Log(LOG_VIDEO, "VideoIoChannel(%p)::Initialize %s opened=%d context=%p channelId=%s remoteServices=%d open=%d coders=%d,%d", this, ((coder == VideoCoder::H264) ? "Windows h.264" : (coder == VideoCoder::VP8) ? "VP8" : (coder == VideoCoder::VP9) ? "VP9" : "AV1"), open, context, channelId, remoteServices, open, this->coder, coder);

    if(open && this->coder == coder) return;
    if(encoder) {
        deletedEncoder = encoder;
        encoder->Close();
        encoder = NULL;
    }
    if(decoder) {
        deletedDecoder = decoder;
        decoder->Close();
        decoder = NULL;
    }
    if(renderer) {
        deletedRenderer = renderer;
        renderer->Close();
        renderer = NULL;
    }
    this->context = context;
    if(this->channelId) free(this->channelId);
    this->channelId = _strdup(channelId);
    this->connType = connType;
    this->coder = coder;
    this->remoteServices = remoteServices;
    if(coder == VideoCoder::H264) {
        decoder = IMediaDecoder::Create(iomux, this, log, VideoCoder::H264);
        this->type = MediaType::MP4_CONTAINER;
        if (connType == REMOTE_RTP) {
            if (remoteServices) renderer = videoIo->OpenVideoRenderStream(this, false, VideoCoder::H264);
            else videoIo->StartRemoteVideoEncoder(context, channelId, coder);
        }
    }
    else if(this->coder == VideoCoder::AV1) {
        decoder = IMediaDecoder::Create(iomux, this, log, VideoCoder::AV1);
        this->type = MediaType::WEBM_VP8_CONTAINER;
        if (remoteServices && connType == REMOTE_RTP) renderer = videoIo->OpenVideoRenderStream(this, false, VideoCoder::AV1);
    }
    else if(coder == VideoCoder::VP8) {
        decoder = IMediaDecoder::Create(iomux, this, log, VideoCoder::VP8);
        this->type = MediaType::WEBM_VP8_CONTAINER;
        if (remoteServices && connType == REMOTE_RTP) renderer = videoIo->OpenVideoRenderStream(this, false, VideoCoder::VP8);
    }
    else {
        decoder = IMediaDecoder::Create(iomux, this, log, VideoCoder::VP9);
        this->type = MediaType::WEBM_VP9_CONTAINER;
        if (remoteServices && connType == REMOTE_RTP) renderer = videoIo->OpenVideoRenderStream(this, false, VideoCoder::VP9);
    }
}

void VideoIoChannel::MediaEncoderCloseResult(class IMediaEncoder * encoder)
{
    log->Log(LOG_VIDEO, "VideoIoChannel(%p)::MediaEncoderCloseResult %p,%p open=%d", this, encoder, this->encoder, open);
 
    if (encoder == deletedEncoder) deletedEncoder = NULL;
    delete encoder;
    if (!open) TryClose();
}

void VideoIoChannel::MediaDecoderCloseResult(class IMediaDecoder * decoder)
{
    log->Log(LOG_VIDEO, "VideoIoChannel(%p)::MediaDecoderCloseResult %p,%p open=%d", this, decoder, this->decoder, open);

    if (decoder == deletedDecoder) deletedDecoder = NULL;
    delete decoder;
    if (!open) TryClose();
}

void VideoIoChannel::RenderVideoStreamCloseResult(class IRenderVideoStream* renderer)
{
    log->Log(LOG_VIDEO, "VideoIoChannel(%p)::RenderVideoStreamCloseResult %p,%p open=%d", this, renderer, this->renderer, open);

    if (renderer == deletedRenderer) {
        deletedRenderer = NULL;
        delete renderer;
    }
    if (!open) TryClose();
}

