LizardByte / LizardByte/Sunshine

macOS: display sleep permanently stalls capture and the stream cannot recover

Open
#5,509 4 comments 0 reactions 0 assignees View on GitHub

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.