[cscore] Add support for USB cameras on macOS (#4846)

The main restriction is there must be an event loop running on the main thread.
No special action is required for GUI applications, but for non-GUI applications, a
RunOsxRunLoop() function is provided that needs to be called from the main thread.
This commit is contained in:
Thad House
2022-12-25 07:36:00 -08:00
committed by GitHub
parent 1696a490fa
commit b893b3d6d3
26 changed files with 1379 additions and 96 deletions

View File

@@ -1,5 +1,4 @@
---
Language: Cpp
BasedOnStyle: Google
AccessModifierOffset: -1
AlignAfterOpenBracket: Align

View File

@@ -46,7 +46,7 @@ jobs:
build-host:
env:
MACOSX_DEPLOYMENT_TARGET: 10.14
MACOSX_DEPLOYMENT_TARGET: 10.15
strategy:
fail-fast: false
matrix:

View File

@@ -9,5 +9,5 @@ repositories {
}
}
dependencies {
implementation "edu.wpi.first:native-utils:2023.10.0"
implementation "edu.wpi.first:native-utils:2023.11.0"
}

View File

@@ -13,6 +13,10 @@ cppSrcFileInclude {
\.cpp$
}
modifiableFileExclude {
objcpp
}
licenseUpdateExclude {
src/main/native/cpp/default_init_allocator\.h$
}

View File

@@ -11,6 +11,7 @@ file(GLOB
cscore_native_src src/main/native/cpp/*.cpp)
file(GLOB cscore_linux_src src/main/native/linux/*.cpp)
file(GLOB cscore_osx_src src/main/native/osx/*.cpp)
file(GLOB cscore_osx_objc_src src/main/native/objcpp/*.mm)
file(GLOB cscore_windows_src src/main/native/windows/*.cpp)
add_library(cscore ${cscore_native_src})
@@ -18,7 +19,9 @@ set_target_properties(cscore PROPERTIES DEBUG_POSTFIX "d")
if(NOT MSVC)
if (APPLE)
target_sources(cscore PRIVATE ${cscore_osx_src})
target_sources(cscore PRIVATE ${cscore_osx_src} ${cscore_osx_objc_src})
target_compile_options(cscore PRIVATE "-fobjc-arc")
set_target_properties(cscore PROPERTIES LINK_FLAGS "-framework CoreFoundation -framework AVFoundation -framework Foundation -framework CoreMedia -framework CoreVideo")
else()
target_sources(cscore PRIVATE ${cscore_linux_src})
endif()

View File

@@ -2,16 +2,16 @@ import org.gradle.internal.os.OperatingSystem
ext {
nativeName = 'cscore'
devMain = 'edu.wpi.cscore.DevMain'
devMain = 'edu.wpi.first.cscore.DevMain'
}
// Removed because having the objective-cpp plugin added breaks
// embedded tools and its toolchain check. It causes an obj-cpp
// source set to be added to all binaries, even cross binaries
// with no support.
// if (OperatingSystem.current().isMacOsX()) {
// apply plugin: 'objective-cpp'
// }
if (OperatingSystem.current().isMacOsX()) {
apply plugin: 'objective-cpp'
}
apply from: "${rootDir}/shared/jni/setupBuild.gradle"
@@ -87,16 +87,16 @@ ext {
splitSetup = {
if (it.targetPlatform.operatingSystem.isMacOsX()) {
it.sources {
// macObjCpp(ObjectiveCppSourceSet) {
// source {
// srcDirs = ['src/main/native/objcpp']
// include '**/*.mm'
// }
// exportedHeaders {
// srcDirs 'src/main/native/include'
// include '**/*.h'
// }
// }
macObjCpp(ObjectiveCppSourceSet) {
source {
srcDirs = ['src/main/native/objcpp']
include '**/*.mm'
}
exportedHeaders {
srcDirs 'src/main/native/include', 'src/main/native/cpp'
include '**/*.h'
}
}
cscoreMacCpp(CppSourceSet) {
source {
srcDirs 'src/main/native/osx'
@@ -157,6 +157,12 @@ Action<List<String>> symbolFilter = { symbols ->
symbols.removeIf({ !it.startsWith('CS_') })
} as Action<List<String>>;
run {
if (OperatingSystem.current().isMacOsX()) {
jvmArgs("-XstartOnFirstThread");
}
}
nativeUtils.exportsConfigs {
cscore {
x64ExcludeSymbols = [

View File

@@ -390,4 +390,10 @@ public class CameraServerJNI {
public static native long allocateRawFrame();
public static native void freeRawFrame(long frame);
public static native void runOsxRunLoop();
public static native int runOsxRunLoopTimeout(double timeoutSeconds);
public static native void stopOsxMainRunLoop();
}

View File

@@ -50,6 +50,7 @@ class SourceImpl : public PropertyContainer {
void SetConnectionStrategy(CS_ConnectionStrategy strategy) {
m_strategy = static_cast<int>(strategy);
NumSinksChanged();
}
bool IsEnabled() const {
return m_strategy == CS_CONNECTION_KEEP_OPEN ||

View File

@@ -154,7 +154,7 @@ template <typename T>
inline std::span<T>
UnlimitedHandleResource<THandle, TStruct, typeValue, TMutex>::GetAll(
wpi::SmallVectorImpl<T>& vec) {
ForEach([&](THandle handle, const TStruct& data) { vec.push_back(handle); });
ForEach([&](THandle handle, const TStruct&) { vec.push_back(handle); });
return vec;
}

View File

@@ -13,6 +13,7 @@
#include "cscore_cpp.h"
#include "cscore_cv.h"
#include "cscore_raw.h"
#include "cscore_runloop.h"
#include "edu_wpi_first_cscore_CameraServerJNI.h"
namespace cv {
@@ -2226,4 +2227,40 @@ Java_edu_wpi_first_cscore_CameraServerJNI_freeRawFrame
delete ptr;
}
/*
* Class: edu_wpi_first_cscore_CameraServerJNI
* Method: runOsxRunLoop
* Signature: ()V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_cscore_CameraServerJNI_runOsxRunLoop
(JNIEnv*, jclass)
{
cs::RunOsxRunLoop();
}
/*
* Class: edu_wpi_first_cscore_CameraServerJNI
* Method: runOsxRunLoopTimeout
* Signature: (D)I
*/
JNIEXPORT jint JNICALL
Java_edu_wpi_first_cscore_CameraServerJNI_runOsxRunLoopTimeout
(JNIEnv*, jclass, jdouble timeoutSeconds)
{
return cs::RunOsxRunLoopTimeout(timeoutSeconds);
}
/*
* Class: edu_wpi_first_cscore_CameraServerJNI
* Method: stopOsxMainRunLoop
* Signature: ()V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_cscore_CameraServerJNI_stopOsxMainRunLoop
(JNIEnv*, jclass)
{
return cs::StopOsxMainRunLoop();
}
} // extern "C"

View File

@@ -90,6 +90,11 @@ struct VideoMode : public CS_VideoMode {
return pixelFormat == other.pixelFormat && width == other.width &&
height == other.height && fps == other.fps;
}
bool CompareWithoutFps(const VideoMode& other) const {
return pixelFormat == other.pixelFormat && width == other.width &&
height == other.height;
}
};
/**

View File

@@ -0,0 +1,11 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#pragma once
namespace cs {
void RunOsxRunLoop();
int RunOsxRunLoopTimeout(double timeoutSeconds);
void StopOsxMainRunLoop();
} // namespace cs

View File

@@ -0,0 +1,13 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include "cscore_runloop.h"
namespace cs {
void RunOsxRunLoop() {}
int RunOsxRunLoopTimeout(double timeoutSeconds) {
return 0;
}
void StopOsxMainRunLoop() {}
} // namespace cs

View File

@@ -0,0 +1,21 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include "cscore_runloop.h"
#include <CoreFoundation/CFRunLoop.h>
namespace cs {
void RunOsxRunLoop() {
CFRunLoopRun();
}
int RunOsxRunLoopTimeout(double timeoutSeconds) {
return CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeoutSeconds, false);
}
void StopOsxMainRunLoop() {
CFRunLoopStop(CFRunLoopGetMain());
}
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#pragma once
#import <AVFoundation/AVFoundation.h>
#include <memory>
namespace cs {
class UsbCameraImpl;
}
@interface UsbCameraDelegate
: NSObject <AVCaptureVideoDataOutputSampleBufferDelegate>
@property(nonatomic) std::weak_ptr<cs::UsbCameraImpl> cppImpl;
- (void)captureOutput:(AVCaptureOutput*)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection*)connection;
@end

View File

@@ -0,0 +1,67 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#import "UsbCameraDelegate.h"
#include "UsbCameraImpl.h"
#include <wpi/timestamp.h>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
@implementation UsbCameraDelegate
- (id)init {
self = [super init];
return self;
}
- (void)captureOutput:(AVCaptureOutput*)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection*)connection {
(void)captureOutput;
(void)sampleBuffer;
(void)connection;
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
return;
}
// Buffer always comes in a 32BGRA
auto imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
CVPixelBufferLockBaseAddress(imageBuffer, 0);
void* baseaddress = CVPixelBufferGetBaseAddress(imageBuffer);
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
size_t rowBytes = CVPixelBufferGetBytesPerRow(imageBuffer);
OSType pixelFormat = CVPixelBufferGetPixelFormatType(imageBuffer);
if (rowBytes == 0) {
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
return;
}
if (pixelFormat != kCVPixelFormatType_32BGRA) {
NSLog(@"Unknown Pixel Format %u", pixelFormat);
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
return;
}
size_t currSize = width * 3 * height;
auto tmpMat = cv::Mat(height, width, CV_8UC4, baseaddress, rowBytes);
auto image = sharedThis->AllocImage(cs::VideoMode::PixelFormat::kBGR, width,
height, currSize);
cv::cvtColor(tmpMat, image->AsMat(), cv::COLOR_BGRA2BGR);
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
sharedThis->objcPutFrame(std::move(image), wpi::Now());
}
@end

View File

@@ -0,0 +1,96 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#pragma once
#import <AVFoundation/AVFoundation.h>
#import "UsbCameraDelegate.h"
#import "UsbCameraImplObjc.h"
#include <memory>
#include <string>
#include <optional>
#include "SourceImpl.h"
namespace cs {
struct CameraFPSRange {
int min;
int max;
bool IsWithinRange(int fps) { return fps >= min && fps <= max; }
};
struct CameraModeStore {
VideoMode mode;
AVCaptureDeviceFormat* format;
std::vector<CameraFPSRange> fpsRanges;
};
class UsbCameraImpl : public SourceImpl {
public:
UsbCameraImpl(std::string_view name, wpi::Logger& logger, Notifier& notifier,
Telemetry& telemetry, std::string_view path);
UsbCameraImpl(std::string_view name, wpi::Logger& logger, Notifier& notifier,
Telemetry& telemetry, int deviceId);
~UsbCameraImpl() override;
void Start() override;
// Property functions
void SetProperty(int property, int value, CS_Status* status) override;
void SetStringProperty(int property, std::string_view value,
CS_Status* status) override;
// Standard common camera properties
void SetBrightness(int brightness, CS_Status* status) override;
int GetBrightness(CS_Status* status) const override;
void SetWhiteBalanceAuto(CS_Status* status) override;
void SetWhiteBalanceHoldCurrent(CS_Status* status) override;
void SetWhiteBalanceManual(int value, CS_Status* status) override;
void SetExposureAuto(CS_Status* status) override;
void SetExposureHoldCurrent(CS_Status* status) override;
void SetExposureManual(int value, CS_Status* status) override;
bool SetVideoMode(const VideoMode& mode, CS_Status* status) override;
bool SetPixelFormat(VideoMode::PixelFormat pixelFormat,
CS_Status* status) override;
bool SetResolution(int width, int height, CS_Status* status) override;
bool SetFPS(int fps, CS_Status* status) override;
void NumSinksChanged() override;
void NumSinksEnabledChanged() override;
cs::Notifier& objcGetNotifier() { return m_notifier; }
void objcSwapVideoModes(std::vector<VideoMode>& modes) {
std::scoped_lock lock(m_mutex);
m_videoModes.swap(modes);
}
void objcSetVideoMode(const VideoMode& mode) {
std::scoped_lock lock(m_mutex);
m_mode = mode;
}
void objcPutFrame(std::unique_ptr<Image> image, Frame::Time time) {
PutFrame(std::move(image), time);
}
const VideoMode& objcGetVideoMode() const { return m_mode; }
std::vector<CameraModeStore>& objcGetPlatformVideoModes() {
return m_platformModes;
}
wpi::Logger& objcGetLogger() { return m_logger; }
UsbCameraImplObjc* cppGetObjc() { return m_objc; }
private:
UsbCameraImplObjc* m_objc;
std::vector<CameraModeStore> m_platformModes;
VideoMode m_mode;
};
} // namespace cs

View File

@@ -0,0 +1,203 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#include <iostream>
#include <vector>
#include <string>
#include <wpi/timestamp.h>
#pragma GCC diagnostic ignored "-Wunused-parameter"
#include "Handle.h"
#include "Log.h"
#include "Notifier.h"
#include "Instance.h"
#include "c_util.h"
#include "cscore_cpp.h"
#include "opencv2/imgproc.hpp"
#include "UsbCameraImpl.h"
namespace cs {
UsbCameraImpl::UsbCameraImpl(std::string_view name, wpi::Logger& logger,
Notifier& notifier, Telemetry& telemetry,
std::string_view path)
: SourceImpl{name, logger, notifier, telemetry} {
UsbCameraImplObjc* objc = [[UsbCameraImplObjc alloc] init];
objc.path = [[NSString alloc] initWithBytes:path.data()
length:path.size()
encoding:NSUTF8StringEncoding];
m_objc = objc;
}
UsbCameraImpl::UsbCameraImpl(std::string_view name, wpi::Logger& logger,
Notifier& notifier, Telemetry& telemetry,
int deviceId)
: SourceImpl{name, logger, notifier, telemetry} {
UsbCameraImplObjc* objc = [[UsbCameraImplObjc alloc] init];
objc.path = nil;
objc.deviceId = deviceId;
m_objc = objc;
}
UsbCameraImpl::~UsbCameraImpl() {
m_objc = nil;
}
void UsbCameraImpl::Start() {
[m_objc start];
}
// Property functions
void UsbCameraImpl::SetProperty(int property, int value, CS_Status* status) {
[m_objc setProperty:property withValue:value status:status];
}
void UsbCameraImpl::SetStringProperty(int property, std::string_view value,
CS_Status* status) {
[m_objc setStringProperty:property withValue:&value status:status];
}
// Standard common camera properties
void UsbCameraImpl::SetBrightness(int brightness, CS_Status* status) {
[m_objc setBrightness:brightness status:status];
}
int UsbCameraImpl::GetBrightness(CS_Status* status) const {
return [m_objc getBrightness:status];
}
void UsbCameraImpl::SetWhiteBalanceAuto(CS_Status* status) {
[m_objc setWhiteBalanceAuto:status];
}
void UsbCameraImpl::SetWhiteBalanceHoldCurrent(CS_Status* status) {
[m_objc setWhiteBalanceHoldCurrent:status];
}
void UsbCameraImpl::SetWhiteBalanceManual(int value, CS_Status* status) {
[m_objc setWhiteBalanceManual:value status:status];
}
void UsbCameraImpl::SetExposureAuto(CS_Status* status) {
[m_objc setExposureAuto:status];
}
void UsbCameraImpl::SetExposureHoldCurrent(CS_Status* status) {
[m_objc setExposureHoldCurrent:status];
}
void UsbCameraImpl::SetExposureManual(int value, CS_Status* status) {
[m_objc setExposureManual:value status:status];
}
bool UsbCameraImpl::SetVideoMode(const VideoMode& mode, CS_Status* status) {
return [m_objc setVideoMode:mode status:status];
}
bool UsbCameraImpl::SetPixelFormat(VideoMode::PixelFormat pixelFormat,
CS_Status* status) {
return [m_objc setPixelFormat:pixelFormat status:status];
}
bool UsbCameraImpl::SetResolution(int width, int height, CS_Status* status) {
return [m_objc setResolutionWidth:width withHeight:height status:status];
}
bool UsbCameraImpl::SetFPS(int fps, CS_Status* status) {
return [m_objc setFPS:fps status:status];
}
void UsbCameraImpl::NumSinksChanged() {
[m_objc numSinksChanged];
}
void UsbCameraImpl::NumSinksEnabledChanged() {
[m_objc numSinksEnabledChanged];
}
CS_Source CreateUsbCameraDev(std::string_view name, int dev,
CS_Status* status) {
std::vector<UsbCameraInfo> devices = cs::EnumerateUsbCameras(status);
if (static_cast<int>(devices.size()) > dev) {
return CreateUsbCameraPath(name, devices[dev].path, status);
}
auto& inst = Instance::GetInstance();
return inst.CreateSource(CS_SOURCE_USB, std::make_shared<UsbCameraImpl>(
name, inst.logger, inst.notifier,
inst.telemetry, dev));
}
CS_Source CreateUsbCameraPath(std::string_view name, std::string_view path,
CS_Status* status) {
(void)status;
auto& inst = Instance::GetInstance();
auto val = std::make_shared<UsbCameraImpl>(name, inst.logger, inst.notifier,
inst.telemetry, path);
val->cppGetObjc().cppImpl = val;
return inst.CreateSource(CS_SOURCE_USB, val);
}
std::vector<UsbCameraInfo> EnumerateUsbCameras(CS_Status* status) {
@autoreleasepool {
(void)status;
std::vector<UsbCameraInfo> retval;
NSArray<AVCaptureDeviceType>* deviceTypes = @[
AVCaptureDeviceTypeBuiltInWideAngleCamera,
AVCaptureDeviceTypeExternalUnknown
];
AVCaptureDeviceDiscoverySession* session = [AVCaptureDeviceDiscoverySession
discoverySessionWithDeviceTypes:deviceTypes
mediaType:AVMediaTypeVideo
position:AVCaptureDevicePositionUnspecified];
NSArray* captureDevices = [session devices];
int count = 0;
for (id device in captureDevices) {
NSString* name = [device localizedName];
NSString* uniqueIdentifier = [(AVCaptureDevice*)device uniqueID];
retval.push_back(
{count, [uniqueIdentifier UTF8String], [name UTF8String], {}});
count++;
}
return retval;
}
}
void SetUsbCameraPath(CS_Source source, std::string_view path,
CS_Status* status) {
auto data = Instance::GetInstance().GetSource(source);
if (!data || data->kind != CS_SOURCE_USB) {
*status = CS_INVALID_HANDLE;
return;
}
[static_cast<UsbCameraImpl&>(*data->source).cppGetObjc()
setNewCameraPath:&path];
}
std::string GetUsbCameraPath(CS_Source source, CS_Status* status) {
auto data = Instance::GetInstance().GetSource(source);
if (!data || data->kind != CS_SOURCE_USB) {
*status = CS_INVALID_HANDLE;
return std::string{};
}
std::string ret;
[static_cast<UsbCameraImpl&>(*data->source).cppGetObjc()
getCurrentCameraPath:&ret];
return ret;
}
UsbCameraInfo GetUsbCameraInfo(CS_Source source, CS_Status* status) {
UsbCameraInfo info;
auto data = Instance::GetInstance().GetSource(source);
if (!data || data->kind != CS_SOURCE_USB) {
*status = CS_INVALID_HANDLE;
return info;
}
[static_cast<UsbCameraImpl&>(*data->source).cppGetObjc()
getCurrentCameraPath:&info.path];
[static_cast<UsbCameraImpl&>(*data->source).cppGetObjc()
getCameraName:&info.name];
info.productId = 0;
info.vendorId = 0;
// ParseVidAndPid(info.path, &info.productId, &info.vendorId);
info.dev = -1; // We have lost dev information by this point in time.
return info;
}
} // namespace cs

View File

@@ -0,0 +1,71 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#pragma once
#import <AVFoundation/AVFoundation.h>
#import "UsbCameraDelegate.h"
#include <memory>
#include <string_view>
#include "cscore_cpp.h"
namespace cs {
class UsbCameraImpl;
}
@interface UsbCameraImplObjc : NSObject
@property(nonatomic) AVCaptureDeviceFormat* currentFormat;
@property(nonatomic) int currentFPS;
@property(nonatomic) std::weak_ptr<cs::UsbCameraImpl> cppImpl;
@property(nonatomic) dispatch_queue_t sessionQueue;
@property(nonatomic) NSString* path;
@property(nonatomic) int deviceId;
@property(nonatomic) bool propertiesCached;
@property(nonatomic) bool streaming;
@property(nonatomic) bool deviceValid;
@property(nonatomic) bool isAuthorized;
@property(nonatomic) AVCaptureDevice* videoDevice;
@property(nonatomic) AVCaptureDeviceInput* videoInput;
@property(nonatomic) UsbCameraDelegate* callback;
@property(nonatomic) AVCaptureVideoDataOutput* videoOutput;
@property(nonatomic) AVCaptureSession* session;
- (void)start;
// Property functions
- (void)setProperty:(int)property
withValue:(int)value
status:(CS_Status*)status;
- (void)setStringProperty:(int)property
withValue:(std::string_view*)value
status:(CS_Status*)status;
// Standard common camera properties
- (void)setBrightness:(int)brightness status:(CS_Status*)status;
- (int)getBrightness:(CS_Status*)status;
- (void)setWhiteBalanceAuto:(CS_Status*)status;
- (void)setWhiteBalanceHoldCurrent:(CS_Status*)status;
- (void)setWhiteBalanceManual:(int)value status:(CS_Status*)status;
- (void)setExposureAuto:(CS_Status*)status;
- (void)setExposureHoldCurrent:(CS_Status*)status;
- (void)setExposureManual:(int)value status:(CS_Status*)status;
- (bool)setVideoMode:(const cs::VideoMode&)mode status:(CS_Status*)status;
- (bool)setPixelFormat:(cs::VideoMode::PixelFormat)pixelFormat
status:(CS_Status*)status;
- (bool)setResolutionWidth:(int)width
withHeight:(int)height
status:(CS_Status*)status;
- (bool)setFPS:(int)fps status:(CS_Status*)status;
- (void)numSinksChanged;
- (void)numSinksEnabledChanged;
- (void)getCurrentCameraPath:(std::string*)path;
- (void)getCameraName:(std::string*)name;
- (void)setNewCameraPath:(std::string_view*)path;
@end

View File

@@ -0,0 +1,669 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#import "UsbCameraImplObjc.h"
#include "UsbCameraImpl.h"
#pragma GCC diagnostic ignored "-Wunused-parameter"
#include "Notifier.h"
#include "Log.h"
template <typename S, typename... Args>
inline void NamedLog(UsbCameraImplObjc* objc, unsigned int level,
const char* file, unsigned int line, const S& format,
Args&&... args) {
auto sharedThis = objc.cppImpl.lock();
if (!sharedThis) {
return;
}
wpi::Logger& logger = sharedThis->objcGetLogger();
std::string_view name = sharedThis->GetName();
if (logger.HasLogger() && level >= logger.min_level()) {
cs::NamedLogV(logger, level, file, line, name, format,
fmt::make_format_args(args...));
}
}
#define OBJCLOG(level, format, ...) \
NamedLog(self, level, __FILE__, __LINE__, \
FMT_STRING(format) __VA_OPT__(, ) __VA_ARGS__)
#define OBJCERROR(format, ...) \
OBJCLOG(::wpi::WPI_LOG_ERROR, format __VA_OPT__(, ) __VA_ARGS__)
#define OBJCWARNING(format, ...) \
OBJCLOG(::wpi::WPI_LOG_WARNING, format __VA_OPT__(, ) __VA_ARGS__)
#define OBJCINFO(format, ...) \
OBJCLOG(::wpi::WPI_LOG_INFO, format __VA_OPT__(, ) __VA_ARGS__)
#ifdef NDEBUG
#define OBJCDEBUG(format, ...) \
do { \
} while (0)
#define OBJCDEBUG1(format, ...) \
do { \
} while (0)
#define OBJCDEBUG2(format, ...) \
do { \
} while (0)
#define OBJCDEBUG3(format, ...) \
do { \
} while (0)
#define OBJCDEBUG4(format, ...) \
do { \
} while (0)
#else
#define OBJCDEBUG(format, ...) \
OBJCLOG(::wpi::WPI_LOG_DEBUG, format __VA_OPT__(, ) __VA_ARGS__)
#define OBJCDEBUG1(format, ...) \
OBJCLOG(::wpi::WPI_LOG_DEBUG1, format __VA_OPT__(, ) __VA_ARGS__)
#define OBJCDEBUG2(format, ...) \
OBJCLOG(::wpi::WPI_LOG_DEBUG2, format __VA_OPT__(, ) __VA_ARGS__)
#define OBJCDEBUG3(format, ...) \
OBJCLOG(::wpi::WPI_LOG_DEBUG3, format __VA_OPT__(, ) __VA_ARGS__)
#define OBJCDEBUG4(format, ...) \
OBJCLOG(::wpi::WPI_LOG_DEBUG4, format __VA_OPT__(, ) __VA_ARGS__)
#endif
using namespace cs;
@implementation UsbCameraImplObjc
- (void)start {
switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
case AVAuthorizationStatusAuthorized:
self.isAuthorized = true;
break;
default:
OBJCERROR(
"Camera access explicitly blocked for application. No cameras are "
"accessable");
self.isAuthorized = false;
// TODO log
break;
case AVAuthorizationStatusNotDetermined:
dispatch_suspend(self.sessionQueue);
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo
completionHandler:^(BOOL granted) {
self.isAuthorized = granted;
dispatch_resume(self.sessionQueue);
}];
break;
}
dispatch_async(self.sessionQueue, ^{
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(cameraConnected:)
name:AVCaptureDeviceWasConnectedNotification
object:nil];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(cameraDisconnected:)
name:AVCaptureDeviceWasDisconnectedNotification
object:nil];
[self deviceConnect];
});
}
// Property functions
- (void)setProperty:(int)property
withValue:(int)value
status:(CS_Status*)status {
}
- (void)setStringProperty:(int)property
withValue:(std::string_view*)value
status:(CS_Status*)status {
}
// Standard common camera properties
- (void)setBrightness:(int)brightness status:(CS_Status*)status {
*status = CS_INVALID_PROPERTY;
}
- (int)getBrightness:(CS_Status*)status {
*status = CS_INVALID_PROPERTY;
return 0;
}
- (void)setWhiteBalanceAuto:(CS_Status*)status {
*status = CS_INVALID_PROPERTY;
}
- (void)setWhiteBalanceHoldCurrent:(CS_Status*)status {
*status = CS_INVALID_PROPERTY;
}
- (void)setWhiteBalanceManual:(int)value status:(CS_Status*)status {
*status = CS_INVALID_PROPERTY;
}
- (void)setExposureAuto:(CS_Status*)status {
*status = CS_INVALID_PROPERTY;
}
- (void)setExposureHoldCurrent:(CS_Status*)status {
*status = CS_INVALID_PROPERTY;
}
- (void)setExposureManual:(int)value status:(CS_Status*)status {
*status = CS_INVALID_PROPERTY;
}
- (bool)setVideoMode:(const cs::VideoMode&)mode status:(CS_Status*)status {
dispatch_async_and_wait(self.sessionQueue, ^{
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
*status = CS_READ_FAILED;
return;
}
[self internalSetMode:mode status:status];
});
return true;
}
- (bool)setPixelFormat:(cs::VideoMode::PixelFormat)pixelFormat
status:(CS_Status*)status {
dispatch_async_and_wait(self.sessionQueue, ^{
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
*status = CS_READ_FAILED;
return;
}
VideoMode newMode;
newMode = sharedThis->objcGetVideoMode();
newMode.pixelFormat = pixelFormat;
[self internalSetMode:newMode status:status];
});
return true;
}
- (bool)setResolutionWidth:(int)width
withHeight:(int)height
status:(CS_Status*)status {
dispatch_async_and_wait(self.sessionQueue, ^{
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
*status = CS_READ_FAILED;
return;
}
VideoMode newMode;
newMode = sharedThis->objcGetVideoMode();
newMode.width = width;
newMode.height = height;
[self internalSetMode:newMode status:status];
});
return true;
}
- (void)internalSetMode:(const cs::VideoMode&)newMode
status:(CS_Status*)status {
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
*status = CS_READ_FAILED;
return;
}
// If device is not connected, just apply and leave.
if (!self.propertiesCached) {
sharedThis->objcSetVideoMode(newMode);
*status = CS_OK;
return;
}
if (newMode != sharedThis->objcGetVideoMode()) {
OBJCDEBUG3("Trying Mode {} {} {} {}", newMode.pixelFormat, newMode.width,
newMode.height, newMode.fps);
int localFPS = 0;
AVCaptureDeviceFormat* newModeType = [self deviceCheckModeValid:&newMode
withFps:&localFPS];
if (newModeType == nil) {
*status = CS_UNSUPPORTED_MODE;
return;
}
self.currentFormat = newModeType;
self.currentFPS = localFPS;
sharedThis->objcSetVideoMode(newMode);
[self deviceDisconnect];
[self deviceConnect];
sharedThis->objcGetNotifier().NotifySourceVideoMode(*sharedThis, newMode);
}
*status = CS_OK;
}
- (bool)setFPS:(int)fps status:(CS_Status*)status {
dispatch_async_and_wait(self.sessionQueue, ^{
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
*status = CS_READ_FAILED;
return;
}
VideoMode newMode;
newMode = sharedThis->objcGetVideoMode();
newMode.fps = fps;
[self internalSetMode:newMode status:status];
});
return true;
}
- (void)numSinksChanged {
dispatch_async(self.sessionQueue, ^{
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
return;
}
if (!sharedThis->IsEnabled()) {
[self deviceStreamOff];
} else if (!self.streaming && sharedThis->IsEnabled()) {
[self deviceStreamOn];
}
});
}
- (void)numSinksEnabledChanged {
[self numSinksChanged];
}
// All above is direct forwarders from C++, must always dispatch to loop
- (void)getCurrentCameraPath:(std::string*)path {
dispatch_async_and_wait(self.sessionQueue, ^{
if (self.videoDevice == nil) {
return;
}
*path = [self.videoDevice.uniqueID UTF8String];
});
}
- (void)getCameraName:(std::string*)name {
dispatch_async_and_wait(self.sessionQueue, ^{
if (self.videoDevice == nil) {
return;
}
*name = [self.videoDevice.localizedName UTF8String];
});
}
- (void)setNewCameraPath:(std::string_view*)path {
dispatch_async_and_wait(self.sessionQueue, ^{
NSString* nsPath = [[NSString alloc] initWithBytes:path->data()
length:path->size()
encoding:NSUTF8StringEncoding];
if (self.path != nil && [self.path isEqualToString:nsPath]) {
return;
}
self.path = nsPath;
[self deviceDisconnect];
[self deviceConnect];
});
}
// All above are called from C++, must always dispatch to loop
- (void)deviceCacheProperties {
if (self.session == nil) {
return;
}
}
static cs::VideoMode::PixelFormat FourCCToPixelFormat(FourCharCode fourcc) {
switch (fourcc) {
case kCVPixelFormatType_422YpCbCr8_yuvs:
case kCVPixelFormatType_422YpCbCr8FullRange:
return cs::VideoMode::PixelFormat::kYUYV;
default:
return cs::VideoMode::PixelFormat::kBGR;
}
}
- (void)deviceCacheVideoModes {
if (self.session == nil) {
return;
}
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
return;
}
std::vector<CameraModeStore>& platformModes =
sharedThis->objcGetPlatformVideoModes();
platformModes.clear();
std::vector<VideoMode> modes;
@autoreleasepool {
NSArray<AVCaptureDeviceFormat*>* formats = self.videoDevice.formats;
int count = 0;
for (AVCaptureDeviceFormat* format in formats) {
CMFormatDescriptionRef cmformat = format.formatDescription;
CMVideoDimensions s1 = CMVideoFormatDescriptionGetDimensions(cmformat);
FourCharCode fourcc = CMFormatDescriptionGetMediaSubType(cmformat);
auto videoFormat = FourCCToPixelFormat(fourcc);
NSArray<AVFrameRateRange*>* frameRates =
format.videoSupportedFrameRateRanges;
CameraModeStore store;
store.mode.pixelFormat = videoFormat;
store.mode.width = static_cast<int>(s1.width);
store.mode.height = static_cast<int>(s1.height);
store.format = format;
int maxFps = 0;
for (AVFrameRateRange* rate in frameRates) {
CMTime highest = rate.minFrameDuration;
CMTime lowest = rate.maxFrameDuration;
int highestFps = highest.timescale / static_cast<double>(highest.value);
int lowestFps = lowest.timescale / static_cast<double>(lowest.value);
store.fpsRanges.emplace_back(CameraFPSRange{lowestFps, highestFps});
if (highestFps > maxFps) {
maxFps = highestFps;
}
}
store.mode.fps = maxFps;
modes.emplace_back(store.mode);
platformModes.emplace_back(store);
count++;
}
}
sharedThis->objcSwapVideoModes(modes);
sharedThis->objcGetNotifier().NotifySource(*sharedThis,
CS_SOURCE_VIDEOMODES_UPDATED);
}
- (AVCaptureDeviceFormat*)deviceCheckModeValid:(const cs::VideoMode*)toCheck
withFps:(int*)fps {
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
return nil;
}
OBJCDEBUG3("Checking mode {} {} {} {}", toCheck->pixelFormat, toCheck->width,
toCheck->height, toCheck->fps);
std::vector<CameraModeStore>& platformModes =
sharedThis->objcGetPlatformVideoModes();
// Find the matching mode
auto match = std::find_if(platformModes.begin(), platformModes.end(),
[&](CameraModeStore& input) {
return input.mode.CompareWithoutFps(*toCheck);
});
if (match == platformModes.end()) {
return nil;
}
// Check FPS
for (CameraFPSRange& range : match->fpsRanges) {
OBJCDEBUG3("Checking Range {} {}", range.min, range.max);
if (range.IsWithinRange(toCheck->fps)) {
*fps = toCheck->fps;
return match->format;
}
}
return nil;
}
- (void)deviceCacheMode {
if (!self.session) {
return;
}
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
return;
}
std::vector<CameraModeStore>& platformModes =
sharedThis->objcGetPlatformVideoModes();
if (platformModes.size() == 0) {
return;
}
if (self.currentFormat == nil) {
int localFps = 0;
self.currentFormat =
[self deviceCheckModeValid:&sharedThis->objcGetVideoMode()
withFps:&localFps];
if (self.currentFormat == nil) {
self.currentFormat = self.videoDevice.activeFormat;
auto result = std::find_if(platformModes.begin(), platformModes.end(),
[f = self.currentFormat](CameraModeStore& i) {
return [f isEqual:i.format];
});
if (result == platformModes.end()) {
auto& firstSupported = platformModes[0];
self.currentFormat = firstSupported.format;
self.currentFPS = firstSupported.mode.fps;
sharedThis->objcSetVideoMode(firstSupported.mode);
} else {
self.currentFPS = result->mode.fps;
sharedThis->objcSetVideoMode(result->mode);
}
} else {
self.currentFPS = localFps;
}
}
[self deviceSetMode];
sharedThis->objcGetNotifier().NotifySourceVideoMode(
*sharedThis, sharedThis->objcGetVideoMode());
}
- (void)deviceSetMode {
self.deviceValid = true;
}
- (bool)deviceStreamOn {
if (self.streaming) {
return false;
}
if (!self.deviceValid) {
return false;
}
self.streaming = true;
[self.session startRunning];
if ([self.videoDevice lockForConfiguration:nil]) {
if (self.currentFormat != nil) {
self.videoDevice.activeFormat = self.currentFormat;
}
if (self.currentFPS != 0) {
self.videoDevice.activeVideoMinFrameDuration =
CMTimeMake(1, self.currentFPS);
self.videoDevice.activeVideoMaxFrameDuration =
CMTimeMake(1, self.currentFPS);
}
[self.videoDevice unlockForConfiguration];
} else {
OBJCERROR("Failed to lock for configuration");
}
return true;
}
- (bool)deviceStreamOff {
if (self.streaming) {
[self.session stopRunning];
}
self.streaming = false;
return true;
}
- (id)init {
self = [super init];
// TODO pass in name, make this queue specific
self.sessionQueue =
dispatch_queue_create("session queue", DISPATCH_QUEUE_SERIAL);
return self;
}
- (void)deviceDisconnect {
std::string pathStr = [self.path UTF8String];
OBJCINFO("Disconnected from {}", pathStr);
[self deviceStreamOff];
self.session = nil;
self.videoOutput = nil;
self.callback = nil;
self.videoInput = nil;
self.videoDevice = nil;
self.streaming = false;
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
return;
}
sharedThis->SetConnected(false);
}
- (bool)deviceConnect {
if (!self.isAuthorized) {
OBJCERROR(
"Camera access not authorized for application. No cameras are "
"accessable");
return false;
}
OSType pixelFormat = kCVPixelFormatType_32BGRA;
NSDictionary* pixelBufferOptions =
@{(id)kCVPixelBufferPixelFormatTypeKey : @(pixelFormat)};
if (self.session != nil) {
return true;
}
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
return false;
}
if (self.path == nil) {
OBJCINFO("Starting for device id {}", self.deviceId);
// Enumerate Devices
CS_Status status = 0;
auto cameras = cs::EnumerateUsbCameras(&status);
if (static_cast<int>(cameras.size()) <= self.deviceId) {
return false;
}
std::string& path = cameras[self.deviceId].path;
self.path = [[NSString alloc] initWithBytes:path.data()
length:path.size()
encoding:NSUTF8StringEncoding];
}
std::string pathStr = [self.path UTF8String];
OBJCINFO("Connecting to USB camera on {}", pathStr);
self.videoDevice = [AVCaptureDevice deviceWithUniqueID:self.path];
if (self.videoDevice == nil) {
OBJCWARNING("Device Not found");
goto err;
}
self.videoInput = [AVCaptureDeviceInput deviceInputWithDevice:self.videoDevice
error:nil];
if (self.videoInput == nil) {
OBJCWARNING("Creating AVCaptureDeviceInput failed");
goto err;
}
self.callback = [[UsbCameraDelegate alloc] init];
if (self.callback == nil) {
OBJCWARNING("Creating Camera Callback failed");
goto err;
}
self.callback.cppImpl = self.cppImpl;
self.videoOutput = [[AVCaptureVideoDataOutput alloc] init];
if (self.videoOutput == nil) {
OBJCWARNING("Creating AVCaptureVideoDataOutput failed");
goto err;
}
[self.videoOutput setSampleBufferDelegate:self.callback
queue:self.sessionQueue];
self.videoOutput.videoSettings = pixelBufferOptions;
self.videoOutput.alwaysDiscardsLateVideoFrames = YES;
self.session = [[AVCaptureSession alloc] init];
if (self.session == nil) {
OBJCWARNING("Creating AVCaptureSession failed");
goto err;
}
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(sessionRuntimeError:)
name:AVCaptureSessionRuntimeErrorNotification
object:self.session];
[self.session addInput:self.videoInput];
[self.session addOutput:self.videoOutput];
sharedThis->SetDescription([self.videoDevice.localizedName UTF8String]);
if (!self.propertiesCached) {
OBJCDEBUG3("Caching properties");
[self deviceCacheProperties];
[self deviceCacheVideoModes];
[self deviceCacheMode];
self.propertiesCached = true;
} else {
OBJCDEBUG3("Restoring Video Mode");
[self deviceSetMode];
}
sharedThis->SetConnected(true);
if (sharedThis->IsEnabled()) {
[self deviceStreamOn];
}
return true;
err:
self.session = nil;
self.videoOutput = nil;
self.callback = nil;
self.videoInput = nil;
self.videoDevice = nil;
return false;
}
// Helpers
- (void)sessionRuntimeError:(NSNotification*)notification {
@autoreleasepool {
NSError* error = notification.userInfo[AVCaptureSessionErrorKey];
const char* str = [error.description UTF8String];
if (str) {
std::string errorStr = str;
OBJCERROR("Capture session runtime error: {}", errorStr);
}
}
}
- (void)cameraDisconnected:(NSNotification*)notification {
AVCaptureDevice* device = notification.object;
dispatch_async(self.sessionQueue, ^{
if (self.path != nil && [device.uniqueID isEqualToString:self.path]) {
[self deviceDisconnect];
}
});
}
- (void)cameraConnected:(NSNotification*)notification {
AVCaptureDevice* device = notification.object;
dispatch_async(self.sessionQueue, ^{
if (self.path == nil || [device.uniqueID isEqualToString:self.path]) {
[self deviceConnect];
}
});
}
@end

View File

@@ -0,0 +1,111 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#import <AVFoundation/AVFoundation.h>
#include "UsbCameraListener.h"
#pragma GCC diagnostic ignored "-Wunused-parameter"
#include "Notifier.h"
using namespace cs;
@interface UsbCameraListenerImpl : NSObject
@property(nonatomic) Notifier* notifier;
@property BOOL started;
@property(nonatomic) dispatch_queue_t sessionQueue;
- (void)start;
- (void)stop;
@end
@implementation UsbCameraListenerImpl
- (id)init {
self = [super init];
self.sessionQueue =
dispatch_queue_create("Camera Listener", DISPATCH_QUEUE_SERIAL);
return self;
}
- (void)start {
dispatch_async(self.sessionQueue, ^{
if (self.started) {
return;
}
self.started = YES;
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(camerasChanged:)
name:AVCaptureDeviceWasConnectedNotification
object:nil];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(camerasChanged:)
name:AVCaptureDeviceWasDisconnectedNotification
object:nil];
});
}
- (void)stop {
dispatch_async_and_wait(self.sessionQueue, ^{
if (!self.started) {
return;
}
self.started = NO;
[[NSNotificationCenter defaultCenter]
removeObserver:self
name:AVCaptureDeviceWasConnectedNotification
object:nil];
[[NSNotificationCenter defaultCenter]
removeObserver:self
name:AVCaptureDeviceWasDisconnectedNotification
object:nil];
});
}
- (void)camerasChanged:(NSNotification*)notification {
@autoreleasepool {
dispatch_async(self.sessionQueue, ^{
if (!self.started) {
return;
}
AVCaptureDevice* device = notification.object;
if ([device.deviceType
isEqualToString:AVCaptureDeviceTypeBuiltInWideAngleCamera] ||
[device.deviceType
isEqualToString:AVCaptureDeviceTypeExternalUnknown]) {
self.notifier->NotifyUsbCamerasChanged();
}
});
}
}
@end
class UsbCameraListener::Impl {
public:
UsbCameraListenerImpl* listener;
explicit Impl(Notifier& notifier) {
listener = [[UsbCameraListenerImpl alloc] init];
listener.notifier = &notifier;
}
};
UsbCameraListener::UsbCameraListener(wpi::Logger&, Notifier& notifier)
: m_impl{std::make_unique<Impl>(notifier)} {}
UsbCameraListener::~UsbCameraListener() {
Stop();
}
void UsbCameraListener::Start() {
[m_impl->listener start];
}
void UsbCameraListener::Stop() {
[m_impl->listener stop];
}

View File

@@ -1,12 +0,0 @@
#import <Foundation/Foundation.h>
@interface XYZPerson : NSObject
- (void)sayHello;
@end
@implementation XYZPerson
- (void)sayHello {
NSLog(@"Hello, World!");
}
@end

View File

@@ -1,48 +0,0 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include "Instance.h"
#include "cscore_cpp.h"
namespace cs {
CS_Source CreateUsbCameraDev(std::string_view name, int dev,
CS_Status* status) {
*status = CS_INVALID_HANDLE;
WPI_ERROR(Instance::GetInstance().logger,
"USB Camera support not implemented for macOS");
return 0;
}
CS_Source CreateUsbCameraPath(std::string_view name, std::string_view path,
CS_Status* status) {
*status = CS_INVALID_HANDLE;
WPI_ERROR(Instance::GetInstance().logger,
"USB Camera support not implemented for macOS");
return 0;
}
void SetUsbCameraPath(CS_Source source, std::string_view path,
CS_Status* status) {
*status = CS_INVALID_HANDLE;
}
std::string GetUsbCameraPath(CS_Source source, CS_Status* status) {
*status = CS_INVALID_HANDLE;
return std::string{};
}
UsbCameraInfo GetUsbCameraInfo(CS_Source source, CS_Status* status) {
*status = CS_INVALID_HANDLE;
return UsbCameraInfo{};
}
std::vector<UsbCameraInfo> EnumerateUsbCameras(CS_Status* status) {
*status = CS_INVALID_HANDLE;
WPI_ERROR(Instance::GetInstance().logger,
"USB Camera support not implemented for macOS");
return std::vector<UsbCameraInfo>{};
}
} // namespace cs

View File

@@ -1,17 +0,0 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include "UsbCameraListener.h"
using namespace cs;
class UsbCameraListener::Impl {};
UsbCameraListener::UsbCameraListener(wpi::Logger& logger, Notifier& notifier) {}
UsbCameraListener::~UsbCameraListener() = default;
void UsbCameraListener::Start() {}
void UsbCameraListener::Stop() {}

View File

@@ -0,0 +1,13 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include "cscore_runloop.h"
namespace cs {
void RunOsxRunLoop() {}
int RunOsxRunLoopTimeout(double timeoutSeconds) {
return 0;
}
void StopOsxMainRunLoop() {}
} // namespace cs

View File

@@ -30,6 +30,8 @@ if (project.name != 'wpilibcExamples') {
nativeUtils.setSinglePrintPerPlatform()
nativeUtils.enableSourceLink()
nativeUtils.wpi.addMacMinimumVersionArg()
nativeUtils.platformConfigs.each {
if (it.name.contains('osx')) {
it.linker.args << '-Wl,-rpath,\'@loader_path\''