macにケーブル接続したiPhoneの画面をリアルタイム取得するミニマム実装を作りました。
これまではQuickTime Playerを起動し「新規ムービー収録」からiPhoneを選択するなどのアプリ外での取り回しが必要だったのですが、これで自作プログラムで実現可能となります。
GitHubにアップしています。
https://github.com/satoshi0212/DeviceCameraMonitorSample
この実装含め、仮想カメラ/AR/映像表現などの情報更新はTwitterで投稿しています。
https://twitter.com/shmdevelop
実装ポイント
プロジェクト設定
「Hardware」「Camera」 の選択が必要。
plist
plistに Privacy - Camera Usage Description
を追加してください。
Device探索時設定
AVCaptureDevice.DiscoverySession
実行前に以下を指定することでオプトインで外部デバイスが表示されるようになります。
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デバイスが特定できます。
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する必要もありました。
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
でサイズ変換しています。
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
})
}