|
- /*
- ==============================================================================
-
- This file is part of the JUCE library.
- Copyright (c) 2020 - Raw Material Software Limited
-
- JUCE is an open source library subject to commercial or open-source
- licensing.
-
- By using JUCE, you agree to the terms of both the JUCE 6 End-User License
- Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
-
- End User License Agreement: www.juce.com/juce-6-licence
- Privacy Policy: www.juce.com/juce-privacy-policy
-
- Or: You may also use this code under the terms of the GPL v3 (see
- www.gnu.org/licenses).
-
- JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
- EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
- DISCLAIMED.
-
- ==============================================================================
- */
-
- struct CameraDevice::Pimpl
- {
- #if defined (MAC_OS_X_VERSION_10_15) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_15
- #define JUCE_USE_NEW_APPLE_CAMERA_API 1
- #else
- #define JUCE_USE_NEW_APPLE_CAMERA_API 0
- #endif
-
- #if JUCE_USE_NEW_APPLE_CAMERA_API
- class PostCatalinaPhotoOutput
- {
- public:
- PostCatalinaPhotoOutput()
- {
- static PhotoOutputDelegateClass cls;
- delegate.reset ([cls.createInstance() init]);
- }
-
- void addImageCapture (AVCaptureSession* s)
- {
- if (imageOutput != nil)
- return;
-
- imageOutput = [[AVCapturePhotoOutput alloc] init];
- [s addOutput: imageOutput];
- }
-
- void removeImageCapture (AVCaptureSession* s)
- {
- if (imageOutput == nil)
- return;
-
- [s removeOutput: imageOutput];
- [imageOutput release];
- imageOutput = nil;
- }
-
- NSArray<AVCaptureConnection*>* getConnections() const
- {
- if (imageOutput != nil)
- return imageOutput.connections;
-
- return nil;
- }
-
- void triggerImageCapture (Pimpl& p)
- {
- if (imageOutput == nil)
- return;
-
- PhotoOutputDelegateClass::setOwner (delegate.get(), &p);
-
- [imageOutput capturePhotoWithSettings: [AVCapturePhotoSettings photoSettings]
- delegate: id<AVCapturePhotoCaptureDelegate> (delegate.get())];
- }
-
- static NSArray* getAvailableDevices()
- {
- auto* discovery = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes: @[AVCaptureDeviceTypeBuiltInWideAngleCamera,
- AVCaptureDeviceTypeExternalUnknown]
- mediaType: AVMediaTypeVideo
- position: AVCaptureDevicePositionUnspecified];
- return [discovery devices];
- }
-
- private:
- class PhotoOutputDelegateClass : public ObjCClass<NSObject>
- {
- public:
- PhotoOutputDelegateClass() : ObjCClass<NSObject> ("PhotoOutputDelegateClass_")
- {
- addMethod (@selector (captureOutput:didFinishProcessingPhoto:error:), didFinishProcessingPhoto, "v@:@@@");
- addIvar<Pimpl*> ("owner");
- registerClass();
- }
-
- static void didFinishProcessingPhoto (id self, SEL, AVCapturePhotoOutput*, AVCapturePhoto* photo, NSError* error)
- {
- if (error != nil)
- {
- String errorString = error != nil ? nsStringToJuce (error.localizedDescription) : String();
- ignoreUnused (errorString);
-
- JUCE_CAMERA_LOG ("Still picture capture failed, error: " + errorString);
- jassertfalse;
-
- return;
- }
-
- auto* imageData = [photo fileDataRepresentation];
- auto image = ImageFileFormat::loadFrom (imageData.bytes, (size_t) imageData.length);
-
- getOwner (self).imageCaptureFinished (image);
- }
-
- static Pimpl& getOwner (id self) { return *getIvar<Pimpl*> (self, "owner"); }
- static void setOwner (id self, Pimpl* t) { object_setInstanceVariable (self, "owner", t); }
- };
-
- AVCapturePhotoOutput* imageOutput = nil;
- std::unique_ptr<NSObject, NSObjectDeleter> delegate;
- };
- #else
- struct PreCatalinaStillImageOutput
- {
- public:
- void addImageCapture (AVCaptureSession* s)
- {
- if (imageOutput != nil)
- return;
-
- const auto codecType =
- #if defined (MAC_OS_X_VERSION_10_13) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_13
- AVVideoCodecTypeJPEG;
- #else
- AVVideoCodecJPEG;
- #endif
-
- imageOutput = [[AVCaptureStillImageOutput alloc] init];
- auto imageSettings = [[NSDictionary alloc] initWithObjectsAndKeys: codecType, AVVideoCodecKey, nil];
- [imageOutput setOutputSettings: imageSettings];
- [imageSettings release];
- [s addOutput: imageOutput];
- }
-
- void removeImageCapture (AVCaptureSession* s)
- {
- if (imageOutput == nil)
- return;
-
- [s removeOutput: imageOutput];
- [imageOutput release];
- imageOutput = nil;
- }
-
- NSArray<AVCaptureConnection*>* getConnections() const
- {
- if (imageOutput != nil)
- return imageOutput.connections;
-
- return nil;
- }
-
- void triggerImageCapture (Pimpl& p)
- {
- if (auto* videoConnection = p.getVideoConnection())
- {
- [imageOutput captureStillImageAsynchronouslyFromConnection: videoConnection
- completionHandler: ^(CMSampleBufferRef sampleBuffer, NSError* error)
- {
- if (error != nil)
- {
- JUCE_CAMERA_LOG ("Still picture capture failed, error: " + nsStringToJuce (error.localizedDescription));
- jassertfalse;
- return;
- }
-
- auto* imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation: sampleBuffer];
- auto image = ImageFileFormat::loadFrom (imageData.bytes, (size_t) imageData.length);
- p.imageCaptureFinished (image);
- }];
- }
- }
-
- static NSArray* getAvailableDevices()
- {
- return [AVCaptureDevice devicesWithMediaType: AVMediaTypeVideo];
- }
-
- private:
- AVCaptureStillImageOutput* imageOutput = nil;
- };
- #endif
-
- Pimpl (CameraDevice& ownerToUse, const String& deviceNameToUse, int /*index*/,
- int /*minWidth*/, int /*minHeight*/,
- int /*maxWidth*/, int /*maxHeight*/,
- bool useHighQuality)
- : owner (ownerToUse),
- deviceName (deviceNameToUse)
- {
- session = [[AVCaptureSession alloc] init];
-
- session.sessionPreset = useHighQuality ? AVCaptureSessionPresetHigh
- : AVCaptureSessionPresetMedium;
-
- refreshConnections();
-
- static DelegateClass cls;
- callbackDelegate = (id<AVCaptureFileOutputRecordingDelegate>) [cls.createInstance() init];
- DelegateClass::setOwner (callbackDelegate, this);
-
- JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
- [[NSNotificationCenter defaultCenter] addObserver: callbackDelegate
- selector: @selector (captureSessionRuntimeError:)
- name: AVCaptureSessionRuntimeErrorNotification
- object: session];
- JUCE_END_IGNORE_WARNINGS_GCC_LIKE
- }
-
- ~Pimpl()
- {
- [[NSNotificationCenter defaultCenter] removeObserver: callbackDelegate];
-
- [session stopRunning];
- removeInput();
- removeImageCapture();
- removeMovieCapture();
- [session release];
- [callbackDelegate release];
- }
-
- //==============================================================================
- bool openedOk() const noexcept { return openingError.isEmpty(); }
-
- void startSession()
- {
- if (! [session isRunning])
- [session startRunning];
- }
-
- void takeStillPicture (std::function<void (const Image&)> pictureTakenCallbackToUse)
- {
- if (pictureTakenCallbackToUse == nullptr)
- {
- jassertfalse;
- return;
- }
-
- pictureTakenCallback = std::move (pictureTakenCallbackToUse);
-
- triggerImageCapture();
- }
-
- void startRecordingToFile (const File& file, int /*quality*/)
- {
- stopRecording();
- refreshIfNeeded();
- firstPresentationTime = Time::getCurrentTime();
- file.deleteFile();
-
- startSession();
- isRecording = true;
- [fileOutput startRecordingToOutputFileURL: createNSURLFromFile (file)
- recordingDelegate: callbackDelegate];
- }
-
- void stopRecording()
- {
- if (isRecording)
- {
- [fileOutput stopRecording];
- isRecording = false;
- }
- }
-
- Time getTimeOfFirstRecordedFrame() const
- {
- return firstPresentationTime;
- }
-
- 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()
- {
- auto* devices = decltype (imageOutput)::getAvailableDevices();
-
- StringArray results;
-
- for (AVCaptureDevice* device : devices)
- results.add (nsStringToJuce ([device localizedName]));
-
- return results;
- }
-
- AVCaptureSession* getCaptureSession()
- {
- return session;
- }
-
- NSView* createVideoCapturePreview()
- {
- // The video preview must be created before the capture session is
- // started. Make sure you haven't called `addListener`,
- // `startRecordingToFile`, or `takeStillPicture` before calling this
- // function.
- jassert (! [session isRunning]);
- startSession();
-
- JUCE_AUTORELEASEPOOL
- {
- NSView* view = [[NSView alloc] init];
- [view setLayer: [AVCaptureVideoPreviewLayer layerWithSession: getCaptureSession()]];
- return view;
- }
- }
-
- private:
- //==============================================================================
- struct DelegateClass : public ObjCClass<NSObject>
- {
- DelegateClass() : ObjCClass<NSObject> ("JUCECameraDelegate_")
- {
- addIvar<Pimpl*> ("owner");
- addProtocol (@protocol (AVCaptureFileOutputRecordingDelegate));
-
- addMethod (@selector (captureOutput:didStartRecordingToOutputFileAtURL: fromConnections:), didStartRecordingToOutputFileAtURL, "v@:@@@");
- addMethod (@selector (captureOutput:didPauseRecordingToOutputFileAtURL: fromConnections:), didPauseRecordingToOutputFileAtURL, "v@:@@@");
- addMethod (@selector (captureOutput:didResumeRecordingToOutputFileAtURL: fromConnections:), didResumeRecordingToOutputFileAtURL, "v@:@@@");
- addMethod (@selector (captureOutput:willFinishRecordingToOutputFileAtURL:fromConnections:error:), willFinishRecordingToOutputFileAtURL, "v@:@@@@");
-
- JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
- addMethod (@selector (captureSessionRuntimeError:), sessionRuntimeError, "v@:@");
- JUCE_END_IGNORE_WARNINGS_GCC_LIKE
-
- registerClass();
- }
-
- static void setOwner (id self, Pimpl* owner) { object_setInstanceVariable (self, "owner", owner); }
- static Pimpl& getOwner (id self) { return *getIvar<Pimpl*> (self, "owner"); }
-
- private:
- static void didStartRecordingToOutputFileAtURL (id, SEL, AVCaptureFileOutput*, NSURL*, NSArray*) {}
- static void didPauseRecordingToOutputFileAtURL (id, SEL, AVCaptureFileOutput*, NSURL*, NSArray*) {}
- static void didResumeRecordingToOutputFileAtURL (id, SEL, AVCaptureFileOutput*, NSURL*, NSArray*) {}
- static void willFinishRecordingToOutputFileAtURL (id, SEL, AVCaptureFileOutput*, NSURL*, NSArray*, NSError*) {}
-
- static void sessionRuntimeError (id self, SEL, NSNotification* notification)
- {
- JUCE_CAMERA_LOG (nsStringToJuce ([notification description]));
-
- NSError* error = notification.userInfo[AVCaptureSessionErrorKey];
- auto errorString = error != nil ? nsStringToJuce (error.localizedDescription) : String();
- getOwner (self).cameraSessionRuntimeError (errorString);
- }
- };
-
- //==============================================================================
- void addImageCapture()
- {
- imageOutput.addImageCapture (session);
- }
-
- void addMovieCapture()
- {
- if (fileOutput == nil)
- {
- fileOutput = [[AVCaptureMovieFileOutput alloc] init];
- [session addOutput: fileOutput];
- }
- }
-
- void removeImageCapture()
- {
- imageOutput.removeImageCapture (session);
- }
-
- void removeMovieCapture()
- {
- if (fileOutput != nil)
- {
- [session removeOutput: fileOutput];
- [fileOutput release];
- fileOutput = nil;
- }
- }
-
- void removeCurrentSessionVideoInputs()
- {
- if (session != nil)
- {
- NSArray<AVCaptureDeviceInput*>* inputs = session.inputs;
-
- for (AVCaptureDeviceInput* input : inputs)
- if ([input.device hasMediaType: AVMediaTypeVideo])
- [session removeInput:input];
- }
- }
-
- void addInput()
- {
- if (currentInput == nil)
- {
- auto* availableDevices = decltype (imageOutput)::getAvailableDevices();
-
- for (AVCaptureDevice* device : availableDevices)
- {
- if (deviceName == nsStringToJuce ([device localizedName]))
- {
- removeCurrentSessionVideoInputs();
-
- NSError* err = nil;
- AVCaptureDeviceInput* inputDevice = [[AVCaptureDeviceInput alloc] initWithDevice: device
- error: &err];
-
- jassert (err == nil);
-
- if ([session canAddInput: inputDevice])
- {
- [session addInput: inputDevice];
- currentInput = inputDevice;
- }
- else
- {
- jassertfalse;
- [inputDevice release];
- }
-
- return;
- }
- }
- }
- }
-
- void removeInput()
- {
- if (currentInput != nil)
- {
- [session removeInput: currentInput];
- [currentInput release];
- currentInput = nil;
- }
- }
-
- void refreshConnections()
- {
- [session beginConfiguration];
- removeInput();
- removeImageCapture();
- removeMovieCapture();
- addInput();
- addImageCapture();
- addMovieCapture();
- [session commitConfiguration];
- }
-
- void refreshIfNeeded()
- {
- if (getVideoConnection() == nullptr)
- refreshConnections();
- }
-
- AVCaptureConnection* getVideoConnection() const
- {
- auto* connections = imageOutput.getConnections();
-
- if (connections != nil)
- for (AVCaptureConnection* connection in connections)
- if ([connection isActive] && [connection isEnabled])
- for (AVCaptureInputPort* port in [connection inputPorts])
- if ([[port mediaType] isEqual: AVMediaTypeVideo])
- return connection;
-
- return nil;
- }
-
- void imageCaptureFinished (const Image& image)
- {
- handleImageCapture (image);
-
- WeakReference<Pimpl> weakRef (this);
- MessageManager::callAsync ([weakRef, image]() mutable
- {
- if (weakRef != nullptr && weakRef->pictureTakenCallback != nullptr)
- weakRef->pictureTakenCallback (image);
- });
- }
-
- void handleImageCapture (const Image& image)
- {
- const ScopedLock sl (listenerLock);
- listeners.call ([=] (Listener& l) { l.imageReceived (image); });
-
- if (! listeners.isEmpty())
- triggerImageCapture();
- }
-
- void triggerImageCapture()
- {
- refreshIfNeeded();
-
- startSession();
-
- if (auto* videoConnection = getVideoConnection())
- imageOutput.triggerImageCapture (*this);
- }
-
- void cameraSessionRuntimeError (const String& error)
- {
- JUCE_CAMERA_LOG ("cameraSessionRuntimeError(), error = " + error);
-
- if (owner.onErrorOccurred != nullptr)
- owner.onErrorOccurred (error);
- }
-
- //==============================================================================
- CameraDevice& owner;
- String deviceName;
-
- AVCaptureSession* session = nil;
- AVCaptureMovieFileOutput* fileOutput = nil;
- #if JUCE_USE_NEW_APPLE_CAMERA_API
- PostCatalinaPhotoOutput imageOutput;
- #else
- PreCatalinaStillImageOutput imageOutput;
- #endif
- AVCaptureDeviceInput* currentInput = nil;
-
- id<AVCaptureFileOutputRecordingDelegate> callbackDelegate = nil;
- String openingError;
- Time firstPresentationTime;
- bool isRecording = false;
-
- CriticalSection listenerLock;
- ListenerList<Listener> listeners;
-
- std::function<void (const Image&)> pictureTakenCallback = nullptr;
-
- //==============================================================================
- JUCE_DECLARE_WEAK_REFERENCEABLE (Pimpl)
- JUCE_DECLARE_NON_COPYABLE (Pimpl)
- };
-
- //==============================================================================
- struct CameraDevice::ViewerComponent : public NSViewComponent
- {
- ViewerComponent (CameraDevice& device)
- {
- setView (device.pimpl->createVideoCapturePreview());
- }
-
- ~ViewerComponent()
- {
- setView (nil);
- }
-
- JUCE_DECLARE_NON_COPYABLE (ViewerComponent)
- };
-
- String CameraDevice::getFileExtension()
- {
- return ".mov";
- }
-
- #undef JUCE_USE_NEW_APPLE_CAMERA_API
|