찹모찌의 기록일지
iOS에서 영상 재생하기 본문
숏폼 플랫폼 Layover를 개발하면서, iOS앱 내에서 영상을 재생할 일이 생겼다.
iOS에서 영상을 재생하려면 어떻게 해야할까?
1. 영상 재생하기
1) AVPlayerViewController
위와 같이 AVKit에서 제공하는 AVPlayerViewController를 쓰면 쉽게 재생을 할 수 있다.
그러나 AVPlayerViewController는 커스텀이 어렵기 때문에 AVPlayerLayer를 사용해 커스텀 재생 화면을 만들 수 있다.
2) AVPlayerLayer
// 먼저 AVPlayerLayer가 있는 VideoView class를 만들어준다.
// AVPlayerLayer는 VideoView에 가득차게 만들어 질 것이다.
final class VideoView: UIView {
private let playerLayer: AVPlayerLayer = AVPlayerLayer()
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .systemBlue
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func layoutSubviews(_ view: UIView) {
super.layoutSubviews()
playerLayer.frame = view.bounds
}
func setLayoutSubViews(_ view: UIView) {
playerLayer.frame = view.bounds
}
func setLayout() {
self.layer.addSublayer(playerLayer)
}
func setPlayer(_ player: AVPlayer) {
playerLayer.player = player
playerLayer.videoGravity = .resizeAspectFill
}
func playVideo() {
playerLayer.player?.play()
}
}
위와 같은 방식으로 AVPlayerLayer를 VideoView라는 Class에 넣어 관리한다.
final class ViewController: UIViewController {
private var video: Video!
private var videoView: VideoView = VideoView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(videoView)
videoView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
videoView.topAnchor.constraint(equalTo: view.topAnchor),
videoView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
videoView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
videoView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
override func viewDidAppear(_ animated: Bool) {
setVideoView()
videoView.playVideo()
}
func createVideo() -> Video {
let url: URL = URL(string: "https://devstreaming-cdn.apple.com/videos/wwdc/2018/103zvtnsrnrijr/103/hls_vod_mvp.m3u8")!
return Video(hlsURL: url, title: "테스트")
}
// AVPlayerItem에 영상 URL을 담아준 후 이 AVPlayerItem을 담은 AVPlayer를 넣어주면 끝!
func setVideoView() {
let video: Video = createVideo()
let item: AVPlayerItem = AVPlayerItem(url: video.hlsURL)
let player: AVPlayer = AVPlayer(playerItem: item)
videoView.setLayout()
videoView.setPlayer(player)
videoView.layoutSubviews(view)
}
}
이렇게 AVPlayerLayer를 이용해 재생할 수 있는데, 위의 AVPlayerViewController와 달리 영상을 재생하는 것 이외에는 아무것도 할 수 없다.
이제부터 요소들을 추가해보자.
3) 추가적인 요소(재생바, 반복 재생)
영상에 필요한 재생바를 넣으려면, UISlider를 이용할 수 있다.
UISlider는 위와 같은 구조로 이루어져 있는데, Thumb를 기준으로 Track의 컬러나 ThumbImage등을 변경할 수 있다.
UISlider와 동영상을 동기화시키면 동영상 재생의 위치에 따라 Thumb도 같이 움직일 수 있을 것이다.
UISlider와 AVPlayer를 동기화시키기 위해서는 'addPeriodicTimeObserver'가 필요하다.
addPeriodicTimeObserver는 "재생 중에 지정된 블록의 주기적 호출을 요청하여 변경 시간을 보고합니다"라고 되어 있다.
func addPeriodicTimeObserver() {
// Invoke callback every half second
let interval = CMTime(seconds: 0.5,
preferredTimescale: CMTimeScale(NSEC_PER_SEC))
// Add time observer. Invoke closure on the main queue.
timeObserverToken =
player.addPeriodicTimeObserver(forInterval: interval, queue: .main) {
[weak self] time in
// update player transport UI
}
}
위와 같은 방식으로 player에 TimeObserver를 걸어 상태 변화를 관찰한다. closure형태로 block을 넣어 player의 변화에 맞게 UI를 업데이트 시키면 된다.
이 때 꼭! 쓰게 되지 않으면 removeObserver를 해줘야 하는데, 이는 후술할 트러블 슈팅에서 서술한다.
// 1초마다 영상의 상태를 감지하여 Slider를 update한다.
func setPlayerSlider() {
let interval: CMTime = CMTimeMakeWithSeconds(1, preferredTimescale: Int32(NSEC_PER_SEC))
timeObserverToken = playerView.player?.addPeriodicTimeObserver(forInterval: interval, queue: .main, using: { [weak self] currentTime in
self?.updateSlider(currentTime: currentTime)
})
}
// updateSlider의 경우 영상의 길이와 현재 시간을 나눠 Slider의 value로 바꾼다. (Slider의 max value는 1)
func updateSlider(currentTime: CMTime) {
guard let currentItem: AVPlayerItem = playerView.player?.currentItem else { return }
let duration: CMTime = currentItem.duration
if CMTIME_IS_INVALID(duration) { return }
playerSlider?.value = Float(CMTimeGetSeconds(currentTime) / CMTimeGetSeconds(duration))
}
이런식으로 AVPlayer와 UISlider를 동기화 시켜 재생바를 표현할 수 있다.
재생 바로 Player이동시키기
AVPlayer에는 seek이란 메서드가 있다.
func seek(to time: CMTime) {
playerLayer?.player?.seek(to: time)
}
이런식으로 AVplayer에 특정 시간대를 주면
해당 AVPlayer는 주어진 시간으로 이동한다. 이를 UISlider와 연동하여 Player를 이동시킬 수 있다.
func addTargetPlayerSlider() {
playerSlider?.addTarget(self, action: #selector(didChangedSliderValue(_:)), for: .valueChanged)
}
@objc private func didChangedSliderValue(_ sender: LOSlider) {
guard let duration: CMTime = playerView.player?.currentItem?.duration else { return }
let value: Float64 = Float64(sender.value) * CMTimeGetSeconds(duration)
let seekTime: CMTime = CMTime(value: CMTimeValue(value), timescale: 1)
playerView.seek(to: seekTime)
playerView.play()
}
이런식으로 UISlider에 valueChanged때마다 Slider의 value에 맞게 seekTime을 만들어 seek할 수 있게 해주면 된다.
반복 재생
AVPlayer의 재생이 다 끝난 경우 처음으로 되돌아 가게 하려면 NotificationCenter를 이용한다.
func playPlayer() {
NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying), name: .AVPlayerItemDidPlayToEndTime, object: playerView.player?.currentItem)
playerView.play()
}
@objc func playerDidFinishPlaying(note: NSNotification) {
playerView.seek(to: CMTime.zero)
playerView.play()
}
NotificationCenter Observer에 AVPlayerItemDidPlayToEndTime이라는 Name이 있어 해당 동작 시에 처음으로 돌아가게 하는 셀렉터를 넣어주면 된다.
이렇게 하면 커스텀 동영상 재생기를 만들 수 있다.
'iOS' 카테고리의 다른 글
내 앨범이 너에게 닿기를: 서버 없이 앨범 전달받기 (0) | 2024.03.15 |
---|---|
설명 뷰가 늘어나면서 올라왔으면 좋겠습니다(애니메이션 구현기). (0) | 2024.03.09 |
숏폼 플랫폼 재생 화면 정보를 불러올 때는 어떻게 불러오는게 좋을까? (1) | 2024.03.08 |
Swift Concurrency와 UICollectionView Prefetch (0) | 2024.03.02 |
iOS에서 영상을 재생시키면서 생긴 문제(addPeriodicTimeObserver, NotificationCenter Observer) (0) | 2024.03.01 |