From cf6d185efb0f26726740d5ed2c1c9a651c69b68a Mon Sep 17 00:00:00 2001 From: Guillaume Buisson Date: Wed, 29 Apr 2026 08:26:56 +0200 Subject: [PATCH 1/5] Fix qtime palindrome loop for MacOS + user play rate on 1st play --- include/cinder/qtime/QuickTimeImplAvf.h | 1 + src/cinder/qtime/QuickTimeImplAvf.mm | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/include/cinder/qtime/QuickTimeImplAvf.h b/include/cinder/qtime/QuickTimeImplAvf.h index 5fe2b3186e..dc2c9ef42b 100644 --- a/include/cinder/qtime/QuickTimeImplAvf.h +++ b/include/cinder/qtime/QuickTimeImplAvf.h @@ -189,6 +189,7 @@ class MovieBase { int32_t mFrameCount; float mFrameRate; float mDuration; + float mPlayRate; std::atomic mAssetLoaded; bool mLoaded, mPlayThroughOk, mPlayable, mProtected; bool mPlayingForward, mLoop, mPalindrome; diff --git a/src/cinder/qtime/QuickTimeImplAvf.mm b/src/cinder/qtime/QuickTimeImplAvf.mm index 476bc20a38..1f42ec3c2e 100644 --- a/src/cinder/qtime/QuickTimeImplAvf.mm +++ b/src/cinder/qtime/QuickTimeImplAvf.mm @@ -410,6 +410,7 @@ - (void)outputSequenceWasFlushed:(AVPlayerItemOutput *)output else success = [mPlayerItem canPlaySlowForward]; + mPlayRate = rate; [mPlayer setRate:rate]; return success; @@ -516,6 +517,7 @@ - (void)outputSequenceWasFlushed:(AVPlayerItemOutput *)output mHeight = -1; mDuration = -1; mFrameCount = -1; + mPlayRate = 1; } void MovieBase::initFromUrl( const Url& url ) @@ -758,6 +760,7 @@ - (void)outputSequenceWasFlushed:(AVPlayerItemOutput *)output { mSignalReady.emit(); + setRate(mPlayRate); // previous setRate calls fail while the player is not ready if( mPlaying ) play(); } @@ -765,9 +768,9 @@ - (void)outputSequenceWasFlushed:(AVPlayerItemOutput *)output void MovieBase::playerItemEnded() { if( mPalindrome ) { - float rate = -[mPlayer rate]; - mPlayingForward = (rate >= 0); - this->setRate( rate ); + mPlayRate = -mPlayRate; + mPlayingForward = (mPlayRate >= 0); + this->setRate( mPlayRate ); } else if( mLoop ) { this->seekToStart(); From e8d4cc52a40d1ff0b6be3ab32c76b8223cfc6cf8 Mon Sep 17 00:00:00 2001 From: Guillaume Buisson Date: Sat, 9 May 2026 15:16:57 +0200 Subject: [PATCH 2/5] fix qtime memory leak on MacOS --- src/cinder/qtime/QuickTimeImplAvf.mm | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cinder/qtime/QuickTimeImplAvf.mm b/src/cinder/qtime/QuickTimeImplAvf.mm index 1f42ec3c2e..4a5ab13a56 100644 --- a/src/cinder/qtime/QuickTimeImplAvf.mm +++ b/src/cinder/qtime/QuickTimeImplAvf.mm @@ -198,6 +198,7 @@ - (void)outputSequenceWasFlushed:(AVPlayerItemOutput *)output // release resources for AVF objects. if( mPlayer ) { [mPlayer cancelPendingPrerolls]; + [mPlayer replaceCurrentItemWithPlayerItem:nil]; [mPlayer release]; } From 3293e99ac7906ab79408ab39625509925c9175ee Mon Sep 17 00:00:00 2001 From: Guillaume Buisson Date: Sat, 9 May 2026 15:18:44 +0200 Subject: [PATCH 3/5] fix qtime data-race on MacOS --- include/cinder/qtime/QuickTimeImplAvf.h | 2 ++ src/cinder/qtime/QuickTimeImplAvf.mm | 28 +++++++++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/include/cinder/qtime/QuickTimeImplAvf.h b/include/cinder/qtime/QuickTimeImplAvf.h index dc2c9ef42b..1d85751f2b 100644 --- a/include/cinder/qtime/QuickTimeImplAvf.h +++ b/include/cinder/qtime/QuickTimeImplAvf.h @@ -32,6 +32,7 @@ #include "cinder/Thread.h" #include "cinder/Url.h" +#include #include typedef struct __CVBuffer *CVBufferRef; @@ -200,6 +201,7 @@ class MovieBase { AVPlayerItem* mPlayerItem; AVURLAsset* mAsset; AVPlayerItemVideoOutput* mPlayerVideoOutput; + dispatch_queue_t mOutputQueue; std::mutex mMutex; diff --git a/src/cinder/qtime/QuickTimeImplAvf.mm b/src/cinder/qtime/QuickTimeImplAvf.mm index 4a5ab13a56..228ec02a1d 100644 --- a/src/cinder/qtime/QuickTimeImplAvf.mm +++ b/src/cinder/qtime/QuickTimeImplAvf.mm @@ -183,6 +183,7 @@ - (void)outputSequenceWasFlushed:(AVPlayerItemOutput *)output mPlayerItem( nil ), mAsset( nil ), mPlayerVideoOutput( nil ), + mOutputQueue( nil ), mPlayerDelegate( nil ), mResponder( nullptr ), mAssetLoaded( false ) @@ -192,6 +193,21 @@ - (void)outputSequenceWasFlushed:(AVPlayerItemOutput *)output MovieBase::~MovieBase() { + // Stop the output delegate first and drain any in-flight background callback, + // e.g. outputSequenceWasFlushed. + if( mPlayerVideoOutput ) { + [mPlayerVideoOutput setDelegate:nil queue:nil]; + if( mOutputQueue ) { + dispatch_sync( mOutputQueue, ^{} ); + } + [mPlayerVideoOutput release]; + mPlayerVideoOutput = nil; + } + if( mOutputQueue ) { + dispatch_release( mOutputQueue ); + mOutputQueue = nil; + } + // remove all observers removeObservers(); @@ -207,9 +223,13 @@ - (void)outputSequenceWasFlushed:(AVPlayerItemOutput *)output [mAsset release]; } - if( mPlayerVideoOutput ) { - [mPlayerVideoOutput release]; + if( mPlayerDelegate ) { + [mPlayerDelegate release]; + mPlayerDelegate = nil; } + + delete mResponder; + mResponder = nullptr; } float MovieBase::getPixelAspectRatio() const @@ -703,8 +723,8 @@ - (void)outputSequenceWasFlushed:(AVPlayerItemOutput *)output void MovieBase::createPlayerItemOutput( const AVPlayerItem* playerItem ) { mPlayerVideoOutput = [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:avPlayerItemOutputDictionary()]; - dispatch_queue_t outputQueue = dispatch_queue_create("movieVideoOutputQueue", DISPATCH_QUEUE_SERIAL); - [mPlayerVideoOutput setDelegate:mPlayerDelegate queue:outputQueue]; + mOutputQueue = dispatch_queue_create("movieVideoOutputQueue", DISPATCH_QUEUE_SERIAL); + [mPlayerVideoOutput setDelegate:mPlayerDelegate queue:mOutputQueue]; mPlayerVideoOutput.suppressesPlayerRendering = YES; [playerItem addOutput:mPlayerVideoOutput]; } From ea57f4612aabb9812352777abae49285a2c17e25 Mon Sep 17 00:00:00 2001 From: Guillaume Buisson Date: Sun, 10 May 2026 22:01:11 +0200 Subject: [PATCH 4/5] add MovieBase getRate accessor --- include/cinder/qtime/QuickTimeImplAvf.h | 2 ++ src/cinder/qtime/QuickTimeImplAvf.mm | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/include/cinder/qtime/QuickTimeImplAvf.h b/include/cinder/qtime/QuickTimeImplAvf.h index 1d85751f2b..3d80bd029c 100644 --- a/include/cinder/qtime/QuickTimeImplAvf.h +++ b/include/cinder/qtime/QuickTimeImplAvf.h @@ -137,6 +137,8 @@ class MovieBase { * Returns a boolean value indicating whether the rate value can be played (some media types cannot be played backwards) */ bool setRate( float rate ); + //! Gets the playback rate. It might be different from the one that was set because setRate failed or palindrome loop kicked off. + float getRate() const; //! Sets the audio playback volume ranging from [0 - 1.0] void setVolume( float volume ); diff --git a/src/cinder/qtime/QuickTimeImplAvf.mm b/src/cinder/qtime/QuickTimeImplAvf.mm index 228ec02a1d..e100dbb981 100644 --- a/src/cinder/qtime/QuickTimeImplAvf.mm +++ b/src/cinder/qtime/QuickTimeImplAvf.mm @@ -437,6 +437,14 @@ - (void)outputSequenceWasFlushed:(AVPlayerItemOutput *)output return success; } +float MovieBase::getRate() const +{ + if( ! mPlayer || ! mPlayerItem ) + return 0.f; + + return mPlayer.rate; +} + void MovieBase::setVolume( float volume ) { if( ! mPlayer ) From c3de0be23a4c8219963e894de0513f869733200d Mon Sep 17 00:00:00 2001 From: Guillaume Buisson Date: Mon, 11 May 2026 20:39:21 +0200 Subject: [PATCH 5/5] fix MacOS fullscreen always on the main display --- include/cinder/app/cocoa/PlatformCocoa.h | 4 ++++ src/cinder/app/cocoa/PlatformCocoa.cpp | 19 +++++++++++++++++++ src/cinder/app/glfw/WindowImplGlfw.cpp | 16 +++++++++++----- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/include/cinder/app/cocoa/PlatformCocoa.h b/include/cinder/app/cocoa/PlatformCocoa.h index 1212234eb8..4efb448489 100644 --- a/include/cinder/app/cocoa/PlatformCocoa.h +++ b/include/cinder/app/cocoa/PlatformCocoa.h @@ -46,6 +46,8 @@ typedef uint32_t CGDisplayChangeSummaryFlags; typedef uint32_t CGDirectDisplayID; +struct GLFWmonitor; + namespace cinder { #if defined( CINDER_MAC ) class DisplayMac; @@ -151,6 +153,8 @@ class DisplayMac : public Display { public: NSScreen* getNsScreen() const; CGDirectDisplayID getCgDirectDisplayId() const { return mDirectDisplayId; } + //! Returns the GLFW monitor handle matching this display, or nullptr if none matches. Only meaningful when Cinder is built with the GLFW backend. + GLFWmonitor* getGlfwMonitor() const; std::string getName() const override; diff --git a/src/cinder/app/cocoa/PlatformCocoa.cpp b/src/cinder/app/cocoa/PlatformCocoa.cpp index 03ad4b3f08..c2c96b4397 100644 --- a/src/cinder/app/cocoa/PlatformCocoa.cpp +++ b/src/cinder/app/cocoa/PlatformCocoa.cpp @@ -44,6 +44,12 @@ #include #include +#if defined( CINDER_GLFW ) + #define GLFW_EXPOSE_NATIVE_COCOA + #include "glfw/glfw3.h" + #include "glfw/glfw3native.h" +#endif + using namespace std; namespace cinder { namespace app { @@ -439,6 +445,19 @@ NSScreen* DisplayMac::getNsScreen() const return findNsScreenForCgDirectDisplayId( mDirectDisplayId ); } +GLFWmonitor* DisplayMac::getGlfwMonitor() const +{ +#if defined( CINDER_GLFW ) + int count = 0; + GLFWmonitor** monitors = ::glfwGetMonitors( &count ); + for( int i = 0; i < count; ++i ) { + if( ::glfwGetCocoaMonitor( monitors[i] ) == mDirectDisplayId ) + return monitors[i]; + } +#endif + return nullptr; +} + DisplayRef app::PlatformCocoa::findFromCgDirectDisplayId( CGDirectDisplayID displayId ) { for( auto &display : getDisplays() ) { diff --git a/src/cinder/app/glfw/WindowImplGlfw.cpp b/src/cinder/app/glfw/WindowImplGlfw.cpp index 12cc32b331..bbf682f5f6 100644 --- a/src/cinder/app/glfw/WindowImplGlfw.cpp +++ b/src/cinder/app/glfw/WindowImplGlfw.cpp @@ -32,6 +32,7 @@ #if defined( CINDER_MAC ) #include "cinder/app/glfw/AppImplGlfwMac.h" + #include "cinder/app/cocoa/PlatformCocoa.h" #endif namespace cinder { namespace app { @@ -75,10 +76,12 @@ WindowImplGlfw::WindowImplGlfw( const Window::Format &format, WindowImplGlfw *sh auto windowSize = displayLinux->getSize(); mGlfwWindow = ::glfwCreateWindow( windowSize.x, windowSize.y, format.getTitle().c_str(), displayLinux->getGlfwMonitor(), sharedGlfwWindow ); #elif defined( CINDER_MAC ) - // On macOS, get the primary monitor for fullscreen - GLFWmonitor* primaryMonitor = ::glfwGetPrimaryMonitor(); + auto* displayMac = dynamic_cast( mDisplay.get() ); + GLFWmonitor* monitor = displayMac ? displayMac->getGlfwMonitor() : nullptr; + if( ! monitor ) + monitor = ::glfwGetPrimaryMonitor(); auto windowSize = mDisplay->getSize(); - mGlfwWindow = ::glfwCreateWindow( windowSize.x, windowSize.y, format.getTitle().c_str(), primaryMonitor, sharedGlfwWindow ); + mGlfwWindow = ::glfwCreateWindow( windowSize.x, windowSize.y, format.getTitle().c_str(), monitor, sharedGlfwWindow ); #endif mWindowedSize = format.getSize(); mWindowedPos = format.getPos(); @@ -140,8 +143,11 @@ void WindowImplGlfw::setFullScreen( bool fullScreen, const app::FullScreenOption cinder::app::DisplayLinux* displayLinux = dynamic_cast( mDisplay.get() ); ::glfwSetWindowMonitor( mGlfwWindow, displayLinux->getGlfwMonitor(), 0, 0, mDisplay->getWidth(), mDisplay->getHeight(), GLFW_DONT_CARE ); #elif defined( CINDER_MAC ) - GLFWmonitor* primaryMonitor = ::glfwGetPrimaryMonitor(); - ::glfwSetWindowMonitor( mGlfwWindow, primaryMonitor, 0, 0, mDisplay->getWidth(), mDisplay->getHeight(), GLFW_DONT_CARE ); + auto* displayMac = dynamic_cast( mDisplay.get() ); + GLFWmonitor* monitor = displayMac ? displayMac->getGlfwMonitor() : nullptr; + if( ! monitor ) + monitor = ::glfwGetPrimaryMonitor(); + ::glfwSetWindowMonitor( mGlfwWindow, monitor, 0, 0, mDisplay->getWidth(), mDisplay->getHeight(), GLFW_DONT_CARE ); #endif } else {