Swift4.1 AVFoundation ビデオカメラの編集方法

こんにちはフリーランスエンジニアの永田です。

題名の実装コードを共有いたします。

環境はタグ通りです。

アプリを作成する上で、機能の一部が複雑なものがあったので、動くサンプルソースを作成しました。
###ソースコードはGitHubにあります。 Youtubeで挙動動画あります。
<a href="https://github.com/daisukenagata/InPutOutPutCamera/tree/InputOutPutVideoCamera"> InputOutPutVideoCamera</a>

動画トリミング、合成する上で必要箇所の一部を紹介致します。

#作成したアプリの操作方法
ビデオカメラの編集機能と合成機能|
---|---
タップジャスチャー|
インカメラとアウトカメラの切り替え|
アップスワイプ|     
動画撮影|
ダウンスワイプ|
動画の合成確認|
右スワイプ|
カメラロールに遷移|
左スワイプ|
保存データのリセット|

###ソースコードはGitHubにあります。 Youtubeで挙動動画あります。
<a href="https://github.com/daisukenagata/InPutOutPutCamera/tree/InputOutPutVideoCamera"> InputOutPutVideoCamera</a>


# リンクの事象に遭遇しました。

[StackOverFlow UIImagePickerController](https://l.facebook.com/l.php?u=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F47127659%2Fios-11-1-uiimagepickercontroller-video-crop-start-time-not-movable&h=ATPaNYbaUCPIB_2aQe7lW-hHzwDbF11yzYc0YO8BOOQlJ5F8houOhTapnuUm-EAcpKwdHghO2DUPyNYcpuDgIjYxSDSBWH4y1OUls67hNIVEYNfL12vGp82xCPH3WAaJLtYqq7AQ0-uvjJ8T)
挙動を実際にデバイスで確認するとトリミングUIが反応しづらい状態でした。これは開発者が可変できない部分のようです。

初めはオリジナルでカスタムViewを作成して、トリミングUIを作ろうと思いましたが、Appleリファレンスの指摘があるので、トリミング自体は編集機能の一部ですし、おとなしくUIVideoEditorControllerを使い編集機能として実装しようかとも考えています。<-検討を進める中で、方針を明確にしました。最後に対応方針を記載しています。




# Appleの解説
[UIimagepickercontrollerのAppleリファレンス](https://developer.apple.com/documentation/uikit/uiimagepickercontroller?changes=_4)
Working with Movies
Movie capture has a default duration limit of 10 minutes but can be adjusted using the videoMaximumDuration property. When a user taps the Share button to send a movie to MMS, MobileMe, YouTube, or another destination, an appropriate duration limit and an appropriate video quality are enforced.
The default camera interface supports editing of previously-saved movies. Editing involves trimming from the start or end of the movie, then saving the trimmed movie. To display an interface dedicated to movie editing, rather than one that also supports recording new movies, use the UIVideoEditorController class instead of this one. See UIVideoEditorController.

UIVideoEditorControllerで動画編集してくださいとのこと。

🔼 動画自体のトリミングや合成機能は、UIimagepickercontrollerでの対応でした。


# 動画撮影をした場合のデリゲード処理
```
    //保留中のすべてのデータが出力ファイルに書き込まれたときに、デリゲードに通知します。
    func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
        PHPhotoLibrary.shared().performChanges({
            PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: outputFileURL)
        }) { completed, error in
            if completed {
                DispatchQueue.main.sync {
                    self.label.text = "録画しました"
                }
                DispatchQueue.main.asyncAfter(wallDeadline: .now() + 1) {
                    self.label.text = ""
                }
            }
            let pickerView = UIImagePickerController()
            pickerView.mediaTypes = [kUTTypeMovie as String]
            pickerView.allowsEditing = true
            pickerView.delegate = self
            self.present(pickerView, animated: true)
        }

    }
　　//カメラロールの編集が終わると、このメソッドが通知されます。
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
        
        guard let mediaType = info[UIImagePickerControllerMediaType] as? String,
            mediaType == (kUTTypeMovie as String),
            let url = info[UIImagePickerControllerMediaURL] as? URL else { return }
        dismiss(animated: true) {
            self.defo.saveMethod(url:url, picker: picker)
        }
    }

```


#編集、合成部分
こちらのリンク先の実装を参照しました。実装部分は多少可変してます。
<a href="https://www.raywenderlich.com/188034/how-to-play-record-and-merge-videos-in-ios-and-swift">https://www.raywenderlich.com/188034/how-to-play-record-and-merge-videos-in-ios-and-swift</a>

実際のアプリに組み込む際は、ロジックをもう少し変更して、共通化メソッドを作成します。


#### AVAssetのクラスからObject、編集時間を設定します。command+Fで検索を使用してソースコードを解析すると読み解きやすくなります。
AVAsset|
---|---
抽象クラスは、ビデオやサウンドなどのタイムドオーディオビジュアルメディアをモデル化するために使用されます。|

CMTimeRangeMake|
---|---
指定された開始時刻と期間で有効なCMTimeRangeを作成します。|

kCMTimeZero|
---|---
この定数を使用して、CMTimeを0に初期化します。|

let mixComposition = AVMutableComposition()|
---|---
既存のアセットから新しいコンポジションを作成するために使用される変更可能なオブジェクト。|

let mainInstruction = AVMutableVideoCompositionInstruction()|
---|---
二つの編集ビデオデータを合成して、再生時間、サイズの設定をします。|

 VideoHelper.videoCompositionInstruction|
---|---
<a href="https://www.raywenderlich.com/188034/how-to-play-record-and-merge-videos-in-ios-and-swift">https://www.raywenderlich.com/188034/how-to-play-record-and-merge-videos-in-ios-and-swift</a>のVideo Orientationに詳細な解説があります。|

AVAssetExportSession|
---|---
アセットソースオブジェクトのコンテンツをトランスコードして、指定されたエクスポートプリセットで記述されたフォームの出力を作成するオブジェクト。を作成します。|

exporter.exportAsynchronously()|
---|---
エクスポートセッションの非同期実行を開始します。|

 if PHPhotoLibrary.authorizationStatus() != .authorized {|
---|---
まだ承認されていなっかった場合にブロック内に入ります。|

 PHPhotoLibrary.requestAuthorization({ status in|
---|---
必要に応じて、フォトライブラリにアクセスするためのユーザーの許可を要求します。|

PHPhotoLibrary.shared().performChanges({|
---|---
変更ブロックを使用して、フォトライブラリの複数の変更を1つのアトミックアップデートに結合します。|
```

    func aVAssetMerge(aVAsset: AVAsset,
                      aVAssetSecound:AVAsset,
                      views: ViewController) {
        
        guard let firstTrack = mixComposition.addMutableTrack(withMediaType: .video,
                                                              preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) else { return }
        do {
            try firstTrack.insertTimeRange(CMTimeRangeMake(kCMTimeZero, aVAsset.duration),
                                           of: aVAsset.tracks(withMediaType: .video)[0],
                                           at: kCMTimeZero)
        } catch {
            print("Failed to load first track")
            return
        }
        
        guard let secondTrack = mixComposition.addMutableTrack(withMediaType: .video,
                                                               preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) else { return }
        do {
            try secondTrack.insertTimeRange(CMTimeRangeMake(kCMTimeZero, aVAssetSecound.duration),
                                            of: aVAssetSecound.tracks(withMediaType: .video)[0],
                                            at: aVAsset.duration)
        } catch {
            print("Failed to load second track")
            return
        }
        
        let mainInstruction = AVMutableVideoCompositionInstruction()
        mainInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, CMTimeAdd (aVAsset.duration, aVAssetSecound.duration))
        
        let firstInstruction = VideoHelper.videoCompositionInstruction(firstTrack, asset: aVAsset)
        firstInstruction.setOpacity(0.0, at: aVAsset.duration)
        let secondInstruction = VideoHelper.videoCompositionInstruction(secondTrack, asset: aVAssetSecound)
        
        mainInstruction.layerInstructions = [firstInstruction,secondInstruction]
        let mainComposition = AVMutableVideoComposition()
        mainComposition.instructions = [mainInstruction]
        mainComposition.frameDuration = CMTimeMake(1, 30)
        mainComposition.renderSize = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
        
        aVAssetExportSet(mainComposition: mainComposition, aVAsset: aVAsset, aVAssetSecound: aVAssetSecound, views: views)
    }
    
    func aVAssetExportSet(mainComposition: AVMutableVideoComposition,
                          aVAsset: AVAsset,
                          aVAssetSecound:AVAsset,
                          views: ViewController) {
        guard let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
        let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .long
        dateFormatter.timeStyle = .short
        let date = dateFormatter.string(from: Date())
        let num = arc4random() % 100000000
        let url = documentDirectory.appendingPathComponent(num.description+"\(date).mov")

        guard let exporter = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality) else { return }
        exporter.outputURL = url
        exporter.outputFileType = AVFileType.mov
        exporter.shouldOptimizeForNetworkUse = true
        exporter.videoComposition = mainComposition
        
        exporter.exportAsynchronously() {
            DispatchQueue.main.async {
                self.exportDidFinish(exporter,aVAsset: aVAsset,
                                     aVAssetSecound: aVAssetSecound,
                                     url: url,
                                     views: views)
            }
        }
    }
    
     func exportDidFinish(_ session: AVAssetExportSession,
                         aVAsset: AVAsset,
                         aVAssetSecound:AVAsset,
                         url: URL, views:ViewController) {

        guard session.status == AVAssetExportSessionStatus.completed,
            let outputURL = session.outputURL else { return }
        
        let saveVideoToPhotos = {
            PHPhotoLibrary.shared().performChanges({ PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: outputURL) }) { saved, error in
                views.pic.mediaTypes = [kUTTypeMovie as String]
                views.pic.allowsEditing = true
                views.pic.delegate = views
                views.present(views.pic, animated: true)
            }
        }
        if PHPhotoLibrary.authorizationStatus() != .authorized {
            PHPhotoLibrary.requestAuthorization({ status in
                if status == .authorized {
                    saveVideoToPhotos()
                }
            })
        } else {
            saveVideoToPhotos()
        }
    }
}

```

#アプリケーションとして

動画の編集、動画の結合ができる機能を公開させていただきました。
全て調整、完成した状態での公開は、ソース量として膨大になり、解析、理解が難しい実装になりますので、最小範囲での実装を提供させていただきました。

###ソースコードはGitHubにあります。 Youtubeで挙動動画あります。
<a href="https://github.com/daisukenagata/InPutOutPutCamera/tree/InputOutPutVideoCamera"> InputOutPutVideoCamera</a>


またiphoneXではある程度可能ですが、左上から動画をトリミングする際のUIが反応しづらい課題がありますので、編集機能はカスタムで作成するよう挑戦してみます。<-数時間でできました。撮影後、撮影部分をサムネイル表示してUIで表示するロジックを作成しました。これにトリミング機能などを合わせればアプリでしたい事ができるので、こちらの実装検討は終了します。この機能を作り込むのみでしたら、もっと検討を進める必要がありますが、アプリ製作が目的なので、次の検討に入ります。

![IMG_2111.TRIM.gif](https://qiita-image-store.s3.amazonaws.com/0/131469/bb102658-8e24-1277-9d67-8b6b42de6576.gif)


以上、繰り返し編集させていただきました。貴重なお時間お読み下さいまして、誠にありがとうございます。