AVFoundation - 비디오의 인코딩(압축) 및 업로드(2) - AssetReader / Writer 이용

2023. 3. 20. 11:41iOS/이슈

 

AVFoundation - 비디오의 인코딩(압축) 및 업로드(1) - ExportSession 이용

 

ExportSession을 사용하지 않게된 이유

exportSession은 프리셋을 사용해 인코딩을 조절할 수 있기 때문에, 원하는 화질을 유지하면서 용량을 낮추는데에 한계가 있었다.  따라서, low level API인 Asset Reader / Writer를 사용하게 되었다. stream을 이용한 파일 입출력과 비슷한 느낌이다.

 

Asset Audio Reader: 오디오가 없는 비디오 case를 고려하여 처리

// Set AudioReader
var assetReaderAudioOutput: AVAssetReaderTrackOutput?

if let audioTrack = asset.tracks(withMediaType: AVMediaType.audio).first {
  let audioReaderSettings: [String : Any] = [
    AVFormatIDKey: kAudioFormatLinearPCM,
    AVSampleRateKey: 44100,
    AVNumberOfChannelsKey: 2
  ]
  assetReaderAudioOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: audioReaderSettings)

  if reader.canAdd(assetReaderAudioOutput!) {
    reader.add(assetReaderAudioOutput!)
  } else {
    print("Couldn't add audio output reader")
    return
  }
}

Asset Video Reader

// Set Video Reader
guard let videoTrack = asset.tracks(withMediaType: AVMediaType.video).first else { return }
    
let videoReaderSettings: [String:Any] = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32ARGB]
let assetReaderVideoOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: videoReaderSettings)

if reader.canAdd(assetReaderVideoOutput) {
      reader.add(assetReaderVideoOutput)
} else {
  print("Couldn't add video output reader")
  return
}

Asset Writer setting - 용량에 영향을 미치는 resolution과 bitrate값 설정

유튜브의 화질 별 권장 비트레이트 값

// Set Writer
let videoSettings:[String:Any] = [
  AVVideoCompressionPropertiesKey: [AVVideoAverageBitRateKey: 890_000], // 890K
  AVVideoCodecKey: AVVideoCodecType.h264,
  AVVideoHeightKey: videoTrack.naturalSize.height,
  AVVideoWidthKey: videoTrack.naturalSize.width,
  AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill
]
    
let audioSettings: [String:Any] = [
      AVFormatIDKey : kAudioFormatMPEG4AAC,
      AVNumberOfChannelsKey : 2,
      AVSampleRateKey : 44100.0,
      AVEncoderBitRateKey: 128000
]

let audioInput = AVAssetWriterInput(mediaType: AVMediaType.audio, outputSettings: audioSettings)
let videoInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: videoSettings)
videoInput.transform = videoTrack.preferredTransform

do {
  let outputURL = createMP4TempURL()
  assetWriter = try AVAssetWriter(outputURL: outputURL, fileType: AVFileType.mp4)
} catch {
  assetWriter = nil
}
guard let writer = assetWriter else {
  print("assetWriter was nil")
  return
}

writer.shouldOptimizeForNetworkUse = true
writer.add(videoInput)
writer.add(audioInput)

writer.startWriting()
reader.startReading()
writer.startSession(atSourceTime: CMTime.zero)

적용된 Setting값으로 AssetWriting 시작

비디오 / 오디오 각각의 시리얼 큐를 선언해주고 requestMediaDataWhenReady()에 파라미터로 넣어준다. input에 data를 요청하여 버퍼에 넣고 버퍼를 다시 writer에 append()한다.

다음 버퍼가 없으면 markAsFinished() 해주고 close Handler를 호출해준다.

핸들러에서 오디오 / 비디오 플래그를 검사하고 writer를 종료해주고, outputURL을 반환한다.

 

let videoInputQueue = DispatchQueue(label: "videoQueue")
let audioInputQueue = DispatchQueue(label: "audioQueue")

// Close Writer Handler
let closeWriter: () -> Void = {
  if (audioFinished && videoFinished) {
    self.assetWriter?.finishWriting(completionHandler: { [weak self] in
      if let outputURL = self?.assetWriter.outputURL {
        completion(outputURL)
      }
    })
    self.assetReader?.cancelReading()
  }
}
// markAsFinished() 호출 전까지 반복적으로 콜백을 보낸다
audioInput.requestMediaDataWhenReady(on: audioInputQueue) {
  while(audioInput.isReadyForMoreMediaData) { // false 이거나 markAsFinsihed() 전까지
    if let cmSampleBuffer = assetReaderAudioOutput?.copyNextSampleBuffer() {
      audioInput.append(cmSampleBuffer)
    } else {
      audioInput.markAsFinished()
      DispatchQueue.main.async {
        audioFinished = true
        closeWriter()
      }
      break
    }
  }
}

videoInput.requestMediaDataWhenReady(on: videoInputQueue) {
  while(videoInput.isReadyForMoreMediaData) {
    if let cmSampleBuffer = assetReaderVideoOutput.copyNextSampleBuffer() {
      videoInput.append(cmSampleBuffer)

      // 버퍼 타임 스탬프와 전체 duration을 이용해 비율 계산. 프로그래스 출력
      let timestamp = CMSampleBufferGetPresentationTimeStamp(cmSampleBuffer)
      let seconds = CMTimeGetSeconds(timestamp)
      let value = seconds / duration
      progressBlock(Float(value))
    } else {
      videoInput.markAsFinished()
      DispatchQueue.main.async {
        videoFinished = true
        closeWriter()
      }
      break
    }
  }
}

 

참고

Github: 백그라운드 테스크와 config 주입 예제 - testfairy github

StackOverflow: swift5 테스트된 bitrate config 질문 답변

StackOverflow: 프리셋 활용을  권장하는 답변. 질문 퀄리티가 좋고, 질문에 있는 예제 코드가 좋음