/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2015 - ROLI Ltd. Permission is granted to use this software under the terms of either: a) the GPL v2 (or any later version) b) the Affero GPL v3 Details of these licenses can be found at: www.gnu.org/licenses JUCE 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. ------------------------------------------------------------------------------ To release a closed-source product which uses JUCE, commercial licenses are available: visit www.juce.com for more information. ============================================================================== */ namespace DirectShowHelpers { bool checkDShowAvailability() { ComSmartPtr graph; return SUCCEEDED (graph.CoCreateInstance (CLSID_FilterGraph)); } //====================================================================== class VideoRenderer { public: VideoRenderer() {} virtual ~VideoRenderer() {} virtual HRESULT create (ComSmartPtr & graphBuilder, ComSmartPtr & baseFilter, HWND hwnd) = 0; virtual void setVideoWindow (HWND hwnd) = 0; virtual void setVideoPosition (HWND hwnd, long videoWidth, long videoHeight) = 0; virtual void repaintVideo (HWND hwnd, HDC hdc) = 0; virtual void displayModeChanged() = 0; virtual HRESULT getVideoSize (long& videoWidth, long& videoHeight) = 0; }; //====================================================================== class VMR7 : public VideoRenderer { public: VMR7() {} HRESULT create (ComSmartPtr & graphBuilder, ComSmartPtr & baseFilter, HWND hwnd) { ComSmartPtr filterConfig; HRESULT hr = baseFilter.CoCreateInstance (CLSID_VideoMixingRenderer); if (SUCCEEDED (hr)) hr = graphBuilder->AddFilter (baseFilter, L"VMR-7"); if (SUCCEEDED (hr)) hr = baseFilter.QueryInterface (filterConfig); if (SUCCEEDED (hr)) hr = filterConfig->SetRenderingMode (VMRMode_Windowless); if (SUCCEEDED (hr)) hr = baseFilter.QueryInterface (windowlessControl); if (SUCCEEDED (hr)) hr = windowlessControl->SetVideoClippingWindow (hwnd); if (SUCCEEDED (hr)) hr = windowlessControl->SetAspectRatioMode (VMR_ARMODE_LETTER_BOX); return hr; } void setVideoWindow (HWND hwnd) { windowlessControl->SetVideoClippingWindow (hwnd); } void setVideoPosition (HWND hwnd, long videoWidth, long videoHeight) { RECT src, dest; SetRect (&src, 0, 0, videoWidth, videoHeight); GetClientRect (hwnd, &dest); windowlessControl->SetVideoPosition (&src, &dest); } void repaintVideo (HWND hwnd, HDC hdc) { windowlessControl->RepaintVideo (hwnd, hdc); } void displayModeChanged() { windowlessControl->DisplayModeChanged(); } HRESULT getVideoSize (long& videoWidth, long& videoHeight) { return windowlessControl->GetNativeVideoSize (&videoWidth, &videoHeight, nullptr, nullptr); } private: ComSmartPtr windowlessControl; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VMR7) }; //====================================================================== #if JUCE_MEDIAFOUNDATION class EVR : public VideoRenderer { public: EVR() {} HRESULT create (ComSmartPtr & graphBuilder, ComSmartPtr & baseFilter, HWND hwnd) { ComSmartPtr getService; HRESULT hr = baseFilter.CoCreateInstance (CLSID_EnhancedVideoRenderer); if (SUCCEEDED (hr)) hr = graphBuilder->AddFilter (baseFilter, L"EVR"); if (SUCCEEDED (hr)) hr = baseFilter.QueryInterface (getService); if (SUCCEEDED (hr)) hr = getService->GetService (MR_VIDEO_RENDER_SERVICE, IID_IMFVideoDisplayControl, (LPVOID*) videoDisplayControl.resetAndGetPointerAddress()); if (SUCCEEDED (hr)) hr = videoDisplayControl->SetVideoWindow (hwnd); if (SUCCEEDED (hr)) hr = videoDisplayControl->SetAspectRatioMode (MFVideoARMode_PreservePicture); return hr; } void setVideoWindow (HWND hwnd) { videoDisplayControl->SetVideoWindow (hwnd); } void setVideoPosition (HWND hwnd, long /*videoWidth*/, long /*videoHeight*/) { const MFVideoNormalizedRect src = { 0.0f, 0.0f, 1.0f, 1.0f }; RECT dest; GetClientRect (hwnd, &dest); videoDisplayControl->SetVideoPosition (&src, &dest); } void repaintVideo (HWND /*hwnd*/, HDC /*hdc*/) { videoDisplayControl->RepaintVideo(); } void displayModeChanged() {} HRESULT getVideoSize (long& videoWidth, long& videoHeight) { SIZE sz; HRESULT hr = videoDisplayControl->GetNativeVideoSize (&sz, nullptr); videoWidth = sz.cx; videoHeight = sz.cy; return hr; } private: ComSmartPtr videoDisplayControl; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (EVR) }; #endif } //====================================================================== class DirectShowComponent::DirectShowContext : public AsyncUpdater { public: DirectShowContext (DirectShowComponent& c, VideoRendererType renderType) : component (c), hwnd (0), hdc (0), state (uninitializedState), hasVideo (false), videoWidth (0), videoHeight (0), type (renderType), needToUpdateViewport (true), needToRecreateNativeWindow (false) { CoInitialize (0); if (type == dshowDefault) { type = dshowVMR7; #if JUCE_MEDIAFOUNDATION if (SystemStats::getOperatingSystemType() >= SystemStats::WinVista) type = dshowEVR; #endif } } ~DirectShowContext() { release(); CoUninitialize(); } //====================================================================== void updateWindowPosition (const Rectangle& newBounds) { nativeWindow->setWindowPosition (newBounds); } void showWindow (bool shouldBeVisible) { nativeWindow->showWindow (shouldBeVisible); } //====================================================================== void repaint() { if (hasVideo) videoRenderer->repaintVideo (nativeWindow->getHandle(), nativeWindow->getContext()); } void updateVideoPosition() { if (hasVideo) videoRenderer->setVideoPosition (nativeWindow->getHandle(), videoWidth, videoHeight); } void displayResolutionChanged() { if (hasVideo) videoRenderer->displayModeChanged(); } //====================================================================== void peerChanged() { deleteNativeWindow(); mediaEvent->SetNotifyWindow (0, 0, 0); if (videoRenderer != nullptr) videoRenderer->setVideoWindow (nullptr); createNativeWindow(); mediaEvent->SetNotifyWindow ((OAHWND) hwnd, graphEventID, 0); if (videoRenderer != nullptr) videoRenderer->setVideoWindow (hwnd); } void handleAsyncUpdate() override { if (hwnd != 0) { if (needToRecreateNativeWindow) { peerChanged(); needToRecreateNativeWindow = false; } if (needToUpdateViewport) { updateVideoPosition(); needToUpdateViewport = false; } repaint(); } else { triggerAsyncUpdate(); } } void recreateNativeWindowAsync() { needToRecreateNativeWindow = true; triggerAsyncUpdate(); } void updateContextPosition() { needToUpdateViewport = true; triggerAsyncUpdate(); } //====================================================================== bool loadFile (const String& fileOrURLPath) { jassert (state == uninitializedState); if (! createNativeWindow()) return false; HRESULT hr = graphBuilder.CoCreateInstance (CLSID_FilterGraph); // basic playback interfaces if (SUCCEEDED (hr)) hr = graphBuilder.QueryInterface (mediaControl); if (SUCCEEDED (hr)) hr = graphBuilder.QueryInterface (mediaPosition); if (SUCCEEDED (hr)) hr = graphBuilder.QueryInterface (mediaEvent); if (SUCCEEDED (hr)) hr = graphBuilder.QueryInterface (basicAudio); // video renderer interface if (SUCCEEDED (hr)) { #if JUCE_MEDIAFOUNDATION if (type == dshowEVR) videoRenderer = new DirectShowHelpers::EVR(); else #endif videoRenderer = new DirectShowHelpers::VMR7(); hr = videoRenderer->create (graphBuilder, baseFilter, hwnd); } // build filter graph if (SUCCEEDED (hr)) { hr = graphBuilder->RenderFile (fileOrURLPath.toWideCharPointer(), nullptr); if (FAILED (hr)) { // Annoyingly, if we don't run the msg loop between failing and deleting the window, the // whole OS message-dispatch system gets itself into a state, and refuses to deliver any // more messages for the whole app. (That's what happens in Win7, anyway) MessageManager::getInstance()->runDispatchLoopUntil (200); } } // remove video renderer if not connected (no video) if (SUCCEEDED (hr)) { if (isRendererConnected()) { hasVideo = true; hr = videoRenderer->getVideoSize (videoWidth, videoHeight); } else { hasVideo = false; graphBuilder->RemoveFilter (baseFilter); videoRenderer = nullptr; baseFilter = nullptr; } } // set window to receive events if (SUCCEEDED (hr)) hr = mediaEvent->SetNotifyWindow ((OAHWND) hwnd, graphEventID, 0); if (SUCCEEDED (hr)) { state = stoppedState; pause(); return true; } // Note that if you're trying to open a file and this method fails, you may // just need to install a suitable codec. It seems that by default DirectShow // doesn't support a very good range of formats. release(); return false; } void release() { if (mediaControl != nullptr) mediaControl->Stop(); if (mediaEvent != nullptr) mediaEvent->SetNotifyWindow (0, 0, 0); if (videoRenderer != nullptr) videoRenderer->setVideoWindow (0); hasVideo = false; videoRenderer = nullptr; baseFilter = nullptr; basicAudio = nullptr; mediaEvent = nullptr; mediaPosition = nullptr; mediaControl = nullptr; graphBuilder = nullptr; state = uninitializedState; videoWidth = 0; videoHeight = 0; if (nativeWindow != nullptr) deleteNativeWindow(); } void graphEventProc() { LONG ec; LONG_PTR p1, p2; jassert (mediaEvent != nullptr); while (SUCCEEDED (mediaEvent->GetEvent (&ec, &p1, &p2, 0))) { switch (ec) { case EC_REPAINT: component.repaint(); break; case EC_COMPLETE: if (component.isLooping()) component.goToStart(); else component.stop(); break; case EC_USERABORT: case EC_ERRORABORT: case EC_ERRORABORTEX: component.closeMovie(); break; default: break; } mediaEvent->FreeEventParams (ec, p1, p2); } } //====================================================================== void run() { mediaControl->Run(); state = runningState; } void stop() { mediaControl->Stop(); state = stoppedState; } void pause() { mediaControl->Pause(); state = pausedState; } //====================================================================== bool isInitialised() const noexcept { return state != uninitializedState; } bool isRunning() const noexcept { return state == runningState; } bool isPaused() const noexcept { return state == pausedState; } bool isStopped() const noexcept { return state == stoppedState; } bool containsVideo() const noexcept { return hasVideo; } int getVideoWidth() const noexcept { return (int) videoWidth; } int getVideoHeight() const noexcept { return (int) videoHeight; } //====================================================================== double getDuration() const { REFTIME duration; mediaPosition->get_Duration (&duration); return duration; } double getPosition() const { REFTIME seconds; mediaPosition->get_CurrentPosition (&seconds); return seconds; } //====================================================================== void setSpeed (const float newSpeed) { mediaPosition->put_Rate (newSpeed); } void setPosition (const double seconds) { mediaPosition->put_CurrentPosition (seconds); } void setVolume (const float newVolume) { basicAudio->put_Volume (convertToDShowVolume (newVolume)); } // in DirectShow, full volume is 0, silence is -10000 static long convertToDShowVolume (const float vol) noexcept { if (vol >= 1.0f) return 0; if (vol <= 0.0f) return -10000; return roundToInt ((vol * 10000.0f) - 10000.0f); } float getVolume() const { long volume; basicAudio->get_Volume (&volume); return (volume + 10000) / 10000.0f; } private: //====================================================================== enum { graphEventID = WM_APP + 0x43f0 }; DirectShowComponent& component; HWND hwnd; HDC hdc; enum State { uninitializedState, runningState, pausedState, stoppedState }; State state; bool hasVideo; long videoWidth, videoHeight; VideoRendererType type; ComSmartPtr graphBuilder; ComSmartPtr mediaControl; ComSmartPtr mediaPosition; ComSmartPtr mediaEvent; ComSmartPtr basicAudio; ComSmartPtr baseFilter; ScopedPointer videoRenderer; bool needToUpdateViewport, needToRecreateNativeWindow; //====================================================================== class NativeWindowClass : private DeletedAtShutdown { public: bool isRegistered() const noexcept { return atom != 0; } LPCTSTR getWindowClassName() const noexcept { return (LPCTSTR) MAKELONG (atom, 0); } juce_DeclareSingleton_SingleThreaded_Minimal (NativeWindowClass) private: NativeWindowClass() : atom (0) { String windowClassName ("JUCE_DIRECTSHOW_"); windowClassName << (int) (Time::currentTimeMillis() & 0x7fffffff); HINSTANCE moduleHandle = (HINSTANCE) Process::getCurrentModuleInstanceHandle(); TCHAR moduleFile [1024] = { 0 }; GetModuleFileName (moduleHandle, moduleFile, 1024); WNDCLASSEX wcex = { 0 }; wcex.cbSize = sizeof (wcex); wcex.style = CS_OWNDC; wcex.lpfnWndProc = (WNDPROC) wndProc; wcex.lpszClassName = windowClassName.toWideCharPointer(); wcex.hInstance = moduleHandle; atom = RegisterClassEx (&wcex); jassert (atom != 0); } ~NativeWindowClass() { if (atom != 0) UnregisterClass (getWindowClassName(), (HINSTANCE) Process::getCurrentModuleInstanceHandle()); clearSingletonInstance(); } static LRESULT CALLBACK wndProc (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { if (DirectShowContext* const c = (DirectShowContext*) GetWindowLongPtr (hwnd, GWLP_USERDATA)) { switch (msg) { case WM_NCHITTEST: return HTTRANSPARENT; case WM_ERASEBKGND: return 1; case WM_DISPLAYCHANGE: c->displayResolutionChanged(); break; case graphEventID: c->graphEventProc(); return 0; default: break; } } return DefWindowProc (hwnd, msg, wParam, lParam); } ATOM atom; JUCE_DECLARE_NON_COPYABLE (NativeWindowClass) }; //====================================================================== class NativeWindow { public: NativeWindow (HWND parentToAddTo, void* const userData) : hwnd (0), hdc (0) { NativeWindowClass* const wc = NativeWindowClass::getInstance(); if (wc->isRegistered()) { DWORD exstyle = 0; DWORD type = WS_CHILD; hwnd = CreateWindowEx (exstyle, wc->getWindowClassName(), L"", type, 0, 0, 0, 0, parentToAddTo, 0, (HINSTANCE) Process::getCurrentModuleInstanceHandle(), 0); if (hwnd != 0) { hdc = GetDC (hwnd); SetWindowLongPtr (hwnd, GWLP_USERDATA, (LONG_PTR) userData); } } jassert (hwnd != 0); } ~NativeWindow() { if (hwnd != 0) { SetWindowLongPtr (hwnd, GWLP_USERDATA, (LONG_PTR) 0); DestroyWindow (hwnd); } } HWND getHandle() const noexcept { return hwnd; } HDC getContext() const noexcept { return hdc; } void setWindowPosition (const Rectangle& newBounds) { SetWindowPos (hwnd, 0, newBounds.getX(), newBounds.getY(), newBounds.getWidth(), newBounds.getHeight(), SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOOWNERZORDER); } void showWindow (const bool shouldBeVisible) { ShowWindow (hwnd, shouldBeVisible ? SW_SHOWNA : SW_HIDE); } private: HWND hwnd; HDC hdc; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NativeWindow) }; ScopedPointer nativeWindow; //====================================================================== bool createNativeWindow() { jassert (nativeWindow == nullptr); if (ComponentPeer* const topLevelPeer = component.getTopLevelComponent()->getPeer()) { nativeWindow = new NativeWindow ((HWND) topLevelPeer->getNativeHandle(), this); hwnd = nativeWindow->getHandle(); if (hwnd != 0) { hdc = GetDC (hwnd); component.updateContextPosition(); component.showContext (component.isShowing()); return true; } else { nativeWindow = nullptr; } } else { jassertfalse; } return false; } void deleteNativeWindow() { jassert (nativeWindow != nullptr); ReleaseDC (hwnd, hdc); hwnd = 0; hdc = 0; nativeWindow = nullptr; } bool isRendererConnected() { ComSmartPtr enumPins; HRESULT hr = baseFilter->EnumPins (enumPins.resetAndGetPointerAddress()); if (SUCCEEDED (hr)) hr = enumPins->Reset(); ComSmartPtr pin; while (SUCCEEDED (hr) && enumPins->Next (1, pin.resetAndGetPointerAddress(), nullptr) == S_OK) { ComSmartPtr otherPin; hr = pin->ConnectedTo (otherPin.resetAndGetPointerAddress()); if (SUCCEEDED (hr)) { PIN_DIRECTION direction; hr = pin->QueryDirection (&direction); if (SUCCEEDED (hr) && direction == PINDIR_INPUT) return true; } else if (hr == VFW_E_NOT_CONNECTED) { hr = S_OK; } } return false; } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DirectShowContext) }; juce_ImplementSingleton_SingleThreaded (DirectShowComponent::DirectShowContext::NativeWindowClass) //====================================================================== class DirectShowComponent::DirectShowComponentWatcher : public ComponentMovementWatcher { public: DirectShowComponentWatcher (DirectShowComponent* const c) : ComponentMovementWatcher (c), owner (c) { } void componentMovedOrResized (bool /*wasMoved*/, bool /*wasResized*/) override { if (owner->videoLoaded) owner->updateContextPosition(); } void componentPeerChanged() override { if (owner->videoLoaded) owner->recreateNativeWindowAsync(); } void componentVisibilityChanged() override { if (owner->videoLoaded) owner->showContext (owner->isShowing()); } private: DirectShowComponent* const owner; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DirectShowComponentWatcher) }; //====================================================================== DirectShowComponent::DirectShowComponent (VideoRendererType type) : videoLoaded (false), looping (false) { setOpaque (true); context = new DirectShowContext (*this, type); componentWatcher = new DirectShowComponentWatcher (this); } DirectShowComponent::~DirectShowComponent() { componentWatcher = nullptr; } bool DirectShowComponent::isDirectShowAvailable() { static bool isDSAvailable = DirectShowHelpers::checkDShowAvailability(); return isDSAvailable; } void DirectShowComponent::recreateNativeWindowAsync() { context->recreateNativeWindowAsync(); repaint(); } void DirectShowComponent::updateContextPosition() { context->updateContextPosition(); if (getWidth() > 0 && getHeight() > 0) if (ComponentPeer* peer = getTopLevelComponent()->getPeer()) context->updateWindowPosition (peer->getAreaCoveredBy (*this)); } void DirectShowComponent::showContext (const bool shouldBeVisible) { context->showWindow (shouldBeVisible); } void DirectShowComponent::paint (Graphics& g) { if (videoLoaded) context->handleUpdateNowIfNeeded(); else g.fillAll (Colours::grey); } //====================================================================== bool DirectShowComponent::loadMovie (const String& fileOrURLPath) { closeMovie(); videoLoaded = context->loadFile (fileOrURLPath); if (videoLoaded) { videoPath = fileOrURLPath; context->updateVideoPosition(); } return videoLoaded; } bool DirectShowComponent::loadMovie (const File& videoFile) { return loadMovie (videoFile.getFullPathName()); } bool DirectShowComponent::loadMovie (const URL& videoURL) { return loadMovie (videoURL.toString (false)); } void DirectShowComponent::closeMovie() { if (videoLoaded) context->release(); videoLoaded = false; videoPath.clear(); } //====================================================================== File DirectShowComponent::getCurrentMoviePath() const { return videoPath; } bool DirectShowComponent::isMovieOpen() const { return videoLoaded; } double DirectShowComponent::getMovieDuration() const { return videoLoaded ? context->getDuration() : 0.0; } void DirectShowComponent::setLooping (const bool shouldLoop) { looping = shouldLoop; } bool DirectShowComponent::isLooping() const { return looping; } void DirectShowComponent::getMovieNormalSize (int &width, int &height) const { width = context->getVideoWidth(); height = context->getVideoHeight(); } //====================================================================== void DirectShowComponent::setBoundsWithCorrectAspectRatio (const Rectangle& spaceToFitWithin, RectanglePlacement placement) { int normalWidth, normalHeight; getMovieNormalSize (normalWidth, normalHeight); const Rectangle normalSize (0, 0, normalWidth, normalHeight); if (! (spaceToFitWithin.isEmpty() || normalSize.isEmpty())) setBounds (placement.appliedTo (normalSize, spaceToFitWithin)); else setBounds (spaceToFitWithin); } //====================================================================== void DirectShowComponent::play() { if (videoLoaded) context->run(); } void DirectShowComponent::stop() { if (videoLoaded) context->pause(); } bool DirectShowComponent::isPlaying() const { return context->isRunning(); } void DirectShowComponent::goToStart() { setPosition (0.0); } void DirectShowComponent::setPosition (const double seconds) { if (videoLoaded) context->setPosition (seconds); } double DirectShowComponent::getPosition() const { return videoLoaded ? context->getPosition() : 0.0; } void DirectShowComponent::setSpeed (const float newSpeed) { if (videoLoaded) context->setSpeed (newSpeed); } void DirectShowComponent::setMovieVolume (const float newVolume) { if (videoLoaded) context->setVolume (newVolume); } float DirectShowComponent::getMovieVolume() const { return videoLoaded ? context->getVolume() : 0.0f; }