├── README.md ├── ZLAVPlayer ├── Assets.xcassets │ ├── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── Bridge-Header.h ├── Utility │ ├── WeakObject.swift │ ├── URL+StreamScheme.swift │ ├── String+MD5Tool.swift │ └── FileManager+Extension.swift ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── AppDelegate.swift ├── ZLCacheLoader │ ├── VideoDownloadTask.swift │ ├── VideoResourceLoader.swift │ ├── VideoCacheManager.swift │ └── VideoDownloadManager.swift ├── ViewController.swift └── ZLAVPlayer │ └── ZLAVPlayer.swift └── ZLAVPlayer.xcodeproj ├── xcuserdata └── tuneszhao.xcuserdatad │ └── xcdebugger │ └── Breakpoints_v2.xcbkptlist ├── project.xcworkspace ├── contents.xcworkspacedata ├── xcuserdata │ └── tuneszhao.xcuserdatad │ │ └── IDEFindNavigatorScopes.plist └── xcshareddata │ └── IDEWorkspaceChecks.plist └── project.pbxproj /README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ZLAVPlayer/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ZLAVPlayer.xcodeproj/xcuserdata/tuneszhao.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /ZLAVPlayer.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ZLAVPlayer.xcodeproj/project.xcworkspace/xcuserdata/tuneszhao.xcuserdatad/IDEFindNavigatorScopes.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ZLAVPlayer/Bridge-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Bridge-Header.h 3 | // ZLAVPlayer 4 | // 5 | // Created by 赵磊 on 2019/4/16. 6 | // Copyright © 2019 zl. All rights reserved. 7 | // 8 | 9 | #ifndef Bridge_Header_h 10 | #define Bridge_Header_h 11 | 12 | #import 13 | 14 | #endif /* Bridge_Header_h */ 15 | -------------------------------------------------------------------------------- /ZLAVPlayer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ZLAVPlayer/Utility/WeakObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeakObject.swift 3 | // ZLAVPlayer 4 | // 5 | // Created by 赵磊 on 2018/9/21. 6 | // Copyright © 2018年 zl. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | 弱引用对象容器 13 | */ 14 | class WeakObject where T: Equatable { 15 | 16 | weak var target: T? 17 | 18 | init(target: T) { 19 | self.target = target 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ZLAVPlayer/Utility/URL+StreamScheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL+StreamScheme.swift 3 | // ZLAVPlayer 4 | // 5 | // Created by 赵磊 on 2018/6/29. 6 | // Copyright © 2018年 zl. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension URL { 12 | 13 | /// 视频代理的scheme 14 | var streamSchemeURL: URL { 15 | var component = URLComponents(url: self, resolvingAgainstBaseURL: true)! 16 | component.scheme = "Stream" 17 | return component.url! 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ZLAVPlayer/Utility/String+MD5Tool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+MD5.swift 3 | // Video 4 | // 5 | // Created by 赵磊 on 2018/6/20. 6 | // Copyright © 2018年 zl. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | extension String { 13 | 14 | /// md5值 15 | var md5String: String { 16 | return withCString { (ptr: UnsafePointer) -> String in 17 | var buffer = UnsafeMutablePointer.allocate(capacity: Int(CC_MD5_DIGEST_LENGTH)) 18 | defer { 19 | buffer.deallocate() 20 | } 21 | 22 | CC_MD5(UnsafeRawPointer(ptr), CC_LONG(lengthOfBytes(using: .utf8)), buffer) 23 | 24 | var hash = String() 25 | for i in 0.. String { 34 | return String(self[.. String { 38 | return String(self[self.index(self.startIndex, offsetBy: index)...]) 39 | } 40 | 41 | func toRange(_ range: NSRange) -> Range? { 42 | guard let from16 = utf16.index(utf16.startIndex, offsetBy: range.location, limitedBy: utf16.endIndex) else { return nil } 43 | guard let to16 = utf16.index(from16, offsetBy: range.length, limitedBy: utf16.endIndex) else { return nil } 44 | guard let from = String.Index(from16, within: self) else { return nil } 45 | guard let to = String.Index(to16, within: self) else { return nil } 46 | return from ..< to 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ZLAVPlayer/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ZLAVPlayer/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | 10 | CFBundleDevelopmentRegion 11 | $(DEVELOPMENT_LANGUAGE) 12 | CFBundleExecutable 13 | $(EXECUTABLE_NAME) 14 | CFBundleIdentifier 15 | $(PRODUCT_BUNDLE_IDENTIFIER) 16 | CFBundleInfoDictionaryVersion 17 | 6.0 18 | CFBundleName 19 | $(PRODUCT_NAME) 20 | CFBundlePackageType 21 | APPL 22 | CFBundleShortVersionString 23 | 1.0 24 | CFBundleVersion 25 | 1 26 | LSRequiresIPhoneOS 27 | 28 | UILaunchStoryboardName 29 | LaunchScreen 30 | UIMainStoryboardFile 31 | Main 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /ZLAVPlayer/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /ZLAVPlayer/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ZLAVPlayer 4 | // 5 | // Created by 赵磊 on 2019/4/16. 6 | // Copyright © 2019 zl. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /ZLAVPlayer/ZLCacheLoader/VideoDownloadTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoDownloadTask.swift 3 | // ZLAVPlayer 4 | // 5 | // Created by 赵磊 on 2018/7/12. 6 | // Copyright © 2018年 zl. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AVFoundation 11 | 12 | // 下载进度回调 13 | typealias VideoDownloadProgressHandler = ((AVAssetResourceLoadingRequest?, VideoDownloadTask) -> Void) 14 | 15 | // 下载完成回调 16 | typealias VideoDownloadCompleteHandler = ((Data?, Error?) -> Void) 17 | 18 | 19 | /** 20 | 视频下载回调结构 21 | */ 22 | class VideoDownloadCallback { 23 | 24 | /// 视频播放器加载request 25 | let loadingRequest: AVAssetResourceLoadingRequest? 26 | 27 | /// 进度回调 28 | let progressHandler: VideoDownloadProgressHandler? 29 | 30 | /// 完成回调 31 | let completeHandler: VideoDownloadCompleteHandler? 32 | 33 | init?(loadingRequest: AVAssetResourceLoadingRequest? = nil, progress: VideoDownloadProgressHandler? = nil, complete: VideoDownloadCompleteHandler? = nil) { 34 | if loadingRequest == nil && progress == nil && complete == nil { 35 | return nil 36 | } 37 | 38 | self.loadingRequest = loadingRequest 39 | self.progressHandler = progress 40 | self.completeHandler = complete 41 | } 42 | } 43 | 44 | /** 45 | 视频下载任务 46 | */ 47 | class VideoDownloadTask { 48 | 49 | /// 下载url 50 | let url: URL 51 | 52 | /// 下载任务 53 | let dataTask: URLSessionDataTask 54 | 55 | private var observers = [WeakObject]() 56 | private let observersLock = NSLock() 57 | 58 | /// 回调 59 | lazy var callbacks = [VideoDownloadCallback]() 60 | private let callbacksLock = NSLock() 61 | 62 | /// 视频data 63 | var cachedData: Data { 64 | dataLock.lock() 65 | defer { 66 | dataLock.unlock() 67 | } 68 | 69 | let dataCopy = data 70 | return dataCopy 71 | } 72 | private lazy var data = Data() 73 | private let dataLock = NSLock() 74 | 75 | /// 视频总长度 76 | var contentLength = 0 77 | 78 | /// 通过URL和DataTask初始化 79 | init(url: URL, dataTask: URLSessionDataTask) { 80 | self.url = url 81 | self.dataTask = dataTask 82 | } 83 | 84 | /// 添加data 85 | func appendData(_ newData: Data) { 86 | dataLock.lock() 87 | if newData.count > 0 { 88 | data.append(newData) 89 | } 90 | dataLock.unlock() 91 | } 92 | 93 | /// 添加回调 94 | func addCallback(_ callback: VideoDownloadCallback) { 95 | callbacksLock.lock() 96 | callbacks.append(callback) 97 | callbacksLock.unlock() 98 | } 99 | 100 | /// 移除回调 101 | func removeCallback(by loadingRequest: AVAssetResourceLoadingRequest) { 102 | callbacksLock.lock() 103 | 104 | if callbacks.count > 0{ 105 | callbacks = callbacks.filter{ task -> Bool in 106 | return task.loadingRequest != loadingRequest 107 | } 108 | } 109 | 110 | callbacksLock.unlock() 111 | } 112 | 113 | /// 添加观察者 114 | func addObsever(_ obsever: WeakObject) { 115 | if obsever.target == nil { 116 | return 117 | } 118 | 119 | observersLock.lock() 120 | observers = observers.filter { return $0.target != nil} 121 | if !observers.contains { $0.target == obsever.target} { 122 | observers.append(obsever) 123 | } 124 | observersLock.unlock() 125 | } 126 | 127 | /// 移除观察者 128 | func removeObsever(_ obsever: WeakObject) { 129 | observersLock.lock() 130 | observers = observers.filter { return $0.target != nil && $0.target != obsever.target} 131 | observersLock.unlock() 132 | } 133 | 134 | /// 过滤空的obsever,返回当前的数量 135 | func filterNullObsever() -> Int { 136 | observersLock.lock() 137 | observers = observers.filter { return $0.target != nil} 138 | observersLock.unlock() 139 | return observers.count 140 | } 141 | } 142 | 143 | // MARK: - VideoDownloadTask Equatable 144 | extension VideoDownloadTask: Equatable { 145 | 146 | static func == (lhs: VideoDownloadTask, rhs: VideoDownloadTask) -> Bool { 147 | return lhs.url == rhs.url 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /ZLAVPlayer/Utility/FileManager+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileManager+Extension.swift 3 | // ZLAVPlayer 4 | // 5 | // Created by 赵磊 on 2018/7/10. 6 | // Copyright © 2018年 zl. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension FileManager { 12 | 13 | /// Documents路径 14 | static let documentsPath: String = { 15 | let array = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) 16 | return array.first! 17 | }() 18 | 19 | /// 获取Users Documents目录 20 | static let usersDocumentsPath: String? = { 21 | let usersDocumentsPath = FileManager.documentsPath.appendingPathComponent("Users") 22 | guard createPath(path: usersDocumentsPath) else { 23 | return nil 24 | } 25 | 26 | return usersDocumentsPath 27 | }() 28 | 29 | /// Caches路径 30 | static let cachesPath: String = { 31 | let array = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true) 32 | return array.first! 33 | }() 34 | 35 | /// 获取Users Cache目录 36 | static let usersCachePath: String? = { 37 | let usersCachePath = FileManager.cachesPath.appendingPathComponent("Users") 38 | guard createPath(path: usersCachePath) else { 39 | return nil 40 | } 41 | 42 | return usersCachePath 43 | }() 44 | 45 | // /// 获取当前用户的Cache目录 46 | // static let currentUserCachePath: String? = { 47 | // guard let usersCachePath = usersCachePath else { 48 | // return nil 49 | // } 50 | // 51 | // var currentUserCachePath: String 52 | // let userID = AccountManager.shared.accountInfo.userID 53 | // if userID.count > 0 { 54 | // currentUserCachePath = usersCachePath.appendingPathComponent(userID) 55 | // } 56 | // else { 57 | // currentUserCachePath = usersCachePath.appendingPathComponent(AppCommon.deviceID) 58 | // } 59 | // 60 | // guard createPath(path: currentUserCachePath) else { 61 | // return nil 62 | // } 63 | // 64 | // return currentUserCachePath 65 | // }() 66 | // 67 | // /// 生成当前用户目录下特定模块的缓存文件 68 | // static func buildCacheFile(module: String..., name: String) -> String? { 69 | // guard let currentUserCachePath = FileManager.currentUserCachePath else { 70 | // return nil 71 | // } 72 | // 73 | // var modulePath = currentUserCachePath 74 | // for moduleItem in module { 75 | // modulePath = modulePath.appendingPathComponent(moduleItem) 76 | // 77 | // guard createPath(path: modulePath) else { 78 | // return nil 79 | // } 80 | // } 81 | // 82 | // let cacheFilePath = modulePath.appendingPathComponent(name) 83 | // 84 | // guard createFile(file: cacheFilePath) else { 85 | // return nil 86 | // } 87 | // 88 | // return cacheFilePath 89 | // } 90 | 91 | /// 创建目录 92 | static func createPath(path: String) -> Bool { 93 | var success = true 94 | 95 | if !FileManager.default.fileExists(atPath: path) { 96 | do { 97 | try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) 98 | } catch { 99 | success = false 100 | print("FileManager", "create path=\(path) error:\(error)") 101 | } 102 | } 103 | 104 | return success 105 | } 106 | 107 | /// 创建文件 108 | static func createFile(file: String) -> Bool { 109 | if !FileManager.default.fileExists(atPath: file) { 110 | return FileManager.default.createFile(atPath: file, contents: nil, attributes: nil) 111 | } 112 | 113 | return true 114 | } 115 | } 116 | 117 | extension String { 118 | 119 | /// lastPathComponent 120 | var lastPathComponent: String { 121 | return (self as NSString).lastPathComponent 122 | } 123 | 124 | /// deletingLastPathComponent 125 | var deletingLastPathComponent: String { 126 | return (self as NSString).deletingLastPathComponent 127 | } 128 | 129 | /// appendingPathComponent 130 | func appendingPathComponent(_ str: String) -> String { 131 | return (self as NSString).appendingPathComponent(str) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /ZLAVPlayer/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // ZLAVPlayer 4 | // 5 | // Created by 赵磊 on 2019/4/16. 6 | // Copyright © 2019 zl. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | private var zlAVPlayer: ZLAVPlayer! 14 | private var url = "http://mirror.aarnet.edu.au/pub/TED-talks/911Mothers_2010W-480p.mp4" 15 | private var manualyPause = false 16 | 17 | @IBOutlet weak var progressView: UIProgressView! 18 | @IBOutlet weak var currentTimeLabel: UILabel! 19 | @IBOutlet weak var totalTimeLabel: UILabel! 20 | @IBOutlet weak var textview: UITextView! 21 | @IBOutlet weak var bufferLabel: UILabel! 22 | @IBOutlet weak var bufferView: UIProgressView! 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | 27 | zlAVPlayer = ZLAVPlayer(withURL: url, 28 | process: { [weak self] (player, progress) in 29 | self?.progressView.progress = Float(progress) 30 | self?.currentTimeLabel.text = self?.timeStringFromSeconds(seconds: player.currentPlayTime ?? 0) 31 | }, compelete: { [weak self] (player) in 32 | self?.textview.text = "播放结束\n\(self?.textview.text ?? "")" 33 | }, loadStatus: { [weak self] (player, status) in 34 | if status == .readyToPlay { 35 | self?.totalTimeLabel.text = self?.timeStringFromSeconds(seconds: player.totalTime ?? 0) 36 | self?.textview.text = "加载状态:准备完毕,可以播放。\n\(self?.textview.text ?? "")" 37 | } else if status == .unknown { 38 | self?.textview.text = "加载状态:未知状态,此时不能播放。\n\(self?.textview.text ?? "")" 39 | } else if status == .failed { 40 | self?.textview.text = "加载状态:加载失败,网络或者服务器出现问题。\(String(describing: player.playerItem?.error))\n\(self?.textview.text ?? "")" 41 | } 42 | }, bufferPercent: { [weak self] (player, bufferPercent) in 43 | self?.bufferLabel.text = "缓冲进度: \(String(format: "%.4f", Float(bufferPercent)))" 44 | self?.bufferView.progress = Float(bufferPercent) 45 | }, willSeek: { (player, curtPos, toPos) in 46 | 47 | }, seekComplete: { (player, prePos, curtPos) in 48 | 49 | }, buffering: { [weak self] (player) in 50 | self?.textview.text = "可用缓冲耗尽,将停止播放!\n\(self?.textview.text ?? "")" 51 | player.pause() 52 | 53 | }, bufferFinish: { [weak self] (player) in 54 | self?.textview.text = "可无延迟播放......\n\(self?.textview.text ?? "")" 55 | guard let `self` = self else { 56 | return 57 | } 58 | if !self.manualyPause { 59 | player.play() 60 | } 61 | 62 | }, error: { [weak self] (player, error) in 63 | self?.textview.text = "播放出错: \(String(describing: error))\n\(self?.textview.text ?? "")" 64 | }) 65 | 66 | view.layer.addSublayer(zlAVPlayer.playerLayer!) 67 | zlAVPlayer.play() 68 | } 69 | 70 | override func viewDidLayoutSubviews() { 71 | super.viewDidLayoutSubviews() 72 | zlAVPlayer.playerLayer?.frame = CGRect(x: 0, y: view.bounds.maxY/3, width: view.bounds.width, height: view.bounds.height/2) 73 | } 74 | 75 | @IBAction func play(_ sender: Any) { 76 | manualyPause = false 77 | zlAVPlayer.play() 78 | } 79 | 80 | @IBAction func pause(_ sender: Any) { 81 | manualyPause = true 82 | zlAVPlayer.pause() 83 | } 84 | 85 | private func timeStringFromSeconds(seconds: Double) -> String { 86 | return String(format: "%ld:%.2ld", Int(seconds)/60, Int(seconds) % 60) 87 | } 88 | 89 | } 90 | 91 | -------------------------------------------------------------------------------- /ZLAVPlayer/ZLCacheLoader/VideoResourceLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoLoaderManager.swift 3 | // Video 4 | // 5 | // Created by 赵磊 on 2018/6/15. 6 | // Copyright © 2018年 zl. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AVFoundation 11 | import MobileCoreServices 12 | 13 | /** 14 | AVPlayer播放器下载代理 15 | */ 16 | class VideoResourceLoader: NSObject { 17 | 18 | /// 加载队列 19 | let resourceLoaderQueue = DispatchQueue(label: "resourceLoaderQueue") 20 | 21 | /// 原始url 22 | let originalURL: URL 23 | 24 | init(originalURL: URL) { 25 | self.originalURL = originalURL 26 | 27 | super.init() 28 | } 29 | 30 | deinit { 31 | // 取消并保存数据 32 | let url = originalURL 33 | DispatchQueue.global().async { // 防止锁逻辑卡到主线程,cancel放到子线程去做 34 | VideoDownloadManager.shared.cancelDownload(url: url) 35 | } 36 | } 37 | 38 | /// 填充数据 39 | @discardableResult 40 | private func respondData(loadingRequest: AVAssetResourceLoadingRequest, data: Data, dataOffset: Int, contentLength: Int, mimeType: String) -> Bool { 41 | if loadingRequest.isCancelled || loadingRequest.isFinished { 42 | return false 43 | } 44 | // 填充信息 45 | if let contentInformationRequest = loadingRequest.contentInformationRequest { 46 | let contentType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType as CFString, nil)?.takeRetainedValue() 47 | contentInformationRequest.contentType = contentType as String? 48 | contentInformationRequest.isByteRangeAccessSupported = true 49 | contentInformationRequest.contentLength = Int64(contentLength) 50 | } 51 | 52 | // 填充数据 53 | if let dataRequest = loadingRequest.dataRequest { 54 | guard dataRequest.currentOffset >= dataOffset && dataRequest.currentOffset < dataOffset.advanced(by: data.count) else { 55 | return false // 没有可用数据 56 | } 57 | 58 | // 可用cache长度 59 | let availableCacheLength = dataOffset.advanced(by: data.count).advanced(by: Int(-dataRequest.currentOffset)) 60 | // 待填充数据长度 61 | let requestedEndIndex = (dataRequest.requestsAllDataToEndOfResource ? contentLength : Int(dataRequest.requestedOffset.advanced(by: dataRequest.requestedLength))) 62 | let unreadLength = requestedEndIndex.advanced(by: Int(-dataRequest.currentOffset)) 63 | // 填充数据长度 64 | let respondDataLength = min(availableCacheLength, Int(unreadLength)) 65 | 66 | // 进行数据填充 67 | let beginIndex = dataRequest.currentOffset.advanced(by: -dataOffset) 68 | let endIndex = beginIndex.advanced(by: respondDataLength) 69 | // print("respondData \(beginIndex)-\(endIndex) totalCacheLength:\(data.count) dataOffset:\(dataOffset)") 70 | let respondData = data.subdata(in: Int(beginIndex)..= dataRequest.requestedOffset.advanced(by: dataRequest.requestedLength) { 75 | print("\(#function) finishLoading") 76 | loadingRequest.finishLoading() 77 | return true 78 | } 79 | } 80 | 81 | return false 82 | } 83 | } 84 | 85 | 86 | // MARK: - AVAssetResourceLoaderDelegate 87 | extension VideoResourceLoader: AVAssetResourceLoaderDelegate { 88 | 89 | public func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool { 90 | print("resourceLoader|shouldWaitForLoadingOfRequestedResource requestedOffset:\(String(describing: loadingRequest.dataRequest?.requestedOffset)) requestedLength:\(String(describing: loadingRequest.dataRequest?.requestedLength)) currentOffset:\(String(describing: loadingRequest.dataRequest?.currentOffset))") 91 | 92 | // 启动下载任务,同时保留loadingRequest, progress 是 URLSession 响应数据的回调处理 93 | VideoDownloadManager.shared.startDownload(url: originalURL, loadingRequest: loadingRequest, progress: { [weak self] (loadingRequest, task) in 94 | if nil == self { 95 | return 96 | } 97 | 98 | self!.resourceLoaderQueue.async { [weak self] in 99 | if nil == self { 100 | return 101 | } 102 | 103 | let isFinish = self!.respondData(loadingRequest: loadingRequest!, data: task.cachedData, dataOffset: 0, contentLength: task.contentLength, mimeType: "video/mp4") 104 | if isFinish { 105 | task.removeCallback(by: loadingRequest!) 106 | } 107 | } 108 | }, complete: { _,_ in 109 | 110 | }, observer: self) 111 | 112 | return true 113 | } 114 | 115 | public func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) { 116 | print("resourceLoader|didCancel") 117 | 118 | VideoDownloadManager.shared.cancelDownloadCallback(url: originalURL, loadingRequest: loadingRequest) 119 | } 120 | } 121 | 122 | 123 | -------------------------------------------------------------------------------- /ZLAVPlayer/ZLCacheLoader/VideoCacheManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoCacheManager.swift 3 | // ZLAVPlayer 4 | // 5 | // Created by 赵磊 on 2018/7/12. 6 | // Copyright © 2018年 zl. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // 视频临时文件key 12 | fileprivate let kDataKey = "data" 13 | fileprivate let kContentLengthKey = "contentLength" 14 | 15 | /** 16 | 视频缓存管理类 17 | */ 18 | class VideoCacheManager { 19 | 20 | static let shared = VideoCacheManager() 21 | 22 | /// IO队列 23 | private let ioQueue: DispatchQueue 24 | 25 | private init() { 26 | ioQueue = DispatchQueue(label: "VideoCacheIOQueue") 27 | 28 | } 29 | 30 | /// 异步载入缓存 31 | func asynLoadCache(url: URL, completeHandler:@escaping (Data?, Int?) -> Void) { 32 | ioQueue.async { 33 | let file = VideoCacheManager.videoFilePath(url: url) 34 | if FileManager.default.fileExists(atPath: file) { 35 | var data: Data? 36 | do { 37 | data = try Data(contentsOf: url) 38 | } catch { 39 | print("Video|cache", "load video file error:\(error)") 40 | } 41 | 42 | if let data = data { 43 | completeHandler(data, data.count) 44 | return // 成功加载完整视频缓存 45 | } 46 | } 47 | 48 | let tmpFile = VideoCacheManager.tmpFilePath(url: url) 49 | if !FileManager.default.fileExists(atPath: tmpFile) { 50 | completeHandler(nil, nil) 51 | return 52 | } 53 | 54 | if let tmpInfo = NSKeyedUnarchiver.unarchiveObject(withFile: tmpFile) as? [String: Any] { 55 | if let data = tmpInfo[kDataKey] as? Data, 56 | let contentLength = tmpInfo[kContentLengthKey] as? Int { 57 | completeHandler(data, contentLength) 58 | return 59 | } else { 60 | print("Video|cache", "video tmp file data invalid") 61 | } 62 | } else { 63 | print("Video|cache", "load video tmp file data fail") 64 | } 65 | 66 | do { 67 | try FileManager.default.removeItem(atPath: tmpFile) 68 | } catch { 69 | print("Video|cache", "remove video tmp file error:\(error)") 70 | } 71 | 72 | completeHandler(nil, nil) 73 | } 74 | } 75 | 76 | /// 保存视频缓存 77 | func storeCache(with downloadTask: VideoDownloadTask, completion: (() -> Void)?) { 78 | ioQueue.async { 79 | if !FileManager.default.fileExists(atPath: VideoCacheManager.videoDirectory) { 80 | do { 81 | try FileManager.default.createDirectory(atPath: VideoCacheManager.videoDirectory, withIntermediateDirectories: true, attributes: nil) 82 | } catch { 83 | print("Video|cache", "create video Directory error:\(error)") 84 | } 85 | } 86 | 87 | let url = downloadTask.url 88 | let tmpFile = VideoCacheManager.tmpFilePath(url: url) 89 | 90 | // 保存完整视频 91 | let data = downloadTask.cachedData 92 | let contentLength = downloadTask.contentLength 93 | let isFinished = (data.count == contentLength) 94 | if isFinished { 95 | let file = VideoCacheManager.videoFilePath(url: url) 96 | do { 97 | try data.write(to: URL(fileURLWithPath: file)) 98 | } catch { 99 | print("Video|cache", "store video error:\(error)") 100 | } 101 | 102 | if FileManager.default.fileExists(atPath: tmpFile) { 103 | do { 104 | try FileManager.default.removeItem(atPath: tmpFile) 105 | } catch { 106 | print("Video|cache", "remove video tmp file error:\(error)") 107 | } 108 | } 109 | } else { // 保存临时视频信息 110 | let tmpInfo = [kDataKey: data, kContentLengthKey: contentLength] as [String : Any] 111 | let success = NSKeyedArchiver.archiveRootObject(tmpInfo, toFile: tmpFile) 112 | if !success { 113 | print("Video|cache", "store video tmp file error") 114 | } 115 | } 116 | 117 | completion?() 118 | } 119 | } 120 | 121 | /// 删除缓存 122 | func removeCache(url: URL, completion: (() -> Void)?) { 123 | ioQueue.async { 124 | // 临时文件 125 | let tmpFile = VideoCacheManager.tmpFilePath(url: url) 126 | if FileManager.default.fileExists(atPath: tmpFile) { 127 | do { 128 | try FileManager.default.removeItem(atPath: tmpFile) 129 | } catch { 130 | print("Video|cache", "remove video tmp file error:\(error)") 131 | } 132 | } 133 | 134 | // 完整文件 135 | let file = VideoCacheManager.videoFilePath(url: url) 136 | if FileManager.default.fileExists(atPath: file) { 137 | do { 138 | try FileManager.default.removeItem(atPath: file) 139 | } catch { 140 | print("Video|cache", "remove video file error:\(error)") 141 | } 142 | } 143 | 144 | completion?() 145 | } 146 | } 147 | } 148 | 149 | // MARK: - Resource Path 150 | extension VideoCacheManager { 151 | 152 | /// 视频目录 153 | // /var/mobile/Containers/Data/Application/B78CD5EE-540B-4C6E-8B49-47EE148881AE/Library/Caches/videos 154 | static var videoDirectory: String = FileManager.cachesPath.appendingPathComponent("videos") 155 | 156 | /// 视频文件名(url md5值) 157 | static func videoFileName(url: URL) -> String { 158 | return url.absoluteString.md5String 159 | } 160 | 161 | /// 视频文件是否存在 162 | static func existVideoFilePath(url: URL) -> String? { 163 | let path = videoFilePath(url: url) 164 | if FileManager.default.fileExists(atPath:path) { 165 | return path 166 | } 167 | 168 | return nil 169 | } 170 | 171 | /// 视频文件路径 172 | static func videoFilePath(url: URL) -> String { 173 | let videoName = videoFileName(url: url) 174 | return videoDirectory.appendingPathComponent(videoName) + ".mp4" 175 | } 176 | 177 | /// 视频临时文件是否存在 178 | static func isTmpFileExist(url: URL) -> Bool { 179 | let path = tmpFilePath(url: url) 180 | return FileManager.default.fileExists(atPath:path) 181 | } 182 | 183 | /// 视频临时文件路径 184 | static func tmpFilePath(url: URL) -> String { 185 | let videoName = videoFileName(url: url) 186 | return videoDirectory.appendingPathComponent(videoName) + ".tmp" 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /ZLAVPlayer/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 35 | 36 | 37 | 38 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /ZLAVPlayer/ZLCacheLoader/VideoDownloadManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoDownloadManager.swift 3 | // ZLAVPlayer 4 | // 5 | // Created by 赵磊 on 2018/7/12. 6 | // Copyright © 2018年 zl. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AVFoundation 11 | 12 | fileprivate let kURLTaskSemaphore = DispatchSemaphore(value: 1) 13 | 14 | 15 | /** 16 | 视频下载管理类 17 | */ 18 | class VideoDownloadManager: NSObject { 19 | 20 | static let shared = VideoDownloadManager() 21 | 22 | /// 存放下载任务的字典 23 | private lazy var urlTasks = [URL: VideoDownloadTask]() 24 | 25 | private var session: URLSession! 26 | 27 | private var downloadQueue: DispatchQueue! 28 | 29 | private override init() { 30 | super.init() 31 | 32 | downloadQueue = DispatchQueue(label: "VideoDownloadQueue") 33 | 34 | let opertionQueue = OperationQueue() 35 | opertionQueue.maxConcurrentOperationCount = 3 36 | opertionQueue.underlyingQueue = downloadQueue 37 | session = URLSession(configuration: .default, delegate: self, delegateQueue: opertionQueue) 38 | } 39 | 40 | /// 启动下载任务 41 | func startDownload(url: URL, loadingRequest: AVAssetResourceLoadingRequest?, progress: VideoDownloadProgressHandler?, complete: VideoDownloadCompleteHandler?, observer: VideoResourceLoader? = nil) { 42 | _ = kURLTaskSemaphore.wait(timeout: .distantFuture) 43 | 44 | // 已经存在任务 45 | if let task = urlTasks[url] { 46 | if let loadingRequest = loadingRequest { 47 | print("Video|download", "start| exist task | url: \(url)") 48 | 49 | /// 添加回调 50 | if let callback = VideoDownloadCallback(loadingRequest: loadingRequest, progress: progress, complete: complete) { 51 | task.addCallback(callback) 52 | } 53 | 54 | /// 添加观察者 55 | if let observer = observer { 56 | let weakObject = WeakObject(target: observer) 57 | task.addObsever(weakObject) 58 | } 59 | 60 | // 回调上层 61 | if let progress = progress { 62 | progress(loadingRequest, task) 63 | } 64 | } 65 | 66 | kURLTaskSemaphore.signal() 67 | } else { // 创建任务 68 | // 框架支持部分缓存,当视频还未缓冲完 playerItem 即被销毁时,将本次已下载数据缓存起来 69 | // 新的 task 先异步载入缓存再继续下载,从而实现“断点续传” 70 | VideoCacheManager.shared.asynLoadCache(url: url) { (data, contentLength) in 71 | let cacheLength = (data != nil) ? data!.count : 0 72 | let isFullCache = (cacheLength > 0 && data!.count == contentLength!) 73 | 74 | print("Video|download", "start| new task| cacheLength:\(cacheLength) isFullCache:\(isFullCache)| url: \(url)") 75 | 76 | // 创建下载任务 77 | let request = self.createURLRequest(url: url, loadingRequest: (isFullCache ? nil : loadingRequest), cacheLength: cacheLength) 78 | let dataTask = self.session.dataTask(with: request) 79 | let newTask = VideoDownloadTask(url: url, dataTask: dataTask) 80 | 81 | /// 添加回调 82 | if let callback = VideoDownloadCallback(loadingRequest: loadingRequest, progress: progress, complete: complete) { 83 | newTask.addCallback(callback) 84 | } 85 | 86 | /// 添加观察者 87 | if let observer = observer { 88 | // 注意:newTask 维护一个 observers 数组,对每个 observer(resourceLoader) 都是强引用 89 | // 避免内存泄漏,使用一个弱引用对象容器封装 90 | let weakObject = WeakObject(target: observer) 91 | newTask.addObsever(weakObject) 92 | } 93 | 94 | // 填充缓存的数据 95 | if let data = data { 96 | newTask.contentLength = contentLength! 97 | newTask.appendData(data) 98 | 99 | // 回调上层 100 | if let loadingRequest = loadingRequest, let progress = progress { 101 | progress(loadingRequest, newTask) 102 | } 103 | } 104 | 105 | self.urlTasks[url] = newTask 106 | 107 | // 任务启动 108 | if !isFullCache { 109 | dataTask.resume() 110 | } 111 | 112 | kURLTaskSemaphore.signal() 113 | } 114 | } 115 | } 116 | 117 | /// 启动下载任务 118 | func preDownload(url: URL) { 119 | if let _ = VideoCacheManager.existVideoFilePath(url: url) { 120 | print("Video|download", "preload| video is cached| url: \(url)") 121 | return 122 | } 123 | 124 | print("Video|download", "preload| url: \(url)") 125 | 126 | // 防止锁逻辑卡到主线程,preDownload放到子线程去做 127 | DispatchQueue.global().async { 128 | self.startDownload(url: url, loadingRequest: nil, progress: nil, complete: nil) 129 | } 130 | } 131 | 132 | /// 删除下载(包括缓存,用于重试的场景) 133 | func removeDownload(url: URL, completion: (() -> Void)?) { 134 | _ = kURLTaskSemaphore.wait(timeout: .distantFuture) 135 | 136 | print("Video|download", "remove|url: \(url)") 137 | 138 | // 取消下载 139 | if let downloadTask = self.urlTasks[url] { 140 | self.urlTasks.removeValue(forKey: url) 141 | downloadTask.dataTask.cancel() 142 | } 143 | 144 | // 删除缓存 145 | VideoCacheManager.shared.removeCache(url: url, completion: completion) 146 | 147 | kURLTaskSemaphore.signal() 148 | } 149 | 150 | /// 取消请求 151 | func cancelDownload(url: URL, observer: VideoResourceLoader? = nil) { 152 | _ = kURLTaskSemaphore.wait(timeout: .distantFuture) 153 | 154 | // 存在任务 155 | if let downloadTask = self.urlTasks[url] { 156 | var shouldCancel = false 157 | if let observer = observer { 158 | downloadTask.removeObsever(WeakObject(target: observer)) 159 | } 160 | 161 | let restCount = downloadTask.filterNullObsever() 162 | if restCount == 0 { 163 | shouldCancel = true // 任务还有其它观察者 164 | } else { 165 | print("Video|download", "cancel|fail|restCount:\(restCount)|taskCount:\(self.urlTasks.count)|url: \(url)") 166 | } 167 | 168 | if shouldCancel { 169 | print("Video|download", "cancel|success|taskCount:\(self.urlTasks.count)|url: \(url)") 170 | 171 | self.urlTasks.removeValue(forKey: url) 172 | downloadTask.dataTask.cancel() 173 | VideoCacheManager.shared.storeCache(with: downloadTask, completion: nil) 174 | } 175 | } else { // 不存在任务 176 | print("Video|download", "cancel|not exist|taskCount:\(self.urlTasks.count)|url: \(url)") 177 | } 178 | 179 | kURLTaskSemaphore.signal() 180 | } 181 | 182 | /// 取消请求 183 | func cancelDownloadCallback(url: URL, loadingRequest: AVAssetResourceLoadingRequest) { 184 | _ = kURLTaskSemaphore.wait(timeout: .distantFuture) 185 | 186 | if let downloadTask = self.urlTasks[url] { 187 | downloadTask.removeCallback(by: loadingRequest) 188 | } 189 | 190 | kURLTaskSemaphore.signal() 191 | } 192 | 193 | /// 创建下载URL请求 194 | private func createURLRequest(url: URL, loadingRequest: AVAssetResourceLoadingRequest?, cacheLength: Int) -> URLRequest { 195 | var request = URLRequest(url: url) 196 | request.cachePolicy = .reloadIgnoringLocalCacheData 197 | 198 | // 设置Range, 如果有部分本地缓存,将缓存终点作为本次请求起点 199 | if let dataRequest = loadingRequest?.dataRequest { 200 | let requestedOffset = (cacheLength > 0 ? Int64(cacheLength) : dataRequest.requestedOffset) 201 | request.addValue("bytes=\(requestedOffset)-", forHTTPHeaderField: "Range") 202 | } 203 | 204 | return request 205 | } 206 | 207 | /// 根据dataTask查找对应的下载任务 208 | private func downloadTask(for dataTask: URLSessionDataTask) -> VideoDownloadTask? { 209 | _ = kURLTaskSemaphore.wait(timeout: .distantFuture) 210 | defer { 211 | kURLTaskSemaphore.signal() 212 | } 213 | 214 | for (_, task) in urlTasks { 215 | if task.dataTask.taskIdentifier == dataTask.taskIdentifier { 216 | return task 217 | } 218 | } 219 | 220 | return nil 221 | } 222 | } 223 | 224 | 225 | // MARK: - URLSessionDataDelegate 226 | extension VideoDownloadManager: URLSessionDataDelegate { 227 | 228 | /// 从响应请求头中获取视频文件总长度 contentLength 229 | public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { 230 | if let downloadTask = downloadTask(for: dataTask) { 231 | guard response.mimeType == "video/mp4" else { 232 | // print("Video|download", "didReceiveResponse|\(dataTask.taskIdentifier)|errorCode:\((response as! HTTPURLResponse).statusCode) mimeType:\(String(describing: response.mimeType)) | url:\(downloadTask.url)") 233 | return 234 | } 235 | 236 | // 请求头有两个字段需要关注 237 | // Content-Length表示本次请求的数据长度 238 | // Content-Range表示本次请求的数据在总媒体文件中的位置,格式是start-end/total,因此就有Content-Length = end - start + 1。 239 | 240 | if let contentRange = (response as! HTTPURLResponse).allHeaderFields["Content-Range"] as? String { 241 | let contentLengthString = contentRange.split(separator: "/").map{String($0)}.last! 242 | downloadTask.contentLength = Int(contentLengthString)! 243 | } else { 244 | downloadTask.contentLength = Int(response.expectedContentLength) 245 | } 246 | 247 | // print("Video|download", "didReceiveResponse \(dataTask.taskIdentifier) contentLength:\(downloadTask.contentLength)| url:\(downloadTask.url)") 248 | } 249 | 250 | completionHandler(.allow) 251 | } 252 | 253 | /// 收到响应数据的处理 254 | public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { 255 | if let downloadTask = downloadTask(for: dataTask) { 256 | // print("Video|download", "\(#function) \(dataTask.taskIdentifier) total:\(downloadTask.contentLength) cacheLength:\(downloadTask.cachedData.count) newLength:\(data.count)") 257 | 258 | // 在已下载数据基础上填充 259 | downloadTask.appendData(data) 260 | 261 | let callbacks = downloadTask.callbacks 262 | for callback in callbacks { 263 | if let progress = callback.progressHandler { 264 | progress(callback.loadingRequest, downloadTask) 265 | } 266 | } 267 | } 268 | } 269 | 270 | public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 271 | if let downloadTask = downloadTask(for: task as! URLSessionDataTask) { 272 | if let error = error as NSError? { 273 | print("Video|download", "finish|fail|\(task.taskIdentifier) errorCode:\(error.code) \(error.localizedDescription) total:\(downloadTask.contentLength) cacheLength:\(downloadTask.cachedData.count)| url:\(downloadTask.url)") 274 | } else { 275 | print("Video|download", "finish|success|\(task.taskIdentifier) total:\(downloadTask.contentLength) cacheLength:\(downloadTask.cachedData.count)| url:\(downloadTask.url)") 276 | } 277 | 278 | // 移除任务 279 | _ = kURLTaskSemaphore.wait(timeout: .distantFuture) 280 | 281 | let callbacks = downloadTask.callbacks 282 | for callback in callbacks { 283 | if let complete = callback.completeHandler { 284 | complete(downloadTask.cachedData, error) 285 | } 286 | } 287 | 288 | VideoCacheManager.shared.storeCache(with: downloadTask, completion: { 289 | DispatchQueue.global().async { // 异步跳出cache的io队列,防止死锁 290 | _ = kURLTaskSemaphore.wait(timeout: .distantFuture) 291 | 292 | let restCount = downloadTask.filterNullObsever() 293 | if restCount == 0 { 294 | self.urlTasks.removeValue(forKey: downloadTask.url) 295 | print("Video|download", "finish|remove task|taskCount:\(self.urlTasks.count)|url: \(downloadTask.url)") 296 | } 297 | 298 | kURLTaskSemaphore.signal() 299 | } 300 | }) 301 | 302 | kURLTaskSemaphore.signal() 303 | } 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /ZLAVPlayer.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7B78F80C226607D4006FD2C4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B78F80B226607D4006FD2C4 /* AppDelegate.swift */; }; 11 | 7B78F80E226607D4006FD2C4 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B78F80D226607D4006FD2C4 /* ViewController.swift */; }; 12 | 7B78F811226607D4006FD2C4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7B78F80F226607D4006FD2C4 /* Main.storyboard */; }; 13 | 7B78F813226607D5006FD2C4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7B78F812226607D5006FD2C4 /* Assets.xcassets */; }; 14 | 7B78F816226607D5006FD2C4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7B78F814226607D5006FD2C4 /* LaunchScreen.storyboard */; }; 15 | 7B78F82222660845006FD2C4 /* VideoResourceLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B78F81E22660845006FD2C4 /* VideoResourceLoader.swift */; }; 16 | 7B78F82322660845006FD2C4 /* VideoDownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B78F81F22660845006FD2C4 /* VideoDownloadManager.swift */; }; 17 | 7B78F82422660845006FD2C4 /* VideoDownloadTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B78F82022660845006FD2C4 /* VideoDownloadTask.swift */; }; 18 | 7B78F82522660845006FD2C4 /* VideoCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B78F82122660845006FD2C4 /* VideoCacheManager.swift */; }; 19 | 7B78F828226608A2006FD2C4 /* WeakObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B78F827226608A2006FD2C4 /* WeakObject.swift */; }; 20 | 7B78F82A226608AD006FD2C4 /* URL+StreamScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B78F829226608AD006FD2C4 /* URL+StreamScheme.swift */; }; 21 | 7B78F82C2266091B006FD2C4 /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B78F82B2266091B006FD2C4 /* FileManager+Extension.swift */; }; 22 | 7B78F8302266094E006FD2C4 /* String+MD5Tool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B78F82F2266094E006FD2C4 /* String+MD5Tool.swift */; }; 23 | 7B78F83D22661A78006FD2C4 /* ZLAVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B78F83C22661A78006FD2C4 /* ZLAVPlayer.swift */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | 7B78F808226607D4006FD2C4 /* ZLAVPlayer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ZLAVPlayer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 28 | 7B78F80B226607D4006FD2C4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 29 | 7B78F80D226607D4006FD2C4 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 30 | 7B78F810226607D4006FD2C4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 31 | 7B78F812226607D5006FD2C4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 32 | 7B78F815226607D5006FD2C4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 33 | 7B78F817226607D5006FD2C4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 34 | 7B78F81E22660845006FD2C4 /* VideoResourceLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoResourceLoader.swift; sourceTree = ""; }; 35 | 7B78F81F22660845006FD2C4 /* VideoDownloadManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoDownloadManager.swift; sourceTree = ""; }; 36 | 7B78F82022660845006FD2C4 /* VideoDownloadTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoDownloadTask.swift; sourceTree = ""; }; 37 | 7B78F82122660845006FD2C4 /* VideoCacheManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCacheManager.swift; sourceTree = ""; }; 38 | 7B78F827226608A2006FD2C4 /* WeakObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakObject.swift; sourceTree = ""; }; 39 | 7B78F829226608AD006FD2C4 /* URL+StreamScheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+StreamScheme.swift"; sourceTree = ""; }; 40 | 7B78F82B2266091B006FD2C4 /* FileManager+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FileManager+Extension.swift"; sourceTree = ""; }; 41 | 7B78F82F2266094E006FD2C4 /* String+MD5Tool.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+MD5Tool.swift"; sourceTree = ""; }; 42 | 7B78F83122660A58006FD2C4 /* Bridge-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Bridge-Header.h"; sourceTree = ""; }; 43 | 7B78F83C22661A78006FD2C4 /* ZLAVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZLAVPlayer.swift; sourceTree = ""; }; 44 | /* End PBXFileReference section */ 45 | 46 | /* Begin PBXFrameworksBuildPhase section */ 47 | 7B78F805226607D4006FD2C4 /* Frameworks */ = { 48 | isa = PBXFrameworksBuildPhase; 49 | buildActionMask = 2147483647; 50 | files = ( 51 | ); 52 | runOnlyForDeploymentPostprocessing = 0; 53 | }; 54 | /* End PBXFrameworksBuildPhase section */ 55 | 56 | /* Begin PBXGroup section */ 57 | 7B78F7FF226607D4006FD2C4 = { 58 | isa = PBXGroup; 59 | children = ( 60 | 7B78F80A226607D4006FD2C4 /* ZLAVPlayer */, 61 | 7B78F809226607D4006FD2C4 /* Products */, 62 | ); 63 | sourceTree = ""; 64 | }; 65 | 7B78F809226607D4006FD2C4 /* Products */ = { 66 | isa = PBXGroup; 67 | children = ( 68 | 7B78F808226607D4006FD2C4 /* ZLAVPlayer.app */, 69 | ); 70 | name = Products; 71 | sourceTree = ""; 72 | }; 73 | 7B78F80A226607D4006FD2C4 /* ZLAVPlayer */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | 7B78F83222660B30006FD2C4 /* ZLAVPlayer */, 77 | 7B78F8262266089B006FD2C4 /* Utility */, 78 | 7B78F81D2266083A006FD2C4 /* ZLCacheLoader */, 79 | 7B78F80B226607D4006FD2C4 /* AppDelegate.swift */, 80 | 7B78F80D226607D4006FD2C4 /* ViewController.swift */, 81 | 7B78F80F226607D4006FD2C4 /* Main.storyboard */, 82 | 7B78F812226607D5006FD2C4 /* Assets.xcassets */, 83 | 7B78F814226607D5006FD2C4 /* LaunchScreen.storyboard */, 84 | 7B78F817226607D5006FD2C4 /* Info.plist */, 85 | 7B78F83122660A58006FD2C4 /* Bridge-Header.h */, 86 | ); 87 | path = ZLAVPlayer; 88 | sourceTree = ""; 89 | }; 90 | 7B78F81D2266083A006FD2C4 /* ZLCacheLoader */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 7B78F82122660845006FD2C4 /* VideoCacheManager.swift */, 94 | 7B78F81F22660845006FD2C4 /* VideoDownloadManager.swift */, 95 | 7B78F82022660845006FD2C4 /* VideoDownloadTask.swift */, 96 | 7B78F81E22660845006FD2C4 /* VideoResourceLoader.swift */, 97 | ); 98 | path = ZLCacheLoader; 99 | sourceTree = ""; 100 | }; 101 | 7B78F8262266089B006FD2C4 /* Utility */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | 7B78F82F2266094E006FD2C4 /* String+MD5Tool.swift */, 105 | 7B78F82B2266091B006FD2C4 /* FileManager+Extension.swift */, 106 | 7B78F829226608AD006FD2C4 /* URL+StreamScheme.swift */, 107 | 7B78F827226608A2006FD2C4 /* WeakObject.swift */, 108 | ); 109 | path = Utility; 110 | sourceTree = ""; 111 | }; 112 | 7B78F83222660B30006FD2C4 /* ZLAVPlayer */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | 7B78F83C22661A78006FD2C4 /* ZLAVPlayer.swift */, 116 | ); 117 | path = ZLAVPlayer; 118 | sourceTree = ""; 119 | }; 120 | /* End PBXGroup section */ 121 | 122 | /* Begin PBXNativeTarget section */ 123 | 7B78F807226607D4006FD2C4 /* ZLAVPlayer */ = { 124 | isa = PBXNativeTarget; 125 | buildConfigurationList = 7B78F81A226607D5006FD2C4 /* Build configuration list for PBXNativeTarget "ZLAVPlayer" */; 126 | buildPhases = ( 127 | 7B78F804226607D4006FD2C4 /* Sources */, 128 | 7B78F805226607D4006FD2C4 /* Frameworks */, 129 | 7B78F806226607D4006FD2C4 /* Resources */, 130 | ); 131 | buildRules = ( 132 | ); 133 | dependencies = ( 134 | ); 135 | name = ZLAVPlayer; 136 | productName = ZLAVPlayer; 137 | productReference = 7B78F808226607D4006FD2C4 /* ZLAVPlayer.app */; 138 | productType = "com.apple.product-type.application"; 139 | }; 140 | /* End PBXNativeTarget section */ 141 | 142 | /* Begin PBXProject section */ 143 | 7B78F800226607D4006FD2C4 /* Project object */ = { 144 | isa = PBXProject; 145 | attributes = { 146 | LastSwiftUpdateCheck = 1020; 147 | LastUpgradeCheck = 1020; 148 | ORGANIZATIONNAME = zl; 149 | TargetAttributes = { 150 | 7B78F807226607D4006FD2C4 = { 151 | CreatedOnToolsVersion = 10.2; 152 | }; 153 | }; 154 | }; 155 | buildConfigurationList = 7B78F803226607D4006FD2C4 /* Build configuration list for PBXProject "ZLAVPlayer" */; 156 | compatibilityVersion = "Xcode 9.3"; 157 | developmentRegion = en; 158 | hasScannedForEncodings = 0; 159 | knownRegions = ( 160 | en, 161 | Base, 162 | ); 163 | mainGroup = 7B78F7FF226607D4006FD2C4; 164 | productRefGroup = 7B78F809226607D4006FD2C4 /* Products */; 165 | projectDirPath = ""; 166 | projectRoot = ""; 167 | targets = ( 168 | 7B78F807226607D4006FD2C4 /* ZLAVPlayer */, 169 | ); 170 | }; 171 | /* End PBXProject section */ 172 | 173 | /* Begin PBXResourcesBuildPhase section */ 174 | 7B78F806226607D4006FD2C4 /* Resources */ = { 175 | isa = PBXResourcesBuildPhase; 176 | buildActionMask = 2147483647; 177 | files = ( 178 | 7B78F816226607D5006FD2C4 /* LaunchScreen.storyboard in Resources */, 179 | 7B78F813226607D5006FD2C4 /* Assets.xcassets in Resources */, 180 | 7B78F811226607D4006FD2C4 /* Main.storyboard in Resources */, 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | }; 184 | /* End PBXResourcesBuildPhase section */ 185 | 186 | /* Begin PBXSourcesBuildPhase section */ 187 | 7B78F804226607D4006FD2C4 /* Sources */ = { 188 | isa = PBXSourcesBuildPhase; 189 | buildActionMask = 2147483647; 190 | files = ( 191 | 7B78F82A226608AD006FD2C4 /* URL+StreamScheme.swift in Sources */, 192 | 7B78F828226608A2006FD2C4 /* WeakObject.swift in Sources */, 193 | 7B78F82522660845006FD2C4 /* VideoCacheManager.swift in Sources */, 194 | 7B78F80E226607D4006FD2C4 /* ViewController.swift in Sources */, 195 | 7B78F82422660845006FD2C4 /* VideoDownloadTask.swift in Sources */, 196 | 7B78F80C226607D4006FD2C4 /* AppDelegate.swift in Sources */, 197 | 7B78F8302266094E006FD2C4 /* String+MD5Tool.swift in Sources */, 198 | 7B78F82222660845006FD2C4 /* VideoResourceLoader.swift in Sources */, 199 | 7B78F82322660845006FD2C4 /* VideoDownloadManager.swift in Sources */, 200 | 7B78F83D22661A78006FD2C4 /* ZLAVPlayer.swift in Sources */, 201 | 7B78F82C2266091B006FD2C4 /* FileManager+Extension.swift in Sources */, 202 | ); 203 | runOnlyForDeploymentPostprocessing = 0; 204 | }; 205 | /* End PBXSourcesBuildPhase section */ 206 | 207 | /* Begin PBXVariantGroup section */ 208 | 7B78F80F226607D4006FD2C4 /* Main.storyboard */ = { 209 | isa = PBXVariantGroup; 210 | children = ( 211 | 7B78F810226607D4006FD2C4 /* Base */, 212 | ); 213 | name = Main.storyboard; 214 | sourceTree = ""; 215 | }; 216 | 7B78F814226607D5006FD2C4 /* LaunchScreen.storyboard */ = { 217 | isa = PBXVariantGroup; 218 | children = ( 219 | 7B78F815226607D5006FD2C4 /* Base */, 220 | ); 221 | name = LaunchScreen.storyboard; 222 | sourceTree = ""; 223 | }; 224 | /* End PBXVariantGroup section */ 225 | 226 | /* Begin XCBuildConfiguration section */ 227 | 7B78F818226607D5006FD2C4 /* Debug */ = { 228 | isa = XCBuildConfiguration; 229 | buildSettings = { 230 | ALWAYS_SEARCH_USER_PATHS = NO; 231 | CLANG_ANALYZER_NONNULL = YES; 232 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 233 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 234 | CLANG_CXX_LIBRARY = "libc++"; 235 | CLANG_ENABLE_MODULES = YES; 236 | CLANG_ENABLE_OBJC_ARC = YES; 237 | CLANG_ENABLE_OBJC_WEAK = YES; 238 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 239 | CLANG_WARN_BOOL_CONVERSION = YES; 240 | CLANG_WARN_COMMA = YES; 241 | CLANG_WARN_CONSTANT_CONVERSION = YES; 242 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 243 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 244 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 245 | CLANG_WARN_EMPTY_BODY = YES; 246 | CLANG_WARN_ENUM_CONVERSION = YES; 247 | CLANG_WARN_INFINITE_RECURSION = YES; 248 | CLANG_WARN_INT_CONVERSION = YES; 249 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 250 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 251 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 252 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 253 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 254 | CLANG_WARN_STRICT_PROTOTYPES = YES; 255 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 256 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 257 | CLANG_WARN_UNREACHABLE_CODE = YES; 258 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 259 | CODE_SIGN_IDENTITY = "iPhone Developer"; 260 | COPY_PHASE_STRIP = NO; 261 | DEBUG_INFORMATION_FORMAT = dwarf; 262 | ENABLE_STRICT_OBJC_MSGSEND = YES; 263 | ENABLE_TESTABILITY = YES; 264 | GCC_C_LANGUAGE_STANDARD = gnu11; 265 | GCC_DYNAMIC_NO_PIC = NO; 266 | GCC_NO_COMMON_BLOCKS = YES; 267 | GCC_OPTIMIZATION_LEVEL = 0; 268 | GCC_PREPROCESSOR_DEFINITIONS = ( 269 | "DEBUG=1", 270 | "$(inherited)", 271 | ); 272 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 273 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 274 | GCC_WARN_UNDECLARED_SELECTOR = YES; 275 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 276 | GCC_WARN_UNUSED_FUNCTION = YES; 277 | GCC_WARN_UNUSED_VARIABLE = YES; 278 | IPHONEOS_DEPLOYMENT_TARGET = 12.2; 279 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 280 | MTL_FAST_MATH = YES; 281 | ONLY_ACTIVE_ARCH = YES; 282 | SDKROOT = iphoneos; 283 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 284 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 285 | }; 286 | name = Debug; 287 | }; 288 | 7B78F819226607D5006FD2C4 /* Release */ = { 289 | isa = XCBuildConfiguration; 290 | buildSettings = { 291 | ALWAYS_SEARCH_USER_PATHS = NO; 292 | CLANG_ANALYZER_NONNULL = YES; 293 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 294 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 295 | CLANG_CXX_LIBRARY = "libc++"; 296 | CLANG_ENABLE_MODULES = YES; 297 | CLANG_ENABLE_OBJC_ARC = YES; 298 | CLANG_ENABLE_OBJC_WEAK = YES; 299 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 300 | CLANG_WARN_BOOL_CONVERSION = YES; 301 | CLANG_WARN_COMMA = YES; 302 | CLANG_WARN_CONSTANT_CONVERSION = YES; 303 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 304 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 305 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 306 | CLANG_WARN_EMPTY_BODY = YES; 307 | CLANG_WARN_ENUM_CONVERSION = YES; 308 | CLANG_WARN_INFINITE_RECURSION = YES; 309 | CLANG_WARN_INT_CONVERSION = YES; 310 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 311 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 312 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 313 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 314 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 315 | CLANG_WARN_STRICT_PROTOTYPES = YES; 316 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 317 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 318 | CLANG_WARN_UNREACHABLE_CODE = YES; 319 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 320 | CODE_SIGN_IDENTITY = "iPhone Developer"; 321 | COPY_PHASE_STRIP = NO; 322 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 323 | ENABLE_NS_ASSERTIONS = NO; 324 | ENABLE_STRICT_OBJC_MSGSEND = YES; 325 | GCC_C_LANGUAGE_STANDARD = gnu11; 326 | GCC_NO_COMMON_BLOCKS = YES; 327 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 328 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 329 | GCC_WARN_UNDECLARED_SELECTOR = YES; 330 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 331 | GCC_WARN_UNUSED_FUNCTION = YES; 332 | GCC_WARN_UNUSED_VARIABLE = YES; 333 | IPHONEOS_DEPLOYMENT_TARGET = 12.2; 334 | MTL_ENABLE_DEBUG_INFO = NO; 335 | MTL_FAST_MATH = YES; 336 | SDKROOT = iphoneos; 337 | SWIFT_COMPILATION_MODE = wholemodule; 338 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 339 | VALIDATE_PRODUCT = YES; 340 | }; 341 | name = Release; 342 | }; 343 | 7B78F81B226607D5006FD2C4 /* Debug */ = { 344 | isa = XCBuildConfiguration; 345 | buildSettings = { 346 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 347 | CODE_SIGN_STYLE = Automatic; 348 | DEVELOPMENT_TEAM = K6S552898A; 349 | INFOPLIST_FILE = ZLAVPlayer/Info.plist; 350 | LD_RUNPATH_SEARCH_PATHS = ( 351 | "$(inherited)", 352 | "@executable_path/Frameworks", 353 | ); 354 | PRODUCT_BUNDLE_IDENTIFIER = com.zl.ZLAVPlayer; 355 | PRODUCT_NAME = "$(TARGET_NAME)"; 356 | SWIFT_OBJC_BRIDGING_HEADER = "ZLAVPlayer/Bridge-Header.h"; 357 | SWIFT_VERSION = 4.2; 358 | TARGETED_DEVICE_FAMILY = "1,2"; 359 | }; 360 | name = Debug; 361 | }; 362 | 7B78F81C226607D5006FD2C4 /* Release */ = { 363 | isa = XCBuildConfiguration; 364 | buildSettings = { 365 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 366 | CODE_SIGN_STYLE = Automatic; 367 | DEVELOPMENT_TEAM = K6S552898A; 368 | INFOPLIST_FILE = ZLAVPlayer/Info.plist; 369 | LD_RUNPATH_SEARCH_PATHS = ( 370 | "$(inherited)", 371 | "@executable_path/Frameworks", 372 | ); 373 | PRODUCT_BUNDLE_IDENTIFIER = com.zl.ZLAVPlayer; 374 | PRODUCT_NAME = "$(TARGET_NAME)"; 375 | SWIFT_OBJC_BRIDGING_HEADER = "ZLAVPlayer/Bridge-Header.h"; 376 | SWIFT_VERSION = 4.2; 377 | TARGETED_DEVICE_FAMILY = "1,2"; 378 | }; 379 | name = Release; 380 | }; 381 | /* End XCBuildConfiguration section */ 382 | 383 | /* Begin XCConfigurationList section */ 384 | 7B78F803226607D4006FD2C4 /* Build configuration list for PBXProject "ZLAVPlayer" */ = { 385 | isa = XCConfigurationList; 386 | buildConfigurations = ( 387 | 7B78F818226607D5006FD2C4 /* Debug */, 388 | 7B78F819226607D5006FD2C4 /* Release */, 389 | ); 390 | defaultConfigurationIsVisible = 0; 391 | defaultConfigurationName = Release; 392 | }; 393 | 7B78F81A226607D5006FD2C4 /* Build configuration list for PBXNativeTarget "ZLAVPlayer" */ = { 394 | isa = XCConfigurationList; 395 | buildConfigurations = ( 396 | 7B78F81B226607D5006FD2C4 /* Debug */, 397 | 7B78F81C226607D5006FD2C4 /* Release */, 398 | ); 399 | defaultConfigurationIsVisible = 0; 400 | defaultConfigurationName = Release; 401 | }; 402 | /* End XCConfigurationList section */ 403 | }; 404 | rootObject = 7B78F800226607D4006FD2C4 /* Project object */; 405 | } 406 | -------------------------------------------------------------------------------- /ZLAVPlayer/ZLAVPlayer/ZLAVPlayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZLAVPlayer.swift 3 | // ZLAVPlayer 4 | // 5 | // Created by 赵磊 on 2019/4/16. 6 | // Copyright © 2019 zl. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Foundation 11 | import AVFoundation 12 | 13 | typealias ProgressHandler = ((_ player: ZLAVPlayer, _ progress: Double) -> Void) //视频播放进度回调 14 | typealias SeekCompleteHandler = ((_ player: ZLAVPlayer, _ prePos: Double, _ curtPos: Double) -> Void) //视频 seek 完成 15 | typealias WillSeekToPosition = ((_ player: ZLAVPlayer, _ curtPos: Double, _ toPos: Double) -> Void) //视频即将 seek 16 | typealias CompleteHandler = ((_ player: ZLAVPlayer) -> Void) //视频播放完成 17 | typealias ErrorHandler = ((_ player: ZLAVPlayer, _ error: Error?) -> Void) //播放错误 18 | typealias LoadStatusHandler = ((_ player: ZLAVPlayer, _ status: AVPlayerItem.Status) -> Void) //流媒体加载状态 19 | typealias BufferPercentHandler = ((_ player: ZLAVPlayer, _ bufferPercent: Double) -> Void) //流媒体缓冲百分比 20 | typealias BufferingHandler = ((_ player: ZLAVPlayer) -> Void) //正在缓冲 21 | typealias BufferFinishHandler = ((_ player: ZLAVPlayer) -> Void) //缓冲结束 22 | 23 | protocol ZLAVPlayerDelegate: class{ 24 | func player(_ player: ZLAVPlayer?, progress: Double) 25 | func playerWillSeek(toPosition player: ZLAVPlayer?, curtPos: Double, toPos: Double) 26 | func playerSeekComplete(_ player: ZLAVPlayer?, prePos: Double, curtPos: Double) 27 | func playerComplete(_ player: ZLAVPlayer?) 28 | func playerBuffering(_ player: ZLAVPlayer?) 29 | func playerBufferFinish(_ player: ZLAVPlayer?) 30 | func player(_ player: ZLAVPlayer?, error: Error?) 31 | func player(_ player: ZLAVPlayer?, load status: AVPlayerItem.Status) 32 | func player(_ player: ZLAVPlayer?, bufferPercent: Double) 33 | 34 | } 35 | 36 | class ZLAVPlayer: NSObject { 37 | 38 | var mediaUrl: String! //视频地址 39 | private var player: AVPlayer? //播放器实例 40 | var playerLayer: AVPlayerLayer? //视频渲染图层 41 | var playerItem: AVPlayerItem? //媒体资源 42 | private var playTimeObserverToken: Any? 43 | private var videoResourceLoader: VideoResourceLoader? //视频下载代理 44 | 45 | private var totalBuffer: Double? //缓冲长度 46 | var currentPlayTime: Double? //当前播放时间 47 | var totalTime: Double? //总时长 48 | private var curtPosition: Double? //当前播放位置 49 | private var seekToPosition: Double? //seek播放位置 50 | 51 | private var retryCount: Int = 2 //重加载次数 52 | 53 | var progressHandler: ProgressHandler? 54 | var seekCompleteHandler: SeekCompleteHandler? 55 | var willSeekToPosition: WillSeekToPosition? 56 | var completeHandler: CompleteHandler? 57 | var errorHandler: ErrorHandler? 58 | var loadStatusHandler: LoadStatusHandler? 59 | var bufferPercentHandler: BufferPercentHandler? 60 | var bufferingHandler: BufferingHandler? 61 | var bufferFinishHandler: BufferFinishHandler? 62 | weak var delegate: ZLAVPlayerDelegate? 63 | 64 | init(withURL url: String?, 65 | process:ProgressHandler?, 66 | compelete: CompleteHandler?, 67 | loadStatus: LoadStatusHandler?, 68 | bufferPercent: BufferPercentHandler?, 69 | willSeek: WillSeekToPosition?, 70 | seekComplete: SeekCompleteHandler?, 71 | buffering: BufferingHandler?, 72 | bufferFinish: BufferFinishHandler?, 73 | error: ErrorHandler?) { 74 | 75 | super.init() 76 | initPlayEnv() 77 | 78 | mediaUrl = url 79 | progressHandler = process 80 | completeHandler = compelete 81 | loadStatusHandler = loadStatus 82 | bufferPercentHandler = bufferPercent 83 | willSeekToPosition = willSeek 84 | seekCompleteHandler = seekComplete 85 | bufferingHandler = buffering 86 | bufferFinishHandler = bufferFinish 87 | errorHandler = error 88 | 89 | preparePlayer() 90 | } 91 | 92 | private func preparePlayer() { 93 | guard let url = URL(string: mediaUrl) else { 94 | return 95 | } 96 | 97 | if player != nil { 98 | removeProgressObserver() 99 | removeObserver(from: playerItem) 100 | removeNotification() 101 | } 102 | 103 | currentPlayTime = 0 104 | totalTime = 0 105 | 106 | if let filePath = VideoCacheManager.existVideoFilePath(url: url) { 107 | //本地缓存 108 | playerItem = AVPlayerItem(url: URL(fileURLWithPath: filePath)) 109 | } else { 110 | //下载 111 | let urlAssrt = AVURLAsset(url: url.streamSchemeURL) 112 | videoResourceLoader = VideoResourceLoader(originalURL: url) 113 | urlAssrt.resourceLoader.setDelegate(videoResourceLoader, queue: videoResourceLoader?.resourceLoaderQueue) 114 | playerItem = AVPlayerItem(asset: urlAssrt) 115 | } 116 | 117 | if player == nil { 118 | player = AVPlayer() 119 | playerLayer?.removeFromSuperlayer() 120 | playerLayer = AVPlayerLayer(player: player) 121 | //设置拉伸模式 122 | playerLayer?.videoGravity = .resizeAspect 123 | playerLayer?.contentsScale = UIScreen.main.scale 124 | player?.replaceCurrentItem(with: playerItem) 125 | 126 | } else { 127 | player?.replaceCurrentItem(with: playerItem) 128 | } 129 | 130 | addProgressObserver() 131 | addObserver(to: playerItem) 132 | addNotification() 133 | } 134 | 135 | deinit { 136 | removeProgressObserver() 137 | removeObserver(from: playerItem) 138 | removeNotification() 139 | } 140 | 141 | private func initPlayEnv() { 142 | //app进入后台 143 | NotificationCenter.default.addObserver(self, selector: #selector(self.appResignActive(_:)), name: UIApplication.willResignActiveNotification, object: nil) 144 | //app进入前台 145 | NotificationCenter.default.addObserver(self, selector: #selector(self.appBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil) 146 | 147 | // 监听耳机插入和拔掉通知 148 | NotificationCenter.default.addObserver(self, selector: #selector(self.audioRouteChangeListenerCallback(_:)), name: AVAudioSession.routeChangeNotification, object: nil) 149 | //中断处理(播放过程中有打电话等系统事件) 150 | NotificationCenter.default.addObserver(self, selector: #selector(self.handleInterruption(_:)), name: AVAudioSession.interruptionNotification, object: nil) 151 | do { 152 | try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback /*允许后台*/, options: .mixWithOthers /*混合播放,不独占*/) 153 | } catch { 154 | } 155 | do { 156 | try AVAudioSession.sharedInstance().setActive(true) 157 | } catch { 158 | } 159 | 160 | } 161 | } 162 | 163 | // MARK:- 播放对象的状态、播放进度监控 164 | extension ZLAVPlayer { 165 | 166 | /// ------------------------------------- 状态监控 -------------------------------------------------- 167 | /** 168 | * 给AVPlayerItem添加监控 169 | * 170 | * @param playerItem AVPlayerItem对象 171 | */ 172 | func addObserver(to playerItem: AVPlayerItem?) { 173 | if playerItem != nil { 174 | //监控播放状态 175 | playerItem?.addObserver(self, forKeyPath: "status", options: .new, context: nil) 176 | //监控网络加载情况 177 | playerItem?.addObserver(self, forKeyPath: "loadedTimeRanges", options: .new, context: nil) 178 | //正在缓冲 179 | playerItem?.addObserver(self, forKeyPath: "playbackBufferEmpty", options: .new, context: nil) 180 | //缓冲结束 181 | playerItem?.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil) 182 | } 183 | } 184 | 185 | func removeObserver(from playerItem: AVPlayerItem?) { 186 | var playerItem = playerItem 187 | if playerItem != nil { 188 | playerItem?.removeObserver(self, forKeyPath: "status") 189 | playerItem?.removeObserver(self, forKeyPath: "loadedTimeRanges") 190 | playerItem?.removeObserver(self, forKeyPath: "playbackBufferEmpty") 191 | playerItem?.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp") 192 | playerItem = nil 193 | } 194 | } 195 | 196 | /** 197 | * 通过KVO监控播放器状态 198 | * 199 | * @param keyPath 监控属性 200 | * @param object 监视器 201 | * @param change 状态改变 202 | * @param context 上下文 203 | */ 204 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 205 | guard let playerItem = object as? AVPlayerItem else { 206 | return 207 | } 208 | 209 | if keyPath == "status" { 210 | switch playerItem.status { 211 | case .unknown: 212 | print("加载状态:未知状态,此时不能播放") 213 | case .readyToPlay: 214 | totalTime = playerItem.asset.duration.seconds 215 | print("加载状态:准备完毕,可以播放,总时长\(String(describing: totalTime))") 216 | case .failed: 217 | print("加载状态:加载失败,网络或者服务器出现问题: \(String(describing: playerItem.error))") 218 | if retryCount > 0 { 219 | retryCount -= 1 220 | VideoDownloadManager.shared.removeDownload(url: URL(string: mediaUrl)!) { 221 | DispatchQueue.main.async { [weak self] in 222 | self?.preparePlayer() 223 | } 224 | } 225 | return 226 | } 227 | 228 | default: 229 | break 230 | } 231 | 232 | if let loadStatusHandler = loadStatusHandler { 233 | loadStatusHandler(self, playerItem.status) 234 | } 235 | if let delegate = delegate { 236 | delegate.player(self, load: playerItem.status) 237 | } 238 | 239 | } else if keyPath == "loadedTimeRanges" { 240 | let ranges = playerItem.loadedTimeRanges 241 | let range = ranges.first?.timeRangeValue //本次缓冲时间范围 242 | totalBuffer = range!.start.seconds + range!.duration.seconds //缓冲总长度 243 | if let bufferPercentHandler = bufferPercentHandler { 244 | bufferPercentHandler(self, totalBuffer! / playerItem.asset.duration.seconds) 245 | } 246 | if let delegate = delegate { 247 | delegate.player(self, bufferPercent: totalBuffer! / playerItem.asset.duration.seconds) 248 | } 249 | 250 | } else if keyPath == "playbackBufferEmpty" { 251 | if let bufferingHandler = bufferingHandler { 252 | bufferingHandler(self) 253 | } 254 | if let delegate = delegate { 255 | delegate.playerBuffering(self) 256 | } 257 | 258 | } else if keyPath == "playbackLikelyToKeepUp" { 259 | if let bufferFinishHandler = bufferFinishHandler { 260 | bufferFinishHandler(self) 261 | } 262 | if let delegate = delegate { 263 | delegate.playerBufferFinish(self) 264 | } 265 | 266 | } 267 | } 268 | 269 | 270 | /// ------------------------------------- 进度监控 -------------------------------------------------- 271 | /** 272 | * 给播放器添加进度更新 273 | */ 274 | func addProgressObserver() { 275 | 276 | //这里设置每秒执行一次 277 | let playerItem: AVPlayerItem? = self.playerItem 278 | 279 | let interval = CMTime(value: 1, timescale: 5) 280 | 281 | playTimeObserverToken = player?.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { [weak self] time in 282 | guard let `self` = self else { 283 | return 284 | } 285 | self.currentPlayTime = CMTimeGetSeconds(time) 286 | self.totalTime = CMTimeGetSeconds(playerItem?.duration ?? CMTime.zero) 287 | self.curtPosition = self.currentPlayTime! / self.totalTime! 288 | 289 | if let progress = self.progressHandler { 290 | progress(self, self.curtPosition ?? 0) 291 | } 292 | if let delegate = self.delegate { 293 | delegate.player(self, progress: self.curtPosition ?? 0) 294 | } 295 | }) 296 | } 297 | 298 | func removeProgressObserver() { 299 | if let observer = playTimeObserverToken { 300 | player?.removeTimeObserver(observer) 301 | } 302 | } 303 | 304 | 305 | /// ------------------------------------- 播放完成、错误通知 -------------------------------------------------- 306 | func addNotification() { 307 | //给 AVPlayerItem 添加播放完成通知 308 | NotificationCenter.default.addObserver(self, selector: #selector(self.playbackFinished(_:)), name: .AVPlayerItemDidPlayToEndTime, object: player?.currentItem) 309 | 310 | //给 AVPlayerItem 添加播放错误通知 311 | NotificationCenter.default.addObserver(self, selector: #selector(self.playbackFail(_:)), name: .AVPlayerItemFailedToPlayToEndTime, object: player?.currentItem) 312 | } 313 | 314 | func removeNotification() { 315 | NotificationCenter.default.removeObserver(self) 316 | } 317 | 318 | @objc func playbackFinished(_ notification: Notification?) { 319 | curtPosition = 0 320 | totalTime = 0 321 | currentPlayTime = totalTime 322 | if let complete = completeHandler { 323 | complete(self) 324 | } 325 | if let delegate = self.delegate { 326 | delegate.playerComplete(self) 327 | } 328 | } 329 | 330 | @objc func playbackFail(_ notification: Notification?) { 331 | curtPosition = 0 332 | totalTime = 0 333 | currentPlayTime = totalTime 334 | if let error = errorHandler { 335 | error(self, player?.error) 336 | } 337 | if let delegate = self.delegate { 338 | delegate.player(self, error: player?.error) 339 | } 340 | } 341 | } 342 | 343 | // MARK - 外部环境监控 344 | extension ZLAVPlayer { 345 | 346 | /// ------------------------------------- app进入前后台 -------------------------------------------------- 347 | @objc func appResignActive(_ notification: Notification?) { 348 | if !playFinish() { 349 | pause() 350 | } 351 | } 352 | 353 | @objc func appBecomeActive(_ notification: Notification?) { 354 | if !playFinish() { 355 | play() 356 | } 357 | } 358 | 359 | /// ------------------------------------- 中断处理 -------------------------------------------------- 360 | @objc func handleInterruption(_ notification: Notification?) { 361 | let interruptionDictionary = notification?.userInfo 362 | 363 | let type = (interruptionDictionary?[AVAudioSessionInterruptionTypeKey] as? NSNumber)?.uintValue 364 | 365 | if type == AVAudioSession.InterruptionType.ended.rawValue { 366 | if UIApplication.shared.applicationState == .active { 367 | DispatchQueue.global(qos: .default).asyncAfter(deadline: DispatchTime.now() + Double(Int64(1.0 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: { 368 | if !self.playFinish() { 369 | self.play() 370 | } 371 | }) 372 | } 373 | } else if type == AVAudioSession.InterruptionType.began.rawValue { 374 | DispatchQueue.global(qos: .default).asyncAfter(deadline: DispatchTime.now() + Double(Int64(1.0 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: { 375 | if !self.playFinish() { 376 | self.pause() 377 | } 378 | }) 379 | } 380 | } 381 | 382 | /// ------------------------------------- 耳机插拔事件 -------------------------------------------------- 383 | @objc func audioRouteChangeListenerCallback(_ notification: Notification?) { 384 | let interuptionDict = notification?.userInfo 385 | 386 | let routeChangeReason: Int = (interuptionDict?[AVAudioSessionRouteChangeReasonKey] as? NSNumber)?.intValue ?? 0 387 | 388 | switch routeChangeReason { 389 | case kAudioSessionRouteChangeReason_NewDeviceAvailable, kAudioSessionRouteChangeReason_OldDeviceUnavailable: 390 | //获取上一线路描述信息并获取上一线路的输出设备类型 391 | let previousRoute = interuptionDict?[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription 392 | let previousOutput = previousRoute?.outputs[0] 393 | let portType = previousOutput?.portType 394 | if (portType == .headphones) { 395 | DispatchQueue.global(qos: .default).asyncAfter(deadline: DispatchTime.now() + Double(Int64(1.0 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: { 396 | // 这里的处理是一秒过后,继续使用手机的扬声器播放 397 | if !self.playFinish() { 398 | self.play() 399 | } 400 | }) 401 | } 402 | case kAudioSessionRouteChangeReason_CategoryChange: 403 | break 404 | default: 405 | break 406 | } 407 | } 408 | 409 | } 410 | 411 | // MARK - 播放控制 412 | extension ZLAVPlayer { 413 | func play() { 414 | if player?.rate == 1.0 { 415 | return 416 | } 417 | player?.play() 418 | } 419 | 420 | func isPlay() -> Bool { 421 | return player?.rate == 1.0 422 | } 423 | 424 | func pause() { 425 | if player?.rate == 0.0 { 426 | return 427 | } 428 | player?.pause() 429 | } 430 | 431 | func isPause() -> Bool { 432 | return player?.rate == 0.0 433 | } 434 | 435 | func playFinish() -> Bool { 436 | return totalTime != nil && currentPlayTime == totalTime 437 | } 438 | 439 | func setSeek(to position: Double) { 440 | seekToPosition = position * totalTime! 441 | 442 | guard var seekToPosition = seekToPosition, let totalTime = totalTime else { 443 | return 444 | } 445 | 446 | if seekToPosition < 0 { 447 | seekToPosition = 0 448 | } 449 | if seekToPosition > totalTime { 450 | seekToPosition = totalTime 451 | } 452 | 453 | let time: CMTime = CMTimeMakeWithSeconds(seekToPosition, preferredTimescale: 600) 454 | 455 | //是否正在播放,YES ,则在seek完成之后恢复播放 456 | let isPlay = self.isPlay() 457 | pause() 458 | 459 | if let willSeekToPosition = willSeekToPosition { 460 | willSeekToPosition(self, curtPosition ?? 0, seekToPosition) 461 | } 462 | if let delegate = delegate { 463 | delegate.playerWillSeek(toPosition: self, curtPos: curtPosition ?? 0, toPos: seekToPosition) 464 | } 465 | 466 | player?.seek(to: time, completionHandler: { [weak self] finish in 467 | guard let `self` = self else { 468 | return 469 | } 470 | 471 | if finish { 472 | self.currentPlayTime = CMTimeGetSeconds(time) 473 | if let seekCompleteHandler = self.seekCompleteHandler { 474 | seekCompleteHandler(self, self.curtPosition ?? 0, seekToPosition) 475 | } 476 | if let delegate = self.delegate { 477 | delegate.playerSeekComplete(self, prePos: self.curtPosition ?? 0, curtPos: seekToPosition) 478 | } 479 | if isPlay { 480 | self.play() 481 | } 482 | } 483 | }) 484 | } 485 | 486 | 487 | } 488 | --------------------------------------------------------------------------------