├── Docs
├── shot.png
└── architecture.png
├── VideoPlayer
├── .DS_Store
├── Assets.xcassets
│ ├── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Base.lproj
│ ├── Main.storyboard
│ └── LaunchScreen.storyboard
├── Info.plist
├── ViewController.swift
├── PoVideoPlayer
│ ├── PoAVPlayerResourceLoaderDelegate.swift
│ ├── FragmentArray.swift
│ ├── PoAVPlayerResourceRequestTask.swift
│ ├── PoAVPlayerSessionDelegate.swift
│ ├── PoAVPlayerResourceCacheFileHandler.swift
│ ├── PoProgressView.swift
│ ├── PoAVPlayerResourceLoader.swift
│ ├── PoAVPlayerControlView.swift
│ ├── String+extension.swift
│ ├── PoAVPlayer.swift
│ └── PoAVPlayerCacheManager.swift
└── AppDelegate.swift
├── VideoPlayer.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcuserdata
│ │ └── huangshange.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcuserdata
│ └── huangshange.xcuserdatad
│ │ ├── xcschemes
│ │ └── xcschememanagement.plist
│ │ └── xcdebugger
│ │ └── Breakpoints_v2.xcbkptlist
└── project.pbxproj
├── LICENSE
├── README.md
└── .gitignore
/Docs/shot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZhongshanHuang/VideoPlayer/HEAD/Docs/shot.png
--------------------------------------------------------------------------------
/Docs/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZhongshanHuang/VideoPlayer/HEAD/Docs/architecture.png
--------------------------------------------------------------------------------
/VideoPlayer/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZhongshanHuang/VideoPlayer/HEAD/VideoPlayer/.DS_Store
--------------------------------------------------------------------------------
/VideoPlayer/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/VideoPlayer.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/VideoPlayer.xcodeproj/project.xcworkspace/xcuserdata/huangshange.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZhongshanHuang/VideoPlayer/HEAD/VideoPlayer.xcodeproj/project.xcworkspace/xcuserdata/huangshange.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/VideoPlayer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/VideoPlayer.xcodeproj/xcuserdata/huangshange.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | VideoPlayer.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 ZhongshanHuang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/VideoPlayer/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 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/VideoPlayer/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 |
--------------------------------------------------------------------------------
/VideoPlayer/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # VideoPlayer
2 | This is a tool that can cache the fragmented data while playing remote video.
3 |
4 | # Environment
5 | xcode: 10.2 beta4
6 | swift: 5.0
7 |
8 |
9 |
10 |
11 |
12 | # Usage
13 |
14 | ``` swift
15 | func playerTest() {
16 | let player = PoAVPlayer(frame: view.bounds)
17 | player.autoresizingMask = [.flexibleWidth, .flexibleHeight]
18 | let control = PoAVPlayerControlView(player: player) // 控制层,可自定义替换
19 | player.addControlLayer(control)
20 | view.addSubview(player)
21 |
22 | let urlArray = ["http://www.w3school.com.cn/example/html5/mov_bbb.mp4",
23 | "https://www.w3schools.com/html/movie.mp4",
24 | "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4",
25 | "https://media.w3.org/2010/05/sintel/trailer.mp4",
26 | "http://mvvideo2.meitudata.com/576bc2fc91ef22121.mp4",
27 | "http://mvvideo10.meitudata.com/5a92ee2fa975d9739_H264_3.mp4",
28 | "http://mvvideo11.meitudata.com/5a44d13c362a23002_H264_11_5.mp4",
29 | "http://mvvideo10.meitudata.com/572ff691113842657.mp4",
30 | "https://api.tuwan.com/apps/Video/play?key=aHR0cHM6Ly92LnFxLmNvbS9pZnJhbWUvcGxheWVyLmh0bWw%2FdmlkPXUwNjk3MmtqNWV6JnRpbnk9MCZhdXRvPTA%3D&aid=381374",
31 | "https://api.tuwan.com/apps/Video/play?key=aHR0cHM6Ly92LnFxLmNvbS9pZnJhbWUvcGxheWVyLmh0bWw%2FdmlkPWswNjk2enBud2xvJnRpbnk9MCZhdXRvPTA%3D&aid=381395"
32 | ]
33 |
34 | player.play(with: URL(string:urlArray.randomElement()!)!, needCache: true)
35 | }
36 | ```
37 |
--------------------------------------------------------------------------------
/VideoPlayer/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 | }
--------------------------------------------------------------------------------
/VideoPlayer/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // VideoPlayer
4 | //
5 | // Created by 黄山哥 on 2019/1/16.
6 | // Copyright © 2019 黄山哥. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class ViewController: UIViewController {
12 |
13 | override func viewDidLoad() {
14 | super.viewDidLoad()
15 | // Do any additional setup after loading the view, typically from a nib.
16 | playerTest()
17 | }
18 |
19 | // avplayer Test
20 | func playerTest() {
21 | PoAVPlayerCacheManager.shared.deleteAllFiles()
22 |
23 | // avplayer是显示层
24 | let player = PoAVPlayer(frame: view.bounds)
25 | player.autoresizingMask = [.flexibleWidth, .flexibleHeight]
26 |
27 | // controlView是播放的交互控制层,可自定义
28 | let control = PoAVPlayerControlView(player: player)
29 | player.addControlLayer(control)
30 |
31 | view.addSubview(player)
32 |
33 | // let urlArray = ["http://www.w3school.com.cn/example/html5/mov_bbb.mp4",
34 | // "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4",
35 | // "https://media.w3.org/2010/05/sintel/trailer.mp4",
36 | // "http://mvvideo2.meitudata.com/576bc2fc91ef22121.mp4",
37 | // "http://mvvideo10.meitudata.com/5a92ee2fa975d9739_H264_3.mp4",
38 | // "http://mvvideo11.meitudata.com/5a44d13c362a23002_H264_11_5.mp4",
39 | // "http://mvvideo10.meitudata.com/572ff691113842657.mp4", "https://api.tuwan.com/apps/Video/play?key=aHR0cHM6Ly92LnFxLmNvbS9pZnJhbWUvcGxheWVyLmh0bWw%2FdmlkPWswNjk2enBud2xvJnRpbnk9MCZhdXRvPTA%3D&aid=381395"
40 | // ]
41 | //
42 | // player.play(with: URL(string:urlArray.randomElement()!)!, needCache: true)
43 |
44 | player.play(with: URL(string:"https://api.tuwan.com/apps/Video/play?key=aHR0cHM6Ly92LnFxLmNvbS9pZnJhbWUvcGxheWVyLmh0bWw%2FdmlkPWswNjk2enBud2xvJnRpbnk9MCZhdXRvPTA%3D&aid=381395")!, needCache: true)
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/VideoPlayer/PoVideoPlayer/PoAVPlayerResourceLoaderDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PoAVPlayerResourceLoaderManager.swift
3 | // VideoPlayer
4 | //
5 | // Created by 黄山哥 on 2019/1/21.
6 | // Copyright © 2019 黄山哥. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AVFoundation
11 |
12 | class PoAVPlayerResourceLoaderDelegate: NSObject {}
13 |
14 | private var pResourceLoaderKey: Void?
15 |
16 | extension AVAssetResourceLoader {
17 | var pResourceLoader: PoAVPlayerResourceLoader? {
18 | get {
19 | return objc_getAssociatedObject(self, &pResourceLoaderKey) as? PoAVPlayerResourceLoader
20 | }
21 | set {
22 | objc_setAssociatedObject(self, &pResourceLoaderKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
23 | }
24 | }
25 | }
26 |
27 |
28 |
29 | // MARK: - PoAVPlayerResourceLoaderManager
30 |
31 | extension PoAVPlayerResourceLoaderDelegate: AVAssetResourceLoaderDelegate {
32 |
33 | /// avasset遇到系统无法处理的url时会调用此方法
34 | func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
35 | if resourceLoader.pResourceLoader != nil {
36 | resourceLoader.pResourceLoader?.appending(request: loadingRequest)
37 | return true
38 | } else if let url = loadingRequest.request.url, url.absoluteString.hasPrefix(PoAVPlayer.scheme) {
39 | let urlStr = url.absoluteString[PoAVPlayer.scheme.endIndex...]
40 | guard let originalUrl = URL(string: String(urlStr)) else { return false }
41 | let loader = PoAVPlayerResourceLoader(resourceIdentifier: originalUrl)
42 | loader.appending(request: loadingRequest)
43 | resourceLoader.pResourceLoader = loader
44 | return true
45 | }
46 | return false
47 | }
48 |
49 | /// 当播放跳转到别的时间时会调用此方法
50 | func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) {
51 | resourceLoader.pResourceLoader?.cancel(loadingRequest)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.gitignore.io/api/swift
2 | # Edit at https://www.gitignore.io/?templates=swift
3 |
4 | ### Swift ###
5 | # Xcode
6 | #
7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
8 |
9 | ## Build generated
10 | build/
11 | DerivedData/
12 |
13 | ## Various settings
14 | *.pbxuser
15 | !default.pbxuser
16 | *.mode1v3
17 | !default.mode1v3
18 | *.mode2v3
19 | !default.mode2v3
20 | *.perspectivev3
21 | !default.perspectivev3
22 | xcuserdata/
23 |
24 | ## Other
25 | *.moved-aside
26 | *.xccheckout
27 | *.xcscmblueprint
28 |
29 | ## Obj-C/Swift specific
30 | *.hmap
31 | *.ipa
32 | *.dSYM.zip
33 | *.dSYM
34 |
35 | ## Playgrounds
36 | timeline.xctimeline
37 | playground.xcworkspace
38 |
39 | # Swift Package Manager
40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
41 | Packages/
42 | Package.pins
43 | Package.resolved
44 | .build/
45 | # Add this line if you want to avoid checking in Xcode SPM integration.
46 | .swiftpm/xcode
47 |
48 | # CocoaPods
49 | # We recommend against adding the Pods directory to your .gitignore. However
50 | # you should judge for yourself, the pros and cons are mentioned at:
51 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
52 | Pods
53 | !Podfile
54 | !Podfile.lock
55 | # Add this line if you want to avoid checking in source code from the Xcode workspace
56 | *.xcworkspace
57 | *.xccheckout
58 | !default.xcworkspace
59 |
60 | # Carthage
61 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
62 | # Carthage/Checkouts
63 |
64 | Carthage/Build
65 |
66 | # Accio dependency management
67 | Dependencies/
68 | .accio/
69 |
70 | # fastlane
71 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
72 | # screenshots whenever they are needed.
73 | # For more information about the recommended setup visit:
74 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
75 |
76 | fastlane/report.xml
77 | fastlane/Preview.html
78 | fastlane/screenshots/**/*.png
79 | fastlane/test_output
80 |
81 | # Code Injection
82 | # After new code Injection tools there's a generated folder /iOSInjectionProject
83 | # https://github.com/johnno1962/injectionforxcode
84 |
85 | iOSInjectionProject/
86 |
87 | # End of https://www.gitignore.io/api/swift
--------------------------------------------------------------------------------
/VideoPlayer/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // VideoPlayer
4 | //
5 | // Created by 黄山哥 on 2019/1/16.
6 | // Copyright © 2019 黄山哥. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 | var allowRotation: Bool = false
16 |
17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
18 | // Override point for customization after application launch.
19 |
20 | window = UIWindow(frame: UIScreen.main.bounds)
21 | window?.backgroundColor = UIColor.white
22 |
23 | window?.rootViewController = ViewController()
24 | window?.makeKeyAndVisible()
25 |
26 | return true
27 | }
28 |
29 | func applicationWillResignActive(_ application: UIApplication) {
30 | // 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.
31 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
32 | }
33 |
34 | func applicationDidEnterBackground(_ application: UIApplication) {
35 | // 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.
36 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
37 | }
38 |
39 | func applicationWillEnterForeground(_ application: UIApplication) {
40 | // 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.
41 | }
42 |
43 | func applicationDidBecomeActive(_ application: UIApplication) {
44 | // 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.
45 | }
46 |
47 | func applicationWillTerminate(_ application: UIApplication) {
48 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
49 | }
50 |
51 |
52 | func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask
53 | {
54 | if allowRotation {
55 | return .landscapeRight
56 | } else {
57 | return .portrait
58 | }
59 | }
60 |
61 | }
62 |
63 | extension AppDelegate {
64 |
65 | func setNewOrientation(rotate: Bool) {
66 | if rotate {
67 | let n1 = NSNumber(value: UIInterfaceOrientation.landscapeRight.rawValue)
68 | UIDevice.current.setValue(n1, forKey: "orientation")
69 | } else {
70 | let n1 = NSNumber(value: UIInterfaceOrientation.portrait.rawValue)
71 | UIDevice.current.setValue(n1, forKey: "orientation")
72 | }
73 |
74 | }
75 |
76 | }
77 |
78 |
--------------------------------------------------------------------------------
/VideoPlayer/PoVideoPlayer/FragmentArray.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FragmentArray.swift
3 | // VideoPlayer
4 | //
5 | // Created by 黄山哥 on 2019/1/31.
6 | // Copyright © 2019 黄山哥. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | struct FragmentArray {
13 |
14 | private var storage: [NSRange]
15 |
16 | init() {
17 | self.storage = []
18 | }
19 |
20 | init(_ array: [NSRange]) {
21 | self.storage = array
22 | }
23 | }
24 |
25 | extension FragmentArray {
26 |
27 | /// 插入数据
28 | mutating func insert(_ newElement: NSRange) {
29 | let index = self.index(for: newElement)
30 | if index < count && storage[index] == newElement {
31 | return
32 | }
33 | // 合并相连的fragment
34 | if index >= 1 && storage[index - 1].upperBound == newElement.location {
35 | storage[index - 1].length += newElement.length
36 | if index < count && storage[index].location == newElement.upperBound {
37 | storage[index - 1].length += storage[index].length
38 | storage.remove(at: index)
39 | }
40 | } else if index < count && storage[index].location == newElement.upperBound {
41 | storage[index - 1].length += storage[index].length
42 | storage.remove(at: index)
43 | } else {
44 | storage.insert(newElement, at: index)
45 | }
46 | }
47 |
48 | /// 查找element的索引
49 | func index(of element: NSRange) -> Int? {
50 | let index = self.index(for: element)
51 | guard index < count, storage[index] == element else { return nil }
52 | return index
53 | }
54 |
55 | /// 是否包含element
56 | func contains(_ element: NSRange) -> Bool {
57 | let index = self.index(for: element)
58 | return index < count && storage[index] == element
59 | }
60 |
61 | /// 对每个element执行body
62 | func forEach(_ body: (NSRange) throws -> Void) rethrows {
63 | try storage.forEach(body)
64 | }
65 |
66 | /// 返回一个有序数组
67 | func sorted() -> [NSRange] {
68 | return storage
69 | }
70 |
71 | private func index(for element: NSRange) -> Int {
72 | var start = 0
73 | var end = count
74 |
75 | while start < end {
76 | let mid = start + (end - start) / 2
77 | if element > storage[mid] {
78 | start = mid + 1
79 | } else {
80 | end = mid
81 | }
82 | }
83 | return start
84 | }
85 | }
86 |
87 |
88 | // MARK: - CustomStringConvertible
89 | extension FragmentArray: CustomStringConvertible {
90 |
91 | var description: String {
92 | let contents = self.lazy.map({"\($0)"}).joined(separator: ",")
93 | return "[\(contents)]"
94 | }
95 |
96 | }
97 |
98 | // MARK: - RandomAccessCollection
99 | extension FragmentArray: RandomAccessCollection {
100 | typealias Indices = CountableRange
101 |
102 | var startIndex: Int { return storage.startIndex }
103 | var endIndex: Int { return storage.endIndex }
104 |
105 | subscript(index: Int) -> NSRange { return storage[index] }
106 | }
107 |
108 |
109 | // MARK: - NSRange
110 |
111 | extension NSRange: Comparable {
112 |
113 | public static func < (lhs: _NSRange, rhs: _NSRange) -> Bool {
114 | return lhs.location < rhs.location
115 | }
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/VideoPlayer/PoVideoPlayer/PoAVPlayerResourceRequestTask.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PoAVPlayerResourceRequestTask.swift
3 | // VideoPlayer
4 | //
5 | // Created by 黄山哥 on 2019/1/22.
6 | // Copyright © 2019 黄山哥. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import AVFoundation
11 |
12 | protocol PoAVPlayerResourceRequestTaskDelegate: class {
13 |
14 | func requestTask(_ task: PoAVPlayerResourceRequestTask, didReceiveResponse response: URLResponse)
15 |
16 | func requestTask(_ task: PoAVPlayerResourceRequestTask, didReceiveData data: Data)
17 |
18 | func requestTask(_ task: PoAVPlayerResourceRequestTask, didCompleteWithError error: Error?)
19 | }
20 |
21 | // MARK: - PoAVPlayerResourceRequestTask
22 |
23 | class PoAVPlayerResourceRequestTask: NSObject {
24 |
25 | weak var delegate: PoAVPlayerResourceRequestTaskDelegate?
26 | let requestRange: NSRange
27 | var currentOffset: Int
28 |
29 | var isCached: Bool = false
30 | var isExecuting: Bool = false
31 | var isFinished: Bool = false
32 | var isCancelled: Bool = false
33 |
34 | init(requestRange: NSRange) {
35 | self.requestRange = requestRange
36 | self.currentOffset = requestRange.location
37 | }
38 |
39 | func start() {
40 | isExecuting = true
41 | }
42 |
43 | func cancel() {
44 | isExecuting = false
45 | isCancelled = true
46 | }
47 | }
48 |
49 |
50 | // MARK: - PoAVPlayerResourceRequestLocalTask
51 | private let kBufferSize = 1024 * 64
52 | class PoAVPlayerResourceRequestLocalTask: PoAVPlayerResourceRequestTask {
53 |
54 | // MARK: - Properties
55 | unowned let fileHandler: PoAVPlayerResourceCacheFileHandler
56 |
57 | init(fileHandler: PoAVPlayerResourceCacheFileHandler, requestRange: NSRange) {
58 | self.fileHandler = fileHandler
59 | super.init(requestRange: requestRange)
60 | }
61 |
62 |
63 | override func start() {
64 | super.start()
65 | DispatchQueue.global().async {
66 | self.loadLocalData()
67 | }
68 | }
69 |
70 | private func loadLocalData() {
71 | if isCancelled {
72 | isFinished = true;
73 | let error = NSError(domain: "local task", code: -1, userInfo: [NSLocalizedDescriptionKey: "local task cacelled"])
74 | self.delegate?.requestTask(self, didCompleteWithError: error)
75 | }
76 |
77 | self.delegate?.requestTask(self, didReceiveResponse: fileHandler.response)
78 | let upperBound = self.requestRange.upperBound
79 |
80 | while currentOffset < upperBound {
81 | if isCancelled { break }
82 | autoreleasepool { () -> Void in
83 | let length = min(upperBound - currentOffset, kBufferSize)
84 | let data = self.fileHandler.readData(offset: currentOffset, length: length)
85 | self.delegate?.requestTask(self, didReceiveData: data)
86 | currentOffset += length
87 | }
88 | }
89 |
90 | if isCancelled {
91 | let error = NSError(domain: "local task", code: -1, userInfo: [NSLocalizedDescriptionKey: "local task cacelled"])
92 | self.delegate?.requestTask(self, didCompleteWithError: error)
93 | } else {
94 | self.delegate?.requestTask(self, didCompleteWithError: nil)
95 | }
96 | isFinished = true
97 | }
98 |
99 | }
100 |
101 |
102 | // MARK: - PoAVPlayerResourceRequestRemoteTask
103 |
104 | class PoAVPlayerResourceRequestRemoteTask: PoAVPlayerResourceRequestTask {
105 |
106 | // MARK: - Properties
107 | let task: URLSessionDataTask
108 |
109 | init(task: URLSessionDataTask, requestRange: NSRange) {
110 | self.task = task
111 | super.init(requestRange: requestRange)
112 | }
113 |
114 | override func start() {
115 | super.start()
116 | task.resume()
117 | }
118 |
119 | override func cancel() {
120 | if isCancelled || isFinished { return }
121 | super.cancel()
122 | task.cancel()
123 | }
124 | }
125 |
126 |
--------------------------------------------------------------------------------
/VideoPlayer/PoVideoPlayer/PoAVPlayerSessionDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PoAVPlayerSessionDelegate.swift
3 | // VideoPlayer
4 | //
5 | // Created by 黄山哥 on 2019/1/23.
6 | // Copyright © 2019 黄山哥. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | class PoAVPlayerSessionDelegate: NSObject {
13 |
14 | // MARK: - Properties
15 |
16 | private var tasks: [URLSessionTask: PoAVPlayerResourceRequestRemoteTask] = [:]
17 | private lazy var session: URLSession = {
18 | let configuration = URLSessionConfiguration.default
19 | configuration.timeoutIntervalForRequest = 15
20 | let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
21 | return session
22 | }()
23 |
24 |
25 | // MARK: - Convenience Initializator
26 |
27 | func remoteDataTask(with url: URL, requestRange: NSRange) -> PoAVPlayerResourceRequestRemoteTask {
28 | var urlRequest = URLRequest(url: url)
29 | urlRequest.httpShouldUsePipelining = true
30 | urlRequest.cachePolicy = .reloadIgnoringLocalCacheData
31 | urlRequest.setValue(correctRange(requestRange), forHTTPHeaderField: "Range")
32 | let dataTask = session.dataTask(with: urlRequest)
33 | let remoteTask = PoAVPlayerResourceRequestRemoteTask(task: dataTask, requestRange: requestRange)
34 | tasks[dataTask] = remoteTask
35 | return remoteTask
36 | }
37 |
38 | // MARK: - Helper
39 |
40 | private func correctRange(_ range: NSRange) -> String? {
41 | guard range.location != NSNotFound || range.length > 0 else { return nil }
42 |
43 | if range.location == NSNotFound {
44 | return "bytes=-\(range.length)"
45 | } else if range.length == .max {
46 | return "bytes=\(range.location)-"
47 | } else {
48 | return "bytes=\(range.location)-\(range.upperBound - 1)"
49 | }
50 | }
51 |
52 | private func task(for task: URLSessionTask) -> PoAVPlayerResourceRequestRemoteTask? {
53 | guard let remoteTask = tasks[task] else { return nil }
54 | guard remoteTask.task.taskIdentifier == task.taskIdentifier else { return nil }
55 | return remoteTask
56 | }
57 |
58 | private func remove(task: URLSessionTask) {
59 | tasks.removeValue(forKey: task)
60 | }
61 | }
62 |
63 |
64 | extension PoAVPlayerSessionDelegate: URLSessionDataDelegate {
65 |
66 | func urlSession(_ session: URLSession,
67 | didReceive challenge: URLAuthenticationChallenge,
68 | completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
69 | if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
70 | let card = URLCredential(trust: challenge.protectionSpace.serverTrust!)
71 | completionHandler(.useCredential, card)
72 | } else {
73 | completionHandler(.performDefaultHandling, nil)
74 | }
75 | }
76 |
77 | func urlSession(_ session: URLSession,
78 | dataTask: URLSessionDataTask,
79 | didReceive response: URLResponse,
80 | completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
81 | guard let task = task(for: dataTask), let response = response as? HTTPURLResponse else {
82 | completionHandler(.cancel)
83 | return
84 | }
85 | if response.statusCode < 400 && response.statusCode != 304 {
86 | if response.mimeType?.contains("video") == true || response.mimeType?.contains("audio") == true {
87 | task.delegate?.requestTask(task, didReceiveResponse: response)
88 | }
89 | completionHandler(.allow)
90 | } else {
91 | completionHandler(.cancel)
92 | }
93 | }
94 |
95 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
96 | guard let task = task(for: dataTask) else { return }
97 |
98 | task.delegate?.requestTask(task, didReceiveData: data)
99 | task.currentOffset += data.count
100 | }
101 |
102 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
103 | if let task = self.task(for: task) {
104 | task.delegate?.requestTask(task, didCompleteWithError: error)
105 | }
106 | remove(task: task)
107 | }
108 |
109 | }
110 |
111 |
--------------------------------------------------------------------------------
/VideoPlayer.xcodeproj/xcuserdata/huangshange.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
8 |
20 |
21 |
22 |
24 |
36 |
37 |
38 |
40 |
52 |
53 |
54 |
56 |
68 |
69 |
70 |
72 |
84 |
85 |
86 |
88 |
101 |
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/VideoPlayer/PoVideoPlayer/PoAVPlayerResourceCacheFileHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PoAVPlayerResourceCacheFileHandler.swift
3 | // VideoPlayer
4 | //
5 | // Created by 黄山哥 on 2019/1/31.
6 | // Copyright © 2019 黄山哥. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class PoAVPlayerResourceCacheFileHandler {
12 |
13 | let indexFilePath: URL
14 | let dataFilePath: String
15 | let response: URLResponse
16 | var cacheInfo: CacheInfo!
17 | private lazy var writeHandle: FileHandle = FileHandle(forWritingAtPath: self.dataFilePath)!
18 | private lazy var readHandle: FileHandle = FileHandle(forReadingAtPath: self.dataFilePath)!
19 |
20 |
21 | init(resourceIdentifier: URL) {
22 | let key = resourceIdentifier.absoluteString
23 |
24 | self.indexFilePath = PoAVPlayerCacheManager.indexFilePathCreateIfNotExist(for: key)
25 | self.dataFilePath = PoAVPlayerCacheManager.dataFilePathCreateIfNotExist(for: key)
26 | do {
27 | let data = try Data(contentsOf: indexFilePath)
28 | cacheInfo = try JSONDecoder().decode(CacheInfo.self, from: data)
29 | } catch (let error) {
30 | fatalError("CacheInfo decode fail: \(error.localizedDescription)")
31 | }
32 | response = HTTPURLResponse(url: resourceIdentifier, mimeType: cacheInfo.mimeType, expectedContentLength: cacheInfo.expectedLength, textEncodingName: nil)
33 | debugPrint(dataFilePath)
34 | }
35 |
36 | deinit {
37 | writeHandle.closeFile()
38 | readHandle.closeFile()
39 | }
40 |
41 | // MARK: - Data
42 |
43 | /// save data
44 | func saveData(_ data: Data, at fileOffset: UInt64) {
45 | PoAVPlayerCacheManager.ioQueue.async {
46 | self.writeHandle.seek(toFileOffset: fileOffset)
47 | self.writeHandle.write(data)
48 | }
49 | }
50 |
51 | /// read data
52 | func readData(offset: Int, length: Int) -> Data {
53 | readHandle.seek(toFileOffset: UInt64(offset))
54 | return readHandle.readData(ofLength: length)
55 | }
56 |
57 | // MARK: - Fragment
58 |
59 | func saveFragment(_ range: NSRange) {
60 | cacheInfo.fragments.insert(range)
61 | }
62 |
63 | /// cached fragment
64 | func firstCachedFragment(in range: NSRange) -> NSRange? {
65 | for fragment in cacheInfo.fragments {
66 | if let intersection = fragment.intersection(range) {
67 | return intersection
68 | }
69 | }
70 | return nil
71 | }
72 |
73 | // MARK: - Synchronize
74 |
75 | func synchronize() {
76 | PoAVPlayerCacheManager.ioQueue.async {
77 | do {
78 | let data = try JSONEncoder().encode(self.cacheInfo)
79 | try data.write(to: self.indexFilePath)
80 | } catch (let error) {
81 | fatalError("CacheInfo encode fail: \(error.localizedDescription)")
82 | }
83 | }
84 | }
85 | }
86 |
87 |
88 | // MARK: - CacheInfo
89 |
90 | extension PoAVPlayerResourceCacheFileHandler {
91 |
92 | struct CacheInfo: Codable {
93 | var expectedLength: Int
94 | var mimeType: String?
95 | var fragments: FragmentArray
96 |
97 | init(expectedLength: Int, mimeType: String?, fragments: [NSRange]) {
98 | self.expectedLength = expectedLength
99 | self.mimeType = mimeType
100 | self.fragments = FragmentArray(fragments)
101 | }
102 |
103 | enum CodingKeys: String, CodingKey {
104 | case expectedLength
105 | case mimeType
106 | case fragments
107 | }
108 |
109 | init(from decoder: Decoder) throws {
110 | let container = try decoder.container(keyedBy: CodingKeys.self)
111 | let expectedLength = try container.decode(Int.self, forKey: .expectedLength)
112 | let mimeType = try container.decode(Optional.self, forKey: .mimeType)
113 | var unkeyedContainer = try container.nestedUnkeyedContainer(forKey: .fragments)
114 |
115 | var fragments = [NSRange]()
116 | while !unkeyedContainer.isAtEnd {
117 | let range = try unkeyedContainer.decode(NSRange.self)
118 | fragments.append(range)
119 | }
120 | self.init(expectedLength: expectedLength, mimeType: mimeType, fragments: fragments)
121 | }
122 |
123 | func encode(to encoder: Encoder) throws {
124 | var container = encoder.container(keyedBy: CodingKeys.self)
125 | try container.encode(expectedLength, forKey: .expectedLength)
126 | try container.encode(mimeType, forKey: .mimeType)
127 | var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .fragments)
128 | try fragments.forEach { (range) in
129 | try unkeyedContainer.encode(range)
130 | }
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/VideoPlayer/PoVideoPlayer/PoProgressView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PoProgressView.swift
3 | // VideoPlayer
4 | //
5 | // Created by 黄山哥 on 2019/3/6.
6 | // Copyright © 2019 黄山哥. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class PoProgressView: UIControl {
12 |
13 | private var _progressValue: Float = 0
14 | var progressValue: Float {
15 | get { return _progressValue }
16 | set {
17 | if newValue.isNaN { return }
18 | if abs(newValue - _progressValue) < 0.01 { return }
19 | if newValue < 0 {
20 | if _progressValue == 0 { return }
21 | _progressValue = 0
22 | } else if newValue > 1 {
23 | if _progressValue == 1 { return }
24 | _progressValue = 1
25 | } else {
26 | _progressValue = newValue
27 | }
28 | setNeedsDisplay()
29 | }
30 | }
31 |
32 | private var _sliderValue: Float = 0
33 | var sliderValue: Float {
34 | get { return _sliderValue }
35 | set {
36 | if newValue.isNaN { return }
37 | if abs(newValue - _sliderValue) < 0.01 { return }
38 | if newValue < 0 {
39 | _sliderValue = 0
40 | } else if newValue > 1 {
41 | _sliderValue = 1
42 | } else {
43 | _sliderValue = newValue
44 | }
45 | setNeedsDisplay()
46 | }
47 | }
48 |
49 | var backgroundTintColor: UIColor = UIColor.darkGray {
50 | didSet { setNeedsDisplay() }
51 | }
52 | var progressTintColor: UIColor = UIColor.white {
53 | didSet { setNeedsDisplay() }
54 | }
55 | var sliderTintColor: UIColor = UIColor.red {
56 | didSet { setNeedsDisplay() }
57 | }
58 | var thumbTintColor: UIColor = UIColor.white {
59 | didSet { setNeedsDisplay() }
60 | }
61 | private var _thumbRect: CGRect = .zero
62 | private var _thumbRadius: CGFloat = 8
63 |
64 | var isContinuous: Bool = true
65 | var isTouching: Bool = false
66 |
67 | override init(frame: CGRect) {
68 | super.init(frame: frame)
69 | setUp()
70 | }
71 |
72 | private func setUp() {
73 | backgroundColor = UIColor.clear
74 | }
75 |
76 | required init?(coder aDecoder: NSCoder) {
77 | fatalError("init(coder:) has not been implemented")
78 | }
79 |
80 | override func draw(_ rect: CGRect) {
81 | guard let context = UIGraphicsGetCurrentContext() else { return }
82 |
83 | let lineWidth: CGFloat = 1
84 | let width = bounds.width - _thumbRadius * 2
85 | let centerY = bounds.height / 2
86 |
87 | let value = CGFloat(1 - max(progressValue, sliderValue)) * width
88 | if value >= 1 {
89 | context.saveGState()
90 | defer { context.restoreGState() }
91 |
92 | let startPoint = CGPoint(x: CGFloat(max(progressValue, sliderValue)) * width + _thumbRadius, y: centerY)
93 | let endPoint = CGPoint(x: width + _thumbRadius, y: centerY)
94 | context.setStrokeColor(backgroundTintColor.cgColor)
95 | context.move(to: startPoint)
96 | context.addLine(to: endPoint)
97 | context.setLineWidth(lineWidth)
98 | context.strokePath()
99 | }
100 |
101 | if sliderValue < progressValue {
102 | context.saveGState()
103 | defer { context.restoreGState() }
104 |
105 | let startPoint = CGPoint(x: CGFloat(sliderValue) * width + _thumbRadius, y: centerY)
106 | let endPoint = CGPoint(x: CGFloat(progressValue) * width + _thumbRadius, y: centerY)
107 | context.setStrokeColor(progressTintColor.cgColor)
108 | context.move(to: startPoint)
109 | context.addLine(to: endPoint)
110 | context.setLineWidth(lineWidth)
111 | context.strokePath()
112 | }
113 |
114 | if sliderValue > 0 {
115 | context.saveGState()
116 | defer { context.restoreGState() }
117 |
118 | let startPoint = CGPoint(x: _thumbRadius, y: centerY)
119 | let endPoint = CGPoint(x: CGFloat(sliderValue) * width + _thumbRadius, y: centerY)
120 | context.setStrokeColor(sliderTintColor.cgColor)
121 | context.move(to: startPoint)
122 | context.addLine(to: endPoint)
123 | context.setLineWidth(lineWidth)
124 | context.strokePath()
125 | }
126 |
127 | context.saveGState()
128 | defer { context.restoreGState() }
129 |
130 | let sliderX = CGFloat(sliderValue) * width
131 | let radius = isTouching ? _thumbRadius * 1.3 : _thumbRadius
132 | _thumbRect = CGRect(x: sliderX, y: centerY - radius, width: radius * 2, height: radius * 2)
133 | context.addEllipse(in: _thumbRect)
134 | context.setFillColor(thumbTintColor.cgColor)
135 | context.fillPath()
136 | context.setStrokeColor(UIColor.gray.cgColor)
137 | context.setLineWidth(1)
138 | context.strokePath()
139 | }
140 | }
141 |
142 | extension PoProgressView {
143 |
144 | override func touchesBegan(_ touches: Set, with event: UIEvent?) {
145 | let point = touches.first!.location(in: self)
146 | if _thumbRect.contains(point) {
147 | isTouching = true
148 | setNeedsDisplay()
149 | } else {
150 | let point = touches.first!.location(in: self)
151 | let value = point.x
152 | let newValue = Float(value / (bounds.width - _thumbRadius * 2))
153 | if abs(newValue - _sliderValue) < 0.01 { return }
154 | sliderValue = newValue
155 | sendActions(for: .valueChanged)
156 | }
157 | }
158 |
159 | override func touchesMoved(_ touches: Set, with event: UIEvent?) {
160 | if isTouching {
161 | let point = touches.first!.location(in: self)
162 | let value = point.x - _thumbRadius
163 | let newValue = Float(value / (bounds.width - _thumbRadius * 2))
164 | if newValue < -0.01 || newValue > 1.01 { return }
165 | if abs(newValue - _sliderValue) < 0.01 { return }
166 | sliderValue = newValue
167 | if isContinuous {
168 | sendActions(for: .valueChanged)
169 | }
170 | }
171 | }
172 |
173 | override func touchesEnded(_ touches: Set, with event: UIEvent?) {
174 | if isTouching {
175 | if !isContinuous {
176 | sendActions(for: .valueChanged)
177 | }
178 | isTouching = false
179 | setNeedsDisplay()
180 | }
181 | }
182 |
183 | override func touchesCancelled(_ touches: Set, with event: UIEvent?) {
184 | if isTouching {
185 | isTouching = false
186 | setNeedsDisplay()
187 | }
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/VideoPlayer/PoVideoPlayer/PoAVPlayerResourceLoader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PoAVPlayerResourceLoader.swift
3 | // VideoPlayer
4 | //
5 | // Created by 黄山哥 on 2019/1/21.
6 | // Copyright © 2019 黄山哥. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AVFoundation
11 | import MobileCoreServices.UTType
12 |
13 | class PoAVPlayerResourceLoader {
14 |
15 | // MARK: - Properties
16 |
17 | let resourceIdentifier: URL
18 | private var requests: [AVAssetResourceLoadingRequest] = []
19 | var isEmpty: Bool {
20 | lock.wait()
21 | let result = requests.isEmpty
22 | lock.signal()
23 | return result
24 | }
25 |
26 | var requestCount: Int {
27 | lock.wait()
28 | let count = requests.count
29 | lock.signal()
30 | return count
31 | }
32 |
33 | private var runningRequest: AVAssetResourceLoadingRequest?
34 | private var requestTasks: [PoAVPlayerResourceRequestTask] = []
35 | private let fileHandler: PoAVPlayerResourceCacheFileHandler
36 | private let sessionDelegate = PoAVPlayerSessionDelegate()
37 | private let lock: DispatchSemaphore = DispatchSemaphore(value: 1)
38 |
39 | // MARK: - Initializator
40 |
41 | init(resourceIdentifier: URL) {
42 | self.resourceIdentifier = resourceIdentifier
43 | self.fileHandler = PoAVPlayerResourceCacheFileHandler(resourceIdentifier: resourceIdentifier)
44 | }
45 |
46 | deinit {
47 | requestTasks.forEach { (task) in
48 | task.cancel()
49 | }
50 | debugPrint("loader die")
51 | }
52 |
53 | // MARK: - Methods
54 |
55 | func appending(request: AVAssetResourceLoadingRequest) {
56 | lock.wait()
57 | requests.append(request)
58 | lock.signal()
59 | startHandleNextRequest()
60 | }
61 |
62 | func cancel(_ request: AVAssetResourceLoadingRequest) {
63 | lock.wait()
64 | if let index = self.requests.firstIndex(of: request) {
65 | let request = self.requests[index]
66 | if request === self.runningRequest {
67 | self.requestTasks.first?.cancel()
68 | } else {
69 | self.requests.remove(at: index)
70 | }
71 | }
72 | lock.signal()
73 | }
74 |
75 |
76 | // MARK: - Helper
77 |
78 | private func startHandleNextRequest() {
79 | // 如果有任务正在执行or任务列表为空, 直接返回
80 | if runningRequest != nil { return }
81 | // 取出最前面的任务,没有直接返回
82 | guard let next = requests.first else { return }
83 |
84 | let local = next.dataRequest?.requestedOffset ?? 0
85 | let length = next.dataRequest?.requestedLength ?? 0
86 | runningRequest = next
87 | let range = NSRange(location: Int(local), length: length)
88 | assignTasks(with: range)
89 | }
90 |
91 | // 分割请求的range,生成task
92 | private func assignTasks(with range: NSRange) {
93 | var offset = range.location
94 | let upper = range.upperBound
95 |
96 | while offset < upper {
97 | if let cachedRange = fileHandler.firstCachedFragment(in: NSRange(location: offset, length: upper - offset)) {
98 | if offset < cachedRange.location {
99 | let remoteTask = sessionDelegate.remoteDataTask(with: resourceIdentifier, requestRange: NSRange(location: offset, length: cachedRange.location - offset))
100 | requestTasks.append(remoteTask)
101 | let localTask = PoAVPlayerResourceRequestLocalTask(fileHandler: fileHandler, requestRange: cachedRange)
102 | requestTasks.append(localTask)
103 | offset = cachedRange.upperBound
104 | } else {
105 | let localTask = PoAVPlayerResourceRequestLocalTask(fileHandler: fileHandler, requestRange: cachedRange)
106 | requestTasks.append(localTask)
107 | offset = cachedRange.upperBound
108 | }
109 | } else {
110 | let remoteTask = sessionDelegate.remoteDataTask(with: resourceIdentifier, requestRange: NSRange(location: offset, length: upper - offset))
111 | requestTasks.append(remoteTask)
112 | offset = upper
113 | }
114 | }
115 |
116 | startHandleNextTask()
117 | }
118 |
119 | private func startHandleNextTask() {
120 | if requestTasks.isEmpty {
121 | requests.removeFirst()
122 | runningRequest?.finishLoading()
123 | runningRequest = nil
124 | startHandleNextRequest()
125 | } else {
126 | requestTasks.first?.delegate = self
127 | requestTasks.first?.start()
128 | }
129 | }
130 |
131 |
132 | private func fillContentInformationRequest(with response: URLResponse, isFromLocal: Bool) {
133 | if isFilledContentInformation { return }
134 | isFilledContentInformation = true
135 |
136 | guard let response = response as? HTTPURLResponse else { return }
137 |
138 | if let mimeType = response.mimeType {
139 | let result = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType as CFString, nil)
140 | runningRequest?.contentInformationRequest?.contentType = result?.takeRetainedValue() as String?
141 | }
142 | runningRequest?.contentInformationRequest?.isByteRangeAccessSupported = true
143 | if !isFromLocal {
144 | if let range = response.allHeaderFields["Content-Range"] as? String {
145 | let ranges = range.components(separatedBy: "/")
146 | if let lengthStr = ranges.last {
147 | runningRequest?.contentInformationRequest?.contentLength = Int64(lengthStr) ?? 0
148 | }
149 | } else if let range = response.allHeaderFields["content-range"] as? String {
150 | let ranges = range.components(separatedBy: "/")
151 | if let lengthStr = ranges.last {
152 | runningRequest?.contentInformationRequest?.contentLength = Int64(lengthStr) ?? 0
153 | }
154 | }
155 | fileHandler.cacheInfo.mimeType = response.mimeType
156 | fileHandler.cacheInfo.expectedLength = Int(runningRequest?.contentInformationRequest?.contentLength ?? 0)
157 | } else {
158 | runningRequest?.contentInformationRequest?.contentLength = response.expectedContentLength
159 | }
160 | }
161 |
162 | private var isFilledContentInformation: Bool = false
163 | }
164 |
165 |
166 | // MARK: - PoAVPlayerResourceRequestTaskDelegate
167 |
168 | extension PoAVPlayerResourceLoader: PoAVPlayerResourceRequestTaskDelegate {
169 |
170 | func requestTask(_ task: PoAVPlayerResourceRequestTask, didReceiveResponse response: URLResponse) {
171 | if task is PoAVPlayerResourceRequestLocalTask {
172 | fillContentInformationRequest(with: response, isFromLocal: true)
173 | } else {
174 | fillContentInformationRequest(with: response, isFromLocal: false)
175 | }
176 | }
177 |
178 | func requestTask(_ task: PoAVPlayerResourceRequestTask, didReceiveData data: Data) {
179 | runningRequest?.dataRequest?.respond(with: data)
180 | if task is PoAVPlayerResourceRequestRemoteTask {
181 | fileHandler.saveData(data, at: UInt64(task.currentOffset))
182 | fileHandler.saveFragment(NSRange(location: task.currentOffset, length: data.count))
183 | }
184 | }
185 |
186 | func requestTask(_ task: PoAVPlayerResourceRequestTask, didCompleteWithError error: Error?) {
187 | if task is PoAVPlayerResourceRequestRemoteTask {
188 | fileHandler.synchronize()
189 | }
190 | if error != nil {
191 | requestTasks.removeAll()
192 | lock.wait()
193 | requests.removeFirst()
194 | lock.signal()
195 | runningRequest?.finishLoading(with: error)
196 | runningRequest = nil
197 | startHandleNextRequest()
198 | return
199 | } else {
200 | requestTasks.removeFirst()
201 | startHandleNextTask()
202 | }
203 | }
204 | }
205 |
--------------------------------------------------------------------------------
/VideoPlayer/PoVideoPlayer/PoAVPlayerControlView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PoAVControlView.swift
3 | // VideoPlayer
4 | //
5 | // Created by 黄山哥 on 2019/1/16.
6 | // Copyright © 2019 黄山哥. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import AVFoundation
11 |
12 | class PoAVPlayerControlView: UIView {
13 |
14 | unowned(unsafe) var player: PoAVPlayer
15 |
16 | private(set) var isPlayToEndTime: Bool = false
17 |
18 | private let topToolBar = UIView()
19 | private let titleLabel: UILabel = UILabel()
20 |
21 | private let activityIndicator: UIActivityIndicatorView = UIActivityIndicatorView(style: .whiteLarge)
22 |
23 | private let bottomToolBar = UIView()
24 | private let currentTimeLabel: UILabel = UILabel()
25 | private let durationTimeLabel: UILabel = UILabel()
26 | private let playButton: UIButton = UIButton()
27 | private let fullScreenButton: UIButton = UIButton()
28 | private let progress: PoProgressView = PoProgressView()
29 | private var isIgnorePeriod: Bool = false
30 |
31 | init(player: PoAVPlayer) {
32 | self.player = player
33 | super.init(frame: .zero)
34 | _setup()
35 | }
36 |
37 | required init?(coder aDecoder: NSCoder) {
38 | fatalError("init(coder:) has not been implemented")
39 | }
40 |
41 | private func _setup() {
42 | _addSubviews()
43 | }
44 |
45 | override func layoutSubviews() {
46 | super.layoutSubviews()
47 | _layoutSubviews()
48 | }
49 |
50 | private func _addSubviews() {
51 | /* ------------------------------topToolBar ------------------------------ */
52 | topToolBar.backgroundColor = UIColor(white: 0.5, alpha: 0.7)
53 | self.addSubview(topToolBar)
54 |
55 | // 名称
56 | titleLabel.textColor = UIColor.white
57 | topToolBar.addSubview(titleLabel)
58 | titleLabel.text = "title"
59 |
60 |
61 | // 网络指示器
62 | self.addSubview(activityIndicator)
63 |
64 | /* -----------------------------bottomToolBar ----------------------------- */
65 | bottomToolBar.backgroundColor = UIColor(white: 0.5, alpha: 0.7)
66 | self.addSubview(bottomToolBar)
67 |
68 | // 播放/暂停按钮
69 | playButton.addTarget(self, action: #selector(PoAVPlayerControlView.playButtonHandle(_:)), for: .touchUpInside)
70 | bottomToolBar.addSubview(playButton)
71 | playButton.setTitle("播放", for: .normal)
72 | playButton.setTitle("暂停", for: .selected)
73 |
74 | // 当前播放时间
75 | currentTimeLabel.text = "00:00:00"
76 | currentTimeLabel.textColor = UIColor.white
77 | currentTimeLabel.font = UIFont.systemFont(ofSize: 13)
78 | bottomToolBar.addSubview(currentTimeLabel)
79 |
80 | // 播放/缓冲进度
81 | progress.isContinuous = false
82 | progress.addTarget(self, action: #selector(PoAVPlayerControlView.progressChangeHandle(_:)), for: .valueChanged)
83 | bottomToolBar.addSubview(progress)
84 |
85 | // 总播放时间
86 | durationTimeLabel.text = "00:00:00"
87 | durationTimeLabel.textColor = UIColor.white
88 | durationTimeLabel.font = UIFont.systemFont(ofSize: 13)
89 | bottomToolBar.addSubview(durationTimeLabel)
90 |
91 | // 全屏
92 | fullScreenButton.addTarget(self, action: #selector(PoAVPlayerControlView.fullScreenButtonHandle(_:)), for: .touchUpInside)
93 | bottomToolBar.addSubview(fullScreenButton)
94 | fullScreenButton.setTitle("全屏", for: .normal)
95 | }
96 |
97 | private func _layoutSubviews() {
98 | /* ------------------------------topToolBar ------------------------------ */
99 |
100 | let size = CGSize(width: 45, height: 30)
101 | let padding: CGFloat = 8
102 |
103 | topToolBar.frame = CGRect(x: 0, y: 0, width: bounds.width, height: 60)
104 |
105 | // 名称
106 | titleLabel.frame = CGRect(x: padding, y: 0, width: topToolBar.bounds.width - 40, height: 60)
107 |
108 |
109 | // 网络指示器
110 | activityIndicator.center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
111 |
112 | /* -----------------------------bottomToolBar ----------------------------- */
113 | bottomToolBar.frame = CGRect(x: 0, y: bounds.height - 60, width: bounds.width, height: 60)
114 |
115 | // 播放/暂停按钮
116 | let y = (bottomToolBar.bounds.height - size.height)/2
117 |
118 | playButton.frame = CGRect(x: padding, y: y, width: size.width, height: size.height)
119 |
120 | // 当前播放时间
121 | currentTimeLabel.frame = CGRect(x: playButton.frame.maxX + padding, y: y, width: 60, height: size.height)
122 |
123 | // 全屏按钮
124 | fullScreenButton.frame = CGRect(x: bounds.width - size.width - padding, y: y, width: size.width, height: size.height)
125 |
126 | // 总时长
127 | durationTimeLabel.frame = CGRect(x: fullScreenButton.frame.minX - padding - 60, y: y, width: 60, height: size.height)
128 |
129 | // 缓冲进度
130 | let width: CGFloat = bounds.width - padding - playButton.bounds.width - padding - currentTimeLabel.bounds.width - padding - padding - durationTimeLabel.bounds.width - padding - fullScreenButton.bounds.width - padding
131 |
132 | progress.frame = CGRect(x: currentTimeLabel.frame.maxX + padding, y: (bottomToolBar.bounds.height - 20)/2, width: width, height: 20)
133 | }
134 |
135 | // MARK: - selector
136 | @objc
137 | private func playButtonHandle(_ sender: UIButton) {
138 | if sender.isSelected {
139 | player.pause()
140 | } else {
141 | if isPlayToEndTime {
142 | player.seekToTime(0) { (finished) in
143 | if finished {
144 | self.player.play()
145 | }
146 | }
147 | isPlayToEndTime = false
148 | } else {
149 | player.play()
150 | }
151 | }
152 | sender.isSelected.toggle()
153 | }
154 |
155 | @objc
156 | private func progressChangeHandle(_ sender: PoProgressView) {
157 | guard let duration = player.duration else { return }
158 |
159 | let isPlaying = player.isPlaying
160 | if isPlaying {
161 | player.pause()
162 | playButton.isSelected = false
163 | isIgnorePeriod = true
164 | }
165 |
166 | let target = Double(sender.sliderValue) * duration
167 | print(target)
168 | player.seekToTime(target) { (finished) in
169 | if finished && isPlaying {
170 | self.player.play()
171 | self.playButton.isSelected = true
172 | self.isIgnorePeriod = false
173 | }
174 | }
175 | }
176 |
177 | @objc
178 | private func fullScreenButtonHandle(_ sender: UIButton) {
179 | debugPrint("not implement.")
180 | }
181 |
182 | // MARK: - helper
183 | private func formartDuration(_ duration: Double) -> String {
184 | let duration = Int(duration)
185 | let second = duration % 60
186 | let minute = (duration % 3600) / 60
187 | let hour = duration / 3600
188 | return String(format: "%02d:%02d:%02d", hour, minute, second)
189 | }
190 | }
191 |
192 |
193 | // MARK: - PoAVPlayerDelegate
194 | extension PoAVPlayerControlView: PoAVPlayerDelegate {
195 |
196 | /// 音视频资源加载的状态,是否可以播放: unknown, readyToPlay, failed
197 | func avplayer(_ player: PoAVPlayer, playerItemStatusChanged status: AVPlayerItem.Status) {
198 | switch status {
199 | case .readyToPlay:
200 | durationTimeLabel.text = formartDuration(player.duration!)
201 | default:
202 | debugPrint("player item can't be played. status: \(status.rawValue)")
203 | }
204 | }
205 |
206 | /// 缓冲到了哪儿
207 | func avplayer(_ player: PoAVPlayer, loadedTimeRange range: CMTimeRange) {
208 | let loaded = range.end.seconds
209 | let duration = player.duration!
210 | progress.progressValue = Float(loaded / duration)
211 | }
212 |
213 | /// 缓冲数据是否够用
214 | func avplayer(_ player: PoAVPlayer, playbackBufferStatus status: PoAVPlayer.PlaybackBufferStatus) {
215 | switch status {
216 | case .full:
217 | activityIndicator.stopAnimating()
218 | case .empty:
219 | activityIndicator.startAnimating()
220 | }
221 | }
222 |
223 | /// 播放时周期性回调
224 | func avplayer(_ player: PoAVPlayer, periodicallyInvoke time: CMTime) {
225 | let current = time.seconds
226 | let duration = player.duration!
227 | if !progress.isTouching && !isIgnorePeriod {
228 | progress.sliderValue = Float(current / duration)
229 | }
230 | currentTimeLabel.text = formartDuration(current)
231 | }
232 |
233 | /// 播放完毕
234 | func avplayerDidPlayToEndTime(_ player: PoAVPlayer) {
235 | isPlayToEndTime = true
236 | playButton.isSelected = false
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/VideoPlayer/PoVideoPlayer/String+extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+extension.swift
3 | // KitDemo
4 | //
5 | // Created by 黄中山 on 2018/7/8.
6 | // Copyright © 2018年 黄中山. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // https://github.com/krzyzanowskim/CryptoSwift
12 | // MARK: - String - MD5
13 | extension String {
14 | public var md5: String {
15 | if let data = self.data(using: .utf8, allowLossyConversion: true) {
16 |
17 | let message = data.withUnsafeBytes { (bytes) -> [UInt8] in
18 | return Array(bytes)
19 | }
20 |
21 | let MD5Calculator = MD5(message)
22 | let MD5Data = MD5Calculator.calculate()
23 |
24 | var MD5String = String()
25 | for c in MD5Data {
26 | MD5String += String(format: "%02x", c)
27 | }
28 | return MD5String
29 |
30 | } else {
31 | return self
32 | }
33 | }
34 | }
35 |
36 |
37 | protocol HashProtocol {
38 | var message: Array { get }
39 |
40 | // Common part for hash calculation. Prepare header data.
41 | func prepare(_ len: Int) -> Array
42 | }
43 |
44 | extension HashProtocol {
45 |
46 | func prepare(_ len: Int) -> Array {
47 | var tmpMessage = message
48 |
49 | // Step 1. Append Padding Bits
50 | tmpMessage.append(0x80) // append one bit (UInt8 with one bit) to message
51 |
52 | // append "0" bit until message length in bits ≡ 448 (mod 512)
53 | var msgLength = tmpMessage.count
54 | var counter = 0
55 |
56 | while msgLength % len != (len - 8) {
57 | counter += 1
58 | msgLength += 1
59 | }
60 |
61 | tmpMessage += Array(repeating: 0, count: counter)
62 | return tmpMessage
63 | }
64 | }
65 |
66 | func toUInt32Array(_ slice: ArraySlice) -> Array {
67 | var result = Array()
68 | result.reserveCapacity(16)
69 |
70 | for idx in stride(from: slice.startIndex, to: slice.endIndex, by: MemoryLayout.size) {
71 | let d0 = UInt32(slice[idx.advanced(by: 3)]) << 24
72 | let d1 = UInt32(slice[idx.advanced(by: 2)]) << 16
73 | let d2 = UInt32(slice[idx.advanced(by: 1)]) << 8
74 | let d3 = UInt32(slice[idx])
75 | let val: UInt32 = d0 | d1 | d2 | d3
76 |
77 | result.append(val)
78 | }
79 | return result
80 | }
81 |
82 | struct BytesIterator: IteratorProtocol {
83 |
84 | let chunkSize: Int
85 | let data: [UInt8]
86 |
87 | init(chunkSize: Int, data: [UInt8]) {
88 | self.chunkSize = chunkSize
89 | self.data = data
90 | }
91 |
92 | var offset = 0
93 |
94 | mutating func next() -> ArraySlice? {
95 | let end = min(chunkSize, data.count - offset)
96 | let result = data[offset.. 0 ? result : nil
99 | }
100 | }
101 |
102 | struct BytesSequence: Sequence {
103 | let chunkSize: Int
104 | let data: [UInt8]
105 |
106 | func makeIterator() -> BytesIterator {
107 | return BytesIterator(chunkSize: chunkSize, data: data)
108 | }
109 | }
110 |
111 | func rotateLeft(_ value: UInt32, bits: UInt32) -> UInt32 {
112 | return ((value << bits) & 0xFFFFFFFF) | (value >> (32 - bits))
113 | }
114 |
115 | // array of bytes, little-endian representation
116 | func arrayOfBytes(_ value: T, length: Int? = nil) -> [UInt8] {
117 | let totalBytes = length ?? (MemoryLayout.size * 8)
118 |
119 | let valuePointer = UnsafeMutablePointer.allocate(capacity: 1)
120 | valuePointer.pointee = value
121 |
122 | let bytes = valuePointer.withMemoryRebound(to: UInt8.self, capacity: totalBytes) { (bytesPointer) -> [UInt8] in
123 | var bytes = [UInt8](repeating: 0, count: totalBytes)
124 | for j in 0...size, totalBytes) {
125 | bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee
126 | }
127 | return bytes
128 | }
129 |
130 | valuePointer.deinitialize(count: 1)
131 | valuePointer.deallocate()
132 |
133 | return bytes
134 | }
135 |
136 | extension Int {
137 | // Array of bytes with optional padding (little-endian)
138 | func bytes(_ totalBytes: Int = MemoryLayout.size) -> [UInt8] {
139 | return arrayOfBytes(self, length: totalBytes)
140 | }
141 |
142 | }
143 |
144 | class MD5: HashProtocol {
145 |
146 | static let size = 16 // 128 / 8
147 | let message: [UInt8]
148 |
149 | init (_ message: [UInt8]) {
150 | self.message = message
151 | }
152 |
153 | // specifies the per-round shift amounts
154 | private let shifts: [UInt32] = [7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
155 | 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
156 | 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
157 | 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21]
158 |
159 | // binary integer part of the sines of integers (Radians)
160 | private let sines: [UInt32] = [0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
161 | 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
162 | 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
163 | 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
164 | 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
165 | 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
166 | 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
167 | 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
168 | 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
169 | 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
170 | 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x4881d05,
171 | 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
172 | 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
173 | 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
174 | 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
175 | 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391]
176 |
177 | private let hashes: [UInt32] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476]
178 |
179 | func calculate() -> [UInt8] {
180 | var tmpMessage = prepare(64)
181 | tmpMessage.reserveCapacity(tmpMessage.count + 4)
182 |
183 | // hash values
184 | var hh = hashes
185 |
186 | // Step 2. Append Length a 64-bit representation of lengthInBits
187 | let lengthInBits = (message.count * 8)
188 | let lengthBytes = lengthInBits.bytes(64 / 8)
189 | tmpMessage += lengthBytes.reversed()
190 |
191 | // Process the message in successive 512-bit chunks:
192 | let chunkSizeBytes = 512 / 8 // 64
193 | for chunk in BytesSequence(chunkSize: chunkSizeBytes, data: tmpMessage) {
194 | // break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15
195 | let M = toUInt32Array(chunk)
196 | assert(M.count == 16, "Invalid array")
197 |
198 | // Initialize hash value for this chunk:
199 | var A: UInt32 = hh[0]
200 | var B: UInt32 = hh[1]
201 | var C: UInt32 = hh[2]
202 | var D: UInt32 = hh[3]
203 |
204 | var dTemp: UInt32 = 0
205 |
206 | // Main loop
207 | for j in 0 ..< sines.count {
208 | var g = 0
209 | var F: UInt32 = 0
210 |
211 | switch j {
212 | case 0...15:
213 | F = (B & C) | ((~B) & D)
214 | g = j
215 | break
216 | case 16...31:
217 | F = (D & B) | (~D & C)
218 | g = (5 * j + 1) % 16
219 | break
220 | case 32...47:
221 | F = B ^ C ^ D
222 | g = (3 * j + 5) % 16
223 | break
224 | case 48...63:
225 | F = C ^ (B | (~D))
226 | g = (7 * j) % 16
227 | break
228 | default:
229 | break
230 | }
231 | dTemp = D
232 | D = C
233 | C = B
234 | B = B &+ rotateLeft((A &+ F &+ sines[j] &+ M[g]), bits: shifts[j])
235 | A = dTemp
236 | }
237 |
238 | hh[0] = hh[0] &+ A
239 | hh[1] = hh[1] &+ B
240 | hh[2] = hh[2] &+ C
241 | hh[3] = hh[3] &+ D
242 | }
243 |
244 | var result = [UInt8]()
245 | result.reserveCapacity(hh.count / 4)
246 |
247 | hh.forEach {
248 | let itemLE = $0.littleEndian
249 | let r1 = UInt8(itemLE & 0xff)
250 | let r2 = UInt8((itemLE >> 8) & 0xff)
251 | let r3 = UInt8((itemLE >> 16) & 0xff)
252 | let r4 = UInt8((itemLE >> 24) & 0xff)
253 | result += [r1, r2, r3, r4]
254 | }
255 | return result
256 | }
257 | }
258 |
259 |
260 |
--------------------------------------------------------------------------------
/VideoPlayer/PoVideoPlayer/PoAVPlayer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PoAVPlayer.swift
3 | // VideoPlayer
4 | //
5 | // Created by 黄山哥 on 2019/1/16.
6 | // Copyright © 2019 黄山哥. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import AVFoundation
11 |
12 | protocol PoAVPlayerDelegate: class {
13 |
14 | /// 音视频资源加载的状态,是否可以播放: unknown, readyToPlay, failed
15 | func avplayer(_ player: PoAVPlayer, playerItemStatusChanged status: AVPlayerItem.Status)
16 | /// 缓冲到了哪儿
17 | func avplayer(_ player: PoAVPlayer, loadedTimeRange range: CMTimeRange)
18 | /// 缓冲数据是否够用
19 | func avplayer(_ player: PoAVPlayer, playbackBufferStatus status: PoAVPlayer.PlaybackBufferStatus)
20 | /// 播放时周期性回调
21 | func avplayer(_ player: PoAVPlayer, periodicallyInvoke time: CMTime)
22 | /// 播放完毕
23 | func avplayerDidPlayToEndTime(_ player: PoAVPlayer)
24 | }
25 |
26 | extension PoAVPlayerDelegate {
27 | /// 音视频资源加载的状态,是否可以播放: unknown, readyToPlay, failed
28 | func avplayer(_ player: PoAVPlayer, playerItemStatusChanged status: AVPlayerItem.Status) {}
29 | /// 缓冲到了哪儿
30 | func avplayer(_ player: PoAVPlayer, loadedTimeRange range: CMTimeRange) {}
31 | /// 缓冲数据是否够用
32 | func avplayer(_ player: PoAVPlayer, playbackBufferStatus status: PoAVPlayer.PlaybackBufferStatus) {}
33 | /// 播放时周期性回调
34 | func avplayer(_ player: PoAVPlayer, periodicallyInvoke time: CMTime) {}
35 | /// 播放完毕
36 | func avplayerDidPlayToEndTime(_ player: PoAVPlayer) {}
37 | }
38 |
39 | extension PoAVPlayer {
40 | enum PlaybackBufferStatus {
41 | case full
42 | case empty
43 | }
44 |
45 | static let scheme = "__PoAVPlayerScheme__"
46 | }
47 |
48 | class PoAVPlayer: UIView {
49 |
50 | // MARK: - Properties
51 |
52 | weak var delegate: PoAVPlayerDelegate?
53 |
54 | /// seconds
55 | var duration: Double? {
56 | return _playerItem?.duration.seconds
57 | }
58 |
59 | /// 当前是否可以播放
60 | var isReadyToPlay: Bool {
61 | return _player.status == .readyToPlay
62 | }
63 |
64 | /// 是否播放中
65 | var isPlaying: Bool {
66 | if #available(iOS 10.0, *) {
67 | return _player.timeControlStatus != .paused
68 | } else {
69 | return _player.rate == 0
70 | }
71 | }
72 |
73 | private lazy var _player: AVPlayer = AVPlayer(playerItem: nil)
74 | private var _playerItem: AVPlayerItem?
75 | private var _timeObserver: Any?
76 | private lazy var _loaderDelegate: PoAVPlayerResourceLoaderDelegate = PoAVPlayerResourceLoaderDelegate()
77 | private var _isPlayingBeforeResignActive: Bool = false
78 |
79 | // MARK: - Override
80 |
81 | override class var layerClass: AnyClass {
82 | return AVPlayerLayer.self
83 | }
84 |
85 | override init(frame: CGRect) {
86 | super.init(frame: frame)
87 | _setup()
88 | }
89 |
90 | convenience init() {
91 | self.init(frame: .zero)
92 | }
93 |
94 | required init?(coder aDecoder: NSCoder) {
95 | super.init(coder: aDecoder)
96 | _setup()
97 | }
98 |
99 | private func _setup() {
100 | (self.layer as! AVPlayerLayer).player = _player
101 | _timeObserver = _player.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 600),
102 | queue: DispatchQueue.main) { [weak self] (time) in
103 | guard let strongSelf = self else { return }
104 | strongSelf.delegate?.avplayer(strongSelf, periodicallyInvoke: time)
105 | }
106 | _addNotification()
107 | }
108 |
109 | deinit {
110 | if let timeObserver = _timeObserver {
111 | _player.removeTimeObserver(timeObserver)
112 | }
113 | _removeObserver(for: _playerItem)
114 | _removeNotification()
115 | _player.pause()
116 | _player.currentItem?.cancelPendingSeeks()
117 | _player.currentItem?.asset.cancelLoading()
118 | }
119 |
120 | // MARK: - Public Method
121 |
122 | /// 添加视频播放控制层
123 | func addControlLayer(_ layer: T) {
124 | self.delegate = layer
125 | addSubview(layer)
126 | layer.translatesAutoresizingMaskIntoConstraints = false
127 | addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[layer]|", options: [], metrics: nil, views: ["layer": layer]))
128 | addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[layer]|", options: [], metrics: nil, views: ["layer": layer]))
129 | }
130 |
131 |
132 | /// 播放url对应的音/视频文件
133 | /// - Parameters:
134 | /// - url: 音/视频文件地址
135 | /// - needCache: 是否需要缓存本地
136 | func play(with url: URL, needCache: Bool = false) {
137 | var item: AVPlayerItem
138 | if needCache {
139 | let url = URL(string: PoAVPlayer.scheme + url.absoluteString)!
140 | let urlAsset = AVURLAsset(url: url)
141 | urlAsset.resourceLoader.setDelegate(_loaderDelegate, queue: DispatchQueue.main)
142 | item = AVPlayerItem(asset: urlAsset)
143 | } else {
144 | item = AVPlayerItem(url: url)
145 | }
146 | play(with: item)
147 | }
148 |
149 |
150 | /// 播放item中的音/视频文件,无法缓存本地
151 | /// - Parameter item: item
152 | private func play(with item: AVPlayerItem) {
153 | _removeObserver(for: _playerItem)
154 | _addObserver(for: item)
155 | _playerItem = item
156 | _player.replaceCurrentItem(with: _playerItem)
157 | }
158 |
159 | /// 播放
160 | func play() {
161 | if _player.status != .readyToPlay { return }
162 | _player.play()
163 | }
164 |
165 | /// 暂停
166 | func pause() {
167 | if _player.status != .readyToPlay { return }
168 | _player.pause()
169 | }
170 |
171 |
172 | /// 跳转到指定时间点
173 | /// - Parameters:
174 | /// - timeInterval: 新的时间点(单位秒)
175 | /// - completionHandler: 跳转完成后执行
176 | func seekToTime(_ timeInterval: TimeInterval, completionHandler: ((Bool) -> Void)? = nil) {
177 | guard let playItem = _playerItem else {
178 | completionHandler?(false)
179 | return
180 | }
181 |
182 | let seconds = playItem.duration.seconds > timeInterval ? timeInterval : playItem.duration.seconds
183 | if let completionHandler = completionHandler {
184 | _player.seek(to: CMTime(seconds: seconds, preferredTimescale: 600), completionHandler: completionHandler)
185 | } else {
186 | _player.seek(to: CMTime(seconds: seconds, preferredTimescale: 600))
187 | }
188 | }
189 |
190 | // MARK: - Observer
191 |
192 | private func _addObserver(for playerItem: AVPlayerItem?) {
193 | playerItem?.addObserver(self, forKeyPath: "loadedTimeRanges", options: [.new], context: nil)
194 | playerItem?.addObserver(self, forKeyPath: "status", options: [.new], context: nil)
195 | playerItem?.addObserver(self, forKeyPath: "playbackBufferEmpty", options: [.new], context: nil)
196 | playerItem?.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: [.new], context: nil)
197 | }
198 |
199 | private func _removeObserver(for playerItem: AVPlayerItem?) {
200 | playerItem?.removeObserver(self, forKeyPath: "loadedTimeRanges")
201 | playerItem?.removeObserver(self, forKeyPath: "status")
202 | playerItem?.removeObserver(self, forKeyPath: "playbackBufferEmpty")
203 | playerItem?.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp")
204 | }
205 |
206 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
207 | guard let playerItem = object as? AVPlayerItem else {
208 | super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
209 | return
210 | }
211 |
212 | if keyPath == "loadedTimeRanges", let range = playerItem.loadedTimeRanges.last {
213 | delegate?.avplayer(self, loadedTimeRange: range.timeRangeValue)
214 | } else if keyPath == "status" {
215 | delegate?.avplayer(self, playerItemStatusChanged: playerItem.status)
216 | } else if keyPath == "playbackBufferEmpty" {
217 | delegate?.avplayer(self, playbackBufferStatus: .empty)
218 | } else if keyPath == "playbackLikelyToKeepUp" {
219 | delegate?.avplayer(self, playbackBufferStatus: .full)
220 | }
221 | }
222 |
223 | // MARK: - Notification
224 |
225 | private func _addNotification() {
226 | NotificationCenter.default.addObserver(self,
227 | selector: #selector(_appResignActive),
228 | name: UIApplication.willResignActiveNotification,
229 | object: nil)
230 | NotificationCenter.default.addObserver(self,
231 | selector: #selector(_appBecomeActive),
232 | name: UIApplication.didBecomeActiveNotification,
233 | object: nil)
234 | NotificationCenter.default.addObserver(self,
235 | selector: #selector(_playerItemDidPlayToEndTime),
236 | name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
237 | object: nil)
238 | }
239 |
240 | private func _removeNotification() {
241 | NotificationCenter.default.removeObserver(self)
242 | }
243 |
244 | @objc
245 | private func _appResignActive() {
246 | _isPlayingBeforeResignActive = isPlaying
247 | if _isPlayingBeforeResignActive {
248 | _player.pause()
249 | }
250 | }
251 |
252 | @objc
253 | private func _appBecomeActive() {
254 | if _isPlayingBeforeResignActive {
255 | _player.play()
256 | }
257 | }
258 |
259 | @objc
260 | private func _playerItemDidPlayToEndTime() {
261 | delegate?.avplayerDidPlayToEndTime(self)
262 | }
263 | }
264 |
--------------------------------------------------------------------------------
/VideoPlayer/PoVideoPlayer/PoAVPlayerCacheManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PoAVPlayerCacheManager.swift
3 | // VideoPlayer
4 | //
5 | // Created by 黄山哥 on 2019/2/21.
6 | // Copyright © 2019 黄山哥. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class PoAVPlayerCacheManager {
12 |
13 | static let shared = PoAVPlayerCacheManager()
14 |
15 | private init() {
16 | let age = (UserDefaults.standard.value(forKey: "PoAVPlayerCacheManager.maxCacheAge") as? TimeInterval) ?? 60 * 60 * 24 * 7
17 | let size = (UserDefaults.standard.value(forKey: "PoAVPlayerCacheManager.maxCacheAge") as? Int) ?? 0
18 | self.maxCacheAge = age
19 | self.maxCacheSize = size
20 |
21 | // Subscribe to app event
22 | NotificationCenter.default.addObserver(self,
23 | selector: #selector(deleteOlderFiles as () -> Void),
24 | name: UIApplication.willTerminateNotification,
25 | object: nil)
26 | NotificationCenter.default.addObserver(self,
27 | selector: #selector(backgroundDeleteOldFiles),
28 | name: UIApplication.didEnterBackgroundNotification,
29 | object: nil)
30 | }
31 |
32 | deinit {
33 | NotificationCenter.default.removeObserver(self)
34 | }
35 |
36 | static let ioQueue: DispatchQueue = DispatchQueue(label: "com.PoAVPlayerResourceLoader.www")
37 |
38 | // MARK: - Properties
39 | var maxCacheAge: TimeInterval {
40 | didSet {
41 | UserDefaults.standard.set(maxCacheAge, forKey: "PoAVPlayerCacheManager.maxCacheAge")
42 | }
43 | }
44 | var maxCacheSize: Int {
45 | didSet {
46 | UserDefaults.standard.set(maxCacheSize, forKey: "PoAVPlayerCacheManager.maxCacheSize")
47 | }
48 | }
49 |
50 | private var backgroundTaskId: UIBackgroundTaskIdentifier = .invalid
51 | private var fileManager: FileManager {
52 | return FileManager.default // 线程安全的
53 | }
54 |
55 | func deleteAllFiles(completion: ((Error?) -> Void)? = nil) {
56 | PoAVPlayerCacheManager.ioQueue.async {
57 | do {
58 | try self.fileManager.removeItem(atPath: PoAVPlayerCacheManager.cacheDomainDirectory())
59 | } catch let error {
60 | completion?(error)
61 | }
62 | completion?(nil)
63 | }
64 | }
65 |
66 | // MARK: - delete single file
67 | func deleteFile(by key: String, completion: ((Error?) -> Void)? = nil) {
68 |
69 | PoAVPlayerCacheManager.ioQueue.async {
70 | if let indexPath = PoAVPlayerCacheManager.indexFilePath(for: key) {
71 | do {
72 | try self.fileManager.removeItem(atPath: indexPath)
73 | if let dataPath = PoAVPlayerCacheManager.dataFilePath(for: key) {
74 | try self.fileManager.removeItem(atPath: dataPath)
75 | }
76 | } catch let error {
77 | completion?(error)
78 | }
79 | }
80 | completion?(nil)
81 | }
82 | }
83 |
84 | // MARK: - delete olderFiles
85 | @objc
86 | private func deleteOlderFiles() {
87 | deleteOlderFiles(with: nil)
88 | }
89 |
90 | @objc
91 | private func backgroundDeleteOldFiles() {
92 | backgroundTaskId = UIApplication.shared.beginBackgroundTask(expirationHandler: { [weak self] in
93 | guard let strongSelf = self else { return }
94 | UIApplication.shared.endBackgroundTask(strongSelf.backgroundTaskId)
95 | strongSelf.backgroundTaskId = .invalid
96 | })
97 |
98 | deleteOlderFiles {
99 | if self.backgroundTaskId != .invalid {
100 | UIApplication.shared.endBackgroundTask(self.backgroundTaskId)
101 | self.backgroundTaskId = .invalid
102 | }
103 | }
104 | }
105 |
106 |
107 | /// SDImageCache 借鉴
108 | private func deleteOlderFiles(with completion: (() -> Void)?) {
109 | let cachePath = URL(fileURLWithPath: PoAVPlayerCacheManager.cacheDomainDirectory(), isDirectory: true)
110 | let resourceKeys: [URLResourceKey] = [.isDirectoryKey, .contentModificationDateKey, .totalFileAllocatedSizeKey]
111 | let fileEnumerator = fileManager.enumerator(at: cachePath, includingPropertiesForKeys: resourceKeys, options: [.skipsHiddenFiles], errorHandler: nil)!
112 | let expirationDate = Date(timeIntervalSinceNow: -maxCacheAge)
113 |
114 | var cacheFiles = [URL: URLResourceValues]()
115 | var currentCacheSize = 0
116 | var urlsToDelete = [URL]()
117 |
118 | for fileUrl in fileEnumerator {
119 | let url = fileUrl as! URL
120 | var resourceValues: URLResourceValues
121 | do {
122 | resourceValues = try url.resourceValues(forKeys: Set(resourceKeys))
123 | } catch let error {
124 | debugPrint("PoAVPlayerCacheManager error: \(error.localizedDescription)")
125 | continue
126 | }
127 |
128 | if resourceValues.isDirectory == true {
129 | continue
130 | }
131 |
132 | if let modifucationDate = resourceValues.contentModificationDate {
133 | if modifucationDate.compare(expirationDate) == .orderedAscending {
134 | urlsToDelete.append(url)
135 | continue
136 | }
137 | }
138 |
139 | if let totalAllocatedSize = resourceValues.totalFileAllocatedSize {
140 | currentCacheSize += totalAllocatedSize
141 | cacheFiles[url] = resourceValues
142 | }
143 | }
144 |
145 | for url in urlsToDelete {
146 | try? fileManager.removeItem(at: url)
147 | }
148 |
149 | if maxCacheSize > 0 && currentCacheSize > maxCacheSize {
150 |
151 | let result = cacheFiles.sorted { (keyValuePair1, keyValuePair2) -> Bool in
152 | return keyValuePair1.value.contentModificationDate!.compare(keyValuePair2.value.contentModificationDate!) == .orderedAscending
153 | }
154 |
155 | for (key, value) in result {
156 | if key.absoluteString.hasSuffix(".index") { continue }
157 | do {
158 | try fileManager.removeItem(at: key)
159 | try fileManager.removeItem(atPath: key.absoluteString + ".index")
160 | } catch {
161 | continue
162 | }
163 | currentCacheSize -= value.totalFileAllocatedSize!
164 | if currentCacheSize < maxCacheSize {
165 | break
166 | }
167 | }
168 | }
169 | if completion != nil {
170 | DispatchQueue.main.async {
171 | completion?()
172 | }
173 | }
174 | }
175 |
176 | }
177 |
178 | // MARK: - Path Helper
179 |
180 | let kCacheDomainName = "/com.avplayercaches.po"
181 | extension PoAVPlayerCacheManager {
182 |
183 | static func cacheDomainDirectory() -> String {
184 | var path = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first!
185 | path += kCacheDomainName
186 | if !FileManager.default.fileExists(atPath: path) {
187 | do {
188 | try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)
189 | } catch (let error) {
190 | fatalError("PoAVPlayerResourceCacheFileHandler init error: [\(error.localizedDescription)].")
191 | }
192 | }
193 | return path
194 | }
195 |
196 | static func indexFilePath(for key: String) -> String? {
197 | let path = cacheDomainDirectory() + "/\(key.md5).index"
198 | if !FileManager.default.fileExists(atPath: path) {
199 | return nil
200 | }
201 | return path
202 | }
203 |
204 | static func indexFileURL(for key: String) -> URL? {
205 | if let path = indexFilePath(for: key) {
206 | return URL(fileURLWithPath: path)
207 | }
208 | return nil
209 | }
210 |
211 | static func indexFilePathCreateIfNotExist(for key: String) -> URL {
212 | let path = cacheDomainDirectory() + "/\(key.md5).index"
213 | if !FileManager.default.fileExists(atPath: path) {
214 | let defaultJSON = #"""
215 | {
216 | "mimeType": null,
217 | "fragments": [],
218 | "expectedLength": -1
219 | }
220 | """#
221 | let success = FileManager.default.createFile(atPath: path, contents: defaultJSON.data(using: .utf8), attributes: nil)
222 | if !success {
223 | fatalError("PoAVPlayerResourceCacheFileHandler create index file fail.")
224 | }
225 | }
226 | return URL(fileURLWithPath: path)
227 | }
228 |
229 | static func dataFilePath(for key: String) -> String? {
230 | let path = cacheDomainDirectory() + "/\(key.md5)"
231 | if !FileManager.default.fileExists(atPath: path) {
232 | return nil
233 | }
234 | return path
235 | }
236 |
237 | static func dataFileURL(for key: String) -> URL? {
238 | if let path = dataFilePath(for: key) {
239 | return URL(fileURLWithPath: path)
240 | }
241 | return nil
242 | }
243 |
244 | static func dataFilePathCreateIfNotExist(for key: String) -> String {
245 | let path = cacheDomainDirectory() + "/\(key.md5)"
246 | if !FileManager.default.fileExists(atPath: path) {
247 | let success = FileManager.default.createFile(atPath: path, contents: nil, attributes: nil)
248 | if !success {
249 | fatalError("PoAVPlayerResourceCacheFileHandler create data file fail.")
250 | }
251 | }
252 | return path
253 | }
254 |
255 | }
256 |
--------------------------------------------------------------------------------
/VideoPlayer.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 5B0C2B362202D61A00FF2643 /* FragmentArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0C2B352202D61A00FF2643 /* FragmentArray.swift */; };
11 | 5B0C2B382203074D00FF2643 /* PoAVPlayerResourceCacheFileHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0C2B372203074D00FF2643 /* PoAVPlayerResourceCacheFileHandler.swift */; };
12 | 5B77714C221E84D3003A28B4 /* PoAVPlayerCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B77714B221E84D3003A28B4 /* PoAVPlayerCacheManager.swift */; };
13 | 5B7D96BB21F849CD00883650 /* PoAVPlayerSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B7D96BA21F849CD00883650 /* PoAVPlayerSessionDelegate.swift */; };
14 | 5B9B37AD222FF767007556A6 /* PoProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9B37AC222FF767007556A6 /* PoProgressView.swift */; };
15 | 5BE8C05121EF12EE00128341 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BE8C05021EF12EE00128341 /* AppDelegate.swift */; };
16 | 5BE8C05321EF12EE00128341 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BE8C05221EF12EE00128341 /* ViewController.swift */; };
17 | 5BE8C05621EF12EE00128341 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5BE8C05421EF12EE00128341 /* Main.storyboard */; };
18 | 5BE8C05821EF12F100128341 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5BE8C05721EF12F100128341 /* Assets.xcassets */; };
19 | 5BE8C05B21EF12F100128341 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5BE8C05921EF12F100128341 /* LaunchScreen.storyboard */; };
20 | 5BE8C06421EF136D00128341 /* PoAVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BE8C06321EF136D00128341 /* PoAVPlayer.swift */; };
21 | 5BE8C06621EF7F5900128341 /* PoAVPlayerControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BE8C06521EF7F5900128341 /* PoAVPlayerControlView.swift */; };
22 | 5BE8C06C21F60D1400128341 /* PoAVPlayerResourceLoaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BE8C06B21F60D1400128341 /* PoAVPlayerResourceLoaderDelegate.swift */; };
23 | 5BE8C06E21F6102E00128341 /* String+extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BE8C06D21F6102E00128341 /* String+extension.swift */; };
24 | 5BE8C07021F614F400128341 /* PoAVPlayerResourceLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BE8C06F21F614F400128341 /* PoAVPlayerResourceLoader.swift */; };
25 | 5BE8C07321F6E38E00128341 /* PoAVPlayerResourceRequestTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BE8C07221F6E38E00128341 /* PoAVPlayerResourceRequestTask.swift */; };
26 | /* End PBXBuildFile section */
27 |
28 | /* Begin PBXFileReference section */
29 | 5B0C2B352202D61A00FF2643 /* FragmentArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FragmentArray.swift; sourceTree = ""; };
30 | 5B0C2B372203074D00FF2643 /* PoAVPlayerResourceCacheFileHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoAVPlayerResourceCacheFileHandler.swift; sourceTree = ""; };
31 | 5B77714B221E84D3003A28B4 /* PoAVPlayerCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoAVPlayerCacheManager.swift; sourceTree = ""; };
32 | 5B7D96BA21F849CD00883650 /* PoAVPlayerSessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoAVPlayerSessionDelegate.swift; sourceTree = ""; };
33 | 5B9B37AC222FF767007556A6 /* PoProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoProgressView.swift; sourceTree = ""; };
34 | 5BE8C04D21EF12EE00128341 /* VideoPlayer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VideoPlayer.app; sourceTree = BUILT_PRODUCTS_DIR; };
35 | 5BE8C05021EF12EE00128341 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
36 | 5BE8C05221EF12EE00128341 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
37 | 5BE8C05521EF12EE00128341 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
38 | 5BE8C05721EF12F100128341 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
39 | 5BE8C05A21EF12F100128341 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
40 | 5BE8C05C21EF12F100128341 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
41 | 5BE8C06321EF136D00128341 /* PoAVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoAVPlayer.swift; sourceTree = ""; };
42 | 5BE8C06521EF7F5900128341 /* PoAVPlayerControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoAVPlayerControlView.swift; sourceTree = ""; };
43 | 5BE8C06B21F60D1400128341 /* PoAVPlayerResourceLoaderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoAVPlayerResourceLoaderDelegate.swift; sourceTree = ""; };
44 | 5BE8C06D21F6102E00128341 /* String+extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+extension.swift"; sourceTree = ""; };
45 | 5BE8C06F21F614F400128341 /* PoAVPlayerResourceLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoAVPlayerResourceLoader.swift; sourceTree = ""; };
46 | 5BE8C07221F6E38E00128341 /* PoAVPlayerResourceRequestTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoAVPlayerResourceRequestTask.swift; sourceTree = ""; };
47 | /* End PBXFileReference section */
48 |
49 | /* Begin PBXFrameworksBuildPhase section */
50 | 5BE8C04A21EF12EE00128341 /* Frameworks */ = {
51 | isa = PBXFrameworksBuildPhase;
52 | buildActionMask = 2147483647;
53 | files = (
54 | );
55 | runOnlyForDeploymentPostprocessing = 0;
56 | };
57 | /* End PBXFrameworksBuildPhase section */
58 |
59 | /* Begin PBXGroup section */
60 | 5BE8C04421EF12EE00128341 = {
61 | isa = PBXGroup;
62 | children = (
63 | 5BE8C04F21EF12EE00128341 /* VideoPlayer */,
64 | 5BE8C04E21EF12EE00128341 /* Products */,
65 | );
66 | sourceTree = "";
67 | };
68 | 5BE8C04E21EF12EE00128341 /* Products */ = {
69 | isa = PBXGroup;
70 | children = (
71 | 5BE8C04D21EF12EE00128341 /* VideoPlayer.app */,
72 | );
73 | name = Products;
74 | sourceTree = "";
75 | };
76 | 5BE8C04F21EF12EE00128341 /* VideoPlayer */ = {
77 | isa = PBXGroup;
78 | children = (
79 | 5BE8C06221EF133A00128341 /* PoVideoPlayer */,
80 | 5BE8C05021EF12EE00128341 /* AppDelegate.swift */,
81 | 5BE8C05221EF12EE00128341 /* ViewController.swift */,
82 | 5BE8C05421EF12EE00128341 /* Main.storyboard */,
83 | 5BE8C05721EF12F100128341 /* Assets.xcassets */,
84 | 5BE8C05921EF12F100128341 /* LaunchScreen.storyboard */,
85 | 5BE8C05C21EF12F100128341 /* Info.plist */,
86 | );
87 | path = VideoPlayer;
88 | sourceTree = "";
89 | };
90 | 5BE8C06221EF133A00128341 /* PoVideoPlayer */ = {
91 | isa = PBXGroup;
92 | children = (
93 | 5B0C2B352202D61A00FF2643 /* FragmentArray.swift */,
94 | 5BE8C06D21F6102E00128341 /* String+extension.swift */,
95 | 5BE8C06321EF136D00128341 /* PoAVPlayer.swift */,
96 | 5BE8C06B21F60D1400128341 /* PoAVPlayerResourceLoaderDelegate.swift */,
97 | 5BE8C06F21F614F400128341 /* PoAVPlayerResourceLoader.swift */,
98 | 5BE8C07221F6E38E00128341 /* PoAVPlayerResourceRequestTask.swift */,
99 | 5B7D96BA21F849CD00883650 /* PoAVPlayerSessionDelegate.swift */,
100 | 5B0C2B372203074D00FF2643 /* PoAVPlayerResourceCacheFileHandler.swift */,
101 | 5B77714B221E84D3003A28B4 /* PoAVPlayerCacheManager.swift */,
102 | 5BE8C06521EF7F5900128341 /* PoAVPlayerControlView.swift */,
103 | 5B9B37AC222FF767007556A6 /* PoProgressView.swift */,
104 | );
105 | path = PoVideoPlayer;
106 | sourceTree = "";
107 | };
108 | /* End PBXGroup section */
109 |
110 | /* Begin PBXNativeTarget section */
111 | 5BE8C04C21EF12EE00128341 /* VideoPlayer */ = {
112 | isa = PBXNativeTarget;
113 | buildConfigurationList = 5BE8C05F21EF12F100128341 /* Build configuration list for PBXNativeTarget "VideoPlayer" */;
114 | buildPhases = (
115 | 5BE8C04921EF12EE00128341 /* Sources */,
116 | 5BE8C04A21EF12EE00128341 /* Frameworks */,
117 | 5BE8C04B21EF12EE00128341 /* Resources */,
118 | );
119 | buildRules = (
120 | );
121 | dependencies = (
122 | );
123 | name = VideoPlayer;
124 | productName = VideoPlayer;
125 | productReference = 5BE8C04D21EF12EE00128341 /* VideoPlayer.app */;
126 | productType = "com.apple.product-type.application";
127 | };
128 | /* End PBXNativeTarget section */
129 |
130 | /* Begin PBXProject section */
131 | 5BE8C04521EF12EE00128341 /* Project object */ = {
132 | isa = PBXProject;
133 | attributes = {
134 | LastSwiftUpdateCheck = 1010;
135 | LastUpgradeCheck = 1010;
136 | ORGANIZATIONNAME = "黄山哥";
137 | TargetAttributes = {
138 | 5BE8C04C21EF12EE00128341 = {
139 | CreatedOnToolsVersion = 10.1;
140 | LastSwiftMigration = 1020;
141 | };
142 | };
143 | };
144 | buildConfigurationList = 5BE8C04821EF12EE00128341 /* Build configuration list for PBXProject "VideoPlayer" */;
145 | compatibilityVersion = "Xcode 9.3";
146 | developmentRegion = en;
147 | hasScannedForEncodings = 0;
148 | knownRegions = (
149 | en,
150 | Base,
151 | );
152 | mainGroup = 5BE8C04421EF12EE00128341;
153 | productRefGroup = 5BE8C04E21EF12EE00128341 /* Products */;
154 | projectDirPath = "";
155 | projectRoot = "";
156 | targets = (
157 | 5BE8C04C21EF12EE00128341 /* VideoPlayer */,
158 | );
159 | };
160 | /* End PBXProject section */
161 |
162 | /* Begin PBXResourcesBuildPhase section */
163 | 5BE8C04B21EF12EE00128341 /* Resources */ = {
164 | isa = PBXResourcesBuildPhase;
165 | buildActionMask = 2147483647;
166 | files = (
167 | 5BE8C05B21EF12F100128341 /* LaunchScreen.storyboard in Resources */,
168 | 5BE8C05821EF12F100128341 /* Assets.xcassets in Resources */,
169 | 5BE8C05621EF12EE00128341 /* Main.storyboard in Resources */,
170 | );
171 | runOnlyForDeploymentPostprocessing = 0;
172 | };
173 | /* End PBXResourcesBuildPhase section */
174 |
175 | /* Begin PBXSourcesBuildPhase section */
176 | 5BE8C04921EF12EE00128341 /* Sources */ = {
177 | isa = PBXSourcesBuildPhase;
178 | buildActionMask = 2147483647;
179 | files = (
180 | 5BE8C05321EF12EE00128341 /* ViewController.swift in Sources */,
181 | 5B77714C221E84D3003A28B4 /* PoAVPlayerCacheManager.swift in Sources */,
182 | 5BE8C07021F614F400128341 /* PoAVPlayerResourceLoader.swift in Sources */,
183 | 5BE8C06C21F60D1400128341 /* PoAVPlayerResourceLoaderDelegate.swift in Sources */,
184 | 5BE8C06E21F6102E00128341 /* String+extension.swift in Sources */,
185 | 5B0C2B362202D61A00FF2643 /* FragmentArray.swift in Sources */,
186 | 5BE8C06421EF136D00128341 /* PoAVPlayer.swift in Sources */,
187 | 5BE8C07321F6E38E00128341 /* PoAVPlayerResourceRequestTask.swift in Sources */,
188 | 5BE8C06621EF7F5900128341 /* PoAVPlayerControlView.swift in Sources */,
189 | 5BE8C05121EF12EE00128341 /* AppDelegate.swift in Sources */,
190 | 5B7D96BB21F849CD00883650 /* PoAVPlayerSessionDelegate.swift in Sources */,
191 | 5B9B37AD222FF767007556A6 /* PoProgressView.swift in Sources */,
192 | 5B0C2B382203074D00FF2643 /* PoAVPlayerResourceCacheFileHandler.swift in Sources */,
193 | );
194 | runOnlyForDeploymentPostprocessing = 0;
195 | };
196 | /* End PBXSourcesBuildPhase section */
197 |
198 | /* Begin PBXVariantGroup section */
199 | 5BE8C05421EF12EE00128341 /* Main.storyboard */ = {
200 | isa = PBXVariantGroup;
201 | children = (
202 | 5BE8C05521EF12EE00128341 /* Base */,
203 | );
204 | name = Main.storyboard;
205 | sourceTree = "";
206 | };
207 | 5BE8C05921EF12F100128341 /* LaunchScreen.storyboard */ = {
208 | isa = PBXVariantGroup;
209 | children = (
210 | 5BE8C05A21EF12F100128341 /* Base */,
211 | );
212 | name = LaunchScreen.storyboard;
213 | sourceTree = "";
214 | };
215 | /* End PBXVariantGroup section */
216 |
217 | /* Begin XCBuildConfiguration section */
218 | 5BE8C05D21EF12F100128341 /* Debug */ = {
219 | isa = XCBuildConfiguration;
220 | buildSettings = {
221 | ALWAYS_SEARCH_USER_PATHS = NO;
222 | CLANG_ANALYZER_NONNULL = YES;
223 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
224 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
225 | CLANG_CXX_LIBRARY = "libc++";
226 | CLANG_ENABLE_MODULES = YES;
227 | CLANG_ENABLE_OBJC_ARC = YES;
228 | CLANG_ENABLE_OBJC_WEAK = YES;
229 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
230 | CLANG_WARN_BOOL_CONVERSION = YES;
231 | CLANG_WARN_COMMA = YES;
232 | CLANG_WARN_CONSTANT_CONVERSION = YES;
233 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
234 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
235 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
236 | CLANG_WARN_EMPTY_BODY = YES;
237 | CLANG_WARN_ENUM_CONVERSION = YES;
238 | CLANG_WARN_INFINITE_RECURSION = YES;
239 | CLANG_WARN_INT_CONVERSION = YES;
240 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
241 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
242 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
243 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
244 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
245 | CLANG_WARN_STRICT_PROTOTYPES = YES;
246 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
247 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
248 | CLANG_WARN_UNREACHABLE_CODE = YES;
249 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
250 | CODE_SIGN_IDENTITY = "iPhone Developer";
251 | COPY_PHASE_STRIP = NO;
252 | DEBUG_INFORMATION_FORMAT = dwarf;
253 | ENABLE_STRICT_OBJC_MSGSEND = YES;
254 | ENABLE_TESTABILITY = YES;
255 | GCC_C_LANGUAGE_STANDARD = gnu11;
256 | GCC_DYNAMIC_NO_PIC = NO;
257 | GCC_NO_COMMON_BLOCKS = YES;
258 | GCC_OPTIMIZATION_LEVEL = 0;
259 | GCC_PREPROCESSOR_DEFINITIONS = (
260 | "DEBUG=1",
261 | "$(inherited)",
262 | );
263 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
264 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
265 | GCC_WARN_UNDECLARED_SELECTOR = YES;
266 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
267 | GCC_WARN_UNUSED_FUNCTION = YES;
268 | GCC_WARN_UNUSED_VARIABLE = YES;
269 | IPHONEOS_DEPLOYMENT_TARGET = 12.1;
270 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
271 | MTL_FAST_MATH = YES;
272 | ONLY_ACTIVE_ARCH = YES;
273 | SDKROOT = iphoneos;
274 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
275 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
276 | };
277 | name = Debug;
278 | };
279 | 5BE8C05E21EF12F100128341 /* Release */ = {
280 | isa = XCBuildConfiguration;
281 | buildSettings = {
282 | ALWAYS_SEARCH_USER_PATHS = NO;
283 | CLANG_ANALYZER_NONNULL = YES;
284 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
285 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
286 | CLANG_CXX_LIBRARY = "libc++";
287 | CLANG_ENABLE_MODULES = YES;
288 | CLANG_ENABLE_OBJC_ARC = YES;
289 | CLANG_ENABLE_OBJC_WEAK = YES;
290 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
291 | CLANG_WARN_BOOL_CONVERSION = YES;
292 | CLANG_WARN_COMMA = YES;
293 | CLANG_WARN_CONSTANT_CONVERSION = YES;
294 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
295 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
296 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
297 | CLANG_WARN_EMPTY_BODY = YES;
298 | CLANG_WARN_ENUM_CONVERSION = YES;
299 | CLANG_WARN_INFINITE_RECURSION = YES;
300 | CLANG_WARN_INT_CONVERSION = YES;
301 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
302 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
303 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
304 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
305 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
306 | CLANG_WARN_STRICT_PROTOTYPES = YES;
307 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
308 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
309 | CLANG_WARN_UNREACHABLE_CODE = YES;
310 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
311 | CODE_SIGN_IDENTITY = "iPhone Developer";
312 | COPY_PHASE_STRIP = NO;
313 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
314 | ENABLE_NS_ASSERTIONS = NO;
315 | ENABLE_STRICT_OBJC_MSGSEND = YES;
316 | GCC_C_LANGUAGE_STANDARD = gnu11;
317 | GCC_NO_COMMON_BLOCKS = YES;
318 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
319 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
320 | GCC_WARN_UNDECLARED_SELECTOR = YES;
321 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
322 | GCC_WARN_UNUSED_FUNCTION = YES;
323 | GCC_WARN_UNUSED_VARIABLE = YES;
324 | IPHONEOS_DEPLOYMENT_TARGET = 12.1;
325 | MTL_ENABLE_DEBUG_INFO = NO;
326 | MTL_FAST_MATH = YES;
327 | SDKROOT = iphoneos;
328 | SWIFT_COMPILATION_MODE = wholemodule;
329 | SWIFT_OPTIMIZATION_LEVEL = "-O";
330 | VALIDATE_PRODUCT = YES;
331 | };
332 | name = Release;
333 | };
334 | 5BE8C06021EF12F100128341 /* Debug */ = {
335 | isa = XCBuildConfiguration;
336 | buildSettings = {
337 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
338 | CODE_SIGN_IDENTITY = "iPhone Developer";
339 | CODE_SIGN_STYLE = Automatic;
340 | DEVELOPMENT_TEAM = HTCFQ79HMN;
341 | INFOPLIST_FILE = VideoPlayer/Info.plist;
342 | IPHONEOS_DEPLOYMENT_TARGET = 10.0;
343 | LD_RUNPATH_SEARCH_PATHS = (
344 | "$(inherited)",
345 | "@executable_path/Frameworks",
346 | );
347 | PRODUCT_BUNDLE_IDENTIFIER = com.hzs.VideoPlayer;
348 | PRODUCT_NAME = "$(TARGET_NAME)";
349 | PROVISIONING_PROFILE_SPECIFIER = "";
350 | SWIFT_VERSION = 5.0;
351 | TARGETED_DEVICE_FAMILY = "1,2";
352 | };
353 | name = Debug;
354 | };
355 | 5BE8C06121EF12F100128341 /* Release */ = {
356 | isa = XCBuildConfiguration;
357 | buildSettings = {
358 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
359 | CODE_SIGN_IDENTITY = "iPhone Developer";
360 | CODE_SIGN_STYLE = Automatic;
361 | DEVELOPMENT_TEAM = HTCFQ79HMN;
362 | INFOPLIST_FILE = VideoPlayer/Info.plist;
363 | IPHONEOS_DEPLOYMENT_TARGET = 10.0;
364 | LD_RUNPATH_SEARCH_PATHS = (
365 | "$(inherited)",
366 | "@executable_path/Frameworks",
367 | );
368 | PRODUCT_BUNDLE_IDENTIFIER = com.hzs.VideoPlayer;
369 | PRODUCT_NAME = "$(TARGET_NAME)";
370 | PROVISIONING_PROFILE_SPECIFIER = "";
371 | SWIFT_VERSION = 5.0;
372 | TARGETED_DEVICE_FAMILY = "1,2";
373 | };
374 | name = Release;
375 | };
376 | /* End XCBuildConfiguration section */
377 |
378 | /* Begin XCConfigurationList section */
379 | 5BE8C04821EF12EE00128341 /* Build configuration list for PBXProject "VideoPlayer" */ = {
380 | isa = XCConfigurationList;
381 | buildConfigurations = (
382 | 5BE8C05D21EF12F100128341 /* Debug */,
383 | 5BE8C05E21EF12F100128341 /* Release */,
384 | );
385 | defaultConfigurationIsVisible = 0;
386 | defaultConfigurationName = Release;
387 | };
388 | 5BE8C05F21EF12F100128341 /* Build configuration list for PBXNativeTarget "VideoPlayer" */ = {
389 | isa = XCConfigurationList;
390 | buildConfigurations = (
391 | 5BE8C06021EF12F100128341 /* Debug */,
392 | 5BE8C06121EF12F100128341 /* Release */,
393 | );
394 | defaultConfigurationIsVisible = 0;
395 | defaultConfigurationName = Release;
396 | };
397 | /* End XCConfigurationList section */
398 | };
399 | rootObject = 5BE8C04521EF12EE00128341 /* Project object */;
400 | }
401 |
--------------------------------------------------------------------------------