Music_Player3/MusicPlayer/MP/Common/Tool(工具封装)/MP_PlayerManager.swift
2024-05-31 21:50:46 +08:00

651 lines
25 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 FreeStreamer
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!{
didSet{
if loadPlayer != nil {
//load
NotificationCenter.notificationKey.post(notificationName: .pup_bottom_show)
}else {
//load
NotificationCenter.notificationKey.post(notificationName: .player_delete_list)
playState = .Null
}
}
}
//
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 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)
//
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)
//
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)
}
///
/// - 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
}
//
let newItem = loadPlayer.currentVideo.resourcePlayerItem
//playerItem
player.replaceCurrentItem(with: newItem)
if center == nil {
setCommandCenter()
}
//
startTimer()
//PlayerItem
if loadPlayer.currentVideo?.isKVO == false {
//
newItem?.addObserver(self, forKeyPath: "status", options: [.old,.new], context: nil)
//
newItem?.addObserver(self, forKeyPath: "loadedTimeRanges", options: [.old,.new], context: nil)
//
newItem?.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: [.old,.new], context: nil)
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
}
///
@objc private func netWorkReachableAction(_ sender:Notification) {
//
if loadPlayer?.currentVideo != nil {
//
let currentTime = loadPlayer.currentVideo!.resourcePlayerItem.currentTime()
//
player.seek(to: currentTime)
player.play()
playState = .Playing
}
}
//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 ?? "") 已经准备好播放")
// if playState != .Playing {
// player.play()
// playState = .Playing
// //
// suspendTimer()
// 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")
// 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 {
print("当前音乐-\(loadPlayer.currentVideo?.title ?? "") 未做好准备播放,失败原因是\(loadPlayer.currentVideo?.resourcePlayerItem.error?.localizedDescription ?? "")")
//
play()
}
case "loadedTimeRanges"://
cacheLoadTimes()
case "playbackLikelyToKeepUp"://
if let playbackLikelyToKeepUp = change?[.newKey] as? Bool, playbackLikelyToKeepUp == true {
if playState != .Playing {
//
player.play()
playState = .Playing
//
suspendTimer()
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")
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://
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://
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 ?? "", isRandom: true)
}else {
//,ID
let song = loadPlayer.randomVideos[nextIndex]
loadPlayer.improveData(song.videoId ?? "", isRandom: true)
}
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
//
player.pause()
//
if let video = sender.object as? MPPositive_SongViewModel {
if video.isKVO == true {
//KVO
video.resourcePlayerItem.removeObserver(self, forKeyPath: "status")
video.resourcePlayerItem.removeObserver(self, forKeyPath: "loadedTimeRanges")
video.resourcePlayerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp")
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 {
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 {
pause()
return .success
}else {
return .noActionableNowPlayingItem
}
})
//
center!.previousTrackCommand.addTarget { [weak self] (event) in
guard let self = self else { return .noActionableNowPlayingItem}
if loadPlayer.currentVideo != nil {
previousEvent()
return .success
}else {
return .noActionableNowPlayingItem
}
}
//
center!.nextTrackCommand.addTarget { [weak self] (event) in
guard let self = self else { return .noActionableNowPlayingItem}
if loadPlayer.currentVideo != nil {
nextEvent()
return .success
}else {
return .noActionableNowPlayingItem
}
}
}
//
func updateNowPlayingInfo() {
//info
var info = [String:Any]()
//
info[MPMediaItemPropertyTitle] = loadPlayer.currentVideo?.title ?? ""
//
info[MPMediaItemPropertyArtist] = loadPlayer.currentVideo?.song?.shortBylineText ?? ""
//
info[MPMediaItemPropertyAlbumTitle] = loadPlayer.currentVideo?.song?.longBylineText
//
info[MPMediaItemPropertyPlaybackDuration] = getMusicDuration()
let reviewURL = URL(string: loadPlayer.currentVideo?.song?.reviewUrls?.last ?? "")!
KingfisherManager.shared.retrieveImage(with: reviewURL) { result in
switch result {
case .success(let imageResult):
let image = imageResult.image
info[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { size in
return image
})
// MPNowPlayingInfoCenter线
DispatchQueue.main.async {
//
MPNowPlayingInfoCenter.default().nowPlayingInfo = info
}
case .failure(_):
info[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: placeholderImage.size, requestHandler: { size in
return placeholderImage
})
// MPNowPlayingInfoCenter线
DispatchQueue.main.async {
//
MPNowPlayingInfoCenter.default().nowPlayingInfo = info
}
}
}
}
}