/** * FreeRDP: A Remote Desktop Protocol Implementation * Audio Output Virtual Channel * * Copyright 2009-2011 Jay Sorg * Copyright 2010-2011 Vic Lee * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #ifndef _WIN32 #include #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "rdpsnd_main.h" #define TIME_DELAY_MS 65 struct rdpsnd_plugin { rdpSvcPlugin plugin; HANDLE thread; wMessageQueue* queue; BYTE cBlockNo; int wCurrentFormatNo; AUDIO_FORMAT* ServerFormats; UINT16 NumberOfServerFormats; AUDIO_FORMAT* ClientFormats; UINT16 NumberOfClientFormats; BOOL expectingWave; BYTE waveData[4]; UINT16 waveDataSize; UINT32 wTimeStamp; int latency; BOOL isOpen; UINT16 fixedFormat; UINT16 fixedChannel; UINT32 fixedRate; char* subsystem; char* device_name; /* Device plugin */ rdpsndDevicePlugin* device; }; void rdpsnd_send_wave_confirm_pdu(rdpsndPlugin* rdpsnd, UINT16 wTimeStamp, BYTE cConfirmedBlockNo); static void* rdpsnd_schedule_thread(void* arg) { wMessage message; UINT16 wTimeDiff; UINT16 wTimeStamp; UINT16 wCurrentTime; RDPSND_WAVE* wave; rdpsndPlugin* rdpsnd = (rdpsndPlugin*) arg; while (1) { if (!MessageQueue_Wait(rdpsnd->queue)) break; if (!MessageQueue_Peek(rdpsnd->queue, &message, TRUE)) break; if (message.id == WMQ_QUIT) break; wave = (RDPSND_WAVE*) message.wParam; wCurrentTime = (UINT16) GetTickCount(); wTimeStamp = wave->wLocalTimeB; if (wCurrentTime <= wTimeStamp) { wTimeDiff = wTimeStamp - wCurrentTime; Sleep(wTimeDiff); } rdpsnd_send_wave_confirm_pdu(rdpsnd, wave->wTimeStampB, wave->cBlockNo); free(wave); } return NULL; } void rdpsnd_send_quality_mode_pdu(rdpsndPlugin* rdpsnd) { wStream* pdu; pdu = Stream_New(NULL, 8); Stream_Write_UINT8(pdu, SNDC_QUALITYMODE); /* msgType */ Stream_Write_UINT8(pdu, 0); /* bPad */ Stream_Write_UINT16(pdu, 4); /* BodySize */ Stream_Write_UINT16(pdu, HIGH_QUALITY); /* wQualityMode */ Stream_Write_UINT16(pdu, 0); /* Reserved */ svc_plugin_send((rdpSvcPlugin*) rdpsnd, pdu); } void rdpsnd_select_supported_audio_formats(rdpsndPlugin* rdpsnd) { int index; AUDIO_FORMAT* serverFormat; AUDIO_FORMAT* clientFormat; rdpsnd_free_audio_formats(rdpsnd->ClientFormats, rdpsnd->NumberOfClientFormats); rdpsnd->NumberOfClientFormats = 0; rdpsnd->ClientFormats = NULL; rdpsnd->ClientFormats = (AUDIO_FORMAT*) malloc(sizeof(AUDIO_FORMAT) * rdpsnd->NumberOfServerFormats); for (index = 0; index < (int) rdpsnd->NumberOfServerFormats; index++) { serverFormat = &rdpsnd->ServerFormats[index]; if (rdpsnd->fixedFormat > 0 && (rdpsnd->fixedFormat != serverFormat->wFormatTag)) continue; if (rdpsnd->fixedChannel > 0 && (rdpsnd->fixedChannel != serverFormat->nChannels)) continue; if (rdpsnd->fixedRate > 0 && (rdpsnd->fixedRate != serverFormat->nSamplesPerSec)) continue; if (rdpsnd->device && rdpsnd->device->FormatSupported(rdpsnd->device, serverFormat)) { clientFormat = &rdpsnd->ClientFormats[rdpsnd->NumberOfClientFormats++]; CopyMemory(clientFormat, serverFormat, sizeof(AUDIO_FORMAT)); clientFormat->cbSize = 0; if (serverFormat->cbSize > 0) { clientFormat->data = (BYTE*) malloc(serverFormat->cbSize); CopyMemory(clientFormat->data, serverFormat->data, serverFormat->cbSize); clientFormat->cbSize = serverFormat->cbSize; } } } #if 0 fprintf(stderr, "Server "); rdpsnd_print_audio_formats(rdpsnd->ServerFormats, rdpsnd->NumberOfServerFormats); fprintf(stderr, "\n"); fprintf(stderr, "Client "); rdpsnd_print_audio_formats(rdpsnd->ClientFormats, rdpsnd->NumberOfClientFormats); fprintf(stderr, "\n"); #endif } void rdpsnd_send_client_audio_formats(rdpsndPlugin* rdpsnd) { int index; wStream* pdu; UINT16 length; UINT32 dwVolume; UINT16 dwVolumeLeft; UINT16 dwVolumeRight; UINT16 wNumberOfFormats; AUDIO_FORMAT* clientFormat; dwVolumeLeft = ((50 * 0xFFFF) / 100); /* 50% */ dwVolumeRight = ((50 * 0xFFFF) / 100); /* 50% */ dwVolume = (dwVolumeLeft << 16) | dwVolumeRight; if (rdpsnd->device) { if (rdpsnd->device->GetVolume) dwVolume = rdpsnd->device->GetVolume(rdpsnd->device); } wNumberOfFormats = rdpsnd->NumberOfClientFormats; length = 4 + 20; for (index = 0; index < (int) wNumberOfFormats; index++) length += (18 + rdpsnd->ClientFormats[index].cbSize); pdu = Stream_New(NULL, length); Stream_Write_UINT8(pdu, SNDC_FORMATS); /* msgType */ Stream_Write_UINT8(pdu, 0); /* bPad */ Stream_Write_UINT16(pdu, length - 4); /* BodySize */ Stream_Write_UINT32(pdu, TSSNDCAPS_ALIVE | TSSNDCAPS_VOLUME); /* dwFlags */ Stream_Write_UINT32(pdu, dwVolume); /* dwVolume */ Stream_Write_UINT32(pdu, 0); /* dwPitch */ Stream_Write_UINT16(pdu, 0); /* wDGramPort */ Stream_Write_UINT16(pdu, wNumberOfFormats); /* wNumberOfFormats */ Stream_Write_UINT8(pdu, 0); /* cLastBlockConfirmed */ Stream_Write_UINT16(pdu, 6); /* wVersion */ Stream_Write_UINT8(pdu, 0); /* bPad */ for (index = 0; index < (int) wNumberOfFormats; index++) { clientFormat = &rdpsnd->ClientFormats[index]; Stream_Write_UINT16(pdu, clientFormat->wFormatTag); Stream_Write_UINT16(pdu, clientFormat->nChannels); Stream_Write_UINT32(pdu, clientFormat->nSamplesPerSec); Stream_Write_UINT32(pdu, clientFormat->nAvgBytesPerSec); Stream_Write_UINT16(pdu, clientFormat->nBlockAlign); Stream_Write_UINT16(pdu, clientFormat->wBitsPerSample); Stream_Write_UINT16(pdu, clientFormat->cbSize); if (clientFormat->cbSize > 0) Stream_Write(pdu, clientFormat->data, clientFormat->cbSize); } svc_plugin_send((rdpSvcPlugin*) rdpsnd, pdu); } void rdpsnd_recv_server_audio_formats_pdu(rdpsndPlugin* rdpsnd, wStream* s) { int index; UINT16 wVersion; AUDIO_FORMAT* format; UINT16 wNumberOfFormats; rdpsnd_free_audio_formats(rdpsnd->ServerFormats, rdpsnd->NumberOfServerFormats); rdpsnd->NumberOfServerFormats = 0; rdpsnd->ServerFormats = NULL; Stream_Seek_UINT32(s); /* dwFlags */ Stream_Seek_UINT32(s); /* dwVolume */ Stream_Seek_UINT32(s); /* dwPitch */ Stream_Seek_UINT16(s); /* wDGramPort */ Stream_Read_UINT16(s, wNumberOfFormats); Stream_Read_UINT8(s, rdpsnd->cBlockNo); /* cLastBlockConfirmed */ Stream_Read_UINT16(s, wVersion); /* wVersion */ Stream_Seek_UINT8(s); /* bPad */ rdpsnd->NumberOfServerFormats = wNumberOfFormats; rdpsnd->ServerFormats = (AUDIO_FORMAT*) malloc(sizeof(AUDIO_FORMAT) * wNumberOfFormats); for (index = 0; index < (int) wNumberOfFormats; index++) { format = &rdpsnd->ServerFormats[index]; Stream_Read_UINT16(s, format->wFormatTag); /* wFormatTag */ Stream_Read_UINT16(s, format->nChannels); /* nChannels */ Stream_Read_UINT32(s, format->nSamplesPerSec); /* nSamplesPerSec */ Stream_Read_UINT32(s, format->nAvgBytesPerSec); /* nAvgBytesPerSec */ Stream_Read_UINT16(s, format->nBlockAlign); /* nBlockAlign */ Stream_Read_UINT16(s, format->wBitsPerSample); /* wBitsPerSample */ Stream_Read_UINT16(s, format->cbSize); /* cbSize */ format->data = (BYTE*) malloc(format->cbSize); Stream_Read(s, format->data, format->cbSize); } rdpsnd_select_supported_audio_formats(rdpsnd); rdpsnd_send_client_audio_formats(rdpsnd); if (wVersion >= 6) rdpsnd_send_quality_mode_pdu(rdpsnd); } void rdpsnd_send_training_confirm_pdu(rdpsndPlugin* rdpsnd, UINT16 wTimeStamp, UINT16 wPackSize) { wStream* pdu; pdu = Stream_New(NULL, 8); Stream_Write_UINT8(pdu, SNDC_TRAINING); /* msgType */ Stream_Write_UINT8(pdu, 0); /* bPad */ Stream_Write_UINT16(pdu, 4); /* BodySize */ Stream_Write_UINT16(pdu, wTimeStamp); Stream_Write_UINT16(pdu, wPackSize); svc_plugin_send((rdpSvcPlugin*) rdpsnd, pdu); } static void rdpsnd_recv_training_pdu(rdpsndPlugin* rdpsnd, wStream* s) { UINT16 wTimeStamp; UINT16 wPackSize; Stream_Read_UINT16(s, wTimeStamp); Stream_Read_UINT16(s, wPackSize); rdpsnd_send_training_confirm_pdu(rdpsnd, wTimeStamp, wPackSize); } static void rdpsnd_recv_wave_info_pdu(rdpsndPlugin* rdpsnd, wStream* s, UINT16 BodySize) { UINT16 wFormatNo; AUDIO_FORMAT* format; rdpsnd->expectingWave = TRUE; Stream_Read_UINT16(s, rdpsnd->wTimeStamp); Stream_Read_UINT16(s, wFormatNo); Stream_Read_UINT8(s, rdpsnd->cBlockNo); Stream_Seek(s, 3); /* bPad */ Stream_Read(s, rdpsnd->waveData, 4); rdpsnd->waveDataSize = BodySize - 8; format = &rdpsnd->ClientFormats[wFormatNo]; if (!rdpsnd->isOpen) { rdpsnd->isOpen = TRUE; rdpsnd->wCurrentFormatNo = wFormatNo; //rdpsnd_print_audio_format(format); if (rdpsnd->device) { IFCALL(rdpsnd->device->Open, rdpsnd->device, format, rdpsnd->latency); } } else if (wFormatNo != rdpsnd->wCurrentFormatNo) { rdpsnd->wCurrentFormatNo = wFormatNo; if (rdpsnd->device) { IFCALL(rdpsnd->device->SetFormat, rdpsnd->device, format, rdpsnd->latency); } } } void rdpsnd_send_wave_confirm_pdu(rdpsndPlugin* rdpsnd, UINT16 wTimeStamp, BYTE cConfirmedBlockNo) { wStream* pdu; pdu = Stream_New(NULL, 8); Stream_Write_UINT8(pdu, SNDC_WAVECONFIRM); Stream_Write_UINT8(pdu, 0); Stream_Write_UINT16(pdu, 4); Stream_Write_UINT16(pdu, wTimeStamp); Stream_Write_UINT8(pdu, cConfirmedBlockNo); /* cConfirmedBlockNo */ Stream_Write_UINT8(pdu, 0); /* bPad */ svc_plugin_send((rdpSvcPlugin*) rdpsnd, pdu); } void rdpsnd_device_send_wave_confirm_pdu(rdpsndDevicePlugin* device, RDPSND_WAVE* wave) { MessageQueue_Post(device->rdpsnd->queue, NULL, 0, (void*) wave, NULL); } static void rdpsnd_recv_wave_pdu(rdpsndPlugin* rdpsnd, wStream* s) { int size; BYTE* data; RDPSND_WAVE* wave; AUDIO_FORMAT* format; rdpsnd->expectingWave = FALSE; /** * The Wave PDU is a special case: it is always sent after a Wave Info PDU, * and we do not process its header. Instead, the header is pad that needs * to be filled with the first four bytes of the audio sample data sent as * part of the preceding Wave Info PDU. */ CopyMemory(Stream_Buffer(s), rdpsnd->waveData, 4); data = Stream_Buffer(s); size = Stream_Capacity(s); wave = (RDPSND_WAVE*) malloc(sizeof(RDPSND_WAVE)); wave->wLocalTimeA = GetTickCount(); wave->wTimeStampA = rdpsnd->wTimeStamp; wave->wFormatNo = rdpsnd->wCurrentFormatNo; wave->cBlockNo = rdpsnd->cBlockNo; wave->data = data; wave->length = size; format = &rdpsnd->ClientFormats[rdpsnd->wCurrentFormatNo]; wave->wAudioLength = rdpsnd_compute_audio_time_length(format, size); if (!rdpsnd->device) return; if (rdpsnd->device->WaveDecode) { IFCALL(rdpsnd->device->WaveDecode, rdpsnd->device, wave); } if (rdpsnd->device->WavePlay) { IFCALL(rdpsnd->device->WavePlay, rdpsnd->device, wave); } else { IFCALL(rdpsnd->device->Play, rdpsnd->device, data, size); } if (!rdpsnd->device->WavePlay) { wave->wTimeStampB = rdpsnd->wTimeStamp + wave->wAudioLength + TIME_DELAY_MS; wave->wLocalTimeB = wave->wLocalTimeA + wave->wAudioLength + TIME_DELAY_MS; rdpsnd->device->WaveConfirm(rdpsnd->device, wave); } } static void rdpsnd_recv_close_pdu(rdpsndPlugin* rdpsnd) { DEBUG_SVC("server closes."); if (rdpsnd->device) { IFCALL(rdpsnd->device->Close, rdpsnd->device); } rdpsnd->isOpen = FALSE; } static void rdpsnd_recv_volume_pdu(rdpsndPlugin* rdpsnd, wStream* s) { UINT32 dwVolume; Stream_Read_UINT32(s, dwVolume); DEBUG_SVC("dwVolume 0x%X", dwVolume); if (rdpsnd->device) { IFCALL(rdpsnd->device->SetVolume, rdpsnd->device, dwVolume); } } static void rdpsnd_recv_pdu(rdpSvcPlugin* plugin, wStream* s) { BYTE msgType; UINT16 BodySize; rdpsndPlugin* rdpsnd = (rdpsndPlugin*) plugin; if (rdpsnd->expectingWave) { rdpsnd_recv_wave_pdu(rdpsnd, s); Stream_Free(s, TRUE); return; } Stream_Read_UINT8(s, msgType); /* msgType */ Stream_Seek_UINT8(s); /* bPad */ Stream_Read_UINT16(s, BodySize); //fprintf(stderr, "msgType %d BodySize %d\n", msgType, BodySize); switch (msgType) { case SNDC_FORMATS: rdpsnd_recv_server_audio_formats_pdu(rdpsnd, s); break; case SNDC_TRAINING: rdpsnd_recv_training_pdu(rdpsnd, s); break; case SNDC_WAVE: rdpsnd_recv_wave_info_pdu(rdpsnd, s, BodySize); break; case SNDC_CLOSE: rdpsnd_recv_close_pdu(rdpsnd); break; case SNDC_SETVOLUME: rdpsnd_recv_volume_pdu(rdpsnd, s); break; default: DEBUG_WARN("unknown msgType %d", msgType); break; } Stream_Free(s, TRUE); } static void rdpsnd_register_device_plugin(rdpsndPlugin* rdpsnd, rdpsndDevicePlugin* device) { if (rdpsnd->device) { DEBUG_WARN("existing device, abort."); return; } rdpsnd->device = device; device->rdpsnd = rdpsnd; device->WaveConfirm = rdpsnd_device_send_wave_confirm_pdu; } static BOOL rdpsnd_load_device_plugin(rdpsndPlugin* rdpsnd, const char* name, ADDIN_ARGV* args) { PFREERDP_RDPSND_DEVICE_ENTRY entry; FREERDP_RDPSND_DEVICE_ENTRY_POINTS entryPoints; entry = (PFREERDP_RDPSND_DEVICE_ENTRY) freerdp_load_channel_addin_entry("rdpsnd", (LPSTR) name, NULL, 0); if (!entry) return FALSE; entryPoints.rdpsnd = rdpsnd; entryPoints.pRegisterRdpsndDevice = rdpsnd_register_device_plugin; entryPoints.args = args; if (entry(&entryPoints) != 0) { DEBUG_WARN("%s entry returns error.", name); return FALSE; } return TRUE; } void rdpsnd_set_subsystem(rdpsndPlugin* rdpsnd, char* subsystem) { if (rdpsnd->subsystem) free(rdpsnd->subsystem); rdpsnd->subsystem = _strdup(subsystem); } void rdpsnd_set_device_name(rdpsndPlugin* rdpsnd, char* device_name) { if (rdpsnd->device_name) free(rdpsnd->device_name); rdpsnd->device_name = _strdup(device_name); } COMMAND_LINE_ARGUMENT_A rdpsnd_args[] = { { "sys", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "subsystem" }, { "dev", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "device" }, { "format", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "format" }, { "rate", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "rate" }, { "channel", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "channel" }, { "latency", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "latency" }, { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } }; static void rdpsnd_process_addin_args(rdpsndPlugin* rdpsnd, ADDIN_ARGV* args) { int status; DWORD flags; COMMAND_LINE_ARGUMENT_A* arg; flags = COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON; status = CommandLineParseArgumentsA(args->argc, (const char**) args->argv, rdpsnd_args, flags, rdpsnd, NULL, NULL); arg = rdpsnd_args; do { if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) continue; CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "sys") { rdpsnd_set_subsystem(rdpsnd, arg->Value); } CommandLineSwitchCase(arg, "dev") { rdpsnd_set_device_name(rdpsnd, arg->Value); } CommandLineSwitchCase(arg, "format") { rdpsnd->fixedFormat = atoi(arg->Value); } CommandLineSwitchCase(arg, "rate") { rdpsnd->fixedRate = atoi(arg->Value); } CommandLineSwitchCase(arg, "channel") { rdpsnd->fixedChannel = atoi(arg->Value); } CommandLineSwitchCase(arg, "latency") { rdpsnd->latency = atoi(arg->Value); } CommandLineSwitchDefault(arg) { } CommandLineSwitchEnd(arg) } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); } static void rdpsnd_process_connect(rdpSvcPlugin* plugin) { ADDIN_ARGV* args; rdpsndPlugin* rdpsnd = (rdpsndPlugin*) plugin; DEBUG_SVC("connecting"); rdpsnd->latency = -1; rdpsnd->queue = MessageQueue_New(); rdpsnd->thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) rdpsnd_schedule_thread, (void*) plugin, 0, NULL); args = (ADDIN_ARGV*) plugin->channel_entry_points.pExtendedData; if (args) rdpsnd_process_addin_args(rdpsnd, args); if (rdpsnd->subsystem) { if (strcmp(rdpsnd->subsystem, "fake") == 0) return; rdpsnd_load_device_plugin(rdpsnd, rdpsnd->subsystem, args); } if (!rdpsnd->device) { rdpsnd_set_subsystem(rdpsnd, "pulse"); rdpsnd_set_device_name(rdpsnd, ""); rdpsnd_load_device_plugin(rdpsnd, rdpsnd->subsystem, args); } if (!rdpsnd->device) { rdpsnd_set_subsystem(rdpsnd, "alsa"); rdpsnd_set_device_name(rdpsnd, "default"); rdpsnd_load_device_plugin(rdpsnd, rdpsnd->subsystem, args); } if (!rdpsnd->device) { rdpsnd_set_subsystem(rdpsnd, "macaudio"); rdpsnd_set_device_name(rdpsnd, "default"); rdpsnd_load_device_plugin(rdpsnd, rdpsnd->subsystem, args); } if (!rdpsnd->device) { rdpsnd_set_subsystem(rdpsnd, "winmm"); rdpsnd_set_device_name(rdpsnd, ""); rdpsnd_load_device_plugin(rdpsnd, rdpsnd->subsystem, args); } if (!rdpsnd->device) { DEBUG_WARN("no sound device."); return; } } static void rdpsnd_process_event(rdpSvcPlugin* plugin, wMessage* event) { freerdp_event_free(event); } static void rdpsnd_process_terminate(rdpSvcPlugin* plugin) { rdpsndPlugin* rdpsnd = (rdpsndPlugin*) plugin; if (rdpsnd->device) IFCALL(rdpsnd->device->Free, rdpsnd->device); MessageQueue_PostQuit(rdpsnd->queue, 0); WaitForSingleObject(rdpsnd->thread, INFINITE); MessageQueue_Free(rdpsnd->queue); CloseHandle(rdpsnd->thread); if (rdpsnd->subsystem) free(rdpsnd->subsystem); if (rdpsnd->device_name) free(rdpsnd->device_name); rdpsnd_free_audio_formats(rdpsnd->ServerFormats, rdpsnd->NumberOfServerFormats); rdpsnd->NumberOfServerFormats = 0; rdpsnd->ServerFormats = NULL; rdpsnd_free_audio_formats(rdpsnd->ClientFormats, rdpsnd->NumberOfClientFormats); rdpsnd->NumberOfClientFormats = 0; rdpsnd->ClientFormats = NULL; } /* rdpsnd is always built-in */ #define VirtualChannelEntry rdpsnd_VirtualChannelEntry int VirtualChannelEntry(PCHANNEL_ENTRY_POINTS pEntryPoints) { rdpsndPlugin* _p; _p = (rdpsndPlugin*) malloc(sizeof(rdpsndPlugin)); ZeroMemory(_p, sizeof(rdpsndPlugin)); _p->plugin.channel_def.options = CHANNEL_OPTION_INITIALIZED | CHANNEL_OPTION_ENCRYPT_RDP; strcpy(_p->plugin.channel_def.name, "rdpsnd"); _p->plugin.connect_callback = rdpsnd_process_connect; _p->plugin.receive_callback = rdpsnd_recv_pdu; _p->plugin.event_callback = rdpsnd_process_event; _p->plugin.terminate_callback = rdpsnd_process_terminate; #if !defined(_WIN32) && !defined(ANDROID) { sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGIO); pthread_sigmask(SIG_BLOCK, &mask, NULL); } #endif svc_plugin_init((rdpSvcPlugin*) _p, pEntryPoints); return 1; }