Quadrophenia

R社の新人エンジニア数人が更新しているブログです

【Swift】GPUImageで動画にフィルターをかけて保存する

8月も半ばを過ぎましたね。
現在10月のリリースを目指して仲間と作っているアプリがありまして、今年の夏休みはほぼ全ての時間をその開発に捧げました。
iosアプリをSwiftで書いているのですが、まだ世に出て1年ほどの言語ということもあって、少し込み入ったことをやろうとするとまとまった日本語の参考記事がほとんど無いという感じです。
で、今回はいつもお世話になっているSwiftコミュニティに貢献するべく、動画関連の処理について書こうと思います。

日本でもこれからもっと動画系のサービスが一般化していくだろうと思います。
よくある、「動画を撮影」→「プレビューしながらフィルターを選択」→「選択されたフィルターを合成して動画を保存」という処理を作ったのですが、結構量が多いので今回は一番需要がありそうな動画にフィルターをかけて保存する部分について紹介します。
今回はGPUImageというフレームワークを使っています。
これは、デフォルトでイケてるフィルターが大量にクラスとして含まれている上に、オリジナルのフィルターもすぐに作れてしまうという優れもののフレームワークです。

僕はcocoapodsで導入しましたが、直接ファイルをプロジェクトに追加するなどの方法もあるので、好きな方法でやると良いと思います。
プロジェクトフォルダ内のPodfileに下記を書いて、

use_frameworks!
pod 'GPUImage'
$pod install

を実行するだけなので超お手軽です。

導入が終わったら早速コードを書いていきます。
まずはインポートします。SwiftFilePathはファイルの存在を確認したり、削除するために使いました。

import GPUImage
import SwiftFilePath

viewDidLoadの中で色々とやっていきます。
まずはGPUImageの動画読み込みと書き込みのためのインスタンスを定義します。

var movieWriter:GPUImageMovieWriter!//書き込み用
var movieFile:GPUImageMovie!//読み込み用

保存先のパス指定などを行います。

let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)//アプリからアクセスできるディレクトリを取得
let documentsDirectory = paths[0] as! String
let filePath : String? = "\(documentsDirectory)/temp.mp4"//フィルターをかけたい動画までのパス、あらかじめこちらに動画を用意しておいてください
let mediaURL : NSURL = NSURL(fileURLWithPath: filePath!)!
        
//表示用ビュー、プレビューが必要ない場合は無しでもOKです
var fileView = GPUImageView()
fileView.frame = CGRectMake(0, 0, 480, 480)
        
//一時保存用パス、フォトアルバムに保存する前に動画を置いておく場所です
let newFilePath = "\(documentsDirectory)/newTemp.mp4"
        
 //一時保存先を空にしておく
var contentsPath = Path(documentsDirectory)
for path in contentsPath.contents! {
      if(path.toString() == newFilePath){
            path.remove()
       }
}

さらに、保存する動画の設定を定義していきます。

let movieURL = NSURL(fileURLWithPath: newFilePath!)!
        
self.movieWriter = GPUImageMovieWriter(movieURL: movieURL, size: CGSize(width: 480, height: 480),fileType:AVFileTypeMPEG4, outputSettings:nil)//保存する動画のサイズ
self.movieWriter.assetWriter.movieFragmentInterval = kCMTimeInvalid//これ書かないと動作が不安定になります
self.movieWriter.shouldPassthroughAudio = true
movieWriter.encodingLiveVideo = true

あとはフィルターと動画を関連付けていきます。

movieFile = GPUImageMovie(URL: mediaURL)
movieFile.playAtActualSpeed = true
       
let filter = GPUImageSepiaFilter()//かけたいフィルターを定義
filter.addTarget(fileView)

self.view.addSubview(fileView)//プレビュー画面を追加
movieFile.addTarget(filter)
filter.addTarget(movieWriter)

あとは、動画の再生時と終了時に下記のメソッドを呼び出してあげればOKです。

//再生時
movieFile.startProcessing()
movieWriter.startRecording()

//終了時
movieWriter!.finishRecording()
UISaveVideoAtPathToSavedPhotosAlbum(filePath, self, Selector("filePath:didFinishSavingWithError:contextInfo:"), nil)

こまごまと書いてきましたが、意外と簡単ですよね。動画のリアルタイム処理がたったこれだけのコードで実現できてしまいます。
フレームワークの力はすごいですね。

その他、ボタンやプログレスバーを追加したサンプルコードを下に貼っておきます。
そのままコピペすれば動くと思います。xcode6.4,Swift1.2で確認しました。

import UIKit
import GPUImage
import AssetsLibrary
import SwiftFilePath
import SpriteKit
import MBCircularProgressBar


class ExistsVideoFilterViewController: UIViewController {

    var filterNum: Int = 0
    let filters = [
        GPUImageSepiaFilter(),
        GPUImagePolkaDotFilter(),
        GPUImageGrayscaleFilter()
    ]
    
    var filter: GPUImageFilter!
    
    var movieWriter:GPUImageMovieWriter!
    var movieFile:GPUImageMovie!
    var newFilePath : String!
    
    private var myButtonStart : UIButton!
    private var myButtonStop : UIButton!
    
    var barView = MBCircularProgressBarView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        filter = filters[filterNum]
        
        let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
        let documentsDirectory = paths[0] as! String
        let filePath : String? = "\(documentsDirectory)/temp.mp4"
        let mediaURL : NSURL = NSURL(fileURLWithPath: filePath!)!
        
        //表示用ビュー
        var fileView = GPUImageView()
        fileView.frame = CGRectMake(0, 0, 480, 480)
        
        //保存用パス
        newFilePath = "\(documentsDirectory)/newTemp.mp4"
        
        //一時保存先を空にしておく
        var contentsPath = Path(documentsDirectory)
        for path in contentsPath.contents! {
            if(path.toString() == newFilePath){
                path.remove()
            }
        }
        let movieURL = NSURL(fileURLWithPath: newFilePath!)!
        
        self.movieWriter = GPUImageMovieWriter(movieURL: movieURL, size: CGSize(width: 480, height: 480),fileType:AVFileTypeMPEG4, outputSettings:nil)
        //"Couldn't write a frame" エラーが出ないための一行
        self.movieWriter.assetWriter.movieFragmentInterval = kCMTimeInvalid
        self.movieWriter.shouldPassthroughAudio = true
        movieWriter.encodingLiveVideo = true
        
        
        movieFile = GPUImageMovie(URL: mediaURL)
        movieFile.playAtActualSpeed = true
        //movieFile.audioEncodingTarget = movieWriter!
       
        //フィルター関連
        filter.addTarget(fileView)
        //self.view.addSubview(fileView)
        movieFile.addTarget(filter)
        filter.addTarget(movieWriter)
        
        makeButton()
        makeBar()
        
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    

    func onClickStartButton() {
        movieFile.startProcessing()
        movieWriter.startRecording()
        println("started")
        var timer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: Selector("printProgress"), userInfo: nil, repeats: false)
    }
    
    func printProgress(){
        //規定のレンダリング時間に達するまでは0.5秒に一度進捗を監視する
        barView.percent = CGFloat(movieFile.progress*100)
        println(movieFile.progress)
        if(movieFile.progress >= 1){
            onClickStopButton()
        }else{
            var timer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: Selector("printProgress"), userInfo: nil, repeats: false)
        }
    }
    
    func onClickStopButton() {
        filter.removeTarget(self.movieWriter!)
        movieWriter!.finishRecording()
        savePhotoAlbum(newFilePath)
        println("completed")
    }
    
    func savePhotoAlbum(filePath:String!){
        
        UISaveVideoAtPathToSavedPhotosAlbum(filePath, self, Selector("filePath:didFinishSavingWithError:contextInfo:"), nil)
    }
    
    func filePath(filePath: String!, didFinishSavingWithError: NSError, contextInfo:UnsafePointer<Void>)       {
        
        if let error = didFinishSavingWithError as NSError? {
            println("error")
        }
        else{
            let removePath = Path(newFilePath)
            removePath.remove()
            println("success!")
        }
    }
    
    func makeButton(){
        
        // UIボタンを作成.
        myButtonStart = UIButton(frame: CGRectMake(0,0,120,50))
        myButtonStop = UIButton(frame: CGRectMake(0,0,120,50))
        
        myButtonStart.backgroundColor = UIColor.redColor();
        myButtonStop.backgroundColor = UIColor.grayColor();
        
        myButtonStart.layer.masksToBounds = true
        myButtonStop.layer.masksToBounds = true
        
        myButtonStart.setTitle("記録", forState: .Normal)
        myButtonStop.setTitle("停止", forState: .Normal)
        
        myButtonStart.layer.cornerRadius = 20.0
        myButtonStop.layer.cornerRadius = 20.0
        
        myButtonStart.layer.position = CGPoint(x: self.view.bounds.width/2 - 70, y:self.view.bounds.height-50)
        myButtonStop.layer.position = CGPoint(x: self.view.bounds.width/2 + 70, y:self.view.bounds.height-50)
        
        myButtonStart.addTarget(self, action: "onClickStartButton", forControlEvents: .TouchUpInside)
        myButtonStop.addTarget(self, action: "onClickStopButton", forControlEvents: .TouchUpInside)
        
        // UIボタンをViewに追加.
        self.view.addSubview(myButtonStart);
        self.view.addSubview(myButtonStop);
        
    }
    
    func makeBar(){
        //プログレスバー
        barView.percent = 0
        barView.fontColor = UIColor.orangeColor()
        barView.progressRotationAngle = 0
        barView.progressAngle = 100
        barView.progressLineWidth = 10
        barView.progressColor = UIColor.redColor()
        barView.progressStrokeColor = UIColor.redColor()
        barView.emptyLineColor = UIColor.orangeColor()
        barView.emptyLineWidth = 10
        barView.backgroundColor = UIColor.clearColor()
        barView.frame = CGRectMake(self.view.bounds.width/2-100, self.view.bounds.height/2-200, 200, 200)
        self.view.addSubview(barView)
        
    }

}

以上っす!
引き続き開発頑張りますので、リリースの暁には是非使ってみてください!