Music_Player3/relax.offline.mp3.music/MP/Common/Tool(工具封装)/MP_AVURLAsset.swift
2024-06-03 09:48:39 +08:00

502 lines
23 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// MP_AVURLAsset.swift
// MusicPlayer
//
// Created by Mr.Zhou on 2024/5/23.
//
import UIKit
import AVFoundation
extension URL {
func convertCustomSchemeURL(_ scheme: String) -> URL? {
var components = URLComponents(url: self, resolvingAgainstBaseURL: false)
components?.scheme = scheme
return components?.url
}
}
///
@objc protocol MP_AVPlayerItemDelegate {
///
@objc func playerItemReadyToPlay(_ playerItem: MP_AVPlayerItem)
///
@objc func playerItem(_ playerItem: MP_AVPlayerItem, progress:Float)
///
@objc func playerItem(_ playerItem: MP_AVPlayerItem, didFinishLoadingData data:Data)
///,
@objc func playerItemPlaybackStalled(_ playerItem: MP_AVPlayerItem)
///
@objc func playerItem(_ playerItem: MP_AVPlayerItem, loadingError error:Error)
}
///
class MP_AVPlayerItem: AVPlayerItem {
///
var resourceLoaderDelegate:MP_ResourceLoaderDelegate!
///
fileprivate var url:URL
///Scheme
fileprivate let initialScheme:String?
///
fileprivate var customFileExtension:String?
///scheme
fileprivate let customScheme = "myapp"
///
fileprivate var title:String = "Random"
///ID
fileprivate var videoId:String = "Random"
///
fileprivate var resourceAsset:MP_AVURLAsset!
///
weak var delegate:MP_AVPlayerItemDelegate?
///
init(url: URL, bitrate:Int64, customFileExtension: String? = "mp4", title:String?, videoId:String?) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let scheme = components.scheme,
var urlWithCustomScheme = url.convertCustomSchemeURL(customScheme) else {
fatalError("不支持没有scheme的URL")
}
self.url = url
self.initialScheme = scheme
//scheme
if let stringPath = customFileExtension {
//
urlWithCustomScheme.deletePathExtension()
//
urlWithCustomScheme.appendPathExtension(stringPath)
//
self.customFileExtension = stringPath
}
if let title = title, let videoId = videoId {
//
self.title = title
self.videoId = videoId
}
resourceLoaderDelegate = .init(bitrate)
resourceAsset = MP_AVURLAsset(url: urlWithCustomScheme, delegate: resourceLoaderDelegate, title: title ?? "")
//
super.init(asset: resourceAsset, automaticallyLoadedAssetKeys: nil)
//playerItem
resourceLoaderDelegate.playItem = self
//kvo
addObserver(self, forKeyPath: "status", options: [.old,.new], context: nil)
//kvo
addObserver(self, forKeyPath: "loadedTimeRanges", options: [.old,.new], context: nil)
//kvo
addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: [.old,.new], context: nil)
//kvo
addObserver(self, forKeyPath: "error", options: [.old,.new], context: nil)
NotificationCenter.default.addObserver(self, selector: #selector(playbackStalledHandler), name:NSNotification.Name.AVPlayerItemPlaybackStalled, object: self)
}
//MARK: - KVO
override open 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 {
//
print("当前音乐-\(title) 已经准备好播放")
}
case "loadedTimeRanges"://
guard self.videoId == MP_PlayerManager.shared.loadPlayer.currentVideo?.song.videoId else {return}
let timeRanges = self.loadedTimeRanges.map({$0.timeRangeValue})
//Item
if let first = timeRanges.first {
//
let startSeconds = first.start.seconds
//
let durationSeconds = first.duration.seconds
//
let bufferedSeconds = startSeconds + durationSeconds
//
let maxDuration = CMTimeGetSeconds(self.duration)
let progress = bufferedSeconds/maxDuration
//
delegate?.playerItem(self, progress: Float(progress))
}
case "playbackLikelyToKeepUp"://
if let playbackLikelyToKeepUp = change?[.newKey] as? Bool, playbackLikelyToKeepUp == true {
//
delegate?.playerItemReadyToPlay(self)
}else {
//
delegate?.playerItemPlaybackStalled(self)
}
case "error":
if let error = change?[.newKey] as? Error {
print("当前音乐-\(title) 未做好准备播放,失败原因是\(error)")
delegate?.playerItem(self, loadingError: error)
}
default:
break
}
}
//playerItem
@objc func playbackStalledHandler() {
delegate?.playerItemPlaybackStalled(self)
}
deinit {
print("已销毁\(title)")
NotificationCenter.default.removeObserver(self)
removeObserver(self, forKeyPath: "status")
removeObserver(self, forKeyPath: "loadedTimeRanges")
removeObserver(self, forKeyPath: "playbackLikelyToKeepUp")
removeObserver(self, forKeyPath: "error")
//
resourceLoaderDelegate.session?.invalidateAndCancel()
//
resourceAsset = nil
}
}
///
class MP_AVURLAsset: AVURLAsset {
init(url URL: URL, delegate:MP_ResourceLoaderDelegate, title:String) {
super.init(url: URL, options: nil)
self.resourceLoader.setDelegate(delegate, queue: .main)
//AssetAssetresourceLoaderdelegate
//
let keys = ["playable"]
self.loadValuesAsynchronously(forKeys: keys) {
[weak self] in
//
for key in keys {
var error: NSError?
let status = self?.statusOfValue(forKey: key, error: &error)
if status == .loaded {
print("开始对\(title)的预加载")
} else {
//
print("无法加载 \(key): \(String(describing: error))")
//
}
}
}
}
}
///
class MP_ResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate, URLSessionDataDelegate, URLSessionTaskDelegate {
//
private var iscompleted:Bool?
//
var mimeType: String?
//
var bitrate:Int64
//
var session: URLSession?
//
var mediaData: Data?
//
var mediaDataBlocks: [MediaDataBlock]?
//
var response: URLResponse?
//
var pendingRequests = Set<AVAssetResourceLoadingRequest>()
//
var maxCount:Int64?
//
var progress:Float = 0
weak var playItem: MP_AVPlayerItem?
//
private var lastRequestedEndOffset:Int64?
init(_ bitrate:Int64) {
self.bitrate = bitrate
super.init()
//seek
NotificationCenter.notificationKey.add(observer: self, selector: #selector(seekAction(_ :)), notificationName: .positive_player_seek)
}
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
guard let initialUrl = playItem?.url else {
fatalError("internal inconsistency")
}
//session
if session == nil {
//
if let media = MP_CacheManager.shared.data(forKey: playItem?.videoId ?? "") {
print("缓存中存在\(playItem?.title ?? ""),取出中")
//
mediaData = media.data
mediaDataBlocks = media.dataBlocks
//
if media.isComplete == false {
print("\(playItem?.title ?? "")数据不完整,开始网络请求相关数据")
maxCount = media.maxCount
//
continuationDataRequest(with: initialUrl, count: Int64(media.data.count))
iscompleted = false
}else {
iscompleted = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self = self else {return}
if MP_PlayerManager.shared.loadPlayer.currentVideo?.song.videoId == playItem?.videoId {
playItem?.delegate?.playerItem(playItem!, didFinishLoadingData: media.data)
}
}
}
}else {
print("开始网络请求\(playItem?.title ?? "")相关数据")
//
startDataRequest(with: initialUrl)
}
}else {
}
pendingRequests.insert(loadingRequest)
// processPendingRequests()
return true
}
@objc private func seekAction(_ sender:Notification) {
if playItem?.videoId == MP_PlayerManager.shared.loadPlayer.currentVideo.song.videoId {
if let progress = sender.object as? Double {
let requestedOffset = progress * Double(maxCount ?? 0)
//
if requestedOffset > Double(mediaData?.count ?? 0) {
print("用户滚动到了当前缓存范围外")
// seekURL
let initialUrl = playItem?.url
continuationDataRequest(with: initialUrl!, count: Int64(requestedOffset))
}
}
}
}
///
/// - Parameters:
/// - url:
func startDataRequest(with url: URL) {
//
session?.invalidateAndCancel()
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
session?.dataTask(with: url).resume()
}
///
/// - Parameters:
/// - url:
/// - data:
func continuationDataRequest(with url: URL, count:Int64) {
if session == nil {
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}else {
//
session?.getAllTasks(completionHandler: { tasks in
//
tasks.forEach({$0.cancel()})
})
}
//
var request = URLRequest(url: url)
//
let bytes = "bytes=\(count)-"
request.addValue(bytes, forHTTPHeaderField: "Range")
request.addValue(url.host ?? "", forHTTPHeaderField: "Host")
request.addValue("video/mp4", forHTTPHeaderField: "Accept")
session?.dataTask(with: request).resume()
}
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) {
pendingRequests.remove(loadingRequest)
}
// MARK: URLSession
///
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
// guard response.mimeType?.contains("video") == true else {
// print("\(playItem?.title ?? "")\(response.mimeType ?? "")")
// //
// MP_NetWorkManager.shared.requestPlayer(playItem?.videoId ?? "", playlistId: "") {[weak self] resourceUrls, coverUrls in
// //
// guard let self = self else {return}
// print("\(playItem?.title ?? "")")
// let url = URL(string: resourceUrls.0.first ?? "")!
// playItem?.url = url
// if mediaData == nil {
// startDataRequest(with: url)
// }else {
// continuationDataRequest(with: url, count: Int64(mediaData!.count))
// }
// }
// return
// }
// completionHandler(Foundation.URLSession.ResponseDisposition.allow)
// if mediaData == nil {
// mediaData = Data()
// mediaDataBlocks = []
// }
// self.response = response
// processPendingRequests()
}
///
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
guard let response = dataTask.response as? HTTPURLResponse,
let contentRange = response.allHeaderFields["Accept-Ranges"] as? String else {
print("\(playItem?.title ?? "")数据更新失败")
return
}
// Content-Range
let rangeInfo = extractRangeInfo(contentRange)
//使 DataSegment
let dataSegment = MediaDataBlock(offset: (rangeInfo?.start ?? 0), data: data)
mediaData?.append(data)
mediaDataBlocks?.append(dataSegment)
if mediaData != nil {
//
MP_CacheManager.shared.cacheData(mediaData!, dataBlocks: mediaDataBlocks!, forKey: MP_PlayerManager.shared.loadPlayer.currentVideo?.song.videoId ?? "", isComplete: false, maxCount: self.response?.expectedContentLength ?? 0)
}
processPendingRequests()
//
self.lastRequestedEndOffset = Int64(data.count)
}
// Content-Range
private func extractRangeInfo(_ contentRange: String) -> (start: Int64, end: Int64)? {
// "bytes start-end/total"
let pattern = "bytes (\\d+)-(\\d+)/(\\d+|\\*)"
//
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
return nil
}
// Content-Range
let nsRange = NSRange(contentRange.startIndex..<contentRange.endIndex, in: contentRange)
guard let match = regex.firstMatch(in: contentRange, options: [], range: nsRange) else {
return nil
}
//
let startString = (contentRange as NSString).substring(with: match.range(at: 1))
let endString = (contentRange as NSString).substring(with: match.range(at: 2))
let totalString = (contentRange as NSString).substring(with: match.range(at: 3))
guard let start = Int64(startString), let end = Int64(endString), let total = totalString == "*" ? nil : Int64(totalString) else {
return nil
}
return (start: start, end: end)
}
////
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let errorUnwrapped = error as NSError?, errorUnwrapped.domain == NSURLErrorDomain && errorUnwrapped.code == NSURLErrorCancelled {
//
print("\(playItem?.title ?? "")网络请求被释放了")
if mediaData != nil {
//
MP_CacheManager.shared.cacheData(mediaData!, dataBlocks: mediaDataBlocks!, forKey: MP_PlayerManager.shared.loadPlayer.currentVideo?.song.videoId ?? "", isComplete: true, maxCount: response?.expectedContentLength ?? 0)
}
processPendingRequests()
}else if let error = error {
print("\(playItem?.title ?? "")-网络请求失败,原因为:\(error)")
playItem?.delegate?.playerItem(playItem!, loadingError: error)
return
}else {
//
if mediaData != nil {
//
MP_CacheManager.shared.cacheData(mediaData!, dataBlocks: mediaDataBlocks!, forKey: MP_PlayerManager.shared.loadPlayer.currentVideo?.song.videoId ?? "", isComplete: true, maxCount: response?.expectedContentLength ?? 0)
}
print("\(playItem?.title ?? "")资源加载完成")
//
self.lastRequestedEndOffset = response?.expectedContentLength
processPendingRequests()
playItem?.delegate?.playerItem(playItem!, didFinishLoadingData: mediaData!)
}
}
func processPendingRequests() {
//
let requestsFulfilled = Set<AVAssetResourceLoadingRequest>(pendingRequests.compactMap {
self.fillInContentInformationRequest($0.contentInformationRequest)
if self.haveEnoughDataToFulfillRequest($0.dataRequest!) {
$0.finishLoading()
return $0
}
return nil
})
//
_ = requestsFulfilled.map { self.pendingRequests.remove($0) }
}
///contentInformationRequest
func fillInContentInformationRequest(_ contentInformationRequest: AVAssetResourceLoadingContentInformationRequest?) {
//
if iscompleted == true {
contentInformationRequest?.contentType = "video/mp4"
self.maxCount = Int64(mediaData!.count)
contentInformationRequest?.contentLength = Int64(mediaData!.count)
contentInformationRequest?.isByteRangeAccessSupported = true
return
}
//contentInformationRequest
if let responseUnwrapped = response {
contentInformationRequest?.contentType = responseUnwrapped.mimeType
self.maxCount = responseUnwrapped.expectedContentLength
contentInformationRequest?.contentLength = responseUnwrapped.expectedContentLength
contentInformationRequest?.isByteRangeAccessSupported = true
}else if let maxCount = maxCount {
contentInformationRequest?.contentType = "video/mp4"
contentInformationRequest?.contentLength = maxCount
contentInformationRequest?.isByteRangeAccessSupported = true
}
}
///
func haveEnoughDataToFulfillRequest(_ dataRequest: AVAssetResourceLoadingDataRequest) -> Bool {
let requestedOffset = Int64(dataRequest.requestedOffset)
let requestedLength = dataRequest.requestedLength
let currentOffset = Int64(dataRequest.currentOffset)
// Data
var responseData = Data()
//
//
for item in (mediaDataBlocks ?? []).sorted(by: { $0.offset < $1.offset }) {
//
if item.offset < currentOffset + Int64(requestedLength) && item.offset + Int64(item.data.count) > currentOffset {
// responseData
let start = max(currentOffset, item.offset) - item.offset
let end = min(currentOffset + Int64(requestedLength), item.offset + Int64(item.data.count)) - item.offset
if start < end { //
responseData.append(item.data.subdata(in: Int(start)..<Int(end)))
}
}
}
// responseData
if responseData.count >= requestedLength {
// 使dataRequest
dataRequest.respond(with: responseData)
return true
}
// false
return false
// guard let songDataUnwrapped = mediaData,
// songDataUnwrapped.count > currentOffset else {
// return false
// }
// let bytesToRespond = min(songDataUnwrapped.count - currentOffset, requestedLength)
// let dataToRespond = songDataUnwrapped.subdata(in: Range(uncheckedBounds: (currentOffset, currentOffset + bytesToRespond)))
// dataRequest.respond(with: dataToRespond)
// return songDataUnwrapped.count >= requestedLength + requestedOffset
}
deinit {
NotificationCenter.default.removeObserver(self)
session?.invalidateAndCancel()
mediaData = nil
mediaDataBlocks = nil
}
}
///
struct MediaDataBlock: Codable {
///
var offset:Int64
///
var data:Data
}