Music_Player3/relax.offline.mp3.music/MP/Common/Tool(工具封装)/MP_PlayerManager.swift

816 lines
34 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// MP_PlayerManager.swift
// MusicPlayer
//
// Created by Mr.Zhou on 2024/5/10.
//
import UIKit
import AVFoundation
import MediaPlayer
import AVKit
import Kingfisher
///
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
}
///
enum MP_TimerStateType:Int {
///
case Resume = 0
///
case Suspend = 1
}
///
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
///
typealias MP_PlayCacheValueAction = (_ currentValue:TimeInterval, _ duration:TimeInterval) -> Void
///
class MP_PlayerManager:NSObject{
///
static let shared = MP_PlayerManager()
///
private var player:AVPlayer = AVPlayer()
//
private var center:MPRemoteCommandCenter?
///
private var timer:DispatchSourceTimer?
///
private var times:TimeInterval = 0
///
private var queue:DispatchQueue?
///load
var loadPlayer:MPPositive_PlayerLoadViewModel!{
willSet{
DispatchQueue.main.async {
[weak self] in
guard let self = self else {return}
guard loadPlayer != nil else {
if newValue != nil {
//load
NotificationCenter.notificationKey.post(notificationName: .pup_bottom_show)
}
return
}
if newValue != nil {
//load
NotificationCenter.notificationKey.post(notificationName: .pup_bottom_show)
}else {
//load
NotificationCenter.notificationKey.post(notificationName: .player_delete_list)
playState = .Null
player.pause()
center?.playCommand.removeTarget(self)
center?.pauseCommand.removeTarget(self)
center?.nextTrackCommand.removeTarget(self)
center?.previousTrackCommand.removeTarget(self)
center?.changePlaybackPositionCommand.removeTarget(self)
center = nil
do {
try AVAudioSession.sharedInstance().setActive(false)
} catch {
print("Error deactivating audio session: \(error)")
}
}
}
}
}
//
private var currentInfo:[String:Any]?
//
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 timerType:MP_TimerStateType = .Suspend
///
private var startActionBlock:MP_PlayTimerStartAction!
///
var runActionBlock:MP_PlayTimerRunAction!
///
var cacheValueBlock:MP_PlayCacheValueAction!
///
private var statusObservation:NSKeyValueObservation?
///
private var loadedTimeRangesObservation:NSKeyValueObservation?
///
private var playbackLikelyToKeepUpObservation:NSKeyValueObservation?
///
private var errorObservation:NSKeyValueObservation?
private override init() {
super.init()
//
timer?.cancel()
queue = DispatchQueue(label: "com.playerTimer.timer",attributes: .concurrent)
timer = DispatchSource.makeTimerSource(queue: queue)
//0.1,0.01
timer?.schedule(deadline: .now(), repeating: .milliseconds(100), leeway: .milliseconds(10))
//
timer?.setEventHandler(handler: { [weak self] in
//0.1
self?.times += 0.1
})
//
NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying(_ :)), name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)
//
NotificationCenter.notificationKey.add(observer: self, selector: #selector(userSwitchCurrentVideoAction(_ :)), notificationName: .positive_player_reload)
//
NotificationCenter.notificationKey.add(observer: self, selector: #selector(netWorkReachableAction(_ :)), notificationName: .net_switch_reachable)
//
// NotificationCenter.default.addObserver(self, selector: #selector(handleAudioSessionRouteChangeAction(_ :)), name: AVAudioSession.routeChangeNotification, object: nil)
//
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 }
cacheLoadTimes()
//
let currentDuration = CMTimeGetSeconds(time)
//
// updateProgress(currentDuration)
updateNowPlayingInfo()
//
let maxDuration = getMusicDuration()
if maxDuration.isNaN == false {
//
if currentDuration <= maxDuration {
//
if runActionBlock != nil {
runActionBlock!(currentDuration, maxDuration)
}
}
}
})
}
deinit {
NotificationCenter.default.removeObserver(self)
center?.playCommand.removeTarget(self)
center?.pauseCommand.removeTarget(self)
center?.nextTrackCommand.removeTarget(self)
center?.previousTrackCommand.removeTarget(self)
timer?.cancel()
timer = nil
}
///
/// - 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)
if center == nil {
setCommandCenter()
}
//
startTimer()
//PlayerItem
if loadPlayer.currentVideo?.isKVO == false {
//
statusObservation?.invalidate()
statusObservation = loadPlayer?.currentVideo?.resourcePlayerItem?.observe(\.status, options: [.old,.new], changeHandler: { [weak self] item, change in
guard let self = self else {return}
if item.status == .readyToPlay {
//
if playState != .Playing {
//statuVlaueplayerItem
print("当前音乐-\(loadPlayer?.currentVideo?.title ?? "") 已经准备好播放")
}
}else {
print("当前音乐-\(loadPlayer.currentVideo?.title ?? "") 未做好准备播放,失败原因是\(loadPlayer.currentVideo?.resourcePlayerItem.error?.localizedDescription ?? "")")
if loadPlayer?.currentVideo?.isKVO == true {
suspendTimer()
MP_AnalyticsManager.shared.player_b_failure_errorAction(loadPlayer?.currentVideo?.song.videoId ?? "", videoname: loadPlayer?.currentVideo?.title ?? "", artistname: loadPlayer?.currentVideo?.song.shortBylineText ?? "", error: loadPlayer.currentVideo?.resourcePlayerItem.error?.localizedDescription ?? "")
loadPlayer?.currentVideo?.isKVO = false
//
loadPlayer.remakeImproveData {
[weak self] in
self?.play()
}
}
}
})
//
loadedTimeRangesObservation?.invalidate()
loadedTimeRangesObservation = loadPlayer?.currentVideo?.resourcePlayerItem?.observe(\.loadedTimeRanges, options: [.old,.new], changeHandler: { [weak self] item, change in
guard let self = self else {return}
cacheLoadTimes()
})
//
playbackLikelyToKeepUpObservation?.invalidate()
playbackLikelyToKeepUpObservation = loadPlayer?.currentVideo?.resourcePlayerItem?.observe(\.isPlaybackLikelyToKeepUp, options: [.old,.new], changeHandler: { [weak self] item, change in
guard let self = self else {return}
if let playbackLikelyToKeepUp = change.newValue, playbackLikelyToKeepUp == true {
if playState != .Playing && playState != .Pause {
//
player.play()
playState = .Playing
//
suspendTimer()
MP_AnalyticsManager.shared.player_b_success_actionAction(loadPlayer?.currentVideo?.song.videoId ?? "", videoname: loadPlayer?.currentVideo?.title ?? "", artistname: loadPlayer?.currentVideo?.song.shortBylineText ?? "")
//
if startActionBlock != nil {
startActionBlock!()
}
}
}else {
//
player.pause()
playState = .Null
}
})
//
errorObservation?.invalidate()
errorObservation = loadPlayer?.currentVideo?.resourcePlayerItem?.observe(\.error, options: [.old,.new], changeHandler: { [weak self] item, change in
guard let self = self else {return}
if let error = change.newValue, let nsError = error {
print("当前音乐-\(loadPlayer?.currentVideo?.title ?? "") 未做好准备播放,失败原因是\(nsError.localizedDescription)")
}
})
loadPlayer.currentVideo.isKVO = true
//0
player.seek(to: .zero)
updateNowPlayingInfo()
}
}
///
func startTimer() {
guard timerType == .Suspend else {
return
}
times = 0
//
timer?.resume()
timerType = .Resume
}
///
func suspendTimer() {
guard timerType == .Resume else {
return
}
//
timer?.suspend()
timerType = .Suspend
guard times != 0 else {
return
}
let times = Int(self.times)
let msTimes = times*1000
MP_AnalyticsManager.shared.player_b_delay_actionAction(loadPlayer?.currentVideo?.song.videoId ?? "", videoname: loadPlayer?.currentVideo?.title ?? "", artistname: loadPlayer?.currentVideo?.song.shortBylineText ?? "", delay: "\(msTimes)ms")
}
///
@objc private func netWorkReachableAction(_ sender:Notification) {
//
if loadPlayer?.currentVideo != nil {
//
let currentTime = loadPlayer.currentVideo!.resourcePlayerItem.currentTime()
//
player.seek(to: currentTime)
player.play()
playState = .Playing
}
}
///
// @objc private func handleAudioSessionRouteChangeAction(_ notification: Notification) {
// guard let info = notification.userInfo, let reasonValue = info[AVAudioSessionRouteChangeReasonKey] as? UInt, let reason = AVAudioSession.RouteChangeReason(rawValue:reasonValue) else {
// return
// }
// switch reason {
// case .newDeviceAvailable://
// let session = AVAudioSession.sharedInstance()
// for output in session.currentRoute.outputs where output.portType == .headphones {
// print("")
// }
// //
// case .oldDeviceUnavailable://
// if let previousRoute = info[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
// for output in previousRoute.outputs where output.portType == .headphones {
// print("")
// }
// }
// //
// pause()
// default:
// break
// }
// }
//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 {
// //
// if playState != .Playing {
// //statuVlaueplayerItem
// print("-\(loadPlayer?.currentVideo?.title ?? "") ")
// }
// }else {
// print("-\(loadPlayer.currentVideo?.title ?? "") \(loadPlayer.currentVideo?.resourcePlayerItem.error?.localizedDescription ?? "")")
// if loadPlayer?.currentVideo?.isKVO == true {
// suspendTimer()
// MP_AnalyticsManager.shared.player_b_failure_errorAction(loadPlayer?.currentVideo?.song.videoId ?? "", videoname: loadPlayer?.currentVideo?.title ?? "", artistname: loadPlayer?.currentVideo?.song.shortBylineText ?? "", error: loadPlayer.currentVideo?.resourcePlayerItem.error?.localizedDescription ?? "")
// //KVO
// loadPlayer?.currentVideo?.resourcePlayerItem.removeObserver(self, forKeyPath: "status")
// loadPlayer?.currentVideo?.resourcePlayerItem.removeObserver(self, forKeyPath: "loadedTimeRanges")
// loadPlayer?.currentVideo?.resourcePlayerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp")
// loadPlayer?.currentVideo?.resourcePlayerItem.removeObserver(self, forKeyPath: "error")
// loadPlayer?.currentVideo?.isKVO = false
// //
// loadPlayer.remakeImproveData {
// [weak self] in
// self?.play()
// }
// }
// }
// case "loadedTimeRanges"://
// cacheLoadTimes()
// case "playbackLikelyToKeepUp"://
// if let playbackLikelyToKeepUp = change?[.newKey] as? Bool, playbackLikelyToKeepUp == true {
// if playState != .Playing && playState != .Pause {
// //
// player.play()
// playState = .Playing
// //
// suspendTimer()
// MP_AnalyticsManager.shared.player_b_success_actionAction(loadPlayer?.currentVideo?.song.videoId ?? "", videoname: loadPlayer?.currentVideo?.title ?? "", artistname: loadPlayer?.currentVideo?.song.shortBylineText ?? "")
// //
// if startActionBlock != nil {
// startActionBlock!()
// }
// }
// }else {
// //
// player.pause()
// playState = .Null
// }
// default:
// break
// }
// }
//
private func cacheLoadTimes() {
//Item
if let timeRanges = loadPlayer?.currentVideo?.resourcePlayerItem?.loadedTimeRanges.map({$0.timeRangeValue}), let first = timeRanges.first {
//
let startSeconds = first.start.seconds
//
let durationSeconds = first.duration.seconds
//
let bufferedSeconds = startSeconds + durationSeconds
//
let maxDuration = getMusicDuration()
//
if cacheValueBlock != nil {
cacheValueBlock!(bufferedSeconds, maxDuration)
}
}
}
//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:
//
player.seek(to: CMTime.zero)
player.play()
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://
//
guard (loadPlayer?.randomVideos?.count ?? 0) != 1 else {
player.seek(to: .zero)
playState = .Playing
player.play()
return
}
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 ?? "", isRandom: true)
}else {
//
let song = loadPlayer.randomVideos[nextIndex]
loadPlayer.improveData(song.videoId ?? "", isRandom: true)
}
default://
guard (loadPlayer?.songVideos?.count ?? 0) != 1 else {
player.seek(to: .zero)
playState = .Playing
player.play()
return
}
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:
guard (loadPlayer?.randomVideos?.count ?? 0) != 1 else {
player.seek(to: .zero)
playState = .Playing
player.play()
return
}
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 ?? "", isRandom: true)
}else {
//,ID
let song = loadPlayer.randomVideos[nextIndex]
loadPlayer.improveData(song.videoId ?? "", isRandom: true)
}
default:
guard (loadPlayer?.songVideos?.count ?? 0) != 1 else {
player.seek(to: .zero)
playState = .Playing
player.play()
return
}
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
//
player.pause()
//
if let video = sender.object as? MPPositive_SongViewModel {
if video.isKVO == true {
//KVO
statusObservation?.invalidate()
playbackLikelyToKeepUpObservation?.invalidate()
errorObservation?.invalidate()
loadedTimeRangesObservation?.invalidate()
video.isKVO = false
}
}
if cacheValueBlock != nil {
cacheValueBlock!(0, 1)
}
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!()
}
}
//MARK: -
private func setCommandCenter() {
//
center = MPRemoteCommandCenter.shared()
//
//
center!.playCommand.addTarget(handler: { [weak self] (event) in
guard let self = self else { return .noActionableNowPlayingItem}
if loadPlayer.currentVideo != nil && playState == .Pause {
resume()
return .success
}else {
return .noActionableNowPlayingItem
}
})
//
center!.pauseCommand.addTarget(handler: { [weak self] (event) in
guard let self = self else { return .noActionableNowPlayingItem}
if loadPlayer.currentVideo != nil && playState == .Playing {
pause()
return .success
}else {
return .noActionableNowPlayingItem
}
})
//
center!.previousTrackCommand.addTarget { [weak self] (event) in
guard let self = self else { return .noActionableNowPlayingItem}
if loadPlayer.currentVideo != nil && MP_NetWorkManager.shared.netWorkStatu == .reachable {
previousEvent()
return .success
}else {
return .noActionableNowPlayingItem
}
}
//
center!.nextTrackCommand.addTarget { [weak self] (event) in
guard let self = self else { return .noActionableNowPlayingItem}
if loadPlayer.currentVideo != nil && MP_NetWorkManager.shared.netWorkStatu == .reachable {
nextEvent()
return .success
}else {
return .noActionableNowPlayingItem
}
}
//
center?.changePlaybackPositionCommand.addTarget(handler: { [weak self] event in
guard let self = self else { return .noActionableNowPlayingItem}
guard MP_NetWorkManager.shared.netWorkStatu == .reachable else {
return .noActionableNowPlayingItem
}
guard let positionEvent = event as? MPChangePlaybackPositionCommandEvent else {
return .commandFailed
}
if loadPlayer.currentVideo != nil {
self.player.seek(to: CMTime(seconds: positionEvent.positionTime, preferredTimescale: 1))
return .success
}else {
return .noActionableNowPlayingItem
}
})
}
//
func updateNowPlayingInfo() {
guard loadPlayer?.currentVideo != nil else {return}
//info
currentInfo = [:]
//
currentInfo![MPMediaItemPropertyTitle] = loadPlayer?.currentVideo?.title ?? ""
//
currentInfo![MPMediaItemPropertyArtist] = loadPlayer?.currentVideo?.song?.shortBylineText ?? ""
//
currentInfo![MPMediaItemPropertyAlbumTitle] = loadPlayer?.currentVideo?.song?.longBylineText
//
currentInfo![MPNowPlayingInfoPropertyElapsedPlaybackTime] = CMTimeGetSeconds(player.currentItem?.currentTime() ?? .zero)
//
currentInfo![MPMediaItemPropertyPlaybackDuration] = CMTimeGetSeconds(player.currentItem?.duration ?? .zero)
currentInfo![MPNowPlayingInfoPropertyPlaybackRate] = 1.0
let reviewURL = URL(string: loadPlayer.currentVideo?.song?.reviewUrls?.last ?? "")!
KingfisherManager.shared.retrieveImage(with: reviewURL) { [weak self]result in
switch result {
case .success(let imageResult):
let image = imageResult.image
self?.currentInfo?[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { size in
return image
})
// MPNowPlayingInfoCenter线
DispatchQueue.main.async {
//
MPNowPlayingInfoCenter.default().nowPlayingInfo = self?.currentInfo
}
case .failure(_):
self?.currentInfo?[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: placeholderImage.size, requestHandler: { size in
return placeholderImage
})
// MPNowPlayingInfoCenter线
DispatchQueue.main.async {
//
MPNowPlayingInfoCenter.default().nowPlayingInfo = self?.currentInfo
}
}
}
}
}