リアルタイムカメラ加工アプリを作る【Swift】【AVFoundation】

# カメラ→加工→動画保存

![Feb-03-2024 03-15-35.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/66b2329b-069c-dd3e-dca8-07ee879c814f.gif)

カメラからの一枚一枚のフレームに任意の加工をして動画として書き込む方法です。
（Core ML でアニメ風の画像にする処理を例とします）

AVCaptureSession でカメラフレームを取得
↓
フレームを加工
↓
AVAssetWriter に流し込む

という手順で動画にします。

# 手順

### infoPlist でアプリでカメラとマイクの使用許可設定

```info.plist
Privacy - Camera Usage Description
Privacy - Microphone Usage Description
```

### プロパティ
これらを使います。

```swift
// capture
private var captureSession = AVCaptureSession()
private var captureVideoOutput = AVCaptureVideoDataOutput()
private let captureAudioOutput = AVCaptureAudioDataOutput()

// writing
private var videoWriter:AVAssetWriter!
private var videoWriterVideoInput:AVAssetWriterInput!
private var videoWriterPixelBufferAdaptor:AVAssetWriterInputPixelBufferAdaptor!
private var videoWriterAudioInput:AVAssetWriterInput!

private var videoSize: CGSize = .zero
private var processing:Bool = false
private var currentSampleBuffer:CMSampleBuffer!
private let ciContext = CIContext()
private var isRecording = false
private var recordingStartTime:CMTime?

// preview
private var imageView = UIImageView()
```

### カメラの映像・音声を取得するセッティング

AVCaptureSessionに入出力を設定し、Sessionをスタートします。
（設定項目）
* Video Input
* Video Output
* Audio Input
* Audio Output

** ここでカメラ映像の幅と高さも取得します（映像を書き込む際に使う）。

```swift
private func setupCaptureSession() {
    
    do {
        // video input
        let captureDevice:AVCaptureDevice = AVCaptureDevice.default(for: .video)!
        let videoInput = try AVCaptureDeviceInput(device: captureDevice)
        if captureSession.canAddInput(videoInput) {
            captureSession.addInput(videoInput)
        }
        
        let dimensions = CMVideoFormatDescriptionGetDimensions(captureDevice.activeFormat.formatDescription)
        videoSize = CGSize(width: CGFloat(dimensions.height), height: CGFloat(dimensions.width))

        // video output
        captureVideoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
        captureVideoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "videoQueue"))
        if captureSession.canAddOutput(captureVideoOutput) {
            captureSession.addOutput(captureVideoOutput)
        }
        
        // audio input
        if let audioDevice = AVCaptureDevice.default(for: .audio),
           let audioInput = try? AVCaptureDeviceInput(device: audioDevice) {
            if captureSession.canAddInput(audioInput) {
                captureSession.addInput(audioInput)
            }
        }

        // audio output
        captureAudioOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "audioQueue"))
        if captureSession.canAddOutput(captureAudioOutput) {
            captureSession.addOutput(captureAudioOutput)
        }

        // start session
        DispatchQueue.global(qos: .userInitiated).async {
            self.captureSession.startRunning()
        }
    } catch {
        print("Error setting up capture session: \(error.localizedDescription)")
    }
}
```

以下のデリゲートを使用しているクラス（今回は ViewController ）に継承させます。
これにより、カメラフレームおよびマイク音声が更新される度にデリゲートメソッドが呼ばれるようになります。

* AVCaptureVideoDataOutputSampleBufferDelegate（video用）
* AVCaptureAudioDataOutputSampleBufferDelegate（audio用）


```swift
class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate
```


### Video書き込みセッティング

宛先ファイルに動画として書き込むセッティングをします。

* 書き込み先URLを作る
  今回の例では、アプリのドキュメントディレクトリに"video.mov"というファイルを作っています。
* AVAssetWriterを初期化
書き込み先URLを渡します。
* videoのinputを設定
  カメラ映像の幅と高さを渡します。
* PixelBufferAdoptorを設定
処理したpixelBufferを書き込む際に使用します。
* audioのinputを設定

```swift
private func setupVideoWriter() {
    if recordingStartTime != nil {
        recordingStartTime = nil
    }
    // set writing destination url
    guard let outputURL = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("video.mov") else { fatalError() }
    try? FileManager.default.removeItem(at: outputURL)
    
    // initialize video writer
    videoWriter = try! AVAssetWriter(outputURL: outputURL, fileType: .mov)
    
    // set video input
    let videoOutputSettings: [String: Any] = [
        AVVideoCodecKey: AVVideoCodecType.h264,
        AVVideoWidthKey: videoSize.width,
        AVVideoHeightKey: videoSize.height
    ]
    videoWriterVideoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoOutputSettings)
    videoWriterVideoInput.expectsMediaDataInRealTime = true
    
    // use adaptor for write processed pixelbuffer
    videoWriterPixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoWriterVideoInput, sourcePixelBufferAttributes: [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)])
    
    // set audio input
    let audioOutputSettings: [String: Any] = [
        AVFormatIDKey: kAudioFormatMPEG4AAC,
        AVNumberOfChannelsKey: 2,
        AVSampleRateKey: 44100,
        AVEncoderBitRateKey: 128000
    ]
    videoWriterAudioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioOutputSettings)
    videoWriterAudioInput.expectsMediaDataInRealTime = true
    
    if videoWriter.canAdd(videoWriterVideoInput) {
        videoWriter.add(videoWriterVideoInput)
    }
    if videoWriter.canAdd(videoWriterAudioInput) {
        videoWriter.add(videoWriterAudioInput)
    }
}
```

### 映像取得・加工・書き込みの実行

* 映像取得
captureOutput はカメラフレームと音声が更新されるたびに呼ばれるメソッドです。
この中でフレームを取得・加工して動画ファイルに書き込みます。
音声も書き込みます。

```swift
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    
    if let videoDataOutput = output as? AVCaptureVideoDataOutput,
       !processing {

       // video
       
        processing = true
        
        // Proceed only when the previous frame has finished processing   
        
        if let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {

            // this is a video frame
            // process a frame here

            startTime = Date()
            guard let processedCIImage = predict(pixelBuffer: pixelBuffer) else {
                return
            }
            
            // Update preview
            updatePreview(processedCIImage: processedCIImage)
            
            if isRecording {
                let presentationTimeStamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)

                // write
                
                writeProcessedVideoFrame(processedCIImage: processedCIImage, presentationTimeStamp: presentationTimeStamp)
                
            }
        }
        processing = false
        
    } else if let audioDataOutput = output as? AVCaptureAudioDataOutput {
    // audio
    // Since the update timing for audio and video is different, we will separate them.
        if isRecording ,
           let recordingStartTime = recordingStartTime{
            if videoWriterAudioInput.isReadyForMoreMediaData,
               videoWriter.status == .writing {
                var copyBuffer : CMSampleBuffer?
                var count: CMItemCount = 1
                var info = CMSampleTimingInfo()
                CMSampleBufferGetSampleTimingInfoArray(sampleBuffer, entryCount: count, arrayToFill: &info, entriesNeededOut: &count)
                info.presentationTimeStamp = CMTimeSubtract(info.presentationTimeStamp, recordingStartTime)
                CMSampleBufferCreateCopyWithNewTiming(allocator: kCFAllocatorDefault,sampleBuffer: sampleBuffer,sampleTimingEntryCount: 1,sampleTimingArray: &info,sampleBufferOut: &copyBuffer)
                
                videoWriterAudioInput.append(copyBuffer!)
            }
        }
    }
}
```

* 加工処理
CVPixelBuffer（カメラ画像）を加工します。
ここは好きな処理に置き換えてください。
今回は Core ML でフレームをアニメ風にしています。


```swift
// initialize ML model
// private var coreMLRequest: VNCoreMLRequest?
// func setupMLModel() {
//     let mlModelConfig = MLModelConfiguration()
//     do {
//         let coreMLModel:MLModel = try animeganHayao(configuration: mlModelConfig).model
//         let vnCoreMLModel:VNCoreMLModel = try VNCoreMLModel(for: coreMLModel)
//         let coreMLRequest:VNCoreMLRequest = VNCoreMLRequest(model: vnCoreMLModel, completionHandler: coreMLRequestCompletionHandler)
//         coreMLRequest.imageCropAndScaleOption = .scaleFill
//         self.coreMLRequest = coreMLRequest
//     } catch let error {
//        fatalError(error.localizedDescription)
//     }
// }

private func predict(pixelBuffer: CVPixelBuffer)->CIImage? {
    guard let coreMLRequest = coreMLRequest else {
        processing = false
        return nil
    }
    let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer,orientation: .right, options: [:])
    do {
        try handler.perform([coreMLRequest])
        guard let result:VNPixelBufferObservation = coreMLRequest.results?.first as? VNPixelBufferObservation else {
            processing = false
            return nil}
        let end = Date()
        let inferenceTime = end.timeIntervalSince(startTime)
        print(inferenceTime)
        let pixelBuffer:CVPixelBuffer = result.pixelBuffer
        let resultCIImage = CIImage(cvPixelBuffer: pixelBuffer)
        let resizedCIImage = resultCIImage.resize(as: videoSize)
        return resizedCIImage
    } catch {
        print("Vision error: \(error.localizedDescription)")
        return nil
    }
}
```

フレーム加工中は次のフレームを加工しないように processing フラグで switch します。
大量の加工処理が発生するとアプリ全体がブロックされてしまうためです。

* preview をユーザーに表示
加工したフレームを UIImageView に表示します。
AVCaptureSessionでは通常 AVPreviewLayer でプレビューを表示しますが、加工したフレームは AVPreviewLayer で表示できないため、 UIImageView に一枚ずつ反映することでプレビューします。

```swift
func updatePreview(processedCIImage: CIImage) {
    
    let processedUIImage = UIImage(ciImage: processedCIImage)
    
    DispatchQueue.main.async {
        
        self.imageView.image = processedUIImage
    
    }
}
```

* 録画

AVAssetWriter の 
startWriting と 
startSession 
を呼んで録画を開始します。

```swift
if videoWriter.status == .unknown {
    self.videoWriter.startWriting()
    self.videoWriter.startSession(atSourceTime: .zero)
}
isRecording = true
```

* 書き込み

AVAssetWriterInputPixelBufferAdaptor に　処理後の CVPixelBuffer を書き込みます。
処理したフレームは CIImage になっているので CVPixelBuffer に戻します。

AVAssetWriter を 開始時間0秒で開始しているので、書き込みスタート時の CMTime 　を フレームの CMTime から引いて書き込みます。
（ audio の方も同じように時間の計算をして書き込んでいます。）

```swift
func writeProcessedVideoFrame(processedCIImage: CIImage, presentationTimeStamp: CMTime) {
    
    // get the time of this video frame.
    if self.videoWriter.status == .writing,
       self.videoWriterVideoInput.isReadyForMoreMediaData == true {
        if recordingStartTime == nil {
            self.recordingStartTime = presentationTimeStamp
        }
        
        // CIImage -> CVPixelBuffer
        var processedPixelBuffer: CVPixelBuffer?
        CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, videoWriterPixelBufferAdaptor.pixelBufferPool!, &processedPixelBuffer)
        self.ciContext.render(processedCIImage, to: processedPixelBuffer!)
        guard let processedPixelBuffer = processedPixelBuffer else { return }
        let time = CMTimeSubtract(presentationTimeStamp, recordingStartTime!)
        // write
        self.videoWriterPixelBufferAdaptor.append(processedPixelBuffer, withPresentationTime: time)
    }
}
```

### 録画の終了・動画の保存
video と audio それぞれの AVAssetWriterInput で
.markAsFinished()
を呼び、

AVAssetWriter の
.finishWriting
を呼んで、 
書き込みを終了します。

書き込み処理が終わると、ハンドラが呼ばれるので、そこで AVAssetWriter に設定した書き込み先 URL で動画を取得し、フォトライブラリに保存します。


```swift
if videoWriter.status == .writing {
    videoWriterVideoInput.markAsFinished()
    videoWriterAudioInput.markAsFinished()
    videoWriter.finishWriting { [weak self] in
        guard let self = self else { return }
        let outputURL = self.videoWriter.outputURL
        self.saveVideoToPhotoLibrary(url: outputURL)
    }
}
```

```swift
func saveVideoToPhotoLibrary(url: URL) {
    PHPhotoLibrary.requestAuthorization { status in
        if status == .authorized {
            PHPhotoLibrary.shared().performChanges({
                PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)
            }) { saved, error in
                DispatchQueue.main.async {
                    if saved {
                        print("Video saved in photo library")
                        self.setupVideoWriter()
                    } else {
                        print("Failed saving: \(String(describing: error))")
                    }
                }
            }
        } else {
            print("Access to photo library　was denided")
        }
    }
}
```


# Tips

・AVAssetWriter の startWriting を呼ぶタイミングによっては書き込みの最初に数秒フレームが静止してしまいます。
captureSession 内で呼んだり、 DispathQueue.global で呼んだりすると静止が発生しました。

だいすけの経験的なベストプラクティスは録画開始ボタンを押したときに呼ぶことです。

・Xcodeでアプリをrunすると、AVAssetWriterのstartSessionでアプリ全体が数秒ブロックされることがありました。一度ビルドした後、デバイス単体で実行すると解消されます。

・AVAssetWriter は、１動画ごとに初期化する必要があります。

・Core MLモデルを使う場合、最初のビルド時にモデルの初期化に数秒かかります。
一度初期化してしまうと、次にアプリを開いたときにはすぐインスタンス化できます。
Xcodeのrunでは毎回初期化に数秒かかります。一度ビルドした後、デバイス単体で実行すると解消されます。

# 全体コード

```swift
//
//  RealTimeCameraInferenceViewController.swift
//
//  Created by 間嶋大輔 on 2024/01/21.
//

import UIKit
import Vision
import AVFoundation
import Photos

class RealTimeCameraInferenceViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate {
    
    private var coreMLRequest: VNCoreMLRequest?
    
    // Capture
    private var captureSession = AVCaptureSession()
    private var captureVideoOutput = AVCaptureVideoDataOutput()
    private let captureAudioOutput = AVCaptureAudioDataOutput()
    
    // Video Writing
    private var videoWriter:AVAssetWriter!
    private var videoWriterVideoInput:AVAssetWriterInput!
    private var videoWriterPixelBufferAdaptor:AVAssetWriterInputPixelBufferAdaptor!
    private var videoWriterAudioInput:AVAssetWriterInput!
    
    private var videoSize: CGSize = .zero
    private var processing:Bool = false
    private var currentSampleBuffer:CMSampleBuffer!
    private let ciContext = CIContext()
    private var isRecording = false
    private var startTime:Date!
    private var recordingStartTime:CMTime?
    
    // View
    private var imageView = UIImageView()
    private var descriptionLabel = UILabel()
    private var recordButton = UIButton()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupCoreMLRequest()
        setupView()
        setupCaptureSession()
        setupVideoWriter()
    }
    
    func setupCoreMLRequest() {
        let mlModelConfig = MLModelConfiguration()
        do {
            let coreMLModel:MLModel = try animeganHayao(configuration: mlModelConfig).model
            let vnCoreMLModel:VNCoreMLModel = try VNCoreMLModel(for: coreMLModel)
            let coreMLRequest:VNCoreMLRequest = VNCoreMLRequest(model: vnCoreMLModel, completionHandler: coreMLRequestCompletionHandler)
            coreMLRequest.imageCropAndScaleOption = .scaleFill
            self.vnCoreMLModel = vnCoreMLModel
            self.coreMLRequest = coreMLRequest
        } catch let error {
            fatalError(error.localizedDescription)
        }
    }
    
    private func predict(pixelBuffer: CVPixelBuffer)->CIImage? {
        guard let coreMLRequest = coreMLRequest else {
            processing = false
            return nil
        }
        let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer,orientation: .right, options: [:])
        do {
            try handler.perform([coreMLRequest])
            guard let result:VNPixelBufferObservation = coreMLRequest.results?.first as? VNPixelBufferObservation else {
                processing = false
                return nil}
            let end = Date()
            let inferenceTime = end.timeIntervalSince(startTime)
            print(inferenceTime)
            let pixelBuffer:CVPixelBuffer = result.pixelBuffer
            let resultCIImage = CIImage(cvPixelBuffer: pixelBuffer)
            let resizedCIImage = resultCIImage.resize(as: videoSize)
            return resizedCIImage
        } catch {
            print("Vision error: \(error.localizedDescription)")
            return nil
        }
    }
    
    // MARK: -Video
    
    @objc func recordVideo() {
        if isRecording {
            if videoWriter.status == .writing {
                videoWriterVideoInput.markAsFinished()
                videoWriterAudioInput.markAsFinished()
                videoWriter.finishWriting { [weak self] in
                    guard let self = self else { return }
                    let outputURL = self.videoWriter.outputURL
                    self.saveVideoToPhotoLibrary(url: outputURL)
                }
            }
        } else {
            if videoWriter.status == .unknown {
                self.videoWriter.startWriting()
                self.videoWriter.startSession(atSourceTime: .zero)
            }
        }
        isRecording.toggle()
    }
    
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        
        if let videoDataOutput = output as? AVCaptureVideoDataOutput,
           !processing {
            processing = true
            
            // Proceed to processing only when the previous frame has finished processing
            // process a video frame here
            
            if let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) { // this is a video frame
                currentSampleBuffer = sampleBuffer
                startTime = Date()
                guard let processedCIImage = predict(pixelBuffer: pixelBuffer) else {
                    return
                }
                
                // Update preview
                updatePreview(processedCIImage: processedCIImage)
                
                if isRecording {
                    
                    let presentationTimeStamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
                    writeProcessedVideoFrame(processedCIImage: processedCIImage, presentationTimeStamp: presentationTimeStamp)
                }
            }
            processing = false
        } else if let audioDataOutput = output as? AVCaptureAudioDataOutput {
            if isRecording ,
               let recordingStartTime = recordingStartTime{
                if videoWriterAudioInput.isReadyForMoreMediaData,
                   videoWriter.status == .writing {
                    var copyBuffer : CMSampleBuffer?
                    var count: CMItemCount = 1
                    var info = CMSampleTimingInfo()
                    CMSampleBufferGetSampleTimingInfoArray(sampleBuffer, entryCount: count, arrayToFill: &info, entriesNeededOut: &count)
                    info.presentationTimeStamp = CMTimeSubtract(info.presentationTimeStamp, recordingStartTime)
                    CMSampleBufferCreateCopyWithNewTiming(allocator: kCFAllocatorDefault,sampleBuffer: sampleBuffer,sampleTimingEntryCount: 1,sampleTimingArray: &info,sampleBufferOut: &copyBuffer)
                    
                    videoWriterAudioInput.append(copyBuffer!)
                }
            }
            
        }
    }
    
    func writeProcessedVideoFrame(processedCIImage: CIImage, presentationTimeStamp: CMTime) {
        
        // get the time of this video frame.
        if self.videoWriter.status == .writing,
           self.videoWriterVideoInput.isReadyForMoreMediaData == true {
            if recordingStartTime == nil {
                self.recordingStartTime = presentationTimeStamp
            }
            
            // CIImage -> CVPixelBuffer
            var processedPixelBuffer: CVPixelBuffer?
            CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, videoWriterPixelBufferAdaptor.pixelBufferPool!, &processedPixelBuffer)
            self.ciContext.render(processedCIImage, to: processedPixelBuffer!)
            guard let processedPixelBuffer = processedPixelBuffer else { return }
            let time = CMTimeSubtract(presentationTimeStamp, recordingStartTime!)
            // write
            self.videoWriterPixelBufferAdaptor.append(processedPixelBuffer, withPresentationTime: time)
        }
    }
    
    func updatePreview(processedCIImage: CIImage) {
        let cgImage = ciContext.createCGImage(processedCIImage, from: processedCIImage.extent)
        
        let processedUIImage = UIImage(cgImage: cgImage!)
        DispatchQueue.main.async {
            
            self.imageView.image = processedUIImage
        }
    }
    
    private func setupCaptureSession() {
        
        do {
            // video input
            let captureDevice:AVCaptureDevice = AVCaptureDevice.default(for: .video)!
            let videoInput = try AVCaptureDeviceInput(device: captureDevice)
            if captureSession.canAddInput(videoInput) {
                captureSession.addInput(videoInput)
            }
            
            let dimensions = CMVideoFormatDescriptionGetDimensions(captureDevice.activeFormat.formatDescription)
            videoSize = CGSize(width: CGFloat(dimensions.height), height: CGFloat(dimensions.width))
            
            // video output
            captureVideoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
            captureVideoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "videoQueue"))
            if captureSession.canAddOutput(captureVideoOutput) {
                captureSession.addOutput(captureVideoOutput)
            }
            
            // audio input
            if let audioDevice = AVCaptureDevice.default(for: .audio),
               let audioInput = try? AVCaptureDeviceInput(device: audioDevice) {
                if captureSession.canAddInput(audioInput) {
                    captureSession.addInput(audioInput)
                }
            }
            
            // audio output
            captureAudioOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "audioQueue"))
            if captureSession.canAddOutput(captureAudioOutput) {
                captureSession.addOutput(captureAudioOutput)
            }
            // start session
            DispatchQueue.global(qos: .userInitiated).async {
                self.captureSession.startRunning()
            }
        } catch {
            print("Error setting up capture session: \(error.localizedDescription)")
        }
    }
    
    
    private func setupVideoWriter() {
        if recordingStartTime != nil {
            recordingStartTime = nil
        }
        // set writing destination url
        guard let outputURL = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("video.mov") else { fatalError() }
        try? FileManager.default.removeItem(at: outputURL)
        
        // initialize video writer
        videoWriter = try! AVAssetWriter(outputURL: outputURL, fileType: .mov)
        
        // set video input
        let videoOutputSettings: [String: Any] = [
            AVVideoCodecKey: AVVideoCodecType.h264,
            AVVideoWidthKey: videoSize.width,
            AVVideoHeightKey: videoSize.height
        ]
        videoWriterVideoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoOutputSettings)
        videoWriterVideoInput.expectsMediaDataInRealTime = true
        
        // use adaptor for write processed pixelbuffer
        videoWriterPixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoWriterVideoInput, sourcePixelBufferAttributes: [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)])
        
        // set audio input
        let audioOutputSettings: [String: Any] = [
            AVFormatIDKey: kAudioFormatMPEG4AAC,
            AVNumberOfChannelsKey: 2,
            AVSampleRateKey: 44100,
            AVEncoderBitRateKey: 128000
        ]
        videoWriterAudioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioOutputSettings)
        videoWriterAudioInput.expectsMediaDataInRealTime = true
        
        if videoWriter.canAdd(videoWriterVideoInput) {
            videoWriter.add(videoWriterVideoInput)
        }
        if videoWriter.canAdd(videoWriterAudioInput) {
            videoWriter.add(videoWriterAudioInput)
        }
    }
    
    func saveVideoToPhotoLibrary(url: URL) {
        PHPhotoLibrary.requestAuthorization { status in
            if status == .authorized {
                PHPhotoLibrary.shared().performChanges({
                    PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)
                }) { saved, error in
                    DispatchQueue.main.async {
                        if saved {
                            print("Video saved in photo library")
                            self.setupVideoWriter()
                        } else {
                            print("Failed saving: \(String(describing: error))")
                        }
                    }
                }
            } else {
                print("Access denided to photo library")
            }
        }
    }
    
    // MARK: -View
    private func setupView() {
        imageView.frame = view.bounds
        descriptionLabel.frame = CGRect(x: 0, y: view.center.y, width: view.bounds.width, height: 100)
        recordButton.frame = CGRect(x: view.center.x - 50, y: view.bounds.maxY - 150, width: 100, height: 100)
        
        view.addSubview(imageView)
        view.addSubview(descriptionLabel)
        view.addSubview(recordButton)
        
        imageView.contentMode = .scaleAspectFit
        descriptionLabel.text = "Core ML model is initializing./n please wait a few seconds..."
        descriptionLabel.numberOfLines = 2
        descriptionLabel.textAlignment = .center
        recordButton.setImage(UIImage(systemName: "video.circle.fill"), for: .normal)
        recordButton.addTarget(self, action: #selector(recordVideo), for: .touchUpInside)
    }
    
    
    // completion patern
    
    //    private func inference(pixelBuffer: CVPixelBuffer) {
    //        guard let coreMLRequest = coreMLRequest else {
    //            processing = false
    //            return
    //        }
    //        let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer,orientation: .right, options: [:])
    //        do {
    //            try handler.perform([coreMLRequest])
    //        } catch {
    //            print("Vision error: \(error.localizedDescription)")
    //            processing = false
    //
    //        }
    //    }
    
    //    func coreMLRequestCompletionHandler(request:VNRequest?, error:Error?) {
    //        guard let result:VNPixelBufferObservation = coreMLRequest?.results?.first as? VNPixelBufferObservation else {
    //            processing = false
    //            return }
    //        let end = Date()
    //        //        let inferenceTime = end.timeIntervalSince(startTime)
    //        let pixelBuffer:CVPixelBuffer = result.pixelBuffer
    //        let resultCIImage = CIImage(cvPixelBuffer: pixelBuffer)
    //        let resizedCIImage = resultCIImage.resize(as: videoSize)
    //        let resultUIImage = UIImage(ciImage: resizedCIImage)
    //        if isRecording {
    //            writeProcessedVideoFrame(processedCIImage: resizedCIImage, sampleBuffer: currentSampleBuffer)
    //        }
    //
    //        DispatchQueue.main.async { [weak self] in
    //            self?.imageView.image = resultUIImage
    //        }
    //        processing = false
    //
    //    }
}

```

任意の処理を録画できます。
<img src=https://github.com/john-rocky/Fast-Neural-Style-Transfer-iOS/assets/23278992/1791e665-28ac-4c47-abad-26827b5ded88>

🐣
****
フリーランスエンジニアです。
AIについて色々記事を書いていますのでよかったらプロフィールを見てみてください。

もし以下のようなご要望をお持ちでしたらお気軽にご相談ください。
AIサービスを開発したい、ビジネスにAIを組み込んで効率化したい、AIを使ったスマホアプリを開発したい、
ARを使ったアプリケーションを作りたい、スマホアプリを作りたいけどどこに相談したらいいかわからない…

いずれも中間コストを省いたリーズナブルな価格でお請けできます。

お仕事のご相談はこちらまで
rockyshikoku@gmail.com

機械学習やAR技術を使ったアプリケーションを作っています。
機械学習／AR関連の情報を発信しています。

[X](https://twitter.com/JackdeS11)
[Medium](https://medium.com/@rockyshikoku)
[GitHub](https://github.com/john-rocky)