Is there an existing issue for this?
Is your issue described in the documentation?
Is your issue present in the latest beta/pre-release?
Present in current master (cf52f4b6), which I built from source to investigate.
Describe the Bug
On macOS, putting the display to sleep while a stream is running stalls capture
permanently. The client stays connected but never receives another frame, and waking the
display does not bring it back. Only quitting and relaunching Sunshine restores streaming.
To reproduce:
- Start a stream
- Put the display to sleep — a hot corner set to Put Display to Sleep does it instantly,
but idle sleep or closing the lid gets there too
- Wake the display
- The stream never resumes. Reconnecting does not help.
Cause
src/platform/macos/display.mm, in capture():
// FIXME: We should time out if an image isn't returned for a while
dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
return capture_e::ok;
That semaphore is not "a frame arrived" — -[AVVideo captureOutput:didOutputSampleBuffer:fromConnection:]
only signals it when the frame callback returns false, i.e. when capture is deliberately
stopped. So the wait is for the entire capture session to end.
A sleeping display stops AVCaptureSession delivering sample buffers, and the session does
not resume when the display wakes. The capture thread is therefore parked for the lifetime
of the process, with nothing able to wake it.
Two more things turn that into a dead end rather than a hiccup:
capture() can only ever return capture_e::ok — it has no path to error or reinit.
- Nothing listens for display sleep/wake or
CGDisplay reconfiguration, so the session is
never rebuilt when the display comes back.
And in src/video.cpp, the capture thread rebuilds the display on exactly one value:
case platf::capture_e::reinit: {
// ... refresh_displays / reset_display ...
}
case platf::capture_e::error:
case platf::capture_e::ok:
case platf::capture_e::timeout:
case platf::capture_e::interrupted:
return; // ends the session
So the one value that would trigger recovery is the one the macOS backend never sends.
dummy_img() has the same unbounded wait, and there the semaphore genuinely does mean "one
frame arrived" — so encoder probing hangs indefinitely if the display happens to be asleep
at startup.
Expected Behavior
When the display wakes, capture is rebuilt and the stream resumes on its own, without
restarting Sunshine.
Additional Context
I have a patch for this and it is verified on hardware — PR to follow, and I'll link it
here.
Two details from writing it that may be useful to anyone else looking at this:
- A frame-arrival timeout is the wrong shape.
AVCaptureScreenInput is change-driven,
so a static desktop looks exactly like a stalled one and would be reinitialized
needlessly. Waiting for a sleep-then-wake transition cannot produce that false positive.
- Stopping the session before returning is not enough, and crashes. The output stays in
the capture session and in AVVideo's three map tables, and -[AVVideo dealloc] releases
those map tables before it stops the session — so the output is released while the
session still holds it, and the process aborts in objc_msgSend during the next
reinitialization. Abandoning a capture needs the same teardown the frame callback performs
when it returns false.
With the fix, the log during recovery looks like this, and the client session continues
uninterrupted:
[11:31:42.133]: Info: Display [1] woke from sleep, reinitializing capture
[11:31:42.219]: Info: Configuring selected display (1) to stream
[11:31:42.229]: Info: Creating encoder [hevc_videotoolbox]
Host Operating System
macOS
Operating System Version
26.6.1 (25G76)
Architecture
arm64
Package
macOS installer (Sunshine.app), and a source build of master for the investigation
GPU Type
Integrated (Apple silicon)
GPU Model
Apple M1 Max
GPU Driver/Mesa Version
n/a — macOS 26.6.1, VideoToolbox
Capture Method
AVFoundation (av_display_t, the macOS default)
Is there an existing issue for this?
Is your issue described in the documentation?
Is your issue present in the latest beta/pre-release?
Present in current
master(cf52f4b6), which I built from source to investigate.Describe the Bug
On macOS, putting the display to sleep while a stream is running stalls capture
permanently. The client stays connected but never receives another frame, and waking the
display does not bring it back. Only quitting and relaunching Sunshine restores streaming.
To reproduce:
but idle sleep or closing the lid gets there too
Cause
src/platform/macos/display.mm, incapture():That semaphore is not "a frame arrived" —
-[AVVideo captureOutput:didOutputSampleBuffer:fromConnection:]only signals it when the frame callback returns
false, i.e. when capture is deliberatelystopped. So the wait is for the entire capture session to end.
A sleeping display stops
AVCaptureSessiondelivering sample buffers, and the session doesnot resume when the display wakes. The capture thread is therefore parked for the lifetime
of the process, with nothing able to wake it.
Two more things turn that into a dead end rather than a hiccup:
capture()can only ever returncapture_e::ok— it has no path toerrororreinit.CGDisplayreconfiguration, so the session isnever rebuilt when the display comes back.
And in
src/video.cpp, the capture thread rebuilds the display on exactly one value:So the one value that would trigger recovery is the one the macOS backend never sends.
dummy_img()has the same unbounded wait, and there the semaphore genuinely does mean "oneframe arrived" — so encoder probing hangs indefinitely if the display happens to be asleep
at startup.
Expected Behavior
When the display wakes, capture is rebuilt and the stream resumes on its own, without
restarting Sunshine.
Additional Context
I have a patch for this and it is verified on hardware — PR to follow, and I'll link it
here.
Two details from writing it that may be useful to anyone else looking at this:
AVCaptureScreenInputis change-driven,so a static desktop looks exactly like a stalled one and would be reinitialized
needlessly. Waiting for a sleep-then-wake transition cannot produce that false positive.
the capture session and in
AVVideo's three map tables, and-[AVVideo dealloc]releasesthose map tables before it stops the session — so the output is released while the
session still holds it, and the process aborts in
objc_msgSendduring the nextreinitialization. Abandoning a capture needs the same teardown the frame callback performs
when it returns
false.With the fix, the log during recovery looks like this, and the client session continues
uninterrupted:
Host Operating System
macOS
Operating System Version
26.6.1 (25G76)
Architecture
arm64
Package
macOS installer (
Sunshine.app), and a source build ofmasterfor the investigationGPU Type
Integrated (Apple silicon)
GPU Model
Apple M1 Max
GPU Driver/Mesa Version
n/a — macOS 26.6.1, VideoToolbox
Capture Method
AVFoundation (
av_display_t, the macOS default)