├── .gitignore ├── Demo ├── KJPlayer-Demo.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ ├── KJPlayer-Demo.xcscheme │ │ └── KJPlayer.xcscheme ├── KJPlayer │ ├── AppDelegate.swift │ ├── DetailViewController.swift │ ├── HomeViewController.swift │ └── Resources │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ └── Info.plist └── KJPlayerTests │ ├── DatabaseTests.swift │ ├── KJPlayerTests.swift │ └── RecordTimeTests.swift ├── KJPlayer.podspec ├── KJPlayer.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── KJPlayer.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE ├── README.md ├── Screenshot ├── AAA.png └── XXX.png └── Sources ├── Core ├── Base │ ├── KJBasePlayer.swift │ └── KJPlayer.swift ├── Database │ ├── APlayer.xcdatamodeld │ │ └── APlayer.xcdatamodel │ │ │ └── contents │ ├── DatabaseConfiguration.swift │ ├── DatabaseManager.swift │ └── PlayerVideoData.swift ├── Downloader │ ├── KJDownloader.h │ ├── KJDownloader.m │ ├── KJDownloaderCommon.h │ ├── KJDownloaderCommon.m │ ├── KJFileHandleInfo.h │ ├── KJFileHandleInfo.m │ ├── KJFileHandleManager.h │ ├── KJFileHandleManager.m │ └── KJPlayer-Bridging-Header.h ├── Extensions │ └── Timer+Extension.swift ├── Setup │ ├── Bridge.swift │ ├── Common.swift │ ├── Enum.swift │ ├── Function.swift │ ├── Notification.swift │ ├── Protocol.swift │ ├── Provider.swift │ ├── Screenshots.swift │ ├── Shared.swift │ └── Timer.swift └── View │ └── KJPlayerView.swift ├── Kernel └── AVPlayer │ └── KJAVPlayer.swift └── Protocols ├── Cache ├── Cache.swift └── CacheManager.swift ├── FreeTime └── FreeTime.swift ├── Pip └── PictureInPicture.swift ├── RecordTime ├── RecordTime.swift └── RecordTimeData.swift └── SkipTime └── SkipTime.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /Demo/KJPlayer-Demo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2005AC9B2769E3BC007573E9 /* RecordTimeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2005AC9A2769E3BC007573E9 /* RecordTimeTests.swift */; }; 11 | 203D5D152768434800883D00 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 203D5D142768434800883D00 /* AppDelegate.swift */; }; 12 | 203D5D192768434800883D00 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 203D5D182768434800883D00 /* HomeViewController.swift */; }; 13 | 203D5D1C2768434800883D00 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 203D5D1A2768434800883D00 /* Main.storyboard */; }; 14 | 203D5D1E2768434900883D00 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 203D5D1D2768434900883D00 /* Assets.xcassets */; }; 15 | 203D5D212768434900883D00 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 203D5D1F2768434900883D00 /* LaunchScreen.storyboard */; }; 16 | 20D38012276890460004E13C /* KJPlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D38011276890460004E13C /* KJPlayerTests.swift */; }; 17 | 20D380192768906A0004E13C /* DatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D380182768906A0004E13C /* DatabaseTests.swift */; }; 18 | 20F4EBB0276C324B00B0FA76 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20F4EBAF276C324B00B0FA76 /* DetailViewController.swift */; }; 19 | 875326A429CAAB850030F655 /* KJPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 875326A329CAAB850030F655 /* KJPlayer.framework */; }; 20 | 875326A529CAAB850030F655 /* KJPlayer.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 875326A329CAAB850030F655 /* KJPlayer.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXContainerItemProxy section */ 24 | 20D38013276890470004E13C /* PBXContainerItemProxy */ = { 25 | isa = PBXContainerItemProxy; 26 | containerPortal = 203D5D092768434800883D00 /* Project object */; 27 | proxyType = 1; 28 | remoteGlobalIDString = 203D5D102768434800883D00; 29 | remoteInfo = KJPlayer; 30 | }; 31 | /* End PBXContainerItemProxy section */ 32 | 33 | /* Begin PBXCopyFilesBuildPhase section */ 34 | 875326A629CAAB860030F655 /* Embed Frameworks */ = { 35 | isa = PBXCopyFilesBuildPhase; 36 | buildActionMask = 2147483647; 37 | dstPath = ""; 38 | dstSubfolderSpec = 10; 39 | files = ( 40 | 875326A529CAAB850030F655 /* KJPlayer.framework in Embed Frameworks */, 41 | ); 42 | name = "Embed Frameworks"; 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | /* End PBXCopyFilesBuildPhase section */ 46 | 47 | /* Begin PBXFileReference section */ 48 | 2005AC9A2769E3BC007573E9 /* RecordTimeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordTimeTests.swift; sourceTree = ""; }; 49 | 203D5D112768434800883D00 /* KJPlayer-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "KJPlayer-Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | 203D5D142768434800883D00 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 51 | 203D5D182768434800883D00 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; 52 | 203D5D1B2768434800883D00 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 53 | 203D5D1D2768434900883D00 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 54 | 203D5D202768434900883D00 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 55 | 203D5D222768434900883D00 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 56 | 20D3800F276890460004E13C /* KJPlayer-DemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "KJPlayer-DemoTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 57 | 20D38011276890460004E13C /* KJPlayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KJPlayerTests.swift; sourceTree = ""; }; 58 | 20D380182768906A0004E13C /* DatabaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseTests.swift; sourceTree = ""; }; 59 | 20F4EBAF276C324B00B0FA76 /* DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = ""; }; 60 | 875326A329CAAB850030F655 /* KJPlayer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KJPlayer.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 61 | /* End PBXFileReference section */ 62 | 63 | /* Begin PBXFrameworksBuildPhase section */ 64 | 203D5D0E2768434800883D00 /* Frameworks */ = { 65 | isa = PBXFrameworksBuildPhase; 66 | buildActionMask = 2147483647; 67 | files = ( 68 | 875326A429CAAB850030F655 /* KJPlayer.framework in Frameworks */, 69 | ); 70 | runOnlyForDeploymentPostprocessing = 0; 71 | }; 72 | 20D3800C276890460004E13C /* Frameworks */ = { 73 | isa = PBXFrameworksBuildPhase; 74 | buildActionMask = 2147483647; 75 | files = ( 76 | ); 77 | runOnlyForDeploymentPostprocessing = 0; 78 | }; 79 | /* End PBXFrameworksBuildPhase section */ 80 | 81 | /* Begin PBXGroup section */ 82 | 203D5D082768434800883D00 = { 83 | isa = PBXGroup; 84 | children = ( 85 | 203D5D132768434800883D00 /* KJPlayer */, 86 | 20D38010276890460004E13C /* KJPlayerTests */, 87 | 203D5D122768434800883D00 /* Products */, 88 | 875326A229CAAB850030F655 /* Frameworks */, 89 | ); 90 | sourceTree = ""; 91 | }; 92 | 203D5D122768434800883D00 /* Products */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | 203D5D112768434800883D00 /* KJPlayer-Demo.app */, 96 | 20D3800F276890460004E13C /* KJPlayer-DemoTests.xctest */, 97 | ); 98 | name = Products; 99 | sourceTree = ""; 100 | }; 101 | 203D5D132768434800883D00 /* KJPlayer */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | 203D5D142768434800883D00 /* AppDelegate.swift */, 105 | 20F4EBAF276C324B00B0FA76 /* DetailViewController.swift */, 106 | 203D5D182768434800883D00 /* HomeViewController.swift */, 107 | 20689A02276845F3000FE077 /* Resources */, 108 | ); 109 | path = KJPlayer; 110 | sourceTree = ""; 111 | }; 112 | 20689A02276845F3000FE077 /* Resources */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | 203D5D1D2768434900883D00 /* Assets.xcassets */, 116 | 203D5D222768434900883D00 /* Info.plist */, 117 | 203D5D1F2768434900883D00 /* LaunchScreen.storyboard */, 118 | 203D5D1A2768434800883D00 /* Main.storyboard */, 119 | ); 120 | path = Resources; 121 | sourceTree = ""; 122 | }; 123 | 20D38010276890460004E13C /* KJPlayerTests */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 20D380182768906A0004E13C /* DatabaseTests.swift */, 127 | 20D38011276890460004E13C /* KJPlayerTests.swift */, 128 | 2005AC9A2769E3BC007573E9 /* RecordTimeTests.swift */, 129 | ); 130 | path = KJPlayerTests; 131 | sourceTree = ""; 132 | }; 133 | 875326A229CAAB850030F655 /* Frameworks */ = { 134 | isa = PBXGroup; 135 | children = ( 136 | 875326A329CAAB850030F655 /* KJPlayer.framework */, 137 | ); 138 | name = Frameworks; 139 | sourceTree = ""; 140 | }; 141 | /* End PBXGroup section */ 142 | 143 | /* Begin PBXNativeTarget section */ 144 | 203D5D102768434800883D00 /* KJPlayer-Demo */ = { 145 | isa = PBXNativeTarget; 146 | buildConfigurationList = 203D5D252768434900883D00 /* Build configuration list for PBXNativeTarget "KJPlayer-Demo" */; 147 | buildPhases = ( 148 | 203D5D0D2768434800883D00 /* Sources */, 149 | 203D5D0E2768434800883D00 /* Frameworks */, 150 | 203D5D0F2768434800883D00 /* Resources */, 151 | 875326A629CAAB860030F655 /* Embed Frameworks */, 152 | ); 153 | buildRules = ( 154 | ); 155 | dependencies = ( 156 | ); 157 | name = "KJPlayer-Demo"; 158 | productName = KJPlayer; 159 | productReference = 203D5D112768434800883D00 /* KJPlayer-Demo.app */; 160 | productType = "com.apple.product-type.application"; 161 | }; 162 | 20D3800E276890460004E13C /* KJPlayer-DemoTests */ = { 163 | isa = PBXNativeTarget; 164 | buildConfigurationList = 20D38015276890470004E13C /* Build configuration list for PBXNativeTarget "KJPlayer-DemoTests" */; 165 | buildPhases = ( 166 | 20D3800B276890460004E13C /* Sources */, 167 | 20D3800C276890460004E13C /* Frameworks */, 168 | 20D3800D276890460004E13C /* Resources */, 169 | ); 170 | buildRules = ( 171 | ); 172 | dependencies = ( 173 | 20D38014276890470004E13C /* PBXTargetDependency */, 174 | ); 175 | name = "KJPlayer-DemoTests"; 176 | productName = KJPlayerTests; 177 | productReference = 20D3800F276890460004E13C /* KJPlayer-DemoTests.xctest */; 178 | productType = "com.apple.product-type.bundle.unit-test"; 179 | }; 180 | /* End PBXNativeTarget section */ 181 | 182 | /* Begin PBXProject section */ 183 | 203D5D092768434800883D00 /* Project object */ = { 184 | isa = PBXProject; 185 | attributes = { 186 | BuildIndependentTargetsInParallel = 1; 187 | LastSwiftUpdateCheck = 1310; 188 | LastUpgradeCheck = 1310; 189 | TargetAttributes = { 190 | 203D5D102768434800883D00 = { 191 | CreatedOnToolsVersion = 13.1; 192 | LastSwiftMigration = 1310; 193 | }; 194 | 20D3800E276890460004E13C = { 195 | CreatedOnToolsVersion = 13.1; 196 | LastSwiftMigration = 1310; 197 | TestTargetID = 203D5D102768434800883D00; 198 | }; 199 | }; 200 | }; 201 | buildConfigurationList = 203D5D0C2768434800883D00 /* Build configuration list for PBXProject "KJPlayer-Demo" */; 202 | compatibilityVersion = "Xcode 13.0"; 203 | developmentRegion = en; 204 | hasScannedForEncodings = 0; 205 | knownRegions = ( 206 | en, 207 | Base, 208 | ); 209 | mainGroup = 203D5D082768434800883D00; 210 | productRefGroup = 203D5D122768434800883D00 /* Products */; 211 | projectDirPath = ""; 212 | projectRoot = ""; 213 | targets = ( 214 | 203D5D102768434800883D00 /* KJPlayer-Demo */, 215 | 20D3800E276890460004E13C /* KJPlayer-DemoTests */, 216 | ); 217 | }; 218 | /* End PBXProject section */ 219 | 220 | /* Begin PBXResourcesBuildPhase section */ 221 | 203D5D0F2768434800883D00 /* Resources */ = { 222 | isa = PBXResourcesBuildPhase; 223 | buildActionMask = 2147483647; 224 | files = ( 225 | 203D5D212768434900883D00 /* LaunchScreen.storyboard in Resources */, 226 | 203D5D1E2768434900883D00 /* Assets.xcassets in Resources */, 227 | 203D5D1C2768434800883D00 /* Main.storyboard in Resources */, 228 | ); 229 | runOnlyForDeploymentPostprocessing = 0; 230 | }; 231 | 20D3800D276890460004E13C /* Resources */ = { 232 | isa = PBXResourcesBuildPhase; 233 | buildActionMask = 2147483647; 234 | files = ( 235 | ); 236 | runOnlyForDeploymentPostprocessing = 0; 237 | }; 238 | /* End PBXResourcesBuildPhase section */ 239 | 240 | /* Begin PBXSourcesBuildPhase section */ 241 | 203D5D0D2768434800883D00 /* Sources */ = { 242 | isa = PBXSourcesBuildPhase; 243 | buildActionMask = 2147483647; 244 | files = ( 245 | 20F4EBB0276C324B00B0FA76 /* DetailViewController.swift in Sources */, 246 | 203D5D192768434800883D00 /* HomeViewController.swift in Sources */, 247 | 203D5D152768434800883D00 /* AppDelegate.swift in Sources */, 248 | ); 249 | runOnlyForDeploymentPostprocessing = 0; 250 | }; 251 | 20D3800B276890460004E13C /* Sources */ = { 252 | isa = PBXSourcesBuildPhase; 253 | buildActionMask = 2147483647; 254 | files = ( 255 | 20D380192768906A0004E13C /* DatabaseTests.swift in Sources */, 256 | 2005AC9B2769E3BC007573E9 /* RecordTimeTests.swift in Sources */, 257 | 20D38012276890460004E13C /* KJPlayerTests.swift in Sources */, 258 | ); 259 | runOnlyForDeploymentPostprocessing = 0; 260 | }; 261 | /* End PBXSourcesBuildPhase section */ 262 | 263 | /* Begin PBXTargetDependency section */ 264 | 20D38014276890470004E13C /* PBXTargetDependency */ = { 265 | isa = PBXTargetDependency; 266 | target = 203D5D102768434800883D00 /* KJPlayer-Demo */; 267 | targetProxy = 20D38013276890470004E13C /* PBXContainerItemProxy */; 268 | }; 269 | /* End PBXTargetDependency section */ 270 | 271 | /* Begin PBXVariantGroup section */ 272 | 203D5D1A2768434800883D00 /* Main.storyboard */ = { 273 | isa = PBXVariantGroup; 274 | children = ( 275 | 203D5D1B2768434800883D00 /* Base */, 276 | ); 277 | name = Main.storyboard; 278 | sourceTree = ""; 279 | }; 280 | 203D5D1F2768434900883D00 /* LaunchScreen.storyboard */ = { 281 | isa = PBXVariantGroup; 282 | children = ( 283 | 203D5D202768434900883D00 /* Base */, 284 | ); 285 | name = LaunchScreen.storyboard; 286 | sourceTree = ""; 287 | }; 288 | /* End PBXVariantGroup section */ 289 | 290 | /* Begin XCBuildConfiguration section */ 291 | 203D5D232768434900883D00 /* Debug */ = { 292 | isa = XCBuildConfiguration; 293 | buildSettings = { 294 | ALWAYS_SEARCH_USER_PATHS = NO; 295 | CLANG_ANALYZER_NONNULL = YES; 296 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 297 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 298 | CLANG_CXX_LIBRARY = "libc++"; 299 | CLANG_ENABLE_MODULES = YES; 300 | CLANG_ENABLE_OBJC_ARC = YES; 301 | CLANG_ENABLE_OBJC_WEAK = YES; 302 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 303 | CLANG_WARN_BOOL_CONVERSION = YES; 304 | CLANG_WARN_COMMA = YES; 305 | CLANG_WARN_CONSTANT_CONVERSION = YES; 306 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 307 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 308 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 309 | CLANG_WARN_EMPTY_BODY = YES; 310 | CLANG_WARN_ENUM_CONVERSION = YES; 311 | CLANG_WARN_INFINITE_RECURSION = YES; 312 | CLANG_WARN_INT_CONVERSION = YES; 313 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 314 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 315 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 316 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 317 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 318 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 319 | CLANG_WARN_STRICT_PROTOTYPES = YES; 320 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 321 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 322 | CLANG_WARN_UNREACHABLE_CODE = YES; 323 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 324 | COPY_PHASE_STRIP = NO; 325 | DEBUG_INFORMATION_FORMAT = dwarf; 326 | ENABLE_STRICT_OBJC_MSGSEND = YES; 327 | ENABLE_TESTABILITY = YES; 328 | GCC_C_LANGUAGE_STANDARD = gnu11; 329 | GCC_DYNAMIC_NO_PIC = NO; 330 | GCC_NO_COMMON_BLOCKS = YES; 331 | GCC_OPTIMIZATION_LEVEL = 0; 332 | GCC_PREPROCESSOR_DEFINITIONS = ( 333 | "DEBUG=1", 334 | "$(inherited)", 335 | ); 336 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 337 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 338 | GCC_WARN_UNDECLARED_SELECTOR = YES; 339 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 340 | GCC_WARN_UNUSED_FUNCTION = YES; 341 | GCC_WARN_UNUSED_VARIABLE = YES; 342 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 343 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 344 | MTL_FAST_MATH = YES; 345 | ONLY_ACTIVE_ARCH = YES; 346 | SDKROOT = iphoneos; 347 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 348 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 349 | }; 350 | name = Debug; 351 | }; 352 | 203D5D242768434900883D00 /* Release */ = { 353 | isa = XCBuildConfiguration; 354 | buildSettings = { 355 | ALWAYS_SEARCH_USER_PATHS = NO; 356 | CLANG_ANALYZER_NONNULL = YES; 357 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 358 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 359 | CLANG_CXX_LIBRARY = "libc++"; 360 | CLANG_ENABLE_MODULES = YES; 361 | CLANG_ENABLE_OBJC_ARC = YES; 362 | CLANG_ENABLE_OBJC_WEAK = YES; 363 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 364 | CLANG_WARN_BOOL_CONVERSION = YES; 365 | CLANG_WARN_COMMA = YES; 366 | CLANG_WARN_CONSTANT_CONVERSION = YES; 367 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 368 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 369 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 370 | CLANG_WARN_EMPTY_BODY = YES; 371 | CLANG_WARN_ENUM_CONVERSION = YES; 372 | CLANG_WARN_INFINITE_RECURSION = YES; 373 | CLANG_WARN_INT_CONVERSION = YES; 374 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 375 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 376 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 377 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 378 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 379 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 380 | CLANG_WARN_STRICT_PROTOTYPES = YES; 381 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 382 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 383 | CLANG_WARN_UNREACHABLE_CODE = YES; 384 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 385 | COPY_PHASE_STRIP = NO; 386 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 387 | ENABLE_NS_ASSERTIONS = NO; 388 | ENABLE_STRICT_OBJC_MSGSEND = YES; 389 | GCC_C_LANGUAGE_STANDARD = gnu11; 390 | GCC_NO_COMMON_BLOCKS = YES; 391 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 392 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 393 | GCC_WARN_UNDECLARED_SELECTOR = YES; 394 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 395 | GCC_WARN_UNUSED_FUNCTION = YES; 396 | GCC_WARN_UNUSED_VARIABLE = YES; 397 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 398 | MTL_ENABLE_DEBUG_INFO = NO; 399 | MTL_FAST_MATH = YES; 400 | SDKROOT = iphoneos; 401 | SWIFT_COMPILATION_MODE = wholemodule; 402 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 403 | VALIDATE_PRODUCT = YES; 404 | }; 405 | name = Release; 406 | }; 407 | 203D5D262768434900883D00 /* Debug */ = { 408 | isa = XCBuildConfiguration; 409 | buildSettings = { 410 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 411 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 412 | CLANG_ENABLE_MODULES = YES; 413 | CODE_SIGN_STYLE = Automatic; 414 | CURRENT_PROJECT_VERSION = 1; 415 | DEVELOPMENT_TEAM = 66T6NB6T4J; 416 | GENERATE_INFOPLIST_FILE = YES; 417 | INFOPLIST_FILE = KJPlayer/Resources/Info.plist; 418 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 419 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 420 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 421 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 422 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 423 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 424 | LD_RUNPATH_SEARCH_PATHS = ( 425 | "$(inherited)", 426 | "@executable_path/Frameworks", 427 | ); 428 | MARKETING_VERSION = 1.0; 429 | PRODUCT_BUNDLE_IDENTIFIER = kj.KJPlayer; 430 | PRODUCT_NAME = "$(TARGET_NAME)"; 431 | SWIFT_EMIT_LOC_STRINGS = YES; 432 | SWIFT_INSTALL_OBJC_HEADER = NO; 433 | SWIFT_OBJC_BRIDGING_HEADER = "../Sources/Core/Downloader/KJPlayer-Bridging-Header.h"; 434 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 435 | SWIFT_VERSION = 5.0; 436 | TARGETED_DEVICE_FAMILY = "1,2"; 437 | }; 438 | name = Debug; 439 | }; 440 | 203D5D272768434900883D00 /* Release */ = { 441 | isa = XCBuildConfiguration; 442 | buildSettings = { 443 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 444 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 445 | CLANG_ENABLE_MODULES = YES; 446 | CODE_SIGN_STYLE = Automatic; 447 | CURRENT_PROJECT_VERSION = 1; 448 | DEVELOPMENT_TEAM = 66T6NB6T4J; 449 | GENERATE_INFOPLIST_FILE = YES; 450 | INFOPLIST_FILE = KJPlayer/Resources/Info.plist; 451 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 452 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 453 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 454 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 455 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 456 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 457 | LD_RUNPATH_SEARCH_PATHS = ( 458 | "$(inherited)", 459 | "@executable_path/Frameworks", 460 | ); 461 | MARKETING_VERSION = 1.0; 462 | PRODUCT_BUNDLE_IDENTIFIER = kj.KJPlayer; 463 | PRODUCT_NAME = "$(TARGET_NAME)"; 464 | SWIFT_EMIT_LOC_STRINGS = YES; 465 | SWIFT_INSTALL_OBJC_HEADER = NO; 466 | SWIFT_OBJC_BRIDGING_HEADER = "../Sources/Core/Downloader/KJPlayer-Bridging-Header.h"; 467 | SWIFT_VERSION = 5.0; 468 | TARGETED_DEVICE_FAMILY = "1,2"; 469 | }; 470 | name = Release; 471 | }; 472 | 20D38016276890470004E13C /* Debug */ = { 473 | isa = XCBuildConfiguration; 474 | buildSettings = { 475 | BUNDLE_LOADER = "$(TEST_HOST)"; 476 | CLANG_ENABLE_MODULES = YES; 477 | CODE_SIGN_STYLE = Automatic; 478 | CURRENT_PROJECT_VERSION = 1; 479 | GENERATE_INFOPLIST_FILE = YES; 480 | LD_RUNPATH_SEARCH_PATHS = ( 481 | "$(inherited)", 482 | "@executable_path/Frameworks", 483 | "@loader_path/Frameworks", 484 | ); 485 | MARKETING_VERSION = 1.0; 486 | PRODUCT_BUNDLE_IDENTIFIER = kj.KJPlayerTests; 487 | PRODUCT_NAME = "$(TARGET_NAME)"; 488 | SWIFT_EMIT_LOC_STRINGS = NO; 489 | SWIFT_OBJC_BRIDGING_HEADER = ""; 490 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 491 | SWIFT_VERSION = 5.0; 492 | TARGETED_DEVICE_FAMILY = "1,2"; 493 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/KJPlayer-Demo.app/KJPlayer-Demo"; 494 | }; 495 | name = Debug; 496 | }; 497 | 20D38017276890470004E13C /* Release */ = { 498 | isa = XCBuildConfiguration; 499 | buildSettings = { 500 | BUNDLE_LOADER = "$(TEST_HOST)"; 501 | CLANG_ENABLE_MODULES = YES; 502 | CODE_SIGN_STYLE = Automatic; 503 | CURRENT_PROJECT_VERSION = 1; 504 | GENERATE_INFOPLIST_FILE = YES; 505 | LD_RUNPATH_SEARCH_PATHS = ( 506 | "$(inherited)", 507 | "@executable_path/Frameworks", 508 | "@loader_path/Frameworks", 509 | ); 510 | MARKETING_VERSION = 1.0; 511 | PRODUCT_BUNDLE_IDENTIFIER = kj.KJPlayerTests; 512 | PRODUCT_NAME = "$(TARGET_NAME)"; 513 | SWIFT_EMIT_LOC_STRINGS = NO; 514 | SWIFT_OBJC_BRIDGING_HEADER = ""; 515 | SWIFT_VERSION = 5.0; 516 | TARGETED_DEVICE_FAMILY = "1,2"; 517 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/KJPlayer-Demo.app/KJPlayer-Demo"; 518 | }; 519 | name = Release; 520 | }; 521 | /* End XCBuildConfiguration section */ 522 | 523 | /* Begin XCConfigurationList section */ 524 | 203D5D0C2768434800883D00 /* Build configuration list for PBXProject "KJPlayer-Demo" */ = { 525 | isa = XCConfigurationList; 526 | buildConfigurations = ( 527 | 203D5D232768434900883D00 /* Debug */, 528 | 203D5D242768434900883D00 /* Release */, 529 | ); 530 | defaultConfigurationIsVisible = 0; 531 | defaultConfigurationName = Release; 532 | }; 533 | 203D5D252768434900883D00 /* Build configuration list for PBXNativeTarget "KJPlayer-Demo" */ = { 534 | isa = XCConfigurationList; 535 | buildConfigurations = ( 536 | 203D5D262768434900883D00 /* Debug */, 537 | 203D5D272768434900883D00 /* Release */, 538 | ); 539 | defaultConfigurationIsVisible = 0; 540 | defaultConfigurationName = Release; 541 | }; 542 | 20D38015276890470004E13C /* Build configuration list for PBXNativeTarget "KJPlayer-DemoTests" */ = { 543 | isa = XCConfigurationList; 544 | buildConfigurations = ( 545 | 20D38016276890470004E13C /* Debug */, 546 | 20D38017276890470004E13C /* Release */, 547 | ); 548 | defaultConfigurationIsVisible = 0; 549 | defaultConfigurationName = Release; 550 | }; 551 | /* End XCConfigurationList section */ 552 | }; 553 | rootObject = 203D5D092768434800883D00 /* Project object */; 554 | } 555 | -------------------------------------------------------------------------------- /Demo/KJPlayer-Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/KJPlayer-Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/KJPlayer-Demo.xcodeproj/xcshareddata/xcschemes/KJPlayer-Demo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Demo/KJPlayer-Demo.xcodeproj/xcshareddata/xcschemes/KJPlayer.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Demo/KJPlayer/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/14. 6 | // 7 | 8 | import UIKit 9 | 10 | @UIApplicationMain 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions 16 | launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(_ application: UIApplication) { 22 | // 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. 23 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(_ application: UIApplication) { 27 | // 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. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | func applicationWillEnterForeground(_ application: UIApplication) { 32 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 33 | } 34 | 35 | func applicationDidBecomeActive(_ application: UIApplication) { 36 | // 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. 37 | } 38 | 39 | func applicationWillTerminate(_ application: UIApplication) { 40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /Demo/KJPlayer/DetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailViewController.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/17. 6 | // 7 | 8 | import UIKit 9 | import KJPlayer 10 | 11 | class DetailViewController: UIViewController { 12 | 13 | lazy var playButton: UIButton = { 14 | let button = UIButton.init(type: .custom) 15 | button.setTitle("play", for: .normal) 16 | button.setTitle("paue", for: .selected) 17 | button.setTitleColor(UIColor.black, for: .normal) 18 | button.setTitleColor(UIColor.black, for: .selected) 19 | button.titleLabel?.font = UIFont.systemFont(ofSize: 14) 20 | button.backgroundColor = UIColor.blue.withAlphaComponent(0.5) 21 | button.addTarget(self, action: #selector(buttonAction(_:)), for: .touchUpInside) 22 | view.addSubview(button) 23 | button.isSelected = true 24 | return button 25 | }() 26 | 27 | lazy var pipButton: UIButton = { 28 | let button = UIButton.init(type: .custom) 29 | button.setTitle("pip", for: .normal) 30 | button.setTitle("close", for: .selected) 31 | button.setTitleColor(UIColor.black, for: .normal) 32 | button.setTitleColor(UIColor.black, for: .selected) 33 | button.titleLabel?.font = UIFont.systemFont(ofSize: 14) 34 | button.backgroundColor = UIColor.blue.withAlphaComponent(0.5) 35 | button.addTarget(self, action: #selector(pipAction(_:)), for: .touchUpInside) 36 | view.addSubview(button) 37 | return button 38 | }() 39 | 40 | lazy var playerView: KJPlayerView = { 41 | let width = self.view.frame.width - 40 42 | let rect = CGRect(x: 20, y: 100, width: width, height: width / 2) 43 | let view = KJPlayerView.init(frame: rect) 44 | view.background = UIColor.red.cgColor 45 | view.backgroundColor = UIColor.green 46 | return view 47 | }() 48 | 49 | lazy var player: KJAVPlayer = { 50 | let provider = Provider.init(videoURL: self.title) 51 | let player = KJAVPlayer.init(withPlayerView: playerView) 52 | player.delegate = self 53 | player.recordDelegate = self 54 | player.provider = provider 55 | return player 56 | }() 57 | 58 | override func viewDidLoad() { 59 | super.viewDidLoad() 60 | // Do any additional setup after loading the view. 61 | self.setupUI() 62 | self.player.kj_play() 63 | } 64 | 65 | func setupUI() { 66 | self.view.backgroundColor = UIColor.white 67 | self.playButton.frame = CGRect(x: 0, y: 0, width: 100, height: 50) 68 | self.playButton.center = self.view.center 69 | self.pipButton.frame = CGRect(x: 0, y: 0, width: 100, height: 50) 70 | self.pipButton.center = CGPoint(x: self.playButton.center.x, y: self.playButton.center.y + 70) 71 | self.view.addSubview(self.playerView) 72 | } 73 | } 74 | 75 | // MARK: - actions 76 | extension DetailViewController { 77 | 78 | @objc func buttonAction(_ button: UIButton) { 79 | button.isSelected = !button.isSelected 80 | if button.isSelected { 81 | self.player.kj_play() 82 | } else { 83 | self.player.kj_pause() 84 | } 85 | } 86 | 87 | @objc func pipAction(_ button: UIButton) { 88 | button.isSelected = !button.isSelected 89 | if button.isSelected { 90 | self.player.openPip() 91 | } else { 92 | self.player.closePip() 93 | } 94 | } 95 | } 96 | 97 | extension DetailViewController: KJPlayerDelegate { 98 | 99 | func kj_player(_ player: KJBasePlayer, state: KJPlayerState) { 100 | print("----state:\(state.mapString)") 101 | } 102 | 103 | func kj_player(_ player: KJBasePlayer, current: TimeInterval) { 104 | //print("----current:\(current)") 105 | } 106 | 107 | func kj_player(_ player: KJBasePlayer, playFailed: NSError) { 108 | print("----playFailed:\(playFailed)") 109 | } 110 | 111 | func kj_player(_ player: KJBasePlayer, loadedTime: TimeInterval) { 112 | print("----loadedTime:\(loadedTime)") 113 | } 114 | 115 | func kj_player(_ player: KJBasePlayer, total: TimeInterval) { 116 | print("----total:\(total)") 117 | } 118 | 119 | func kj_player(_ player: KJBasePlayer, videoSize: CGSize) { 120 | print("----videoSize:\(videoSize)") 121 | } 122 | 123 | func kj_player(_ player: KJBasePlayer, playFinished: TimeInterval) { 124 | print("----playFinished:\(playFinished)") 125 | player.kj_replay() 126 | } 127 | } 128 | 129 | extension DetailViewController: KJPlayerRecordDelegate { 130 | 131 | func kj_recordTime(with player: KJBasePlayer) -> Bool { 132 | return true 133 | } 134 | 135 | func kj_recordTime(with player: KJBasePlayer, lastTime: TimeInterval) { 136 | print("----lastTime:\(lastTime)") 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Demo/KJPlayer/HomeViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeViewController.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/14. 6 | // 7 | 8 | import UIKit 9 | 10 | class HomeViewController: UIViewController { 11 | 12 | lazy var pushButton: UIButton = { 13 | let button = UIButton.init(type: .custom) 14 | button.setTitle("online", for: .normal) 15 | button.setTitleColor(UIColor.black, for: .normal) 16 | button.titleLabel?.font = UIFont.systemFont(ofSize: 14) 17 | button.backgroundColor = UIColor.blue.withAlphaComponent(0.5) 18 | button.addTarget(self, action: #selector(buttonAction(_:)), for: .touchUpInside) 19 | view.addSubview(button) 20 | return button 21 | }() 22 | 23 | lazy var localButton: UIButton = { 24 | let button = UIButton.init(type: .custom) 25 | button.setTitle("local", for: .normal) 26 | button.setTitleColor(UIColor.black, for: .normal) 27 | button.titleLabel?.font = UIFont.systemFont(ofSize: 14) 28 | button.backgroundColor = UIColor.blue.withAlphaComponent(0.5) 29 | button.addTarget(self, action: #selector(localAction(_:)), for: .touchUpInside) 30 | view.addSubview(button) 31 | return button 32 | }() 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | self.pushButton.frame = CGRect(x: 0, y: 0, width: 100, height: 50) 37 | self.pushButton.center = self.view.center 38 | self.localButton.frame = CGRect(x: 0, y: 0, width: 100, height: 50) 39 | self.localButton.center = CGPoint(x: self.pushButton.center.x, y: self.pushButton.center.y + 88) 40 | } 41 | } 42 | 43 | //MARK: - actions 44 | extension HomeViewController { 45 | 46 | @objc func buttonAction(_ button: UIButton) { 47 | let vc = DetailViewController.init() 48 | vc.title = "https://mp4.vjshi.com/2017-11-21/7c2b143eeb27d9f2bf98c4ab03360cfe.mp4" 49 | self.navigationController?.pushViewController(vc, animated: true) 50 | } 51 | 52 | @objc func localAction(_ button: UIButton) { 53 | let vc = DetailViewController.init() 54 | vc.title = "rock.mp4" 55 | self.navigationController?.pushViewController(vc, animated: true) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Demo/KJPlayer/Resources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo/KJPlayer/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Demo/KJPlayer/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/KJPlayer/Resources/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/KJPlayer/Resources/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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Demo/KJPlayer/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Demo/KJPlayerTests/DatabaseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Database.swift 3 | // KJPlayerTests 4 | // 5 | // Created by abas on 2021/12/14. 6 | // 7 | 8 | import XCTest 9 | import KJPlayer 10 | 11 | class Database: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | func testPerformanceExample() throws { 27 | // This is an example of a performance test case. 28 | self.measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | func testDatabase() throws { 34 | let dbid = "testdbid" + "\(arc4random() % 77)" 35 | 36 | let model = DatabaseManager.queryOne(with: dbid) 37 | XCTAssertNil(model) 38 | 39 | let data = PlayerVideoData.init(context: DatabaseManager.context) 40 | data.dbid = dbid 41 | data.videoUrl = "https://www.baidu.com" 42 | data.videoDownloaded = true 43 | data.videoTotalTime = Double(arc4random() % 200) 44 | 45 | let x = DatabaseManager.insert(with: data) 46 | XCTAssertTrue(x, "insert successed") 47 | 48 | let m2 = DatabaseManager.queryOne(with: dbid)! 49 | XCTAssertNotNil(m2, "\(m2)") 50 | 51 | data.videoUrl = "https://github.com" 52 | let b = DatabaseManager.update(with: data, playedTime: nil) 53 | XCTAssertTrue(b, "update successed") 54 | 55 | let m3 = DatabaseManager.queryOne(with: dbid)! 56 | XCTAssertNotNil(m3, "\(m3)") 57 | 58 | let d = DatabaseManager.delete(with: dbid) 59 | XCTAssertTrue(d, "delete successed") 60 | 61 | let m4 = DatabaseManager.queryOne(with: dbid) 62 | XCTAssertNil(m4) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Demo/KJPlayerTests/KJPlayerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KJPlayerTests.swift 3 | // KJPlayerTests 4 | // 5 | // Created by abas on 2021/12/14. 6 | // 7 | 8 | import XCTest 9 | 10 | class KJPlayerTests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | } 15 | 16 | override func tearDownWithError() throws { 17 | // Put teardown code here. This method is called after the invocation of each test method in the class. 18 | } 19 | 20 | func testExample() throws { 21 | // This is an example of a functional test case. 22 | // Use XCTAssert and related functions to verify your tests produce the correct results. 23 | } 24 | 25 | func testPerformanceExample() throws { 26 | // This is an example of a performance test case. 27 | measure { 28 | // Put the code you want to measure the time of here. 29 | } 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Demo/KJPlayerTests/RecordTimeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecordTimeTests.swift 3 | // KJPlayerTests 4 | // 5 | // Created by abas on 2021/12/15. 6 | // 7 | 8 | import XCTest 9 | 10 | class RecordTimeTests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 19 | XCUIApplication().launch() 20 | 21 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 22 | } 23 | 24 | override func tearDownWithError() throws { 25 | // Put teardown code here. This method is called after the invocation of each test method in the class. 26 | } 27 | 28 | func testExample() throws { 29 | // Use recording to get started writing UI tests. 30 | // Use XCTAssert and related functions to verify your tests produce the correct results. 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /KJPlayer.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run 'pod lib lint KJPlayer.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'KJPlayer' 11 | s.version = "2.2.0" 12 | s.summary = "KJPlayer play and cache, AVPlayer / MIDIPlayer / IJKPlayer" 13 | 14 | # This description is used to generate tags and improve search results. 15 | # * Think: What does it do? Why did you write it? What is the focus? 16 | # * Try to keep it short, snappy and to the point. 17 | # * Write the description between the DESC delimiters below. 18 | # * Finally, don't worry about the indent, CocoaPods strips it! 19 | 20 | s.homepage = "https://github.com/yangKJ/KJPlayerDemo" 21 | s.license = "Copyright (c) 2019 yangkejun" 22 | s.author = { "77" => "yangkj310@gmail.com" } 23 | s.license = { :type => "MIT", :file => "LICENSE" } 24 | s.source = { :git => "https://github.com/yangKJ/KJPlayerDemo.git", :tag => "#{s.version}" } 25 | s.platform = :ios 26 | s.requires_arc = true 27 | s.static_framework = true 28 | 29 | s.swift_version = '5.0' 30 | s.ios.deployment_target = '10.0' 31 | 32 | s.frameworks = 'Foundation', 'UIKit', 'AVFoundation', 'MediaPlayer' 33 | 34 | ## 视频基础模块 35 | s.subspec 'Core' do |xx| 36 | xx.source_files = "Sources/Core/**/*.{swift,h,m}" 37 | xx.prefix_header_contents = '#import "KJPlayer-Bridging-Header.h"' 38 | xx.resources = "Sources/Core/Database/*.{xcdatamodeld}" 39 | xx.resource_bundles = { 'KJPlayer' => ['Sources/Core/View/*.{ttf}'] } 40 | end 41 | 42 | ## AVPlayer内核模块 43 | s.subspec 'AVPlayer' do |xx| 44 | xx.source_files = "Sources/Kernel/AVPlayer/*.swift" 45 | xx.frameworks = 'MobileCoreServices' 46 | xx.dependency 'KJPlayer/Core' 47 | end 48 | 49 | ## ---------------------- 功能模块 ---------------------- 50 | 51 | ## 缓存至数据库模块 52 | s.subspec 'Cache' do |xx| 53 | xx.source_files = "Sources/Protocols/Cache/*.swift" 54 | xx.dependency 'KJPlayer/Core' 55 | end 56 | 57 | ## 自动记忆播放时间模块 58 | s.subspec 'RecordTime' do |xx| 59 | xx.source_files = "Sources/Protocols/RecordTime/*.swift" 60 | xx.dependency 'KJPlayer/Core' 61 | end 62 | 63 | ## 主动跳过片头和片尾模块 64 | s.subspec 'SkipTime' do |xx| 65 | xx.source_files = "Sources/Protocols/SkipTime/*.swift" 66 | xx.dependency 'KJPlayer/Core' 67 | end 68 | 69 | ## 免费时间时间模块 70 | s.subspec 'FreeTime' do |xx| 71 | xx.source_files = "Sources/Protocols/FreeTime/*.swift" 72 | xx.dependency 'KJPlayer/Core' 73 | end 74 | 75 | ## AVPlayer画中画模块 76 | s.subspec 'Pip' do |xx| 77 | xx.source_files = "Sources/Protocols/Pip/*.swift" 78 | xx.dependency 'KJPlayer/Core' 79 | end 80 | 81 | end 82 | -------------------------------------------------------------------------------- /KJPlayer.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8753267D29CAAB700030F655 /* APlayer.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 8753264B29CAAB700030F655 /* APlayer.xcdatamodeld */; }; 11 | 8753267E29CAAB700030F655 /* PlayerVideoData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753264D29CAAB700030F655 /* PlayerVideoData.swift */; }; 12 | 8753267F29CAAB700030F655 /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753264E29CAAB700030F655 /* DatabaseManager.swift */; }; 13 | 8753268029CAAB700030F655 /* DatabaseConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753264F29CAAB700030F655 /* DatabaseConfiguration.swift */; }; 14 | 8753268129CAAB700030F655 /* FreeTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753265129CAAB700030F655 /* FreeTime.swift */; }; 15 | 8753268229CAAB700030F655 /* Screenshots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753265329CAAB700030F655 /* Screenshots.swift */; }; 16 | 8753268329CAAB700030F655 /* Bridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753265529CAAB700030F655 /* Bridge.swift */; }; 17 | 8753268429CAAB700030F655 /* Protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753265629CAAB700030F655 /* Protocol.swift */; }; 18 | 8753268529CAAB700030F655 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753265729CAAB700030F655 /* Shared.swift */; }; 19 | 8753268629CAAB700030F655 /* Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753265829CAAB700030F655 /* Provider.swift */; }; 20 | 8753268729CAAB700030F655 /* Timer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753265929CAAB700030F655 /* Timer.swift */; }; 21 | 8753268829CAAB700030F655 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753265A29CAAB700030F655 /* Notification.swift */; }; 22 | 8753268929CAAB700030F655 /* KJBasePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753265B29CAAB700030F655 /* KJBasePlayer.swift */; }; 23 | 8753268A29CAAB700030F655 /* KJPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753265C29CAAB700030F655 /* KJPlayer.swift */; }; 24 | 8753268B29CAAB700030F655 /* KJPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753265D29CAAB700030F655 /* KJPlayerView.swift */; }; 25 | 8753268C29CAAB700030F655 /* Function.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753265E29CAAB700030F655 /* Function.swift */; }; 26 | 8753268D29CAAB700030F655 /* Enum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753265F29CAAB700030F655 /* Enum.swift */; }; 27 | 8753268E29CAAB700030F655 /* Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753266129CAAB700030F655 /* Common.swift */; }; 28 | 8753268F29CAAB700030F655 /* Timer+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753266229CAAB700030F655 /* Timer+Extension.swift */; }; 29 | 8753269029CAAB700030F655 /* CacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753266429CAAB700030F655 /* CacheManager.swift */; }; 30 | 8753269129CAAB700030F655 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753266529CAAB700030F655 /* Cache.swift */; }; 31 | 8753269229CAAB700030F655 /* KJDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 8753266729CAAB700030F655 /* KJDownloader.m */; }; 32 | 8753269329CAAB700030F655 /* KJPlayer-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = 8753266829CAAB700030F655 /* KJPlayer-Bridging-Header.h */; }; 33 | 8753269429CAAB700030F655 /* KJDownloaderCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = 8753266929CAAB700030F655 /* KJDownloaderCommon.m */; }; 34 | 8753269529CAAB700030F655 /* KJFileHandleInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 8753266A29CAAB700030F655 /* KJFileHandleInfo.m */; }; 35 | 8753269629CAAB700030F655 /* KJFileHandleManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 8753266B29CAAB700030F655 /* KJFileHandleManager.m */; }; 36 | 8753269729CAAB700030F655 /* KJDownloader.h in Headers */ = {isa = PBXBuildFile; fileRef = 8753266C29CAAB700030F655 /* KJDownloader.h */; }; 37 | 8753269829CAAB700030F655 /* KJFileHandleInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 8753266D29CAAB700030F655 /* KJFileHandleInfo.h */; }; 38 | 8753269929CAAB700030F655 /* KJDownloaderCommon.h in Headers */ = {isa = PBXBuildFile; fileRef = 8753266E29CAAB700030F655 /* KJDownloaderCommon.h */; }; 39 | 8753269A29CAAB700030F655 /* KJFileHandleManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 8753266F29CAAB700030F655 /* KJFileHandleManager.h */; }; 40 | 8753269B29CAAB700030F655 /* KJAVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753267129CAAB700030F655 /* KJAVPlayer.swift */; }; 41 | 8753269C29CAAB700030F655 /* PictureInPicture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753267329CAAB700030F655 /* PictureInPicture.swift */; }; 42 | 8753269E29CAAB700030F655 /* SkipTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753267729CAAB700030F655 /* SkipTime.swift */; }; 43 | 8753269F29CAAB700030F655 /* RecordTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753267929CAAB700030F655 /* RecordTime.swift */; }; 44 | 875326A029CAAB700030F655 /* RecordTimeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8753267A29CAAB700030F655 /* RecordTimeData.swift */; }; 45 | 875326AC29CAAD940030F655 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 875326AA29CAAD940030F655 /* README.md */; }; 46 | 875326AD29CAAD940030F655 /* KJPlayer.podspec in Resources */ = {isa = PBXBuildFile; fileRef = 875326AB29CAAD940030F655 /* KJPlayer.podspec */; }; 47 | /* End PBXBuildFile section */ 48 | 49 | /* Begin PBXFileReference section */ 50 | 8753264C29CAAB700030F655 /* APlayer.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = APlayer.xcdatamodel; sourceTree = ""; }; 51 | 8753264D29CAAB700030F655 /* PlayerVideoData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerVideoData.swift; sourceTree = ""; }; 52 | 8753264E29CAAB700030F655 /* DatabaseManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = ""; }; 53 | 8753264F29CAAB700030F655 /* DatabaseConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseConfiguration.swift; sourceTree = ""; }; 54 | 8753265129CAAB700030F655 /* FreeTime.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FreeTime.swift; sourceTree = ""; }; 55 | 8753265329CAAB700030F655 /* Screenshots.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Screenshots.swift; sourceTree = ""; }; 56 | 8753265529CAAB700030F655 /* Bridge.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bridge.swift; sourceTree = ""; }; 57 | 8753265629CAAB700030F655 /* Protocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Protocol.swift; sourceTree = ""; }; 58 | 8753265729CAAB700030F655 /* Shared.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shared.swift; sourceTree = ""; }; 59 | 8753265829CAAB700030F655 /* Provider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Provider.swift; sourceTree = ""; }; 60 | 8753265929CAAB700030F655 /* Timer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Timer.swift; sourceTree = ""; }; 61 | 8753265A29CAAB700030F655 /* Notification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; 62 | 8753265B29CAAB700030F655 /* KJBasePlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KJBasePlayer.swift; sourceTree = ""; }; 63 | 8753265C29CAAB700030F655 /* KJPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KJPlayer.swift; sourceTree = ""; }; 64 | 8753265D29CAAB700030F655 /* KJPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KJPlayerView.swift; sourceTree = ""; }; 65 | 8753265E29CAAB700030F655 /* Function.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Function.swift; sourceTree = ""; }; 66 | 8753265F29CAAB700030F655 /* Enum.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Enum.swift; sourceTree = ""; }; 67 | 8753266129CAAB700030F655 /* Common.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = ""; }; 68 | 8753266229CAAB700030F655 /* Timer+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Timer+Extension.swift"; sourceTree = ""; }; 69 | 8753266429CAAB700030F655 /* CacheManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheManager.swift; sourceTree = ""; }; 70 | 8753266529CAAB700030F655 /* Cache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = ""; }; 71 | 8753266729CAAB700030F655 /* KJDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KJDownloader.m; sourceTree = ""; }; 72 | 8753266829CAAB700030F655 /* KJPlayer-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "KJPlayer-Bridging-Header.h"; sourceTree = ""; }; 73 | 8753266929CAAB700030F655 /* KJDownloaderCommon.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KJDownloaderCommon.m; sourceTree = ""; }; 74 | 8753266A29CAAB700030F655 /* KJFileHandleInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KJFileHandleInfo.m; sourceTree = ""; }; 75 | 8753266B29CAAB700030F655 /* KJFileHandleManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KJFileHandleManager.m; sourceTree = ""; }; 76 | 8753266C29CAAB700030F655 /* KJDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KJDownloader.h; sourceTree = ""; }; 77 | 8753266D29CAAB700030F655 /* KJFileHandleInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KJFileHandleInfo.h; sourceTree = ""; }; 78 | 8753266E29CAAB700030F655 /* KJDownloaderCommon.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KJDownloaderCommon.h; sourceTree = ""; }; 79 | 8753266F29CAAB700030F655 /* KJFileHandleManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KJFileHandleManager.h; sourceTree = ""; }; 80 | 8753267129CAAB700030F655 /* KJAVPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KJAVPlayer.swift; sourceTree = ""; }; 81 | 8753267329CAAB700030F655 /* PictureInPicture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PictureInPicture.swift; sourceTree = ""; }; 82 | 8753267729CAAB700030F655 /* SkipTime.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SkipTime.swift; sourceTree = ""; }; 83 | 8753267929CAAB700030F655 /* RecordTime.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordTime.swift; sourceTree = ""; }; 84 | 8753267A29CAAB700030F655 /* RecordTimeData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordTimeData.swift; sourceTree = ""; }; 85 | 875326AA29CAAD940030F655 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 86 | 875326AB29CAAD940030F655 /* KJPlayer.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = KJPlayer.podspec; sourceTree = ""; }; 87 | 875326AF29CAAEBD0030F655 /* KJPlayer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = KJPlayer.framework; path = "/Users/a2019/Desktop/Git/Moonlit/build/Debug-iphoneos/KJPlayer.framework"; sourceTree = ""; }; 88 | /* End PBXFileReference section */ 89 | 90 | /* Begin PBXFrameworksBuildPhase section */ 91 | 873B7E5329CAA93C004ACFE5 /* Frameworks */ = { 92 | isa = PBXFrameworksBuildPhase; 93 | buildActionMask = 2147483647; 94 | files = ( 95 | ); 96 | runOnlyForDeploymentPostprocessing = 0; 97 | }; 98 | /* End PBXFrameworksBuildPhase section */ 99 | 100 | /* Begin PBXGroup section */ 101 | 873B7E4C29CAA93C004ACFE5 = { 102 | isa = PBXGroup; 103 | children = ( 104 | 875326AB29CAAD940030F655 /* KJPlayer.podspec */, 105 | 875326AA29CAAD940030F655 /* README.md */, 106 | 8753264929CAAB700030F655 /* Sources */, 107 | ); 108 | sourceTree = ""; 109 | }; 110 | 8753264929CAAB700030F655 /* Sources */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | 8753265429CAAB700030F655 /* Core */, 114 | 875326A729CAAC8C0030F655 /* Kernel */, 115 | 875326A829CAACDA0030F655 /* Protocols */, 116 | ); 117 | path = Sources; 118 | sourceTree = ""; 119 | }; 120 | 8753264A29CAAB700030F655 /* Database */ = { 121 | isa = PBXGroup; 122 | children = ( 123 | 8753264B29CAAB700030F655 /* APlayer.xcdatamodeld */, 124 | 8753264F29CAAB700030F655 /* DatabaseConfiguration.swift */, 125 | 8753264E29CAAB700030F655 /* DatabaseManager.swift */, 126 | 8753264D29CAAB700030F655 /* PlayerVideoData.swift */, 127 | ); 128 | path = Database; 129 | sourceTree = ""; 130 | }; 131 | 8753265029CAAB700030F655 /* FreeTime */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | 8753265129CAAB700030F655 /* FreeTime.swift */, 135 | ); 136 | path = FreeTime; 137 | sourceTree = ""; 138 | }; 139 | 8753265429CAAB700030F655 /* Core */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | 875326B129CAE56A0030F655 /* Base */, 143 | 8753264A29CAAB700030F655 /* Database */, 144 | 8753266629CAAB700030F655 /* Downloader */, 145 | 8753266029CAAB700030F655 /* Extensions */, 146 | 875326A929CAAD5B0030F655 /* Setup */, 147 | 875326B029CAE4460030F655 /* View */, 148 | ); 149 | path = Core; 150 | sourceTree = ""; 151 | }; 152 | 8753266029CAAB700030F655 /* Extensions */ = { 153 | isa = PBXGroup; 154 | children = ( 155 | 8753266229CAAB700030F655 /* Timer+Extension.swift */, 156 | ); 157 | path = Extensions; 158 | sourceTree = ""; 159 | }; 160 | 8753266329CAAB700030F655 /* Cache */ = { 161 | isa = PBXGroup; 162 | children = ( 163 | 8753266529CAAB700030F655 /* Cache.swift */, 164 | 8753266429CAAB700030F655 /* CacheManager.swift */, 165 | ); 166 | path = Cache; 167 | sourceTree = ""; 168 | }; 169 | 8753266629CAAB700030F655 /* Downloader */ = { 170 | isa = PBXGroup; 171 | children = ( 172 | 8753266C29CAAB700030F655 /* KJDownloader.h */, 173 | 8753266729CAAB700030F655 /* KJDownloader.m */, 174 | 8753266E29CAAB700030F655 /* KJDownloaderCommon.h */, 175 | 8753266929CAAB700030F655 /* KJDownloaderCommon.m */, 176 | 8753266D29CAAB700030F655 /* KJFileHandleInfo.h */, 177 | 8753266A29CAAB700030F655 /* KJFileHandleInfo.m */, 178 | 8753266F29CAAB700030F655 /* KJFileHandleManager.h */, 179 | 8753266B29CAAB700030F655 /* KJFileHandleManager.m */, 180 | 8753266829CAAB700030F655 /* KJPlayer-Bridging-Header.h */, 181 | ); 182 | path = Downloader; 183 | sourceTree = ""; 184 | }; 185 | 8753267029CAAB700030F655 /* AVPlayer */ = { 186 | isa = PBXGroup; 187 | children = ( 188 | 8753267129CAAB700030F655 /* KJAVPlayer.swift */, 189 | ); 190 | path = AVPlayer; 191 | sourceTree = ""; 192 | }; 193 | 8753267229CAAB700030F655 /* Pip */ = { 194 | isa = PBXGroup; 195 | children = ( 196 | 8753267329CAAB700030F655 /* PictureInPicture.swift */, 197 | ); 198 | path = Pip; 199 | sourceTree = ""; 200 | }; 201 | 8753267629CAAB700030F655 /* SkipTime */ = { 202 | isa = PBXGroup; 203 | children = ( 204 | 8753267729CAAB700030F655 /* SkipTime.swift */, 205 | ); 206 | path = SkipTime; 207 | sourceTree = ""; 208 | }; 209 | 8753267829CAAB700030F655 /* RecordTime */ = { 210 | isa = PBXGroup; 211 | children = ( 212 | 8753267929CAAB700030F655 /* RecordTime.swift */, 213 | 8753267A29CAAB700030F655 /* RecordTimeData.swift */, 214 | ); 215 | path = RecordTime; 216 | sourceTree = ""; 217 | }; 218 | 875326A729CAAC8C0030F655 /* Kernel */ = { 219 | isa = PBXGroup; 220 | children = ( 221 | 8753267029CAAB700030F655 /* AVPlayer */, 222 | ); 223 | path = "Kernel "; 224 | sourceTree = ""; 225 | }; 226 | 875326A829CAACDA0030F655 /* Protocols */ = { 227 | isa = PBXGroup; 228 | children = ( 229 | 8753266329CAAB700030F655 /* Cache */, 230 | 8753265029CAAB700030F655 /* FreeTime */, 231 | 8753267229CAAB700030F655 /* Pip */, 232 | 8753267829CAAB700030F655 /* RecordTime */, 233 | 8753267629CAAB700030F655 /* SkipTime */, 234 | ); 235 | path = Protocols; 236 | sourceTree = ""; 237 | }; 238 | 875326A929CAAD5B0030F655 /* Setup */ = { 239 | isa = PBXGroup; 240 | children = ( 241 | 8753265529CAAB700030F655 /* Bridge.swift */, 242 | 8753266129CAAB700030F655 /* Common.swift */, 243 | 8753265F29CAAB700030F655 /* Enum.swift */, 244 | 8753265E29CAAB700030F655 /* Function.swift */, 245 | 8753265A29CAAB700030F655 /* Notification.swift */, 246 | 8753265629CAAB700030F655 /* Protocol.swift */, 247 | 8753265829CAAB700030F655 /* Provider.swift */, 248 | 8753265329CAAB700030F655 /* Screenshots.swift */, 249 | 8753265729CAAB700030F655 /* Shared.swift */, 250 | 8753265929CAAB700030F655 /* Timer.swift */, 251 | ); 252 | path = Setup; 253 | sourceTree = ""; 254 | }; 255 | 875326B029CAE4460030F655 /* View */ = { 256 | isa = PBXGroup; 257 | children = ( 258 | 8753265D29CAAB700030F655 /* KJPlayerView.swift */, 259 | ); 260 | path = View; 261 | sourceTree = ""; 262 | }; 263 | 875326B129CAE56A0030F655 /* Base */ = { 264 | isa = PBXGroup; 265 | children = ( 266 | 8753265B29CAAB700030F655 /* KJBasePlayer.swift */, 267 | 8753265C29CAAB700030F655 /* KJPlayer.swift */, 268 | ); 269 | path = Base; 270 | sourceTree = ""; 271 | }; 272 | /* End PBXGroup section */ 273 | 274 | /* Begin PBXHeadersBuildPhase section */ 275 | 873B7E5129CAA93C004ACFE5 /* Headers */ = { 276 | isa = PBXHeadersBuildPhase; 277 | buildActionMask = 2147483647; 278 | files = ( 279 | 8753269A29CAAB700030F655 /* KJFileHandleManager.h in Headers */, 280 | 8753269829CAAB700030F655 /* KJFileHandleInfo.h in Headers */, 281 | 8753269729CAAB700030F655 /* KJDownloader.h in Headers */, 282 | 8753269329CAAB700030F655 /* KJPlayer-Bridging-Header.h in Headers */, 283 | 8753269929CAAB700030F655 /* KJDownloaderCommon.h in Headers */, 284 | ); 285 | runOnlyForDeploymentPostprocessing = 0; 286 | }; 287 | /* End PBXHeadersBuildPhase section */ 288 | 289 | /* Begin PBXNativeTarget section */ 290 | 873B7E5529CAA93C004ACFE5 /* KJPlayer */ = { 291 | isa = PBXNativeTarget; 292 | buildConfigurationList = 873B7E5D29CAA93C004ACFE5 /* Build configuration list for PBXNativeTarget "KJPlayer" */; 293 | buildPhases = ( 294 | 873B7E5129CAA93C004ACFE5 /* Headers */, 295 | 873B7E5229CAA93C004ACFE5 /* Sources */, 296 | 873B7E5329CAA93C004ACFE5 /* Frameworks */, 297 | 873B7E5429CAA93C004ACFE5 /* Resources */, 298 | ); 299 | buildRules = ( 300 | ); 301 | dependencies = ( 302 | ); 303 | name = KJPlayer; 304 | productName = KJPlayer; 305 | productReference = 875326AF29CAAEBD0030F655 /* KJPlayer.framework */; 306 | productType = "com.apple.product-type.framework"; 307 | }; 308 | /* End PBXNativeTarget section */ 309 | 310 | /* Begin PBXProject section */ 311 | 873B7E4D29CAA93C004ACFE5 /* Project object */ = { 312 | isa = PBXProject; 313 | attributes = { 314 | BuildIndependentTargetsInParallel = 1; 315 | LastUpgradeCheck = 1420; 316 | TargetAttributes = { 317 | 873B7E5529CAA93C004ACFE5 = { 318 | CreatedOnToolsVersion = 14.2; 319 | }; 320 | }; 321 | }; 322 | buildConfigurationList = 873B7E5029CAA93C004ACFE5 /* Build configuration list for PBXProject "KJPlayer" */; 323 | compatibilityVersion = "Xcode 14.0"; 324 | developmentRegion = en; 325 | hasScannedForEncodings = 0; 326 | knownRegions = ( 327 | en, 328 | Base, 329 | ); 330 | mainGroup = 873B7E4C29CAA93C004ACFE5; 331 | productRefGroup = 873B7E4C29CAA93C004ACFE5; 332 | projectDirPath = ""; 333 | projectRoot = ""; 334 | targets = ( 335 | 873B7E5529CAA93C004ACFE5 /* KJPlayer */, 336 | ); 337 | }; 338 | /* End PBXProject section */ 339 | 340 | /* Begin PBXResourcesBuildPhase section */ 341 | 873B7E5429CAA93C004ACFE5 /* Resources */ = { 342 | isa = PBXResourcesBuildPhase; 343 | buildActionMask = 2147483647; 344 | files = ( 345 | 875326AD29CAAD940030F655 /* KJPlayer.podspec in Resources */, 346 | 875326AC29CAAD940030F655 /* README.md in Resources */, 347 | ); 348 | runOnlyForDeploymentPostprocessing = 0; 349 | }; 350 | /* End PBXResourcesBuildPhase section */ 351 | 352 | /* Begin PBXSourcesBuildPhase section */ 353 | 873B7E5229CAA93C004ACFE5 /* Sources */ = { 354 | isa = PBXSourcesBuildPhase; 355 | buildActionMask = 2147483647; 356 | files = ( 357 | 8753267E29CAAB700030F655 /* PlayerVideoData.swift in Sources */, 358 | 8753268229CAAB700030F655 /* Screenshots.swift in Sources */, 359 | 8753268F29CAAB700030F655 /* Timer+Extension.swift in Sources */, 360 | 8753268329CAAB700030F655 /* Bridge.swift in Sources */, 361 | 8753268429CAAB700030F655 /* Protocol.swift in Sources */, 362 | 8753268E29CAAB700030F655 /* Common.swift in Sources */, 363 | 8753269F29CAAB700030F655 /* RecordTime.swift in Sources */, 364 | 8753268129CAAB700030F655 /* FreeTime.swift in Sources */, 365 | 8753269129CAAB700030F655 /* Cache.swift in Sources */, 366 | 8753267D29CAAB700030F655 /* APlayer.xcdatamodeld in Sources */, 367 | 8753268529CAAB700030F655 /* Shared.swift in Sources */, 368 | 8753269529CAAB700030F655 /* KJFileHandleInfo.m in Sources */, 369 | 8753268A29CAAB700030F655 /* KJPlayer.swift in Sources */, 370 | 8753269429CAAB700030F655 /* KJDownloaderCommon.m in Sources */, 371 | 8753267F29CAAB700030F655 /* DatabaseManager.swift in Sources */, 372 | 8753268829CAAB700030F655 /* Notification.swift in Sources */, 373 | 8753268629CAAB700030F655 /* Provider.swift in Sources */, 374 | 8753268729CAAB700030F655 /* Timer.swift in Sources */, 375 | 8753268929CAAB700030F655 /* KJBasePlayer.swift in Sources */, 376 | 875326A029CAAB700030F655 /* RecordTimeData.swift in Sources */, 377 | 8753268D29CAAB700030F655 /* Enum.swift in Sources */, 378 | 8753269C29CAAB700030F655 /* PictureInPicture.swift in Sources */, 379 | 8753269229CAAB700030F655 /* KJDownloader.m in Sources */, 380 | 8753269B29CAAB700030F655 /* KJAVPlayer.swift in Sources */, 381 | 8753269E29CAAB700030F655 /* SkipTime.swift in Sources */, 382 | 8753269629CAAB700030F655 /* KJFileHandleManager.m in Sources */, 383 | 8753269029CAAB700030F655 /* CacheManager.swift in Sources */, 384 | 8753268029CAAB700030F655 /* DatabaseConfiguration.swift in Sources */, 385 | 8753268B29CAAB700030F655 /* KJPlayerView.swift in Sources */, 386 | 8753268C29CAAB700030F655 /* Function.swift in Sources */, 387 | ); 388 | runOnlyForDeploymentPostprocessing = 0; 389 | }; 390 | /* End PBXSourcesBuildPhase section */ 391 | 392 | /* Begin XCBuildConfiguration section */ 393 | 873B7E5B29CAA93C004ACFE5 /* Debug */ = { 394 | isa = XCBuildConfiguration; 395 | buildSettings = { 396 | ALWAYS_SEARCH_USER_PATHS = NO; 397 | CLANG_ANALYZER_NONNULL = YES; 398 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 399 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 400 | CLANG_ENABLE_MODULES = YES; 401 | CLANG_ENABLE_OBJC_ARC = YES; 402 | CLANG_ENABLE_OBJC_WEAK = YES; 403 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 404 | CLANG_WARN_BOOL_CONVERSION = YES; 405 | CLANG_WARN_COMMA = YES; 406 | CLANG_WARN_CONSTANT_CONVERSION = YES; 407 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 408 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 409 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 410 | CLANG_WARN_EMPTY_BODY = YES; 411 | CLANG_WARN_ENUM_CONVERSION = YES; 412 | CLANG_WARN_INFINITE_RECURSION = YES; 413 | CLANG_WARN_INT_CONVERSION = YES; 414 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 415 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 416 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 417 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 418 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 419 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 420 | CLANG_WARN_STRICT_PROTOTYPES = YES; 421 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 422 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 423 | CLANG_WARN_UNREACHABLE_CODE = YES; 424 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 425 | COPY_PHASE_STRIP = NO; 426 | CURRENT_PROJECT_VERSION = 1; 427 | DEBUG_INFORMATION_FORMAT = dwarf; 428 | ENABLE_STRICT_OBJC_MSGSEND = YES; 429 | ENABLE_TESTABILITY = YES; 430 | GCC_C_LANGUAGE_STANDARD = gnu11; 431 | GCC_DYNAMIC_NO_PIC = NO; 432 | GCC_NO_COMMON_BLOCKS = YES; 433 | GCC_OPTIMIZATION_LEVEL = 0; 434 | GCC_PREPROCESSOR_DEFINITIONS = ( 435 | "DEBUG=1", 436 | "$(inherited)", 437 | ); 438 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 439 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 440 | GCC_WARN_UNDECLARED_SELECTOR = YES; 441 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 442 | GCC_WARN_UNUSED_FUNCTION = YES; 443 | GCC_WARN_UNUSED_VARIABLE = YES; 444 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 445 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 446 | MTL_FAST_MATH = YES; 447 | ONLY_ACTIVE_ARCH = YES; 448 | SDKROOT = iphoneos; 449 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 450 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 451 | VERSIONING_SYSTEM = "apple-generic"; 452 | VERSION_INFO_PREFIX = ""; 453 | }; 454 | name = Debug; 455 | }; 456 | 873B7E5C29CAA93C004ACFE5 /* Release */ = { 457 | isa = XCBuildConfiguration; 458 | buildSettings = { 459 | ALWAYS_SEARCH_USER_PATHS = NO; 460 | CLANG_ANALYZER_NONNULL = YES; 461 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 462 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 463 | CLANG_ENABLE_MODULES = YES; 464 | CLANG_ENABLE_OBJC_ARC = YES; 465 | CLANG_ENABLE_OBJC_WEAK = YES; 466 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 467 | CLANG_WARN_BOOL_CONVERSION = YES; 468 | CLANG_WARN_COMMA = YES; 469 | CLANG_WARN_CONSTANT_CONVERSION = YES; 470 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 471 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 472 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 473 | CLANG_WARN_EMPTY_BODY = YES; 474 | CLANG_WARN_ENUM_CONVERSION = YES; 475 | CLANG_WARN_INFINITE_RECURSION = YES; 476 | CLANG_WARN_INT_CONVERSION = YES; 477 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 478 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 479 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 480 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 481 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 482 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 483 | CLANG_WARN_STRICT_PROTOTYPES = YES; 484 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 485 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 486 | CLANG_WARN_UNREACHABLE_CODE = YES; 487 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 488 | COPY_PHASE_STRIP = NO; 489 | CURRENT_PROJECT_VERSION = 1; 490 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 491 | ENABLE_NS_ASSERTIONS = NO; 492 | ENABLE_STRICT_OBJC_MSGSEND = YES; 493 | GCC_C_LANGUAGE_STANDARD = gnu11; 494 | GCC_NO_COMMON_BLOCKS = YES; 495 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 496 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 497 | GCC_WARN_UNDECLARED_SELECTOR = YES; 498 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 499 | GCC_WARN_UNUSED_FUNCTION = YES; 500 | GCC_WARN_UNUSED_VARIABLE = YES; 501 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 502 | MTL_ENABLE_DEBUG_INFO = NO; 503 | MTL_FAST_MATH = YES; 504 | SDKROOT = iphoneos; 505 | SWIFT_COMPILATION_MODE = wholemodule; 506 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 507 | VALIDATE_PRODUCT = YES; 508 | VERSIONING_SYSTEM = "apple-generic"; 509 | VERSION_INFO_PREFIX = ""; 510 | }; 511 | name = Release; 512 | }; 513 | 873B7E5E29CAA93C004ACFE5 /* Debug */ = { 514 | isa = XCBuildConfiguration; 515 | buildSettings = { 516 | CODE_SIGN_STYLE = Automatic; 517 | CURRENT_PROJECT_VERSION = 1; 518 | DEFINES_MODULE = YES; 519 | DEVELOPMENT_TEAM = AAHMKJNWLX; 520 | DYLIB_COMPATIBILITY_VERSION = 1; 521 | DYLIB_CURRENT_VERSION = 1; 522 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 523 | GENERATE_INFOPLIST_FILE = YES; 524 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 525 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 526 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 527 | LD_RUNPATH_SEARCH_PATHS = ( 528 | "$(inherited)", 529 | "@executable_path/Frameworks", 530 | "@loader_path/Frameworks", 531 | ); 532 | MARKETING_VERSION = 1.0; 533 | PRODUCT_BUNDLE_IDENTIFIER = Apple.KJPlayer; 534 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 535 | SKIP_INSTALL = YES; 536 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 537 | SUPPORTS_MACCATALYST = NO; 538 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 539 | SWIFT_EMIT_LOC_STRINGS = YES; 540 | SWIFT_VERSION = 5.0; 541 | TARGETED_DEVICE_FAMILY = "1,2"; 542 | }; 543 | name = Debug; 544 | }; 545 | 873B7E5F29CAA93C004ACFE5 /* Release */ = { 546 | isa = XCBuildConfiguration; 547 | buildSettings = { 548 | CODE_SIGN_STYLE = Automatic; 549 | CURRENT_PROJECT_VERSION = 1; 550 | DEFINES_MODULE = YES; 551 | DEVELOPMENT_TEAM = AAHMKJNWLX; 552 | DYLIB_COMPATIBILITY_VERSION = 1; 553 | DYLIB_CURRENT_VERSION = 1; 554 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 555 | GENERATE_INFOPLIST_FILE = YES; 556 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 557 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 558 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 559 | LD_RUNPATH_SEARCH_PATHS = ( 560 | "$(inherited)", 561 | "@executable_path/Frameworks", 562 | "@loader_path/Frameworks", 563 | ); 564 | MARKETING_VERSION = 1.0; 565 | PRODUCT_BUNDLE_IDENTIFIER = Apple.KJPlayer; 566 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 567 | SKIP_INSTALL = YES; 568 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 569 | SUPPORTS_MACCATALYST = NO; 570 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 571 | SWIFT_EMIT_LOC_STRINGS = YES; 572 | SWIFT_VERSION = 5.0; 573 | TARGETED_DEVICE_FAMILY = "1,2"; 574 | }; 575 | name = Release; 576 | }; 577 | /* End XCBuildConfiguration section */ 578 | 579 | /* Begin XCConfigurationList section */ 580 | 873B7E5029CAA93C004ACFE5 /* Build configuration list for PBXProject "KJPlayer" */ = { 581 | isa = XCConfigurationList; 582 | buildConfigurations = ( 583 | 873B7E5B29CAA93C004ACFE5 /* Debug */, 584 | 873B7E5C29CAA93C004ACFE5 /* Release */, 585 | ); 586 | defaultConfigurationIsVisible = 0; 587 | defaultConfigurationName = Release; 588 | }; 589 | 873B7E5D29CAA93C004ACFE5 /* Build configuration list for PBXNativeTarget "KJPlayer" */ = { 590 | isa = XCConfigurationList; 591 | buildConfigurations = ( 592 | 873B7E5E29CAA93C004ACFE5 /* Debug */, 593 | 873B7E5F29CAA93C004ACFE5 /* Release */, 594 | ); 595 | defaultConfigurationIsVisible = 0; 596 | defaultConfigurationName = Release; 597 | }; 598 | /* End XCConfigurationList section */ 599 | 600 | /* Begin XCVersionGroup section */ 601 | 8753264B29CAAB700030F655 /* APlayer.xcdatamodeld */ = { 602 | isa = XCVersionGroup; 603 | children = ( 604 | 8753264C29CAAB700030F655 /* APlayer.xcdatamodel */, 605 | ); 606 | currentVersion = 8753264C29CAAB700030F655 /* APlayer.xcdatamodel */; 607 | path = APlayer.xcdatamodeld; 608 | sourceTree = ""; 609 | versionGroupType = wrapper.xcdatamodel; 610 | }; 611 | /* End XCVersionGroup section */ 612 | }; 613 | rootObject = 873B7E4D29CAA93C004ACFE5 /* Project object */; 614 | } 615 | -------------------------------------------------------------------------------- /KJPlayer.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /KJPlayer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /KJPlayer.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /KJPlayer.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 yangkejun 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KJPlayer 2 | 3 | ### 功能介绍 4 | **动态切换内核,支持边下边播的播放器方案** 5 | 6 | * 支持音/视频播放,midi文件播放 7 | * 支持在线播放/本地播放 8 | * 支持后台播放,音频提取播放 9 | * 支持视频边下边播,分片下载播放存储 10 | * 支持断点续载续播,下次直接优先从缓冲读取播放 11 | * 支持缓存管理,清除时间段缓存 12 | * 支持试看,自动跳过片头片尾 13 | * 支持记录上次播放时间 14 | * 支持自动播放,自动连续播放 15 | * 支持随机/重复/顺序播放 16 | * 支持重力感应,全屏/半屏切换 17 | * 支持基本手势操作,进度音量等 18 | * 支持锁定屏幕 19 | * 长按快进快退等操作 20 | * 支持倍速播放 21 | * 支持切换不同分辨率视频 22 | * 支持直播流媒体播放 23 | * 持续更新ing... 24 | 25 | ---------------------------------------- 26 | > 视频支持格式:mp4、m3u8、wav、avi 27 | > 音频支持格式:midi、mp3、 28 | 29 | ---------------------------------------- 30 | 31 | ### 效果图 32 | | | | 33 | | --- | --- | 34 | 35 | ### 免费试看功能 36 | - 该功能类似于Vip会员观看性质,充值之后继续播放观看模式 37 | 38 | ```swift 39 | // MARK: - KJPlayerFreeDelegate 40 | extension DetailsViewController: KJPlayerFreeDelegate { 41 | /// 获取免费试看时间 42 | /// - Parameter player: 播放器内核 43 | /// - Returns: 试看时间,返回零不限制 44 | func kj_freeLookTime(with player: KJBasePlayer) -> TimeInterval { 45 | return 50 46 | } 47 | 48 | /// 试看结束响应 49 | /// - Parameters: 50 | /// - player: 播放器内核 51 | /// - currentTime: 当前播放时间 52 | func kj_freeLookTime(with player: KJBasePlayer, currentTime: TimeInterval) { 53 | 54 | } 55 | } 56 | ``` 57 | - 充值之后恢复观看权限 58 | 59 | ```swift 60 | self.player.kj_closeFreeLookTimeLimit() 61 | ``` 62 | 63 | ### 跳过片头片尾功能 64 | - 该功能很明确就是类似于观看视频跳过片头和片尾功能 65 | 66 | ```swift 67 | // MARK: - KJPlayerSkipDelegate 68 | extension DetailsViewController: KJPlayerSkipDelegate { 69 | /// 跳过片头 70 | /// - Parameter player: 内核 71 | /// - Returns: 需要跳过的时间 72 | func kj_skipOpeningTime(with player: KJBasePlayer) -> TimeInterval { 73 | return 18 74 | } 75 | 76 | /// 跳过片头响应 77 | /// - Parameters: 78 | /// - player: 内核 79 | /// - openingTime: 跳过播放时间 80 | func kj_skipOpeningTime(with player: KJBasePlayer, openingTime: TimeInterval) { 81 | self.backview.hintTextLayer.kj_displayHintText("跳过片头,自动播放", 82 | time: 5, 83 | position: KJPlayerHintPositionBottom) 84 | } 85 | } 86 | ``` 87 | 88 | ### 记忆播放功能 89 | - 该功能会自动记忆上次播放时间,下次直接无缝开始继续播放 90 | - 备注提示:该功能大于跳过片头功能,简单讲就是该功能实现之后下次会直接从上次播放位置开始继续观看 91 | 92 | ```swift 93 | // MARK: - KJPlayerRecordDelegate 94 | extension DetailsViewController: KJPlayerRecordDelegate { 95 | /// 获取是否需要记录响应 96 | /// - Parameter player: 播放器内核 97 | /// - Returns: 是否需要记忆播放 98 | func kj_recordTime(with player: KJBasePlayer) -> Bool { 99 | return true 100 | } 101 | 102 | /// 获取到上次播放时间响应 103 | /// - Parameters: 104 | /// - player: 播放器内核 105 | /// - lastTime: 上次播放时间 106 | func kj_recordTime(with player: KJBasePlayer, lastTime: TimeInterval) { 107 | 108 | } 109 | } 110 | ``` 111 | - 主动选择储存记忆 112 | 113 | ```swift 114 | self.player.kj_saveRecordLastTime() 115 | ``` 116 | 117 | |✌️ 118 | 119 | ---- 120 | 121 | ##### **总结:先把基本的壳子完善,后面再慢慢来补充其他的内核,如若觉得有帮助请帮忙点个星,有什么问题和需求也可以Issues** 122 | 123 | ### 关于作者 124 | - 🎷 **邮箱地址:[ykj310@126.com](ykj310@126.com) 🎷** 125 | - 🎸 **GitHub地址:[yangKJ](https://github.com/yangKJ) 🎸** 126 | - 🎺 **掘金地址:[茶底世界之下](https://juejin.cn/user/1987535102554472/posts) 🎺** 127 | - 🚴🏻 **简书地址:[77___](https://www.jianshu.com/u/c84c00476ab6) 🚴🏻** 128 | 129 | **救救孩子吧,谢谢各位老板。** 130 | 131 | 🥺 132 | 133 | ----- 134 | -------------------------------------------------------------------------------- /Screenshot/AAA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yangKJ/Moonlit/16a727c1dc64f493fde701df2f4652daa6ea1082/Screenshot/AAA.png -------------------------------------------------------------------------------- /Screenshot/XXX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yangKJ/Moonlit/16a727c1dc64f493fde701df2f4652daa6ea1082/Screenshot/XXX.png -------------------------------------------------------------------------------- /Sources/Core/Base/KJBasePlayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KJBasePlayer.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/14. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | @objc(KJBasePlayer) 12 | open class KJBasePlayer: NSObject { 13 | 14 | @objc public weak var delegate: KJPlayerDelegate? 15 | /// Player control 16 | @objc public weak var playerView: KJPlayerView? 17 | /// Configuration information. 18 | @objc public var provider: Provider? = nil 19 | 20 | /// Play speed, 0 ~ 2. 21 | @objc public var speed: Float = 1.0 22 | /// is mute 23 | @objc public var muted: Bool = false 24 | /// play volume 25 | @objc public var volume: Float = 1.0 26 | /// Whether to open auto play 27 | @objc public var autoPlay: Bool = true 28 | 29 | // MARK: - private 30 | private var _originalURL: NSURL? = nil 31 | private var _playURL: NSURL? = nil 32 | private var userPause: Bool = false 33 | private var localed: Bool = false 34 | private var replay: Bool = false 35 | private var playing: Bool = false 36 | 37 | @objc public convenience init(withPlayerView view: KJPlayerView) { 38 | self.init() 39 | self.playerView = view 40 | } 41 | 42 | @objc public required override init() { 43 | super.init() 44 | self.setupTimer(1) 45 | self.setupNotification() 46 | } 47 | 48 | deinit { 49 | #if DEBUG 50 | print("🎷\(String(describing: self)): Deinited") 51 | #endif 52 | NotificationCenter.default.removeObserver(self) 53 | self.deinitTimer() 54 | BridgeMethod.deinit(self).dealloc() 55 | } 56 | 57 | internal func getVideoFrame() -> CGRect { 58 | guard let playerView = playerView else { 59 | return .zero 60 | } 61 | return playerView.bounds 62 | } 63 | 64 | /// Mainly deal with state and UI related here, 65 | /// `Sub class deal with the main logical ideas 66 | var playerStatus: PlayerStatus? { 67 | didSet { 68 | guard let playerStatus = playerStatus else { 69 | return 70 | } 71 | switch playerStatus { 72 | case .prepare(let provider): 73 | self.playFailedObserve = nil 74 | self.userPause = false 75 | self.replay = false 76 | self.playing = false 77 | self.playFinishedTimeObserve = 0.0 78 | self.setupVideoURL(provider.videoURL) 79 | break 80 | case .beginPlay: 81 | self.userPause = false 82 | self.playing = true 83 | break 84 | case .playing(let time): 85 | self.playing = true 86 | if userPause == false, !BridgeMethod.freeLookEnded(self) { 87 | self.playStateObserve = .playing 88 | } 89 | self.currentTimeObserve = time 90 | break 91 | case .paused(let user): 92 | self.playing = false 93 | if user == true { 94 | self.playStateObserve = .paused 95 | } 96 | self.userPause = user 97 | break 98 | case .playFinished(let skip): 99 | self.playing = false 100 | if skip { 101 | self.playFinishedTimeObserve = self.currentTime 102 | } else { 103 | self.playFinishedTimeObserve = self.totalTime 104 | } 105 | BridgeMethod.end(self).playFinished() 106 | break 107 | case .failed(let error): 108 | self.playing = false 109 | self.playFailedObserve = error 110 | break 111 | } 112 | } 113 | } 114 | 115 | /// Configuration play ink 116 | @discardableResult 117 | private func setupVideoURL(_ urlString: String?) -> NSURL? { 118 | var videoURL: NSURL? = nil 119 | if let videoURLString = urlString { 120 | if Common.Function.isOnlineResource(videoURLString) { 121 | self.localed = false 122 | videoURL = NSURL.init(string: videoURLString) 123 | } else { 124 | self.localed = true 125 | videoURL = NSURL.init(fileURLWithPath: videoURLString) 126 | } 127 | } 128 | self._originalURL = videoURL 129 | self._playURL = videoURL 130 | return videoURL 131 | } 132 | 133 | // MARK: - Observe 134 | internal var playStateObserve: KJPlayerState? { 135 | willSet { 136 | guard let newState = newValue else { return } 137 | var state: KJPlayerState? 138 | if playStateObserve == nil { 139 | state = newState 140 | } else if newState != playStateObserve { 141 | state = newState 142 | } else { 143 | return 144 | } 145 | DispatchQueue.main.async { 146 | self.delegate?.kj_player(_:state:)?(self, state!) 147 | } 148 | } 149 | } 150 | 151 | internal var currentTimeObserve: TimeInterval? = 0.0 { 152 | willSet { 153 | guard let newTime = newValue, let oldTime = currentTimeObserve else { return } 154 | if newTime != oldTime { 155 | DispatchQueue.main.async { 156 | self.delegate?.kj_player(_:current:)?(self, newTime) 157 | } 158 | } 159 | } 160 | } 161 | 162 | internal var loadedTimeObserve: TimeInterval? = 0.0 { 163 | willSet { 164 | guard let newTime = newValue, let oldTime = loadedTimeObserve else { return } 165 | if newTime != oldTime, newTime > 0 { 166 | DispatchQueue.main.async { 167 | self.delegate?.kj_player(_:loadedTime:)?(self, newTime) 168 | } 169 | } 170 | } 171 | } 172 | 173 | internal var totalTimeObserve: TimeInterval? = 0.0 { 174 | willSet { 175 | guard let newTime = newValue, let oldTime = totalTimeObserve else { return } 176 | if newTime != oldTime, newTime > 0 { 177 | DispatchQueue.main.async { 178 | self.delegate?.kj_player(_:total:)?(self, newTime) 179 | } 180 | } 181 | } 182 | } 183 | 184 | internal var videoSizeObserve: CGSize? = .zero { 185 | willSet { 186 | guard let newVideoSize = newValue, let oldVideoSize = videoSizeObserve else { return } 187 | if newVideoSize != oldVideoSize { 188 | DispatchQueue.main.async { 189 | self.delegate?.kj_player(_:videoSize:)?(self, newVideoSize) 190 | } 191 | } 192 | } 193 | } 194 | 195 | internal var playFailedObserve: NSError? = nil { 196 | willSet { 197 | guard let newError = newValue, let oldError = playFailedObserve else { return } 198 | if newError != oldError { 199 | self.delegate?.kj_player(_:playFailed:)?(self, newError) 200 | } 201 | } 202 | } 203 | 204 | /// End of play observer 205 | private var playFinishedTimeObserve: TimeInterval? { 206 | willSet { 207 | guard let newTime = newValue else { return } 208 | if (playFinishedTimeObserve == nil || newTime != playFinishedTimeObserve) && newTime > 0 { 209 | DispatchQueue.main.async { 210 | self.delegate?.kj_player(_:playFinished:)?(self, newTime) 211 | } 212 | } 213 | } 214 | } 215 | } 216 | 217 | extension KJBasePlayer: KJPlayer { 218 | @objc public var currentTime: TimeInterval { return self.currentTimeObserve! } 219 | @objc public var totalTime: TimeInterval { return self.totalTimeObserve! } 220 | @objc public var videoSize: CGSize { return self.videoSizeObserve! } 221 | @objc public var originalURL: NSURL? { return self._originalURL } 222 | @objc public var playURL: NSURL? { return self._playURL } 223 | @objc public var isPlaying: Bool { return self.playing } 224 | @objc public var isUserPause: Bool { return self.userPause } 225 | @objc public var isOnlineSource: Bool { return self.localed } 226 | @objc public var isReplay: Bool { return self.replay } 227 | @objc public var loadedProgress: Float { 228 | if self.isOnlineSource == false { 229 | return 1.0 230 | } 231 | if self.totalTime <= 0 { 232 | return 0 233 | } 234 | return Float(min(self.loadedTimeObserve! / self.totalTime, 1)) 235 | } 236 | 237 | @objc public var isLiveStreaming: Bool { 238 | if let videoURL = self._originalURL { 239 | return Common.Function.videoAesset(videoURL) == .HLS 240 | } else { 241 | return false 242 | } 243 | } 244 | 245 | @objc public func kj_play() { 246 | self.userPause = false 247 | self.playerStatus = .beginPlay 248 | } 249 | 250 | @objc public func kj_replay() { 251 | self.replay = true 252 | self.userPause = false 253 | let time = BridgeMethod.skipTime(self) 254 | self.kj_appointTime(time) 255 | } 256 | 257 | @objc public func kj_pause() { 258 | self.userPause = true 259 | self.playerStatus = .paused(user: true) 260 | } 261 | 262 | @objc public func kj_stop() { 263 | self.userPause = false 264 | DispatchQueue.main.async { 265 | self.delegate?.kj_player(_:stopped:)?(self, self.currentTime) 266 | } 267 | } 268 | 269 | @objc public func kj_appointTime(_ time: TimeInterval) { 270 | self.currentTimeObserve = time 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /Sources/Core/Base/KJPlayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KJPlayer.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/14. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | /// kjplayer common protocol method 12 | public protocol KJPlayer { 13 | /// The original video address, used to replay the error and record the last play 14 | var originalURL: NSURL? { get } 15 | /// Link for play 16 | var playURL: NSURL? { get } 17 | /// Is it playing 18 | var isPlaying: Bool { get } 19 | /// Whether to pause for the user 20 | var isUserPause: Bool { get } 21 | /// Whether it is a live streaming media, the total time during the live broadcast is invalid 22 | var isLiveStreaming: Bool { get } 23 | /// Whether it is an online resource 24 | var isOnlineSource: Bool { get } 25 | /// Replay 26 | var isReplay: Bool { get } 27 | /// Current playing time 28 | var currentTime: TimeInterval { get } 29 | /// Total video time 30 | var totalTime: TimeInterval { get } 31 | /// Current video size 32 | var videoSize: CGSize { get } 33 | /// Loaded progress 34 | var loadedProgress: Float { get } 35 | 36 | /// Whether the current playback is a video playback 37 | var isVideo: Bool { get } 38 | 39 | // MARK: - methods 40 | /// Start playing will respond to the last playing time and skip the opening 41 | /// The last play priority is higher than the skip opening 42 | func kj_play() 43 | /// Replay will respond to skip the opening 44 | func kj_replay() 45 | /// Pause 46 | func kj_pause() 47 | /// Stop 48 | func kj_stop() 49 | /// Play at specified time, fast forward or rewind function 50 | func kj_appointTime(_ time: TimeInterval) 51 | /// Screenshot of the current time 52 | func kj_currentTimeScreenshots(_ screenshots: @escaping (_ image: UIImage) -> Void) 53 | } 54 | 55 | extension KJPlayer { 56 | public var originalURL: NSURL? { return nil } 57 | public var playURL: NSURL? { return nil } 58 | public var isPlaying: Bool { return false } 59 | public var isUserPause: Bool { return false } 60 | public var isLiveStreaming: Bool { return false } 61 | public var isOnlineSource: Bool { return true } 62 | public var isReplay: Bool { return false } 63 | public var currentTime: TimeInterval { return 0 } 64 | public var totalTime: TimeInterval { return 0 } 65 | public var videoSize: CGSize { return .zero } 66 | public var loadedProgress: Float { return 0.0 } 67 | public var isVideo: Bool { return true } 68 | } 69 | 70 | extension KJPlayer { 71 | public func kj_play() { } 72 | public func kj_replay() { } 73 | public func kj_pause() { } 74 | public func kj_stop() { } 75 | public func kj_appointTime(_ time: TimeInterval) { } 76 | public func kj_currentTimeScreenshots(_ screenshots: (_ image: UIImage) -> Void) { } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/Core/Database/APlayer.xcdatamodeld/APlayer.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Sources/Core/Database/DatabaseConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabaseConfiguration.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/14. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | extension DatabaseManager { 12 | /// Instance objects 13 | /// `let data = PlayerVideoData.init(context: DatabaseManager.context) 14 | public static var context: NSManagedObjectContext { 15 | get { 16 | if let ctx = DatabaseManager.Configuration.ctx { 17 | return ctx 18 | } 19 | let ctx = DatabaseManager.Configuration.context() 20 | return ctx! 21 | } 22 | } 23 | } 24 | 25 | extension DatabaseManager { struct Configuration { } } 26 | 27 | /// Configuration information, please set in the App startup time 28 | extension DatabaseManager.Configuration { 29 | /// `xcdatamodeld` database name 30 | static let resourceName = "APlayer" 31 | 32 | static var ctx: NSManagedObjectContext? = nil 33 | 34 | @available(iOS 10.0, *) 35 | static func context() -> NSManagedObjectContext? { 36 | guard let url = contextURL(), let model = NSManagedObjectModel(contentsOf: url) else { 37 | return nil 38 | } 39 | let persisContext = NSPersistentContainer(name: PlayerVideoData.entityName, managedObjectModel: model) 40 | persisContext.loadPersistentStores(completionHandler: { _, _ in }) 41 | ctx = persisContext.viewContext 42 | return ctx 43 | } 44 | 45 | static func contextURL(forResource: String = "KJPlayer") -> URL? { 46 | let bundle: Bundle? 47 | if let bundlePath = Bundle.main.path(forResource: forResource, ofType: "xcdatamodeld") { 48 | bundle = Bundle.init(path: bundlePath) 49 | } else { 50 | bundle = Bundle.main 51 | } 52 | return bundle?.url(forResource: DatabaseManager.Configuration.resourceName, withExtension: "momd") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Core/Database/DatabaseManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabaseManager.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/14. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | public struct DatabaseManager { 12 | 13 | @discardableResult 14 | public static func insert(with data: PlayerVideoData) -> Bool { 15 | guard let context = DatabaseManager.Configuration.context(), 16 | let model = PlayerVideoData.insertNewObject(context: context) else { 17 | return false 18 | } 19 | model.dbid = data.dbid 20 | model.recordID = data.recordID ?? data.dbid 21 | model.sandboxPath = data.sandboxPath ?? "" 22 | model.saveTime = Date().timeIntervalSince1970 23 | model.videoContentLength = data.videoContentLength 24 | model.videoData = data.videoData ?? Data() 25 | model.videoFormat = data.videoFormat ?? ".mp4" 26 | model.videoDownloaded = data.videoDownloaded 27 | model.videoTotalTime = data.videoTotalTime 28 | model.videoUrl = data.videoUrl ?? "" 29 | do { 30 | try context.save() 31 | return true 32 | } catch { 33 | return false 34 | } 35 | } 36 | 37 | @discardableResult 38 | public static func update(with data: PlayerVideoData, playedTime: TimeInterval?) -> Bool { 39 | guard let context = DatabaseManager.Configuration.context() else { return false } 40 | let request = PlayerVideoData.fetchRequest() 41 | request.predicate = NSPredicate(format: "%K == %@", #keyPath(PlayerVideoData.dbid), data.dbid!) 42 | do { 43 | let datas = try context.fetch(request) 44 | if !datas.isEmpty, let model = datas.first { 45 | model.recordID = data.recordID ?? data.dbid 46 | model.sandboxPath = data.sandboxPath ?? model.sandboxPath 47 | model.videoData = data.videoData ?? model.videoData 48 | model.videoFormat = data.videoFormat ?? model.videoFormat 49 | model.videoUrl = data.videoUrl ?? model.videoUrl 50 | model.videoDownloaded = model.videoDownloaded == data.videoDownloaded ? model.videoDownloaded : model.videoDownloaded 51 | if data.saveTime > 0 { model.saveTime = data.saveTime } 52 | if data.videoContentLength > 0 { model.videoContentLength = data.videoContentLength } 53 | if data.videoTotalTime > 0 { model.videoTotalTime = data.videoTotalTime } 54 | if context.hasChanges { 55 | do { 56 | try context.save() 57 | return true 58 | } catch { } 59 | } 60 | } 61 | } catch { } 62 | return false 63 | } 64 | 65 | @discardableResult 66 | public static func queryOne(with dbid: String) -> PlayerVideoData? { 67 | let datas = DatabaseManager.query(with: dbid) 68 | if datas.isEmpty { 69 | return nil 70 | } else { 71 | return datas.first 72 | } 73 | } 74 | 75 | @discardableResult 76 | public static func query(with dbid: String) -> [PlayerVideoData] { 77 | guard let context = DatabaseManager.Configuration.context() else { return [] } 78 | let request = PlayerVideoData.fetchRequest() 79 | request.predicate = NSPredicate(format: "%K == %@", #keyPath(PlayerVideoData.dbid), dbid) 80 | do { 81 | let datas = try context.fetch(request) 82 | return datas 83 | } catch { 84 | return [] 85 | } 86 | } 87 | 88 | @discardableResult 89 | public static func delete(with dbid: String) -> Bool { 90 | guard let context = DatabaseManager.Configuration.context() else { return false } 91 | let request = PlayerVideoData.fetchRequest() 92 | request.predicate = NSPredicate(format: "%K == %@", #keyPath(PlayerVideoData.dbid), dbid) 93 | do { 94 | let datas = try context.fetch(request) 95 | for data in datas { 96 | context.delete(data) 97 | } 98 | if context.hasChanges { 99 | do { 100 | try context.save() 101 | return true 102 | } catch { } 103 | } 104 | } catch { } 105 | return false 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Sources/Core/Database/PlayerVideoData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayerVideoData.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/14. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | /// 这里之前遇见的问题,使用自定义模型时刻 12 | /// 关于表名和模型名称一致时,报`Invalid redeclaration of 'PlayerVideoData'` 13 | /// 解决方案:表名`Video`和模型名`PlayerVideoData`不一致即可 14 | /// 15 | /// The problem encountered here, use custom model moments 16 | /// When the table name and the model name are consistent, report `invalid redeclaration of'PlayerVideoData'` 17 | /// Solution to the cause of the table name and model name 18 | /// 19 | /// 20 | ///`[PlayerVideoData setDbid:]: unrecognized selector sent to instance 0x6000006bc000 (NSInvalidArgumentException)` 21 | /// 重新关联对象模型 22 | /// 解决方案: 23 | /// 记得修改`Entity`处的`Class`为对应关联模型`PlayerVideoData` 24 | /// 将`Codegen`修改为`None` 25 | /// 26 | /// Re-associate the object model 27 | /// Solution: 28 | /// Remember to modify the `Class` at `Entity` to correspond to the associated model `PlayerVideoData` 29 | /// Modify `Codegen` to `None` 30 | /// https://stackoverflow.com/questions/45434556/an-nsmanagedobject-of-class-classname-must-have-a-valid-nsentitydescription 31 | 32 | @objc(PlayerVideoData) 33 | public class PlayerVideoData: NSManagedObject { 34 | /// `ENTITIES` table name 35 | static let entityName = "Video" 36 | } 37 | 38 | extension PlayerVideoData { 39 | 40 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 41 | return NSFetchRequest(entityName: Self.entityName) 42 | } 43 | 44 | @nonobjc public class func insertNewObject(context: NSManagedObjectContext) -> PlayerVideoData? { 45 | return NSEntityDescription.insertNewObject(forEntityName: Self.entityName, into: context) as? Self 46 | } 47 | 48 | /// Primary key ID, video link remove SCHEME and then MD5 49 | @NSManaged public var dbid: String? 50 | /// The primary key of the associated `Record` table 51 | @NSManaged public var recordID: String? 52 | /// Sandbox address for video storage 53 | @NSManaged public var sandboxPath: String? 54 | /// Save timestamp, convenient sorting 55 | @NSManaged public var saveTime: Double 56 | /// Video content length 57 | @NSManaged public var videoContentLength: Int16 58 | /// Video data 59 | @NSManaged public var videoData: Data? 60 | /// The video has been downloaded 61 | @NSManaged public var videoDownloaded: Bool 62 | /// Video format suffix, default `.mp4` 63 | @NSManaged public var videoFormat: String? 64 | /// Video total time 65 | @NSManaged public var videoTotalTime: Double 66 | /// Video link 67 | @NSManaged public var videoUrl: String? 68 | } 69 | 70 | extension PlayerVideoData { 71 | 72 | /// Get video total time 73 | public static func videoTotalTime(with dbid: String) -> TimeInterval { 74 | if let data = DatabaseManager.queryOne(with: dbid) { 75 | return data.videoTotalTime 76 | } 77 | return 0.0 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/Core/Downloader/KJDownloader.h: -------------------------------------------------------------------------------- 1 | // 2 | // KJDownloader.h 3 | // KJPlayerDemo 4 | // 5 | // Created by 杨科军 on 2021/2/10. 6 | // Copyright © 2021 杨科军. All rights reserved. 7 | // https://github.com/yangKJ/KJPlayerDemo 8 | 9 | #import 10 | #import "KJFileHandleManager.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | /// 下载管理器 15 | @interface KJDownloader : NSObject 16 | /// 链接 17 | @property (nonatomic,strong,readonly) NSURL *videoURL; 18 | /// 写入和读取文件管理 19 | @property (nonatomic,strong,readonly) KJFileHandleManager *fileHandleManager; 20 | /// 是否缓存 21 | @property (nonatomic,assign) BOOL saveToCache; 22 | 23 | /// 初始化 24 | /// @param url 链接 25 | - (instancetype)initWithURL:(NSURL *)url; 26 | 27 | /// 指定下载,是否下载到末尾全部数据 28 | /// @param range 指定区间 29 | /// @param whole 是否下载到末尾 30 | - (void)kj_downloadTaskRange:(NSRange)range whole:(BOOL)whole; 31 | 32 | /// 开始下载 33 | - (void)kj_startDownload; 34 | 35 | /// 取消下载 36 | - (void)kj_cancelDownload; 37 | 38 | @end 39 | 40 | @interface KJDownloader (KJRequestBlock) 41 | /// 当服务端开始接收数据时调用 42 | @property (nonatomic,copy,readwrite) void (^kDidReceiveResponse)(KJDownloader *downloader, NSURLResponse *response); 43 | /// 当接收到数据的时候调用,该方法多次被调用返回接收到的服务端二进制数据 44 | @property (nonatomic,copy,readwrite) void (^kDidReceiveData)(KJDownloader *downloader, NSData *data); 45 | /// 当请求错误的时候调用 46 | @property (nonatomic,copy,readwrite) void (^kDidFinished)(KJDownloader *downloader, NSError *error); 47 | 48 | @end 49 | 50 | NS_ASSUME_NONNULL_END 51 | -------------------------------------------------------------------------------- /Sources/Core/Downloader/KJDownloader.m: -------------------------------------------------------------------------------- 1 | // 2 | // KJDownloader.m 3 | // KJPlayerDemo 4 | // 5 | // Created by 杨科军 on 2021/2/10. 6 | // Copyright © 2021 杨科军. All rights reserved. 7 | // https://github.com/yangKJ/KJPlayerDemo 8 | 9 | #import "KJDownloader.h" 10 | #import 11 | #import 12 | #import "KJDownloaderCommon.h" 13 | 14 | @protocol KJDownloaderManagerDelegate; 15 | @interface KJDownloadTask : NSObject 16 | @property (nonatomic,weak) id delegate; 17 | @property (nonatomic,assign) BOOL canSaveToCache; 18 | 19 | /// 初始化 20 | /// @param fragments 碎片信息组 21 | /// @param url 链接地址 22 | /// @param manager 文件管理器 23 | - (instancetype)initWithCachedFragments:(NSArray *)fragments 24 | videoURL:(NSURL *)url 25 | manager:(KJFileHandleManager *)manager; 26 | /// 开始下载,处理碎片 27 | - (void)kj_startDownloading; 28 | /// 取消下载 29 | - (void)kj_cancelDownloading; 30 | 31 | @end 32 | 33 | @protocol KJDownloaderManagerDelegate 34 | 35 | /// 开始接收数据,传递配置信息 36 | /// @param response NSURLResponse 37 | - (void)kj_didReceiveResponse:(NSURLResponse *)response; 38 | 39 | /// 接收数据,是否为已经缓存的本地数据 40 | /// @param data 下载数据 41 | /// @param cached 是否成功缓存 42 | - (void)kj_didReceiveData:(NSData *)data cached:(BOOL)cached; 43 | 44 | /// 接收错误 45 | /// @param error 错误数据,nil 46 | - (void)kj_didFinishWithError:(nullable NSError *)error; 47 | 48 | @end 49 | 50 | // ************************************** 黄金分割线 ********************************************* 51 | 52 | @interface KJDownloader () 53 | @property (nonatomic,strong) NSURL *videoURL; 54 | @property (nonatomic,strong) NSString *contentType; 55 | @property (nonatomic,assign) NSUInteger contentLength; 56 | @property (nonatomic,strong) KJFileHandleManager *fileHandleManager; 57 | @property (nonatomic,strong) KJDownloadTask *downloadTask; 58 | 59 | @end 60 | 61 | @implementation KJDownloader 62 | 63 | - (void)dealloc{ 64 | [KJDownloaderCommon.shared kj_removeDownloadURL:self.videoURL]; 65 | } 66 | - (instancetype)initWithURL:(NSURL *)url{ 67 | if (self = [super init]) { 68 | self.saveToCache = YES; 69 | self.videoURL = url; 70 | self.fileHandleManager = [[KJFileHandleManager alloc] initWithURL:url]; 71 | self.contentLength = self.fileHandleManager.cacheInfo.contentLength; 72 | self.contentType = self.fileHandleManager.cacheInfo.contentType; 73 | [KJDownloaderCommon.shared kj_addDownloadURL:self.videoURL]; 74 | } 75 | return self; 76 | } 77 | - (void)kj_createDownloaderManagerWithRange:(NSRange)range{ 78 | NSArray *fragments = [self.fileHandleManager kj_getCachedFragmentsWithRange:range]; 79 | self.downloadTask = [[KJDownloadTask alloc] initWithCachedFragments:fragments 80 | videoURL:self.videoURL 81 | manager:self.fileHandleManager]; 82 | self.downloadTask.canSaveToCache = self.saveToCache; 83 | self.downloadTask.delegate = self; 84 | [self.downloadTask kj_startDownloading]; 85 | } 86 | 87 | - (void)kj_downloadTaskRange:(NSRange)range whole:(BOOL)whole{ 88 | if (whole) range.length = self.contentLength - range.location; 89 | [self kj_createDownloaderManagerWithRange:range]; 90 | } 91 | - (void)kj_startDownload{ 92 | [self kj_createDownloaderManagerWithRange:NSMakeRange(0,2)]; 93 | } 94 | - (void)kj_cancelDownload{ 95 | self.downloadTask.delegate = nil; 96 | [KJDownloaderCommon.shared kj_removeDownloadURL:self.videoURL]; 97 | [self.downloadTask kj_cancelDownloading]; 98 | self.downloadTask = nil; 99 | } 100 | 101 | #pragma mark - KJDownloaderManagerDelegate 102 | 103 | /// 开始接收数据,传递配置信息 104 | - (void)kj_didReceiveResponse:(NSURLResponse *)response{ 105 | if ([response isKindOfClass:[NSHTTPURLResponse class]]){ 106 | NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; 107 | NSArray *array = [httpResponse.allHeaderFields[@"Content-Range"] componentsSeparatedByString:@"/"]; 108 | NSString *length = array.lastObject; 109 | if ([length integerValue] == 0){ 110 | self.contentLength = (NSUInteger)httpResponse.expectedContentLength; 111 | } else { 112 | self.contentLength = [length integerValue]; 113 | } 114 | } 115 | CFStringRef contentType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef)(response.MIMEType), NULL); 116 | self.contentType = CFBridgingRelease(contentType); 117 | [self.fileHandleManager kj_setWriteHandleContentLenght:self.contentLength]; 118 | self.fileHandleManager.cacheInfo.contentLength = self.contentLength; 119 | self.fileHandleManager.cacheInfo.contentType = self.contentType; 120 | if (self.kDidReceiveResponse) { 121 | self.kDidReceiveResponse(self, response); 122 | } 123 | } 124 | /// 接收数据,是否为已经缓存的本地数据 125 | - (void)kj_didReceiveData:(NSData *)data cached:(BOOL)cached{ 126 | if (self.kDidReceiveData) { 127 | self.kDidReceiveData(self, data); 128 | } 129 | } 130 | /// 接收错误或者接收完成,错误为空表示接收完成 131 | - (void)kj_didFinishWithError:(NSError * _Nullable)error{ 132 | [KJDownloaderCommon.shared kj_removeDownloadURL:self.videoURL]; 133 | if (self.kDidFinished) { 134 | self.kDidFinished(self, error); 135 | } 136 | } 137 | 138 | @end 139 | 140 | @implementation KJDownloader (KJRequestBlock) 141 | 142 | #pragma mark - Associated 143 | 144 | - (void (^)(KJDownloader *, NSURLResponse *))kDidReceiveResponse{ 145 | return objc_getAssociatedObject(self, _cmd); 146 | } 147 | - (void (^)(KJDownloader *, NSData *))kDidReceiveData{ 148 | return objc_getAssociatedObject(self, _cmd); 149 | } 150 | - (void (^)(KJDownloader *, NSError *))kDidFinished{ 151 | return objc_getAssociatedObject(self, _cmd); 152 | } 153 | - (void)setKDidReceiveResponse:(void (^)(KJDownloader *, NSURLResponse *))kDidReceiveResponse{ 154 | objc_setAssociatedObject(self, @selector(kDidReceiveResponse), kDidReceiveResponse, OBJC_ASSOCIATION_COPY_NONATOMIC); 155 | } 156 | - (void)setKDidReceiveData:(void (^)(KJDownloader *, NSData *))kDidReceiveData{ 157 | objc_setAssociatedObject(self, @selector(kDidReceiveData), kDidReceiveData, OBJC_ASSOCIATION_COPY_NONATOMIC); 158 | } 159 | - (void)setKDidFinished:(void (^)(KJDownloader *, NSError *))kDidFinished{ 160 | objc_setAssociatedObject(self, @selector(kDidFinished), kDidFinished, OBJC_ASSOCIATION_COPY_NONATOMIC); 161 | } 162 | 163 | @end 164 | 165 | // ************************************** 黄金分割线 ********************************************* 166 | 167 | @interface KJSessionAgent : NSObject 168 | @property (nonatomic,copy,readwrite) void (^kDidReceiveResponse)(NSURLResponse *response, 169 | void(^completionHandler)(NSURLSessionResponseDisposition)); 170 | @property (nonatomic,copy,readwrite) void (^kDidReceiveData)(NSData *data); 171 | @property (nonatomic,copy,readwrite) void (^kDidFinished)(NSError *error); 172 | 173 | @end 174 | 175 | @interface KJDownloadTask () 176 | @property (nonatomic,strong) KJSessionAgent *sessionAgent; 177 | @property (nonatomic,strong) KJFileHandleManager *fileHandleManager; 178 | @property (nonatomic,strong) NSMutableArray *fragments; 179 | @property (nonatomic,strong) NSURLSession *session; 180 | @property (nonatomic,strong) NSURLSessionDataTask *task; 181 | @property (nonatomic,assign) NSInteger startOffset; 182 | @property (nonatomic,strong) NSURL *videoURL; 183 | @property (nonatomic,assign) BOOL cancelLoading; 184 | @property (nonatomic,assign) BOOL once; 185 | 186 | @end 187 | 188 | @implementation KJDownloadTask 189 | 190 | - (void)dealloc{ 191 | [self kj_cancelDownloading]; 192 | } 193 | - (instancetype)initWithCachedFragments:(NSArray *)fragments 194 | videoURL:(NSURL *)url 195 | manager:(KJFileHandleManager *)manager{ 196 | if (self = [super init]) { 197 | self.canSaveToCache = YES; 198 | self.fragments = [NSMutableArray arrayWithArray:fragments]; 199 | self.fileHandleManager = manager; 200 | self.videoURL = url; 201 | } 202 | return self; 203 | } 204 | - (void)kj_startDownloading{ 205 | self.once = NO; 206 | if (_session) [self.session invalidateAndCancel]; 207 | [self kj_downlingFragment]; 208 | } 209 | - (void)kj_cancelDownloading{ 210 | if (_session) [self.session invalidateAndCancel]; 211 | self.cancelLoading = YES; 212 | self.once = NO; 213 | } 214 | /// 下载分片数据 215 | - (void)kj_downlingFragment{ 216 | if (self.cancelLoading) return; 217 | if (self.fragments.count == 0){ 218 | /// 特别备注:此处别乱改要传nil出去,否则会出现播放不起的现象 219 | if ([self.delegate respondsToSelector:@selector(kj_didFinishWithError:)]){ 220 | [self.delegate kj_didFinishWithError:nil]; 221 | } 222 | return; 223 | } 224 | KJCacheFragment fragment = [KJFileHandleInfo kj_getCacheFragment:self.fragments.firstObject]; 225 | [self.fragments removeObjectAtIndex:0]; 226 | if (fragment.type){// 远端碎片,即开始下载 227 | unsigned long fromOffset = fragment.range.location; 228 | unsigned long endOffset = fragment.range.location + fragment.range.length - 1; 229 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:self.videoURL]; 230 | request.cachePolicy = NSURLRequestReloadIgnoringLocalAndRemoteCacheData; 231 | NSString *range = [NSString stringWithFormat:@"bytes=%lu-%lu", fromOffset, endOffset]; 232 | [request setValue:range forHTTPHeaderField:@"Range"]; 233 | self.startOffset = fragment.range.location; 234 | self.task = [self.session dataTaskWithRequest:request]; 235 | [self.task resume]; 236 | } else {//本地碎片 237 | NSData * localData = [self.fileHandleManager kj_readCachedDataWithRange:fragment.range]; 238 | if (self.once == NO && localData == nil) { 239 | self.once = YES; 240 | localData = [self.fileHandleManager kj_readCachedDataWithRange:fragment.range]; 241 | if (localData == nil) { 242 | fragment.type = 1; 243 | unsigned long fromOffset = fragment.range.location; 244 | unsigned long endOffset = fragment.range.location + fragment.range.length - 1; 245 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:self.videoURL]; 246 | request.cachePolicy = NSURLRequestReloadIgnoringLocalAndRemoteCacheData; 247 | NSString *range = [NSString stringWithFormat:@"bytes=%lu-%lu", fromOffset, endOffset]; 248 | [request setValue:range forHTTPHeaderField:@"Range"]; 249 | self.startOffset = fragment.range.location; 250 | self.task = [self.session dataTaskWithRequest:request]; 251 | [self.task resume]; 252 | } 253 | } 254 | if (localData) { 255 | if ([self.delegate respondsToSelector:@selector(kj_didReceiveData:cached:)]) { 256 | [self.delegate kj_didReceiveData:localData cached:YES]; 257 | } 258 | [self kj_downlingFragment]; 259 | } else { 260 | self.once = NO; 261 | if ([self.delegate respondsToSelector:@selector(kj_didFinishWithError:)]) { 262 | NSError * error = [NSError errorWithDomain:@"read cache data file" 263 | code:KJDownloaderFailedCodeReadCachedDataFailed 264 | userInfo:@{NSLocalizedDescriptionKey: @"read cache data file"}]; 265 | [self.delegate kj_didFinishWithError:error]; 266 | } 267 | } 268 | } 269 | } 270 | 271 | #pragma mark - lazy 272 | 273 | - (NSURLSession *)session{ 274 | if (!_session){ 275 | NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; 276 | NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration 277 | delegate:self.sessionAgent 278 | delegateQueue:nil]; 279 | _session = session; 280 | } 281 | return _session; 282 | } 283 | - (KJSessionAgent *)sessionAgent{ 284 | if (!_sessionAgent){ 285 | _sessionAgent = [[KJSessionAgent alloc] init]; 286 | __weak __typeof(self) weakself = self; 287 | _sessionAgent.kDidReceiveResponse = ^(NSURLResponse * response, void (^completionHandler)(NSURLSessionResponseDisposition)) { 288 | NSString *mimeType = response.MIMEType; 289 | if ([mimeType rangeOfString:@"video/"].location == NSNotFound && 290 | [mimeType rangeOfString:@"audio/"].location == NSNotFound && 291 | [mimeType rangeOfString:@"application"].location == NSNotFound){ 292 | completionHandler(NSURLSessionResponseCancel); 293 | } else { 294 | if ([weakself.delegate respondsToSelector:@selector(kj_didReceiveResponse:)]) { 295 | [weakself.delegate kj_didReceiveResponse:response]; 296 | } 297 | [weakself.fileHandleManager kj_startWritting]; 298 | completionHandler(NSURLSessionResponseAllow); 299 | } 300 | }; 301 | _sessionAgent.kDidReceiveData = ^(NSData * data) { 302 | if (weakself.cancelLoading) return; 303 | if (weakself.canSaveToCache) { 304 | NSRange range = NSMakeRange(weakself.startOffset, data.length); 305 | NSError *error = [weakself.fileHandleManager kj_writeCacheData:data range:range]; 306 | if (error) { 307 | if ([weakself.delegate respondsToSelector:@selector(kj_didFinishWithError:)]) { 308 | [weakself.delegate kj_didFinishWithError:error]; 309 | } 310 | return; 311 | } 312 | [weakself.fileHandleManager kj_writeSave]; 313 | } 314 | weakself.startOffset += data.length; 315 | if ([weakself.delegate respondsToSelector:@selector(kj_didReceiveData:cached:)]){ 316 | [weakself.delegate kj_didReceiveData:data cached:NO]; 317 | } 318 | if (weakself.fileHandleManager.cacheInfo) { 319 | NSDictionary * __autoreleasing userInfo = @{ 320 | kPlayerFileHandleInfoKey : weakself.fileHandleManager.cacheInfo 321 | }; 322 | [[NSNotificationCenter defaultCenter] postNotificationName:kPlayerFileHandleInfoNotification 323 | object:weakself 324 | userInfo:userInfo]; 325 | } 326 | }; 327 | _sessionAgent.kDidFinished = ^(NSError * error) { 328 | [weakself.fileHandleManager kj_finishWritting]; 329 | if (weakself.canSaveToCache){ 330 | [weakself.fileHandleManager kj_writeSave]; 331 | } 332 | if (weakself.fileHandleManager.cacheInfo.progress >= 1.0) { 333 | if ([weakself.delegate respondsToSelector:@selector(kj_didFinishWithError:)]){ 334 | NSError * error = [NSError errorWithDomain:@"cache complete" 335 | code:KJDownloaderFailedCodeCachedSuccessful 336 | userInfo:nil]; 337 | [weakself.delegate kj_didFinishWithError:error]; 338 | } 339 | return; 340 | } 341 | if (error){ 342 | if ([weakself.delegate respondsToSelector:@selector(kj_didFinishWithError:)]){ 343 | [weakself.delegate kj_didFinishWithError:error]; 344 | } 345 | } else { 346 | [weakself kj_downlingFragment]; 347 | } 348 | }; 349 | } 350 | return _sessionAgent; 351 | } 352 | 353 | @end 354 | 355 | #pragma mark - NSURLSession的代理人 356 | @interface KJSessionAgent () 357 | /// 设置一个NSMutableData类型的对象, 用于接收返回的数据 358 | @property (nonatomic,retain) NSMutableData *bufferData; 359 | 360 | @end 361 | 362 | @implementation KJSessionAgent 363 | 364 | #pragma mark - NSURLSessionDataDelegate 365 | 366 | - (void)URLSession:(NSURLSession *)session 367 | didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge 368 | completionHandler:(void(^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler{ 369 | NSURLCredential *card = [[NSURLCredential alloc] initWithTrust:challenge.protectionSpace.serverTrust]; 370 | completionHandler(NSURLSessionAuthChallengeUseCredential, card); 371 | } 372 | - (void)URLSession:(NSURLSession *)session 373 | dataTask:(NSURLSessionDataTask *)dataTask 374 | didReceiveResponse:(NSURLResponse *)response 375 | completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler{ 376 | self.bufferData = [NSMutableData data]; 377 | if (self.kDidReceiveResponse) { 378 | self.kDidReceiveResponse(response,completionHandler); 379 | } 380 | } 381 | - (void)URLSession:(NSURLSession *)session 382 | dataTask:(NSURLSessionDataTask *)dataTask 383 | didReceiveData:(NSData *)data{ 384 | @synchronized (self.bufferData){ 385 | [self.bufferData appendData:data]; 386 | if (self.bufferData.length >= 10 * 1024){// 10kb丢出去,开始播放 387 | NSRange chunkRange = NSMakeRange(0, self.bufferData.length); 388 | NSData *chunkData = [self.bufferData subdataWithRange:chunkRange]; 389 | [self.bufferData replaceBytesInRange:chunkRange withBytes:NULL length:0]; 390 | if (self.kDidReceiveData) { 391 | self.kDidReceiveData(chunkData); 392 | } 393 | } 394 | } 395 | } 396 | - (void)URLSession:(NSURLSession *)session 397 | task:(NSURLSessionDataTask *)task 398 | didCompleteWithError:(nullable NSError *)error{ 399 | @synchronized (self.bufferData){ 400 | if (self.bufferData.length > 0 && error == nil){ 401 | NSRange chunkRange = NSMakeRange(0, self.bufferData.length); 402 | NSData *chunkData = [self.bufferData subdataWithRange:chunkRange]; 403 | [self.bufferData replaceBytesInRange:chunkRange withBytes:NULL length:0]; 404 | if (self.kDidReceiveData) { 405 | self.kDidReceiveData(chunkData); 406 | } 407 | } 408 | } 409 | if (self.kDidFinished) { 410 | self.kDidFinished(error); 411 | } 412 | } 413 | 414 | @end 415 | -------------------------------------------------------------------------------- /Sources/Core/Downloader/KJDownloaderCommon.h: -------------------------------------------------------------------------------- 1 | // 2 | // KJDownloaderCommon.h 3 | // KJPlayer 4 | // 5 | // Created by 77。 on 2021/8/18. 6 | // https://github.com/yangKJ/KJPlayerDemo 7 | 8 | #import 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | /// 下载模块错误情况 14 | typedef NS_ENUM(NSInteger, KJDownloaderFailedCode) { 15 | /// 缓存成功 16 | KJDownloaderFailedCodeCachedSuccessful, 17 | /// 读取缓存文件失败 18 | KJDownloaderFailedCodeReadCachedDataFailed, 19 | /// 写入缓存文件失败 20 | KJDownloaderFailedCodeWriteFileFailed, 21 | }; 22 | /// 缓存相关信息通知 23 | UIKIT_EXTERN NSNotificationName const kPlayerFileHandleInfoNotification; 24 | /// 缓存相关信息接收key 25 | UIKIT_EXTERN NSNotificationName const kPlayerFileHandleInfoKey; 26 | 27 | // 临时路径名称 28 | #define PLAYER_TEMP_READ_NAME @"player.temp.read" 29 | // 缓存路径 30 | #define PLAYER_CACHE_PATH NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject 31 | #define PLAYER_CACHE_VIDEO_DIRECTORY [PLAYER_CACHE_PATH stringByAppendingPathComponent:@"videos"] 32 | 33 | /// 下载器公共配置信息 34 | @interface KJDownloaderCommon : NSObject 35 | 36 | /// 单例属性 37 | @property (nonatomic,strong,class,readonly,getter=kj_sharedInstance) KJDownloaderCommon *shared; 38 | 39 | /// 正在下载的请求 40 | @property (nonatomic,strong,readonly) NSMutableSet * downloadings; 41 | 42 | #pragma mark - 下载地址管理 43 | 44 | /// 新增网址 45 | - (void)kj_addDownloadURL:(NSURL *)url; 46 | /// 移出网址 47 | - (void)kj_removeDownloadURL:(NSURL *)url; 48 | /// 是否包含网址 49 | - (BOOL)kj_containsDownloadURL:(NSURL *)url; 50 | 51 | extern NSString * kMD5FileNameFormVideoURL(NSURL * videoURL); 52 | 53 | @end 54 | 55 | NS_ASSUME_NONNULL_END 56 | -------------------------------------------------------------------------------- /Sources/Core/Downloader/KJDownloaderCommon.m: -------------------------------------------------------------------------------- 1 | // 2 | // KJDownloaderCommon.m 3 | // KJPlayer 4 | // 5 | // Created by 77。 on 2021/8/18. 6 | // https://github.com/yangKJ/KJPlayerDemo 7 | 8 | #import "KJDownloaderCommon.h" 9 | #import 10 | 11 | /// 缓存相关信息通知 12 | NSNotificationName const kPlayerFileHandleInfoNotification = @"kHomeRefreshRecentlyUseNotification"; 13 | /// 缓存相关信息接收key 14 | NSNotificationName const kPlayerFileHandleInfoKey = @"kHomeAddRecentlyUseKey"; 15 | 16 | @interface KJDownloaderCommon () 17 | /// 正在下载的链接 18 | @property(nonatomic,strong) NSMutableSet *downloadings; 19 | 20 | @end 21 | 22 | @implementation KJDownloaderCommon 23 | 24 | static KJDownloaderCommon *_instance = nil; 25 | static dispatch_once_t onceToken; 26 | + (instancetype)kj_sharedInstance{ 27 | dispatch_once(&onceToken, ^{ 28 | if (_instance == nil) { 29 | _instance = [[self alloc] init]; 30 | } 31 | }); 32 | return _instance; 33 | } 34 | - (NSMutableSet *)downloadings{ 35 | if (!_downloadings) { 36 | _downloadings = [NSMutableSet set]; 37 | } 38 | return _downloadings; 39 | } 40 | 41 | #pragma mark - 下载地址管理 42 | 43 | - (void)kj_addDownloadURL:(NSURL*)url{ 44 | @synchronized (self.downloadings) { 45 | [self.downloadings addObject:url]; 46 | } 47 | } 48 | - (void)kj_removeDownloadURL:(NSURL*)url{ 49 | @synchronized (self.downloadings) { 50 | [self.downloadings removeObject:url]; 51 | } 52 | } 53 | - (BOOL)kj_containsDownloadURL:(NSURL*)url{ 54 | @synchronized (self.downloadings) { 55 | return [self.downloadings containsObject:url]; 56 | } 57 | } 58 | 59 | NSString * kMD5FileNameFormVideoURL(NSURL * videoURL) { 60 | NSString * string = videoURL.resourceSpecifier ?: videoURL.absoluteString; 61 | const char * cstr = [string cStringUsingEncoding:NSUTF8StringEncoding]; 62 | NSData * data = [NSData dataWithBytes:cstr length:string.length]; 63 | uint8_t digest[CC_SHA512_DIGEST_LENGTH]; 64 | CC_SHA512(data.bytes, (CC_LONG)data.length, digest); 65 | NSMutableString * output = [NSMutableString stringWithCapacity:CC_SHA512_DIGEST_LENGTH * 2]; 66 | for (int i = 0; i < CC_SHA512_DIGEST_LENGTH; i++) 67 | [output appendFormat:@"%02x", digest[i]]; 68 | return [NSString stringWithString:output]; 69 | } 70 | 71 | @end 72 | -------------------------------------------------------------------------------- /Sources/Core/Downloader/KJFileHandleInfo.h: -------------------------------------------------------------------------------- 1 | // 2 | // KJFileHandleInfo.h 3 | // KJPlayerDemo 4 | // 5 | // Created by 杨科军 on 2021/2/10. 6 | // Copyright © 2021 杨科军. All rights reserved. 7 | // https://github.com/yangKJ/KJPlayerDemo 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | /// 告诉编译器保存当前的对齐方式,并将对齐方式设置为1字节 14 | #pragma pack(push, 1) 15 | /// 缓存碎片结构体 16 | struct KJCacheFragment { 17 | NSInteger type;/// 0:本地碎片,1:远端碎片 18 | NSRange range;/// 位置长度 19 | }; 20 | typedef struct KJCacheFragment KJCacheFragment; 21 | /// 告诉编译器恢复保存的对齐方式 22 | #pragma pack(pop) 23 | 24 | /// 缓存相关信息资源 25 | @interface KJFileHandleInfo : NSObject 26 | /// 链接地址 27 | @property (nonatomic,strong,readonly) NSURL *videoURL; 28 | /// 文件名,视频链接去掉SCHEME然后MD5 29 | @property (nonatomic,strong,readonly) NSString *fileName; 30 | /// 文件信息 31 | @property (nonatomic,strong,readonly) NSString *fileFormat; 32 | /// 已缓存分片 33 | @property (nonatomic,strong,readonly) NSArray *cacheFragments; 34 | /// 已下载长度 35 | @property (nonatomic,assign,readonly) int64_t downloadedBytes; 36 | /// 下载进度 37 | @property (nonatomic,assign,readonly) float progress; 38 | /// 下载耗时 39 | @property (nonatomic,assign) NSTimeInterval downloadTime; 40 | /// 文件类型 41 | @property (nonatomic,strong) NSString *contentType; 42 | /// 文件大小总长度 43 | @property (nonatomic,assign) NSUInteger contentLength; 44 | 45 | /// 初始化,优先读取归档数据 46 | + (instancetype)kj_createFileHandleInfoWithURL:(NSURL *)url; 47 | 48 | /// 归档存储 49 | - (void)kj_keyedArchiverSave; 50 | 51 | /// 继续写入碎片 52 | - (void)kj_continueCacheFragmentRange:(NSRange)range; 53 | 54 | #pragma mark - 结构体相关 55 | 56 | /// 缓存碎片结构体转对象 57 | + (NSValue *)kj_cacheFragment:(KJCacheFragment)fragment; 58 | /// 缓存碎片对象转结构体 59 | + (KJCacheFragment)kj_getCacheFragment:(id)obj; 60 | 61 | #pragma mark - NSFileManager 62 | 63 | /// 创建文件夹 64 | /// @param path 路径 65 | + (BOOL)kj_createFilePath:(NSString *)path; 66 | 67 | #pragma mark - Sandbox板块 68 | 69 | /// 创建视频缓存文件完整路径 70 | /// @param url 链接 71 | + (NSString *)kj_createVideoCachedPath:(NSURL *)url; 72 | 73 | /// 追加视频临时缓存路径,用于播放器读取 74 | /// @param url 链接 75 | + (NSString *)kj_appendingVideoTempPath:(NSURL *)url; 76 | 77 | @end 78 | 79 | NS_ASSUME_NONNULL_END 80 | -------------------------------------------------------------------------------- /Sources/Core/Downloader/KJFileHandleInfo.m: -------------------------------------------------------------------------------- 1 | // 2 | // KJFileHandleInfo.m 3 | // KJPlayerDemo 4 | // 5 | // Created by 杨科军 on 2021/2/10. 6 | // Copyright © 2021 杨科军. All rights reserved. 7 | // https://github.com/yangKJ/KJPlayerDemo 8 | 9 | #import "KJFileHandleInfo.h" 10 | #import 11 | #import "KJDownloaderCommon.h" 12 | 13 | #pragma clang diagnostic push 14 | #pragma clang diagnostic ignored"-Wdeprecated-declarations" 15 | 16 | @interface KJFileHandleInfo () 17 | @property (nonatomic,strong) NSArray *cacheFragments; 18 | @property (nonatomic,strong) NSString *fileName; 19 | @property (nonatomic,strong) NSString *fileFormat; 20 | @property (nonatomic,strong) NSURL *videoURL; 21 | 22 | @end 23 | 24 | @implementation KJFileHandleInfo 25 | 26 | #pragma mark - NSCopying 27 | 28 | - (id)copyWithZone:(nullable NSZone *)zone { 29 | KJFileHandleInfo *info = [[[self class] allocWithZone:zone] init]; 30 | unsigned int count = 0; 31 | Ivar *ivars = class_copyIvarList([self class], &count); 32 | for (int i = 0; i*temps = [NSMutableArray arrayWithArray:self.cacheFragments]; 105 | NSInteger count = temps.count; 106 | if (count == 0) { 107 | [temps addObject:[NSValue valueWithRange:range]]; 108 | } else { 109 | NSMutableIndexSet *indexSet = [NSMutableIndexSet indexSet]; 110 | [temps enumerateObjectsUsingBlock:^(NSValue *obj, NSUInteger idx, BOOL *stop) { 111 | NSRange ran = obj.rangeValue; 112 | if ((range.location + range.length) <= ran.location) { 113 | if (indexSet.count == 0) { 114 | [indexSet addIndex:idx]; 115 | } 116 | *stop = YES; 117 | } else if (range.location <= (ran.location + ran.length) && 118 | (range.location + range.length) > ran.location) { 119 | [indexSet addIndex:idx]; 120 | } else if (range.location >= ran.location + ran.length) { 121 | if (idx == count - 1) { 122 | [indexSet addIndex:idx]; 123 | } 124 | } 125 | }]; 126 | 127 | if (indexSet.count > 1) { 128 | NSRange firstRange = temps[indexSet.firstIndex].rangeValue; 129 | NSRange lastRange = temps[indexSet.lastIndex].rangeValue; 130 | NSInteger location = MIN(firstRange.location, range.location); 131 | NSInteger endOffset = MAX(lastRange.location + lastRange.length, range.location + range.length); 132 | NSRange combineRange = NSMakeRange(location, endOffset - location); 133 | [temps removeObjectsAtIndexes:indexSet]; 134 | [temps insertObject:[NSValue valueWithRange:combineRange] atIndex:indexSet.firstIndex]; 135 | } else if (indexSet.count == 1) { 136 | NSRange firstRange = temps[indexSet.firstIndex].rangeValue; 137 | NSRange expandFirstRange = NSMakeRange(firstRange.location, firstRange.length + 1); 138 | NSRange expandFragmentRange = NSMakeRange(range.location, range.length + 1); 139 | NSRange intersectionRange = NSIntersectionRange(expandFirstRange, expandFragmentRange); 140 | if (intersectionRange.length > 0) { 141 | NSInteger location = MIN(firstRange.location, range.location); 142 | NSInteger endOffset = MAX(firstRange.location + firstRange.length, range.location + range.length); 143 | NSRange combineRange = NSMakeRange(location, endOffset - location); 144 | [temps removeObjectAtIndex:indexSet.firstIndex]; 145 | [temps insertObject:[NSValue valueWithRange:combineRange] atIndex:indexSet.firstIndex]; 146 | } else { 147 | if (firstRange.location > range.location) { 148 | [temps insertObject:[NSValue valueWithRange:range] atIndex:indexSet.lastIndex]; 149 | } else { 150 | [temps insertObject:[NSValue valueWithRange:range] atIndex:indexSet.lastIndex+1]; 151 | } 152 | } 153 | } 154 | } 155 | self.cacheFragments = temps.mutableCopy; 156 | } 157 | } 158 | 159 | #pragma mark - 结构体相关 160 | 161 | /// 缓存碎片结构体转对象 162 | + (NSValue *)kj_cacheFragment:(KJCacheFragment)fragment{ 163 | return [NSValue valueWithBytes:&fragment objCType:@encode(struct KJCacheFragment)]; 164 | } 165 | /// 缓存碎片对象转结构体 166 | + (KJCacheFragment)kj_getCacheFragment:(id)obj{ 167 | KJCacheFragment fragment; 168 | [obj getValue:&fragment]; 169 | return fragment; 170 | } 171 | 172 | #pragma mark - NSFileManager 173 | 174 | /// 创建文件夹 175 | + (BOOL)kj_createFilePath:(NSString *)path{ 176 | NSError * error = nil; 177 | NSString *cacheFolder = [path stringByDeletingLastPathComponent]; 178 | if (![[NSFileManager defaultManager] fileExistsAtPath:cacheFolder]) { 179 | [[NSFileManager defaultManager] createDirectoryAtPath:cacheFolder 180 | withIntermediateDirectories:YES 181 | attributes:nil 182 | error:&error]; 183 | } else { 184 | return NO; 185 | } 186 | return error == nil ? YES : NO; 187 | } 188 | 189 | #pragma mark - Sandbox板块 190 | 191 | /// 创建缓存文件完整路径 192 | + (NSString *)kj_createVideoCachedPath:(NSURL *)url{ 193 | NSString *pathComponent = kMD5FileNameFormVideoURL(url); 194 | pathComponent = [pathComponent stringByAppendingPathExtension:url.pathExtension]; 195 | return [PLAYER_CACHE_VIDEO_DIRECTORY stringByAppendingPathComponent:pathComponent]; 196 | } 197 | /// 追加临时缓存路径,用于播放器读取 198 | + (NSString *)kj_appendingVideoTempPath:(NSURL *)url{ 199 | return [[self kj_createVideoCachedPath:url] stringByAppendingPathExtension:PLAYER_TEMP_READ_NAME]; 200 | } 201 | 202 | @end 203 | 204 | #pragma clang diagnostic pop 205 | -------------------------------------------------------------------------------- /Sources/Core/Downloader/KJFileHandleManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // KJFileHandleManager.h 3 | // KJPlayerDemo 4 | // 5 | // Created by 杨科军 on 2021/2/10. 6 | // Copyright © 2021 杨科军. All rights reserved. 7 | // https://github.com/yangKJ/KJPlayerDemo 8 | 9 | #import 10 | #import "KJFileHandleInfo.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | /// 写入和读取文件管理器 15 | @interface KJFileHandleManager : NSObject 16 | /// 缓存相关信息资源 17 | @property (nonatomic,strong,readonly) KJFileHandleInfo *cacheInfo; 18 | 19 | /// 初始化 20 | /// @param url 链接地址 21 | - (instancetype)initWithURL:(NSURL *)url; 22 | 23 | /// 设置需要写入的总长度 24 | /// @param contentLength 总长度 25 | - (void)kj_setWriteHandleContentLenght:(NSUInteger)contentLength; 26 | 27 | /// 获取指定区间已经缓存的碎片 28 | /// @param range 指定区间 29 | - (NSArray *)kj_getCachedFragmentsWithRange:(NSRange)range; 30 | 31 | /// 写入已下载分片数据 32 | /// @param data 写入数据 33 | /// @param range 指定区间 34 | - (NSError *)kj_writeCacheData:(NSData *)data range:(NSRange)range; 35 | 36 | /// 读取已下载分片缓存数据 37 | /// @param range 指定区间 38 | - (NSData *)kj_readCachedDataWithRange:(NSRange)range; 39 | 40 | /// 写入存储 41 | - (void)kj_writeSave; 42 | 43 | /// 开始写入 44 | - (void)kj_startWritting; 45 | 46 | /// 结束写入 47 | - (void)kj_finishWritting; 48 | 49 | @end 50 | 51 | NS_ASSUME_NONNULL_END 52 | -------------------------------------------------------------------------------- /Sources/Core/Downloader/KJFileHandleManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // KJFileHandleManager.m 3 | // KJPlayerDemo 4 | // 5 | // Created by 杨科军 on 2021/2/10. 6 | // Copyright © 2021 杨科军. All rights reserved. 7 | // https://github.com/yangKJ/KJPlayerDemo 8 | 9 | #import "KJFileHandleManager.h" 10 | #import "KJDownloaderCommon.h" 11 | 12 | @interface KJFileHandleManager () { 13 | NSInteger kPackageLength; 14 | } 15 | @property (nonatomic,strong) KJFileHandleInfo *cacheInfo; 16 | @property (nonatomic,strong) NSFileHandle *readHandle; 17 | @property (nonatomic,strong) NSFileHandle *writeHandle; 18 | @property (nonatomic,strong) NSDate *startDate; 19 | 20 | @end 21 | 22 | @implementation KJFileHandleManager 23 | 24 | - (void)dealloc{ 25 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 26 | [self kj_writeSave]; 27 | [_readHandle closeFile]; 28 | [_writeHandle closeFile]; 29 | } 30 | 31 | - (instancetype)initWithURL:(NSURL *)url{ 32 | if (self = [super init]){ 33 | kPackageLength = 204800; 34 | NSString * filePath = [KJFileHandleInfo kj_createVideoCachedPath:url]; 35 | [KJFileHandleInfo kj_createFilePath:filePath]; 36 | if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]){ 37 | [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil]; 38 | } 39 | NSURL * fileURL = [NSURL fileURLWithPath:filePath]; 40 | self.readHandle = [NSFileHandle fileHandleForReadingFromURL:fileURL error:nil]; 41 | self.writeHandle = [NSFileHandle fileHandleForWritingToURL:fileURL error:nil]; 42 | self.cacheInfo = [KJFileHandleInfo kj_createFileHandleInfoWithURL:url]; 43 | [[NSNotificationCenter defaultCenter] addObserver:self 44 | selector:@selector(applicationDidEnterBackground:) 45 | name:UIApplicationDidEnterBackgroundNotification 46 | object:nil]; 47 | } 48 | return self; 49 | } 50 | /// 进入后台 51 | - (void)applicationDidEnterBackground:(NSNotification *)notification{ 52 | [self kj_writeSave]; 53 | } 54 | /// 设置需要写入的总长度 55 | - (void)kj_setWriteHandleContentLenght:(NSUInteger)contentLength{ 56 | [self.writeHandle truncateFileAtOffset:contentLength]; 57 | [self.writeHandle synchronizeFile]; 58 | } 59 | /// 获取指定区间已经缓存的碎片 60 | - (NSArray *)kj_getCachedFragmentsWithRange:(NSRange)range{ 61 | if (range.location == NSNotFound) return [NSMutableArray array].mutableCopy; 62 | NSInteger endOffset = range.location + range.length; 63 | NSMutableArray * fragments = [NSMutableArray array]; 64 | [self.cacheInfo.cacheFragments enumerateObjectsUsingBlock:^(NSValue * obj, NSUInteger idx, BOOL * stop){ 65 | NSRange inRange = NSIntersectionRange(range, obj.rangeValue); 66 | if (inRange.length > 0){ 67 | NSInteger package = inRange.length / kPackageLength; 68 | for (NSInteger i = 0; i <= package; i++){ 69 | KJCacheFragment fragment; 70 | fragment.type = 0; 71 | NSInteger offset = inRange.location + i * kPackageLength; 72 | NSInteger maxLocation = inRange.location + inRange.length; 73 | NSInteger length = (offset + kPackageLength) > maxLocation ? (maxLocation - offset) : kPackageLength; 74 | fragment.range = NSMakeRange(offset, length); 75 | [fragments addObject:[KJFileHandleInfo kj_cacheFragment:fragment]]; 76 | } 77 | } else if (obj.rangeValue.location >= endOffset){ 78 | * stop = YES; 79 | } 80 | }]; 81 | if (fragments.count == 0) { 82 | [fragments addObject:[KJFileHandleInfo kj_cacheFragment:(KJCacheFragment){1, range}]]; 83 | } else {//远端服务器碎片 84 | NSMutableArray *remoteFragments = [NSMutableArray array]; 85 | [fragments enumerateObjectsUsingBlock:^(NSValue *obj, NSUInteger idx, BOOL *stop){ 86 | KJCacheFragment fragment = [KJFileHandleInfo kj_getCacheFragment:obj]; 87 | if (idx == 0){ 88 | if (range.location < fragment.range.location){ 89 | KJCacheFragment action = {1, NSMakeRange(range.location, fragment.range.location - range.location)}; 90 | [remoteFragments addObject:[KJFileHandleInfo kj_cacheFragment:action]]; 91 | } 92 | [remoteFragments addObject:[KJFileHandleInfo kj_cacheFragment:fragment]]; 93 | } else { 94 | KJCacheFragment lastFragment = [KJFileHandleInfo kj_getCacheFragment:remoteFragments.lastObject]; 95 | NSInteger lastOffset = lastFragment.range.location + lastFragment.range.length; 96 | if (fragment.range.location > lastOffset) { 97 | NSValue * value = [KJFileHandleInfo kj_cacheFragment:(KJCacheFragment){ 98 | 1, NSMakeRange(lastOffset, fragment.range.location - lastOffset) 99 | }]; 100 | [remoteFragments addObject:value]; 101 | } 102 | [remoteFragments addObject:[KJFileHandleInfo kj_cacheFragment:fragment]]; 103 | } 104 | if (idx == fragments.count - 1){ 105 | NSInteger localEndOffset = fragment.range.location + fragment.range.length; 106 | if (endOffset > localEndOffset) { 107 | NSValue * value = [KJFileHandleInfo kj_cacheFragment:(KJCacheFragment){ 108 | 1, NSMakeRange(localEndOffset, endOffset - localEndOffset) 109 | }]; 110 | [remoteFragments addObject:value]; 111 | } 112 | } 113 | }]; 114 | fragments = remoteFragments; 115 | } 116 | return [fragments copy]; 117 | } 118 | /// 写入已下载分片数据 119 | - (NSError *)kj_writeCacheData:(NSData *)data range:(NSRange)range{ 120 | @synchronized(self.writeHandle) { 121 | NSError * error; 122 | @try { 123 | [self.writeHandle seekToFileOffset:range.location]; 124 | [self.writeHandle writeData:data]; 125 | [self.cacheInfo kj_continueCacheFragmentRange:range]; 126 | } @catch (NSException *exception) { 127 | error = [NSError errorWithDomain:@"write file failed" 128 | code:KJDownloaderFailedCodeWriteFileFailed 129 | userInfo:@{NSLocalizedDescriptionKey: exception.name}]; 130 | } @finally { 131 | return error; 132 | } 133 | } 134 | } 135 | /// 读取已下载分片缓存数据 136 | - (NSData *)kj_readCachedDataWithRange:(NSRange)range{ 137 | @synchronized(self.readHandle) { 138 | [self.readHandle seekToFileOffset:range.location]; 139 | return [self.readHandle readDataOfLength:range.length]; 140 | } 141 | } 142 | /// 写入存储 143 | - (void)kj_writeSave{ 144 | @synchronized (self.writeHandle){ 145 | [self.writeHandle synchronizeFile]; 146 | [self.cacheInfo kj_keyedArchiverSave]; 147 | } 148 | } 149 | /// 开始写入 150 | - (void)kj_startWritting{ 151 | self.startDate = [NSDate date]; 152 | } 153 | /// 结束写入 154 | - (void)kj_finishWritting{ 155 | self.cacheInfo.downloadTime = [[NSDate date] timeIntervalSinceDate:self.startDate]; 156 | } 157 | 158 | @end 159 | -------------------------------------------------------------------------------- /Sources/Core/Downloader/KJPlayer-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import "KJDownloader.h" 6 | #import "KJFileHandleInfo.h" 7 | #import "KJDownloaderCommon.h" 8 | #import "KJFileHandleManager.h" 9 | -------------------------------------------------------------------------------- /Sources/Core/Extensions/Timer+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Timer+Extension.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/18. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 已解决循环引用的计时器 11 | /// Circular referenced timers resolved 12 | extension Timer { 13 | 14 | /// Circular referenced timers resolved 15 | /// 16 | /// Example: 17 | /// 18 | /// self.timer = Timer.kj_scheduledTimer(withTimeInterval: time ?? 1, 19 | /// repeats: true, 20 | /// block: { [weak self] (timer) in 21 | /// guard let `self` = self else { return } 22 | /// // do something... 23 | /// }) 24 | /// 25 | /// - Parameters: 26 | /// - interval: time interval 27 | /// - repeats: whether to repeat 28 | /// - block: callback response 29 | /// - Returns: timer 30 | @discardableResult 31 | public static func kj_scheduledTimer(withTimeInterval interval: TimeInterval, 32 | repeats: Bool, 33 | block: @escaping (_ timer: Timer) -> Void) -> Timer { 34 | if #available(iOS 10.0, *) { 35 | return Timer.scheduledTimer(withTimeInterval: interval, repeats: repeats, block: block) 36 | } 37 | return scheduledTimer(timeInterval: interval, 38 | target: self, 39 | selector: #selector(handerTimerAction(sender:)), 40 | userInfo: TimerBlock(block), 41 | repeats: repeats) 42 | } 43 | 44 | ///`[NSTimer handerTimerAction:]: unrecognized selector sent to class 0x1105c30c0' 45 | /// Timer is a class object, you can only call class methods, not instance methods 46 | @objc private static func handerTimerAction(sender: Timer) { 47 | if let block = sender.userInfo as? TimerBlock<(Timer) -> Void> { 48 | block.type(sender) 49 | } 50 | } 51 | 52 | private struct TimerBlock { 53 | let type: T 54 | init(_ type: T) { 55 | self.type = type 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/Core/Setup/Bridge.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bridge.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/15. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 桥接相关,主要连接各个功能点和内核之间的联系 11 | /// Bridging related, mainly connecting the connection between each function point and the kernel 12 | internal enum BridgeMethod { 13 | ///`Initialization player` 14 | case `init`(KJBasePlayer) 15 | ///`Player destruction` 16 | case `deinit`(KJBasePlayer) 17 | ///`Start preparing play` 18 | case begin(KJBasePlayer) 19 | ///`Play end` 20 | case end(KJBasePlayer) 21 | ///`Playing` 22 | case playing(KJBasePlayer, time: TimeInterval) 23 | ///`The player status has changed` 24 | case playState(KJBasePlayer, state: KJPlayerState) 25 | } 26 | 27 | extension BridgeMethod { 28 | 29 | @discardableResult 30 | internal func initialization() -> (isBackground: Bool, isPlaying: Bool)? { 31 | switch self { 32 | case .`init`: 33 | return (true, true) 34 | default: 35 | return nil 36 | } 37 | } 38 | } 39 | 40 | extension BridgeMethod { 41 | 42 | internal func preparing() -> TimeInterval { 43 | var time = 0.0 44 | switch self { 45 | case .begin(let player): 46 | time = self.recordTime(player) 47 | if time > 0 { break } 48 | time = BridgeMethod.skipTime(player) 49 | if time > 0 { break } 50 | default: 51 | break 52 | } 53 | return time 54 | } 55 | 56 | private func recordTime(_ player: KJBasePlayer) -> TimeInterval { 57 | return KJBasePlayer.RecordTime.lastPlayedTimeIMP(player) 58 | } 59 | 60 | internal static func skipTime(_ player: KJBasePlayer) -> TimeInterval { 61 | // TODO: - 跳过片头播放 62 | return 10.0 63 | } 64 | } 65 | 66 | extension BridgeMethod { 67 | 68 | internal var playing: Bool { 69 | switch self { 70 | case .playing(let player, let time): 71 | if self.uncontinueLook(player, time: time) { 72 | return true 73 | } 74 | return false 75 | default: 76 | return false 77 | } 78 | } 79 | 80 | /// Can't continue watching 81 | private func uncontinueLook(_ player: KJBasePlayer, time: TimeInterval) -> Bool { 82 | ///`1.Played to or more than reached time 83 | if KJBasePlayer.FreeTime.canContinueLook(player, time: time) { 84 | return true 85 | } 86 | return false 87 | } 88 | } 89 | 90 | extension BridgeMethod { 91 | 92 | internal func playFinished() { 93 | switch self { 94 | case .end(let player): 95 | KJBasePlayer.RecordTime.deletePlayedTimeIMP(player) 96 | break 97 | default: 98 | break 99 | } 100 | } 101 | } 102 | 103 | extension BridgeMethod { 104 | 105 | internal func dealloc() { 106 | switch self { 107 | case .`deinit`(let player): 108 | KJBasePlayer.RecordTime.recordPlayedTimeIMP(player) 109 | break 110 | default: 111 | break 112 | } 113 | } 114 | } 115 | 116 | extension BridgeMethod { 117 | 118 | internal static func freeLookEnded(_ player: KJBasePlayer) -> Bool { 119 | return KJBasePlayer.FreeTime.freeTimeEnded(player) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Sources/Core/Setup/Common.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Common.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/14. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import CommonCrypto 11 | 12 | internal struct Common { 13 | internal struct Constant { } 14 | internal struct View { } 15 | internal struct Crypto { } 16 | } 17 | 18 | extension Common.Constant { 19 | 20 | static let width = UIScreen.main.bounds.width 21 | static let height = UIScreen.main.bounds.height 22 | static let navigationHeight = barHeight + 44.0 23 | static let tabBarHeight = (barHeight == 44 ? 83 : 49) 24 | static let kTopSafeAreaHeight = (barHeight - 20) 25 | static let kBottomSafeAreaHeight = (tabBarHeight - 49) 26 | static let barHeight: CGFloat = { 27 | if #available(iOS 13.0, *) { 28 | return Common.View.keyWindow?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0 29 | } else { 30 | return UIApplication.shared.statusBarFrame.height 31 | } 32 | }() 33 | 34 | static let isIphoneX: Bool = { 35 | if UIDevice.current.userInterfaceIdiom == .pad { 36 | return false 37 | } 38 | let size = UIScreen.main.bounds.size 39 | let notchValue: Int = Int(size.width / size.height * 100) 40 | if notchValue == 216 || notchValue == 46 { 41 | return true 42 | } 43 | guard #available(iOS 11.0, *) else { return false } 44 | if let bottomHeight = Common.View.keyWindow?.safeAreaInsets.bottom { 45 | return bottomHeight > 30 46 | } 47 | return false 48 | }() 49 | } 50 | 51 | extension Common.View { 52 | 53 | static var keyWindow: UIWindow? { 54 | if #available(iOS 13.0, *) { 55 | return UIApplication.shared.connectedScenes 56 | .filter { $0.activationState == .foregroundActive } 57 | .first(where: { $0 is UIWindowScene }) 58 | .flatMap({ $0 as? UIWindowScene })?.windows 59 | .first(where: \.isKeyWindow) 60 | } else { 61 | return UIApplication.shared.keyWindow 62 | } 63 | } 64 | 65 | static var keyWindowPresentedController: UIViewController? { 66 | var viewController = Common.View.keyWindow?.rootViewController 67 | if let presentedController = viewController as? UITabBarController { 68 | viewController = presentedController.selectedViewController 69 | } 70 | while let presentedController = viewController?.presentedViewController { 71 | if let presentedController = presentedController as? UITabBarController { 72 | viewController = presentedController.selectedViewController 73 | } else { 74 | viewController = presentedController 75 | } 76 | } 77 | return viewController 78 | } 79 | } 80 | 81 | extension Common.Crypto { 82 | 83 | /// MD5 84 | static func MD5(_ string: String) -> String { 85 | let ccharArray = string.cString(using: String.Encoding.utf8) 86 | var uint8Array = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH)) 87 | CC_MD5(ccharArray, CC_LONG(ccharArray!.count - 1), &uint8Array) 88 | return uint8Array.reduce("") { $0 + String(format: "%02X", $1) } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/Core/Setup/Enum.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Enum.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/14. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | @objc public enum KJPlayerState : Int { 12 | case playing 13 | case paused 14 | 15 | public var mapString: String { 16 | switch self { 17 | case .playing: 18 | return "playing" 19 | case .paused: 20 | return "paused" 21 | @unknown default: 22 | return "" 23 | } 24 | } 25 | 26 | var controlPlayImage: UIImage? { 27 | switch self { 28 | case .playing: 29 | return UIImage(named: "") 30 | case .paused: 31 | return UIImage(named: "") 32 | default: 33 | return nil 34 | } 35 | } 36 | } 37 | 38 | /// Player video full type 39 | @objc public enum KJPlayerVideoGravity : Int { 40 | /// 最大边等比充满,按比例压缩 41 | /// The largest side is filled proportionally and compressed proportionally 42 | case resizeAspect = 0 43 | /// 原始尺寸,视频不会有黑边 44 | /// Original size, the video will not have black borders 45 | case resizeAspectFill 46 | /// 拉伸充满,视频会变形 47 | /// Stretched to full, the video will be distorted 48 | case resizeOriginal 49 | } 50 | 51 | // MARK: - Internal use of the project 52 | internal enum PlayerAsset: Int { 53 | case NONE, FILE, HLS 54 | } 55 | 56 | internal enum PlayerFailed { 57 | case knownFailed(_ error: NSError) 58 | case customFailed(_ code: Int, message: String = "", userInfo: [String: Any]? = nil) 59 | 60 | var playerFailed: NSError { 61 | switch self { 62 | case .knownFailed(let error): 63 | return error 64 | case .customFailed(let code, let message, let userInfo): 65 | if message == "" { 66 | return NSError.init(domain: "player.domain", code: code, userInfo: userInfo) 67 | } 68 | return NSError.init(domain: message, code: code, userInfo: userInfo) 69 | } 70 | } 71 | } 72 | 73 | internal enum PlayerStatus { 74 | /// Start preparing to play 75 | case prepare(provider: Provider) 76 | /// Whether to enable playback for the user 77 | case beginPlay 78 | /// Is it playing 79 | case playing(time: TimeInterval) 80 | /// Whether the user actively chooses to pause 81 | case paused(user: Bool) 82 | /// Whether the playback is complete, 83 | /// the video will respond to the end of the complete play and skip end play 84 | case playFinished(skip: Bool) 85 | /// Whether the playback is wrong 86 | case failed(error: NSError?) 87 | } 88 | -------------------------------------------------------------------------------- /Sources/Core/Setup/Function.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Function.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/19. 6 | // 7 | 8 | import Foundation 9 | import ObjectiveC 10 | 11 | extension Common { 12 | internal struct Function { } 13 | } 14 | 15 | extension Common.Function { 16 | 17 | /// Name 18 | /// - Parameter url: Link 19 | /// - Returns: Video link remove SCHEME 20 | static func intactName(_ url: NSURL) -> String { 21 | if let resourceSpecifier = url.resourceSpecifier { 22 | return resourceSpecifier 23 | } 24 | if let absoluteString = url.absoluteString { 25 | return absoluteString 26 | } 27 | return "" 28 | } 29 | 30 | /// Video Aesset type 31 | static func videoAesset(_ url: NSURL?) -> PlayerAsset { 32 | guard let videoURL = url else { return .NONE } 33 | if let pathExtension = videoURL.pathExtension { 34 | if pathExtension.contains("m3u8") || pathExtension.contains("ts") { 35 | return .HLS 36 | } else { 37 | return .FILE 38 | } 39 | } 40 | let array = videoURL.path?.components(separatedBy: ".") 41 | if array?.count == 0 { 42 | return .NONE 43 | } 44 | if let last = array?.last, (last.contains("m3u8") || last.contains("ts")) { 45 | return .HLS 46 | } 47 | return .FILE 48 | } 49 | 50 | /// Convert seconds to display time string 51 | static func timeConvert(_ time: TimeInterval) -> String { 52 | let dateFormatter = DateFormatter() 53 | dateFormatter.timeZone = NSTimeZone(name: "GMT") as TimeZone? 54 | if time / 3600 >= 1 { 55 | dateFormatter.dateFormat = "HH:mm:ss" 56 | } else { 57 | dateFormatter.dateFormat = "mm:ss" 58 | } 59 | return dateFormatter.string(from: Date(timeIntervalSince1970: time)) 60 | } 61 | 62 | /// Determine whether it is a network resource 63 | static func isOnlineResource(_ urlString: String) -> Bool { 64 | return urlString.starts(with: "http") 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/Core/Setup/Notification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notification.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/15. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension KJBasePlayer { 12 | internal struct kNotification { 13 | /// Notification of changes in the position and size of the control 14 | static let playViewRectName = Notification.Name(rawValue: "kPlayerViewRectNotification") 15 | /// Size information change key 16 | static let playViewRectKey = "kPlayerViewRectKey" 17 | /// The timer will keep running 18 | static let timerKey = NSNotification.Name(rawValue: "kPlayerRuningCommonTimerKey") 19 | } 20 | 21 | /// Add notification observer 22 | internal func setupNotification() { 23 | NotificationCenter.default.addObserver(self, 24 | selector: #selector(playerViewRectChanged(_:)), 25 | name: KJBasePlayer.kNotification.playViewRectName, 26 | object: nil) 27 | } 28 | 29 | /// The position and size of the control changes 30 | /// - Parameter notification: Notification message 31 | @objc func playerViewRectChanged(_ notification: Notification) { 32 | guard let userInfo = notification.userInfo, 33 | let _ = userInfo[kNotification.playViewRectKey] as? CGRect else { 34 | return 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Core/Setup/Protocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Protocol.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/14. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | @objc public protocol KJPlayerDelegate: NSObjectProtocol { 12 | 13 | /// Current player status 14 | @objc(kj_player:state:) 15 | optional func kj_player(_ player: KJBasePlayer, state: KJPlayerState) 16 | 17 | /// Player play failed 18 | @objc(kj_player:playFailed:) 19 | optional func kj_player(_ player: KJBasePlayer, playFailed: NSError) 20 | 21 | /// Video loaded time 22 | @objc(kj_player:loadedTime:) 23 | optional func kj_player(_ player: KJBasePlayer, loadedTime: TimeInterval) 24 | 25 | @objc(kj_player:currentTime:) 26 | optional func kj_player(_ player: KJBasePlayer, current: TimeInterval) 27 | 28 | @objc(kj_player:videoTime:) 29 | optional func kj_player(_ player: KJBasePlayer, total: TimeInterval) 30 | 31 | @objc(kj_player:videoSize:) 32 | optional func kj_player(_ player: KJBasePlayer, videoSize: CGSize) 33 | 34 | /// At the end of the playback, the video will respond to the end of the complete play and skip end play 35 | /// 播放结束,音视频`自然完整播放`和`跳过片尾播放结束`均会响应 36 | @objc(kj_player:playFinished:) 37 | optional func kj_player(_ player: KJBasePlayer, playFinished: TimeInterval) 38 | 39 | @objc(kj_player:stopped:) 40 | optional func kj_player(_ player: KJBasePlayer, stopped: TimeInterval) 41 | } 42 | 43 | @objc public protocol KJPlayerBaseViewDelegate: NSObjectProtocol { 44 | 45 | /// Single tap gesture feedback 46 | @objc(kj_basePlayerView:singleTap:) 47 | optional func kj_basePlayerView(_ view: KJPlayerView, singleTap: CGPoint) 48 | 49 | /// Double tap gesture feedback 50 | @objc(kj_basePlayerView:doubleTap:) 51 | optional func kj_basePlayerView(_ view: KJPlayerView, doubleTap: Bool) 52 | 53 | /// Long press gesture feedback 54 | @objc(kj_basePlayerView:longPress:) 55 | optional func kj_basePlayerView(_ view: KJPlayerView, longPress: UILongPressGestureRecognizer) 56 | 57 | /// Volume gesture feedback 58 | @objc(kj_basePlayerView:volumeValue:) 59 | optional func kj_basePlayerView(_ view: KJPlayerView, volumeValue: Float) -> Bool 60 | 61 | /// Brightness gesture feedback 62 | @objc(kj_basePlayerView:brightnessValue:) 63 | optional func kj_basePlayerView(_ view: KJPlayerView, brightnessValue: Float) -> Bool 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Core/Setup/Provider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Provider.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/16. 6 | // 7 | 8 | import Foundation 9 | 10 | @objc public class Provider: NSObject { 11 | 12 | /// Video link 13 | internal var videoURL: String? = "" 14 | /// Video request header 15 | internal var requestHeader: [String: Any]? = nil 16 | 17 | /// Audio link 18 | internal var audioURL: String? = "" 19 | 20 | @objc var title: String = "" 21 | /// Time space 22 | @objc var timeSpace: Double = 1.0 23 | /// How many seconds can the buffer be played before it can be played? 24 | /// 缓冲达到该目标之后才能自动播放 25 | @objc var cacheTime: Double = 0.0 26 | /// Whether to enable only fast forward to the cached position 27 | /// 是否开启只能快进到缓冲位置 28 | @objc var openAdvanceCache: Bool = false 29 | 30 | private override init() { } 31 | 32 | @objc public init(videoURL: String?, requestHeader: [String: Any]? = nil) { 33 | self.videoURL = videoURL 34 | self.requestHeader = requestHeader 35 | } 36 | 37 | @objc public init(audioURL: String?) { 38 | self.audioURL = audioURL 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Core/Setup/Screenshots.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Screenshots.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/18. 6 | // 7 | 8 | import Foundation 9 | import AVFoundation 10 | 11 | @objc public final class Screenshots: NSObject { 12 | 13 | @objc public static func screenshots(player: KJBasePlayer, time: TimeInterval) -> UIImage { 14 | return screenshots(player, time: time) ?? UIImage() 15 | } 16 | } 17 | 18 | extension Screenshots { 19 | 20 | public static func screenshots(_ player: KJBasePlayer, time: TimeInterval) -> UIImage? { 21 | if let player = player as? KJAVPlayer, let asset = player.playerItem?.asset { 22 | let generator = AVAssetImageGenerator(asset: asset) 23 | generator.appliesPreferredTrackTransform = true 24 | generator.requestedTimeToleranceAfter = CMTime.zero 25 | generator.requestedTimeToleranceBefore = CMTime.zero 26 | let _time = CMTimeMakeWithSeconds(time, preferredTimescale: Int32(NSEC_PER_SEC)) 27 | var actualTime = CMTimeMake(value: 0, timescale: 0) 28 | guard let imageRef = try? generator.copyCGImage(at: _time, actualTime: &actualTime) else { 29 | return nil 30 | } 31 | return UIImage(cgImage: imageRef) 32 | } 33 | return nil 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Core/Setup/Shared.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Shared.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/15. 6 | // 7 | 8 | import Foundation 9 | import ObjectiveC 10 | 11 | fileprivate var SharedContext: UInt8 = 0 12 | 13 | public protocol SharedInstance: AnyObject { 14 | associatedtype Player 15 | static var shared: Player { get set } 16 | } 17 | 18 | extension SharedInstance where Player: KJBasePlayer { 19 | 20 | public static var shared: Player { 21 | get { 22 | return synchronizedSharedInstance { 23 | if let player = objc_getAssociatedObject(self, &SharedContext) as? Player { 24 | return player 25 | } 26 | let player = Player.init() 27 | objc_setAssociatedObject(self, &SharedContext, player, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 28 | return player 29 | } 30 | } 31 | set { 32 | synchronizedSharedInstance { 33 | objc_setAssociatedObject(self, &SharedContext, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 34 | } 35 | } 36 | } 37 | 38 | public static func deinitShared() { 39 | synchronizedSharedInstance { 40 | objc_setAssociatedObject(self, &SharedContext, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 41 | } 42 | } 43 | 44 | internal static func synchronizedSharedInstance( _ action: () -> T) -> T { 45 | objc_sync_enter(self) 46 | let result = action() 47 | objc_sync_exit(self) 48 | return result 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Core/Setup/Timer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Timer.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/18. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | /// 该计时器会一直跑起来,需要使用的地方自行借取 12 | /// The timer will keep running and borrow it from the place where it needs to be used 13 | extension KJBasePlayer { 14 | private struct Keys { 15 | static var timer = "timerKey" 16 | } 17 | 18 | private var timer: Timer? { 19 | get { objc_getAssociatedObject(self, &Keys.timer) as? Timer } 20 | set { objc_setAssociatedObject(self, &Keys.timer, newValue, .OBJC_ASSOCIATION_RETAIN) } 21 | } 22 | 23 | /// Initialize the timer 24 | /// - Parameter time: time interval, default `1` 25 | internal func setupTimer(_ time: TimeInterval?) { 26 | self.timer?.invalidate() 27 | self.timer = Timer.kj_scheduledTimer(withTimeInterval: time ?? 1, 28 | repeats: true, 29 | block: { [weak self] (timer) in 30 | guard let `self` = self else { return } 31 | self.runingCommonTimer(sender: timer) 32 | }) 33 | RunLoop.current.add(self.timer!, forMode: .common) 34 | } 35 | 36 | internal func deinitTimer() { 37 | if let _ = self.timer { 38 | self.timer?.invalidate() 39 | self.timer = nil 40 | } 41 | } 42 | 43 | /// Modify the time interval 44 | /// - Parameter time: time interval 45 | internal func changeTimeInterval(_ time: TimeInterval) { 46 | self.deinitTimer() 47 | self.setupTimer(time) 48 | } 49 | 50 | @objc internal func runingCommonTimer(sender: Timer?) { 51 | NotificationCenter.default.post(name: KJBasePlayer.kNotification.timerKey, object: self) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Core/View/KJPlayerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KJPlayerView.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/14. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | @objc @IBDesignable public class KJPlayerView: UIImageView { 12 | 13 | @IBOutlet weak var delegate: KJPlayerBaseViewDelegate? 14 | 15 | /// Placeholder image 16 | @IBOutlet public var placeholder: UIImage? = nil 17 | /// background color 18 | @IBOutlet public var background: CGColor! = UIColor.black.cgColor 19 | /// Video display mode 20 | public var videoGravity: KJPlayerVideoGravity = .resizeAspect 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Kernel /AVPlayer/KJAVPlayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KJAVPlayer.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/14. 6 | // 7 | 8 | import Foundation 9 | import AVFoundation 10 | 11 | @objc(KJAVPlayer) 12 | public class KJAVPlayer: KJBasePlayer, SharedInstance { 13 | private var timeObserver: Any? = nil 14 | 15 | public typealias Player = KJAVPlayer 16 | 17 | @objc public private(set) var videoPlayer: AVPlayer? { 18 | didSet { 19 | if let videoPlayer = videoPlayer { 20 | videoPlayer.usesExternalPlaybackWhileExternalScreenIsActive = true 21 | } 22 | } 23 | } 24 | 25 | @objc public private(set) var playerLayer: AVPlayerLayer? { 26 | didSet { 27 | guard let playerLayer = playerLayer, let view = playerView else { 28 | return 29 | } 30 | DispatchQueue.main.async { 31 | playerLayer.frame = self.getVideoFrame() 32 | playerLayer.backgroundColor = view.background 33 | switch view.videoGravity { 34 | case .resizeAspect: 35 | playerLayer.videoGravity = .resizeAspect 36 | break 37 | case .resizeAspectFill: 38 | playerLayer.videoGravity = .resizeAspectFill 39 | break 40 | case .resizeOriginal: 41 | playerLayer.videoGravity = .resize 42 | break 43 | } 44 | if playerLayer.superlayer == nil { 45 | view.layer.addSublayer(playerLayer) 46 | } 47 | } 48 | } 49 | } 50 | 51 | /// Media Resource Management Object 52 | @objc public private(set) var playerItem: AVPlayerItem? { 53 | didSet { 54 | if let item = playerItem { 55 | item.addObserver(self, forKeyPath: KJAVPlayer.Keys.status, options: .new, context: nil) 56 | item.addObserver(self, forKeyPath: KJAVPlayer.Keys.videoSize, options: .new, context: nil) 57 | item.addObserver(self, forKeyPath: KJAVPlayer.Keys.loadedTime, options: .new, context: nil) 58 | NotificationCenter.default.addObserver(self, 59 | selector: #selector(avplayerItemDidPlayToEndTime(_:)), 60 | name: .AVPlayerItemDidPlayToEndTime, 61 | object: item) 62 | } 63 | } 64 | } 65 | 66 | deinit { 67 | self.resetVideoPlayer() 68 | } 69 | 70 | /// Video play finished 71 | @objc func avplayerItemDidPlayToEndTime(_ notification: Notification) { 72 | self.playerStatus = .playFinished(skip: false) 73 | } 74 | 75 | // MARK: - override method 76 | @objc public override var muted: Bool { 77 | didSet { 78 | if let videoPlayer = videoPlayer { 79 | videoPlayer.isMuted = muted 80 | } 81 | } 82 | } 83 | 84 | @objc public override var speed: Float { 85 | didSet { 86 | guard let videoPlayer = videoPlayer, fabsf(videoPlayer.rate) > 0.00001 else { 87 | return 88 | } 89 | let speed = max(min(speed, 2), 0) 90 | videoPlayer.rate = speed 91 | } 92 | } 93 | 94 | @objc public override var volume: Float { 95 | didSet { 96 | guard let videoPlayer = videoPlayer else { 97 | return 98 | } 99 | let volume = max(min(volume, 1), 0) 100 | videoPlayer.volume = volume 101 | } 102 | } 103 | 104 | @objc public override var provider: Provider? { 105 | didSet { 106 | guard let provider = provider else { 107 | return 108 | } 109 | self.playerPreparing(provider) 110 | } 111 | } 112 | 113 | override var playerStatus: PlayerStatus? { 114 | didSet { 115 | guard let status = playerStatus else { return } 116 | super.playerStatus = status 117 | switch status { 118 | case .prepare(_): 119 | videoPlayer?.pause() 120 | break 121 | case .beginPlay: 122 | self.beginPlay() 123 | break 124 | case .playing(let time): 125 | self.playing(time: time) 126 | break 127 | case .paused(let user): 128 | self.pausedPlay(user: user) 129 | break 130 | default: break 131 | } 132 | } 133 | } 134 | 135 | override func runingCommonTimer(sender: Timer?) { 136 | super.runingCommonTimer(sender: sender) 137 | guard let player = videoPlayer, 138 | BridgeMethod.playing(self, time: self.currentTime).playing else { 139 | return 140 | } 141 | if isPlaying == false, autoPlay { 142 | self.configPlayer(player) 143 | } 144 | } 145 | } 146 | 147 | // MARK: - kvo 148 | extension KJAVPlayer { 149 | private struct Keys { 150 | static let status = "status" 151 | static let videoSize = "presentationSize" 152 | static let loadedTime = "loadedTimeRanges" 153 | } 154 | 155 | public override func observeValue(forKeyPath keyPath: String?, 156 | of object: Any?, 157 | change: [NSKeyValueChangeKey : Any]?, 158 | context: UnsafeMutableRawPointer?) { 159 | guard let item = self.playerItem else { 160 | super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) 161 | return 162 | } 163 | // Monitor player status 164 | if keyPath == KJAVPlayer.Keys.status { 165 | if item.status == .readyToPlay { 166 | self.totalTimeObserve = CMTimeGetSeconds(item.duration) 167 | self.playerStatus = .beginPlay 168 | } else { 169 | self.playerStatus = .failed(error: item.error as NSError?) 170 | } 171 | } 172 | // Monitor video size 173 | else if keyPath == KJAVPlayer.Keys.videoSize { 174 | self.videoSizeObserve = item.presentationSize 175 | } 176 | // Monitor video loaded time 177 | else if keyPath == KJAVPlayer.Keys.loadedTime { 178 | guard let ranges = item.loadedTimeRanges.first?.timeRangeValue else { 179 | return 180 | } 181 | let start = CMTimeGetSeconds(ranges.start) 182 | let duration = CMTimeGetSeconds(ranges.duration) 183 | self.loadedTimeObserve = start + duration 184 | } 185 | } 186 | } 187 | 188 | // MARK: - Private player methods 189 | extension KJAVPlayer { 190 | 191 | private func resetVideoPlayer() { 192 | videoPlayer?.pause() 193 | videoPlayer?.replaceCurrentItem(with: nil) 194 | if let item = playerItem { 195 | item.removeObserver(self, forKeyPath: KJAVPlayer.Keys.status, context: nil) 196 | item.removeObserver(self, forKeyPath: KJAVPlayer.Keys.loadedTime, context: nil) 197 | item.removeObserver(self, forKeyPath: KJAVPlayer.Keys.videoSize, context: nil) 198 | NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: item) 199 | playerItem = nil 200 | } 201 | if videoPlayer != nil && timeObserver != nil { 202 | videoPlayer!.removeTimeObserver(timeObserver!) 203 | timeObserver = nil 204 | } 205 | } 206 | 207 | private func playerPreparing(_ provider: Provider) { 208 | self.playerStatus = .prepare(provider: provider) 209 | self.setupVideoPlayer(self.playURL) 210 | let time = BridgeMethod.begin(self).preparing() 211 | if time > 0 { 212 | self.seekTime(time) 213 | } 214 | } 215 | 216 | private func setupVideoPlayer(_ videoURL: NSURL?) { 217 | guard let videoURL = videoURL, let provider = provider else { return } 218 | let asset = AVURLAsset.init(url: videoURL as URL, options: provider.requestHeader) 219 | self.playerItem = AVPlayerItem.init(asset: asset) 220 | //self.playerItem = AVPlayerItem.init(url: videoURL as URL) 221 | if videoPlayer == nil { 222 | self.videoPlayer = AVPlayer.init(playerItem: self.playerItem) 223 | } else { 224 | self.videoPlayer?.replaceCurrentItem(with: self.playerItem) 225 | } 226 | if let playerLayer = playerLayer { 227 | playerLayer.player = videoPlayer 228 | } else { 229 | playerLayer = AVPlayerLayer.init(player: videoPlayer) 230 | } 231 | self.setupTimeObserver() 232 | } 233 | 234 | /// Monitor play time change 235 | private func setupTimeObserver() { 236 | guard let videoPlayer = videoPlayer, let provider = provider else { 237 | return 238 | } 239 | if timeObserver != nil { 240 | videoPlayer.removeTimeObserver(timeObserver!) 241 | timeObserver = nil 242 | } 243 | let ctime = CMTime(seconds: provider.timeSpace, preferredTimescale: 1) 244 | timeObserver = videoPlayer.addPeriodicTimeObserver(forInterval: ctime, 245 | queue: DispatchQueue.main, 246 | using: { [weak self] (time) in 247 | guard let `self` = self, !self.isLiveStreaming else { return } 248 | let sec = CMTimeGetSeconds(time) 249 | self.playerStatus = .playing(time: sec) 250 | }) 251 | } 252 | 253 | private func configPlayer(_ player: AVPlayer) { 254 | player.play() 255 | player.volume = max(min(volume, 1), 0) 256 | player.rate = max(min(speed, 2), 0) 257 | player.isMuted = muted 258 | } 259 | 260 | private func beginPlay() { 261 | guard let player = videoPlayer else { return } 262 | let canPlay = !BridgeMethod.playing(self, time: self.currentTime).playing 263 | if canPlay { 264 | self.configPlayer(player) 265 | } else { 266 | player.pause() 267 | } 268 | } 269 | 270 | private func playing(time: TimeInterval) { 271 | if BridgeMethod.playing(self, time: time).playing { 272 | self.playerStatus = .paused(user: false) 273 | return 274 | } 275 | } 276 | 277 | private func pausedPlay(user: Bool) { 278 | guard let player = videoPlayer else { return } 279 | player.pause() 280 | } 281 | 282 | private func seekTime(_ time: TimeInterval) { 283 | guard let videoPlayer = videoPlayer, !BridgeMethod.playing(self, time: time).playing else { 284 | return 285 | } 286 | let ctime = CMTime(seconds: time, preferredTimescale: 1) 287 | videoPlayer.seek(to: ctime) 288 | } 289 | } 290 | 291 | // MARK: - KJPlayer Protocol 292 | extension KJAVPlayer { 293 | 294 | @objc public override func kj_replay() { 295 | super.kj_replay() 296 | self.playerStatus = .beginPlay 297 | } 298 | 299 | @objc public override func kj_stop() { 300 | super.kj_stop() 301 | self.resetVideoPlayer() 302 | } 303 | 304 | @objc public override func kj_appointTime(_ time: TimeInterval) { 305 | super.kj_appointTime(time) 306 | self.seekTime(time) 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /Sources/Protocols/Cache/Cache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cache.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/19. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 视频缓存协议 11 | @objc public protocol KJPlayerCacheDelegate { 12 | 13 | /// Get whether the cache function needs to be turned on 14 | @objc(kj_cacheWithPlayer:) 15 | optional func kj_cache(with player: KJBasePlayer) -> Bool 16 | 17 | /// The current resource cache is successful 18 | @objc(kj_cacheSuccessedWithPlayer:) 19 | optional func kj_cacheSuccessed(with player: KJBasePlayer) 20 | 21 | /// Whether the currently playing video has a cache 22 | /// - Parameters: 23 | /// - player: Player Kernel 24 | /// - haveCached: Whether there is a cache 25 | /// - cacheURL: Cache link, can be played directly 26 | @objc(kj_cacheBeginPlayHaveCachedWithPlayer:haveCached:cacheURL:) 27 | optional func kj_cacheBeginPlayHaveCached(with player: KJBasePlayer, haveCached: Bool, cacheURL: NSURL) 28 | } 29 | 30 | extension KJBasePlayer { 31 | 32 | /// Video caching protocol 33 | @objc public weak var cacheDelegate: KJPlayerCacheDelegate? { 34 | get { objc_getAssociatedObject(self, &Keys.cacheDelegate) as? KJPlayerCacheDelegate } 35 | set { 36 | objc_setAssociatedObject(self, &Keys.cacheDelegate, newValue, .OBJC_ASSOCIATION_ASSIGN) 37 | guard let function = newValue?.kj_cache(with:) else { return } 38 | self.openCache = function(self) 39 | } 40 | } 41 | 42 | /// Whether there is a cache 43 | @objc public var haveCached: Bool { 44 | get { 45 | if let _ = self.isCached { 46 | return self.isCached! 47 | } else { 48 | return false 49 | } 50 | } 51 | } 52 | 53 | /// Whether to enable the cache function 54 | @objc public var cache: Bool { 55 | get { 56 | if let _ = self.openCache { 57 | return self.openCache! 58 | } else { 59 | return false 60 | } 61 | } 62 | } 63 | } 64 | 65 | extension KJBasePlayer { 66 | internal struct Cache { } 67 | private struct Keys { 68 | static var cacheDelegate = "cacheDelegateKey" 69 | static var isCached = "haveCachedKey" 70 | static var openCache = "openCacheKey" 71 | } 72 | 73 | private var isCached: Bool? { 74 | get { objc_getAssociatedObject(self, &Keys.isCached) as? Bool } 75 | set { objc_setAssociatedObject(self, &Keys.isCached, newValue, .OBJC_ASSOCIATION_ASSIGN) } 76 | } 77 | 78 | private var openCache: Bool? { 79 | get { objc_getAssociatedObject(self, &Keys.openCache) as? Bool } 80 | set { objc_setAssociatedObject(self, &Keys.openCache, newValue, .OBJC_ASSOCIATION_ASSIGN) } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/Protocols/Cache/CacheManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CacheManager.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/19. 6 | // 7 | 8 | import Foundation 9 | 10 | internal struct CacheManager { 11 | struct Sandbox { } 12 | struct File { } 13 | } 14 | 15 | extension CacheManager.File { 16 | 17 | } 18 | 19 | extension CacheManager.Sandbox { 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Protocols/FreeTime/FreeTime.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TryLookTime.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/18. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 免费观看时间协议 11 | @objc public protocol KJPlayerFreeDelegate { 12 | 13 | /// Get free watching time 14 | @objc(kj_freeLookTimeWithPlayer:) 15 | func kj_freeLookTime(with player: KJBasePlayer) -> TimeInterval 16 | 17 | /// Free watching time has ended 18 | @objc(kj_freeLookTimeWithPlayer:currentTime:) 19 | optional func kj_freeLookTime(with player: KJBasePlayer, currentTime: TimeInterval) 20 | } 21 | 22 | extension KJBasePlayer { 23 | 24 | /// Free watching time protocol 25 | @objc public weak var freeDelegate: KJPlayerFreeDelegate? { 26 | get { objc_getAssociatedObject(self, &Keys.freeDelegate) as? KJPlayerFreeDelegate } 27 | set { 28 | objc_setAssociatedObject(self, &Keys.freeDelegate, newValue, .OBJC_ASSOCIATION_ASSIGN) 29 | guard let function = newValue?.kj_freeLookTime(with:) else { return } 30 | self.tryTime = function(self) 31 | } 32 | } 33 | 34 | @objc public var freeTime: TimeInterval { 35 | get { return (self.tryTime != nil) ? self.tryTime! : 0.0 } 36 | } 37 | 38 | /// 关闭免费试看限制 39 | @objc public func kj_closeFreeLookTimeLimit() { 40 | self.closeTLook = true 41 | self.tryLooked = false 42 | self.playerStatus = .beginPlay 43 | } 44 | 45 | /// 继续开启试看限制,播放下一个不同视频可以不用管, 46 | /// 主要针对于打开试看限制之后,重播会不再开启试看限制的影响 47 | /// Continue to open the free watching limit, and you can leave it alone when playing the next different video, 48 | /// Mainly aimed at the effect that the replay will no longer open the trial limit after opening the trial limit 49 | @objc public func kj_againOpenFreeLookTimeLimit() { 50 | self.closeTLook = false 51 | } 52 | } 53 | 54 | extension KJBasePlayer { 55 | internal struct FreeTime { } 56 | private struct Keys { 57 | static var freeDelegate = "freeDelegateKey" 58 | static var closeTLook = "closeTLookKey" 59 | static var tryTime = "tryTimeKey" 60 | static var tryLooked = "tryLookedKey" 61 | } 62 | private var tryTime: TimeInterval? { 63 | get { objc_getAssociatedObject(self, &Keys.tryTime) as? TimeInterval } 64 | set { 65 | objc_setAssociatedObject(self, &Keys.tryTime, newValue, .OBJC_ASSOCIATION_ASSIGN) 66 | if let time = newValue, time > 0 { 67 | self.closeTLook = false 68 | } 69 | } 70 | } 71 | private var closeTLook: Bool? { 72 | get { objc_getAssociatedObject(self, &Keys.closeTLook) as? Bool } 73 | set { objc_setAssociatedObject(self, &Keys.closeTLook, newValue, .OBJC_ASSOCIATION_ASSIGN) } 74 | } 75 | private var tryLooked: Bool? { 76 | get { objc_getAssociatedObject(self, &Keys.tryLooked) as? Bool } 77 | set { objc_setAssociatedObject(self, &Keys.tryLooked, newValue, .OBJC_ASSOCIATION_ASSIGN) } 78 | } 79 | } 80 | 81 | extension KJBasePlayer.FreeTime { 82 | 83 | internal static func freeTimeEnded(_ player: KJBasePlayer) -> Bool { 84 | return (player.tryLooked != nil) ? player.tryLooked! : false 85 | } 86 | 87 | internal static func canContinueLook(_ player: KJBasePlayer, time: TimeInterval) -> Bool { 88 | guard let tryTime = player.tryTime, tryTime > 0 else { return false } 89 | /// 总时长为零时刻不处理试看 90 | if player.totalTime <= 0 { 91 | player.tryLooked = false 92 | return false 93 | } 94 | if let close = player.closeTLook, close { 95 | player.tryLooked = false 96 | return false 97 | } 98 | if time >= tryTime { 99 | player.currentTimeObserve = tryTime 100 | if let tryLooked = player.tryLooked, tryLooked == false { 101 | player.tryLooked = true 102 | player.playerStatus = .paused(user: false) 103 | if let function = player.freeDelegate?.kj_freeLookTime(with:currentTime:) { 104 | function(player, tryTime) 105 | } 106 | } 107 | } else { 108 | player.tryLooked = false 109 | } 110 | return player.tryLooked! 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Sources/Protocols/Pip/PictureInPicture.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PictureInPicture.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/16. 6 | // 7 | 8 | import Foundation 9 | import AVFoundation 10 | @_exported import AVKit 11 | 12 | /// 开启画中画协议,该功能只有`AVPlayer`内核才具有 13 | /// Open the Picture-in-Picture protocol, this function is only available in the `AVPlayer` kernel 14 | @objc public protocol KJPlayerPipDelegate { 15 | 16 | /// Open pip 17 | @objc(kj_pipDidOpen:viewController:) 18 | func kj_pipDidOpen(with player: KJAVPlayer, viewController: AVPictureInPictureController) 19 | 20 | /// Stop pip 21 | @objc(kj_pipDidStop:viewController:) 22 | func kj_pipDidStop(with player: KJAVPlayer, viewController: AVPictureInPictureController) 23 | 24 | @objc(kj_pipFailed:failed:viewController:) 25 | optional func kj_pipFailed(with player: KJAVPlayer, failed: NSError, viewController: AVPictureInPictureController) 26 | 27 | @objc(kj_pipWillOpen:viewController:) 28 | optional func kj_pipWillOpen(with player: KJAVPlayer, viewController: AVPictureInPictureController) 29 | 30 | @objc(kj_pipWillStop:viewController:) 31 | optional func kj_pipWillStop(with player: KJAVPlayer, viewController: AVPictureInPictureController) 32 | } 33 | 34 | extension KJAVPlayer { 35 | 36 | @objc public weak var pipDelegate: KJPlayerPipDelegate? { 37 | get { objc_getAssociatedObject(self, &Keys.pipDelegate) as? KJPlayerPipDelegate } 38 | set { objc_setAssociatedObject(self, &Keys.pipDelegate, newValue, .OBJC_ASSOCIATION_ASSIGN) } 39 | } 40 | 41 | /// Open pip 42 | @objc public func openPip() { 43 | if let playerLayer = playerLayer, self.pipViewController == nil { 44 | self.setupAVPictureInPictureController(playerLayer) 45 | } 46 | self.pipViewController?.startPictureInPicture() 47 | } 48 | 49 | @objc public func closePip() { 50 | if self.pipViewController != nil { 51 | pipViewController?.stopPictureInPicture() 52 | self.pipViewController = nil 53 | } 54 | } 55 | } 56 | 57 | extension KJAVPlayer { 58 | 59 | private struct Keys { 60 | static var pipDelegate = "pipDelegateKey" 61 | static var pipViewController = "pipViewControllerKey" 62 | } 63 | private var pipViewController: AVPictureInPictureController? { 64 | get { objc_getAssociatedObject(self, &Keys.pipViewController) as? AVPictureInPictureController } 65 | set { objc_setAssociatedObject(self, &Keys.pipViewController, newValue, .OBJC_ASSOCIATION_RETAIN) } 66 | } 67 | 68 | /// Configure picture-in-picture view controller 69 | /// - Parameter layer: video display layer 70 | private func setupAVPictureInPictureController(_ layer: AVPlayerLayer) { 71 | if AVPictureInPictureController.isPictureInPictureSupported() { 72 | do { 73 | try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback) 74 | try AVAudioSession.sharedInstance().setActive(true) 75 | } catch let error { 76 | print(error) 77 | } 78 | self.pipViewController?.delegate = nil 79 | self.pipViewController = AVPictureInPictureController(playerLayer: layer) 80 | self.pipViewController?.delegate = self 81 | } 82 | } 83 | } 84 | 85 | extension KJAVPlayer: AVPictureInPictureControllerDelegate { 86 | 87 | /// 即将开启画中画 88 | public func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { 89 | if let function = self.pipDelegate?.kj_pipWillOpen(with:viewController:) { 90 | function(self, pictureInPictureController) 91 | } 92 | } 93 | 94 | /// 已经开启画中画 95 | public func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { 96 | if let function = self.pipDelegate?.kj_pipDidOpen(with:viewController:) { 97 | function(self, pictureInPictureController) 98 | } 99 | } 100 | 101 | /// 开启画中画失败 102 | public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) { 103 | if let function = self.pipDelegate?.kj_pipFailed(with:failed:viewController:) { 104 | function(self, error as NSError, pictureInPictureController) 105 | } 106 | } 107 | 108 | /// 即将关闭画中画 109 | public func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { 110 | if let function = self.pipDelegate?.kj_pipWillStop(with:viewController:) { 111 | function(self, pictureInPictureController) 112 | } 113 | } 114 | 115 | /// 已经关闭画中画 116 | public func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { 117 | if let function = self.pipDelegate?.kj_pipDidStop(with:viewController:) { 118 | function(self, pictureInPictureController) 119 | } 120 | } 121 | 122 | /// 关闭画中画且恢复播放界面 123 | public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) { 124 | 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Sources/Protocols/RecordTime/RecordTime.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecordTime.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/15. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 记录播放时间协议 11 | @objc public protocol KJPlayerRecordDelegate { 12 | 13 | /// Get whether the response needs to be recorded 14 | @objc(kj_recordTimeWithPlayer:) 15 | func kj_recordTime(with player: KJBasePlayer) -> Bool 16 | 17 | /// Get the response to the last play time 18 | @objc(kj_recordTimeWithPlayer:lastTime:) 19 | optional func kj_recordTime(with player: KJBasePlayer, lastTime: TimeInterval) 20 | } 21 | 22 | extension KJBasePlayer { 23 | 24 | /// Record last played time protocol, priority is higher than skip title 25 | @objc public weak var recordDelegate: KJPlayerRecordDelegate? { 26 | get { objc_getAssociatedObject(self, &Keys.recordDelegate) as? KJPlayerRecordDelegate } 27 | set { 28 | objc_setAssociatedObject(self, &Keys.recordDelegate, newValue, .OBJC_ASSOCIATION_ASSIGN) 29 | guard let function = newValue?.kj_recordTime(with:) else { return } 30 | self.record = function(self) 31 | } 32 | } 33 | 34 | /// Actively save the current played time 35 | @objc public func kj_saveRecordLastTime() { 36 | KJBasePlayer.RecordTime.recordPlayedTimeIMP(self) 37 | } 38 | 39 | /// Reset current recorded time, Zero 40 | @objc public func kj_resetRecordedTime() { 41 | KJBasePlayer.RecordTime.deletePlayedTimeIMP(self) 42 | } 43 | 44 | /// Stop record played time 45 | @objc public func kj_stopRecordPlayedTime(_ stop: Bool) { 46 | if self.record != nil { 47 | self.record = !stop 48 | } 49 | } 50 | } 51 | 52 | extension KJBasePlayer { 53 | internal struct RecordTime { } 54 | private struct Keys { 55 | static var recordDelegate = "recordDelegateKey" 56 | static var record = "recordKey" 57 | } 58 | 59 | private var record: Bool? { 60 | get { objc_getAssociatedObject(self, &Keys.record) as? Bool } 61 | set { objc_setAssociatedObject(self, &Keys.record, newValue, .OBJC_ASSOCIATION_ASSIGN) } 62 | } 63 | } 64 | 65 | extension KJBasePlayer.RecordTime { 66 | 67 | /// Get the last playing time 68 | internal static func lastPlayedTimeIMP(_ player: KJBasePlayer) -> TimeInterval { 69 | guard let _ = player.record, let originalURL = player.originalURL else { return 0.0 } 70 | let name = Common.Function.intactName(originalURL) 71 | let dbid = Common.Crypto.MD5(name) 72 | let (total, time) = RecordTimeData.lastPlayTime(with: dbid) 73 | if total > 0 { 74 | player.totalTimeObserve = total 75 | } 76 | if let function = player.recordDelegate?.kj_recordTime(with:lastTime:) { 77 | function(player, time) 78 | } 79 | return time 80 | } 81 | 82 | /// Save played time 83 | internal static func recordPlayedTimeIMP(_ player: KJBasePlayer) { 84 | guard let _ = player.record, let originalURL = player.originalURL else { return } 85 | let name = Common.Function.intactName(originalURL) 86 | let dbid = Common.Crypto.MD5(name) 87 | RecordTimeData.savePlayedTime(player.currentTime, total: player.totalTime, dbid: dbid) 88 | } 89 | 90 | /// Delete played time record 91 | internal static func deletePlayedTimeIMP(_ player: KJBasePlayer) { 92 | guard let originalURL = player.originalURL else { return } 93 | let name = Common.Function.intactName(originalURL) 94 | let dbid = Common.Crypto.MD5(name) 95 | RecordTimeData.DB.delete(with: dbid) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Sources/Protocols/RecordTime/RecordTimeData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecordTimeData.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/18. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | @objc(RecordTimeData) 12 | public class RecordTimeData: NSManagedObject { 13 | 14 | /// `ENTITIES` table name 15 | static let entityName = "Record" 16 | 17 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 18 | return NSFetchRequest(entityName: RecordTimeData.entityName) 19 | } 20 | 21 | @nonobjc public class func insertNewObject(context: NSManagedObjectContext) -> RecordTimeData? { 22 | let model = NSEntityDescription.insertNewObject(forEntityName: RecordTimeData.entityName, 23 | into: context) as? RecordTimeData 24 | return model 25 | } 26 | 27 | /// Primary key ID, video link remove SCHEME and then MD5 28 | @NSManaged public var dbid: String? 29 | /// Video played time 30 | @NSManaged public var lastTime: Double 31 | /// Video total time 32 | @NSManaged public var totalTime: Double 33 | } 34 | 35 | extension RecordTimeData { 36 | internal struct DB { } 37 | 38 | /// Record the last play time 39 | /// - Parameters: 40 | /// - time: Current playing time, If it is empty, the played time will be reset 41 | /// - total: Video total time 42 | /// - dbid: Primary key ID 43 | /// - Returns: whether succeed 44 | @discardableResult 45 | public static func savePlayedTime(_ time: TimeInterval?, total: TimeInterval, dbid: String) -> Bool { 46 | if let data = RecordTimeData.DB.queryOne(with: dbid) { 47 | data.lastTime = time ?? 0.0 48 | data.totalTime = total 49 | return RecordTimeData.DB.update(with: data) 50 | } 51 | let data = RecordTimeData.init(context: DatabaseManager.context) 52 | data.dbid = dbid 53 | data.lastTime = time ?? 0.0 54 | data.totalTime = total 55 | return RecordTimeData.DB.insert(with: data) 56 | } 57 | 58 | /// Get last played time and total time 59 | /// - Parameter dbid: Primary key ID 60 | /// - Returns: Played time 61 | public static func lastPlayTime(with dbid: String) -> (total: TimeInterval, lastTime: TimeInterval) { 62 | if let data = RecordTimeData.DB.queryOne(with: dbid) { 63 | return (data.totalTime, data.lastTime) 64 | } 65 | return (0.0, 0.0) 66 | } 67 | } 68 | 69 | extension RecordTimeData.DB { 70 | 71 | @discardableResult 72 | public static func insert(with data: RecordTimeData) -> Bool { 73 | guard let context = DatabaseManager.Configuration.context(), 74 | let model = RecordTimeData.insertNewObject(context: context) else { 75 | return false 76 | } 77 | model.dbid = data.dbid 78 | model.lastTime = data.lastTime 79 | model.totalTime = data.totalTime 80 | do { 81 | try context.save() 82 | return true 83 | } catch { 84 | return false 85 | } 86 | } 87 | 88 | @discardableResult 89 | public static func update(with data: RecordTimeData) -> Bool { 90 | guard let context = DatabaseManager.Configuration.context() else { return false } 91 | let request = RecordTimeData.fetchRequest() 92 | request.predicate = NSPredicate(format: "%K == %@", #keyPath(RecordTimeData.dbid), data.dbid!) 93 | do { 94 | let datas = try context.fetch(request) 95 | if !datas.isEmpty, let model = datas.first { 96 | model.lastTime = data.lastTime 97 | model.totalTime = data.totalTime 98 | if context.hasChanges { 99 | do { 100 | try context.save() 101 | return true 102 | } catch { } 103 | } 104 | } 105 | } catch { } 106 | return false 107 | } 108 | 109 | @discardableResult 110 | public static func queryOne(with dbid: String) -> RecordTimeData? { 111 | let datas = RecordTimeData.DB.query(with: dbid) 112 | if datas.isEmpty { 113 | return nil 114 | } else { 115 | return datas.first 116 | } 117 | } 118 | 119 | @discardableResult 120 | public static func query(with dbid: String) -> [RecordTimeData] { 121 | guard let context = DatabaseManager.Configuration.context() else { return [] } 122 | let request = RecordTimeData.fetchRequest() 123 | request.predicate = NSPredicate(format: "%K == %@", #keyPath(RecordTimeData.dbid), dbid) 124 | do { 125 | let datas = try context.fetch(request) 126 | return datas 127 | } catch { 128 | return [] 129 | } 130 | } 131 | 132 | @discardableResult 133 | public static func delete(with dbid: String) -> Bool { 134 | guard let context = DatabaseManager.Configuration.context() else { return false } 135 | let request = RecordTimeData.fetchRequest() 136 | request.predicate = NSPredicate(format: "%K == %@", #keyPath(RecordTimeData.dbid), dbid) 137 | do { 138 | let datas = try context.fetch(request) 139 | for data in datas { 140 | context.delete(data) 141 | } 142 | if context.hasChanges { 143 | do { 144 | try context.save() 145 | return true 146 | } catch { } 147 | } 148 | } catch { } 149 | return false 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Sources/Protocols/SkipTime/SkipTime.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SkipTime.swift 3 | // KJPlayer 4 | // 5 | // Created by abas on 2021/12/18. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 跳过片头片尾协议 11 | @objc public protocol KJPlayerSkipDelegate { 12 | 13 | /// Get the opening time of the beginning of the play 14 | @objc(kj_skipOpeningTimeWithPlayer:) 15 | optional func kj_skipOpeningTime(with player: KJBasePlayer) -> TimeInterval 16 | 17 | /// Get the ending time of the ending time 18 | @objc(kj_skipEndingTimeWithPlayer:) 19 | optional func kj_skipEndingTime(with player: KJBasePlayer) -> TimeInterval 20 | 21 | @objc(kj_skipOpeningTimeWithPlayer:openingTime:) 22 | optional func kj_skipOpeningTime(with player: KJBasePlayer, openingTime: TimeInterval) 23 | 24 | @objc(kj_skipEndingTimeWithPlayer:endingTime:) 25 | optional func kj_skipEndingTime(with player: KJBasePlayer, endingTime: TimeInterval) 26 | } 27 | 28 | extension KJBasePlayer { 29 | 30 | } 31 | --------------------------------------------------------------------------------