AVFoundation - 비디오의 인코딩(압축) 및 업로드(2) - AssetReader / Writer 이용
2023. 3. 20. 11:41ㆍiOS/이슈
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: 프리셋 활용을 권장하는 답변. 질문 퀄리티가 좋고, 질문에 있는 예제 코드가 좋음
'iOS > 이슈' 카테고리의 다른 글
Nested ScrollView - 프로 프로필 피드 탭 구현 (0) | 2023.03.29 |
---|---|
AVFoundation - 비디오의 인코딩(압축) 및 업로드(3) - 업로드 개선 (0) | 2023.03.21 |
AVFoundation - 비디오의 인코딩(압축) 및 업로드(1) - Export Session 이용 (0) | 2023.03.18 |
잘못된 코드로 인하여 AWS 폭탄 맞은 이야기 (0) | 2023.03.17 |
[AVAsset]에서 duration load delay 이슈 (0) | 2023.02.03 |