| @@ -4,33 +4,6 @@ JUCE breaking changes | |||||
| Develop | 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 | Change | ||||
| ------ | ------ | ||||
| JUCE no longer supports OS X deployment targets earlier than 10.7. | JUCE no longer supports OS X deployment targets earlier than 10.7. | ||||
| @@ -180,6 +180,18 @@ void CameraDevice::stopRecording() | |||||
| pimpl->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() | StringArray CameraDevice::getAvailableDevices() | ||||
| { | { | ||||
| @@ -174,6 +174,48 @@ public: | |||||
| and reopen the device to be able to use it further. */ | and reopen the device to be able to use it further. */ | ||||
| std::function<void (const String& /*error*/)> onErrorOccurred; | std::function<void (const String& /*error*/)> 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: | private: | ||||
| String name; | String name; | ||||
| @@ -534,6 +534,9 @@ struct CameraDevice::Pimpl | |||||
| void startRecordingToFile (const File&, int) {} | void startRecordingToFile (const File&, int) {} | ||||
| void stopRecording() {} | void stopRecording() {} | ||||
| void addListener (CameraDevice::Listener*) {} | |||||
| void removeListener (CameraDevice::Listener*) {} | |||||
| String getCameraId() const noexcept { return {}; } | String getCameraId() const noexcept { return {}; } | ||||
| bool openedOk() const noexcept { return false; } | bool openedOk() const noexcept { return false; } | ||||
| Time getTimeOfFirstRecordedFrame() const { return {}; } | Time getTimeOfFirstRecordedFrame() const { return {}; } | ||||
| @@ -668,6 +671,21 @@ struct CameraDevice::Pimpl | |||||
| return results; | 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: | private: | ||||
| enum | enum | ||||
| { | { | ||||
| @@ -1197,9 +1215,11 @@ private: | |||||
| WeakReference<ImageReader> safeThis (this); | WeakReference<ImageReader> safeThis (this); | ||||
| owner.callListeners (image); | |||||
| // Android may take multiple pictures before it handles a request to stop. | // Android may take multiple pictures before it handles a request to stop. | ||||
| if (hasNotifiedListeners.compareAndSetBool (1, 0)) | 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 | struct ImageBuffer | ||||
| @@ -1901,7 +1921,9 @@ private: | |||||
| // Delay still picture capture for devices that can't handle it right after | // Delay still picture capture for devices that can't handle it right after | ||||
| // stopRepeating/abortCaptures calls. | // 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); | env->CallBooleanMethod (handler, AndroidHandler.postDelayed, delayedCaptureRunnable.get(), (jlong) 200); | ||||
| } | } | ||||
| @@ -1951,8 +1973,6 @@ private: | |||||
| if (Pimpl::checkHasExceptionOccurred()) | if (Pimpl::checkHasExceptionOccurred()) | ||||
| return; | return; | ||||
| delayedCaptureRunnable.clear(); | |||||
| // NB: for preview, using preview capture request again | // NB: for preview, using preview capture request again | ||||
| env->CallIntMethod (captureSession, CameraCaptureSession.setRepeatingRequest, previewCaptureRequest.get(), | env->CallIntMethod (captureSession, CameraCaptureSession.setRepeatingRequest, previewCaptureRequest.get(), | ||||
| nullptr, handler.get()); | nullptr, handler.get()); | ||||
| @@ -2773,6 +2793,9 @@ private: | |||||
| std::unique_ptr<ScopedCameraDevice> scopedCameraDevice; | std::unique_ptr<ScopedCameraDevice> scopedCameraDevice; | ||||
| CriticalSection listenerLock; | |||||
| ListenerList<Listener> listeners; | |||||
| std::function<void (const Image&)> pictureTakenCallback; | std::function<void (const Image&)> pictureTakenCallback; | ||||
| Time firstRecordedFrameTimeMs; | Time firstRecordedFrameTimeMs; | ||||
| @@ -2888,9 +2911,16 @@ private: | |||||
| cameraOpenCallback (cameraId, error); | 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) | if (pictureTakenCallback != nullptr) | ||||
| pictureTakenCallback (image); | pictureTakenCallback (image); | ||||
| @@ -123,6 +123,21 @@ struct CameraDevice::Pimpl | |||||
| return results; | 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: | private: | ||||
| static NSArray<AVCaptureDevice*>* getDevices() | static NSArray<AVCaptureDevice*>* getDevices() | ||||
| { | { | ||||
| @@ -512,11 +527,11 @@ private: | |||||
| { | { | ||||
| #pragma clang diagnostic push | #pragma clang diagnostic push | ||||
| #pragma clang diagnostic ignored "-Wundeclared-selector" | #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 | #pragma clang diagnostic pop | ||||
| addIvar<CaptureSession*> ("owner"); | addIvar<CaptureSession*> ("owner"); | ||||
| @@ -641,7 +656,10 @@ private: | |||||
| NSData* imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation: imageSampleBuffer]; | NSData* imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation: imageSampleBuffer]; | ||||
| auto image = ImageFileFormat::loadFrom (imageData.bytes, (size_t) imageData.length); | 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 | else | ||||
| @@ -815,7 +833,10 @@ private: | |||||
| auto* imageData = UIImageJPEGRepresentation (uiImage, 0.f); | auto* imageData = UIImageJPEGRepresentation (uiImage, 0.f); | ||||
| auto image = ImageFileFormat::loadFrom (imageData.bytes, (size_t) imageData.length); | 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, | static UIImage* getImageWithCorrectOrientation (CGImagePropertyOrientation imageOrientation, | ||||
| @@ -912,7 +933,10 @@ private: | |||||
| auto* imageData = UIImageJPEGRepresentation (uiImage, 0.f); | auto* imageData = UIImageJPEGRepresentation (uiImage, 0.f); | ||||
| auto image = ImageFileFormat::loadFrom (imageData.bytes, (size_t) imageData.length); | 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) | 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; | takingPicture = false; | ||||
| captureSession.notifyImageReceived (image); | |||||
| captureSession.notifyPictureTaken (image); | |||||
| } | } | ||||
| CaptureSession& captureSession; | CaptureSession& captureSession; | ||||
| @@ -1105,9 +1134,14 @@ private: | |||||
| owner.cameraSessionRuntimeError (error); | 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; | 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) | if (pictureTakenCallback != nullptr) | ||||
| pictureTakenCallback (image); | pictureTakenCallback (image); | ||||
| @@ -1171,6 +1211,9 @@ private: | |||||
| String cameraId; | String cameraId; | ||||
| InternalOpenCameraResultCallback cameraOpenCallback; | InternalOpenCameraResultCallback cameraOpenCallback; | ||||
| CriticalSection listenerLock; | |||||
| ListenerList<Listener> listeners; | |||||
| std::function<void (const Image&)> pictureTakenCallback; | std::function<void (const Image&)> pictureTakenCallback; | ||||
| CaptureSession captureSession; | CaptureSession captureSession; | ||||
| @@ -175,8 +175,8 @@ struct CameraDevice::Pimpl | |||||
| void handleImageCapture (const Image& image) | void handleImageCapture (const Image& image) | ||||
| { | { | ||||
| if (pictureTakenCallback != nullptr) | |||||
| pictureTakenCallback (image); | |||||
| const ScopedLock sl (listenerLock); | |||||
| listeners.call ([=] (Listener& l) { l.imageReceived (image); }); | |||||
| } | } | ||||
| void triggerImageCapture() | void triggerImageCapture() | ||||
| @@ -199,12 +199,33 @@ struct CameraDevice::Pimpl | |||||
| auto image = ImageFileFormat::loadFrom (imageData.bytes, (size_t) imageData.length); | auto image = ImageFileFormat::loadFrom (imageData.bytes, (size_t) imageData.length); | ||||
| handleImageCapture (image); | |||||
| WeakReference<Pimpl> weakRef (this); | WeakReference<Pimpl> 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() | static StringArray getAvailableDevices() | ||||
| { | { | ||||
| StringArray results; | StringArray results; | ||||
| @@ -231,6 +252,9 @@ struct CameraDevice::Pimpl | |||||
| Time firstPresentationTime; | Time firstPresentationTime; | ||||
| bool isRecording = false; | bool isRecording = false; | ||||
| CriticalSection listenerLock; | |||||
| ListenerList<Listener> listeners; | |||||
| std::function<void (const Image&)> pictureTakenCallback; | std::function<void (const Image&)> pictureTakenCallback; | ||||
| JUCE_DECLARE_WEAK_REFERENCEABLE (Pimpl) | JUCE_DECLARE_WEAK_REFERENCEABLE (Pimpl) | ||||
| @@ -195,7 +195,7 @@ struct CameraDevice::Pimpl : public ChangeBroadcaster | |||||
| void takeStillPicture (std::function<void (const Image&)> pictureTakenCallbackToUse) | void takeStillPicture (std::function<void (const Image&)> pictureTakenCallbackToUse) | ||||
| { | { | ||||
| { | { | ||||
| const ScopedLock sl (callbackLock); | |||||
| const ScopedLock sl (pictureTakenCallbackLock); | |||||
| jassert (pictureTakenCallbackToUse != nullptr); | jassert (pictureTakenCallbackToUse != nullptr); | ||||
| @@ -229,10 +229,35 @@ struct CameraDevice::Pimpl : public ChangeBroadcaster | |||||
| return firstRecordedTime; | 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) | if (pictureTakenCallback == nullptr) | ||||
| return; | return; | ||||
| @@ -305,7 +330,10 @@ struct CameraDevice::Pimpl : public ChangeBroadcaster | |||||
| imageNeedsFlipping = true; | imageNeedsFlipping = true; | ||||
| } | } | ||||
| notifyImageReceivedIfNeeded (loadingImage); | |||||
| if (listeners.size() > 0) | |||||
| callListeners (loadingImage); | |||||
| notifyPictureTakenIfNeeded (loadingImage); | |||||
| sendChangeMessage(); | sendChangeMessage(); | ||||
| } | } | ||||
| @@ -534,6 +562,12 @@ struct CameraDevice::Pimpl : public ChangeBroadcaster | |||||
| ComSmartPtr<GrabberCallback> callback; | ComSmartPtr<GrabberCallback> callback; | ||||
| CriticalSection listenerLock; | |||||
| ListenerList<Listener> listeners; | |||||
| CriticalSection pictureTakenCallbackLock; | |||||
| std::function<void (const Image&)> pictureTakenCallback; | |||||
| bool isRecording, openedSuccessfully; | bool isRecording, openedSuccessfully; | ||||
| int width, height; | int width, height; | ||||
| Time firstRecordedTime; | Time firstRecordedTime; | ||||
| @@ -557,9 +591,6 @@ struct CameraDevice::Pimpl : public ChangeBroadcaster | |||||
| bool recordNextFrameTime; | bool recordNextFrameTime; | ||||
| int previewMaxFPS; | int previewMaxFPS; | ||||
| CriticalSection callbackLock; | |||||
| std::function<void (const Image&)> pictureTakenCallback; | |||||
| JUCE_DECLARE_WEAK_REFERENCEABLE (Pimpl) | JUCE_DECLARE_WEAK_REFERENCEABLE (Pimpl) | ||||
| private: | private: | ||||