macにケーブル接続したiPhoneの画面をリアルタイム取得する

macにケーブル接続したiPhoneの画面をリアルタイム取得するミニマム実装を作りました。

これまではQuickTime Playerを起動し「新規ムービー収録」からiPhoneを選択するなどのアプリ外での取り回しが必要だったのですが、これで自作プログラムで実現可能となります。

![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63205/2b7fb44d-a817-edcd-7ed9-df6a6b82afd8.gif)

GitHubにアップしています。
https://github.com/satoshi0212/DeviceCameraMonitorSample

この実装含め、仮想カメラ/AR/映像表現などの情報更新はTwitterで投稿しています。
https://twitter.com/shmdevelop

## 実装ポイント

### プロジェクト設定

「Hardware」「Camera」 の選択が必要。

![スクリーンショット 2020-10-11 23.41.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63205/8d7a2a5b-a6d7-3a58-c202-4de3be7292d2.png)

### plist

plistに `Privacy - Camera Usage Description` を追加してください。

![スクリーンショット 2020-10-12 2.26.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63205/f7fd7139-7590-e5c2-bdfa-5fb0156ff0aa.png)

### Device探索時設定

`AVCaptureDevice.DiscoverySession` 実行前に以下を指定することでオプトインで外部デバイスが表示されるようになります。

```swift

        var prop = CMIOObjectPropertyAddress(
            mSelector: CMIOObjectPropertySelector(kCMIOHardwarePropertyAllowScreenCaptureDevices),
            mScope: CMIOObjectPropertyScope(kCMIOObjectPropertyScopeGlobal),
            mElement: CMIOObjectPropertyElement(kCMIOObjectPropertyElementMaster))
        var allow: UInt32 = 1;
        CMIOObjectSetPropertyData(CMIOObjectID(kCMIOObjectSystemObject), &prop, 0, nil, UInt32(MemoryLayout.size(ofValue: allow)), &allow)
```

そして以下のパラメータで探索するとdevicesにiPhoneが含まれています。
見つかったdevicesを `modelID`、`manufacturer` で適宜フィルタするとiPhoneデバイスが特定できます。

```swift

        let devices = AVCaptureDevice.DiscoverySession(deviceTypes: [.externalUnknown], mediaType: nil, position: .unspecified).devices
        if let device = devices.filter({ $0.modelID == "iOS Device" && $0.manufacturer == "Apple Inc." }).first {
            ...
        }
```

ただし起動直後や探索直後はiPhoneが見つからない場合があるため `AVCaptureDeviceWasConnectedNotification` のnotificationをobserveする必要もありました。

```swift

        let nc = NotificationCenter.default
        nc.addObserver(forName: NSNotification.Name(rawValue: "AVCaptureDeviceWasConnectedNotification"), object: nil, queue: .main) { (notification) in
            print(notification)
            guard let device = notification.object as? AVCaptureDevice else { return }
            ...
        }
```

### 余談: 表示用リサイズ

アップした実装では画面表示用にリサイズしました。

高さを固定値として比率を計算し幅を算出しimageViewのサイズ指定。
画像の方が `CGAffineTransform` でサイズ変換しています。

```swift

    private func resizeIfNeeded(w: CGFloat, h: CGFloat) {
        guard targetRect == nil else { return }
        let aspect = h / fixedHeight
        let rect = CGRect(x: 0, y: 0, width: floor(w / aspect), height: fixedHeight)
        imageView.frame = rect
        targetRect = rect
    }

    ...

    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        connection.videoOrientation = .portrait

        DispatchQueue.main.async(execute: {
            let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)!
            let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
            let w = CGFloat(CVPixelBufferGetWidth(pixelBuffer))
            let h = CGFloat(CVPixelBufferGetHeight(pixelBuffer))
            self.resizeIfNeeded(w: w, h: h)

            guard let targetRect = self.targetRect else { return }
            let m = CGAffineTransform(scaleX: targetRect.width / w, y: targetRect.height / h)
            let resizedImage = ciImage.transformed(by: m)
            let cgimage = self.context.createCGImage(resizedImage, from: targetRect)!
            let image = NSImage(cgImage: cgimage, size: targetRect.size)
            self.imageView.image = image
        })
    }
```