// // MP_PlayerManager.swift // MusicPlayer // // Created by Mr.Zhou on 2024/5/10. // import UIKit import AVFoundation import MediaPlayer import AVKit ///播放器播放状态 enum MP_PlayerStateType:Int { ///未启动 case Null = 0 ///运行中 case Playing = 1 ///暂停中 case Pause = 2 } ///播放器播放方式 enum MP_PlayerPlayType:Int { ///列表顺序播放 case normal = 0 ///列表随机播放 case random = 1 ///单曲播放(当前音乐无限循环) case single = 2 } ///播放器启动时执行事件(播放的音乐) typealias MP_PlayTimerStartAction = () -> Void ///播放器运行时执行事件(当前时间值,最大时间值) typealias MP_PlayTimerRunAction = (_ currentTime:TimeInterval, _ duration:TimeInterval) -> Void ///播放器结束时执行事件 typealias MP_PlayTimerEndAction = () -> Void ///播放器暂停时执行事件 typealias MP_PlayTimerPauseAction = () -> Void ///播放器继续时执行事件 typealias MP_PlayTimerResumeAction = () -> Void ///播放器终止时执行事件 typealias MP_PlayTimerStopAction = () -> Void ///播放器调整进度时执行事件 typealias MP_PlayTimerEditEndAction = () -> Void ///播放器 class MP_PlayerManager:NSObject{ ///控制器单例 static let shared = MP_PlayerManager() ///播放器 private var player:AVPlayer = AVPlayer() ///load模块 var loadPlayer:MPPositive_PlayerLoadViewModel!{ didSet{ if loadPlayer != nil { //当load模块接受到新值的时候,发出通知,提醒底部模块状态切换 NotificationCenter.notificationKey.post(notificationName: .pup_bottom_show) }else { //用户清空了load模块,隐藏播放器 NotificationCenter.notificationKey.post(notificationName: .player_delete_list) } } } //当前播放器状态 private var playState:MP_PlayerStateType = .Null{ didSet{ //当播放器状态发生变化时,对播放器按钮状态进行切换 NotificationCenter.notificationKey.post(notificationName: .switch_player_status, object: playState) } } ///获取播放器播放状态 func getPlayState() -> MP_PlayerStateType { return playState } ///当前播放器播放方法 private var playType:MP_PlayerPlayType = .normal{ didSet{ //当播放器播放方式变化后,发出通知 NotificationCenter.notificationKey.post(notificationName: .player_type_switch) } } ///获取播放器播放方法 func getPlayType() -> MP_PlayerPlayType { return playType } /// 设置播放器播放方式 /// - Parameter type: 新的类型 func setPlayType(_ type:MP_PlayerPlayType) { playType = type if playType == .random { } } ///播放器启动时执行事件记录 private var startActionBlock:MP_PlayTimerStartAction! ///播放器运行时执行事件记录 var runActionBlock:MP_PlayTimerRunAction! private override init() { super.init() // 添加观察者,监听播放结束事件 NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying(_ :)), name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem) NotificationCenter.notificationKey.add(observer: self, selector: #selector(userSwitchCurrentVideoAction(_ :)), notificationName: .positive_player_reload) } deinit { NotificationCenter.default.removeObserver(self) } /// 开始播放音乐 /// - Parameters: /// - startAction: 开始播放时需要执行的事件 /// - runAction: 播放途中需要执行的事件 /// - endAction: 结束播放时需要执行的事件 func play(startAction:MP_PlayTimerStartAction? = nil) { guard loadPlayer != nil, loadPlayer.currentVideo != nil else { //当两项数据皆为空时,播放器无法播放 print("Player No Data") return } //检索播放器状态 switch playState { case .Null://未启动 break case .Playing://启动中 player.pause() case .Pause://暂停中 break } //记录事件 if startAction != nil { startActionBlock = startAction } //覆盖播放器原有的playerItem player.replaceCurrentItem(with: loadPlayer.currentVideo.resourcePlayerItem) //将进度回归为0 player.seek(to: .zero) //设置一个秒为刻度的时间值 let interval:CMTime = .init(seconds: 1, preferredTimescale: .init(1)) //为播放器添加运行时主线程每秒触发事件 player.addPeriodicTimeObserver(forInterval: interval, queue: .main, using: { [weak self] (time) in guard let self = self else { return } //转化为当前播放进度秒值 let currentDuration = CMTimeGetSeconds(time) //获取当前播放音乐资源的最大时间值 let maxDuration = getMusicDuration() if maxDuration.isNaN == false { //判断当值进度是否超越最大时间值 if currentDuration <= maxDuration { //没有,执行运行时时间 if runActionBlock != nil { runActionBlock!(currentDuration, maxDuration) } } } }) //判断当前Video是否完成预加载 if loadPlayer.currentVideo.isPreloading == true { //已经完成了预加载 print("开始播放音乐-\(loadPlayer.currentVideo.title ?? "")") player.play() playState = .Playing //执行开始播放闭包 if startActionBlock != nil { startActionBlock!() } }else { //未完成预加载,通过KVO来准确控制播放 //为这个currentVideo的resourcePlayerItem创建KVO,分别监听这个item的status,playbackLikelyToKeepUp loadPlayer.currentVideo.resourcePlayerItem.addObserver(self, forKeyPath: "status", options: .new, context: nil) loadPlayer.currentVideo.resourcePlayerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil) } //启动除了当前播放Video以外的Item的预加载内容 // for item in loadPlayer.listViewVideos where item.song.videoId != loadPlayer.currentVideo.song.videoId { // item.preloadPlayerItem() // } } //实现KVO监听 override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { guard let keyPath = keyPath else { return } //根据keyPath检索 switch keyPath { case "status"://playerItem状态 if let statuValue = change?[.newKey] as? Int, statuValue == AVPlayerItem.Status.readyToPlay.rawValue { //当statuVlaue值等于playerItem准备播放的值,说明已经准备好播放 print("当前音乐-\(loadPlayer.currentVideo.title ?? "") 已经准备好播放") }else { print("当前音乐-\(loadPlayer.currentVideo.title ?? "") 未做好准备播放") //当不能播放时,调整内容,再次播放 } case "playbackLikelyToKeepUp"://是否存在足够的数据开始播放 if let playbackLikelyToKeepUp = change?[.newKey] as? Bool, playbackLikelyToKeepUp == true { //播放器已经加载足够的数据,能够支撑播放 print("当前音乐-\(loadPlayer.currentVideo.title ?? "") 有足够的缓存来播放") //判断当前播放器是否在播放当前音乐中 if playState != .Playing { //还未播放当前音乐,启动播放 print("开始播放音乐-\(loadPlayer.currentVideo.title ?? "")") player.play() playState = .Playing //执行开始播放闭包 if startActionBlock != nil { startActionBlock!() } }else { //播放器已经在播放了,不需要操作 } } default: break } } //MARK: - 获取当前音乐总长度 ///获取音乐资源总时长 private func getMusicDuration() -> TimeInterval { return CMTimeGetSeconds(player.currentItem?.duration ?? .zero) } //MARK: - 音乐播放结束 //当前音乐播放结束时 @objc private func playerDidFinishPlaying(_ sender:Notification) { //检索播放器对象 guard playState == .Playing else { return } switch playType { case .single: playState = .Null //重播 player.seek(to: CMTime.zero) default: //当前音乐播放器正在播放中,下一首 nextEvent() } } //MARK: - 暂停播放 ///内部暂停播放 private func pause() { //检索播放状态,是否进行中 guard playState == .Playing else { //未处于播放中 print("Player is not in playing") return } //暂停播放器 player.pause() //切换播放器状态 playState = .Pause } /// 暂停播放 /// - Parameter pauseAction: 暂停时要执行的事件 func pause(_ pauseAction:MP_PlayTimerPauseAction? = nil) { //检索播放状态,是否进行中 guard playState == .Playing else { //未处于播放中 print("Player is not in playing") return } if pauseAction != nil { pauseAction!() } //暂停播放器 player.pause() //切换播放器状态 playState = .Pause } //MARK: - 继续播放 /// 继续播放 /// - Parameter resumeAction: 继续时要执行的事件 func resume(_ resumeAction:MP_PlayTimerResumeAction? = nil) { //检索播放状态,是否暂停中 guard playState == .Pause else { //未处于暂停中 print("Player is not paused") return } if resumeAction != nil { resumeAction!() } //继续播放器 player.play() //切换播放器状态 playState = .Playing } ///内部继续播放 private func resume() { //检索播放状态,是否暂停中 guard playState == .Pause else { //未处于暂停中 print("Player is not paused") return } //继续播放器 player.play() //切换播放器状态 playState = .Playing } //MARK: - 停止播放 //停止播放 func stop() { //检索播放状态,是否已启动 guard playState != .Null else { //未启动 print("Player is not started") return } player.pause() playState = .Null } //MARK: - 切歌(上一首/下一首) ///上一首歌事件 func previousEvent() { //将播放器状态调整未播放 playState = .Null var nextIndex:Int = 0 //判断当前音乐播放方式 switch playType { case .random://随机,播放随机列表内容 for (index, item) in loadPlayer.randomVideos.enumerated() { if item.videoId == loadPlayer.currentVideo.song.videoId { //找到播放音乐的索引 nextIndex = index - 1 } } //假如next为负数,则直接播放列表最后一首 if nextIndex < 0 { //播放列表最后一首 let last = loadPlayer.randomVideos.last loadPlayer.improveData(last?.videoId ?? "") }else { //查询列表对应单曲 let song = loadPlayer.randomVideos[nextIndex] loadPlayer.improveData(song.videoId ?? "") } default://常规播放或者单曲播放 for (index, item) in loadPlayer.songVideos.enumerated() { if item.videoId == loadPlayer.currentVideo.song.videoId { //找到播放音乐的索引 nextIndex = index - 1 } } //假如next为负数,则直接播放列表最后一首 if nextIndex < 0 { //播放列表最后一首 let last = loadPlayer.songVideos.last loadPlayer.improveData(last?.videoId ?? "") }else { //查询列表对应单曲 let song = loadPlayer.songVideos[nextIndex] loadPlayer.improveData(song.videoId ?? "") } } } ///下一首歌事件 func nextEvent() { //将播放器状态调整未播放 playState = .Null var nextIndex:Int = 0 switch playType { case .random: for (index, item) in loadPlayer.randomVideos.enumerated() { if item.videoId == loadPlayer.currentVideo.song.videoId { //找到播放音乐的索引 nextIndex = index + 1 } } //超出播放列表数 if nextIndex > (loadPlayer.randomVideos.count-1) { //播放列表第一首 let first = loadPlayer.randomVideos.first loadPlayer.improveData(first?.videoId ?? "") }else { //存在下一首,获取下一首ID,并播放 let song = loadPlayer.randomVideos[nextIndex] loadPlayer.improveData(song.videoId ?? "") } default: for (index, item) in loadPlayer.songVideos.enumerated() { if item.videoId == loadPlayer.currentVideo.song.videoId { //找到播放音乐的索引 nextIndex = index + 1 } } //超出播放列表数 if nextIndex > (loadPlayer.songVideos.count-1) { //播放列表第一首 let first = loadPlayer.songVideos.first loadPlayer.improveData(first?.videoId ?? "") }else { //存在下一首,获取下一首ID,并播放 let song = loadPlayer.songVideos[nextIndex] loadPlayer.improveData(song.videoId ?? "") } } } ///监听到用户切换当前音乐 @objc private func userSwitchCurrentVideoAction(_ sender:Notification) { //将播放器状态调整未播放 playState = .Null //优先获取传递的值 if let video = sender.object as? MPPositive_SongViewModel { video.resourcePlayerItem.removeObserver(self, forKeyPath: "status") video.resourcePlayerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp") } if loadPlayer.currentVideo != nil { //开始播放 play(startAction: startActionBlock) } } ///播放器进度调整状态 func setEditPorgressStatu() { guard playState != .Null else { return } //播放器进入暂停状态 pause() } /// 调整播放器进度值,必须和 setEditPorgressStatu()搭配使用 /// - Parameters: /// - progress: 要调整进度值(保证在0-1范围内,超出该方法不会响应) func setEditProgressEnd(_ progress:Float, endAction:MP_PlayTimerEditEndAction? = nil) { guard playState != .Null else { return } guard progress >= 0, progress <= 1 else { return } //根据当前进度值设置时间节点 let timePoint:Double = Double(progress)*getMusicDuration() //设置对应的时间值 let time:CMTime = .init(seconds: timePoint, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) //调整播放器时间 player.seek(to: time) //恢复播放 resume() if endAction != nil { endAction!() } } }