iOS
エンコード
動画
mp4
Swift

[switf4] 動画をmp4にエンコードする時に動画の向きを90°回転させる

はじめに

この記事では動画をmp4にエンコードする時に動画の向きを90°回転させる方法について記述します
前回書いた記事 iPhoneで撮影した動画をMOVからmp4に変換する の続きになります

注意
この記事では動画を撮影する機能は実装しません

動画をmp4にエンコードする時に動画の向きを90°回転させる理由

前回の記事でiPhoneで撮影した動画をMOVからMP4に変換し、無事にHLSで動画を再生できるようになりました
が、しかし
いざ動画を再生させると映像は表示できるようになったものの...
動画が90°回転されている!!!!!!
という問題が起きたためです

動画が90°回転されてしまう理由

Invisorというアプリケーションを使って、mp4に変換した動画のメタデータを確認したところ Rotation: 90° というメタデータが追加されていることに気がつきました
Rotationがある動画は90°回転し再生され、そうでない動画は正しい向きで再生されました。
では、なぜmp4に変換するとRotationというメタデータが追加されてしまうのか?

原因はAVAssetExportSessionにありました
AVAssetExportSessionついて詳しく調べてみると、AVAssetExportSessionでエンコードした動画はデフォルトの挙動で90°回転させてしまうようです

なので、考えられる解決方法としては
動画をmp4にエンコードする前に-90°回転させておく!
もっといい方法があるかもしれませんが現状これしか思いつきませんでしたw

と、いうことで前置きが長くなりましたが開発環境から実装方法を説明します

参考:AVMutableVideoComposition rotated video captured in portrait mode

開発環境

Xcode :9.3
swift :4.1
iPhone:8以上

実装手順

1、動画のアセットと音声と映像2つのトラックを作成
2、音声と映像2つのコンポジション(動画を編集するためのプロパティ)を作成
3、各コンポジションの設定
4、合成用コンポジションを作成し、インストラクションを合成用コンポジションに設定
5、合成用コンポジションをAVAssetExportSessionに設定

※コードを見ればわかりますが、本当の手順はもう少し細かいです

ソースコードの完成形

今回は必要なコードが多かったので、手順ごとに解説はしていません

func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {

        // 動画のアセットとトラックを作成
        let videoAsset = AVURLAsset(url: outputFileURL, options: nil)
        var videoTrack: AVAssetTrack
        var audioTrack: AVAssetTrack

        // 映像トラック
        let videoTracks = videoAsset.tracks(withMediaType: AVMediaType.video)
        videoTrack = videoTracks[0]

        // 音声トラック
        let audioTracks = videoAsset.tracks(withMediaType: AVMediaType.audio)
        audioTrack = audioTracks[0]

        // コンポジション作成
        let mixComposition : AVMutableComposition = AVMutableComposition()
        // ベースとなる動画のコンポジション作成
        let compositionVideoTrack: AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: kCMPersistentTrackID_Invalid)
        // ベースとなる音声のコンポジション作成
        let compositionAudioTrack: AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: kCMPersistentTrackID_Invalid)

        // コンポジションの設定
        // 動画の長さ設定
        try! compositionVideoTrack.insertTimeRange(CMTimeRangeMake(kCMTimeZero, videoAsset.duration), of: videoTrack, at: kCMTimeZero)
        // 音声の長さ設定
        try! compositionAudioTrack.insertTimeRange(CMTimeRangeMake(kCMTimeZero, videoAsset.duration), of: audioTrack, at: kCMTimeZero)
        // 回転方向の設定(AVAssetExportを使用するとデフォルトで動画が90°回転するため必須)
        compositionVideoTrack.preferredTransform = videoTrack.preferredTransform

        // 動画のサイズを取得
        var videoSize: CGSize = videoTrack.naturalSize
        var isPortrait: Bool = false

        // ビデオを縦横方向
        let txf = videoTrack.preferredTransform
        if txf.tx == 0 || txf.ty == 0 {
            isPortrait = true
            videoSize = CGSize(width: videoSize.height, height: videoSize.width)
        }

        // 合成用コンポジション作成
        let videoComp: AVMutableVideoComposition = AVMutableVideoComposition()
        videoComp.renderSize = videoSize
        videoComp.frameDuration = CMTimeMake(1, 30)

        // インストラクションを合成用コンポジションに設定
        let instruction: AVMutableVideoCompositionInstruction = AVMutableVideoCompositionInstruction()
        instruction.timeRange = CMTimeRangeMake(kCMTimeZero, videoAsset.duration)
        let layerInstruction: AVMutableVideoCompositionLayerInstruction = AVMutableVideoCompositionLayerInstruction.init(assetTrack: compositionVideoTrack)
        instruction.layerInstructions = [layerInstruction]

        // 縦方向で撮影なら90°回転させる
        if isPortrait {
            let FirstAssetScaleFactor: CGAffineTransform = CGAffineTransform(scaleX: 1.0, y: 1.0)
            layerInstruction.setTransform(videoTrack.preferredTransform.concatenating(FirstAssetScaleFactor), at: kCMTimeZero)
        }

        videoComp.instructions = [instruction]

        // AVAssetExportSessionを使ってExportする
        let exportSession = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality)
        // 合成用コンポジションを設定
        exportSession?.videoComposition = videoComp
        exportSession?.outputFileType = AVFileType.mp4
        exportSession?.shouldOptimizeForNetworkUse = true

        let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
        let documentsDirectory = paths[0] as String

        let fileName = "\(Date().timeIntervalSince1970).mp4"
        let filePath = "\(documentsDirectory)" + fileName
        self.fileURL = NSURL(fileURLWithPath: filePath) as URL
        exportSession?.outputURL = self.fileURL

        // 録画したビデオ(mov)をMP4に変換する
        exportSession?.exportAsynchronously(completionHandler: {
            switch exportSession?.status {
            case .completed?:
                // 撮影した動画を削除
                do {
                    try FileManager.default.removeItem(atPath: self.filePath!)
                } catch {
                    //エラー
                }

                // ライブラリへの保存
                PHPhotoLibrary.shared().performChanges({
                    PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: self.fileURL!)
                }) { completed, error in
                    if completed {
                        debugPrint("Video is saved!")

                        DispatchQueue.main.async {
                            let storyboard = UIStoryboard(name: Strings.videoUpload, bundle: nil)
                            let viewController = storyboard.instantiateInitialViewController() as! VideoUploadViewController
                            viewController.fileUrl = self.fileURL
                            self.show(viewController, sender: nil)
                        }
                    }
                }
                break

            case .failed?:
                debugPrint("failed: \(String(describing: exportSession?.error))")
                break

            case .cancelled?:
                break

            default:
                break
            }
        })

    }

参考記事

AVFoundationを使ったiOSの動画編集
SWIFT3で動画を撮影して保存 – XCODE9

自社紹介

株式会社ManhattanCode