// // PlugIn.mm // obs-mac-virtualcam // // Created by John Boiles on 4/9/20. // // obs-mac-virtualcam is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 2 of the License, or // (at your option) any later version. // // obs-mac-virtualcam is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with obs-mac-virtualcam. If not, see . #import "OBSDALPlugIn.h" #import "Logging.h" typedef enum { PlugInStateNotStarted = 0, PlugInStateWaitingForServer, PlugInStateReceivingFrames, } OBSDALPlugInState; @interface OBSDALPlugin () { //! Serial queue for all state changes that need to be concerned with thread safety dispatch_queue_t _stateQueue; //! Repeated timer for driving the mach server re-connection dispatch_source_t _machConnectTimer; //! Timeout timer when we haven't received frames for 5s dispatch_source_t _timeoutTimer; } @property OBSDALPlugInState state; @property OBSDALMachClient *machClient; @end @implementation OBSDALPlugin + (OBSDALPlugin *)SharedPlugIn { static OBSDALPlugin *sPlugIn = nil; static dispatch_once_t sOnceToken; dispatch_once(&sOnceToken, ^{ sPlugIn = [[self alloc] init]; }); return sPlugIn; } - (instancetype)init { if (self = [super init]) { _stateQueue = dispatch_queue_create("com.obsproject.obs-mac-virtualcam.dal.state", DISPATCH_QUEUE_SERIAL); _timeoutTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _stateQueue); __weak __typeof(self) weakSelf = self; dispatch_source_set_event_handler(_timeoutTimer, ^{ if (weakSelf.state == PlugInStateReceivingFrames) { DLog(@"No frames received for 5s, restarting connection"); [self stopStream]; [self startStream]; } }); _machClient = [[OBSDALMachClient alloc] init]; _machClient.delegate = self; _machConnectTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _stateQueue); dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, 0); uint64_t intervalTime = (int64_t) (1 * NSEC_PER_SEC); dispatch_source_set_timer(_machConnectTimer, startTime, intervalTime, 0); dispatch_source_set_event_handler(_machConnectTimer, ^{ __strong __typeof(weakSelf) strongSelf = weakSelf; if (![[strongSelf machClient] isServerAvailable]) { DLog(@"Server is not available"); } else if (strongSelf.state == PlugInStateWaitingForServer) { DLog(@"Attempting connection"); [[strongSelf machClient] connectToServer]; } }); } return self; } - (void)startStream { dispatch_async(_stateQueue, ^{ if (self->_state == PlugInStateNotStarted) { dispatch_resume(self->_machConnectTimer); [self.stream startServingDefaultFrames]; self->_state = PlugInStateWaitingForServer; } }); } - (void)stopStream { dispatch_async(_stateQueue, ^{ if (self->_state == PlugInStateWaitingForServer) { dispatch_suspend(self->_machConnectTimer); [self.stream stopServingDefaultFrames]; } else if (self->_state == PlugInStateReceivingFrames) { // TODO: Disconnect from the mach server? dispatch_suspend(self->_timeoutTimer); } self->_state = PlugInStateNotStarted; }); } - (void)initialize {} - (void)teardown {} #pragma mark - CMIOObject - (BOOL)hasPropertyWithAddress:(CMIOObjectPropertyAddress)address { switch (address.mSelector) { case kCMIOObjectPropertyName: return true; default: DLog(@"PlugIn unhandled hasPropertyWithAddress for %@", [OBSDALObjectStore StringFromPropertySelector:address.mSelector]); return false; }; } - (BOOL)isPropertySettableWithAddress:(CMIOObjectPropertyAddress)address { switch (address.mSelector) { case kCMIOObjectPropertyName: return false; default: DLog(@"PlugIn unhandled isPropertySettableWithAddress for %@", [OBSDALObjectStore StringFromPropertySelector:address.mSelector]); return false; }; } - (UInt32)getPropertyDataSizeWithAddress:(CMIOObjectPropertyAddress)address qualifierDataSize:(UInt32)qualifierDataSize qualifierData:(const void *)qualifierData { switch (address.mSelector) { case kCMIOObjectPropertyName: return sizeof(CFStringRef); default: DLog(@"PlugIn unhandled getPropertyDataSizeWithAddress for %@", [OBSDALObjectStore StringFromPropertySelector:address.mSelector]); return 0; }; } - (void)getPropertyDataWithAddress:(CMIOObjectPropertyAddress)address qualifierDataSize:(UInt32)qualifierDataSize qualifierData:(nonnull const void *)qualifierData dataSize:(UInt32)dataSize dataUsed:(nonnull UInt32 *)dataUsed data:(nonnull void *)data { switch (address.mSelector) { case kCMIOObjectPropertyName: *static_cast(data) = CFSTR("OBS Virtual Camera Plugin"); *dataUsed = sizeof(CFStringRef); return; default: DLog(@"PlugIn unhandled getPropertyDataWithAddress for %@", [OBSDALObjectStore StringFromPropertySelector:address.mSelector]); return; }; } - (void)setPropertyDataWithAddress:(CMIOObjectPropertyAddress)address qualifierDataSize:(UInt32)qualifierDataSize qualifierData:(nonnull const void *)qualifierData dataSize:(UInt32)dataSize data:(nonnull const void *)data { DLog(@"PlugIn unhandled setPropertyDataWithAddress for %@", [OBSDALObjectStore StringFromPropertySelector:address.mSelector]); } #pragma mark - MachClientDelegate - (void)receivedPixelBuffer:(CVPixelBufferRef)frame timestamp:(uint64_t)timestamp fpsNumerator:(uint32_t)fpsNumerator fpsDenominator:(uint32_t)fpsDenominator { size_t width = CVPixelBufferGetWidth(frame); size_t height = CVPixelBufferGetHeight(frame); dispatch_sync(_stateQueue, ^{ if (_state == PlugInStateWaitingForServer) { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults setInteger:(long) width forKey:kTestCardWidthKey]; [defaults setInteger:(long) height forKey:kTestCardHeightKey]; [defaults setDouble:(double) fpsNumerator / (double) fpsDenominator forKey:kTestCardFPSKey]; dispatch_suspend(_machConnectTimer); [self.stream stopServingDefaultFrames]; dispatch_resume(_timeoutTimer); _state = PlugInStateReceivingFrames; } }); // Add 5 more seconds onto the timeout timer dispatch_source_set_timer(_timeoutTimer, dispatch_time(DISPATCH_TIME_NOW, (int64_t) (5.0 * NSEC_PER_SEC)), (uint64_t) (5.0 * NSEC_PER_SEC), (1ull * NSEC_PER_SEC) / 10); [self.stream queuePixelBuffer:frame timestamp:timestamp fpsNumerator:fpsNumerator fpsDenominator:fpsDenominator]; } - (void)receivedStop { DLogFunc(@"Restarting connection"); [self stopStream]; [self startStream]; } @end