macOS 26:源窗口处于非活跃Space时如何捕获实时内容?
macOS菜单栏应用:非活跃Space窗口实时内容捕获问题
背景
我开发了一款macOS菜单栏应用(Swift,macOS 14.5,Apple Silicon,SIP已启用),核心功能是将其他应用窗口的实时缩略图固定在小型浮动NSPanel中。该面板设置了collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary],因此固定缩略图可跟随用户切换至所有Space。
我使用ScreenCaptureKit捕获源窗口,并将缓冲区渲染至AVSampleBufferDisplayLayer,核心实现代码如下:
let filter = SCContentFilter(desktopIndependentWindow: scWindow) let config = SCStreamConfiguration() config.sourceRect = sourceRect config.width = Int(sourceRect.width * scale) config.height = Int(sourceRect.height * scale) config.minimumFrameInterval = CMTime(value: 1, timescale: CMTimeScale(frameRate)) config.pixelFormat = kCVPixelFormatType_32BGRA let stream = SCStream(filter: filter, configuration: config, delegate: self) try stream.addStreamOutput(streamOutput, type: .screen, sampleHandlerQueue: .main) try await stream.startCapture() // each sample buffer -> displayLayer.enqueue(buffer)
当源窗口处于活跃Space时,该功能运行完全正常。
核心问题
切换至其他Space/桌面后,源窗口不再被合成,捕获内容停止更新:
- 对于静态窗口,最后一帧会保留,显示正常;
- 对于硬件合成内容(如浏览器中播放的视频),固定区域变为空白/黑色。ScreenCaptureKit仍会交付缓冲区,且从帧状态来看始终标记为
SCFrameStatusComplete,无法仅通过状态区分有效帧与空白帧:
// inside SCStreamOutput.stream(_:didOutputSampleBuffer:of:) let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: false) as? [[SCStreamFrameInfo: Any]] let status = (attachments?.first?[.status] as? Int).flatMap(SCFrameStatus.init) // 非活跃Space中的视频,状态仍为.complete,但像素是空白的
我可通过CGWindowListCopyWindowInfo可靠检测源窗口是否处于非活跃Space(窗口ID会从屏幕列表中消失),目前的处理方式是冻结最后一帧并显示“已暂停”标记:
func isWindowOnActiveSpace(_ windowID: CGWindowID) -> Bool { guard let list = CGWindowListCopyWindowInfo( [.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID ) as? [[String: Any]] else { return true } return list.contains { ($0[kCGWindowNumber as String] as? CGWindowID) == windowID } }
这种方案可行,但我实际希望源窗口处于其他Space时仍能保持内容实时更新。
已尝试但无效的方法
- 定期重新应用流配置以尝试获取新帧——无效果:
try await stream.updateConfiguration(config)
- 通过私有CoreGraphics-Services标签将源窗口标记为“粘性”(显示在所有Space),SIP启用下无明显效果:
typealias SetTagsFn = @convention(c) (Int32, Int32, UnsafeMutablePointer<Int32>, Int32) -> Int32 let h = dlopen(nil, RTLD_NOW) let CGSMainConnectionID = unsafeBitCast(dlsym(h, "CGSMainConnectionID")!, to: (@convention(c) () -> Int32).self) let CGSSetWindowTags = unsafeBitCast(dlsym(h, "CGSSetWindowTags")!, to: SetTagsFn.self) var tags: [Int32] = [0x0800, 0] // CGSTagSticky CGSSetWindowTags(CGSMainConnectionID(), Int32(bitPattern: windowID), &tags, 32)
源窗口未显示在其他Space中,切换Space后固定区域仍变为空白。(据了解,自macOS 14.5起,跨Space移动/设置粘性窗口需部分禁用SIP并注入Dock脚本扩展,如yabai,我希望避免这种方式。)
疑问
是否存在无需禁用SIP的公开API或可行私有方法,以实现以下任一目标:
- 让处于非活跃Space的窗口持续渲染,使ScreenCaptureKit能捕获实时帧;
- 以其他方式获取该窗口在非活跃Space时的实时帧?
或者,由于WindowServer根本不会合成非活跃Space的窗口,因此只能采用源端方案(如针对浏览器,使用扩展直接读取<video>元素)?
内容的提问来源于stack exchange,提问作者Adel




