Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions cmake/compile_definitions/macos.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -55,6 +56,16 @@ 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_video.h"
"${CMAKE_SOURCE_DIR}/src/platform/macos/sc_video.m"
"${CMAKE_SOURCE_DIR}/third-party/TPCircularBuffer/TPCircularBuffer.c"
"${CMAKE_SOURCE_DIR}/third-party/TPCircularBuffer/TPCircularBuffer.h"
${APPLE_PLIST_FILE})

# sc_video.m is written against ARC for clarity (SCK APIs are async/
# block-heavy and benefit from ARC). The rest of the macOS Obj-C
# sources remain MRC; objects flowing across the boundary follow the
# standard +1-retain alloc/init convention so both modes interoperate.
set_source_files_properties(
"${CMAKE_SOURCE_DIR}/src/platform/macos/sc_video.m"
PROPERTIES COMPILE_FLAGS "-fobjc-arc")
8 changes: 8 additions & 0 deletions cmake/dependencies/macos.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ FIND_LIBRARY(CORE_MEDIA_LIBRARY CoreMedia)
FIND_LIBRARY(CORE_VIDEO_LIBRARY CoreVideo)
FIND_LIBRARY(FOUNDATION_LIBRARY Foundation)
FIND_LIBRARY(VIDEO_TOOLBOX_LIBRARY VideoToolbox)
# ScreenCaptureKit is the modern (macOS 12.3+) replacement for the
# deprecated AVCaptureScreenInput-based capture path. Sunshine's
# sc_video.{h,m} is unconditionally compiled into the macOS target;
# fail configure with a clear message rather than failing the build
# later on header lookup when the SDK doesn't ship the framework
# (e.g., when building with an Xcode older than 13.3 / SDK older than
# 12.3, which dropped out of routine compatibility long ago).
FIND_LIBRARY(SCREEN_CAPTURE_KIT_LIBRARY ScreenCaptureKit REQUIRED)

if(SUNSHINE_ENABLE_TRAY)
FIND_LIBRARY(COCOA Cocoa REQUIRED)
Expand Down
25 changes: 23 additions & 2 deletions src/platform/macos/av_video.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,36 @@ struct CaptureSession {

static const int kMaxDisplays = 32;

@interface AVVideo: NSObject <AVCaptureVideoDataOutputSampleBufferDelegate>
typedef bool (^FrameCallbackBlock)(CMSampleBufferRef);

/**
* @brief Shared interface for macOS screen capture backends.
*
* Both the legacy AVCaptureScreenInput-based implementation (AVVideo) and
* the modern ScreenCaptureKit-based implementation (SCVideo) conform to
* this protocol so display.mm can hold either behind a single pointer
* type and branch on macOS version at construction.
*/
@protocol SunshineVideoCapture <NSObject>

@property (nonatomic, assign) CGDirectDisplayID displayID;
@property (nonatomic, assign) CMTime minFrameDuration;
@property (nonatomic, assign) OSType pixelFormat;
@property (nonatomic, assign) int frameWidth;
@property (nonatomic, assign) int frameHeight;

typedef bool (^FrameCallbackBlock)(CMSampleBufferRef);
- (void)setFrameWidth:(int)frameWidth frameHeight:(int)frameHeight;
- (dispatch_semaphore_t)capture:(FrameCallbackBlock)frameCallback;

@end

@interface AVVideo: NSObject <AVCaptureVideoDataOutputSampleBufferDelegate, SunshineVideoCapture>

@property (nonatomic, assign) CGDirectDisplayID displayID;
@property (nonatomic, assign) CMTime minFrameDuration;
@property (nonatomic, assign) OSType pixelFormat;
@property (nonatomic, assign) int frameWidth;
@property (nonatomic, assign) int frameHeight;

@property (nonatomic, assign) AVCaptureSession *session;
@property (nonatomic, assign) NSMapTable<AVCaptureConnection *, AVCaptureVideoDataOutput *> *videoOutputs;
Expand Down
26 changes: 21 additions & 5 deletions src/platform/macos/display.mm
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "src/platform/macos/av_video.h"
#include "src/platform/macos/misc.h"
#include "src/platform/macos/nv12_zero_device.h"
#include "src/platform/macos/sc_video.h"

// Avoid conflict between AVFoundation and libavutil both defining AVMediaType
#define AVMediaType AVMediaType_FFmpeg
Expand All @@ -22,7 +23,7 @@
using namespace std::literals;

struct av_display_t: public display_t {
AVVideo *av_capture {};
id<SunshineVideoCapture> av_capture {};
CGDirectDisplayID display_id {};

~av_display_t() override {
Expand Down Expand Up @@ -86,7 +87,7 @@ capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const
} else if (pix_fmt == pix_fmt_e::nv12 || pix_fmt == pix_fmt_e::p010) {
auto device = std::make_unique<nv12_zero_device>();

device->init(static_cast<void *>(av_capture), pix_fmt, setResolution, setPixelFormat);
device->init((void *) av_capture, pix_fmt, setResolution, setPixelFormat);

return device;
} else {
Expand Down Expand Up @@ -143,11 +144,11 @@ int dummy_img(img_t *img) override {
* height --> the intended capture height
*/
static void setResolution(void *display, int width, int height) {
[static_cast<AVVideo *>(display) setFrameWidth:width frameHeight:height];
[(id<SunshineVideoCapture>) display setFrameWidth:width frameHeight:height];
}

static void setPixelFormat(void *display, OSType pixelFormat) {
static_cast<AVVideo *>(display).pixelFormat = pixelFormat;
((id<SunshineVideoCapture>) display).pixelFormat = pixelFormat;
}
};

Expand Down Expand Up @@ -177,7 +178,22 @@ static void setPixelFormat(void *display, OSType pixelFormat) {
}
BOOST_LOG(info) << "Configuring selected display ("sv << display->display_id << ") to stream"sv;

display->av_capture = [[AVVideo alloc] initWithDisplay:display->display_id frameRate:config.framerate];
// Prefer ScreenCaptureKit on macOS 12.3+ (AVCaptureScreenInput was
// deprecated in macOS 13 and is hardcoded to 8-bit BGRA). Fall back to
// the legacy AVCaptureScreenInput path on older macOS.
if (@available(macOS 12.3, *)) {
// hdrAllowed reflects the negotiated `enable_hdr` for this session
// (rtsp.cpp maps `x-nv-video[0].dynamicRangeMode` into config.dynamicRange).
// SCK uses this together with the chosen pixel format depth to decide
// whether to flip captureDynamicRange to HDRLocalDisplay; neither
// condition alone is sufficient. See sc_video.m::applyDynamicRangeForPixelFormat:.
const BOOL hdr_allowed = config.dynamicRange ? YES : NO;
BOOST_LOG(info) << "Using ScreenCaptureKit capture backend (HDR "sv << (hdr_allowed ? "allowed" : "blocked") << ")"sv;
display->av_capture = [[SCVideo alloc] initWithDisplay:display->display_id frameRate:config.framerate hdrAllowed:hdr_allowed];
} else {
BOOST_LOG(info) << "Using legacy AVCaptureScreenInput capture backend"sv;
display->av_capture = [[AVVideo alloc] initWithDisplay:display->display_id frameRate:config.framerate];
}

if (!display->av_capture) {
BOOST_LOG(error) << "Video setup failed."sv;
Expand Down
37 changes: 37 additions & 0 deletions src/platform/macos/sc_video.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* @file src/platform/macos/sc_video.h
* @brief Declarations for ScreenCaptureKit-based video capture on macOS.
*
* Modern replacement for AVCaptureScreenInput (which was deprecated in
* macOS 13). SCVideo conforms to the same SunshineVideoCapture protocol
* as the legacy AVVideo class so callers can swap implementations at
* runtime based on @available(macOS 12.3, *) without other code changes.
*/
#pragma once

#import "av_video.h"

#import <AppKit/AppKit.h>

API_AVAILABLE(macos(12.3))
@interface SCVideo: NSObject <SunshineVideoCapture>

@property (nonatomic, assign) CGDirectDisplayID displayID;
@property (nonatomic, assign) CMTime minFrameDuration;
@property (nonatomic, assign) OSType pixelFormat;
@property (nonatomic, assign) int frameWidth;
@property (nonatomic, assign) int frameHeight;

// YES iff the negotiated streaming session enabled HDR (Moonlight's
// hdrMode flag). Required (in combination with a 10-bit pixel format)
// before SCK is allowed to flip captureDynamicRange to HDRLocalDisplay
// on macOS 14+. Defaults to NO; the SDR capture path is always safe.
@property (nonatomic, assign) BOOL hdrAllowed;

- (instancetype)initWithDisplay:(CGDirectDisplayID)displayID frameRate:(int)frameRate;
- (instancetype)initWithDisplay:(CGDirectDisplayID)displayID frameRate:(int)frameRate hdrAllowed:(BOOL)hdrAllowed;

- (void)setFrameWidth:(int)frameWidth frameHeight:(int)frameHeight;
- (dispatch_semaphore_t)capture:(FrameCallbackBlock)frameCallback;

@end
Loading