diff --git a/BREAKING-CHANGES.txt b/BREAKING-CHANGES.txt index 9a9e932c55..ee39738aa4 100644 --- a/BREAKING-CHANGES.txt +++ b/BREAKING-CHANGES.txt @@ -4,33 +4,6 @@ JUCE breaking changes Develop ======= -Change ------- -CameraDevice::Listener::imageReceived() has been replaced by a new function -CameraDevice::takeStillPicture(). The callback passed in takeStillPicture() -will always be triggered on the message thread. - -Possible Issues ---------------- -The code handling image capture needs to be adjusted to work well on a message -thread. This means that you should not perform any lengthy operations in your -callback, as this will stall your UI. - -Workaround ----------- -Use CameraDevice::takeStillPicture() instead of old listener callback. Pass -your lambda or other std::function compliant object to takeStillPicture which -will be called for you when the capture has finished. If you want to perform -any time consuming operation upon receiving the picture, schedule it on a -separate worker thread. - -Rationale ---------- -The old Listener interface was not working in a typical listener pattern. It -feels more natural to request still picture capture with a dedicated function. -This is also more compliant with async mobile APIs. - - Change ------ JUCE no longer supports OS X deployment targets earlier than 10.7. diff --git a/modules/juce_video/capture/juce_CameraDevice.cpp b/modules/juce_video/capture/juce_CameraDevice.cpp index bfed050cb0..636be58f25 100644 --- a/modules/juce_video/capture/juce_CameraDevice.cpp +++ b/modules/juce_video/capture/juce_CameraDevice.cpp @@ -180,6 +180,18 @@ void CameraDevice::stopRecording() pimpl->stopRecording(); } +void CameraDevice::addListener (Listener* listenerToAdd) +{ + if (listenerToAdd != nullptr) + pimpl->addListener (listenerToAdd); +} + +void CameraDevice::removeListener (Listener* listenerToRemove) +{ + if (listenerToRemove != nullptr) + pimpl->removeListener (listenerToRemove); +} + //============================================================================== StringArray CameraDevice::getAvailableDevices() { diff --git a/modules/juce_video/capture/juce_CameraDevice.h b/modules/juce_video/capture/juce_CameraDevice.h index 751990e7ef..b362d1c634 100644 --- a/modules/juce_video/capture/juce_CameraDevice.h +++ b/modules/juce_video/capture/juce_CameraDevice.h @@ -174,6 +174,48 @@ public: and reopen the device to be able to use it further. */ std::function onErrorOccurred; + //============================================================================== + /** + Receives callbacks with individual frames from a CameraDevice. It is mainly + useful for processing multiple frames that has to be done as quickly as + possible. The callbacks can be called from any thread. + + If you just need to take one picture, you should use takeStillPicture() instead. + + @see CameraDevice::addListener + */ + class JUCE_API Listener + { + public: + Listener() {} + virtual ~Listener() {} + + /** This method is called when a new image arrives. + + This may be called by any thread, so be careful about thread-safety, + and make sure that you process the data as quickly as possible to + avoid glitching! + + Simply add a listener to be continuously notified about new frames becoming + available and remove the listener when you no longer need new frames. + + If you just need to take one picture, use takeStillPicture() instead. + + @see CameraDevice::takeStillPicture + */ + virtual void imageReceived (const Image& image) = 0; + }; + + /** Adds a listener to receive images from the camera. + + Be very careful not to delete the listener without first removing it by calling + removeListener(). + */ + void addListener (Listener* listenerToAdd); + + /** Removes a listener that was previously added with addListener(). */ + void removeListener (Listener* listenerToRemove); + private: String name; diff --git a/modules/juce_video/native/juce_android_CameraDevice.h b/modules/juce_video/native/juce_android_CameraDevice.h index 58c7413b19..ee424b03ef 100644 --- a/modules/juce_video/native/juce_android_CameraDevice.h +++ b/modules/juce_video/native/juce_android_CameraDevice.h @@ -534,6 +534,9 @@ struct CameraDevice::Pimpl void startRecordingToFile (const File&, int) {} void stopRecording() {} + void addListener (CameraDevice::Listener*) {} + void removeListener (CameraDevice::Listener*) {} + String getCameraId() const noexcept { return {}; } bool openedOk() const noexcept { return false; } Time getTimeOfFirstRecordedFrame() const { return {}; } @@ -668,6 +671,21 @@ struct CameraDevice::Pimpl return results; } + void addListener (CameraDevice::Listener* listenerToAdd) + { + const ScopedLock sl (listenerLock); + listeners.add (listenerToAdd); + + if (listeners.size() == 1) + triggerStillPictureCapture(); + } + + void removeListener (CameraDevice::Listener* listenerToRemove) + { + const ScopedLock sl (listenerLock); + listeners.remove (listenerToRemove); + } + private: enum { @@ -1197,9 +1215,11 @@ private: WeakReference safeThis (this); + owner.callListeners (image); + // Android may take multiple pictures before it handles a request to stop. if (hasNotifiedListeners.compareAndSetBool (1, 0)) - MessageManager::callAsync ([safeThis, image]() mutable { if (safeThis != nullptr) safeThis->owner.notifyImageReceived (image); }); + MessageManager::callAsync ([safeThis, image]() mutable { if (safeThis != nullptr) safeThis->owner.notifyPictureTaken (image); }); } struct ImageBuffer @@ -1901,7 +1921,9 @@ private: // Delay still picture capture for devices that can't handle it right after // stopRepeating/abortCaptures calls. - delayedCaptureRunnable = GlobalRef (CreateJavaInterface (&runnable, "java/lang/Runnable").get()); + if (delayedCaptureRunnable.get() == nullptr) + delayedCaptureRunnable = GlobalRef (CreateJavaInterface (&runnable, "java/lang/Runnable").get()); + env->CallBooleanMethod (handler, AndroidHandler.postDelayed, delayedCaptureRunnable.get(), (jlong) 200); } @@ -1951,8 +1973,6 @@ private: if (Pimpl::checkHasExceptionOccurred()) return; - delayedCaptureRunnable.clear(); - // NB: for preview, using preview capture request again env->CallIntMethod (captureSession, CameraCaptureSession.setRepeatingRequest, previewCaptureRequest.get(), nullptr, handler.get()); @@ -2773,6 +2793,9 @@ private: std::unique_ptr scopedCameraDevice; + CriticalSection listenerLock; + ListenerList listeners; + std::function pictureTakenCallback; Time firstRecordedFrameTimeMs; @@ -2888,9 +2911,16 @@ private: cameraOpenCallback (cameraId, error); } - void notifyImageReceived (const Image& image) + //============================================================================== + void callListeners (const Image& image) + { + const ScopedLock sl (listenerLock); + listeners.call ([=] (Listener& l) { l.imageReceived (image); }); + } + + void notifyPictureTaken (const Image& image) { - JUCE_CAMERA_LOG ("notifyImageReceived()"); + JUCE_CAMERA_LOG ("notifyPictureTaken()"); if (pictureTakenCallback != nullptr) pictureTakenCallback (image); diff --git a/modules/juce_video/native/juce_ios_CameraDevice.h b/modules/juce_video/native/juce_ios_CameraDevice.h index c44b8c9277..8724b19270 100644 --- a/modules/juce_video/native/juce_ios_CameraDevice.h +++ b/modules/juce_video/native/juce_ios_CameraDevice.h @@ -123,6 +123,21 @@ struct CameraDevice::Pimpl return results; } + void addListener (CameraDevice::Listener* listenerToAdd) + { + const ScopedLock sl (listenerLock); + listeners.add (listenerToAdd); + + if (listeners.size() == 1) + triggerStillPictureCapture(); + } + + void removeListener (CameraDevice::Listener* listenerToRemove) + { + const ScopedLock sl (listenerLock); + listeners.remove (listenerToRemove); + } + private: static NSArray* getDevices() { @@ -512,11 +527,11 @@ private: { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wundeclared-selector" - addMethod (@selector (sessionDidStartRunning:), started, "v@:@"); - addMethod (@selector (sessionDidStopRunning:), stopped, "v@:@"); - addMethod (@selector (sessionRuntimeError:), runtimeError, "v@:@"); - addMethod (@selector (sessionWasInterrupted:), interrupted, "v@:@"); - addMethod (@selector (sessionDidStartRunning:), interruptionEnded, "v@:@"); + addMethod (@selector (sessionDidStartRunning:), started, "v@:@"); + addMethod (@selector (sessionDidStopRunning:), stopped, "v@:@"); + addMethod (@selector (sessionRuntimeError:), runtimeError, "v@:@"); + addMethod (@selector (sessionWasInterrupted:), interrupted, "v@:@"); + addMethod (@selector (sessionInterruptionEnded:), interruptionEnded, "v@:@"); #pragma clang diagnostic pop addIvar ("owner"); @@ -641,7 +656,10 @@ private: NSData* imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation: imageSampleBuffer]; auto image = ImageFileFormat::loadFrom (imageData.bytes, (size_t) imageData.length); - MessageManager::callAsync ([this, image]() { imageTaken (image); }); + + callListeners (image); + + MessageManager::callAsync ([this, image]() { notifyPictureTaken (image); }); }]; } else @@ -815,7 +833,10 @@ private: auto* imageData = UIImageJPEGRepresentation (uiImage, 0.f); auto image = ImageFileFormat::loadFrom (imageData.bytes, (size_t) imageData.length); - MessageManager::callAsync ([self, image]() { getOwner (self).imageTaken (image); }); + + getOwner (self).callListeners (image); + + MessageManager::callAsync ([self, image]() { getOwner (self).notifyPictureTaken (image); }); } static UIImage* getImageWithCorrectOrientation (CGImagePropertyOrientation imageOrientation, @@ -912,7 +933,10 @@ private: auto* imageData = UIImageJPEGRepresentation (uiImage, 0.f); auto image = ImageFileFormat::loadFrom (imageData.bytes, (size_t) imageData.length); - MessageManager::callAsync ([self, image]() { getOwner (self).imageTaken (image); }); + + getOwner (self).callListeners (image); + + MessageManager::callAsync ([self, image]() { getOwner (self).notifyPictureTaken (image); }); } static CGImagePropertyOrientation uiImageOrientationToCGImageOrientation (UIImageOrientation orientation) @@ -932,11 +956,16 @@ private: }; //============================================================================== - void imageTaken (const Image& image) + void callListeners (const Image& image) + { + captureSession.callListeners (image); + } + + void notifyPictureTaken (const Image& image) { takingPicture = false; - captureSession.notifyImageReceived (image); + captureSession.notifyPictureTaken (image); } CaptureSession& captureSession; @@ -1105,9 +1134,14 @@ private: owner.cameraSessionRuntimeError (error); } - void notifyImageReceived (const Image& image) + void callListeners (const Image& image) { - owner.notifyImageReceived (image); + owner.callListeners (image); + } + + void notifyPictureTaken (const Image& image) + { + owner.notifyPictureTaken (image); } Pimpl& owner; @@ -1152,9 +1186,15 @@ private: } } - void notifyImageReceived (const Image& image) + void callListeners (const Image& image) { - JUCE_CAMERA_LOG ("notifyImageReceived()"); + const ScopedLock sl (listenerLock); + listeners.call ([=] (Listener& l) { l.imageReceived (image); }); + } + + void notifyPictureTaken (const Image& image) + { + JUCE_CAMERA_LOG ("notifyPictureTaken()"); if (pictureTakenCallback != nullptr) pictureTakenCallback (image); @@ -1171,6 +1211,9 @@ private: String cameraId; InternalOpenCameraResultCallback cameraOpenCallback; + CriticalSection listenerLock; + ListenerList listeners; + std::function pictureTakenCallback; CaptureSession captureSession; diff --git a/modules/juce_video/native/juce_mac_CameraDevice.h b/modules/juce_video/native/juce_mac_CameraDevice.h index 66a8fc3915..e60b59b095 100644 --- a/modules/juce_video/native/juce_mac_CameraDevice.h +++ b/modules/juce_video/native/juce_mac_CameraDevice.h @@ -175,8 +175,8 @@ struct CameraDevice::Pimpl void handleImageCapture (const Image& image) { - if (pictureTakenCallback != nullptr) - pictureTakenCallback (image); + const ScopedLock sl (listenerLock); + listeners.call ([=] (Listener& l) { l.imageReceived (image); }); } void triggerImageCapture() @@ -199,12 +199,33 @@ struct CameraDevice::Pimpl auto image = ImageFileFormat::loadFrom (imageData.bytes, (size_t) imageData.length); + handleImageCapture (image); + WeakReference weakRef (this); - MessageManager::callAsync ([weakRef, image]() mutable { if (weakRef != nullptr) weakRef->handleImageCapture (image); }); + MessageManager::callAsync ([weakRef, image]() mutable + { + if (weakRef != nullptr && weakRef->pictureTakenCallback != nullptr) + weakRef->pictureTakenCallback (image); + }); }]; } } + void addListener (CameraDevice::Listener* listenerToAdd) + { + const ScopedLock sl (listenerLock); + listeners.add (listenerToAdd); + + if (listeners.size() == 1) + triggerImageCapture(); + } + + void removeListener (CameraDevice::Listener* listenerToRemove) + { + const ScopedLock sl (listenerLock); + listeners.remove (listenerToRemove); + } + static StringArray getAvailableDevices() { StringArray results; @@ -231,6 +252,9 @@ struct CameraDevice::Pimpl Time firstPresentationTime; bool isRecording = false; + CriticalSection listenerLock; + ListenerList listeners; + std::function pictureTakenCallback; JUCE_DECLARE_WEAK_REFERENCEABLE (Pimpl) diff --git a/modules/juce_video/native/juce_win32_CameraDevice.h b/modules/juce_video/native/juce_win32_CameraDevice.h index f775de6fb7..dc631b4821 100644 --- a/modules/juce_video/native/juce_win32_CameraDevice.h +++ b/modules/juce_video/native/juce_win32_CameraDevice.h @@ -195,7 +195,7 @@ struct CameraDevice::Pimpl : public ChangeBroadcaster void takeStillPicture (std::function pictureTakenCallbackToUse) { { - const ScopedLock sl (callbackLock); + const ScopedLock sl (pictureTakenCallbackLock); jassert (pictureTakenCallbackToUse != nullptr); @@ -229,10 +229,35 @@ struct CameraDevice::Pimpl : public ChangeBroadcaster return firstRecordedTime; } - void notifyImageReceivedIfNeeded (const Image& image) + void addListener (CameraDevice::Listener* listenerToAdd) + { + const ScopedLock sl (listenerLock); + + if (listeners.size() == 0) + addUser(); + + listeners.add (listenerToAdd); + } + + void removeListener (CameraDevice::Listener* listenerToRemove) + { + const ScopedLock sl (listenerLock); + listeners.remove (listenerToRemove); + + if (listeners.size() == 0) + removeUser(); + } + + void callListeners (const Image& image) + { + const ScopedLock sl (listenerLock); + listeners.call ([=] (Listener& l) { l.imageReceived (image); }); + } + + void notifyPictureTakenIfNeeded (const Image& image) { { - const ScopedLock sl (callbackLock); + const ScopedLock sl (pictureTakenCallbackLock); if (pictureTakenCallback == nullptr) return; @@ -305,7 +330,10 @@ struct CameraDevice::Pimpl : public ChangeBroadcaster imageNeedsFlipping = true; } - notifyImageReceivedIfNeeded (loadingImage); + if (listeners.size() > 0) + callListeners (loadingImage); + + notifyPictureTakenIfNeeded (loadingImage); sendChangeMessage(); } @@ -534,6 +562,12 @@ struct CameraDevice::Pimpl : public ChangeBroadcaster ComSmartPtr callback; + CriticalSection listenerLock; + ListenerList listeners; + + CriticalSection pictureTakenCallbackLock; + std::function pictureTakenCallback; + bool isRecording, openedSuccessfully; int width, height; Time firstRecordedTime; @@ -557,9 +591,6 @@ struct CameraDevice::Pimpl : public ChangeBroadcaster bool recordNextFrameTime; int previewMaxFPS; - CriticalSection callbackLock; - std::function pictureTakenCallback; - JUCE_DECLARE_WEAK_REFERENCEABLE (Pimpl) private: