Skip to content

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

Description

@harrison001

Is there an existing issue for this?

  • I have searched the existing issues

Is your issue described in the documentation?

  • 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():

// 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions