[Unity]macOSでデスクトップキャプチャ

# macOS で macOS でデスクトップキャプチャ
* macOS のデスクトップを unity でキャプチャできるプラグインを作ってみました

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">unityでmacのデスクトップキャプチャできた！ <a href="https://t.co/x2AkZ6z6zF">pic.twitter.com/x2AkZ6z6zF</a></p>&mdash; ふじき (@fzkqi) <a href="https://twitter.com/fzkqi/status/1402810152642760704?ref_src=twsrc%5Etfw">June 10, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

* リポジトリはこちらです

https://github.com/fuziki/mcDesktopCapture

# 実装の仕組み
1. macOS は AVCaptureScreenInput を使うことで、デスクトップをキャプチャする事が可能です
1. Unity から直接 AVCaptureScreenInput にアクセスできないので、Swift で Native Plugin を実装します
1. CMSampleBuffer を取得できるので、MTLTexture に変換します
1. MTLTexture のポインタを Unity へ渡して、Texture2D.CreateExternalTexture を使って Texture2D を得ます

## AVCaptureScreenInput
* macOSは、[AVCaptureScreenInput](https://developer.apple.com/documentation/avfoundation/avcapturescreeninput) を使うことで、デスクトップをキャプチャする事が可能です
* この記事と同じやり方で Native Plugin を実装します

https://qiita.com/fuziki/items/974f70b663ebfadfb136

* AVCaptureScreenInput は他の AVCaptureInput と同じように使えます
* AVCaptureVideoDataOutput を使って、キャプチャしたフレームを受け取ります

```swift
let captureSession = AVCaptureSession()

let displayID = CGDirectDisplayID(CGMainDisplayID())
let videoDataInput: AVCaptureScreenInput = AVCaptureScreenInput(displayID: displayID)!
captureSession.addInput(videoDataInput)
        
let videoDataOutput: AVCaptureVideoDataOutput = AVCaptureVideoDataOutput()
videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_32BGRA] as [String : Any]
videoDataOutput.setSampleBufferDelegate(self, queue: videoQueue)
videoDataOutput.alwaysDiscardsLateVideoFrames = true
captureSession.addOutput(videoDataOutput)

captureSession. startRunning()
```
 
## CMSampleBuffer -> MTLTexture

* AVCaptureVideoDataOutputSampleBufferDelegate を実装します
* CVPixelBuffer を取得し、MTLTexture に変換します

```swift
extension DesktopCapture: AVCaptureVideoDataOutputSampleBufferDelegate {
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        let imageBuffer: CVPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)!
        
        var textureCache : CVMetalTextureCache! = nil
        CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, mtlDevice, nil, &textureCache)
        
        let width = CVPixelBufferGetWidth(imageBuffer)
        let height = CVPixelBufferGetHeight(imageBuffer)

        var imageTexture: CVMetalTexture! = nil
        _ = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache,
                                                      imageBuffer, nil, .bgra8Unorm_srgb,
                                                      width, height, 0, &imageTexture)
        
        let texture: MTLTexture = CVMetalTextureGetTexture(imageTexture)!        
    }
```

## MTLTexture -> Texture

* Unity から呼び出される mcDesktopCapture_getCurrentFrame 関数を実装します
* retain して、テクスチャのポインタを Unity に返します

```swift
@_cdecl("mcDesktopCapture_getCurrentFrame")
public func mcDesktopCapture_getCurrentFrame() -> FrameEntity {
    let texturePtr = Unmanaged.passRetained(texture).toOpaque()
    let e = FrameEntity(width: texture.width, height: texture.height, texturePtr: texturePtr)
    return e
}
```

* FrameEntity を取得して、Texture2D.CreateExternalTexture を使って、Textur2D を作成します
* 作成した、Texture2D は通常の Texture 同様に利用可能です

```c#
FrameEntity frameEntity = mcDesktopCapture_getCurrentFrame();
Texture2D texture = Texture2D.CreateExternalTexture((int)frameEntity.width, (int)frameEntity.height, TextureFormat.ARGB32, false, false, frameEntity.texturePtr);
Renderer m_Renderer = GetComponent<Renderer>();
m_Renderer.material.SetTexture("_MainTex", texture);
```

# おわりに
* macOS では、API が用意されているため、デスクトップキャプチャの仕組みを容易に実装できました
* macOS はアプリ単位で取得する方法が見つからず、画面全体をキャプチャすることにしました