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