AVPlayer + NotificationCenter
● AudioSession 구성과 오디오 로드
private func setupAudioSession() {
do {
try session.setCategory(.playback, mode: .default)
try session.setActive(true)
} catch {
print("AudioManager, setupAudioSession // AVAudioSession 설정 실패: \(error.localizedDescription)")
}
}
func loadAudio(from url: URL, completion: @escaping (Bool) -> Void) {
playerItem = AVPlayerItem(url: url)
player = AVPlayer(playerItem: playerItem)
NotificationCenter.default.addObserver(
self,
selector: #selector(playerItemDidPlayToEnd),
name: .AVPlayerItemDidPlayToEndTime,
object: playerItem
)
}
Why? 그냥 재생만하면 되는거 아닌가? 왜 굳이 AVAudioSession이 필요한거지?
- 앱 간의 오디오 리소스 관리
- -ios는 동시에 여러 앱이 오디오를 사용하려고 할 때, 충돌을 방지하기 위해 앱 간 오디오 우선순위를 관리한다.
- avaudiosession을 설정하면 ios에 내 앱이 오디오를 재생하고 싶다는 의사를 전달하여 오디오를 재생할 리소스를 할당받게된다.
- 오디오 사용 목적을 명시적으로 정의.
- .playback : 오디오를 재생하는 앱 (스트리밍, 음악, 영화)
- .record : 녹음하는 앱 (음성녹음, 화상통화)
- .playAndRecord : 동시에 재생과 녹음을 지원 (통화, 화상 회의)
- .ambient : 백그라운드 음악과 함께 효과음을 재생 (게임)
- .soloAmbient : 효과음을 재생하지만 다른 오디오를 멈춤 (알람)
- . 오디오 장치를 구성
- 스피커, 이어폰, 블루투스 장치등 다양한 출력 자잋를 사용할 수 있음.
- AVAudioSession을 활성화(setActive(true))하면, ios가 적절한 장치를 선택하고 초기화하여 소리를 출력한다.
Why? 근데 이걸 iOS가 자동적으로 컨트롤하지 않고 내가 직접해야하지?
iOS는 앱개발자에게 유연성과 제어권을 주기 위해 이런 설계를 채택함.
각 앱이 오디오를 사용하는 방식이 다 다르기 때문에, 개발자가 적절한 오디오 동작을 직접 정의해야한다.
또한, iOS가 알아서 설정해버리면 개발자가 원하는 동작을 구현할 수 있는 자유가 제한된다.
왜 굳이 NotificationCenter를 쓰는가?
코드를 구현하니 AVAudioSesion, AVPlayer, AVPlayerItem을 제공하는 AVFoundation은 대부분 NotificationCenter를 통해서 구독, 관찰을 하던데 단순히 구독과 관찰인 observing을 구현하려면 ViewModel이나 Combine을 쓰는건 어떨까 싶었는데, 여러 문답을 통해 NotificationCenter가 가장 이상적인 것으로 나타났다.
물론 NotificationCenter를 사용하지 않고도 구현이 가능하다. 단, 아래 2가지 조건에는 NotificationCenter가 유용하다.
- 재생완료 시 추가작업등 ( UI update, 다음 트랙 재생)
- 재생 상태를 실시간으로 모니터링해야하는 경우 (재생 시간 업데이터, 버퍼링 상태 표시)
Why? 왜 Apple은 NotificationCenter를 통해 오디오 재생 이벤트를 탐지하게 했을까?
- AVFoundation 프레임워크는 Objective-C로 개발되었는데, 당시에는 NotificationCenter가 이벤트 전달의 표준방식이자 iOS/macOS 생태계에서 표준화된 이벤트 처리방식이다.
- AVPlayer, UIApplication, URLSession등 다양한 프레임워크에서 이벤트를 노티피케이션으로 알려서, 개발자는 일관된 방식으로 이벤트 처리가 가능하다.
- 이후 Swift로 전환되면서도 기존 프레임워크와의 호환성을 유지하기 위해 NotificationCenter를 계속 사용했다.
- N:M관계 통신의 적합성떄문이다. 오디오 재생 상태는 여러 객체에서 관찰해야 할 수 있다.
- 오디오 재생 완료시 UI 업데이트하는 뷰 컨트롤러라는 객체
- 재생 완료 시 다음 트랙을 재생하는 로직이라는 객체
- 재생 상태를 로깅하는 모니터링 시스템이라는 객체 등등 여러 객체가 동일한 이벤트( 오디오 재생상태)를 구독해야하는 경우, NotificationCenter가 매우 적합하다.
- 오디오 재생상태는 비동기적으로 발새하기 때문에 비동기적으로 이벤트 처리하는 NotificationCenter가 적합하다.
- 오디오 재생이 완료되면 : AVPlayerItemDidPlayToEndTime, 네트워크 상태가 변경되면 AVPlayerItemPlayBackStalled.
- 이벤트가 발생하면 즉시 모든 구독자에게 '비동기'적으로 전달하며 메인 쓰레드의 블로킹을 방지하고 앱의 반응성을 유지한다.
- 간단하고 직관적이다.
- 이벤트 구독? addObserver
- 이벤트 발생? post
- 이벤트 구독 종료? removeObserver
- 기술적 한계
- Delegate 패턴
- 1:1통신에 적합하다.
- 오디오 재생 상태를 여러 객체에서 관찰해야하는경우 (n:m), delegate패턴은 적합하지 않다.
- Clouser
- Swift에서 도입된 기술로 Objective-C코드와 호환성이 떨어짐.
- 클로저를 사용하면 이벤트 처리 로직이 발신자와 강하게 결합될 수 있다.
- Combine
- SwiftUI와 함게 도입된 새로운 기술이라 Objective-C와 호환성이 떨어진다.
- 학습곡선이 높고 모든 프로젝트에서 사용하기에는 적합하지 않다.
- ViewModel
- 단순한 기능을 구현해야하는 경우 오버헤드가 발생할수도 있음.
- NotificationCenter는 손쉽고 빠르게 전역이벤트를 처리하는 반면에 VM은 특정 뷰와 화면 등 추가적인 설계가 필요하다.
- 백그라운드에서 상태를 유지하려면 'AVAudioSession'설정, VM상태 영구저장등 추가적인 처리가 필요한 반면에 NotificationCenter는 백그라운드에서도 유연하게 이벤트를 발행하고 구독할 수 있다.
- Delegate 패턴
Why? NotificationCenter대신 VM을 쓸 수도 있지 않았을까?
써도 된다. 다만, 이전에도 언급했다싶이 효율적인 측면에서 NotificaitonCenter가 압도적으로 좋다. 그 이유를 알아보자
- NotificationCenter에 비해 VM으로 전역이벤트를 처리하려면 특정 뷰와 화면 등 추가적인 설계가 필요하다.
- 백그라운드에서도 상태를 유지하려면 VM상태 영구저장등 추가적인 처리가 필요하다.
코드로 한번 알아보자.
func notificationExample(){
NotificationCenter.default.addObserver(
self,
selector: #selector(handleFunction(_:))
name: nil,
object: nil
)
}
- NotificationCenter.default
- 싱글톤 NotificaiotnCenter의 인스턴스인default를 가져오는 것이다.
- self
- 관찰자(Observer)로, 현재 객체를 지정한다.
- Notification이 발생하면 현재 객체의 메서드가 호출된다.
- #selector(handleFunction(_:)) : 호출할 메서드
- name: 관찰(구독)할 노티피케이션의 이름이다.
- object: 발신자
- nill이면 모든 발신자의 노티피케이션을 받는다는 의미.
deinit{
NOtificationCenter.default.removeObserver(self)
}
노티피케이션 제거는 메모리 해제 시 꼭 해줘야 한다. 특히 deinit에서.
- removeObserver(self)란?
- NotificationcEnter는 내부적으로 관찰자(self)와 해당 관찰자가 구독한 노티피케이션 목록을 관리한다.
- removeObserver(self)를 호출하면, NotificationCenter는 내부 목록에서 self와 관련된 모든 관찰자를 제거한다.
- 이는 등록된 모든 노티피케이션을 한 번에 해지하는 간편한 방법.
- 주의사항
- removeObserver(self)는 현재 객체가 등록한 모든 노티피케이션 관찰자를 해지한다.
특정 관찰자 제거하는 코드
NotificationCenter.default.removeObserver(self, name: NetworkMonitor.statusChanged, object: nil)
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: playerItem)
- 특정 노티피케이션만 해지하려면 removeObserver(_:name:object:)를 사용해야 한다.
- NotificationCenter에 등록한 관찰자는 반드시 해제하지 않으면 메모리 누수가 발생한다.
- deinit에서 removeObserver(self)를 호출하여 안전하게 관찰자를 해지한다.
- NotificationCenter는 스레드 안전하지 않으므로 메인 스레드에서 관찰자를 추가하거나 제거하는 것이 좋다.
'iOS' 카테고리의 다른 글
[iOS] AVPlayer, AVPlayerItem, AVPlayer.currentItem (Video) (0) | 2025.02.11 |
---|---|
[iOS] AVPlayer + NotificationCenter (ep 02) + Delegate패턴 도입은 적합한가? (0) | 2025.01.17 |
[iOS] TabLayout (0) | 2023.08.10 |
[iOS] info.plist없이 권한 추가 설정하는법. (0) | 2022.12.14 |