//
// 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