diff --git a/include/cinder/qtime/QuickTime.h b/include/cinder/qtime/QuickTime.h index f080b213ff..d63ea9217c 100644 --- a/include/cinder/qtime/QuickTime.h +++ b/include/cinder/qtime/QuickTime.h @@ -1,16 +1,16 @@ /* Copyright (c) 2014, The Cinder Project, All rights reserved. - + This code is intended for use with the Cinder C++ library: http://libcinder.org - + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR @@ -30,6 +30,8 @@ #include "cinder/qtime/QuickTimeImplLegacy.h" #elif ( defined( CINDER_MAC ) && ( MAC_OS_X_VERSION_MIN_REQUIRED >= 1080 ) ) || defined( CINDER_COCOA_TOUCH ) #include "cinder/qtime/QuickTimeImplAvf.h" +#elif defined( CINDER_MSW ) + #include "cinder/qtime/QuickTimeImplMsw.h" #else - // QuickTime is not supported on 10.7 64-bit or Windows 64-bit + // QuickTime is not supported on 10.7 64-bit #endif diff --git a/include/cinder/qtime/QuickTimeGl.h b/include/cinder/qtime/QuickTimeGl.h index cc5ec735fc..19fa70eb8b 100644 --- a/include/cinder/qtime/QuickTimeGl.h +++ b/include/cinder/qtime/QuickTimeGl.h @@ -1,16 +1,16 @@ /* Copyright (c) 2014, The Cinder Project, All rights reserved. - + This code is intended for use with the Cinder C++ library: http://libcinder.org - + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR @@ -31,12 +31,15 @@ #include "cinder/qtime/QuickTimeGlImplLegacy.h" #elif ( ! defined( CINDER_MAC_USE_GSTREAMER ) && defined( CINDER_MAC ) && ( MAC_OS_X_VERSION_MIN_REQUIRED >= 1080 ) ) || defined( CINDER_COCOA_TOUCH ) #include "cinder/qtime/QuickTimeGlImplAvf.h" +#elif defined( CINDER_MSW ) && defined( _WIN64 ) && !defined( CINDER_MSW_USE_GSTREAMER ) + // Windows 64-bit uses Media Foundation via IMFMediaEngine with D3D11/GL interop + #include "cinder/qtime/QuickTimeGlImplMsw.h" #elif defined( CINDER_MAC_USE_GSTREAMER ) || defined( CINDER_MSW_USE_GSTREAMER ) - #include "cinder/linux/Movie.h" + #include "cinder/linux/Movie.h" #elif defined( CINDER_ANDROID ) #include "cinder/android/video/MovieGl.h" -#elif defined( CINDER_LINUX ) - #include "cinder/linux/Movie.h" +#elif defined( CINDER_LINUX ) + #include "cinder/linux/Movie.h" #else - // QuickTime is not supported on 10.7 64-bit or Windows 64-bit + // QuickTime is not supported on 10.7 64-bit #endif diff --git a/include/cinder/qtime/QuickTimeGlImplMsw.h b/include/cinder/qtime/QuickTimeGlImplMsw.h new file mode 100644 index 0000000000..97f4d42b35 --- /dev/null +++ b/include/cinder/qtime/QuickTimeGlImplMsw.h @@ -0,0 +1,82 @@ +/* + Copyright (c) 2025, The Cinder Project, All rights reserved. + + This code is intended for use with the Cinder C++ library: http://libcinder.org + + Redistribution and use in source and binary forms, with or without modification, are permitted provided that + the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and + the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and + the following disclaimer in the documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + Windows 64-bit Media Foundation GL implementation. + Based on AX-MediaPlayer by Andrew Wright (@axjxwright). + + Hardware acceleration uses WGL_NV_DX_interop for D3D11/OpenGL texture sharing. + Falls back to WIC/CPU path if hardware acceleration is unavailable. + */ + +// This file is only meant to be included by QuickTimeGl.h +#pragma once + +#include "cinder/Cinder.h" +#include "cinder/qtime/QuickTimeImplMsw.h" +#include "cinder/gl/Texture.h" + +namespace cinder { namespace qtime { + +typedef std::shared_ptr MovieGlRef; + +/** \brief Media Foundation movie playback as OpenGL textures (Windows 64-bit) + * + * Uses IMFMediaEngine with optional hardware-accelerated D3D11/GL interop. + * When hardware acceleration is available, textures are shared directly + * between D3D11 and OpenGL via WGL_NV_DX_interop extension. + * + * \remarks The destination OpenGL context must be current when MovieGl is constructed. + * A call to app::restoreWindowContext() can be used to force this. + **/ +class MovieGl : public MovieBase { + public: + virtual ~MovieGl(); + + static MovieGlRef create( const Url& url ) { return MovieGlRef( new MovieGl( url ) ); } + static MovieGlRef create( const fs::path& path ) { return MovieGlRef( new MovieGl( path ) ); } + static MovieGlRef create( const MovieLoaderRef &loader ) { return MovieGlRef( new MovieGl( *loader ) ); } + + //! Returns the gl::Texture representing the Movie's current frame + //! \note Texture is valid until the next call to getTexture() or until the MovieGl is destroyed + gl::TextureRef getTexture(); + + //! Returns whether hardware acceleration (D3D11/GL interop) is being used + bool isHardwareAccelerated() const { return mHardwareAccelerated; } + + protected: + MovieGl( const Url& url ); + MovieGl( const fs::path& path ); + MovieGl( const MovieLoader& loader ); + + void initGl( bool hardwareAccelerated = true ); + void initFromUrl( const Url& url ); + void initFromPath( const fs::path& filePath ); + + gl::TextureRef mTexture; + bool mHardwareAccelerated; + + //! Opaque holder for the current frame lease - keeps texture locked during draw + struct FrameLeaseHolder; + std::unique_ptr mFrameLeaseHolder; +}; + +} } // namespace cinder::qtime diff --git a/include/cinder/qtime/QuickTimeImplMsw.h b/include/cinder/qtime/QuickTimeImplMsw.h new file mode 100644 index 0000000000..8186da252a --- /dev/null +++ b/include/cinder/qtime/QuickTimeImplMsw.h @@ -0,0 +1,266 @@ +/* + Copyright (c) 2025, The Cinder Project, All rights reserved. + + This code is intended for use with the Cinder C++ library: http://libcinder.org + + Redistribution and use in source and binary forms, with or without modification, are permitted provided that + the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and + the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and + the following disclaimer in the documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + Windows 64-bit Media Foundation implementation. + Based on AX-MediaPlayer by Andrew Wright (@axjxwright). + + Requirements: + - Windows 10+ + - OpenGL driver with WGL_NV_DX_interop support (hardware acceleration) + - Falls back to WIC/CPU path if hardware acceleration unavailable + */ + +// This file is only meant to be included by QuickTime.h +#pragma once + +#include "cinder/gl/Texture.h" +#include "cinder/DataSource.h" +#include "cinder/Exception.h" +#include "cinder/Signals.h" +#include "cinder/Surface.h" +#include "cinder/Thread.h" +#include "cinder/Url.h" + +#include +#include +#include +#include + +namespace cinder { namespace qtime { + +class MovieLoader; +typedef std::shared_ptr MovieLoaderRef; + +//! \class MovieBase +//! Base class for Media Foundation-based video playback on Windows 64-bit +class MovieBase { + public: + virtual ~MovieBase(); + + //! Returns the width of the movie in pixels + int32_t getWidth() const { return mWidth; } + //! Returns the height of the movie in pixels + int32_t getHeight() const { return mHeight; } + //! Returns the size of the movie in pixels + ivec2 getSize() const { return ivec2( getWidth(), getHeight() ); } + //! Returns the movie's aspect ratio, the ratio of its width to its height + float getAspectRatio() const { return mWidth > 0 ? static_cast(mWidth) / static_cast(mHeight) : 1.0f; } + //! the Area defining the Movie's bounds in pixels: [0,0]-[width,height] + Area getBounds() const { return Area( 0, 0, getWidth(), getHeight() ); } + + //! Returns the movie's pixel aspect ratio. Returns 1.0 if the movie does not contain an explicit pixel aspect ratio. + float getPixelAspectRatio() const { return 1.0f; } + + //! Returns whether the movie has loaded and buffered enough to playback without interruption + bool checkPlaythroughOk(); + //! Returns whether the movie is in a loaded state, implying its structures are ready for reading but it may not be ready for playback + bool isLoaded() const { return mLoaded; } + //! Returns whether the movie is playable, implying the movie is fully formed and can be played but media data is still downloading + bool isPlayable() const { return mLoaded; } + //! Returns true if the content represented by the movie is protected by DRM + bool isProtected() const { return false; } + //! Returns the movie's length measured in seconds + float getDuration() const { return mDuration; } + //! Returns the movie's framerate measured as frames per second + float getFramerate() const { return mFrameRate; } + //! Returns the total number of frames (video samples) in the movie + int32_t getNumFrames(); + + //! Returns whether a movie contains at least one visual track, defined as Video, MPEG, Sprite, QuickDraw3D, Text, or TimeCode tracks + bool hasVisuals() const { return mHasVideo; } + //! Returns whether a movie contains at least one audio track, defined as Sound, Music, or MPEG tracks + bool hasAudio() const { return mHasAudio; } + //! Returns whether the first video track in the movie contains an alpha channel. Returns false in the absence of visual media. + virtual bool hasAlpha() const { return false; } + + //! Returns whether a movie has a new frame available + bool checkNewFrame(); + + //! Returns the current time of a movie in seconds + float getCurrentTime() const; + //! Sets the movie to the time \a seconds + void seekToTime( float seconds ); + //! Sets the movie time to the start time of frame \a frame + void seekToFrame( int frame ); + //! Sets the movie time to its beginning + void seekToStart(); + //! Sets the movie time to its end + void seekToEnd(); + //! Limits the active portion of a movie to a subset beginning at \a startTime seconds and lasting for \a duration seconds. + //! \note Not supported on Windows Media Foundation. Logs a warning. + void setActiveSegment( float startTime, float duration ); + //! Resets the active segment to be the entire movie + void resetActiveSegment(); + + //! Sets whether the movie is set to loop during playback. If \a palindrome is true, the movie will "ping-pong" back and forth + //! \note Palindrome mode is not supported on Windows Media Foundation. Will loop normally instead. + void setLoop( bool loop = true, bool palindrome = false ); + //! Advances the movie by one frame (a single video sample). Ignores looping settings. + bool stepForward(); + //! Steps backward by one frame (a single video sample). Ignores looping settings. + bool stepBackward(); + /** Sets the playback rate, which begins playback immediately for nonzero values. + * 1.0 represents normal speed. Negative values indicate reverse playback and \c 0 stops. + * + * Returns a boolean value indicating whether the rate value can be played (some media types cannot be played backwards) + */ + bool setRate( float rate ); + + //! Sets the audio playback volume ranging from [0 - 1.0] + void setVolume( float volume ); + //! Gets the audio playback volume ranging from [0 - 1.0] + float getVolume() const; + //! Returns whether the movie is currently playing or is paused/stopped. + bool isPlaying() const; + //! Returns whether the movie has completely finished playing + bool isDone() const; + //! Begins movie playback. + void play( bool toggle = false ); + //! Stops playback + void stop(); + + signals::Signal& getNewFrameSignal() { return mSignalNewFrame; } + signals::Signal& getReadySignal() { return mSignalReady; } + signals::Signal& getCancelledSignal() { return mSignalCancelled; } + signals::Signal& getEndedSignal() { return mSignalEnded; } + signals::Signal& getJumpedSignal() { return mSignalJumped; } + signals::Signal& getOutputWasFlushedSignal() { return mSignalOutputWasFlushed; } + + protected: + MovieBase(); + void init(); + void initFromUrl( const Url& url ); + void initFromPath( const fs::path& filePath ); + void initFromLoader( const MovieLoader& loader ); + + void updateFrame(); + void connectSignals(); + + int32_t mWidth, mHeight; + int32_t mFrameCount; + float mFrameRate; + float mDuration; + std::atomic mLoaded; + bool mPlayThroughOk; + bool mPlayingForward, mLoop, mPalindrome; + bool mHasAudio, mHasVideo; + bool mPlaying; + + mutable std::mutex mMutex; + + signals::Signal mSignalNewFrame, mSignalReady, mSignalCancelled, mSignalEnded, mSignalJumped, mSignalOutputWasFlushed; + + // Internal implementation (Media Foundation based) + class Impl; + std::shared_ptr mImpl; + signals::Connection mUpdateConnection; + + friend class MovieLoader; +}; + +typedef std::shared_ptr MovieSurfaceRef; +//! \class MovieSurface +//! Media Foundation movie playback with CPU-based Surface output +class MovieSurface : public MovieBase { + public: + virtual ~MovieSurface(); + + static MovieSurfaceRef create( const ci::Url& url ) { return MovieSurfaceRef( new MovieSurface( url ) ); } + static MovieSurfaceRef create( const fs::path& path ) { return MovieSurfaceRef( new MovieSurface( path ) ); } + static MovieSurfaceRef create( const MovieLoaderRef &loader ); + + //! Returns the Surface8u representing the Movie's current frame + Surface8uRef getSurface(); + + protected: + MovieSurface() : MovieBase() {} + MovieSurface( const Url& url ); + MovieSurface( const fs::path& path ); + MovieSurface( const MovieLoader& loader ); + + Surface8uRef mSurface; +}; + + +//! \class MovieLoader +//! Async movie loading utility for Media Foundation +class MovieLoader { + public: + MovieLoader() {} + MovieLoader( const Url &url ); + ~MovieLoader(); + + static MovieLoaderRef create( const Url &url ) { return std::shared_ptr( new MovieLoader( url ) ); } + + //! Returns whether the movie is in a loaded state, implying its structures are ready for reading but it may not be ready for playback + bool checkLoaded() const; + //! Returns whether the movie is playable, implying the movie is fully formed and can be played but media data is still downloading + bool checkPlayable() const; + //! Returns whether the movie is ready for playthrough, implying media data is still downloading, but all data is expected to arrive before it is needed + bool checkPlaythroughOk() const; + //! Returns whether the movie has content protection applied to it + bool checkProtection() const { return false; } + + //! Waits until the movie is in a loaded state, which implies its structures are ready for reading but it is not ready for playback + void waitForLoaded() const; + //! Waits until the movie is in a playable state, implying the movie is fully formed and can be played but media data is still downloading + void waitForPlayable() const; + //! Waits until the movie is ready for playthrough, implying media data is still downloading, but all data is expected to arrive before it is needed + void waitForPlayThroughOk() const; + + //! Returns whether the object is considered to own the movie asset (and thus will destroy it upon deletion) + bool ownsMovie() const { return mOwnsMovie; } + + //! Returns the original Url that the MovieLoader is loading + const Url& getUrl() const { return mUrl; } + + //! Returns internal implementation (for MovieBase construction) + std::shared_ptr getImpl() const { return mImpl; } + + //! Transfers ownership of the implementation + std::shared_ptr transferImpl() const { mOwnsMovie = false; return mImpl; } + + protected: + std::shared_ptr mImpl; + Url mUrl; + mutable bool mOwnsMovie; +}; + +class MswExc : public Exception { +}; + +class MswPathInvalidExc : public MswExc { +}; + +class MswFileInvalidExc : public MswExc { +}; + +class MswUrlInvalidExc : public MswExc { +}; + +class MswErrorLoadingExc : public MswExc { +}; + +class MswTextureErrorExc : public MswExc { +}; + +} } // namespace cinder::qtime diff --git a/include/cinder/qtime/mf/DXGIRenderPath.h b/include/cinder/qtime/mf/DXGIRenderPath.h new file mode 100644 index 0000000000..1b4e473c29 --- /dev/null +++ b/include/cinder/qtime/mf/DXGIRenderPath.h @@ -0,0 +1,86 @@ +/* + Copyright (c) 2024, The Cinder Project, All rights reserved. + + Based on AX-MediaPlayer by Andrew Wright (@axjxwright). + (c) 2021 AX Interactive (axinteractive.com.au) + + DXGI/OpenGL interop render path for hardware-accelerated video playback. + Uses WGL_NV_DX_interop extension for D3D11/OpenGL texture sharing. + */ + +#pragma once + +#ifdef CINDER_MSW +#ifdef _WIN64 + +#include "MediaEnginePlayer.h" +#include "cinder/gl/gl.h" +#include +#include +#include + +namespace cinder { namespace qtime { namespace mf { + +//! Shared D3D11/OpenGL texture for hardware-accelerated video frames +class SharedTexture : public std::enable_shared_from_this +{ +public: + SharedTexture( const ci::ivec2& size ); + ~SharedTexture(); + + bool lock(); + bool unlock(); + bool isLocked() const { return mIsLocked; } + + bool isValid() const { return mIsValid; } + ID3D11Texture2D* dxTextureHandle() const { return mDxTexture.Get(); } + const ci::gl::TextureRef& glTextureHandle() const { return mGlTexture; } + +protected: + mutable std::mutex mLockMutex; + ci::gl::TextureRef mGlTexture; + ComPtr mDxTexture{ nullptr }; + HANDLE mShareHandle{ nullptr }; + bool mIsValid{ false }; + bool mIsLocked{ false }; +}; + +using SharedTextureRef = std::shared_ptr; + +//! DXGI render path using D3D11/OpenGL interop for hardware-accelerated playback +class DXGIRenderPath : public RenderPath +{ +public: + DXGIRenderPath( MediaEnginePlayer& owner, const ci::DataSourceRef& source ); + ~DXGIRenderPath(); + + bool initialize( IMFAttributes& attributes ) override; + bool initializeRenderTarget( const ci::ivec2& size ) override; + bool processFrame() override; + MediaEnginePlayer::FrameLeaseRef getFrameLease() const override; + + //! Returns true if the D3D11/GL interop context is valid + bool isContextValid() const; + +protected: + SharedTextureRef mSharedTexture{ nullptr }; +}; + +//! Frame lease that locks the shared texture during its lifetime +class DXGIRenderPathFrameLease : public MediaEnginePlayer::FrameLease +{ +public: + DXGIRenderPathFrameLease( const SharedTextureRef& texture ); + ~DXGIRenderPathFrameLease(); + + bool isValid() const override { return toTexture() != nullptr; } + ci::gl::TextureRef toTexture() const override; + +protected: + SharedTextureRef mTexture; +}; + +} } } // namespace cinder::qtime::mf + +#endif // _WIN64 +#endif // CINDER_MSW diff --git a/include/cinder/qtime/mf/MediaEnginePlayer.h b/include/cinder/qtime/mf/MediaEnginePlayer.h new file mode 100644 index 0000000000..9028ec112c --- /dev/null +++ b/include/cinder/qtime/mf/MediaEnginePlayer.h @@ -0,0 +1,238 @@ +/* + Copyright (c) 2024, The Cinder Project, All rights reserved. + + Based on AX-MediaPlayer by Andrew Wright (@axjxwright). + (c) 2021 AX Interactive (axinteractive.com.au) + + Internal Media Foundation implementation for Windows 64-bit video playback. + */ + +#pragma once + +#ifdef CINDER_MSW +#ifdef _WIN64 + +#include "cinder/Cinder.h" +#include "cinder/Surface.h" +#include "cinder/Signals.h" +#include "cinder/gl/Texture.h" +#include "cinder/Filesystem.h" +#include "cinder/DataSource.h" +#include "cinder/Noncopyable.h" + +#include +#include +#include +#include +#include + +#ifdef WINVER + #undef WINVER +#endif +#define WINVER _WIN32_WINNT_WIN10 + +#include +#include +#include +#include + +using namespace Microsoft::WRL; + +#pragma comment(lib, "mf.lib") +#pragma comment(lib, "mfplat.lib") +#pragma comment(lib, "mfuuid.lib") + +namespace cinder { namespace qtime { namespace mf { + +//! Run a callback synchronously on the MTA thread (required for audio functions) +void runSynchronousInMTAThread( std::function callback ); + +//! Run a callback synchronously on the main thread +void runSynchronousInMainThread( std::function callback ); + +class RenderPath; +using RenderPathRef = std::unique_ptr; + +//! Internal Media Foundation player implementation +class MediaEnginePlayer : public IMFMediaEngineNotify, public ci::Noncopyable +{ +public: + enum class Error + { + NoError = 0, + Aborted = 1, + NetworkError = 2, + DecodingError = 3, + SourceNotSupported = 4, + Encrypted = 5, + }; + + //! Frame lease for DXGI texture access - holds lock during lifetime + class FrameLease + { + public: + virtual ~FrameLease() {} + + operator bool() const { return isValid(); } + operator ci::gl::TextureRef() const { return toTexture(); } + virtual ci::gl::TextureRef toTexture() const { return nullptr; } + + protected: + virtual bool isValid() const { return false; } + }; + + using FrameLeaseRef = std::unique_ptr; + + struct Format + { + Format& audio( bool enabled ) { mAudioEnabled = enabled; return *this; } + Format& audioOnly( bool audioOnly ) { mAudioOnly = audioOnly; return *this; } + Format& hardwareAccelerated( bool accelerated ) { mHardwareAccelerated = accelerated; return *this; } + + bool isAudioEnabled() const { return mAudioEnabled; } + bool isAudioOnly() const { return mAudioOnly; } + bool isHardwareAccelerated() const { return mHardwareAccelerated; } + + protected: + bool mAudioEnabled{ true }; + bool mAudioOnly{ false }; + bool mHardwareAccelerated{ true }; + }; + + static void staticInitialize(); + static void staticShutdown(); + static const std::string& errorToString( Error error ); + + MediaEnginePlayer( const ci::DataSourceRef& source, const Format& format = Format() ); + ~MediaEnginePlayer(); + + bool update(); + + // Playback state + bool isComplete() const; + bool isPlaying() const; + bool isPaused() const; + bool isSeeking() const; + bool isReady() const; + + // Media info + bool hasAudio() const; + bool hasVideo() const; + const ci::ivec2& getSize() const { return mSize; } + float getDuration() const { return mDuration; } + float getFrameRate() const { return mFrameRate; } + + // Playback control + void play(); + void pause(); + void togglePlayback(); + + bool setPlaybackRate( float rate ); + float getPlaybackRate() const; + bool isPlaybackRateSupported( float rate ) const; + + void setMuted( bool mute ); + bool isMuted() const; + + void setVolume( float volume ); + float getVolume() const; + + void setLoop( bool loop ); + bool isLooping() const; + + void seekToSeconds( float seconds, bool approximate = false ); + void seekToPercentage( float normalizedTime, bool approximate = false ); + float getPositionInSeconds() const; + + void frameStep( int delta ); + + // Frame access + bool checkNewFrame() const { return mHasNewFrame.load(); } + const ci::Surface8uRef& getSurface() const; + FrameLeaseRef getTexture() const; + + bool isHardwareAccelerated() const { return mFormat.isHardwareAccelerated(); } + + // Signals + ci::signals::Signal signalReady; + ci::signals::Signal signalComplete; + ci::signals::Signal signalPlay; + ci::signals::Signal signalPause; + ci::signals::Signal signalSeekStart; + ci::signals::Signal signalSeekEnd; + ci::signals::Signal signalBufferingStart; + ci::signals::Signal signalBufferingEnd; + ci::signals::Signal signalError; + + // IMFMediaEngineNotify interface + HRESULT STDMETHODCALLTYPE EventNotify( DWORD event, DWORD_PTR param1, DWORD param2 ) override; + HRESULT STDMETHODCALLTYPE QueryInterface( REFIID riid, LPVOID* ppvObj ) override; + ULONG STDMETHODCALLTYPE AddRef() override; + ULONG STDMETHODCALLTYPE Release() override; + +protected: + friend class RenderPath; + friend class DXGIRenderPath; + friend class WICRenderPath; + + void updateEvents(); + void processEvent( DWORD evt, DWORD_PTR param1, DWORD param2 ); + + ci::DataSourceRef mSource; + ci::ivec2 mSize{ 0, 0 }; + Format mFormat; + float mDuration{ 0.0f }; + float mFrameRate{ 30.0f }; + bool mHasMetadata{ false }; + ci::Surface8uRef mSurface{ nullptr }; + RenderPathRef mRenderPath; + ComPtr mMediaEngine{ nullptr }; + ComPtr mMediaEngineEx{ nullptr }; + mutable std::atomic mHasNewFrame{ false }; + + // Thread safety + mutable std::shared_mutex mShutdownMutex; + std::atomic mIsShuttingDown{ false }; + std::mutex mEventMutex; + + // Loop detection workaround + float mTimeInSecondsAtStartOfSeek{ 0.0f }; + + struct Event + { + DWORD eventId{ 0 }; + DWORD_PTR param1{ 0 }; + DWORD param2{ 0 }; + }; + std::queue mEventQueue; +}; + +using MediaEnginePlayerRef = std::shared_ptr; + +//! Base class for render paths (DXGI hardware or WIC software) +class RenderPath +{ +public: + RenderPath( MediaEnginePlayer& owner, const ci::DataSourceRef& source ) + : mOwner( owner ) + , mSource( source ) + {} + + virtual ~RenderPath() {} + + virtual bool initialize( IMFAttributes& attributes ) { return true; } + virtual bool initializeRenderTarget( const ci::ivec2& size ) = 0; + virtual bool processFrame() = 0; + virtual MediaEnginePlayer::FrameLeaseRef getFrameLease() const { return nullptr; } + const ci::ivec2& getSize() const { return mSize; } + +protected: + ci::DataSourceRef mSource; + MediaEnginePlayer& mOwner; + ci::ivec2 mSize{ 0, 0 }; +}; + +} } } // namespace cinder::qtime::mf + +#endif // _WIN64 +#endif // CINDER_MSW diff --git a/include/cinder/qtime/mf/WICRenderPath.h b/include/cinder/qtime/mf/WICRenderPath.h new file mode 100644 index 0000000000..b6c0295079 --- /dev/null +++ b/include/cinder/qtime/mf/WICRenderPath.h @@ -0,0 +1,51 @@ +/* + Copyright (c) 2024, The Cinder Project, All rights reserved. + + Based on AX-MediaPlayer by Andrew Wright (@axjxwright). + (c) 2021 AX Interactive (axinteractive.com.au) + + WIC (Windows Imaging Component) render path for CPU-based video playback. + Used as fallback when hardware acceleration is unavailable. + */ + +#pragma once + +#ifdef CINDER_MSW +#ifdef _WIN64 + +#include "MediaEnginePlayer.h" + +namespace cinder { namespace qtime { namespace mf { + +//! WIC render path for CPU-based video decoding (fallback path) +class WICRenderPath : public RenderPath +{ +public: + WICRenderPath( MediaEnginePlayer& owner, const ci::DataSourceRef& source ); + + bool processFrame() override; + bool initializeRenderTarget( const ci::ivec2& size ) override; + MediaEnginePlayer::FrameLeaseRef getFrameLease() const override; + +protected: + ComPtr mWicBitmap{ nullptr }; + ComPtr mWicFactory{ nullptr }; +}; + +//! Frame lease for WIC path - creates texture on demand +class WICRenderPathFrameLease : public MediaEnginePlayer::FrameLease +{ +public: + WICRenderPathFrameLease( const ci::Surface8uRef& surface ); + + bool isValid() const override { return toTexture() != nullptr; } + ci::gl::TextureRef toTexture() const override { return mTexture; } + +protected: + ci::gl::TextureRef mTexture{ nullptr }; +}; + +} } } // namespace cinder::qtime::mf + +#endif // _WIN64 +#endif // CINDER_MSW diff --git a/proj/cmake/platform_msw.cmake b/proj/cmake/platform_msw.cmake index 97f3908a95..f8940c6c80 100644 --- a/proj/cmake/platform_msw.cmake +++ b/proj/cmake/platform_msw.cmake @@ -50,7 +50,7 @@ if( NOT CINDER_DISABLE_VIDEO ) if( CINDER_MSW_USE_GSTREAMER ) set( GST_ROOT $ENV{GSTREAMER_1_0_ROOT_X86_64} ) if( GST_ROOT ) - list( APPEND CINDER_LIBS_DEPENDS + list( APPEND CINDER_LIBS_DEPENDS ${GST_ROOT}/lib/gstreamer-1.0.lib ${GST_ROOT}/lib/gstapp-1.0.lib ${GST_ROOT}/lib/gstvideo-1.0.lib @@ -64,22 +64,33 @@ if( NOT CINDER_DISABLE_VIDEO ) ${GST_ROOT}/lib/glib-2.0.lib ${GST_ROOT}/lib/gio-2.0.lib ) - list( APPEND CINDER_INCLUDE_SYSTEM_PRIVATE - ${GST_ROOT}/include + list( APPEND CINDER_INCLUDE_SYSTEM_PRIVATE + ${GST_ROOT}/include ${GST_ROOT}/include/gstreamer-1.0 ${GST_ROOT}/include/glib-2.0 ${GST_ROOT}/lib/gstreamer-1.0/include ${GST_ROOT}/lib/glib-2.0/include ${CINDER_INC_DIR}/cinder/linux ) - list( APPEND CINDER_SRC_FILES - ${CINDER_SRC_DIR}/cinder/linux/GstPlayer.cpp + list( APPEND CINDER_SRC_FILES + ${CINDER_SRC_DIR}/cinder/linux/GstPlayer.cpp ${CINDER_SRC_DIR}/cinder/linux/Movie.cpp ) list( APPEND CINDER_DEFINES CINDER_MSW_USE_GSTREAMER ) else() message( WARNING "Requested GStreamer video playback support for MSW but no suitable GStreamer installation found. Make sure that GStreamer is installed properly and GSTREAMER_1_0_ROOT_X86_64 is defined in your env variables. " ) endif() + else() + # Default: Use MediaFoundation for video playback on Windows + list( APPEND SRC_SET_VIDEO_MSW + ${CINDER_SRC_DIR}/cinder/qtime/QuickTimeImplMsw.cpp + ${CINDER_SRC_DIR}/cinder/qtime/QuickTimeGlImplMsw.cpp + ${CINDER_SRC_DIR}/cinder/qtime/mf/MediaEnginePlayer.cpp + ${CINDER_SRC_DIR}/cinder/qtime/mf/DXGIRenderPath.cpp + ${CINDER_SRC_DIR}/cinder/qtime/mf/WICRenderPath.cpp + ) + + list( APPEND CINDER_SRC_FILES ${SRC_SET_VIDEO_MSW} ) endif() endif() diff --git a/proj/vc2022/cinder.vcxproj b/proj/vc2022/cinder.vcxproj index 91e0033ea1..cb2ab7bb18 100644 --- a/proj/vc2022/cinder.vcxproj +++ b/proj/vc2022/cinder.vcxproj @@ -1,4 +1,4 @@ - + @@ -553,6 +553,11 @@ + + + + + @@ -815,6 +820,10 @@ + + + + diff --git a/proj/vc2022/cinder.vcxproj.filters b/proj/vc2022/cinder.vcxproj.filters index d941f25de5..774ae0ea90 100644 --- a/proj/vc2022/cinder.vcxproj.filters +++ b/proj/vc2022/cinder.vcxproj.filters @@ -235,6 +235,15 @@ {7ff32b0d-8190-49b2-ab18-187db6196a2b} + + {34354d97-72cb-43e6-8615-7b0c1073fe23} + + + {8f27f7d6-4f67-44bd-844a-96ddae12b659} + + + {5bf44c8b-738b-496d-b667-813b1ef8e7cd} + @@ -1029,6 +1038,27 @@ Source Files + + Source Files + + + Source Files + + + Source Files\qtime + + + Source Files\qtime + + + Source Files\qtime\mf + + + Source Files\qtime\mf + + + Source Files\qtime\mf + @@ -2168,8 +2198,26 @@ Header Files + + Header Files + + + Header Files + + + Header Files\qtime + + + Header Files\qtime + + + Header Files\qtime + + + Header Files\qtime + - + \ No newline at end of file diff --git a/samples/QuickTimeAdvanced/include/Resources.h b/samples/QuickTimeAdvanced/include/Resources.h new file mode 100644 index 0000000000..3bdaead2a6 --- /dev/null +++ b/samples/QuickTimeAdvanced/include/Resources.h @@ -0,0 +1,4 @@ +#pragma once +#include "cinder/CinderResources.h" + +//#define RES_MY_RES CINDER_RESOURCE( ../resources/, image_name.png, 128, IMAGE ) diff --git a/samples/QuickTimeAdvanced/src/QTimeAdvApp.cpp b/samples/QuickTimeAdvanced/src/QTimeAdvApp.cpp index 2bd526e828..3d8e15929b 100644 --- a/samples/QuickTimeAdvanced/src/QTimeAdvApp.cpp +++ b/samples/QuickTimeAdvanced/src/QTimeAdvApp.cpp @@ -5,48 +5,62 @@ #include "cinder/gl/Texture.h" #include "cinder/Rand.h" #include "cinder/qtime/QuickTimeGl.h" +#include "cinder/CinderImGui.h" +#include "cinder/Log.h" using namespace ci; using namespace ci::app; using namespace std; +// Per-video state for UI +struct VideoSlot +{ + qtime::MovieGlRef movie; + string name; + float seekPosition = 0.0f; + float volume = 1.0f; + float rate = 1.0f; + bool loop = true; + bool selected = false; + bool isSeeking = false; // True while user is dragging seek slider +}; + class QTimeAdvApp : public App { public: - void prepareSettings( Settings *settings ); - void setup(); + void setup() override; + void keyDown( KeyEvent event ) override; + void fileDrop( FileDropEvent event ) override; + void update() override; + void draw() override; - void keyDown( KeyEvent event ); - void fileDrop( FileDropEvent event ); + void loadMovieFile( const fs::path& path ); + void drawVideoGrid(); + void drawControlPanel(); + void drawVideoControls( VideoSlot& slot, int index ); - void update(); - void draw(); + private: + static constexpr float kPanelWidth = 320.0f; - void addActiveMovie( qtime::MovieGlRef movie ); - void loadMovieUrl( const std::string &urlString ); - void loadMovieFile( const fs::path &path ); + vector mVideos; + gl::TextureFontRef mFont; + int mVideoToDelete = -1; // Deferred deletion index - - fs::path mLastPath; - // all of the actively playing movies - vector mMovies; - // movies we're still waiting on to be loaded - vector mLoadingMovies; + fs::path mLastPath; + int mGridColumns = 2; + bool mShowControls = true; + bool mAutoPlay = true; + int mSelectedVideo = -1; }; - -void QTimeAdvApp::prepareSettings( Settings *settings ) -{ - settings->setWindowSize( 640, 480 ); - settings->setFullScreen( false ); - settings->setResizable( true ); -} - void QTimeAdvApp::setup() { - srand( 133 ); - fs::path moviePath = getOpenFilePath(); - if( ! moviePath.empty() ) - loadMovieFile( moviePath ); + // Initialize ImGui + ImGui::Initialize( ImGui::Options().window( getWindow() ).enableKeyboard( true ) ); + + // Cache font for placeholder text + mFont = gl::TextureFont::create( Font( "Arial", 24 ) ); + + CI_LOG_I( "QuickTimeAdvanced" ); } void QTimeAdvApp::keyDown( KeyEvent event ) @@ -59,117 +73,314 @@ void QTimeAdvApp::keyDown( KeyEvent event ) if( ! moviePath.empty() ) loadMovieFile( moviePath ); } - else if( event.getChar() == 'O' ) { - if( ! mLastPath.empty() ) - loadMovieFile( mLastPath ); + else if( event.getChar() == 'c' ) { + mShowControls = ! mShowControls; } else if( event.getChar() == 'x' ) { - mMovies.clear(); - mLoadingMovies.clear(); + mVideos.clear(); + mSelectedVideo = -1; } - else if( event.getChar() == '2' ) { - if( ! mMovies.empty() ) - mMovies.back()->setRate( 2.0f ); + else if( event.getChar() == ' ' ) { + // Toggle play/pause on selected or all videos + if( mSelectedVideo >= 0 && mSelectedVideo < (int)mVideos.size() ) { + auto& movie = mVideos[mSelectedVideo].movie; + if( movie ) { + if( movie->isPlaying() ) + movie->stop(); + else + movie->play(); + } + } + else { + for( auto& slot : mVideos ) { + if( slot.movie ) { + if( slot.movie->isPlaying() ) + slot.movie->stop(); + else + slot.movie->play(); + } + } + } } - else if( event.getChar() == 'd' ) { - if( ! mMovies.empty() ) - mMovies.erase( mMovies.begin() + ( rand() % mMovies.size() ) ); +} + +void QTimeAdvApp::loadMovieFile( const fs::path& moviePath ) +{ + try { + auto movie = qtime::MovieGl::create( moviePath ); + + VideoSlot slot; + slot.movie = movie; + slot.name = moviePath.filename().string(); + slot.loop = true; + + movie->setLoop( true, false ); + if( mAutoPlay ) + movie->play(); + + mVideos.push_back( slot ); + mLastPath = moviePath; + + CI_LOG_I( "Loaded: " << slot.name << " (" << movie->getWidth() << "x" << movie->getHeight() << ")" ); } - else if( event.getChar() == 'u' ) { - vector randomMovie; - randomMovie.push_back( "http://movies.apple.com/movies/us/hd_gallery/gl1800/480p/bbc_earth_m480p.mov" ); - randomMovie.push_back( "http://movies.apple.com/media/us/quicktime/guide/hd/480p/noisettes_m480p.mov" ); - randomMovie.push_back( "http://pdl.warnerbros.com/wbol/us/dd/med/northbynorthwest/quicktime_page/nbnf_airplane_explosion_qt_500.mov" ); - loadMovieUrl( randomMovie[rand() % randomMovie.size()] ); + catch( ci::Exception& exc ) { + CI_LOG_E( "Failed to load: " << moviePath << " - " << exc.what() ); } } -void QTimeAdvApp::addActiveMovie( qtime::MovieGlRef movie ) +void QTimeAdvApp::fileDrop( FileDropEvent event ) { - console() << "Dimensions:" << movie->getWidth() << " x " << movie->getHeight() << std::endl; - console() << "Duration: " << movie->getDuration() << " seconds" << std::endl; - console() << "Frames: " << movie->getNumFrames() << std::endl; - console() << "Framerate: " << movie->getFramerate() << std::endl; - movie->setLoop( true, false ); - - mMovies.push_back( movie ); - movie->play(); + for( size_t i = 0; i < event.getNumFiles(); ++i ) + loadMovieFile( event.getFile( i ) ); } -void QTimeAdvApp::loadMovieUrl( const string &urlString ) +void QTimeAdvApp::update() { - try { - mLoadingMovies.push_back( qtime::MovieLoader::create( Url( urlString ) ) ); + // Handle deferred video deletion (safe outside of iteration) + if( mVideoToDelete >= 0 && mVideoToDelete < (int)mVideos.size() ) { + // Adjust selected video index if needed + if( mSelectedVideo == mVideoToDelete ) { + mSelectedVideo = -1; + } + else if( mSelectedVideo > mVideoToDelete ) { + --mSelectedVideo; + } + mVideos.erase( mVideos.begin() + mVideoToDelete ); + mVideoToDelete = -1; } - catch( ci::Exception &exc ) { - console() << "Exception caught trying to load the movie from URL: " << urlString << ", what: " << exc.what() << std::endl; + + // Sync UI state with video state (skip if user is seeking) + for( auto& slot : mVideos ) { + if( slot.movie && ! slot.isSeeking ) { + slot.seekPosition = slot.movie->getCurrentTime(); + } } } -void QTimeAdvApp::loadMovieFile( const fs::path &moviePath ) +void QTimeAdvApp::draw() { - qtime::MovieGlRef movie; - - try { - movie = qtime::MovieGl::create( moviePath ); + gl::clear( Color( 0.1f, 0.1f, 0.12f ) ); - addActiveMovie( movie ); - mLastPath = moviePath; - } - catch( ci::Exception &exc ) { - console() << "Exception caught trying to load the movie from path: " << moviePath << ", what: " << exc.what() << std::endl; - return; + // Draw video grid + drawVideoGrid(); + + // Draw ImGui control panel + if( mShowControls ) { + drawControlPanel(); } } -void QTimeAdvApp::fileDrop( FileDropEvent event ) +void QTimeAdvApp::drawVideoGrid() { - for( size_t s = 0; s < event.getNumFiles(); ++s ) - loadMovieFile( event.getFile( s ) ); + if( mVideos.empty() ) { + // Draw placeholder text using cached font + gl::color( 0.5f, 0.5f, 0.5f ); + string msg = "Drag & drop video files here\nor press 'O' to open"; + vec2 size = mFont->measureString( msg ); + mFont->drawString( msg, vec2( getWindowCenter() ) - size * 0.5f ); + return; + } + + int cols = std::min( mGridColumns, (int)mVideos.size() ); + int rows = ( (int)mVideos.size() + cols - 1 ) / cols; + + float cellWidth = getWindowWidth() / (float)cols; + float cellHeight = getWindowHeight() / (float)rows; + + // Adjust for control panel if visible + float panelWidth = mShowControls ? kPanelWidth : 0.0f; + float availableWidth = getWindowWidth() - panelWidth; + cellWidth = availableWidth / (float)cols; + + for( size_t i = 0; i < mVideos.size(); ++i ) { + auto& slot = mVideos[i]; + auto texture = slot.movie->getTexture(); + + int col = (int)i % cols; + int row = (int)i / cols; + + float x = col * cellWidth; + float y = row * cellHeight; + + if( texture ) { + // Calculate aspect-correct size + float videoAspect = slot.movie->getAspectRatio(); + float cellAspect = cellWidth / cellHeight; + + float drawWidth, drawHeight; + if( videoAspect > cellAspect ) { + drawWidth = cellWidth - 8; + drawHeight = drawWidth / videoAspect; + } + else { + drawHeight = cellHeight - 8; + drawWidth = drawHeight * videoAspect; + } + + float drawX = x + ( cellWidth - drawWidth ) / 2; + float drawY = y + ( cellHeight - drawHeight ) / 2; + + // Draw selection highlight + if( (int)i == mSelectedVideo ) { + gl::color( 0.3f, 0.5f, 0.8f ); + gl::drawStrokedRect( Rectf( drawX - 2, drawY - 2, drawX + drawWidth + 2, drawY + drawHeight + 2 ), 3.0f ); + } + + gl::color( Color::white() ); + gl::draw( texture, Rectf( drawX, drawY, drawX + drawWidth, drawY + drawHeight ) ); + } + else { + // Draw placeholder + gl::color( 0.2f, 0.2f, 0.2f ); + gl::drawSolidRect( Rectf( x + 4, y + 4, x + cellWidth - 4, y + cellHeight - 4 ) ); + } + } } -void QTimeAdvApp::update() +void QTimeAdvApp::drawControlPanel() { - // let's see if any of our loading movies have finished loading and can be made active - for( auto loaderIt = mLoadingMovies.begin(); loaderIt != mLoadingMovies.end(); ) { - try { - if( (*loaderIt)->checkPlaythroughOk() ) { - addActiveMovie( qtime::MovieGl::create( *loaderIt ) ); - loaderIt = mLoadingMovies.erase( loaderIt ); + ImGui::SetNextWindowPos( ImVec2( getWindowWidth() - kPanelWidth, 0 ), ImGuiCond_Always ); + ImGui::SetNextWindowSize( ImVec2( kPanelWidth, (float)getWindowHeight() ), ImGuiCond_Always ); + + ImGui::Begin( "Video Controls", &mShowControls, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize ); + + // Global controls + if( ImGui::CollapsingHeader( "Global", ImGuiTreeNodeFlags_DefaultOpen ) ) { + ImGui::SliderInt( "Grid Columns", &mGridColumns, 1, 4 ); + ImGui::Checkbox( "Auto-play on load", &mAutoPlay ); + + if( ImGui::Button( "Open File (O)" ) ) { + fs::path moviePath = getOpenFilePath(); + if( ! moviePath.empty() ) + loadMovieFile( moviePath ); + } + ImGui::SameLine(); + if( ImGui::Button( "Clear All (X)" ) ) { + mVideos.clear(); + mSelectedVideo = -1; + } + + ImGui::Separator(); + + // Play/Pause all + if( ImGui::Button( "Play All" ) ) { + for( auto& slot : mVideos ) + if( slot.movie ) slot.movie->play(); + } + ImGui::SameLine(); + if( ImGui::Button( "Pause All" ) ) { + for( auto& slot : mVideos ) + if( slot.movie ) slot.movie->stop(); + } + ImGui::SameLine(); + if( ImGui::Button( "Restart All" ) ) { + for( auto& slot : mVideos ) { + if( slot.movie ) { + slot.movie->seekToStart(); + slot.movie->play(); + } } - else - ++loaderIt; } - catch( ci::Exception &exc ) { - console() << "Exception caught trying to load the movie from URL: " << *loaderIt << ", what: " << exc.what() << std::endl; - loaderIt = mLoadingMovies.erase( loaderIt ); + } + + ImGui::Separator(); + + // Per-video controls + if( ImGui::CollapsingHeader( "Videos", ImGuiTreeNodeFlags_DefaultOpen ) ) { + if( mVideos.empty() ) { + ImGui::TextColored( ImVec4( 0.5f, 0.5f, 0.5f, 1.0f ), "No videos loaded" ); + } + else { + for( int i = 0; i < (int)mVideos.size(); ++i ) { + ImGui::PushID( i ); + drawVideoControls( mVideos[i], i ); + ImGui::PopID(); + } } } + + ImGui::Separator(); + + // Stats + if( ImGui::CollapsingHeader( "Stats" ) ) { + ImGui::Text( "Videos loaded: %d", (int)mVideos.size() ); + float fps = getAverageFps(); + ImGui::Text( "FPS: %.1f", fps ); + ImGui::Text( "Frame time: %.2f ms", fps > 0.0f ? 1000.0f / fps : 0.0f ); + } + + ImGui::End(); } -void QTimeAdvApp::draw() +void QTimeAdvApp::drawVideoControls( VideoSlot& slot, int index ) { - gl::clear( Color( 0, 0, 0 ) ); + bool isSelected = ( index == mSelectedVideo ); - int totalWidth = 0; - for( size_t m = 0; m < mMovies.size(); ++m ) - totalWidth += mMovies[m]->getWidth(); + // Selectable header + if( ImGui::Selectable( slot.name.c_str(), isSelected ) ) { + mSelectedVideo = isSelected ? -1 : index; + } - int drawOffsetX = 0; - for( size_t m = 0; m < mMovies.size(); ++m ) { - float relativeWidth = mMovies[m]->getWidth() / (float)totalWidth; - gl::TextureRef texture = mMovies[m]->getTexture(); - if( texture ) { - float drawWidth = getWindowWidth() * relativeWidth; - float drawHeight = ( getWindowWidth() * relativeWidth ) / mMovies[m]->getAspectRatio(); - float x = drawOffsetX; - float y = ( getWindowHeight() - drawHeight ) / 2.0f; + if( ! slot.movie ) + return; - gl::color( Color::white() ); - gl::draw( texture, Rectf( x, y, x + drawWidth, y + drawHeight ) ); + ImGui::Indent(); + + // Info line + ImGui::TextColored( ImVec4( 0.6f, 0.6f, 0.6f, 1.0f ), "%dx%d | %.1fs | %.1f fps", + slot.movie->getWidth(), slot.movie->getHeight(), + slot.movie->getDuration(), slot.movie->getFramerate() ); + + // Play/Pause + bool isPlaying = slot.movie->isPlaying(); + if( ImGui::Button( isPlaying ? "Pause" : "Play", ImVec2( 50, 0 ) ) ) { + if( isPlaying ) + slot.movie->stop(); + else + slot.movie->play(); + } + ImGui::SameLine(); + if( ImGui::Button( "<<", ImVec2( 30, 0 ) ) ) + slot.movie->seekToStart(); + ImGui::SameLine(); + if( ImGui::Button( ">>", ImVec2( 30, 0 ) ) ) + slot.movie->seekToEnd(); + ImGui::SameLine(); + if( ImGui::Button( "X", ImVec2( 25, 0 ) ) ) { + // Defer deletion to update() to avoid vector invalidation during iteration + mVideoToDelete = index; + } + + // Seek slider with drag tracking to prevent jitter + float duration = slot.movie->getDuration(); + if( duration > 0 ) { + float pos = slot.seekPosition; + if( ImGui::SliderFloat( "##seek", &pos, 0.0f, duration, "%.1fs" ) ) { + slot.movie->seekToTime( pos ); + slot.seekPosition = pos; } - drawOffsetX += getWindowWidth() * relativeWidth; + // Track whether user is actively dragging the slider + slot.isSeeking = ImGui::IsItemActive(); } + + // Volume + if( ImGui::SliderFloat( "Vol", &slot.volume, 0.0f, 1.0f, "%.2f" ) ) + slot.movie->setVolume( slot.volume ); + + // Rate + if( ImGui::SliderFloat( "Rate", &slot.rate, 0.0f, 4.0f, "%.2fx" ) ) + slot.movie->setRate( slot.rate ); + + // Loop + if( ImGui::Checkbox( "Loop", &slot.loop ) ) + slot.movie->setLoop( slot.loop, false ); + + ImGui::Unindent(); + ImGui::Spacing(); } -CINDER_APP( QTimeAdvApp, RendererGl ) \ No newline at end of file +CINDER_APP( QTimeAdvApp, RendererGl, []( App::Settings* settings ) { + settings->setWindowSize( 1280, 720 ); + settings->setTitle( "QuickTime Advanced" ); + settings->setResizable( true ); +} ) \ No newline at end of file diff --git a/samples/QuickTimeAdvanced/vc2022/QuickTimeAdvanced.sln b/samples/QuickTimeAdvanced/vc2022/QuickTimeAdvanced.sln new file mode 100644 index 0000000000..aa7bb64e63 --- /dev/null +++ b/samples/QuickTimeAdvanced/vc2022/QuickTimeAdvanced.sln @@ -0,0 +1,30 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.0.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "QuickTimeAdvanced", "QuickTimeAdvanced.vcxproj", "{B2C3D4E5-F678-9012-BCDE-F12345678901}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug_ANGLE|x64 = Debug_ANGLE|x64 + Debug|x64 = Debug|x64 + Release_ANGLE|x64 = Release_ANGLE|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Debug_ANGLE|x64.ActiveCfg = Debug_ANGLE|x64 + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Debug_ANGLE|x64.Build.0 = Debug_ANGLE|x64 + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Debug|x64.ActiveCfg = Debug|x64 + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Debug|x64.Build.0 = Debug|x64 + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Release_ANGLE|x64.ActiveCfg = Release_ANGLE|x64 + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Release_ANGLE|x64.Build.0 = Release_ANGLE|x64 + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Release|x64.ActiveCfg = Release|x64 + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {23456789-BCDE-F012-3456-7890ABCDEF01} + EndGlobalSection +EndGlobal diff --git a/samples/QuickTimeAdvanced/vc2022/QuickTimeAdvanced.vcxproj b/samples/QuickTimeAdvanced/vc2022/QuickTimeAdvanced.vcxproj new file mode 100644 index 0000000000..a3841a466d --- /dev/null +++ b/samples/QuickTimeAdvanced/vc2022/QuickTimeAdvanced.vcxproj @@ -0,0 +1,214 @@ + + + + + Debug_ANGLE + x64 + + + Debug + x64 + + + Release_ANGLE + x64 + + + Release + x64 + + + + {B2C3D4E5-F678-9012-BCDE-F12345678901} + QuickTimeAdvanced + Win32Proj + 10.0 + + + + Application + false + v143 + Unicode + true + + + Application + false + v143 + Unicode + true + + + Application + true + v143 + Unicode + + + Application + true + v143 + Unicode + + + + + + + + + + + + + + + + + + <_ProjectFileVersion>10.0.30319.1 + true + true + false + false + + + + Disabled + ..\include;"..\..\..\include" + WIN32;_DEBUG;_WINDOWS;NOMINMAX;%(PreprocessorDefinitions) + EnableFastChecks + MultiThreadedDebug + + Level3 + ProgramDatabase + true + stdcpp20 + + + "..\..\..\include";..\include + + + cinder.lib;OpenGL32.lib;%(AdditionalDependencies) + ..\..\..\lib\msw\$(PlatformTarget);..\..\..\lib\msw\$(PlatformTarget)\$(Configuration)\$(PlatformToolset) + true + Windows + false + + LIBCMT;LIBCPMT + + + + + Disabled + ..\include;"..\..\..\include";..\..\..\include\ANGLE + CINDER_GL_ANGLE;WIN32;_DEBUG;_WINDOWS;NOMINMAX;%(PreprocessorDefinitions) + EnableFastChecks + MultiThreadedDebug + + + Level3 + ProgramDatabase + true + stdcpp20 + + + "..\..\..\include";..\include + + + cinder.lib;libEGL.lib;libGLESv2.lib;%(AdditionalDependencies) + ..\..\..\lib\msw\$(PlatformTarget);..\..\..\lib\msw\$(PlatformTarget)\$(Configuration)\$(PlatformToolset) + true + Windows + false + + + LIBCMT;LIBCPMT + + + xcopy /y "..\..\..\lib\msw\$(PlatformTarget)\libGLESv2.dll" "$(OutDir)" +xcopy /y "..\..\..\lib\msw\$(PlatformTarget)\libEGL.dll" "$(OutDir)" +xcopy /y "..\..\..\lib\msw\$(PlatformTarget)\d3dcompiler_46.dll" "$(OutDir)" + + + + + ..\include;"..\..\..\include" + WIN32;NDEBUG;_WINDOWS;NOMINMAX;%(PreprocessorDefinitions) + MultiThreaded + + Level3 + ProgramDatabase + true + stdcpp20 + + + true + + + "..\..\..\include";..\include + + + cinder.lib;OpenGL32.lib;%(AdditionalDependencies) + ..\..\..\lib\msw\$(PlatformTarget);..\..\..\lib\msw\$(PlatformTarget)\$(Configuration)\$(PlatformToolset) + false + true + Windows + true + + false + + + + + + ..\include;"..\..\..\include";..\..\..\include\ANGLE + CINDER_GL_ANGLE;WIN32;NDEBUG;_WINDOWS;NOMINMAX;%(PreprocessorDefinitions) + MultiThreaded + + + Level3 + ProgramDatabase + true + stdcpp20 + + + true + + + "..\..\..\include";..\include + + + cinder.lib;libEGL.lib;libGLESv2.lib;%(AdditionalDependencies) + ..\..\..\lib\msw\$(PlatformTarget);..\..\..\lib\msw\$(PlatformTarget)\$(Configuration)\$(PlatformToolset) + false + true + Windows + true + + + false + + + + + xcopy /y "..\..\..\lib\msw\$(PlatformTarget)\libGLESv2.dll" "$(OutDir)" +xcopy /y "..\..\..\lib\msw\$(PlatformTarget)\libEGL.dll" "$(OutDir)" +xcopy /y "..\..\..\lib\msw\$(PlatformTarget)\d3dcompiler_46.dll" "$(OutDir)" + + + + + + + + + + + + + + + + diff --git a/samples/QuickTimeAdvanced/vc2022/QuickTimeAdvanced.vcxproj.filters b/samples/QuickTimeAdvanced/vc2022/QuickTimeAdvanced.vcxproj.filters new file mode 100644 index 0000000000..729da85851 --- /dev/null +++ b/samples/QuickTimeAdvanced/vc2022/QuickTimeAdvanced.vcxproj.filters @@ -0,0 +1,31 @@ + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav + + + + + Source Files + + + + + Header Files + + + + + Resource Files + + + diff --git a/samples/QuickTimeAdvanced/vc2022/Resources.rc b/samples/QuickTimeAdvanced/vc2022/Resources.rc new file mode 100644 index 0000000000..a4f824c3ac --- /dev/null +++ b/samples/QuickTimeAdvanced/vc2022/Resources.rc @@ -0,0 +1,3 @@ +#include "../include/Resources.h" + +1 ICON "..\..\data\cinder_app_icon.ico" diff --git a/samples/QuickTimeAdvanced/xcode/Info.plist b/samples/QuickTimeAdvanced/xcode/Info.plist index f93d9fb671..079c9971e8 100644 --- a/samples/QuickTimeAdvanced/xcode/Info.plist +++ b/samples/QuickTimeAdvanced/xcode/Info.plist @@ -7,9 +7,9 @@ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIconFile - + CinderApp.icns CFBundleIdentifier - com.barbariangroup.QuickTimeAdvanced + org.libcinder.QuickTimeAdvanced CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/samples/QuickTimeAdvanced/xcode/QTimeAdvApp.xcodeproj/project.pbxproj b/samples/QuickTimeAdvanced/xcode/QTimeAdvApp.xcodeproj/project.pbxproj index 70d761f20b..3177f79740 100644 --- a/samples/QuickTimeAdvanced/xcode/QTimeAdvApp.xcodeproj/project.pbxproj +++ b/samples/QuickTimeAdvanced/xcode/QTimeAdvApp.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 006D730E1995336F008149E2 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 006D730C1995336F008149E2 /* AVFoundation.framework */; }; 006D730F1995336F008149E2 /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 006D730D1995336F008149E2 /* CoreMedia.framework */; }; 0091D8F90E81B9330029341E /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0091D8F80E81B9330029341E /* OpenGL.framework */; }; + 00AB96BA2F0AEF3200CC35CA /* CinderApp.icns in Resources */ = {isa = PBXBuildFile; fileRef = 00AB96B92F0AEF3200CC35CA /* CinderApp.icns */; }; 00B9955A1B128DF400A5C623 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 00B995581B128DF400A5C623 /* IOKit.framework */; }; 00B9955B1B128DF400A5C623 /* IOSurface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 00B995591B128DF400A5C623 /* IOSurface.framework */; }; 5323E6B20EAFCA74003A9687 /* CoreVideo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5323E6B10EAFCA74003A9687 /* CoreVideo.framework */; }; @@ -33,6 +34,7 @@ 006D730D1995336F008149E2 /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; 0091D8F80E81B9330029341E /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = /System/Library/Frameworks/OpenGL.framework; sourceTree = ""; }; 0097E3E40F3E9819005A4392 /* QuickTime.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickTime.framework; path = /System/Library/Frameworks/QuickTime.framework; sourceTree = ""; }; + 00AB96B92F0AEF3200CC35CA /* CinderApp.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; name = CinderApp.icns; path = ../../data/CinderApp.icns; sourceTree = ""; }; 00B995581B128DF400A5C623 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; }; 00B995591B128DF400A5C623 /* IOSurface.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOSurface.framework; path = System/Library/Frameworks/IOSurface.framework; sourceTree = SDKROOT; }; 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; @@ -137,6 +139,7 @@ 29B97317FDCFA39411CA2CEA /* Resources */ = { isa = PBXGroup; children = ( + 00AB96B92F0AEF3200CC35CA /* CinderApp.icns */, 8D1107310486CEB800E47090 /* Info.plist */, ); name = Resources; @@ -208,6 +211,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 00AB96BA2F0AEF3200CC35CA /* CinderApp.icns in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -228,7 +232,6 @@ C01FCF4B08A954540054247B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ONLY_ACTIVE_ARCH = YES; CLANG_CXX_LIBRARY = "libc++"; COPY_PHASE_STRIP = NO; GCC_DYNAMIC_NO_PIC = NO; @@ -240,6 +243,7 @@ GCC_SYMBOLS_PRIVATE_EXTERN = NO; INFOPLIST_FILE = Info.plist; INSTALL_PATH = "$(HOME)/Applications"; + ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = "$(CINDER_PATH)/lib/macosx/$(CONFIGURATION)/libcinder.a"; PRODUCT_NAME = QTimeAdvApp; WRAPPER_EXTENSION = app; @@ -250,7 +254,6 @@ C01FCF4C08A954540054247B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ONLY_ACTIVE_ARCH = YES; CLANG_CXX_LIBRARY = "libc++"; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; @@ -263,6 +266,7 @@ GCC_SYMBOLS_PRIVATE_EXTERN = NO; INFOPLIST_FILE = Info.plist; INSTALL_PATH = "$(HOME)/Applications"; + ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = "$(CINDER_PATH)/lib/macosx/$(CONFIGURATION)/libcinder.a"; PRODUCT_NAME = QTimeAdvApp; WRAPPER_EXTENSION = app; @@ -272,7 +276,6 @@ C01FCF4F08A954540054247B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ONLY_ACTIVE_ARCH = YES; ALWAYS_SEARCH_USER_PATHS = NO; ARCHS = "$(ARCHS_STANDARD)"; CINDER_PATH = ../../..; @@ -281,6 +284,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; HEADER_SEARCH_PATHS = "$(CINDER_PATH)/include"; MACOSX_DEPLOYMENT_TARGET = 10.13; + ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; USER_HEADER_SEARCH_PATHS = "$(CINDER_PATH)/include ../include"; }; @@ -289,7 +293,6 @@ C01FCF5008A954540054247B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ONLY_ACTIVE_ARCH = YES; ALWAYS_SEARCH_USER_PATHS = NO; ARCHS = "$(ARCHS_STANDARD)"; CINDER_PATH = ../../..; @@ -298,6 +301,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; HEADER_SEARCH_PATHS = "$(CINDER_PATH)/include"; MACOSX_DEPLOYMENT_TARGET = 10.13; + ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; USER_HEADER_SEARCH_PATHS = "$(CINDER_PATH)/include ../include"; }; diff --git a/samples/QuickTimeBasic/src/QuickTimeBasicApp.cpp b/samples/QuickTimeBasic/src/QuickTimeBasicApp.cpp index 7a8d6782ee..53159d6a7a 100644 --- a/samples/QuickTimeBasic/src/QuickTimeBasicApp.cpp +++ b/samples/QuickTimeBasic/src/QuickTimeBasicApp.cpp @@ -34,12 +34,7 @@ class QuickTimeSampleApp : public App { void QuickTimeSampleApp::setup() { -#if defined( CINDER_ANDROID ) || defined( CINDER_LINUX ) - fs::path moviePath = getAssetPath( "bbb.mp4" ); -#else fs::path moviePath = getOpenFilePath(); - console() << "moviePath: " << moviePath << std::endl; -#endif if( ! moviePath.empty() ) loadMovieFile( moviePath ); } @@ -90,7 +85,7 @@ void QuickTimeSampleApp::update() mFrameTexture = mMovie->getTexture(); static bool sPrintedDone = false; - if( ! sPrintedDone && mMovie->isDone() ) { + if( ! sPrintedDone && mMovie && mMovie->isDone() ) { console() << "Done Playing" << std::endl; sPrintedDone = true; } diff --git a/samples/QuickTimeBasic/vc2022/QuickTimeBasic.sln b/samples/QuickTimeBasic/vc2022/QuickTimeBasic.sln new file mode 100644 index 0000000000..d5766110e4 --- /dev/null +++ b/samples/QuickTimeBasic/vc2022/QuickTimeBasic.sln @@ -0,0 +1,30 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.0.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "QuickTimeBasic", "QuickTimeBasic.vcxproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug_ANGLE|x64 = Debug_ANGLE|x64 + Debug|x64 = Debug|x64 + Release_ANGLE|x64 = Release_ANGLE|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug_ANGLE|x64.ActiveCfg = Debug_ANGLE|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug_ANGLE|x64.Build.0 = Debug_ANGLE|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release_ANGLE|x64.ActiveCfg = Release_ANGLE|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release_ANGLE|x64.Build.0 = Release_ANGLE|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {12345678-ABCD-EF01-2345-67890ABCDEF0} + EndGlobalSection +EndGlobal diff --git a/samples/QuickTimeBasic/vc2022/QuickTimeBasic.vcxproj b/samples/QuickTimeBasic/vc2022/QuickTimeBasic.vcxproj new file mode 100644 index 0000000000..81ee3158fb --- /dev/null +++ b/samples/QuickTimeBasic/vc2022/QuickTimeBasic.vcxproj @@ -0,0 +1,214 @@ + + + + + Debug_ANGLE + x64 + + + Debug + x64 + + + Release_ANGLE + x64 + + + Release + x64 + + + + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} + QuickTimeBasic + Win32Proj + 10.0 + + + + Application + false + v143 + Unicode + true + + + Application + false + v143 + Unicode + true + + + Application + true + v143 + Unicode + + + Application + true + v143 + Unicode + + + + + + + + + + + + + + + + + + <_ProjectFileVersion>10.0.30319.1 + true + true + false + false + + + + Disabled + ..\include;"..\..\..\include" + WIN32;_DEBUG;_WINDOWS;NOMINMAX;%(PreprocessorDefinitions) + EnableFastChecks + MultiThreadedDebug + + Level3 + ProgramDatabase + true + stdcpp20 + + + "..\..\..\include";..\include + + + cinder.lib;OpenGL32.lib;%(AdditionalDependencies) + ..\..\..\lib\msw\$(PlatformTarget);..\..\..\lib\msw\$(PlatformTarget)\$(Configuration)\$(PlatformToolset) + true + Windows + false + + LIBCMT;LIBCPMT + + + + + Disabled + ..\include;"..\..\..\include";..\..\..\include\ANGLE + CINDER_GL_ANGLE;WIN32;_DEBUG;_WINDOWS;NOMINMAX;%(PreprocessorDefinitions) + EnableFastChecks + MultiThreadedDebug + + + Level3 + ProgramDatabase + true + stdcpp20 + + + "..\..\..\include";..\include + + + cinder.lib;libEGL.lib;libGLESv2.lib;%(AdditionalDependencies) + ..\..\..\lib\msw\$(PlatformTarget);..\..\..\lib\msw\$(PlatformTarget)\$(Configuration)\$(PlatformToolset) + true + Windows + false + + + LIBCMT;LIBCPMT + + + xcopy /y "..\..\..\lib\msw\$(PlatformTarget)\libGLESv2.dll" "$(OutDir)" +xcopy /y "..\..\..\lib\msw\$(PlatformTarget)\libEGL.dll" "$(OutDir)" +xcopy /y "..\..\..\lib\msw\$(PlatformTarget)\d3dcompiler_46.dll" "$(OutDir)" + + + + + ..\include;"..\..\..\include" + WIN32;NDEBUG;_WINDOWS;NOMINMAX;%(PreprocessorDefinitions) + MultiThreaded + + Level3 + ProgramDatabase + true + stdcpp20 + + + true + + + "..\..\..\include";..\include + + + cinder.lib;OpenGL32.lib;%(AdditionalDependencies) + ..\..\..\lib\msw\$(PlatformTarget);..\..\..\lib\msw\$(PlatformTarget)\$(Configuration)\$(PlatformToolset) + false + true + Windows + true + + false + + + + + + ..\include;"..\..\..\include";..\..\..\include\ANGLE + CINDER_GL_ANGLE;WIN32;NDEBUG;_WINDOWS;NOMINMAX;%(PreprocessorDefinitions) + MultiThreaded + + + Level3 + ProgramDatabase + true + stdcpp20 + + + true + + + "..\..\..\include";..\include + + + cinder.lib;libEGL.lib;libGLESv2.lib;%(AdditionalDependencies) + ..\..\..\lib\msw\$(PlatformTarget);..\..\..\lib\msw\$(PlatformTarget)\$(Configuration)\$(PlatformToolset) + false + true + Windows + true + + + false + + + + + xcopy /y "..\..\..\lib\msw\$(PlatformTarget)\libGLESv2.dll" "$(OutDir)" +xcopy /y "..\..\..\lib\msw\$(PlatformTarget)\libEGL.dll" "$(OutDir)" +xcopy /y "..\..\..\lib\msw\$(PlatformTarget)\d3dcompiler_46.dll" "$(OutDir)" + + + + + + + + + + + + + + + + diff --git a/samples/QuickTimeBasic/vc2022/QuickTimeBasic.vcxproj.filters b/samples/QuickTimeBasic/vc2022/QuickTimeBasic.vcxproj.filters new file mode 100644 index 0000000000..108ff86b5f --- /dev/null +++ b/samples/QuickTimeBasic/vc2022/QuickTimeBasic.vcxproj.filters @@ -0,0 +1,31 @@ + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav + + + + + Source Files + + + + + Header Files + + + + + Resource Files + + + diff --git a/samples/QuickTimeBasic/vc2022/Resources.rc b/samples/QuickTimeBasic/vc2022/Resources.rc new file mode 100644 index 0000000000..a4f824c3ac --- /dev/null +++ b/samples/QuickTimeBasic/vc2022/Resources.rc @@ -0,0 +1,3 @@ +#include "../include/Resources.h" + +1 ICON "..\..\data\cinder_app_icon.ico" diff --git a/src/cinder/linux/Movie.cpp b/src/cinder/linux/Movie.cpp index 9965beb056..381e1980e9 100644 --- a/src/cinder/linux/Movie.cpp +++ b/src/cinder/linux/Movie.cpp @@ -58,6 +58,8 @@ void MovieBase::initFromUrl( const Url& url ) } mGstPlayer->load( url.str() ); + mWidth = mGstPlayer->width(); + mHeight = mGstPlayer->height(); } void MovieBase::initFromPath( const fs::path& filePath ) @@ -67,6 +69,8 @@ void MovieBase::initFromPath( const fs::path& filePath ) } mGstPlayer->load( filePath.string() ); + mWidth = mGstPlayer->width(); + mHeight = mGstPlayer->height(); init(); } diff --git a/src/cinder/qtime/QuickTimeGlImplMsw.cpp b/src/cinder/qtime/QuickTimeGlImplMsw.cpp new file mode 100644 index 0000000000..8aed9aee63 --- /dev/null +++ b/src/cinder/qtime/QuickTimeGlImplMsw.cpp @@ -0,0 +1,151 @@ +/* + Copyright (c) 2025, The Cinder Project, All rights reserved. + + This code is intended for use with the Cinder C++ library: http://libcinder.org + + Redistribution and use in source and binary forms, with or without modification, are permitted provided that + the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and + the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and + the following disclaimer in the documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + Windows 64-bit Media Foundation GL implementation. + Based on AX-MediaPlayer by Andrew Wright (@axjxwright). + + Hardware acceleration uses WGL_NV_DX_interop for D3D11/OpenGL texture sharing. + Falls back to WIC/CPU path if hardware acceleration is unavailable. +*/ + +#include "cinder/Cinder.h" + +#include "cinder/qtime/QuickTimeGlImplMsw.h" +#include "cinder/qtime/mf/MediaEnginePlayer.h" +#include "cinder/app/App.h" +#include "cinder/Log.h" + +using namespace ci; + +namespace cinder { namespace qtime { + +// MovieBase::Impl definition (must match QuickTimeImplMsw.cpp) +class MovieBase::Impl : public mf::MediaEnginePlayer { + public: + using mf::MediaEnginePlayer::MediaEnginePlayer; +}; + +// FrameLeaseHolder - stores the frame lease to keep the texture locked during draw +struct MovieGl::FrameLeaseHolder { + mf::MediaEnginePlayer::FrameLeaseRef lease; +}; + +MovieGl::MovieGl( const Url& url ) + : MovieBase() + , mHardwareAccelerated( true ) +{ + initGl( true ); + initFromUrl( url ); +} + +MovieGl::MovieGl( const fs::path& path ) + : MovieBase() + , mHardwareAccelerated( true ) +{ + initGl( true ); + initFromPath( path ); +} + +MovieGl::MovieGl( const MovieLoader& loader ) + : MovieBase() + , mHardwareAccelerated( false ) // Loader uses software path +{ + initFromLoader( loader ); +} + +void MovieGl::initGl( bool hardwareAccelerated ) +{ + mHardwareAccelerated = hardwareAccelerated; +} + +void MovieGl::initFromUrl( const Url& url ) +{ + try { + mf::MediaEnginePlayer::staticInitialize(); + + auto format = mf::MediaEnginePlayer::Format().hardwareAccelerated( mHardwareAccelerated ); + mImpl = std::make_shared( loadUrl( url ), format ); + mHardwareAccelerated = mImpl->isHardwareAccelerated(); + connectSignals(); + + mUpdateConnection = app::App::get()->getSignalUpdate().connect( [this] { updateFrame(); } ); + } + catch( const std::exception& e ) { + CI_LOG_E( "Failed to load movie from URL: " << e.what() ); + throw MswUrlInvalidExc(); + } +} + +void MovieGl::initFromPath( const fs::path& filePath ) +{ + if( ! fs::exists( filePath ) ) { + throw MswPathInvalidExc(); + } + + try { + mf::MediaEnginePlayer::staticInitialize(); + + auto format = mf::MediaEnginePlayer::Format().hardwareAccelerated( mHardwareAccelerated ); + mImpl = std::make_shared( loadFile( filePath ), format ); + mHardwareAccelerated = mImpl->isHardwareAccelerated(); + connectSignals(); + + mUpdateConnection = app::App::get()->getSignalUpdate().connect( [this] { updateFrame(); } ); + } + catch( const std::exception& e ) { + CI_LOG_E( "Failed to load movie from path: " << e.what() ); + throw MswFileInvalidExc(); + } +} + +MovieGl::~MovieGl() +{ + // Release the frame lease before mImpl is destroyed + mFrameLeaseHolder.reset(); +} + +gl::TextureRef MovieGl::getTexture() +{ + if( ! mImpl ) + return nullptr; + + // Release old lease (unlocks texture) before acquiring new one (locks for GL access) + if( ! mFrameLeaseHolder ) + mFrameLeaseHolder = std::make_unique(); + mFrameLeaseHolder->lease.reset(); + + if( auto lease = mImpl->getTexture() ) { + mFrameLeaseHolder->lease = std::move( lease ); + if( auto tex = mFrameLeaseHolder->lease->toTexture() ) + mTexture = tex; + } + else if( ! mHardwareAccelerated ) { + // For software path, create texture from surface + if( auto surface = mImpl->getSurface() ) { + mTexture = gl::Texture::create( *surface, gl::Texture::Format().loadTopDown() ); + } + } + + return mTexture; +} + +} } // namespace cinder::qtime \ No newline at end of file diff --git a/src/cinder/qtime/QuickTimeImplMsw.cpp b/src/cinder/qtime/QuickTimeImplMsw.cpp new file mode 100644 index 0000000000..ca2d6039c3 --- /dev/null +++ b/src/cinder/qtime/QuickTimeImplMsw.cpp @@ -0,0 +1,374 @@ +/* + Copyright (c) 2025, The Cinder Project, All rights reserved. + + This code is intended for use with the Cinder C++ library: http://libcinder.org + + Redistribution and use in source and binary forms, with or without modification, are permitted provided that + the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and + the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and + the following disclaimer in the documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + Windows 64-bit Media Foundation GL implementation. + Based on AX-MediaPlayer by Andrew Wright (@axjxwright). + + Hardware acceleration uses WGL_NV_DX_interop for D3D11/OpenGL texture sharing. + Falls back to WIC/CPU path if hardware acceleration is unavailable. + */ + +#include "cinder/Cinder.h" + +#include "cinder/qtime/QuickTimeImplMsw.h" +#include "cinder/qtime/mf/MediaEnginePlayer.h" +#include "cinder/app/App.h" +#include "cinder/Log.h" + +using namespace ci; + +namespace cinder { namespace qtime { + +// MovieBase::Impl is just a typedef for the internal MediaEnginePlayer +class MovieBase::Impl : public mf::MediaEnginePlayer { + public: + using mf::MediaEnginePlayer::MediaEnginePlayer; +}; + +// MovieBase implementation + +MovieBase::MovieBase() + : mWidth( 0 ) , mHeight( 0 ) , mFrameCount( -1 ) + , mFrameRate( 30.0f ) , mDuration( 0.0f ) + , mLoaded( false ), mPlayThroughOk( false ), mPlayingForward( true ), mLoop( false ) + , mPalindrome( false ), mHasAudio( false ), mHasVideo( false ), mPlaying( false ) +{ +} + +MovieBase::~MovieBase() +{ + if( mUpdateConnection.isConnected() ) + mUpdateConnection.disconnect(); +} + +void MovieBase::init() +{ + // Called after mImpl is set up +} + +void MovieBase::initFromUrl( const Url& url ) +{ + try { + mf::MediaEnginePlayer::staticInitialize(); + + auto format = mf::MediaEnginePlayer::Format().hardwareAccelerated( false ); // Surface uses WIC + mImpl = std::make_shared( loadUrl( url ), format ); + connectSignals(); + + // Connect to app update for frame processing + mUpdateConnection = app::App::get()->getSignalUpdate().connect( [this] { updateFrame(); } ); + } + catch( const std::exception& e ) { + CI_LOG_E( "Failed to load movie from URL: " << e.what() ); + throw MswUrlInvalidExc(); + } +} + +void MovieBase::initFromPath( const fs::path& filePath ) +{ + if( ! fs::exists( filePath ) ) { + throw MswPathInvalidExc(); + } + + try { + mf::MediaEnginePlayer::staticInitialize(); + + auto format = mf::MediaEnginePlayer::Format().hardwareAccelerated( false ); + mImpl = std::make_shared( loadFile( filePath ), format ); + connectSignals(); + + mUpdateConnection = app::App::get()->getSignalUpdate().connect( [this] { updateFrame(); } ); + } + catch( const std::exception& e ) { + CI_LOG_E( "Failed to load movie from path: " << e.what() ); + throw MswFileInvalidExc(); + } +} + +void MovieBase::initFromLoader( const MovieLoader& loader ) +{ + mImpl = std::const_pointer_cast( std::static_pointer_cast( loader.getImpl() ) ); + if( mImpl ) { + connectSignals(); + mUpdateConnection = app::App::get()->getSignalUpdate().connect( [this] { updateFrame(); } ); + } +} + +void MovieBase::connectSignals() +{ + if( ! mImpl ) + return; + + mImpl->signalReady.connect( [this] { + mWidth = mImpl->getSize().x; + mHeight = mImpl->getSize().y; + mDuration = mImpl->getDuration(); + mFrameRate = mImpl->getFrameRate(); + mHasAudio = mImpl->hasAudio(); + mHasVideo = mImpl->hasVideo(); + mLoaded = true; + mSignalReady.emit(); + } ); + + mImpl->signalComplete.connect( [this] { mSignalEnded.emit(); } ); + + mImpl->signalSeekEnd.connect( [this] { mSignalJumped.emit(); } ); +} + +void MovieBase::updateFrame() +{ + if( mImpl ) { + mImpl->update(); + + if( mImpl->checkNewFrame() ) { + mSignalNewFrame.emit(); + } + } +} + +bool MovieBase::checkPlaythroughOk() +{ + // For local files, assume always OK once loaded + return mLoaded; +} + +int32_t MovieBase::getNumFrames() +{ + if( mFrameCount < 0 && mDuration > 0 && mFrameRate > 0 ) { + mFrameCount = static_cast( mDuration * mFrameRate ); + } + return mFrameCount; +} + +bool MovieBase::checkNewFrame() +{ + return mImpl ? mImpl->checkNewFrame() : false; +} + +float MovieBase::getCurrentTime() const +{ + return mImpl ? mImpl->getPositionInSeconds() : 0.0f; +} + +void MovieBase::seekToTime( float seconds ) +{ + if( mImpl ) + mImpl->seekToSeconds( seconds ); +} + +void MovieBase::seekToFrame( int frame ) +{ + if( mFrameRate > 0 ) { + seekToTime( static_cast( frame ) / mFrameRate ); + } +} + +void MovieBase::seekToStart() +{ + seekToTime( 0.0f ); +} + +void MovieBase::seekToEnd() +{ + if( mDuration > 0 ) + seekToTime( mDuration ); +} + +void MovieBase::setActiveSegment( float startTime, float duration ) +{ + CI_LOG_W( "setActiveSegment() is not supported on Windows Media Foundation" ); +} + +void MovieBase::resetActiveSegment() +{ + // No-op - active segments not supported +} + +void MovieBase::setLoop( bool loop, bool palindrome ) +{ + mLoop = loop; + mPalindrome = palindrome; + + if( palindrome ) { + CI_LOG_W( "Palindrome looping is not supported on Windows Media Foundation, using normal loop" ); + } + + if( mImpl ) + mImpl->setLoop( loop ); +} + +bool MovieBase::stepForward() +{ + if( mImpl ) { + mImpl->frameStep( 1 ); + return true; + } + return false; +} + +bool MovieBase::stepBackward() +{ + if( mImpl ) { + mImpl->frameStep( -1 ); + return true; + } + return false; +} + +bool MovieBase::setRate( float rate ) +{ + if( mImpl ) { + mPlayingForward = ( rate >= 0 ); + return mImpl->setPlaybackRate( rate ); + } + return false; +} + +void MovieBase::setVolume( float volume ) +{ + if( mImpl ) + mImpl->setVolume( volume ); +} + +float MovieBase::getVolume() const +{ + return mImpl ? mImpl->getVolume() : 1.0f; +} + +bool MovieBase::isPlaying() const +{ + return mImpl ? mImpl->isPlaying() : false; +} + +bool MovieBase::isDone() const +{ + return mImpl ? mImpl->isComplete() : true; +} + +void MovieBase::play( bool toggle ) +{ + if( mImpl ) { + if( toggle ) + mImpl->togglePlayback(); + else + mImpl->play(); + } +} + +void MovieBase::stop() +{ + if( mImpl ) + mImpl->pause(); +} + +// MovieSurface implementation + +MovieSurface::MovieSurface( const Url& url ) + : MovieBase() +{ + initFromUrl( url ); +} + +MovieSurface::MovieSurface( const fs::path& path ) + : MovieBase() +{ + initFromPath( path ); +} + +MovieSurface::MovieSurface( const MovieLoader& loader ) + : MovieBase() +{ + initFromLoader( loader ); +} + +MovieSurface::~MovieSurface() +{ +} + +MovieSurfaceRef MovieSurface::create( const MovieLoaderRef& loader ) +{ + return MovieSurfaceRef( new MovieSurface( *loader ) ); +} + +Surface8uRef MovieSurface::getSurface() +{ + if( mImpl ) { + return mImpl->getSurface(); + } + return nullptr; +} + +// MovieLoader implementation + +MovieLoader::MovieLoader( const Url& url ) + : mUrl( url ) + , mOwnsMovie( true ) +{ + mf::MediaEnginePlayer::staticInitialize(); + + try { + auto format = mf::MediaEnginePlayer::Format().hardwareAccelerated( false ); + mImpl = std::make_shared( loadUrl( url ), format ); + } + catch( const std::exception& e ) { + CI_LOG_E( "MovieLoader failed: " << e.what() ); + } +} + +MovieLoader::~MovieLoader() +{ +} + +bool MovieLoader::checkLoaded() const +{ + return mImpl ? mImpl->isReady() : false; +} + +bool MovieLoader::checkPlayable() const +{ + return checkLoaded(); +} + +bool MovieLoader::checkPlaythroughOk() const +{ + return checkLoaded(); +} + +void MovieLoader::waitForLoaded() const +{ + // Spin wait - not ideal but matches existing API + while( mImpl && ! mImpl->isReady() ) { + std::this_thread::sleep_for( std::chrono::milliseconds( 10 ) ); + } +} + +void MovieLoader::waitForPlayable() const +{ + waitForLoaded(); +} + +void MovieLoader::waitForPlayThroughOk() const +{ + waitForLoaded(); +} + +} } // namespace cinder::qtime \ No newline at end of file diff --git a/src/cinder/qtime/mf/DXGIRenderPath.cpp b/src/cinder/qtime/mf/DXGIRenderPath.cpp new file mode 100644 index 0000000000..dcd9ad286e --- /dev/null +++ b/src/cinder/qtime/mf/DXGIRenderPath.cpp @@ -0,0 +1,322 @@ +/* + Copyright (c) 2025, The Cinder Project, All rights reserved. + + This code is intended for use with the Cinder C++ library: http://libcinder.org + + Redistribution and use in source and binary forms, with or without modification, are permitted provided that + the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and + the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and + the following disclaimer in the documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + Windows 64-bit Media Foundation GL implementation. + Based on AX-MediaPlayer by Andrew Wright (@axjxwright). + + DXGI/OpenGL interop render path for hardware-accelerated video playback. +*/ + +#include "cinder/Cinder.h" + +#pragma comment( lib, "d3d11.lib" ) + +#include "cinder/qtime/mf/DXGIRenderPath.h" +#include "cinder/app/App.h" +#include "cinder/Log.h" + +#include "glad/glad_wgl.h" + +using namespace ci; + +namespace cinder { namespace qtime { namespace mf { + +//! Singleton D3D/GL interop context +class InteropContext : public ci::Noncopyable { + public: + static void staticInitialize(); + static InteropContext& get(); + + ~InteropContext(); + + ID3D11Device* device() const { return mDevice.Get(); } + HANDLE handle() const { return mInteropHandle; } + IMFDXGIDeviceManager* dxgiManager() const { return mDxgiManager.Get(); } + + SharedTextureRef createSharedTexture( const ivec2& size ); + bool isValid() const { return mIsValid; } + + protected: + InteropContext(); + + ComPtr mDevice{ nullptr }; + ComPtr mDxgiManager{ nullptr }; + UINT mDxgiResetToken{ 0 }; + + HANDLE mInteropHandle{ nullptr }; + bool mIsValid{ false }; +}; + +// Singleton - deliberately leaked to outlive all MediaPlayers +static InteropContext* sInteropContext{ nullptr }; +static std::once_flag sInteropInitFlag; + +void InteropContext::staticInitialize() +{ + std::call_once( sInteropInitFlag, []() { + if( ! sInteropContext ) { + sInteropContext = new InteropContext(); + } + } ); +} + +InteropContext& InteropContext::get() +{ + assert( sInteropContext ); + return *sInteropContext; +} + +InteropContext::InteropContext() + : mIsValid( false ) +{ + UINT deviceFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_VIDEO_SUPPORT; + +#ifndef NDEBUG + // Try with debug layer first, fall back if not installed + deviceFlags |= D3D11_CREATE_DEVICE_DEBUG; +#endif + + if( ! SUCCEEDED( ::MFCreateDXGIDeviceManager( &mDxgiResetToken, mDxgiManager.GetAddressOf() ) ) ) { + CI_LOG_E( "Failed to create DXGI device manager" ); + return; + } + + HRESULT hr = ::D3D11CreateDevice( nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, deviceFlags, nullptr, 0, D3D11_SDK_VERSION, mDevice.GetAddressOf(), nullptr, nullptr ); + +#ifndef NDEBUG + if( FAILED( hr ) && ( deviceFlags & D3D11_CREATE_DEVICE_DEBUG ) ) { + // Debug layer not installed, retry without + deviceFlags &= ~D3D11_CREATE_DEVICE_DEBUG; + hr = ::D3D11CreateDevice( nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, deviceFlags, nullptr, 0, D3D11_SDK_VERSION, mDevice.GetAddressOf(), nullptr, nullptr ); + } +#endif + + if( FAILED( hr ) ) { + CI_LOG_E( "Failed to create D3D11 device: 0x" << std::hex << hr ); + return; + } + + ComPtr multiThread{ nullptr }; + if( SUCCEEDED( mDevice->QueryInterface( multiThread.GetAddressOf() ) ) ) { + multiThread->SetMultithreadProtected( true ); + } + else { + CI_LOG_E( "Failed to enable D3D11 multithreading" ); + return; + } + + if( ! SUCCEEDED( mDxgiManager->ResetDevice( mDevice.Get(), mDxgiResetToken ) ) ) { + CI_LOG_E( "Failed to reset DXGI device" ); + return; + } + + // Check for WGL_NV_DX_interop support + if( ! GLAD_WGL_NV_DX_interop ) { + CI_LOG_W( "WGL_NV_DX_interop not available - D3D11/GL interop disabled" ); + return; + } + + mInteropHandle = ::wglDXOpenDeviceNV( mDevice.Get() ); + mIsValid = mInteropHandle != nullptr; + + if( ! mIsValid ) { + CI_LOG_W( "Failed to open DX interop device - D3D11/GL interop disabled" ); + } +} + +SharedTextureRef InteropContext::createSharedTexture( const ivec2& size ) +{ + auto texture = std::make_shared( size ); + if( texture->isValid() ) + return texture; + + return nullptr; +} + +InteropContext::~InteropContext() +{ + if( mInteropHandle != nullptr ) { + ::wglDXCloseDeviceNV( mInteropHandle ); + mInteropHandle = nullptr; + } + + mDxgiManager = nullptr; +} + +// SharedTexture implementation + +SharedTexture::SharedTexture( const ivec2& size ) +{ + D3D11_TEXTURE2D_DESC desc = {}; + desc.Width = size.x; + desc.Height = size.y; + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + desc.SampleDesc.Count = 1; + desc.SampleDesc.Quality = 0; + desc.BindFlags = D3D11_BIND_RENDER_TARGET; + desc.Usage = D3D11_USAGE_DEFAULT; + + auto& context = InteropContext::get(); + + if( SUCCEEDED( context.device()->CreateTexture2D( &desc, nullptr, mDxTexture.GetAddressOf() ) ) ) { + gl::Texture::Format fmt; + fmt.internalFormat( GL_RGBA ).loadTopDown(); + + mGlTexture = gl::Texture::create( size.x, size.y, fmt ); + mShareHandle = ::wglDXRegisterObjectNV( context.handle(), mDxTexture.Get(), mGlTexture->getId(), GL_TEXTURE_2D, WGL_ACCESS_READ_ONLY_NV ); + mIsValid = mShareHandle != nullptr; + } +} + +bool SharedTexture::lock() +{ + std::lock_guard guard( mLockMutex ); + if( mIsLocked ) + return false; // Already locked + mIsLocked = ::wglDXLockObjectsNV( InteropContext::get().handle(), 1, &mShareHandle ); + return mIsLocked; +} + +bool SharedTexture::unlock() +{ + std::lock_guard guard( mLockMutex ); + if( ! mIsLocked ) + return false; // Not locked + if( ::wglDXUnlockObjectsNV( InteropContext::get().handle(), 1, &mShareHandle ) ) { + mIsLocked = false; + return true; + } + return false; +} + +SharedTexture::~SharedTexture() +{ + if( mShareHandle != nullptr ) { + // Only cleanup if we have a valid GL context + if( ::wglGetCurrentContext() != nullptr ) { + if( isLocked() ) + ::wglDXUnlockObjectsNV( InteropContext::get().handle(), 1, &mShareHandle ); + ::wglDXUnregisterObjectNV( InteropContext::get().handle(), mShareHandle ); + mShareHandle = nullptr; + } + } +} + +// DXGIRenderPath implementation + +DXGIRenderPath::DXGIRenderPath( MediaEnginePlayer& owner, const ci::DataSourceRef& source ) + : RenderPath( owner, source ) +{ +} + +bool DXGIRenderPath::initialize( IMFAttributes& attributes ) +{ + InteropContext::staticInitialize(); + + auto& interop = InteropContext::get(); + if( ! interop.isValid() ) + return false; + + if( SUCCEEDED( attributes.SetUnknown( MF_MEDIA_ENGINE_DXGI_MANAGER, interop.dxgiManager() ) ) ) + return true; + + CI_LOG_E( "Failed to set DXGI manager attribute" ); + return false; +} + +bool DXGIRenderPath::initializeRenderTarget( const ci::ivec2& size ) +{ + if( ! mSharedTexture || size != mSize ) { + mSize = size; + mSharedTexture = InteropContext::get().createSharedTexture( size ); + } + + return ( mSharedTexture != nullptr ); +} + +bool DXGIRenderPath::processFrame() +{ + if( mSharedTexture ) { + // Unlock for D3D11 write access + if( mSharedTexture->isLocked() ) + mSharedTexture->unlock(); + + auto& engine = mOwner.mMediaEngine; + + MFVideoNormalizedRect srcRect{ 0.0f, 0.0f, 1.0f, 1.0f }; + RECT dstRect{ 0, 0, mSize.x, mSize.y }; + MFARGB black{ 0, 0, 0, 0 }; + + HRESULT hr = engine->TransferVideoFrame( mSharedTexture->dxTextureHandle(), &srcRect, &dstRect, &black ); + if( SUCCEEDED( hr ) ) { + // Flush D3D11 commands before GL can safely access the texture + ComPtr context; + InteropContext::get().device()->GetImmediateContext( context.GetAddressOf() ); + if( context ) + context->Flush(); + return true; + } + } + + return false; +} + +MediaEnginePlayer::FrameLeaseRef DXGIRenderPath::getFrameLease() const +{ + return std::make_unique( mSharedTexture ); +} + +DXGIRenderPath::~DXGIRenderPath() +{ + mSharedTexture = nullptr; +} + +bool DXGIRenderPath::isContextValid() const +{ + InteropContext::staticInitialize(); + return InteropContext::get().isValid(); +} + +// DXGIRenderPathFrameLease implementation + +DXGIRenderPathFrameLease::DXGIRenderPathFrameLease( const SharedTextureRef& texture ) + : mTexture( texture ) +{ + if( mTexture ) + mTexture->lock(); +} + +ci::gl::TextureRef DXGIRenderPathFrameLease::toTexture() const +{ + return mTexture ? mTexture->glTextureHandle() : nullptr; +} + +DXGIRenderPathFrameLease::~DXGIRenderPathFrameLease() +{ + if( mTexture && mTexture->isLocked() ) { + mTexture->unlock(); + } +} + +} } } // namespace cinder::qtime::mf diff --git a/src/cinder/qtime/mf/MediaEnginePlayer.cpp b/src/cinder/qtime/mf/MediaEnginePlayer.cpp new file mode 100644 index 0000000000..620b7fd328 --- /dev/null +++ b/src/cinder/qtime/mf/MediaEnginePlayer.cpp @@ -0,0 +1,749 @@ +/* + Copyright (c) 2025, The Cinder Project, All rights reserved. + + This code is intended for use with the Cinder C++ library: http://libcinder.org + + Redistribution and use in source and binary forms, with or without modification, are permitted provided that + the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and + the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and + the following disclaimer in the documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + Windows 64-bit Media Foundation GL implementation. + Based on AX-MediaPlayer by Andrew Wright (@axjxwright). + + Hardware acceleration uses WGL_NV_DX_interop for D3D11/OpenGL texture sharing. + Falls back to WIC/CPU path if hardware acceleration is unavailable. + */ +#include "cinder/Cinder.h" + +#include "cinder/qtime/mf/MediaEnginePlayer.h" +#include "cinder/qtime/mf/DXGIRenderPath.h" +#include "cinder/qtime/mf/WICRenderPath.h" + +#include "cinder/app/App.h" +#include "cinder/DataSource.h" +#include "cinder/Log.h" + +#include +#include +#include + +#include +#include +#include +#include + +#pragma comment( lib, "Shlwapi.lib" ) + +using namespace ci; + +namespace cinder { namespace qtime { namespace mf { + +namespace { + +std::atomic sNumMediaFoundationInstances{ 0 }; +std::atomic sIsMFInitialized{ false }; +std::once_flag sMFInitFlag; + +void onMediaPlayerCreated() +{ + if( sNumMediaFoundationInstances++ == 0 ) { + HRESULT hr = ::MFStartup( MF_VERSION ); + sIsMFInitialized = SUCCEEDED( hr ); + if( ! sIsMFInitialized ) { + CI_LOG_E( "MFStartup failed with error: " << std::hex << hr ); + } + } +} + +void onMediaPlayerDestroyed() +{ + if( --sNumMediaFoundationInstances == 0 ) { + ::MFShutdown(); + sIsMFInitialized = false; + } +} + +class MFCallbackBase : public IMFAsyncCallback { + public: + MFCallbackBase( DWORD flags = 0, DWORD queue = MFASYNC_CALLBACK_QUEUE_MULTITHREADED ) + : mFlags( flags ) + , mQueue( queue ) + { + } + virtual ~MFCallbackBase() = default; + + DWORD getQueue() const { return mQueue; } + DWORD getFlags() const { return mFlags; } + + IFACEMETHODIMP GetParameters( _Out_ DWORD* flags, _Out_ DWORD* queue ) + { + *flags = mFlags; + *queue = mQueue; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE QueryInterface( REFIID riid, LPVOID* ppvObj ) + { + if( ! ppvObj ) + return E_INVALIDARG; + + *ppvObj = NULL; + if( riid == IID_IMFAsyncCallback ) { + *ppvObj = (LPVOID)this; + AddRef(); + return NOERROR; + } + return E_NOINTERFACE; + } + + ULONG STDMETHODCALLTYPE AddRef() + { + ::InterlockedIncrement( &mRefCount ); + return mRefCount; + } + + ULONG STDMETHODCALLTYPE Release() + { + ULONG count = ::InterlockedDecrement( &mRefCount ); + if( 0 == mRefCount ) { + delete this; + } + return count; + } + + private: + DWORD mFlags = 0; + DWORD mQueue = 0; + ULONG mRefCount = 1; // Start at 1, caller owns initial ref +}; + +class MFWorkItem : public MFCallbackBase { + public: + MFWorkItem( std::function callback, DWORD flags = 0, DWORD queue = MFASYNC_CALLBACK_QUEUE_MULTITHREADED ) + : MFCallbackBase( flags, queue ) + , mCallback( callback ) + { + } + + IFACEMETHODIMP Invoke( _In_opt_ IMFAsyncResult* /*result*/ ) noexcept override + try { + mCallback(); + Release(); + return S_OK; + } + catch( const std::exception& e ) { + CI_LOG_E( "MFWorkItem error: " << e.what() ); + return E_ABORT; + } + + private: + std::function mCallback; +}; + +void MFPutWorkItemInternal( std::function callback ) +{ + ComPtr workItem{ new MFWorkItem( callback ) }; + // workItem starts with refcount 1, MFPutWorkItem2 will AddRef internally + // ComPtr destructor releases one ref, MF holds another until Invoke completes + ::MFPutWorkItem2( workItem->getQueue(), 0, workItem.Get(), nullptr ); +} + +MediaEnginePlayer::Error AXErrorFromMFError( MF_MEDIA_ENGINE_ERR error ) +{ + return static_cast( error ); +} + +struct SafeBSTR { + SafeBSTR( const std::wstring& str ) + { + assert( ! str.empty() ); + mStr = ::SysAllocStringLen( str.data(), static_cast( str.size() ) ); + } + + operator BSTR() const { return mStr; } + + ~SafeBSTR() + { + ::SysFreeString( mStr ); + mStr = nullptr; + } + + protected: + BSTR mStr{ nullptr }; +}; + +} // anonymous namespace + +void runSynchronousInMTAThread( std::function callback ) +{ + APTTYPE apartmentType = {}; + APTTYPEQUALIFIER qualifier = {}; + + bool inited = SUCCEEDED( ::CoGetApartmentType( &apartmentType, &qualifier ) ); + assert( inited ); + + if( apartmentType == APTTYPE_MTA ) { + callback(); + } + else { + std::condition_variable wait; + std::mutex lock; + std::atomic isDone{ false }; + + MFPutWorkItemInternal( [&]() { + callback(); + isDone.store( true ); + wait.notify_one(); + } ); + + std::unique_lock lk{ lock }; + wait.wait( lk, [&] { return isDone.load(); } ); + } +} + +void runSynchronousInMainThread( std::function callback ) +{ + app::App::get()->dispatchSync( [&] { callback(); } ); +} + +void MediaEnginePlayer::staticInitialize() +{ + std::call_once( sMFInitFlag, []() { + if( ! sIsMFInitialized ) { + sIsMFInitialized = SUCCEEDED( ::MFStartup( MF_VERSION ) ); + } + } ); +} + +void MediaEnginePlayer::staticShutdown() +{ + if( sIsMFInitialized ) { + ::MFShutdown(); + sIsMFInitialized = false; + } +} + +const std::string& MediaEnginePlayer::errorToString( Error error ) +{ + static std::unordered_map sErrors = { + { Error::NoError, "No Error" }, + { Error::Aborted, "Aborted" }, + { Error::NetworkError, "Network Error" }, + { Error::DecodingError, "Decoding Error" }, + { Error::SourceNotSupported, "Source Not Supported" }, + { Error::Encrypted, "Source Is Encrypted" }, + }; + + auto it = sErrors.find( error ); + if( it != sErrors.end() ) { + return it->second; + } + else { + static std::string sUnknownError = "Unknown Error"; + return sUnknownError; + } +} + +MediaEnginePlayer::MediaEnginePlayer( const DataSourceRef& source, const Format& format ) + : mSource( source ) + , mFormat( format ) +{ + onMediaPlayerCreated(); + + if( ! sIsMFInitialized ) { + throw std::runtime_error( "MediaFoundation not initialized!" ); + } + + ComPtr factory; + if( SUCCEEDED( ::CoCreateInstance( CLSID_MFMediaEngineClassFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS( &factory ) ) ) ) { + ComPtr attributes; + ::MFCreateAttributes( attributes.GetAddressOf(), 0 ); + attributes->SetUINT32( MF_MEDIA_ENGINE_VIDEO_OUTPUT_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM ); + attributes->SetUnknown( MF_MEDIA_ENGINE_CALLBACK, this ); + + DWORD flags = MF_MEDIA_ENGINE_REAL_TIME_MODE; + + if( ! mFormat.isAudioEnabled() ) { + flags |= MF_MEDIA_ENGINE_FORCEMUTE; + } + + if( mFormat.isAudioOnly() ) { + flags |= MF_MEDIA_ENGINE_AUDIOONLY; + } + + // Set up render path - DXGI for hardware accelerated, WIC for software + if( mFormat.isHardwareAccelerated() ) { + mRenderPath = std::make_unique( *this, source ); + if( ! mRenderPath->initialize( *attributes.Get() ) ) { + CI_LOG_W( "DXGI initialization failed, falling back to WIC" ); + mRenderPath = std::make_unique( *this, source ); + mRenderPath->initialize( *attributes.Get() ); + mFormat = Format().hardwareAccelerated( false ); + } + } + else { + mRenderPath = std::make_unique( *this, source ); + mRenderPath->initialize( *attributes.Get() ); + } + + HRESULT hr = factory->CreateInstance( flags, attributes.Get(), mMediaEngine.GetAddressOf() ); + if( SUCCEEDED( hr ) ) { + std::wstring actualUrl; + if( mSource->isUrl() ) { + auto str = mSource->getUrl().str(); + actualUrl = { str.begin(), str.end() }; + } + else { + // Convert Windows path to file:// URL using Windows API + std::wstring filePath = mSource->getFilePath().wstring(); + DWORD urlLen = 2048; + wchar_t urlBuffer[2048]; + HRESULT urlHr = ::UrlCreateFromPathW( filePath.c_str(), urlBuffer, &urlLen, 0 ); + if( SUCCEEDED( urlHr ) ) { + actualUrl = urlBuffer; + } + else { + // Fallback: use raw path + CI_LOG_W( "UrlCreateFromPathW failed, using raw path" ); + actualUrl = filePath; + } + } + + mMediaEngine->SetSource( SafeBSTR{ actualUrl } ); + mMediaEngine->Load(); + mMediaEngine->QueryInterface( mMediaEngineEx.GetAddressOf() ); + } + else { + CI_LOG_E( "MediaEngine CreateInstance failed: 0x" << std::hex << hr ); + } + } +} + +HRESULT MediaEnginePlayer::EventNotify( DWORD event, DWORD_PTR param1, DWORD param2 ) +{ + // For ERROR events, log but don't abort - video may still work if only audio codec is unsupported + if( event == MF_MEDIA_ENGINE_EVENT_ERROR ) { + MF_MEDIA_ENGINE_ERR errorCode = static_cast( param1 ); + HRESULT extendedError = static_cast( param2 ); + std::string errorDesc; + switch( errorCode ) { + case MF_MEDIA_ENGINE_ERR_NOERROR: + errorDesc = "No error"; + break; + case MF_MEDIA_ENGINE_ERR_ABORTED: + errorDesc = "Aborted"; + break; + case MF_MEDIA_ENGINE_ERR_NETWORK: + errorDesc = "Network error"; + break; + case MF_MEDIA_ENGINE_ERR_DECODE: + errorDesc = "Decode error"; + break; + case MF_MEDIA_ENGINE_ERR_SRC_NOT_SUPPORTED: + errorDesc = "Source not supported (possibly unsupported codec)"; + break; + case MF_MEDIA_ENGINE_ERR_ENCRYPTED: + errorDesc = "Encrypted content"; + break; + default: + errorDesc = "Unknown error code " + std::to_string( errorCode ); + break; + } + CI_LOG_W( "MediaEngine error: " << errorDesc << " (HRESULT=0x" << std::hex << extendedError << ") - continuing playback" ); + + // Don't queue error events - just log them. Video may still play with missing audio. + return S_OK; + } + + // Don't queue events if shutting down + if( mIsShuttingDown.load() ) + return S_OK; + + // Queue events for main thread processing + std::unique_lock lk( mEventMutex ); + if( ! mIsShuttingDown.load() ) // Double-check after acquiring lock + { + mEventQueue.push( Event{ event, param1, param2 } ); + } + return S_OK; +} + +void MediaEnginePlayer::processEvent( DWORD evt, DWORD_PTR param1, DWORD param2 ) +{ + switch( evt ) { + case MF_MEDIA_ENGINE_EVENT_DURATIONCHANGE: + { + mDuration = static_cast( mMediaEngine->GetDuration() ); + break; + } + + case MF_MEDIA_ENGINE_EVENT_LOADEDMETADATA: + { + mDuration = static_cast( mMediaEngine->GetDuration() ); + + DWORD w, h; + if( SUCCEEDED( mMediaEngine->GetNativeVideoSize( &w, &h ) ) ) { + mSize = ivec2( w, h ); + mRenderPath->initializeRenderTarget( mSize ); + } + + // IMFMediaEngine doesn't directly expose framerate, use default + mFrameRate = 30.0f; + + mHasMetadata = true; + signalReady.emit(); + break; + } + + case MF_MEDIA_ENGINE_EVENT_PLAY: + signalPlay.emit(); + break; + + case MF_MEDIA_ENGINE_EVENT_PAUSE: + signalPause.emit(); + break; + + case MF_MEDIA_ENGINE_EVENT_ENDED: + signalComplete.emit(); + break; + + case MF_MEDIA_ENGINE_EVENT_SEEKING: + signalSeekStart.emit(); + mTimeInSecondsAtStartOfSeek = getPositionInSeconds(); + break; + + case MF_MEDIA_ENGINE_EVENT_SEEKED: + { + signalSeekEnd.emit(); + if( isLooping() ) { + // Detect loop by checking if we seeked back to start + auto now = getPositionInSeconds(); + if( now < 0.05f && ( ( now - mTimeInSecondsAtStartOfSeek ) < 0.01f ) ) { + signalComplete.emit(); + } + } + break; + } + + case MF_MEDIA_ENGINE_EVENT_BUFFERINGSTARTED: + signalBufferingStart.emit(); + break; + + case MF_MEDIA_ENGINE_EVENT_BUFFERINGENDED: + signalBufferingEnd.emit(); + break; + + case MF_MEDIA_ENGINE_EVENT_ERROR: + { + MF_MEDIA_ENGINE_ERR error = static_cast( param1 ); + signalError.emit( AXErrorFromMFError( error ) ); + break; + } + } +} + +void MediaEnginePlayer::updateEvents() +{ + Event evt; + bool hasEvent = false; + do { + hasEvent = false; + { + std::unique_lock lk( mEventMutex ); + if( ! mEventQueue.empty() ) { + evt = mEventQueue.front(); + mEventQueue.pop(); + hasEvent = true; + } + } + if( hasEvent ) { + processEvent( evt.eventId, evt.param1, evt.param2 ); + } + } while( hasEvent ); +} + +HRESULT STDMETHODCALLTYPE MediaEnginePlayer::QueryInterface( REFIID riid, LPVOID* ppvObj ) +{ + if( __uuidof( IMFMediaEngineNotify ) == riid ) { + *ppvObj = static_cast( this ); + } + else { + *ppvObj = nullptr; + return E_NOINTERFACE; + } + + AddRef(); + return S_OK; +} + +// Memory is owned by shared_ptr, but we need valid COM ref counting behavior +// Return non-zero values to satisfy COM expectations without actually destroying +ULONG STDMETHODCALLTYPE MediaEnginePlayer::AddRef() +{ + return 2; +} +ULONG STDMETHODCALLTYPE MediaEnginePlayer::Release() +{ + return 1; +} + +void MediaEnginePlayer::play() +{ + if( mMediaEngine ) { + mMediaEngine->Play(); + } +} + +void MediaEnginePlayer::pause() +{ + if( mMediaEngine ) { + mMediaEngine->Pause(); + } +} + +void MediaEnginePlayer::togglePlayback() +{ + if( isPaused() ) + play(); + else + pause(); +} + +bool MediaEnginePlayer::setPlaybackRate( float rate ) +{ + if( mMediaEngine && isPlaybackRateSupported( rate ) ) { + return SUCCEEDED( mMediaEngine->SetPlaybackRate( static_cast( rate ) ) ); + } + return false; +} + +float MediaEnginePlayer::getPlaybackRate() const +{ + if( mMediaEngine ) { + return static_cast( mMediaEngine->GetPlaybackRate() ); + } + return 1.0f; +} + +bool MediaEnginePlayer::isPlaybackRateSupported( float rate ) const +{ + if( mMediaEngineEx ) { + return mMediaEngineEx->IsPlaybackRateSupported( static_cast( rate ) ); + } + return false; +} + +void MediaEnginePlayer::setMuted( bool mute ) +{ + if( mMediaEngine ) { + runSynchronousInMTAThread( [&] { mMediaEngine->SetMuted( mute ); } ); + } +} + +bool MediaEnginePlayer::isMuted() const +{ + if( mMediaEngine ) { + return static_cast( mMediaEngine->GetMuted() ); + } + return false; +} + +void MediaEnginePlayer::setVolume( float volume ) +{ + if( mMediaEngine ) { + runSynchronousInMTAThread( [&] { mMediaEngine->SetVolume( volume ); } ); + } +} + +float MediaEnginePlayer::getVolume() const +{ + if( mMediaEngine ) { + return static_cast( mMediaEngine->GetVolume() ); + } + return 1.0f; +} + +void MediaEnginePlayer::setLoop( bool loop ) +{ + if( mMediaEngine ) { + mMediaEngine->SetLoop( static_cast( loop ) ); + } +} + +bool MediaEnginePlayer::isLooping() const +{ + if( mMediaEngine ) { + return static_cast( mMediaEngine->GetLoop() ); + } + return false; +} + +float MediaEnginePlayer::getPositionInSeconds() const +{ + if( ! mMediaEngine ) + return -1.0f; + return static_cast( mMediaEngine->GetCurrentTime() ); +} + +void MediaEnginePlayer::seekToSeconds( float seconds, bool approximate ) +{ + if( mMediaEngineEx ) { + mMediaEngineEx->SetCurrentTimeEx( seconds, approximate ? MF_MEDIA_ENGINE_SEEK_MODE_APPROXIMATE : MF_MEDIA_ENGINE_SEEK_MODE_NORMAL ); + } + else if( mMediaEngine ) { + mMediaEngine->SetCurrentTime( static_cast( seconds ) ); + } +} + +void MediaEnginePlayer::seekToPercentage( float normalizedTime, bool approximate ) +{ + if( mDuration > 0.0f ) { + seekToSeconds( normalizedTime * mDuration, approximate ); + } +} + +void MediaEnginePlayer::frameStep( int delta ) +{ + if( mMediaEngineEx ) { + mMediaEngineEx->FrameStep( delta > 0 ? true : false ); + } +} + +bool MediaEnginePlayer::isComplete() const +{ + if( mMediaEngine ) { + return mMediaEngine->IsEnded(); + } + return true; +} + +bool MediaEnginePlayer::isPaused() const +{ + if( mMediaEngine ) { + return mMediaEngine->IsPaused(); + } + return false; +} + +bool MediaEnginePlayer::isPlaying() const +{ + return ! isPaused(); +} + +bool MediaEnginePlayer::isSeeking() const +{ + if( mMediaEngine ) { + return mMediaEngine->IsSeeking(); + } + return false; +} + +bool MediaEnginePlayer::isReady() const +{ + return mHasMetadata; +} + +bool MediaEnginePlayer::hasAudio() const +{ + if( mMediaEngine ) { + return mMediaEngine->HasAudio(); + } + return false; +} + +bool MediaEnginePlayer::hasVideo() const +{ + if( mMediaEngine ) { + return mMediaEngine->HasVideo(); + } + return false; +} + +bool MediaEnginePlayer::update() +{ + // Check if shutting down + if( mIsShuttingDown.load() ) + return false; + + std::shared_lock lock( mShutdownMutex ); + + if( mMediaEngine ) { + // Fallback init if LOADEDMETADATA event wasn't received + if( mRenderPath && mSize.x == 0 && mSize.y == 0 ) { + DWORD w, h; + if( SUCCEEDED( mMediaEngine->GetNativeVideoSize( &w, &h ) ) && w > 0 && h > 0 ) { + mSize = ivec2( w, h ); + mDuration = static_cast( mMediaEngine->GetDuration() ); + if( mRenderPath->initializeRenderTarget( mSize ) ) { + mHasMetadata = true; + signalReady.emit(); + } + } + } + + // Process video frames if we have video OR if we have a valid size + if( hasVideo() || ( mSize.x > 0 && mSize.y > 0 ) ) { + LONGLONG time; + if( mMediaEngine->OnVideoStreamTick( &time ) == S_OK ) { + if( mRenderPath && mRenderPath->processFrame() ) { + mHasNewFrame.store( true ); + } + } + } + } + + updateEvents(); + return false; +} + +const Surface8uRef& MediaEnginePlayer::getSurface() const +{ + mHasNewFrame.store( false ); + return mSurface; +} + +MediaEnginePlayer::FrameLeaseRef MediaEnginePlayer::getTexture() const +{ + mHasNewFrame.store( false ); + return mRenderPath ? mRenderPath->getFrameLease() : nullptr; +} + +MediaEnginePlayer::~MediaEnginePlayer() +{ + // Signal shutdown first + mIsShuttingDown.store( true ); + + // Wait for any in-flight updates + { + std::unique_lock lock( mShutdownMutex ); + } + + // Clean up render path + mRenderPath = nullptr; + mHasNewFrame.store( false ); + + mMediaEngineEx = nullptr; + + if( mMediaEngine ) { + runSynchronousInMTAThread( [&] { mMediaEngine->Shutdown(); } ); + + mMediaEngine = nullptr; + } + + onMediaPlayerDestroyed(); +} + +} } } // namespace cinder::qtime::mf diff --git a/src/cinder/qtime/mf/WICRenderPath.cpp b/src/cinder/qtime/mf/WICRenderPath.cpp new file mode 100644 index 0000000000..225d69a216 --- /dev/null +++ b/src/cinder/qtime/mf/WICRenderPath.cpp @@ -0,0 +1,112 @@ +/* + Copyright (c) 2025, The Cinder Project, All rights reserved. + + This code is intended for use with the Cinder C++ library: http://libcinder.org + + Redistribution and use in source and binary forms, with or without modification, are permitted provided that + the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and + the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and + the following disclaimer in the documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + Windows 64-bit Media Foundation GL implementation. + Based on AX-MediaPlayer by Andrew Wright (@axjxwright). + + Hardware acceleration uses WGL_NV_DX_interop for D3D11/OpenGL texture sharing. + Falls back to WIC/CPU path if hardware acceleration is unavailable. + */ + +#include "cinder/Cinder.h" + +#include "cinder/qtime/mf/WICRenderPath.h" +#include "cinder/Log.h" + +using namespace ci; + +namespace cinder { namespace qtime { namespace mf { + +WICRenderPath::WICRenderPath( MediaEnginePlayer& owner, const ci::DataSourceRef& source ) + : RenderPath( owner, source ) +{ + HRESULT hr = ::CoCreateInstance( CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS( &mWicFactory ) ); + if( FAILED( hr ) ) { + CI_LOG_E( "Failed to create WIC factory, HRESULT: " << std::hex << hr ); + } +} + +bool WICRenderPath::initializeRenderTarget( const ci::ivec2& size ) +{ + if( ! mWicFactory ) + return false; + + if( ! mWicBitmap || size != mSize ) { + mSize = size; + + mOwner.mSurface = Surface8u::create( size.x, size.y, true, SurfaceChannelOrder::BGRA ); + return SUCCEEDED( mWicFactory->CreateBitmap( size.x, size.y, GUID_WICPixelFormat32bppBGRA, WICBitmapCacheOnDemand, mWicBitmap.GetAddressOf() ) ); + } + + return ( mWicBitmap != nullptr ); +} + +bool WICRenderPath::processFrame() +{ + auto& engine = mOwner.mMediaEngine; + if( mWicBitmap ) { + MFVideoNormalizedRect srcRect{ 0.0f, 0.0f, 1.0f, 1.0f }; + RECT dstRect{ 0, 0, mSize.x, mSize.y }; + MFARGB black{ 0, 0, 0, 0 }; + + if( SUCCEEDED( engine->TransferVideoFrame( mWicBitmap.Get(), &srcRect, &dstRect, &black ) ) ) { + ComPtr lockedData; + DWORD flags = WICBitmapLockRead; + WICRect wicSrcRect{ 0, 0, mSize.x, mSize.y }; + + if( SUCCEEDED( mWicBitmap->Lock( &wicSrcRect, flags, lockedData.GetAddressOf() ) ) ) { + UINT stride{ 0 }; + + if( SUCCEEDED( lockedData->GetStride( &stride ) ) ) { + UINT bufferSize{ 0 }; + BYTE* data{ nullptr }; + + if( SUCCEEDED( lockedData->GetDataPointer( &bufferSize, &data ) ) ) { + Surface8u surface( data, mSize.x, mSize.y, stride, SurfaceChannelOrder::BGRA ); + assert( mOwner.mSurface->getSize() == surface.getSize() ); + + mOwner.mSurface->copyFrom( surface, surface.getBounds() ); + return true; + } + } + } + } + } + + return false; +} + +MediaEnginePlayer::FrameLeaseRef WICRenderPath::getFrameLease() const +{ + return std::make_unique( mOwner.mSurface ); +} + +// WICRenderPathFrameLease implementation + +WICRenderPathFrameLease::WICRenderPathFrameLease( const ci::Surface8uRef& surface ) +{ + if( surface ) { + mTexture = gl::Texture::create( *surface, gl::Texture::Format().loadTopDown() ); + } +} + +} } } // namespace cinder::qtime::mf diff --git a/test/QuickTimeStressTest/include/Resources.h b/test/QuickTimeStressTest/include/Resources.h new file mode 100644 index 0000000000..3bdaead2a6 --- /dev/null +++ b/test/QuickTimeStressTest/include/Resources.h @@ -0,0 +1,4 @@ +#pragma once +#include "cinder/CinderResources.h" + +//#define RES_MY_RES CINDER_RESOURCE( ../resources/, image_name.png, 128, IMAGE ) diff --git a/test/QuickTimeStressTest/proj/cmake/CMakeLists.txt b/test/QuickTimeStressTest/proj/cmake/CMakeLists.txt new file mode 100644 index 0000000000..47c167c658 --- /dev/null +++ b/test/QuickTimeStressTest/proj/cmake/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required( VERSION 3.16 FATAL_ERROR ) +set( CMAKE_VERBOSE_MAKEFILE ON ) + +project( QuickTimeStressTest ) + +get_filename_component( CINDER_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../../.." ABSOLUTE ) +get_filename_component( APP_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../" ABSOLUTE ) + +include( "${CINDER_PATH}/proj/cmake/modules/cinderMakeApp.cmake" ) + +ci_make_app( + SOURCES ${APP_PATH}/src/QuickTimeStressTestApp.cpp + CINDER_PATH ${CINDER_PATH} +) diff --git a/test/QuickTimeStressTest/src/QuickTimeStressTestApp.cpp b/test/QuickTimeStressTest/src/QuickTimeStressTestApp.cpp new file mode 100644 index 0000000000..6d96639fa1 --- /dev/null +++ b/test/QuickTimeStressTest/src/QuickTimeStressTestApp.cpp @@ -0,0 +1,550 @@ +#include "cinder/app/App.h" +#include "cinder/app/RendererGl.h" +#include "cinder/gl/gl.h" +#include "cinder/gl/Texture.h" +#include "cinder/Rand.h" +#include "cinder/qtime/QuickTimeGl.h" +#include "cinder/CinderImGui.h" +#include "cinder/Log.h" + +#include + +using namespace ci; +using namespace ci::app; +using namespace std; + +// Configuration +static constexpr int HARD_CAP = 10; // Absolute max simultaneous videos +static constexpr float AVG_OP_INTERVAL = 0.5f; // Average seconds between random operations +static constexpr float PANEL_WIDTH = 400.0f; +static constexpr float CELL_PADDING = 8.0f; // Padding around video cells + +// Log entry for history +struct LogEntry +{ + double timestamp; + string message; + bool isError; +}; + +// Active video slot +struct VideoSlot +{ + qtime::MovieGlRef movie; + fs::path path; + string name; + double loadTime; + int operationCount{ 0 }; +}; + +class QuickTimeStressTestApp : public App { + public: + void setup() override; + void update() override; + void draw() override; + void cleanup() override; + + private: + void scanDirectory( const fs::path& dir ); + void loadRandomVideo(); + void closeRandomVideo(); + void performRandomOperation(); + void logMessage( const string& msg, bool isError = false ); + + void drawVideoGrid(); + void drawStatsPanel(); + + // Candidate video files + vector mCandidates; + set mFailedPaths; + + // Active videos + vector mActiveVideos; + + // Stats + int mTotalLoads{ 0 }; + int mTotalCloses{ 0 }; + int mTotalOperations{ 0 }; + int mTotalErrors{ 0 }; + double mStartTime{ 0.0 }; + double mLastOpTime{ 0.0 }; + double mNextOpTime{ 0.0 }; + + // Log history + deque mLogHistory; + static constexpr size_t MAX_LOG_ENTRIES = 100; + + // UI state + bool mShowStats{ true }; + bool mPaused{ false }; + gl::TextureFontRef mFont; + + // Operation toggles + bool mEnableSeeking{ true }; + bool mEnablePausing{ true }; + bool mEnableRateChanges{ true }; +}; + +void QuickTimeStressTestApp::setup() +{ + ImGui::Initialize( ImGui::Options().window( getWindow() ) ); + mFont = gl::TextureFont::create( Font( "Arial", 18 ) ); + mStartTime = getElapsedSeconds(); + mNextOpTime = mStartTime + randFloat( 0.1f, AVG_OP_INTERVAL * 2.0f ); + + // Prompt for directory + fs::path dir = getFolderPath(); + if( dir.empty() ) { + logMessage( "No directory selected, quitting.", true ); + quit(); + return; + } + + logMessage( "Scanning directory: " + dir.string() ); + scanDirectory( dir ); + + if( mCandidates.empty() ) { + logMessage( "No video files found!", true ); + quit(); + return; + } + + logMessage( "Found " + to_string( mCandidates.size() ) + " video files" ); + CI_LOG_I( "QuickTimeStressTest: Found " << mCandidates.size() << " candidates" ); +} + +void QuickTimeStressTestApp::cleanup() +{ + mActiveVideos.clear(); +} + +void QuickTimeStressTestApp::scanDirectory( const fs::path& dir ) +{ + try { + for( auto& entry : fs::recursive_directory_iterator( dir ) ) { + if( ! entry.is_regular_file() ) + continue; + + auto ext = entry.path().extension().string(); + // Convert to lowercase for comparison + transform( ext.begin(), ext.end(), ext.begin(), ::tolower ); + + if( ext == ".mp4" || ext == ".mov" ) { + mCandidates.push_back( entry.path() ); + } + } + } + catch( const fs::filesystem_error& e ) { + logMessage( "Filesystem error: " + string( e.what() ), true ); + } +} + +void QuickTimeStressTestApp::loadRandomVideo() +{ + if( mCandidates.empty() || (int)mActiveVideos.size() >= HARD_CAP ) + return; + + // Pick a random candidate that's not already loaded + vector available; + set loadedPaths; + for( auto& slot : mActiveVideos ) + loadedPaths.insert( slot.path ); + + for( auto& path : mCandidates ) { + if( loadedPaths.find( path ) == loadedPaths.end() ) + available.push_back( path ); + } + + if( available.empty() ) + return; + + fs::path chosen = available[randInt( (int)available.size() )]; + + try { + auto movie = qtime::MovieGl::create( chosen ); + movie->setLoop( true, false ); + movie->play(); + + VideoSlot slot; + slot.movie = movie; + slot.path = chosen; + slot.name = chosen.filename().string(); + slot.loadTime = getElapsedSeconds(); + + mActiveVideos.push_back( slot ); + mTotalLoads++; + + logMessage( "Loaded: " + slot.name + " (" + to_string( movie->getWidth() ) + "x" + to_string( movie->getHeight() ) + ")" ); + } + catch( const ci::Exception& e ) { + mTotalErrors++; + logMessage( "Failed to load: " + chosen.filename().string() + " - " + e.what(), true ); + + // Remove from candidates + mCandidates.erase( remove( mCandidates.begin(), mCandidates.end(), chosen ), mCandidates.end() ); + mFailedPaths.insert( chosen ); + } +} + +void QuickTimeStressTestApp::closeRandomVideo() +{ + if( mActiveVideos.empty() ) + return; + + int idx = randInt( (int)mActiveVideos.size() ); + logMessage( "Closed: " + mActiveVideos[idx].name + " (ops: " + to_string( mActiveVideos[idx].operationCount ) + ")" ); + mActiveVideos.erase( mActiveVideos.begin() + idx ); + mTotalCloses++; +} + +void QuickTimeStressTestApp::performRandomOperation() +{ + if( mActiveVideos.empty() ) + return; + + int idx = randInt( (int)mActiveVideos.size() ); + auto& slot = mActiveVideos[idx]; + if( ! slot.movie ) + return; + + // Build weighted list of enabled operations + // Format: { operation_id, weight } + // 0=SeekRandom, 1=SeekStart, 2=Pause, 3=Play, 4=Toggle, 5=Volume, 6=Rate + vector> ops; + + if( mEnableSeeking ) { + ops.push_back( { 0, 40.0f } ); // Seek random + ops.push_back( { 1, 15.0f } ); // Seek to start + } + if( mEnablePausing ) { + ops.push_back( { 2, 5.0f } ); // Pause + ops.push_back( { 4, 5.0f } ); // Toggle + } + ops.push_back( { 3, 15.0f } ); // Play (always enabled) + ops.push_back( { 5, 10.0f } ); // Volume (always enabled) + if( mEnableRateChanges ) { + ops.push_back( { 6, 10.0f } ); // Rate + } + + if( ops.empty() ) + return; + + // Calculate total weight and pick random operation + float totalWeight = 0.0f; + for( auto& op : ops ) + totalWeight += op.second; + + float r = randFloat( totalWeight ); + float cumulative = 0.0f; + int selectedOp = ops.back().first; + + for( auto& op : ops ) { + cumulative += op.second; + if( r < cumulative ) { + selectedOp = op.first; + break; + } + } + + slot.operationCount++; + mTotalOperations++; + + switch( selectedOp ) { + case 0: { // Seek random + float duration = slot.movie->getDuration(); + if( duration > 0 ) { + float seekTo = randFloat( duration ); + slot.movie->seekToTime( seekTo ); + } + break; + } + case 1: // Seek to start + slot.movie->seekToStart(); + break; + case 2: // Pause + slot.movie->stop(); + break; + case 3: // Play + slot.movie->play(); + break; + case 4: // Toggle play/pause + if( slot.movie->isPlaying() ) + slot.movie->stop(); + else + slot.movie->play(); + break; + case 5: // Volume + slot.movie->setVolume( randFloat() ); + break; + case 6: // Rate + slot.movie->setRate( randFloat( 0.5f, 2.0f ) ); + break; + } +} + +void QuickTimeStressTestApp::logMessage( const string& msg, bool isError ) +{ + LogEntry entry; + entry.timestamp = getElapsedSeconds() - mStartTime; + entry.message = msg; + entry.isError = isError; + + mLogHistory.push_front( entry ); + while( mLogHistory.size() > MAX_LOG_ENTRIES ) + mLogHistory.pop_back(); + + if( isError ) + CI_LOG_E( msg ); + else + CI_LOG_I( msg ); +} + +void QuickTimeStressTestApp::update() +{ + if( mPaused ) + return; + + double now = getElapsedSeconds(); + + // Perform random operations at intervals + if( now >= mNextOpTime ) { + // Determine target video count (half of candidates, capped at HARD_CAP/2) + int target = min( (int)mCandidates.size() / 2, HARD_CAP / 2 ); + target = max( target, 1 ); + + int current = (int)mActiveVideos.size(); + + // Probabilities based on current vs target + float loadProb, closeProb; + if( current < target ) { + loadProb = 0.35f; // More likely to load when below target + closeProb = 0.10f; + } + else if( current > target ) { + loadProb = 0.10f; // More likely to close when above target + closeProb = 0.35f; + } + else { + loadProb = 0.20f; // Balanced when at target + closeProb = 0.20f; + } + + // Also allow for zero videos sometimes + if( current == 0 && randFloat() < 0.9f ) { + loadRandomVideo(); + } + // Don't exceed hard cap + else if( current >= HARD_CAP ) { + if( randFloat() < 0.5f ) + closeRandomVideo(); + else + performRandomOperation(); + } + else { + float r = randFloat(); + if( r < loadProb ) { + loadRandomVideo(); + } + else if( r < loadProb + closeProb ) { + closeRandomVideo(); + } + else { + performRandomOperation(); + } + } + + mLastOpTime = now; + // Random interval centered around AVG_OP_INTERVAL + mNextOpTime = now + randFloat( AVG_OP_INTERVAL * 0.2f, AVG_OP_INTERVAL * 1.8f ); + } +} + +void QuickTimeStressTestApp::draw() +{ + gl::clear( Color( 0.08f, 0.08f, 0.1f ) ); + + drawVideoGrid(); + + if( mShowStats ) + drawStatsPanel(); +} + +void QuickTimeStressTestApp::drawVideoGrid() +{ + if( mActiveVideos.empty() ) { + gl::color( 0.4f, 0.4f, 0.4f ); + string msg = "Stress test running...\nVideos loaded: 0"; + vec2 size = mFont->measureString( msg ); + mFont->drawString( msg, vec2( getWindowCenter() ) - size * 0.5f ); + return; + } + + float panelWidth = mShowStats ? PANEL_WIDTH : 0.0f; + float availableWidth = getWindowWidth() - panelWidth; + float windowHeight = (float)getWindowHeight(); + + // Guard against zero dimensions + if( availableWidth <= 0 || windowHeight <= 0 ) + return; + + int count = (int)mActiveVideos.size(); + int cols = (int)ceil( sqrt( (double)count ) ); + int rows = ( count + cols - 1 ) / cols; + + float cellWidth = availableWidth / (float)cols; + float cellHeight = windowHeight / (float)rows; + + // Guard against zero cell dimensions + if( cellWidth <= 0 || cellHeight <= 0 ) + return; + + for( int i = 0; i < count; ++i ) { + auto& slot = mActiveVideos[i]; + auto texture = slot.movie ? slot.movie->getTexture() : nullptr; + + int col = i % cols; + int row = i / cols; + + float x = col * cellWidth; + float y = row * cellHeight; + + if( texture && slot.movie ) { + float videoAspect = slot.movie->getAspectRatio(); + if( videoAspect <= 0 ) videoAspect = 1.0f; // Guard against invalid aspect + float cellAspect = cellWidth / cellHeight; + + float drawWidth, drawHeight; + if( videoAspect > cellAspect ) { + drawWidth = cellWidth - CELL_PADDING; + drawHeight = drawWidth / videoAspect; + } + else { + drawHeight = cellHeight - CELL_PADDING; + drawWidth = drawHeight * videoAspect; + } + + float drawX = x + ( cellWidth - drawWidth ) / 2; + float drawY = y + ( cellHeight - drawHeight ) / 2; + + gl::color( Color::white() ); + gl::draw( texture, Rectf( drawX, drawY, drawX + drawWidth, drawY + drawHeight ) ); + + // Draw status indicator + gl::color( slot.movie->isPlaying() ? ColorA( 0, 1, 0, 0.7f ) : ColorA( 1, 0.5f, 0, 0.7f ) ); + gl::drawSolidCircle( vec2( drawX + 10, drawY + 10 ), 5 ); + } + else { + gl::color( 0.15f, 0.15f, 0.15f ); + gl::drawSolidRect( Rectf( x + CELL_PADDING / 2, y + CELL_PADDING / 2, + x + cellWidth - CELL_PADDING / 2, y + cellHeight - CELL_PADDING / 2 ) ); + } + } +} + +void QuickTimeStressTestApp::drawStatsPanel() +{ + ImGui::SetNextWindowPos( ImVec2( getWindowWidth() - PANEL_WIDTH, 0 ), ImGuiCond_Always ); + ImGui::SetNextWindowSize( ImVec2( PANEL_WIDTH, (float)getWindowHeight() ), ImGuiCond_Always ); + + ImGui::Begin( "Stress Test Stats", &mShowStats, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize ); + + // Controls + if( ImGui::Checkbox( "Paused", &mPaused ) ) { + logMessage( mPaused ? "Test paused" : "Test resumed" ); + } + + ImGui::Separator(); + + // Operation toggles + if( ImGui::CollapsingHeader( "Operation Toggles", ImGuiTreeNodeFlags_DefaultOpen ) ) { + ImGui::Checkbox( "Enable Seeking", &mEnableSeeking ); + ImGui::Checkbox( "Enable Pausing", &mEnablePausing ); + ImGui::Checkbox( "Enable Rate Changes", &mEnableRateChanges ); + } + + ImGui::Separator(); + + // Global stats + if( ImGui::CollapsingHeader( "Global Stats", ImGuiTreeNodeFlags_DefaultOpen ) ) { + double elapsed = getElapsedSeconds() - mStartTime; + int mins = (int)( elapsed / 60 ); + int secs = (int)elapsed % 60; + + ImGui::Text( "Runtime: %d:%02d", mins, secs ); + ImGui::Text( "FPS: %.1f", getAverageFps() ); + ImGui::Separator(); + ImGui::Text( "Candidate files: %d", (int)mCandidates.size() ); + ImGui::Text( "Failed files: %d", (int)mFailedPaths.size() ); + ImGui::Separator(); + ImGui::Text( "Total loads: %d", mTotalLoads ); + ImGui::Text( "Total closes: %d", mTotalCloses ); + ImGui::Text( "Total operations: %d", mTotalOperations ); + ImGui::Text( "Total errors: %d", mTotalErrors ); + + if( elapsed > 0 ) { + ImGui::Text( "Ops/sec: %.2f", mTotalOperations / elapsed ); + } + } + + ImGui::Separator(); + + // Active videos + if( ImGui::CollapsingHeader( "Active Videos", ImGuiTreeNodeFlags_DefaultOpen ) ) { + ImGui::Text( "Count: %d / %d (hard cap)", (int)mActiveVideos.size(), HARD_CAP ); + + for( size_t i = 0; i < mActiveVideos.size(); ++i ) { + auto& slot = mActiveVideos[i]; + ImGui::PushID( (int)i ); + + bool playing = slot.movie && slot.movie->isPlaying(); + ImVec4 color = playing ? ImVec4( 0.4f, 0.9f, 0.4f, 1.0f ) : ImVec4( 0.9f, 0.6f, 0.2f, 1.0f ); + ImGui::TextColored( color, "%s", playing ? ">" : "||" ); + ImGui::SameLine(); + + // Truncate long names + string displayName = slot.name; + if( displayName.length() > 25 ) + displayName = displayName.substr( 0, 22 ) + "..."; + + ImGui::Text( "%s", displayName.c_str() ); + + if( slot.movie ) { + ImGui::SameLine(); + ImGui::TextColored( ImVec4( 0.5f, 0.5f, 0.5f, 1.0f ), "(%dx%d)", slot.movie->getWidth(), slot.movie->getHeight() ); + } + + ImGui::PopID(); + } + + if( mActiveVideos.empty() ) { + ImGui::TextColored( ImVec4( 0.5f, 0.5f, 0.5f, 1.0f ), "No active videos" ); + } + } + + ImGui::Separator(); + + // Log history + if( ImGui::CollapsingHeader( "Log History", ImGuiTreeNodeFlags_DefaultOpen ) ) { + ImGui::BeginChild( "LogScroll", ImVec2( 0, 200 ), true ); + + for( auto& entry : mLogHistory ) { + ImVec4 color = entry.isError ? ImVec4( 1.0f, 0.4f, 0.4f, 1.0f ) : ImVec4( 0.8f, 0.8f, 0.8f, 1.0f ); + + int mins = (int)( entry.timestamp / 60 ); + int secs = (int)entry.timestamp % 60; + ImGui::TextColored( ImVec4( 0.5f, 0.5f, 0.5f, 1.0f ), "[%d:%02d]", mins, secs ); + ImGui::SameLine(); + ImGui::TextColored( color, "%s", entry.message.c_str() ); + } + + ImGui::EndChild(); + } + + ImGui::End(); +} + +CINDER_APP( QuickTimeStressTestApp, RendererGl, []( App::Settings* settings ) { + settings->setWindowSize( 1600, 900 ); + settings->setTitle( "QuickTime Stress Test" ); + settings->setResizable( true ); +} ) diff --git a/test/QuickTimeStressTest/vc2022/QuickTimeStressTest.sln b/test/QuickTimeStressTest/vc2022/QuickTimeStressTest.sln new file mode 100644 index 0000000000..ee555dc1d0 --- /dev/null +++ b/test/QuickTimeStressTest/vc2022/QuickTimeStressTest.sln @@ -0,0 +1,30 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.0.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "QuickTimeStressTest", "QuickTimeStressTest.vcxproj", "{D3E4F5A6-B789-0123-CDEF-456789ABCDEF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug_ANGLE|x64 = Debug_ANGLE|x64 + Debug|x64 = Debug|x64 + Release_ANGLE|x64 = Release_ANGLE|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D3E4F5A6-B789-0123-CDEF-456789ABCDEF}.Debug_ANGLE|x64.ActiveCfg = Debug_ANGLE|x64 + {D3E4F5A6-B789-0123-CDEF-456789ABCDEF}.Debug_ANGLE|x64.Build.0 = Debug_ANGLE|x64 + {D3E4F5A6-B789-0123-CDEF-456789ABCDEF}.Debug|x64.ActiveCfg = Debug|x64 + {D3E4F5A6-B789-0123-CDEF-456789ABCDEF}.Debug|x64.Build.0 = Debug|x64 + {D3E4F5A6-B789-0123-CDEF-456789ABCDEF}.Release_ANGLE|x64.ActiveCfg = Release_ANGLE|x64 + {D3E4F5A6-B789-0123-CDEF-456789ABCDEF}.Release_ANGLE|x64.Build.0 = Release_ANGLE|x64 + {D3E4F5A6-B789-0123-CDEF-456789ABCDEF}.Release|x64.ActiveCfg = Release|x64 + {D3E4F5A6-B789-0123-CDEF-456789ABCDEF}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {E5F6A7B8-C901-2345-6789-0ABCDEF12345} + EndGlobalSection +EndGlobal diff --git a/test/QuickTimeStressTest/vc2022/QuickTimeStressTest.vcxproj b/test/QuickTimeStressTest/vc2022/QuickTimeStressTest.vcxproj new file mode 100644 index 0000000000..e1f7b4b73e --- /dev/null +++ b/test/QuickTimeStressTest/vc2022/QuickTimeStressTest.vcxproj @@ -0,0 +1,214 @@ + + + + + Debug_ANGLE + x64 + + + Debug + x64 + + + Release_ANGLE + x64 + + + Release + x64 + + + + {D3E4F5A6-B789-0123-CDEF-456789ABCDEF} + QuickTimeStressTest + Win32Proj + 10.0 + + + + Application + false + v143 + Unicode + true + + + Application + false + v143 + Unicode + true + + + Application + true + v143 + Unicode + + + Application + true + v143 + Unicode + + + + + + + + + + + + + + + + + + <_ProjectFileVersion>10.0.30319.1 + true + true + false + false + + + + Disabled + ..\include;"..\..\..\include" + WIN32;_DEBUG;_WINDOWS;NOMINMAX;%(PreprocessorDefinitions) + EnableFastChecks + MultiThreadedDebug + + Level3 + ProgramDatabase + true + stdcpp20 + + + "..\..\..\include";..\include + + + cinder.lib;OpenGL32.lib;%(AdditionalDependencies) + ..\..\..\lib\msw\$(PlatformTarget);..\..\..\lib\msw\$(PlatformTarget)\$(Configuration)\$(PlatformToolset) + true + Windows + false + + LIBCMT;LIBCPMT + + + + + Disabled + ..\include;"..\..\..\include";..\..\..\include\ANGLE + CINDER_GL_ANGLE;WIN32;_DEBUG;_WINDOWS;NOMINMAX;%(PreprocessorDefinitions) + EnableFastChecks + MultiThreadedDebug + + + Level3 + ProgramDatabase + true + stdcpp20 + + + "..\..\..\include";..\include + + + cinder.lib;libEGL.lib;libGLESv2.lib;%(AdditionalDependencies) + ..\..\..\lib\msw\$(PlatformTarget);..\..\..\lib\msw\$(PlatformTarget)\$(Configuration)\$(PlatformToolset) + true + Windows + false + + + LIBCMT;LIBCPMT + + + xcopy /y "..\..\..\lib\msw\$(PlatformTarget)\libGLESv2.dll" "$(OutDir)" +xcopy /y "..\..\..\lib\msw\$(PlatformTarget)\libEGL.dll" "$(OutDir)" +xcopy /y "..\..\..\lib\msw\$(PlatformTarget)\d3dcompiler_46.dll" "$(OutDir)" + + + + + ..\include;"..\..\..\include" + WIN32;NDEBUG;_WINDOWS;NOMINMAX;%(PreprocessorDefinitions) + MultiThreaded + + Level3 + ProgramDatabase + true + stdcpp20 + + + true + + + "..\..\..\include";..\include + + + cinder.lib;OpenGL32.lib;%(AdditionalDependencies) + ..\..\..\lib\msw\$(PlatformTarget);..\..\..\lib\msw\$(PlatformTarget)\$(Configuration)\$(PlatformToolset) + false + true + Windows + true + + false + + + + + + ..\include;"..\..\..\include";..\..\..\include\ANGLE + CINDER_GL_ANGLE;WIN32;NDEBUG;_WINDOWS;NOMINMAX;%(PreprocessorDefinitions) + MultiThreaded + + + Level3 + ProgramDatabase + true + stdcpp20 + + + true + + + "..\..\..\include";..\include + + + cinder.lib;libEGL.lib;libGLESv2.lib;%(AdditionalDependencies) + ..\..\..\lib\msw\$(PlatformTarget);..\..\..\lib\msw\$(PlatformTarget)\$(Configuration)\$(PlatformToolset) + false + true + Windows + true + + + false + + + + + xcopy /y "..\..\..\lib\msw\$(PlatformTarget)\libGLESv2.dll" "$(OutDir)" +xcopy /y "..\..\..\lib\msw\$(PlatformTarget)\libEGL.dll" "$(OutDir)" +xcopy /y "..\..\..\lib\msw\$(PlatformTarget)\d3dcompiler_46.dll" "$(OutDir)" + + + + + + + + + + + + + + + + diff --git a/test/QuickTimeStressTest/vc2022/QuickTimeStressTest.vcxproj.filters b/test/QuickTimeStressTest/vc2022/QuickTimeStressTest.vcxproj.filters new file mode 100644 index 0000000000..1cae8e8a11 --- /dev/null +++ b/test/QuickTimeStressTest/vc2022/QuickTimeStressTest.vcxproj.filters @@ -0,0 +1,31 @@ + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav + + + + + Source Files + + + + + Header Files + + + + + Resource Files + + + diff --git a/test/QuickTimeStressTest/vc2022/Resources.rc b/test/QuickTimeStressTest/vc2022/Resources.rc new file mode 100644 index 0000000000..40ce959e23 --- /dev/null +++ b/test/QuickTimeStressTest/vc2022/Resources.rc @@ -0,0 +1,3 @@ +#include "../include/Resources.h" + +1 ICON "..\..\..\samples\data\cinder_app_icon.ico" diff --git a/test/QuickTimeStressTest/xcode/Info.plist b/test/QuickTimeStressTest/xcode/Info.plist new file mode 100644 index 0000000000..cefdf64533 --- /dev/null +++ b/test/QuickTimeStressTest/xcode/Info.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + CinderApp.icns + CFBundleIdentifier + org.libcinder.QuickTimeStressTest + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleSignature + ???? + CFBundleVersion + 1.0 + NSHighResolutionCapable + + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/test/QuickTimeStressTest/xcode/QuickTimeStressTest.xcodeproj/project.pbxproj b/test/QuickTimeStressTest/xcode/QuickTimeStressTest.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..6fa067e3f9 --- /dev/null +++ b/test/QuickTimeStressTest/xcode/QuickTimeStressTest.xcodeproj/project.pbxproj @@ -0,0 +1,330 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + A156FF0F1030B198002FC39B /* QuickTimeStressTestApp.cpp in Sources */ = {isa = PBXBuildFile; fileRef = A156FF0E1030B198002FC39B /* QuickTimeStressTestApp.cpp */; }; + A156FF1B1030B1D3002FC39B /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A156FF171030B1D3002FC39B /* Accelerate.framework */; }; + A156FF1C1030B1D3002FC39B /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A156FF181030B1D3002FC39B /* AudioToolbox.framework */; }; + A156FF1D1030B1D3002FC39B /* AudioUnit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A156FF191030B1D3002FC39B /* AudioUnit.framework */; }; + A156FF1E1030B1D3002FC39B /* CoreAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A156FF1A1030B1D3002FC39B /* CoreAudio.framework */; }; + A16D730E1995336F008149E2 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A16D730C1995336F008149E2 /* AVFoundation.framework */; }; + A16D730F1995336F008149E2 /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A16D730D1995336F008149E2 /* CoreMedia.framework */; }; + A191D8F90E81B9330029341E /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A191D8F80E81B9330029341E /* OpenGL.framework */; }; + A1B9955A1B128DF400A5C623 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1B995581B128DF400A5C623 /* IOKit.framework */; }; + A1B9955B1B128DF400A5C623 /* IOSurface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1B995591B128DF400A5C623 /* IOSurface.framework */; }; + A323E6B20EAFCA74003A9687 /* CoreVideo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A323E6B10EAFCA74003A9687 /* CoreVideo.framework */; }; + A323E6B60EAFCA7E003A9687 /* QTKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A323E6B50EAFCA7E003A9687 /* QTKit.framework */; }; + A3E3CDFC0E86099300238D2B /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A3E3CDFB0E86099300238D2B /* Carbon.framework */; }; + AD11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A156FF0E1030B198002FC39B /* QuickTimeStressTestApp.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = QuickTimeStressTestApp.cpp; path = ../src/QuickTimeStressTestApp.cpp; sourceTree = SOURCE_ROOT; }; + A156FF171030B1D3002FC39B /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = /System/Library/Frameworks/Accelerate.framework; sourceTree = ""; }; + A156FF181030B1D3002FC39B /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = /System/Library/Frameworks/AudioToolbox.framework; sourceTree = ""; }; + A156FF191030B1D3002FC39B /* AudioUnit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioUnit.framework; path = /System/Library/Frameworks/AudioUnit.framework; sourceTree = ""; }; + A156FF1A1030B1D3002FC39B /* CoreAudio.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreAudio.framework; path = /System/Library/Frameworks/CoreAudio.framework; sourceTree = ""; }; + A16D730C1995336F008149E2 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; + A16D730D1995336F008149E2 /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; + A191D8F80E81B9330029341E /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = /System/Library/Frameworks/OpenGL.framework; sourceTree = ""; }; + A197E3E40F3E9819005A4392 /* QuickTime.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickTime.framework; path = /System/Library/Frameworks/QuickTime.framework; sourceTree = ""; }; + A1B995581B128DF400A5C623 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; }; + A1B995591B128DF400A5C623 /* IOSurface.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOSurface.framework; path = System/Library/Frameworks/IOSurface.framework; sourceTree = SDKROOT; }; + A058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; + A3E42FB307B3F0F600E4EEF1 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = ""; }; + A9B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; + A9B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; + A2CA4F630368D1EE00C91783 /* QuickTimeStressTest_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QuickTimeStressTest_Prefix.pch; sourceTree = ""; }; + A323E6B10EAFCA74003A9687 /* CoreVideo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreVideo.framework; path = /System/Library/Frameworks/CoreVideo.framework; sourceTree = ""; }; + A323E6B50EAFCA7E003A9687 /* QTKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QTKit.framework; path = /System/Library/Frameworks/QTKit.framework; sourceTree = ""; }; + A3E3CDFB0E86099300238D2B /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = /System/Library/Frameworks/Carbon.framework; sourceTree = ""; }; + AD1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AD1107320486CEB800E47090 /* QuickTimeStressTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickTimeStressTest.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + AD11072E0486CEB800E47090 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A1B9955B1B128DF400A5C623 /* IOSurface.framework in Frameworks */, + A1B9955A1B128DF400A5C623 /* IOKit.framework in Frameworks */, + A16D730E1995336F008149E2 /* AVFoundation.framework in Frameworks */, + A16D730F1995336F008149E2 /* CoreMedia.framework in Frameworks */, + AD11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */, + A191D8F90E81B9330029341E /* OpenGL.framework in Frameworks */, + A3E3CDFC0E86099300238D2B /* Carbon.framework in Frameworks */, + A323E6B20EAFCA74003A9687 /* CoreVideo.framework in Frameworks */, + A323E6B60EAFCA7E003A9687 /* QTKit.framework in Frameworks */, + A156FF1B1030B1D3002FC39B /* Accelerate.framework in Frameworks */, + A156FF1C1030B1D3002FC39B /* AudioToolbox.framework in Frameworks */, + A156FF1D1030B1D3002FC39B /* AudioUnit.framework in Frameworks */, + A156FF1E1030B1D3002FC39B /* CoreAudio.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A80E96DDFE201D6D7F000001 /* Source */ = { + isa = PBXGroup; + children = ( + A156FF0E1030B198002FC39B /* QuickTimeStressTestApp.cpp */, + ); + name = Source; + sourceTree = ""; + }; + A058C7A0FEA54F0111CA2CBB /* Linked Frameworks */ = { + isa = PBXGroup; + children = ( + A156FF171030B1D3002FC39B /* Accelerate.framework */, + A156FF181030B1D3002FC39B /* AudioToolbox.framework */, + A156FF191030B1D3002FC39B /* AudioUnit.framework */, + A156FF1A1030B1D3002FC39B /* CoreAudio.framework */, + A197E3E40F3E9819005A4392 /* QuickTime.framework */, + A323E6B50EAFCA7E003A9687 /* QTKit.framework */, + A323E6B10EAFCA74003A9687 /* CoreVideo.framework */, + A3E3CDFB0E86099300238D2B /* Carbon.framework */, + A191D8F80E81B9330029341E /* OpenGL.framework */, + A058C7A1FEA54F0111CA2CBB /* Cocoa.framework */, + ); + name = "Linked Frameworks"; + sourceTree = ""; + }; + A058C7A2FEA54F0111CA2CBB /* Other Frameworks */ = { + isa = PBXGroup; + children = ( + A9B97324FDCFA39411CA2CEA /* AppKit.framework */, + A3E42FB307B3F0F600E4EEF1 /* CoreData.framework */, + A9B97325FDCFA39411CA2CEA /* Foundation.framework */, + ); + name = "Other Frameworks"; + sourceTree = ""; + }; + A9C28FACFE9D520D11CA2CBB /* Products */ = { + isa = PBXGroup; + children = ( + AD1107320486CEB800E47090 /* QuickTimeStressTest.app */, + ); + name = Products; + sourceTree = ""; + }; + A9B97314FDCFA39411CA2CEA /* QuickTimeStressTest */ = { + isa = PBXGroup; + children = ( + A80E96DDFE201D6D7F000001 /* Source */, + A9B97315FDCFA39411CA2CEA /* Other Sources */, + A9B97317FDCFA39411CA2CEA /* Resources */, + A9B97323FDCFA39411CA2CEA /* Frameworks */, + A9C28FACFE9D520D11CA2CBB /* Products */, + ); + name = QuickTimeStressTest; + sourceTree = ""; + }; + A9B97315FDCFA39411CA2CEA /* Other Sources */ = { + isa = PBXGroup; + children = ( + A2CA4F630368D1EE00C91783 /* QuickTimeStressTest_Prefix.pch */, + ); + name = "Other Sources"; + sourceTree = ""; + }; + A9B97317FDCFA39411CA2CEA /* Resources */ = { + isa = PBXGroup; + children = ( + AD1107310486CEB800E47090 /* Info.plist */, + ); + name = Resources; + sourceTree = ""; + }; + A9B97323FDCFA39411CA2CEA /* Frameworks */ = { + isa = PBXGroup; + children = ( + A1B995581B128DF400A5C623 /* IOKit.framework */, + A1B995591B128DF400A5C623 /* IOSurface.framework */, + A16D730C1995336F008149E2 /* AVFoundation.framework */, + A16D730D1995336F008149E2 /* CoreMedia.framework */, + A058C7A0FEA54F0111CA2CBB /* Linked Frameworks */, + A058C7A2FEA54F0111CA2CBB /* Other Frameworks */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + AD1107260486CEB800E47090 /* QuickTimeStressTest */ = { + isa = PBXNativeTarget; + buildConfigurationList = C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "QuickTimeStressTest" */; + buildPhases = ( + AD1107290486CEB800E47090 /* Resources */, + AD11072C0486CEB800E47090 /* Sources */, + AD11072E0486CEB800E47090 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = QuickTimeStressTest; + productInstallPath = "$(HOME)/Applications"; + productName = QuickTimeStressTest; + productReference = AD1107320486CEB800E47090 /* QuickTimeStressTest.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A9B97313FDCFA39411CA2CEA /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0420; + }; + buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "QuickTimeStressTest" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 1; + knownRegions = ( + English, + Japanese, + French, + German, + ); + mainGroup = A9B97314FDCFA39411CA2CEA /* QuickTimeStressTest */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AD1107260486CEB800E47090 /* QuickTimeStressTest */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + AD1107290486CEB800E47090 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + AD11072C0486CEB800E47090 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A156FF0F1030B198002FC39B /* QuickTimeStressTestApp.cpp in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + C01FCF4B08A954540054247B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LIBRARY = "libc++"; + COPY_PHASE_STRIP = NO; + GCC_DYNAMIC_NO_PIC = NO; + GCC_INLINES_ARE_PRIVATE_EXTERN = YES; + GCC_MODEL_TUNING = G5; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = QuickTimeStressTest_Prefix.pch; + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(HOME)/Applications"; + ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = "$(CINDER_PATH)/lib/macosx/$(CONFIGURATION)/libcinder.a"; + PRODUCT_NAME = QuickTimeStressTest; + WRAPPER_EXTENSION = app; + ZERO_LINK = YES; + }; + name = Debug; + }; + C01FCF4C08A954540054247B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LIBRARY = "libc++"; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_FAST_MATH = YES; + GCC_INLINES_ARE_PRIVATE_EXTERN = YES; + GCC_MODEL_TUNING = G5; + GCC_OPTIMIZATION_LEVEL = 3; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = QuickTimeStressTest_Prefix.pch; + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(HOME)/Applications"; + ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = "$(CINDER_PATH)/lib/macosx/$(CONFIGURATION)/libcinder.a"; + PRODUCT_NAME = QuickTimeStressTest; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; + C01FCF4F08A954540054247B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + CINDER_PATH = ../../..; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = "$(CINDER_PATH)/include"; + MACOSX_DEPLOYMENT_TARGET = 10.13; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + USER_HEADER_SEARCH_PATHS = "$(CINDER_PATH)/include ../include"; + }; + name = Debug; + }; + C01FCF5008A954540054247B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + CINDER_PATH = ../../..; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = "$(CINDER_PATH)/include"; + MACOSX_DEPLOYMENT_TARGET = 10.13; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + USER_HEADER_SEARCH_PATHS = "$(CINDER_PATH)/include ../include"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "QuickTimeStressTest" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C01FCF4B08A954540054247B /* Debug */, + C01FCF4C08A954540054247B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C01FCF4E08A954540054247B /* Build configuration list for PBXProject "QuickTimeStressTest" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C01FCF4F08A954540054247B /* Debug */, + C01FCF5008A954540054247B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = A9B97313FDCFA39411CA2CEA /* Project object */; +} diff --git a/test/QuickTimeStressTest/xcode/QuickTimeStressTest_Prefix.pch b/test/QuickTimeStressTest/xcode/QuickTimeStressTest_Prefix.pch new file mode 100644 index 0000000000..aabef477dd --- /dev/null +++ b/test/QuickTimeStressTest/xcode/QuickTimeStressTest_Prefix.pch @@ -0,0 +1,3 @@ +#ifdef __OBJC__ + #import +#endif