概要
iOSのAVFoundationを用いて汎用カメラモジュールを作ります。
Vision.frameworkと組み合わせて画像認識したり,リアルタイムビデオエフェクトアプリを作る際にも利用できるように,汎用のモジュールとして作りましょう。いろいろと使いまわせて便利です。
まずはシングルトンのカメラコントローラクラスを作り,そこにAVCaptureSessionのインスタンスを持たせます。これがカメラのセッションを管理するクラスですね。
このAVCaptureSessionに,インプットとアフトプットを接続し,さらにカメラプレビューとなるAVCaptureVideoPreviewLayerを接続すれば準備完了。
AVCaptureSessionをrunしてやればカメラが起動し,AVCaptureVideoPreviewLayerにカメラの映像が表示されます。
CameraControllerの実装
汎用カメラオブジェクトとして,私はいつもCameraControllerというクラスを作っています。
class CameraController: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVCapturePhotoCaptureDelegate {
// MARK: - lifecycle
static let shared = CameraController()
private override init() {
}
}
Swiftでのシングルトンの実装方法もいくつかあるようですが,シンプルに。このクラスはSampleBufferDelegateとPhotoCaptureDelegateを採用していますが,必要な機能によっては他にも採用するプロトコルはあり得ます。
また,カメラの起動に必要なものは,プロパティとして宣言しておきます。
// MARK: - properties
let captureSession:AVCaptureSession = AVCaptureSession()
let videoOutput:AVCaptureVideoDataOutput = AVCaptureVideoDataOutput()
let photoOutput:AVCapturePhotoOutput = AVCapturePhotoOutput()
let previewLayer = AVCaptureVideoPreviewLayer()
var preview:UIView!
最後のpreviewだけ,宣言時にはnilです。
このクラス,例えば他のviewControllerなどから簡単に呼べるように,カメラをスタートするメソッドとストップするメソッドを持たせます。カメラ起動中の映像を表示するために,スタートメソッドにはプレビューを表示するUIViewをパラメータに持たせます。スタートメソッドで受け取ったviewを,プロパティのpreviewにセットするわけですね。
次に,カメラをスタートするメソッド,ストップするメソッドを追加します。スタートメソッド内でcapture sessionやinput, outputを設定します。
func startSession(preview:UIView){
//カメラプレビュー
self.preview = preview
self.previewLayer.setSessionWithNoConnection(self.captureSession)
self.previewLayer.frame = preview.bounds
self.previewLayer.videoGravity = .resizeAspectFill
preview.layer.addSublayer(self.previewLayer)
self.captureSession.beginConfiguration()
// input
guard let videoDevice = AVCaptureDevice.default(for: .video) else { return }
do {
let deviceInput = try AVCaptureDeviceInput(device: videoDevice)
if self.captureSession.canAddInput(deviceInput){
self.captureSession.addInput(deviceInput)
}
else{
print("input error")
}
}
catch {
}
// output
if self.captureSession.canAddOutput(self.photoOutput){
self.captureSession.addOutput(self.photoOutput)
self.photoOutput.isHighResolutionCaptureEnabled = true
self.photoOutput.isLivePhotoCaptureEnabled = self.photoOutput.isLivePhotoCaptureSupported
}
else{
print("photo output error!")
}
if self.captureSession.canAddOutput(self.videoOutput){
self.videoOutput.setSampleBufferDelegate(self, queue: self.sampleBufferQueue)
self.captureSession.addOutput(self.videoOutput)
}
else{
print("video output error!")
}
self.captureSession.commitConfiguration()
self.captureSession.startRunning()
}
本来はマルチスレッドを活用して,UIに関わる部分以外はバックグラウンドのqueueで行いましょう。エラーハンドリングも適切に。このコードでは割愛してます。
続いてカメラをストップするメソッドを書きたいところですが,長くなるのでこれも割愛。基本的には,sessionをstopしてinputとoutputを外しておけばOKです。
最後に,必要なデリゲートプロトコルを実装。ここでは使用頻度が高いと思われる2つだけ書いておきます。メソッドの中身は,必要に応じて頑張って書きましょう。
// MARK: - capture photo delegate
// 写真撮影後に呼ばれる
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?){
}
// MARK: - sample buffer delegate
// 1フレームごとに呼ばれる
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
}
これがCameraControllerの実装は終わり(実際にはもっと書かなきゃいけませんよ!)。viewControllerからカメラを起動する場合には,例えば以下のようにします。
override func viewDidAppear(_ animated:Bool){
super.viewDidAppear(animated)
//ここ
CameraController.shared.startSession(self.view)
}
以上,汎用カメラクラスの基本設計でした。
カメラなど,特定のデバイスを扱うコードはできる限りシングルトンの専用クラスに書いておいた方が良いと思います。しばしばViewControllerにすべて書いた実装を見ますが,それはあまり良くないです。理由は以下。
・デバイスは1つしかないので,カメラを制御するコードが複数のインスタンスから同時にコールされないようにするため
・コードの再利用が容易(←重要)
・専用のオブジェクトにまとめておけば,どのviewControllerからでも呼びやすい
というところです。
上記コードですが,くれぐれも,これだけではまだ十分ではないのでいろいろと頑張りましょう。改善提案,間違いの指摘は歓迎です。