SwiftUIでAVFundationを導入する【Video Capture偏】
AVFundationはiOS/macOSともに使えるフレームワークで、端末のカメラやオーディオ機器にアクセスするためのフレームワークです。 今回の記事ではAVFundationをつかって、iPhoneのカメラ映像をアプリに表示する方法を解説します。
はじめに
こんにちは。たけしです。
この記事は自分のAVFundationの勉強のために書かれた記事です。
AVFundationはiOS/macOSともに使えるフレームワークで、端末のカメラやオーディオ機器にアクセスするためのフレームワークです。
今回の記事ではAVFundationをつかって、iPhoneのカメラ映像をアプリに表示する方法を解説します。
開発環境
サンプルアプリは以下の環境で開発されました。
- Xcode 11.5
- iPhone 11 Pro (iOS 13.4.1)
アプリ概要
iPhoneのカメラの映像をスクリーンに全画面表示するだけのアプリです。
アーキテクチャはVIPERで作ります。
VIPERに馴染みがない方は以前書いた記事があるので御覧ください。
/2020/05/31/viper-architecture-in-swiftui/
ソース
https://github.com/SatoTakeshiX/SwiftUI-AVFundation
カメラのアクセス許可
Video映像をアプリから取得するにはカメラのアクセス許可が必要です。
アプリのInfo.plistにNSCameraUsageDescriptionを追加しましょう。
<key>NSCameraUsageDescription</key>
<string>take your video for sample swiftUI app</string>
Viewの作成
まずはViewから作成します。
任意のCALayerを表示するCALayerView
とLayerを全画面表示させるSimpleVideoCaptureView
の2つを作ります。
CALayerView
AVFundationではカメラからの映像をキャプチャするのにAVCaptureVideoPreviewLayer
という型が用意されています。
これはCALayer
のサブクラスで、これを通してカメラの映像をViewに表示させることができます。
しかし、CALayer
なので、そのままではSwiftUIに表示させることができません。
任意のCALayerをSwiftUIで表示するため、UIViewControllerRepresentable
に準拠したCALayerView
を作成します。
struct CALayerView: UIViewControllerRepresentable {
typealias UIViewControllerType = UIViewController
var caLayer: CALayer
func makeUIViewController(context: Context) -> UIViewController {
let viewController = UIViewController()
viewController.view.layer.addSublayer(caLayer)
caLayer.frame = viewController.view.layer.frame
return viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
caLayer.frame = uiViewController.view.layer.frame
}
}
イニシャライザーにCALayer
を取ります。
makeUIViewController
でView Controllerを作成してCALayer
を追加しています。
calayerのframeはviewのlayerに合わせています。
View Controllerのviewは特に指定がない限り、自動で端末の大きさと同じになるはずなので、caLayer
もその大きさに合わせています。
UIViewRepresentableで作ったところWidth,Heightの指定が必要?
ところでUIViewControllerRepresentable
ではなくUIViewRepresentable
で一度作ったところ、Layerのframeがwidth:0, height:0になり、Viewに表示されないことがありました。
SwiftUIのほうでFrameを指定する必要があるかもしません。
UIViewControllerRepresentable
だとvc.view.frameが自動的に端末サイズになるので、サイズに悩まなくてもよさそうです。
struct CALayerView: UIViewRepresentable {
typealias UIViewType = UIView
var caLayer: CALayer
func makeUIView(context: Context) -> UIView {
let view = UIView()
view.layer.addSublayer(caLayer)
caLayer.frame = view.layer.frame
view.layer.masksToBounds = true
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
caLayer.frame = uiView.layer.bounds
}
}
SimpleVideoCaptureView
アプリの大本となる画面をSimpleVideoCaptureView
という名前で作ります。
struct SimpleVideoCaptureView: View {
@ObservedObject
var presenter: SimpleVideoCapturePresenter
var body: some View {
ZStack {
CALayerView(caLayer: presenter.previewLayer)
}
.edgesIgnoringSafeArea(.all)
.onAppear {
self.presenter.apply(inputs: .onAppear)
}
.onDisappear {
self.presenter.apply(inputs: .onDisappear)
}
}
}
ZStack
を.edgesIgnoringSafeArea(.all)
にすることで、SafeAreaを無視した全画面表示にしています。
さきほど作ったCALayerView
をZStackに入れます。
presenter
についてはまた後で解説します。
SceneDelegate
でSimpleVideoCaptureView
が最初に読み込まれるように修正します。
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = SimpleVideoCaptureView(presenter: SimpleVideoCapturePresenter())
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
Presenterの作成
Presenterの役割はViewからのイベントを受けて、Interactorにデータ作成を依頼し、作られたデータをViewに通知することです。
final class SimpleVideoCapturePresenter: ObservableObject {
var previewLayer: CALayer {
return interactor.previewLayer!
}
enum Inputs {
case onAppear
case tappedCameraButton
case onDisappear
}
init() {
interactor.setupAVCaptureSession()
}
func apply(inputs: Inputs) {
switch inputs {
case .onAppear:
interactor.startSettion()
break
case .tappedCameraButton:
break
case .onDisappear:
interactor.stopSettion()
}
}
// MARK: Privates
private let interactor = SimpleVideoCaptureInteractor()
}
previewLayer
プロパティでInteractorのpreviewLayer
を返してます。
apply
メソッドでViewのイベント、onAppearやonDisappearをハンドリングしています。
Interactorの作成
さて、いよいよInteractorを使ってVideoキャプチャのデータを取得します。
import Foundation
import AVKit
final class SimpleVideoCaptureInteractor: NSObject, ObservableObject {
private let captureSession = AVCaptureSession()
@Published var previewLayer: AVCaptureVideoPreviewLayer?
private var captureDevice: AVCaptureDevice?
}
AVCaptureSession/AVCaptureDevice
AVCaptureSessionはiOS/macOSのOSのメディア装置を扱うクラスです。入力デバイスからメディアを出力するデータの流れを管理します。
Setting Up a Capture Session | Apple Developer Documentation
Setting Up a Capture Session Figure 1 から引用
AVCaptureSessionにAVCaptureDeviceをAVCaptureDeviceInputとして登録することで、入力デバイスの登録をすることができます。図ではback cameraとmicrophoneを登録することで、画像と音声を出力できるようにしています。
setupAVCaptureSessionメソッド
setupAVCaptureSession
メソッドを定義してCapture Sessionを準備しましょう。
func setupAVCaptureSession() {
captureSession.sessionPreset = .photo
if let availableDevice = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .back).devices.first {
captureDevice = availableDevice
}
AVCaptureDevice.DiscoverySession
メソッドで端末で利用できるデバイスを取得します。
今回は組み込みの広角カメラ、builtInWideAngleCamera
を選択しています。
builtInWideAngleCameraは多くの利用目的にあったカメラなのでこれを指定しておけばまず大丈夫です。
mediaType
にvideo
、positionに.back
を選ぶことで背面カメラを取得しています。
positionに.front
を選ぶと全面カメラが取得できます。
AVCaptureDeviceInputの作成
続いてAVCaptureDeviceInputを作成してAVCaptureSessionに登録します。
さきほど作ったAVCaptureDeviceをもとにAVCaptureDeviceInputを作ります。
そして、addInput
メソッドでcaptureSession
に登録します。
do {
let captureDeviceInput = try AVCaptureDeviceInput(device: captureDevice!)
captureSession.addInput(captureDeviceInput)
} catch let error {
print(error.localizedDescription)
}
AVCaptureVideoPreviewLayerの作成
続いてAVCaptureVideoPreviewLayerを作成します。
AVCaptureVideoPreviewLayerはcaptureSessionの映像をCALayerとして表示するものです。
カメラの映像をアプリに表示したいときに利用します。
let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer.name = "CameraPreview"
previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
previewLayer.backgroundColor = UIColor.black.cgColor
self.previewLayer = previewLayer
ここで注意すべきなのはvideoGravity
にAVLayerVideoGravity.resizeAspectFill
を指定することです。
videoGravity
はAVCaptureVideoPreviewLayerの矩形領域をどう表示するかを決定するプロパティで次の3つが指定できます。
- resizeAspect: アスペクト比をレイヤー内に収まるように保持する。デフォルト値。
- resizeAspectFill: アスペクト比を保ったままレイヤー矩形いっぱいに表示する。
- resize: アスペクト比を無視し、レイヤー矩形いっぱいに表示する。
3つそれぞれ指定した場合のAVCaptureVideoPreviewLayerの表示をみてみます。
今回は、カメラ映像をView全面に表示したいのでresizeAspectFill
を指定します。
最後にcommitConfiguration()
メソッドでSessionの編集をコミットすればOKです。
SimpleVideoCaptureInteractorの全体コードはこちらです。
final class SimpleVideoCaptureInteractor: NSObject, ObservableObject {
private let captureSession = AVCaptureSession()
@Published var previewLayer: AVCaptureVideoPreviewLayer?
private var captureDevice: AVCaptureDevice?
/// - Tag: CreateCaptureSession
func setupAVCaptureSession() {
print(#function)
captureSession.sessionPreset = .photo
if let availableDevice = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .back).devices.first {
captureDevice = availableDevice
}
do {
let captureDeviceInput = try AVCaptureDeviceInput(device: captureDevice!)
captureSession.addInput(captureDeviceInput)
} catch let error {
print(error.localizedDescription)
}
let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer.name = "CameraPreview"
previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
previewLayer.backgroundColor = UIColor.black.cgColor
self.previewLayer = previewLayer
let dataOutput = AVCaptureVideoDataOutput()
dataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String:kCVPixelFormatType_32BGRA]
if captureSession.canAddOutput(dataOutput) {
captureSession.addOutput(dataOutput)
}
captureSession.commitConfiguration()
}
func startSettion() {
if captureSession.isRunning { return }
captureSession.startRunning()
}
func stopSettion() {
if !captureSession.isRunning { return }
captureSession.stopRunning()
}
}
まとめ
iPhoneのカメラ映像をアプリに表示するところまでを実装しました。
UIViewControllerRepresentable
でCALayer
を表示するViewをSwiftUIで扱えるようにしました。
AVCaptureSessionを作成し、AVCaptureDeviceでカメラを選択してAVCaptureDeviceInputを通してSessionに登録する方法を解説しました。
AVCaptureVideoPreviewLayerではvideoGravityプロパティでアスペクト比表示の指定ができるので、アスペクト比を保ったまま全画面表示をするresizeAspectFillを指定しました。
次回はカメラ映像から画像キャプチャを取得する方法を実装します。
参考
- Cameras and Media Capture | Apple Developer Documentation
- SwiftUIでAVFoundationを使ってみた - Qiita
- Setting Up a Capture Session | Apple Developer Documentation
画像
- Robert LischkaによるPixabayからの画像
- https://www.anthonyboyd.graphics/mockups/floating-iphone-11-pro-max-mockup/
宣伝
インプレスR&D社より、「1人でアプリを作る人を支えるSwiftUI開発レシピ」発売中です。
「SwiftUIでアプリを作る!」をコンセプトにSwiftUI自体の解説とそれを組み合わせた豊富なサンプルアプリでどんな風にアプリ実装すればいいかが理解できる本となっています。
iOS 14対応、Widgetの作成も一章まるまるハンズオンで解説しています。
ぜひどうぞ。