티스토리 뷰

iOS

[iOS] AVAssetWriter로 비디오 저장

Peppo 2023. 3. 26. 21:13
728x90

회사에서 움직임 감지 기능구현을 하면서 두 가지 기능이 필요했다. 
 
1. 영상 녹화/ 저장
2. 움직임 감지
 
일단 영상 녹화/저장은 참고해볼 자료가 많아서 리서치 하는데 오래 걸리진 않았지만.. 
움직임 감지기능과 영상을 녹화,저장을 동시에 하는 구현을 하면서 겪었던 산전수전을 기록하고자 한다. 
 


삽질시작

빨리 끝낼 수 있는것 부터 손보려고 했고,
영상 녹화/ 저장 으로 가장 많이 나왔던 키워드는 AVCaptureMovieFileOutput, PHPhotoLibrary 였다.
이전 Camera설정 - AVCaptureSession 에서 블로깅했듯, 어렵지 않게 영상 녹화/ 저장 부분을 구현할 수 있었다.
 
그런 다음 움직임 감지를 하기 위해 제가 여러가지 리서치를 해보며 추려낸 키워드는 AVCaptureVideoDataOutput 였다.
 
 

아 그럼 CameraSession에 addOutput으로 
AVCaptureMovieFileOutput, AVCaptureVideoDataOutput 추가 하면 되겠군!!

.
.
.

응 안돼~

 
이상하게도 MovieFileOutput을 사용해 영상녹화를 할때 VideoDataOutput에서 받아야할 데이터들이 뚝 끊겨 작동을 하지 않았고
여러가지 검색을 해보면서 많은 시도들을 해봤다.
(captureSession multiple output, addOutput movieFileOutput videoDataOutput 등등..)
 

검색: AVCaptureMovieFileOutput, AVCaptureVideoDataOutput 두개 사용 가능?

 
지금에서야 다시 검색해서 보면 답변 첫줄에 쓸 수 없다고 나와있는데,
MovieFileOutput으로 녹화를 구현해놨기 때문에 놓고 싶지 않았던것 같다..
 
그리고 영상 녹화를 하려면 MovieFileOutput으로만 해야하는 줄 알았다.
 


AVAssetWriter

그럼 다시 돌아가서 AVCaptureVideoDataOutput 를 사용해서 영상 저장을 할 수 있을까? 라는 의문점에 찾아보니 있었다!
 

you can use AVCaptureVideoDataOutput and analyse or modify on the data,
then use AVAsseWriter to write the frames to a file.

AVCaptureVideoDataOutput에서 받아오는 data를 이용해 AVAssetWriter로 `파일`에 저장하믄 돼!
 
바로 파봤다. 도와줘요 공식문서~~

뭔말인지 모르겠서~~

 
사실 AVAssetWriter에 대한 정보는 많이 없다.. ㅠ
 
조금 정리를 해보자면 답변에서 처럼 media data를 가지고 파일에 저장하고 QuickTime 동영상 파일 형식MPEG-4 파일 형식과 같은 파일로 저장 한다.
 
 


Usage

 
(들어가기전)
AVCaptureVideoDataOutput class를 까보면 아래의 프로토콜과 메서드가 있다.

AVCaptureVideoDataOutputSampleBufferDelegate
captureOutput(output:, didOutput:, connection:)

AVCaptureVideoDataOutputSampleBufferDelegate를 채택후 captureOutput 메서드에서 print를 찍어보면,

위 처럼 뭔가 와다다다다 호출되는걸 볼 수 있는데
didOutput에 sampleBuffer가 위에서 계속 언급했던 data로 이해하면 될것 같다.

 

즉, captureOutput에서 약 0.1초마다 카메라에 입력되는 data들을 assetWriter를 이용해 저장한다는 뜻

 
data를 받아오고 AVAssetWriter로 `파일`에 저장 하는 방법으로, 큰 흐름을 적어보자면 아래와 같다.
(Camera설정 - AVCaptureSession 적용되어 있는걸로 가정)
 

  1. 녹화상태 설정 & CaptureSession 추가설정
  2. (녹화시작) 저장 파일설정 (경로, 유형)
  3. (녹화중) 데이터 수집
  4. (녹화종료) 수집된 데이터, 설정해둔 파일경로에 저장

0.  녹화상태  설정 & CaptureSession 추가설정

 

• 녹화상태 

`대기`, `녹화시작`, `녹화중`, `녹화종료` 상태 를 통해 assetWriter의 역할을 나누어야 하므로, 
CaptureStatus 라는 열거형을 하나 만들자. 

private enum CaptureStatus {
        case idle, start, capturing, end
}

 
기본값은 .idle 로 설정 (대기중)

import AVFoundation


class SomeViewController {
    private enum CaptureStatus {
        case idle, start, capturing, end
    }
    
    private var captureStatus: CaptureStatus = .idle
    
    // viewDidLoad()
    // ...

}

 

• CaptureSession 추가 설정

기존 CaptureSession에 아래와 같이 추가해준다. 

private func setupCaptureSession() {
        let session = AVCaptureSession()
        guard let videoDevice = getAccessibleCameraInput(),
              let audioDevice = AVCaptureDevice.default(for: .audio),
              let videoInput = try? AVCaptureDeviceInput(device: videoDevice),
              let audioInput = try? AVCaptureDeviceInput(device: audioDevice),
              session.canAddInput(videoInput), session.canAddInput(audioInput)
        else {
            print("카메라 또는 오디오를 찾을 수 없음.")
            listener?.request(.camNotFoundError)
            return
        }
        
        // 1. 추가 - video, audio output
        let videoOutput = AVCaptureVideoDataOutput()
        let audioOutput = AVCaptureAudioDataOutput()
        guard session.canAddOutput(videoOutput), session.canAddOutput(audioOutput) else {
            print("output을 찾을 수 없음")
            return
        }
        // 2. 추가 - SerialQueue
        let queue = DispatchQueue(label: "recordingQueue", qos: .userInteractive)
        videoOutput.setSampleBufferDelegate(self, queue: queue)
        audioOutput.setSampleBufferDelegate(self, queue: queue)
        
        session.beginConfiguration() // 세션구성 시작
        
        // 3. session에 추가
        session.addInput(videoInput)
        session.addInput(audioInput)
        session.addOutput(videoOutput)
        session.addOutput(audioOutput)
        
        session.commitConfiguration()
        
        previewView?.videoPreviewLayer.session = session
        videoDataOutput = videoOutput
        audioDataOutput = audioOutput
        captureSession = session
        
        DispatchQueue.global(qos: .default).async {
            session.startRunning()
        }
    }

 
1. AVCaptureVideoDataOutputAVCaptureAudioDataOutput 인스턴스를 만들어 준다.
2. 추후 AVCaptureVideoDataOutputSampleBufferDelegatecaptureOutput에서 sampleBuffer값을 가져올건데 SerialQueue를 통해 받아오도록 queue를 설정해준다. 
3. captureSession에 addOutput을 이용해 1번에서 설정한 output들을 추가 해 준다.
 


1. (녹화시작) 저장 파일 설정 (경로, 유형) 

녹화가 시작될때 FileManager를 이용해서 파일 경로를 설정해준다.
 

import AVFoundation


class SomeViewController {

   // enum CaptureStatus ...

    private var captureStatus: CaptureStatus = .idle
    private var assetWriter: AVAssetWriter?
    private var assetWriterVideoInput: AVAssetWriterInput?
    private let fileManager = FileManager.default
    private var fileURL: URL?
    private var fileName: String = ""
    
    // viewDidLoad()
    // ...

}

extension SomeViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
    
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    
        switch captureStatus {
        case .start:
        
          // 1 저장파일 설정
          let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
          fileName = "파일이름.mp4"
          fileURL = documentDirectory.appendingPathComponent(fileName)
        
          // 2. assetWriter로 파일경로, 파일타입 설정
          do {
              assetWriter = try AVAssetWriter(outputURL: fileURL!, fileType: .mp4)
          } catch {
              print("Error creating asset writer: \(error.localizedDescription)")
              return
          }
        
          // 3. 영상 화질 설정
          let videoSettings: [String: Any] = [
              AVVideoCodecKey: AVVideoCodecType.h264,
              AVVideoWidthKey: 320,
              AVVideoHeightKey: 240
          ]
        
          // 4. 미디어타입, 영상화질 input설정
          assetWriterVideoInput = AVAssetWriterInput(
              mediaType: .video,
              outputSettings: videoSettings
          )
          assetWriterVideoInput?.expectsMediaDataInRealTime = true
          assetWriter?.add(assetWriterVideoInput!)
        
          // 5. write 시작
          assetWriter?.startWriting()
          assetWriter?.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(sampleBuffer))
        
          // 6. captureStatus 변경
          captureStatus = .capturing
        }
        
    }
}

2. (녹화중) 데이터 수집

녹화중일 때, captureOutput 메서드의 파라미터 'output'에서 출력되는 data를 assetWriterIntput에 append 한다.
- sampleBuffer를 계속해서 append 해줌

import AVFoundation


class SomeViewController {

    private var assetWriter: AVAssetWriter?
    private var assetWriterVideoInput: AVAssetWriterInput?
    private let fileManager = FileManager.default
    private var fileURL: URL?
    private var fileName: String = ""
    
    // viewDidLoad()
    // ...

}

extension SomeViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
    
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    
       // (녹화시작시)
       // ...
       
       // (녹화중)
       case .capturing:
       
       // 1. 비디오 data만 assetWriter에 추가할 경우
       if output == videoDataOutput, assetWriterVideoInput?.isReadyForMoreMediaData == true {
                assetWriterVideoInput?.append(sampleBuffer)
            }
       
    }
    
}

 
비디오를 저장할때 대부분 오디오도 같이 저장하니깐 
오디오 data를 저장할땐 AVCaptureAudioDataOutputSampleBufferDelegate 를 채택 해 준다.

import AVFoundation


class SomeViewController {

    private var assetWriter: AVAssetWriter?
    private var assetWriterVideoInput: AVAssetWriterInput?
    private var assetWriterAudioInput: AVAssetWriterInput? // 추가
    private let fileManager = FileManager.default
    private var fileURL: URL?
    private var fileName: String = ""
    
    // viewDidLoad()
    // ...

}

extension SomeViewController: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate {
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    
       // (녹화시작시)
       // ...
       
       // (녹화중)
         case .capturing:
         // 1. 비디오 data를 assetWriter에 추가할 경우
         if output == videoDataOutput, assetWriterVideoInput?.isReadyForMoreMediaData == true {
                  assetWriterVideoInput?.append(sampleBuffer)
              }
       
         // 2. 오디오 data를 assetWriter에 추가할 경우
         if output == audioDataOutput, assetWriterAudioInput?.isReadyForMoreMediaData == true {
                  assetWriterAudioInput?.append(sampleBuffer)
              }
       
      }
}

3. (녹화종료) 수집된 데이터를 파일 경로에 저장

마지막으로 녹화종료가 될때 assetWriter를 종료시켜준다.
 

import AVFoundation

class SomeViewController {

    // viewDidLoad()
    // ...
    
}

extension SomeViewController: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate {
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    
       // (녹화시작시)
       // ...
       
       // (녹화중)
       // ...
       
       // (녹화종료시)
       case .end:
         // 1. video, audio write 작업 종료
         assetWriterVideoInput?.markAsFinished()
         assetWriterAudioInput?.markAsFinished()
         assetWriter?.finishWriting {
             // writing 끝나고 작업할게 있으면 여기 추가.
         }
         // 2. assetWriter 초기화
         assetWriter = nil
         assetWriterVideoInput = nil
         assetWriterAudioInput = nil
         
         // 3. captureStatus 초기화
         captureStatus = .idle
      }
}

 
코드가 상태별로 지정해줘야해서 따라하기 어려울 수 있지만, 아래 '참고' 했던 github링크를 남겨놓겠다.
삽질도 많이 했지만, Rx도 적용해보고 기능 처음부터 배포까지 할 수 있었던 좋은 경험이었던것 같다. 
조만간 리팩토링하면서 문서화도 해봐야겠다. 
 


느낀점

기능 구현함에 있어서 여러 가능성을 열어두자 최대한 시도는 많이 해보되, 안된다 싶으면 다른방안을 찾아 빠르게 적용해보는게 오히려 시간을 단축하는것 같다. 
이번 구현을 통해서 stackOverflow도 좋지만, 위와 같이 큰 흐름을 읽어야하는 경우에는 github예시 코드들을 참고하는게 더 많은 도움이 되었던것 같다. 
 
 
다음 블로깅 때는 FileManager에 대해 작성 해봐야겠다.
 


참고

 
1. https://developer.apple.com/documentation/avfoundation/avassetwriter

 

AVAssetWriter | Apple Developer Documentation

An object that writes media data to a container file.

developer.apple.com

2. https://gist.github.com/yusuke024/b5cd3909d9d7f9e919291491f6b381f0

 

Recording video with AVAssetWriter

Recording video with AVAssetWriter. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

 

728x90