├── .gitignore ├── Demo ├── Demo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Demo.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Demo │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── video_cover.imageset │ │ │ ├── Contents.json │ │ │ └── video_cover.png │ │ ├── video_pause.imageset │ │ │ ├── Contents.json │ │ │ └── video_detail_pause.pdf │ │ ├── video_play.imageset │ │ │ ├── Contents.json │ │ │ └── video_detail_play.pdf │ │ └── video_slider.imageset │ │ │ ├── Contents.json │ │ │ └── video_detail_slider.pdf │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ ├── PL │ │ ├── PLVideoPlayer.swift │ │ └── WeakObject.swift │ ├── PictureInPicture.swift │ ├── SimplePlayerViewController.swift │ ├── Storyboard.swift │ ├── View │ │ ├── VideoPlayerControlView.swift │ │ ├── VideoPlayerCoverView.swift │ │ ├── VideoPlayerErrorView.swift │ │ ├── VideoPlayerFinishView.swift │ │ ├── VideoPlayerProvider.swift │ │ └── VideoPlayerSlider.swift │ └── ViewController.swift ├── Podfile └── Podfile.lock ├── LICENSE ├── README.md ├── Sources ├── AV │ ├── AVVideoPlayer.swift │ └── AVVideoResourceLoader.swift ├── Core │ ├── VideoPlayer.swift │ ├── VideoPlayerConfiguration.swift │ ├── VideoPlayerDelegate.swift │ ├── VideoPlayerDelegates.swift │ ├── VideoPlayerRemote.swift │ ├── VideoPlayerView.swift │ └── VideoPlayerable.swift ├── Info.plist ├── PrivacyInfo.xcprivacy └── VideoPlayer.h ├── VideoPlayer.podspec └── VideoPlayer.xcodeproj ├── project.pbxproj ├── project.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist └── xcshareddata └── xcschemes └── VideoPlayer.xcscheme /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 51; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 6A595E95EAE9E20D30D1A8D6 /* Pods_Demo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DF996BB2479B7EE857452E0D /* Pods_Demo.framework */; }; 11 | 9B39F44D23169BAA00505AD0 /* SimplePlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B39F44C23169BAA00505AD0 /* SimplePlayerViewController.swift */; }; 12 | 9B39F44F23169C8900505AD0 /* Storyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B39F44E23169C8900505AD0 /* Storyboard.swift */; }; 13 | 9B5CC42E2832651A00435CFA /* PictureInPicture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B5CC42D2832651A00435CFA /* PictureInPicture.swift */; }; 14 | 9B69F78825243032006B3F22 /* PLVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B69F78625243032006B3F22 /* PLVideoPlayer.swift */; }; 15 | 9B69F78925243032006B3F22 /* WeakObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B69F78725243032006B3F22 /* WeakObject.swift */; }; 16 | 9B8B2533231CF08F00FCC4C0 /* VideoPlayerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B8B252D231CF08F00FCC4C0 /* VideoPlayerProvider.swift */; }; 17 | 9B8B2534231CF08F00FCC4C0 /* VideoPlayerCoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B8B252E231CF08F00FCC4C0 /* VideoPlayerCoverView.swift */; }; 18 | 9B8B2535231CF08F00FCC4C0 /* VideoPlayerErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B8B252F231CF08F00FCC4C0 /* VideoPlayerErrorView.swift */; }; 19 | 9B8B2536231CF08F00FCC4C0 /* VideoPlayerControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B8B2530231CF08F00FCC4C0 /* VideoPlayerControlView.swift */; }; 20 | 9B8B2537231CF08F00FCC4C0 /* VideoPlayerFinishView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B8B2531231CF08F00FCC4C0 /* VideoPlayerFinishView.swift */; }; 21 | 9B8B2538231CF08F00FCC4C0 /* VideoPlayerSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B8B2532231CF08F00FCC4C0 /* VideoPlayerSlider.swift */; }; 22 | C9D028B222607F8200B5E061 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D028B122607F8200B5E061 /* AppDelegate.swift */; }; 23 | C9D028B422607F8200B5E061 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D028B322607F8200B5E061 /* ViewController.swift */; }; 24 | C9D028B722607F8200B5E061 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C9D028B522607F8200B5E061 /* Main.storyboard */; }; 25 | C9D028B922607F8200B5E061 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C9D028B822607F8200B5E061 /* Assets.xcassets */; }; 26 | C9D028BC22607F8200B5E061 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C9D028BA22607F8200B5E061 /* LaunchScreen.storyboard */; }; 27 | /* End PBXBuildFile section */ 28 | 29 | /* Begin PBXFileReference section */ 30 | 4FD45B063BA419F7B2949502 /* Pods-Demo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Demo.debug.xcconfig"; path = "Target Support Files/Pods-Demo/Pods-Demo.debug.xcconfig"; sourceTree = ""; }; 31 | 9B39F44C23169BAA00505AD0 /* SimplePlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimplePlayerViewController.swift; sourceTree = ""; }; 32 | 9B39F44E23169C8900505AD0 /* Storyboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storyboard.swift; sourceTree = ""; }; 33 | 9B5CC42D2832651A00435CFA /* PictureInPicture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PictureInPicture.swift; sourceTree = ""; }; 34 | 9B69F78625243032006B3F22 /* PLVideoPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PLVideoPlayer.swift; sourceTree = ""; }; 35 | 9B69F78725243032006B3F22 /* WeakObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakObject.swift; sourceTree = ""; }; 36 | 9B8B252D231CF08F00FCC4C0 /* VideoPlayerProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerProvider.swift; sourceTree = ""; }; 37 | 9B8B252E231CF08F00FCC4C0 /* VideoPlayerCoverView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerCoverView.swift; sourceTree = ""; }; 38 | 9B8B252F231CF08F00FCC4C0 /* VideoPlayerErrorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerErrorView.swift; sourceTree = ""; }; 39 | 9B8B2530231CF08F00FCC4C0 /* VideoPlayerControlView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerControlView.swift; sourceTree = ""; }; 40 | 9B8B2531231CF08F00FCC4C0 /* VideoPlayerFinishView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerFinishView.swift; sourceTree = ""; }; 41 | 9B8B2532231CF08F00FCC4C0 /* VideoPlayerSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerSlider.swift; sourceTree = ""; }; 42 | C9D028AE22607F8200B5E061 /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 43 | C9D028B122607F8200B5E061 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 44 | C9D028B322607F8200B5E061 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 45 | C9D028B622607F8200B5E061 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 46 | C9D028B822607F8200B5E061 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 47 | C9D028BB22607F8200B5E061 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 48 | C9D028BD22607F8200B5E061 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 49 | CEB7AEFC933E088EF7C01816 /* Pods-Demo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Demo.release.xcconfig"; path = "Target Support Files/Pods-Demo/Pods-Demo.release.xcconfig"; sourceTree = ""; }; 50 | DF996BB2479B7EE857452E0D /* Pods_Demo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Demo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | /* End PBXFileReference section */ 52 | 53 | /* Begin PBXFrameworksBuildPhase section */ 54 | C9D028AB22607F8200B5E061 /* Frameworks */ = { 55 | isa = PBXFrameworksBuildPhase; 56 | buildActionMask = 2147483647; 57 | files = ( 58 | 6A595E95EAE9E20D30D1A8D6 /* Pods_Demo.framework in Frameworks */, 59 | ); 60 | runOnlyForDeploymentPostprocessing = 0; 61 | }; 62 | /* End PBXFrameworksBuildPhase section */ 63 | 64 | /* Begin PBXGroup section */ 65 | 3E4CDB657E061C16E3075E06 /* Pods */ = { 66 | isa = PBXGroup; 67 | children = ( 68 | 4FD45B063BA419F7B2949502 /* Pods-Demo.debug.xcconfig */, 69 | CEB7AEFC933E088EF7C01816 /* Pods-Demo.release.xcconfig */, 70 | ); 71 | path = Pods; 72 | sourceTree = ""; 73 | }; 74 | 9B69F78525243032006B3F22 /* PL */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | 9B69F78625243032006B3F22 /* PLVideoPlayer.swift */, 78 | 9B69F78725243032006B3F22 /* WeakObject.swift */, 79 | ); 80 | path = PL; 81 | sourceTree = ""; 82 | }; 83 | 9B8B252C231CF08F00FCC4C0 /* View */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | 9B8B252D231CF08F00FCC4C0 /* VideoPlayerProvider.swift */, 87 | 9B8B252E231CF08F00FCC4C0 /* VideoPlayerCoverView.swift */, 88 | 9B8B252F231CF08F00FCC4C0 /* VideoPlayerErrorView.swift */, 89 | 9B8B2530231CF08F00FCC4C0 /* VideoPlayerControlView.swift */, 90 | 9B8B2531231CF08F00FCC4C0 /* VideoPlayerFinishView.swift */, 91 | 9B8B2532231CF08F00FCC4C0 /* VideoPlayerSlider.swift */, 92 | ); 93 | path = View; 94 | sourceTree = ""; 95 | }; 96 | C9D028A522607F8200B5E061 = { 97 | isa = PBXGroup; 98 | children = ( 99 | C9D028B022607F8200B5E061 /* Demo */, 100 | C9D028AF22607F8200B5E061 /* Products */, 101 | 3E4CDB657E061C16E3075E06 /* Pods */, 102 | EA37C84C061630303B9B0626 /* Frameworks */, 103 | ); 104 | sourceTree = ""; 105 | }; 106 | C9D028AF22607F8200B5E061 /* Products */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | C9D028AE22607F8200B5E061 /* Demo.app */, 110 | ); 111 | name = Products; 112 | sourceTree = ""; 113 | }; 114 | C9D028B022607F8200B5E061 /* Demo */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | 9B69F78525243032006B3F22 /* PL */, 118 | 9B8B252C231CF08F00FCC4C0 /* View */, 119 | 9B39F44E23169C8900505AD0 /* Storyboard.swift */, 120 | C9D028B122607F8200B5E061 /* AppDelegate.swift */, 121 | C9D028B322607F8200B5E061 /* ViewController.swift */, 122 | 9B39F44C23169BAA00505AD0 /* SimplePlayerViewController.swift */, 123 | C9D028B522607F8200B5E061 /* Main.storyboard */, 124 | C9D028B822607F8200B5E061 /* Assets.xcassets */, 125 | C9D028BA22607F8200B5E061 /* LaunchScreen.storyboard */, 126 | C9D028BD22607F8200B5E061 /* Info.plist */, 127 | 9B5CC42D2832651A00435CFA /* PictureInPicture.swift */, 128 | ); 129 | path = Demo; 130 | sourceTree = ""; 131 | }; 132 | EA37C84C061630303B9B0626 /* Frameworks */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | DF996BB2479B7EE857452E0D /* Pods_Demo.framework */, 136 | ); 137 | name = Frameworks; 138 | sourceTree = ""; 139 | }; 140 | /* End PBXGroup section */ 141 | 142 | /* Begin PBXNativeTarget section */ 143 | C9D028AD22607F8200B5E061 /* Demo */ = { 144 | isa = PBXNativeTarget; 145 | buildConfigurationList = C9D028C022607F8200B5E061 /* Build configuration list for PBXNativeTarget "Demo" */; 146 | buildPhases = ( 147 | 9D62DF0D51785BD3712034FB /* [CP] Check Pods Manifest.lock */, 148 | C9D028AA22607F8200B5E061 /* Sources */, 149 | C9D028AB22607F8200B5E061 /* Frameworks */, 150 | C9D028AC22607F8200B5E061 /* Resources */, 151 | 2E6B551DCCBC94FBB8FCB0B7 /* [CP] Embed Pods Frameworks */, 152 | ); 153 | buildRules = ( 154 | ); 155 | dependencies = ( 156 | ); 157 | name = Demo; 158 | productName = Demo; 159 | productReference = C9D028AE22607F8200B5E061 /* Demo.app */; 160 | productType = "com.apple.product-type.application"; 161 | }; 162 | /* End PBXNativeTarget section */ 163 | 164 | /* Begin PBXProject section */ 165 | C9D028A622607F8200B5E061 /* Project object */ = { 166 | isa = PBXProject; 167 | attributes = { 168 | LastSwiftUpdateCheck = 1020; 169 | LastUpgradeCheck = 1020; 170 | ORGANIZATIONNAME = swift; 171 | TargetAttributes = { 172 | C9D028AD22607F8200B5E061 = { 173 | CreatedOnToolsVersion = 10.2; 174 | SystemCapabilities = { 175 | com.apple.BackgroundModes = { 176 | enabled = 1; 177 | }; 178 | }; 179 | }; 180 | }; 181 | }; 182 | buildConfigurationList = C9D028A922607F8200B5E061 /* Build configuration list for PBXProject "Demo" */; 183 | compatibilityVersion = "Xcode 9.3"; 184 | developmentRegion = en; 185 | hasScannedForEncodings = 0; 186 | knownRegions = ( 187 | en, 188 | Base, 189 | ); 190 | mainGroup = C9D028A522607F8200B5E061; 191 | productRefGroup = C9D028AF22607F8200B5E061 /* Products */; 192 | projectDirPath = ""; 193 | projectRoot = ""; 194 | targets = ( 195 | C9D028AD22607F8200B5E061 /* Demo */, 196 | ); 197 | }; 198 | /* End PBXProject section */ 199 | 200 | /* Begin PBXResourcesBuildPhase section */ 201 | C9D028AC22607F8200B5E061 /* Resources */ = { 202 | isa = PBXResourcesBuildPhase; 203 | buildActionMask = 2147483647; 204 | files = ( 205 | C9D028BC22607F8200B5E061 /* LaunchScreen.storyboard in Resources */, 206 | C9D028B922607F8200B5E061 /* Assets.xcassets in Resources */, 207 | C9D028B722607F8200B5E061 /* Main.storyboard in Resources */, 208 | ); 209 | runOnlyForDeploymentPostprocessing = 0; 210 | }; 211 | /* End PBXResourcesBuildPhase section */ 212 | 213 | /* Begin PBXShellScriptBuildPhase section */ 214 | 2E6B551DCCBC94FBB8FCB0B7 /* [CP] Embed Pods Frameworks */ = { 215 | isa = PBXShellScriptBuildPhase; 216 | buildActionMask = 2147483647; 217 | files = ( 218 | ); 219 | inputFileListPaths = ( 220 | "${PODS_ROOT}/Target Support Files/Pods-Demo/Pods-Demo-frameworks-${CONFIGURATION}-input-files.xcfilelist", 221 | ); 222 | name = "[CP] Embed Pods Frameworks"; 223 | outputFileListPaths = ( 224 | "${PODS_ROOT}/Target Support Files/Pods-Demo/Pods-Demo-frameworks-${CONFIGURATION}-output-files.xcfilelist", 225 | ); 226 | runOnlyForDeploymentPostprocessing = 0; 227 | shellPath = /bin/sh; 228 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Demo/Pods-Demo-frameworks.sh\"\n"; 229 | showEnvVarsInLog = 0; 230 | }; 231 | 9D62DF0D51785BD3712034FB /* [CP] Check Pods Manifest.lock */ = { 232 | isa = PBXShellScriptBuildPhase; 233 | buildActionMask = 2147483647; 234 | files = ( 235 | ); 236 | inputFileListPaths = ( 237 | ); 238 | inputPaths = ( 239 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 240 | "${PODS_ROOT}/Manifest.lock", 241 | ); 242 | name = "[CP] Check Pods Manifest.lock"; 243 | outputFileListPaths = ( 244 | ); 245 | outputPaths = ( 246 | "$(DERIVED_FILE_DIR)/Pods-Demo-checkManifestLockResult.txt", 247 | ); 248 | runOnlyForDeploymentPostprocessing = 0; 249 | shellPath = /bin/sh; 250 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 251 | showEnvVarsInLog = 0; 252 | }; 253 | /* End PBXShellScriptBuildPhase section */ 254 | 255 | /* Begin PBXSourcesBuildPhase section */ 256 | C9D028AA22607F8200B5E061 /* Sources */ = { 257 | isa = PBXSourcesBuildPhase; 258 | buildActionMask = 2147483647; 259 | files = ( 260 | 9B8B2537231CF08F00FCC4C0 /* VideoPlayerFinishView.swift in Sources */, 261 | 9B8B2534231CF08F00FCC4C0 /* VideoPlayerCoverView.swift in Sources */, 262 | 9B69F78925243032006B3F22 /* WeakObject.swift in Sources */, 263 | C9D028B422607F8200B5E061 /* ViewController.swift in Sources */, 264 | 9B5CC42E2832651A00435CFA /* PictureInPicture.swift in Sources */, 265 | 9B8B2533231CF08F00FCC4C0 /* VideoPlayerProvider.swift in Sources */, 266 | 9B39F44F23169C8900505AD0 /* Storyboard.swift in Sources */, 267 | 9B8B2538231CF08F00FCC4C0 /* VideoPlayerSlider.swift in Sources */, 268 | 9B69F78825243032006B3F22 /* PLVideoPlayer.swift in Sources */, 269 | 9B8B2536231CF08F00FCC4C0 /* VideoPlayerControlView.swift in Sources */, 270 | 9B39F44D23169BAA00505AD0 /* SimplePlayerViewController.swift in Sources */, 271 | 9B8B2535231CF08F00FCC4C0 /* VideoPlayerErrorView.swift in Sources */, 272 | C9D028B222607F8200B5E061 /* AppDelegate.swift in Sources */, 273 | ); 274 | runOnlyForDeploymentPostprocessing = 0; 275 | }; 276 | /* End PBXSourcesBuildPhase section */ 277 | 278 | /* Begin PBXVariantGroup section */ 279 | C9D028B522607F8200B5E061 /* Main.storyboard */ = { 280 | isa = PBXVariantGroup; 281 | children = ( 282 | C9D028B622607F8200B5E061 /* Base */, 283 | ); 284 | name = Main.storyboard; 285 | sourceTree = ""; 286 | }; 287 | C9D028BA22607F8200B5E061 /* LaunchScreen.storyboard */ = { 288 | isa = PBXVariantGroup; 289 | children = ( 290 | C9D028BB22607F8200B5E061 /* Base */, 291 | ); 292 | name = LaunchScreen.storyboard; 293 | sourceTree = ""; 294 | }; 295 | /* End PBXVariantGroup section */ 296 | 297 | /* Begin XCBuildConfiguration section */ 298 | C9D028BE22607F8200B5E061 /* Debug */ = { 299 | isa = XCBuildConfiguration; 300 | buildSettings = { 301 | ALWAYS_SEARCH_USER_PATHS = NO; 302 | CLANG_ANALYZER_NONNULL = YES; 303 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 304 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 305 | CLANG_CXX_LIBRARY = "libc++"; 306 | CLANG_ENABLE_MODULES = YES; 307 | CLANG_ENABLE_OBJC_ARC = YES; 308 | CLANG_ENABLE_OBJC_WEAK = YES; 309 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 310 | CLANG_WARN_BOOL_CONVERSION = YES; 311 | CLANG_WARN_COMMA = YES; 312 | CLANG_WARN_CONSTANT_CONVERSION = YES; 313 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 314 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 315 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 316 | CLANG_WARN_EMPTY_BODY = YES; 317 | CLANG_WARN_ENUM_CONVERSION = YES; 318 | CLANG_WARN_INFINITE_RECURSION = YES; 319 | CLANG_WARN_INT_CONVERSION = YES; 320 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 321 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 322 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 323 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 324 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 325 | CLANG_WARN_STRICT_PROTOTYPES = YES; 326 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 327 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 328 | CLANG_WARN_UNREACHABLE_CODE = YES; 329 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 330 | CODE_SIGN_IDENTITY = "iPhone Developer"; 331 | COPY_PHASE_STRIP = NO; 332 | DEBUG_INFORMATION_FORMAT = dwarf; 333 | ENABLE_STRICT_OBJC_MSGSEND = YES; 334 | ENABLE_TESTABILITY = YES; 335 | GCC_C_LANGUAGE_STANDARD = gnu11; 336 | GCC_DYNAMIC_NO_PIC = NO; 337 | GCC_NO_COMMON_BLOCKS = YES; 338 | GCC_OPTIMIZATION_LEVEL = 0; 339 | GCC_PREPROCESSOR_DEFINITIONS = ( 340 | "DEBUG=1", 341 | "$(inherited)", 342 | ); 343 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 344 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 345 | GCC_WARN_UNDECLARED_SELECTOR = YES; 346 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 347 | GCC_WARN_UNUSED_FUNCTION = YES; 348 | GCC_WARN_UNUSED_VARIABLE = YES; 349 | IPHONEOS_DEPLOYMENT_TARGET = 12.2; 350 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 351 | MTL_FAST_MATH = YES; 352 | ONLY_ACTIVE_ARCH = YES; 353 | SDKROOT = iphoneos; 354 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 355 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 356 | }; 357 | name = Debug; 358 | }; 359 | C9D028BF22607F8200B5E061 /* Release */ = { 360 | isa = XCBuildConfiguration; 361 | buildSettings = { 362 | ALWAYS_SEARCH_USER_PATHS = NO; 363 | CLANG_ANALYZER_NONNULL = YES; 364 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 365 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 366 | CLANG_CXX_LIBRARY = "libc++"; 367 | CLANG_ENABLE_MODULES = YES; 368 | CLANG_ENABLE_OBJC_ARC = YES; 369 | CLANG_ENABLE_OBJC_WEAK = YES; 370 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 371 | CLANG_WARN_BOOL_CONVERSION = YES; 372 | CLANG_WARN_COMMA = YES; 373 | CLANG_WARN_CONSTANT_CONVERSION = YES; 374 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 375 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 376 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 377 | CLANG_WARN_EMPTY_BODY = YES; 378 | CLANG_WARN_ENUM_CONVERSION = YES; 379 | CLANG_WARN_INFINITE_RECURSION = YES; 380 | CLANG_WARN_INT_CONVERSION = YES; 381 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 382 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 383 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 384 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 385 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 386 | CLANG_WARN_STRICT_PROTOTYPES = YES; 387 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 388 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 389 | CLANG_WARN_UNREACHABLE_CODE = YES; 390 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 391 | CODE_SIGN_IDENTITY = "iPhone Developer"; 392 | COPY_PHASE_STRIP = NO; 393 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 394 | ENABLE_NS_ASSERTIONS = NO; 395 | ENABLE_STRICT_OBJC_MSGSEND = YES; 396 | GCC_C_LANGUAGE_STANDARD = gnu11; 397 | GCC_NO_COMMON_BLOCKS = YES; 398 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 399 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 400 | GCC_WARN_UNDECLARED_SELECTOR = YES; 401 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 402 | GCC_WARN_UNUSED_FUNCTION = YES; 403 | GCC_WARN_UNUSED_VARIABLE = YES; 404 | IPHONEOS_DEPLOYMENT_TARGET = 12.2; 405 | MTL_ENABLE_DEBUG_INFO = NO; 406 | MTL_FAST_MATH = YES; 407 | SDKROOT = iphoneos; 408 | SWIFT_COMPILATION_MODE = wholemodule; 409 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 410 | VALIDATE_PRODUCT = YES; 411 | }; 412 | name = Release; 413 | }; 414 | C9D028C122607F8200B5E061 /* Debug */ = { 415 | isa = XCBuildConfiguration; 416 | baseConfigurationReference = 4FD45B063BA419F7B2949502 /* Pods-Demo.debug.xcconfig */; 417 | buildSettings = { 418 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 419 | CODE_SIGN_STYLE = Automatic; 420 | DEVELOPMENT_TEAM = 8G74YECJ4Z; 421 | INFOPLIST_FILE = Demo/Info.plist; 422 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 423 | LD_RUNPATH_SEARCH_PATHS = ( 424 | "$(inherited)", 425 | "@executable_path/Frameworks", 426 | ); 427 | PRODUCT_BUNDLE_IDENTIFIER = com.video.player.demo; 428 | PRODUCT_NAME = "$(TARGET_NAME)"; 429 | SWIFT_VERSION = 5.0; 430 | TARGETED_DEVICE_FAMILY = "1,2"; 431 | }; 432 | name = Debug; 433 | }; 434 | C9D028C222607F8200B5E061 /* Release */ = { 435 | isa = XCBuildConfiguration; 436 | baseConfigurationReference = CEB7AEFC933E088EF7C01816 /* Pods-Demo.release.xcconfig */; 437 | buildSettings = { 438 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 439 | CODE_SIGN_STYLE = Automatic; 440 | DEVELOPMENT_TEAM = 8G74YECJ4Z; 441 | INFOPLIST_FILE = Demo/Info.plist; 442 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 443 | LD_RUNPATH_SEARCH_PATHS = ( 444 | "$(inherited)", 445 | "@executable_path/Frameworks", 446 | ); 447 | PRODUCT_BUNDLE_IDENTIFIER = com.video.player.demo; 448 | PRODUCT_NAME = "$(TARGET_NAME)"; 449 | SWIFT_VERSION = 5.0; 450 | TARGETED_DEVICE_FAMILY = "1,2"; 451 | }; 452 | name = Release; 453 | }; 454 | /* End XCBuildConfiguration section */ 455 | 456 | /* Begin XCConfigurationList section */ 457 | C9D028A922607F8200B5E061 /* Build configuration list for PBXProject "Demo" */ = { 458 | isa = XCConfigurationList; 459 | buildConfigurations = ( 460 | C9D028BE22607F8200B5E061 /* Debug */, 461 | C9D028BF22607F8200B5E061 /* Release */, 462 | ); 463 | defaultConfigurationIsVisible = 0; 464 | defaultConfigurationName = Release; 465 | }; 466 | C9D028C022607F8200B5E061 /* Build configuration list for PBXNativeTarget "Demo" */ = { 467 | isa = XCConfigurationList; 468 | buildConfigurations = ( 469 | C9D028C122607F8200B5E061 /* Debug */, 470 | C9D028C222607F8200B5E061 /* Release */, 471 | ); 472 | defaultConfigurationIsVisible = 0; 473 | defaultConfigurationName = Release; 474 | }; 475 | /* End XCConfigurationList section */ 476 | }; 477 | rootObject = C9D028A622607F8200B5E061 /* Project object */; 478 | } 479 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/Demo.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Demo/Demo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/Demo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Demo 4 | // 5 | // Created by 李响 on 2019/4/12. 6 | // Copyright © 2019 swift. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | static var shared: AppDelegate { 15 | return UIApplication.shared.delegate as! AppDelegate 16 | } 17 | 18 | var window: UIWindow? 19 | 20 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 21 | 22 | return true 23 | } 24 | 25 | func applicationWillResignActive(_ application: UIApplication) { 26 | // 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. 27 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 28 | } 29 | 30 | func applicationDidEnterBackground(_ application: UIApplication) { 31 | // 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. 32 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 33 | } 34 | 35 | func applicationWillEnterForeground(_ application: UIApplication) { 36 | // 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. 37 | } 38 | 39 | func applicationDidBecomeActive(_ application: UIApplication) { 40 | // 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. 41 | } 42 | 43 | func applicationWillTerminate(_ application: UIApplication) { 44 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Demo/Demo/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 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/video_cover.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "video_cover.png" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/video_cover.imageset/video_cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiang1994/VideoPlayer/efa21e26d676d416f0eea5aadeb764d4ca365456/Demo/Demo/Assets.xcassets/video_cover.imageset/video_cover.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/video_pause.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "video_detail_pause.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/video_pause.imageset/video_detail_pause.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiang1994/VideoPlayer/efa21e26d676d416f0eea5aadeb764d4ca365456/Demo/Demo/Assets.xcassets/video_pause.imageset/video_detail_pause.pdf -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/video_play.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "video_detail_play.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/video_play.imageset/video_detail_play.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiang1994/VideoPlayer/efa21e26d676d416f0eea5aadeb764d4ca365456/Demo/Demo/Assets.xcassets/video_play.imageset/video_detail_play.pdf -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/video_slider.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "video_detail_slider.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/video_slider.imageset/video_detail_slider.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiang1994/VideoPlayer/efa21e26d676d416f0eea5aadeb764d4ca365456/Demo/Demo/Assets.xcassets/video_slider.imageset/video_detail_slider.pdf -------------------------------------------------------------------------------- /Demo/Demo/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 | -------------------------------------------------------------------------------- /Demo/Demo/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 | 26 | 27 | 28 | 29 | 30 | 31 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /Demo/Demo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | VideoPlayer 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleURLTypes 22 | 23 | 24 | CFBundleTypeRole 25 | Editor 26 | CFBundleURLSchemes 27 | 28 | router 29 | 30 | 31 | 32 | CFBundleVersion 33 | 1 34 | LSRequiresIPhoneOS 35 | 36 | NSAppTransportSecurity 37 | 38 | NSAllowsArbitraryLoads 39 | 40 | 41 | UIBackgroundModes 42 | 43 | audio 44 | 45 | UILaunchStoryboardName 46 | LaunchScreen 47 | UIMainStoryboardFile 48 | Main 49 | UIRequiredDeviceCapabilities 50 | 51 | armv7 52 | 53 | UISupportedInterfaceOrientations 54 | 55 | UIInterfaceOrientationPortrait 56 | 57 | UISupportedInterfaceOrientations~ipad 58 | 59 | UIInterfaceOrientationPortrait 60 | UIInterfaceOrientationPortraitUpsideDown 61 | UIInterfaceOrientationLandscapeLeft 62 | UIInterfaceOrientationLandscapeRight 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /Demo/Demo/PL/PLVideoPlayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PLVideoPlayer.swift 3 | // ┌─┐ ┌───────┐ ┌───────┐ 4 | // │ │ │ ┌─────┘ │ ┌─────┘ 5 | // │ │ │ └─────┐ │ └─────┐ 6 | // │ │ │ ┌─────┘ │ ┌─────┘ 7 | // │ └─────┐│ └─────┐ │ └─────┐ 8 | // └───────┘└───────┘ └───────┘ 9 | // 10 | import UIKit 11 | import VideoPlayer 12 | 13 | #if !targetEnvironment(simulator) 14 | 15 | import PLPlayerKit 16 | 17 | extension VideoPlayer { 18 | 19 | static let pl: Builder = .init { PLVideoPlayer($0) } 20 | } 21 | 22 | class PLVideoPlayer: NSObject { 23 | 24 | /// 当前URL 25 | private(set) var resource: VideoPlayerURLAsset? 26 | 27 | /// 配置 28 | private(set) var configuration: VideoPlayerConfiguration 29 | 30 | /// 播放状态 31 | private (set) var state: VideoPlayer.State = .stopped { 32 | didSet { 33 | delegate { $0.videoPlayerState(self, state: state) } 34 | } 35 | } 36 | 37 | /// 控制状态 38 | private(set) var control: VideoPlayer.ControlState = .pausing { 39 | didSet { 40 | delegate { $0.videoPlayerControlState(self, state: control) } 41 | } 42 | } 43 | 44 | /// 加载状态 45 | private(set) var loading: VideoPlayer.LoadingState = .ended { 46 | didSet { 47 | delegate { $0.videoPlayerLoadingState(self, state: loading) } 48 | } 49 | } 50 | 51 | private(set) var buffer: Double = 0 52 | 53 | /// 播放速率 0.5 - 2.0 54 | var rate: Double = 1.0 { 55 | didSet { player?.playSpeed = rate } 56 | } 57 | /// 音量 0 - 1 58 | var volume: Double = 1.0 { 59 | didSet { player?.setVolume(.init(volume)) } 60 | } 61 | /// 是否静音 62 | var isMuted: Bool = false { 63 | didSet { 64 | player?.isMute = isMuted 65 | } 66 | } 67 | /// 是否循环播放 68 | var isLoop: Bool = false { 69 | didSet { 70 | player?.loopPlay = isLoop 71 | } 72 | } 73 | 74 | var delegates: [VideoPlayerDelegateBridge] = [] 75 | 76 | private var playTimer: Timer? 77 | private var player: PLPlayer? 78 | private var playerView = VideoPlayerView(.init()) 79 | 80 | /// 当前时间校准器 用于解决时间精度偏差问题. 81 | private var currentTimeCalibrator: TimeInterval? 82 | /// 跳转意图 例如当非playing状态时 如果调用了seek(to:) 记录状态 在playing时设置跳转 83 | private var intendedToSeek: VideoPlayer.Seek? 84 | /// 播放意图 例如当seeking时如果调用了play() 或者 pasue() 记录状态 在seeking结束时设置对应状态 85 | private var intendedToPlay: Bool = false 86 | 87 | init(_ configuration: VideoPlayerConfiguration) { 88 | self.configuration = configuration 89 | super.init() 90 | 91 | setup() 92 | setupNotification() 93 | } 94 | 95 | private func setup() { 96 | rate = 1.0 97 | volume = 1.0 98 | isMuted = false 99 | isLoop = false 100 | 101 | let timer = Timer( 102 | timeInterval: 0.1, 103 | target: WeakObject(self), 104 | selector: #selector(timerAction), 105 | userInfo: nil, 106 | repeats: true 107 | ) 108 | RunLoop.main.add(timer, forMode: .common) 109 | timer.fireDate = .distantFuture 110 | playTimer = timer 111 | } 112 | 113 | private func setupNotification() { 114 | NotificationCenter.default.addObserver( 115 | self, 116 | selector: #selector(sessionRouteChange), 117 | name: AVAudioSession.routeChangeNotification, 118 | object: AVAudioSession.sharedInstance() 119 | ) 120 | 121 | NotificationCenter.default.addObserver( 122 | self, 123 | selector: #selector(sessionInterruption), 124 | name: AVAudioSession.interruptionNotification, 125 | object: AVAudioSession.sharedInstance() 126 | ) 127 | } 128 | 129 | private func pauseNoUser() { 130 | player?.pause() 131 | } 132 | 133 | deinit { 134 | playTimer?.invalidate() 135 | playTimer = nil 136 | } 137 | } 138 | 139 | extension PLVideoPlayer { 140 | 141 | @objc func timerAction() { 142 | delegate { $0.videoPlayer(self, updatedCurrent: current) } 143 | delegate { $0.videoPlayer(self, updatedDuration: duration) } 144 | } 145 | 146 | /// 会话线路变更通知 147 | @objc func sessionRouteChange(_ notification: NSNotification) { 148 | guard 149 | let info = notification.userInfo, 150 | let reason = info[AVAudioSessionRouteChangeReasonKey] as? Int else { 151 | return 152 | } 153 | guard let _ = player else { return } 154 | 155 | switch AVAudioSession.RouteChangeReason(rawValue: .init(reason)) { 156 | case .oldDeviceUnavailable?: 157 | DispatchQueue.main.async { [weak self] in 158 | self?.pauseNoUser() 159 | } 160 | default: break 161 | } 162 | } 163 | 164 | /// 会话中断通知 165 | @objc 166 | private func sessionInterruption(_ notification: Notification) { 167 | guard 168 | let info = notification.userInfo, 169 | let type = info[AVAudioSessionInterruptionTypeKey] as? Int else { 170 | return 171 | } 172 | 173 | switch AVAudioSession.InterruptionType(rawValue: .init(type)) { 174 | case .began? where intendedToPlay: 175 | pauseNoUser() 176 | 177 | case .ended? where intendedToPlay: 178 | play() 179 | 180 | default: 181 | break 182 | } 183 | } 184 | } 185 | 186 | extension PLVideoPlayer { 187 | 188 | /// 清理 189 | private func clear() { 190 | player?.stop() 191 | playTimer?.fireDate = .distantFuture 192 | // 重置缓冲进度 193 | buffer = 0 194 | // 重置加载状态 195 | loading = .ended 196 | // 清空资源 197 | resource = nil 198 | // 清理意图 199 | intendedToSeek = nil 200 | intendedToPlay = false 201 | } 202 | 203 | /// 错误 204 | private func error(_ value: Swift.Error?) { 205 | clear() 206 | state = .failed(value) 207 | } 208 | } 209 | 210 | extension PLVideoPlayer: PLPlayerDelegate { 211 | /* 212 | PLPlayerStatusUnknow 初始化时指定的状态,不会有任何状态会跳转到这一状态 213 | PLPlayerStatusPreparing 播放器正在准备当中 214 | PLPlayerStatusReady 播放器准备完成的状态 215 | PLPlayerStatusOpen 播放器准备开始连接的状态 216 | PLPlayerStatusCaching 播放器正在缓存的状态 217 | PLPlayerStatusPlaying 播放器正在播放的状态 218 | PLPlayerStatusPaused 播放器暂停的状态 219 | PLPlayerStatusStopped 播放器播放结束或手动停止的状态 220 | PLPlayerStatusError 播放器出现错误的状态 221 | PLPlayerStateAutoReconnecting 播放器开始自动重连 222 | PLPlayerStatusCompleted 点播播放完成 223 | */ 224 | func player(_ player: PLPlayer, statusDidChange state: PLPlayerStatus) { 225 | switch state { 226 | case .statusPreparing: 227 | // 播放器正在准备当中 228 | print("播放器正在准备当中") 229 | loading = .began 230 | 231 | case .statusCaching: 232 | // 播放器正在缓存的状态 233 | print("缓存状态") 234 | loading = .began 235 | 236 | case .statusReady: 237 | print("准备完成") 238 | loading = .ended 239 | 240 | case .statusOpen: 241 | print("开始连接") 242 | loading = .ended 243 | 244 | case .statusPlaying: 245 | // 播放器正在播放的状态 246 | print("开始播放") 247 | loading = .ended 248 | 249 | if case .prepare = self.state { 250 | // 查看是否有需要的Seek 251 | if let seek = self.intendedToSeek { 252 | player.pause() 253 | self.seek(to: seek) 254 | 255 | } else { 256 | self.state = .playing 257 | 258 | if self.intendedToPlay { 259 | self.play() 260 | 261 | } else { 262 | self.pause() 263 | } 264 | } 265 | } 266 | 267 | case .statusPaused: 268 | // 播放器暂停的状态 269 | print("暂停播放") 270 | control = .pausing 271 | 272 | case .statusError: 273 | // 播放器错误的状态 274 | print("播放错误") 275 | error(nil) 276 | 277 | case .stateAutoReconnecting: 278 | // 播放器开始自动重连 279 | loading = .began 280 | 281 | case .statusCompleted: 282 | loading = .ended 283 | self.state = .finished 284 | 285 | default: break 286 | } 287 | } 288 | 289 | func player(_ player: PLPlayer, stoppedWithError error: Error?) { 290 | self.error(error) 291 | } 292 | 293 | func player(_ player: PLPlayer, loadedTimeRange timeRange: CMTime) { 294 | guard duration > 0 else { return } 295 | // 缓冲进度 296 | let progress = timeRange.seconds / duration 297 | 298 | print( 299 | """ 300 | ==========pl=========== 301 | duration \(duration)\n 302 | progress \(progress)\n 303 | """ 304 | ) 305 | buffer = progress 306 | delegate { $0.videoPlayer(self, updatedBuffer: progress) } 307 | } 308 | 309 | func player(_ player: PLPlayer, seekToCompleted isCompleted: Bool) { 310 | guard let target = intendedToSeek else { 311 | return 312 | } 313 | // 停止加载 314 | loading = .ended 315 | // 清空跳转意图 316 | intendedToSeek = nil 317 | // 设置当前时间校准器 318 | currentTimeCalibrator = target.time 319 | // 根据播放意图继续播放 320 | if isCompleted, intendedToPlay { 321 | play() 322 | } 323 | // 代理回调 结束跳转 324 | delegate { $0.videoPlayer(self, seekEnded: target) } 325 | 326 | // 如果是准备阶段 则切换到播放状态 327 | if case .prepare = self.state { 328 | self.state = .playing 329 | 330 | if self.intendedToPlay { 331 | self.play() 332 | 333 | } else { 334 | self.pause() 335 | } 336 | } 337 | } 338 | 339 | func playerWillBeginBackgroundTask(_ player: PLPlayer) { 340 | guard let _ = player.playerView else { return } 341 | 342 | playTimer?.fireDate = .distantFuture 343 | if case .playing = state, intendedToPlay { pauseNoUser() } 344 | } 345 | 346 | func playerWillEndBackgroundTask(_ player: PLPlayer) { 347 | guard let _ = player.playerView else { return } 348 | 349 | playTimer?.fireDate = .init() 350 | if case .playing = state, intendedToPlay { play() } 351 | } 352 | } 353 | 354 | extension PLVideoPlayer: VideoPlayerDelegates { 355 | 356 | typealias Element = VideoPlayerDelegate 357 | } 358 | 359 | extension PLVideoPlayer: VideoPlayerable { 360 | 361 | @discardableResult 362 | func prepare(resource: VideoPlayerURLAsset) -> VideoPlayerView { 363 | // 清理原有资源 364 | clear() 365 | // 重置当前状态 366 | loading = .began 367 | state = .prepare 368 | 369 | guard 370 | let player = PLPlayer(url: resource.value, option: PLPlayerOption.default()), 371 | let view = player.playerView else { 372 | state = .failed(.none) 373 | return VideoPlayerView(.init()) 374 | } 375 | 376 | player.delegate = self 377 | player.isBackgroundPlayEnable = false 378 | player.loopPlay = isLoop 379 | player.playSpeed = rate 380 | player.setVolume(.init(volume)) 381 | player.isMute = isMuted 382 | self.player = player 383 | 384 | playerView = VideoPlayerView(view.layer) 385 | playerView.observe { (contentMode) in 386 | view.contentMode = contentMode 387 | } 388 | playerView.observe { (size, animation) in 389 | view.frame = .init(origin: .zero, size: size) 390 | } 391 | playerView.backgroundColor = .clear 392 | playerView.contentMode = .scaleAspectFit 393 | 394 | playTimer?.fireDate = .init() 395 | 396 | player.play() 397 | 398 | // 设置初始播放意图 399 | intendedToPlay = configuration.isAutoplay 400 | 401 | return playerView 402 | } 403 | 404 | func play() { 405 | switch state { 406 | case .prepare: 407 | intendedToPlay = true 408 | 409 | case .playing where intendedToSeek != nil: 410 | intendedToPlay = true 411 | 412 | case .playing where intendedToSeek == nil: 413 | intendedToPlay = true 414 | player?.resume() 415 | control = .playing 416 | playTimer?.fireDate = .init() 417 | 418 | case .finished: 419 | state = .playing 420 | intendedToPlay = true 421 | // Seek到起始位置 422 | seek(to: .init(time: .zero)) 423 | 424 | default: 425 | break 426 | } 427 | } 428 | 429 | func pause() { 430 | intendedToPlay = false 431 | control = .pausing 432 | player?.pause() 433 | } 434 | 435 | func stop() { 436 | clear() 437 | state = .stopped 438 | } 439 | 440 | func seek(to target: VideoPlayer.Seek) { 441 | guard 442 | let player = player, 443 | player.status == .statusCaching || 444 | player.status == .statusPlaying || 445 | player.status == .statusPaused, 446 | case .playing = state else { 447 | // 设置跳转意图 448 | intendedToSeek = target 449 | return 450 | } 451 | // 设置跳转意图 452 | intendedToSeek = target 453 | // 暂停当前播放 454 | player.pause() 455 | // 开始加载中 456 | loading = .began 457 | // 代理回调 当前时间为目标时间 458 | delegate { $0.videoPlayer(self, updatedCurrent: target.time) } 459 | // 代理回调 开始跳转 460 | delegate { $0.videoPlayer(self, seekBegan: target) } 461 | // 开始Seek 462 | player.seek(to: CMTimeMakeWithSeconds(target.time, preferredTimescale: 1000)) 463 | } 464 | 465 | var current: TimeInterval { 466 | guard let duration = player?.currentTime else { return 0 } 467 | let time = duration.seconds 468 | // 如果有跳转意图 则返回跳转的目标时间 469 | if let seek = intendedToSeek { 470 | return seek.time 471 | } 472 | // 当前时间校准器 如果大于 当前时间, 则返回校准时间, 否则清空校准器 返回当前时间. 473 | if let temp = currentTimeCalibrator, temp > time { 474 | return temp 475 | } 476 | currentTimeCalibrator = nil 477 | return time.isNaN ? 0 : time 478 | } 479 | 480 | var duration: TimeInterval { 481 | guard let duration = player?.totalDuration else { return 0 } 482 | 483 | let time = duration.seconds 484 | return time.isNaN ? 0 : time 485 | } 486 | 487 | var view: VideoPlayerView { 488 | return playerView 489 | } 490 | 491 | func screenshot(completion: @escaping (UIImage?) -> Void) { 492 | guard let player = player else { 493 | completion(.none) 494 | return 495 | } 496 | player.getScreenShot(completionHandler: { (image) in 497 | completion(image) 498 | }) 499 | } 500 | } 501 | 502 | #endif 503 | -------------------------------------------------------------------------------- /Demo/Demo/PL/WeakObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeakObject.swift 3 | // ┌─┐ ┌───────┐ ┌───────┐ 4 | // │ │ │ ┌─────┘ │ ┌─────┘ 5 | // │ │ │ └─────┐ │ └─────┐ 6 | // │ │ │ ┌─────┘ │ ┌─────┘ 7 | // │ └─────┐│ └─────┐ │ └─────┐ 8 | // └───────┘└───────┘ └───────┘ 9 | // 10 | import Foundation 11 | 12 | class WeakObject: NSObject { 13 | 14 | private weak var target: AnyObject? 15 | 16 | init(_ target: AnyObject) { 17 | self.target = target 18 | super.init() 19 | } 20 | 21 | override func forwardingTarget(for aSelector: Selector!) -> Any? { 22 | return target 23 | } 24 | 25 | override func responds(to aSelector: Selector!) -> Bool { 26 | return target?.responds(to: aSelector) ?? super.responds(to: aSelector) 27 | } 28 | 29 | override func method(for aSelector: Selector!) -> IMP! { 30 | return target?.method(for: aSelector) ?? super.method(for: aSelector) 31 | } 32 | 33 | override func isEqual(_ object: Any?) -> Bool { 34 | return target?.isEqual(object) ?? super.isEqual(object) 35 | } 36 | 37 | override func isKind(of aClass: AnyClass) -> Bool { 38 | return target?.isKind(of: aClass) ?? super.isKind(of: aClass) 39 | } 40 | 41 | override var superclass: AnyClass? { 42 | return target?.superclass 43 | } 44 | 45 | override func isProxy() -> Bool { 46 | return target?.isProxy() ?? super.isProxy() 47 | } 48 | 49 | override var hash: Int { 50 | return target?.hash ?? super.hash 51 | } 52 | 53 | override var description: String { 54 | return target?.description ?? super.description 55 | } 56 | 57 | override var debugDescription: String { 58 | return target?.debugDescription ?? super.debugDescription 59 | } 60 | 61 | deinit { print("deinit:\t\(classForCoder)") } 62 | } 63 | -------------------------------------------------------------------------------- /Demo/Demo/PictureInPicture.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PictureInPicture.swift 3 | // Demo 4 | // 5 | // Created by 李响 on 2022/5/16. 6 | // Copyright © 2022 swift. All rights reserved. 7 | // 8 | 9 | import AVKit 10 | 11 | class PictureInPicture: NSObject { 12 | 13 | weak var delegate: AVPictureInPictureControllerDelegate? 14 | 15 | var isSuspended: Bool { 16 | AVPictureInPictureController.isPictureInPictureSupported() 17 | } 18 | var isActive: Bool { 19 | pictureController?.isPictureInPictureActive ?? false 20 | } 21 | 22 | /// 画中画控制器 23 | private var pictureController: AVPictureInPictureController? 24 | /// 画中画是否关闭 (用于区分点击了画中画"X"按钮, 还是收起按钮) 25 | private var isPictureClose = true 26 | 27 | private(set) weak var player: AVPlayerLayer? 28 | 29 | override init() { 30 | super.init() 31 | 32 | NotificationCenter.default.addObserver( 33 | forName: UIApplication.willEnterForegroundNotification, 34 | object: nil, 35 | queue: .main 36 | ) { [weak self] sender in 37 | guard let self = self else { return } 38 | guard self.isSuspended, self.isActive else { return } 39 | // 从后台回来 延迟1秒自动停止画中画 40 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 41 | self.pictureController?.stopPictureInPicture() 42 | } 43 | } 44 | } 45 | 46 | func setup(player: AVPlayerLayer) { 47 | guard isSuspended else { return } 48 | 49 | self.player = player 50 | 51 | if #available(iOS 15.0, *) { 52 | pictureController = AVPictureInPictureController(contentSource: .init(playerLayer: player)) 53 | 54 | } else { 55 | pictureController = AVPictureInPictureController(playerLayer: player) 56 | } 57 | 58 | if #available(iOS 14.2, *) { 59 | pictureController?.canStartPictureInPictureAutomaticallyFromInline = true 60 | } 61 | 62 | pictureController?.delegate = self 63 | } 64 | 65 | func close() { 66 | guard isSuspended else { return } 67 | pictureController = nil 68 | } 69 | 70 | func invalidatePlaybackState() { 71 | guard isSuspended else { return } 72 | if #available(iOS 15.0, *) { 73 | pictureController?.invalidatePlaybackState() 74 | } 75 | } 76 | 77 | func start() { 78 | guard isSuspended else { return } 79 | pictureController?.startPictureInPicture() 80 | } 81 | 82 | func stop() { 83 | guard isSuspended else { return } 84 | pictureController?.stopPictureInPicture() 85 | } 86 | } 87 | 88 | /* 89 | 90 | 全局画中画注意点 91 | 92 | 通过一个全局变量持有画中画控制器,可以在pictureInPictureControllerWillStartPictureInPicture持有,pictureInPictureControllerDidStopPictureInPicture释放; 93 | 有可能不是点画中画按钮,而是从其它途径来打开当前画中画控制器,可以在viewWillAppear 进行判断并关闭; 94 | 已有画中画的情况下开启新的画中画,需要等完全关闭完再开启新的,防止有未知的错误出现,因为关闭画中画是有过程的; 95 | 如果创建AVPictureInPictureController并同时开启画中画功能,有可能会失效,出现这种情况延迟开启画中画功能即可。 96 | 97 | */ 98 | 99 | extension PictureInPicture: AVPictureInPictureControllerDelegate { 100 | 101 | func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { 102 | print("将要开始PictureInPicture的代理方法") 103 | delegate?.pictureInPictureControllerWillStartPictureInPicture?(pictureInPictureController) 104 | } 105 | 106 | func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { 107 | print("已经开始PictureInPicture的代理方法") 108 | delegate?.pictureInPictureControllerDidStartPictureInPicture?(pictureInPictureController) 109 | } 110 | 111 | func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) { 112 | print("启动PictureInPicture失败的代理方法") 113 | delegate?.pictureInPictureController?(pictureInPictureController, failedToStartPictureInPictureWithError: error) 114 | } 115 | 116 | func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { 117 | print("将要停止PictureInPicture的代理方法") 118 | delegate?.pictureInPictureControllerWillStopPictureInPicture?(pictureInPictureController) 119 | } 120 | 121 | func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { 122 | print("已经停止PictureInPicture的代理方法") 123 | // 处理画中画关闭 124 | defer { isPictureClose = true } 125 | guard isPictureClose else { return } 126 | delegate?.pictureInPictureControllerWillStartPictureInPicture?(pictureInPictureController) 127 | } 128 | 129 | func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) { 130 | print("PictureInPicture停止之前恢复用户界面") 131 | isPictureClose = false 132 | 133 | // 设置非画中画关闭 134 | completionHandler(true) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Demo/Demo/SimplePlayerViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimplePlayerViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2019/8/28. 6 | // Copyright © 2019 swift. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AVKit 11 | import SnapKit 12 | import VideoPlayer 13 | 14 | class SimplePlayerViewController: UIViewController { 15 | 16 | private var type: Int = 0 17 | 18 | private var provider: VideoPlayerProvider? 19 | private lazy var playerView = UIView() 20 | private lazy var statusView = UIView() 21 | 22 | private let pip = PictureInPicture() 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | 27 | setup() 28 | } 29 | 30 | private func setup() { 31 | view.addSubview(playerView) 32 | 33 | playerView.snp.makeConstraints { (make) in 34 | if #available(iOS 11.0, *) { 35 | make.top.equalTo(view.safeAreaLayoutGuide.snp.top) 36 | 37 | } else { 38 | make.top.equalTo(topLayoutGuide.snp.bottom) 39 | } 40 | make.left.right.equalToSuperview() 41 | make.height.equalTo(playerView.snp.width).multipliedBy(9.0 / 16.0) 42 | } 43 | 44 | let url = URL(string: "https://devstreaming-cdn.apple.com/videos/wwdc/2019/808knty6w7kjssfl/808/hls_vod_mvp.m3u8")! 45 | 46 | // 初始化播放器 47 | let player: VideoPlayerable 48 | switch type { 49 | case 0: 50 | player = VideoPlayer.av.instance() 51 | 52 | default: 53 | #if targetEnvironment(simulator) 54 | player = VideoPlayer.av.instance() 55 | #else 56 | player = VideoPlayer.pl.instance() 57 | #endif 58 | } 59 | // 简单设置 60 | player.isLoop = false 61 | player.add(delegate: self) 62 | 63 | // 初始化相关视图 64 | let controlView = VideoPlayerControlView() 65 | let coverView = VideoPlayerCoverView() 66 | let errorView = VideoPlayerErrorView() 67 | let finishView = VideoPlayerFinishView() 68 | 69 | view.layoutIfNeeded() 70 | view.addSubview(statusView) 71 | statusView.addSubview(controlView) 72 | statusView.addSubview(coverView) 73 | statusView.addSubview(errorView) 74 | statusView.addSubview(finishView) 75 | 76 | statusView.snp.makeConstraints { (make) in 77 | make.edges.equalTo(playerView) 78 | } 79 | controlView.snp.makeConstraints { (make) in 80 | make.edges.equalToSuperview() 81 | } 82 | coverView.snp.makeConstraints { (make) in 83 | make.edges.equalToSuperview() 84 | } 85 | errorView.snp.makeConstraints { (make) in 86 | make.edges.equalToSuperview() 87 | } 88 | finishView.snp.makeConstraints { (make) in 89 | make.edges.equalToSuperview() 90 | } 91 | 92 | coverView.imageView.image = #imageLiteral(resourceName: "video_cover") 93 | 94 | let provider = VideoPlayerProvider( 95 | control: controlView, 96 | finish: finishView, 97 | error: errorView, 98 | cover: coverView 99 | ) { [weak self] in 100 | guard let self = self else { return } 101 | 102 | let view = player.prepare(resource: url) 103 | view.backgroundColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1) 104 | self.playerView.subviews.forEach({ $0.removeFromSuperview() }) 105 | self.playerView.addSubview(view) 106 | 107 | view.snp.remakeConstraints { (make) in 108 | make.edges.equalToSuperview() 109 | } 110 | } 111 | provider.set(player: player) 112 | self.provider = provider 113 | } 114 | 115 | private static func instance() -> Self { 116 | return StoryBoard.main.instance() 117 | } 118 | 119 | static func instance(_ type: Int) -> Self { 120 | let controller = instance() 121 | controller.type = type 122 | return controller 123 | } 124 | } 125 | 126 | extension SimplePlayerViewController { 127 | 128 | @objc 129 | private func startPictureAction() { 130 | pip.start() 131 | } 132 | 133 | @objc 134 | private func stopPictureAction() { 135 | pip.stop() 136 | } 137 | } 138 | 139 | extension SimplePlayerViewController: VideoPlayerDelegate { 140 | 141 | func videoPlayerControlState(_ player: VideoPlayerable, state: VideoPlayer.ControlState) { 142 | pip.invalidatePlaybackState() 143 | } 144 | 145 | func videoPlayerState(_ player: VideoPlayerable, state: VideoPlayer.State) { 146 | switch state { 147 | case .playing: 148 | guard 149 | pip.isSuspended, 150 | let layer = player.view.playerLayer as? AVPlayerLayer else { 151 | return 152 | } 153 | // 播放中时 设置画中画 154 | pip.setup(player: layer) 155 | pip.delegate = self 156 | 157 | if #available(iOS 14.0, *) { 158 | navigationItem.rightBarButtonItem = UIBarButtonItem( 159 | image: AVPictureInPictureController.pictureInPictureButtonStartImage, 160 | style: .plain, 161 | target: self, 162 | action: #selector(startPictureAction) 163 | ) 164 | } 165 | 166 | case .finished, .stopped, .failed: 167 | // 不在播放中时 则关闭画中画 168 | pip.close() 169 | 170 | navigationItem.rightBarButtonItem = nil 171 | 172 | default: 173 | break 174 | } 175 | } 176 | } 177 | 178 | extension SimplePlayerViewController: AVPictureInPictureControllerDelegate { 179 | 180 | func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { 181 | // 已经开始PictureInPicture 182 | // 隐藏视图 183 | statusView.isHidden = true 184 | 185 | if #available(iOS 14.0, *) { 186 | navigationItem.rightBarButtonItem = UIBarButtonItem( 187 | image: AVPictureInPictureController.pictureInPictureButtonStopImage, 188 | style: .plain, 189 | target: self, 190 | action: #selector(stopPictureAction) 191 | ) 192 | } 193 | } 194 | 195 | func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { 196 | // 将要停止PictureInPicture的代理方法 197 | // 显示视图 198 | statusView.isHidden = false 199 | 200 | if #available(iOS 14.0, *) { 201 | navigationItem.rightBarButtonItem = UIBarButtonItem( 202 | image: AVPictureInPictureController.pictureInPictureButtonStartImage, 203 | style: .plain, 204 | target: self, 205 | action: #selector(startPictureAction) 206 | ) 207 | } 208 | } 209 | 210 | func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { 211 | // 已经停止PictureInPicture的代理方法 212 | // 停止播放器 213 | provider?.player?.stop() 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /Demo/Demo/Storyboard.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | enum StoryBoard: String { 4 | case main = "Main" 5 | 6 | var storyboard: UIStoryboard { 7 | return UIStoryboard(name: rawValue, bundle: nil) 8 | } 9 | 10 | func instance() -> T { 11 | return storyboard.instantiateViewController(withIdentifier: String(describing: T.self)) as! T 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Demo/Demo/View/VideoPlayerControlView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol VideoPlayerControlViewable: NSObjectProtocol { 4 | 5 | /// 设置代理对象 6 | /// 7 | /// - Parameter delegate: 代理 8 | func set(delegate: VideoPlayerControlViewDelegate?) 9 | 10 | /// 设置状态 11 | /// 12 | /// - Parameter state: true 播放, false 暂停 13 | func set(state: Bool) 14 | 15 | /// 设置缓冲进度 16 | /// 17 | /// - Parameters: 18 | /// - progress: 进度 19 | /// - animated: 是否动画 20 | func set(buffer progress: Double, animated: Bool) 21 | 22 | /// 设置当前播放时长 23 | /// 24 | /// - Parameter time: 时间(秒) 25 | func set(current time: TimeInterval) 26 | 27 | /// 设置总播放时长 28 | /// 29 | /// - Parameter time: 时间(秒) 30 | func set(duration time: TimeInterval) 31 | 32 | /// 加载状态 33 | func loadingBegin() 34 | func loadingEnd() 35 | 36 | /// 设置启用 (当准备完成时可以启用子控件, 未准备完成时禁用子控件) 37 | /// 38 | /// - Parameter enabled: true or false 39 | func set(enabled: Bool) 40 | } 41 | 42 | protocol VideoPlayerControlViewDelegate: NSObjectProtocol { 43 | 44 | /// 控制播放 45 | func controlPlay() 46 | /// 控制暂停 47 | func controlPause() 48 | /// 控制跳转指定时间 49 | func controlSeek(time: Double) 50 | } 51 | 52 | class VideoPlayerControlView: UIView { 53 | 54 | private weak var delegate: VideoPlayerControlViewDelegate? 55 | 56 | lazy var loadingView: UIActivityIndicatorView = { 57 | $0.style = .white 58 | $0.hidesWhenStopped = true 59 | return $0 60 | }( UIActivityIndicatorView() ) 61 | 62 | lazy var stateButton: UIButton = { 63 | $0.isUserInteractionEnabled = false 64 | $0.bounds = CGRect(x: 0, y: 0, width: 66, height: 66) 65 | $0.setImage(#imageLiteral(resourceName: "video_play"), for: .normal) 66 | $0.setImage(#imageLiteral(resourceName: "video_pause"), for: .selected) 67 | $0.addTarget(self, action: #selector(stateAction), for: .touchUpInside) 68 | return $0 69 | }( UIButton(type: .custom) ) 70 | 71 | lazy var bottomView: UIView = { 72 | $0.backgroundColor = .clear 73 | return $0 74 | }( UIView() ) 75 | 76 | lazy var progressView: UIProgressView = { 77 | $0.progressViewStyle = .default 78 | $0.progressTintColor = .lightGray 79 | $0.trackTintColor = UIColor.lightGray.withAlphaComponent(0.3) 80 | return $0 81 | }( UIProgressView() ) 82 | 83 | lazy var sliderView: VideoPlayerSlider = { 84 | $0.isEnabled = false 85 | $0.setThumbImage(#imageLiteral(resourceName: "video_slider"), for: .normal) 86 | $0.minimumTrackTintColor = .cyan 87 | $0.maximumTrackTintColor = .clear 88 | $0.addTarget(self, action: #selector(sliderTouchBegin), for: .touchDown) 89 | $0.addTarget(self, action: #selector(sliderTouchEnd), for: [.touchUpInside, .touchUpOutside]) 90 | $0.addTarget(self, action: #selector(sliderTouchCancel), for: .touchCancel) 91 | $0.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged) 92 | return $0 93 | }( VideoPlayerSlider() ) 94 | 95 | lazy var currentLabel: UILabel = { 96 | $0.font = .systemFont(ofSize: 10.0) 97 | $0.textAlignment = .center 98 | $0.textColor = .white 99 | $0.text = "00:00" 100 | return $0 101 | }( UILabel() ) 102 | 103 | lazy var totalLabel: UILabel = { 104 | $0.font = .systemFont(ofSize: 10.0) 105 | $0.textAlignment = .center 106 | $0.textColor = .white 107 | $0.text = "00:00" 108 | return $0 109 | }( UILabel() ) 110 | 111 | private var isShow: Bool = true { didSet { if isShow { show() } else { hide() } } } 112 | private var isDraging: Bool = false 113 | private var autoHideTask: DispatchWorkItem? 114 | private let format = DateFormatter() 115 | 116 | override init(frame: CGRect) { 117 | super.init(frame: frame) 118 | 119 | setup() 120 | setupLayout() 121 | 122 | autoHide() 123 | } 124 | 125 | required init?(coder aDecoder: NSCoder) { 126 | super.init(coder: aDecoder) 127 | 128 | setup() 129 | setupLayout() 130 | 131 | autoHide() 132 | } 133 | 134 | override func layoutSubviews() { 135 | super.layoutSubviews() 136 | 137 | setupLayout() 138 | } 139 | 140 | private func setup() { 141 | 142 | backgroundColor = .clear 143 | 144 | addSubview(loadingView) 145 | addSubview(stateButton) 146 | addSubview(bottomView) 147 | bottomView.addSubview(progressView) 148 | bottomView.addSubview(sliderView) 149 | bottomView.addSubview(currentLabel) 150 | bottomView.addSubview(totalLabel) 151 | 152 | let single = UITapGestureRecognizer(target: self, action: #selector(singleTapAction(_:))) 153 | single.numberOfTapsRequired = 1 154 | addGestureRecognizer(single) 155 | 156 | let double = UITapGestureRecognizer(target: self, action: #selector(doubleTapAction(_:))) 157 | double.numberOfTapsRequired = 2 158 | addGestureRecognizer(double) 159 | 160 | single.require(toFail: double) 161 | } 162 | 163 | private func setupLayout() { 164 | 165 | let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2) 166 | loadingView.center = center 167 | stateButton.center = center 168 | 169 | bottomView.frame = CGRect( 170 | x: 0, 171 | y: bounds.height - 30, 172 | width: bounds.width, 173 | height: 30 174 | ) 175 | 176 | progressView.frame = CGRect( 177 | x: 50, 178 | y: bottomView.bounds.height - 15, 179 | width: bottomView.bounds.width - 100, 180 | height: 15 181 | ) 182 | sliderView.frame = progressView.frame 183 | currentLabel.frame = CGRect( 184 | x: 0, 185 | y: 0, 186 | width: 50, 187 | height: 30 188 | ) 189 | totalLabel.frame = CGRect( 190 | x: bottomView.bounds.width - 50, 191 | y: 0, 192 | width: 50, 193 | height: 30 194 | ) 195 | } 196 | } 197 | 198 | /// 事件处理 199 | extension VideoPlayerControlView { 200 | 201 | @objc 202 | private func stateAction(_ sender: UIButton) { 203 | sender.isSelected = !sender.isSelected 204 | if sender.isSelected { 205 | delegate?.controlPlay() 206 | } else { 207 | delegate?.controlPause() 208 | } 209 | } 210 | 211 | @objc 212 | private func sliderTouchBegin(_ sender: UISlider) { 213 | isDraging = true 214 | autoHide(true) 215 | } 216 | 217 | @objc 218 | private func sliderTouchEnd(_ sender: UISlider) { 219 | isDraging = false 220 | delegate?.controlSeek(time: Double(sender.value)) 221 | autoHide() 222 | } 223 | 224 | @objc 225 | private func sliderTouchCancel(_ sender: UISlider) { 226 | isDraging = false 227 | delegate?.controlSeek(time: Double(sender.value)) 228 | autoHide() 229 | } 230 | 231 | @objc 232 | private func sliderValueChanged(_ sender: UISlider) { 233 | 234 | currentLabel.text = timeToHMS(time: Float64(sender.value)) 235 | } 236 | 237 | @objc 238 | private func singleTapAction(_ gesture: UITapGestureRecognizer) { 239 | isShow = !isShow 240 | autoHide() 241 | } 242 | 243 | @objc 244 | private func doubleTapAction(_ gesture: UITapGestureRecognizer) { 245 | stateAction(stateButton) 246 | } 247 | } 248 | 249 | /// 显示与隐藏控制视图 250 | extension VideoPlayerControlView { 251 | 252 | private func show() { 253 | UIView.beginAnimations("", context: nil) 254 | UIView.setAnimationDuration(0.2) 255 | stateButton.alpha = 1.0 256 | bottomView.alpha = 1.0 257 | UIView.commitAnimations() 258 | } 259 | 260 | private func hide() { 261 | UIView.beginAnimations("", context: nil) 262 | UIView.setAnimationDuration(0.2) 263 | stateButton.alpha = 0.0 264 | bottomView.alpha = 0.0 265 | UIView.commitAnimations() 266 | } 267 | 268 | private func autoHide(_ cancel: Bool = false) { 269 | autoHideTask?.cancel() 270 | autoHideTask = nil 271 | 272 | if cancel { return } 273 | 274 | let item = DispatchWorkItem { [weak self] in 275 | guard let self = self else { return } 276 | 277 | self.isShow = false 278 | } 279 | autoHideTask = item 280 | DispatchQueue.main.asyncAfter(deadline: .now() + 4.0, execute: item) 281 | } 282 | } 283 | 284 | extension VideoPlayerControlView: VideoPlayerControlViewable { 285 | 286 | func set(delegate: VideoPlayerControlViewDelegate?) { 287 | self.delegate = delegate 288 | } 289 | 290 | func set(state: Bool) { 291 | stateButton.isSelected = state 292 | } 293 | 294 | func set(buffer progress: Double, animated: Bool = true) { 295 | progressView.setProgress(Float(progress), animated: animated) 296 | } 297 | 298 | func set(current time: TimeInterval) { 299 | guard !isDraging else { return } 300 | 301 | sliderView.value = Float(time) 302 | currentLabel.text = timeToHMS(time: time) 303 | } 304 | 305 | func set(duration time: TimeInterval) { 306 | sliderView.maximumValue = Float(time) 307 | totalLabel.text = timeToHMS(time: time) 308 | } 309 | 310 | func loadingBegin() { 311 | loadingView.startAnimating() 312 | } 313 | 314 | func loadingEnd() { 315 | loadingView.stopAnimating() 316 | } 317 | 318 | func set(enabled: Bool) { 319 | sliderView.isEnabled = enabled 320 | stateButton.isUserInteractionEnabled = enabled 321 | } 322 | } 323 | 324 | extension VideoPlayerControlView { 325 | 326 | private func timeToHMS(time: TimeInterval) -> String { 327 | 328 | format.timeZone = TimeZone(secondsFromGMT: 0) 329 | if time / 3600 >= 1 { 330 | format.dateFormat = "HH:mm:ss" 331 | } else { 332 | format.dateFormat = "mm:ss" 333 | } 334 | let date = Date(timeIntervalSince1970: TimeInterval(time)) 335 | let string = format.string(from: date) 336 | return string 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /Demo/Demo/View/VideoPlayerCoverView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol VideoPlayerCoverViewable: NSObjectProtocol { 4 | 5 | /// 设置代理对象 6 | /// 7 | /// - Parameter delegate: 代理 8 | func set(delegate: VideoPlayerCoverViewDelegate?) 9 | } 10 | 11 | protocol VideoPlayerCoverViewDelegate: NSObjectProtocol { 12 | 13 | /// 开始播放 14 | func play() 15 | } 16 | 17 | class VideoPlayerCoverView: UIView { 18 | 19 | private weak var delegate: VideoPlayerCoverViewDelegate? 20 | 21 | lazy var imageView: UIImageView = { 22 | $0.contentMode = .scaleAspectFill 23 | return $0 24 | } ( UIImageView() ) 25 | 26 | lazy var playButton: UIButton = { 27 | $0.bounds = CGRect(x: 0, y: 0, width: 66, height: 66) 28 | $0.setImage(#imageLiteral(resourceName: "video_play"), for: .normal) 29 | $0.addTarget(self, action: #selector(playAction), for: .touchUpInside) 30 | return $0 31 | } ( UIButton(type: .custom) ) 32 | 33 | override init(frame: CGRect) { 34 | super.init(frame: frame) 35 | setup() 36 | setupLayout() 37 | } 38 | 39 | required init?(coder aDecoder: NSCoder) { 40 | super.init(coder: aDecoder) 41 | setup() 42 | setupLayout() 43 | } 44 | 45 | override func layoutSubviews() { 46 | super.layoutSubviews() 47 | setupLayout() 48 | } 49 | 50 | private func setup() { 51 | addSubview(imageView) 52 | addSubview(playButton) 53 | 54 | let tap = UITapGestureRecognizer(target: self, action: #selector(tapAction)) 55 | imageView.addGestureRecognizer(tap) 56 | } 57 | 58 | private func setupLayout() { 59 | imageView.frame = bounds 60 | playButton.center = CGPoint(x: bounds.width / 2, y: bounds.height / 2) 61 | } 62 | 63 | @objc private func tapAction() { 64 | delegate?.play() 65 | } 66 | 67 | @objc private func playAction() { 68 | delegate?.play() 69 | } 70 | } 71 | 72 | extension VideoPlayerCoverView: VideoPlayerCoverViewable { 73 | 74 | func set(delegate: VideoPlayerCoverViewDelegate?) { 75 | self.delegate = delegate 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Demo/Demo/View/VideoPlayerErrorView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol VideoPlayerErrorViewable: NSObjectProtocol { 4 | 5 | /// 设置代理对象 6 | /// 7 | /// - Parameter delegate: 代理 8 | func set(delegate: VideoPlayerErrorViewDelegate?) 9 | } 10 | 11 | protocol VideoPlayerErrorViewDelegate: NSObjectProtocol { 12 | 13 | /// 错误重试 14 | func errorRetry() 15 | } 16 | 17 | class VideoPlayerErrorView: UIView { 18 | 19 | private weak var delegate: VideoPlayerErrorViewDelegate? 20 | 21 | lazy var retryButton: UIButton = { 22 | $0.bounds = CGRect(x: 0, y: 0, width: 180, height: 60) 23 | $0.setTitle("播放异常, 点击重试", for: .normal) 24 | $0.setTitleColor(.white, for: .normal) 25 | $0.titleLabel?.font = .systemFont(ofSize: 18) 26 | $0.addTarget(self, action: #selector(retryAction), for: .touchUpInside) 27 | $0.layer.shadowColor = UIColor.black.cgColor 28 | $0.layer.shadowOffset = .zero 29 | $0.layer.shadowRadius = 4 30 | $0.layer.shadowOpacity = 0.2 31 | return $0 32 | } ( UIButton(type: .custom) ) 33 | 34 | override init(frame: CGRect) { 35 | super.init(frame: frame) 36 | setup() 37 | setupLayout() 38 | } 39 | 40 | required init?(coder aDecoder: NSCoder) { 41 | super.init(coder: aDecoder) 42 | setup() 43 | setupLayout() 44 | } 45 | 46 | override func layoutSubviews() { 47 | super.layoutSubviews() 48 | setupLayout() 49 | } 50 | 51 | private func setup() { 52 | backgroundColor = .black 53 | addSubview(retryButton) 54 | } 55 | 56 | private func setupLayout() { 57 | retryButton.center = CGPoint(x: bounds.width / 2, y: bounds.height / 2) 58 | } 59 | 60 | @objc private func retryAction() { 61 | delegate?.errorRetry() 62 | } 63 | } 64 | 65 | extension VideoPlayerErrorView: VideoPlayerErrorViewable { 66 | 67 | func set(delegate: VideoPlayerErrorViewDelegate?) { 68 | self.delegate = delegate 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Demo/Demo/View/VideoPlayerFinishView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol VideoPlayerFinishViewable: NSObjectProtocol { 4 | 5 | /// 设置代理对象 6 | /// 7 | /// - Parameter delegate: 代理 8 | func set(delegate: VideoPlayerFinishViewDelegate?) 9 | } 10 | 11 | protocol VideoPlayerFinishViewDelegate: NSObjectProtocol { 12 | 13 | /// 完成重试 14 | func finishReplay() 15 | } 16 | 17 | class VideoPlayerFinishView: UIView { 18 | 19 | private weak var delegate: VideoPlayerFinishViewDelegate? 20 | 21 | lazy var replayButton: UIButton = { 22 | $0.bounds = CGRect(x: 0, y: 0, width: 180, height: 60) 23 | $0.setTitle("播放完成, 点击重播", for: .normal) 24 | $0.setTitleColor(.white, for: .normal) 25 | $0.titleLabel?.font = .systemFont(ofSize: 18) 26 | $0.addTarget(self, action: #selector(replayAction), for: .touchUpInside) 27 | $0.layer.shadowColor = UIColor.black.cgColor 28 | $0.layer.shadowOffset = .zero 29 | $0.layer.shadowRadius = 4 30 | $0.layer.shadowOpacity = 0.2 31 | return $0 32 | } ( UIButton(type: .custom) ) 33 | 34 | override init(frame: CGRect) { 35 | super.init(frame: frame) 36 | setup() 37 | setupLayout() 38 | } 39 | 40 | required init?(coder aDecoder: NSCoder) { 41 | super.init(coder: aDecoder) 42 | setup() 43 | setupLayout() 44 | } 45 | 46 | override func layoutSubviews() { 47 | super.layoutSubviews() 48 | setupLayout() 49 | } 50 | 51 | private func setup() { 52 | backgroundColor = .black 53 | addSubview(replayButton) 54 | } 55 | 56 | private func setupLayout() { 57 | replayButton.center = CGPoint(x: bounds.width / 2, y: bounds.height / 2) 58 | } 59 | 60 | @objc private func replayAction() { 61 | delegate?.finishReplay() 62 | } 63 | } 64 | 65 | extension VideoPlayerFinishView: VideoPlayerFinishViewable { 66 | 67 | func set(delegate: VideoPlayerFinishViewDelegate?) { 68 | self.delegate = delegate 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Demo/Demo/View/VideoPlayerProvider.swift: -------------------------------------------------------------------------------- 1 | import VideoPlayer 2 | 3 | class VideoPlayerProvider: NSObject { 4 | 5 | typealias ControlView = UIView & VideoPlayerControlViewable 6 | typealias FinishView = UIView & VideoPlayerFinishViewable 7 | typealias ErrorView = UIView & VideoPlayerErrorViewable 8 | typealias CoverView = UIView & VideoPlayerCoverViewable 9 | 10 | private(set) weak var player: VideoPlayerable? 11 | 12 | private let controlView: ControlView? 13 | private let finishView: FinishView? 14 | private let errorView: ErrorView? 15 | private let coverView: CoverView? 16 | private let playHandle: (() -> Void) 17 | 18 | init(control: ControlView?, 19 | finish: FinishView?, 20 | error: ErrorView?, 21 | cover: CoverView?, 22 | playHandle handle: @escaping (() -> Void)) { 23 | 24 | controlView = control 25 | finishView = finish 26 | errorView = error 27 | coverView = cover 28 | playHandle = handle 29 | super.init() 30 | 31 | controlView?.isHidden = true 32 | controlView?.set(delegate: self) 33 | 34 | finishView?.isHidden = true 35 | finishView?.set(delegate: self) 36 | 37 | errorView?.isHidden = true 38 | errorView?.set(delegate: self) 39 | 40 | coverView?.isHidden = true 41 | coverView?.set(delegate: self) 42 | } 43 | 44 | deinit { print("deinit:\t\(classForCoder)") } 45 | } 46 | 47 | extension VideoPlayerProvider { 48 | 49 | /// 设置播放器 50 | /// 51 | /// - Parameter player: 播放器 52 | func set(player: VideoPlayerable?) { 53 | defer { 54 | self.player?.remove(delegate: self) 55 | self.player = player 56 | player?.add(delegate: self) 57 | } 58 | guard let player = player else { 59 | return 60 | } 61 | videoPlayerState(player, state: player.state) 62 | videoPlayerLoadingState(player, state: player.loading) 63 | videoPlayerControlState(player, state: player.control) 64 | } 65 | } 66 | 67 | extension VideoPlayerProvider: VideoPlayerDelegate { 68 | 69 | func videoPlayerLoadingState(_ player: VideoPlayerable, state: VideoPlayer.LoadingState) { 70 | switch state { 71 | case .began: controlView?.loadingBegin() 72 | case .ended: controlView?.loadingEnd() 73 | } 74 | } 75 | 76 | func videoPlayerControlState(_ player: VideoPlayerable, state: VideoPlayer.ControlState) { 77 | switch state { 78 | case .playing: 79 | controlView?.set(state: true) 80 | 81 | case .pausing: 82 | controlView?.set(state: false) 83 | } 84 | } 85 | 86 | func videoPlayerState(_ player: VideoPlayerable, state: VideoPlayer.State) { 87 | switch state { 88 | case .prepare: 89 | controlView?.set(enabled: false) 90 | controlView?.isHidden = false 91 | finishView?.isHidden = true 92 | errorView?.isHidden = true 93 | coverView?.isHidden = true 94 | 95 | case .playing: 96 | controlView?.set(enabled: true) 97 | controlView?.isHidden = false 98 | finishView?.isHidden = true 99 | errorView?.isHidden = true 100 | coverView?.isHidden = true 101 | 102 | case .stopped: 103 | controlView?.isHidden = true 104 | finishView?.isHidden = true 105 | errorView?.isHidden = true 106 | coverView?.isHidden = false 107 | 108 | case .finished: 109 | controlView?.isHidden = true 110 | finishView?.isHidden = false 111 | errorView?.isHidden = true 112 | coverView?.isHidden = true 113 | 114 | case .failed(let error): 115 | controlView?.isHidden = true 116 | finishView?.isHidden = true 117 | errorView?.isHidden = false 118 | coverView?.isHidden = true 119 | print(error?.localizedDescription ?? "") 120 | } 121 | } 122 | 123 | func videoPlayer(_ player: VideoPlayerable, updatedBuffer progress: Double) { 124 | controlView?.set(buffer: progress, animated: true) 125 | } 126 | 127 | func videoPlayer(_ player: VideoPlayerable, updatedDuration time: Double) { 128 | controlView?.set(duration: time) 129 | } 130 | 131 | func videoPlayer(_ player: VideoPlayerable, updatedCurrent time: Double) { 132 | controlView?.set(current: time) 133 | } 134 | 135 | func videoPlayerSeekBegan(_ player: VideoPlayerable) { 136 | // 可以做一些跳转Toast什么的. 137 | } 138 | 139 | func videoPlayerSeekEnded(_ player: VideoPlayerable) { 140 | // 跳转结束 隐藏Toast什么的. 141 | } 142 | } 143 | 144 | extension VideoPlayerProvider: VideoPlayerControlViewDelegate { 145 | 146 | func controlPlay() { 147 | player?.play() 148 | } 149 | 150 | func controlPause() { 151 | player?.pause() 152 | } 153 | 154 | func controlSeek(time: Double) { 155 | player?.seek(to: .init(time: time)) 156 | } 157 | } 158 | 159 | extension VideoPlayerProvider: VideoPlayerFinishViewDelegate { 160 | 161 | func finishReplay() { 162 | // 播放 163 | player?.play() 164 | // 恢复视图 165 | controlView?.isHidden = false 166 | finishView?.isHidden = true 167 | } 168 | } 169 | 170 | extension VideoPlayerProvider: VideoPlayerErrorViewDelegate { 171 | 172 | func errorRetry() { 173 | playHandle() 174 | } 175 | } 176 | 177 | extension VideoPlayerProvider: VideoPlayerCoverViewDelegate { 178 | 179 | func play() { 180 | playHandle() 181 | coverView?.isHidden = true 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /Demo/Demo/View/VideoPlayerSlider.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class VideoPlayerSlider: UISlider { 4 | 5 | @IBInspectable var height: CGFloat = 2.0 6 | 7 | private let thumbBoundX: CGFloat = 10 8 | private let thumbBoundY: CGFloat = 20 9 | private var lastBounds: CGRect? 10 | 11 | override func minimumValueImageRect(forBounds bounds: CGRect) -> CGRect { 12 | return bounds 13 | } 14 | 15 | override func maximumValueImageRect(forBounds bounds: CGRect) -> CGRect { 16 | return bounds 17 | } 18 | 19 | /// 控制slider的宽和高,这个方法才是真正的改变slider滑道的高的 20 | override func trackRect(forBounds bounds: CGRect) -> CGRect { 21 | super.trackRect(forBounds: bounds) 22 | return CGRect(x: 0, 23 | y: (bounds.size.height - height) / 2, 24 | width: bounds.size.width, 25 | height: height) 26 | } 27 | 28 | /// 改变滑块的触摸范围 29 | override func thumbRect(forBounds bounds: CGRect, trackRect rect: CGRect, value: Float) -> CGRect { 30 | let result = super.thumbRect(forBounds: bounds, 31 | trackRect: rect, 32 | value: value) 33 | lastBounds = result 34 | return result 35 | } 36 | 37 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 38 | let result = super.hitTest(point, with: event) 39 | guard let lastBounds = lastBounds else { 40 | return result 41 | } 42 | guard point.x >= 0, point.x <= bounds.width else { 43 | return result 44 | } 45 | 46 | if ((point.y >= -thumbBoundY) && 47 | (point.y < lastBounds.height + thumbBoundY)) { 48 | var value: CGFloat = 0.0 49 | value = point.x - bounds.origin.x 50 | value = value / bounds.width 51 | 52 | value = value < 0 ? 0 : value 53 | value = value > 1 ? 1: value 54 | 55 | value = value * CGFloat(maximumValue - minimumValue) + CGFloat(minimumValue) 56 | setValue(Float(value), animated: true) 57 | } 58 | return result 59 | } 60 | 61 | override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { 62 | var result = super.point(inside: point, with: event) 63 | 64 | guard let lastBounds = lastBounds else { 65 | return result 66 | } 67 | 68 | if (!result && point.y > -10) { 69 | if ((point.x >= lastBounds.origin.x - thumbBoundX) && 70 | (point.x <= (lastBounds.origin.x + lastBounds.size.width + thumbBoundX)) && 71 | (point.y < (lastBounds.size.height + thumbBoundY))) { 72 | result = true 73 | } 74 | 75 | } 76 | return result 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Demo/Demo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Demo 4 | // 5 | // Created by 李响 on 2019/4/12. 6 | // Copyright © 2019 swift. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | @IBOutlet weak var tableView: UITableView! 14 | 15 | private var list: [String] = [ 16 | "基于AVPlayer的播放器", 17 | "基于PLPlayer的播放器" 18 | ] 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | 23 | setup() 24 | } 25 | 26 | private func setup() { 27 | tableView.delegate = self 28 | tableView.dataSource = self 29 | } 30 | } 31 | 32 | extension ViewController: UITableViewDelegate { 33 | 34 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 35 | return 50 36 | } 37 | } 38 | 39 | extension ViewController: UITableViewDataSource { 40 | 41 | func numberOfSections(in tableView: UITableView) -> Int { 42 | return 1 43 | } 44 | 45 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 46 | return list.count 47 | } 48 | 49 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 50 | let cell = tableView.dequeueReusableCell( 51 | withIdentifier: "cell", 52 | for: indexPath 53 | ) 54 | cell.textLabel?.text = list[indexPath.row] 55 | return cell 56 | } 57 | 58 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 59 | tableView.deselectRow(at: indexPath, animated: true) 60 | 61 | switch indexPath.row { 62 | case 0: // 基于AVPlayer的播放器 63 | let controller = SimplePlayerViewController.instance(0) 64 | navigationController?.pushViewController(controller, animated: true) 65 | 66 | case 1: // 基于PLPlayer的播放器 67 | let controller = SimplePlayerViewController.instance(1) 68 | navigationController?.pushViewController(controller, animated: true) 69 | 70 | default: 71 | break 72 | } 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /Demo/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '10.0' 2 | inhibit_all_warnings! 3 | 4 | target 'Demo' do 5 | use_frameworks! 6 | 7 | pod 'VideoPlayer', :path => "../" 8 | pod 'PLPlayerKit', '3.4.3' 9 | pod 'SnapKit' , '~>5.0.0' 10 | 11 | end 12 | -------------------------------------------------------------------------------- /Demo/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - PLPlayerKit (3.4.3): 3 | - PLPlayerKit/iphoneos (= 3.4.3) 4 | - PLPlayerKit/iphoneos (3.4.3) 5 | - SnapKit (5.0.1) 6 | - VideoPlayer (5.2.0): 7 | - VideoPlayer/AVPlayer (= 5.2.0) 8 | - VideoPlayer/Core (= 5.2.0) 9 | - VideoPlayer/AVPlayer (5.2.0): 10 | - VideoPlayer/Core 11 | - VideoPlayer/Core (5.2.0): 12 | - VideoPlayer/Privacy 13 | - VideoPlayer/Privacy (5.2.0) 14 | 15 | DEPENDENCIES: 16 | - PLPlayerKit (= 3.4.3) 17 | - SnapKit (~> 5.0.0) 18 | - VideoPlayer (from `../`) 19 | 20 | SPEC REPOS: 21 | trunk: 22 | - PLPlayerKit 23 | - SnapKit 24 | 25 | EXTERNAL SOURCES: 26 | VideoPlayer: 27 | :path: "../" 28 | 29 | SPEC CHECKSUMS: 30 | PLPlayerKit: a44734dc78c1f8f9fb22c537fa1916612fa46b06 31 | SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb 32 | VideoPlayer: 2c312eef12f0edea12894b161d128ab098b37f86 33 | 34 | PODFILE CHECKSUM: 273ff2c8e4d66cfdbdf70069686deccdfacac1a1 35 | 36 | COCOAPODS: 1.15.2 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 LEE 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VideoPlayer 2 | 3 | 开发中... 4 | -------------------------------------------------------------------------------- /Sources/AV/AVVideoPlayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AVVideoPlayer.swift 3 | // ┌─┐ ┌───────┐ ┌───────┐ 4 | // │ │ │ ┌─────┘ │ ┌─────┘ 5 | // │ │ │ └─────┐ │ └─────┐ 6 | // │ │ │ ┌─────┘ │ ┌─────┘ 7 | // │ └─────┐│ └─────┐ │ └─────┐ 8 | // └───────┘└───────┘ └───────┘ 9 | // 10 | import UIKit 11 | import Foundation 12 | import AVFoundation 13 | 14 | public extension VideoPlayer { 15 | 16 | static let av: Builder = .init { AVVideoPlayer($0) } 17 | } 18 | 19 | class AVVideoPlayer: NSObject { 20 | 21 | /// 当前URL 22 | private(set) var resource: VideoPlayerURLAsset? 23 | 24 | /// 配置 25 | private(set) var configuration: VideoPlayerConfiguration 26 | 27 | /// 播放状态 28 | private (set) var state: VideoPlayer.State = .stopped { 29 | didSet { 30 | delegate { $0.videoPlayerState(self, state: state) } 31 | } 32 | } 33 | 34 | /// 控制状态 35 | private(set) var control: VideoPlayer.ControlState = .pausing { 36 | didSet { 37 | delegate { $0.videoPlayerControlState(self, state: control) } 38 | } 39 | } 40 | 41 | /// 加载状态 42 | private(set) var loading: VideoPlayer.LoadingState = .ended { 43 | didSet { 44 | delegate { $0.videoPlayerLoadingState(self, state: loading) } 45 | } 46 | } 47 | 48 | /// 播放速率 0.5 - 2.0 49 | var rate: Double = 1.0 { 50 | didSet { 51 | guard case .playing = state, case .playing = control else { return } 52 | player.rate = .init(rate) 53 | } 54 | } 55 | /// 音量 0 - 1 56 | var volume: Double = 1.0 { 57 | didSet { player.volume = .init(volume)} 58 | } 59 | /// 是否静音 60 | var isMuted: Bool = false { 61 | didSet { 62 | player.isMuted = isMuted 63 | } 64 | } 65 | /// 是否循环播放 66 | var isLoop: Bool = false 67 | 68 | var delegates: [VideoPlayerDelegateBridge] = [] 69 | 70 | private lazy var player = AVPlayer() 71 | private lazy var playerLayer = AVPlayerLayer(player: player) 72 | private lazy var playerView: VideoPlayerView = VideoPlayerView(.init()) 73 | private lazy var playerOutput = AVPlayerItemVideoOutput() 74 | 75 | private var playerTimeObserver: Any? 76 | 77 | /// 当前时间校准器 用于解决时间精度偏差问题. 78 | private var currentTimeCalibrator: TimeInterval? 79 | /// 跳转意图 例如当非playing状态时 如果调用了seek(to:) 记录状态 在playing时设置跳转 80 | private var intendedToSeek: VideoPlayer.Seek? 81 | /// 播放意图 例如当seeking时如果调用了play() 或者 pasue() 记录状态 在seeking结束时设置对应状态 82 | private var intendedToPlay: Bool = false 83 | 84 | private var timeControlStatusObservation: NSKeyValueObservation? 85 | private var reasonForWaitingToPlayObservation: NSKeyValueObservation? 86 | 87 | private var itemStatusObservation: NSKeyValueObservation? 88 | private var itemDurationObservation: NSKeyValueObservation? 89 | private var itemLoadedTimeRangesObservation: NSKeyValueObservation? 90 | private var itemPlaybackLikelyToKeepUpObservation: NSKeyValueObservation? 91 | 92 | private var itemPlaybackStalledObserver: Any? 93 | private var itemDidPlayToEndTimeObserver: Any? 94 | private var itemFailedToPlayToEndTimeObserver: Any? 95 | 96 | init(_ configuration: VideoPlayerConfiguration) { 97 | self.configuration = configuration 98 | super.init() 99 | 100 | setup() 101 | setupNotification() 102 | } 103 | 104 | private func setup() { 105 | rate = 1.0 106 | volume = 1.0 107 | isMuted = false 108 | isLoop = false 109 | } 110 | 111 | private func setupNotification() { 112 | NotificationCenter.default.addObserver( 113 | self, 114 | selector: #selector(sessionInterruption), 115 | name: AVAudioSession.interruptionNotification, 116 | object: AVAudioSession.sharedInstance() 117 | ) 118 | NotificationCenter.default.addObserver( 119 | self, 120 | selector: #selector(willEnterForeground), 121 | name: UIApplication.willEnterForegroundNotification, 122 | object: nil 123 | ) 124 | } 125 | } 126 | 127 | extension AVVideoPlayer { 128 | 129 | /// 错误 130 | private func error(_ value: Swift.Error?) { 131 | clear() 132 | state = .failed(value) 133 | } 134 | 135 | /// 清理 136 | private func clear() { 137 | guard let item = player.currentItem else { return } 138 | 139 | loading = .ended 140 | 141 | player.pause() 142 | 143 | // 取消相关 144 | item.cancelPendingSeeks() 145 | item.asset.cancelLoading() 146 | 147 | // 移除监听 148 | removeObserver() 149 | removeObserver(item: item) 150 | removeNotification(item: item) 151 | 152 | // 移除item 153 | player.replaceCurrentItem(with: nil) 154 | // 清空资源 155 | resource = nil 156 | // 清理意图 157 | intendedToSeek = nil 158 | intendedToPlay = false 159 | } 160 | 161 | private func addObserver() { 162 | // 移除原有观察者 163 | removeObserver() 164 | // 当前播放时间回调间隔 (每秒N次) 165 | let interval = CMTime( 166 | value: 1, 167 | timescale: .init(configuration.currentTimeCallbackPeriodicInterval) 168 | ) 169 | playerTimeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { 170 | [weak self] (time) in 171 | guard let self = self else { return } 172 | guard case .playing = self.state else { return } 173 | let time = CMTimeGetSeconds(time) 174 | // 如果有跳转意图 则返回跳转的目标时间 175 | if let seek = self.intendedToSeek { 176 | self.delegate { $0.videoPlayer(self, updatedCurrent: seek.time) } 177 | return 178 | } 179 | // 当前时间校准器 如果大于 当前时间, 则返回校准时间, 否则清空校准器 返回当前时间. 180 | if let temp = self.currentTimeCalibrator, temp > time { 181 | self.delegate { $0.videoPlayer(self, updatedCurrent: temp) } 182 | return 183 | } 184 | self.currentTimeCalibrator = nil 185 | 186 | self.delegate { $0.videoPlayer(self, updatedCurrent: time) } 187 | } 188 | 189 | timeControlStatusObservation = player.observe(\.timeControlStatus) { 190 | [weak self] (observer, change) in 191 | guard let self = self else { return } 192 | guard case .playing = self.state else { return } 193 | 194 | switch observer.timeControlStatus { 195 | case .paused: 196 | self.control = .pausing 197 | 198 | case .playing: 199 | // 校准播放速率 200 | if observer.rate == .init(self.rate) { 201 | self.control = .playing 202 | 203 | } else { 204 | observer.rate = .init(self.rate) 205 | } 206 | 207 | default: 208 | break 209 | } 210 | } 211 | 212 | reasonForWaitingToPlayObservation = player.observe(\.reasonForWaitingToPlay) { 213 | [weak self] (observer, change) in 214 | guard let self = self else { return } 215 | guard observer.automaticallyWaitsToMinimizeStalling else { return } 216 | guard observer.timeControlStatus == .waitingToPlayAtSpecifiedRate else { return } 217 | 218 | switch observer.reasonForWaitingToPlay { 219 | case .toMinimizeStalls?: 220 | print("toMinimizeStalls") 221 | self.loading = .began 222 | 223 | case .evaluatingBufferingRate?: 224 | print("evaluatingBufferingRate") 225 | 226 | case .noItemToPlay?: 227 | print("noItemToPlay") 228 | 229 | default: 230 | self.loading = .ended 231 | } 232 | } 233 | } 234 | private func removeObserver() { 235 | if let observer = playerTimeObserver { 236 | playerTimeObserver = nil 237 | player.removeTimeObserver(observer) 238 | } 239 | 240 | if let observer = timeControlStatusObservation { 241 | observer.invalidate() 242 | timeControlStatusObservation = nil 243 | } 244 | 245 | if let observer = reasonForWaitingToPlayObservation { 246 | observer.invalidate() 247 | reasonForWaitingToPlayObservation = nil 248 | } 249 | } 250 | 251 | private func addObserver(item: AVPlayerItem) { 252 | do { 253 | let observation = item.observe(\.status) { 254 | [weak self] (observer, change) in 255 | guard let self = self else { return } 256 | 257 | switch observer.status { 258 | case .readyToPlay: 259 | let handle = { [weak self] in 260 | guard let self = self else { return } 261 | self.intendedToSeek = nil 262 | self.state = .playing 263 | 264 | if self.intendedToPlay { 265 | self.play() 266 | 267 | } else { 268 | self.pause() 269 | } 270 | } 271 | 272 | // 查看是否有需要的Seek 273 | if let seek = self.intendedToSeek { 274 | self.player.pause() 275 | self.seek(to: seek, for: item) { _ in 276 | handle() 277 | } 278 | 279 | } else { 280 | handle() 281 | } 282 | 283 | self.itemStatusObservation = nil 284 | 285 | case .failed: 286 | self.error(item.error) 287 | 288 | default: 289 | break 290 | } 291 | } 292 | itemStatusObservation = observation 293 | } 294 | do { 295 | let observation = item.observe(\.duration, options: [.new, .old]) { 296 | [weak self] (observer, change) in 297 | guard let self = self else { return } 298 | guard change.newValue != change.oldValue else { return } 299 | 300 | let time = observer.duration.seconds.isNaN ? 0 : observer.duration.seconds 301 | self.delegate { $0.videoPlayer(self, updatedDuration: time) } 302 | } 303 | itemDurationObservation = observation 304 | } 305 | do { 306 | let observation = item.observe(\.loadedTimeRanges, options: [.new, .old]) { 307 | [weak self] (observer, change) in 308 | guard let self = self else { return } 309 | guard change.newValue != change.oldValue else { return } 310 | 311 | self.delegate { $0.videoPlayer(self, updatedBuffer: self.buffer) } 312 | } 313 | itemLoadedTimeRangesObservation = observation 314 | } 315 | do { 316 | let observation = item.observe(\.isPlaybackLikelyToKeepUp, options: [.new, .old]) { 317 | [weak self] (observer, change) in 318 | guard let self = self else { return } 319 | guard change.newValue != change.oldValue else { return } 320 | 321 | self.loading = !observer.isPlaybackLikelyToKeepUp ? .began : .ended 322 | } 323 | itemPlaybackLikelyToKeepUpObservation = observation 324 | } 325 | } 326 | private func removeObserver(item: AVPlayerItem) { 327 | itemStatusObservation = nil 328 | itemDurationObservation = nil 329 | itemLoadedTimeRangesObservation = nil 330 | itemPlaybackLikelyToKeepUpObservation = nil 331 | } 332 | 333 | private func addNotification(item: AVPlayerItem) { 334 | do { 335 | let observation = NotificationCenter.default.addObserver( 336 | forName: .AVPlayerItemPlaybackStalled, 337 | object: item, 338 | queue: .main 339 | ) { [weak self] sender in 340 | guard let self = self else { return } 341 | self.itemPlaybackStalled(sender) 342 | } 343 | itemPlaybackStalledObserver = observation 344 | } 345 | do { 346 | let observation = NotificationCenter.default.addObserver( 347 | forName: .AVPlayerItemDidPlayToEndTime, 348 | object: item, 349 | queue: .main 350 | ) { [weak self] sender in 351 | guard let self = self else { return } 352 | self.itemDidPlayToEndTime(sender) 353 | } 354 | itemDidPlayToEndTimeObserver = observation 355 | } 356 | do { 357 | let observation = NotificationCenter.default.addObserver( 358 | forName: .AVPlayerItemFailedToPlayToEndTime, 359 | object: item, 360 | queue: .main 361 | ) { [weak self] sender in 362 | guard let self = self else { return } 363 | self.itemFailedToPlayToEndTime(sender) 364 | } 365 | itemFailedToPlayToEndTimeObserver = observation 366 | } 367 | } 368 | 369 | private func removeNotification(item: AVPlayerItem) { 370 | if let observer = itemPlaybackStalledObserver { 371 | NotificationCenter.default.removeObserver( 372 | observer, 373 | name: .AVPlayerItemPlaybackStalled, 374 | object: item 375 | ) 376 | } 377 | if let observer = itemDidPlayToEndTimeObserver { 378 | NotificationCenter.default.removeObserver( 379 | observer, 380 | name: .AVPlayerItemDidPlayToEndTime, 381 | object: item 382 | ) 383 | } 384 | if let observer = itemFailedToPlayToEndTimeObserver { 385 | NotificationCenter.default.removeObserver( 386 | observer, 387 | name: .AVPlayerItemFailedToPlayToEndTime, 388 | object: item 389 | ) 390 | } 391 | } 392 | } 393 | 394 | extension AVVideoPlayer { 395 | 396 | /// 播放中断通知 397 | @objc 398 | private func itemPlaybackStalled(_ notification: Notification) { 399 | guard case .playing = state, intendedToPlay else { return } 400 | play() 401 | } 402 | 403 | /// 播放结束通知 404 | @objc 405 | private func itemDidPlayToEndTime(_ notification: Notification) { 406 | // 判断是否循环播放 407 | if isLoop { 408 | // Seek到起始位置 409 | seek(to: .init(time: .zero)) 410 | 411 | } else { 412 | // 暂停播放 413 | pause() 414 | // 设置完成状态 415 | state = .finished 416 | } 417 | } 418 | 419 | /// 播放失败通知 420 | @objc 421 | private func itemFailedToPlayToEndTime(_ notification: Notification) { 422 | error(notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error) 423 | } 424 | 425 | /// 会话中断通知 426 | @objc 427 | private func sessionInterruption(_ notification: Notification) { 428 | guard 429 | let info = notification.userInfo, 430 | let type = info[AVAudioSessionInterruptionTypeKey] as? Int else { 431 | return 432 | } 433 | guard let _ = player.currentItem else { return } 434 | 435 | switch AVAudioSession.InterruptionType(rawValue: .init(type)) { 436 | case .began? where intendedToPlay: 437 | player.pause() 438 | 439 | case .ended? where intendedToPlay: 440 | play() 441 | 442 | default: 443 | break 444 | } 445 | } 446 | 447 | @objc 448 | private func willEnterForeground(_ notification: Notification) { 449 | guard player.currentItem != .none else { return } 450 | guard case .playing = state, intendedToPlay else { return } 451 | // 继续播放 452 | play() 453 | } 454 | } 455 | 456 | extension AVVideoPlayer: VideoPlayerable { 457 | 458 | @discardableResult 459 | func prepare(resource: VideoPlayerURLAsset) -> VideoPlayerView { 460 | // 清理原有资源 461 | clear() 462 | // 重置当前状态 463 | loading = .began 464 | state = .prepare 465 | 466 | // 设置当前资源 467 | self.resource = resource 468 | 469 | let asset: AVURLAsset 470 | if let temp = resource as? AVURLAsset { 471 | asset = temp 472 | 473 | } else { 474 | asset = AVURLAsset(url: resource.value) 475 | } 476 | 477 | // if asset.resourceLoader.delegate == nil { 478 | // asset.resourceLoader.setDelegate(AVAssetResourceLoader(), queue: .main) 479 | // } 480 | 481 | // 初始化播放项 482 | let item = AVPlayerItem(asset: asset) 483 | item.canUseNetworkResourcesForLiveStreamingWhilePaused = true 484 | // 预缓冲时长 默认自动选择 485 | item.preferredForwardBufferDuration = configuration.preferredBufferDuration 486 | // 控制倍速播放的质量: 音频质量适中,计算成本较低,适合语音. 可变率从1/32到32; 487 | item.audioTimePitchAlgorithm = .timeDomain 488 | 489 | playerOutput = AVPlayerItemVideoOutput() 490 | item.add(playerOutput) 491 | 492 | player = AVPlayer(playerItem: item) 493 | player.actionAtItemEnd = .pause 494 | player.rate = .init(rate) 495 | player.volume = .init(volume) 496 | player.isMuted = isMuted 497 | 498 | player.automaticallyWaitsToMinimizeStalling = false 499 | 500 | // 添加监听 501 | addObserver() 502 | addObserver(item: item) 503 | addNotification(item: item) 504 | 505 | // 设置初始播放意图 506 | intendedToPlay = configuration.isAutoplay 507 | 508 | let layer = AVPlayerLayer(player: player) 509 | layer.masksToBounds = true 510 | self.playerLayer = layer 511 | 512 | // 构建播放视图 513 | playerView = VideoPlayerView(layer) 514 | playerView.observe { (size, animation) in 515 | if let animation = animation { 516 | CATransaction.begin() 517 | CATransaction.setAnimationDuration(animation.duration) 518 | CATransaction.setAnimationTimingFunction(animation.timingFunction) 519 | layer.frame = .init(origin: .zero, size: size) 520 | CATransaction.commit() 521 | 522 | } else { 523 | layer.frame = .init(origin: .zero, size: size) 524 | } 525 | } 526 | playerView.observe { (contentMode) in 527 | switch contentMode { 528 | case .scaleToFill: 529 | layer.videoGravity = .resize 530 | 531 | case .scaleAspectFit: 532 | layer.videoGravity = .resizeAspect 533 | 534 | case .scaleAspectFill: 535 | layer.videoGravity = .resizeAspectFill 536 | 537 | default: 538 | layer.videoGravity = .resizeAspectFill 539 | } 540 | } 541 | playerView.contentMode = .scaleAspectFit 542 | 543 | return playerView 544 | } 545 | 546 | func play() { 547 | switch state { 548 | case .prepare: 549 | intendedToPlay = true 550 | 551 | case .playing where intendedToSeek != nil: 552 | intendedToPlay = true 553 | 554 | case .playing where intendedToSeek == nil: 555 | intendedToPlay = true 556 | player.rate = .init(rate) 557 | 558 | case .finished: 559 | state = .playing 560 | intendedToPlay = true 561 | // Seek到意图位置或起始位置 562 | seek(to: intendedToSeek ?? .init(time: .zero)) 563 | 564 | default: 565 | break 566 | } 567 | } 568 | 569 | func pause() { 570 | intendedToPlay = false 571 | player.pause() 572 | } 573 | 574 | func stop() { 575 | clear() 576 | state = .stopped 577 | } 578 | 579 | func seek(to target: VideoPlayer.Seek) { 580 | guard 581 | let item = player.currentItem, 582 | player.status == .readyToPlay, 583 | case .playing = state else { 584 | // 设置跳转意图 585 | intendedToSeek = target 586 | return 587 | } 588 | // 先取消上一个 保证Seek状态 589 | item.cancelPendingSeeks() 590 | // 设置跳转意图 591 | intendedToSeek = target 592 | // 暂停当前播放 593 | player.pause() 594 | // 代理回调 当前时间为目标时间 595 | delegate { $0.videoPlayer(self, updatedCurrent: target.time) } 596 | // 代理回调 开始跳转 597 | delegate { $0.videoPlayer(self, seekBegan: target) } 598 | // 开始Seek 599 | seek(to: target, for: item) { [weak self] finished in 600 | guard let self = self else { return } 601 | // 清空跳转意图 602 | self.intendedToSeek = nil 603 | // 设置当前时间校准器 604 | self.currentTimeCalibrator = target.time 605 | // 根据播放意图继续播放 606 | if finished, self.intendedToPlay { 607 | self.play() 608 | } 609 | // 代理回调 结束跳转 610 | self.delegate { $0.videoPlayer(self, seekEnded: target) } 611 | } 612 | } 613 | 614 | private func seek(to target: VideoPlayer.Seek, for item: AVPlayerItem, with completion: @escaping ((Bool) -> Void)) { 615 | var time = CMTime( 616 | seconds: target.time, 617 | preferredTimescale: item.duration.timescale 618 | ) 619 | // 校验目标时间是否可跳转 620 | let isSeekable = item.seekableTimeRanges.contains { value in 621 | value.timeRangeValue.containsTime(time) 622 | } 623 | if !isSeekable { 624 | // 限制跳转时间 625 | time = min(max(time, .zero), item.duration) 626 | } 627 | item.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero) { (finished) in 628 | // 完成回调 629 | target.completion?(finished) 630 | completion(finished) 631 | } 632 | } 633 | 634 | var current: TimeInterval { 635 | guard let item = player.currentItem else { return 0 } 636 | let time = item.currentTime().seconds 637 | // 如果有跳转意图 则返回跳转的目标时间 638 | if let seek = intendedToSeek { 639 | return seek.time 640 | } 641 | // 当前时间校准器 如果大于 当前时间, 则返回校准时间, 否则清空校准器 返回当前时间. 642 | if let temp = currentTimeCalibrator, temp > time { 643 | return temp 644 | } 645 | currentTimeCalibrator = nil 646 | return time.isNaN ? 0 : time 647 | } 648 | 649 | var duration: TimeInterval { 650 | guard let item = player.currentItem else { return 0 } 651 | let time = CMTimeGetSeconds(item.duration) 652 | return time.isNaN ? 0 : time 653 | } 654 | 655 | var buffer: Double { 656 | guard let item = player.currentItem else { return 0 } 657 | guard let range = item.loadedTimeRanges.first?.timeRangeValue else { return 0 } 658 | guard duration > 0 else { return 0 } 659 | let buffer = range.start.seconds + range.duration.seconds 660 | return buffer / duration 661 | } 662 | 663 | var view: VideoPlayerView { 664 | return playerView 665 | } 666 | 667 | func screenshot(completion: (UIImage?) -> Void) { 668 | guard let item = player.currentItem else { 669 | completion(.none) 670 | return 671 | } 672 | guard 673 | let pixelBuffer = playerOutput.copyPixelBuffer( 674 | forItemTime: item.currentTime(), 675 | itemTimeForDisplay: nil 676 | ) else { 677 | completion(.none) 678 | return 679 | } 680 | 681 | let ciimage = CIImage(cvPixelBuffer: pixelBuffer) 682 | let context = CIContext() 683 | guard 684 | let cgimage = context.createCGImage( 685 | ciimage, 686 | from: .init( 687 | x: 0, 688 | y: 0, 689 | width: CVPixelBufferGetWidth(pixelBuffer), 690 | height: CVPixelBufferGetHeight(pixelBuffer) 691 | ) 692 | ) else { 693 | completion(.none) 694 | return 695 | } 696 | completion(.init(cgImage: cgimage)) 697 | } 698 | } 699 | 700 | extension AVVideoPlayer: VideoPlayerDelegates { 701 | 702 | typealias Element = VideoPlayerDelegate 703 | } 704 | 705 | extension AVURLAsset: VideoPlayerURLAsset { 706 | 707 | public var value: URL { 708 | return url 709 | } 710 | } 711 | -------------------------------------------------------------------------------- /Sources/AV/AVVideoResourceLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AVVideoResourceLoader.swift 3 | // VideoPlayer 4 | // 5 | // Created by 李响 on 2021/1/15. 6 | // Copyright © 2021 swift. All rights reserved. 7 | // 8 | 9 | import AVFoundation 10 | 11 | class AVVideoResourceLoader: NSObject, AVAssetResourceLoaderDelegate { 12 | 13 | func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool { 14 | 15 | return true 16 | } 17 | 18 | func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) { 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Core/VideoPlayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoPlayer.swift 3 | // ┌─┐ ┌───────┐ ┌───────┐ 4 | // │ │ │ ┌─────┘ │ ┌─────┘ 5 | // │ │ │ └─────┐ │ └─────┐ 6 | // │ │ │ ┌─────┘ │ ┌─────┘ 7 | // │ └─────┐│ └─────┐ │ └─────┐ 8 | // └───────┘└───────┘ └───────┘ 9 | // 10 | import Foundation 11 | import UIKit 12 | import AVFoundation 13 | 14 | public enum VideoPlayer { 15 | /// 播放状态 16 | /// stopped -> prepare -> playing -> finished 17 | public enum State { 18 | /// 准备播放: 调用`prepare(resource:)`后的状态. 19 | case prepare 20 | /// 正在播放: `prepare`处理完成后的状态, 当`finished`状态时再次调用`play()`也会回到该状态. 21 | case playing 22 | /// 播放停止: 默认的初始状态, 调用`stop()`后的状态. 23 | case stopped 24 | /// 播放完成: 在`isLoop = false`时会触发. 25 | case finished 26 | /// 播放失败: 调用`prepare(resource:)`后的任何时候 只要发生了异常便会触发该状态. 27 | case failed(Swift.Error?) 28 | } 29 | 30 | /// 控制状态: 仅在 state 为 .playing 状态时可用 31 | public enum ControlState { 32 | /// 播放中 33 | case playing 34 | /// 暂停中 35 | case pausing 36 | } 37 | 38 | /// 加载状态 39 | public enum LoadingState { 40 | /// 已开始 41 | case began 42 | /// 已结束 43 | case ended 44 | } 45 | 46 | public struct Seek { 47 | /// 目标时间 (秒) 48 | public let time: TimeInterval 49 | /// 完成回调 (成功为true, 失败为false, 失败可能是由于网络问题或被其他Seek抢占导致的) 50 | let completion: ((Bool) -> Void)? 51 | 52 | public init(time: TimeInterval, completion: ((Bool) -> Void)? = .none) { 53 | self.time = time 54 | self.completion = completion 55 | } 56 | } 57 | } 58 | 59 | public extension VideoPlayer { 60 | 61 | static func setupAudioSession(in queue: DispatchQueue, with completion: @escaping (() -> Void) = {}) { 62 | UIApplication.shared.beginReceivingRemoteControlEvents() 63 | queue.async { 64 | do { 65 | let session = AVAudioSession.sharedInstance() 66 | try session.setCategory(.playback, mode: .default) 67 | try session.setActive(true, options: [.notifyOthersOnDeactivation]) 68 | } catch { 69 | print("音频会话创建失败") 70 | } 71 | 72 | DispatchQueue.sync(safe: .main, execute: completion) 73 | } 74 | } 75 | 76 | static func removeAudioSession(in queue: DispatchQueue, with completion: @escaping (() -> Void) = {}) { 77 | UIApplication.shared.endReceivingRemoteControlEvents() 78 | queue.async { 79 | do { 80 | let session = AVAudioSession.sharedInstance() 81 | try session.setCategory(.playback, mode: .default) 82 | try session.setActive(false, options: [.notifyOthersOnDeactivation]) 83 | } catch { 84 | print("音频会话释放失败") 85 | } 86 | 87 | DispatchQueue.sync(safe: .main, execute: completion) 88 | } 89 | } 90 | } 91 | 92 | extension VideoPlayer { 93 | 94 | public class Builder { 95 | 96 | public typealias Generator = (VideoPlayerConfiguration) -> VideoPlayerable 97 | 98 | private var generator: Generator 99 | 100 | public private(set) lazy var shared = generator(.init()) 101 | 102 | public init(_ generator: @escaping Generator) { 103 | self.generator = generator 104 | } 105 | 106 | public func instance(_ configuration: VideoPlayerConfiguration = .init()) -> VideoPlayerable { 107 | return generator(configuration) 108 | } 109 | } 110 | } 111 | 112 | extension DispatchQueue { 113 | 114 | public static let audioSession: DispatchQueue = .init(label: "com.audio.session.queue") 115 | } 116 | 117 | extension DispatchQueue { 118 | 119 | private static func isCurrent(_ queue: DispatchQueue) -> Bool { 120 | let key = DispatchSpecificKey() 121 | 122 | queue.setSpecific(key: key, value: ()) 123 | defer { queue.setSpecific(key: key, value: nil) } 124 | 125 | return getSpecific(key: key) != nil 126 | } 127 | 128 | /// 安全同步执行 (可防止死锁) 129 | /// - Parameter queue: 队列 130 | /// - Parameter work: 执行 131 | static func sync(safe queue: DispatchQueue, execute work: () -> Void) { 132 | isCurrent(queue) ? work() : queue.sync(execute: work) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Sources/Core/VideoPlayerConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoPlayerConfiguration.swift 3 | // VideoPlayer 4 | // 5 | // Created by 李响 on 2022/7/22. 6 | // Copyright © 2022 swift. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct VideoPlayerConfiguration { 12 | 13 | /// 是否自动播放 默认: true 14 | public var isAutoplay: Bool = true 15 | 16 | /// 预缓冲时长 默认 0 自动选择 17 | public var preferredBufferDuration: TimeInterval = 0 18 | 19 | /// 当前时间回调周期间隔 每秒次数 默认: 每秒10次 20 | public var currentTimeCallbackPeriodicInterval: UInt = 10 21 | 22 | public init() { 23 | 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Core/VideoPlayerDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoPlayerDelegate.swift 3 | // ┌─┐ ┌───────┐ ┌───────┐ 4 | // │ │ │ ┌─────┘ │ ┌─────┘ 5 | // │ │ │ └─────┐ │ └─────┐ 6 | // │ │ │ ┌─────┘ │ ┌─────┘ 7 | // │ └─────┐│ └─────┐ │ └─────┐ 8 | // └───────┘└───────┘ └───────┘ 9 | // 10 | import Foundation 11 | 12 | public protocol VideoPlayerDelegate: AnyObject { 13 | /// 加载状态 14 | func videoPlayerLoadingState(_ player: VideoPlayerable, state: VideoPlayer.LoadingState) 15 | /// 控制状态 16 | func videoPlayerControlState(_ player: VideoPlayerable, state: VideoPlayer.ControlState) 17 | /// 播放状态 18 | func videoPlayerState(_ player: VideoPlayerable, state: VideoPlayer.State) 19 | 20 | /// 更新缓冲进度 21 | func videoPlayer(_ player: VideoPlayerable, updatedBuffer progress: Double) 22 | /// 更新总时间 (秒) 23 | func videoPlayer(_ player: VideoPlayerable, updatedDuration time: Double) 24 | /// 更新当前时间 (秒) 25 | func videoPlayer(_ player: VideoPlayerable, updatedCurrent time: Double) 26 | /// 跳转开始 27 | func videoPlayer(_ player: VideoPlayerable, seekBegan: VideoPlayer.Seek) 28 | /// 跳转结束 29 | func videoPlayer(_ player: VideoPlayerable, seekEnded: VideoPlayer.Seek) 30 | } 31 | 32 | public extension VideoPlayerDelegate { 33 | 34 | func videoPlayerLoadingState(_ player: VideoPlayerable, state: VideoPlayer.LoadingState) { } 35 | func videoPlayerControlState(_ player: VideoPlayerable, state: VideoPlayer.ControlState) { } 36 | func videoPlayerState(_ player: VideoPlayerable, state: VideoPlayer.State) { } 37 | 38 | func videoPlayer(_ player: VideoPlayerable, updatedBuffer progress: Double) { } 39 | func videoPlayer(_ player: VideoPlayerable, updatedDuration time: Double) { } 40 | func videoPlayer(_ player: VideoPlayerable, updatedCurrent time: Double) { } 41 | func videoPlayer(_ player: VideoPlayerable, seekBegan: VideoPlayer.Seek) { } 42 | func videoPlayer(_ player: VideoPlayerable, seekEnded: VideoPlayer.Seek) { } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Core/VideoPlayerDelegates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoPlayerDelegates.swift 3 | // ┌─┐ ┌───────┐ ┌───────┐ 4 | // │ │ │ ┌─────┘ │ ┌─────┘ 5 | // │ │ │ └─────┐ │ └─────┐ 6 | // │ │ │ ┌─────┘ │ ┌─────┘ 7 | // │ └─────┐│ └─────┐ │ └─────┐ 8 | // └───────┘└───────┘ └───────┘ 9 | // 10 | import Foundation 11 | 12 | public protocol VideoPlayerDelegates: NSObjectProtocol { 13 | 14 | associatedtype Element 15 | 16 | var delegates: [VideoPlayerDelegateBridge] { get set } 17 | } 18 | 19 | extension VideoPlayerDelegates { 20 | 21 | public func add(delegate: Element) { 22 | guard !delegates.contains(where: { $0.object === delegate as AnyObject }) else { 23 | return 24 | } 25 | delegates.append(.init(delegate as AnyObject)) 26 | } 27 | 28 | public func remove(delegate: Element) { 29 | guard let index = delegates.firstIndex(where: { $0.object === delegate as AnyObject }) else { 30 | return 31 | } 32 | delegates.remove(at: index) 33 | } 34 | 35 | public func delegate(_ operat: (Element) -> Void) { 36 | delegates = delegates.filter({ $0.object != nil }) 37 | for delegate in delegates { 38 | guard let object = delegate.object as? Element else { continue } 39 | operat(object) 40 | } 41 | } 42 | } 43 | 44 | public class VideoPlayerDelegateBridge { 45 | weak var object: I? 46 | init(_ object: I?) { 47 | self.object = object 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/Core/VideoPlayerRemote.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoPlayerRemote.swift 3 | // ┌─┐ ┌───────┐ ┌───────┐ 4 | // │ │ │ ┌─────┘ │ ┌─────┘ 5 | // │ │ │ └─────┐ │ └─────┐ 6 | // │ │ │ ┌─────┘ │ ┌─────┘ 7 | // │ └─────┐│ └─────┐ │ └─────┐ 8 | // └───────┘└───────┘ └───────┘ 9 | // 10 | import MediaPlayer.MPNowPlayingInfoCenter 11 | import MediaPlayer.MPRemoteCommandCenter 12 | 13 | open class VideoPlayerRemoteControl: NSObject { 14 | 15 | public let player: VideoPlayerable 16 | 17 | public init(_ player: VideoPlayerable) { 18 | self.player = player 19 | super.init() 20 | // 添加播放器代理 21 | player.add(delegate: self) 22 | 23 | setupCommand() 24 | } 25 | 26 | /// 设置远程控制 27 | open func setupCommand() { 28 | cleanCommand() 29 | 30 | let remote = MPRemoteCommandCenter.shared() 31 | remote.playCommand.addTarget(self, action: #selector(playCommandAction)) 32 | remote.pauseCommand.addTarget(self, action: #selector(pauseCommandAction)) 33 | 34 | updatePlayingInfo() 35 | } 36 | 37 | /// 清理远程控制 38 | open func cleanCommand() { 39 | let remote = MPRemoteCommandCenter.shared() 40 | remote.playCommand.removeTarget(self) 41 | remote.pauseCommand.removeTarget(self) 42 | } 43 | 44 | /// 更新播放信息 45 | public func updatePlayingInfo() { 46 | var info = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] 47 | info[MPMediaItemPropertyPlaybackDuration] = player.duration 48 | info[MPNowPlayingInfoPropertyPlaybackRate] = player.rate 49 | info[MPNowPlayingInfoPropertyDefaultPlaybackRate] = 1.0 50 | info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player.current 51 | MPNowPlayingInfoCenter.default().nowPlayingInfo = info 52 | } 53 | 54 | public func cleanPlayingInfo() { 55 | MPNowPlayingInfoCenter.default().nowPlayingInfo = nil 56 | } 57 | 58 | deinit { 59 | cleanCommand() 60 | } 61 | 62 | /// 设置播放信息 63 | /// 64 | /// - Parameters: 65 | /// - title: 标题 66 | /// - artist: 作者 67 | /// - thumb: 封面 68 | /// - url: 链接 69 | open func set(title: String, artist: String, thumb: UIImage, url: URL) { 70 | var info: [String : Any] = [:] 71 | info[MPMediaItemPropertyTitle] = title 72 | info[MPMediaItemPropertyArtist] = artist 73 | 74 | if #available(iOS 10.3, *) { 75 | // 当前URL 76 | info[MPNowPlayingInfoPropertyAssetURL] = url 77 | } 78 | 79 | if #available(iOS 10.0, *) { 80 | // 封面图 81 | let artwork = MPMediaItemArtwork( 82 | boundsSize: thumb.size, 83 | requestHandler: { (size) -> UIImage in 84 | return thumb 85 | } 86 | ) 87 | info[MPMediaItemPropertyArtwork] = artwork 88 | // 媒体类型 89 | info[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.video.rawValue 90 | 91 | } else { 92 | // 封面图 93 | let artwork = MPMediaItemArtwork(image: thumb) 94 | info[MPMediaItemPropertyArtwork] = artwork 95 | } 96 | 97 | MPNowPlayingInfoCenter.default().nowPlayingInfo = info 98 | } 99 | 100 | @objc 101 | private func playCommandAction(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { 102 | switch player.state { 103 | case .playing where player.control == .pausing: 104 | player.play() 105 | return .success 106 | 107 | case .finished where player.control == .pausing: 108 | player.play() 109 | return .success 110 | 111 | default: 112 | return .noSuchContent 113 | } 114 | } 115 | 116 | @objc 117 | private func pauseCommandAction(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { 118 | guard case .playing = player.state else { return .noSuchContent } 119 | guard player.control == .playing else { return .noSuchContent } 120 | 121 | player.pause() 122 | return .success 123 | } 124 | } 125 | 126 | extension VideoPlayerRemoteControl: VideoPlayerDelegate { 127 | 128 | public func videoPlayerLoadingState(_ player: VideoPlayerable, state: VideoPlayer.LoadingState) { 129 | updatePlayingInfo() 130 | } 131 | 132 | public func videoPlayerControlState(_ player: VideoPlayerable, state: VideoPlayer.ControlState) { 133 | updatePlayingInfo() 134 | } 135 | 136 | public func videoPlayerState(_ player: VideoPlayerable, state: VideoPlayer.State) { 137 | switch state { 138 | case .prepare: 139 | cleanPlayingInfo() 140 | 141 | case .playing: 142 | setupCommand() 143 | 144 | case .stopped: 145 | cleanCommand() 146 | cleanPlayingInfo() 147 | 148 | case .finished: 149 | updatePlayingInfo() 150 | 151 | case .failed: 152 | cleanCommand() 153 | cleanPlayingInfo() 154 | } 155 | } 156 | 157 | public func videoPlayer(_ player: VideoPlayerable, updatedCurrent time: Double) { 158 | updatePlayingInfo() 159 | } 160 | 161 | public func videoPlayer(_ player: VideoPlayerable, updatedDuration time: Double) { 162 | updatePlayingInfo() 163 | } 164 | 165 | public func videoPlayer(_ player: VideoPlayerable, seekEnded: VideoPlayer.Seek) { 166 | updatePlayingInfo() 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Sources/Core/VideoPlayerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoPlayerView.swift 3 | // ┌─┐ ┌───────┐ ┌───────┐ 4 | // │ │ │ ┌─────┘ │ ┌─────┘ 5 | // │ │ │ └─────┐ │ └─────┐ 6 | // │ │ │ ┌─────┘ │ ┌─────┘ 7 | // │ └─────┐│ └─────┐ │ └─────┐ 8 | // └───────┘└───────┘ └───────┘ 9 | // 10 | import UIKit 11 | import AVKit 12 | 13 | public class VideoPlayerView: UIView { 14 | 15 | private var updateContentMode: ((UIView.ContentMode) -> Void)? 16 | private var updateLayout: ((CGSize, CAAnimation?) -> Void)? 17 | 18 | public let playerLayer: CALayer 19 | 20 | public init(_ layer: CALayer) { 21 | playerLayer = layer 22 | super.init(frame: .zero) 23 | clipsToBounds = true 24 | self.layer.addSublayer(layer) 25 | } 26 | 27 | required init?(coder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | 31 | public func observe(contentMode: @escaping ((UIView.ContentMode) -> Void)) { 32 | updateContentMode = { (mode) in 33 | contentMode(mode) 34 | } 35 | updateContentMode?(self.contentMode) 36 | } 37 | 38 | public func observe(layout: @escaping ((CGSize, CAAnimation?) -> Void)) { 39 | updateLayout = { (size, animation) in 40 | layout(size, animation) 41 | } 42 | layoutSubviews() 43 | } 44 | 45 | public override var contentMode: UIView.ContentMode { 46 | get { 47 | return super.contentMode 48 | } 49 | set { 50 | super.contentMode = newValue 51 | self.updateContentMode?(newValue) 52 | } 53 | } 54 | 55 | public override func layoutSubviews() { 56 | super.layoutSubviews() 57 | 58 | updateLayout?(bounds.size, layer.animation(forKey: "bounds.size")) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/Core/VideoPlayerable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoPlayerable.swift 3 | // ┌─┐ ┌───────┐ ┌───────┐ 4 | // │ │ │ ┌─────┘ │ ┌─────┘ 5 | // │ │ │ └─────┐ │ └─────┐ 6 | // │ │ │ ┌─────┘ │ ┌─────┘ 7 | // │ └─────┐│ └─────┐ │ └─────┐ 8 | // └───────┘└───────┘ └───────┘ 9 | // 10 | import Foundation 11 | 12 | public protocol VideoPlayerURLAsset { 13 | var value: URL { get } 14 | } 15 | 16 | public protocol VideoPlayerable: NSObjectProtocol { 17 | 18 | /// 准备 19 | @discardableResult 20 | func prepare(resource: VideoPlayerURLAsset) -> VideoPlayerView 21 | /// 播放 22 | func play() 23 | /// 暂停 24 | func pause() 25 | /// 停止 26 | func stop() 27 | /// 快速定位到指定播放时间点 (多次调用 以最后一次为准) 28 | func seek(to target: VideoPlayer.Seek) 29 | 30 | /// 资源 31 | var resource: VideoPlayerURLAsset? { get } 32 | /// 播放器当前状态 33 | var state: VideoPlayer.State { get } 34 | /// 播放器控制状态 35 | var control: VideoPlayer.ControlState { get } 36 | /// 播放器加载状态 37 | var loading: VideoPlayer.LoadingState { get } 38 | 39 | /// 当前时间 40 | var current: TimeInterval { get } 41 | /// 视频总时长 42 | var duration: TimeInterval { get } 43 | /// 缓冲进度 0 - 1 44 | var buffer: Double { get } 45 | /// VideoPlayer 的画面输出到该 UIView 对象 46 | var view: VideoPlayerView { get } 47 | 48 | /// 播放速率 49 | var rate: Double { get set } 50 | /// 是否静音 51 | var isMuted: Bool { get set } 52 | /// 音量控制 53 | var volume: Double { get set } 54 | /// 是否循环播放 默认: false 55 | var isLoop: Bool { get set } 56 | 57 | /// 播放配置 58 | var configuration: VideoPlayerConfiguration { get } 59 | 60 | /// 添加委托 61 | func add(delegate: VideoPlayerDelegate) 62 | /// 移除委托 63 | func remove(delegate: VideoPlayerDelegate) 64 | /// 截图 获取当前播放的画面截图 65 | func screenshot(completion: @escaping (UIImage?) -> Void) 66 | } 67 | 68 | extension URL: VideoPlayerURLAsset { 69 | 70 | public var value: URL { 71 | return self 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | NSPrivacyAccessedAPITypes 8 | 9 | NSPrivacyTrackingDomains 10 | 11 | NSPrivacyCollectedDataTypes 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Sources/VideoPlayer.h: -------------------------------------------------------------------------------- 1 | // 2 | // VideoPlayer.h 3 | // ┌─┐ ┌───────┐ ┌───────┐ 4 | // │ │ │ ┌─────┘ │ ┌─────┘ 5 | // │ │ │ └─────┐ │ └─────┐ 6 | // │ │ │ ┌─────┘ │ ┌─────┘ 7 | // │ └─────┐│ └─────┐ │ └─────┐ 8 | // └───────┘└───────┘ └───────┘ 9 | // 10 | #import 11 | 12 | //! Project version number for SwiftRouter. 13 | FOUNDATION_EXPORT double SwiftRouterVersionNumber; 14 | 15 | //! Project version string for SwiftRouter. 16 | FOUNDATION_EXPORT const unsigned char SwiftRouterVersionString[]; 17 | 18 | // In this header, you should import all the public headers of your framework using statements like #import 19 | 20 | 21 | -------------------------------------------------------------------------------- /VideoPlayer.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "VideoPlayer" 4 | s.version = "5.2.1" 5 | s.summary = "视频播放器" 6 | 7 | s.homepage = "https://github.com/lixiang1994/VideoPlayer" 8 | 9 | s.license = { :type => "MIT", :file => "LICENSE" } 10 | 11 | s.author = { "LEE" => "18611401994@163.com" } 12 | 13 | s.platform = :ios, "10.0" 14 | 15 | s.source = { :git => "https://github.com/lixiang1994/VideoPlayer.git", :tag => s.version } 16 | 17 | s.requires_arc = true 18 | 19 | s.frameworks = 'UIKit', 'Foundation', 'AVFoundation', 'MediaPlayer' 20 | 21 | s.swift_version = '5.2' 22 | 23 | s.default_subspec = 'Core', 'AVPlayer' 24 | 25 | s.subspec 'Core' do |sub| 26 | sub.source_files = 'Sources/Core/*.swift' 27 | sub.dependency 'VideoPlayer/Privacy' 28 | end 29 | 30 | s.subspec 'AVPlayer' do |sub| 31 | sub.dependency 'VideoPlayer/Core' 32 | sub.source_files = 'Sources/AV/*.swift' 33 | end 34 | 35 | s.subspec 'Privacy' do |ss| 36 | ss.resource_bundles = { 37 | s.name => 'Sources/PrivacyInfo.xcprivacy' 38 | } 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /VideoPlayer.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 9B39F4332316842400505AD0 /* VideoPlayerable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B39F42F2316842300505AD0 /* VideoPlayerable.swift */; }; 11 | 9B39F4342316842400505AD0 /* VideoPlayerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B39F4302316842300505AD0 /* VideoPlayerDelegate.swift */; }; 12 | 9B39F4352316842400505AD0 /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B39F4312316842300505AD0 /* VideoPlayer.swift */; }; 13 | 9B39F4382316845D00505AD0 /* AVVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B39F4372316845D00505AD0 /* AVVideoPlayer.swift */; }; 14 | 9B39F43D2316948700505AD0 /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B39F43C2316948700505AD0 /* VideoPlayerView.swift */; }; 15 | 9B39F449231694B600505AD0 /* VideoPlayerDelegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B39F448231694B600505AD0 /* VideoPlayerDelegates.swift */; }; 16 | 9B7AEA9625B1940E0040B3EA /* AVVideoResourceLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B7AEA9525B1940E0040B3EA /* AVVideoResourceLoader.swift */; }; 17 | 9B838A04288AA08E00EAA590 /* VideoPlayerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B838A03288AA08E00EAA590 /* VideoPlayerConfiguration.swift */; }; 18 | C9D0289522607AD100B5E061 /* VideoPlayer.h in Headers */ = {isa = PBXBuildFile; fileRef = C9D0289322607AD100B5E061 /* VideoPlayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXFileReference section */ 22 | 3AF41860838A4FC97A843309 /* Pods_VideoPlayer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_VideoPlayer.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | 9B39F42F2316842300505AD0 /* VideoPlayerable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerable.swift; sourceTree = ""; }; 24 | 9B39F4302316842300505AD0 /* VideoPlayerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerDelegate.swift; sourceTree = ""; }; 25 | 9B39F4312316842300505AD0 /* VideoPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = ""; }; 26 | 9B39F4372316845D00505AD0 /* AVVideoPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AVVideoPlayer.swift; sourceTree = ""; }; 27 | 9B39F43C2316948700505AD0 /* VideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = ""; }; 28 | 9B39F448231694B600505AD0 /* VideoPlayerDelegates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerDelegates.swift; sourceTree = ""; }; 29 | 9B7AEA9525B1940E0040B3EA /* AVVideoResourceLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVVideoResourceLoader.swift; sourceTree = ""; }; 30 | 9B838A03288AA08E00EAA590 /* VideoPlayerConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerConfiguration.swift; sourceTree = ""; }; 31 | C9D0289022607AD100B5E061 /* VideoPlayer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = VideoPlayer.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 32 | C9D0289322607AD100B5E061 /* VideoPlayer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VideoPlayer.h; sourceTree = ""; }; 33 | C9D0289422607AD100B5E061 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 34 | /* End PBXFileReference section */ 35 | 36 | /* Begin PBXFrameworksBuildPhase section */ 37 | C9D0288D22607AD100B5E061 /* Frameworks */ = { 38 | isa = PBXFrameworksBuildPhase; 39 | buildActionMask = 2147483647; 40 | files = ( 41 | ); 42 | runOnlyForDeploymentPostprocessing = 0; 43 | }; 44 | /* End PBXFrameworksBuildPhase section */ 45 | 46 | /* Begin PBXGroup section */ 47 | 335179C6A7819FE55E984E38 /* Frameworks */ = { 48 | isa = PBXGroup; 49 | children = ( 50 | 3AF41860838A4FC97A843309 /* Pods_VideoPlayer.framework */, 51 | ); 52 | name = Frameworks; 53 | sourceTree = ""; 54 | }; 55 | 9B39F42D231683A100505AD0 /* AV */ = { 56 | isa = PBXGroup; 57 | children = ( 58 | 9B39F4372316845D00505AD0 /* AVVideoPlayer.swift */, 59 | 9B7AEA9525B1940E0040B3EA /* AVVideoResourceLoader.swift */, 60 | ); 61 | path = AV; 62 | sourceTree = ""; 63 | }; 64 | 9B39F45023169EE800505AD0 /* Core */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | 9B39F4312316842300505AD0 /* VideoPlayer.swift */, 68 | 9B39F42F2316842300505AD0 /* VideoPlayerable.swift */, 69 | 9B838A03288AA08E00EAA590 /* VideoPlayerConfiguration.swift */, 70 | 9B39F4302316842300505AD0 /* VideoPlayerDelegate.swift */, 71 | 9B39F448231694B600505AD0 /* VideoPlayerDelegates.swift */, 72 | 9B39F43C2316948700505AD0 /* VideoPlayerView.swift */, 73 | ); 74 | path = Core; 75 | sourceTree = ""; 76 | }; 77 | C9D0288622607AD100B5E061 = { 78 | isa = PBXGroup; 79 | children = ( 80 | C9D0289222607AD100B5E061 /* Sources */, 81 | C9D0289122607AD100B5E061 /* Products */, 82 | 335179C6A7819FE55E984E38 /* Frameworks */, 83 | ); 84 | sourceTree = ""; 85 | }; 86 | C9D0289122607AD100B5E061 /* Products */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | C9D0289022607AD100B5E061 /* VideoPlayer.framework */, 90 | ); 91 | name = Products; 92 | sourceTree = ""; 93 | }; 94 | C9D0289222607AD100B5E061 /* Sources */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 9B39F45023169EE800505AD0 /* Core */, 98 | 9B39F42D231683A100505AD0 /* AV */, 99 | C9D0289322607AD100B5E061 /* VideoPlayer.h */, 100 | C9D0289422607AD100B5E061 /* Info.plist */, 101 | ); 102 | path = Sources; 103 | sourceTree = ""; 104 | }; 105 | /* End PBXGroup section */ 106 | 107 | /* Begin PBXHeadersBuildPhase section */ 108 | C9D0288B22607AD100B5E061 /* Headers */ = { 109 | isa = PBXHeadersBuildPhase; 110 | buildActionMask = 2147483647; 111 | files = ( 112 | C9D0289522607AD100B5E061 /* VideoPlayer.h in Headers */, 113 | ); 114 | runOnlyForDeploymentPostprocessing = 0; 115 | }; 116 | /* End PBXHeadersBuildPhase section */ 117 | 118 | /* Begin PBXNativeTarget section */ 119 | C9D0288F22607AD100B5E061 /* VideoPlayer */ = { 120 | isa = PBXNativeTarget; 121 | buildConfigurationList = C9D0289822607AD100B5E061 /* Build configuration list for PBXNativeTarget "VideoPlayer" */; 122 | buildPhases = ( 123 | C9D0288B22607AD100B5E061 /* Headers */, 124 | C9D0288C22607AD100B5E061 /* Sources */, 125 | C9D0288D22607AD100B5E061 /* Frameworks */, 126 | C9D0288E22607AD100B5E061 /* Resources */, 127 | ); 128 | buildRules = ( 129 | ); 130 | dependencies = ( 131 | ); 132 | name = VideoPlayer; 133 | productName = SwiftRouter; 134 | productReference = C9D0289022607AD100B5E061 /* VideoPlayer.framework */; 135 | productType = "com.apple.product-type.framework"; 136 | }; 137 | /* End PBXNativeTarget section */ 138 | 139 | /* Begin PBXProject section */ 140 | C9D0288722607AD100B5E061 /* Project object */ = { 141 | isa = PBXProject; 142 | attributes = { 143 | LastUpgradeCheck = 1020; 144 | ORGANIZATIONNAME = swift; 145 | TargetAttributes = { 146 | C9D0288F22607AD100B5E061 = { 147 | CreatedOnToolsVersion = 10.2; 148 | LastSwiftMigration = 1030; 149 | }; 150 | }; 151 | }; 152 | buildConfigurationList = C9D0288A22607AD100B5E061 /* Build configuration list for PBXProject "VideoPlayer" */; 153 | compatibilityVersion = "Xcode 9.3"; 154 | developmentRegion = en; 155 | hasScannedForEncodings = 0; 156 | knownRegions = ( 157 | en, 158 | ); 159 | mainGroup = C9D0288622607AD100B5E061; 160 | productRefGroup = C9D0289122607AD100B5E061 /* Products */; 161 | projectDirPath = ""; 162 | projectRoot = ""; 163 | targets = ( 164 | C9D0288F22607AD100B5E061 /* VideoPlayer */, 165 | ); 166 | }; 167 | /* End PBXProject section */ 168 | 169 | /* Begin PBXResourcesBuildPhase section */ 170 | C9D0288E22607AD100B5E061 /* Resources */ = { 171 | isa = PBXResourcesBuildPhase; 172 | buildActionMask = 2147483647; 173 | files = ( 174 | ); 175 | runOnlyForDeploymentPostprocessing = 0; 176 | }; 177 | /* End PBXResourcesBuildPhase section */ 178 | 179 | /* Begin PBXSourcesBuildPhase section */ 180 | C9D0288C22607AD100B5E061 /* Sources */ = { 181 | isa = PBXSourcesBuildPhase; 182 | buildActionMask = 2147483647; 183 | files = ( 184 | 9B838A04288AA08E00EAA590 /* VideoPlayerConfiguration.swift in Sources */, 185 | 9B39F4332316842400505AD0 /* VideoPlayerable.swift in Sources */, 186 | 9B39F4342316842400505AD0 /* VideoPlayerDelegate.swift in Sources */, 187 | 9B7AEA9625B1940E0040B3EA /* AVVideoResourceLoader.swift in Sources */, 188 | 9B39F4382316845D00505AD0 /* AVVideoPlayer.swift in Sources */, 189 | 9B39F4352316842400505AD0 /* VideoPlayer.swift in Sources */, 190 | 9B39F449231694B600505AD0 /* VideoPlayerDelegates.swift in Sources */, 191 | 9B39F43D2316948700505AD0 /* VideoPlayerView.swift in Sources */, 192 | ); 193 | runOnlyForDeploymentPostprocessing = 0; 194 | }; 195 | /* End PBXSourcesBuildPhase section */ 196 | 197 | /* Begin XCBuildConfiguration section */ 198 | C9D0289622607AD100B5E061 /* Debug */ = { 199 | isa = XCBuildConfiguration; 200 | buildSettings = { 201 | ALWAYS_SEARCH_USER_PATHS = NO; 202 | CLANG_ANALYZER_NONNULL = YES; 203 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 204 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 205 | CLANG_CXX_LIBRARY = "libc++"; 206 | CLANG_ENABLE_MODULES = YES; 207 | CLANG_ENABLE_OBJC_ARC = YES; 208 | CLANG_ENABLE_OBJC_WEAK = YES; 209 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 210 | CLANG_WARN_BOOL_CONVERSION = YES; 211 | CLANG_WARN_COMMA = YES; 212 | CLANG_WARN_CONSTANT_CONVERSION = YES; 213 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 214 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 215 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 216 | CLANG_WARN_EMPTY_BODY = YES; 217 | CLANG_WARN_ENUM_CONVERSION = YES; 218 | CLANG_WARN_INFINITE_RECURSION = YES; 219 | CLANG_WARN_INT_CONVERSION = YES; 220 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 221 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 222 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 223 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 224 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 225 | CLANG_WARN_STRICT_PROTOTYPES = YES; 226 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 227 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 228 | CLANG_WARN_UNREACHABLE_CODE = YES; 229 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 230 | CODE_SIGN_IDENTITY = "iPhone Developer"; 231 | COPY_PHASE_STRIP = NO; 232 | CURRENT_PROJECT_VERSION = 1; 233 | DEBUG_INFORMATION_FORMAT = dwarf; 234 | ENABLE_STRICT_OBJC_MSGSEND = YES; 235 | ENABLE_TESTABILITY = YES; 236 | GCC_C_LANGUAGE_STANDARD = gnu11; 237 | GCC_DYNAMIC_NO_PIC = NO; 238 | GCC_NO_COMMON_BLOCKS = YES; 239 | GCC_OPTIMIZATION_LEVEL = 0; 240 | GCC_PREPROCESSOR_DEFINITIONS = ( 241 | "DEBUG=1", 242 | "$(inherited)", 243 | ); 244 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 245 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 246 | GCC_WARN_UNDECLARED_SELECTOR = YES; 247 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 248 | GCC_WARN_UNUSED_FUNCTION = YES; 249 | GCC_WARN_UNUSED_VARIABLE = YES; 250 | IPHONEOS_DEPLOYMENT_TARGET = 12.2; 251 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 252 | MTL_FAST_MATH = YES; 253 | ONLY_ACTIVE_ARCH = YES; 254 | SDKROOT = iphoneos; 255 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 256 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 257 | VERSIONING_SYSTEM = "apple-generic"; 258 | VERSION_INFO_PREFIX = ""; 259 | }; 260 | name = Debug; 261 | }; 262 | C9D0289722607AD100B5E061 /* Release */ = { 263 | isa = XCBuildConfiguration; 264 | buildSettings = { 265 | ALWAYS_SEARCH_USER_PATHS = NO; 266 | CLANG_ANALYZER_NONNULL = YES; 267 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 268 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 269 | CLANG_CXX_LIBRARY = "libc++"; 270 | CLANG_ENABLE_MODULES = YES; 271 | CLANG_ENABLE_OBJC_ARC = YES; 272 | CLANG_ENABLE_OBJC_WEAK = YES; 273 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 274 | CLANG_WARN_BOOL_CONVERSION = YES; 275 | CLANG_WARN_COMMA = YES; 276 | CLANG_WARN_CONSTANT_CONVERSION = YES; 277 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 278 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 279 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 280 | CLANG_WARN_EMPTY_BODY = YES; 281 | CLANG_WARN_ENUM_CONVERSION = YES; 282 | CLANG_WARN_INFINITE_RECURSION = YES; 283 | CLANG_WARN_INT_CONVERSION = YES; 284 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 285 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 286 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 287 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 288 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 289 | CLANG_WARN_STRICT_PROTOTYPES = YES; 290 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 291 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 292 | CLANG_WARN_UNREACHABLE_CODE = YES; 293 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 294 | CODE_SIGN_IDENTITY = "iPhone Developer"; 295 | COPY_PHASE_STRIP = NO; 296 | CURRENT_PROJECT_VERSION = 1; 297 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 298 | ENABLE_NS_ASSERTIONS = NO; 299 | ENABLE_STRICT_OBJC_MSGSEND = YES; 300 | GCC_C_LANGUAGE_STANDARD = gnu11; 301 | GCC_NO_COMMON_BLOCKS = YES; 302 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 303 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 304 | GCC_WARN_UNDECLARED_SELECTOR = YES; 305 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 306 | GCC_WARN_UNUSED_FUNCTION = YES; 307 | GCC_WARN_UNUSED_VARIABLE = YES; 308 | IPHONEOS_DEPLOYMENT_TARGET = 12.2; 309 | MTL_ENABLE_DEBUG_INFO = NO; 310 | MTL_FAST_MATH = YES; 311 | SDKROOT = iphoneos; 312 | SWIFT_COMPILATION_MODE = wholemodule; 313 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 314 | VALIDATE_PRODUCT = YES; 315 | VERSIONING_SYSTEM = "apple-generic"; 316 | VERSION_INFO_PREFIX = ""; 317 | }; 318 | name = Release; 319 | }; 320 | C9D0289922607AD100B5E061 /* Debug */ = { 321 | isa = XCBuildConfiguration; 322 | buildSettings = { 323 | CLANG_ENABLE_MODULES = YES; 324 | CODE_SIGN_IDENTITY = ""; 325 | CODE_SIGN_STYLE = Automatic; 326 | DEFINES_MODULE = YES; 327 | DEPLOYMENT_POSTPROCESSING = NO; 328 | DEVELOPMENT_TEAM = J3LFY9VS6Z; 329 | DYLIB_COMPATIBILITY_VERSION = 1; 330 | DYLIB_CURRENT_VERSION = 1; 331 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 332 | GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; 333 | INFOPLIST_FILE = Sources/Info.plist; 334 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 335 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 336 | LD_RUNPATH_SEARCH_PATHS = ( 337 | "$(inherited)", 338 | "@executable_path/Frameworks", 339 | "@loader_path/Frameworks", 340 | ); 341 | MARKETING_VERSION = 2.1.0; 342 | OTHER_CFLAGS = "-fembed-bitcode"; 343 | OTHER_CPLUSPLUSFLAGS = "$(OTHER_CFLAGS)"; 344 | PRODUCT_BUNDLE_IDENTIFIER = com.lee.videoplayer; 345 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 346 | SKIP_INSTALL = YES; 347 | SUPPORTS_MACCATALYST = NO; 348 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 349 | SWIFT_VERSION = 5.0; 350 | TARGETED_DEVICE_FAMILY = "1,2"; 351 | }; 352 | name = Debug; 353 | }; 354 | C9D0289A22607AD100B5E061 /* Release */ = { 355 | isa = XCBuildConfiguration; 356 | buildSettings = { 357 | CLANG_ENABLE_MODULES = YES; 358 | CODE_SIGN_IDENTITY = ""; 359 | CODE_SIGN_STYLE = Automatic; 360 | DEFINES_MODULE = YES; 361 | DEPLOYMENT_POSTPROCESSING = NO; 362 | DEVELOPMENT_TEAM = J3LFY9VS6Z; 363 | DYLIB_COMPATIBILITY_VERSION = 1; 364 | DYLIB_CURRENT_VERSION = 1; 365 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 366 | GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; 367 | INFOPLIST_FILE = Sources/Info.plist; 368 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 369 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 370 | LD_RUNPATH_SEARCH_PATHS = ( 371 | "$(inherited)", 372 | "@executable_path/Frameworks", 373 | "@loader_path/Frameworks", 374 | ); 375 | MARKETING_VERSION = 2.1.0; 376 | OTHER_CFLAGS = "-fembed-bitcode"; 377 | OTHER_CPLUSPLUSFLAGS = "$(OTHER_CFLAGS)"; 378 | PRODUCT_BUNDLE_IDENTIFIER = com.lee.videoplayer; 379 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 380 | SKIP_INSTALL = YES; 381 | SUPPORTS_MACCATALYST = NO; 382 | SWIFT_VERSION = 5.0; 383 | TARGETED_DEVICE_FAMILY = "1,2"; 384 | }; 385 | name = Release; 386 | }; 387 | /* End XCBuildConfiguration section */ 388 | 389 | /* Begin XCConfigurationList section */ 390 | C9D0288A22607AD100B5E061 /* Build configuration list for PBXProject "VideoPlayer" */ = { 391 | isa = XCConfigurationList; 392 | buildConfigurations = ( 393 | C9D0289622607AD100B5E061 /* Debug */, 394 | C9D0289722607AD100B5E061 /* Release */, 395 | ); 396 | defaultConfigurationIsVisible = 0; 397 | defaultConfigurationName = Release; 398 | }; 399 | C9D0289822607AD100B5E061 /* Build configuration list for PBXNativeTarget "VideoPlayer" */ = { 400 | isa = XCConfigurationList; 401 | buildConfigurations = ( 402 | C9D0289922607AD100B5E061 /* Debug */, 403 | C9D0289A22607AD100B5E061 /* Release */, 404 | ); 405 | defaultConfigurationIsVisible = 0; 406 | defaultConfigurationName = Release; 407 | }; 408 | /* End XCConfigurationList section */ 409 | }; 410 | rootObject = C9D0288722607AD100B5E061 /* Project object */; 411 | } 412 | -------------------------------------------------------------------------------- /VideoPlayer.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /VideoPlayer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /VideoPlayer.xcodeproj/xcshareddata/xcschemes/VideoPlayer.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | --------------------------------------------------------------------------------