Swift AVAudioPlayer's playing status always be true? - ios

Here's my code:
#objc func playSmusic1() {
guard let url = Bundle.main.url(forResource: "Snote6", withExtension: "wav") else { return }
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.wav.rawValue)
guard let player = player else { return }
while (true) {
player.play()
player.enableRate = true;
player.rate = playrate
if !player.isPlaying {
break
}
}
} catch let error {
print(error.localizedDescription)
}
}
I found player.isPlaying properties always be true, so sometimes a tone will be play for 2 or 3 times. How to fix this bug? Thanks a lot!

First: Don't use while(true) for checking because it blocks the main thread!
You should add KVO observer to check this property asynchronously e.g.:
class YourController : UIViewController {
var player: AVAudioPlayer?
//...
deinit {
player?.removeObserver(self, forKeyPath: #keyPath(AVAudioPlayer.isPlaying))
}
func play() {
//...
player?.addObserver(self,
forKeyPath: #keyPath(AVAudioPlayer.isPlaying),
options: .new,
context: nil)
player?.play()
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == #keyPath(AVAudioPlayer.isPlaying) {
if let isPlaying = player?.isPlaying {
print(isPlaying)
}
}
else {
self.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
}

Related

I want to add activity indicator to my avplayer when its buffering

private func startVideo() {
if let url = URL(string: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/VolkswagenGTIReview.mp4") {
player = AVPlayer(url: url)
let playerViewController = AVPlayerViewController()
playerViewController.player = player
playerViewController.view.frame = avPlayerView.bounds
addChild(playerViewController)
avPlayerView.addSubview(playerViewController.view)
playerViewController.didMove(toParent: self)
player?.play()
}
}
need to add a activity loader whenever the video is buffering
You can get this working using the following code. Observing the KeyPath on the Player can make you achieve this.
var activityIndicator: UIActivityIndicatorView!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if player != nil {
player.play()
player.addObserver(self, forKeyPath: "timeControlStatus", options: [.old, .new], context: nil)
}
}
override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "timeControlStatus", let change = change, let newValue = change[NSKeyValueChangeKey.newKey] as? Int, let oldValue = change[NSKeyValueChangeKey.oldKey] as? Int {
if #available(iOS 10.0, *) {
let oldStatus = AVPlayer.TimeControlStatus(rawValue: oldValue)
let newStatus = AVPlayer.TimeControlStatus(rawValue: newValue)
if newStatus != oldStatus {
DispatchQueue.main.async {[weak self] in
if newStatus == .playing || newStatus == .paused {
self!.activityIndicator.stopAnimating()
} else {
self!.activityIndicator.startAnimating()
}
}
}
} else {
// Fallback on earlier versions
self.activityIndicator.stopAnimating()
}
}
}

Observing AVPlayer: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled

Hi I'm trying to observe an AVPlayerItem and an AVPlayer in my UIViewController.
But I got some trouble doing it. I've googled a lot and found lot's of examples but none of these worked for me.
Here is my ViewController:
class VideoAudioViewController: UIViewController {
var audioPlayer: AVPlayer!
var playerItem: AVPlayerItem!
private var audio: Audio
private let ITEM_STATUS_PATH = #keyPath(AVPlayerItem.status)
private let PLAYER_STATUS_PATH = #keyPath(AVPlayer.status)
private let PLAYER_RATE_PATH = #keyPath(AVPlayer.rate)
init(for audio: Audio) {
self.audio = audio
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
// Play audio
playerItem = AVPlayerItem(url: audio.url)
playerItem.addObserver(self, forKeyPath: ITEM_STATUS_PATH, options: [.old, .new], context: nil)
audioPlayer = AVPlayer(playerItem: playerItem)
audioPlayer!.play()
// audioPlayer!.
var audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(AVAudioSessionCategoryPlayback)
} catch {
print("error")
}
NotificationCenter.default.addObserver(
self, selector: #selector(handleInterruption), name: .AVAudioSessionInterruption, object: audioSession)
audioPlayer.addObserver(self, forKeyPath: PLAYER_STATUS_PATH, options: [.old, .new], context: nil)
audioPlayer.addObserver(self, forKeyPath: PLAYER_RATE_PATH, options: [.old, .new], context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?,
change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "status" {
switch (audioPlayer.status) {
case AVPlayerStatus.readyToPlay:
print("player status ready to play")
audioPlayer.play()
break
case AVPlayerStatus.failed:
print("player status failed")
break
default:
break
}
return
} else if keyPath == "rate" {
if audioPlayer.rate != 0 {
playPauseButton.setImage(#imageLiteral(resourceName: "ic_pause_circle_outline_white"), for: .normal)
} else {
playPauseButton.setImage(#imageLiteral(resourceName: "ic_play_circle_outline_white"), for: .normal)
}
} else if keyPath == "status" {
switch (playerItem.status) {
case AVPlayerItemStatus.readyToPlay:
print("player item ready to play")
break
case AVPlayerItemStatus.failed:
print("player item failed")
break
default:
break
}
return
}
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
// MARK: - AVAudioPlayer delegate
func handleInterruption(_ notification: Notification) {
guard let info = notification.userInfo,
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSessionInterruptionType(rawValue: typeValue) else {
return
}
if type == .began {
playPauseButton.setImage(#imageLiteral(resourceName: "ic_play_circle_outline_white"), for: .normal)
}
else if type == .ended {
// guard let optionsValue =
// userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else {
// return
// }
// let options = AVAudioSessionInterruptionOptions(rawValue: optionsValue)
// if options.contains(.shouldResume) {
// Interruption Ended - playback should resume
audioPlayer?.play()
// }
}
}
}
My problem is this:
Terminati
ng app due to uncaught exception 'NSInternalInconsistencyException', reason: '<TheSimpleClub_Nachhilfe.VideoAudioViewController: 0x7fa6ee2b0c10>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
Key path: rate
Observed object: <AVPlayer: 0x60000021c8c0>
Change: {
kind = 1;
new = 1;
old = 1;
}
Context: 0
x0'
The error occurrs in this line:
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
What can I do to resolve this?

How to set AVQuePlayer to the begining after it finishes playing all in Que

I have an AVQuePlayer that is downloading videos a user posts to firebase and then playing them in order via an AVQuePlayer. Everything works fine until the video que plays all vidoes, in which case, if you try and replay the que, a SIGABRT 1 error occurs. I believe this is due to the fact that it is looking for videos to play but none are present. I have already tried to reset the playerItem array after the que finishes but it has come with no luck. Here is what I have so far.
Loads data from Firebase:
func loadStory() {
DataService.ds.REF_POSTS.observe(.value, with: { (snapshot) in
self.posts = []
if let snapshot = snapshot.children.allObjects as? [DataSnapshot] {
for snap in snapshot {
if let dict = snap.value as? Dictionary<String, Any> {
let key = snap.key
let post = HotPosts(postID: key, postData: dict)
self.posts.append(post)
let media = post.mediaURL
let ref = Storage.storage().reference(forURL: media)
ref.downloadURL(completion: { (url, error) in
if error != nil {
print(error!)
} else {
self.videos.append(url!)
}
})
}
}
}
})
}
Downloads the urls:
func playStory() {
for vid in videos{
asset = AVAsset(url: vid)
let assetKeys = ["playable", "hasProtectedContent"]
let item = AVPlayerItem(asset: asset, automaticallyLoadedAssetKeys: assetKeys)
item.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.status), options: [.old, .new], context: &myContext)
self.playerItem.append(item)
}
player = AVQueuePlayer(items: playerItem)
print(playerItem.count)
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = self.view.bounds
self.view.layer.addSublayer(playerLayer)
}
Handles buffering and plays video when ready:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
print("playing1")
guard context == &myContext else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}
print("playing2")
if keyPath == #keyPath(AVPlayerItem.status) {
let status: AVPlayerItemStatus
print("playing3")
if let statusNumber = change?[.newKey] as? NSNumber {
status = AVPlayerItemStatus(rawValue: statusNumber.intValue)!
} else {
status = .unknown
}
switch status {
case .readyToPlay:
print("We ARE IN THE CLEAR BOIIIIII")
player.play()
break
case .failed:
print("ITem Fialed")
break
case .unknown:
print("Could Not play the video")
break
}
}
}
Try subscribing to this notification and see if it works correctly for your situation:
NotificationCenter.default.addObserver(self, selector: #selector(endOfAudio), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
I also found this in a piece of Apple code, which illustrates the use of some KVO on avQueuePlayer ( https://developer.apple.com/library/content/samplecode/avloopplayer/Listings/Projects_VideoLooper_VideoLooper_QueuePlayerLooper_swift.html )
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this sample’s licensing information
Abstract:
An object that uses AVQueuePlayer to loop a video.
*/
// MARK: Convenience
private func startObserving() {
guard let player = player, !isObserving else { return }
player.addObserver(self, forKeyPath: ObserverContexts.playerStatusKey, options: .new, context: &ObserverContexts.playerStatus)
player.addObserver(self, forKeyPath: ObserverContexts.currentItemKey, options: .old, context: &ObserverContexts.currentItem)
player.addObserver(self, forKeyPath: ObserverContexts.currentItemStatusKey, options: .new, context: &ObserverContexts.currentItemStatus)
isObserving = true
}
private func stopObserving() {
guard let player = player, isObserving else { return }
player.removeObserver(self, forKeyPath: ObserverContexts.playerStatusKey, context: &ObserverContexts.playerStatus)
player.removeObserver(self, forKeyPath: ObserverContexts.currentItemKey, context: &ObserverContexts.currentItem)
player.removeObserver(self, forKeyPath: ObserverContexts.currentItemStatusKey, context: &ObserverContexts.currentItemStatus)
isObserving = false
}
// MARK: KVO
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &ObserverContexts.playerStatus {
guard let newPlayerStatus = change?[.newKey] as? AVPlayerStatus else { return }
if newPlayerStatus == AVPlayerStatus.failed {
print("End looping since player has failed with error: \(player?.error)")
stop()
}
}
else if context == &ObserverContexts.currentItem {
guard let player = player else { return }
if player.items().isEmpty {
print("Play queue emptied out due to bad player item. End looping")
stop()
}
else {
// If `loopCount` has been set, check if looping needs to stop.
if numberOfTimesToPlay > 0 {
numberOfTimesPlayed = numberOfTimesPlayed + 1
if numberOfTimesPlayed >= numberOfTimesToPlay {
print("Looped \(numberOfTimesToPlay) times. Stopping.");
stop()
}
}
/*
Append the previous current item to the player's queue. An initial
change from a nil currentItem yields NSNull here. Check to make
sure the class is AVPlayerItem before appending it to the end
of the queue.
*/
if let itemRemoved = change?[.oldKey] as? AVPlayerItem {
itemRemoved.seek(to: kCMTimeZero)
stopObserving()
player.insert(itemRemoved, after: nil)
startObserving()
}
}
}
else if context == &ObserverContexts.currentItemStatus {
guard let newPlayerItemStatus = change?[.newKey] as? AVPlayerItemStatus else { return }
if newPlayerItemStatus == .failed {
print("End looping since player item has failed with error: \(player?.currentItem?.error)")
stop()
}
}
else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
}
The problem with AVQueuePlayer is that it removes the AVPlayerItems that have finished playing. An option might be to create a copy of your AVQueuePlayer when your QueuePlayer is completely done playing, then you can just play your copy and make a new copy to play again once that one also finishes.
You could also maintain all your AVPlayerItems in an array and load up a new AVQueuePlayer whenever it's needed.
There is a project on github that might also be helpful, which extends AVQueuePlayer and maintains previous playerItems that have been played on your Queue Player. It can be found through: https://github.com/dgiovann/AVQueuePlayerPrevious

How to detect AVPlayer actually started to play in swift

Hello I have set my UISliderminimum value to 0.00. Then I set it's max value in this way.
self.viewPlayer.layer.addSublayer(playerLayer)
let duration : CMTime = avPlayer.avPlayer.currentItem!.asset.duration
let seconds : Float64 = CMTimeGetSeconds(duration)
sliderBar.maximumValue=Float(seconds)
sliderBar!.isContinuous = false
sliderBar!.tintColor = UIColor.green
But I am getting this exception
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Attempting to set a slider's minimumValue (0.000000) to be larger than the maximumValue (nan)'
enter code here
I know after prepareForPlay() to actual playing it takes some time to really play the video. So how can I detect when the player really started to play the video?
Please help me.
Thanks
Since iOS 10 you can observe timeControlStatus property of AVPlayer. It can be .playing.
Check the code:
private func setupAVPlayer() {
avPlayer.addObserver(self, forKeyPath: "status", options: [.old, .new], context: nil)
if #available(iOS 10.0, *) {
avPlayer.addObserver(self, forKeyPath: "timeControlStatus", options: [.old, .new], context: nil)
} else {
avPlayer.addObserver(self, forKeyPath: "rate", options: [.old, .new], context: nil)
}
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if object as AnyObject? === avPlayer {
if keyPath == "status" {
if avPlayer.status == .readyToPlay {
avPlayer.play()
}
} else if keyPath == "timeControlStatus" {
if #available(iOS 10.0, *) {
if avPlayer.timeControlStatus == .playing {
videoCell?.muteButton.isHidden = false
} else {
videoCell?.muteButton.isHidden = true
}
}
} else if keyPath == "rate" {
if avPlayer.rate > 0 {
videoCell?.muteButton.isHidden = false
} else {
videoCell?.muteButton.isHidden = true
}
}
}
}
Here is what I did to actually know when video started (not when it's only ready to start).
Swift 4
player = AVPlayer(url: URL(fileURLWithPath: path))
player.addObserver(self, forKeyPath: "rate", options: NSKeyValueObservingOptions.new, context: nil)
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "rate" {
if player.rate > 0 {
print("video started")
}
}
}
You can add an observer on the object of your AVPlayer like this
player.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.new, context: nil)
and you can check the status change with your AVPlayer like this
func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutableRawPointer) {
if keyPath == "status" {
print(player.status)
}
}
From Apple's docs:
You can observe an AVPlayerLayer object’s readyForDisplay property to be notified when the layer has user-visible content. In particular, you might insert the player layer into the layer tree only when there is something for the user to look at and then perform a transition from
You could create a Combine publisher and observe the rate property:
let isPlayingPublisher: AnyPublisher<Bool, Never> = NotificationCenter.default
.publisher(for: AVPlayer.rateDidChangeNotification, object: player)
.compactMap({ $0.object as? AVPlayer })
.map(\.rate)
.map({ $0 != 0 })
.eraseToAnyPublisher()
In SwiftUI you could then receive the value:
.onReceive(isPlayingPublisher) { isPlaying in
}
Declare AVPlayer Global
var streamPlayer = AVPlayer()
func playStreamAudio( for audioUrl:String)
{
guard streamPlayer.timeControlStatus != .playing else {
streamPlayer.pause()
return
}
let url = audioUrl //"http://192.168.71.11:7891/rec.wav"
let playerItem = AVPlayerItem(url: URL(string:url)!)
streamPlayer = AVPlayer(playerItem:playerItem)
streamPlayer.rate = 1.0;
streamPlayer.volume = 1.0
streamPlayer.play()
}
SUPER EASY SOLUTION SWIFT 4-5:
Just check the timeControlStatus!: (also works with PlayerQueue)
if avPlayer?.timeControlStatus.rawValue == 2 {
//video is playing (playing)
} else if avPlayer?.timeControlStatus.rawValue == 0 {
//video is not playing (paused)
}
the raw value will give you its current state :)
Another, simpler, approach is something like:
if videoPlayer.rate != 0 && videoPlayer.error == nil {
print("video player is playing.................")
} else {
print("video player is NOT playing.")
}
Where videoPlayer is of type AVPlayer, obviously.

Detect volume button press

Volume button notification function is not being called.
Code:
func listenVolumeButton(){
// Option #1
NSNotificationCenter.defaultCenter().addObserver(self, selector: "volumeChanged:", name: "AVSystemController_SystemVolumeDidChangeNotification", object: nil)
// Option #2
var audioSession = AVAudioSession()
audioSession.setActive(true, error: nil)
audioSession.addObserver(self, forKeyPath: "volumeChanged", options: NSKeyValueObservingOptions.New, context: nil)
}
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
if keyPath == "volumeChanged"{
print("got in here")
}
}
func volumeChanged(notification: NSNotification){
print("got in here")
}
listenVolumeButton() is being called in viewWillAppear
The code is not getting to the print statement "got in here", in either case.
I am trying two different ways to do it, neither way is working.
I have followed this: Detect iPhone Volume Button Up Press?
Using the second method, the value of the key path should be "outputVolume". That is the property we are observing.
So change the code to,
var outputVolumeObserve: NSKeyValueObservation?
let audioSession = AVAudioSession.sharedInstance()
func listenVolumeButton() {
do {
try audioSession.setActive(true)
} catch {}
outputVolumeObserve = audioSession.observe(\.outputVolume) { (audioSession, changes) in
/// TODOs
}
}
The code above won't work in Swift 3, in that case, try this:
func listenVolumeButton() {
do {
try audioSession.setActive(true)
} catch {
print("some error")
}
audioSession.addObserver(self, forKeyPath: "outputVolume", options: NSKeyValueObservingOptions.new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "outputVolume" {
print("got in here")
}
}
With this code you can listen whenever the user taps the volume hardware button.
class VolumeListener {
static let kVolumeKey = "volume"
static let shared = VolumeListener()
private let kAudioVolumeChangeReasonNotificationParameter = "AVSystemController_AudioVolumeChangeReasonNotificationParameter"
private let kAudioVolumeNotificationParameter = "AVSystemController_AudioVolumeNotificationParameter"
private let kExplicitVolumeChange = "ExplicitVolumeChange"
private let kSystemVolumeDidChangeNotificationName = NSNotification.Name(rawValue: "AVSystemController_SystemVolumeDidChangeNotification")
private var hasSetup = false
func start() {
guard !self.hasSetup else {
return
}
self.setup()
self.hasSetup = true
}
private func setup() {
guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else {
return
}
let volumeView = MPVolumeView(frame: CGRect.zero)
volumeView.clipsToBounds = true
rootViewController.view.addSubview(volumeView)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.volumeChanged),
name: kSystemVolumeDidChangeNotificationName,
object: nil
)
volumeView.removeFromSuperview()
}
#objc func volumeChanged(_ notification: NSNotification) {
guard let userInfo = notification.userInfo,
let volume = userInfo[kAudioVolumeNotificationParameter] as? Float,
let changeReason = userInfo[kAudioVolumeChangeReasonNotificationParameter] as? String,
changeReason == kExplicitVolumeChange
else {
return
}
NotificationCenter.default.post(name: "volumeListenerUserDidInteractWithVolume", object: nil,
userInfo: [VolumeListener.kVolumeKey: volume])
}
}
And to listen you just need to add the observer:
NotificationCenter.default.addObserver(self, selector: #selector(self.userInteractedWithVolume),
name: "volumeListenerUserDidInteractWithVolume", object: nil)
You can access the volume value by checking the userInfo:
#objc private func userInteractedWithVolume(_ notification: Notification) {
guard let volume = notification.userInfo?[VolumeListener.kVolumeKey] as? Float else {
return
}
print("volume: \(volume)")
}
import AVFoundation
import MediaPlayer
override func viewDidLoad() {
super.viewDidLoad()
let volumeView = MPVolumeView(frame: CGRect.zero)
for subview in volumeView.subviews {
if let button = subview as? UIButton {
button.setImage(nil, for: .normal)
button.isEnabled = false
button.sizeToFit()
}
}
UIApplication.shared.windows.first?.addSubview(volumeView)
UIApplication.shared.windows.first?.sendSubview(toBack: volumeView)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
AVAudioSession.sharedInstance().addObserver(self, forKeyPath: "outputVolume", options: NSKeyValueObservingOptions.new, context: nil)
do { try AVAudioSession.sharedInstance().setActive(true) }
catch { debugPrint("\(error)") }
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
AVAudioSession.sharedInstance().removeObserver(self, forKeyPath: "outputVolume")
do { try AVAudioSession.sharedInstance().setActive(false) }
catch { debugPrint("\(error)") }
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let key = keyPath else { return }
switch key {
case "outputVolume":
guard let dict = change, let temp = dict[NSKeyValueChangeKey.newKey] as? Float, temp != 0.5 else { return }
let systemSlider = MPVolumeView().subviews.first { (aView) -> Bool in
return NSStringFromClass(aView.classForCoder) == "MPVolumeSlider" ? true : false
} as? UISlider
systemSlider?.setValue(0.5, animated: false)
guard systemSlider != nil else { return }
debugPrint("Either volume button tapped.")
default:
break
}
}
When observing a new value, I set the system volume back to 0.5. This will probably anger users using music simultaneously, therefore I do not recommend my own answer in production.
If interested here is a RxSwift version.
func volumeRx() -> Observable<Void> {
Observable<Void>.create {
subscriber in
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setActive(true)
} catch let e {
subscriber.onError(e)
}
let outputVolumeObserve = audioSession.observe(\.outputVolume) {
(audioSession, changes) in
subscriber.onNext(Void())
}
return Disposables.create {
outputVolumeObserve.invalidate()
}
}
}
Usage
volumeRx()
.subscribe(onNext: {
print("Volume changed")
}).disposed(by: disposeBag)

Resources