B面1.0.6,缓存以及播放流畅度优化
This commit is contained in:
parent
61ea3a59ec
commit
c54252ca0e
@ -33,7 +33,7 @@ class MP_LunchViewController: UIViewController, GADFullScreenContentDelegate {
|
||||
view.backgroundColor = .init(hex: "#000000")
|
||||
timer = CADisplayLink(target: self, selector: #selector(timerActionClick(_ :)))
|
||||
//一秒执行多少次
|
||||
timer.preferredFramesPerSecond = 40
|
||||
timer.preferredFramesPerSecond = 10
|
||||
//开辟线程
|
||||
timer.add(to: RunLoop.current, forMode: .common)
|
||||
//启动计时器
|
||||
@ -113,7 +113,7 @@ class MP_LunchViewController: UIViewController, GADFullScreenContentDelegate {
|
||||
@objc fileprivate func timerActionClick(_ link:CADisplayLink) {
|
||||
if maxTimes > currentTimes {
|
||||
//未加载完
|
||||
currentTimes += 0.025
|
||||
currentTimes += 0.1
|
||||
let value = (currentTimes/maxTimes).isNaN ? 0:(currentTimes/maxTimes)
|
||||
DispatchQueue.main.async {
|
||||
[weak self] in
|
||||
|
||||
@ -8,87 +8,72 @@
|
||||
import UIKit
|
||||
|
||||
class MP_Lunch_ProgressView: UIView {
|
||||
struct Constant {
|
||||
//进度条宽度
|
||||
static let lineWidth: CGFloat = 6*width
|
||||
//进度槽颜色
|
||||
static let trackColor:UIColor = .init(hex: "#FFFFFF")
|
||||
//进度条颜色
|
||||
static let progressColors:[CGColor] = [
|
||||
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,
|
||||
UIColor(red: 0.796, green: 0.839, blue: 0.294, alpha: 1).cgColor
|
||||
]
|
||||
// 渐变层
|
||||
private var gradientLayer: CAGradientLayer!
|
||||
// 进度条层(作为渐变层的遮罩)
|
||||
private var progressLayer: CALayer!
|
||||
|
||||
// 当前进度 (0.0 - 1.0)
|
||||
var progress: CGFloat = 0.0 {
|
||||
didSet {
|
||||
updateProgressLayer()
|
||||
}
|
||||
}
|
||||
//渐变层
|
||||
private var gradientLayer:CAGradientLayer!
|
||||
//进度槽
|
||||
private var trackLayer:CAShapeLayer!
|
||||
//进度条路径(水平线)
|
||||
private var path:UIBezierPath!
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setUpLayers()
|
||||
setupGradientLayer()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setUpLayers()
|
||||
setupGradientLayer()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
// 保证子图层的尺寸和视图尺寸一致
|
||||
gradientLayer.frame = self.bounds
|
||||
updateProgressLayer()
|
||||
}
|
||||
override func draw(_ rect: CGRect) {
|
||||
//初始化圆角矩形路径
|
||||
path = UIBezierPath()
|
||||
//获取起点
|
||||
let startPoint = CGPoint(x: bounds.minX, y: bounds.maxY)
|
||||
//获取终点
|
||||
let endPoint = CGPoint(x: bounds.maxX, y: bounds.maxY)
|
||||
//路径起始点
|
||||
path.move(to: startPoint)
|
||||
//增加路线
|
||||
path.addLine(to: endPoint)
|
||||
//初始化进度槽
|
||||
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)
|
||||
|
||||
private func setupGradientLayer() {
|
||||
backgroundColor = .init(hex: "#FFFFFF",alpha: 0.1)
|
||||
// 初始化并设置渐变层
|
||||
gradientLayer = CAGradientLayer()
|
||||
gradientLayer.colors = [
|
||||
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,
|
||||
UIColor(red: 0.796, green: 0.839, blue: 0.294, alpha: 1).cgColor
|
||||
]
|
||||
gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
|
||||
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
|
||||
layer.addSublayer(gradientLayer)
|
||||
//渐变层遮罩
|
||||
gradientLayer.mask = trackLayer
|
||||
|
||||
// 初始化进度条层并设置为渐变层的遮罩
|
||||
progressLayer = CALayer()
|
||||
progressLayer.backgroundColor = UIColor.white.cgColor // 颜色可以是任意的,因为这个图层只是用来作为形状遮罩
|
||||
gradientLayer.mask = progressLayer
|
||||
}
|
||||
//layer添加
|
||||
private func setUpLayers() {
|
||||
backgroundColor = .init(hex: "#FFFFFF", alpha: 0.15)
|
||||
|
||||
private func updateProgressLayer() {
|
||||
// 根据当前进度和视图的宽度调整进度条层的大小
|
||||
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) {
|
||||
if progress <= 1 {
|
||||
//进度条动画
|
||||
trackLayer.strokeEnd = progress
|
||||
|
||||
// 当需要设置进度时,调用此方法
|
||||
func setProgress(_ progress: CGFloat, animated: Bool = false) {
|
||||
self.progress = min(max(progress, 0.0), 1.0) // 确保进度值在有效范围内
|
||||
if animated {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -97,6 +97,16 @@ extension NotificationCenter{
|
||||
case positive_nav_pop
|
||||
///b面网络请求报错
|
||||
case netWork_error_deal
|
||||
///b面媒体资源得到服务器响应
|
||||
case asset_response
|
||||
///b面媒体资源从服务器接收到数据
|
||||
case asset_receiveData
|
||||
///b面媒体资源缓存结果
|
||||
case asset_isCached
|
||||
///b面媒体资源是否报错
|
||||
case asset_errorCode
|
||||
///b面播放器403事件
|
||||
case player_asset_403
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -6,138 +6,185 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
///缓存实体资源
|
||||
class CachedMedia: NSObject, Codable {
|
||||
//数据
|
||||
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 {
|
||||
///缓存/归档管理工具
|
||||
class MP_CacheAndArchiverManager {
|
||||
// 单例模式,提供全局访问点
|
||||
static let shared = MP_CacheManager()
|
||||
// 缓存实体,字典形式,键值1为videoID,键值2为对应的资源数据
|
||||
private let memoryCache = NSCache<NSString, CachedMedia>()
|
||||
static let shared = MP_CacheAndArchiverManager()
|
||||
//文件管理器
|
||||
private let fileManager = FileManager.default
|
||||
//缓存文件地址
|
||||
private let cacheDirectory: URL
|
||||
//缓存专用线程
|
||||
private var cacheQueue = DispatchQueue(label: "com.MP_CacheManager.cacheQueue")
|
||||
//缓存专用
|
||||
private var cacheOperations: [String: DispatchWorkItem] = [:]
|
||||
//专用节流时间间隔
|
||||
private let throttleInterval: TimeInterval = 1.0
|
||||
//固定缓存时间
|
||||
private let expirationInterval: TimeInterval = 86400 // 24小时
|
||||
let fileManager = FileManager.default
|
||||
///归档指定数据字典
|
||||
// private var archiverDic:[String: MP_PlayerRequestModel]!
|
||||
///缓存状态更新
|
||||
private var isCompleteds:[String:Bool] = [:]
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// 保存缓存数据
|
||||
/// - Parameters:
|
||||
/// - data: 数据本身
|
||||
/// - key: 使用videoId作为对应的键值
|
||||
func cacheData(_ data: Data, dataBlocks:[MediaDataBlock], forKey key: String, isComplete: Bool = false, maxCount:Int64) {
|
||||
// 在你的 cacheQueue 里执行操作来确保线程安全
|
||||
cacheQueue.async { [weak self] in
|
||||
// 取消这个 key 的任何现有缓存操作
|
||||
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)
|
||||
///创建播放器缓存文件
|
||||
func createCachePath() -> String? {
|
||||
// 所有缓存文件都放在了沙盒Cache文件夹下MP_PlayerCache文件夹里
|
||||
let cacheDirectory = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first!
|
||||
let path = cacheDirectory+"/"+"MP_PlayerCache"
|
||||
//检索这个缓存文件夹是否存在
|
||||
guard fileManager.fileExists(atPath: path) == false else {
|
||||
//存在,直接返回这个文件夹
|
||||
return path
|
||||
}
|
||||
}
|
||||
//保存缓存数据
|
||||
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 {
|
||||
let newData = try encoder.encode(contentToCache)
|
||||
// 写入到文件
|
||||
try newData.write(to: tempURL, options: .atomicWrite)
|
||||
// 检查目标位置是否已经存在文件,如果存在,则删除
|
||||
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)
|
||||
//创建这个缓存文件夹
|
||||
try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)
|
||||
return path
|
||||
} catch {
|
||||
// 如果发生错误,删除临时文件
|
||||
try? self.fileManager.removeItem(at: tempURL)
|
||||
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)")
|
||||
}
|
||||
//创建失败
|
||||
print("创建播放器缓存文件夹失败,失败原因:\(error)")
|
||||
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"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -38,13 +38,11 @@ class MP_NetWorkManager: NSObject {
|
||||
configuration.httpMaximumConnectionsPerHost = 4
|
||||
return Alamofire.Session(configuration: configuration, interceptor: MP_CustomRetrier())
|
||||
}()
|
||||
///播放相关内容ID和歌词ID记录组
|
||||
private var relatedRequests: [String:DataRequest] = [:]
|
||||
///播放资源请求记录组
|
||||
private var playerRequests: [PlayerRequest] = []
|
||||
///播放资源请求结构体
|
||||
struct PlayerRequest {
|
||||
let request: DataRequest
|
||||
let onCancel: (() -> Void)
|
||||
}
|
||||
private var playerRequests: [String:DataRequest] = [:]
|
||||
|
||||
//MARK: - API接口
|
||||
///IP获取
|
||||
private let iPInfo:String = "https://api.tikustok.com/app/common/getIPInfo"
|
||||
@ -665,14 +663,14 @@ extension MP_NetWorkManager {
|
||||
]
|
||||
]
|
||||
//guard netWorkStatu != .notReachable else {return}
|
||||
requestPostNextLyricsAndRelated(url, parameters: parameters) { result in
|
||||
requestPostNextLyricsAndRelated(url, videoId: item.videoId, parameters: parameters) { result in
|
||||
completion(result)
|
||||
}
|
||||
}
|
||||
//请求请求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请求
|
||||
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}
|
||||
switch response.result {
|
||||
case .success(let value):
|
||||
@ -684,6 +682,7 @@ extension MP_NetWorkManager {
|
||||
handleError(url, error: error)
|
||||
}
|
||||
}
|
||||
relatedRequests[videoId] = request
|
||||
}
|
||||
|
||||
//MARK: - 请求player播放资源
|
||||
@ -713,21 +712,11 @@ extension MP_NetWorkManager {
|
||||
]
|
||||
]
|
||||
//guard netWorkStatu != .notReachable else {return}
|
||||
requestAndroidPostPlayer(url, parameters: parameters){ resourceUlrs, coverUrls in
|
||||
requestAndroidPostPlayer(url, videoId: videoId, parameters: parameters){ resourceUlrs, coverUrls in
|
||||
completion(resourceUlrs, coverUrls)
|
||||
}
|
||||
}
|
||||
private func requestAndroidPostPlayer(_ url:URL, 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)")
|
||||
}
|
||||
private func requestAndroidPostPlayer(_ url:URL, videoId:String, parameters:Parameters, completion:@escaping((([String],[Int],[String])?, [String]?) -> Void)) {
|
||||
//发送post请求
|
||||
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}
|
||||
@ -741,10 +730,15 @@ extension MP_NetWorkManager {
|
||||
handleError(url, error: error)
|
||||
}
|
||||
}
|
||||
// 将新请求添加到追踪数组
|
||||
playerRequests.append(.init(request: request, onCancel: {
|
||||
completion(nil,nil)
|
||||
}))
|
||||
//根据当前videoId添加request
|
||||
playerRequests[videoId] = request
|
||||
}
|
||||
///移除补全任务请求任务
|
||||
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)){
|
||||
// //拼接出player路径
|
||||
|
||||
@ -142,6 +142,14 @@ class MP_PlayerManager:NSObject{
|
||||
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()
|
||||
//初始化计时器
|
||||
@ -230,11 +238,66 @@ class MP_PlayerManager:NSObject{
|
||||
//对当前播放PlayerItem设置监听状态
|
||||
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 {
|
||||
//当statuVlaue值等于playerItem准备播放的值,说明已经准备好播放
|
||||
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
|
||||
//将进度回归为0
|
||||
player.seek(to: .zero)
|
||||
@ -305,65 +368,66 @@ class MP_PlayerManager:NSObject{
|
||||
// }
|
||||
|
||||
//实现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 {
|
||||
//当statuVlaue值等于playerItem准备播放的值,说明已经准备好播放
|
||||
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?.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
|
||||
}
|
||||
}
|
||||
// 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 {
|
||||
// //当statuVlaue值等于playerItem准备播放的值,说明已经准备好播放
|
||||
// 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 {
|
||||
if let timeRanges = loadPlayer?.currentVideo?.resourcePlayerItem?.loadedTimeRanges.map({$0.timeRangeValue}), let first = timeRanges.first {
|
||||
//获取开始时间的秒数
|
||||
let startSeconds = first.start.seconds
|
||||
//获取缓冲区的持续时间
|
||||
@ -597,9 +661,10 @@ class MP_PlayerManager:NSObject{
|
||||
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")
|
||||
statusObservation?.invalidate()
|
||||
playbackLikelyToKeepUpObservation?.invalidate()
|
||||
errorObservation?.invalidate()
|
||||
loadedTimeRangesObservation?.invalidate()
|
||||
video.isKVO = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,9 +13,9 @@ class MPPositive_SongViewModel: NSObject {
|
||||
///排序号
|
||||
var index:Int!
|
||||
///播放实例
|
||||
var resourcePlayerItem:AVPlayerItem!
|
||||
@objc dynamic var resourcePlayerItem:AVPlayerItem!
|
||||
///播放媒体
|
||||
var resourcePlayerAsset:AVURLAsset!
|
||||
var resourcePlayerAsset:MP_AVURLAsset!
|
||||
///播放路径
|
||||
var resourcePlayerURL:URL!
|
||||
///封面
|
||||
@ -40,11 +40,16 @@ class MPPositive_SongViewModel: NSObject {
|
||||
var song:MPPositive_SongItemModel!
|
||||
init(_ song:MPPositive_SongItemModel) {
|
||||
super.init()
|
||||
NotificationCenter.notificationKey.add(observer: self, selector: #selector(switchURL403Action(_ :)), notificationName: .player_asset_403)
|
||||
self.song = song
|
||||
configure()
|
||||
}
|
||||
deinit {
|
||||
print("\(title ?? "")已经销毁了")
|
||||
print("\(title ?? "")被销毁了")
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
//同步取消任务
|
||||
MP_NetWorkManager.shared.removeVideoResource(song.videoId)
|
||||
//销毁实例
|
||||
resourcePlayerItem = nil
|
||||
resourcePlayerAsset = nil
|
||||
resourcePlayerURL = nil
|
||||
@ -62,97 +67,94 @@ class MPPositive_SongViewModel: NSObject {
|
||||
if song.shortBylineText != nil {
|
||||
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 {
|
||||
coverUrl = .init(string: song.reviewUrls!.last!)
|
||||
}
|
||||
//歌词
|
||||
if song.lyrics != nil {
|
||||
lyrics = song.lyrics
|
||||
//创建队列组
|
||||
let group = DispatchGroup()
|
||||
//执行数据补全
|
||||
group.enter()
|
||||
//补全歌词id和相关内容id
|
||||
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 {
|
||||
if song.lyricsID != nil {
|
||||
//执行网络请求拿到歌词数据
|
||||
MP_NetWorkManager.shared.requestLyric(song.lyricsID!) {[weak self] lyrics in
|
||||
self?.lyrics = lyrics
|
||||
self?.song.lyrics = lyrics
|
||||
//歌词id和相关内容id是完整的
|
||||
group.leave()
|
||||
}
|
||||
group.enter()
|
||||
//补全资源路径和封面路径
|
||||
//判断当前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 {
|
||||
relatedId = song.relatedID
|
||||
//当所有内容都修改完毕后
|
||||
group.notify(queue: .main) {
|
||||
[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() {
|
||||
//检索是否收藏
|
||||
isCollection = MPPositive_CollectionSongModel.fetch(.init(format: "videoId == %@", song.videoId)).count != 0
|
||||
|
||||
//检索是否下载
|
||||
isDlownd = MPPositive_DownloadItemModel.fetch(.init(format: "videoId == %@", song.videoId)).count != 0
|
||||
}
|
||||
//执行预加载
|
||||
func preloadAsset(_ asset:AVURLAsset) {
|
||||
guard isPreload == false else {
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@objc private func switchURL403Action(_ notification:Notification) {
|
||||
guard let path = notification.object as? String, song != nil else {return}
|
||||
song.resourceUrls?[0] = path
|
||||
resourcePlayerURL = .init(string: path)
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,28 +14,23 @@ class MPPositive_PlayerLoadViewModel: NSObject {
|
||||
var randomVideos:[MPPositive_SongItemModel]!
|
||||
///当前播放音乐ViewModel
|
||||
var currentVideo:MPPositive_SongViewModel!{
|
||||
willSet{
|
||||
DispatchQueue.main.asyncAfter(deadline: .now()) {
|
||||
didSet{
|
||||
DispatchQueue.main.async {
|
||||
[weak self] in
|
||||
guard let self = self else {return}
|
||||
if newValue != nil {
|
||||
MP_AnalyticsManager.shared.player_b_pvAction(newValue.song.videoId, videoname: newValue.title ?? "", artistname: newValue.song.shortBylineText ?? "")
|
||||
if currentVideo != nil {
|
||||
//当值变化时通知播放器页面,更新UI
|
||||
NotificationCenter.notificationKey.post(notificationName: .positive_player_reload, object: currentVideo)
|
||||
}else {
|
||||
//当值变化时通知播放器页面,更新UI
|
||||
NotificationCenter.notificationKey.post(notificationName: .positive_player_reload)
|
||||
}
|
||||
}
|
||||
print("当前播放音乐是:\(currentVideo.song.title ?? "")")
|
||||
//启动监听器
|
||||
startObservingCurrentVideoItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//监听器
|
||||
private var itemObservation: NSKeyValueObservation?
|
||||
// private var loadQueue:DispatchQueue!
|
||||
///异步请求组
|
||||
// private var group:DispatchGroup!
|
||||
///单曲播放队列ViewModel
|
||||
var listViewVideos:[MPPositive_SongViewModel]!
|
||||
///异步请求组
|
||||
var group:DispatchGroup?
|
||||
///palyer管理model初始化方法
|
||||
/// - Parameters:
|
||||
/// - songs: 全部歌曲列表
|
||||
@ -46,8 +41,47 @@ class MPPositive_PlayerLoadViewModel: NSObject {
|
||||
//根据列表生成一份随机播放列表
|
||||
self.randomVideos = self.songVideos.shuffled()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///将选中Video的上1位,下两位项包括本身总计4项Video进行补全转为ViewModel,并播放这首音乐
|
||||
func improveData(_ targetVideoId:String, isRandom:Bool = false) {
|
||||
//对于选中Video的集合
|
||||
@ -58,11 +92,11 @@ class MPPositive_PlayerLoadViewModel: NSObject {
|
||||
return
|
||||
}
|
||||
array.append(self.randomVideos[targetIndex])
|
||||
//获取上一位
|
||||
let previousIndex = targetIndex-1
|
||||
if previousIndex >= 0 {
|
||||
array.append(self.randomVideos[previousIndex])
|
||||
}
|
||||
// //获取上一位
|
||||
// let previousIndex = targetIndex-1
|
||||
// if previousIndex >= 0 {
|
||||
// array.append(self.randomVideos[previousIndex])
|
||||
// }
|
||||
let nextIndex = targetIndex+1
|
||||
let lastIndex = targetIndex+2
|
||||
if nextIndex < randomVideos.count {
|
||||
@ -77,11 +111,11 @@ class MPPositive_PlayerLoadViewModel: NSObject {
|
||||
return
|
||||
}
|
||||
array.append(self.songVideos[targetIndex])
|
||||
//获取上一位
|
||||
let previousIndex = targetIndex-1
|
||||
if previousIndex >= 0 {
|
||||
array.append(self.songVideos[previousIndex])
|
||||
}
|
||||
// //获取上一位
|
||||
// let previousIndex = targetIndex-1
|
||||
// if previousIndex >= 0 {
|
||||
// array.append(self.songVideos[previousIndex])
|
||||
// }
|
||||
let nextIndex = targetIndex+1
|
||||
let lastIndex = targetIndex+2
|
||||
if nextIndex < songVideos.count {
|
||||
@ -95,54 +129,14 @@ class MPPositive_PlayerLoadViewModel: NSObject {
|
||||
let videoIDs = Set(listViewVideos.map({$0.song.videoId}))
|
||||
//比较videoID,去掉已经补完的内容
|
||||
array = array.filter({!videoIDs.contains($0.videoId)})
|
||||
|
||||
group = DispatchGroup()
|
||||
|
||||
//去重完毕,对剩下内容补完
|
||||
for item in array {
|
||||
group?.enter()
|
||||
//补全歌词id和相关内容id
|
||||
if item.lyricsID == nil || item.relatedID == nil {
|
||||
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]
|
||||
//补全完成,转化为ViewModel,并添加进listViewVideos
|
||||
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
|
||||
//补全完成,转化为ViewModel,并添加进listViewVideos
|
||||
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
|
||||
})
|
||||
//全部转化为MPPositive_SongViewModel
|
||||
let viewModels = array.map({(MPPositive_SongViewModel($0))})
|
||||
//添加新的ViewModel
|
||||
self.listViewVideos.append(contentsOf: viewModels)
|
||||
//只保留最后三首
|
||||
self.listViewVideos = self.listViewVideos.suffix(3)
|
||||
//更新当前播放音乐
|
||||
self.currentVideo = self.listViewVideos.first(where: {$0.song.videoId == targetVideoId})
|
||||
}
|
||||
///重新获取指定歌曲资源
|
||||
func remakeImproveData(_ completion:@escaping (() -> Void)) {
|
||||
@ -176,4 +170,8 @@ class MPPositive_PlayerLoadViewModel: NSObject {
|
||||
private func findVideoIdForDocument(_ videoId:String) -> Bool {
|
||||
return MPPositive_DownloadItemModel.fetch(.init(format: "videoId == %@", videoId)).count != 0
|
||||
}
|
||||
//销毁时
|
||||
deinit {
|
||||
itemObservation?.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
@ -335,7 +335,7 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont
|
||||
private func uploadUI() {
|
||||
DispatchQueue.main.async {
|
||||
[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 ?? "")刷新了页面")
|
||||
//填充数据
|
||||
backImageView.kf.setImage(with: MP_PlayerManager.shared.loadPlayer.currentVideo?.coverUrl, placeholder: placeholderImage)
|
||||
|
||||
@ -84,7 +84,7 @@ class MPPositive_MusicItemShowTableViewCell: UITableViewCell {
|
||||
}
|
||||
contentView.addSubview(rankLabel)
|
||||
rankLabel.snp.makeConstraints { make in
|
||||
make.centerX.equalTo(coverImageView)
|
||||
make.center.equalTo(coverImageView)
|
||||
make.width.equalTo(coverImageView)
|
||||
}
|
||||
contentView.addSubview(moreBtn)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user