본문 바로가기
iOS

🎥 iOS 비디오 재생, 녹화, 병합 튜토리얼 (2)

by HaningYa 2020. 9. 1.
728x90

 

source: https://www.raywenderlich.com/10857372-how-to-play-record-and-merge-videos-in-ios-and-swift [출처]

이전 포스팅

 

🎥 iOS 비디오 재생, 녹화, 병합 튜토리얼 (1)

[출처] How to Play, Record and Merge Videos in iOS and Swift Learn the basics of working with videos on iOS with AV Foundation in this tutorial. You’ll play, record and even do some light video ed..

haningya.tistory.com

출처

 

How to Play, Record and Merge Videos in iOS and Swift

Learn the basics of working with videos on iOS with AV Foundation in this tutorial. You’ll play, record and even do some light video editing!

www.raywenderlich.com


이전까지 구현한 것

  • 포토앨범에서 비디오 불러와 재생하기
  • 카메라로 비디오 찍은 후 포토앨범에 저장하기
  • 병합할 비디오1, 2 와 음악 불러오기

목표 

  • 비디오 2개와 음악 합치기
  • 비디오는 2개를 연결, 그 위에 음악 올리기

*실제 기기에서 실행하려면 Privacy 에 관한 설명을 info.plist 에 써줘야 한다.


Assets(비디오, 오디오) 병합 completion 함수 작성

병합할 때 중간에 돌아가는 로딩 indicator 를 추가해 준다.

activityMonitor로 이름

import Photos
func exportDidFinish(_ session: AVAssetExportSession) {
  // 1
  activityMonitor.stopAnimating()
  firstAsset = nil
  secondAsset = nil
  audioAsset = nil

  // 2
  guard
    session.status == AVAssetExportSession.Status.completed,
    let outputURL = session.outputURL
    else { return }

  // 3
  let saveVideoToPhotos = {
    // 4
    let changes: () -> Void = {
      PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: outputURL)
    }
    PHPhotoLibrary.shared().performChanges(changes) { saved, error in
      DispatchQueue.main.async {
        let success = saved && (error == nil)
        let title = success ? "Success" : "Error"
        let message = success ? "Video saved" : "Failed to save video"

        let alert = UIAlertController(
          title: title,
          message: message,
          preferredStyle: .alert)
        alert.addAction(UIAlertAction(
          title: "OK",
          style: UIAlertAction.Style.cancel,
          handler: nil))
        self.present(alert, animated: true, completion: nil)
      }
    }
  }
    
  // 5
  if PHPhotoLibrary.authorizationStatus() != .authorized {
    PHPhotoLibrary.requestAuthorization { status in
      if status == .authorized {
        saveVideoToPhotos()
      }
    }
  } else {
    saveVideoToPhotos()
  }
}
  • 위 함수는 Export 가 끝났을 때 호출되는 함수이다.
  • 함수가 끝나면 activityMonitor를 멈추고 (로딩 indicator)
  • 사용한 Asset 들을 초기화 한다. (nil할당)
  • session.status를 통해 export 가 완료되었는지 확인 한 뒤 완료된 파일의 url 을 outputURL 에 저장한다.
  • PHPhotoLibrary를 통해 (파일 접근과 같은 객체는 singleton패턴으로 shared를 통해 일관성을 유지한다) 비디오를 저장한다.
  • 비디오가 저장되면 AlertController로 완료되었다고 알려준다.
  • 마지막은 만약 포토앨범 권한이 없으면 먼저 권한 요청부터 하는 코드이다.

실제 Asset 병합 코드 작성

코드가 많이 기니까 주석으로 설명하겠다.

  func merge(){
 		//비디오 두개가 선택되었는지 guard 문으로 확인해본다.
        guard
            let firstAsset = firstAsset,
            let secondAsset = secondAsset
            else { return }
        //로딩 indicator 를 시작한다.
        activityMonitor.startAnimating()
        
        // 1 비디오와 오디오 트랙을 잠시 저장해둘 AVMutableComposition을 선언한다.
        let mixComposition = AVMutableComposition()
        
        // 2 해당 AVMutableComposition에 첫번째 비디오 트랙을 추가한다.
        guard
            let firstTrack = mixComposition.addMutableTrack(
                withMediaType: .video,
                preferredTrackID: Int32(kCMPersistentTrackID_Invalid))
            else { return }
        
        // 3 insertTimeRange를 통해 첫번째 비디오의 어떤 부분까지 넣을지 정한다. (코드는 전체부분입력)
        do {
            try firstTrack.insertTimeRange(
                CMTimeRangeMake(start: .zero, duration: firstAsset.duration),
                of: firstAsset.tracks(withMediaType: .video)[0],
                at: .zero)
        } catch {
            print("Failed to load first track")
            return
        }
        
        // 4 동일한 방법으로 두번째 비디오를 추가하고 어느 부분을 넣을지 정해준다. (zero ~ duration 이니 전체)
        guard
            let secondTrack = mixComposition.addMutableTrack(
                withMediaType: .video,
                preferredTrackID: Int32(kCMPersistentTrackID_Invalid))
            else { return }
        
        do {
            try secondTrack.insertTimeRange(
                CMTimeRangeMake(start: .zero, duration: secondAsset.duration),
                of: secondAsset.tracks(withMediaType: .video)[0],
                at: firstAsset.duration)
        } catch {
            print("Failed to load second track")
            return
        }
        
        // 5 두개의 AVMutableCompisitionTrace 인스턴스를 준비했으니
        // 6 AVMutableVideoCompositionLayerInstruction을 통해 각각의 트랙에 편집이 가능하도록 만든다.
        //mainInstruction을 설정해 편집된 비디오의 totalTime이 first + second 비디오 재생시간의 합으로 한다.
        let mainInstruction = AVMutableVideoCompositionInstruction()
        mainInstruction.timeRange = CMTimeRangeMake(
            start: .zero,
            duration: CMTimeAdd(firstAsset.duration, secondAsset.duration))
        
        // 7 각각의 asset 에 맞게 두개의 instruction을 준비한다. 
        // first instruction은 투명하게 만들어 두번째 비디오가 시작될 때 보여지지 않게 설정한다.
        let firstInstruction = AVMutableVideoCompositionLayerInstruction(
            assetTrack: firstTrack)
        firstInstruction.setOpacity(0.0, at: firstAsset.duration)
        let secondInstruction = AVMutableVideoCompositionLayerInstruction(
            assetTrack: secondTrack)
        
        // 8 위에서 세팅한 두개의 instruction 인스턴스를 mainInstruction에 추가해준다.
        // main instruction을 AVMutableVideoComposition 의 프로퍼티에 추가해준다.
        //frame rate 은 30fps이다.
        mainInstruction.layerInstructions = [firstInstruction, secondInstruction]
        let mainComposition = AVMutableVideoComposition()
        mainComposition.instructions = [mainInstruction]
        mainComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
        mainComposition.renderSize = CGSize(
            width: UIScreen.main.bounds.width,
            height: UIScreen.main.bounds.height)
        
        // 9
        // 10 오디오 에셋을 불러온다. 시간 범위는 첫번째와 두번째 비디오 재생길이의 합으로 설정한따.
     
        if let loadedAudioAsset = audioAsset {
            let audioTrack = mixComposition.addMutableTrack(
                withMediaType: .audio,
                preferredTrackID: 0)
            do {
                try audioTrack?.insertTimeRange(
                    CMTimeRangeMake(
                        start: .zero,
                        duration: CMTimeAdd(
                            firstAsset.duration,
                            secondAsset.duration)),
                    of: loadedAudioAsset.tracks(withMediaType: .audio)[0],
                    at: .zero)
            } catch {
                print("Failed to load Audio track")
            }
        }
        
        // 11 완성된 비디오를 저장하기 전에 먼저 저장될 경로가 필요하다.
        //현재 시간을 활용해 unique 한 파일 이름을 설정해준다.
        guard
            let documentDirectory = FileManager.default.urls(
                for: .documentDirectory,
                in: .userDomainMask).first
            else { return }
        let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .long
        dateFormatter.timeStyle = .short
        let date = dateFormatter.string(from: Date())
        let url = documentDirectory.appendingPathComponent("mergeVideo-\(date).mov")
        
        // 12 합쳐진 비디오를 render하고 export 해야 한다. 먼저 AVAssetExportSession을 만들어 
        //export 설정과 함께 content 을 transcode 한다. 이전에 AVMutableVideoComposition을 설정해놨기 때문에 exporter 에 assign만 하면 된다.
        guard let exporter = AVAssetExportSession(
            asset: mixComposition,
            presetName: AVAssetExportPresetHighestQuality)
            else { return }
        exporter.outputURL = url
        exporter.outputFileType = AVFileType.mov
        exporter.shouldOptimizeForNetworkUse = true
        exporter.videoComposition = mainComposition
        
        // 13 export session을 초기화 한 뒤 exportAsynchrously()를 통해 export 작업을 시작할 수 있다.
        // 비동기적으로 동작하기 때문에 함수는 바로 리턴한다. completion handler를 통해 성공/실패를 전닫ㄹ한다.
        exporter.exportAsynchronously {
            DispatchQueue.main.async {
                self.exportDidFinish(exporter)
            }
        }
    }
  • AVcomposition은 미디어 데이터를 여러 파일 소스에서 가져와 합친다.
  • AVComposition의 최상단 레벨에는 각각 audio 나 video 미디어를 표현하는 track 들의 집합으로 구성되어있다.
  • AVCompotisionTrack 이 각각의 트랙을 나타낸다.
  • AVMutableComposition과 AVMutableCompositionTrack 도 비슷하게
    composition을 만드는 고차원 수준의 인터페이스를 제공한다.
  • 이 객첻들은 insertion, removal, scaling 등의 operation을 제공한다.

결과

움짤이라 음악이 안나오지만 오디오도 제대로 들어갔다.


편집된 영상을 보면 해상도가 안맞거나 orientation이 맞지 않는걸 볼 수 있다.

preferredTransform을 설정해주면 되는데 그건 원본 튜토리얼의 Orienting Video 파트를 보면 알 수 있다.
[원본튜토리얼]


[프로젝트 코드]

 

KimTaeHyeong17/Dreamin_iOS_2020

한화 드림인 아카데미 레포. Contribute to KimTaeHyeong17/Dreamin_iOS_2020 development by creating an account on GitHub.

github.com

 

 

728x90

댓글