Swift で Vine 風の細切れにした動画を撮るサンプルを作ってみました。動作するサンプルコードは下部にリンクを置いています。
一部コードを抜粋しながらやっていることをまとめてみました。
iOS アプリで動画を保存する方法
大きく2種類あります。(他にもあったら教えて下さい)
AVCaptureMovieFileOutput を使う
こっちはシンプルです、保存先を指定して1つのメソッドを呼び出すだけです。下記記事を参考にしてください。
Swift で動画を撮影・保存するサンプル その1 - シンプル編
AVAssetWriter を使う
今回の Vine 風のビデオを撮る、などの細かいことをしたい場合は AVAssetWriter を使います。
概要
やっていることの流れはこんな感じです。
- 必要な framework の追加
- Session, Device の準備
- プレビュー画面の生成
- AVAssetWriter の準備
- Pause/Resume(細切れにした動画を作るため) の対応
- ライブラリに追加
必要な framework の追加
AVFoundation, AssetsLibrary の 2 つです。
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 で動画を撮影・保存するサンプル その1 - シンプル編
AVAssetsWriter の準備
AVAssetsWriter の生成
ビデオ用の AVAssetWriterInput とオーディオ用の AVAssetWriterInput を作って AVAssetWriter に追加します。
出力時のフォーマットは outputSettings に NSDictionary 形式で指定します。
サンプルではビデオは H264, オーディオは AAC にしています。
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) というものもあります。
ここでは、
- オーディオを保存する際、最後に保存したオーディオの PTS を覚えておく
- pause(一度録画を止める)
- resume(録画を再開する)
- 最後に保存したオーディオの 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 で動画を撮影・保存するサンプル その1 - シンプル編
おわり
以上で Vine 風なビデオが撮れるようになりました。
ソースコードはこちら
XCode 6.1 iOS 8.1 で確認しています。