ScreenCaptureKitでmacOSで画面録画

# ScreenCaptureKitでmacOSで画面録画
macOS 12.3からScreenCaptureKitが追加されました
これまでの[Quartz Display Services](https://developer.apple.com/documentation/coregraphics/quartz_display_services)はディスプレイ単位でしか画面をキャプチャできませんでした
しかし、このFrameworkを利用する事で、キャプチャしたいコンテンツの選択などキメ細かい制御をして画面をキャプチャする事ができるようになりました

https://developer.apple.com/documentation/screencapturekit

# ScreenCaptureKitで画面録画する

macOS12.4, Xcode13.4で試してみました

![screen-capture.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/a49a7fa3-e9ec-21db-b6ee-02b12989da96.gif)

## SCStreamの作成

SCWindowのリストを取得し、お目当てのwindowを使って、SCContentFilterとSCStreamConfigurationを設定し、SCStreamを作成します

```swift
let windows: [SCWindow] = try await SCShareableContent.current.windows
guard let window = windows.first(where: { $0.owningApplication?.applicationName == "Google Chrome" }) else { return }
let filter = SCContentFilter(desktopIndependentWindow: window)
let configuration = SCStreamConfiguration()
configuration.width = Int(window.frame.width)
configuration.height = Int(window.frame.height)
configuration.showsCursor = true
let stream = SCStream(filter: filter, configuration: configuration, delegate: nil)
```

## Captureの実行とCMSampleBufferの処理

StreamOutput を追加して、Captureを開始します
CMSampleBufferはNSImageに変換し、表示させます
CMSampleBufferからCVPixelBufferを取得し、CIImageとCGImageを経由させて、NSImageに変換します

```swift
class ContentViewModel {
    func start() async throws {
        /* ~~ 中略 ~~ */
        try stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: DispatchQueue.main)
        try await stream.startCapture()
    }
}
extension ContentViewModel: SCStreamOutput {
    func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) {
        let pixelBuffer: CVPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)!
        let size = CGSize(width: CVPixelBufferGetWidth(pixelBuffer), height: CVPixelBufferGetHeight(pixelBuffer))
        let ci = CIImage(cvPixelBuffer: pixelBuffer)
        let cg = CIContext().createCGImage(ci, from: .init(origin: .zero, size: size))!
        self.image = NSImage(cgImage: cg, size: size)
    }
}
```

## NSImageの表示

SwiftUIのImageを使って表示しました

```swift
struct ContentView: View {
    @ObservedObject var viewModel = ContentViewModel()
    var body: some View {
        if let image = viewModel.image {
            Image(nsImage: image)
                .resizable()
                .scaledToFit()
        } else {
            Text("Hello, world!")
                .padding(120)
        }
    }
}
```

## プライバシーにアプリを登録

システム環境設定 > セキュリティとプライバシー > プライバシー > 画面収録 に作成したアプリを登録します
再度、アプリを起動すると画面をキャプチャできるようになっています

![スクリーンショット 2022-07-23 22.04.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/6b54e8ae-cba8-2998-1edf-a0869ab41d0d.png)

# コード全文

```swift
import ScreenCaptureKit
import SwiftUI

struct ContentView: View {
    @ObservedObject var viewModel = ContentViewModel()
    var body: some View {
        if let image = viewModel.image {
            Image(nsImage: image)
                .resizable()
                .scaledToFit()
        } else {
            Text("Hello, world!")
                .padding(120)
        }
    }
}

class ContentViewModel: NSObject, ObservableObject {
    @Published var image: NSImage?
    var stream: SCStream!
    override init() {
        super.init()
        Task {
            do {
                try await setup()
            } catch let error {
                print("error: \(error)")
            }
        }
    }
    @MainActor func setup() async throws {
        let windows: [SCWindow] = try await SCShareableContent.current.windows
        guard let window = windows.first(where: { $0.owningApplication?.applicationName == "Google Chrome" }) else { return }
        let filter = SCContentFilter(desktopIndependentWindow: window)
        let configuration = SCStreamConfiguration()
        configuration.width = Int(window.frame.width)
        configuration.height = Int(window.frame.height)
        configuration.showsCursor = true
        stream = SCStream(filter: filter, configuration: configuration, delegate: nil)
        try stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: DispatchQueue.main)
        try await stream.startCapture()
    }
}
extension ContentViewModel: SCStreamOutput {
    func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) {
        let pixelBuffer: CVPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)!
        let size = CGSize(width: CVPixelBufferGetWidth(pixelBuffer), height: CVPixelBufferGetHeight(pixelBuffer))
        let ci = CIImage(cvPixelBuffer: pixelBuffer)
        let cg = CIContext().createCGImage(ci, from: .init(origin: .zero, size: size))!
        self.image = NSImage(cgImage: cg, size: size)
    }
}
```

# まとめ
ScreenCaptureKitを使うと簡単に画面録画ができるようになっていました
filterやconfigurationで細かくキャプチャ内容を制御できる上に、Quartz Display Servicesよりも性能が高いように感じます、とても良さそうです