/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ //============================================================================== struct VideoComponent::Pimpl : public Component, private ComponentPeer::ScaleFactorListener { Pimpl (VideoComponent& ownerToUse, bool) : owner (ownerToUse) { setOpaque (true); context.reset (new MediaFoundationContext (*this)); componentWatcher.reset (new ComponentWatcher (*this)); } ~Pimpl() override { videoLoaded = false; context = nullptr; componentWatcher = nullptr; if (currentPeer != nullptr) currentPeer->removeScaleFactorListener (this); } Result loadFromString (const String& fileOrURLPath) { auto r = context->load (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::removeEscapeChars (url.toString (true))); if (r.wasOk()) currentURL = url; return r; } void close() { videoLoaded = false; currentFile = File(); currentURL = {}; context->close(); } //============================================================================== void play() { context->play(); } void stop() { context->stop(); } //============================================================================== bool isOpen() const { return videoLoaded; } bool isPlaying() const { return context->state == MediaFoundationContext::runningState; } //============================================================================== double getDuration() const { return context->getDuration(); } Rectangle getNativeSize() const { return context->getVideoSize(); } //============================================================================== double getSpeed() const { return context->getSpeed(); } float getVolume() const { return context->getVolume(); } double getPosition() const { return context->getPosition(); } void setSpeed (double newSpeed) { context->setSpeed (newSpeed); } void setVolume (float newVolume) { context->setVolume (newVolume); } void setPosition (double newPosition) { context->setPosition (newPosition); } //============================================================================== void paint (Graphics& g) override { if (videoLoaded) context->handleUpdateNowIfNeeded(); else g.fillAll (Colours::black); } //============================================================================== void updateContextPosition() { context->updateContextPosition(); if (getWidth() > 0 && getHeight() > 0) if (auto* peer = getTopLevelComponent()->getPeer()) context->updateWindowPosition ((peer->getAreaCoveredBy (*this).toDouble() * peer->getPlatformScaleFactor()).toNearestInt()); } void updateContextVisibility() { context->showWindow (isShowing()); } void recreateNativeWindowAsync() { context->recreateNativeWindowAsync(); repaint(); } void playbackStarted() { NullCheckedInvocation::invoke (owner.onPlaybackStarted); } void playbackStopped() { NullCheckedInvocation::invoke (owner.onPlaybackStopped); } void errorOccurred (const String& errorMessage) { NullCheckedInvocation::invoke (owner.onErrorOccurred, errorMessage); } File currentFile; URL currentURL; private: VideoComponent& owner; ComponentPeer* currentPeer = nullptr; bool videoLoaded = false; //============================================================================== void nativeScaleFactorChanged (double /*newScaleFactor*/) override { if (videoLoaded) updateContextPosition(); } //============================================================================== struct ComponentWatcher : public ComponentMovementWatcher { ComponentWatcher (Pimpl& c) : ComponentMovementWatcher (&c), owner (c) { } using ComponentMovementWatcher::componentMovedOrResized; void componentMovedOrResized (bool, bool) override { if (owner.videoLoaded) owner.updateContextPosition(); } void componentPeerChanged() override { if (owner.currentPeer != nullptr) owner.currentPeer->removeScaleFactorListener (&owner); if (owner.videoLoaded) owner.recreateNativeWindowAsync(); } using ComponentMovementWatcher::componentVisibilityChanged; void componentVisibilityChanged() override { if (owner.videoLoaded) owner.updateContextVisibility(); } Pimpl& owner; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ComponentWatcher) }; std::unique_ptr componentWatcher; //============================================================================== struct MediaFoundationContext : public AsyncUpdater { MediaFoundationContext (Pimpl& c) : component (c) { HRESULT hr = CoInitializeEx (nullptr, COINIT_APARTMENTTHREADED); if (SUCCEEDED (hr)) hr = MFStartup (MF_VERSION); if (SUCCEEDED (hr)) hr = VideoRenderers::EVR::CreateInstance (&videoRenderer); if (SUCCEEDED (hr)) state = readyState; jassert (SUCCEEDED (hr)); } ~MediaFoundationContext() override { videoRenderer->Shutdown(); videoRenderer->Release(); nativeWindow = nullptr; MFShutdown(); CoUninitialize(); } //============================================================================== void updateWindowPosition (const Rectangle& newBounds) { nativeWindow->setWindowPosition (newBounds); } void showWindow (bool shouldBeVisible) { nativeWindow->showWindow (shouldBeVisible); } //============================================================================== void repaint() { videoRenderer->Repaint(); } void updateVideoPosition() { videoRenderer->Resize(); } //============================================================================== void peerChanged() { component.close(); nativeWindow = nullptr; } void handleAsyncUpdate() override { if (hwnd != nullptr) { if (needToRecreateNativeWindow) { peerChanged(); needToRecreateNativeWindow = false; } if (needToUpdateViewport) { updateVideoPosition(); needToUpdateViewport = false; } repaint(); } else { triggerAsyncUpdate(); } } void recreateNativeWindowAsync() { needToRecreateNativeWindow = true; triggerAsyncUpdate(); } void updateContextPosition() { needToUpdateViewport = true; triggerAsyncUpdate(); } //============================================================================== Result load (const String& fileOrURLPath) { if (state == uninitializedState) return Result::fail ("Platform not initialized"); component.close(); HRESULT hr = createNativeWindow(); if (SUCCEEDED (hr)) hr = videoRenderer->OpenURL (hwnd, hwnd, fileOrURLPath.toWideCharPointer()); if (SUCCEEDED (hr)) { hasVideo = true; state = stoppedState; return Result::ok(); } videoRenderer->Shutdown(); return getErrorMessageFromResult (hr); } static Result getErrorMessageFromResult (HRESULT hr) { TCHAR messageBuffer[512] = { 0 }; FormatMessage (FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, (DWORD) hr, MAKELANGID (LANG_NEUTRAL, SUBLANG_DEFAULT), messageBuffer, (DWORD) numElementsInArray (messageBuffer) - 1, nullptr); return Result::fail (String (messageBuffer)); } void handleEvent (WPARAM wParam) { HRESULT hr = videoRenderer->HandleEvent (wParam); jassert (SUCCEEDED (hr)); } //============================================================================== void play() { if (hasVideo) { videoRenderer->Play(); state = runningState; } } void stop() { if (hasVideo) { videoRenderer->Stop(); state = stoppedState; } } void pause() { if (hasVideo) { videoRenderer->Pause(); state = pausedState; } } void close() { hasVideo = false; videoRenderer->Shutdown(); state = readyState; } //============================================================================== Rectangle getVideoSize() const { return videoRenderer->GetVideoSize(); } double getDuration() const { return videoRenderer->GetDuration(); } //============================================================================== double getSpeed() const { return videoRenderer->GetRate(); } double getPosition() const { return videoRenderer->GetPosition(); } float getVolume() const { return videoRenderer->GetVolume(); } void setSpeed (double newSpeed) { videoRenderer->SetRate (newSpeed); } void setPosition (double seconds) { videoRenderer->SetPosition (seconds); } void setVolume (float newVolume) { videoRenderer->SetVolume (newVolume); } enum State { uninitializedState, readyState, runningState, pausedState, stoppedState }; State state = uninitializedState; private: //============================================================================== enum { EVR_MSG_ID = VideoRenderers::MSG_ID }; Pimpl& component; HWND hwnd = {}; HDC hdc = {}; VideoRenderers::EVR* videoRenderer = NULL; bool hasVideo = false, needToUpdateViewport = true, needToRecreateNativeWindow = false; //============================================================================== HRESULT createNativeWindow() { if (auto* topLevelPeer = component.getTopLevelComponent()->getPeer()) { nativeWindow.reset (new NativeWindow ((HWND) topLevelPeer->getNativeHandle(), this)); hwnd = nativeWindow->hwnd; component.currentPeer = topLevelPeer; component.currentPeer->addScaleFactorListener (&component); if (hwnd != nullptr) { hdc = GetDC (hwnd); component.updateContextPosition(); component.updateContextVisibility(); return S_OK; } nativeWindow = nullptr; } return E_HANDLE; } //============================================================================== 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_MF_EVR_"); windowClassName << (int) (Time::currentTimeMillis() & 0x7fffffff); HINSTANCE moduleHandle = (HINSTANCE) Process::getCurrentModuleInstanceHandle(); TCHAR moduleFile [1024] = {}; GetModuleFileName (moduleHandle, moduleFile, 1024); WNDCLASSEX wcex = {}; 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 = (MediaFoundationContext*) GetWindowLongPtr (hwnd, GWLP_USERDATA)) { switch (msg) { case WM_NCHITTEST: return HTTRANSPARENT; case WM_ERASEBKGND: return 1; case EVR_MSG_ID: c->handleEvent (wParam); 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, nullptr, (HINSTANCE) Process::getCurrentModuleInstanceHandle(), nullptr); if (hwnd != nullptr) { hdc = GetDC (hwnd); SetWindowLongPtr (hwnd, GWLP_USERDATA, (LONG_PTR) userData); } } jassert (hwnd != nullptr); } ~NativeWindow() { if (hwnd != nullptr) { SetWindowLongPtr (hwnd, GWLP_USERDATA, (LONG_PTR) 0); ReleaseDC (hwnd, hdc); DestroyWindow (hwnd); } } void setWindowPosition (Rectangle newBounds) { SetWindowPos (hwnd, nullptr, 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 (MediaFoundationContext) }; std::unique_ptr context; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl) }; JUCE_IMPLEMENT_SINGLETON (VideoComponent::Pimpl::MediaFoundationContext::NativeWindowClass)