You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

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或可行私有方法,以实现以下任一目标:

  1. 让处于非活跃Space的窗口持续渲染,使ScreenCaptureKit能捕获实时帧;
  2. 以其他方式获取该窗口在非活跃Space时的实时帧?

或者,由于WindowServer根本不会合成非活跃Space的窗口,因此只能采用源端方案(如针对浏览器,使用扩展直接读取<video>元素)?

内容的提问来源于stack exchange,提问作者Adel

火山引擎 最新活动