diff options
Diffstat (limited to 'tinyDAV/src/video/winm/tdav_producer_winm.cxx')
-rw-r--r-- | tinyDAV/src/video/winm/tdav_producer_winm.cxx | 737 |
1 files changed, 737 insertions, 0 deletions
diff --git a/tinyDAV/src/video/winm/tdav_producer_winm.cxx b/tinyDAV/src/video/winm/tdav_producer_winm.cxx new file mode 100644 index 0000000..398340a --- /dev/null +++ b/tinyDAV/src/video/winm/tdav_producer_winm.cxx @@ -0,0 +1,737 @@ +/*Copyright (C) 2013 Doubango Telecom <http://www.doubango.org> +* +* This file is part of Open Source Doubango Framework. +* +* DOUBANGO is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* DOUBANGO 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 General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with DOUBANGO. +*/ +/**@file tdav_producer_winm.cxx +* @brief Microsoft Windows Media (WinM) video producer. +* +*/ +#include "tinydav/video/winm/tdav_producer_winm.h" + +#if HAVE_WINM + +#include "tsk_string.h" +#include "tsk_debug.h" + +#if TDAV_UNDER_WINDOWS_PHONE + +#include <windows.h> +#include <implements.h> +#include <Windows.Phone.Media.Capture.h> +#include <Windows.Phone.Media.Capture.Native.h> + +using namespace Windows::System::Threading; +using namespace Microsoft::WRL; +using namespace Windows::Foundation; +using namespace Platform; +using namespace Windows::Phone::Media::Capture; +using namespace Windows::Storage::Streams; + +struct tdav_producer_winm_s; + +namespace Doubango +{ + namespace VoIP + { + class CaptureSampleSink : + public Microsoft::WRL::RuntimeClass< + Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::RuntimeClassType::ClassicCom>, + ICameraCaptureSampleSink> + { + DWORD m_dwSampleCount; + const struct tdav_producer_winm_s* m_pProducer; + + public: + + STDMETHODIMP RuntimeClassInitialize(const struct tdav_producer_winm_s* pProducer) + { + m_dwSampleCount = 0; + m_pProducer = pProducer; + return S_OK; + } + + DWORD GetSampleCount() + { + return m_dwSampleCount; + } + + IFACEMETHODIMP_(void) + OnSampleAvailable( + ULONGLONG hnsPresentationTime, + ULONGLONG hnsSampleDuration, + DWORD cbSample, + BYTE* pSample) + { + m_dwSampleCount++; + if(m_pProducer && TMEDIA_PRODUCER(m_pProducer)->enc_cb.callback) + { + TMEDIA_PRODUCER(m_pProducer)->enc_cb.callback(TMEDIA_PRODUCER(m_pProducer)->enc_cb.callback_data, pSample, cbSample); + } + } + }; + + ref class VideoCapturePhone sealed + { + public: + virtual ~VideoCapturePhone(); + internal: + VideoCapturePhone(); + + int Prepare(const struct tdav_producer_winm_s* winm); + int Start(); + int Pause(); + int Stop(); + void SetCameraLocation(Windows::Phone::Media::Capture::CameraSensorLocation cameraLocation); + void ToggleCamera(); + + private: + int UnPrepare(); + void ToggleCameraThread(Windows::Foundation::IAsyncAction^ operation); + + tsk_mutex_handle_t* m_hMutex; + + const tdav_producer_winm_s* m_pWrappedPlugin; + + // Has capture started? + bool m_bStarted, m_bPrepared; + + // Events to signal whether capture has stopped/started + HANDLE m_hStopCompleted; + HANDLE m_hStartCompleted; + + IAsyncOperation<AudioVideoCaptureDevice^> ^m_pOpenOperation; + + Windows::Foundation::IAsyncAction^ m_ToggleThread; + + // Native sink and video device + CaptureSampleSink *m_pVideoSink; + IAudioVideoCaptureDeviceNative *m_pVideoDevice; + + Windows::Phone::Media::Capture::CameraSensorLocation m_eCameraLocation; + + Windows::Phone::Media::Capture::AudioVideoCaptureDevice ^m_pVideoOnlyDevice; + Windows::Foundation::IAsyncAction ^m_pVideoCaptureAction; + }; + } +} + +using namespace Doubango::VoIP; + +#endif + + +typedef struct tdav_producer_winm_s +{ + TMEDIA_DECLARE_PRODUCER; +#if TDAV_UNDER_WINDOWS_PHONE + VideoCapturePhone^ videoCapturePhone; +#endif +} +tdav_producer_winm_t; + + +/* ============ Media Producer Interface ================= */ +static int tdav_producer_winm_set(tmedia_producer_t *self, const tmedia_param_t* param) +{ + int ret = 0; + tdav_producer_winm_t* producer = (tdav_producer_winm_t*)self; + + if(!producer || !param){ + TSK_DEBUG_ERROR("Invalid parameter"); + return -1; + } + + if(param->value_type == tmedia_pvt_int32){ + if(tsk_striequals(param->key, "camera-location")){ + Windows::Phone::Media::Capture::CameraSensorLocation cameraLocation = (Windows::Phone::Media::Capture::CameraSensorLocation)*((int32_t*)param->value); + if(producer->videoCapturePhone) + { + producer->videoCapturePhone->SetCameraLocation(cameraLocation); + return 0; + } + } + } + + return ret; +} + +static int tdav_producer_winm_prepare(tmedia_producer_t* self, const tmedia_codec_t* codec) +{ + tdav_producer_winm_t* producer = (tdav_producer_winm_t*)self; + + if(!producer || !codec && codec->plugin){ + TSK_DEBUG_ERROR("Invalid parameter"); + return -1; + } + + if(codec->id == tmedia_codec_id_h264_bp || codec->id == tmedia_codec_id_h264_mp) { + TMEDIA_PRODUCER(producer)->encoder.codec_id = codec->id; + } + else { + TMEDIA_PRODUCER(producer)->encoder.codec_id = tmedia_codec_id_none; + } + TMEDIA_PRODUCER(producer)->video.fps = TMEDIA_CODEC_VIDEO(codec)->out.fps; + TMEDIA_PRODUCER(producer)->video.width = TMEDIA_CODEC_VIDEO(codec)->out.width; + TMEDIA_PRODUCER(producer)->video.height = TMEDIA_CODEC_VIDEO(codec)->out.height; + + +#if TDAV_UNDER_WINDOWS_PHONE + return producer->videoCapturePhone->Prepare(producer); +#else + TSK_DEBUG_ERROR("Unexpected code called"); + return -1; +#endif +} + +static int tdav_producer_winm_start(tmedia_producer_t* self) +{ + tdav_producer_winm_t* producer = (tdav_producer_winm_t*)self; + + if(!producer){ + TSK_DEBUG_ERROR("Invalid parameter"); + return -1; + } + +#if TDAV_UNDER_WINDOWS_PHONE + return producer->videoCapturePhone->Start(); +#else + TSK_DEBUG_ERROR("Unexpected code called"); + return -1; +#endif +} + +static int tdav_producer_winm_pause(tmedia_producer_t* self) +{ + tdav_producer_winm_t* producer = (tdav_producer_winm_t*)self; + + if(!producer){ + TSK_DEBUG_ERROR("Invalid parameter"); + return -1; + } + +#if TDAV_UNDER_WINDOWS_PHONE + return producer->videoCapturePhone->Pause(); +#else + TSK_DEBUG_ERROR("Unexpected code called"); + return -1; +#endif +} + +static int tdav_producer_winm_stop(tmedia_producer_t* self) +{ + tdav_producer_winm_t* producer = (tdav_producer_winm_t*)self; + + if(!self){ + TSK_DEBUG_ERROR("Invalid parameter"); + return -1; + } + +#if TDAV_UNDER_WINDOWS_PHONE + return producer->videoCapturePhone->Stop(); +#else + TSK_DEBUG_ERROR("Unexpected code called"); + return -1; +#endif +} + + +#if TDAV_UNDER_WINDOWS_PHONE + +VideoCapturePhone::VideoCapturePhone() : + m_bStarted(false), + m_bPrepared(false), + m_pVideoOnlyDevice(nullptr), + m_pVideoSink(NULL), + m_pVideoDevice(NULL), + m_pWrappedPlugin(NULL), + m_pOpenOperation(nullptr), + m_eCameraLocation(CameraSensorLocation::Front) +{ + if(!(m_hMutex = tsk_mutex_create())){ + throw ref new Platform::FailureException(L"Failed to create mutex"); + } + + m_hStopCompleted = CreateEventEx(NULL, NULL, CREATE_EVENT_MANUAL_RESET, EVENT_ALL_ACCESS); + if (!m_hStopCompleted) + { + throw ref new Platform::Exception(HRESULT_FROM_WIN32(GetLastError()), L"Could not create shutdown event"); + } + + m_hStartCompleted = CreateEventEx(NULL, NULL, CREATE_EVENT_MANUAL_RESET, EVENT_ALL_ACCESS); + if (!m_hStartCompleted) + { + throw ref new Platform::Exception(HRESULT_FROM_WIN32(GetLastError()), L"Could not create start event"); + } +} + +VideoCapturePhone::~VideoCapturePhone() +{ + Stop(); + + if(m_ToggleThread) + { + m_ToggleThread->Cancel(); + m_ToggleThread->Close(); + m_ToggleThread = nullptr; + } + + tsk_mutex_destroy(&m_hMutex); +} + +int VideoCapturePhone::Prepare(const struct tdav_producer_winm_s* winm) +{ + HRESULT hr = E_FAIL; + int ret = 0; + Windows::Foundation::Size dimensionsRequested, dimensionsClosest; + Collections::IVectorView<Size> ^availableSizes; + Collections::IIterator<Windows::Foundation::Size> ^availableSizesIterator; + bool bClosestFound = false; + + #define WINM_SET_ERROR(code) ret = (code); goto bail; + + tsk_mutex_lock(m_hMutex); + + if(m_bPrepared) + { + TSK_DEBUG_INFO("#WASAPI: Audio producer already prepared"); + goto bail; + } + + if(!winm) + { + TSK_DEBUG_ERROR("Invalid parameter"); + WINM_SET_ERROR(-1); + } + + if(m_pVideoCaptureAction || m_pVideoDevice || m_pVideoOnlyDevice || m_pVideoSink || m_pOpenOperation){ + TSK_DEBUG_ERROR("Producer already prepared"); + WINM_SET_ERROR(-2); + } + + dimensionsClosest.Width = dimensionsRequested.Width = (float)TMEDIA_PRODUCER(winm)->video.width; + dimensionsClosest.Height = dimensionsRequested.Height = (float)TMEDIA_PRODUCER(winm)->video.height; + availableSizes = AudioVideoCaptureDevice::GetAvailableCaptureResolutions(m_eCameraLocation); + availableSizesIterator = availableSizes->First(); + + while(!m_pOpenOperation && availableSizesIterator->HasCurrent) + { + TSK_DEBUG_INFO("Camera Supported size: (%f, %f)", availableSizesIterator->Current.Width, availableSizesIterator->Current.Height); + if(availableSizesIterator->Current.Height == dimensionsRequested.Width && availableSizesIterator->Current.Width == dimensionsRequested.Height) + { + m_pOpenOperation = AudioVideoCaptureDevice::OpenForVideoOnlyAsync(m_eCameraLocation, dimensionsRequested); + TSK_DEBUG_INFO("Camera::Open(%d, %d)", dimensionsRequested.Width, dimensionsRequested.Height); + break; + } + else if(!bClosestFound && (availableSizesIterator->Current.Height <= dimensionsRequested.Height && availableSizesIterator->Current.Width <= dimensionsRequested.Width)) + { + dimensionsClosest.Height = availableSizesIterator->Current.Height; + dimensionsClosest.Width = availableSizesIterator->Current.Width; + bClosestFound = true; + } + availableSizesIterator->MoveNext(); + } + + if(!m_pOpenOperation) + { + m_pOpenOperation = AudioVideoCaptureDevice::OpenForVideoOnlyAsync(m_eCameraLocation, dimensionsClosest); + TSK_DEBUG_INFO("Camera::Open(%f, %f)", dimensionsClosest.Width, dimensionsClosest.Height); + } + +bail: + if(ret != 0){ + UnPrepare(); + } + if((m_bPrepared = (ret == 0))) + { + m_pWrappedPlugin = winm; + } + + tsk_mutex_unlock(m_hMutex); + + return ret; +} + +int VideoCapturePhone::Start() +{ + tsk_mutex_lock(m_hMutex); + + if(m_bStarted) + { + TSK_DEBUG_INFO("#WINM: Video producer already started"); + goto bail; + } + if(!m_bPrepared) + { + TSK_DEBUG_ERROR("#WINM: Video producer not prepared"); + goto bail; + } + + m_bStarted = true; + + m_pOpenOperation->Completed = ref new AsyncOperationCompletedHandler<AudioVideoCaptureDevice^>([this] (IAsyncOperation<AudioVideoCaptureDevice^> ^operation, Windows::Foundation::AsyncStatus status) + { + tsk_mutex_lock(m_hMutex); + if(m_bStarted) + { + + if(status == Windows::Foundation::AsyncStatus::Completed) + { + + TSK_DEBUG_INFO("+[VideoCapturePhone::Prepare] => OpenAsyncOperation started"); + + auto videoDevice = operation->GetResults(); + + m_pVideoOnlyDevice = videoDevice; + IAudioVideoCaptureDeviceNative *pNativeDevice = NULL; + HRESULT hr = reinterpret_cast<IUnknown*>(videoDevice)->QueryInterface(__uuidof(IAudioVideoCaptureDeviceNative), (void**) &pNativeDevice); + + if (NULL == pNativeDevice || FAILED(hr)) + { + throw ref new FailureException("Unable to QI IAudioVideoCaptureDeviceNative"); + } + + // Save off the native device + m_pVideoDevice = pNativeDevice; + + // Set Fps + CameraCapturePropertyRange^ cameraCapturePropertyRange = m_pVideoOnlyDevice->GetSupportedPropertyRange(m_eCameraLocation, KnownCameraAudioVideoProperties::VideoFrameRate); + if(cameraCapturePropertyRange) + { + try + { + Windows::Foundation::IPropertyValue^ vMin = dynamic_cast<Windows::Foundation::IPropertyValue^>(cameraCapturePropertyRange->Min); + Windows::Foundation::IPropertyValue^ vMax = dynamic_cast<Windows::Foundation::IPropertyValue^>(cameraCapturePropertyRange->Max); + UINT32 nFps = TSK_CLAMP(vMin->GetUInt32(), (UINT32)TMEDIA_PRODUCER(m_pWrappedPlugin)->video.fps, vMax->GetUInt32()); + m_pVideoOnlyDevice->SetProperty(KnownCameraAudioVideoProperties::VideoFrameRate, nFps); + } + catch(...){ } + } + + // Set Camera Rotation + try + { + m_pVideoOnlyDevice->SetProperty( + KnownCameraGeneralProperties::EncodeWithOrientation, + m_eCameraLocation == Windows::Phone::Media::Capture::CameraSensorLocation::Back ? 90 : -90 + ); + } + catch(...){ } + + // Create the sink + MakeAndInitialize<CaptureSampleSink>(&(m_pVideoSink), m_pWrappedPlugin); + pNativeDevice->SetVideoSampleSink(m_pVideoSink); + + // Use the same encoding format as in VideoMediaStreamSource.cs + videoDevice->VideoEncodingFormat = CameraCaptureVideoFormat::H264; + + SetEvent(m_hStartCompleted); + + // Start recording to our sink + m_pVideoCaptureAction = videoDevice->StartRecordingToSinkAsync(); + m_pVideoCaptureAction->Completed = ref new AsyncActionCompletedHandler([this] (IAsyncAction ^asyncInfo, Windows::Foundation::AsyncStatus status) + { + if(status == Windows::Foundation::AsyncStatus::Completed) + { + TSK_DEBUG_INFO("[VideoCapturePhone::Prepare] => StartRecordingToSinkAsync completed"); + } + else if(status == Windows::Foundation::AsyncStatus::Error || status == Windows::Foundation::AsyncStatus::Canceled) + { + TSK_DEBUG_INFO("[VideoCapturePhone::Prepare] => StartRecordingToSinkAsync did not complete"); + } + }); + + TSK_DEBUG_INFO("-[VideoCapturePhone::Prepare] => OpenAsyncOperation Completed"); + } + else if(status == Windows::Foundation::AsyncStatus::Canceled) + { + TSK_DEBUG_INFO("[VideoCapturePhone::Prepare] => OpenAsyncOperation Canceled"); + } + else if(status == Windows::Foundation::AsyncStatus::Error) + { + TSK_DEBUG_INFO("[VideoCapturePhone::Prepare] => OpenAsyncOperation encountered an error"); + } + } + + tsk_mutex_unlock(m_hMutex); + }); + +bail: + tsk_mutex_unlock(m_hMutex); + + return (m_bStarted ? 0 : -2); +} + +int VideoCapturePhone::Pause() +{ + tsk_mutex_lock(m_hMutex); + + if(m_bStarted) + { + + } + + tsk_mutex_unlock(m_hMutex); + + return 0; +} + +int VideoCapturePhone::Stop() +{ + tsk_mutex_lock(m_hMutex); + + TSK_DEBUG_INFO("+[VideoCapturePhone::Stop] => Trying to stop capture"); + if (m_pVideoOnlyDevice) + { + TSK_DEBUG_INFO("Destroying VideoCaptureDevice"); + + try + { + if(m_bStarted) + { + m_pVideoOnlyDevice->StopRecordingAsync()->Completed = ref new AsyncActionCompletedHandler([this] (IAsyncAction ^action, Windows::Foundation::AsyncStatus status){ + if(status == Windows::Foundation::AsyncStatus::Completed) + { + TSK_DEBUG_INFO("[VideoCapturePhone::StopRecordingAsync] Video successfully stopped"); + } + else + { + TSK_DEBUG_INFO("[VideoCapturePhone::StopRecordingAsync] Error occurred while stopping recording"); + } + m_pVideoCaptureAction = nullptr; + m_pVideoOnlyDevice = nullptr; + m_bStarted = false; + SetEvent(m_hStopCompleted); + }); + } + } + catch(...) + { + // A Platform::ObjectDisposedException can be raised if the app has had its access + // to video revoked (most commonly when the app is going out of the foreground) + TSK_DEBUG_ERROR("Exception caught while destroying video capture"); + m_pVideoCaptureAction = nullptr; + m_pVideoOnlyDevice = nullptr; + m_bStarted = false; + SetEvent(m_hStopCompleted); + } + + if (m_pVideoDevice) + { + m_pVideoDevice->Release(); + m_pVideoDevice = NULL; + } + + if (m_pVideoSink) + { + m_pVideoSink->Release(); + m_pVideoSink = NULL; + } + } + else + { + m_bStarted = false; + } + + TSK_DEBUG_INFO("-[VideoCapturePhone::Stop] => finished stopping capture\n"); + + // will be prepared again before next start() + UnPrepare(); + + tsk_mutex_unlock(m_hMutex); + + return 0; +} + +void VideoCapturePhone::SetCameraLocation(Windows::Phone::Media::Capture::CameraSensorLocation cameraLocation) +{ + if(m_eCameraLocation != cameraLocation) + { + if(m_bStarted) + { + ToggleCamera(); + } + else + { + m_eCameraLocation = cameraLocation; + } + } +} + +int VideoCapturePhone::UnPrepare() +{ + tsk_mutex_lock(m_hMutex); + + if(m_bStarted) + { + ResetEvent(m_hStopCompleted); + Stop(); + DWORD waitResult = WaitForSingleObjectEx(m_hStopCompleted, 5000, FALSE); + if(waitResult != WAIT_OBJECT_0) + { + TSK_DEBUG_ERROR("Failed to stop video producer"); + } + } + + if (m_pVideoDevice) + { + m_pVideoDevice->Release(); + m_pVideoDevice = NULL; + } + + if (m_pVideoSink) + { + m_pVideoSink->Release(); + m_pVideoSink = NULL; + } + + m_pOpenOperation = nullptr; + + m_bPrepared = false; + + tsk_mutex_unlock(m_hMutex); + + return 0; +} + +void VideoCapturePhone::ToggleCamera() +{ + tsk_mutex_lock(m_hMutex); + + if(m_ToggleThread) + { + m_ToggleThread->Cancel(); + m_ToggleThread->Close(); + m_ToggleThread = nullptr; + } + + m_ToggleThread = ThreadPool::RunAsync(ref new WorkItemHandler(this, &VideoCapturePhone::ToggleCameraThread), WorkItemPriority::High, WorkItemOptions::TimeSliced); + + tsk_mutex_unlock(m_hMutex); +} + + +void VideoCapturePhone::ToggleCameraThread(Windows::Foundation::IAsyncAction^ operation) +{ + TSK_DEBUG_INFO("+[VideoCapturePhone::ToggleCamera] => Toggling camera"); + + ResetEvent(m_hStopCompleted); + Stop(); + DWORD waitResult = WaitForSingleObjectEx(m_hStopCompleted, INFINITE, FALSE); + if(waitResult == WAIT_OBJECT_0) + { + ResetEvent(m_hStartCompleted); + if(m_eCameraLocation == Windows::Phone::Media::Capture::CameraSensorLocation::Back) + { + m_eCameraLocation = Windows::Phone::Media::Capture::CameraSensorLocation::Front; + } + else + { + m_eCameraLocation = Windows::Phone::Media::Capture::CameraSensorLocation::Back; + } + Prepare(m_pWrappedPlugin); + Start(); + } + else + { + throw ref new Platform::Exception(HRESULT_FROM_WIN32(waitResult), L"Error waiting for capture to stop when toggling cameras"); + } + + waitResult = WaitForSingleObjectEx(m_hStartCompleted, INFINITE, FALSE); + if(waitResult == WAIT_OBJECT_0) + { + // CameraLocationChanged(newCameraLocation); + } + else + { + throw ref new Platform::Exception(HRESULT_FROM_WIN32(waitResult), L"Error waiting for capture to start when toggling cameras"); + } + TSK_DEBUG_INFO("-[VideoCapturePhone::ToggleCamera] => Toggling camera"); +} + +#endif /* TDAV_UNDER_WINDOWS_PHONE */ + + +// +// Windows Media video producer object definition +// +/* constructor */ +static tsk_object_t* tdav_producer_winm_ctor(tsk_object_t * self, va_list * app) +{ + tdav_producer_winm_t *producer = (tdav_producer_winm_t *)self; + if(producer){ + /* init base */ + tmedia_producer_init(TMEDIA_PRODUCER(producer)); + TMEDIA_PRODUCER(producer)->video.chroma = tmedia_chroma_yuv420p; // To avoid chroma conversion + /* init self with default values*/ + + TMEDIA_PRODUCER(producer)->video.fps = 15; + TMEDIA_PRODUCER(producer)->video.width = 352; + TMEDIA_PRODUCER(producer)->video.height = 288; + +#if TDAV_UNDER_WINDOWS_PHONE + producer->videoCapturePhone = ref new VideoCapturePhone(); +#endif + } + return self; +} +/* destructor */ +static tsk_object_t* tdav_producer_winm_dtor(tsk_object_t * self) +{ + tdav_producer_winm_t *producer = (tdav_producer_winm_t *)self; + if(producer){ + /* stop */ + //if(producer->started){ + tdav_producer_winm_stop((tmedia_producer_t*)self); + //} + + /* deinit base */ + tmedia_producer_deinit(TMEDIA_PRODUCER(producer)); + /* deinit self */ +#if TDAV_UNDER_WINDOWS_PHONE + if(producer->videoCapturePhone) + { + delete producer->videoCapturePhone; + } +#endif + } + + return self; +} +/* object definition */ +static const tsk_object_def_t tdav_producer_winm_def_s = +{ + sizeof(tdav_producer_winm_t), + tdav_producer_winm_ctor, + tdav_producer_winm_dtor, + tsk_null, +}; +/* plugin definition*/ +static const tmedia_producer_plugin_def_t tdav_producer_winm_plugin_def_s = +{ + &tdav_producer_winm_def_s, + + tmedia_video, + "Microsoft Windows Media producer (Video)", + + tdav_producer_winm_set, + tdav_producer_winm_prepare, + tdav_producer_winm_start, + tdav_producer_winm_pause, + tdav_producer_winm_stop +}; +const tmedia_producer_plugin_def_t *tdav_producer_winm_plugin_def_t = &tdav_producer_winm_plugin_def_s; + +#endif /* HAVE_WINM */ |