From 3c204139868bb9405ae79bb107d301defb892296 Mon Sep 17 00:00:00 2001 From: "Mr.zhou" <1422157428@qq.com> Date: Wed, 29 May 2024 17:48:19 +0800 Subject: [PATCH] =?UTF-8?q?=E6=92=AD=E6=94=BE=E5=99=A8=E6=9B=B4=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Tool(工具封装)/MP_PlayerManager.swift | 369 ++++++------------ .../MPPositive_SongViewModel.swift | 78 +++- .../MPPositive_PlayerViewController.swift | 9 +- 3 files changed, 195 insertions(+), 261 deletions(-) diff --git a/MusicPlayer/MP/Common/Tool(工具封装)/MP_PlayerManager.swift b/MusicPlayer/MP/Common/Tool(工具封装)/MP_PlayerManager.swift index 8f3e953..62bc3df 100644 --- a/MusicPlayer/MP/Common/Tool(工具封装)/MP_PlayerManager.swift +++ b/MusicPlayer/MP/Common/Tool(工具封装)/MP_PlayerManager.swift @@ -44,20 +44,13 @@ typealias MP_PlayTimerStopAction = () -> Void ///播放器调整进度时执行事件 typealias MP_PlayTimerEditEndAction = () -> Void ///播放器缓存值执行事件 -typealias MP_PlayCacheValueAction = (Float) -> 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 player:FSAudioStream! -// ///预加载下一首流 - private var next:FSAudioStream! - ///计时器 - private var timer:DispatchSourceTimer! + private var player:AVPlayer = AVPlayer() ///load模块 var loadPlayer:MPPositive_PlayerLoadViewModel!{ didSet{ @@ -76,7 +69,6 @@ class MP_PlayerManager:NSObject{ didSet{ //当播放器状态发生变化时,对播放器按钮状态进行切换 NotificationCenter.notificationKey.post(notificationName: .switch_player_status, object: playState) - } } ///获取播放器播放状态 @@ -104,73 +96,19 @@ class MP_PlayerManager:NSObject{ } ///播放器启动时执行事件记录 private var startActionBlock:MP_PlayTimerStartAction! - ///播放器运行时执行事件记录 var runActionBlock:MP_PlayTimerRunAction! ///播放器缓存值闭包 var cacheValueBlock:MP_PlayCacheValueAction! private override init() { super.init() -// player.automaticallyWaitsToMinimizeStalling = false -//// player.delegate = self -// // 添加观察者,监听播放结束事件 -// NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying(_ :)), name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem) - //监听用户切换了音乐 + // 添加观察者,监听播放结束事件 + 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 queue = DispatchQueue(label: "com.MPPlayerTimer.queue") - //创建倒计时器 - timer = DispatchSource.makeTimerSource(queue: queue) - //设置计时器的起始时间以及触发事件频率为一秒一次 - timer!.schedule(deadline: .now(), repeating: .seconds(1)) - //计时器设置触发事件 - timer.setEventHandler { - [weak self] in - guard let self = self else {return} - timerAction() - } - //计时器启动 - timer.resume() } deinit { NotificationCenter.default.removeObserver(self) - player = nil - timer.cancel() - timer = nil } - ///计时器每秒执行事件 - private func timerAction() { - //判断当前播放器是否存在 - guard let player = player, playState == .Playing else {return} - DispatchQueue.main.async {[weak self] in - guard let self = self, findTurePlayer(player) else {return} - //存在,每秒获取播放进度(当前时间,总时间) - let duration = player.duration.playbackTimeInSeconds - let currentTime = player.currentTimePlayed.playbackTimeInSeconds - //调用进度闭包 - if self.runActionBlock != nil { - self.runActionBlock!(TimeInterval(currentTime), TimeInterval(duration)) - } - //检索媒体是否存入缓存 - if player.cached == true || loadPlayer.currentVideo.isDlownd == true { - if self.cacheValueBlock != nil { - self.cacheValueBlock!(1) - } - }else { - //伪装每秒缓存进度 - let byte = currentTime + 30 - let rate = Float(byte)/duration - if self.cacheValueBlock != nil { - self.cacheValueBlock!(rate) - } - } - } - } - /// 开始播放音乐 /// - Parameters: /// - startAction: 开始播放时需要执行的事件 @@ -182,103 +120,120 @@ class MP_PlayerManager:NSObject{ print("Player No Data") return } - //清除旧流媒体 - stopAndReleaseStream(&player) + //检索播放器状态 + switch playState { + case .Null://未启动 + break + case .Playing://启动中 + player.pause() + case .Pause://暂停中 + break + } //记录事件 if startAction != nil { startActionBlock = startAction } - if next != nil, (next.url == (loadPlayer.currentVideo.resourcePlayerURL! as NSURL)) { - player = next - }else { - //配置当前播放音乐 - player = .init(url: loadPlayer.currentVideo.resourcePlayerURL!) - player.maxRetryCount = 2 - } - //预加载下一首(假如有的话) - let index = loadPlayer.listViewVideos.firstIndex(of: loadPlayer.currentVideo) ?? 0 - if (loadPlayer.listViewVideos.count-1) > index { - stopAndReleaseStream(&next) - //纯在下一首,获取下一位的URL - let nextURL = loadPlayer.listViewVideos[index + 1].resourcePlayerURL - next = preloadNext(nextURL!) - } - //开始播放 - player.play() - //获取播放器状态 - player.onStateChange = { - [weak self] status in - guard let self = self, findTurePlayer(player) else {return} - switch status { - case .fsAudioStreamFailed://加载失败 - print("\(loadPlayer.currentVideo?.title ?? "")加载失败") - playState = .Null - case .fsAudioStreamRetryingFailed://重试都失败了 - print("\(loadPlayer.currentVideo?.title ?? "")重试失败") - print("失败URL:\(String(describing: loadPlayer.currentVideo?.resourcePlayerURL))") - //重新获取资源 - loadPlayer.remakeImproveData { [weak self] in - guard let self = self else {return} - //配置当前播放音乐 - player?.url = loadPlayer.currentVideo.resourcePlayerURL! as NSURL - } - case .fsAudioStreamPlaying://加载成功 - //开始播放/正在播放 - print("\(loadPlayer.currentVideo?.title ?? "")开始播放") - if playState != .Playing { - playState = .Playing - if startAction != nil { - 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) } } - case .fsAudioStreamPlaybackCompleted://播放完成 - playerDidFinishPlaying() - case .fsAudioStreamEndOfFile://加载完成 - print("\(loadPlayer.currentVideo?.title ?? "")加载完毕") - default: - break } - } - + }) + //对当前播放PlayerItem设置监听状态 + //准备状态 + loadPlayer.currentVideo?.resourcePlayerItem?.addObserver(self, forKeyPath: "status", options: [.old,.new], context: nil) + //当前缓冲值 + loadPlayer.currentVideo?.resourcePlayerItem.addObserver(self, forKeyPath: "loadedTimeRanges", options: [.old,.new], context: nil) } - ///对流的检查,判断当前调用流是否是播放流 - private func findTurePlayer(_ stream:FSAudioStream) -> Bool { - guard let currentVideoURL = loadPlayer?.currentVideo?.resourcePlayerURL as? NSURL else { - return false + //实现KVO监听 + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + guard let keyPath = keyPath else { + return } - let streamURL = stream.url - if streamURL == currentVideoURL { - return true - }else { - return false + //根据keyPath检索 + switch keyPath { + case "status"://playerItem状态 + if let statuValue = change?[.newKey] as? Int, statuValue == AVPlayerItem.Status.readyToPlay.rawValue { + //判断当前播放器是否在播放当前音乐中 + if playState != .Playing { + //当statuVlaue值等于playerItem准备播放的值,说明已经准备好播放 + print("当前音乐-\(loadPlayer.currentVideo?.title ?? "") 已经准备好播放") + //还未播放当前音乐,启动播放 + print("开始播放音乐-\(loadPlayer.currentVideo?.title ?? "")") + player.play() + playState = .Playing + //执行开始播放闭包 + if startActionBlock != nil { + startActionBlock!() + } + } + }else { + print("当前音乐-\(loadPlayer.currentVideo?.title ?? "") 未做好准备播放,失败原因是\(loadPlayer.currentVideo?.resourcePlayerItem.error?.localizedDescription ?? "")") + //资源更新,重新配置一下相关内容 + loadPlayer.remakeImproveData { + [weak self] in + guard let self = self else {return} + //重新播放 + play() + } + } + case "loadedTimeRanges"://当前缓冲进度 + //获取当前播放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) + } + } + + case "playbackLikelyToKeepUp"://是否存在足够的数据开始播放 + break + default: + break } } - - ///预加载下一首流 - private func preloadNext(_ url:URL) -> FSAudioStream{ - let stream = FSAudioStream(url: url) - stream?.maxRetryCount = 1 - // 开始预加载数据 - stream!.preload() - print("下一首已经在预加载") - return stream! + //MARK: - 获取当前音乐总长度 + ///获取音乐资源总时长 + private func getMusicDuration() -> TimeInterval { + return CMTimeGetSeconds(player.currentItem?.duration ?? .zero) } - - //MARK: - 音乐播放结束 //当前音乐播放结束时 - @objc private func playerDidFinishPlaying() { + @objc private func playerDidFinishPlaying(_ sender:Notification) { //检索播放器对象 guard playState == .Playing else { return } switch playType { case .single: - var postion = FSStreamPosition() - postion.position = 0 //重播 - player.seek(to: postion) + player.seek(to: CMTime.zero) player.play() default: //当前音乐播放器正在播放中,下一首 @@ -330,8 +285,7 @@ class MP_PlayerManager:NSObject{ resumeAction!() } //继续播放器 -// player.play() - player.pause() + player.play() //切换播放器状态 playState = .Playing } @@ -344,8 +298,7 @@ class MP_PlayerManager:NSObject{ return } //继续播放器 -// player.play() - player.pause() + player.play() //切换播放器状态 playState = .Playing } @@ -359,7 +312,7 @@ class MP_PlayerManager:NSObject{ print("Player is not started") return } - player.stop() + player.pause() playState = .Null } //MARK: - 切歌(上一首/下一首) @@ -372,7 +325,7 @@ class MP_PlayerManager:NSObject{ switch playType { case .random://随机,播放随机列表内容 for (index, item) in loadPlayer.randomVideos.enumerated() { - if item.videoId == loadPlayer.currentVideo?.song.videoId { + if item.videoId == loadPlayer.currentVideo.song.videoId { //找到播放音乐的索引 nextIndex = index - 1 } @@ -381,15 +334,15 @@ class MP_PlayerManager:NSObject{ if nextIndex < 0 { //播放列表最后一首 let last = loadPlayer.randomVideos.last - loadPlayer.improveData(last?.videoId ?? "", isRandom: true) + loadPlayer.improveData(last?.videoId ?? "") }else { //查询列表对应单曲 let song = loadPlayer.randomVideos[nextIndex] - loadPlayer.improveData(song.videoId ?? "", isRandom: true) + loadPlayer.improveData(song.videoId ?? "") } default://常规播放或者单曲播放 for (index, item) in loadPlayer.songVideos.enumerated() { - if item.videoId == loadPlayer.currentVideo?.song.videoId { + if item.videoId == loadPlayer.currentVideo.song.videoId { //找到播放音乐的索引 nextIndex = index - 1 } @@ -414,7 +367,7 @@ class MP_PlayerManager:NSObject{ switch playType { case .random: for (index, item) in loadPlayer.randomVideos.enumerated() { - if item.videoId == loadPlayer.currentVideo?.song.videoId { + if item.videoId == loadPlayer.currentVideo.song.videoId { //找到播放音乐的索引 nextIndex = index + 1 } @@ -423,15 +376,15 @@ class MP_PlayerManager:NSObject{ if nextIndex > (loadPlayer.randomVideos.count-1) { //播放列表第一首 let first = loadPlayer.randomVideos.first - loadPlayer.improveData(first?.videoId ?? "", isRandom: true) + loadPlayer.improveData(first?.videoId ?? "") }else { //存在下一首,获取下一首ID,并播放 let song = loadPlayer.randomVideos[nextIndex] - loadPlayer.improveData(song.videoId ?? "", isRandom: true) + loadPlayer.improveData(song.videoId ?? "") } default: for (index, item) in loadPlayer.songVideos.enumerated() { - if item.videoId == loadPlayer.currentVideo?.song.videoId { + if item.videoId == loadPlayer.currentVideo.song.videoId { //找到播放音乐的索引 nextIndex = index + 1 } @@ -452,21 +405,18 @@ class MP_PlayerManager:NSObject{ @objc private func userSwitchCurrentVideoAction(_ sender:Notification) { //将播放器状态调整未播放 playState = .Null - //清理所有的流 - if player != nil { - //清除所有流 - stopAndReleaseStream(&player) + //暂停播放 + player.pause() + //优先获取传递的值 + if let video = sender.object as? MPPositive_SongViewModel { + //切歌时移除KVO监听 + video.resourcePlayerItem.removeObserver(self, forKeyPath: "status") + video.resourcePlayerItem.removeObserver(self, forKeyPath: "loadedTimeRanges") + // video.resourcePlayerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp") } - if cacheValueBlock != nil { - cacheValueBlock!(0) - } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - [weak self] in - guard let self = self else {return} - if loadPlayer.currentVideo != nil { - //开始播放 - play(startAction: startActionBlock) - } + if loadPlayer.currentVideo != nil { + //开始播放 + play(startAction: startActionBlock) } } @@ -482,85 +432,22 @@ class MP_PlayerManager:NSObject{ /// - Parameters: /// - progress: 要调整进度值(保证在0-1范围内,超出该方法不会响应) func setEditProgressEnd(_ progress:Float, endAction:MP_PlayTimerEditEndAction? = nil) { - guard playState != .Null, let player = player, findTurePlayer(player) else { + guard playState != .Null else { return } guard progress >= 0, progress <= 1 else { return } + //根据当前进度值设置时间节点 + let timePoint:Double = Double(progress)*getMusicDuration() //设置对应的时间值 - var time:FSStreamPosition = .init() - time.position = progress - //获取当前值的大小 - let currentTime = player.currentTimePlayed.playbackTimeInSeconds - if progress != currentTime { - //调整播放器时间 - player.seek(to: time) - //恢复播放 - resume() - if endAction != nil { - endAction!() - } + let time:CMTime = .init(seconds: timePoint, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + //调整播放器时间 + player.seek(to: time) + //恢复播放 + resume() + if endAction != nil { + endAction!() } } - ///清除流的方法 - private func stopAndReleaseStream(_ stream: inout FSAudioStream?) { - stream?.stop() // 停止流 - stream?.onStateChange = nil // 清除所有可能的回调 - stream?.onFailure = nil - stream = nil - } } -////MARK: - 媒体项目协议处理 -//extension MP_PlayerManager: MP_AVPlayerItemDelegate { -// ///当媒体项目初次缓存后 -// func playerItemReadyToPlay(_ playerItem: MP_AVPlayerItem) { -// DispatchQueue.main.async { -// [weak self] in -// guard let self = self else {return} -// if playState != .Playing { -// //还未播放当前音乐,启动播放 -// player.play() -// playState = .Playing -// //执行开始播放闭包 -// if startActionBlock != nil { -// startActionBlock!() -// } -// -// } -// } -// } -// ///当媒体项目收到新缓存后 -// func playerItem(_ playerItem: MP_AVPlayerItem, progress:Float) { -// DispatchQueue.main.async { -// [weak self] in -// guard let self = self else {return} -// //传递缓存值 -// if cacheValueBlock != nil { -// cacheValueBlock!(progress) -// } -// } -// } -// ///当媒体项目完全加载后 -// func playerItem(_ playerItem: MP_AVPlayerItem, didFinishLoadingData data: Data) { -// print("\(loadPlayer.currentVideo.title ?? "") 已经完全缓存完毕") -// DispatchQueue.main.async { -// [weak self] in -// guard let self = self else {return} -// //传递缓存值 -// if cacheValueBlock != nil { -// cacheValueBlock!(1) -// } -// } -// } -// ///当媒体项目加载数据中断(比如断网了),导致停止播放时 -// func playerItemPlaybackStalled(_ playerItem: MP_AVPlayerItem) { -// print("中断了") -// } -// ///当媒体项目加载错误时调用。 -// func playerItem(_ playerItem: MP_AVPlayerItem, loadingError error: any Error) { -// print("\(loadPlayer.currentVideo.title ?? "") 发生错误,---\(error)") -// } -// -// -//} diff --git a/MusicPlayer/MP/MPPositive/Models/ViewModels/ListViewModels/MPPositive_SongViewModel.swift b/MusicPlayer/MP/MPPositive/Models/ViewModels/ListViewModels/MPPositive_SongViewModel.swift index 2d4193f..b9a6559 100644 --- a/MusicPlayer/MP/MPPositive/Models/ViewModels/ListViewModels/MPPositive_SongViewModel.swift +++ b/MusicPlayer/MP/MPPositive/Models/ViewModels/ListViewModels/MPPositive_SongViewModel.swift @@ -8,20 +8,16 @@ import UIKit import AVKit import AVFoundation -import FreeStreamer + class MPPositive_SongViewModel: NSObject { ///排序号 var index:Int! - ///音乐资源路径 -// var resourcePlayerItem:AVPlayerItem! -// var resourcePlayerItem:MP_AVPlayerItem! -// var resourcePlayerItem:CachingPlayerItem! ///播放实例 -// var resourcePlayerItem:FSAudioStream? + var resourcePlayerItem:AVPlayerItem! + ///播放媒体 + var resourcePlayerAsset:AVURLAsset! ///播放路径 - var resourcePlayerURL:URL? - ///资源加载路径 -// var resourceAsset:MP_AVURLAsset! + var resourcePlayerURL:URL! ///封面 var coverUrl:URL? ///标题 @@ -41,10 +37,11 @@ class MPPositive_SongViewModel: NSObject { init(_ song:MPPositive_SongItemModel) { super.init() self.song = song -// resourcePlayerItem = nil configure() } deinit { + resourcePlayerItem = nil + resourcePlayerAsset = nil resourcePlayerURL = nil } //数据配置 @@ -53,14 +50,12 @@ class MPPositive_SongViewModel: NSObject { index = song.index if let first = song.resourceUrls?.first { - //判断是否下载 - if isDlownd == true { - resourcePlayerURL = .init(string:first) - }else { - //没有完成下载,使用网络路径 - resourcePlayerURL = .init(string: first) - } + resourcePlayerURL = .init(string:first) + resourcePlayerAsset = .init(url: resourcePlayerURL) + preloadAsset(resourcePlayerAsset) + resourcePlayerItem = .init(asset: resourcePlayerAsset) } + //封面路径默认取最后一条 if song.reviewUrls?.first != nil { coverUrl = .init(string: song.reviewUrls!.last!) @@ -98,4 +93,53 @@ class MPPositive_SongViewModel: NSObject { //检索是否下载 isDlownd = MPPositive_DownloadItemModel.fetch(.init(format: "videoId == %@", song.videoId)).count != 0 } + //执行预加载 + func preloadAsset(_ asset:AVURLAsset) { + //执行预加载 + if #available(iOS 16, *) { + //ios16以上的情况 + Task{ + do{ + let playable = try await asset.load(.isPlayable) + if playable == true { + print("\(self.title ?? "")预加载成功") + }else { + //检索预加载失败原因 + switch asset.status(of: .isPlayable) { + case .failed(let erro): + print("\(title ?? "")预加载失败,失败原因:\(erro.localizedDescription)") + default: + break + } + } + }catch{ + print("预加载失败:\(error.localizedDescription)") + } + } + }else { + //ios16以下的情况 + let keys = ["playable"] + asset.loadValuesAsynchronously(forKeys: keys) { + [weak self] in + guard let self = self else {return} + for key in keys { + var error: NSError? = nil + let status = asset.statusOfValue(forKey: key, error: &error) + switch status { + case .loaded: + // key成功加载,资源准备就绪 + DispatchQueue.main.async { + print("\(self.title ?? "")预加载成功") + } + case .failed: + print("\(title ?? "")预加载失败,失败原因:\(error?.localizedDescription ?? "")") + case .cancelled: + print("\(title ?? "")预加载被取消了") + default: + break + } + } + } + } + } } diff --git a/MusicPlayer/MP/MPPositive/ViewControllers/Player(播放器)/MPPositive_PlayerViewController.swift b/MusicPlayer/MP/MPPositive/ViewControllers/Player(播放器)/MPPositive_PlayerViewController.swift index f59e693..7c4f837 100644 --- a/MusicPlayer/MP/MPPositive/ViewControllers/Player(播放器)/MPPositive_PlayerViewController.swift +++ b/MusicPlayer/MP/MPPositive/ViewControllers/Player(播放器)/MPPositive_PlayerViewController.swift @@ -133,11 +133,14 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont coverView.sliderView.value = Float(value) } //当缓存变化时 - MP_PlayerManager.shared.cacheValueBlock = { [weak self] progress in + MP_PlayerManager.shared.cacheValueBlock = { [weak self] (value, duration) in guard let self = self else { return } - if progress <= 1 { - coverView.progressView.setProgress(progress, animated: false) + if value < duration { + //进度缓存中 + let float = value/duration + coverView.progressView.setProgress(Float(float), animated: false) }else { + //进度缓存满了 coverView.progressView.setProgress(1, animated: false) } }