Swift で動画を撮影・保存するサンプル その２ - Vine 風のビデオを撮る


Swift で Vine 風の細切れにした動画を撮るサンプルを作ってみました。動作するサンプルコードは下部にリンクを置いています。

一部コードを抜粋しながらやっていることをまとめてみました。

# iOS アプリで動画を保存する方法

大きく２種類あります。(他にもあったら教えて下さい)

## AVCaptureMovieFileOutput を使う

こっちはシンプルです、保存先を指定して１つのメソッドを呼び出すだけです。下記記事を参考にしてください。

[Swift で動画を撮影・保存するサンプル その１ - シンプル編](http://qiita.com/takecian/items/2cee0f958c8bed00a69a "")

## AVAssetWriter を使う

今回の Vine 風のビデオを撮る、などの細かいことをしたい場合は AVAssetWriter を使います。

# 概要

やっていることの流れはこんな感じです。

1. 必要な framework の追加
1. Session, Device の準備
1. プレビュー画面の生成
1. AVAssetWriter の準備
1. Pause/Resume(細切れにした動画を作るため) の対応
1. ライブラリに追加

## 必要な framework の追加

AVFoundation, AssetsLibrary の 2 つです。

![スクリーンショット 2014-11-13 16.46.39.png](https://qiita-image-store.s3.amazonaws.com/0/27210/3a9c2782-e6c8-45f3-dd11-c494cc476937.png "スクリーンショット 2014-11-13 16.46.39.png")

## Session, Device の準備 

iOS では動画を AVCaptureSession で扱います。ビデオ、オーディオの入力を AVCaptureDevice, AVCaptureDeviceInput として扱います。

```
    let captureSession = AVCaptureSession()
    let videoDevice = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
    let audioDevice = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeAudio)

    self.videoDevice.activeVideoMinFrameDuration = CMTimeMake(1, 30)
    let videoInput = AVCaptureDeviceInput.deviceInputWithDevice(self.videoDevice, error: nil) as AVCaptureDeviceInput
    self.captureSession.addInput(videoInput)

    let audioInput = AVCaptureDeviceInput.deviceInputWithDevice(self.audioDevice, error: nil)  as AVCaptureDeviceInput
    self.captureSession.addInput(audioInput);
```

出力はビデオ、オーディオでそれぞれ AVCaptureVideoDataOutput、 AVCaptureAudioDataOutput というクラスで扱います。

これらのインスタンスを AVCaptureSession に渡したあと、startRunning を呼び出します。

```
    var videoDataOutput = AVCaptureVideoDataOutput()
    videoDataOutput.setSampleBufferDelegate(self, queue: self.recordingQueue)
    videoDataOutput.alwaysDiscardsLateVideoFrames = true
    videoDataOutput.videoSettings = [
        kCVPixelBufferPixelFormatTypeKey : kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
    ]
    self.captureSession.addOutput(videoDataOutput)
        
    var audioDataOutput = AVCaptureAudioDataOutput(
    audioDataOutput.setSampleBufferDelegate(self, queue: self.recordingQueue)
    self.captureSession.addOutput(audioDataOutput)
        
    self.captureSession.startRunning()
```

## 流れてくるデータを扱う

startRunning() を呼び出したあと、AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate を介してカメラ、マイクから得たビデオ、オーディオが流れてきます。

具体的には下記メソッドの sampleBuffer にビデオ、オーディオが入っています。

```流れてくるデータを扱う
func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!) {
        if !self.isCapturing || self.isPaused { // 録画開始していない、ポーズ中の場合
            return
        }
            
        // ここで保存する処理
}
```

このメソッドは startRunning() を実行したあとから呼び出されるようになるので、いつデータを保存するかはアプリ側で判断しないといけません。

そのためアプリ側で録画中、ポーズ中などの状態を持ち、必要な時のみ保存する処理を行います。

# プレビュー画面の生成

下記エントリで書いています。
[Swift で動画を撮影・保存するサンプル その１ - シンプル編](http://qiita.com/takecian/items/2cee0f958c8bed00a69a "")

# AVAssetsWriter の準備

## AVAssetsWriter の生成 

ビデオ用の AVAssetWriterInput とオーディオ用の AVAssetWriterInput を作って AVAssetWriter に追加します。
出力時のフォーマットは outputSettings に NSDictionary 形式で指定します。

サンプルではビデオは H264, オーディオは AAC にしています。

```AVAssetsWriterの生成
        self.fileWriter = AVAssetWriter(URL: fileUrl, fileType: AVFileTypeQuickTimeMovie, error: nil)
        
        let videoOutputSettings: Dictionary<String, AnyObject> = [
            AVVideoCodecKey : AVVideoCodecH264,
            AVVideoWidthKey : width,
            AVVideoHeightKey : height
        ];
        self.videoInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: videoOutputSettings)
        self.videoInput.expectsMediaDataInRealTime = true
        self.fileWriter.addInput(self.videoInput)
        
        let audioOutputSettings: Dictionary<String, AnyObject> = [
            AVFormatIDKey : kAudioFormatMPEG4AAC,
            AVNumberOfChannelsKey : channels,
            AVSampleRateKey : samples,
            AVEncoderBitRateKey : 128000
        ]
        self.audioInput = AVAssetWriterInput(mediaType: AVMediaTypeAudio, outputSettings: audioOutputSettings)
        self.audioInput.expectsMediaDataInRealTime = true
        self.fileWriter.addInput(self.audioInput)
```

## 実際に書きだす準備

実際に書きだす部分はシンプルです。ビデオ、オーディオの AVAssetWriterInput インスタンスに流れてきたデータ(CMSampleBufferRef)を appendSampleBuffer します。

```書きだす準備
    func write(sample: CMSampleBufferRef, isVideo: Bool){
        if CMSampleBufferDataIsReady(sample) != 0 {
            ...
            if isVideo {
                if self.videoInput.readyForMoreMediaData {
                    self.videoInput.appendSampleBuffer(sample)
                }
            }else{
                if self.audioInput.readyForMoreMediaData {
                    self.audioInput.appendSampleBuffer(sample)
                }
            }
        }
    }

```

# Pause/Resume(細切れにした動画を作るため) の対応

ここまでで動画を撮影・保存することはできるようになりました。

Vine 風(細切れの動画を繋げたもの)にするには、

* 録画中の動画データをファイルに書き込む
* ポーズ中の動画データはファイルに書き込まずに破棄する

をすればいいのですが少し工夫が必要です。

というのも途切れた動画データの間では TimeStamp が連続しなくなっているので、そこの差分を吸収してあげる必要があるからです。

### ここでちょっとメモ

動画の再生については iOS でも 他のプラットフォームでも基本的にオーディオが時間軸の基準になっています。
そのためオーディオの時刻データを元に調整を行います。

動画の再生で用いられる時間データに PTS (Presentation Time Stamp) というものがあります。
その名の通り「このデータをいつ表示(再生)するか」という情報です。
この PTS が正しく入っていないと(値が大きく飛んでいたり)、正しく再生されません。
＃他にもビデオには DTS (Decode Time Stamp) というものもあります。

ここでは、

1. オーディオを保存する際、最後に保存したオーディオの PTS を覚えておく
1. pause(一度録画を止める)
1. resume(録画を再開する)
1. 最後に保存したオーディオの PTS とこれから保存するオーディオの PTS の差分を吸収する

という処理を入れます。

```差分を計算する処理
            if self.isDiscontinue { // 一度 pause していた(データが非連続になっている)場合
                if isVideo {
                    return
                }

                var pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)

                let isAudioPtsValid = self.lastAudioPts!.flags & CMTimeFlags.Valid // PTS(audio) が有効か
                if isAudioPtsValid.rawValue != 0 {

                    let isTimeOffsetPtsValid = self.timeOffset.flags & CMTimeFlags.Valid // PTS(offset) が有効か
                    if isTimeOffsetPtsValid.rawValue != 0 {
                        pts = CMTimeSubtract(pts, self.timeOffset);
                    }
                    let offset = CMTimeSubtract(pts, self.lastAudioPts!); // 差分を計算

                    if (self.timeOffset.value == 0) // 差分を保持
                    {
                        self.timeOffset = offset;
                    }
                    else
                    {
                        self.timeOffset = CMTimeAdd(self.timeOffset, offset);
                    }
                }
                self.lastAudioPts!.flags = CMTimeFlags.allZeros
                self.isDiscontinue = false
            }

```

差分を計算したら、データを書き出す前に PTS を調整する処理を入れます。

```
        var buffer = sampleBuffer
        if self.timeOffset.value > 0 {
            buffer = self.ajustTimeStamp(sampleBuffer, offset: self.timeOffset)
        }
```
```差分を修正する処理
    func ajustTimeStamp(sample: CMSampleBufferRef, offset: CMTime) -> CMSampleBufferRef {
        var count: CMItemCount = 0
        CMSampleBufferGetSampleTimingInfoArray(sample, 0, nil, &count);
        var info = [CMSampleTimingInfo](count: count, 
                                        repeatedValue: CMSampleTimingInfo(duration: CMTimeMake(0, 0), 
                                                                          presentationTimeStamp: CMTimeMake(0, 0),
                                                                          decodeTimeStamp: CMTimeMake(0, 0)))
        CMSampleBufferGetSampleTimingInfoArray(sample, count, &info, &count);

        for i in 0..<count {
            info[i].decodeTimeStamp = CMTimeSubtract(info[i].decodeTimeStamp, offset);
            info[i].presentationTimeStamp = CMTimeSubtract(info[i].presentationTimeStamp, offset);
        }

        var out: Unmanaged<CMSampleBuffer>?
        CMSampleBufferCreateCopyWithNewTiming(nil, sample, count, &info, &out);
        return out!.takeRetainedValue()
    }

```

# ライブラリに追加

録画が終わったらライブラリに移します。下記エントリで書いています。
[Swift で動画を撮影・保存するサンプル その１ - シンプル編](http://qiita.com/takecian/items/2cee0f958c8bed00a69a "")

# おわり

以上で Vine 風なビデオが撮れるようになりました。

## ソースコードはこちら

XCode 6.1 iOS 8.1 で確認しています。

[github.com/takecian/VineVideo](https://github.com/takecian/VineVideo "")