본문 바로가기

iOS

[iOS] AVPlayer, AVPlayerItem, AVPlayer.currentItem (Video)

 

AVPlayerItem VS AVPlayer

AVPlayerItem AVPlayer
미디어 데이터(video, audio 파일 등)의 정보를 관리(로딩 상태, 버퍼링 상태, 시간 범위 등) AVPlayerItem을 재생하는 역할(Play, Pause, Stop, Seek등)
영상, 오디오 등을 불러옴. (CD) 영상, 오디오 등을 재생함. (CD 플레이어)

 

let url = URL(string: "https://example.com/video.mp4")!
let playerItem = AVPlayerItem(url: url)   // 🎥 영상 데이터를 담는 AVPlayerItem
let player = AVPlayer(playerItem: playerItem)  // 🔊 재생을 담당하는 AVPlayer

player.play()  // ✅ AVPlayer를 통해 재생


// AVPlayer는 하나의 AVPlayerItem을 가지고 있으며, 이를 통해 미디어를 재생한다.

 

 

AVPlayer.currentItem vs playerItem

AVPlayer.currentItem playerItem
AVPlayer가 '현재 재생 중인 AVPlayerItem'을 나타낸다. 특정 AVPlayerItem을 저장하기 위해 사용되는 '커스텀 변수'이다.
playerItem과 같은 인스턴스를 가질 수 있지만, 항상 자동으로 해당 인스턴스로 동기화 되지는 않는다. playerItem 변수를 선언한다고 하여 player.currentItem이 자동으로 해당 값을 가지지는 않는다.

 

player.currentItem?.addObserver(self, forKeyPath: "status", options: .new, context: nil)

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if keyPath == "status" {
        if player.currentItem?.status == .readyToPlay {
            print("비디오 준비 완료")
        } else if player.currentItem?.status == .failed {
            print("비디오 로딩 실패")
        }
    }
}

 

 

위의 코드를 보시다싶이 playerItem은 KVO(Key-Value Observing)방식을 채택하고 있다.

 

왜 애플 개발자들은 KVO 방식을 채택했을까?

 

1.  AVPlayerItem.status는 비동기적 속성이다.

AVPlayerItem은 네트워크를 통해 비디오를 스트리밍하거나 로컬 파일을 로드하는 과정을 비동기적으로 처리한다.

AVPlayerItem이 생성될 때 status는 즉시 초기값인 .unknown을 리턴하고, 네트워크 요청이 완료되면 .readyToPlay, 오류 발생 시 .failed로 변경된걸 감시한다.

 

2. KVO는 객체의 특정 속성이 변경될 떄 즉시 감지 가능하다.

이처럼 특정 속성이 변경될 때 즉시 감지하는 방법은 두가지가 있는데, KVO와 NotificationCenter이 있다.

KVO 특정 객체 속성(status)이 변경될 떄 자동감지
NotificationCenter 특정 이벤트(비디오 종료) 감지

Notificationcenter는 이벤트 중심이라 속성 변화를 직접 감지하는 용도로는 적합하지 않다. 반면 KVO는 속성의 변경을 감지하는 데 최적화된 방식이므로 AVPlayerItem.status를 KVO로 감지한다.

특정 객체의 속성과 이벤트를 헷갈릴 수가 있는데 아래처럼 구분해보자.

속성 : 객체의 상태를 나타내는 값 ( 속성은 항상 존재하며, 변경될 수 있는 값 )

 - 비디오 로드 상태 ( .unknown, .readyToPlay, .failed)

 - 비디오 음량 변경 

이벤트 : 특정한 순간에 발생하는 행동

 - 비디오가 끝까지 재생되면 한 번 발생하는 이벤트라 특정 순간에만 발생하고 사라짐.

 - 끝난 후에는 더 이상 발생하지 않는 trigger가 되는 행동.

 

오디오로 예를 들자면, 이벤트는 오디오를 재생, 멈춤에 해당하고, 객체 속성은 오디오의 음량변경에 해당된다고 볼 수 있겠다.

 

3. AVPlayerItem.status 외에도 KVO가 필요한 속성들.

비디오가 로드가 되었는지, 버퍼링 상태는 어떤지 또한 네트워크 환경이 나빠져 버퍼 상태가 변경되는 등 수많은 속성을 컨트롤해야하는 경우가 있기에 이벤트만 처리하는 notification center보다는 KVO가 더욱 적합하다.

 

 

서버로부터 비디오를 재생하는 예제 코드

import AVFoundation

final class VideoPlayerManager: NSObject {
    static let shared = VideoPlayerManager() // ✅ 싱글톤 인스턴스

    private var player: AVPlayer?
    private var playerItem: AVPlayerItem? {
        willSet {
            removeObservers() // 기존 옵저버 제거
        }
        didSet {
            addObservers() // 새로운 옵저버 추가
        }
    }

    private override init() {
        super.init()
    }

    // 🎥 비디오 로드 및 준비
    func loadVideo(with url: String) {
        guard let videoURL = URL(string: url) else {
            print("❌ 잘못된 비디오 URL")
            return
        }

        stop() // ✅ 기존 플레이어 정리

        let newItem = AVPlayerItem(url: videoURL)
        player = AVPlayer(playerItem: newItem)
        playerItem = newItem // ✅ 옵저버 자동 적용됨
    }

    // ▶️ 재생
    func play() {
        player?.play()
    }

    // ⏸ 일시정지
    func pause() {
        player?.pause()
    }

    // 🛑 정지 및 정리
    func stop() {
        player?.pause()
        player = nil
        playerItem = nil // ✅ 옵저버 자동 해제
    }

    // 🔄 KVO 옵저버 추가
    private func addObservers() {
        guard let playerItem = playerItem else { return }

        playerItem.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.status), options: [.new], context: nil)
        playerItem.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.loadedTimeRanges), options: [.new], context: nil)
        playerItem.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.isPlaybackBufferEmpty), options: [.new], context: nil)
    }

    // 🗑️ KVO 옵저버 제거
    private func removeObservers() {
        guard let playerItem = playerItem else { return }

        playerItem.removeObserver(self, forKeyPath: #keyPath(AVPlayerItem.status))
        playerItem.removeObserver(self, forKeyPath: #keyPath(AVPlayerItem.loadedTimeRanges))
        playerItem.removeObserver(self, forKeyPath: #keyPath(AVPlayerItem.isPlaybackBufferEmpty))
    }

    // 🎯 KVO 변화 감지
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
        guard let playerItem = object as? AVPlayerItem else { return }

        switch keyPath {
        case #keyPath(AVPlayerItem.status):
            handleStatusChange(playerItem)
        case #keyPath(AVPlayerItem.loadedTimeRanges):
            handleLoadedTimeRanges(playerItem)
        case #keyPath(AVPlayerItem.isPlaybackBufferEmpty):
            handleBufferStatus(playerItem)
        default:
            break
        }
    }

    // ✅ AVPlayerItem.status 변경 감지
    private func handleStatusChange(_ playerItem: AVPlayerItem) {
        switch playerItem.status {
        case .readyToPlay:
            print("✅ 비디오 재생 준비 완료")
            play() // 자동 재생
        case .failed:
            print("❌ 비디오 로드 실패: \(playerItem.error?.localizedDescription ?? "알 수 없는 오류")")
        case .unknown:
            print("⚠️ 비디오 상태 알 수 없음")
        @unknown default:
            print("⚠️ 알 수 없는 비디오 상태")
        }
    }

    // ✅ AVPlayerItem.loadedTimeRanges 변경 감지
    private func handleLoadedTimeRanges(_ playerItem: AVPlayerItem) {
        let loadedTimeRanges = playerItem.loadedTimeRanges
        print("📶 로드된 시간 범위: \(loadedTimeRanges)")
    }

    // ✅ AVPlayerItem.isPlaybackBufferEmpty 변경 감지
    private func handleBufferStatus(_ playerItem: AVPlayerItem) {
        if playerItem.isPlaybackBufferEmpty {
            print("⚠️ 버퍼가 비어 있음 (로딩 중)")
        }
    }

    deinit {
        stop()
    }
}