#if __APPLE__
#include "ccap_imp_apple.h"
#include "ccap_file_reader_apple.h"
#include "ccap_convert.h"
#include "ccap_convert_frame.h"
#import <AVFoundation/AVFoundation.h>
#import <Accelerate/Accelerate.h>
#import <Foundation/Foundation.h>
#include <cassert>
#include <cmath>
#if _CCAP_LOG_ENABLED_
#include <deque>
#endif
using namespace ccap;
#if _CCAP_LOG_ENABLED_
#define CCAP_NSLOG(logLevel, ...) CCAP_CALL_LOG(logLevel, NSLog(__VA_ARGS__))
#define CCAP_NSLOG_E(...) CCAP_NSLOG(LogLevel::Error, __VA_ARGS__)
#define CCAP_NSLOG_W(...) CCAP_NSLOG(LogLevel::Warning, __VA_ARGS__)
#define CCAP_NSLOG_I(...) CCAP_NSLOG(LogLevel::Info, __VA_ARGS__)
#define CCAP_NSLOG_V(...) CCAP_NSLOG(LogLevel::Verbose, __VA_ARGS__)
#else
#define CCAP_NSLOG_E(...) ((void)0)
#define CCAP_NSLOG_W(...) ((void)0)
#define CCAP_NSLOG_I(...) ((void)0)
#define CCAP_NSLOG_V(...) ((void)0)
#endif
#if defined(DEBUG) && _CCAP_LOG_ENABLED_
#include <sys/sysctl.h>
#include <sys/types.h>
#include <unistd.h>
namespace ccap {
extern bool globalLogLevelChanged;
}
[[maybe_unused]] static void optimizeLogIfNotSet() {
if (!globalLogLevelChanged) {
int mib[4];
struct kinfo_proc info{};
size_t size = sizeof(info);
mib[0] = CTL_KERN;
mib[1] = KERN_PROC;
mib[2] = KERN_PROC_PID;
mib[3] = getpid();
info.kp_proc.p_flag = 0;
sysctl(mib, 4, &info, &size, NULL, 0);
if ((info.kp_proc.p_flag & P_TRACED) !=
0) { setLogLevel(LogLevel::Verbose);
fputs("ccap: Debug mode detected, set log level to verbose.\n", stderr);
}
}
}
#else
#define optimizeLogIfNotSet() (void)0
#endif
namespace {
constexpr FrameOrientation kDefaultFrameOrientation = FrameOrientation::TopToBottom;
struct PixelFormatInfo {
NSString* name{ nil };
ccap::PixelFormat format{ ccap::PixelFormat::Unknown };
std::string description;
};
#define MakeFormatInfo(format) format, #format
PixelFormatInfo getPixelFormatInfo(OSType format) { constexpr const char* unavailableMsg = "ccap unavailable for now";
switch (format) {
case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
return { @"kCVPixelFormatType_420YpCbCr8BiPlanarFullRange", MakeFormatInfo(PixelFormat::NV12f) };
case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
return { @"kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange", MakeFormatInfo(PixelFormat::NV12) };
case kCVPixelFormatType_420YpCbCr8PlanarFullRange:
return { @"kCVPixelFormatType_420YpCbCr8PlanarFullRange", MakeFormatInfo(PixelFormat::I420f) };
case kCVPixelFormatType_420YpCbCr8Planar:
return { @"kCVPixelFormatType_420YpCbCr8Planar", MakeFormatInfo(PixelFormat::I420) };
case kCVPixelFormatType_422YpCbCr8:
return { @"kCVPixelFormatType_422YpCbCr8", PixelFormat::Unknown, unavailableMsg };
case kCVPixelFormatType_422YpCbCr8_yuvs:
return { @"kCVPixelFormatType_422YpCbCr8_yuvs", PixelFormat::Unknown, unavailableMsg };
case kCVPixelFormatType_32BGRA:
return { @"kCVPixelFormatType_32BGRA", MakeFormatInfo(PixelFormat::BGRA32) };
case kCVPixelFormatType_24BGR:
return { @"kCVPixelFormatType_24BGR", MakeFormatInfo(PixelFormat::BGR24) };
case kCVPixelFormatType_24RGB:
return { @"kCVPixelFormatType_24RGB", MakeFormatInfo(PixelFormat::RGB24) };
case kCVPixelFormatType_32RGBA:
return { @"kCVPixelFormatType_32RGBA", MakeFormatInfo(PixelFormat::RGBA32) };
case kCVPixelFormatType_32ARGB:
return { @"kCVPixelFormatType_32ARGB", PixelFormat::Unknown, unavailableMsg };
default:
return { [NSString stringWithFormat:@"Unknown(0x%08x)", (unsigned int)format], MakeFormatInfo(PixelFormat::Unknown) };
}
}
struct ResolutionInfo {
AVCaptureSessionPreset preset = nil;
DeviceInfo::Resolution resolution{};
};
std::vector<ResolutionInfo> allSupportedResolutions(AVCaptureSession* session) {
std::vector<ResolutionInfo> info = {
#if CCAP_MACOS
{ AVCaptureSessionPreset320x240, { 320, 240 } },
#endif
{ AVCaptureSessionPreset352x288, { 352, 288 } },
{ AVCaptureSessionPreset640x480, { 640, 480 } },
#if CCAP_MACOS
{ AVCaptureSessionPreset960x540, { 960, 540 } },
#endif
{ AVCaptureSessionPreset1280x720, { 1280, 720 } },
{ AVCaptureSessionPreset1920x1080, { 1920, 1080 } },
{ AVCaptureSessionPreset3840x2160, { 3840, 2160 } },
};
auto r = std::remove_if(info.begin(), info.end(), ^bool(const ResolutionInfo& i) { return ![session canSetSessionPreset:i.preset]; });
if (r != info.end()) {
info.erase(r, info.end());
}
return info;
}
}
NSArray<AVCaptureDevice*>* findAllDeviceName() {
NSMutableArray* allTypes = [NSMutableArray new];
[allTypes addObject:AVCaptureDeviceTypeBuiltInWideAngleCamera];
#if CCAP_IOS
[allTypes addObject:AVCaptureDeviceTypeBuiltInTelephotoCamera];
[allTypes addObject:AVCaptureDeviceTypeBuiltInDualCamera];
if (@available(iOS 13.0, *)) {
[allTypes addObject:AVCaptureDeviceTypeBuiltInUltraWideCamera];
[allTypes addObject:AVCaptureDeviceTypeBuiltInDualWideCamera];
[allTypes addObject:AVCaptureDeviceTypeBuiltInTripleCamera];
}
#else
if (@available(macOS 14.0, *)) {
[allTypes addObject:AVCaptureDeviceTypeExternal];
} else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[allTypes addObject:AVCaptureDeviceTypeExternalUnknown];
#pragma clang diagnostic pop
}
#endif
AVCaptureDeviceDiscoverySession* discoverySession =
[AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:allTypes mediaType:AVMediaTypeVideo
position:AVCaptureDevicePositionUnspecified];
if (infoLogEnabled()) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{ CCAP_NSLOG_I(@"ccap: Available camera devices: %@", discoverySession.devices); });
}
std::vector<std::string_view> virtualDevicePatterns = {
"obs",
"virtual",
};
auto countIndex = [&](AVCaptureDevice* device) {
NSUInteger idx = [allTypes indexOfObject:device.deviceType] * 100 + 1000;
if (idx == NSNotFound) return allTypes.count * 100;
std::string name = [[device.localizedName lowercaseString] UTF8String];
for (auto& pattern : virtualDevicePatterns) {
if (name.find(pattern) != std::string::npos) {
idx += 10;
break;
}
}
if (int c = (int)device.formats.count; c > 1) {
idx -= device.formats.count;
}
return idx;
};
return [discoverySession.devices sortedArrayUsingComparator:^NSComparisonResult(AVCaptureDevice* d1, AVCaptureDevice* d2) {
NSUInteger idx1 = countIndex(d1);
NSUInteger idx2 = countIndex(d2);
if (idx1 < idx2) return NSOrderedAscending;
if (idx1 > idx2) return NSOrderedDescending;
return NSOrderedSame;
}];
}
@interface CameraCaptureObjc : NSObject<AVCaptureVideoDataOutputSampleBufferDelegate> {
ProviderApple* _provider;
std::vector<uint8_t> _memoryCache; }
@property (nonatomic, strong) AVCaptureSession* session;
@property (nonatomic, strong) AVCaptureDevice* device;
@property (nonatomic, strong) NSString* cameraName;
@property (nonatomic, strong) AVCaptureDeviceInput* videoInput;
@property (nonatomic, strong) AVCaptureVideoDataOutput* videoOutput;
@property (nonatomic, strong) dispatch_queue_t captureQueue;
@property (nonatomic) CGSize resolution;
@property (nonatomic) OSType cvPixelFormat;
@property (nonatomic) BOOL opened;
- (instancetype)initWithProvider:(ProviderApple*)provider;
- (BOOL)start;
- (void)stop;
@end
@implementation CameraCaptureObjc
- (instancetype)initWithProvider:(ProviderApple*)provider {
self = [super init];
if (self) {
_provider = provider;
_opened = NO;
}
return self;
}
- (BOOL)open {
AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
if (authStatus == AVAuthorizationStatusNotDetermined) {
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
void (^requestAccess)(void) = ^(void) {
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
CCAP_NSLOG_I(@"ccap: Camera access %@", granted ? @"granted" : @"denied");
dispatch_semaphore_signal(sema);
}];
};
if (![NSThread isMainThread]) {
dispatch_async(dispatch_get_main_queue(), ^{ requestAccess(); });
} else {
requestAccess();
}
CCAP_NSLOG_I(@"ccap: Waiting for camera access permission...");
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
}
if (authStatus != AVAuthorizationStatusAuthorized) {
reportError(ErrorCode::DeviceOpenFailed, "Camera access not authorized");
return NO;
}
_session = [[AVCaptureSession alloc] init];
[_session beginConfiguration];
if (AVCaptureSessionPreset preset = AVCaptureSessionPresetHigh; _resolution.width > 0 && _resolution.height > 0) {
auto allInfo = allSupportedResolutions(_session);
CGSize inputSize = _resolution;
for (auto& info : allInfo) {
auto width = info.resolution.width;
auto height = info.resolution.height;
if (width >= _resolution.width && height >= _resolution.height) {
_resolution.width = width;
_resolution.height = height;
preset = info.preset;
break;
}
}
if (![_session canSetSessionPreset:preset]) {
CCAP_NSLOG_W(@"ccap: CameraCaptureObjc init - session preset not supported, using AVCaptureSessionPresetHigh");
preset = AVCaptureSessionPresetHigh;
}
[_session setSessionPreset:preset];
CCAP_NSLOG_I(@"ccap: Expected camera resolution: (%gx%g), actual matched camera resolution: (%gx%g)", inputSize.width,
inputSize.height, _resolution.width, _resolution.height);
}
if (_cameraName != nil && _cameraName.length > 0) { NSArray<AVCaptureDevice*>* devices = findAllDeviceName();
for (AVCaptureDevice* d in devices) {
if ([d.localizedName caseInsensitiveCompare:_cameraName] == NSOrderedSame) {
_device = d;
break;
}
}
}
if (!_device) { _device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
}
if (![_device hasMediaType:AVMediaTypeVideo]) { reportError(ErrorCode::NoDeviceFound, "No video device found");
return NO;
}
CCAP_NSLOG_I(@"ccap: The camera to be used: %@", _device);
NSError* error = nil;
if ([_device lockForConfiguration:&error]) {
if ([_device isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) {
[_device setFocusMode:AVCaptureFocusModeContinuousAutoFocus];
CCAP_NSLOG_V(@"ccap: Set focus mode to continuous auto focus");
}
[_device unlockForConfiguration];
} else {
CCAP_NSLOG_W(@"ccap: lockForConfiguration failed: %@", error.localizedDescription);
}
_videoInput = [AVCaptureDeviceInput deviceInputWithDevice:_device error:&error];
if (!_videoInput || error) {
reportError(ErrorCode::DeviceOpenFailed, "Open camera failed" + (error && error.localizedDescription ? (": " + std::string([error.localizedDescription UTF8String])) : ""));
return NO;
}
if ([_session canAddInput:_videoInput]) {
[_session addInput:_videoInput];
CCAP_NSLOG_V(@"ccap: Add input to session");
} else {
reportError(ErrorCode::DeviceOpenFailed, "Session cannot add input device");
return NO;
}
_videoOutput = [[AVCaptureVideoDataOutput alloc] init];
[_videoOutput setAlwaysDiscardsLateVideoFrames:YES];
auto& requiredPixelFormat = _provider->getFrameProperty().outputPixelFormat;
if (requiredPixelFormat == PixelFormat::Unknown) { requiredPixelFormat = PixelFormat::BGRA32;
}
auto& cameraPixelFormat = _provider->getFrameProperty().cameraPixelFormat;
if (cameraPixelFormat == PixelFormat::Unknown) {
cameraPixelFormat = requiredPixelFormat;
}
{ static_assert(sizeof(cameraPixelFormat) == sizeof(uint32_t), "size must match");
[self fixPixelFormat];
NSArray* supportedFormats = [_videoOutput availableVideoCVPixelFormatTypes];
if (infoLogEnabled()) {
NSMutableArray* arr = [NSMutableArray new];
for (NSNumber* format in supportedFormats) {
auto info = getPixelFormatInfo([format unsignedIntValue]);
[arr addObject:[NSString stringWithFormat:@"%@ (%s)", info.name, info.description.c_str()]];
}
CCAP_NSLOG_I(@"ccap: Supported pixel format: %@", arr);
}
OSType preferredFormat = _cvPixelFormat;
if (![supportedFormats containsObject:@(preferredFormat)]) {
_cvPixelFormat = 0;
if (bool hasYUV = cameraPixelFormat & kPixelFormatYUVColorBit) { auto hasFullRange = cameraPixelFormat & kPixelFormatYUVColorFullRangeBit;
auto supportFullRange = [supportedFormats containsObject:@(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)];
auto supportVideoRange = [supportedFormats containsObject:@(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange)];
if (supportFullRange || supportVideoRange) {
if (!hasFullRange && supportVideoRange) {
cameraPixelFormat = PixelFormat::NV12;
_cvPixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
} else {
cameraPixelFormat = PixelFormat::NV12f;
_cvPixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange;
}
}
}
if (_cvPixelFormat == 0) {
auto hasOnlyRGB = pixelFormatInclude(cameraPixelFormat, kPixelFormatRGBColorBit);
auto supportRGB = [supportedFormats containsObject:@(kCVPixelFormatType_24RGB)];
auto supportBGR = [supportedFormats containsObject:@(kCVPixelFormatType_24BGR)];
auto supportBGRA = [supportedFormats containsObject:@(kCVPixelFormatType_32BGRA)];
if (hasOnlyRGB && (supportRGB || supportBGR)) {
if (supportBGR) {
cameraPixelFormat = PixelFormat::BGR24;
_cvPixelFormat = kCVPixelFormatType_24BGR;
} else {
cameraPixelFormat = PixelFormat::RGB24;
_cvPixelFormat = kCVPixelFormatType_24RGB;
}
} else {
if (supportBGRA) {
cameraPixelFormat = PixelFormat::BGRA32;
_cvPixelFormat = kCVPixelFormatType_32BGRA;
} else {
cameraPixelFormat = PixelFormat::RGBA32;
_cvPixelFormat = kCVPixelFormatType_32RGBA;
}
}
}
if (_cvPixelFormat == 0) { _cvPixelFormat = kCVPixelFormatType_32BGRA;
cameraPixelFormat = PixelFormat::BGRA32;
}
if ((cameraPixelFormat & kPixelFormatRGBColorBit) &&
(requiredPixelFormat & kPixelFormatYUVColorBit)) { requiredPixelFormat = cameraPixelFormat;
}
if (ccap::errorLogEnabled()) {
if (cameraPixelFormat != requiredPixelFormat) {
if (!(cameraPixelFormat & kPixelFormatRGBColorBit)) { CCAP_NSLOG_E(@"ccap: CameraCaptureObjc init - convert pixel format not supported!!!");
}
}
auto preferredInfo = getPixelFormatInfo(preferredFormat);
auto fallbackInfo = getPixelFormatInfo(_cvPixelFormat);
CCAP_NSLOG_E(@"ccap: Preferred pixel format (%@-%s) not supported, fallback to: (%@-%s)", preferredInfo.name,
preferredInfo.description.c_str(), fallbackInfo.name, fallbackInfo.description.c_str());
reportError(ErrorCode::UnsupportedPixelFormat, "Preferred pixel format not supported, fallback to: " + fallbackInfo.description);
}
}
_videoOutput.videoSettings = @{(id)kCVPixelBufferPixelFormatTypeKey : @(_cvPixelFormat)};
}
_captureQueue = dispatch_queue_create("ccap.queue", DISPATCH_QUEUE_SERIAL);
[_videoOutput setSampleBufferDelegate:self queue:_captureQueue];
if ([_session canAddOutput:_videoOutput]) {
[_session addOutput:_videoOutput];
CCAP_NSLOG_V(@"ccap: Add output to session");
} else {
reportError(ErrorCode::DeviceOpenFailed, "Session cannot add output device");
return NO;
}
[_session commitConfiguration];
if (infoLogEnabled() && _device) {
NSMutableString* formatInfo = [NSMutableString stringWithString:@"ccap: Available formats and frame rates:\n"];
for (AVCaptureDeviceFormat* format in _device.formats) {
CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(format.formatDescription);
[formatInfo appendFormat:@" %dx%d: ", dimensions.width, dimensions.height];
for (AVFrameRateRange* range in format.videoSupportedFrameRateRanges) {
[formatInfo appendFormat:@"%.1f-%.1f fps, ", range.minFrameRate, range.maxFrameRate];
}
[formatInfo appendString:@"\n"];
}
CCAP_NSLOG_I(@"%@", formatInfo);
}
if (auto fps = _provider->getFrameProperty().fps; fps > 0.0) {
[self setFrameRate:_provider->getFrameProperty().fps];
}
CCAP_NSLOG_V(@"ccap: videoOutput.connections,count = %lu", (unsigned long)_videoOutput.connections.count);
[self flushResolution];
_opened = YES;
return _opened;
}
- (void)setFrameRate:(double)fps {
if (_device) {
NSError* error;
[_device lockForConfiguration:&error];
if ([_device respondsToSelector:@selector(setActiveVideoMinFrameDuration:)] &&
[_device respondsToSelector:@selector(setActiveVideoMaxFrameDuration:)]) {
if (error == nil) {
double desiredFps = fps;
AVCaptureDeviceFormat* bestFormat = nil;
AVFrameRateRange* bestRange = nil;
double bestFps = desiredFps;
double bestDistance = 1e9;
CGSize currentResolution = _resolution;
if (currentResolution.width <= 0 || currentResolution.height <= 0) {
if (_device.activeFormat) {
CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(_device.activeFormat.formatDescription);
currentResolution = CGSizeMake(dimensions.width, dimensions.height);
}
}
for (AVCaptureDeviceFormat* format in _device.formats) {
CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(format.formatDescription);
if (currentResolution.width > 0 && currentResolution.height > 0) {
double widthRatio = (double)dimensions.width / currentResolution.width;
double heightRatio = (double)dimensions.height / currentResolution.height;
if (widthRatio < 0.8 || widthRatio > 1.25 || heightRatio < 0.8 || heightRatio > 1.25) {
continue;
}
}
for (AVFrameRateRange* range in format.videoSupportedFrameRateRanges) {
double actualFps;
if (desiredFps >= range.minFrameRate && desiredFps <= range.maxFrameRate) {
actualFps = desiredFps;
} else {
if (desiredFps < range.minFrameRate) {
actualFps = range.minFrameRate;
} else {
actualFps = range.maxFrameRate;
}
}
double distance = std::abs(actualFps - desiredFps);
if (distance < bestDistance) {
bestFormat = format;
bestRange = range;
bestFps = actualFps;
bestDistance = distance;
}
}
}
if (bestFormat && bestFormat != _device.activeFormat) {
_device.activeFormat = bestFormat;
CCAP_NSLOG_I(@"ccap: Switched to format that supports %g fps", bestFps);
CMVideoDimensions newDimensions = CMVideoFormatDescriptionGetDimensions(bestFormat.formatDescription);
_resolution = CGSizeMake(newDimensions.width, newDimensions.height);
if (_provider) {
auto& prop = _provider->getFrameProperty();
prop.width = newDimensions.width;
prop.height = newDimensions.height;
}
}
fps = bestFps;
if (bestRange) {
CMTime frameDuration;
const double fpsEps = 0.01; auto approx = [&](double t) { return std::abs(fps - t) < fpsEps; };
if (approx(239.76)) {
frameDuration = CMTimeMake(1001, 240000);
} else if (approx(240.0)) {
frameDuration = CMTimeMake(1, 240);
} else if (approx(119.88)) {
frameDuration = CMTimeMake(1001, 120000);
} else if (approx(120.0)) {
frameDuration = CMTimeMake(1, 120);
} else if (approx(59.94)) {
frameDuration = CMTimeMake(1001, 60000);
} else if (approx(60.0)) {
frameDuration = CMTimeMake(1, 60);
} else if (approx(29.97)) {
frameDuration = CMTimeMake(1001, 30000);
} else if (approx(30.0)) {
frameDuration = CMTimeMake(1, 30);
} else if (approx(23.976)) {
frameDuration = CMTimeMake(1001, 24000);
} else if (approx(24.0)) {
frameDuration = CMTimeMake(1, 24);
} else {
frameDuration = CMTimeMakeWithSeconds(1.0 / fps, 60000);
}
if (CMTimeCompare(frameDuration, bestRange.minFrameDuration) < 0) {
frameDuration = bestRange.minFrameDuration;
} else if (CMTimeCompare(frameDuration, bestRange.maxFrameDuration) > 0) {
frameDuration = bestRange.maxFrameDuration;
}
[_device setActiveVideoMinFrameDuration:frameDuration];
[_device setActiveVideoMaxFrameDuration:frameDuration];
_provider->getFrameProperty().fps = fps;
if (infoLogEnabled()) {
if (std::abs(fps - desiredFps) > 0.01) {
CCAP_NSLOG_I(@"ccap: Set fps to %g, but actual fps is %g", desiredFps, fps);
} else {
CCAP_NSLOG_I(@"ccap: Set fps to %g", fps);
}
}
} else {
reportError(ErrorCode::FrameRateSetFailed, "Desired fps (" + std::to_string(fps) + ") not supported, using fallback");
}
}
} else {
for (AVCaptureConnection* connection in _videoOutput.connections) {
for (AVCaptureInputPort* port in connection.inputPorts) {
if ([port.mediaType isEqualToString:AVMediaTypeVideo]) {
auto tm = CMTimeMakeWithSeconds(1.0 / fps, 60000);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
connection.videoMinFrameDuration = tm;
connection.videoMaxFrameDuration = tm;
#pragma clang diagnostic pop
}
}
}
}
[_device unlockForConfiguration];
}
}
- (void)fixPixelFormat {
auto& internalFormat = _provider->getFrameProperty().cameraPixelFormat;
switch (internalFormat) {
case PixelFormat::I420:
reportError(ErrorCode::UnsupportedPixelFormat, "I420 is not supported on macOS, fallback to NV12");
case PixelFormat::NV12:
_cvPixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
internalFormat = PixelFormat::NV12;
break;
case PixelFormat::I420f:
reportError(ErrorCode::UnsupportedPixelFormat, "I420f is not supported on macOS, fallback to NV12f");
case PixelFormat::NV12f:
_cvPixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange;
internalFormat = PixelFormat::NV12f;
break;
case PixelFormat::BGRA32:
_cvPixelFormat = kCVPixelFormatType_32BGRA;
break;
case PixelFormat::RGBA32:
_cvPixelFormat = kCVPixelFormatType_32RGBA;
break;
case PixelFormat::BGR24:
_cvPixelFormat = kCVPixelFormatType_24BGR;
break;
default: case PixelFormat::RGB24:
_cvPixelFormat = kCVPixelFormatType_24RGB;
internalFormat = PixelFormat::RGB24;
break;
}
}
- (void)flushResolution {
if (@available(iOS 7.0, *)) {
if ([_device.activeFormat respondsToSelector:@selector(formatDescription)]) {
_resolution = CGSizeMake(CMVideoFormatDescriptionGetDimensions(_device.activeFormat.formatDescription).width,
CMVideoFormatDescriptionGetDimensions(_device.activeFormat.formatDescription).height);
return;
}
}
if (_videoOutput && _videoOutput.connections.count > 0) {
AVCaptureConnection* connection = [_videoOutput connections][0];
if (connection) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
CMFormatDescriptionRef formatDescription =
connection.supportsVideoMinFrameDuration ? connection.inputPorts[0].formatDescription : nil;
#pragma clang diagnostic pop
if (formatDescription) {
CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(formatDescription);
if (dimensions.width != _resolution.width || dimensions.height != _resolution.height) {
CCAP_NSLOG_I(@"ccap: Actual camera resolution: %dx%d", dimensions.width, dimensions.height);
_resolution.width = dimensions.width;
_resolution.height = dimensions.height;
if (_provider) {
auto& prop = _provider->getFrameProperty();
prop.width = dimensions.width;
prop.height = dimensions.height;
}
}
}
return;
}
}
reportError(ErrorCode::InitializationFailed, "No connections available");
}
- (BOOL)start {
if (_session && _opened && ![_session isRunning]) {
CGSize targetResolution = _resolution;
CCAP_NSLOG_V(@"ccap: CameraCaptureObjc start");
[_session startRunning];
if (_device && _device.activeFormat && targetResolution.width > 0 && targetResolution.height > 0) {
CMVideoDimensions afterStart = CMVideoFormatDescriptionGetDimensions(_device.activeFormat.formatDescription);
if (afterStart.width != targetResolution.width || afterStart.height != targetResolution.height) {
CCAP_NSLOG_V(@"ccap: Session start changed format from %gx%g, restoring", targetResolution.width, targetResolution.height);
[self setCameraResolution:targetResolution];
}
}
}
return [_session isRunning];
}
- (void)setCameraResolution:(CGSize)targetResolution {
if (!_device) return;
NSError* error = nil;
if ([_device lockForConfiguration:&error]) {
AVCaptureDeviceFormat* bestFormat = nil;
double closestDistance = 1e9;
for (AVCaptureDeviceFormat* format in _device.formats) {
CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(format.formatDescription);
if (dimensions.width == targetResolution.width && dimensions.height == targetResolution.height) {
bestFormat = format;
break;
}
double distance = std::abs(dimensions.width - targetResolution.width) + std::abs(dimensions.height - targetResolution.height);
if (distance < closestDistance) {
closestDistance = distance;
bestFormat = format;
}
}
if (bestFormat) {
[_device setActiveFormat:bestFormat];
CMVideoDimensions actualDimensions = CMVideoFormatDescriptionGetDimensions(bestFormat.formatDescription);
CCAP_NSLOG_V(@"ccap: Restored device format to: %dx%d", actualDimensions.width, actualDimensions.height);
_resolution = CGSizeMake(actualDimensions.width, actualDimensions.height);
}
[_device unlockForConfiguration];
} else {
CCAP_NSLOG_W(@"ccap: Failed to lock device for format restoration: %@", error.localizedDescription);
}
}
- (void)stop {
if (_session && [_session isRunning]) {
CCAP_NSLOG_V(@"ccap: CameraCaptureObjc stop");
[_session stopRunning];
}
if (_provider) {
_provider->notifyGrabWaiters();
}
}
- (BOOL)isRunning {
return [_session isRunning];
}
- (void)destroy {
@autoreleasepool {
if (_session) {
CCAP_NSLOG_V(@"ccap: CameraCaptureObjc destroy");
if ([_session isRunning]) {
[_session stopRunning];
}
[_videoOutput setSampleBufferDelegate:nil queue:dispatch_get_main_queue()];
[_session beginConfiguration];
if (_videoInput) {
[_session removeInput:_videoInput];
[_session removeOutput:_videoOutput];
_videoInput = nil;
_videoOutput = nil;
}
[_session commitConfiguration];
_session = nil;
}
_opened = NO;
}
}
- (void)dealloc {
[self destroy];
}
- (void)captureOutput:(AVCaptureOutput*)output
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection*)connection {
if (!_provider) {
reportError(ErrorCode::InitializationFailed, "CameraCaptureObjc captureOutput - provider is nil");
return;
}
if (_provider->tooManyNewFrames()) {
if (!_provider->hasNewFrameCallback()) {
CCAP_NSLOG_I(@"ccap: VideoFrame dropped to avoid memory leak: grab() called less frequently than camera frame rate.");
return;
} else {
CCAP_NSLOG_I(
@"ccap: new frame callback returned false, but grab() was not called or is called less frequently than the camera frame rate");
}
}
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
auto newFrame = _provider->getFreeFrame();
CMTime timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
auto internalFormat = _provider->getFrameProperty().cameraPixelFormat;
auto outputFormat = _provider->getFrameProperty().outputPixelFormat;
newFrame->timestamp = (uint64_t)(CMTimeGetSeconds(timestamp) * 1e9);
newFrame->width = (uint32_t)CVPixelBufferGetWidth(imageBuffer);
newFrame->height = (uint32_t)CVPixelBufferGetHeight(imageBuffer);
newFrame->pixelFormat = internalFormat;
newFrame->nativeHandle = imageBuffer;
newFrame->sizeInBytes = (uint32_t)CVPixelBufferGetDataSize(imageBuffer);
assert(!((internalFormat & kPixelFormatRGBColorBit) && (outputFormat & kPixelFormatYUVColorBit)));
if ((internalFormat & kPixelFormatYUVColorBit)) {
uint32_t yBytesPerRow = (uint32_t)CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0);
uint32_t uvBytesPerRow = (uint32_t)CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 1);
newFrame->data[0] = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0);
newFrame->data[1] = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 1);
newFrame->data[2] = nullptr;
newFrame->stride[0] = yBytesPerRow;
newFrame->stride[1] = uvBytesPerRow;
newFrame->stride[2] = 0;
} else {
newFrame->data[0] = (uint8_t*)CVPixelBufferGetBaseAddress(imageBuffer);
newFrame->data[1] = nullptr;
newFrame->data[2] = nullptr;
newFrame->stride[0] = (uint32_t)CVPixelBufferGetBytesPerRow(imageBuffer);
newFrame->stride[1] = 0;
newFrame->stride[2] = 0;
}
bool zeroCopy = ((internalFormat & kPixelFormatYUVColorBit) && (outputFormat & kPixelFormatYUVColorBit)) ||
(internalFormat == outputFormat && _provider->frameOrientation() == kDefaultFrameOrientation);
if (!zeroCopy) {
newFrame->orientation = _provider->frameOrientation();
if (!newFrame->allocator) {
auto&& f = _provider->getAllocatorFactory();
newFrame->allocator = f ? f() : std::make_shared<DefaultAllocator>();
}
std::chrono::steady_clock::time_point startConvertTime;
if (verboseLogEnabled()) {
startConvertTime = std::chrono::steady_clock::now();
}
zeroCopy = !inplaceConvertFrame(newFrame.get(), outputFormat, (int)(newFrame->orientation != kDefaultFrameOrientation));
CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
if (verboseLogEnabled()) {
#ifdef DEBUG
constexpr const char* mode = "(Debug)";
#else
constexpr const char* mode = "(Release)";
#endif
double durInMs = (std::chrono::steady_clock::now() - startConvertTime).count() / 1.e6;
static double s_allCostTime = 0;
static double s_frames = 0;
if (s_frames > 60) {
s_allCostTime = 0;
s_frames = 0;
}
s_allCostTime += durInMs;
++s_frames;
CCAP_NSLOG_V(
@"ccap: inplaceConvertFrame requested pixel format: %s, actual pixel format: %s, flip: %d, cost time %s: (cur %g ms, avg %g ms)",
pixelFormatToString(_provider->getFrameProperty().outputPixelFormat).data(),
pixelFormatToString(_provider->getFrameProperty().cameraPixelFormat).data(),
(int)(newFrame->orientation != kDefaultFrameOrientation), mode, durInMs, s_allCostTime / s_frames);
}
}
if (zeroCopy) {
newFrame->orientation = kDefaultFrameOrientation;
CFRetain(imageBuffer);
auto manager = std::make_shared<FakeFrame>([imageBuffer, newFrame]() mutable {
CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
CFRelease(imageBuffer);
CCAP_NSLOG_V(@"ccap: recycled frame, width: %d, height: %d", (int)newFrame->width, (int)newFrame->height);
newFrame->nativeHandle = nullptr;
newFrame = nullptr;
});
auto fakeFrame = std::shared_ptr<VideoFrame>(manager, newFrame.get());
newFrame = fakeFrame;
}
newFrame->frameIndex = _provider->frameIndex()++;
if (verboseLogEnabled()) { static uint64_t s_lastFrameTime;
static std::deque<uint64_t> s_durations;
if (s_lastFrameTime != 0) {
auto dur = newFrame->timestamp - s_lastFrameTime;
s_durations.emplace_back(dur);
}
s_lastFrameTime = newFrame->timestamp;
if (s_durations.size() > 30) {
s_durations.pop_front();
}
double fps = 0.0;
if (!s_durations.empty()) {
double sum = 0.0;
for (auto& d : s_durations) {
sum += d / 1e9f;
}
fps = std::round(s_durations.size() / sum * 10) / 10.0;
}
CCAP_NSLOG_V(@"ccap: New frame available: %ux%u, bytes %u, Data address: %p, fps: %g", newFrame->width, newFrame->height,
newFrame->sizeInBytes, newFrame->data[0], fps);
}
_provider->newFrameAvailable(std::move(newFrame));
}
@end
namespace ccap {
ProviderApple::ProviderApple() {
#ifdef CCAP_MACOS
optimizeLogIfNotSet();
#endif
m_frameOrientation = kDefaultFrameOrientation;
}
ProviderApple::~ProviderApple() { ProviderApple::close(); }
std::vector<std::string> ProviderApple::findDeviceNames() {
@autoreleasepool {
NSArray<AVCaptureDevice*>* devices = findAllDeviceName();
std::vector<std::string> names;
if (devices.count != 0) {
names.reserve(devices.count);
for (AVCaptureDevice* d in devices) {
names.emplace_back([d.localizedName UTF8String]);
}
}
return names;
}
}
bool ProviderApple::open(std::string_view deviceNameOrFilePath) {
#ifdef CCAP_ENABLE_FILE_PLAYBACK
if (looksLikeFilePath(deviceNameOrFilePath)) {
return openFile(deviceNameOrFilePath);
}
#endif
return openCamera(deviceNameOrFilePath);
}
bool ProviderApple::openCamera(std::string_view deviceName) {
if (m_imp != nil || m_fileReader) {
reportError(ErrorCode::DeviceOpenFailed, "Camera or file is already opened");
return false;
}
m_isFileMode = false;
@autoreleasepool {
m_imp = [[CameraCaptureObjc alloc] initWithProvider:this];
if (!deviceName.empty()) {
[m_imp setCameraName:@(deviceName.data())];
}
[m_imp setResolution:CGSizeMake(m_frameProp.width, m_frameProp.height)];
return [m_imp open];
}
}
bool ProviderApple::openFile(std::string_view filePath) {
if (m_imp != nil || m_fileReader) {
reportError(ErrorCode::DeviceOpenFailed, "Camera or file is already opened");
return false;
}
m_isFileMode = true;
m_fileReader = std::make_unique<FileReaderApple>(this);
if (!m_fileReader->open(filePath)) {
m_fileReader.reset();
m_isFileMode = false;
return false;
}
return true;
}
bool ProviderApple::isOpened() const {
if (m_isFileMode) {
return m_fileReader && m_fileReader->isOpened();
}
return m_imp && m_imp.session && m_imp.opened;
}
std::optional<DeviceInfo> ProviderApple::getDeviceInfo() const {
std::optional<DeviceInfo> deviceInfo;
if (m_isFileMode && m_fileReader) {
deviceInfo.emplace();
deviceInfo->deviceName = "Video File";
deviceInfo->supportedPixelFormats.push_back(PixelFormat::BGRA32);
deviceInfo->supportedPixelFormats.push_back(PixelFormat::NV12f);
deviceInfo->supportedResolutions.push_back({(uint32_t)m_fileReader->getWidth(), (uint32_t)m_fileReader->getHeight()});
return deviceInfo;
}
if (m_imp && m_imp.videoOutput) {
@autoreleasepool {
NSString* deviceName = [m_imp.device localizedName];
if ([deviceName length] > 0) {
deviceInfo.emplace();
deviceInfo->deviceName = [deviceName UTF8String];
NSArray* supportedFormats = [m_imp.videoOutput availableVideoCVPixelFormatTypes];
auto& formats = deviceInfo->supportedPixelFormats;
formats.reserve(supportedFormats.count);
for (NSNumber* format in supportedFormats) {
auto info = getPixelFormatInfo((OSType)[format unsignedIntValue]);
if (info.format != PixelFormat::Unknown) {
formats.emplace_back(info.format);
} else {
CCAP_NSLOG_V(@"ccap: The OS native pixel format %@ currently not implemented", info.name);
}
}
auto allResolutions = allSupportedResolutions(m_imp.session);
for (auto& info : allResolutions) {
deviceInfo->supportedResolutions.emplace_back(info.resolution);
}
}
}
}
if (!deviceInfo) {
reportError(ErrorCode::InitializationFailed, "getDeviceInfo called with no device opened");
}
return deviceInfo;
}
void ProviderApple::close() {
if (m_fileReader) {
m_fileReader->close();
m_fileReader.reset();
m_isFileMode = false;
}
if (m_imp) {
[m_imp destroy];
m_imp = nil;
}
{
std::lock_guard<std::mutex> lock(m_availableFrameMutex);
m_availableFrames = {};
}
ccap::resetSharedAllocator();
}
bool ProviderApple::start() {
if (!isOpened()) {
CCAP_NSLOG_W(@"ccap: start called with no device/file opened");
reportError(ErrorCode::DeviceStartFailed, "Start called with no device/file opened");
return false;
}
if (m_isFileMode && m_fileReader) {
return m_fileReader->start();
}
@autoreleasepool {
return [m_imp start];
}
}
void ProviderApple::stop() {
if (m_isFileMode && m_fileReader) {
m_fileReader->stop();
return;
}
if (m_imp) {
@autoreleasepool {
[m_imp stop];
}
}
}
bool ProviderApple::isStarted() const {
if (m_isFileMode && m_fileReader) {
return m_fileReader->isStarted();
}
return m_imp && [m_imp isRunning];
}
bool ProviderApple::setFileProperty(PropertyName prop, double value) {
if (!m_isFileMode || !m_fileReader) {
return false;
}
switch (prop) {
case PropertyName::CurrentTime:
return m_fileReader->seekToTime(value);
case PropertyName::PlaybackSpeed:
return m_fileReader->setPlaybackSpeed(value);
case PropertyName::CurrentFrameIndex:
return m_fileReader->seekToFrame(static_cast<int64_t>(value));
default:
return false;
}
}
double ProviderApple::getFileProperty(PropertyName prop) const {
if (!m_isFileMode || !m_fileReader) {
return NAN;
}
switch (prop) {
case PropertyName::Duration:
return m_fileReader->getDuration();
case PropertyName::CurrentTime:
return m_fileReader->getCurrentTime();
case PropertyName::PlaybackSpeed:
return m_fileReader->getPlaybackSpeed();
case PropertyName::FrameCount:
return m_fileReader->getFrameCount();
case PropertyName::CurrentFrameIndex:
return m_fileReader->getCurrentFrameIndex();
default:
return NAN;
}
}
ProviderImp* createProviderApple() { return new ProviderApple(); }
}
#endif