You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
640 lines
18 KiB
640 lines
18 KiB
/*****************************************************************************
|
|
* winstore.c : Windows Multimedia Device API audio output plugin for VLC
|
|
*****************************************************************************
|
|
* Copyright (C) 2012 Rémi Denis-Courmont
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation; either version 2.1 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with this program; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
|
*****************************************************************************/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
# include <config.h>
|
|
#endif
|
|
|
|
#define INITGUID
|
|
#define COBJMACROS
|
|
#define CONST_VTABLE
|
|
|
|
#include <audiopolicy.h>
|
|
|
|
#include <vlc_common.h>
|
|
#include <vlc_plugin.h>
|
|
#include <vlc_aout.h>
|
|
#include <vlc_charset.h> // ToWide
|
|
#include <vlc_modules.h>
|
|
#include "audio_output/mmdevice.h"
|
|
|
|
#include <audioclient.h>
|
|
#include <mmdeviceapi.h>
|
|
|
|
DEFINE_GUID (GUID_VLC_AUD_OUT, 0x4533f59d, 0x59ee, 0x00c6,
|
|
0xad, 0xb2, 0xc6, 0x8b, 0x50, 0x1a, 0x66, 0x55);
|
|
|
|
static void EnterMTA(void)
|
|
{
|
|
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
|
|
if (unlikely(FAILED(hr)))
|
|
abort();
|
|
}
|
|
|
|
static void LeaveMTA(void)
|
|
{
|
|
CoUninitialize();
|
|
}
|
|
|
|
typedef struct
|
|
{
|
|
aout_stream_t *stream; /**< Underlying audio output stream */
|
|
module_t *module;
|
|
IAudioClient *client;
|
|
wchar_t* acquired_device;
|
|
wchar_t* requested_device;
|
|
wchar_t* default_device; // read once on open
|
|
|
|
float gain;
|
|
|
|
// IActivateAudioInterfaceCompletionHandler interface
|
|
IActivateAudioInterfaceCompletionHandler client_locator;
|
|
vlc_sem_t async_completed;
|
|
LONG refs;
|
|
CRITICAL_SECTION lock;
|
|
} aout_sys_t;
|
|
|
|
|
|
/* MMDeviceLocator IUnknown methods */
|
|
static STDMETHODIMP_(ULONG) MMDeviceLocator_AddRef(IActivateAudioInterfaceCompletionHandler *This)
|
|
{
|
|
aout_sys_t *sys = container_of(This, aout_sys_t, client_locator);
|
|
return InterlockedIncrement(&sys->refs);
|
|
}
|
|
|
|
static STDMETHODIMP_(ULONG) MMDeviceLocator_Release(IActivateAudioInterfaceCompletionHandler *This)
|
|
{
|
|
aout_sys_t *sys = container_of(This, aout_sys_t, client_locator);
|
|
return InterlockedDecrement(&sys->refs);
|
|
}
|
|
|
|
static STDMETHODIMP MMDeviceLocator_QueryInterface(IActivateAudioInterfaceCompletionHandler *This,
|
|
REFIID riid, void **ppv)
|
|
{
|
|
if( IsEqualIID(riid, &IID_IUnknown) ||
|
|
IsEqualIID(riid, &IID_IActivateAudioInterfaceCompletionHandler) )
|
|
{
|
|
MMDeviceLocator_AddRef(This);
|
|
*ppv = This;
|
|
return S_OK;
|
|
}
|
|
*ppv = NULL;
|
|
if( IsEqualIID(riid, &IID_IAgileObject) )
|
|
{
|
|
return S_OK;
|
|
}
|
|
return ResultFromScode( E_NOINTERFACE );
|
|
}
|
|
|
|
/* MMDeviceLocator IActivateAudioInterfaceCompletionHandler methods */
|
|
static HRESULT MMDeviceLocator_ActivateCompleted(IActivateAudioInterfaceCompletionHandler *This,
|
|
IActivateAudioInterfaceAsyncOperation *operation)
|
|
{
|
|
(void)operation;
|
|
aout_sys_t *sys = container_of(This, aout_sys_t, client_locator);
|
|
vlc_sem_post( &sys->async_completed );
|
|
return S_OK;
|
|
}
|
|
|
|
/* MMDeviceLocator vtable */
|
|
static const struct IActivateAudioInterfaceCompletionHandlerVtbl MMDeviceLocator_vtable =
|
|
{
|
|
MMDeviceLocator_QueryInterface,
|
|
MMDeviceLocator_AddRef,
|
|
MMDeviceLocator_Release,
|
|
|
|
MMDeviceLocator_ActivateCompleted,
|
|
};
|
|
|
|
static void WaitForAudioClient(audio_output_t *aout)
|
|
{
|
|
aout_sys_t *sys = aout->sys;
|
|
IActivateAudioInterfaceAsyncOperation* asyncOp = NULL;
|
|
|
|
const wchar_t* devId = sys->requested_device ? sys->requested_device : sys->default_device;
|
|
|
|
assert(sys->refs == 0);
|
|
sys->refs = 0;
|
|
assert(sys->client == NULL);
|
|
sys->client = NULL;
|
|
free(sys->acquired_device);
|
|
sys->acquired_device = NULL;
|
|
ActivateAudioInterfaceAsync(devId, &IID_IAudioClient, NULL, &sys->client_locator, &asyncOp);
|
|
|
|
vlc_sem_wait( &sys->async_completed );
|
|
|
|
if (asyncOp)
|
|
{
|
|
HRESULT hr;
|
|
HRESULT hrActivateResult;
|
|
IUnknown *audioInterface;
|
|
|
|
hr = IActivateAudioInterfaceAsyncOperation_GetActivateResult(asyncOp, &hrActivateResult, &audioInterface);
|
|
IActivateAudioInterfaceAsyncOperation_Release(asyncOp);
|
|
if (unlikely(FAILED(hr)))
|
|
msg_Dbg(aout, "Failed to get the activation result. (hr=0x%lX)", hr);
|
|
else if (FAILED(hrActivateResult))
|
|
msg_Dbg(aout, "Failed to activate the device. (hr=0x%lX)", hr);
|
|
else if (unlikely(audioInterface == NULL))
|
|
msg_Dbg(aout, "Failed to get the device instance.");
|
|
else
|
|
{
|
|
hr = IUnknown_QueryInterface(audioInterface, &IID_IAudioClient, (void**)&sys->client);
|
|
IUnknown_Release(audioInterface);
|
|
if (unlikely(FAILED(hr)))
|
|
msg_Warn(aout, "The received interface is not a IAudioClient. (hr=0x%lX)", hr);
|
|
else
|
|
{
|
|
sys->acquired_device = wcsdup(devId);
|
|
|
|
char *report = FromWide(devId);
|
|
if (likely(report))
|
|
{
|
|
aout_DeviceReport(aout, report);
|
|
free(report);
|
|
}
|
|
|
|
IAudioClient2 *audioClient2;
|
|
if (SUCCEEDED(IAudioClient_QueryInterface(sys->client, &IID_IAudioClient2, (void**)&audioClient2))
|
|
&& audioClient2)
|
|
{
|
|
// "BackgroundCapableMedia" does not work in UWP
|
|
AudioClientProperties props = (AudioClientProperties) {
|
|
.cbSize = sizeof(props),
|
|
.bIsOffload = FALSE,
|
|
.eCategory = AudioCategory_Movie,
|
|
.Options = AUDCLNT_STREAMOPTIONS_NONE
|
|
};
|
|
if (FAILED(IAudioClient2_SetClientProperties(audioClient2, &props))) {
|
|
msg_Dbg(aout, "Failed to set audio client properties");
|
|
}
|
|
IAudioClient2_Release(audioClient2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool SetRequestedDevice(audio_output_t *aout, wchar_t *id)
|
|
{
|
|
aout_sys_t* sys = aout->sys;
|
|
if (sys->requested_device != id)
|
|
{
|
|
if (sys->requested_device != sys->default_device)
|
|
free(sys->requested_device);
|
|
sys->requested_device = id;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static int DeviceRequestLocked(audio_output_t *aout)
|
|
{
|
|
aout_sys_t *sys = aout->sys;
|
|
assert(sys->requested_device);
|
|
|
|
WaitForAudioClient(aout);
|
|
|
|
if (sys->stream != NULL && sys->client != NULL)
|
|
/* Request restart of stream with the new device */
|
|
aout_RestartRequest(aout, AOUT_RESTART_OUTPUT);
|
|
return (sys->client != NULL) ? 0 : -1;
|
|
}
|
|
|
|
static int DeviceRestartLocked(audio_output_t *aout)
|
|
{
|
|
aout_sys_t *sys = aout->sys;
|
|
|
|
if (sys->client)
|
|
{
|
|
assert(sys->acquired_device);
|
|
free(sys->acquired_device);
|
|
sys->acquired_device = NULL;
|
|
IAudioClient_Release(sys->client);
|
|
sys->client = NULL;
|
|
}
|
|
|
|
return DeviceRequestLocked(aout);
|
|
}
|
|
|
|
static int DeviceSelectLocked(audio_output_t *aout, const char* id)
|
|
{
|
|
aout_sys_t *sys = aout->sys;
|
|
bool changed;
|
|
if( id == NULL )
|
|
{
|
|
changed = SetRequestedDevice(aout, sys->default_device);
|
|
}
|
|
else
|
|
{
|
|
wchar_t *requested_device = ToWide(id);
|
|
if (unlikely(requested_device == NULL))
|
|
return VLC_ENOMEM;
|
|
changed = SetRequestedDevice(aout, requested_device);
|
|
}
|
|
if (!changed)
|
|
return VLC_EGENERIC;
|
|
return DeviceRestartLocked(aout);
|
|
}
|
|
|
|
static int DeviceSelect(audio_output_t *aout, const char* id)
|
|
{
|
|
aout_sys_t *sys = aout->sys;
|
|
EnterCriticalSection(&sys->lock);
|
|
int ret = DeviceSelectLocked(aout, id);
|
|
LeaveCriticalSection(&sys->lock);
|
|
return ret;
|
|
}
|
|
|
|
static void ResetInvalidatedClient(audio_output_t *aout, HRESULT hr)
|
|
{
|
|
/* Select the default device (and restart) on unplug */
|
|
if (unlikely(hr == AUDCLNT_E_DEVICE_INVALIDATED ||
|
|
hr == AUDCLNT_E_RESOURCES_INVALIDATED))
|
|
{
|
|
// Select the default device (and restart) on unplug
|
|
DeviceSelect(aout, NULL);
|
|
}
|
|
}
|
|
|
|
static int VolumeSet(audio_output_t *aout, float vol)
|
|
{
|
|
aout_sys_t *sys = aout->sys;
|
|
if( unlikely( sys->client == NULL ) )
|
|
return VLC_EGENERIC;
|
|
HRESULT hr;
|
|
ISimpleAudioVolume *pc_AudioVolume = NULL;
|
|
|
|
float linear_vol = vol * vol * vol; /* ISimpleAudioVolume is tapered linearly. */
|
|
|
|
if (linear_vol > 1.f)
|
|
{
|
|
sys->gain = linear_vol;
|
|
linear_vol = 1.f;
|
|
}
|
|
else
|
|
sys->gain = 1.f;
|
|
|
|
aout_GainRequest(aout, sys->gain);
|
|
|
|
hr = IAudioClient_GetService(sys->client, &IID_ISimpleAudioVolume, (void**)&pc_AudioVolume);
|
|
if (FAILED(hr))
|
|
{
|
|
msg_Err(aout, "cannot get volume service (error 0x%lX)", hr);
|
|
goto done;
|
|
}
|
|
|
|
hr = ISimpleAudioVolume_SetMasterVolume(pc_AudioVolume, linear_vol, NULL);
|
|
if (FAILED(hr))
|
|
{
|
|
msg_Err(aout, "cannot set volume (error 0x%lX)", hr);
|
|
goto done;
|
|
}
|
|
|
|
aout_VolumeReport(aout, vol);
|
|
|
|
done:
|
|
if (pc_AudioVolume)
|
|
ISimpleAudioVolume_Release(pc_AudioVolume);
|
|
|
|
return SUCCEEDED(hr) ? 0 : -1;
|
|
}
|
|
|
|
static int MuteSet(audio_output_t *aout, bool mute)
|
|
{
|
|
aout_sys_t *sys = aout->sys;
|
|
if( unlikely( sys->client == NULL ) )
|
|
return VLC_EGENERIC;
|
|
HRESULT hr;
|
|
ISimpleAudioVolume *pc_AudioVolume = NULL;
|
|
|
|
hr = IAudioClient_GetService(sys->client, &IID_ISimpleAudioVolume, (void**)&pc_AudioVolume);
|
|
if (FAILED(hr))
|
|
{
|
|
msg_Err(aout, "cannot get volume service (error 0x%lX)", hr);
|
|
goto done;
|
|
}
|
|
|
|
hr = ISimpleAudioVolume_SetMute(pc_AudioVolume, mute, NULL);
|
|
if (FAILED(hr))
|
|
{
|
|
msg_Err(aout, "cannot set mute (error 0x%lX)", hr);
|
|
goto done;
|
|
}
|
|
|
|
aout_MuteReport(aout, mute);
|
|
|
|
done:
|
|
if (pc_AudioVolume)
|
|
ISimpleAudioVolume_Release(pc_AudioVolume);
|
|
|
|
return SUCCEEDED(hr) ? 0 : -1;
|
|
}
|
|
|
|
static int TimeGet(audio_output_t *aout, vlc_tick_t *restrict delay)
|
|
{
|
|
aout_sys_t *sys = aout->sys;
|
|
if( unlikely( sys->client == NULL ) )
|
|
return VLC_EGENERIC;
|
|
HRESULT hr;
|
|
|
|
EnterMTA();
|
|
hr = aout_stream_TimeGet(sys->stream, delay);
|
|
LeaveMTA();
|
|
|
|
return SUCCEEDED(hr) ? 0 : -1;
|
|
}
|
|
|
|
static void Play(audio_output_t *aout, block_t *block, vlc_tick_t date)
|
|
{
|
|
aout_sys_t *sys = aout->sys;
|
|
if( unlikely( sys->client == NULL ) )
|
|
return;
|
|
|
|
EnterMTA();
|
|
HRESULT hr = aout_stream_Play(sys->stream, block, date);
|
|
LeaveMTA();
|
|
|
|
ResetInvalidatedClient(aout, hr);
|
|
(void) date;
|
|
}
|
|
|
|
static void Pause(audio_output_t *aout, bool paused, vlc_tick_t date)
|
|
{
|
|
aout_sys_t *sys = aout->sys;
|
|
if( unlikely( sys->client == NULL ) )
|
|
return;
|
|
|
|
EnterMTA();
|
|
HRESULT hr = aout_stream_Pause(sys->stream, paused);
|
|
LeaveMTA();
|
|
|
|
(void) date;
|
|
ResetInvalidatedClient(aout, hr);
|
|
}
|
|
|
|
static void Flush(audio_output_t *aout)
|
|
{
|
|
aout_sys_t *sys = aout->sys;
|
|
if( unlikely( sys->client == NULL ) )
|
|
return;
|
|
|
|
EnterMTA();
|
|
HRESULT hr = aout_stream_Flush(sys->stream);
|
|
LeaveMTA();
|
|
|
|
ResetInvalidatedClient(aout, hr);
|
|
}
|
|
|
|
static HRESULT ActivateDevice(void *opaque, REFIID iid, PROPVARIANT *actparms,
|
|
void **restrict pv)
|
|
{
|
|
IAudioClient *client = opaque;
|
|
|
|
if (!IsEqualIID(iid, &IID_IAudioClient))
|
|
return E_NOINTERFACE;
|
|
if (actparms != NULL || client == NULL )
|
|
return E_INVALIDARG;
|
|
|
|
IAudioClient_AddRef(client); // as would IMMDevice_Activate do
|
|
*pv = opaque;
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
static int aout_stream_Start(void *func, bool forced, va_list ap)
|
|
{
|
|
aout_stream_start_t start = func;
|
|
aout_stream_t *s = va_arg(ap, aout_stream_t *);
|
|
audio_sample_format_t *fmt = va_arg(ap, audio_sample_format_t *);
|
|
HRESULT *hr = va_arg(ap, HRESULT *);
|
|
|
|
(void) forced;
|
|
*hr = start(s, fmt, &GUID_VLC_AUD_OUT);
|
|
return SUCCEEDED(*hr) ? VLC_SUCCESS : VLC_EGENERIC;
|
|
}
|
|
|
|
static int Start(audio_output_t *aout, audio_sample_format_t *restrict fmt)
|
|
{
|
|
aout_sys_t *sys = aout->sys;
|
|
HRESULT hr;
|
|
|
|
aout_stream_t *s = vlc_object_create(aout, sizeof (*s));
|
|
if (unlikely(s == NULL))
|
|
return -1;
|
|
|
|
// Load the "out stream" for the requested device
|
|
EnterMTA();
|
|
EnterCriticalSection(&sys->lock);
|
|
|
|
if (sys->requested_device != NULL)
|
|
{
|
|
if (sys->acquired_device == NULL || wcscmp(sys->acquired_device, sys->requested_device))
|
|
{
|
|
// we have a pending request for a new device
|
|
DeviceRestartLocked(aout);
|
|
if (sys->client == NULL)
|
|
{
|
|
LeaveCriticalSection(&sys->lock);
|
|
LeaveMTA();
|
|
vlc_object_delete(&s->obj);
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
s->owner.activate = ActivateDevice;
|
|
for (;;)
|
|
{
|
|
s->owner.device = sys->client;
|
|
sys->module = vlc_module_load(s, "aout stream", NULL, false,
|
|
aout_stream_Start, s, fmt, &hr);
|
|
|
|
int ret = -1;
|
|
if (hr == AUDCLNT_E_DEVICE_INVALIDATED)
|
|
{
|
|
// the requested device is not usable, try the default device
|
|
ret = DeviceSelectLocked(aout, NULL);
|
|
}
|
|
else if (hr == AUDCLNT_E_ALREADY_INITIALIZED)
|
|
{
|
|
/* From MSDN: "If the initial call to Initialize fails, subsequent
|
|
* Initialize calls might fail and return error code
|
|
* E_ALREADY_INITIALIZED, even though the interface has not been
|
|
* initialized. If this occurs, release the IAudioClient interface
|
|
* and obtain a new IAudioClient interface from the MMDevice API
|
|
* before calling Initialize again."
|
|
*
|
|
* Therefore, request to MMThread the same device and try again. */
|
|
|
|
ret = DeviceRestartLocked(aout);
|
|
}
|
|
if (ret != VLC_SUCCESS)
|
|
break;
|
|
|
|
if (sys->client == NULL || sys->module != NULL)
|
|
break;
|
|
}
|
|
|
|
// Report the initial volume and mute status to the core
|
|
if (sys->client != NULL)
|
|
{
|
|
ISimpleAudioVolume* pc_AudioVolume = NULL;
|
|
|
|
hr = IAudioClient_GetService(sys->client, &IID_ISimpleAudioVolume, (void**)&pc_AudioVolume);
|
|
if (FAILED(hr))
|
|
{
|
|
msg_Err(aout, "cannot get volume service (error 0x%lx)", hr);
|
|
goto done;
|
|
}
|
|
|
|
float vol;
|
|
hr = ISimpleAudioVolume_GetMasterVolume(pc_AudioVolume, &vol);
|
|
if (FAILED(hr))
|
|
{
|
|
msg_Err(aout, "cannot get initial volume (error 0x%lX)", hr);
|
|
goto done;
|
|
}
|
|
|
|
WINBOOL mute;
|
|
hr = ISimpleAudioVolume_GetMute(pc_AudioVolume, &mute);
|
|
if (FAILED(hr))
|
|
{
|
|
msg_Err(aout, "cannot get initial mute (error 0x%lX)", hr);
|
|
goto done;
|
|
}
|
|
|
|
aout_VolumeReport(aout, cbrtf(vol * sys->gain));
|
|
aout_MuteReport(aout, mute != 0);
|
|
|
|
done:
|
|
if (pc_AudioVolume)
|
|
ISimpleAudioVolume_Release(pc_AudioVolume);
|
|
}
|
|
|
|
LeaveCriticalSection(&sys->lock);
|
|
LeaveMTA();
|
|
|
|
if (sys->module == NULL)
|
|
{
|
|
vlc_object_delete(s);
|
|
return -1;
|
|
}
|
|
|
|
if (sys->client)
|
|
{
|
|
// the requested device has been used, reset it
|
|
// we keep the corresponding sys->client until a new request is started
|
|
SetRequestedDevice(aout, NULL);
|
|
}
|
|
|
|
assert (sys->stream == NULL);
|
|
sys->stream = s;
|
|
return 0;
|
|
}
|
|
|
|
static void Stop(audio_output_t *aout)
|
|
{
|
|
aout_sys_t *sys = aout->sys;
|
|
|
|
assert (sys->stream != NULL);
|
|
|
|
EnterMTA();
|
|
aout_stream_Stop(sys->stream);
|
|
LeaveMTA();
|
|
|
|
vlc_object_delete(sys->stream);
|
|
sys->stream = NULL;
|
|
}
|
|
|
|
static int Open(vlc_object_t *obj)
|
|
{
|
|
audio_output_t *aout = (audio_output_t *)obj;
|
|
|
|
aout_sys_t *sys = malloc(sizeof (*sys));
|
|
if (unlikely(sys == NULL))
|
|
return VLC_ENOMEM;
|
|
|
|
if (unlikely(FAILED(StringFromIID(&DEVINTERFACE_AUDIO_RENDER, &sys->default_device))))
|
|
{
|
|
msg_Dbg(obj, "Failed to get the default renderer string");
|
|
free(sys);
|
|
return VLC_EGENERIC;
|
|
}
|
|
|
|
char *psz_default = FromWide(sys->default_device);
|
|
if (likely(psz_default != NULL))
|
|
{
|
|
aout_HotplugReport(aout, psz_default, _("Default"));
|
|
free(psz_default);
|
|
}
|
|
|
|
InitializeCriticalSection(&sys->lock);
|
|
|
|
vlc_sem_init(&sys->async_completed, 0);
|
|
sys->refs = 0;
|
|
sys->requested_device = sys->default_device;
|
|
sys->acquired_device = NULL;
|
|
sys->client_locator = (IActivateAudioInterfaceCompletionHandler) { &MMDeviceLocator_vtable };
|
|
sys->gain = 1.f;
|
|
|
|
aout->sys = sys;
|
|
sys->stream = NULL;
|
|
sys->client = NULL;
|
|
aout->start = Start;
|
|
aout->stop = Stop;
|
|
aout->time_get = TimeGet;
|
|
aout->volume_set = VolumeSet;
|
|
aout->mute_set = MuteSet;
|
|
aout->play = Play;
|
|
aout->pause = Pause;
|
|
aout->flush = Flush;
|
|
aout->device_select = DeviceSelect;
|
|
return VLC_SUCCESS;
|
|
}
|
|
|
|
static void Close(vlc_object_t *obj)
|
|
{
|
|
audio_output_t *aout = (audio_output_t *)obj;
|
|
aout_sys_t *sys = aout->sys;
|
|
|
|
if(sys->client != NULL)
|
|
IAudioClient_Release(sys->client);
|
|
|
|
assert(sys->refs == 0);
|
|
|
|
free(sys->acquired_device);
|
|
if (sys->requested_device != sys->default_device)
|
|
free(sys->requested_device);
|
|
CoTaskMemFree(sys->default_device);
|
|
DeleteCriticalSection(&sys->lock);
|
|
|
|
free(sys);
|
|
}
|
|
|
|
vlc_module_begin()
|
|
set_shortname("winstore")
|
|
set_description("Windows Store audio output")
|
|
set_capability("audio output", 0)
|
|
set_category(CAT_AUDIO)
|
|
set_subcategory(SUBCAT_AUDIO_AOUT)
|
|
add_shortcut("wasapi")
|
|
set_callbacks(Open, Close)
|
|
vlc_module_end()
|
|
|