/* ============================================================================== 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 VideoRenderers { //====================================================================== struct Base { virtual ~Base() {} virtual HRESULT create (ComSmartPtr&, ComSmartPtr&, HWND) = 0; virtual void setVideoWindow (HWND) = 0; virtual void setVideoPosition (HWND) = 0; virtual void repaintVideo (HWND, HDC) = 0; virtual void displayModeChanged() = 0; virtual HRESULT getVideoSize (long& videoWidth, long& videoHeight) = 0; }; //====================================================================== struct VMR7 : public Base { VMR7() {} HRESULT create (ComSmartPtr& graphBuilder, ComSmartPtr& baseFilter, HWND hwnd) override { 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) override { windowlessControl->SetVideoClippingWindow (hwnd); } void setVideoPosition (HWND hwnd) override { long videoWidth = 0, videoHeight = 0; windowlessControl->GetNativeVideoSize (&videoWidth, &videoHeight, nullptr, nullptr); RECT src, dest; SetRect (&src, 0, 0, videoWidth, videoHeight); GetClientRect (hwnd, &dest); windowlessControl->SetVideoPosition (&src, &dest); } void repaintVideo (HWND hwnd, HDC hdc) override { windowlessControl->RepaintVideo (hwnd, hdc); } void displayModeChanged() override { windowlessControl->DisplayModeChanged(); } HRESULT getVideoSize (long& videoWidth, long& videoHeight) override { return windowlessControl->GetNativeVideoSize (&videoWidth, &videoHeight, nullptr, nullptr); } ComSmartPtr windowlessControl; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VMR7) }; //====================================================================== struct EVR : public Base { EVR() {} HRESULT create (ComSmartPtr& graphBuilder, ComSmartPtr& baseFilter, HWND hwnd) override { 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, (void**) videoDisplayControl.resetAndGetPointerAddress()); if (SUCCEEDED (hr)) hr = videoDisplayControl->SetVideoWindow (hwnd); if (SUCCEEDED (hr)) hr = videoDisplayControl->SetAspectRatioMode (MFVideoARMode_PreservePicture); return hr; } void setVideoWindow (HWND hwnd) override { videoDisplayControl->SetVideoWindow (hwnd); } void setVideoPosition (HWND hwnd) override { const MFVideoNormalizedRect src = { 0.0f, 0.0f, 1.0f, 1.0f }; RECT dest; GetClientRect (hwnd, &dest); videoDisplayControl->SetVideoPosition (&src, &dest); } void repaintVideo (HWND, HDC) override { videoDisplayControl->RepaintVideo(); } void displayModeChanged() override {} HRESULT getVideoSize (long& videoWidth, long& videoHeight) override { SIZE sz = { 0, 0 }; HRESULT hr = videoDisplayControl->GetNativeVideoSize (&sz, nullptr); videoWidth = sz.cx; videoHeight = sz.cy; return hr; } ComSmartPtr videoDisplayControl; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (EVR) }; }; //============================================================================== struct VideoComponent::Pimpl : public Component { Pimpl() : videoLoaded (false) { setOpaque (true); context.reset (new DirectShowContext (*this)); componentWatcher.reset (new ComponentWatcher (*this)); } ~Pimpl() { close(); context = nullptr; componentWatcher = nullptr; } Result loadFromString (const String& fileOrURLPath) { close(); auto r = context->loadFile (fileOrURLPath); if (r.wasOk()) { videoLoaded = true; context->updateVideoPosition(); } return r; } Result load (const File& file) { auto r = loadFromString (file.getFullPathName()); if (r.wasOk()) currentFile = file; return r; } Result load (const URL& url) { auto r = loadFromString (url.toString (true)); if (r.wasOk()) currentURL = url; return r; } void close() { stop(); context->release(); videoLoaded = false; currentFile = File(); currentURL = {}; } bool isOpen() const { return videoLoaded; } bool isPlaying() const { return context->state == DirectShowContext::runningState; } void play() { if (videoLoaded) context->play(); } void stop() { if (videoLoaded) context->pause(); } void setPosition (double newPosition) { if (videoLoaded) context->setPosition (newPosition); } double getPosition() const { return videoLoaded ? context->getPosition() : 0.0; } void setSpeed (double newSpeed) { if (videoLoaded) context->setSpeed (newSpeed); } Rectangle getNativeSize() const { return videoLoaded ? context->getVideoSize() : Rectangle(); } double getDuration() const { return videoLoaded ? context->getDuration() : 0.0; } void setVolume (float newVolume) { if (videoLoaded) context->setVolume (newVolume); } float getVolume() const { return videoLoaded ? context->getVolume() : 0.0f; } void paint (Graphics& g) override { if (videoLoaded) context->handleUpdateNowIfNeeded(); else g.fillAll (Colours::grey); } void updateContextPosition() { context->updateContextPosition(); if (getWidth() > 0 && getHeight() > 0) if (auto* peer = getTopLevelComponent()->getPeer()) context->updateWindowPosition (peer->getAreaCoveredBy (*this)); } void updateContextVisibility() { context->showWindow (isShowing()); } void recreateNativeWindowAsync() { context->recreateNativeWindowAsync(); repaint(); } File currentFile; URL currentURL; private: bool videoLoaded; //============================================================================== struct ComponentWatcher : public ComponentMovementWatcher { ComponentWatcher (Pimpl& c) : ComponentMovementWatcher (&c), owner (c) { } void componentMovedOrResized (bool, bool) override { if (owner.videoLoaded) owner.updateContextPosition(); } void componentPeerChanged() override { if (owner.videoLoaded) owner.recreateNativeWindowAsync(); } void componentVisibilityChanged() override { if (owner.videoLoaded) owner.updateContextVisibility(); } Pimpl& owner; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ComponentWatcher) }; std::unique_ptr componentWatcher; //====================================================================== struct DirectShowContext : public AsyncUpdater { DirectShowContext (Pimpl& c) : component (c) { CoInitialize (0); } ~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->hwnd, nativeWindow->hdc); } void updateVideoPosition() { if (hasVideo) videoRenderer->setVideoPosition (nativeWindow->hwnd); } 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(); } //====================================================================== Result loadFile (const String& fileOrURLPath) { jassert (state == uninitializedState); if (! createNativeWindow()) return Result::fail ("Can't create window"); 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 (SystemStats::getOperatingSystemType() >= SystemStats::WinVista) { videoRenderer.reset (new VideoRenderers::EVR()); hr = videoRenderer->create (graphBuilder, baseFilter, hwnd); if (FAILED (hr)) videoRenderer = nullptr; } if (videoRenderer == nullptr) { videoRenderer.reset (new VideoRenderers::VMR7()); hr = videoRenderer->create (graphBuilder, baseFilter, hwnd); } } // build filter graph if (SUCCEEDED (hr)) { hr = graphBuilder->RenderFile (fileOrURLPath.toWideCharPointer(), nullptr); if (FAILED (hr)) { #if JUCE_MODAL_LOOPS_PERMITTED // 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); #endif } } // remove video renderer if not connected (no video) if (SUCCEEDED (hr)) { if (isRendererConnected()) { hasVideo = true; } 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 Result::ok(); } // 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 getErrorMessageFromResult (hr); } static Result getErrorMessageFromResult (HRESULT hr) { switch (hr) { case VFW_E_INVALID_FILE_FORMAT: return Result::fail ("Invalid file format"); case VFW_E_NOT_FOUND: return Result::fail ("File not found"); case VFW_E_UNKNOWN_FILE_TYPE: return Result::fail ("Unknown file type"); case VFW_E_UNSUPPORTED_STREAM: return Result::fail ("Unsupported stream"); case VFW_E_CANNOT_CONNECT: return Result::fail ("Cannot connect"); case VFW_E_CANNOT_LOAD_SOURCE_FILTER: return Result::fail ("Cannot load source filter"); } TCHAR messageBuffer[512] = { 0 }; FormatMessage (FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, hr, MAKELANGID (LANG_NEUTRAL, SUBLANG_DEFAULT), messageBuffer, (DWORD) numElementsInArray (messageBuffer) - 1, nullptr); return Result::fail (String (messageBuffer)); } 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; if (nativeWindow != nullptr) deleteNativeWindow(); } void graphEventProc() { LONG ec = 0; LONG_PTR p1 = {}, p2 = {}; jassert (mediaEvent != nullptr); while (SUCCEEDED (mediaEvent->GetEvent (&ec, &p1, &p2, 0))) { mediaEvent->FreeEventParams (ec, p1, p2); switch (ec) { case EC_REPAINT: component.repaint(); break; case EC_COMPLETE: component.stop(); break; case EC_USERABORT: case EC_ERRORABORT: case EC_ERRORABORTEX: component.close(); break; default: break; } } } //====================================================================== void play() { mediaControl->Run(); state = runningState; } void stop() { mediaControl->Stop(); state = stoppedState; } void pause() { mediaControl->Pause(); state = pausedState; } //====================================================================== Rectangle getVideoSize() const noexcept { long width = 0, height = 0; if (hasVideo) videoRenderer->getVideoSize (width, height); return { (int) width, (int) height }; } //====================================================================== double getDuration() const { REFTIME duration; mediaPosition->get_Duration (&duration); return duration; } double getPosition() const { REFTIME seconds; mediaPosition->get_CurrentPosition (&seconds); return seconds; } void setSpeed (double newSpeed) { mediaPosition->put_Rate (newSpeed); } void setPosition (double seconds) { mediaPosition->put_CurrentPosition (seconds); } void setVolume (float newVolume) { basicAudio->put_Volume (convertToDShowVolume (newVolume)); } // in DirectShow, full volume is 0, silence is -10000 static long convertToDShowVolume (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; } enum State { uninitializedState, runningState, pausedState, stoppedState }; State state = uninitializedState; private: //====================================================================== enum { graphEventID = WM_APP + 0x43f0 }; Pimpl& component; HWND hwnd = {}; HDC hdc = {}; ComSmartPtr graphBuilder; ComSmartPtr mediaControl; ComSmartPtr mediaPosition; ComSmartPtr mediaEvent; ComSmartPtr basicAudio; ComSmartPtr baseFilter; std::unique_ptr videoRenderer; bool hasVideo = false, needToUpdateViewport = true, needToRecreateNativeWindow = false; //====================================================================== bool createNativeWindow() { jassert (nativeWindow == nullptr); if (auto* topLevelPeer = component.getTopLevelComponent()->getPeer()) { nativeWindow.reset (new NativeWindow ((HWND) topLevelPeer->getNativeHandle(), this)); hwnd = nativeWindow->hwnd; if (hwnd != 0) { hdc = GetDC (hwnd); component.updateContextPosition(); component.updateContextVisibility(); return true; } nativeWindow = nullptr; } else { jassertfalse; } return false; } void deleteNativeWindow() { jassert (nativeWindow != nullptr); ReleaseDC (hwnd, hdc); hwnd = {}; hdc = {}; 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; } //====================================================================== struct NativeWindowClass : private DeletedAtShutdown { bool isRegistered() const noexcept { return atom != 0; } LPCTSTR getWindowClassName() const noexcept { return (LPCTSTR) (pointer_sized_uint) MAKELONG (atom, 0); } JUCE_DECLARE_SINGLETON_SINGLETHREADED_MINIMAL (NativeWindowClass) private: NativeWindowClass() { 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 (auto* 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) }; //====================================================================== struct NativeWindow { NativeWindow (HWND parentToAddTo, void* userData) { auto* 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); } } void setWindowPosition (Rectangle newBounds) { SetWindowPos (hwnd, 0, newBounds.getX(), newBounds.getY(), newBounds.getWidth(), newBounds.getHeight(), SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOOWNERZORDER); } void showWindow (bool shouldBeVisible) { ShowWindow (hwnd, shouldBeVisible ? SW_SHOWNA : SW_HIDE); } HWND hwnd = {}; HDC hdc = {}; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NativeWindow) }; std::unique_ptr nativeWindow; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DirectShowContext) }; std::unique_ptr context; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl) }; JUCE_IMPLEMENT_SINGLETON (VideoComponent::Pimpl::DirectShowContext::NativeWindowClass)