├── 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 |
--------------------------------------------------------------------------------