[iOS] バーストカメラ（連射カメラ）を実装してみる

# はじめに
以前AVFundationを利用した[カメラの実装サンプル](http://qiita.com/eKushida/items/a9f68daa0518998f2c50)を書いた際、
バースト（連射）カメラの実装サンプルも書いてほしいと
要望がありましたので、纏めました。

完成版は、[Github](https://github.com/stv-ekushida/ios-burst-camera-demo)を御覧ください。

# 開発環境
開発環境は、下記のとおりです。

| category | Version | 
|---|---| 
| XCode | 8.2 | 
| iOS | 10.0〜 | 
| Swift | 3.0.2 | 

# 登場人物
バースト（連射）カメラで利用する主なクラスは、下記のとおりです。

| クラス名 | 説明 |
|---|---|
|AVCaptureDeviceInput | 入力デバイスとして、全面カメラ、背面カメラなどを設定する|
|AVCaptureSession | カメラ撮影用のセッション?? |
|AVCapturePhotoOutput | 出力に、静止画を設定する |
|AVCaputreVideoOutput | 出力に、動画フレームデータを設定する |


※iOS10では、AVCaptureStillImageOutputが廃止になり、
AVCapturePhotoOutputを使います。

# 実装手順
1. カメラの利用許可の設定
2. 入出力の初期化および、接続
3. セッションの開始/終了
4. フレームごとに画像を取得
5. 使ってみる

# 1. カメラの利用許可の設定

```xml:Info.plist
<key>NSCameraUsageDescription</key>
<string>カメラへアクセスするために必要です</string>
 ```

# 2. 入出力の初期化および、接続
## 2.1. セッションのインスタンス化

```swift:BurstCameraUtil.swift
    private let session = AVCaptureSession()
```    

## 2.2. 入力デバイスの初期化

```swift:BurstCameraUtil.swift

	func findDevice(position: AVCaptureDevicePosition) -> AVCaptureDevice? {
        
        let device = AVCaptureDevice.defaultDevice(withDeviceType: .builtInWideAngleCamera,
                                                   mediaType: AVMediaTypeVideo,
                                                   position: position)
        
        device?.activeVideoMinFrameDuration = CMTimeMake(1, 30)
        return device
    }
```    

## 2.3. 入力デバイスの接続

```swift:BurstCameraUtil.swift    
    func createVideoPreviewLayer(device: AVCaptureDevice?) -> AVCaptureVideoPreviewLayer?{
        
        let videoInput = try! AVCaptureDeviceInput.init(device: device)
        
        if (session.canAddInput(videoInput)) {
            session.addInput(videoInput)
        }
        
        if (session.canAddOutput(photoOutput)) {
            session.addOutput(photoOutput)
        }
        
        return AVCaptureVideoPreviewLayer.init(session: session)
    }
```

## 2.4. 出力デバイスの初期化

```swift:BurstCameraUtil.swift

    private let photoOutput = AVCapturePhotoOutput()
    private let videoDataOutput = AVCaptureVideoDataOutput()
```

## 2.5. 出力デバイスの接続

```swift:BurstCameraUtil.swift
func createVideoDataOutput() {
        
        videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as AnyHashable : Int(kCVPixelFormatType_32BGRA)]
        videoDataOutput.alwaysDiscardsLateVideoFrames = true
        videoDataOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)
        
        session.addOutput(videoDataOutput)
        session.sessionPreset = AVCaptureSessionPreset1920x1080
    }
```

# 3. セッションの開始/終了
## 3.1. セッションの開始

```swift:BurstCameraUtil.swift

	func startRunning() {
        session.startRunning()
    }

```

## 3.2. セッションの終了
```swift:BurstCameraUtil.swift  

    func stopRunning() {
        session.stopRunning()
    }
```

# 4. フレームごとに画像を取得
## 4.1. 写真を保存するための配列等を準備する

```swift:BurstCameraUtil.swift
    fileprivate var images:[UIImage] = []
    fileprivate var isShooting = false
    fileprivate var counter = 0
```

## 4.2. AVCaptureVideoDataOutputSampleBufferDelegate

```swift:BurstCameraUtil.swift
extension BurstCameraUtil: AVCaptureVideoDataOutputSampleBufferDelegate {
    
    func captureOutput(_ captureOutput: AVCaptureOutput!,
                       didOutputSampleBuffer sampleBuffer: CMSampleBuffer!,
                       from connection: AVCaptureConnection!) {
        
        if counter % 3 == 0 { // 1/10秒だけ処理する
            if isShooting {
                let image = imageFromSampleBuffer(sampleBuffer: sampleBuffer)
                images.append(image)
            }
        }
        counter += 1
    }
    
    private func imageFromSampleBuffer(sampleBuffer :CMSampleBuffer) -> UIImage {
        
        let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)!
        
        CVPixelBufferLockBaseAddress(imageBuffer,
                                     CVPixelBufferLockFlags(rawValue: 0))
        
        let base = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0)!
        let bytesPerRow = UInt(CVPixelBufferGetBytesPerRow(imageBuffer))
        let width = UInt(CVPixelBufferGetWidth(imageBuffer))
        let height = UInt(CVPixelBufferGetHeight(imageBuffer))
        
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bitsPerCompornent = 8
        let bitmapInfo = CGBitmapInfo(rawValue: (CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue) as UInt32)
        let newContext = CGContext(data: base,
                                   width: Int(width),
                                   height: Int(height),
                                   bitsPerComponent: Int(bitsPerCompornent),
                                   bytesPerRow: Int(bytesPerRow),
                                   space: colorSpace,
                                   bitmapInfo: bitmapInfo.rawValue)! as CGContext
        
        let imageRef = newContext.makeImage()!
        let image = UIImage(cgImage: imageRef,
                            scale: 1.0,
                            orientation: UIImageOrientation.right)
        
        CVPixelBufferUnlockBaseAddress(imageBuffer,
                                       CVPixelBufferLockFlags(rawValue: 0))
        return image
    }
}
```

## 4.3. 連射開始、終了、連射写真の画像リスト

```swift:BurstCameraUtil.swift
    func start() {
        images = []
        isShooting = true
    }
    
    func stop() {
        isShooting = false
    }
    
    func photos() -> [UIImage] {
        return images
    }
```   

#5. 使ってみる
UI周りは、割愛します。


```swift:ViewController.swift
import UIKit
import AVFoundation

final class ViewController: UIViewController {
    
    @IBOutlet weak var baseView: UIView!
    
    private var camera: CameraTakeable?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupCameraView(camera: BurstCameraUtil())
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        camera?.startRunning()
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        camera?.stopRunning()
        camera?.removeVideoInput()
    }
    
    private func setupCameraView(camera: CameraTakeable?) {
        
        self.camera = camera
        
        let device = camera?.findDevice(position: .back)
        camera?.createVideoDataOutput()
        
        if let videoLayer = camera?.createVideoPreviewLayer(device: device) {            
            videoLayer.frame = baseView.bounds
            videoLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
            baseView.layer.addSublayer(videoLayer)
        } else {
            fatalError("VideoLayer is Nil")
        }
    }
    
    //MARK:- Actions
    @IBAction func photoDidTapDown(_ sender: UIButton) {
        camera?.start()
    }
    
    @IBAction func photoDidTapUp(_ sender: UIButton) {
        camera?.stop()
        
        if let images = camera?.photos() {
            print("I took \(images.count) photos")
        }
    }
}
```

# まとめ
完成版は、[Github](https://github.com/stv-ekushida/ios-burst-camera-demo)を御覧ください。

カメラを利用して、様々な出力が出来ますね。

| クラス名 | 説明 |
|---|---|
| AVCaptureMovieFileOutput| 動画ファイル | 
| AVCaptureAudioFileOutput | 音声ファイル | 
| AVCaptureVideoDataOutput| 動画フレームデータ | 
| AVCaptureAudioDataOutput| 音声データ | 
| AVCapturePhotoOutput| 静止画  | 
| AVCaptureMetadataOutput| メタデータ  |