관리 메뉴

찹모찌의 기록일지

iOS에서 영상 재생하기 본문

iOS

iOS에서 영상 재생하기

찹모찌 2024. 3. 1. 23:26

숏폼 플랫폼 Layover를 개발하면서, iOS앱 내에서 영상을 재생할 일이 생겼다.

iOS에서 영상을 재생하려면 어떻게 해야할까?

1. 영상 재생하기

1) AVPlayerViewController

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 재생

이렇게 AVPlayerLayer를 이용해 재생할 수 있는데, 위의 AVPlayerViewController와 달리 영상을 재생하는 것 이외에는 아무것도 할 수 없다.

이제부터 요소들을 추가해보자.

3) 추가적인 요소(재생바, 반복 재생)

영상에 필요한 재생바를 넣으려면, UISlider를 이용할 수 있다.

Custom UISlider

UISlider는 위와 같은 구조로 이루어져 있는데, Thumb를 기준으로 Track의 컬러나 ThumbImage등을 변경할 수 있다.

UISlider와 동영상을 동기화시키면 동영상 재생의 위치에 따라 Thumb도 같이 움직일 수 있을 것이다.

UISlider와 AVPlayer를 동기화시키기 위해서는 'addPeriodicTimeObserver'가 필요하다.

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이 있어 해당 동작 시에 처음으로 돌아가게 하는 셀렉터를 넣어주면 된다.

 

이렇게 하면 커스텀 동영상 재생기를 만들 수 있다.

재생 화면 동영상