LizardByte / LizardByte/Sunshine
macOS: display sleep permanently stalls capture and the stream cannot recover
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 41.3k
- Forks
- 2.1k
- Avg merge
- 23h 47m
- Merged PRs (30d)
- 124
Description
### Is there an existing issue for this?
- [x] I have searched the existing issues
### Is your issue described in the documentation?
- [x] I have read 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:
1. Start a stream
2. 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
3. Wake the display
4. The stream never resumes. Reconnecting does not help.
**Cause**
`src/platform/macos/display.mm`, in `capture()`:
```objc
// 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:
```cpp
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)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with src/platform/macos/display.mm, especially capture(), dummy_img(), and AVVideo's capture callbacks and teardown. Then inspect src/video.cpp to understand how capture_e::reinit rebuilds displays. Reproduce display sleep and wake on macOS, and verify that capture is rebuilt and streaming resumes without restarting Sunshine.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, objective-c
- Domain
- audio-video-rtc, desktop
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100