【Swift 3】UIImageの配列から動画を生成する【AVAssetWriter】

どもも。

UIImageから動画生成がやっとある程度形になったので紹介していきます。

といっても詳しくは端折り、コードと行き詰まりポイント程度にしておきます。

なのでまずは全体コードから。

 

全体コード

MovieCreator.swift

import Foundation
import AVFoundation
import UIKit

class MovieCreator: NSObject {
    
    override init() {
        
        super.init()
    }
    
    func create(images:[UIImage],size:CGSize,time:Int) -> URL{
        
        //保存先のURL
        let url = NSURL(fileURLWithPath:NSTemporaryDirectory()).appendingPathComponent("\(NSUUID().uuidString).mp4")
        // AVAssetWriter
        guard let videoWriter = try? AVAssetWriter(outputURL: url!, fileType: AVFileTypeQuickTimeMovie) else {
            fatalError("AVAssetWriter error")
        }
        
        //画像サイズを変える
        let width = size.width
        let height = size.height
        
        // AVAssetWriterInput
        let outputSettings = [
            AVVideoCodecKey: AVVideoCodecH264,
            AVVideoWidthKey: width,
            AVVideoHeightKey: height
            ] as [String : Any]
        let writerInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: outputSettings as [String : AnyObject])
        videoWriter.add(writerInput)
        
        // AVAssetWriterInputPixelBufferAdaptor
        let adaptor = AVAssetWriterInputPixelBufferAdaptor(
            assetWriterInput: writerInput,
            sourcePixelBufferAttributes: [
                kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32ARGB),
                kCVPixelBufferWidthKey as String: width,
                kCVPixelBufferHeightKey as String: height,
                ]
        )
        
        writerInput.expectsMediaDataInRealTime = true
        
        // 動画の生成開始
        
        // 生成できるか確認
        if (!videoWriter.startWriting()) {
            // error
            print("error videoWriter startWriting")
        }
        
        // 動画生成開始
        videoWriter.startSession(atSourceTime: kCMTimeZero)
        
        // pixel bufferを宣言
        var buffer: CVPixelBuffer? = nil
        
        // 現在のフレームカウント
        var frameCount = 0
        
        // 各画像の表示する時間
        let durationForEachImage = time
        
        // FPS
        let fps: __int32_t = 60
        
        // 全画像をbufferに埋め込む
        for image in images {
            
            if (!adaptor.assetWriterInput.isReadyForMoreMediaData) {
                break
            }
            
            // 動画の時間を生成(その画像の表示する時間/開始時点と表示時間を渡す)
            let frameTime: CMTime = CMTimeMake(Int64(__int32_t(frameCount) * __int32_t(durationForEachImage)), fps)
            //時間経過を確認(確認用)
            let second = CMTimeGetSeconds(frameTime)
            print(second)
            
            let resize = resizeImage(image: image, contentSize: size)
            // CGImageからBufferを生成
            buffer = self.pixelBufferFromCGImage(cgImage: resize.cgImage!)
            
            // 生成したBufferを追加
            if (!adaptor.append(buffer!, withPresentationTime: frameTime)) {
                // Error!
                print("adaptError")
                print(videoWriter.error!)
            }
            
            frameCount += 1
        }
        
        // 動画生成終了
        writerInput.markAsFinished()
        videoWriter.endSession(atSourceTime: CMTimeMake(Int64((__int32_t(frameCount)) *  __int32_t(durationForEachImage)), fps))
        videoWriter.finishWriting(completionHandler: {
            // Finish!
            print("movie created.")
        })
        
        return url!
        
    }
    
    func pixelBufferFromCGImage(cgImage: CGImage) -> CVPixelBuffer {
        
        let options = [
            kCVPixelBufferCGImageCompatibilityKey as String: true,
            kCVPixelBufferCGBitmapContextCompatibilityKey as String: true
        ]
        
        var pxBuffer: CVPixelBuffer? = nil
        
        let width = cgImage.width
        let height = cgImage.height
        
        CVPixelBufferCreate(kCFAllocatorDefault,
                            width,
                            height,
                            kCVPixelFormatType_32ARGB,
                            options as CFDictionary?,
                            &pxBuffer)
        
        CVPixelBufferLockBaseAddress(pxBuffer!, CVPixelBufferLockFlags(rawValue: 0))
        
        let pxdata = CVPixelBufferGetBaseAddress(pxBuffer!)
        
        let bitsPerComponent: size_t = 8
        let bytesPerRow: size_t = 4 * width
        
        let rgbColorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB()
        let context = CGContext(data: pxdata,
                                width: width,
                                height: height,
                                bitsPerComponent: bitsPerComponent,
                                bytesPerRow: bytesPerRow,
                                space: rgbColorSpace,
                                bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue)
        
        context?.draw(cgImage, in: CGRect(x:0, y:0, width:CGFloat(width),height:CGFloat(height)))
        
        CVPixelBufferUnlockBaseAddress(pxBuffer!, CVPixelBufferLockFlags(rawValue: 0))
        
        return pxBuffer!
    }
    
    private func resizeImage(image:UIImage,contentSize:CGSize) -> UIImage{
        // リサイズ処理
        let origWidth  = Int(image.size.width)
        let origHeight = Int(image.size.height)
        var resizeWidth:Int = 0, resizeHeight:Int = 0
        if (origWidth < origHeight) {
            resizeWidth = Int(contentSize.width)
            resizeHeight = origHeight * resizeWidth / origWidth
        } else {
            resizeHeight = Int(contentSize.height)
            resizeWidth = origWidth * resizeHeight / origHeight
        }
        
        let resizeSize = CGSize(width:CGFloat(resizeWidth), height:CGFloat(resizeHeight))
        UIGraphicsBeginImageContext(resizeSize)
        
        image.draw(in: CGRect(x:0,y: 0,width: CGFloat(resizeWidth), height:CGFloat(resizeHeight)))
        
        let resizeImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
    
        return resizeImage!
    }
    
}

 

呼び出したい場所(適当なViewController)

let movie = MovieCreator()
//16の倍数
let movieSize = CGSize(width:480,height:640)
// time / 60 秒表示する
let time = 1
//動画を生成
let movieURL = movie.create(imageData:imageArray,size:movieSize,time:time)

 

あとはつまずきポイントの説明だけしたいと思います。

動画の保存先URL

最初に動画をどこに書き出すかを指定しています。

        //保存先のURL
        let url = NSURL(fileURLWithPath:NSTemporaryDirectory()).appendingPathComponent("\(NSUUID().uuidString).mp4")
        // AVAssetWriter
        guard let videoWriter = try? AVAssetWriter(outputURL: url!, fileType: AVFileTypeQuickTimeMovie) else {
            fatalError("AVAssetWriter error")
        }

これがまた色々やったんですが、上手くできず・・・。

上記コードの大部分が「複数枚のUIImageから動画を生成したい」を参考にさせていただいているのですが、うまいこと解決できず、結局こんな感じになりました。

 

1フレームの長さ

1枚の写真をどれだけの時間表示するか、ということなのですが、なんか結構はまりました。

この辺です。

            // 動画の時間を生成(その画像の表示する時間/開始時点と表示時間を渡す)
            let frameTime: CMTime = CMTimeMake(Int64(__int32_t(frameCount) * __int32_t(durationForEachImage)), fps)
            //時間経過を確認(確認用)
            let second = CMTimeGetSeconds(frameTime)
            print(second)

 

このCMTimeは、「[Swift]CMTimeを使う」を参考にさせていただきなんとか解決。というか使い方をよくわかっていなかっただけでした。

CMTime(a,b)は a / b 秒を表しています。

durationForEachImage = 1

fps = 60とします。

1枚目の画像 0(frameCount) × 1  / 60 = 0 なので開始位置は0

2枚目の画像 1(frameCount) × 1 / 60 = 0.016 なので開始位置は0.016秒

3枚目の画像 2(frameCount) × 1 / 60 = 0.033 なので開始位置は 0.033秒

といった風になります。

この a / b を1 / 1とした場合は、1秒に1枚画像を表示します。

実際に時間がどのように変わっているのか見る場合はCMTimeGetSeconds() で確認できます。

 

動画のサイズ

今回一番ハマりました。まじでわけがわからんかった・・・

結論からいうと、画像の横幅は16の倍数でなければなりません。

それに気づかないとこんなことになります。

めっちゃずれてます。じゃみじゃみです。

横幅が16の倍数だとちゃんと表示されます。

これはこちらの「How do I export UIImage array as a movie in Swift 3?」のページのコードを見ていて気づきました。

 

意外と情報自体はあるのに、なかなか上手くいかなかった動画生成でした・・・。

 

(2017/9/5)配列からではなく一枚ずつ継ぎ足して動画を生成する方法を記事にしました。そっちのが詳しく説明してあります〜【Swift 3】UIImageを1枚ずつ継ぎ足して動画を生成する【AVAssetWriter】

 

とりあえずは一番大きい山を越えたかなぁといった感じ。本当に今月中旬までには新しいアプリ出します〜。

ではでは

2018/04/12追記 Gistにあげました。
>(2017/9/5)配列からではなく一枚ずつ継ぎ足して動画を生成する方法を記事にしました。そっちのが詳しく説明してあります〜「【Swift 3】UIImageを1枚ずつ継ぎ足して動画を生成する【AVAssetWriter】」

で一応少し違うやり方で作ったものをGithubにupしています。uruly / MovieCreator

のですが、一応こっちでもSwift4に対応したものを書いてみたのでついでにgistにあげておきました。

AVFileTypeQuickTimeMovieっていう指定がSwift4ではできなくなっているのでそこは.mp4にしておきました。

あとmovieの横幅は16の倍数と書いてますが、そもそも画像自体が16の倍数でなかったらぐにゃります。

Comments...

コメントは認証制です。詳しくは下記の注意をお読みください。お気軽にコメントお願いします!

Write a Comment

コメント時の注意

「Twitter」「Facebook」「Google+」「WordPress」のいずれかのアカウントをお持ちの方は各アカウントと連携することでコメントできます。 コメントしたことはSNSに流れませんので、アカウントをお持ちの方はこちらの方法でコメントを投稿して下さると嬉しいです。 アカウントをお持ちでない方はメールアドレスで投稿することができます。 初回コメント時は承認後に表示されます。

Related Memo...

記事を書くほどでもないけれどメモっておきたいこと

テスト投稿。

例えばiphone7 の画面サイズ

750 × 1334
半分375 × 667

iOS
more