Here I’ve created a common AudioPlayer for all kind of music lists, But when music is already playing, and play another track then instead of overwrite or stop old track both the instance of player runs simultaneously and played double of music. To overcome this issue , already tried to Stop the player, and also setting nil for AudioPlayer but still facing issue.
class SessionManager {
static let shared = SessionManager()
var isPlayingMusic: Bool = false
var isAdFree: Bool = false
var audioPlayer: AudioPlayer?
var updateShuffleBtnUI: (() -> Void)?
var didShuffleList: Bool = false
var isShuffleOn: Bool = false {
didSet {
updateShuffleBtnUI?()
}
}
func setTheme() {
UIView.transition(with: appDelegate?.window ?? UIWindow(), duration: 0.5, options: [.preferredFramesPerSecond60]) {
appDelegate?.window?.overrideUserInterfaceStyle = self.isDarkTheme ? .dark : .light
}
}
func clear() {
isPlayingMusic = false
audioPlayer?.clearPlayerInstance()
}
}
Note:- Some of methods and objects has been removed from below class, to keep question in format and maintain quality of question.
import Foundation
import AVFoundation
import UIKit
import MediaPlayer
class AudioPlayer: NSObject {
public var audioPlayerConfig: [String: Any] = [
"loop": false,
"volume": 1.0
]
private var playerViewControllerKVOContext = 0
var audioQueueObserver: NSKeyValueObservation?
var audioQueueStatusObserver: NSKeyValueObservation?
var audioQueueBufferEmptyObserver: NSKeyValueObservation?
var audioQueueBufferAlmostThereObserver: NSKeyValueObservation?
var audioQueueBufferFullObserver: NSKeyValueObservation?
var audioQueueStallObserver: NSKeyValueObservation?
var audioQueueWaitingObserver: NSKeyValueObservation?
var assetPoolObserver: NSKeyValueObservation?
var playbackLikelyToKeepUpKeyPathObserver: NSKeyValueObservation?
var playbackBufferEmptyObserver: NSKeyValueObservation?
var playbackBufferFullObserver: NSKeyValueObservation?
/////
var audioItem: AVPlayerItem?
let onError = Signal<(message: String, error: Error)>()
let onCollectionReady = Signal<Bool>()
// Data Transfer Closure
var updatePlayPauseStatus: ((Bool) -> Void)?
var updateSliderAndTime: ((_ listenedTime: String, _ sliderProgress: Float, _ passTimeStr: String?, _ totalDuration: Float?) -> Void)?
var updateRepeatBtnUI: (() -> Void)?
var loadingStatus: ((Bool) -> Void)?
var loadBottomBarViewInTab: ((Bool, _ newItemMetaData: SongModel?) -> Void)?
var firstAssetIsLoaded: (() -> Void)?
var songDetailClosure: ((SongModel?) -> Void)?
var mediaItemChange: ((_ newItemMetaData: SongModel?) -> Void)?
// Media observers
private var mediaTimeObserver: Any?
var streamPosition: TimeInterval?
var streamDuration: TimeInterval?
var playingItemIndex = 0
// Repeat Vars
var isRepeat: Bool = false {
didSet {
updateRepeatBtnUI?()
}
}
var playerLooper: NSObject?
var repeatItem: AVPlayerItem?
var repeatItemIndex: Int?
// Shuffle Vars
var shuffleCompletionClosure: (() -> Void)?
// Media controls in notification bar
var nowPlayingInfo = [String: Any]()
var trackArr: [String] = []
var musicMetaDataList = [SongModel]()
var songObj: SongModel? {
didSet {
setupNowPlayingInfo()
}
}
var viewModel = MusicViewModel()
var playerType: PlayerType = .audio
//////////////////
let assetQueue = DispatchQueue(label: "randomQueue", qos: .utility)
let group = DispatchGroup()
let assetKeysRequiredToPlay = [
"playable"
]
var player = AVPlayer()
var playerQueue: AVQueuePlayer?
var seeking: Bool = false
var AVItemPool: [AVPlayerItem] = [] {
didSet {
print("item was added", self.AVItemPool.count)
if self.AVItemPool.count == trackArr.count {
self.onCollectionReady.fire(true)
}
}
}
var asset: AVURLAsset? {
didSet {
guard let newAsset = asset else { return }
asynchronouslyLoadURLAsset(newAsset, appendDirectly: false)
}
}
var dynamicAsset: AVURLAsset? {
didSet {
guard let newDAsset = dynamicAsset else { return }
asynchronouslyLoadURLAsset(newDAsset, appendDirectly: true)
}
}
public var duration: Double {
guard let currentItem = player.currentItem else { return 0.0 }
return CMTimeGetSeconds(currentItem.duration)
}
var rate: Float {
get {
return player.rate
}
set {
player.rate = newValue
}
}
/*
A formatter for individual date components used to provide an appropriate
value for the `startTimeLabel` and `durationLabel`.
*/
let timeRemainingFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.zeroFormattingBehavior = .pad
formatter.allowedUnits = [.minute, .second]
return formatter
}()
/** Play previous track from queue */
func previousTrack() {
playingItemIndex -= 1
var index = 0
clearRepeat()
if let currentItem = playerQueue?.currentItem {
index = (AVItemPool.firstIndex(of: currentItem) ?? 0)
play(at: (index > 0) ? (index - 1) : index)
} else {
play(at: AVItemPool.count-2)
}
resume()
}
/** Play next track from queue */
func nextTrack() {
var index = 0
playingItemIndex += 1
clearRepeat()
if let currentItem = playerQueue?.currentItem {
index = (AVItemPool.firstIndex(of: currentItem) ?? 0)
play(at: (index < (AVItemPool.count - 1)) ? (index + 1) : index)
} else {
play(at: AVItemPool.count-1)
}
resume()
}
/** Play track from queue at any index */
func play(at index: Int) {
playerQueue?.removeAllItems()
for i in index..<AVItemPool.count {
let obj = AVItemPool[i]
if playerQueue?.canInsert(obj, after: nil) == true {
obj.seek(to: .zero)
playerQueue?.insert(obj, after: nil)
}
}
}
/**
Loads and appends directly a track to the currently playing queue
*/
public func appendQueueItems(track: String) {
self.assetQueue.async {
self.group.wait()
self.group.enter()
let fileURL = NSURL(string: track)
self.dynamicAsset = AVURLAsset(url: fileURL! as URL, options: nil)
}
}
/**
Return if player is currently playing a track
*/
public func isPlaying() -> Bool {
return (self.playerQueue?.rate ?? 0.0) > Float(0.0)
}
/**
Pause playback of audio player
*/
public func pause() {
self.viewModel.event.set(.idle())
self.playerQueue?.pause()
}
/**
Play or Resume playback of current audio player
*/
public func resume() {
self.viewModel.event.set(.idle())
self.playerQueue?.play()
}
/// Integer conversion to Hours/Minutes/Seconds
func secondsToHoursMinutesSeconds(_ seconds: Int) -> (String, String, String) {
var hour = String(seconds / 3600)
var minute = String((seconds % 3600) / 60)
var second = String((seconds % 3600) % 60)
(hour.count == 1) ? hour.insert("0", at: hour.startIndex) : Void()
(minute.count == 1) ? minute.insert("0", at: minute.startIndex) : Void()
(second.count == 1) ? second.insert("0", at: second.startIndex) : Void()
return (hour, minute, second)
}
/// Get Current Playing Item
func getCurrentPlayingItem() -> AVPlayerItem? {
if let item = playerQueue?.currentItem {
return item
}
return nil
}
/// Clear All Repeat Track Related properties clear method
func clearRepeat() {
self.isRepeat = false
self.playerLooper = nil
self.repeatItem = nil
self.repeatItemIndex = nil
}
public func initialize(config: [String: Any]?) {
if config != nil {
self.audioPlayerConfig = config ?? [String: Any]()
loadingStatus?(true)
}
// load assets as PlayerItems
self.group.enter()
var counter = 0
for item in self.trackArr {
print("adding asset: \(item)")
if counter > 0 {
self.assetQueue.async {
self.group.wait()
self.group.enter()
let fileURL = NSURL(string: item)
self.asset = AVURLAsset(url: fileURL! as URL, options: nil)
}
} else {
self.assetQueue.async {
let fileURL = NSURL(string: item)
self.asset = AVURLAsset(url: fileURL! as URL, options: nil)
}
}
counter += 1
}
}
func clearPlayerInstance() {
SessionManager.shared.audioPlayer?.playerQueue?.removeAllItems()
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
SessionManager.shared.audioPlayer = nil
}
override init() {
super.init()
}
deinit {
/// Remove any KVO observer.
self.audioQueueObserver?.invalidate()
self.audioQueueStatusObserver?.invalidate()
self.audioQueueBufferEmptyObserver?.invalidate()
self.audioQueueBufferAlmostThereObserver?.invalidate()
self.audioQueueBufferFullObserver?.invalidate()
var audioQueueStallObserver: NSKeyValueObservation?
var audioQueueWaitingObserver: NSKeyValueObservation?
var mediaTimeObserver: Any?
self.onCollectionReady.cancelAllSubscriptions()
}
// MARK: - Error Handling
func handleErrorWithMessage(_ message: String?, error: Error? = nil) {
NSLog("Error occured with message: \(String(describing: message)), error: \(String(describing: error)).")
}
// MARK: Convenience
func createTimeString(time: Float) -> String {
let components = NSDateComponents()
components.second = Int(max(0.0, time))
return timeRemainingFormatter.string(from: components as DateComponents)!
}
func findIndex(ofPlayerItem valueToFind: AVPlayerItem, in array: [AVPlayerItem]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
}
- Code of navigating for Playing track
func openMusicPlayer(type: PlayerType, songObj: SongModel? = nil, musicUrlList: [SongModel]? = nil) {
let musicPlayer = Router.musicPlayer.instance as? MusicPlayerVC ?? MusicPlayerVC()
musicPlayer.type = type
musicPlayer.songObj = songObj
musicPlayer.musicUrlList = musicUrlList
musicPlayer.hidesBottomBarWhenPushed = true
// In here below i'm getting isPlayingMusic = true, but when i tried to remove old instance Player or try to remove old track from player. Getting "AudioPlayer = nil"
if SessionManager.shared.isPlayingMusic {
SessionManager.shared.audioPlayer?.audioQueueObserver?.invalidate()
SessionManager.shared.audioPlayer?.audioQueueStatusObserver?.invalidate()
SessionManager.shared.audioPlayer?.audioQueueBufferEmptyObserver?.invalidate()
SessionManager.shared.audioPlayer?.audioQueueBufferAlmostThereObserver?.invalidate()
SessionManager.shared.audioPlayer?.audioQueueBufferFullObserver?.invalidate()
var audioQueueStallObserver: NSKeyValueObservation?
var audioQueueWaitingObserver: NSKeyValueObservation?
var mediaTimeObserver: Any?
SessionManager.shared.audioPlayer?.onCollectionReady.cancelAllSubscriptions()
SessionManager.shared.isPlayingMusic = false
SessionManager.shared.audioPlayer?.pause()
SessionManager.shared.audioPlayer?.player.replaceCurrentItem(with: nil)
SessionManager.shared.audioPlayer = nil
}
if type == .audio {
navigationController?.pushViewController(musicPlayer, animated: true)
} else {
showLoginRequiredAlert { [weak self] in
guard let self = self else { return }
self.navigationController?.pushViewController(musicPlayer, animated: true)
}
}
}