f8a4c6a9fe
Currently, all FreeRDP-based clients don't send any WaveConfirm PDUs for received samples, when using a dynamic channel for audio output redirection. [MS-RDPEA] 2.2.3.8 Wave Confirm PDU mentions, that a WaveConfirm PDU MUST be sent, when a WaveInfo PDU + Wave PDU, or Wave2 PDU is received and when the audio data sample is emitted to completion by the client. The first WaveConfirm PDU is used by the server to determine the network latency and the second WaveConfirm PDU is used by the server to determine the render latency. So, fix the current behaviour, where FreeRDP currently does not send any WaveConfirm PDU, when using the dynamic channel, or only sends one WaveConfirm PDU for the sample. For the first WaveConfirm PDU, use the same timestamp, that was included in the first WaveInfo/Wave2 PDU. For the second WaveConfirm PDU, add the render latency on top of the arrival timestamp.
1707 lines
45 KiB
C
1707 lines
45 KiB
C
/**
|
|
* FreeRDP: A Remote Desktop Protocol Implementation
|
|
* Audio Output Virtual Channel
|
|
*
|
|
* Copyright 2009-2011 Jay Sorg
|
|
* Copyright 2010-2011 Vic Lee
|
|
* Copyright 2012-2013 Marc-Andre Moreau <marcandre.moreau@gmail.com>
|
|
* Copyright 2015 Thincast Technologies GmbH
|
|
* Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com>
|
|
* Copyright 2016 David PHAM-VAN <d.phamvan@inuvika.com>
|
|
*
|
|
* 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 <sys/time.h>
|
|
#include <signal.h>
|
|
#endif
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
|
|
#include <winpr/crt.h>
|
|
#include <winpr/wlog.h>
|
|
#include <winpr/stream.h>
|
|
#include <winpr/cmdline.h>
|
|
#include <winpr/sysinfo.h>
|
|
#include <winpr/collections.h>
|
|
|
|
#include <freerdp/types.h>
|
|
#include <freerdp/addin.h>
|
|
#include <freerdp/codec/dsp.h>
|
|
|
|
#include "rdpsnd_common.h"
|
|
#include "rdpsnd_main.h"
|
|
|
|
struct _RDPSND_CHANNEL_CALLBACK
|
|
{
|
|
IWTSVirtualChannelCallback iface;
|
|
|
|
IWTSPlugin* plugin;
|
|
IWTSVirtualChannelManager* channel_mgr;
|
|
IWTSVirtualChannel* channel;
|
|
};
|
|
typedef struct _RDPSND_CHANNEL_CALLBACK RDPSND_CHANNEL_CALLBACK;
|
|
|
|
struct _RDPSND_LISTENER_CALLBACK
|
|
{
|
|
IWTSListenerCallback iface;
|
|
|
|
IWTSPlugin* plugin;
|
|
IWTSVirtualChannelManager* channel_mgr;
|
|
RDPSND_CHANNEL_CALLBACK* channel_callback;
|
|
};
|
|
typedef struct _RDPSND_LISTENER_CALLBACK RDPSND_LISTENER_CALLBACK;
|
|
|
|
struct rdpsnd_plugin
|
|
{
|
|
IWTSPlugin iface;
|
|
IWTSListener* listener;
|
|
RDPSND_LISTENER_CALLBACK* listener_callback;
|
|
|
|
CHANNEL_DEF channelDef;
|
|
CHANNEL_ENTRY_POINTS_FREERDP_EX channelEntryPoints;
|
|
|
|
wStreamPool* pool;
|
|
wStream* data_in;
|
|
|
|
void* InitHandle;
|
|
DWORD OpenHandle;
|
|
|
|
wLog* log;
|
|
|
|
BYTE cBlockNo;
|
|
UINT16 wQualityMode;
|
|
UINT16 wCurrentFormatNo;
|
|
|
|
AUDIO_FORMAT* ServerFormats;
|
|
UINT16 NumberOfServerFormats;
|
|
|
|
AUDIO_FORMAT* ClientFormats;
|
|
UINT16 NumberOfClientFormats;
|
|
|
|
BOOL attached;
|
|
BOOL connected;
|
|
BOOL dynamic;
|
|
|
|
BOOL expectingWave;
|
|
BYTE waveData[4];
|
|
UINT16 waveDataSize;
|
|
UINT16 wTimeStamp;
|
|
UINT64 wArrivalTime;
|
|
|
|
UINT32 latency;
|
|
BOOL isOpen;
|
|
AUDIO_FORMAT* fixed_format;
|
|
|
|
UINT32 startPlayTime;
|
|
size_t totalPlaySize;
|
|
|
|
char* subsystem;
|
|
char* device_name;
|
|
|
|
/* Device plugin */
|
|
rdpsndDevicePlugin* device;
|
|
rdpContext* rdpcontext;
|
|
|
|
FREERDP_DSP_CONTEXT* dsp_context;
|
|
|
|
HANDLE thread;
|
|
wMessageQueue* queue;
|
|
BOOL initialized;
|
|
};
|
|
|
|
static const char* rdpsnd_is_dyn_str(BOOL dynamic)
|
|
{
|
|
if (dynamic)
|
|
return "[dynamic]";
|
|
return "[static]";
|
|
}
|
|
|
|
static void rdpsnd_virtual_channel_event_terminated(rdpsndPlugin* rdpsnd);
|
|
|
|
/**
|
|
* Function description
|
|
*
|
|
* @return 0 on success, otherwise a Win32 error code
|
|
*/
|
|
static UINT rdpsnd_virtual_channel_write(rdpsndPlugin* rdpsnd, wStream* s);
|
|
|
|
/**
|
|
* Function description
|
|
*
|
|
* @return 0 on success, otherwise a Win32 error code
|
|
*/
|
|
static UINT rdpsnd_send_quality_mode_pdu(rdpsndPlugin* rdpsnd)
|
|
{
|
|
wStream* pdu;
|
|
pdu = Stream_New(NULL, 8);
|
|
|
|
if (!pdu)
|
|
{
|
|
WLog_ERR(TAG, "%s Stream_New failed!", rdpsnd_is_dyn_str(rdpsnd->dynamic));
|
|
return CHANNEL_RC_NO_MEMORY;
|
|
}
|
|
|
|
Stream_Write_UINT8(pdu, SNDC_QUALITYMODE); /* msgType */
|
|
Stream_Write_UINT8(pdu, 0); /* bPad */
|
|
Stream_Write_UINT16(pdu, 4); /* BodySize */
|
|
Stream_Write_UINT16(pdu, rdpsnd->wQualityMode); /* wQualityMode */
|
|
Stream_Write_UINT16(pdu, 0); /* Reserved */
|
|
WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s QualityMode: %" PRIu16 "",
|
|
rdpsnd_is_dyn_str(rdpsnd->dynamic), rdpsnd->wQualityMode);
|
|
return rdpsnd_virtual_channel_write(rdpsnd, pdu);
|
|
}
|
|
|
|
static void rdpsnd_select_supported_audio_formats(rdpsndPlugin* rdpsnd)
|
|
{
|
|
UINT16 index;
|
|
audio_formats_free(rdpsnd->ClientFormats, rdpsnd->NumberOfClientFormats);
|
|
rdpsnd->NumberOfClientFormats = 0;
|
|
rdpsnd->ClientFormats = NULL;
|
|
|
|
if (!rdpsnd->NumberOfServerFormats)
|
|
return;
|
|
|
|
rdpsnd->ClientFormats = audio_formats_new(rdpsnd->NumberOfServerFormats);
|
|
|
|
if (!rdpsnd->ClientFormats || !rdpsnd->device)
|
|
return;
|
|
|
|
for (index = 0; index < rdpsnd->NumberOfServerFormats; index++)
|
|
{
|
|
const AUDIO_FORMAT* serverFormat = &rdpsnd->ServerFormats[index];
|
|
|
|
if (!audio_format_compatible(rdpsnd->fixed_format, serverFormat))
|
|
continue;
|
|
|
|
if (freerdp_dsp_supports_format(serverFormat, FALSE) ||
|
|
rdpsnd->device->FormatSupported(rdpsnd->device, serverFormat))
|
|
{
|
|
AUDIO_FORMAT* clientFormat = &rdpsnd->ClientFormats[rdpsnd->NumberOfClientFormats++];
|
|
audio_format_copy(serverFormat, clientFormat);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function description
|
|
*
|
|
* @return 0 on success, otherwise a Win32 error code
|
|
*/
|
|
static UINT rdpsnd_send_client_audio_formats(rdpsndPlugin* rdpsnd)
|
|
{
|
|
UINT16 index;
|
|
wStream* pdu;
|
|
UINT16 length;
|
|
UINT32 dwVolume;
|
|
UINT16 wNumberOfFormats;
|
|
|
|
if (!rdpsnd->device || (!rdpsnd->dynamic && (rdpsnd->OpenHandle == 0)))
|
|
return CHANNEL_RC_INITIALIZATION_ERROR;
|
|
|
|
dwVolume = IFCALLRESULT(0, rdpsnd->device->GetVolume, rdpsnd->device);
|
|
wNumberOfFormats = rdpsnd->NumberOfClientFormats;
|
|
length = 4 + 20;
|
|
|
|
for (index = 0; index < wNumberOfFormats; index++)
|
|
length += (18 + rdpsnd->ClientFormats[index].cbSize);
|
|
|
|
pdu = Stream_New(NULL, length);
|
|
|
|
if (!pdu)
|
|
{
|
|
WLog_ERR(TAG, "%s Stream_New failed!", rdpsnd_is_dyn_str(rdpsnd->dynamic));
|
|
return CHANNEL_RC_NO_MEMORY;
|
|
}
|
|
|
|
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, CHANNEL_VERSION_WIN_MAX); /* wVersion */
|
|
Stream_Write_UINT8(pdu, 0); /* bPad */
|
|
|
|
for (index = 0; index < wNumberOfFormats; index++)
|
|
{
|
|
const AUDIO_FORMAT* clientFormat = &rdpsnd->ClientFormats[index];
|
|
|
|
if (!audio_format_write(pdu, clientFormat))
|
|
{
|
|
Stream_Free(pdu, TRUE);
|
|
return ERROR_INTERNAL_ERROR;
|
|
}
|
|
}
|
|
|
|
WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Client Audio Formats",
|
|
rdpsnd_is_dyn_str(rdpsnd->dynamic));
|
|
return rdpsnd_virtual_channel_write(rdpsnd, pdu);
|
|
}
|
|
|
|
/**
|
|
* Function description
|
|
*
|
|
* @return 0 on success, otherwise a Win32 error code
|
|
*/
|
|
static UINT rdpsnd_recv_server_audio_formats_pdu(rdpsndPlugin* rdpsnd, wStream* s)
|
|
{
|
|
UINT16 index;
|
|
UINT16 wVersion;
|
|
UINT16 wNumberOfFormats;
|
|
UINT ret = ERROR_BAD_LENGTH;
|
|
audio_formats_free(rdpsnd->ServerFormats, rdpsnd->NumberOfServerFormats);
|
|
rdpsnd->NumberOfServerFormats = 0;
|
|
rdpsnd->ServerFormats = NULL;
|
|
|
|
if (Stream_GetRemainingLength(s) < 30)
|
|
return ERROR_BAD_LENGTH;
|
|
|
|
/* http://msdn.microsoft.com/en-us/library/cc240956.aspx */
|
|
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;
|
|
|
|
if (Stream_GetRemainingLength(s) / 14 < wNumberOfFormats)
|
|
return ERROR_BAD_LENGTH;
|
|
|
|
rdpsnd->ServerFormats = audio_formats_new(wNumberOfFormats);
|
|
|
|
if (!rdpsnd->ServerFormats)
|
|
return CHANNEL_RC_NO_MEMORY;
|
|
|
|
for (index = 0; index < wNumberOfFormats; index++)
|
|
{
|
|
AUDIO_FORMAT* format = &rdpsnd->ServerFormats[index];
|
|
|
|
if (!audio_format_read(s, format))
|
|
goto out_fail;
|
|
}
|
|
|
|
rdpsnd_select_supported_audio_formats(rdpsnd);
|
|
WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Server Audio Formats",
|
|
rdpsnd_is_dyn_str(rdpsnd->dynamic));
|
|
ret = rdpsnd_send_client_audio_formats(rdpsnd);
|
|
|
|
if (ret == CHANNEL_RC_OK)
|
|
{
|
|
if (wVersion >= CHANNEL_VERSION_WIN_7)
|
|
ret = rdpsnd_send_quality_mode_pdu(rdpsnd);
|
|
}
|
|
|
|
return ret;
|
|
out_fail:
|
|
audio_formats_free(rdpsnd->ServerFormats, rdpsnd->NumberOfServerFormats);
|
|
rdpsnd->ServerFormats = NULL;
|
|
rdpsnd->NumberOfServerFormats = 0;
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Function description
|
|
*
|
|
* @return 0 on success, otherwise a Win32 error code
|
|
*/
|
|
static UINT rdpsnd_send_training_confirm_pdu(rdpsndPlugin* rdpsnd, UINT16 wTimeStamp,
|
|
UINT16 wPackSize)
|
|
{
|
|
wStream* pdu;
|
|
pdu = Stream_New(NULL, 8);
|
|
|
|
if (!pdu)
|
|
{
|
|
WLog_ERR(TAG, "%s Stream_New failed!", rdpsnd_is_dyn_str(rdpsnd->dynamic));
|
|
return CHANNEL_RC_NO_MEMORY;
|
|
}
|
|
|
|
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);
|
|
WLog_Print(rdpsnd->log, WLOG_DEBUG,
|
|
"%s Training Response: wTimeStamp: %" PRIu16 " wPackSize: %" PRIu16 "",
|
|
rdpsnd_is_dyn_str(rdpsnd->dynamic), wTimeStamp, wPackSize);
|
|
return rdpsnd_virtual_channel_write(rdpsnd, pdu);
|
|
}
|
|
|
|
/**
|
|
* Function description
|
|
*
|
|
* @return 0 on success, otherwise a Win32 error code
|
|
*/
|
|
static UINT rdpsnd_recv_training_pdu(rdpsndPlugin* rdpsnd, wStream* s)
|
|
{
|
|
UINT16 wTimeStamp;
|
|
UINT16 wPackSize;
|
|
|
|
if (Stream_GetRemainingLength(s) < 4)
|
|
return ERROR_BAD_LENGTH;
|
|
|
|
Stream_Read_UINT16(s, wTimeStamp);
|
|
Stream_Read_UINT16(s, wPackSize);
|
|
WLog_Print(rdpsnd->log, WLOG_DEBUG,
|
|
"%s Training Request: wTimeStamp: %" PRIu16 " wPackSize: %" PRIu16 "",
|
|
rdpsnd_is_dyn_str(rdpsnd->dynamic), wTimeStamp, wPackSize);
|
|
return rdpsnd_send_training_confirm_pdu(rdpsnd, wTimeStamp, wPackSize);
|
|
}
|
|
|
|
static BOOL rdpsnd_ensure_device_is_open(rdpsndPlugin* rdpsnd, UINT32 wFormatNo,
|
|
const AUDIO_FORMAT* format)
|
|
{
|
|
if (!rdpsnd)
|
|
return FALSE;
|
|
|
|
if (!rdpsnd->isOpen || (wFormatNo != rdpsnd->wCurrentFormatNo))
|
|
{
|
|
BOOL rc;
|
|
BOOL supported;
|
|
AUDIO_FORMAT deviceFormat = *format;
|
|
|
|
IFCALL(rdpsnd->device->Close, rdpsnd->device);
|
|
supported = IFCALLRESULT(FALSE, rdpsnd->device->FormatSupported, rdpsnd->device, format);
|
|
|
|
if (!supported)
|
|
{
|
|
if (!IFCALLRESULT(FALSE, rdpsnd->device->DefaultFormat, rdpsnd->device, format,
|
|
&deviceFormat))
|
|
{
|
|
deviceFormat.wFormatTag = WAVE_FORMAT_PCM;
|
|
deviceFormat.wBitsPerSample = 16;
|
|
deviceFormat.cbSize = 0;
|
|
}
|
|
}
|
|
|
|
WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Opening device with format %s [backend %s]",
|
|
rdpsnd_is_dyn_str(rdpsnd->dynamic),
|
|
audio_format_get_tag_string(format->wFormatTag),
|
|
audio_format_get_tag_string(deviceFormat.wFormatTag));
|
|
rc = IFCALLRESULT(FALSE, rdpsnd->device->Open, rdpsnd->device, &deviceFormat,
|
|
rdpsnd->latency);
|
|
|
|
if (!rc)
|
|
return FALSE;
|
|
|
|
if (!supported)
|
|
{
|
|
if (!freerdp_dsp_context_reset(rdpsnd->dsp_context, format))
|
|
return FALSE;
|
|
}
|
|
|
|
rdpsnd->isOpen = TRUE;
|
|
rdpsnd->wCurrentFormatNo = wFormatNo;
|
|
rdpsnd->startPlayTime = 0;
|
|
rdpsnd->totalPlaySize = 0;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* Function description
|
|
*
|
|
* @return 0 on success, otherwise a Win32 error code
|
|
*/
|
|
static UINT rdpsnd_recv_wave_info_pdu(rdpsndPlugin* rdpsnd, wStream* s, UINT16 BodySize)
|
|
{
|
|
UINT16 wFormatNo;
|
|
const AUDIO_FORMAT* format;
|
|
|
|
if (Stream_GetRemainingLength(s) < 12)
|
|
return ERROR_BAD_LENGTH;
|
|
|
|
rdpsnd->wArrivalTime = GetTickCount64();
|
|
Stream_Read_UINT16(s, rdpsnd->wTimeStamp);
|
|
Stream_Read_UINT16(s, wFormatNo);
|
|
|
|
if (wFormatNo >= rdpsnd->NumberOfClientFormats)
|
|
return ERROR_INVALID_DATA;
|
|
|
|
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];
|
|
WLog_Print(rdpsnd->log, WLOG_DEBUG,
|
|
"%s WaveInfo: cBlockNo: %" PRIu8 " wFormatNo: %" PRIu16 " [%s]",
|
|
rdpsnd_is_dyn_str(rdpsnd->dynamic), rdpsnd->cBlockNo, wFormatNo,
|
|
audio_format_get_tag_string(format->wFormatTag));
|
|
|
|
if (!rdpsnd_ensure_device_is_open(rdpsnd, wFormatNo, format))
|
|
return ERROR_INTERNAL_ERROR;
|
|
|
|
rdpsnd->expectingWave = TRUE;
|
|
return CHANNEL_RC_OK;
|
|
}
|
|
|
|
/**
|
|
* Function description
|
|
*
|
|
* @return 0 on success, otherwise a Win32 error code
|
|
*/
|
|
static UINT rdpsnd_send_wave_confirm_pdu(rdpsndPlugin* rdpsnd, UINT16 wTimeStamp,
|
|
BYTE cConfirmedBlockNo)
|
|
{
|
|
wStream* pdu;
|
|
pdu = Stream_New(NULL, 8);
|
|
|
|
if (!pdu)
|
|
{
|
|
WLog_ERR(TAG, "%s Stream_New failed!", rdpsnd_is_dyn_str(rdpsnd->dynamic));
|
|
return CHANNEL_RC_NO_MEMORY;
|
|
}
|
|
|
|
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 */
|
|
return rdpsnd_virtual_channel_write(rdpsnd, pdu);
|
|
}
|
|
|
|
static BOOL rdpsnd_detect_overrun(rdpsndPlugin* rdpsnd, const AUDIO_FORMAT* format, size_t size)
|
|
{
|
|
UINT32 bpf;
|
|
UINT32 now;
|
|
UINT32 duration;
|
|
UINT32 totalDuration;
|
|
UINT32 remainingDuration;
|
|
UINT32 maxDuration;
|
|
|
|
if (!rdpsnd || !format)
|
|
return FALSE;
|
|
|
|
/* Older windows RDP servers do not limit the send buffer, which can
|
|
* cause quite a large amount of sound data buffered client side.
|
|
* If e.g. sound is paused server side the client will keep playing
|
|
* for a long time instead of pausing playback.
|
|
*
|
|
* To avoid this we check:
|
|
*
|
|
* 1. Is the sound sample received from a known format these servers
|
|
* support
|
|
* 2. If it is calculate the size of the client side sound buffer
|
|
* 3. If the buffer is too large silently drop the sample which will
|
|
* trigger a retransmit later on.
|
|
*
|
|
* This check must only be applied to these known formats, because
|
|
* with newer and other formats the sample size can not be calculated
|
|
* without decompressing the sample first.
|
|
*/
|
|
switch (format->wFormatTag)
|
|
{
|
|
case WAVE_FORMAT_PCM:
|
|
case WAVE_FORMAT_DVI_ADPCM:
|
|
case WAVE_FORMAT_ADPCM:
|
|
case WAVE_FORMAT_ALAW:
|
|
case WAVE_FORMAT_MULAW:
|
|
break;
|
|
case WAVE_FORMAT_MSG723:
|
|
case WAVE_FORMAT_GSM610:
|
|
case WAVE_FORMAT_AAC_MS:
|
|
default:
|
|
return FALSE;
|
|
}
|
|
|
|
audio_format_print(WLog_Get(TAG), WLOG_DEBUG, format);
|
|
bpf = format->nChannels * format->wBitsPerSample * format->nSamplesPerSec / 8;
|
|
if (bpf == 0)
|
|
return FALSE;
|
|
|
|
duration = (UINT32)(1000 * size / bpf);
|
|
totalDuration = (UINT32)(1000 * rdpsnd->totalPlaySize / bpf);
|
|
now = GetTickCountPrecise();
|
|
if (rdpsnd->startPlayTime == 0)
|
|
{
|
|
rdpsnd->startPlayTime = now;
|
|
rdpsnd->totalPlaySize = size;
|
|
return FALSE;
|
|
}
|
|
else if (now - rdpsnd->startPlayTime > totalDuration + 10)
|
|
{
|
|
/* Buffer underrun */
|
|
WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Buffer underrun by %u ms",
|
|
rdpsnd_is_dyn_str(rdpsnd->dynamic),
|
|
(UINT)(now - rdpsnd->startPlayTime - totalDuration));
|
|
rdpsnd->startPlayTime = now;
|
|
rdpsnd->totalPlaySize = size;
|
|
return FALSE;
|
|
}
|
|
else
|
|
{
|
|
/* Calculate remaining duration to be played */
|
|
remainingDuration = totalDuration - (now - rdpsnd->startPlayTime);
|
|
|
|
/* Maximum allow duration calculation */
|
|
maxDuration = duration * 2 + rdpsnd->latency;
|
|
|
|
if (remainingDuration + duration > maxDuration)
|
|
{
|
|
WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Buffer overrun pending %u ms dropping %u ms",
|
|
rdpsnd_is_dyn_str(rdpsnd->dynamic), remainingDuration, duration);
|
|
return TRUE;
|
|
}
|
|
|
|
rdpsnd->totalPlaySize += size;
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
static UINT rdpsnd_treat_wave(rdpsndPlugin* rdpsnd, wStream* s, size_t size)
|
|
{
|
|
BYTE* data;
|
|
AUDIO_FORMAT* format;
|
|
UINT64 end;
|
|
UINT64 diffMS, ts;
|
|
UINT latency = 0;
|
|
UINT error;
|
|
|
|
if (Stream_GetRemainingLength(s) < size)
|
|
return ERROR_BAD_LENGTH;
|
|
|
|
if (rdpsnd->wCurrentFormatNo >= rdpsnd->NumberOfClientFormats)
|
|
return ERROR_INTERNAL_ERROR;
|
|
|
|
/*
|
|
* Send the first WaveConfirm PDU. The server side uses this to determine the
|
|
* network latency.
|
|
* See also [MS-RDPEA] 2.2.3.8 Wave Confirm PDU
|
|
*/
|
|
error = rdpsnd_send_wave_confirm_pdu(rdpsnd, rdpsnd->wTimeStamp, rdpsnd->cBlockNo);
|
|
if (error)
|
|
return error;
|
|
|
|
data = Stream_Pointer(s);
|
|
format = &rdpsnd->ClientFormats[rdpsnd->wCurrentFormatNo];
|
|
WLog_Print(rdpsnd->log, WLOG_DEBUG,
|
|
"%s Wave: cBlockNo: %" PRIu8 " wTimeStamp: %" PRIu16 ", size: %" PRIdz,
|
|
rdpsnd_is_dyn_str(rdpsnd->dynamic), rdpsnd->cBlockNo, rdpsnd->wTimeStamp, size);
|
|
|
|
if (rdpsnd->device && rdpsnd->attached && !rdpsnd_detect_overrun(rdpsnd, format, size))
|
|
{
|
|
UINT status = CHANNEL_RC_OK;
|
|
wStream* pcmData = StreamPool_Take(rdpsnd->pool, 4096);
|
|
|
|
if (rdpsnd->device->FormatSupported(rdpsnd->device, format))
|
|
latency = IFCALLRESULT(0, rdpsnd->device->Play, rdpsnd->device, data, size);
|
|
else if (freerdp_dsp_decode(rdpsnd->dsp_context, format, data, size, pcmData))
|
|
{
|
|
Stream_SealLength(pcmData);
|
|
latency = IFCALLRESULT(0, rdpsnd->device->Play, rdpsnd->device, Stream_Buffer(pcmData),
|
|
Stream_Length(pcmData));
|
|
}
|
|
else
|
|
status = ERROR_INTERNAL_ERROR;
|
|
|
|
Stream_Release(pcmData);
|
|
|
|
if (status != CHANNEL_RC_OK)
|
|
return status;
|
|
}
|
|
|
|
end = GetTickCount64();
|
|
diffMS = end - rdpsnd->wArrivalTime + latency;
|
|
ts = (rdpsnd->wTimeStamp + diffMS) % UINT16_MAX;
|
|
|
|
/*
|
|
* Send the second WaveConfirm PDU. With the first WaveConfirm PDU,
|
|
* the server side uses this second WaveConfirm PDU to determine the actual
|
|
* render latency.
|
|
*/
|
|
return rdpsnd_send_wave_confirm_pdu(rdpsnd, (UINT16)ts, rdpsnd->cBlockNo);
|
|
}
|
|
|
|
/**
|
|
* Function description
|
|
*
|
|
* @return 0 on success, otherwise a Win32 error code
|
|
*/
|
|
static UINT rdpsnd_recv_wave_pdu(rdpsndPlugin* rdpsnd, wStream* s)
|
|
{
|
|
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.
|
|
*/
|
|
if (Stream_GetRemainingLength(s) < 4)
|
|
return ERROR_INVALID_DATA;
|
|
|
|
CopyMemory(Stream_Buffer(s), rdpsnd->waveData, 4);
|
|
return rdpsnd_treat_wave(rdpsnd, s, rdpsnd->waveDataSize);
|
|
}
|
|
|
|
static UINT rdpsnd_recv_wave2_pdu(rdpsndPlugin* rdpsnd, wStream* s, UINT16 BodySize)
|
|
{
|
|
UINT16 wFormatNo;
|
|
AUDIO_FORMAT* format;
|
|
UINT32 dwAudioTimeStamp;
|
|
|
|
if (Stream_GetRemainingLength(s) < 12)
|
|
return ERROR_BAD_LENGTH;
|
|
|
|
Stream_Read_UINT16(s, rdpsnd->wTimeStamp);
|
|
Stream_Read_UINT16(s, wFormatNo);
|
|
Stream_Read_UINT8(s, rdpsnd->cBlockNo);
|
|
Stream_Seek(s, 3); /* bPad */
|
|
Stream_Read_UINT32(s, dwAudioTimeStamp);
|
|
if (wFormatNo >= rdpsnd->NumberOfClientFormats)
|
|
return ERROR_INVALID_DATA;
|
|
format = &rdpsnd->ClientFormats[wFormatNo];
|
|
rdpsnd->waveDataSize = BodySize - 12;
|
|
rdpsnd->wArrivalTime = GetTickCount64();
|
|
WLog_Print(rdpsnd->log, WLOG_DEBUG,
|
|
"%s Wave2PDU: cBlockNo: %" PRIu8 " wFormatNo: %" PRIu16 " [%s] , align=%hu",
|
|
rdpsnd_is_dyn_str(rdpsnd->dynamic), rdpsnd->cBlockNo, wFormatNo,
|
|
audio_format_get_tag_string(format->wFormatTag), format->nBlockAlign);
|
|
|
|
if (!rdpsnd_ensure_device_is_open(rdpsnd, wFormatNo, format))
|
|
return ERROR_INTERNAL_ERROR;
|
|
|
|
return rdpsnd_treat_wave(rdpsnd, s, rdpsnd->waveDataSize);
|
|
}
|
|
|
|
static void rdpsnd_recv_close_pdu(rdpsndPlugin* rdpsnd)
|
|
{
|
|
if (rdpsnd->isOpen)
|
|
{
|
|
WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Closing device",
|
|
rdpsnd_is_dyn_str(rdpsnd->dynamic));
|
|
}
|
|
else
|
|
WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Device already closed",
|
|
rdpsnd_is_dyn_str(rdpsnd->dynamic));
|
|
}
|
|
|
|
/**
|
|
* Function description
|
|
*
|
|
* @return 0 on success, otherwise a Win32 error code
|
|
*/
|
|
static UINT rdpsnd_recv_volume_pdu(rdpsndPlugin* rdpsnd, wStream* s)
|
|
{
|
|
BOOL rc = FALSE;
|
|
UINT32 dwVolume;
|
|
|
|
if (Stream_GetRemainingLength(s) < 4)
|
|
return ERROR_BAD_LENGTH;
|
|
|
|
Stream_Read_UINT32(s, dwVolume);
|
|
WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Volume: 0x%08" PRIX32 "",
|
|
rdpsnd_is_dyn_str(rdpsnd->dynamic), dwVolume);
|
|
if (rdpsnd->device)
|
|
rc = IFCALLRESULT(FALSE, rdpsnd->device->SetVolume, rdpsnd->device, dwVolume);
|
|
|
|
if (!rc)
|
|
{
|
|
WLog_ERR(TAG, "%s error setting volume", rdpsnd_is_dyn_str(rdpsnd->dynamic));
|
|
return CHANNEL_RC_INITIALIZATION_ERROR;
|
|
}
|
|
|
|
return CHANNEL_RC_OK;
|
|
}
|
|
|
|
/**
|
|
* Function description
|
|
*
|
|
* @return 0 on success, otherwise a Win32 error code
|
|
*/
|
|
static UINT rdpsnd_recv_pdu(rdpsndPlugin* rdpsnd, wStream* s)
|
|
{
|
|
BYTE msgType;
|
|
UINT16 BodySize;
|
|
UINT status = CHANNEL_RC_OK;
|
|
|
|
if (rdpsnd->expectingWave)
|
|
{
|
|
status = rdpsnd_recv_wave_pdu(rdpsnd, s);
|
|
goto out;
|
|
}
|
|
|
|
if (Stream_GetRemainingLength(s) < 4)
|
|
{
|
|
status = ERROR_BAD_LENGTH;
|
|
goto out;
|
|
}
|
|
|
|
Stream_Read_UINT8(s, msgType); /* msgType */
|
|
Stream_Seek_UINT8(s); /* bPad */
|
|
Stream_Read_UINT16(s, BodySize);
|
|
|
|
switch (msgType)
|
|
{
|
|
case SNDC_FORMATS:
|
|
status = rdpsnd_recv_server_audio_formats_pdu(rdpsnd, s);
|
|
break;
|
|
|
|
case SNDC_TRAINING:
|
|
status = rdpsnd_recv_training_pdu(rdpsnd, s);
|
|
break;
|
|
|
|
case SNDC_WAVE:
|
|
status = rdpsnd_recv_wave_info_pdu(rdpsnd, s, BodySize);
|
|
break;
|
|
|
|
case SNDC_CLOSE:
|
|
rdpsnd_recv_close_pdu(rdpsnd);
|
|
break;
|
|
|
|
case SNDC_SETVOLUME:
|
|
status = rdpsnd_recv_volume_pdu(rdpsnd, s);
|
|
break;
|
|
|
|
case SNDC_WAVE2:
|
|
status = rdpsnd_recv_wave2_pdu(rdpsnd, s, BodySize);
|
|
break;
|
|
|
|
default:
|
|
WLog_ERR(TAG, "%s unknown msgType %" PRIu8 "", rdpsnd_is_dyn_str(rdpsnd->dynamic),
|
|
msgType);
|
|
break;
|
|
}
|
|
|
|
out:
|
|
Stream_Release(s);
|
|
return status;
|
|
}
|
|
|
|
static void rdpsnd_register_device_plugin(rdpsndPlugin* rdpsnd, rdpsndDevicePlugin* device)
|
|
{
|
|
if (rdpsnd->device)
|
|
{
|
|
WLog_ERR(TAG, "%s existing device, abort.", rdpsnd_is_dyn_str(FALSE));
|
|
return;
|
|
}
|
|
|
|
rdpsnd->device = device;
|
|
device->rdpsnd = rdpsnd;
|
|
}
|
|
|
|
/**
|
|
* Function description
|
|
*
|
|
* @return 0 on success, otherwise a Win32 error code
|
|
*/
|
|
static UINT rdpsnd_load_device_plugin(rdpsndPlugin* rdpsnd, const char* name, ADDIN_ARGV* args)
|
|
{
|
|
PFREERDP_RDPSND_DEVICE_ENTRY entry;
|
|
FREERDP_RDPSND_DEVICE_ENTRY_POINTS entryPoints;
|
|
UINT error;
|
|
DWORD flags = FREERDP_ADDIN_CHANNEL_STATIC | FREERDP_ADDIN_CHANNEL_ENTRYEX;
|
|
if (rdpsnd->dynamic)
|
|
flags = FREERDP_ADDIN_CHANNEL_DYNAMIC;
|
|
entry =
|
|
(PFREERDP_RDPSND_DEVICE_ENTRY)freerdp_load_channel_addin_entry("rdpsnd", name, NULL, flags);
|
|
|
|
if (!entry)
|
|
return ERROR_INTERNAL_ERROR;
|
|
|
|
entryPoints.rdpsnd = rdpsnd;
|
|
entryPoints.pRegisterRdpsndDevice = rdpsnd_register_device_plugin;
|
|
entryPoints.args = args;
|
|
|
|
if ((error = entry(&entryPoints)))
|
|
WLog_ERR(TAG, "%s %s entry returns error %" PRIu32 "", rdpsnd_is_dyn_str(rdpsnd->dynamic),
|
|
name, error);
|
|
|
|
WLog_INFO(TAG, "%s Loaded %s backend for rdpsnd", rdpsnd_is_dyn_str(rdpsnd->dynamic), name);
|
|
return error;
|
|
}
|
|
|
|
static BOOL rdpsnd_set_subsystem(rdpsndPlugin* rdpsnd, const char* subsystem)
|
|
{
|
|
free(rdpsnd->subsystem);
|
|
rdpsnd->subsystem = _strdup(subsystem);
|
|
return (rdpsnd->subsystem != NULL);
|
|
}
|
|
|
|
static BOOL rdpsnd_set_device_name(rdpsndPlugin* rdpsnd, const char* device_name)
|
|
{
|
|
free(rdpsnd->device_name);
|
|
rdpsnd->device_name = _strdup(device_name);
|
|
return (rdpsnd->device_name != NULL);
|
|
}
|
|
|
|
/**
|
|
* Function description
|
|
*
|
|
* @return 0 on success, otherwise a Win32 error code
|
|
*/
|
|
static UINT rdpsnd_process_addin_args(rdpsndPlugin* rdpsnd, ADDIN_ARGV* args)
|
|
{
|
|
int status;
|
|
DWORD flags;
|
|
COMMAND_LINE_ARGUMENT_A* arg;
|
|
COMMAND_LINE_ARGUMENT_A rdpsnd_args[] = {
|
|
{ "sys", COMMAND_LINE_VALUE_REQUIRED, "<subsystem>", NULL, NULL, -1, NULL, "subsystem" },
|
|
{ "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", NULL, NULL, -1, NULL, "device" },
|
|
{ "format", COMMAND_LINE_VALUE_REQUIRED, "<format>", NULL, NULL, -1, NULL, "format" },
|
|
{ "rate", COMMAND_LINE_VALUE_REQUIRED, "<rate>", NULL, NULL, -1, NULL, "rate" },
|
|
{ "channel", COMMAND_LINE_VALUE_REQUIRED, "<channel>", NULL, NULL, -1, NULL, "channel" },
|
|
{ "latency", COMMAND_LINE_VALUE_REQUIRED, "<latency>", NULL, NULL, -1, NULL, "latency" },
|
|
{ "quality", COMMAND_LINE_VALUE_REQUIRED, "<quality mode>", NULL, NULL, -1, NULL,
|
|
"quality mode" },
|
|
{ NULL, 0, NULL, NULL, NULL, -1, NULL, NULL }
|
|
};
|
|
rdpsnd->wQualityMode = HIGH_QUALITY; /* default quality mode */
|
|
|
|
if (args->argc > 1)
|
|
{
|
|
flags = COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON;
|
|
status = CommandLineParseArgumentsA(args->argc, args->argv, rdpsnd_args, flags, rdpsnd,
|
|
NULL, NULL);
|
|
|
|
if (status < 0)
|
|
return CHANNEL_RC_INITIALIZATION_ERROR;
|
|
|
|
arg = rdpsnd_args;
|
|
errno = 0;
|
|
|
|
do
|
|
{
|
|
if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT))
|
|
continue;
|
|
|
|
CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "sys")
|
|
{
|
|
if (!rdpsnd_set_subsystem(rdpsnd, arg->Value))
|
|
return CHANNEL_RC_NO_MEMORY;
|
|
}
|
|
CommandLineSwitchCase(arg, "dev")
|
|
{
|
|
if (!rdpsnd_set_device_name(rdpsnd, arg->Value))
|
|
return CHANNEL_RC_NO_MEMORY;
|
|
}
|
|
CommandLineSwitchCase(arg, "format")
|
|
{
|
|
unsigned long val = strtoul(arg->Value, NULL, 0);
|
|
|
|
if ((errno != 0) || (val > UINT16_MAX))
|
|
return CHANNEL_RC_INITIALIZATION_ERROR;
|
|
|
|
rdpsnd->fixed_format->wFormatTag = (UINT16)val;
|
|
}
|
|
CommandLineSwitchCase(arg, "rate")
|
|
{
|
|
unsigned long val = strtoul(arg->Value, NULL, 0);
|
|
|
|
if ((errno != 0) || (val > UINT32_MAX))
|
|
return CHANNEL_RC_INITIALIZATION_ERROR;
|
|
|
|
rdpsnd->fixed_format->nSamplesPerSec = val;
|
|
}
|
|
CommandLineSwitchCase(arg, "channel")
|
|
{
|
|
unsigned long val = strtoul(arg->Value, NULL, 0);
|
|
|
|
if ((errno != 0) || (val > UINT16_MAX))
|
|
return CHANNEL_RC_INITIALIZATION_ERROR;
|
|
|
|
rdpsnd->fixed_format->nChannels = (UINT16)val;
|
|
}
|
|
CommandLineSwitchCase(arg, "latency")
|
|
{
|
|
unsigned long val = strtoul(arg->Value, NULL, 0);
|
|
|
|
if ((errno != 0) || (val > INT32_MAX))
|
|
return CHANNEL_RC_INITIALIZATION_ERROR;
|
|
|
|
rdpsnd->latency = val;
|
|
}
|
|
CommandLineSwitchCase(arg, "quality")
|
|
{
|
|
long wQualityMode = DYNAMIC_QUALITY;
|
|
|
|
if (_stricmp(arg->Value, "dynamic") == 0)
|
|
wQualityMode = DYNAMIC_QUALITY;
|
|
else if (_stricmp(arg->Value, "medium") == 0)
|
|
wQualityMode = MEDIUM_QUALITY;
|
|
else if (_stricmp(arg->Value, "high") == 0)
|
|
wQualityMode = HIGH_QUALITY;
|
|
else
|
|
{
|
|
wQualityMode = strtol(arg->Value, NULL, 0);
|
|
|
|
if (errno != 0)
|
|
return CHANNEL_RC_INITIALIZATION_ERROR;
|
|
}
|
|
|
|
if ((wQualityMode < 0) || (wQualityMode > 2))
|
|
wQualityMode = DYNAMIC_QUALITY;
|
|
|
|
rdpsnd->wQualityMode = (UINT16)wQualityMode;
|
|
}
|
|
CommandLineSwitchDefault(arg)
|
|
{
|
|
}
|
|
CommandLineSwitchEnd(arg)
|
|
} while ((arg = CommandLineFindNextArgumentA(arg)) != NULL);
|
|
}
|
|
|
|
return CHANNEL_RC_OK;
|
|
}
|
|
|
|
/**
|
|
* Function description
|
|
*
|
|
* @return 0 on success, otherwise a Win32 error code
|
|
*/
|
|
static UINT rdpsnd_process_connect(rdpsndPlugin* rdpsnd)
|
|
{
|
|
const struct
|
|
{
|
|
const char* subsystem;
|
|
const char* device;
|
|
} backends[] = {
|
|
#if defined(WITH_IOSAUDIO)
|
|
{ "ios", "" },
|
|
#endif
|
|
#if defined(WITH_OPENSLES)
|
|
{ "opensles", "" },
|
|
#endif
|
|
#if defined(WITH_PULSE)
|
|
{ "pulse", "" },
|
|
#endif
|
|
#if defined(WITH_ALSA)
|
|
{ "alsa", "default" },
|
|
#endif
|
|
#if defined(WITH_OSS)
|
|
{ "oss", "" },
|
|
#endif
|
|
#if defined(WITH_MACAUDIO)
|
|
{ "mac", "default" },
|
|
#endif
|
|
#if defined(WITH_WINMM)
|
|
{ "winmm", "" },
|
|
#endif
|
|
{ "fake", "" }
|
|
};
|
|
ADDIN_ARGV* args;
|
|
UINT status = ERROR_INTERNAL_ERROR;
|
|
rdpsnd->latency = 0;
|
|
args = (ADDIN_ARGV*)rdpsnd->channelEntryPoints.pExtendedData;
|
|
|
|
if (args)
|
|
{
|
|
status = rdpsnd_process_addin_args(rdpsnd, args);
|
|
|
|
if (status != CHANNEL_RC_OK)
|
|
return status;
|
|
}
|
|
|
|
if (rdpsnd->subsystem)
|
|
{
|
|
if ((status = rdpsnd_load_device_plugin(rdpsnd, rdpsnd->subsystem, args)))
|
|
{
|
|
WLog_ERR(TAG,
|
|
"%s Unable to load sound playback subsystem %s because of error %" PRIu32 "",
|
|
rdpsnd_is_dyn_str(rdpsnd->dynamic), rdpsnd->subsystem, status);
|
|
return status;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
size_t x;
|
|
|
|
for (x = 0; x < ARRAYSIZE(backends); x++)
|
|
{
|
|
const char* subsystem_name = backends[x].subsystem;
|
|
const char* device_name = backends[x].device;
|
|
|
|
if ((status = rdpsnd_load_device_plugin(rdpsnd, subsystem_name, args)))
|
|
WLog_ERR(TAG,
|
|
"%s Unable to load sound playback subsystem %s because of error %" PRIu32
|
|
"",
|
|
rdpsnd_is_dyn_str(rdpsnd->dynamic), subsystem_name, status);
|
|
|
|
if (!rdpsnd->device)
|
|
continue;
|
|
|
|
if (!rdpsnd_set_subsystem(rdpsnd, subsystem_name) ||
|
|
!rdpsnd_set_device_name(rdpsnd, device_name))
|
|
return CHANNEL_RC_NO_MEMORY;
|
|
|
|
break;
|
|
}
|
|
|
|
if (!rdpsnd->device || status)
|
|
return CHANNEL_RC_INITIALIZATION_ERROR;
|
|
}
|
|
|
|
return CHANNEL_RC_OK;
|
|
}
|
|
|
|
/**
|
|
* Function description
|
|
*
|
|
* @return 0 on success, otherwise a Win32 error code
|
|
*/
|
|
UINT rdpsnd_virtual_channel_write(rdpsndPlugin* rdpsnd, wStream* s)
|
|
{
|
|
UINT status = CHANNEL_RC_BAD_INIT_HANDLE;
|
|
|
|
if (rdpsnd)
|
|
{
|
|
if (rdpsnd->dynamic)
|
|
{
|
|
IWTSVirtualChannel* channel;
|
|
if (rdpsnd->listener_callback)
|
|
{
|
|
channel = rdpsnd->listener_callback->channel_callback->channel;
|
|
status = channel->Write(channel, (UINT32)Stream_Length(s), Stream_Buffer(s), NULL);
|
|
}
|
|
Stream_Free(s, TRUE);
|
|
}
|
|
else
|
|
{
|
|
status = rdpsnd->channelEntryPoints.pVirtualChannelWriteEx(
|
|
rdpsnd->InitHandle, rdpsnd->OpenHandle, Stream_Buffer(s),
|
|
(UINT32)Stream_GetPosition(s), s);
|
|
|
|
if (status != CHANNEL_RC_OK)
|
|
{
|
|
Stream_Free(s, TRUE);
|
|
WLog_ERR(TAG, "%s pVirtualChannelWriteEx failed with %s [%08" PRIX32 "]",
|
|
rdpsnd_is_dyn_str(FALSE), WTSErrorToString(status), status);
|
|
}
|
|
}
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
/**
|
|
* Function description
|
|
*
|
|
* @return 0 on success, otherwise a Win32 error code
|
|
*/
|
|
static UINT rdpsnd_virtual_channel_event_data_received(rdpsndPlugin* plugin, void* pData,
|
|
UINT32 dataLength, UINT32 totalLength,
|
|
UINT32 dataFlags)
|
|
{
|
|
if ((dataFlags & CHANNEL_FLAG_SUSPEND) || (dataFlags & CHANNEL_FLAG_RESUME))
|
|
return CHANNEL_RC_OK;
|
|
|
|
if (dataFlags & CHANNEL_FLAG_FIRST)
|
|
{
|
|
if (!plugin->data_in)
|
|
plugin->data_in = StreamPool_Take(plugin->pool, totalLength);
|
|
|
|
Stream_SetPosition(plugin->data_in, 0);
|
|
}
|
|
|
|
if (!Stream_EnsureRemainingCapacity(plugin->data_in, dataLength))
|
|
return CHANNEL_RC_NO_MEMORY;
|
|
|
|
Stream_Write(plugin->data_in, pData, dataLength);
|
|
|
|
if (dataFlags & CHANNEL_FLAG_LAST)
|
|
{
|
|
Stream_SealLength(plugin->data_in);
|
|
Stream_SetPosition(plugin->data_in, 0);
|
|
|
|
if (!MessageQueue_Post(plugin->queue, NULL, 0, plugin->data_in, NULL))
|
|
return ERROR_INTERNAL_ERROR;
|
|
|
|
plugin->data_in = NULL;
|
|
}
|
|
|
|
return CHANNEL_RC_OK;
|
|
}
|
|
|
|
static VOID VCAPITYPE rdpsnd_virtual_channel_open_event_ex(LPVOID lpUserParam, DWORD openHandle,
|
|
UINT event, LPVOID pData,
|
|
UINT32 dataLength, UINT32 totalLength,
|
|
UINT32 dataFlags)
|
|
{
|
|
UINT error = CHANNEL_RC_OK;
|
|
rdpsndPlugin* rdpsnd = (rdpsndPlugin*)lpUserParam;
|
|
|
|
switch (event)
|
|
{
|
|
case CHANNEL_EVENT_DATA_RECEIVED:
|
|
if (!rdpsnd)
|
|
return;
|
|
|
|
if (rdpsnd->OpenHandle != openHandle)
|
|
{
|
|
WLog_ERR(TAG, "%s error no match", rdpsnd_is_dyn_str(rdpsnd->dynamic));
|
|
return;
|
|
}
|
|
if ((error = rdpsnd_virtual_channel_event_data_received(rdpsnd, pData, dataLength,
|
|
totalLength, dataFlags)))
|
|
WLog_ERR(TAG,
|
|
"%s rdpsnd_virtual_channel_event_data_received failed with error %" PRIu32
|
|
"",
|
|
rdpsnd_is_dyn_str(rdpsnd->dynamic), error);
|
|
|
|
break;
|
|
|
|
case CHANNEL_EVENT_WRITE_CANCELLED:
|
|
case CHANNEL_EVENT_WRITE_COMPLETE:
|
|
{
|
|
wStream* s = (wStream*)pData;
|
|
Stream_Free(s, TRUE);
|
|
}
|
|
break;
|
|
|
|
case CHANNEL_EVENT_USER:
|
|
break;
|
|
}
|
|
|
|
if (error && rdpsnd && rdpsnd->rdpcontext)
|
|
{
|
|
char buffer[8192];
|
|
_snprintf(buffer, sizeof(buffer),
|
|
"%s rdpsnd_virtual_channel_open_event_ex reported an error",
|
|
rdpsnd_is_dyn_str(rdpsnd->dynamic));
|
|
setChannelError(rdpsnd->rdpcontext, error, buffer);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function description
|
|
*
|
|
* @return 0 on success, otherwise a Win32 error code
|
|
*/
|
|
static UINT rdpsnd_virtual_channel_event_connected(rdpsndPlugin* rdpsnd, LPVOID pData,
|
|
UINT32 dataLength)
|
|
{
|
|
UINT32 status;
|
|
DWORD opened = 0;
|
|
WINPR_UNUSED(pData);
|
|
WINPR_UNUSED(dataLength);
|
|
|
|
status = rdpsnd->channelEntryPoints.pVirtualChannelOpenEx(
|
|
rdpsnd->InitHandle, &opened, rdpsnd->channelDef.name, rdpsnd_virtual_channel_open_event_ex);
|
|
|
|
if (status != CHANNEL_RC_OK)
|
|
{
|
|
WLog_ERR(TAG, "%s pVirtualChannelOpenEx failed with %s [%08" PRIX32 "]",
|
|
rdpsnd_is_dyn_str(rdpsnd->dynamic), WTSErrorToString(status), status);
|
|
goto fail;
|
|
}
|
|
|
|
if (rdpsnd_process_connect(rdpsnd) != CHANNEL_RC_OK)
|
|
goto fail;
|
|
|
|
rdpsnd->OpenHandle = opened;
|
|
return CHANNEL_RC_OK;
|
|
fail:
|
|
if (opened != 0)
|
|
rdpsnd->channelEntryPoints.pVirtualChannelCloseEx(rdpsnd->InitHandle, opened);
|
|
|
|
return CHANNEL_RC_NO_MEMORY;
|
|
}
|
|
|
|
static void cleanup_internals(rdpsndPlugin* rdpsnd)
|
|
{
|
|
if (!rdpsnd)
|
|
return;
|
|
|
|
if (rdpsnd->pool)
|
|
StreamPool_Return(rdpsnd->pool, rdpsnd->data_in);
|
|
|
|
audio_formats_free(rdpsnd->ClientFormats, rdpsnd->NumberOfClientFormats);
|
|
audio_formats_free(rdpsnd->ServerFormats, rdpsnd->NumberOfServerFormats);
|
|
|
|
rdpsnd->NumberOfClientFormats = 0;
|
|
rdpsnd->ClientFormats = NULL;
|
|
rdpsnd->NumberOfServerFormats = 0;
|
|
rdpsnd->ServerFormats = NULL;
|
|
|
|
rdpsnd->data_in = NULL;
|
|
}
|
|
|
|
/**
|
|
* Function description
|
|
*
|
|
* @return 0 on success, otherwise a Win32 error code
|
|
*/
|
|
static UINT rdpsnd_virtual_channel_event_disconnected(rdpsndPlugin* rdpsnd)
|
|
{
|
|
UINT error;
|
|
|
|
if (rdpsnd->OpenHandle != 0)
|
|
{
|
|
DWORD opened = rdpsnd->OpenHandle;
|
|
rdpsnd->OpenHandle = 0;
|
|
|
|
if (rdpsnd->device)
|
|
IFCALL(rdpsnd->device->Close, rdpsnd->device);
|
|
|
|
error = rdpsnd->channelEntryPoints.pVirtualChannelCloseEx(rdpsnd->InitHandle, opened);
|
|
|
|
if (CHANNEL_RC_OK != error)
|
|
{
|
|
WLog_ERR(TAG, "%s pVirtualChannelCloseEx failed with %s [%08" PRIX32 "]",
|
|
rdpsnd_is_dyn_str(rdpsnd->dynamic), WTSErrorToString(error), error);
|
|
return error;
|
|
}
|
|
}
|
|
|
|
cleanup_internals(rdpsnd);
|
|
|
|
if (rdpsnd->device)
|
|
{
|
|
IFCALL(rdpsnd->device->Free, rdpsnd->device);
|
|
rdpsnd->device = NULL;
|
|
}
|
|
|
|
return CHANNEL_RC_OK;
|
|
}
|
|
|
|
static void _queue_free(void* obj)
|
|
{
|
|
wStream* s = obj;
|
|
Stream_Release(s);
|
|
}
|
|
|
|
static void free_internals(rdpsndPlugin* rdpsnd)
|
|
{
|
|
if (!rdpsnd)
|
|
return;
|
|
|
|
freerdp_dsp_context_free(rdpsnd->dsp_context);
|
|
StreamPool_Free(rdpsnd->pool);
|
|
rdpsnd->pool = NULL;
|
|
rdpsnd->dsp_context = NULL;
|
|
}
|
|
|
|
static BOOL allocate_internals(rdpsndPlugin* rdpsnd)
|
|
{
|
|
if (!rdpsnd->pool)
|
|
{
|
|
rdpsnd->pool = StreamPool_New(TRUE, 4096);
|
|
if (!rdpsnd->pool)
|
|
return FALSE;
|
|
}
|
|
|
|
if (!rdpsnd->dsp_context)
|
|
{
|
|
rdpsnd->dsp_context = freerdp_dsp_context_new(FALSE);
|
|
if (!rdpsnd->dsp_context)
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static DWORD WINAPI play_thread(LPVOID arg)
|
|
{
|
|
UINT error = CHANNEL_RC_OK;
|
|
rdpsndPlugin* rdpsnd = arg;
|
|
|
|
if (!rdpsnd || !rdpsnd->queue)
|
|
return ERROR_INVALID_PARAMETER;
|
|
|
|
while (TRUE)
|
|
{
|
|
int rc;
|
|
wMessage message;
|
|
wStream* s;
|
|
HANDLE handle = MessageQueue_Event(rdpsnd->queue);
|
|
WaitForSingleObject(handle, INFINITE);
|
|
|
|
rc = MessageQueue_Peek(rdpsnd->queue, &message, TRUE);
|
|
if (rc < 1)
|
|
continue;
|
|
|
|
if (message.id == WMQ_QUIT)
|
|
break;
|
|
|
|
s = message.wParam;
|
|
error = rdpsnd_recv_pdu(rdpsnd, s);
|
|
|
|
if (error)
|
|
return error;
|
|
}
|
|
|
|
return CHANNEL_RC_OK;
|
|
}
|
|
|
|
static UINT rdpsnd_virtual_channel_event_initialized(rdpsndPlugin* rdpsnd)
|
|
{
|
|
wObject obj = { 0 };
|
|
|
|
if (!rdpsnd)
|
|
return ERROR_INVALID_PARAMETER;
|
|
|
|
obj.fnObjectFree = _queue_free;
|
|
rdpsnd->queue = MessageQueue_New(&obj);
|
|
if (!rdpsnd->queue)
|
|
return CHANNEL_RC_NO_MEMORY;
|
|
|
|
if (!allocate_internals(rdpsnd))
|
|
return CHANNEL_RC_NO_MEMORY;
|
|
|
|
rdpsnd->thread = CreateThread(NULL, 0, play_thread, rdpsnd, 0, NULL);
|
|
if (!rdpsnd->thread)
|
|
return CHANNEL_RC_INITIALIZATION_ERROR;
|
|
|
|
return CHANNEL_RC_OK;
|
|
}
|
|
|
|
void rdpsnd_virtual_channel_event_terminated(rdpsndPlugin* rdpsnd)
|
|
{
|
|
if (rdpsnd)
|
|
{
|
|
if (rdpsnd->queue)
|
|
MessageQueue_PostQuit(rdpsnd->queue, 0);
|
|
if (rdpsnd->thread)
|
|
{
|
|
WaitForSingleObject(rdpsnd->thread, INFINITE);
|
|
CloseHandle(rdpsnd->thread);
|
|
}
|
|
MessageQueue_Free(rdpsnd->queue);
|
|
|
|
free_internals(rdpsnd);
|
|
audio_formats_free(rdpsnd->fixed_format, 1);
|
|
free(rdpsnd->subsystem);
|
|
free(rdpsnd->device_name);
|
|
rdpsnd->InitHandle = 0;
|
|
}
|
|
|
|
free(rdpsnd);
|
|
}
|
|
|
|
static VOID VCAPITYPE rdpsnd_virtual_channel_init_event_ex(LPVOID lpUserParam, LPVOID pInitHandle,
|
|
UINT event, LPVOID pData,
|
|
UINT dataLength)
|
|
{
|
|
UINT error = CHANNEL_RC_OK;
|
|
rdpsndPlugin* plugin = (rdpsndPlugin*)lpUserParam;
|
|
|
|
if (!plugin)
|
|
return;
|
|
|
|
if (plugin->InitHandle != pInitHandle)
|
|
{
|
|
WLog_ERR(TAG, "%s error no match", rdpsnd_is_dyn_str(plugin->dynamic));
|
|
return;
|
|
}
|
|
|
|
switch (event)
|
|
{
|
|
case CHANNEL_EVENT_INITIALIZED:
|
|
error = rdpsnd_virtual_channel_event_initialized(plugin);
|
|
break;
|
|
|
|
case CHANNEL_EVENT_CONNECTED:
|
|
error = rdpsnd_virtual_channel_event_connected(plugin, pData, dataLength);
|
|
break;
|
|
|
|
case CHANNEL_EVENT_DISCONNECTED:
|
|
error = rdpsnd_virtual_channel_event_disconnected(plugin);
|
|
break;
|
|
|
|
case CHANNEL_EVENT_TERMINATED:
|
|
rdpsnd_virtual_channel_event_terminated(plugin);
|
|
plugin = NULL;
|
|
break;
|
|
|
|
case CHANNEL_EVENT_ATTACHED:
|
|
plugin->attached = TRUE;
|
|
break;
|
|
|
|
case CHANNEL_EVENT_DETACHED:
|
|
plugin->attached = FALSE;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (error && plugin && plugin->rdpcontext)
|
|
{
|
|
char buffer[8192];
|
|
_snprintf(buffer, sizeof(buffer), "%s %s reported an error",
|
|
rdpsnd_is_dyn_str(plugin->dynamic), __FUNCTION__);
|
|
setChannelError(plugin->rdpcontext, error, buffer);
|
|
}
|
|
}
|
|
|
|
rdpContext* freerdp_rdpsnd_get_context(rdpsndPlugin* plugin)
|
|
{
|
|
if (!plugin)
|
|
return NULL;
|
|
|
|
return plugin->rdpcontext;
|
|
}
|
|
|
|
static rdpsndPlugin* allocatePlugin(void)
|
|
{
|
|
rdpsndPlugin* rdpsnd = (rdpsndPlugin*)calloc(1, sizeof(rdpsndPlugin));
|
|
if (!rdpsnd)
|
|
goto fail;
|
|
|
|
rdpsnd->fixed_format = audio_format_new();
|
|
if (!rdpsnd->fixed_format)
|
|
goto fail;
|
|
rdpsnd->log = WLog_Get("com.freerdp.channels.rdpsnd.client");
|
|
if (!rdpsnd->log)
|
|
goto fail;
|
|
|
|
rdpsnd->attached = TRUE;
|
|
return rdpsnd;
|
|
|
|
fail:
|
|
if (rdpsnd)
|
|
audio_format_free(rdpsnd->fixed_format);
|
|
return NULL;
|
|
}
|
|
/* rdpsnd is always built-in */
|
|
BOOL VCAPITYPE rdpsnd_VirtualChannelEntryEx(PCHANNEL_ENTRY_POINTS pEntryPoints, PVOID pInitHandle)
|
|
{
|
|
UINT rc;
|
|
rdpsndPlugin* rdpsnd;
|
|
CHANNEL_ENTRY_POINTS_FREERDP_EX* pEntryPointsEx;
|
|
|
|
if (!pEntryPoints)
|
|
return FALSE;
|
|
|
|
rdpsnd = allocatePlugin();
|
|
|
|
if (!rdpsnd)
|
|
return FALSE;
|
|
|
|
rdpsnd->channelDef.options = CHANNEL_OPTION_INITIALIZED | CHANNEL_OPTION_ENCRYPT_RDP;
|
|
sprintf_s(rdpsnd->channelDef.name, ARRAYSIZE(rdpsnd->channelDef.name), "rdpsnd");
|
|
pEntryPointsEx = (CHANNEL_ENTRY_POINTS_FREERDP_EX*)pEntryPoints;
|
|
|
|
if ((pEntryPointsEx->cbSize >= sizeof(CHANNEL_ENTRY_POINTS_FREERDP_EX)) &&
|
|
(pEntryPointsEx->MagicNumber == FREERDP_CHANNEL_MAGIC_NUMBER))
|
|
{
|
|
rdpsnd->rdpcontext = pEntryPointsEx->context;
|
|
}
|
|
|
|
CopyMemory(&(rdpsnd->channelEntryPoints), pEntryPoints,
|
|
sizeof(CHANNEL_ENTRY_POINTS_FREERDP_EX));
|
|
rdpsnd->InitHandle = pInitHandle;
|
|
rc = rdpsnd->channelEntryPoints.pVirtualChannelInitEx(
|
|
rdpsnd, NULL, pInitHandle, &rdpsnd->channelDef, 1, VIRTUAL_CHANNEL_VERSION_WIN2000,
|
|
rdpsnd_virtual_channel_init_event_ex);
|
|
|
|
if (CHANNEL_RC_OK != rc)
|
|
{
|
|
WLog_ERR(TAG, "%s pVirtualChannelInitEx failed with %s [%08" PRIX32 "]",
|
|
rdpsnd_is_dyn_str(FALSE), WTSErrorToString(rc), rc);
|
|
rdpsnd_virtual_channel_event_terminated(rdpsnd);
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static UINT rdpsnd_on_open(IWTSVirtualChannelCallback* pChannelCallback)
|
|
{
|
|
RDPSND_CHANNEL_CALLBACK* callback = (RDPSND_CHANNEL_CALLBACK*)pChannelCallback;
|
|
rdpsndPlugin* rdpsnd = (rdpsndPlugin*)callback->plugin;
|
|
|
|
if (!allocate_internals(rdpsnd))
|
|
return ERROR_OUTOFMEMORY;
|
|
|
|
return rdpsnd_process_connect(rdpsnd);
|
|
}
|
|
|
|
static UINT rdpsnd_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* data)
|
|
{
|
|
RDPSND_CHANNEL_CALLBACK* callback = (RDPSND_CHANNEL_CALLBACK*)pChannelCallback;
|
|
rdpsndPlugin* plugin;
|
|
wStream* copy;
|
|
size_t len;
|
|
|
|
len = Stream_GetRemainingLength(data);
|
|
|
|
if (!callback || !callback->plugin)
|
|
return ERROR_INVALID_PARAMETER;
|
|
plugin = (rdpsndPlugin*)callback->plugin;
|
|
|
|
copy = StreamPool_Take(plugin->pool, len);
|
|
if (!copy)
|
|
return ERROR_OUTOFMEMORY;
|
|
Stream_Copy(data, copy, len);
|
|
Stream_SealLength(copy);
|
|
Stream_SetPosition(copy, 0);
|
|
|
|
if (!MessageQueue_Post(plugin->queue, NULL, 0, copy, NULL))
|
|
{
|
|
Stream_Release(copy);
|
|
return ERROR_INTERNAL_ERROR;
|
|
}
|
|
|
|
return CHANNEL_RC_OK;
|
|
}
|
|
|
|
static UINT rdpsnd_on_close(IWTSVirtualChannelCallback* pChannelCallback)
|
|
{
|
|
RDPSND_CHANNEL_CALLBACK* callback = (RDPSND_CHANNEL_CALLBACK*)pChannelCallback;
|
|
rdpsndPlugin* rdpsnd = (rdpsndPlugin*)callback->plugin;
|
|
|
|
if (rdpsnd->device)
|
|
IFCALL(rdpsnd->device->Close, rdpsnd->device);
|
|
|
|
cleanup_internals(rdpsnd);
|
|
|
|
if (rdpsnd->device)
|
|
{
|
|
IFCALL(rdpsnd->device->Free, rdpsnd->device);
|
|
rdpsnd->device = NULL;
|
|
}
|
|
|
|
free_internals(rdpsnd);
|
|
free(pChannelCallback);
|
|
return CHANNEL_RC_OK;
|
|
}
|
|
|
|
static UINT rdpsnd_on_new_channel_connection(IWTSListenerCallback* pListenerCallback,
|
|
IWTSVirtualChannel* pChannel, BYTE* Data,
|
|
BOOL* pbAccept,
|
|
IWTSVirtualChannelCallback** ppCallback)
|
|
{
|
|
RDPSND_CHANNEL_CALLBACK* callback;
|
|
RDPSND_LISTENER_CALLBACK* listener_callback = (RDPSND_LISTENER_CALLBACK*)pListenerCallback;
|
|
callback = (RDPSND_CHANNEL_CALLBACK*)calloc(1, sizeof(RDPSND_CHANNEL_CALLBACK));
|
|
|
|
WINPR_UNUSED(Data);
|
|
WINPR_UNUSED(pbAccept);
|
|
|
|
if (!callback)
|
|
{
|
|
WLog_ERR(TAG, "%s calloc failed!", rdpsnd_is_dyn_str(TRUE));
|
|
return CHANNEL_RC_NO_MEMORY;
|
|
}
|
|
|
|
callback->iface.OnOpen = rdpsnd_on_open;
|
|
callback->iface.OnDataReceived = rdpsnd_on_data_received;
|
|
callback->iface.OnClose = rdpsnd_on_close;
|
|
callback->plugin = listener_callback->plugin;
|
|
callback->channel_mgr = listener_callback->channel_mgr;
|
|
callback->channel = pChannel;
|
|
listener_callback->channel_callback = callback;
|
|
*ppCallback = (IWTSVirtualChannelCallback*)callback;
|
|
return CHANNEL_RC_OK;
|
|
}
|
|
|
|
static UINT rdpsnd_plugin_initialize(IWTSPlugin* pPlugin, IWTSVirtualChannelManager* pChannelMgr)
|
|
{
|
|
UINT status;
|
|
rdpsndPlugin* rdpsnd = (rdpsndPlugin*)pPlugin;
|
|
if (rdpsnd->initialized)
|
|
{
|
|
WLog_ERR(TAG, "[%s] channel initialized twice, aborting", RDPSND_DVC_CHANNEL_NAME);
|
|
return ERROR_INVALID_DATA;
|
|
}
|
|
rdpsnd->listener_callback =
|
|
(RDPSND_LISTENER_CALLBACK*)calloc(1, sizeof(RDPSND_LISTENER_CALLBACK));
|
|
|
|
if (!rdpsnd->listener_callback)
|
|
{
|
|
WLog_ERR(TAG, "%s calloc failed!", rdpsnd_is_dyn_str(TRUE));
|
|
return CHANNEL_RC_NO_MEMORY;
|
|
}
|
|
|
|
rdpsnd->listener_callback->iface.OnNewChannelConnection = rdpsnd_on_new_channel_connection;
|
|
rdpsnd->listener_callback->plugin = pPlugin;
|
|
rdpsnd->listener_callback->channel_mgr = pChannelMgr;
|
|
status = pChannelMgr->CreateListener(pChannelMgr, RDPSND_DVC_CHANNEL_NAME, 0,
|
|
&rdpsnd->listener_callback->iface, &(rdpsnd->listener));
|
|
rdpsnd->listener->pInterface = rdpsnd->iface.pInterface;
|
|
status = rdpsnd_virtual_channel_event_initialized(rdpsnd);
|
|
|
|
rdpsnd->initialized = status == CHANNEL_RC_OK;
|
|
return status;
|
|
}
|
|
|
|
/**
|
|
* Function description
|
|
*
|
|
* @return 0 on success, otherwise a Win32 error code
|
|
*/
|
|
static UINT rdpsnd_plugin_terminated(IWTSPlugin* pPlugin)
|
|
{
|
|
rdpsndPlugin* rdpsnd = (rdpsndPlugin*)pPlugin;
|
|
if (rdpsnd)
|
|
{
|
|
if (rdpsnd->listener_callback)
|
|
{
|
|
IWTSVirtualChannelManager* mgr = rdpsnd->listener_callback->channel_mgr;
|
|
if (mgr)
|
|
IFCALL(mgr->DestroyListener, mgr, rdpsnd->listener);
|
|
}
|
|
free(rdpsnd->listener_callback);
|
|
free(rdpsnd->iface.pInterface);
|
|
}
|
|
rdpsnd_virtual_channel_event_terminated(rdpsnd);
|
|
return CHANNEL_RC_OK;
|
|
}
|
|
|
|
/**
|
|
* Function description
|
|
*
|
|
* @return 0 on success, otherwise a Win32 error code
|
|
*/
|
|
UINT rdpsnd_DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints)
|
|
{
|
|
UINT error = CHANNEL_RC_OK;
|
|
rdpsndPlugin* rdpsnd = (rdpsndPlugin*)pEntryPoints->GetPlugin(pEntryPoints, "rdpsnd");
|
|
|
|
if (!rdpsnd)
|
|
{
|
|
rdpsnd = allocatePlugin();
|
|
if (!rdpsnd)
|
|
{
|
|
WLog_ERR(TAG, "%s calloc failed!", rdpsnd_is_dyn_str(TRUE));
|
|
return CHANNEL_RC_NO_MEMORY;
|
|
}
|
|
|
|
rdpsnd->iface.Initialize = rdpsnd_plugin_initialize;
|
|
rdpsnd->iface.Connected = NULL;
|
|
rdpsnd->iface.Disconnected = NULL;
|
|
rdpsnd->iface.Terminated = rdpsnd_plugin_terminated;
|
|
rdpsnd->dynamic = TRUE;
|
|
|
|
/* user data pointer is not const, cast to avoid warning. */
|
|
rdpsnd->channelEntryPoints.pExtendedData = (void*)pEntryPoints->GetPluginData(pEntryPoints);
|
|
|
|
error = pEntryPoints->RegisterPlugin(pEntryPoints, "rdpsnd", &rdpsnd->iface);
|
|
}
|
|
else
|
|
{
|
|
WLog_ERR(TAG, "%s could not get rdpsnd Plugin.", rdpsnd_is_dyn_str(TRUE));
|
|
return CHANNEL_RC_BAD_CHANNEL;
|
|
}
|
|
|
|
return error;
|
|
}
|