You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ScreenCaptureKit 滚动缓存录制最后30秒屏幕的queueDepth与API流程问题

ScreenCaptureKit 滚动缓存录制最后30秒屏幕的queueDepth与API流程问题

我完全懂你现在的纠结——想用ScreenCaptureKit做一个「随时保存最后30秒屏幕内容」的工具,之前靠硬拉queueDepth到900凑出了功能,但苹果明确说这个参数不能超过8,改回合规值后又只能拿到寥寥几帧,完全达不到需求。其实核心问题是你误解了queueDepth的作用,把它当成了自己的滚动缓存,现在我来帮你理清正确的API使用逻辑,以及怎么用合规的方式实现需求。

问题根源:误解了queueDepth的作用

苹果文档里的queueDepth是ScreenCaptureKit内部的帧缓冲区大小,用来临时存储还没来得及被你的代码处理的帧,防止因为你的回调处理不及时导致丢帧。这个参数的合理范围是1-8,设成900不仅会占用巨量内存,还违反了SDK的设计意图,甚至可能导致性能问题或者被系统限制。

你之前能靠900的queueDepth「录到30秒」,本质是让SDK帮你存了900帧,但这完全不是正确的用法——你的代码应该自己接收每一个输出的帧,然后维护自己的滚动缓存,而不是依赖SDK的内部缓冲区。

正确的API流程

  1. 初始化流时,保持queueDepth在1-8之间:比如设为8,遵循苹果的建议,让SDK负责内部的帧缓冲防丢帧。
  2. 持续接收每一个输出的有效帧:通过自定义的SCStreamOutput(你的ScreenStreamOutput)接收每一个状态为.complete的帧。
  3. 维护自己的线程安全滚动缓存:把每一个有效帧存入缓存,同时定时清理超过30秒的旧帧。
  4. 触发保存时,从缓存提取帧生成视频:用AVAssetWriter把缓存中的帧转成MP4文件。

针对你的代码的具体修改建议

1. 修正queueDepth的值

首先把流配置里的queueDepth改成8:

config.queueDepth = 8 // 严格遵循苹果建议的最大值

2. 优化滚动缓存的帧存储逻辑

你的rollingFrameBuffer是对的方向,但要注意:ScreenCaptureKit输出的CMSampleBuffer可能会被SDK回收,所以必须复制一份再存入缓存,同时要高效清理旧帧:

private func handleFrame(_ sampleBuffer: CMSampleBuffer) {
    rollingFrameBufferQueue.async { [weak self] in
        guard let self = self else { return }
        
        // 复制Sample Buffer,确保我们持有独立的引用,避免SDK回收原帧
        guard let copiedBuffer = CMSampleBufferCreateCopy(nil, sampleBuffer) else {
            print("[DEBUG] Failed to copy sample buffer for cache")
            return
        }
        
        let presentationTime = CMSampleBufferGetPresentationTimeStamp(copiedBuffer)
        self.rollingFrameBuffer.append((copiedBuffer, presentationTime))
        
        // 清理超过30秒的旧帧,保持滚动缓存的大小
        let cutoffTime = CMTimeSubtract(
            presentationTime,
            CMTimeMakeWithSeconds(self.rollingBufferDuration, preferredTimescale: 1000000000)
        )
        self.rollingFrameBuffer = self.rollingFrameBuffer.filter {
            CMTimeCompare($0.1, cutoffTime) >= 0
        }
        
        // 可选:打印缓存当前状态,调试用
        print("[DEBUG] Rolling buffer now contains \(self.rollingFrameBuffer.count) frames")
    }
}

3. 确保流持续输出帧

你之前改queueDepth后只能拿到8帧,大概率是误解了queueDepth的作用——只要你的ScreenStreamOutput回调能及时处理帧,SDK会持续输出新的帧,不会只停在8帧。检查你的ScreenStreamOutput实现,确保没有过滤掉后续的帧。

4. 优化缓存的线程安全

你已经用了rollingFrameBufferQueue,继续保持所有对rollingFrameBuffer的读写操作都在这个队列里执行,避免多线程竞争。

5. 修正保存逻辑的细节

保存时,确保你正确使用AVAssetWriter写入帧,注意设置和缓存帧匹配的视频格式、帧率、分辨率:

private func writeFramesToDisk(frames: [(CMSampleBuffer, CMTime)], to outputURL: URL, completion: @escaping (Bool) -> Void) {
    guard !frames.isEmpty else {
        completion(false)
        return
    }
    
    do {
        let assetWriter = try AVAssetWriter(outputURL: outputURL, fileType: .mp4)
        let firstFrame = frames.first!.0
        
        // 获取视频格式信息
        guard let formatDescription = CMSampleBufferGetFormatDescription(firstFrame) else {
            completion(false)
            return
        }
        let videoSettings: [String: Any] = [
            AVVideoCodecKey: AVVideoCodecType.h264,
            AVVideoWidthKey: NSNumber(value: CMSampleBufferGetWidth(firstFrame)),
            AVVideoHeightKey: NSNumber(value: CMSampleBufferGetHeight(firstFrame))
        ]
        
        let assetWriterInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings)
        assetWriterInput.expectsMediaDataInRealTime = false // 非实时写入,因为我们是批量处理缓存帧
        assetWriterInput.transform = .identity // 根据需要调整旋转
        
        if assetWriter.canAdd(assetWriterInput) {
            assetWriter.add(assetWriterInput)
        } else {
            completion(false)
            return
        }
        
        assetWriter.startWriting()
        assetWriter.startSession(atSourceTime: frames.first!.1)
        
        let dispatchGroup = DispatchGroup()
        
        for (sampleBuffer, _) in frames {
            dispatchGroup.enter()
            if assetWriterInput.isReadyForMoreMediaData {
                assetWriterInput.append(sampleBuffer)
                dispatchGroup.leave()
            } else {
                // 等待输入准备好
                assetWriterInput.requestMediaDataWhenReady(on: .global()) {
                    assetWriterInput.append(sampleBuffer)
                    dispatchGroup.leave()
                }
            }
        }
        
        dispatchGroup.notify(queue: .global()) {
            assetWriterInput.markAsFinished()
            assetWriter.finishWriting {
                completion(assetWriter.status == .completed)
            }
        }
    } catch {
        print("[DEBUG] Failed to initialize AVAssetWriter: \(error)")
        completion(false)
    }
}

关键注意事项

  • 内存管理:CMSampleBuffer占用内存较大,30秒30fps的1080p帧大概会占用数GB内存,你可以考虑降低帧率(比如15fps)或者在缓存中存储压缩后的帧,避免内存溢出。
  • 显示器切换处理:当用户切换显示器时,要正确停止旧的流,启动新的流,此时可以选择清空缓存或者继续保留(根据产品需求)。
  • 帧状态检查:一定要只处理状态为.complete的帧,避免无效或部分渲染的帧影响最终视频质量。
  • 流的生命周期:确保stream和screenStreamOutput的引用正确管理,避免提前释放导致流中断。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 13:23:04