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")
|
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
|
||||||
|
|||||||
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
///b面播放器403事件
|
||||||
|
case player_asset_403
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -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()
|
||||||
// 缓存实体,字典形式,键值1为videoID,键值2为对应的资源数据
|
|
||||||
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:
|
// 所有缓存文件都放在了沙盒Cache文件夹下MP_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"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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())
|
||||||
}()
|
}()
|
||||||
|
///播放相关内容ID和歌词ID记录组
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 将新请求添加到追踪数组
|
//根据当前videoId添加request
|
||||||
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路径
|
||||||
|
|||||||
@ -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 {
|
||||||
|
//当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
|
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 {
|
||||||
//当statuVlaue值等于playerItem准备播放的值,说明已经准备好播放
|
// //当statuVlaue值等于playerItem准备播放的值,说明已经准备好播放
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()
|
||||||
|
//补全歌词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 {
|
}else {
|
||||||
if song.lyricsID != nil {
|
//歌词id和相关内容id是完整的
|
||||||
//执行网络请求拿到歌词数据
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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?
|
|
||||||
///palyer管理model初始化方法
|
///palyer管理model初始化方法
|
||||||
/// - 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///将选中Video的上1位,下两位项包括本身总计4项Video进行补全转为ViewModel,并播放这首音乐
|
///将选中Video的上1位,下两位项包括本身总计4项Video进行补全转为ViewModel,并播放这首音乐
|
||||||
@ -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)
|
||||||
//补全歌词id和相关内容id
|
//更新当前播放音乐
|
||||||
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]
|
|
||||||
//补全完成,转化为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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
///重新获取指定歌曲资源
|
///重新获取指定歌曲资源
|
||||||
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user