AVFoundation은 동영상, 음악과 관련된 앱을 제작한다면 필수적으로 알아야할 프레임워크다.
이번에는 AVFoundation을 적절히 이용하여
영상의 특정 위치로 바로 이동할 수 있는 UISlider를 구현해보도록 하자.
참고로 AVFoundation의 OverView는 아래 링크에서 살펴볼 수 있다.
https://developer.apple.com/av-foundation/
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이란 시간 값을 분자로, 스케일을 분모로 하여 값을 만들어낸 후 시간을 표현하는 구조체이다.
AVFoundation에서 제공하는 이 구조체는 주로 미디어의 특정 위치를 시간으로 표현할 수 있는 도구라고 생각하면 된다.
쉽게 Seconds가 분자, Timescale이 분모로 들어가서 CMTime 객체가 만들어진다고 생각하면 된다.
만약 CMTime(seconds: 2, preferredTimescale: 2)이라면 CMTime은 1초가 된다.
그리고 현재 재생되는 AVPlayer 객체인 player에 addPeriodicTimeObserver 메서드를 불러주고 있다.
이 메서드는 세 개의 인자를 받고 있는데,
이 메서드에 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는 영상 길이에 맞춰서 이동하게 된다 !
이제 남은 것은 재생 바를 드래그를 하면 영상이 특정 위치로 이동하는 로직을 구현해야 한다.
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는 드래그 하면 특정 위치의 영상을 재생하게 된다 !