536 lines
20 KiB
Text
536 lines
20 KiB
Text
//
|
|
// Stream.mm
|
|
// obs-mac-virtualcam
|
|
//
|
|
// Created by John Boiles on 4/10/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 <http://www.gnu.org/licenses/>.
|
|
|
|
#import "OBSDALStream.h"
|
|
|
|
#import <AppKit/AppKit.h>
|
|
#import <mach/mach_time.h>
|
|
|
|
#import "Logging.h"
|
|
#import "CMSampleBufferUtils.h"
|
|
#import "OBSDALPlugIn.h"
|
|
|
|
@interface OBSDALStream () {
|
|
CMSimpleQueueRef _queue;
|
|
CFTypeRef _clock;
|
|
NSImage *_testCardImage;
|
|
dispatch_source_t _frameDispatchSource;
|
|
NSSize _testCardSize;
|
|
Float64 _fps;
|
|
}
|
|
|
|
@property CMIODeviceStreamQueueAlteredProc alteredProc;
|
|
@property void *alteredRefCon;
|
|
@property (readonly) CMSimpleQueueRef queue;
|
|
@property (readonly) CFTypeRef clock;
|
|
@property UInt64 sequenceNumber;
|
|
@property (readonly) NSImage *testCardImage;
|
|
@property (readonly) NSSize testCardSize;
|
|
@property (readonly) Float64 fps;
|
|
|
|
@end
|
|
|
|
@implementation OBSDALStream
|
|
|
|
#define DEFAULT_FPS 30.0
|
|
#define DEFAULT_WIDTH 1280
|
|
#define DEFAULT_HEIGHT 720
|
|
|
|
- (instancetype _Nonnull)init
|
|
{
|
|
self = [super init];
|
|
if (self) {
|
|
_frameDispatchSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,
|
|
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
|
|
__weak __typeof(self) wself = self;
|
|
dispatch_source_set_event_handler(_frameDispatchSource, ^{
|
|
[wself fillFrame];
|
|
});
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
DLog(@"Stream Dealloc");
|
|
CMIOStreamClockInvalidate(_clock);
|
|
CFRelease(_clock);
|
|
_clock = NULL;
|
|
CFRelease(_queue);
|
|
_queue = NULL;
|
|
dispatch_suspend(_frameDispatchSource);
|
|
}
|
|
|
|
- (void)startServingDefaultFrames
|
|
{
|
|
DLogFunc(@"");
|
|
_testCardImage = nil;
|
|
_testCardSize = NSZeroSize;
|
|
_fps = 0;
|
|
dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, 0);
|
|
uint64_t intervalTime = (int64_t) (NSEC_PER_SEC / self.fps);
|
|
dispatch_source_set_timer(_frameDispatchSource, startTime, intervalTime, 0);
|
|
dispatch_resume(_frameDispatchSource);
|
|
}
|
|
|
|
- (void)stopServingDefaultFrames
|
|
{
|
|
DLogFunc(@"");
|
|
dispatch_suspend(_frameDispatchSource);
|
|
}
|
|
|
|
- (CMSimpleQueueRef)queue
|
|
{
|
|
if (_queue == NULL) {
|
|
// Allocate a one-second long queue, which we can use our FPS constant for.
|
|
OSStatus err = CMSimpleQueueCreate(kCFAllocatorDefault, (int32_t) self.fps, &_queue);
|
|
if (err != noErr) {
|
|
DLog(@"Err %d in CMSimpleQueueCreate", err);
|
|
}
|
|
}
|
|
return _queue;
|
|
}
|
|
|
|
- (CFTypeRef)clock
|
|
{
|
|
if (_clock == NULL) {
|
|
OSStatus err = CMIOStreamClockCreate(kCFAllocatorDefault, CFSTR("obs-mac-virtualcam::Stream::clock"),
|
|
(__bridge void *) self, CMTimeMake(1, 10), 100, 10, &_clock);
|
|
if (err != noErr) {
|
|
DLog(@"Error %d from CMIOStreamClockCreate", err);
|
|
}
|
|
}
|
|
return _clock;
|
|
}
|
|
|
|
- (NSSize)testCardSize
|
|
{
|
|
if (NSEqualSizes(_testCardSize, NSZeroSize)) {
|
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
NSInteger width = [[defaults objectForKey:kTestCardWidthKey] integerValue];
|
|
NSInteger height = [[defaults objectForKey:kTestCardHeightKey] integerValue];
|
|
if (width == 0 || height == 0) {
|
|
_testCardSize = NSMakeSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
|
} else {
|
|
_testCardSize = NSMakeSize(width, height);
|
|
}
|
|
}
|
|
return _testCardSize;
|
|
}
|
|
|
|
- (Float64)fps
|
|
{
|
|
if (_fps == 0) {
|
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
double fps = [[defaults objectForKey:kTestCardFPSKey] doubleValue];
|
|
if (fps == 0) {
|
|
_fps = DEFAULT_FPS;
|
|
} else {
|
|
_fps = fps;
|
|
}
|
|
}
|
|
return _fps;
|
|
}
|
|
|
|
- (NSImage *)testCardImage
|
|
{
|
|
if (_testCardImage == nil) {
|
|
NSString *bundlePath = [[NSBundle bundleForClass:[OBSDALStream class]] bundlePath];
|
|
NSString *placeHolderPath = [bundlePath stringByAppendingString:@"/Contents/Resources/placeholder.png"];
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
NSURL *homeUrl = [fileManager homeDirectoryForCurrentUser];
|
|
NSURL *customUrl =
|
|
[homeUrl URLByAppendingPathComponent:
|
|
@"Library/Application Support/obs-studio/plugin_config/mac-virtualcam/placeholder.png"];
|
|
NSString *customPlaceHolder = customUrl.path;
|
|
if ([fileManager isReadableFileAtPath:customPlaceHolder])
|
|
placeHolderPath = customPlaceHolder;
|
|
DLog(@"PlaceHolder:%@", placeHolderPath);
|
|
NSImage *placeholderImage = [[NSImage alloc] initWithContentsOfFile:placeHolderPath];
|
|
|
|
NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
|
|
pixelsWide:(NSInteger) self.testCardSize.width
|
|
pixelsHigh:(NSInteger) self.testCardSize.height
|
|
bitsPerSample:8
|
|
samplesPerPixel:4
|
|
hasAlpha:YES
|
|
isPlanar:NO
|
|
colorSpaceName:NSCalibratedRGBColorSpace
|
|
bytesPerRow:0
|
|
bitsPerPixel:0];
|
|
rep.size = self.testCardSize;
|
|
|
|
double hScale = placeholderImage.size.width / self.testCardSize.width;
|
|
double vScale = placeholderImage.size.height / self.testCardSize.height;
|
|
|
|
double scaling = fmax(hScale, vScale);
|
|
|
|
double newWidth = placeholderImage.size.width / scaling;
|
|
double newHeight = placeholderImage.size.height / scaling;
|
|
|
|
double leftOffset = (self.testCardSize.width - newWidth) / 2;
|
|
double topOffset = (self.testCardSize.height - newHeight) / 2;
|
|
|
|
[NSGraphicsContext saveGraphicsState];
|
|
[NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithBitmapImageRep:rep]];
|
|
|
|
NSColor *backgroundColor = [NSColor blackColor];
|
|
[backgroundColor set];
|
|
NSRectFill(NSMakeRect(0, 0, self.testCardSize.width, self.testCardSize.height));
|
|
|
|
[placeholderImage drawInRect:NSMakeRect(leftOffset, topOffset, newWidth, newHeight) fromRect:NSZeroRect
|
|
operation:NSCompositingOperationCopy
|
|
fraction:1.0];
|
|
[NSGraphicsContext restoreGraphicsState];
|
|
|
|
NSImage *testCardImage = [[NSImage alloc] initWithSize:self.testCardSize];
|
|
[testCardImage addRepresentation:rep];
|
|
|
|
_testCardImage = testCardImage;
|
|
}
|
|
return _testCardImage;
|
|
}
|
|
|
|
- (CMSimpleQueueRef)copyBufferQueueWithAlteredProc:(CMIODeviceStreamQueueAlteredProc)alteredProc
|
|
alteredRefCon:(void *)alteredRefCon
|
|
{
|
|
self.alteredProc = alteredProc;
|
|
self.alteredRefCon = alteredRefCon;
|
|
|
|
// Retain this since it's a copy operation
|
|
CFRetain(self.queue);
|
|
|
|
return self.queue;
|
|
}
|
|
|
|
- (CVPixelBufferRef)createPixelBufferWithTestAnimation
|
|
{
|
|
int width = (int) self.testCardSize.width;
|
|
int height = (int) self.testCardSize.height;
|
|
|
|
NSDictionary *options = [NSDictionary
|
|
dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey,
|
|
[NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey, nil];
|
|
CVPixelBufferRef pxbuffer = NULL;
|
|
CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_32ARGB,
|
|
(__bridge CFDictionaryRef) options, &pxbuffer);
|
|
|
|
NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
|
|
|
|
CVPixelBufferLockBaseAddress(pxbuffer, 0);
|
|
void *pxdata = CVPixelBufferGetBaseAddressOfPlane(pxbuffer, 0);
|
|
NSParameterAssert(pxdata != NULL);
|
|
|
|
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
|
|
CGContextRef context = CGBitmapContextCreate(pxdata, width, height, 8,
|
|
CVPixelBufferGetBytesPerRowOfPlane(pxbuffer, 0), rgbColorSpace,
|
|
kCGImageAlphaPremultipliedFirst | kCGImageByteOrder32Big);
|
|
CFRelease(rgbColorSpace);
|
|
NSParameterAssert(context);
|
|
|
|
NSGraphicsContext *nsContext = [NSGraphicsContext graphicsContextWithCGContext:context flipped:NO];
|
|
[NSGraphicsContext setCurrentContext:nsContext];
|
|
|
|
NSRect rect = NSMakeRect(0, 0, self.testCardImage.size.width, self.testCardImage.size.height);
|
|
CGImageRef image = [self.testCardImage CGImageForProposedRect:&rect context:nsContext hints:nil];
|
|
CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image), CGImageGetHeight(image)), image);
|
|
|
|
// DrawDialWithFrame(
|
|
// NSMakeRect(0, 0, width, height),
|
|
// (int(self.fps) - self.sequenceNumber % int(self.fps)) * 360 /
|
|
// int(self.fps));
|
|
|
|
CGContextRelease(context);
|
|
|
|
CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
|
|
|
|
return pxbuffer;
|
|
}
|
|
|
|
- (void)fillFrame
|
|
{
|
|
if (CMSimpleQueueGetFullness(self.queue) >= 1.0) {
|
|
return;
|
|
}
|
|
|
|
CVPixelBufferRef pixelBuffer = [self createPixelBufferWithTestAnimation];
|
|
|
|
uint64_t hostTime = clock_gettime_nsec_np(CLOCK_UPTIME_RAW);
|
|
CMSampleTimingInfo timingInfo = CMSampleTimingInfoForTimestamp(hostTime, (uint32_t) self.fps, 1);
|
|
|
|
OSStatus err = CMIOStreamClockPostTimingEvent(timingInfo.presentationTimeStamp, hostTime, true, self.clock);
|
|
if (err != noErr) {
|
|
DLog(@"CMIOStreamClockPostTimingEvent err %d", err);
|
|
}
|
|
|
|
CMFormatDescriptionRef format;
|
|
CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault, pixelBuffer, &format);
|
|
|
|
self.sequenceNumber = CMIOGetNextSequenceNumber(self.sequenceNumber);
|
|
|
|
CMSampleBufferRef buffer;
|
|
err = CMIOSampleBufferCreateForImageBuffer(kCFAllocatorDefault, pixelBuffer, format, &timingInfo,
|
|
self.sequenceNumber, kCMIOSampleBufferNoDiscontinuities, &buffer);
|
|
CFRelease(pixelBuffer);
|
|
CFRelease(format);
|
|
if (err != noErr) {
|
|
DLog(@"CMIOSampleBufferCreateForImageBuffer err %d", err);
|
|
}
|
|
|
|
CMSimpleQueueEnqueue(self.queue, buffer);
|
|
|
|
// Inform the clients that the queue has been altered
|
|
if (self.alteredProc != NULL) {
|
|
(self.alteredProc)(self.objectId, buffer, self.alteredRefCon);
|
|
}
|
|
}
|
|
|
|
- (void)queuePixelBuffer:(CVPixelBufferRef)frame
|
|
timestamp:(uint64_t)timestamp
|
|
fpsNumerator:(uint32_t)fpsNumerator
|
|
fpsDenominator:(uint32_t)fpsDenominator
|
|
{
|
|
if (CMSimpleQueueGetFullness(self.queue) >= 1.0) {
|
|
DLog(@"Queue is full, bailing out");
|
|
return;
|
|
}
|
|
OSStatus err = noErr;
|
|
|
|
CMSampleTimingInfo timingInfo = CMSampleTimingInfoForTimestamp(timestamp, fpsNumerator, fpsDenominator);
|
|
|
|
err = CMIOStreamClockPostTimingEvent(timingInfo.presentationTimeStamp, clock_gettime_nsec_np(CLOCK_UPTIME_RAW),
|
|
true, self.clock);
|
|
if (err != noErr) {
|
|
DLog(@"CMIOStreamClockPostTimingEvent err %d", err);
|
|
}
|
|
|
|
self.sequenceNumber = CMIOGetNextSequenceNumber(self.sequenceNumber);
|
|
|
|
CMSampleBufferRef sampleBuffer;
|
|
|
|
// Generate the video format description from that pixel buffer
|
|
CMVideoFormatDescriptionRef format;
|
|
err = CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault, frame, &format);
|
|
if (err != noErr) {
|
|
DLog(@"CMVideoFormatDescriptionCreateForImageBuffer err %d", err);
|
|
return;
|
|
}
|
|
|
|
err = CMIOSampleBufferCreateForImageBuffer(kCFAllocatorDefault, frame, format, &timingInfo, self.sequenceNumber,
|
|
kCMIOSampleBufferNoDiscontinuities, &sampleBuffer);
|
|
|
|
CFRelease(format);
|
|
|
|
if (err != noErr) {
|
|
DLog(@"CMIOSampleBufferCreateForImageBuffer err %d", err);
|
|
return;
|
|
}
|
|
|
|
err = CMSimpleQueueEnqueue(self.queue, sampleBuffer);
|
|
|
|
if (err != noErr) {
|
|
CFRelease(sampleBuffer);
|
|
|
|
DLog(@"CMSimpleQueueEnqueue err %d", err);
|
|
return;
|
|
}
|
|
|
|
// Inform the clients that the queue has been altered
|
|
if (self.alteredProc != NULL) {
|
|
(self.alteredProc)(self.objectId, sampleBuffer, self.alteredRefCon);
|
|
}
|
|
}
|
|
|
|
- (CMVideoFormatDescriptionRef)getFormatDescription
|
|
{
|
|
CMVideoFormatDescriptionRef formatDescription;
|
|
OSStatus err = CMVideoFormatDescriptionCreate(kCFAllocatorDefault, kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
|
|
(int32_t) self.testCardSize.width, (int32_t) self.testCardSize.height,
|
|
NULL, &formatDescription);
|
|
if (err != noErr) {
|
|
DLog(@"Error %d from CMVideoFormatDescriptionCreate", err);
|
|
}
|
|
return formatDescription;
|
|
}
|
|
|
|
#pragma mark - CMIOObject
|
|
|
|
- (UInt32)getPropertyDataSizeWithAddress:(CMIOObjectPropertyAddress)address
|
|
qualifierDataSize:(UInt32)qualifierDataSize
|
|
qualifierData:(nonnull const void *)qualifierData
|
|
{
|
|
switch (address.mSelector) {
|
|
case kCMIOStreamPropertyInitialPresentationTimeStampForLinkedAndSyncedAudio:
|
|
return sizeof(CMTime);
|
|
case kCMIOStreamPropertyOutputBuffersNeededForThrottledPlayback:
|
|
return sizeof(UInt32);
|
|
case kCMIOObjectPropertyName:
|
|
return sizeof(CFStringRef);
|
|
case kCMIOObjectPropertyManufacturer:
|
|
return sizeof(CFStringRef);
|
|
case kCMIOObjectPropertyElementName:
|
|
return sizeof(CFStringRef);
|
|
case kCMIOObjectPropertyElementCategoryName:
|
|
return sizeof(CFStringRef);
|
|
case kCMIOObjectPropertyElementNumberName:
|
|
return sizeof(CFStringRef);
|
|
case kCMIOStreamPropertyDirection:
|
|
return sizeof(UInt32);
|
|
case kCMIOStreamPropertyTerminalType:
|
|
return sizeof(UInt32);
|
|
case kCMIOStreamPropertyStartingChannel:
|
|
return sizeof(UInt32);
|
|
case kCMIOStreamPropertyLatency:
|
|
return sizeof(UInt32);
|
|
case kCMIOStreamPropertyFormatDescriptions:
|
|
return sizeof(CFArrayRef);
|
|
case kCMIOStreamPropertyFormatDescription:
|
|
return sizeof(CMFormatDescriptionRef);
|
|
case kCMIOStreamPropertyFrameRateRanges:
|
|
return sizeof(AudioValueRange);
|
|
case kCMIOStreamPropertyFrameRate:
|
|
case kCMIOStreamPropertyFrameRates:
|
|
return sizeof(Float64);
|
|
case kCMIOStreamPropertyMinimumFrameRate:
|
|
return sizeof(Float64);
|
|
case kCMIOStreamPropertyClock:
|
|
return sizeof(CFTypeRef);
|
|
default:
|
|
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<CFStringRef *>(data) = CFSTR("OBS Virtual Camera");
|
|
*dataUsed = sizeof(CFStringRef);
|
|
break;
|
|
case kCMIOObjectPropertyElementName:
|
|
*static_cast<CFStringRef *>(data) = CFSTR("OBS Virtual Camera Stream Element");
|
|
*dataUsed = sizeof(CFStringRef);
|
|
break;
|
|
case kCMIOObjectPropertyManufacturer:
|
|
case kCMIOObjectPropertyElementCategoryName:
|
|
case kCMIOObjectPropertyElementNumberName:
|
|
case kCMIOStreamPropertyTerminalType:
|
|
case kCMIOStreamPropertyStartingChannel:
|
|
case kCMIOStreamPropertyLatency:
|
|
case kCMIOStreamPropertyInitialPresentationTimeStampForLinkedAndSyncedAudio:
|
|
case kCMIOStreamPropertyOutputBuffersNeededForThrottledPlayback:
|
|
break;
|
|
case kCMIOStreamPropertyDirection:
|
|
*static_cast<UInt32 *>(data) = 1;
|
|
*dataUsed = sizeof(UInt32);
|
|
break;
|
|
case kCMIOStreamPropertyFormatDescriptions:
|
|
*static_cast<CFArrayRef *>(data) = (__bridge_retained CFArrayRef)
|
|
[NSArray arrayWithObject:(__bridge_transfer NSObject *) [self getFormatDescription]];
|
|
*dataUsed = sizeof(CFArrayRef);
|
|
break;
|
|
case kCMIOStreamPropertyFormatDescription:
|
|
*static_cast<CMVideoFormatDescriptionRef *>(data) = [self getFormatDescription];
|
|
*dataUsed = sizeof(CMVideoFormatDescriptionRef);
|
|
break;
|
|
case kCMIOStreamPropertyFrameRateRanges:
|
|
AudioValueRange range;
|
|
range.mMinimum = self.fps;
|
|
range.mMaximum = self.fps;
|
|
*static_cast<AudioValueRange *>(data) = range;
|
|
*dataUsed = sizeof(AudioValueRange);
|
|
break;
|
|
case kCMIOStreamPropertyFrameRate:
|
|
case kCMIOStreamPropertyFrameRates:
|
|
*static_cast<Float64 *>(data) = self.fps;
|
|
*dataUsed = sizeof(Float64);
|
|
break;
|
|
case kCMIOStreamPropertyMinimumFrameRate:
|
|
*static_cast<Float64 *>(data) = self.fps;
|
|
*dataUsed = sizeof(Float64);
|
|
break;
|
|
case kCMIOStreamPropertyClock:
|
|
*static_cast<CFTypeRef *>(data) = self.clock;
|
|
// This one was incredibly tricky and cost me many hours to find. It seems that DAL expects
|
|
// the clock to be retained when returned. It's unclear why, and that seems inconsistent
|
|
// with other properties that don't have the same behavior. But this is what Apple's sample
|
|
// code does.
|
|
// https://github.com/lvsti/CoreMediaIO-DAL-Example/blob/0392cb/Sources/Extras/CoreMediaIO/DeviceAbstractionLayer/Devices/DP/Properties/CMIO_DP_Property_Clock.cpp#L75
|
|
CFRetain(*static_cast<CFTypeRef *>(data));
|
|
*dataUsed = sizeof(CFTypeRef);
|
|
break;
|
|
default:
|
|
*dataUsed = 0;
|
|
};
|
|
}
|
|
|
|
- (BOOL)hasPropertyWithAddress:(CMIOObjectPropertyAddress)address
|
|
{
|
|
switch (address.mSelector) {
|
|
case kCMIOObjectPropertyName:
|
|
case kCMIOObjectPropertyElementName:
|
|
case kCMIOStreamPropertyFormatDescriptions:
|
|
case kCMIOStreamPropertyFormatDescription:
|
|
case kCMIOStreamPropertyFrameRateRanges:
|
|
case kCMIOStreamPropertyFrameRate:
|
|
case kCMIOStreamPropertyFrameRates:
|
|
case kCMIOStreamPropertyMinimumFrameRate:
|
|
case kCMIOStreamPropertyClock:
|
|
return true;
|
|
case kCMIOObjectPropertyManufacturer:
|
|
case kCMIOObjectPropertyElementCategoryName:
|
|
case kCMIOObjectPropertyElementNumberName:
|
|
case kCMIOStreamPropertyDirection:
|
|
case kCMIOStreamPropertyTerminalType:
|
|
case kCMIOStreamPropertyStartingChannel:
|
|
case kCMIOStreamPropertyLatency:
|
|
case kCMIOStreamPropertyInitialPresentationTimeStampForLinkedAndSyncedAudio:
|
|
case kCMIOStreamPropertyOutputBuffersNeededForThrottledPlayback:
|
|
DLog(@"TODO: %@", [OBSDALObjectStore StringFromPropertySelector:address.mSelector]);
|
|
return false;
|
|
default:
|
|
return false;
|
|
};
|
|
}
|
|
|
|
- (BOOL)isPropertySettableWithAddress:(CMIOObjectPropertyAddress)address
|
|
{
|
|
switch (address.mSelector) {
|
|
case kCMIOStreamPropertyFormatDescription:
|
|
case kCMIOStreamPropertyFrameRate:
|
|
// Suppress error logs complaining about the application not being able to set the desired format or frame rate.
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
- (void)setPropertyDataWithAddress:(CMIOObjectPropertyAddress)address
|
|
qualifierDataSize:(UInt32)qualifierDataSize
|
|
qualifierData:(nonnull const void *)qualifierData
|
|
dataSize:(UInt32)dataSize
|
|
data:(nonnull const void *)data
|
|
{}
|
|
|
|
@end
|