티스토리 뷰
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 형태로 가공했다.
'iOS' 카테고리의 다른 글
[iOS] FLO 앱 만들기(5) - 자동으로 스크롤 되는 가사 TableView (1) | 2023.10.21 |
---|---|
[iOS] FLO 앱 만들기(4) - Seek 뷰와 기능 만들기 (0) | 2023.10.20 |
[iOS] FLO 앱 만들기(2) - 재사용을 고려한 플레이어 버튼 만들기 (0) | 2023.10.14 |
[iOS] FLO 앱 만들기(1) - AVFoundation 개요 / 앱 구조 설계 (1) | 2023.10.14 |
[iOS] URL과 전화번호를 인식하는 Label View 만들기(TextView, dataDetectorTypes, LineBreakStrategy) (0) | 2023.10.13 |
- Total
- Today
- Yesterday
- watch connectivity
- 토큰
- 애플워치
- Xcode15
- TextField
- DateFormatter
- locale
- avplayer
- 소수점
- KVO
- flo
- 회고
- open-api-generator
- easy cue
- swift날짜
- SwiftUI
- Xcode
- IOS
- demical
- watchOS
- musicplayer
- openapi-generator
- auth
- OAS
- Swift
- 2024년
- keyboardtype
- retry
- AVFoundation
- 애플워치 데이터 전송
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |