diff --git a/cmake/compile_definitions/macos.cmake b/cmake/compile_definitions/macos.cmake index 42a5dbe10f1..7c63468f3f9 100644 --- a/cmake/compile_definitions/macos.cmake +++ b/cmake/compile_definitions/macos.cmake @@ -35,6 +35,7 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${CORE_MEDIA_LIBRARY} ${CORE_VIDEO_LIBRARY} ${FOUNDATION_LIBRARY} + ${SCREEN_CAPTURE_KIT_LIBRARY} ${VIDEO_TOOLBOX_LIBRARY}) set(APPLE_PLIST_TEMPLATE "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/build/Info.plist.in") @@ -55,6 +56,8 @@ set(PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/macos/nv12_zero_device.cpp" "${CMAKE_SOURCE_DIR}/src/platform/macos/nv12_zero_device.h" "${CMAKE_SOURCE_DIR}/src/platform/macos/publish.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/macos/sc_capture.h" + "${CMAKE_SOURCE_DIR}/src/platform/macos/sc_capture.m" "${CMAKE_SOURCE_DIR}/third-party/TPCircularBuffer/TPCircularBuffer.c" "${CMAKE_SOURCE_DIR}/third-party/TPCircularBuffer/TPCircularBuffer.h" ${APPLE_PLIST_FILE}) diff --git a/cmake/dependencies/macos.cmake b/cmake/dependencies/macos.cmake index 5e225fdac21..e202cfb277b 100644 --- a/cmake/dependencies/macos.cmake +++ b/cmake/dependencies/macos.cmake @@ -9,6 +9,7 @@ FIND_LIBRARY(CORE_AUDIO_LIBRARY CoreAudio) FIND_LIBRARY(CORE_MEDIA_LIBRARY CoreMedia) FIND_LIBRARY(CORE_VIDEO_LIBRARY CoreVideo) FIND_LIBRARY(FOUNDATION_LIBRARY Foundation) +FIND_LIBRARY(SCREEN_CAPTURE_KIT_LIBRARY ScreenCaptureKit) FIND_LIBRARY(VIDEO_TOOLBOX_LIBRARY VideoToolbox) if(SUNSHINE_ENABLE_TRAY) diff --git a/src/platform/macos/display.mm b/src/platform/macos/display.mm index be124b2d331..3d5e1e406bd 100644 --- a/src/platform/macos/display.mm +++ b/src/platform/macos/display.mm @@ -2,6 +2,11 @@ * @file src/platform/macos/display.mm * @brief Definitions for display capture on macOS. */ +// standard includes +#include +#include +#include + // local includes #include "src/config.h" #include "src/logging.h" @@ -10,6 +15,8 @@ #include "src/platform/macos/av_video.h" #include "src/platform/macos/misc.h" #include "src/platform/macos/nv12_zero_device.h" +#import "src/platform/macos/sc_capture.h" +#include "src/utility.h" // Avoid conflict between AVFoundation and libavutil both defining AVMediaType #define AVMediaType AVMediaType_FFmpeg @@ -20,6 +27,105 @@ namespace platf { using namespace std::literals; + static constexpr auto SCKIT_SCREENSHOT_POLL_INTERVAL_NS = NSEC_PER_SEC / 60; + + static bool process_frame(CMSampleBufferRef sampleBuffer, img_t *img) { + auto pixel_buffer = CMSampleBufferGetImageBuffer(sampleBuffer); + if (!pixel_buffer) { + return false; + } + + auto new_sample_buffer = std::make_shared(sampleBuffer); + auto new_pixel_buffer = std::make_shared(new_sample_buffer->buf); + + auto av_img = (av_img_t *) img; + + auto old_data_retainer = std::make_shared( + av_img->sample_buffer, + av_img->pixel_buffer, + img->data + ); + + av_img->sample_buffer = new_sample_buffer; + av_img->pixel_buffer = new_pixel_buffer; + img->data = new_pixel_buffer->data(); + + img->width = (int) CVPixelBufferGetWidth(new_pixel_buffer->buf); + img->height = (int) CVPixelBufferGetHeight(new_pixel_buffer->buf); + img->row_pitch = CVPixelBufferIsPlanar(new_pixel_buffer->buf) ? + (int) CVPixelBufferGetBytesPerRowOfPlane(new_pixel_buffer->buf, 0) : + (int) CVPixelBufferGetBytesPerRow(new_pixel_buffer->buf); + img->pixel_pitch = img->row_pitch / img->width; + + old_data_retainer = nullptr; + return true; + } + + static void clear_pixel_buffer(CVPixelBufferRef pixel_buffer) { + CVPixelBufferLockBaseAddress(pixel_buffer, 0); + + if (CVPixelBufferIsPlanar(pixel_buffer)) { + for (size_t plane = 0; plane < CVPixelBufferGetPlaneCount(pixel_buffer); ++plane) { + auto *base = static_cast(CVPixelBufferGetBaseAddressOfPlane(pixel_buffer, plane)); + auto bytes_per_row = CVPixelBufferGetBytesPerRowOfPlane(pixel_buffer, plane); + auto height = CVPixelBufferGetHeightOfPlane(pixel_buffer, plane); + std::memset(base, 0, bytes_per_row * height); + } + } else { + auto *base = static_cast(CVPixelBufferGetBaseAddress(pixel_buffer)); + std::memset(base, 0, CVPixelBufferGetBytesPerRow(pixel_buffer) * CVPixelBufferGetHeight(pixel_buffer)); + } + + CVPixelBufferUnlockBaseAddress(pixel_buffer, 0); + } + + static int make_dummy_img(img_t *img, int width, int height, OSType pixel_format, std::string_view backend_name) { + CVPixelBufferRef pixel_buffer = nullptr; + NSDictionary *attrs = @{ + (NSString *) kCVPixelBufferIOSurfacePropertiesKey: @ {}, + }; + + auto status = CVPixelBufferCreate( + kCFAllocatorDefault, + width, + height, + pixel_format, + (__bridge CFDictionaryRef) attrs, + &pixel_buffer + ); + + if (status != kCVReturnSuccess || !pixel_buffer) { + BOOST_LOG(error) << backend_name << " dummy_img: failed to create pixel buffer"sv; + return 1; + } + + clear_pixel_buffer(pixel_buffer); + + CMVideoFormatDescriptionRef format_desc = nullptr; + status = CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault, pixel_buffer, &format_desc); + if (status != noErr || !format_desc) { + CVPixelBufferRelease(pixel_buffer); + BOOST_LOG(error) << backend_name << " dummy_img: failed to create format description"sv; + return 1; + } + + CMSampleTimingInfo timing = {kCMTimeInvalid, kCMTimeInvalid, kCMTimeInvalid}; + CMSampleBufferRef sample_buffer = nullptr; + status = CMSampleBufferCreateForImageBuffer(kCFAllocatorDefault, pixel_buffer, YES, nullptr, nullptr, format_desc, &timing, &sample_buffer); + CFRelease(format_desc); + + if (status != noErr || !sample_buffer) { + CVPixelBufferRelease(pixel_buffer); + BOOST_LOG(error) << backend_name << " dummy_img: failed to create sample buffer"sv; + return 1; + } + + auto ret = process_frame(sample_buffer, img) ? 0 : 1; + CFRelease(sample_buffer); + CVPixelBufferRelease(pixel_buffer); + + return ret; + } struct av_display_t: public display_t { AVVideo *av_capture {}; @@ -151,32 +257,152 @@ static void setPixelFormat(void *display, OSType pixelFormat) { } }; + struct sc_display_t: public display_t { + SCCapture *sc_capture {}; + CGDirectDisplayID display_id {}; + + ~sc_display_t() override { + [sc_capture stopCapture]; + [sc_capture release]; + } + + capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { + auto signal = [sc_capture captureVideo]; + if (!signal) { + BOOST_LOG(error) << "SCCapture failed to start video capture"sv; + return capture_e::error; + } + + auto frame_signal = sc_capture.frameSignal; + + while (true) { + auto frame_status = dispatch_semaphore_wait(frame_signal, dispatch_time(DISPATCH_TIME_NOW, SCKIT_SCREENSHOT_POLL_INTERVAL_NS)); + if (dispatch_semaphore_wait(signal, DISPATCH_TIME_NOW) == 0) { + break; + } + + CMSampleBufferRef sampleBuffer = nullptr; + if (frame_status == 0) { + sampleBuffer = [sc_capture copyLatestSampleBuffer]; + } else { + sampleBuffer = [sc_capture copyScreenshotSampleBuffer]; + } + + if (!sampleBuffer) { + std::shared_ptr probe_img; + if (!pull_free_image_cb(probe_img)) { + [sc_capture stopCapture]; + break; + } + continue; + } + + auto release_sample_buffer = util::fail_guard([sampleBuffer]() { + CFRelease(sampleBuffer); + }); + + std::shared_ptr img_out; + if (!pull_free_image_cb(img_out)) { + [sc_capture stopCapture]; + break; + } + + if (!process_frame(sampleBuffer, img_out.get())) { + continue; + } + + if (!push_captured_image_cb(std::move(img_out), true)) { + [sc_capture stopCapture]; + break; + } + } + + return capture_e::ok; + } + + std::shared_ptr alloc_img() override { + return std::make_shared(); + } + + std::unique_ptr make_avcodec_encode_device(pix_fmt_e pix_fmt) override { + if (pix_fmt == pix_fmt_e::yuv420p) { + sc_capture.pixelFormat = kCVPixelFormatType_32BGRA; + + return std::make_unique(); + } else if (pix_fmt == pix_fmt_e::nv12 || pix_fmt == pix_fmt_e::p010) { + auto device = std::make_unique(); + + device->init(static_cast(sc_capture), pix_fmt, setResolution, setPixelFormat); + + return device; + } else { + BOOST_LOG(error) << "Unsupported Pixel Format."sv; + return nullptr; + } + } + + int dummy_img(img_t *img) override { + if (!platf::is_screen_capture_allowed()) { + return 1; + } + + return make_dummy_img(img, sc_capture.frameWidth, sc_capture.frameHeight, sc_capture.pixelFormat, "SCCapture"sv); + } + + static void setResolution(void *display, int width, int height) { + [static_cast(display) setFrameWidth:width frameHeight:height]; + } + + static void setPixelFormat(void *display, OSType pixelFormat) { + static_cast(display).pixelFormat = pixelFormat; + } + }; + std::shared_ptr display(platf::mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { if (hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::videotoolbox) { BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv; return nullptr; } - auto display = std::make_shared(); - // Default to main display - display->display_id = CGMainDisplayID(); + auto display_id = CGMainDisplayID(); // Print all displays available with it's name and id auto display_array = [AVVideo displayNames]; BOOST_LOG(info) << "Detecting displays"sv; for (NSDictionary *item in display_array) { - NSNumber *display_id = item[@"id"]; + NSNumber *item_display_id = item[@"id"]; // We need show display's product name and corresponding display number given by user NSString *name = item[@"displayName"]; // We are using CGGetActiveDisplayList that only returns active displays so hardcoded connected value in log to true - BOOST_LOG(info) << "Detected display: "sv << name.UTF8String << " (id: "sv << [NSString stringWithFormat:@"%@", display_id].UTF8String << ") connected: true"sv; - if (!display_name.empty() && std::atoi(display_name.c_str()) == [display_id unsignedIntValue]) { - display->display_id = [display_id unsignedIntValue]; + BOOST_LOG(info) << "Detected display: "sv << name.UTF8String << " (id: "sv << [NSString stringWithFormat:@"%@", item_display_id].UTF8String << ") connected: true"sv; + if (!display_name.empty() && std::atoi(display_name.c_str()) == [item_display_id unsignedIntValue]) { + display_id = [item_display_id unsignedIntValue]; + } + } + BOOST_LOG(info) << "Configuring selected display ("sv << display_id << ") to stream"sv; + + if (@available(macOS 12.3, *)) { + if ([SCCapture isAvailable]) { + auto display = std::make_shared(); + display->display_id = display_id; + display->sc_capture = [[SCCapture alloc] initWithDisplay:display_id frameRate:config.framerate]; + + if (display->sc_capture) { + display->width = display->sc_capture.frameWidth; + display->height = display->sc_capture.frameHeight; + display->env_width = display->width; + display->env_height = display->height; + + return display; + } + + BOOST_LOG(error) << "SCCapture setup failed, trying AVFoundation..."sv; } } - BOOST_LOG(info) << "Configuring selected display ("sv << display->display_id << ") to stream"sv; + auto display = std::make_shared(); + display->display_id = display_id; display->av_capture = [[AVVideo alloc] initWithDisplay:display->display_id frameRate:config.framerate]; if (!display->av_capture) { diff --git a/src/platform/macos/nv12_zero_device.cpp b/src/platform/macos/nv12_zero_device.cpp index b4fb28cb736..b4d6aa33f47 100644 --- a/src/platform/macos/nv12_zero_device.cpp +++ b/src/platform/macos/nv12_zero_device.cpp @@ -29,6 +29,10 @@ namespace platf { int nv12_zero_device::convert(platf::img_t &img) { auto *av_img = (av_img_t *) &img; + if (!av_img->pixel_buffer || !av_img->pixel_buffer->buf) { + return -1; + } + // Release any existing CVPixelBuffer previously retained for encoding av_buffer_unref(&av_frame->buf[0]); diff --git a/src/platform/macos/sc_capture.h b/src/platform/macos/sc_capture.h new file mode 100644 index 00000000000..1c7f29860f7 --- /dev/null +++ b/src/platform/macos/sc_capture.h @@ -0,0 +1,47 @@ +/** + * @file src/platform/macos/sc_capture.h + * @brief Declarations for ScreenCaptureKit-based display capture on macOS. + */ +#pragma once + +#import +#import +#import +#import + +API_AVAILABLE(macos(12.3)) +@interface SCCapture: NSObject + +#define kMaxDisplays 32 + +@property (nonatomic, assign) CGDirectDisplayID displayID; +@property (nonatomic, assign) int frameRate; +@property (nonatomic, assign) OSType pixelFormat; +@property (nonatomic, assign) int frameWidth; +@property (nonatomic, assign) int frameHeight; + +@property (nonatomic, strong) SCStream *stream; +@property (nonatomic, strong) SCContentFilter *contentFilter; +@property (nonatomic, strong) SCStreamConfiguration *streamConfiguration; +@property (nonatomic, strong) SCShareableContent *shareableContent; +@property (nonatomic, strong) dispatch_queue_t videoQueue; + +@property (nonatomic, strong) dispatch_semaphore_t captureSignal; +@property (nonatomic, strong) dispatch_semaphore_t frameSignal; +@property (nonatomic, assign) BOOL stopping; +@property (nonatomic, assign) CMSampleBufferRef latestSampleBuffer; + ++ (BOOL)isAvailable; ++ (NSArray *)displayNames; ++ (NSString *)getDisplayName:(CGDirectDisplayID)displayID; + +- (instancetype)initWithDisplay:(CGDirectDisplayID)displayID + frameRate:(int)frameRate; + +- (void)setFrameWidth:(int)frameWidth frameHeight:(int)frameHeight; +- (dispatch_semaphore_t)captureVideo; +- (CMSampleBufferRef)copyLatestSampleBuffer; +- (CMSampleBufferRef)copyScreenshotSampleBuffer; +- (void)stopCapture; + +@end diff --git a/src/platform/macos/sc_capture.m b/src/platform/macos/sc_capture.m new file mode 100644 index 00000000000..9a4522efd90 --- /dev/null +++ b/src/platform/macos/sc_capture.m @@ -0,0 +1,388 @@ +/** + * @file src/platform/macos/sc_capture.m + * @brief ScreenCaptureKit-based display capture implementation. + */ +#import "sc_capture.h" + +API_AVAILABLE(macos(12.3)) +@implementation SCCapture + +static BOOL isCompleteScreenFrame(CMSampleBufferRef sampleBuffer) { + if (!CMSampleBufferIsValid(sampleBuffer)) { + return NO; + } + + CFArrayRef attachmentsArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, NO); + if (!attachmentsArray || CFArrayGetCount(attachmentsArray) == 0) { + return NO; + } + + CFDictionaryRef attachments = (CFDictionaryRef) CFArrayGetValueAtIndex(attachmentsArray, 0); + if (!attachments) { + return NO; + } + + NSNumber *status = (NSNumber *) CFDictionaryGetValue(attachments, SCStreamFrameInfoStatus); + if (!status) { + return NO; + } + + return status.integerValue == SCFrameStatusComplete; +} + +static BOOL isUsableImageSampleBuffer(CMSampleBufferRef sampleBuffer) { + return sampleBuffer && CMSampleBufferIsValid(sampleBuffer) && CMSampleBufferGetImageBuffer(sampleBuffer); +} + ++ (BOOL)isAvailable { + if (@available(macOS 12.3, *)) { + return YES; + } + return NO; +} + ++ (NSArray *)displayNames { + CGDirectDisplayID displays[kMaxDisplays]; + uint32_t count; + if (CGGetActiveDisplayList(kMaxDisplays, displays, &count) != kCGErrorSuccess) { + return [NSArray array]; + } + + NSMutableArray *result = [NSMutableArray array]; + + for (uint32_t i = 0; i < count; i++) { + [result addObject:@{ + @"id": [NSNumber numberWithUnsignedInt:displays[i]], + @"name": [NSString stringWithFormat:@"%d", displays[i]], + @"displayName": [self getDisplayName:displays[i]] ?: @"Unknown Display", + }]; + } + + return [NSArray arrayWithArray:result]; +} + ++ (NSString *)getDisplayName:(CGDirectDisplayID)displayID { + for (NSScreen *screen in [NSScreen screens]) { + if ([screen.deviceDescription[@"NSScreenNumber"] isEqualToNumber:[NSNumber numberWithUnsignedInt:displayID]]) { + return screen.localizedName; + } + } + return nil; +} + +- (instancetype)initWithDisplay:(CGDirectDisplayID)displayID + frameRate:(int)frameRate { + self = [super init]; + if (self) { + CGDisplayModeRef mode = CGDisplayCopyDisplayMode(displayID); + + self.displayID = displayID; + self.frameRate = frameRate; + self.pixelFormat = kCVPixelFormatType_32BGRA; + + if (mode) { + self.frameWidth = (int) CGDisplayModeGetPixelWidth(mode); + self.frameHeight = (int) CGDisplayModeGetPixelHeight(mode); + CFRelease(mode); + } else { + self.frameWidth = (int) CGDisplayPixelsWide(displayID); + self.frameHeight = (int) CGDisplayPixelsHigh(displayID); + } + + dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class( + DISPATCH_QUEUE_SERIAL, + QOS_CLASS_USER_INITIATED, + DISPATCH_QUEUE_PRIORITY_HIGH + ); + self.videoQueue = dispatch_queue_create("dev.lizardbyte.sunshine.sckVideoQueue", qos); + + dispatch_semaphore_t initSemaphore = dispatch_semaphore_create(0); + __block BOOL initSuccess = NO; + + [SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *content, NSError *error) { + if (error) { + NSLog(@"[SCCapture] Failed to get shareable content: %@", error.localizedDescription); + } else { + self.shareableContent = content; + initSuccess = YES; + } + dispatch_semaphore_signal(initSemaphore); + }]; + + dispatch_semaphore_wait(initSemaphore, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)); + + if (!initSuccess) { + return nil; + } + } + return self; +} + +- (void)dealloc { + [self stopCapture]; + [super dealloc]; +} + +- (void)setFrameWidth:(int)frameWidth frameHeight:(int)frameHeight { + self.frameWidth = frameWidth; + self.frameHeight = frameHeight; +} + +- (SCDisplay *)findDisplayWithID:(CGDirectDisplayID)displayID { + for (SCDisplay *display in self.shareableContent.displays) { + if (display.displayID == displayID) { + return display; + } + } + return nil; +} + +- (SCDisplay *)findDisplayWithIDRetrying:(CGDirectDisplayID)displayID { + SCDisplay *display = [self findDisplayWithID:displayID]; + if (display) { + return display; + } + + for (int attempt = 1; attempt <= 3; attempt++) { + NSLog(@"[SCCapture] Display %u not found in SCShareableContent, refreshing (attempt %d/3)", displayID, attempt); + [NSThread sleepForTimeInterval:1.0]; + + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + __block BOOL success = NO; + + [SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *content, NSError *error) { + if (!error && content) { + self.shareableContent = content; + success = YES; + } + dispatch_semaphore_signal(sem); + }]; + + dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)); + + if (success) { + display = [self findDisplayWithID:displayID]; + if (display) { + NSLog(@"[SCCapture] Found display %u after refresh", displayID); + return display; + } + } + } + + return nil; +} + +- (dispatch_semaphore_t)captureVideo { + @synchronized(self) { + if (self.stream) { + dispatch_semaphore_t stopSem = dispatch_semaphore_create(0); + [self.stream stopCaptureWithCompletionHandler:^(NSError *error) { + dispatch_semaphore_signal(stopSem); + }]; + dispatch_semaphore_wait(stopSem, dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC)); + self.stream = nil; + } + + if (self.latestSampleBuffer) { + CFRelease(self.latestSampleBuffer); + self.latestSampleBuffer = NULL; + } + + self.stopping = NO; + self.captureSignal = dispatch_semaphore_create(0); + self.frameSignal = dispatch_semaphore_create(0); + + SCDisplay *display = [self findDisplayWithIDRetrying:self.displayID]; + if (!display) { + NSLog(@"[SCCapture] Display not found after retries: %u", self.displayID); + return nil; + } + + SCContentFilter *filter = [[SCContentFilter alloc] initWithDisplay:display excludingWindows:@[]]; + self.contentFilter = filter; + [filter release]; + + SCStreamConfiguration *config = [[SCStreamConfiguration alloc] init]; + config.width = self.frameWidth; + config.height = self.frameHeight; + config.minimumFrameInterval = CMTimeMake(1, self.frameRate); + config.pixelFormat = self.pixelFormat; + config.queueDepth = 5; + config.showsCursor = YES; + self.streamConfiguration = config; + [config release]; + + NSError *error = nil; + SCStream *stream = [[SCStream alloc] initWithFilter:self.contentFilter configuration:self.streamConfiguration delegate:self]; + self.stream = stream; + [stream release]; + + if (!self.stream) { + NSLog(@"[SCCapture] Failed to create SCStream"); + return nil; + } + + if (![self.stream addStreamOutput:self type:SCStreamOutputTypeScreen sampleHandlerQueue:self.videoQueue error:&error]) { + NSLog(@"[SCCapture] Failed to add video output: %@", error.localizedDescription); + return nil; + } + + dispatch_semaphore_t startSemaphore = dispatch_semaphore_create(0); + __block BOOL startSuccess = NO; + + [self.stream startCaptureWithCompletionHandler:^(NSError *error) { + if (error) { + NSLog(@"[SCCapture] Failed to start capture: %@", error.localizedDescription); + } else { + NSLog(@"[SCCapture] Capture started successfully"); + startSuccess = YES; + } + dispatch_semaphore_signal(startSemaphore); + }]; + + dispatch_semaphore_wait(startSemaphore, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)); + + if (!startSuccess) { + self.captureSignal = nil; + self.frameSignal = nil; + self.contentFilter = nil; + self.streamConfiguration = nil; + return nil; + } + + return self.captureSignal; + } +} + +- (CMSampleBufferRef)copyLatestSampleBuffer { + @synchronized(self) { + CMSampleBufferRef sampleBuffer = self.latestSampleBuffer; + self.latestSampleBuffer = NULL; + return sampleBuffer; + } +} + +- (CMSampleBufferRef)copyScreenshotSampleBuffer { + if (@available(macOS 14.0, *)) { + SCContentFilter *filter = nil; + SCStreamConfiguration *config = nil; + + @synchronized(self) { + if (self.stopping || !self.contentFilter || !self.streamConfiguration) { + return NULL; + } + + filter = [self.contentFilter retain]; + config = [self.streamConfiguration retain]; + } + + dispatch_semaphore_t screenshotSemaphore = dispatch_semaphore_create(0); + __block BOOL timedOut = NO; + __block CMSampleBufferRef screenshotSampleBuffer = NULL; + + [SCScreenshotManager captureSampleBufferWithFilter:filter + configuration:config + completionHandler:^(CMSampleBufferRef sampleBuffer, NSError *error) { + if (!timedOut && !error && isUsableImageSampleBuffer(sampleBuffer)) { + screenshotSampleBuffer = (CMSampleBufferRef) CFRetain(sampleBuffer); + } + + dispatch_semaphore_signal(screenshotSemaphore); + }]; + + if (dispatch_semaphore_wait(screenshotSemaphore, dispatch_time(DISPATCH_TIME_NOW, 500 * NSEC_PER_MSEC)) != 0) { + timedOut = YES; + } + + [filter release]; + [config release]; + + return screenshotSampleBuffer; + } + + return NULL; +} + +- (void)stopCapture { + @synchronized(self) { + self.stopping = YES; + + if (self.stream) { + dispatch_semaphore_t stopSemaphore = dispatch_semaphore_create(0); + + [self.stream stopCaptureWithCompletionHandler:^(NSError *error) { + if (error) { + NSLog(@"[SCCapture] Error stopping capture: %@", error.localizedDescription); + } + dispatch_semaphore_signal(stopSemaphore); + }]; + + dispatch_semaphore_wait(stopSemaphore, dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC)); + self.stream = nil; + } + + self.contentFilter = nil; + self.streamConfiguration = nil; + + if (self.latestSampleBuffer) { + CFRelease(self.latestSampleBuffer); + self.latestSampleBuffer = NULL; + } + + if (self.frameSignal) { + dispatch_semaphore_signal(self.frameSignal); + } + + if (self.captureSignal) { + dispatch_semaphore_signal(self.captureSignal); + } + } +} + +#pragma mark - SCStreamDelegate + +- (void)stream:(SCStream *)stream didStopWithError:(NSError *)error { + NSLog(@"[SCCapture] Stream stopped with error: %@", error.localizedDescription); + if (self.frameSignal) { + dispatch_semaphore_signal(self.frameSignal); + } + if (self.captureSignal) { + dispatch_semaphore_signal(self.captureSignal); + } +} + +#pragma mark - SCStreamOutput + +- (void)stream:(SCStream *)stream + didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer + ofType:(SCStreamOutputType)type { + if (type != SCStreamOutputTypeScreen) { + return; + } + + if (!isCompleteScreenFrame(sampleBuffer)) { + return; + } + + if (!isUsableImageSampleBuffer(sampleBuffer)) { + return; + } + + @synchronized(self) { + if (self.stopping) { + return; + } + + BOOL shouldSignal = self.latestSampleBuffer == NULL; + if (self.latestSampleBuffer) { + CFRelease(self.latestSampleBuffer); + } + self.latestSampleBuffer = (CMSampleBufferRef) CFRetain(sampleBuffer); + + if (shouldSignal && self.frameSignal) { + dispatch_semaphore_signal(self.frameSignal); + } + } +} + +@end