• Mac OS 使用ScreenCaptureKit进行窗口抓取和系统声音抓取


    ScreenCaptureKit

    概述

    ScreenCaptureKitMac 应用程序下的高性能屏幕录制库,当捕获新的视频帧和音频样本时,它会将它们作为包含媒体数据及其相关元数据的CMSampleBuffer对象传递给你的应用程序。
    本文将为大家汇总一下ScreenCaptureKit的基本用法:

    • 获取所有的screen内容
    • 获取所有的window内容
    • 过滤screen及window的方法
    • 获取系统声音
    • 过滤当前app的内容及声音
    • 使用Metal渲染捕捉流
    • 如何切换采集对象并渲染

    文末有完整demo

    内容

    Shareable content

    • SCShareableContent:将返回displayswindowsapplications,需要配合SCContentFilterSCStream过滤使用。
    • SCDisplay:表示显示设备的对象。
    • SCRunningApplication:表示在设备上运行的应用程序的对象。
    • SCWindow:表示屏幕窗口的对象。

    Content capture

    • SCStream:表示可共享内容流的对象。
    • SCStreamConfiguration:为流提供输出配置的对象。
    • SCContentFilter:过滤流捕获的内容的对象。
    • SCStreamDelegate:用于响应流事件的委托协议。

    Output processing

    • SCStreamOutput:用于接收流输出事件的协议。
    • SCStreamOutputType:表示流的输出类型的常量。
    • SCStreamFrameInfo:定义用于从系统捕获的帧中检索元数据的键的结构。
    • SCFrameStatus:流中帧的状态值。

    Stream errors

    • SCStreamErrorDomain:字符串表示形式的错误内容。
    • SCStreamError:表示框架错误的结构。

    创建一个捕捉程序(如图示例)

    实例gif图

    • 首先获取权限,判断是否获取到屏幕录制权限:
    //获取是否可以录制,返回结果
    var canRecord: Bool {
        get async {
            do {
                // If the app doesn't have Screen Recording permission, this call generates an exception.
                try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true)
                return true
            } catch {
                return false
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    //更新当前可用window,apps,dislays
    public func refreshAvailableContent(_ scwindows: (([SCWindow])->())? = nil, _ displays: (([SCDisplay])->())? = nil) async {
        do {
            // Retrieve the available screen content to capture.
            let availableContent = try await SCShareableContent.excludingDesktopWindows(false,
                                                                                        onScreenWindowsOnly: true)
            availableDisplays = availableContent.displays
            
            let windows = filterWindows(availableContent.windows)
            if windows != availableWindows {
                availableWindows = windows
            }
            availableApps = availableContent.applications
            
            if selectedDisplay == nil {
                selectedDisplay = availableDisplays.first
            }
            if selectedWindow == nil {
                selectedWindow = availableWindows.first
            }
            
            if displays != nil { displays!(availableDisplays) }
            if scwindows != nil { scwindows!(availableWindows) }
        } catch {
            print("Failed to get the shareable content: \(error.localizedDescription)")
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    //创建流配置
    private var streamConfiguration: SCStreamConfiguration {
        
        let streamConfig = SCStreamConfiguration()
        
        // 是否捕捉音频
        streamConfig.capturesAudio = isAudioCaptureEnabled
        streamConfig.excludesCurrentProcessAudio = false
        
        // display的宽高设置
        if captureType == .Display, let display = selectedDisplay {
            streamConfig.width = display.width
            streamConfig.height = display.height
        }
        
        // window宽高设置
        if captureType == .Window, let window = selectedWindow {
            streamConfig.width = Int(window.frame.width) * 2
            streamConfig.height = Int(window.frame.height) * 2
        }
        
        // 1秒60帧设置
        streamConfig.minimumFrameInterval = CMTime(value: 1, timescale: 60)
        
        // 设置内存占用
        streamConfig.queueDepth = 5
        
        return streamConfig
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    //内容过滤器
    private var contentFilter: SCContentFilter {
        let filter: SCContentFilter
        switch captureType {
        case .Display:
            guard let display = selectedDisplay else { fatalError("No display selected.") }
            var excludedApps = [SCRunningApplication]()
            //是否捕捉当前app内容
            if isAppExcluded {
                excludedApps = availableApps.filter { app in
                    Bundle.main.bundleIdentifier == app.bundleIdentifier
                }
            }
            filter = SCContentFilter(display: display,
                                     excludingApplications: excludedApps,
                                     exceptingWindows: [])
        case .Window:
            guard let window = selectedWindow else { fatalError("No window selected.") }
            filter = SCContentFilter(desktopIndependentWindow: window)
        }
        return filter
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    //进行scwindow的过滤
    private func filterWindows(_ windows: [SCWindow]) -> [SCWindow] {
        windows
            .sorted { $0.owningApplication?.applicationName ?? "" < $1.owningApplication?.applicationName ?? "" }
            .filter { $0.owningApplication != nil && $0.owningApplication?.applicationName != "" }
            .filter { $0.owningApplication?.bundleIdentifier != Bundle.main.bundleIdentifier }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    //开始捕捉
    public func startCapture() {
        do {
            let config = streamConfiguration
            let filter = contentFilter
            stream = SCStream(filter: filter, configuration: config, delegate: self)
            // Add a stream output to capture screen content.
            try stream?.addStreamOutput(streamOutput!, type: .screen, sampleHandlerQueue: videoCaptureBufferQueue)
            try stream?.addStreamOutput(streamOutput!, type: .audio, sampleHandlerQueue: audioCaptureBufferQueue)
            stream?.startCapture()
            isRunning = true
        } catch {
            isRunning = false
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    //停止捕捉
    func stopCapture() {
        guard isRunning else { return }
        stream?.stopCapture()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    //MARK: SCStreamOutput通过回调来处理samplebuffer
    func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) {
        switch type {
        case .audio:
            break
        case .screen:
            DispatchQueue.main.async {
                self.mtView.render(sampleBuffer)
            }
            break
        default:
            break
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    Demo链接

  • 相关阅读:
    JVM 高级面试题及答案整理,最新面试题
    yum -y install samba samba-client 使用这个安装的,安装在哪个文件夹下
    Ubuntu安装Qt出现bash: ./qt-unified-linux-x64-4.6.1-online.run: 权限不够的问题解决
    商业化广告--体系学习-- 10 -- 业务实战篇 -- 效果优化:如何一步步从提升曝光量深入到提升销量?
    设置私有 Git 服务器笔记
    ⑬、企业快速开发平台Spring Cloud之HTML 字符实体
    多进程编程(一):基本概念
    6 种在 JavaScript 中将字符串转换为数组的方法
    手写ButterKnife
    c++primer 正则表达式详细介绍和实例
  • 原文地址:https://blog.csdn.net/quanhaoH/article/details/126869537