티스토리 뷰

FLO MusicPlayer 앱에서 사용하는 AVPlayer는 음악을 재생시키고, 컨트롤를 하는 핵심 요소로 사용된다.

AVPlayer를 어떻게 사용했는지 알아보자

 

먼저, iOS의 프레임워크들에서 자주 사용했던 자연스럽게 delegate를 채택하려고 했지만,

delegate가 AVPlayer에는 없다.

 

대신, KVO(Key-Value-Observing) 방식으로 AVPlayer의 상태값을 받아올 수 있다.

KVO는 존재만 알고 있었던지라, 이번 기회에 공부를 해보았다.

 

KVO 짧은 정리

  • KVO는 NSObject의 기능으로 willSet, didSet 과 유사하다고 볼 수 있음
  • 프로퍼티의 상태 변화에 대해 '외부' 에서 옵저버를 추가할 수 있음 (willSet, didSet은 내부에서 추가 필요)
  • 외부에서 추가할 수 있기 때문에 프레임워크나 라이브러리에 붙여서 사용하기에 유리함
  • Objective-c 의존 (objc 런타임 할당으로 인한 동적 디스패치 로 성능이 비교적 안좋을 것으로 보임)
  • removeObserver를 해야 하나, iOS 11 이상부터는 알아서 remove 해줌
<그냥 내 생각>
- 정적 디스패치를 기반으로 하는 Swift와는 반대로 KVO는 동적 디스패치 기반이고 objc의 런타임 의존임
- Swift가 추구하는 방향과는 맞지 않아 보임
- 그래서 최신의 iOS 프레임워크는 KVO를 대체할 수 있는 다른 방식을 제공하는 것 같음
(delegate, property observer, combine 등)

 

다시 AVPlayer로 돌아와 정리를 하자면

AVPlayer의 상태값을 저장하는 내부의 속성을 바깥에서 KVO 방식으로 관찰할 수 있다.

코드에서는 changeHandler의 클로저 블록을 실행하도록 한다.

 

아래는 MusicPlayer의 코드다.

class MusicPlayer {
    //MARK: - Instance
    static let shared = MusicPlayer()
    private init() {}
    
        //MARK: - Properties
    var player: AVPlayer?
    var playerItem: AVPlayerItem?
    
        /// 플레이어중인 아이템의  현재 초
    let currentSecond = BehaviorRelay<Double>(value: 0)
    /// 플레이 상태(playing, notPlaying)
    var playStatus = BehaviorRelay<PlayStatus>(value: .notPlaying)
    /// 플레이 타임라인의 비율(%)
    let currentPlaybackRatio = BehaviorRelay<Float>(value: 0)
    
        //MARK: - Methods
    func start(musicUrl: String?, prepareHandler: @escaping () -> Void) {
        guard let music = musicUrl, let url = URL(string: music) else { return }

        self.playerItem = AVPlayerItem(url: url)
        self.player = AVPlayer(playerItem: playerItem)

        // 재생 가능한 시점파악, 재생
        self.playerItemStatusObserver = playerItem?.observe(\.status, options: [.new], changeHandler: { [weak self] (playerItem, change) in
            if playerItem.status == .readyToPlay {
                self?.player?.play()
                prepareHandler()
                self?.setupTimeObserver()
            } else if playerItem.status == .failed {
                print("플레이 실패")
                print("Error: \(playerItem.error?.localizedDescription ?? "Unknown error")")
            }
        })
        
        // player의 rate에 대한 옵저버 (playing or notPlaying)
        self.playerRateObserver = player?.observe(\.rate, options: [.old, .new], changeHandler: { [weak self] player, change in
//            print("Player: \(player.rate), old: \(change.oldValue), new: \(change.newValue)")
            if player.rate == 0 {
                // 정지 혹은 일시정지
                self?.playStatus.accept(.notPlaying)
            } else {
                // 정상 재생중
                self?.playStatus.accept(.playing)
            }
        })
    }
    
        func setupTimeObserver() {
        // 현재 재생 시간을 1초 간격으로 업데이트.
        let interval = CMTime(seconds: 1, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
        self.timeObserver = player?.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main) { [weak self] currentTime in
            guard let self = self else { return }
            if self.isSeekProgress == false {
                // 현재 초 구해서 accept
                let currentSecond = CMTimeGetSeconds(currentTime)
                self.currentSecond.accept(currentSecond)
                // 비율 계산
                guard let duration = self.durationTime else { return }
                let rawRatio = currentSecond / CMTimeGetSeconds(duration)
                let value = Float(round(rawRatio * 1000) / 1000)
                self.currentPlaybackRatio.accept(value)
            }
        }
    }
}

1. 플레이 가능 여부

- 외부에서 불리는 start 함수는 url을 파라미터로 받아 AVPlayerItem, AVPlayer를 생성한다.

- AVPlayer의 status 속성에 옵저버를 할당하여서 재생이 가능한 상태(readyToPlay)가 된 경우 플레이시킨다.

 

2. 플레이 상태

- AVPlayer의 rate 속성에 옵저버를 할당하며 float 값을 받는다. 

- rate는 재생 속도이다. 0인 경우 정지된 상태이며, 1.0인 경우 정상 재생되는 상태이다.

(1.0보다 크면 빠르게 재생, 1.0보다 작으면 느리게 재생)

 

3. 플레이 시간

- 현재 플레이 되는 시간을 받는 것은 addPeriodicTimeObserver 라는 메소드가 만들어져 있다 !

- 이곳에서는 현재 재생되는 시간이 입력한 time interval 간격으로 클로저 블록이 호출되며, 현재 재생중인 아이템의 시간이 CMTime 타입으로 들어온다.

 

 

이렇게 얻은 AVPlayer의 상태값 데이터들을 앱에서 사용할 수 있도록 적절한 처리를 거쳐 Observable 형태로 가공했다.

 

소스코드: https://github.com/jisu15-kim/FloMusicPlayer

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함