Skip to content

Commit 670d019

Browse files
committed
fix(macos): recover capture from sleeping displays
Wake the selected display before encoder probing, hold a display-sleep assertion for the capture lifetime, and bound first-frame waits so a sleeping virtual display cannot hang launch indefinitely. Based on the macOS power assertion approach from upstream Sunshine PR LizardByte#5291 (c9863eb).
1 parent 132a728 commit 670d019

5 files changed

Lines changed: 241 additions & 31 deletions

File tree

‎cmake/compile_definitions/macos.cmake‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
3535
${CORE_MEDIA_LIBRARY}
3636
${CORE_VIDEO_LIBRARY}
3737
${FOUNDATION_LIBRARY}
38+
${IOKIT_LIBRARY}
3839
${SCREEN_CAPTURE_KIT_LIBRARY}
3940
${VIDEO_TOOLBOX_LIBRARY})
4041

‎cmake/dependencies/macos.cmake‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ FIND_LIBRARY(CORE_AUDIO_LIBRARY CoreAudio)
99
FIND_LIBRARY(CORE_MEDIA_LIBRARY CoreMedia)
1010
FIND_LIBRARY(CORE_VIDEO_LIBRARY CoreVideo)
1111
FIND_LIBRARY(FOUNDATION_LIBRARY Foundation)
12+
FIND_LIBRARY(IOKIT_LIBRARY IOKit)
1213
FIND_LIBRARY(SCREEN_CAPTURE_KIT_LIBRARY ScreenCaptureKit)
1314
FIND_LIBRARY(VIDEO_TOOLBOX_LIBRARY VideoToolbox)
1415

‎src/platform/macos/av_video.m‎

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,15 @@ - (void)dealloc {
146146
self.screenFrameCallback = nil;
147147
[self.screenStream release];
148148
[self.screenContentFilter release];
149+
150+
for (AVCaptureVideoDataOutput *videoOutput in [self.videoOutputs objectEnumerator]) {
151+
[videoOutput setSampleBufferDelegate:nil queue:nil];
152+
}
153+
[self.session stopRunning];
154+
149155
[self.videoOutputs release];
150156
[self.captureCallbacks release];
151157
[self.captureSignals release];
152-
[self.session stopRunning];
153158
[super dealloc];
154159
}
155160

@@ -336,32 +341,76 @@ - (dispatch_semaphore_t)capture:(FrameCallbackBlock)frameCallback {
336341
return self.hdrCaptureEnabled ? [self captureHDR:frameCallback] : [self captureAVFoundation:frameCallback];
337342
}
338343

344+
- (void)finishAVFoundationCaptureConnection:(AVCaptureConnection *)connection failed:(BOOL)failed {
345+
// Removing an output can release the last owner of this delegate. Keep self
346+
// alive until all state is detached and the waiting C++ thread is signalled.
347+
[self retain];
348+
349+
dispatch_semaphore_t signal = nil;
350+
@synchronized(self) {
351+
self.captureFailed = self.captureFailed || failed;
352+
353+
AVCaptureVideoDataOutput *videoOutput = [self.videoOutputs objectForKey:connection];
354+
signal = [self.captureSignals objectForKey:connection];
355+
356+
if (videoOutput) {
357+
[self.session stopRunning];
358+
[videoOutput setSampleBufferDelegate:nil queue:nil];
359+
}
360+
361+
[self.captureCallbacks removeObjectForKey:connection];
362+
if (videoOutput) {
363+
[self.session removeOutput:videoOutput];
364+
}
365+
[self.videoOutputs removeObjectForKey:connection];
366+
[self.captureSignals removeObjectForKey:connection];
367+
368+
if (videoOutput && !self.captureStopRequested) {
369+
[self.session startRunning];
370+
}
371+
}
372+
373+
if (signal) {
374+
dispatch_semaphore_signal(signal);
375+
}
376+
377+
[self release];
378+
}
379+
339380
- (void)stopCapture {
381+
NSArray<AVCaptureConnection *> *connections = nil;
340382
@synchronized(self) {
341383
self.captureStopRequested = YES;
384+
if (!self.hdrCaptureEnabled) {
385+
connections = [[[self.captureCallbacks keyEnumerator] allObjects] retain];
386+
}
342387
}
343388

344389
if (self.hdrCaptureEnabled) {
345390
[self finishScreenCaptureStoppingStream:YES failed:NO error:nil];
391+
return;
392+
}
393+
394+
for (AVCaptureConnection *connection in connections) {
395+
[self finishAVFoundationCaptureConnection:connection failed:NO];
346396
}
397+
[connections release];
347398
}
348399

349400
- (void)captureOutput:(AVCaptureOutput *)captureOutput
350401
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
351402
fromConnection:(AVCaptureConnection *)connection {
352-
FrameCallbackBlock callback = [self.captureCallbacks objectForKey:connection];
403+
FrameCallbackBlock callback = nil;
404+
@synchronized(self) {
405+
callback = [[self.captureCallbacks objectForKey:connection] copy];
406+
}
353407

354408
if (callback != nil) {
355-
if (!callback(sampleBuffer)) {
356-
@synchronized(self) {
357-
[self.session stopRunning];
358-
[self.captureCallbacks removeObjectForKey:connection];
359-
[self.session removeOutput:[self.videoOutputs objectForKey:connection]];
360-
[self.videoOutputs removeObjectForKey:connection];
361-
dispatch_semaphore_signal([self.captureSignals objectForKey:connection]);
362-
[self.captureSignals removeObjectForKey:connection];
363-
[self.session startRunning];
364-
}
409+
const BOOL keepCapturing = callback(sampleBuffer);
410+
[callback release];
411+
412+
if (!keepCapturing) {
413+
[self finishAVFoundationCaptureConnection:connection failed:NO];
365414
}
366415
}
367416
}

‎src/platform/macos/display.mm‎

Lines changed: 151 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@
33
* @brief Definitions for display capture on macOS.
44
*/
55
// standard includes
6+
#include <charconv>
67
#include <cstring>
78
#include <mutex>
9+
#include <thread>
10+
11+
// platform includes
12+
#include <IOKit/pwr_mgt/IOPMLib.h>
813

914
// local includes
1015
#include "src/config.h"
@@ -25,6 +30,75 @@
2530
namespace platf {
2631
using namespace std::literals;
2732

33+
namespace {
34+
bool display_selector_is_active(std::string_view selector) {
35+
CGDirectDisplayID active_displays[kMaxDisplays] {};
36+
uint32_t display_count {};
37+
if (CGGetActiveDisplayList(kMaxDisplays, active_displays, &display_count) != kCGErrorSuccess) {
38+
return false;
39+
}
40+
41+
if (selector.empty()) {
42+
return display_count > 0;
43+
}
44+
45+
CGDirectDisplayID numeric_display_id {};
46+
const auto numeric_result = std::from_chars(
47+
selector.data(),
48+
selector.data() + selector.size(),
49+
numeric_display_id
50+
);
51+
const bool is_numeric_selector =
52+
numeric_result.ec == std::errc {} && numeric_result.ptr == selector.data() + selector.size();
53+
54+
for (uint32_t index = 0; index < display_count; ++index) {
55+
const auto display_id = active_displays[index];
56+
if ((is_numeric_selector && display_id == numeric_display_id) ||
57+
(!is_numeric_selector && selector == display_uuid_selector(display_id))) {
58+
return true;
59+
}
60+
}
61+
62+
return false;
63+
}
64+
65+
void wake_display_for_capture(std::string_view selector) {
66+
if (display_selector_is_active(selector)) {
67+
return;
68+
}
69+
70+
IOPMAssertionID wake_assertion = kIOPMNullAssertionID;
71+
const auto result = IOPMAssertionDeclareUserActivity(
72+
CFSTR("Sunshine display detection"),
73+
kIOPMUserActiveRemote,
74+
&wake_assertion
75+
);
76+
77+
if (result != kIOReturnSuccess) {
78+
BOOST_LOG(warning) << "Unable to declare remote user activity to wake displays, IOReturn: "sv << result;
79+
return;
80+
}
81+
82+
BOOST_LOG(info) << "Declared remote user activity to wake displays"sv;
83+
84+
for (int attempt = 0; attempt < 30 && !display_selector_is_active(selector); ++attempt) {
85+
std::this_thread::sleep_for(100ms);
86+
}
87+
88+
if (!display_selector_is_active(selector)) {
89+
BOOST_LOG(warning) << "Display wake attempt did not expose the requested display ["sv
90+
<< selector << "] in the active display list."sv;
91+
}
92+
93+
if (wake_assertion != kIOPMNullAssertionID) {
94+
const auto release_result = IOPMAssertionRelease(wake_assertion);
95+
if (release_result != kIOReturnSuccess) {
96+
BOOST_LOG(warning) << "Unable to release display wake assertion, IOReturn: "sv << release_result;
97+
}
98+
}
99+
}
100+
} // namespace
101+
28102
struct capture_callback_context_t {
29103
capture_callback_context_t(
30104
const display_t::push_captured_image_cb_t &push_callback,
@@ -40,12 +114,51 @@
40114
display_t::pull_free_image_cb_t pull_callback;
41115
};
42116

117+
struct dummy_capture_context_t {
118+
explicit dummy_capture_context_t(img_t *image):
119+
image {image} {
120+
}
121+
122+
std::mutex mutex;
123+
bool active {true};
124+
img_t *image;
125+
};
126+
43127
struct av_display_t: public display_t {
44128
AVVideo *av_capture {};
45129
CGDirectDisplayID display_id {};
130+
IOPMAssertionID display_sleep_assertion {kIOPMNullAssertionID};
46131

47132
~av_display_t() override {
48133
[av_capture release];
134+
135+
if (display_sleep_assertion != kIOPMNullAssertionID) {
136+
const auto result = IOPMAssertionRelease(display_sleep_assertion);
137+
if (result != kIOReturnSuccess) {
138+
BOOST_LOG(warning) << "Unable to release display sleep assertion, IOReturn: "sv << result;
139+
}
140+
}
141+
}
142+
143+
void prevent_display_sleep() {
144+
if (display_sleep_assertion != kIOPMNullAssertionID) {
145+
return;
146+
}
147+
148+
const auto result = IOPMAssertionCreateWithName(
149+
kIOPMAssertPreventUserIdleDisplaySleep,
150+
kIOPMAssertionLevelOn,
151+
CFSTR("Sunshine display capture"),
152+
&display_sleep_assertion
153+
);
154+
155+
if (result == kIOReturnSuccess) {
156+
BOOST_LOG(debug) << "Keeping display awake for capture"sv;
157+
return;
158+
}
159+
160+
display_sleep_assertion = kIOPMNullAssertionID;
161+
BOOST_LOG(warning) << "Unable to create display sleep prevention assertion, IOReturn: "sv << result;
49162
}
50163

51164
void stop_capture() override {
@@ -151,26 +264,33 @@ int dummy_img(img_t *img) override {
151264
return 1;
152265
}
153266

267+
auto capture_context = std::make_shared<dummy_capture_context_t>(img);
154268
auto signal = [av_capture capture:^(CMSampleBufferRef sampleBuffer) {
269+
std::lock_guard<std::mutex> lock {capture_context->mutex};
270+
if (!capture_context->active || !capture_context->image) {
271+
return false;
272+
}
273+
155274
auto new_sample_buffer = std::make_shared<av_sample_buf_t>(sampleBuffer);
156275
auto new_pixel_buffer = std::make_shared<av_pixel_buf_t>(new_sample_buffer->buf);
157276

158-
auto av_img = (av_img_t *) img;
277+
auto image = capture_context->image;
278+
auto av_img = (av_img_t *) image;
159279

160280
auto old_data_retainer = std::make_shared<temp_retain_av_img_t>(
161281
av_img->sample_buffer,
162282
av_img->pixel_buffer,
163-
img->data
283+
image->data
164284
);
165285

166286
av_img->sample_buffer = new_sample_buffer;
167287
av_img->pixel_buffer = new_pixel_buffer;
168-
img->data = new_pixel_buffer->data();
288+
image->data = new_pixel_buffer->data();
169289

170-
img->width = (int) CVPixelBufferGetWidth(new_pixel_buffer->buf);
171-
img->height = (int) CVPixelBufferGetHeight(new_pixel_buffer->buf);
172-
img->row_pitch = (int) CVPixelBufferGetBytesPerRow(new_pixel_buffer->buf);
173-
img->pixel_pitch = img->row_pitch / img->width;
290+
image->width = (int) CVPixelBufferGetWidth(new_pixel_buffer->buf);
291+
image->height = (int) CVPixelBufferGetHeight(new_pixel_buffer->buf);
292+
image->row_pitch = (int) CVPixelBufferGetBytesPerRow(new_pixel_buffer->buf);
293+
image->pixel_pitch = image->row_pitch / image->width;
174294

175295
old_data_retainer = nullptr;
176296

@@ -182,7 +302,24 @@ int dummy_img(img_t *img) override {
182302
return 1;
183303
}
184304

185-
dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
305+
constexpr auto first_frame_timeout = 5s;
306+
const auto timeout = dispatch_time(
307+
DISPATCH_TIME_NOW,
308+
std::chrono::duration_cast<std::chrono::nanoseconds>(first_frame_timeout).count()
309+
);
310+
const bool timed_out = dispatch_semaphore_wait(signal, timeout) != 0;
311+
312+
{
313+
std::lock_guard<std::mutex> lock {capture_context->mutex};
314+
capture_context->active = false;
315+
capture_context->image = nullptr;
316+
}
317+
318+
if (timed_out) {
319+
BOOST_LOG(error) << "Timed out waiting for the first frame from macOS display capture"sv;
320+
[av_capture stopCapture];
321+
return 1;
322+
}
186323

187324
return av_capture.captureFailed ? 1 : 0;
188325
}
@@ -242,6 +379,12 @@ bool get_hdr_metadata(SS_HDR_METADATA &metadata) override {
242379

243380
auto display = std::make_shared<av_display_t>();
244381

382+
// AVFoundation and ScreenCaptureKit do not deliver frames for sleeping
383+
// displays. Hold the display awake for this capture object's lifetime and
384+
// wake it before resolving the configured stable selector.
385+
display->prevent_display_sleep();
386+
wake_display_for_capture(display_name);
387+
245388
// Print all displays available with it's name and id
246389
auto display_array = [AVVideo displayNames];
247390
BOOST_LOG(info) << "Detecting displays"sv;

‎src/platform/macos/misc.mm‎

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <charconv>
1313
#include <fcntl.h>
1414
#include <ifaddrs.h>
15+
#include <optional>
1516

1617
// platform includes
1718
#include <arpa/inet.h>
@@ -95,19 +96,34 @@ CGDirectDisplayID display_id_from_selector(std::string_view selector, CGDirectDi
9596
const bool is_numeric_selector =
9697
numeric_result.ec == std::errc {} && numeric_result.ptr == selector.data() + selector.size();
9798

98-
constexpr uint32_t max_displays = 32;
99-
CGDirectDisplayID active_displays[max_displays] {};
100-
uint32_t display_count {};
101-
if (CGGetActiveDisplayList(max_displays, active_displays, &display_count) != kCGErrorSuccess) {
102-
return fallback;
103-
}
99+
auto find_matching_display = [&](auto get_display_list) -> std::optional<CGDirectDisplayID> {
100+
constexpr uint32_t max_displays = 32;
101+
CGDirectDisplayID displays[max_displays] {};
102+
uint32_t display_count {};
103+
if (get_display_list(max_displays, displays, &display_count) != kCGErrorSuccess) {
104+
return std::nullopt;
105+
}
104106

105-
for (uint32_t index = 0; index < display_count; ++index) {
106-
const auto display_id = active_displays[index];
107-
if ((is_numeric_selector && display_id == numeric_display_id) ||
108-
(!is_numeric_selector && selector == display_uuid_selector(display_id))) {
109-
return display_id;
107+
for (uint32_t index = 0; index < display_count; ++index) {
108+
const auto display_id = displays[index];
109+
if ((is_numeric_selector && display_id == numeric_display_id) ||
110+
(!is_numeric_selector && selector == display_uuid_selector(display_id))) {
111+
return display_id;
112+
}
110113
}
114+
115+
return std::nullopt;
116+
};
117+
118+
if (const auto active_display = find_matching_display(CGGetActiveDisplayList)) {
119+
return *active_display;
120+
}
121+
122+
// Sleeping virtual displays remain online but disappear from the active
123+
// list. Resolve them here so stable UUID selectors do not fall back to an
124+
// unrelated main display while the wake request is still completing.
125+
if (const auto online_display = find_matching_display(CGGetOnlineDisplayList)) {
126+
return *online_display;
111127
}
112128

113129
return fallback;

0 commit comments

Comments
 (0)