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()
}
}
'iOS' 카테고리의 다른 글
[iOS] AVPlayer + NotificationCenter (ep 02) + Delegate패턴 도입은 적합한가? (0) | 2025.01.17 |
---|---|
[iOS] AVPlayer + NotificationCenter (ep 01) (0) | 2025.01.17 |
[iOS] TabLayout (0) | 2023.08.10 |
[iOS] info.plist없이 권한 추가 설정하는법. (0) | 2022.12.14 |