B面1.0.6,缓存以及播放流畅度优化

This commit is contained in:
Mr.zhou 2024-06-17 09:35:03 +08:00
parent 61ea3a59ec
commit c54252ca0e
11 changed files with 1116 additions and 890 deletions

View File

@ -33,7 +33,7 @@ class MP_LunchViewController: UIViewController, GADFullScreenContentDelegate {
view.backgroundColor = .init(hex: "#000000") view.backgroundColor = .init(hex: "#000000")
timer = CADisplayLink(target: self, selector: #selector(timerActionClick(_ :))) timer = CADisplayLink(target: self, selector: #selector(timerActionClick(_ :)))
// //
timer.preferredFramesPerSecond = 40 timer.preferredFramesPerSecond = 10
//线 //线
timer.add(to: RunLoop.current, forMode: .common) timer.add(to: RunLoop.current, forMode: .common)
// //
@ -113,7 +113,7 @@ class MP_LunchViewController: UIViewController, GADFullScreenContentDelegate {
@objc fileprivate func timerActionClick(_ link:CADisplayLink) { @objc fileprivate func timerActionClick(_ link:CADisplayLink) {
if maxTimes > currentTimes { if maxTimes > currentTimes {
// //
currentTimes += 0.025 currentTimes += 0.1
let value = (currentTimes/maxTimes).isNaN ? 0:(currentTimes/maxTimes) let value = (currentTimes/maxTimes).isNaN ? 0:(currentTimes/maxTimes)
DispatchQueue.main.async { DispatchQueue.main.async {
[weak self] in [weak self] in

View File

@ -8,87 +8,72 @@
import UIKit import UIKit
class MP_Lunch_ProgressView: UIView { class MP_Lunch_ProgressView: UIView {
struct Constant { //
// private var gradientLayer: CAGradientLayer!
static let lineWidth: CGFloat = 6*width //
// private var progressLayer: CALayer!
static let trackColor:UIColor = .init(hex: "#FFFFFF")
// // (0.0 - 1.0)
static let progressColors:[CGColor] = [ var progress: CGFloat = 0.0 {
UIColor(red: 0.109, green: 0.784, blue: 0.932, alpha: 1).cgColor, didSet {
UIColor(red: 0.412, green: 0.996, blue: 0.451, alpha: 1).cgColor, updateProgressLayer()
UIColor(red: 0.796, green: 0.839, blue: 0.294, alpha: 1).cgColor }
]
} }
//
private var gradientLayer:CAGradientLayer!
//
private var trackLayer:CAShapeLayer!
//线
private var path:UIBezierPath!
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
setUpLayers() setupGradientLayer()
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
super.init(coder: coder) super.init(coder: coder)
setUpLayers() setupGradientLayer()
} }
override func layoutSubviews() { override func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
//
gradientLayer.frame = self.bounds
updateProgressLayer()
} }
override func draw(_ rect: CGRect) {
// private func setupGradientLayer() {
path = UIBezierPath() backgroundColor = .init(hex: "#FFFFFF",alpha: 0.1)
// //
let startPoint = CGPoint(x: bounds.minX, y: bounds.maxY) gradientLayer = CAGradientLayer()
// gradientLayer.colors = [
let endPoint = CGPoint(x: bounds.maxX, y: bounds.maxY) UIColor(red: 0.109, green: 0.784, blue: 0.932, alpha: 1).cgColor,
// UIColor(red: 0.412, green: 0.996, blue: 0.451, alpha: 1).cgColor,
path.move(to: startPoint) UIColor(red: 0.796, green: 0.839, blue: 0.294, alpha: 1).cgColor
//线 ]
path.addLine(to: endPoint) gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
// gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
trackLayer = .init()
//
trackLayer.lineWidth = bounds.height
trackLayer.fillColor = UIColor.clear.cgColor
//
trackLayer.strokeColor = Constant.trackColor.cgColor
//
trackLayer.strokeStart = 0
//
trackLayer.strokeEnd = 0
//
trackLayer.lineCap = .round
trackLayer.path = path.cgPath
layer.addSublayer(trackLayer)
//
gradientLayer = .init()
//
gradientLayer.frame = .init(x: 0, y: -2, width: bounds.size.width, height: bounds.size.height+1)
// gradientLayer.shadowPath = trackLayer.path
//
gradientLayer.colors = Constant.progressColors
//
gradientLayer.startPoint = .init(x: 0, y: 1)
//
gradientLayer.endPoint = .init(x: 1, y: 1)
layer.addSublayer(gradientLayer) layer.addSublayer(gradientLayer)
//
gradientLayer.mask = trackLayer //
progressLayer = CALayer()
progressLayer.backgroundColor = UIColor.white.cgColor //
gradientLayer.mask = progressLayer
} }
//layer
private func setUpLayers() { private func updateProgressLayer() {
backgroundColor = .init(hex: "#FFFFFF", alpha: 0.15) //
let progressWidth = self.bounds.width * progress
progressLayer.frame = CGRect(x: 0, y: 0, width: progressWidth, height: self.bounds.height)
} }
///
/// - Parameter progress: //
func setProgress(_ progress: CGFloat) { func setProgress(_ progress: CGFloat, animated: Bool = false) {
if progress <= 1 { self.progress = min(max(progress, 0.0), 1.0) //
// if animated {
trackLayer.strokeEnd = progress let animation = CABasicAnimation(keyPath: "bounds.size.width")
animation.toValue = self.bounds.width * self.progress
animation.duration = 0.1 //
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
progressLayer.add(animation, forKey: "progressWidth")
} }
} }
} }

View File

@ -97,6 +97,16 @@ extension NotificationCenter{
case positive_nav_pop case positive_nav_pop
///b ///b
case netWork_error_deal case netWork_error_deal
///b
case asset_response
///b
case asset_receiveData
///b
case asset_isCached
///b
case asset_errorCode
///b403
case player_asset_403
} }
} }
} }

View File

@ -6,138 +6,185 @@
// //
import Foundation import Foundation
/// ////
class CachedMedia: NSObject, Codable { class MP_CacheAndArchiverManager {
//
let data: Data
//
let dataBlocks:[MediaDataBlock]
//
let isComplete: Bool
//
let maxCount:Int64
init(data: Data, dataBlocks:[MediaDataBlock], isComplete: Bool, maxCount:Int64) {
self.data = data
self.dataBlocks = dataBlocks
self.isComplete = isComplete
self.maxCount = maxCount
}
}
///
class MP_CacheManager {
// 访 // 访
static let shared = MP_CacheManager() static let shared = MP_CacheAndArchiverManager()
// 1videoID2
private let memoryCache = NSCache<NSString, CachedMedia>()
// //
private let fileManager = FileManager.default let fileManager = FileManager.default
// ///
private let cacheDirectory: URL // private var archiverDic:[String: MP_PlayerRequestModel]!
//线 ///
private var cacheQueue = DispatchQueue(label: "com.MP_CacheManager.cacheQueue") private var isCompleteds:[String:Bool] = [:]
//
private var cacheOperations: [String: DispatchWorkItem] = [:]
//
private let throttleInterval: TimeInterval = 1.0
//
private let expirationInterval: TimeInterval = 86400 // 24
private init() { private init() {
//
let urls = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)
//
cacheDirectory = urls[0].appendingPathComponent("MyAssetCacheManager")
//
if !fileManager.fileExists(atPath: cacheDirectory.path) {
//
try? fileManager.createDirectory(at: cacheDirectory, withIntermediateDirectories: true, attributes: nil)
}
} }
///
/// func createCachePath() -> String? {
/// - Parameters: // CacheMP_PlayerCache
/// - data: let cacheDirectory = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first!
/// - key: 使videoId let path = cacheDirectory+"/"+"MP_PlayerCache"
func cacheData(_ data: Data, dataBlocks:[MediaDataBlock], forKey key: String, isComplete: Bool = false, maxCount:Int64) { //
// cacheQueue 线 guard fileManager.fileExists(atPath: path) == false else {
cacheQueue.async { [weak self] in //
// key return path
self?.cacheOperations[key]?.cancel()
//
let operation = DispatchWorkItem {
self?.performCacheData(data, dataBlocks: dataBlocks, forKey: key, isComplete: isComplete, maxCount: maxCount)
// cacheQueue
self?.cacheQueue.async {
self?.cacheOperations.removeValue(forKey: key)
}
}
// operation
self?.cacheOperations[key] = operation
// operation
self?.cacheQueue.asyncAfter(deadline: .now() + self!.throttleInterval, execute: operation)
} }
} //
//
private func performCacheData(_ data: Data, dataBlocks:[MediaDataBlock], forKey key: String, isComplete: Bool = false, maxCount:Int64) {
let contentToCache = CachedMedia(data: data, dataBlocks: dataBlocks, isComplete: isComplete, maxCount: maxCount)
memoryCache.setObject(contentToCache, forKey: key as NSString)
//
let fileURL = self.cacheDirectory.appendingPathComponent(key)
//
let tempURL = fileURL.appendingPathExtension("tmp")
let encoder = JSONEncoder()
do { do {
let newData = try encoder.encode(contentToCache) //
// try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)
try newData.write(to: tempURL, options: .atomicWrite) return path
//
if self.fileManager.fileExists(atPath: fileURL.path) {
try self.fileManager.removeItem(at: fileURL)
}
//
try self.fileManager.moveItem(at: tempURL, to: fileURL)
//
let expirationDate = Date().addingTimeInterval(self.expirationInterval)
try self.fileManager.setAttributes([.modificationDate: expirationDate], ofItemAtPath: fileURL.path)
} catch { } catch {
// //
try? self.fileManager.removeItem(at: tempURL) print("创建播放器缓存文件夹失败,失败原因:\(error)")
print("无法将密钥数据写入磁盘 \(key) - 错误: \(error)")
}
}
///
/// - Parameter key: 使videoId
/// - Returns:
func data(forKey key: String) -> CachedMedia? {
if let cachedContent = memoryCache.object(forKey: key as NSString){
return cachedContent
}
return cacheQueue.sync {
[weak self] in
guard let self = self else { return nil }
let fileURL = self.cacheDirectory.appendingPathComponent(key)
guard let jsonData = try? Data(contentsOf: fileURL) else { return nil }
// JSON CachedMedia
let decoder = JSONDecoder()
do{
let cachedContent = try decoder.decode(CachedMedia.self, from: jsonData)
//
let attributes = try fileManager.attributesOfItem(atPath: fileURL.path)
guard let modificationDate = attributes[.modificationDate] as? Date else { return nil }
if Date().timeIntervalSince(modificationDate) < expirationInterval{
//
memoryCache.setObject(cachedContent, forKey: key as NSString)
return cachedContent
} else {
//
try fileManager.removeItem(at: fileURL)
}
} catch {
print("读取缓存数据失败 \(key) - 错误: \(error)")
}
return nil return nil
} }
} }
///
func createTempFile(_ videoID:String) -> Bool {
//
let path = tempPath(videoID)
if fileManager.fileExists(atPath: path) {
//
do{
try fileManager.removeItem(atPath: path)
}catch {
print("删除临时文件失败error: \(error)")
}
}
//
return fileManager.createFile(atPath: path, contents: nil, attributes: nil)
}
///
func writeDataToAudioFileTempPathWithData(_ data:Data, videoId:String) {
guard let handle = FileHandle(forWritingAtPath: tempPath(videoId)) else {return}
if #available(iOS 13.4, *) {
do{
try handle.seekToEnd()
}catch {
print("Seek到末尾失败失败原因:\(error)")
}
} else {
handle.seekToEndOfFile()
}
//
if #available(iOS 13.4, *) {
do{
try handle.write(contentsOf: data)
}catch{
print("写入数据失败,失败原因:\(error)")
}
} else {
handle.write(data)
}
do {
if #available(iOS 13.4, *) {
try? handle.close() //
} else {
handle.closeFile()
}
}
}
///
func readTempFileDataWithOffset(_ offset:UInt64, length:Int, videoId:String) -> Data? {
var allhandle:FileHandle!
//
if isCompleteds[videoId] == true {
//
let path = audioCachedPath() ?? ""
let audioName = "/"+videoId+".mp4"
let cachePath = path+audioName
guard let handle = FileHandle(forReadingAtPath: cachePath) else {return nil}
allhandle = handle
}else {
let path = tempPath(videoId)
guard let handle = FileHandle(forReadingAtPath: path) else {return nil}
allhandle = handle
}
if #available(iOS 13.0, *) {
do{
try allhandle.seek(toOffset: offset)
}catch{
print("Seek到指定位置失败失败原因:\(error)")
try? allhandle.close()
}
} else {
allhandle.seek(toFileOffset: offset)
}
if #available(iOS 13.4, *) {
do{
let data = try allhandle.read(upToCount: length)
try allhandle.close()
return data
}catch{
print("读取到指定位置失败,失败原因:\(error)")
return nil
}
} else {
let data = allhandle.readData(ofLength: length)
allhandle.closeFile()
return data
}
}
///
/// - Parameter videoId: 使videoId
func moveAudioFileFromTempPathToCachePath(_ videoId: String) -> Bool {
guard let path = audioCachedPath() else {return false }
// 1.
if !(fileManager.fileExists(atPath: path)) {
//
do{
try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true)
}catch {
print("创建缓存夹失败,失败原因:\(error)")
return false
}
}
let audioName = "/"+videoId+".mp4"
let cachePath = path+audioName
//
if fileManager.fileExists(atPath: cachePath) {
// do {
// print("")
// try fileManager.removeItem(atPath: cachePath)
// } catch {
// print(": \(error)")
// return false
// }
return true
}
// 2.
let tempFilePath = tempPath(videoId)
guard fileManager.fileExists(atPath: tempFilePath) else {
// print(": \(tempFilePath)")
return false
}
// 3.
do {
try fileManager.moveItem(atPath: tempFilePath, toPath: cachePath)
// print("\(videoId):\(cachePath)")
//
isCompleteds[videoId] = true
return true
} catch {
print("移动 \(videoId) 缓存文件失败,失败原因: \(error)")
// deleteKeyValueIfHaveArchivedWithVideoId(videoId) //
return false
}
}
///
private func audioCachedPath() -> String? {
return (createCachePath() ?? "")
}
///
private func tempPath(_ videoID:String) -> String {
return (NSTemporaryDirectory())+"/"+"\(videoID).mp4"
}
///
private func archiverPath() -> String {
return ((createCachePath() ?? ""))+"/"+"MP_Player.archiver"
}
} }

View File

@ -38,13 +38,11 @@ class MP_NetWorkManager: NSObject {
configuration.httpMaximumConnectionsPerHost = 4 configuration.httpMaximumConnectionsPerHost = 4
return Alamofire.Session(configuration: configuration, interceptor: MP_CustomRetrier()) return Alamofire.Session(configuration: configuration, interceptor: MP_CustomRetrier())
}() }()
///IDID
private var relatedRequests: [String:DataRequest] = [:]
/// ///
private var playerRequests: [PlayerRequest] = [] private var playerRequests: [String:DataRequest] = [:]
///
struct PlayerRequest {
let request: DataRequest
let onCancel: (() -> Void)
}
//MARK: - API //MARK: - API
///IP ///IP
private let iPInfo:String = "https://api.tikustok.com/app/common/getIPInfo" private let iPInfo:String = "https://api.tikustok.com/app/common/getIPInfo"
@ -665,14 +663,14 @@ extension MP_NetWorkManager {
] ]
] ]
//guard netWorkStatu != .notReachable else {return} //guard netWorkStatu != .notReachable else {return}
requestPostNextLyricsAndRelated(url, parameters: parameters) { result in requestPostNextLyricsAndRelated(url, videoId: item.videoId, parameters: parameters) { result in
completion(result) completion(result)
} }
} }
//Next/ //Next/
private func requestPostNextLyricsAndRelated(_ url:URL, parameters:Parameters, completion:@escaping(((String?,String?)) -> Void)) { private func requestPostNextLyricsAndRelated(_ url:URL, videoId:String, parameters:Parameters, completion:@escaping(((String?,String?)) -> Void)) {
//post //post
MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonNext.self) { [weak self] (response) in let request = MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonNext.self) { [weak self] (response) in
guard let self = self else {return} guard let self = self else {return}
switch response.result { switch response.result {
case .success(let value): case .success(let value):
@ -684,6 +682,7 @@ extension MP_NetWorkManager {
handleError(url, error: error) handleError(url, error: error)
} }
} }
relatedRequests[videoId] = request
} }
//MARK: - player //MARK: - player
@ -713,21 +712,11 @@ extension MP_NetWorkManager {
] ]
] ]
//guard netWorkStatu != .notReachable else {return} //guard netWorkStatu != .notReachable else {return}
requestAndroidPostPlayer(url, parameters: parameters){ resourceUlrs, coverUrls in requestAndroidPostPlayer(url, videoId: videoId, parameters: parameters){ resourceUlrs, coverUrls in
completion(resourceUlrs, coverUrls) completion(resourceUlrs, coverUrls)
} }
} }
private func requestAndroidPostPlayer(_ url:URL, parameters:Parameters, completion:@escaping((([String],[Int],[String])?, [String]?) -> Void)) { private func requestAndroidPostPlayer(_ url:URL, videoId:String, parameters:Parameters, completion:@escaping((([String],[Int],[String])?, [String]?) -> Void)) {
//
playerRequests = playerRequests.filter({$0.request.task?.state == .running})
//
if playerRequests.count >= 4, let requestToCancel = playerRequests.first {
requestToCancel.request.cancel()
//
requestToCancel.onCancel()
playerRequests.removeFirst()
print("取消多余的Player资源请求: \(requestToCancel)")
}
//post //post
let request = PlayerSeesion.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonAndroidPlayer.self) { [weak self] (response) in let request = PlayerSeesion.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonAndroidPlayer.self) { [weak self] (response) in
guard let self = self else {return} guard let self = self else {return}
@ -741,10 +730,15 @@ extension MP_NetWorkManager {
handleError(url, error: error) handleError(url, error: error)
} }
} }
// //videoIdrequest
playerRequests.append(.init(request: request, onCancel: { playerRequests[videoId] = request
completion(nil,nil) }
})) ///
func removeVideoResource(_ videoId: String) {
playerRequests[videoId]?.cancel()
playerRequests.removeValue(forKey: videoId)
relatedRequests[videoId]?.cancel()
relatedRequests.removeValue(forKey: videoId)
} }
// func requestPlayer(_ videoId: String, playlistId: String, completion:@escaping ((([String],[Float],[String]), [String]?) -> Void)){ // func requestPlayer(_ videoId: String, playlistId: String, completion:@escaping ((([String],[Float],[String]), [String]?) -> Void)){
// //player // //player

View File

@ -142,6 +142,14 @@ class MP_PlayerManager:NSObject{
var runActionBlock:MP_PlayTimerRunAction! var runActionBlock:MP_PlayTimerRunAction!
/// ///
var cacheValueBlock:MP_PlayCacheValueAction! var cacheValueBlock:MP_PlayCacheValueAction!
///
private var statusObservation:NSKeyValueObservation?
///
private var loadedTimeRangesObservation:NSKeyValueObservation?
///
private var playbackLikelyToKeepUpObservation:NSKeyValueObservation?
///
private var errorObservation:NSKeyValueObservation?
private override init() { private override init() {
super.init() super.init()
// //
@ -230,11 +238,66 @@ class MP_PlayerManager:NSObject{
//PlayerItem //PlayerItem
if loadPlayer.currentVideo?.isKVO == false { if loadPlayer.currentVideo?.isKVO == false {
// //
loadPlayer?.currentVideo?.resourcePlayerItem?.addObserver(self, forKeyPath: "status", options: [.old,.new], context: nil) 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()
}
}
}
})
// //
loadPlayer?.currentVideo?.resourcePlayerItem?.addObserver(self, forKeyPath: "loadedTimeRanges", options: [.old,.new], context: nil) loadedTimeRangesObservation?.invalidate()
loadedTimeRangesObservation = loadPlayer?.currentVideo?.resourcePlayerItem?.observe(\.loadedTimeRanges, options: [.old,.new], changeHandler: { [weak self] item, change in
guard let self = self else {return}
cacheLoadTimes()
})
// //
loadPlayer?.currentVideo?.resourcePlayerItem?.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: [.old,.new], context: nil) 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 loadPlayer.currentVideo.isKVO = true
//0 //0
player.seek(to: .zero) player.seek(to: .zero)
@ -305,65 +368,66 @@ class MP_PlayerManager:NSObject{
// } // }
//KVO //KVO
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { // override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let keyPath = keyPath else { // guard let keyPath = keyPath else {
return // return
} // }
//keyPath // //keyPath
switch keyPath { // switch keyPath {
case "status"://playerItem // case "status"://playerItem
if let statuValue = change?[.newKey] as? Int, statuValue == AVPlayerItem.Status.readyToPlay.rawValue { // if let statuValue = change?[.newKey] as? Int, statuValue == AVPlayerItem.Status.readyToPlay.rawValue {
// // //
if playState != .Playing { // if playState != .Playing {
//statuVlaueplayerItem // //statuVlaueplayerItem
print("当前音乐-\(loadPlayer?.currentVideo?.title ?? "") 已经准备好播放") // print("-\(loadPlayer?.currentVideo?.title ?? "") ")
} // }
}else { // }else {
print("当前音乐-\(loadPlayer.currentVideo?.title ?? "") 未做好准备播放,失败原因是\(loadPlayer.currentVideo?.resourcePlayerItem.error?.localizedDescription ?? "")") // print("-\(loadPlayer.currentVideo?.title ?? "") \(loadPlayer.currentVideo?.resourcePlayerItem.error?.localizedDescription ?? "")")
if loadPlayer?.currentVideo?.isKVO == true { // if loadPlayer?.currentVideo?.isKVO == true {
suspendTimer() // 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 ?? "") // 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 // //KVO
loadPlayer?.currentVideo?.resourcePlayerItem.removeObserver(self, forKeyPath: "status") // loadPlayer?.currentVideo?.resourcePlayerItem.removeObserver(self, forKeyPath: "status")
loadPlayer?.currentVideo?.resourcePlayerItem.removeObserver(self, forKeyPath: "loadedTimeRanges") // loadPlayer?.currentVideo?.resourcePlayerItem.removeObserver(self, forKeyPath: "loadedTimeRanges")
loadPlayer?.currentVideo?.resourcePlayerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp") // loadPlayer?.currentVideo?.resourcePlayerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp")
loadPlayer?.currentVideo?.isKVO = false // loadPlayer?.currentVideo?.resourcePlayerItem.removeObserver(self, forKeyPath: "error")
// // loadPlayer?.currentVideo?.isKVO = false
loadPlayer.remakeImproveData { // //
[weak self] in // loadPlayer.remakeImproveData {
self?.play() // [weak self] in
} // self?.play()
} // }
} // }
case "loadedTimeRanges":// // }
cacheLoadTimes() // case "loadedTimeRanges"://
case "playbackLikelyToKeepUp":// // cacheLoadTimes()
if let playbackLikelyToKeepUp = change?[.newKey] as? Bool, playbackLikelyToKeepUp == true { // case "playbackLikelyToKeepUp"://
if playState != .Playing && playState != .Pause { // if let playbackLikelyToKeepUp = change?[.newKey] as? Bool, playbackLikelyToKeepUp == true {
// // if playState != .Playing && playState != .Pause {
player.play() // //
playState = .Playing // 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 ?? "") // 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!() // if startActionBlock != nil {
} // startActionBlock!()
} // }
}else { // }
// // }else {
player.pause() // //
playState = .Null // player.pause()
} // playState = .Null
default: // }
break // default:
} // break
} // }
// }
// //
private func cacheLoadTimes() { private func cacheLoadTimes() {
//Item //Item
if let timeRanges = loadPlayer?.currentVideo?.resourcePlayerItem.loadedTimeRanges.map({$0.timeRangeValue}), let first = timeRanges.first { if let timeRanges = loadPlayer?.currentVideo?.resourcePlayerItem?.loadedTimeRanges.map({$0.timeRangeValue}), let first = timeRanges.first {
// //
let startSeconds = first.start.seconds let startSeconds = first.start.seconds
// //
@ -597,9 +661,10 @@ class MP_PlayerManager:NSObject{
if let video = sender.object as? MPPositive_SongViewModel { if let video = sender.object as? MPPositive_SongViewModel {
if video.isKVO == true { if video.isKVO == true {
//KVO //KVO
video.resourcePlayerItem.removeObserver(self, forKeyPath: "status") statusObservation?.invalidate()
video.resourcePlayerItem.removeObserver(self, forKeyPath: "loadedTimeRanges") playbackLikelyToKeepUpObservation?.invalidate()
video.resourcePlayerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp") errorObservation?.invalidate()
loadedTimeRangesObservation?.invalidate()
video.isKVO = false video.isKVO = false
} }
} }

View File

@ -13,9 +13,9 @@ class MPPositive_SongViewModel: NSObject {
/// ///
var index:Int! var index:Int!
/// ///
var resourcePlayerItem:AVPlayerItem! @objc dynamic var resourcePlayerItem:AVPlayerItem!
/// ///
var resourcePlayerAsset:AVURLAsset! var resourcePlayerAsset:MP_AVURLAsset!
/// ///
var resourcePlayerURL:URL! var resourcePlayerURL:URL!
/// ///
@ -40,11 +40,16 @@ class MPPositive_SongViewModel: NSObject {
var song:MPPositive_SongItemModel! var song:MPPositive_SongItemModel!
init(_ song:MPPositive_SongItemModel) { init(_ song:MPPositive_SongItemModel) {
super.init() super.init()
NotificationCenter.notificationKey.add(observer: self, selector: #selector(switchURL403Action(_ :)), notificationName: .player_asset_403)
self.song = song self.song = song
configure() configure()
} }
deinit { deinit {
print("\(title ?? "")已经销毁了") print("\(title ?? "")被销毁了")
NotificationCenter.default.removeObserver(self)
//
MP_NetWorkManager.shared.removeVideoResource(song.videoId)
//
resourcePlayerItem = nil resourcePlayerItem = nil
resourcePlayerAsset = nil resourcePlayerAsset = nil
resourcePlayerURL = nil resourcePlayerURL = nil
@ -62,97 +67,94 @@ class MPPositive_SongViewModel: NSObject {
if song.shortBylineText != nil { if song.shortBylineText != nil {
subtitle = song.shortBylineText! subtitle = song.shortBylineText!
} }
if let first = song.resourceUrls?.first {
resourcePlayerURL = .init(string:first)
resourcePlayerAsset = .init(url: resourcePlayerURL)
preloadAsset(resourcePlayerAsset)
resourcePlayerItem = .init(asset: resourcePlayerAsset)
}
// //
if song.reviewUrls?.first != nil { if song.reviewUrls?.first != nil {
coverUrl = .init(string: song.reviewUrls!.last!) coverUrl = .init(string: song.reviewUrls!.last!)
} }
// //
if song.lyrics != nil { let group = DispatchGroup()
lyrics = song.lyrics //
group.enter()
//idid
if song.lyricsID == nil || song.relatedID == nil {
//
improveDataforLycirsAndRelated(song) {[weak self] (result) in
guard let self = self else {return}
song.lyricsID = result.0
song.relatedID = result.1
group.leave()
}
}else { }else {
if song.lyricsID != nil { //idid
// group.leave()
MP_NetWorkManager.shared.requestLyric(song.lyricsID!) {[weak self] lyrics in }
self?.lyrics = lyrics group.enter()
self?.song.lyrics = lyrics //
//videoID
if let resource = getDocumentsFileURL(song.videoId) {
//
song.resourceUrls = [resource]
group.leave()
}else {
//
//
improveDataforResouceAndCover(song) { [weak self] resourceUrls, coverUrls in
guard let self = self else {return}
if let resourceUrls = resourceUrls {
song.resourceUrls = resourceUrls.0
song.itags = resourceUrls.1
song.mimeTypes = resourceUrls.2
} }
song.coverUrls = coverUrls
group.leave()
} }
} }
// //
if song.relatedID != nil { group.notify(queue: .main) {
relatedId = song.relatedID [weak self] in
guard let self = self else {return}
//
if let first = song.resourceUrls?.first {
resourcePlayerURL = .init(string:first)
if isDlownd == true {
//
resourcePlayerAsset = .init(LocalURL: resourcePlayerURL!, videoId: song.videoId, title: title ?? "")
}else {
//
resourcePlayerAsset = .init(resourcePlayerURL!, videoId: song.videoId, title: title ?? "")
}
//
print("成功装填了\(song.title ?? "")的媒体数据")
resourcePlayerItem = .init(asset: resourcePlayerAsset)
}
//
if song.lyrics != nil {
lyrics = song.lyrics
}else {
if song.lyricsID != nil {
//
MP_NetWorkManager.shared.requestLyric(song.lyricsID!) {[weak self] lyrics in
self?.lyrics = lyrics
self?.song.lyrics = lyrics
}
}
}
//
if song.relatedID != nil {
relatedId = song.relatedID
}
} }
} }
// //
func reloadCollectionAndDownLoad() { func reloadCollectionAndDownLoad() {
// //
isCollection = MPPositive_CollectionSongModel.fetch(.init(format: "videoId == %@", song.videoId)).count != 0 isCollection = MPPositive_CollectionSongModel.fetch(.init(format: "videoId == %@", song.videoId)).count != 0
// //
isDlownd = MPPositive_DownloadItemModel.fetch(.init(format: "videoId == %@", song.videoId)).count != 0 isDlownd = MPPositive_DownloadItemModel.fetch(.init(format: "videoId == %@", song.videoId)).count != 0
} }
// @objc private func switchURL403Action(_ notification:Notification) {
func preloadAsset(_ asset:AVURLAsset) { guard let path = notification.object as? String, song != nil else {return}
guard isPreload == false else { song.resourceUrls?[0] = path
return resourcePlayerURL = .init(string: path)
}
print("\(title ?? "")开始预加载")
//
if #available(iOS 16, *) {
//ios16
Task{
do{
let playable = try await asset.load(.isPlayable)
if playable == true {
print("\(self.title ?? "")预加载成功")
isPreload = true
}else {
//
switch asset.status(of: .isPlayable) {
case .failed(let erro):
print("\(title ?? "")预加载失败,失败原因:\(erro.localizedDescription)")
preloadAsset(asset)
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 ?? "")预加载成功")
self.isPreload = true
}
case .failed:
print("\(title ?? "")预加载失败,失败原因:\(error?.localizedDescription ?? "")")
preloadAsset(asset)
case .cancelled:
print("\(title ?? "")预加载被取消了")
default:
break
}
}
}
}
} }
} }

View File

@ -14,28 +14,23 @@ class MPPositive_PlayerLoadViewModel: NSObject {
var randomVideos:[MPPositive_SongItemModel]! var randomVideos:[MPPositive_SongItemModel]!
///ViewModel ///ViewModel
var currentVideo:MPPositive_SongViewModel!{ var currentVideo:MPPositive_SongViewModel!{
willSet{ didSet{
DispatchQueue.main.asyncAfter(deadline: .now()) { DispatchQueue.main.async {
[weak self] in [weak self] in
guard let self = self else {return} guard let self = self else {return}
if newValue != nil { print("当前播放音乐是:\(currentVideo.song.title ?? "")")
MP_AnalyticsManager.shared.player_b_pvAction(newValue.song.videoId, videoname: newValue.title ?? "", artistname: newValue.song.shortBylineText ?? "") //
if currentVideo != nil { startObservingCurrentVideoItem()
//UI
NotificationCenter.notificationKey.post(notificationName: .positive_player_reload, object: currentVideo)
}else {
//UI
NotificationCenter.notificationKey.post(notificationName: .positive_player_reload)
}
}
} }
} }
} }
//
private var itemObservation: NSKeyValueObservation?
// private var loadQueue:DispatchQueue!
///
// private var group:DispatchGroup!
///ViewModel ///ViewModel
var listViewVideos:[MPPositive_SongViewModel]! var listViewVideos:[MPPositive_SongViewModel]!
///
var group:DispatchGroup?
///palyermodel ///palyermodel
/// - Parameters: /// - Parameters:
/// - songs: /// - songs:
@ -46,6 +41,45 @@ class MPPositive_PlayerLoadViewModel: NSObject {
// //
self.randomVideos = self.songVideos.shuffled() self.randomVideos = self.songVideos.shuffled()
self.listViewVideos = [] self.listViewVideos = []
}
//
private func startObservingCurrentVideoItem() {
itemObservation?.invalidate() //
//currentVideo
if currentVideo?.resourcePlayerItem != nil {
//
postNoticeAction()
}else {
//
itemObservation = currentVideo?.observe(\.resourcePlayerItem, options: [.new], changeHandler: { [weak self] video, change in
guard let self = self else {return}
if let newItem = change.newValue {
// newItem
postNoticeAction()
}else {
//nil
print("无效更新")
}
})
}
}
//
private func postNoticeAction() {
DispatchQueue.main.asyncAfter(deadline: .now()) {
[weak self] in
guard let self = self else {return}
if currentVideo != nil {
MP_AnalyticsManager.shared.player_b_pvAction(currentVideo.song.videoId, videoname: currentVideo.title ?? "", artistname: currentVideo.song.shortBylineText ?? "")
if currentVideo != nil {
//UI
NotificationCenter.notificationKey.post(notificationName: .positive_player_reload, object: currentVideo)
}else {
//UI
NotificationCenter.notificationKey.post(notificationName: .positive_player_reload)
}
}
}
} }
///Video14VideoViewModel, ///Video14VideoViewModel,
@ -58,11 +92,11 @@ class MPPositive_PlayerLoadViewModel: NSObject {
return return
} }
array.append(self.randomVideos[targetIndex]) array.append(self.randomVideos[targetIndex])
// // //
let previousIndex = targetIndex-1 // let previousIndex = targetIndex-1
if previousIndex >= 0 { // if previousIndex >= 0 {
array.append(self.randomVideos[previousIndex]) // array.append(self.randomVideos[previousIndex])
} // }
let nextIndex = targetIndex+1 let nextIndex = targetIndex+1
let lastIndex = targetIndex+2 let lastIndex = targetIndex+2
if nextIndex < randomVideos.count { if nextIndex < randomVideos.count {
@ -77,11 +111,11 @@ class MPPositive_PlayerLoadViewModel: NSObject {
return return
} }
array.append(self.songVideos[targetIndex]) array.append(self.songVideos[targetIndex])
// // //
let previousIndex = targetIndex-1 // let previousIndex = targetIndex-1
if previousIndex >= 0 { // if previousIndex >= 0 {
array.append(self.songVideos[previousIndex]) // array.append(self.songVideos[previousIndex])
} // }
let nextIndex = targetIndex+1 let nextIndex = targetIndex+1
let lastIndex = targetIndex+2 let lastIndex = targetIndex+2
if nextIndex < songVideos.count { if nextIndex < songVideos.count {
@ -95,54 +129,14 @@ class MPPositive_PlayerLoadViewModel: NSObject {
let videoIDs = Set(listViewVideos.map({$0.song.videoId})) let videoIDs = Set(listViewVideos.map({$0.song.videoId}))
//videoID, //videoID,
array = array.filter({!videoIDs.contains($0.videoId)}) array = array.filter({!videoIDs.contains($0.videoId)})
//MPPositive_SongViewModel
group = DispatchGroup() let viewModels = array.map({(MPPositive_SongViewModel($0))})
//ViewModel
//, self.listViewVideos.append(contentsOf: viewModels)
for item in array { //
group?.enter() self.listViewVideos = self.listViewVideos.suffix(3)
//idid //
if item.lyricsID == nil || item.relatedID == nil { self.currentVideo = self.listViewVideos.first(where: {$0.song.videoId == targetVideoId})
improveDataforLycirsAndRelated(item) {[weak self] (result) in
item.lyricsID = result.0
item.relatedID = result.1
self?.group?.leave()
}
}else {
self.group?.leave()
}
group?.enter()
//videoID
if let resource = getDocumentsFileURL(item.videoId) {
//resource
item.resourceUrls = [resource]
//,ViewModellistViewVideos
listViewVideos.append(.init(item))
self.group?.leave()
}else {
//
//
improveDataforResouceAndCover(item) {[weak self] resourceUrls, coverUrls in
if let resourceUrls = resourceUrls {
item.resourceUrls = resourceUrls.0
item.itags = resourceUrls.1
item.mimeTypes = resourceUrls.2
}
item.coverUrls = coverUrls
//,ViewModellistViewVideos
self?.listViewVideos.append(.init(item))
self?.group?.leave()
}
}
}
group?.notify(queue: .main, execute: {
[weak self] in
//
self?.currentVideo = self?.listViewVideos.first(where: {$0.song.videoId == targetVideoId})
//
self?.listViewVideos = self?.listViewVideos.suffix(4)
self?.group = nil
})
} }
/// ///
func remakeImproveData(_ completion:@escaping (() -> Void)) { func remakeImproveData(_ completion:@escaping (() -> Void)) {
@ -176,4 +170,8 @@ class MPPositive_PlayerLoadViewModel: NSObject {
private func findVideoIdForDocument(_ videoId:String) -> Bool { private func findVideoIdForDocument(_ videoId:String) -> Bool {
return MPPositive_DownloadItemModel.fetch(.init(format: "videoId == %@", videoId)).count != 0 return MPPositive_DownloadItemModel.fetch(.init(format: "videoId == %@", videoId)).count != 0
} }
//
deinit {
itemObservation?.invalidate()
}
} }

View File

@ -335,7 +335,7 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont
private func uploadUI() { private func uploadUI() {
DispatchQueue.main.async { DispatchQueue.main.async {
[weak self] in [weak self] in
guard let self = self else {return} guard let self = self, MP_PlayerManager.shared.loadPlayer?.currentVideo != nil else {return}
print("\(MP_PlayerManager.shared.loadPlayer?.currentVideo?.title ?? "")刷新了页面") print("\(MP_PlayerManager.shared.loadPlayer?.currentVideo?.title ?? "")刷新了页面")
// //
backImageView.kf.setImage(with: MP_PlayerManager.shared.loadPlayer.currentVideo?.coverUrl, placeholder: placeholderImage) backImageView.kf.setImage(with: MP_PlayerManager.shared.loadPlayer.currentVideo?.coverUrl, placeholder: placeholderImage)

View File

@ -84,7 +84,7 @@ class MPPositive_MusicItemShowTableViewCell: UITableViewCell {
} }
contentView.addSubview(rankLabel) contentView.addSubview(rankLabel)
rankLabel.snp.makeConstraints { make in rankLabel.snp.makeConstraints { make in
make.centerX.equalTo(coverImageView) make.center.equalTo(coverImageView)
make.width.equalTo(coverImageView) make.width.equalTo(coverImageView)
} }
contentView.addSubview(moreBtn) contentView.addSubview(moreBtn)