AVAssetWriterで動画の出力


`AVAssetExportSession`で失敗する`AVAsset`の書き出しも`AVAssetReader`で読み込んで（バッファリングして）、`AVAssetWriter`で書き出すと上手く保存できたので忘れないようにメモ。

```swift
///
// MARK: ------------------------------ AVAsset
///
///
///
extension AVAsset {
    ///
    // MARK: ------------------------------ func
    ///
    /// Readerで読み込んでWriterで書き出し
    ///
    /// ```
    /// asset.wroteTempDir { (err: NSError?, wroteUrl: URL?) in
    ///     if let err = err {
    ///         print("\(err.localizedDescription)")
    ///     } else {
    ///         /// success
    ///     }
    /// }
    /// ```
    ///
    func wroteTempDir(_ cbWrote: ((_ err: NSError?, _ wroteUrl: URL?) -> Void)?) {
        let err: NSError = NSError.init(domain: "hoge", code: 999, userInfo: [NSLocalizedDescriptionKey: "エラー説明"])
        let wroteUrl: URL = NSHomeDirectory() + "/tmp/wrote.MOV"
        ///
        /// setup reader & writer 
        ///
        guard let reader: AVAssetReader = try? AVAssetReader.init(asset: self) else {
            cbWrote?(err, nil)
            return
        }
        guard let writer: AVAssetWriter = try? AVAssetWriter(outputURL: wroteUrl, fileType: AVFileType.mov) else {
            cbWrote?(err, nil)
            return
        }
        ///
        /// setup finish closure
        ///
        var audioFinished: Bool = false
        var videoFinished: Bool = false
        let finishClosure: (() -> Void) = {
            if audioFinished == true && videoFinished == true {
                writer.finishWriting {
                    cbWrote?(nil, wroteUrl)
                }
                reader.cancelReading()
            }
        }
        ///
        /// prepare reader for video
        ///
        let readerVideoOutput: AVAssetReaderTrackOutput = AVAssetReaderTrackOutput(
            track: self.tracks(withMediaType: AVMediaType.video)[0],
            outputSettings: [
                kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange),
            ]
        )
        reader.add(readerVideoOutput)
        ///
        /// prepare reader for audio
        ///
        var readerAudioOutput: AVAssetReaderTrackOutput!
        if self.tracks(withMediaType: AVMediaType.audio).count <= 0 {
            /// 無音も有り得るので考慮
            audioFinished = true
        } else {
            readerAudioOutput = AVAssetReaderTrackOutput.init(
                track: self.tracks(withMediaType: AVMediaType.audio)[0],
                outputSettings: [
                    AVSampleRateKey: 44100,
                    AVFormatIDKey:   kAudioFormatLinearPCM,
                ]
            )
            if reader.canAdd(readerAudioOutput) {
                reader.add(readerAudioOutput)
            } else {
                print("Cannot add audio output reader")
                audioFinished = true
            }
        }
        ///
        /// prepare writer input for video
        ///
        let writerVideoInput: AVAssetWriterInput = AVAssetWriterInput.init(
            mediaType: AVMediaType.video,
            outputSettings: [
                AVVideoCodecKey:                 AVVideoCodecType.h264,
                AVVideoWidthKey:                 self.tracks(withMediaType: AVMediaType.video)[0].naturalSize.width,
                AVVideoHeightKey:                self.tracks(withMediaType: AVMediaType.video)[0].naturalSize.height,
                AVVideoCompressionPropertiesKey: [
                    AVVideoAverageBitRateKey: self.tracks(withMediaType: AVMediaType.video)[0].estimatedDataRate,
                ],
            ]
        )
        writerVideoInput.expectsMediaDataInRealTime = false
        writerVideoInput.transform = self.tracks(withMediaType: AVMediaType.video)[0].preferredTransform
        writer.add(writerVideoInput)
        ///
        /// prepare writer input for audio
        ///
        var writerAudioInput: AVAssetWriterInput! = nil
        if self.tracks(withMediaType: AVMediaType.audio).count > 0 {
            let formatDesc: [Any] = self.tracks(withMediaType: AVMediaType.audio)[0].formatDescriptions
            var channels: UInt32 = 1
            var sampleRate: Float64 = 44100.000000
            for i in 0 ..< formatDesc.count {
                guard let bobTheDesc: UnsafePointer<AudioStreamBasicDescription> = CMAudioFormatDescriptionGetStreamBasicDescription(formatDesc[i] as! CMAudioFormatDescription) else {
                    continue
                }
                channels = bobTheDesc.pointee.mChannelsPerFrame
                sampleRate = bobTheDesc.pointee.mSampleRate
                break
            }
            writerAudioInput = AVAssetWriterInput.init(
                mediaType: AVMediaType.audio,
                outputSettings: [
                    AVFormatIDKey:         kAudioFormatMPEG4AAC,
                    AVNumberOfChannelsKey: channels,
                    AVSampleRateKey:       sampleRate,
                    AVEncoderBitRateKey:   128000,
                ]
            )
            writerAudioInput.expectsMediaDataInRealTime = true
            writer.add(writerAudioInput)
        }
        ///
        /// write
        ///
        let videoQueue = DispatchQueue.init(label: "videoQueue")
        let audioQueue = DispatchQueue.init(label: "audioQueue")
        writer.startWriting()
        reader.startReading()
        writer.startSession(atSourceTime: CMTime.zero)
        ///
        /// write for video
        ///
        writerVideoInput.requestMediaDataWhenReady(on: videoQueue) {
            while writerVideoInput.isReadyForMoreMediaData {
                autoreleasepool {
                    let buffer = readerVideoOutput.copyNextSampleBuffer()
                    if buffer != nil {
                        writerVideoInput.append(buffer!)
                    } else {
                        writerVideoInput.markAsFinished()
                        DispatchQueue.main.async {
                            videoFinished = true
                            finishClosure()
                        }
                    }
                }
            }
        }
        if writerAudioInput != nil {
            writerAudioInput.requestMediaDataWhenReady(on: audioQueue) {
                while writerAudioInput.isReadyForMoreMediaData {
                    autoreleasepool {
                        let buffer = readerAudioOutput.copyNextSampleBuffer()
                        if buffer != nil {
                            writerAudioInput.append(buffer!)
                        } else {
                            writerAudioInput.markAsFinished()
                            DispatchQueue.main.async {
                                audioFinished = true
                                finishClosure()
                            }
                        }
                    }
                }
            }
        }
    }

```


こちらの記事が大変参考になりました🙇‍♂️

https://samkirkiles.svbtle.com/swift-using-avassetwriter-to-compress-video-files-for-network-transfer
https://qiita.com/takecian/items/f66942df058695b46f5f

動画のマスク処理もできそうなので、自分のアプリ「[ShapingMovie](https://itunes.apple.com/jp/app/shapingmovie-%E3%81%84%E3%82%8D%E3%82%93%E3%81%AA%E5%BD%A2%E3%81%AB%E5%8B%95%E7%94%BB%E3%82%92%E5%88%87%E3%82%8A%E6%8A%9C%E3%81%8F/id1357569470?mt=8)」もバージョンアップしようと思います✨