[iOS] AVFoundation을 이용하여 영상 진행바(UISlider) 구현하기

eung7_·2022년 5월 8일
0

iOS

목록 보기
12/17

AVFoundation

AVFoundation은 동영상, 음악과 관련된 앱을 제작한다면 필수적으로 알아야할 프레임워크다.

이번에는 AVFoundation을 적절히 이용하여
영상의 특정 위치로 바로 이동할 수 있는 UISlider를 구현해보도록 하자.

참고로 AVFoundation의 OverView는 아래 링크에서 살펴볼 수 있다.
https://developer.apple.com/av-foundation/


Player에 Observer 추가하기

func prepareVideo(url: String) {
    guard let url = URL(string: url) else { return }

    player = AVPlayer(url: url)

    let playerLayer = AVPlayerLayer(player: player)
    playerLayer.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.height, height: UIScreen.main.bounds.width)

    videoPlayerView.layer.addSublayer(playerLayer)

    let interval = CMTime(seconds: 0.001, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
    player?.addPeriodicTimeObserver(forInterval:interval, queue: DispatchQueue.main, using: { [weak self] currentTime in
        self?.updateSlider(currentTime)
        self?.updateRemainingText(currentTime)
    })
}

위의 메서드는 간략하게 말하자면 특정 URL을 받아서 동영상을 재생 시키는 메서드이다.

    let interval = CMTime(seconds: 0.001, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
    player?.addPeriodicTimeObserver(forInterval:interval, queue: DispatchQueue.main, using: { [weak self] currentTime in
        self?.updateSlider(currentTime)
    })
}

여기서 주목해야 할 것은 위의 코드이다.
Interval 이라는 상수는 CMTime이라는 객체를 받고 있는데,
0.001초마다 Player가 반응하게 만들기 위해서 0.001을 넣어주었다.

CMTime

CMTime이란 시간 값을 분자로, 스케일을 분모로 하여 값을 만들어낸 후 시간을 표현하는 구조체이다.
AVFoundation에서 제공하는 이 구조체는 주로 미디어의 특정 위치를 시간으로 표현할 수 있는 도구라고 생각하면 된다.

쉽게 Seconds가 분자, Timescale이 분모로 들어가서 CMTime 객체가 만들어진다고 생각하면 된다.
만약 CMTime(seconds: 2, preferredTimescale: 2)이라면 CMTime은 1초가 된다.

addPeriodicTimeObserver

그리고 현재 재생되는 AVPlayer 객체인 player에 addPeriodicTimeObserver 메서드를 불러주고 있다.
이 메서드는 세 개의 인자를 받고 있는데,

  1. forInterval
    이 인자 값으로는 CMTime 타입만 올 수 있으며
    영상이 인자에 들어온 CMTime 값만큼의 간격으로 Observer를 두어
    콜백함수에 작성한 클로저를 실행시킨다.
  1. queue
    특정 간격마다 실행되는 클로저가 어떤 Queue에서 작동할지 정해준다.
    나는 UI의 작업을 수행할 예정이라 메인 스레드로 이동 시켜 주었다.
  2. using
    영상이 정해준 간격만큼 시간이 지날때마다 실제로 실행되는 콜백함수이다.
    이 콜백 함수의 인자로써 현재 시간을 나타내는 CMTime 객체가 들어온다.
    이 인자를 사용하여 Slider를 실시간으로 업데이트 시켜주면 된다.
    실제로 currentTime이란 객체를 받아서 updateSlider라는 메서드를 실행시켜주는 것을 볼 수 있다.

이 메서드에 using 클로저 안에는 updateSlider라는 메서드를 불러주고 있다.
그 메서드의 자세한 사항은 아래에서 볼 수 있다.


콜백함수 로직 구현하기

이제 본격적으로 Slider의 실제 값을 넣어줄 차례이다.

    func updateSlider(_ currentTime: CMTime) {
        if let currentItem = player?.currentItem {
            let duration = currentItem.duration
            if CMTIME_IS_INVALID(duration) {
                return
            }
            progressBar.value = Float(CMTimeGetSeconds(currentTime) / CMTimeGetSeconds(duration))
        }
    }

이 메서드는 인자로 currentTime 즉 0.001초 간격으로 생성되는 CMTime을 계속해서 받고 있는 셈이다 !
먼저 AVPlayer 객체로 부터 현재 재생되는 영상의 총 재생 시간을 받아오고 있다.
물론 duration이라는 것은 CMTime 타입이다. 그리고 이것을 CMTimeGetSeconds라는 메서드를 통해서
우리가 식별할 수 있는 초 단위의 Float형으로 반환할 수 있게 된다.

그리고 재생 바, 즉 progressBar에 value 값은 총 1이 되어야 한다.
그래서 분모 값은 영상 전체의 길이인 duration을 넣어주었고
분자 값으로는 실시간으로 변경되는 currentTime을 초 단위의 시간으로 바꿔서 넣어주었다.
이제 영상을 재생시키면 ProgressBar는 영상 길이에 맞춰서 이동하게 된다 !


재생 바에 addTarget 추가하기

이제 남은 것은 재생 바를 드래그를 하면 영상이 특정 위치로 이동하는 로직을 구현해야 한다.

    lazy var progressBar: UISlider = {
        let slider = UISlider()
        slider.tintColor = .systemRed
        slider.addTarget(self, action: #selector(didChangedProgressBar(_:)), for: .valueChanged)
        
        return slider
    }()
    
    @objc func didChangedProgressBar(_ sender: UISlider) {
        guard let duration = player?.currentItem?.duration else { return }

        let value = Float64(sender.value) * CMTimeGetSeconds(duration)
        
        let seekTime = CMTime(value: CMTimeValue(value), timescale: 1)
        
        player?.seek(to: seekTime)
    }

필자는 UI를 오직 코드로 짜기 때문에 progressBar에 addTarget을 추가했다.
여기서 주의할 점은 Slider의 드래그를 감지하기 위해서는 .valueChanged가 선택되어야 한다는 것이다.
이제 드래그가 될 때마다 didChangedProgressBar(_:)라는 메서드가 호출이 된다.

이 메서드에서도 필수적으로 영상의 총 길이가 필요하다.
sender로써 현재 사용하고 있는 UISlider라는 객체가 내려오고 이 sender의 value 값은 우리는 추출해낼 수 있다.
그리고 영상의 총 길이와 곱해주면 내가 드래그 한 위치의 값을 알게 된다.
그리고 CMTime객체로 다시 변형하여 AVPlayer 내부의 메서드로 구현된 seek(to:)에 찾아야 할 위치를 넣어주면 된다.

이제 player는 드래그 하면 특정 위치의 영상을 재생하게 된다 !

profile
안녕하세요. SW Engineer eung7입니다.

0개의 댓글