├── .gitignore ├── DKVideo.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── DKVideo.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── DKVideo ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon.png │ │ ├── icon_20pt@2x.png │ │ ├── icon_20pt@3x.png │ │ ├── icon_29pt.png │ │ ├── icon_29pt@2x.png │ │ ├── icon_29pt@3x.png │ │ ├── icon_40pt@2x.png │ │ ├── icon_40pt@3x.png │ │ ├── icon_60pt@2x.png │ │ ├── icon_60pt@3x.png │ │ ├── icon_76pt.png │ │ ├── icon_76pt@2x.png │ │ └── icon_83.5@2x.png │ ├── Contents.json │ ├── icon_cell_check.imageset │ │ ├── Contents.json │ │ └── icon_cell_check.pdf │ ├── icon_cell_dir.imageset │ │ ├── Contents.json │ │ └── icon_cell_dir.pdf │ ├── icon_cell_disclosure.imageset │ │ ├── Contents.json │ │ └── icon_cell_disclosure.pdf │ ├── icon_cell_night_mode.imageset │ │ ├── Contents.json │ │ └── icon_cell_night_mode.pdf │ ├── icon_cell_theme.imageset │ │ ├── Contents.json │ │ └── icon_cell_theme.pdf │ ├── icon_common_select.imageset │ │ ├── Contents.json │ │ └── icon_common_select.png │ ├── icon_day.imageset │ │ ├── Contents.json │ │ └── icon_day.png │ ├── icon_navigation_back.imageset │ │ ├── Contents.json │ │ └── icon_navigation_back.pdf │ ├── icon_navigation_close.imageset │ │ ├── Contents.json │ │ └── icon_navigation_close.pdf │ ├── icon_navigation_forward.imageset │ │ ├── Contents.json │ │ └── icon_navigation_forward.pdf │ ├── icon_navigation_refresh.imageset │ │ ├── Contents.json │ │ └── icon_navigation_refresh.pdf │ ├── icon_navigation_stop.imageset │ │ ├── Contents.json │ │ └── icon_navigation_stop.pdf │ ├── icon_navigation_theme.imageset │ │ ├── Contents.json │ │ └── icon_navigation_theme.pdf │ ├── icon_navigation_web.imageset │ │ ├── Contents.json │ │ └── icon_navigation_web.pdf │ ├── icon_night.imageset │ │ ├── Contents.json │ │ └── icon_night.png │ ├── icon_tabbar_search.imageset │ │ ├── Contents.json │ │ └── icon_tabbar_search.pdf │ ├── icon_tabbar_settings.imageset │ │ ├── Contents.json │ │ └── icon_tabbar_settings.pdf │ ├── icon_vip.imageset │ │ ├── Contents.json │ │ └── icon_vip.png │ ├── icon_whatsnew_theme.imageset │ │ ├── Contents.json │ │ └── icon_whatsnew_theme.pdf │ ├── icon_whatsnew_whats_new.imageset │ │ ├── Contents.json │ │ └── icon_whatsnew_whats_new.pdf │ └── pic_common_404.imageset │ │ ├── Contents.json │ │ ├── pic_common_404@2x.png │ │ └── pic_common_404@3x.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Extensions │ ├── Fundation │ │ ├── HandyJson+SwiftyJson.swift │ │ ├── Notification+Custom.swift │ │ ├── RuntimeExtension.swift │ │ ├── String+Utils.swift │ │ └── UserDefaults+Custom.swift │ ├── RxSwift │ │ ├── KafkaRefresh+Rx.swift │ │ └── Observable+Operators.swift │ └── UIKit │ │ ├── UIView+Utils.swift │ │ └── UIViewController+Utils.swift ├── Info.plist ├── LaunchViewController.swift ├── Managers │ ├── ActivityIndicator.swift │ ├── HUDUtils.swift │ ├── LibsManager.swift │ ├── LogManager.swift │ ├── ThemeManager.swift │ └── URLIntercept.swift ├── Modules │ ├── Common │ │ ├── BaseTableViewCell.swift │ │ ├── CollectionFlowLayout.swift │ │ ├── CommonSelectVC.swift │ │ ├── NavigationController.swift │ │ ├── PageViewController.swift │ │ ├── PageViewController2.swift │ │ ├── PopUpMenu.swift │ │ ├── TableCellViewModel.swift │ │ ├── TableViewCell.swift │ │ ├── TableViewController.swift │ │ ├── TestWebVC.swift │ │ ├── TextView.swift │ │ ├── VideoPlayerVC.swift │ │ ├── View.swift │ │ ├── ViewController.swift │ │ ├── ViewModelType.swift │ │ ├── VipWebsites.swift │ │ └── WebViewController.swift │ ├── Download │ │ ├── DKM3u8Helper.swift │ │ ├── DownLoadManage.swift │ │ ├── DownloadViewController.swift │ │ ├── M3U8Downloader.swift │ │ ├── SegmentDownloader.swift │ │ └── VideoDownloader.swift │ ├── Home │ │ ├── HomeTabbarVC.swift │ │ └── HomeViewController.swift │ └── Settings │ │ ├── SettingViewCells.swift │ │ ├── SettingViewController.swift │ │ └── SettingViewModel.swift └── Resources │ ├── AdStringFile.swift │ ├── Platform.json │ ├── R.generated.swift │ ├── Vipwebsites.json │ ├── blank.caf │ └── index.html ├── HowToInterceptRequests.md ├── LICENSE ├── Podfile ├── Podfile.lock ├── Readme.md └── VideoShare ├── Info.plist ├── ShareVC.swift ├── ShareViewController.swift └── VideoShare.entitlements /.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 | Pods 92 | IPADir 93 | exportTest.plist 94 | archive.sh 95 | exportAppstore.plist 96 | -------------------------------------------------------------------------------- /DKVideo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DKVideo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DKVideo.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /DKVideo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DKVideo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/12/3. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import Aspects 10 | import RxSwift 11 | import SuperPlayer 12 | import Tiercel 13 | import UIKit 14 | import WebKit 15 | import AVFoundation 16 | let appDelegate = UIApplication.shared.delegate as! AppDelegate 17 | 18 | @UIApplicationMain 19 | class AppDelegate: UIResponder, UIApplicationDelegate, URLSessionDelegate { 20 | var window: UIWindow? 21 | /* 无声音频播放器 */ 22 | var blankPlayer: AVAudioPlayer? 23 | /* 后台任务标识符 */ 24 | var bgTaskIdentifier: UIBackgroundTaskIdentifier! 25 | var bgTaskTimer: Timer? 26 | 27 | var sessionManagerBackground: SessionManager = { 28 | var configuration = SessionConfiguration() 29 | configuration.allowsCellularAccess = UserDefaults.downloadWithoutWifi 30 | let manager = SessionManager("com.dkjone.DKVideo.normal", configuration: configuration, operationQueue: DispatchQueue(label: "com.dkjone.DKVideo.Normal")) 31 | return manager 32 | }() 33 | 34 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 35 | let browCont = WKWebView().value(forKey: "browsingContextController") 36 | let classType = type(of: browCont!) as! AnyClass 37 | if let method = extractMethodFrom(owner: classType, selector: NSSelectorFromString("registerSchemeForCustomProtocol:")) { 38 | _ = method("http") 39 | _ = method("https") 40 | } 41 | 42 | URLProtocol.registerClass(URLIntercept.self) 43 | LibsManager.shared.configTheme() 44 | URLIntercept.videoUrl.filterEmpty().distinctUntilChanged { 45 | $1.contains("127.0.0.1") || $0 == $1 46 | }.throttle(.seconds(2), scheduler: MainScheduler.asyncInstance).observeOn(MainScheduler.asyncInstance).bind { urlStr in 47 | let videoVC = VideoPlayerVC.shared 48 | if videoVC.isVisible { 49 | let playerModel = videoVC.playerView.playerModel 50 | let playurl = SuperPlayerUrl() 51 | playurl.title = Date().string(withFormat: "yyyyMMddHHmmss1") 52 | playurl.url = urlStr 53 | playerModel?.multiVideoURLs.append(playurl) 54 | videoVC.playerView.play(with: playerModel!) 55 | print("----------\(playurl)------") 56 | } else { 57 | videoVC.urlStr = urlStr 58 | VideoPlayerVC.show() 59 | } 60 | }.disposed(by: rx.disposeBag) 61 | 62 | return true 63 | } 64 | 65 | func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { 66 | if !url.absoluteString.contains("DKVideo://") { return false } 67 | let videoUrl = url.absoluteString.removingPrefix("DKVideo://") 68 | print("openUrl:\(videoUrl)") 69 | let webvc = WebViewController() 70 | webvc.requestURL = URL(string: videoUrl) 71 | if let navc = UINavigationController.currentViewController()?.navigationController { 72 | navc.pushViewController(webvc) 73 | } else { 74 | waitToPresentVC = nil 75 | } 76 | return true 77 | } 78 | 79 | func applicationWillEnterForeground(_ application: UIApplication) {} 80 | 81 | func applicationDidEnterBackground(_ application: UIApplication) { 82 | if UserDefaults.backgroundDownload{ enterBackgroundHandler()} 83 | } 84 | 85 | func applicationDidBecomeActive(_ application: UIApplication) {} 86 | 87 | // 程序进入后台处理 88 | func enterBackgroundHandler() { 89 | let app = UIApplication.shared 90 | bgTaskIdentifier = app.beginBackgroundTask(expirationHandler: { 91 | app.endBackgroundTask(self.bgTaskIdentifier) 92 | self.bgTaskIdentifier = UIBackgroundTaskIdentifier.invalid 93 | }) 94 | bgTaskTimer = Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(requestMoreTime), userInfo: nil, repeats: true) 95 | bgTaskTimer?.fire() 96 | } 97 | 98 | @objc func requestMoreTime() { 99 | if UIApplication.shared.backgroundTimeRemaining < 30 { 100 | playBlankAudio() 101 | UIApplication.shared.endBackgroundTask(bgTaskIdentifier) 102 | bgTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: { 103 | UIApplication.shared.endBackgroundTask(self.bgTaskIdentifier) 104 | self.bgTaskIdentifier = UIBackgroundTaskIdentifier.invalid 105 | }) 106 | } 107 | } 108 | 109 | // 播放无声音频 110 | func playBlankAudio() { 111 | playAudio(forResource: "blank", ofType: "caf") 112 | } 113 | 114 | // 开始播放音频 115 | func playAudio(forResource resource: String?, ofType: String?) { 116 | try? AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers) 117 | try? AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation) // .setActive(true, error: &activationErr) 118 | let blankSoundURL = R.file.blankCaf() // URL(string: Bundle.main.path(forResource: resource, ofType: ofType) ?? "") 119 | 120 | if let blankSoundURL = blankSoundURL { 121 | blankPlayer = try? AVAudioPlayer(contentsOf: blankSoundURL) 122 | blankPlayer?.play() 123 | } 124 | } 125 | } 126 | 127 | extension AppDelegate { 128 | func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { 129 | let downloadManagers = [ sessionManagerBackground] 130 | for manager in downloadManagers { 131 | if manager.identifier == identifier { 132 | manager.completionHandler = completionHandler 133 | break 134 | } 135 | } 136 | } 137 | } 138 | 139 | var waitToPresentVC: UIViewController? 140 | -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "icon_20pt@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "icon_20pt@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "icon_29pt.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "icon_29pt@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "icon_29pt@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "icon_40pt@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "icon_40pt@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "icon_60pt@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "icon_60pt@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "idiom" : "ipad", 59 | "size" : "20x20", 60 | "scale" : "1x" 61 | }, 62 | { 63 | "idiom" : "ipad", 64 | "size" : "20x20", 65 | "scale" : "2x" 66 | }, 67 | { 68 | "idiom" : "ipad", 69 | "size" : "29x29", 70 | "scale" : "1x" 71 | }, 72 | { 73 | "idiom" : "ipad", 74 | "size" : "29x29", 75 | "scale" : "2x" 76 | }, 77 | { 78 | "idiom" : "ipad", 79 | "size" : "40x40", 80 | "scale" : "1x" 81 | }, 82 | { 83 | "idiom" : "ipad", 84 | "size" : "40x40", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "icon_76pt.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "icon_76pt@2x.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "icon_83.5@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "1024x1024", 107 | "idiom" : "ios-marketing", 108 | "filename" : "Icon.png", 109 | "scale" : "1x" 110 | } 111 | ], 112 | "info" : { 113 | "version" : 1, 114 | "author" : "xcode" 115 | } 116 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/AppIcon.appiconset/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/Icon.png -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/AppIcon.appiconset/icon_29pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_29pt.png -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/AppIcon.appiconset/icon_76pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_76pt.png -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_cell_check.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon_cell_check.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_cell_check.imageset/icon_cell_check.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_cell_check.imageset/icon_cell_check.pdf -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_cell_dir.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon_cell_dir.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_cell_dir.imageset/icon_cell_dir.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_cell_dir.imageset/icon_cell_dir.pdf -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_cell_disclosure.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon_cell_disclosure.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_cell_disclosure.imageset/icon_cell_disclosure.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_cell_disclosure.imageset/icon_cell_disclosure.pdf -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_cell_night_mode.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon_cell_night_mode.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_cell_night_mode.imageset/icon_cell_night_mode.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_cell_night_mode.imageset/icon_cell_night_mode.pdf -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_cell_theme.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon_cell_theme.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_cell_theme.imageset/icon_cell_theme.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_cell_theme.imageset/icon_cell_theme.pdf -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_common_select.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "icon_common_select.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_common_select.imageset/icon_common_select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_common_select.imageset/icon_common_select.png -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_day.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "icon_day.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_day.imageset/icon_day.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_day.imageset/icon_day.png -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_navigation_back.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon_navigation_back.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_navigation_back.imageset/icon_navigation_back.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_navigation_back.imageset/icon_navigation_back.pdf -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_navigation_close.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon_navigation_close.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_navigation_close.imageset/icon_navigation_close.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_navigation_close.imageset/icon_navigation_close.pdf -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_navigation_forward.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon_navigation_forward.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_navigation_forward.imageset/icon_navigation_forward.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_navigation_forward.imageset/icon_navigation_forward.pdf -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_navigation_refresh.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon_navigation_refresh.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_navigation_refresh.imageset/icon_navigation_refresh.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_navigation_refresh.imageset/icon_navigation_refresh.pdf -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_navigation_stop.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon_navigation_stop.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_navigation_stop.imageset/icon_navigation_stop.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_navigation_stop.imageset/icon_navigation_stop.pdf -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_navigation_theme.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon_navigation_theme.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_navigation_theme.imageset/icon_navigation_theme.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_navigation_theme.imageset/icon_navigation_theme.pdf -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_navigation_web.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon_navigation_web.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_navigation_web.imageset/icon_navigation_web.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_navigation_web.imageset/icon_navigation_web.pdf -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_night.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "icon_night.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_night.imageset/icon_night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_night.imageset/icon_night.png -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_tabbar_search.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon_tabbar_search.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_tabbar_search.imageset/icon_tabbar_search.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_tabbar_search.imageset/icon_tabbar_search.pdf -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_tabbar_settings.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon_tabbar_settings.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_tabbar_settings.imageset/icon_tabbar_settings.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_tabbar_settings.imageset/icon_tabbar_settings.pdf -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_vip.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "icon_vip.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_vip.imageset/icon_vip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_vip.imageset/icon_vip.png -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_whatsnew_theme.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon_whatsnew_theme.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_whatsnew_theme.imageset/icon_whatsnew_theme.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_whatsnew_theme.imageset/icon_whatsnew_theme.pdf -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_whatsnew_whats_new.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon_whatsnew_whats_new.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/icon_whatsnew_whats_new.imageset/icon_whatsnew_whats_new.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_whatsnew_whats_new.imageset/icon_whatsnew_whats_new.pdf -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/pic_common_404.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "pic_common_404@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "pic_common_404@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/pic_common_404.imageset/pic_common_404@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/pic_common_404.imageset/pic_common_404@2x.png -------------------------------------------------------------------------------- /DKVideo/Assets.xcassets/pic_common_404.imageset/pic_common_404@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/pic_common_404.imageset/pic_common_404@3x.png -------------------------------------------------------------------------------- /DKVideo/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 | 27 | 28 | -------------------------------------------------------------------------------- /DKVideo/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 | -------------------------------------------------------------------------------- /DKVideo/Extensions/Fundation/HandyJson+SwiftyJson.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HandyJson+SwiftyJson.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/12/9. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | import HandyJSON 12 | import SwiftyJSON 13 | 14 | protocol JsonInit { 15 | static func from(json: JSON) -> Self 16 | } 17 | 18 | extension HandyJSON { 19 | static func from(json: JSON) -> Self { 20 | return Self.deserialize(from: (json.rawString() ?? "")) ?? Self() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /DKVideo/Extensions/Fundation/Notification+Custom.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notification+Custom.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/5/31. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Notification.Name { 12 | /// 接收推送消息 13 | struct Message { 14 | /// 预警消息 15 | static let warning = Notification.Name(rawValue: "sjzx.notifications.earlyWarning") 16 | 17 | /// 通知消息 18 | static let msg = Notification.Name(rawValue: "sjzx.notifications.msg") 19 | } 20 | static let ProjectSeted = Notification.Name(rawValue: "sjzx.notifications.project") 21 | static let UserInfoChanged = Notification.Name(rawValue: "sjzx.notifications.UserInfoChanged") 22 | } 23 | -------------------------------------------------------------------------------- /DKVideo/Extensions/Fundation/RuntimeExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RuntimeExtension.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/12/4. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /*: Example 12 | if let method = extractMethodFrom(owner: classOrObject, selector: NSSelectorFromString("methodName")) { 13 | method("Object") 14 | } 15 | */ 16 | 17 | 18 | /// 动态获取方法 19 | /// - Parameters: 20 | /// - owner: 对象或者类对象 21 | /// - selector: 方法选择子 22 | func extractMethodFrom(owner: AnyObject, selector: Selector) -> ((Any?) -> Any)? { 23 | let method: Method? 24 | if owner is AnyClass { 25 | method = class_getClassMethod(owner as? AnyClass, selector) 26 | } else { 27 | method = class_getInstanceMethod(type(of: owner), selector) 28 | } 29 | 30 | if let one = method { 31 | let implementation = method_getImplementation(one) 32 | 33 | typealias Function = @convention(c) (AnyObject, Selector, Any?) -> Void 34 | 35 | let function = unsafeBitCast(implementation, to: Function.self) 36 | 37 | return { userinfo in function(owner, selector, userinfo) } 38 | 39 | } else { 40 | return nil 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /DKVideo/Extensions/Fundation/String+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Utils.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/4/1. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CommonCrypto 11 | 12 | extension String{ 13 | 14 | var dk_dateAndTime:String{ 15 | return self.slicing(from: 0, length: 16) ?? "" 16 | } 17 | var dk_date:String{ 18 | return self.slicing(from: 0, length: 10) ?? "" 19 | } 20 | 21 | var md5: String { 22 | guard let data = self.data(using: .utf8) else { 23 | return self 24 | } 25 | var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH)) 26 | #if swift(>=5.0) 27 | _ = data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in 28 | return CC_MD5(bytes.baseAddress, CC_LONG(data.count), &digest) 29 | } 30 | #else 31 | _ = data.withUnsafeBytes { bytes in 32 | return CC_MD5(bytes, CC_LONG(data.count), &digest) 33 | } 34 | #endif 35 | 36 | return digest.map { String(format: "%02x", $0) }.joined() 37 | } 38 | 39 | /// 判断是不是九宫格 40 | /// - Returns: true false 41 | func isNineKeyBoard()->Bool{ 42 | let other : NSString = "➋➌➍➎➏➐➑➒" 43 | let len = self.count 44 | for _ in 0 ..< len { 45 | if !(other.range(of: self).location != NSNotFound) { 46 | return false 47 | } 48 | } 49 | return true 50 | } 51 | /// 判断是不是Emoji 52 | /// - Returns: true false 53 | func containsEmoji()->Bool{ 54 | for scalar in unicodeScalars { 55 | switch scalar.value { 56 | case 0x1F600...0x1F64F, 57 | 0x1F300...0x1F5FF, 58 | 0x1F680...0x1F6FF, 59 | 0x2600...0x26FF, 60 | 0x2700...0x27BF, 61 | 0xFE00...0xFE0F: 62 | return true 63 | default: 64 | continue 65 | } 66 | } 67 | return false 68 | } 69 | 70 | /// 判断是不是Emoji 71 | /// - Returns: true false 72 | func hasEmoji()->Bool { 73 | let pattern = "[^\\u0020-\\u007E\\u00A0-\\u00BE\\u2E80-\\uA4CF\\uF900-\\uFAFF\\uFE30-\\uFE4F\\uFF00-\\uFFEF\\u0080-\\u009F\\u2000-\\u201f\r\n]" 74 | let pred = NSPredicate(format: "SELF MATCHES %@",pattern) 75 | return pred.evaluate(with: self) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /DKVideo/Extensions/Fundation/UserDefaults+Custom.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults+Custom.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/3/7. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - 自定义缓存 12 | 13 | extension UserDefaults { 14 | /// 是否开启黑色模式 15 | var isDark: Bool { 16 | get { return bool(forKey: #function) } 17 | set { setValue(newValue, forKey: #function) } 18 | } 19 | 20 | var themeColor: Int { 21 | get { return integer(forKey: #function) } 22 | set { setValue(newValue, forKey: #function) } 23 | } 24 | 25 | /// 只检查一次黑暗模式 26 | static var hasCheckedDarkModel: Bool { 27 | get { standard.bool(forKey: #function) } 28 | set { standard.setValue(newValue, forKey: #function) } 29 | } 30 | 31 | /// 上一次打开的应用版本 32 | static var lastVersion: String { 33 | get { standard.string(forKey: #function) ?? "" } 34 | set { standard.setValue(newValue, forKey: #function) } 35 | } 36 | 37 | static var currentVip: VipAnalysis { 38 | get { 39 | return .from(json: JSON(standard.string(forKey: #function) ?? "")) 40 | } 41 | set { 42 | standard.setValue(newValue.toJSONString() ?? "", forKey: #function) 43 | } 44 | } 45 | 46 | /// 模仿电脑版请求 47 | static var isPCAgent: Bool { 48 | get { return standard.bool(forKey: #function) } 49 | set { standard.setValue(newValue, forKey: #function) } 50 | } 51 | 52 | /// 打开APP自动开始下载 53 | static var autoStartDownload: Bool { 54 | get { return standard.bool(forKey: #function) } 55 | set { standard.setValue(newValue, forKey: #function) } 56 | } 57 | /// APP后台时下载 58 | static var backgroundDownload: Bool { 59 | get { return standard.bool(forKey: #function) } 60 | set { standard.setValue(newValue, forKey: #function) } 61 | } 62 | 63 | /// APP使用流量时下载 64 | static var downloadWithoutWifi: Bool { 65 | get { return standard.bool(forKey: #function) } 66 | set { standard.setValue(newValue, forKey: #function) } 67 | } 68 | 69 | /// 使用WKWebview 70 | static var useWKWebview: Bool { 71 | get { return standard.bool(forKey: #function) } 72 | set { standard.setValue(newValue, forKey: #function) } 73 | } 74 | 75 | // 显示解析的Webview 76 | static var showVipWebView: Bool { 77 | get { return standard.bool(forKey: #function) } 78 | set { standard.setValue(newValue, forKey: #function) } 79 | } 80 | 81 | static var defaultConfig: Bool { 82 | get { return standard.bool(forKey: #function) } 83 | set { standard.setValue(newValue, forKey: #function) } 84 | } 85 | 86 | /// 同一个任务最多的Ts下载个数 87 | static var maxDownloadTS: Int { 88 | get { max(standard.integer(forKey: #function), 3)} 89 | set { standard.setValue(newValue, forKey: #function) } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /DKVideo/Extensions/RxSwift/KafkaRefresh+Rx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KafkaRefresh+Rx.swift 3 | // 4 | // 5 | // Created by 朱德坤 on 2019/3/7. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxCocoa 11 | import RxSwift 12 | import KafkaRefresh 13 | 14 | extension Reactive where Base: KafkaRefreshControl { 15 | 16 | public var isAnimating: Binder { 17 | return Binder(self.base) { refreshControl, active in 18 | if active { 19 | refreshControl.beginRefreshing() 20 | } else { 21 | refreshControl.endRefreshing() 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /DKVideo/Extensions/RxSwift/Observable+Operators.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Observable+Operators.swift 3 | // szwhExpressway 4 | // 5 | // Created by 朱德坤 on 2019/3/6. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | import RxCocoa 12 | import RxGesture 13 | 14 | extension Reactive where Base: UIView { 15 | func tap() -> Observable { 16 | return tapGesture().when(.recognized).mapToVoid() 17 | } 18 | } 19 | extension Reactive where Base: UIImageView { 20 | public var isHighlighted: Binder { 21 | return Binder(self.base) { view, attr in 22 | view.isHighlighted = attr 23 | } 24 | } 25 | } 26 | 27 | extension Reactive where Base:UITextField{ 28 | public var attributedPlaceholder: Binder { 29 | return Binder(self.base){ filed,attr in 30 | filed.attributedPlaceholder = attr 31 | } 32 | } 33 | } 34 | 35 | protocol OptionalType { 36 | associatedtype Wrapped 37 | 38 | var value: Wrapped? { get } 39 | } 40 | 41 | extension Optional: OptionalType { 42 | var value: Wrapped? { 43 | return self 44 | } 45 | } 46 | 47 | extension Observable where Element: OptionalType { 48 | func filterNil() -> Observable { 49 | return flatMap { (element) -> Observable in 50 | if let value = element.value { 51 | return .just(value) 52 | } else { 53 | return .empty() 54 | } 55 | } 56 | } 57 | 58 | func filterNilKeepOptional() -> Observable { 59 | return self.filter { (element) -> Bool in 60 | return element.value != nil 61 | } 62 | } 63 | 64 | func replaceNil(with nilValue: Element.Wrapped) -> Observable { 65 | return flatMap { (element) -> Observable in 66 | if let value = element.value { 67 | return .just(value) 68 | } else { 69 | return .just(nilValue) 70 | } 71 | } 72 | } 73 | } 74 | 75 | protocol BooleanType { 76 | var boolValue: Bool { get } 77 | } 78 | extension Bool: BooleanType { 79 | var boolValue: Bool { return self } 80 | } 81 | 82 | // Maps true to false and vice versa 83 | extension Observable where Element: BooleanType { 84 | func not() -> Observable { 85 | return self.map { input in 86 | return !input.boolValue 87 | } 88 | } 89 | } 90 | 91 | extension Observable where Element: Equatable { 92 | func ignore(value: Element) -> Observable { 93 | return filter { (selfE) -> Bool in 94 | return value != selfE 95 | } 96 | } 97 | } 98 | 99 | extension ObservableType where E == Bool { 100 | /// Boolean not operator 101 | public func not() -> Observable { 102 | return self.map(!) 103 | } 104 | } 105 | 106 | extension SharedSequenceConvertibleType { 107 | func mapToVoid() -> SharedSequence { 108 | return map { _ in } 109 | } 110 | } 111 | 112 | extension ObservableType { 113 | 114 | func catchErrorJustComplete() -> Observable { 115 | return catchError { _ in 116 | return Observable.empty() 117 | } 118 | } 119 | 120 | func asDriverOnErrorJustComplete() -> Driver { 121 | return asDriver { error in 122 | assertionFailure("Error \(error)") 123 | return Driver.empty() 124 | } 125 | } 126 | 127 | func mapToVoid() -> Observable { 128 | return map { _ in } 129 | } 130 | } 131 | 132 | //https://gist.github.com/brocoo/aaabf12c6c2b13d292f43c971ab91dfa 133 | extension Reactive where Base: UIScrollView { 134 | public var reachedBottom: Observable { 135 | let scrollView = self.base as UIScrollView 136 | return self.contentOffset.flatMap { [weak scrollView] (contentOffset) -> Observable in 137 | guard let scrollView = scrollView else { return Observable.empty() } 138 | let visibleHeight = scrollView.frame.height - self.base.contentInset.top - scrollView.contentInset.bottom 139 | let y = contentOffset.y + scrollView.contentInset.top 140 | let threshold = max(0.0, scrollView.contentSize.height - visibleHeight) 141 | return (y > threshold) ? Observable.just(()) : Observable.empty() 142 | } 143 | } 144 | } 145 | 146 | // Two way binding operator between control property and variable, that's all it takes { 147 | 148 | infix operator <-> : DefaultPrecedence 149 | 150 | func nonMarkedText(_ textInput: UITextInput) -> String? { 151 | let start = textInput.beginningOfDocument 152 | let end = textInput.endOfDocument 153 | 154 | guard let rangeAll = textInput.textRange(from: start, to: end), 155 | let text = textInput.text(in: rangeAll) else { 156 | return nil 157 | } 158 | 159 | guard let markedTextRange = textInput.markedTextRange else { 160 | return text 161 | } 162 | 163 | guard let startRange = textInput.textRange(from: start, to: markedTextRange.start), 164 | let endRange = textInput.textRange(from: markedTextRange.end, to: end) else { 165 | return text 166 | } 167 | 168 | return (textInput.text(in: startRange) ?? "") + (textInput.text(in: endRange) ?? "") 169 | } 170 | 171 | func <-> (textInput: TextInput, variable: BehaviorRelay) -> Disposable { 172 | let bindToUIDisposable = variable.asObservable() 173 | .bind(to: textInput.text) 174 | let bindToVariable = textInput.text 175 | .subscribe(onNext: { [weak base = textInput.base] value in 176 | guard let base = base else { 177 | return 178 | } 179 | 180 | let nonMarkedTextValue = nonMarkedText(base) 181 | 182 | /** 183 | In some cases `textInput.textRangeFromPosition(start, toPosition: end)` will return nil even though the underlying 184 | value is not nil. This appears to be an Apple bug. If it's not, and we are doing something wrong, please let us know. 185 | The can be reproed easily if replace bottom code with 186 | 187 | if nonMarkedTextValue != variable.value { 188 | variable.value = nonMarkedTextValue ?? "" 189 | } 190 | 191 | and you hit "Done" button on keyboard. 192 | */ 193 | if let nonMarkedTextValue = nonMarkedTextValue, nonMarkedTextValue != variable.value { 194 | variable.accept(nonMarkedTextValue) 195 | } 196 | }, onCompleted: { 197 | bindToUIDisposable.dispose() 198 | }) 199 | 200 | return Disposables.create(bindToUIDisposable, bindToVariable) 201 | } 202 | 203 | func <-> (property: ControlProperty, variable: BehaviorRelay) -> Disposable { 204 | if T.self == String.self { 205 | #if DEBUG 206 | fatalError("It is ok to delete this message, but this is here to warn that you are maybe trying to bind to some `rx.text` property directly to variable.\n" + 207 | "That will usually work ok, but for some languages that use IME, that simplistic method could cause unexpected issues because it will return intermediate results while text is being inputed.\n" + 208 | "REMEDY: Just use `textField <-> variable` instead of `textField.rx.text <-> variable`.\n" + 209 | "Find out more here: https://github.com/ReactiveX/RxSwift/issues/649\n" 210 | ) 211 | #endif 212 | } 213 | 214 | let bindToUIDisposable = variable.asObservable() 215 | .bind(to: property) 216 | let bindToVariable = property 217 | .subscribe(onNext: { value in 218 | variable.accept(value) 219 | }, onCompleted: { 220 | bindToUIDisposable.dispose() 221 | }) 222 | 223 | return Disposables.create(bindToUIDisposable, bindToVariable) 224 | } 225 | -------------------------------------------------------------------------------- /DKVideo/Extensions/UIKit/UIViewController+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Utils.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/3/28. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | let safeAreaBottomHeight = CGFloat(UIApplication.shared.statusBarFrame.height == 44 ? 34 : 0) 12 | let safeAreaTopHeight = UIApplication.shared.statusBarFrame.height 13 | let screenWidth = UIScreen.main.bounds.width 14 | let screenHeight = UIScreen.main.bounds.height 15 | 16 | extension UIViewController { 17 | class func currentViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? { 18 | if let nav = base as? UINavigationController { 19 | return currentViewController(base: nav.visibleViewController) 20 | } 21 | if let tab = base as? UITabBarController { 22 | return currentViewController(base: tab.selectedViewController) 23 | } 24 | if let presented = base?.presentedViewController { 25 | return currentViewController(base: presented) 26 | } 27 | if let main = (base as? UISplitViewController)?.children.last { 28 | return currentViewController(base: main) 29 | } 30 | return base 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /DKVideo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleURLTypes 20 | 21 | 22 | CFBundleTypeRole 23 | Editor 24 | CFBundleURLName 25 | DKVideo 26 | CFBundleURLSchemes 27 | 28 | DKVideo 29 | 30 | 31 | 32 | CFBundleVersion 33 | 1 34 | LSRequiresIPhoneOS 35 | 36 | NSAppTransportSecurity 37 | 38 | NSAllowsArbitraryLoads 39 | 40 | NSExceptionDomains 41 | 42 | jpush.cn 43 | 44 | NSExceptionAllowsInsecureHTTPLoads 45 | 46 | NSIncludesSubdomains 47 | 48 | 49 | 50 | 51 | UIBackgroundModes 52 | 53 | audio 54 | fetch 55 | processing 56 | 57 | UILaunchStoryboardName 58 | LaunchScreen 59 | UIMainStoryboardFile 60 | Main 61 | UIRequiredDeviceCapabilities 62 | 63 | armv7 64 | 65 | UISupportedInterfaceOrientations 66 | 67 | UIInterfaceOrientationPortraitUpsideDown 68 | UIInterfaceOrientationPortrait 69 | 70 | UISupportedInterfaceOrientations~ipad 71 | 72 | UIInterfaceOrientationPortrait 73 | UIInterfaceOrientationPortraitUpsideDown 74 | UIInterfaceOrientationLandscapeLeft 75 | UIInterfaceOrientationLandscapeRight 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /DKVideo/LaunchViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LaunchViewController.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/12/6. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import WebKit 11 | class LaunchViewController: ViewController { 12 | override func makeUI() { 13 | view.backgroundColor = .black 14 | let webview = WKWebView() 15 | webview.backgroundColor = .black 16 | view.addSubview(webview) 17 | webview.snp.makeConstraints { 18 | $0.edges.equalTo(UIEdgeInsets(top: -50, left: 0, bottom: -50, right: 0)) 19 | } 20 | if let url = Bundle.main.url(forResource: "index", withExtension: "html") { 21 | var htmlStr = (try? String(contentsOf: url)) ?? "" 22 | htmlStr = htmlStr.replacingOccurrences(of: "screenWidth", with: "\(screenWidth)") 23 | htmlStr = htmlStr.replacingOccurrences(of: "screenHeight", with: "\(screenHeight)") 24 | // webview.loadHTMLString(htmlStr, baseURL: nil) 25 | } 26 | 27 | DispatchQueue.main.asyncAfter(deadline: .now() + 7) { [weak self] in 28 | self?.switchToHome() 29 | } 30 | } 31 | 32 | override func bindViewModel() { 33 | view.rx.tap().delay(.seconds(1), scheduler: MainScheduler.asyncInstance).bind { [unowned self] _ in 34 | //self.switchToHome() 35 | }.disposed(by: rx.disposeBag) 36 | } 37 | override func viewDidAppear(_ animated: Bool) { 38 | super.viewDidAppear(animated) 39 | switchToHome() 40 | } 41 | 42 | func switchToHome() { 43 | let vc = UISplitViewController() 44 | vc.maximumPrimaryColumnWidth = screenWidth / 2 45 | vc.preferredPrimaryColumnWidthFraction = 0.5 46 | vc.viewControllers = [HomeTabbarVC(), NavigationController(rootViewController: WebViewController())] 47 | vc.preferredDisplayMode = .primaryOverlay 48 | keyWindow.rootViewController = vc 49 | } 50 | } 51 | 52 | var currentWebVC: WebViewController { 53 | if let vc = ((keyWindow.rootViewController as? UISplitViewController)?.viewControllers.last?.children.first as? WebViewController) { 54 | return vc 55 | } 56 | return WebViewController() 57 | } 58 | -------------------------------------------------------------------------------- /DKVideo/Managers/ActivityIndicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityIndicator.swift 3 | // DKVideo 4 | // 5 | // Created by Krunoslav Zaher on 10/18/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | #if !RX_NO_MODULE 10 | import RxSwift 11 | import RxCocoa 12 | #endif 13 | 14 | private struct ActivityToken : ObservableConvertibleType, Disposable { 15 | private let _source: Observable 16 | private let _dispose: Cancelable 17 | 18 | init(source: Observable, disposeAction: @escaping () -> Void) { 19 | _source = source 20 | _dispose = Disposables.create(with: disposeAction) 21 | } 22 | 23 | func dispose() { 24 | _dispose.dispose() 25 | } 26 | 27 | func asObservable() -> Observable { 28 | return _source 29 | } 30 | } 31 | 32 | /** 33 | Enables monitoring of sequence computation. 34 | 35 | If there is at least one sequence computation in progress, `true` will be sent. 36 | When all activities complete `false` will be sent. 37 | */ 38 | public class ActivityIndicator: SharedSequenceConvertibleType { 39 | public typealias Element = Bool 40 | public typealias SharingStrategy = DriverSharingStrategy 41 | 42 | private let _lock = NSRecursiveLock() 43 | private let _variable = BehaviorRelay(value: 0) 44 | private let _loading: SharedSequence 45 | 46 | public init() { 47 | _loading = _variable.asDriver() 48 | .map { $0 > 0 } 49 | .distinctUntilChanged() 50 | } 51 | 52 | fileprivate func trackActivityOfObservable(_ source: O) -> Observable { 53 | return Observable.using({ () -> ActivityToken in 54 | self.increment() 55 | return ActivityToken(source: source.asObservable(), disposeAction: self.decrement) 56 | }, observableFactory: { value in 57 | return value.asObservable() 58 | }) 59 | } 60 | 61 | func increment() { 62 | _lock.lock() 63 | _variable.accept(_variable.value + 1) 64 | _lock.unlock() 65 | } 66 | 67 | private func decrement() { 68 | _lock.lock() 69 | _variable.accept(_variable.value - 1) 70 | _lock.unlock() 71 | } 72 | 73 | func stop() { 74 | 75 | } 76 | 77 | 78 | public func asSharedSequence() -> SharedSequence { 79 | return _loading 80 | } 81 | } 82 | 83 | extension ObservableConvertibleType { 84 | public func trackActivity(_ activityIndicator: ActivityIndicator) -> Observable { 85 | return activityIndicator.trackActivityOfObservable(self) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /DKVideo/Managers/HUDUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HUDUtils.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/3/20. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Toast_Swift 11 | // 12 | func showLoadHud(inView: UIView = keyWindow) { 13 | inView.makeToastActivity(.center) 14 | } 15 | 16 | func hideAllHud(inView: UIView = keyWindow) { 17 | inView.hideAllToasts(includeActivity: true, clearQueue: true) 18 | 19 | } 20 | 21 | func showMessage(message: String, 22 | inView: UIView = keyWindow, 23 | duration: TimeInterval = 1.5, 24 | position: ToastPosition = .center, 25 | title: String? = nil, 26 | image: UIImage? = nil, 27 | style: ToastStyle = ToastStyle(), 28 | completion: ((Bool) -> Void)? = nil) { 29 | 30 | keyWindow.makeToast(message, duration: duration, position: position, title: title, image:image, style: style, completion: completion) 31 | } 32 | 33 | var keyWindow: UIWindow { 34 | return UIApplication.shared.keyWindow! 35 | } 36 | -------------------------------------------------------------------------------- /DKVideo/Managers/LibsManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LibsManager.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/3/6. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | //import Bugly 10 | @_exported import ChameleonFramework 11 | 12 | @_exported import HandyJSON 13 | import IQKeyboardManagerSwift 14 | @_exported import KafkaRefresh 15 | @_exported import NSObject_Rx 16 | import NVActivityIndicatorView 17 | @_exported import Rswift 18 | @_exported import RxCocoa 19 | @_exported import RxOptional 20 | @_exported import RxSwift 21 | @_exported import SwifterSwift 22 | @_exported import SwiftyJSON 23 | @_exported import SDWebImage 24 | 25 | #if DEBUG 26 | import FLEX 27 | #endif 28 | 29 | /// 配置各框架 30 | class LibsManager: NSObject { 31 | static let shared = LibsManager() 32 | 33 | override init() { 34 | super.init() 35 | self.setBugly() 36 | self.setupActivityView() 37 | self.setupKafkaRefresh() 38 | self.setupKeyboardManager() 39 | // 加载下载项 40 | DispatchQueue.global().async { 41 | print(DownLoadManage.shared) 42 | } 43 | if !UserDefaults.defaultConfig{ 44 | UserDefaults.useWKWebview = true 45 | UserDefaults.showVipWebView = true 46 | UserDefaults.isPCAgent = true 47 | UserDefaults.defaultConfig = true 48 | } 49 | 50 | } 51 | 52 | func showFlex() { 53 | #if DEBUG 54 | FLEXManager.shared.showExplorer() 55 | #endif 56 | } 57 | 58 | func setBugly() { 59 | // var config = BuglyConfig() 60 | // config.reportLogLevel = .error 61 | // Bugly.start(withAppId: "8096bc4c87", config: config) 62 | } 63 | 64 | func configTheme() { 65 | var theme = ThemeType.currentTheme() 66 | // theme = theme.toggled() 67 | themeService.switch(theme) 68 | 69 | if #available(iOS 13.0, *) { 70 | globalStatusBarStyle.accept(.default) 71 | } else { 72 | globalStatusBarStyle.accept(.default) 73 | } 74 | } 75 | 76 | func setupKafkaRefresh() { 77 | if let defaults = KafkaRefreshDefaults.standard() { 78 | defaults.headDefaultStyle = .replicatorAllen 79 | defaults.footDefaultStyle = .native 80 | defaults.backgroundColor = .clear 81 | themeService.rx 82 | .bind({ $0.secondary }, to: defaults.rx.themeColor) 83 | .disposed(by: rx.disposeBag) 84 | } 85 | } 86 | 87 | func setupActivityView() { 88 | NVActivityIndicatorView.DEFAULT_TYPE = .ballRotateChase 89 | NVActivityIndicatorView.DEFAULT_COLOR = .secondary() 90 | } 91 | 92 | func setupKeyboardManager() { 93 | IQKeyboardManager.shared.enable = true 94 | IQKeyboardManager.shared.enableAutoToolbar = false 95 | IQKeyboardManager.shared.shouldResignOnTouchOutside = true 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /DKVideo/Managers/LogManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogManager.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/3/20. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | 12 | public func logDebug(_ message: @autoclosure () -> String, file: String = #file, function: String = #function, line: Int = #line) { 13 | let text = "\n[\(Date()) Debug] \(file.lastPathComponent.deletingPathExtension).\(function.replacingOccurrences(of: "()", with: "")):\(line)" + message() 14 | #if DEBUG 15 | print(text) 16 | #else 17 | //Log.debug(text) 18 | #endif 19 | } 20 | 21 | public func logError(_ message: @autoclosure () -> String, file: String = #file, function: String = #function, line: Int = #line) { 22 | let text = "\n[\(Date()) Error] \(file.lastPathComponent.deletingPathExtension).\(function.replacingOccurrences(of: "()", with: "")):\(line)" + message() 23 | #if DEBUG 24 | print(text) 25 | #else 26 | //Log.error(text) 27 | #endif 28 | } 29 | 30 | public func logInfo(_ message: @autoclosure () -> String, file: String = #file, function: String = #function, line: Int = #line) { 31 | let text = "\n[\(Date()) Info] \(file.lastPathComponent.deletingPathExtension).\(function.replacingOccurrences(of: "()", with: "")):\(line)" + message() 32 | #if DEBUG 33 | print(text) 34 | #else 35 | // Log.info(text) 36 | #endif 37 | } 38 | 39 | public func logVerbose(_ message: @autoclosure () -> String, file: String = #file, function: String = #function, line: Int = #line) { 40 | let text = "\n[\(Date()) Verbose] \(file.lastPathComponent.deletingPathExtension).\(function.replacingOccurrences(of: "()", with: "")):\(line)" + message() 41 | #if DEBUG 42 | print(text) 43 | #else 44 | //Log.verbose(text) 45 | #endif 46 | } 47 | 48 | public func logWarn(_ message: @autoclosure () -> String, file: String = #file, function: String = #function, line: Int = #line) { 49 | let text = "\n[\(Date()) Warn] \(file.lastPathComponent.deletingPathExtension).\(function.replacingOccurrences(of: "()", with: "")):\(line)" + message() 50 | #if DEBUG 51 | print(text) 52 | #else 53 | // Log.warning(text) 54 | #endif 55 | } 56 | 57 | public func logResourcesCount() { 58 | #if DEBUG 59 | logDebug("RxSwift resources count: \(RxSwift.Resources.total)") 60 | #endif 61 | } 62 | -------------------------------------------------------------------------------- /DKVideo/Managers/URLIntercept.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLIntercept.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/12/4. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import SuperPlayer 10 | import SwifterSwift 11 | import RxRelay 12 | let URLInterceptKey = "Intercepted" 13 | /// 网络请求拦截器 14 | class URLIntercept: URLProtocol { 15 | static let videoUrl = BehaviorRelay(value: "") 16 | var newTask: URLSessionTask? 17 | /// 返回是否监控此条网络请求 18 | /// - Parameter request: 网络请求 19 | override class func canInit(with request: URLRequest) -> Bool { 20 | print("--caninit--" + (request.url?.absoluteString ?? "")) 21 | // 如果是已经拦截过的就放行,避免出现死循环 22 | if URLProtocol.property(forKey: URLInterceptKey, in: request) as? Bool ?? false { 23 | return false 24 | } 25 | if request.allHTTPHeaderFields.isNilOrEmpty { 26 | print("##########\(request.description)") 27 | return false 28 | } 29 | // 不是网络请求,不处理 30 | if let urlScheme = request.url?.scheme?.lowercased() { 31 | if ["http", "https", "ftp"].contains(urlScheme) { 32 | return true 33 | } 34 | } 35 | 36 | // 不拦截其他 37 | return false 38 | } 39 | 40 | /// 设置我们自己的自定义请求 41 | /// - Parameter request: 当前的网络请求 42 | override class func canonicalRequest(for request: URLRequest) -> URLRequest { 43 | var mutableReqeust: URLRequest = request 44 | guard let urlStr = request.url?.absoluteString else { return request } 45 | // 视频播放拦截 46 | print("+++++++++++++" + urlStr.pathExtension) 47 | if urlStr.pathExtension.hasPrefix("m3u8") && !urlStr.contains("jx.688ing"){ 48 | // mutableReqeust.url = nil 49 | print("=========video=======\(urlStr)") 50 | videoUrl.accept(urlStr) 51 | } 52 | return mutableReqeust 53 | } 54 | 55 | override func startLoading() { 56 | // 给我们处理过的请求设置一个标识符, 防止无限循环, 57 | var request = self.request 58 | URLProtocol.setProperty(true, forKey: URLInterceptKey, in: request as! NSMutableURLRequest) 59 | 60 | // 广告拦截标识字符 61 | var isAD = false 62 | adString.forEach { str in 63 | if (request.url?.absoluteString ?? "").contains(str) { isAD = true; return } 64 | } 65 | let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil) 66 | if UserDefaults.isPCAgent { 67 | request.setValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Safari/605.1.15", forHTTPHeaderField: "User-Agent") 68 | } else { 69 | request.setValue("Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", forHTTPHeaderField: "User-Agent") 70 | } 71 | if isAD {} else { 72 | self.newTask = session.dataTask(with: request) 73 | print("====REQUEST:====\(request.url?.absoluteString ?? "")") 74 | self.newTask?.resume() 75 | } 76 | } 77 | 78 | override func stopLoading() { 79 | self.newTask?.cancel() 80 | } 81 | } 82 | 83 | extension URLIntercept: URLSessionDelegate, URLSessionDataDelegate { 84 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { 85 | client?.urlProtocol(self, didLoad: data) 86 | } 87 | 88 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { 89 | client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed) 90 | completionHandler(.allow) 91 | } 92 | 93 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 94 | client?.urlProtocolDidFinishLoading(self) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /DKVideo/Modules/Common/BaseTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseTableViewCell.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/12/6. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class BaseTableViewCell: TableViewCell { 12 | var viewModel = TableCellViewModel() 13 | 14 | func bindViewModel(viewModel: TableCellViewModel) { 15 | self.viewModel = viewModel 16 | 17 | viewModel.title.asDriver().drive(titleLabel.rx.text).disposed(by: rx.disposeBag) 18 | viewModel.title.asDriver().replaceNilWith("").map { $0.isEmpty }.drive(titleLabel.rx.isHidden).disposed(by: rx.disposeBag) 19 | 20 | viewModel.detail.asDriver().drive(detailLabel.rx.text).disposed(by: rx.disposeBag) 21 | viewModel.detail.asDriver().replaceNilWith("").map { $0.isEmpty }.drive(detailLabel.rx.isHidden).disposed(by: rx.disposeBag) 22 | 23 | viewModel.hidesDisclosure.asDriver().drive(rightImageView.rx.isHidden).disposed(by: rx.disposeBag) 24 | 25 | viewModel.image.asDriver().filterNil() 26 | .drive(leftImageView.rx.image).disposed(by: rx.disposeBag) 27 | } 28 | 29 | lazy var leftImageView: UIImageView = { 30 | let view = UIImageView(frame: CGRect()) 31 | view.contentMode = .scaleAspectFit 32 | view.snp.makeConstraints { make in 33 | make.size.equalTo(50) 34 | } 35 | return view 36 | }() 37 | 38 | lazy var textsStackView: UIStackView = { 39 | let views: [UIView] = [self.titleLabel, self.detailLabel] 40 | let view = UIStackView(arrangedSubviews: views) 41 | view.spacing = 2 42 | return view 43 | }() 44 | 45 | let titleLabel: UILabel = UILabel(fontSize: 14, text: "") 46 | 47 | let detailLabel: UILabel = UILabel(fontSize: 12, text: "") 48 | 49 | lazy var rightImageView: UIImageView = { 50 | let view = UIImageView(frame: CGRect()) 51 | view.image = R.image.icon_cell_disclosure()?.template 52 | view.snp.makeConstraints { make in 53 | make.width.equalTo(20) 54 | } 55 | return view 56 | }() 57 | 58 | override func makeUI() { 59 | super.makeUI() 60 | 61 | themeService.rx 62 | .bind({ $0.text }, to: titleLabel.rx.textColor) 63 | .bind({ $0.textGray }, to: detailLabel.rx.textColor) 64 | .bind({ $0.secondary }, to: [leftImageView.rx.tintColor, rightImageView.rx.tintColor]) 65 | .disposed(by: rx.disposeBag) 66 | 67 | stackView.addArrangedSubview(leftImageView) 68 | stackView.addArrangedSubview(textsStackView) 69 | stackView.addArrangedSubview(rightImageView) 70 | stackView.snp.remakeConstraints { make in 71 | let inset: CGFloat = 15 72 | make.edges.equalToSuperview().inset(UIEdgeInsets(top: inset / 2, left: inset, bottom: inset / 2, right: inset)) 73 | make.height.greaterThanOrEqualTo(45) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /DKVideo/Modules/Common/CollectionFlowLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionFlowLayout.swift 3 | // 4 | // 5 | // Created by 朱德坤 on 2019/4/4. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum AlignType : NSInteger { 12 | case left = 0 13 | case center = 1 14 | case right = 2 15 | } 16 | class CollectionFlowLayout: UICollectionViewFlowLayout { 17 | //两个Cell之间的距离 18 | var betweenOfCell : CGFloat{ 19 | didSet{ 20 | self.minimumInteritemSpacing = betweenOfCell 21 | } 22 | } 23 | //cell对齐方式 24 | var cellType : AlignType = AlignType.center 25 | //在居中对齐的时候需要知道这行所有cell的宽度总和 26 | var sumCellWidth : CGFloat = 0.0 27 | 28 | override init() { 29 | betweenOfCell = 5.0 30 | super.init() 31 | scrollDirection = .vertical 32 | minimumLineSpacing = 5 33 | sectionInset = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5) 34 | } 35 | convenience init(_ cellType:AlignType){ 36 | self.init() 37 | self.cellType = cellType 38 | } 39 | convenience init(_ cellType: AlignType, _ betweenOfCell: CGFloat){ 40 | self.init() 41 | self.cellType = cellType 42 | self.betweenOfCell = betweenOfCell 43 | } 44 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 45 | 46 | let layoutAttributes_super : [UICollectionViewLayoutAttributes] = super.layoutAttributesForElements(in: rect) ?? [UICollectionViewLayoutAttributes]() 47 | let layoutAttributes:[UICollectionViewLayoutAttributes] = NSArray(array: layoutAttributes_super, copyItems:true)as! [UICollectionViewLayoutAttributes] 48 | var layoutAttributes_t : [UICollectionViewLayoutAttributes] = [UICollectionViewLayoutAttributes]() 49 | for index in 0.. CommonListData { 19 | return .init(id: id, text: text, selected: selected) 20 | } 21 | } 22 | 23 | /// 默认的列表协议数据实现 24 | struct CommonListData: ListAble { 25 | var icon: UIImage? 26 | 27 | var text: String 28 | 29 | var id: String 30 | 31 | var selected: Bool 32 | init(id: String = "", text: String = "", selected: Bool = false,icon:UIImage? = nil) { 33 | self.id = id 34 | self.text = text 35 | self.selected = selected 36 | self.icon = icon 37 | } 38 | } 39 | 40 | /// 通用列表选择界面 41 | class CommonSelectVC: ViewController, UITableViewDelegate, UITableViewDataSource { 42 | /// 是否可以多选 43 | var shouldMutableSelect = false 44 | let tableView = UITableView(frame: UIScreen.main.bounds, style: .plain) 45 | /// 列表数据源 46 | var listDataProvider: ((inout [T]) -> Void)! 47 | /// 列表数据 48 | var listData = [T]() { 49 | didSet { 50 | tableView.reloadData() 51 | } 52 | } 53 | 54 | /// 选择完成的回调 55 | var commitHandle: (([T]) -> Void)! 56 | /// 返回时是否需要动画 57 | var animate = true 58 | override func viewDidLoad() { 59 | super.viewDidLoad() 60 | 61 | let bgView = UIVisualEffectView(frame: view.frame) 62 | bgView.effect = UIBlurEffect(style: .dark) 63 | bgView.alpha = 0.5 64 | bgView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(dismissSelect))) 65 | view.addSubviews([bgView, tableView]) 66 | tableView.dataSource = self 67 | tableView.delegate = self 68 | tableView.tableFooterView = UIView() 69 | tableView.showsVerticalScrollIndicator = false 70 | tableView.snp.makeConstraints { $0.edges.equalTo(UIEdgeInsets.zero) } 71 | if shouldMutableSelect { 72 | let item = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(commit)) 73 | navigationItem.setRightBarButton(item, animated: true) 74 | } 75 | themeService.rx 76 | .bind({ $0.background }, to: tableView.rx.backgroundColor) 77 | .disposed(by: rx.disposeBag) 78 | } 79 | 80 | override func viewWillAppear(_ animated: Bool) { 81 | super.viewWillAppear(animated) 82 | listDataProvider?(&listData) 83 | } 84 | 85 | @objc func commit() { 86 | let selectedList = (listData.filter { $0.selected }) 87 | commitHandle(selectedList) 88 | dismissSelect() 89 | } 90 | 91 | @objc func dismissSelect() { 92 | dismiss(animated: true) 93 | } 94 | 95 | public convenience init(title: String = "请选择", shouldMutableSelect: Bool = false, listDataProvider: @escaping (inout [T]) -> Void, commitHandle: @escaping (([T]) -> Void)) { 96 | self.init() 97 | // self.init(title: title) 98 | self.title = title 99 | self.shouldMutableSelect = shouldMutableSelect 100 | self.listDataProvider = listDataProvider 101 | self.commitHandle = commitHandle 102 | } 103 | 104 | func showSelect(in vc: UIViewController, frame: CGRect = CGRect(x: 20, y: 60, width: screenWidth - 40, height: screenHeight - 120)) { 105 | tableView.frame = frame 106 | view.backgroundColor = .clear 107 | modalPresentationStyle = .custom 108 | vc.present(self, animated: false) {} 109 | } 110 | 111 | func numberOfSections(in tableView: UITableView) -> Int { 112 | return 1 // listData.count 113 | } 114 | 115 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 116 | return listData.count 117 | } 118 | 119 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 120 | var cell = tableView.dequeueReusableCell(withIdentifier: "CommonSelectCell") 121 | if cell == nil { 122 | cell = UITableViewCell(style: .default, reuseIdentifier: "CommonSelectCell") 123 | } 124 | cell?.textLabel?.text = listData[indexPath.row].text 125 | // cell?.imageView?.image = listData[indexPath.row].icon 126 | cell?.accessoryView = listData[indexPath.row].selected ? UIImageView(image: R.image.icon_common_select()) : nil 127 | cell?.textLabel?.textColor = listData[indexPath.row].selected ? .flatBlue : .darkGray 128 | cell?.textLabel?.font = UIFont.systemFont(ofSize: 13) 129 | cell?.backgroundColor = UIColor.clear 130 | cell?.textLabel?.textColor = .text() 131 | cell?.imageView?.image = listData[indexPath.row].icon 132 | cell?.imageView?.contentMode = .center 133 | return cell! 134 | } 135 | 136 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 137 | tableView.deselectRow(at: indexPath, animated: true) 138 | let cell = tableView.cellForRow(at: indexPath) 139 | if !shouldMutableSelect { 140 | for i in 0..(inVC: UIViewController, title: String = "请选择", isMutableSelect: Bool = false, height: CGFloat = screenHeight - 200, listDataProvider: @escaping ((inout [T]) -> Void), commitHandle: @escaping (([T]) -> Void)) { 164 | let alert = UIAlertController(title: title, message: "", preferredStyle: .alert) 165 | let vc = CommonSelectVC(listDataProvider: listDataProvider, commitHandle: commitHandle) 166 | alert.setValue(vc, forKey: "contentViewController") 167 | 168 | vc.preferredContentSize.height = height 169 | alert.preferredContentSize.height = height 170 | vc.shouldMutableSelect = isMutableSelect 171 | if isMutableSelect { 172 | alert.addAction(title: "确定", style: .default, isEnabled: true) { _ in 173 | vc.commit() 174 | } 175 | } else { 176 | alert.addAction(title: "取消", style: .cancel, isEnabled: true, handler: nil) 177 | } 178 | 179 | inVC.present(alert, animated: true, completion: nil) 180 | } 181 | 182 | -------------------------------------------------------------------------------- /DKVideo/Modules/Common/NavigationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationController.swift 3 | // szwhExpressway 4 | // 5 | // Created by 朱德坤 on 2019/3/20. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AttributedLib 11 | 12 | class NavigationController: UINavigationController { 13 | 14 | override var preferredStatusBarStyle: UIStatusBarStyle { 15 | return globalStatusBarStyle.value 16 | } 17 | 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | // Do any additional setup after loading the view. 23 | // hero.isEnabled = true 24 | // hero.navigationAnimationType = .fade 25 | // hero.modalAnimationType = .autoReverse(presenting: .fade) 26 | // hero.navigationAnimationType = .autoReverse(presenting: .slide(direction: .left)) 27 | if #available(iOS 13.0, *) { 28 | if self.modalPresentationStyle == .pageSheet{ 29 | self.modalPresentationStyle = .fullScreen 30 | } 31 | overrideUserInterfaceStyle = .light 32 | } 33 | navigationBar.isTranslucent = false 34 | navigationBar.backIndicatorImage = R.image.icon_navigation_back() 35 | navigationBar.backIndicatorTransitionMaskImage = R.image.icon_navigation_back() 36 | 37 | themeService.rx 38 | .bind({ $0.text }, to: navigationBar.rx.tintColor) 39 | .bind({ $0.primary}, to: navigationBar.rx.barTintColor) 40 | .bind({ [NSAttributedString.Key.foregroundColor: $0.text] }, to: navigationBar.rx.titleTextAttributes) 41 | .disposed(by: rx.disposeBag) 42 | } 43 | override func pushViewController(_ viewController: UIViewController, animated: Bool) { 44 | if (self.children.count==1) { 45 | viewController.hidesBottomBarWhenPushed = true 46 | } 47 | super.pushViewController(viewController, animated: animated) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /DKVideo/Modules/Common/PageViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageViewController.swift 3 | // szwhExpressway 4 | // 5 | // Created by 朱德坤 on 2019/4/10. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XLPagerTabStrip 11 | class PageViewController: ButtonBarPagerTabStripViewController { 12 | /// viewControllers 13 | var pages = [UIViewController]() 14 | var automaticallyAdjustsLeftBarButtonItem = true 15 | lazy var barTitleColor = UIColor.textGray() 16 | lazy var selectTitleColor = UIColor.secondary() 17 | lazy var backBarButton: UIBarButtonItem = { 18 | let view = UIBarButtonItem() 19 | view.title = "" 20 | return view 21 | }() 22 | 23 | lazy var closeBarButton: UIBarButtonItem = { 24 | let view = UIBarButtonItem(image: R.image.icon_navigation_back(), 25 | style: .plain, 26 | target: self, 27 | action: nil) 28 | return view 29 | }() 30 | 31 | var navigationTitle = "" { 32 | didSet { navigationItem.title = navigationTitle } 33 | } 34 | 35 | var hasRedPointIndexs = [Int]() { 36 | didSet { 37 | buttonBarView.reloadData() 38 | } 39 | } 40 | 41 | lazy var setting: ButtonBarPagerTabStripSettings = { 42 | settings.style.buttonBarBackgroundColor = .primary() 43 | settings.style.buttonBarHeight = 40 44 | settings.style.buttonBarItemBackgroundColor = .primary() 45 | settings.style.selectedBarBackgroundColor = selectTitleColor 46 | settings.style.buttonBarItemFont = .systemFont(ofSize: 15) 47 | settings.style.selectedBarHeight = 2.0 48 | settings.style.buttonBarMinimumLineSpacing = 0 49 | settings.style.buttonBarItemTitleColor = barTitleColor 50 | settings.style.buttonBarItemsShouldFillAvailableWidth = true 51 | settings.style.buttonBarLeftContentInset = 15 52 | settings.style.buttonBarRightContentInset = 15 53 | var setting = settings 54 | return setting 55 | }() 56 | 57 | override func viewDidLoad() { 58 | // settings 设置要在 viewDidLoad前设置 59 | settings = setting 60 | 61 | changeCurrentIndexProgressive = { [weak self] (oldCell: ButtonBarViewCell?, newCell: ButtonBarViewCell?, _: CGFloat, changeCurrentIndex: Bool, _: Bool) -> Void in 62 | guard changeCurrentIndex == true else { return } 63 | oldCell?.label.textColor = self?.barTitleColor 64 | newCell?.label.textColor = self?.selectTitleColor 65 | } 66 | 67 | super.viewDidLoad() 68 | makeUI() 69 | } 70 | 71 | func makeUI() { 72 | // hero.isEnabled = true 73 | navigationItem.backBarButtonItem = backBarButton 74 | closeBarButton.rx.tap.bind { [weak self] () in 75 | self?.dismiss(animated: true, completion: nil) 76 | }.disposed(by: rx.disposeBag) 77 | themeService.rx 78 | .bind({ $0.background }, to: view.rx.backgroundColor) 79 | .bind({ $0.primary }, to: buttonBarView.rx.backgroundColor) 80 | .bind({ $0.secondaryDark }, to: [backBarButton.rx.tintColor, closeBarButton.rx.tintColor]) 81 | .disposed(by: rx.disposeBag) 82 | themeService.attrsStream.bind { [unowned self] theme in 83 | self.settings.style.buttonBarItemBackgroundColor = theme.primary 84 | self.barTitleColor = theme.textGray 85 | self.selectTitleColor = theme.secondary 86 | self.settings.style.selectedBarBackgroundColor = self.selectTitleColor 87 | self.settings.style.buttonBarItemTitleColor = self.barTitleColor 88 | self.buttonBarView.reloadData() 89 | }.disposed(by: rx.disposeBag) 90 | buttonBarView.shadowOffset = CGSize(width: 0, height: 0.3) 91 | buttonBarView.shadowOpacity = 0.3 92 | buttonBarView.shadowRadius = 0.3 93 | buttonBarView.clipsToBounds = false 94 | } 95 | 96 | public override func viewWillAppear(_ animated: Bool) { 97 | super.viewWillAppear(animated) 98 | 99 | if automaticallyAdjustsLeftBarButtonItem { 100 | adjustLeftBarButtonItem() 101 | } 102 | } 103 | 104 | public override func viewDidAppear(_ animated: Bool) { 105 | super.viewDidAppear(animated) 106 | logResourcesCount() 107 | } 108 | 109 | deinit { 110 | logDebug("\(type(of: self)): Deinited") 111 | logResourcesCount() 112 | } 113 | 114 | func adjustLeftBarButtonItem() { 115 | if navigationController?.viewControllers.count ?? 0 > 1 { // Pushed 116 | navigationItem.leftBarButtonItem = nil 117 | } else if presentingViewController != nil { // presented 118 | navigationItem.leftBarButtonItem = closeBarButton 119 | } 120 | } 121 | 122 | @objc func closeAction(sender: AnyObject) { 123 | dismiss(animated: true, completion: nil) 124 | } 125 | 126 | override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] { 127 | return pages 128 | } 129 | 130 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 131 | let cell = super.collectionView(collectionView, cellForItemAt: indexPath) 132 | guard let label = (cell as? ButtonBarViewCell)?.label else { return cell } 133 | if let redPoint = cell.contentView.viewWithTag(10010) { 134 | redPoint.isHidden = !hasRedPointIndexs.contains(indexPath.row) 135 | } else { 136 | let contenntV = cell.contentView 137 | let redPoint = UIView() 138 | redPoint.tag = 10010 139 | contenntV.addSubview(redPoint) 140 | redPoint.snp.makeConstraints { make in 141 | make.centerY.equalTo(label.snp.top) 142 | make.centerX.equalTo(label.snp.right) 143 | make.width.height.equalTo(10) 144 | } 145 | redPoint.cornerRadius = 5 146 | redPoint.backgroundColor = .red 147 | redPoint.isHidden = !hasRedPointIndexs.contains(indexPath.row) 148 | } 149 | cell.clipsToBounds = false 150 | return cell 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /DKVideo/Modules/Common/PageViewController2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageViewController2.swift 3 | // szwhExpressway 4 | // 5 | // Created by 朱德坤 on 2019/8/20. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XLPagerTabStrip 11 | class PageViewController2: ButtonBarPagerTabStripViewController { 12 | /// viewControllers 13 | var pages = [UIViewController]() 14 | var automaticallyAdjustsLeftBarButtonItem = true 15 | lazy var barTitleColor = themeService.type.associatedObject.textGray 16 | lazy var selectTitleColor = UIColor.primary() 17 | lazy var backBarButton: UIBarButtonItem = { 18 | let view = UIBarButtonItem() 19 | view.title = "" 20 | return view 21 | }() 22 | var hasRedPointIndexs = [Int](){ 23 | didSet{ 24 | buttonBarView.reloadData() 25 | } 26 | } 27 | 28 | lazy var closeBarButton: UIBarButtonItem = { 29 | let view = UIBarButtonItem(image: R.image.icon_navigation_back(), 30 | style: .plain, 31 | target: self, 32 | action: nil) 33 | return view 34 | }() 35 | 36 | var navigationTitle = "" { 37 | didSet { navigationItem.title = navigationTitle } 38 | } 39 | 40 | lazy var setting : ButtonBarPagerTabStripSettings = { 41 | settings.style.buttonBarBackgroundColor = .primary() 42 | settings.style.buttonBarHeight = 40 43 | settings.style.buttonBarItemBackgroundColor = .primary() 44 | settings.style.selectedBarBackgroundColor = .clear 45 | settings.style.buttonBarItemFont = .systemFont(ofSize: 15) 46 | settings.style.selectedBarHeight = 0 47 | settings.style.buttonBarMinimumLineSpacing = 20 48 | settings.style.buttonBarItemTitleColor = barTitleColor 49 | settings.style.buttonBarItemsShouldFillAvailableWidth = false 50 | settings.style.buttonBarLeftContentInset = 15 51 | settings.style.buttonBarRightContentInset = 15 52 | var setting = settings 53 | return setting 54 | }() 55 | override func viewDidLoad() { 56 | // settings 设置要在 viewDidLoad前设置 57 | settings = setting 58 | changeCurrentIndexProgressive = { [weak self] (oldCell: ButtonBarViewCell?, newCell: ButtonBarViewCell?, _: CGFloat, changeCurrentIndex: Bool, _: Bool) -> Void in 59 | guard changeCurrentIndex == true else { return } 60 | oldCell?.label.textColor = self?.barTitleColor 61 | oldCell?.contentView.viewWithTag(10086)?.backgroundColor = UIColor(hex: 0xe6e6e6) 62 | newCell?.label.textColor = self?.selectTitleColor 63 | newCell?.contentView.viewWithTag(10086)?.backgroundColor = .secondary() 64 | } 65 | 66 | super.viewDidLoad() 67 | makeUI() 68 | } 69 | 70 | func makeUI() { 71 | // hero.isEnabled = true 72 | navigationItem.backBarButtonItem = backBarButton 73 | closeBarButton.rx.tap.bind { [weak self] () in 74 | self?.dismiss(animated: true, completion: nil) 75 | }.disposed(by: rx.disposeBag) 76 | themeService.rx 77 | .bind({ $0.background }, to: view.rx.backgroundColor) 78 | .bind({ $0.primary }, to: buttonBarView.rx.backgroundColor) 79 | .bind({ $0.secondaryDark }, to: [backBarButton.rx.tintColor, closeBarButton.rx.tintColor]) 80 | .disposed(by: rx.disposeBag) 81 | themeService.attrsStream.bind { [unowned self] theme in 82 | self.settings.style.buttonBarItemBackgroundColor = theme.primary 83 | self.barTitleColor = theme.textGray 84 | self.selectTitleColor = theme.primary 85 | self.settings.style.buttonBarItemTitleColor = self.barTitleColor 86 | self.buttonBarView.reloadData() 87 | }.disposed(by: rx.disposeBag) 88 | buttonBarView.shadowOffset = CGSize(width: 0, height: 0.3) 89 | buttonBarView.shadowOpacity = 0.3 90 | buttonBarView.shadowRadius = 0.3 91 | buttonBarView.clipsToBounds = false 92 | } 93 | 94 | public override func viewWillAppear(_ animated: Bool) { 95 | super.viewWillAppear(animated) 96 | 97 | if automaticallyAdjustsLeftBarButtonItem { 98 | adjustLeftBarButtonItem() 99 | } 100 | } 101 | 102 | public override func viewDidAppear(_ animated: Bool) { 103 | super.viewDidAppear(animated) 104 | logResourcesCount() 105 | } 106 | 107 | deinit { 108 | logDebug("\(type(of: self)): Deinited") 109 | logResourcesCount() 110 | } 111 | 112 | func adjustLeftBarButtonItem() { 113 | if navigationController?.viewControllers.count ?? 0 > 1 { // Pushed 114 | navigationItem.leftBarButtonItem = nil 115 | } else if presentingViewController != nil { // presented 116 | navigationItem.leftBarButtonItem = closeBarButton 117 | } 118 | } 119 | 120 | @objc func closeAction(sender: AnyObject) { 121 | dismiss(animated: true, completion: nil) 122 | } 123 | 124 | override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] { 125 | return pages 126 | } 127 | 128 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 129 | let cell = super.collectionView(collectionView, cellForItemAt: indexPath) 130 | if let bgview = cell.contentView.viewWithTag(10086) { 131 | bgview.backgroundColor = indexPath.row == currentIndex ? .secondary(): UIColor(hex: 0xe6e6e6) 132 | } else { 133 | let contenntV = cell.contentView 134 | let redPoint = UIView() 135 | let bgview = UIView() 136 | bgview.tag = 10086 137 | redPoint.tag = 10010 138 | contenntV.insertSubview(bgview, at: 0) 139 | contenntV.addSubview(redPoint) 140 | bgview.backgroundColor = indexPath.row == currentIndex ? .secondary(): UIColor(hex: 0xe6e6e6) 141 | bgview.snp.makeConstraints { make in 142 | make.edges.equalTo(UIEdgeInsets(top: 5, left: 0, bottom: 5, right: 0)) 143 | } 144 | redPoint.snp.makeConstraints { (make) in 145 | make.centerY.equalTo(bgview.snp.top) 146 | make.centerX.equalTo(bgview.snp.right) 147 | make.width.height.equalTo(10) 148 | } 149 | redPoint.cornerRadius = 5 150 | redPoint.backgroundColor = .red 151 | bgview.cornerRadius = 3 152 | redPoint.isHidden = !hasRedPointIndexs.contains(indexPath.row) 153 | } 154 | if let redPoint = cell.contentView.viewWithTag(10010){ 155 | redPoint.isHidden = !hasRedPointIndexs.contains(indexPath.row) 156 | } 157 | cell.clipsToBounds = false 158 | return cell 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /DKVideo/Modules/Common/TableCellViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableCellViewModel.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/12/6. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | class TableCellViewModel:NSObject{ 11 | let title = BehaviorRelay(value: nil) 12 | let detail = BehaviorRelay(value: nil) 13 | let image = BehaviorRelay(value: nil) 14 | let hidesDisclosure = BehaviorRelay(value: false) 15 | } 16 | -------------------------------------------------------------------------------- /DKVideo/Modules/Common/TableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewCell.swift 3 | // szwhExpressway 4 | // 5 | // Created by 朱德坤 on 2019/3/25. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import UIKit 11 | class TableViewCell: UITableViewCell { 12 | var disposeBag = DisposeBag() 13 | 14 | static var identifier: String { 15 | return String(describing: self) 16 | } 17 | 18 | var isSelection = false 19 | var selectionColor: UIColor? { 20 | didSet { 21 | setSelected(isSelected, animated: true) 22 | } 23 | } 24 | 25 | lazy var containerView: UIView = { 26 | let view = UIView() 27 | view.backgroundColor = .clear 28 | self.addSubview(view) 29 | view.snp.makeConstraints { make in 30 | make.edges.equalToSuperview() 31 | } 32 | return view 33 | }() 34 | 35 | lazy var stackView: UIStackView = { 36 | let subviews: [UIView] = [] 37 | let view = UIStackView(arrangedSubviews: subviews) 38 | view.axis = .horizontal 39 | view.alignment = .center 40 | self.containerView.addSubview(view) 41 | view.snp.makeConstraints { make in 42 | make.edges.equalToSuperview() 43 | } 44 | return view 45 | }() 46 | 47 | 48 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 49 | super.init(style: .value2, reuseIdentifier: reuseIdentifier) 50 | makeUI() 51 | } 52 | 53 | required init?(coder aDecoder: NSCoder) { 54 | super.init(coder: aDecoder) 55 | makeUI() 56 | } 57 | 58 | override func prepareForReuse() { 59 | super.prepareForReuse() 60 | disposeBag = DisposeBag() 61 | } 62 | 63 | override func setSelected(_ selected: Bool, animated: Bool) { 64 | super.setSelected(selected, animated: animated) 65 | backgroundColor = selected ? selectionColor : .clear 66 | } 67 | 68 | func makeUI() { 69 | layer.masksToBounds = true 70 | selectionStyle = .none 71 | backgroundColor = .clear 72 | 73 | themeService.rx 74 | .bind({ $0.primaryDark }, to: rx.selectionColor) 75 | .bind({ $0.textGray }, to: textLabel!.rx.textColor) 76 | .bind({ $0.text }, to: detailTextLabel!.rx.textColor) 77 | .disposed(by: rx.disposeBag) 78 | 79 | updateUI() 80 | } 81 | 82 | func updateUI() { 83 | setNeedsDisplay() 84 | } 85 | } 86 | 87 | extension Reactive where Base: TableViewCell { 88 | var selectionColor: Binder { 89 | return Binder(base) { view, attr in 90 | view.selectionColor = attr 91 | } 92 | } 93 | } 94 | 95 | class TableViewHeaderFooter: UITableViewHeaderFooterView { 96 | var disposeBag = DisposeBag() 97 | let bgView = UIView() 98 | override init(reuseIdentifier: String?) { 99 | super.init(reuseIdentifier: reuseIdentifier) 100 | makeUI() 101 | } 102 | 103 | required init?(coder aDecoder: NSCoder) { 104 | fatalError("init(coder:) has not been implemented") 105 | } 106 | 107 | override func prepareForReuse() { 108 | super.prepareForReuse() 109 | disposeBag = DisposeBag() 110 | } 111 | 112 | func makeUI() { 113 | contentView.addSubview(bgView) 114 | bgView.snp.makeConstraints { $0.edges.equalToSuperview() } 115 | themeService.rx 116 | .bind({ $0.primary }, to: bgView.rx.backgroundColor) 117 | .disposed(by: rx.disposeBag) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /DKVideo/Modules/Common/TableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewController.swift 3 | // szwhExpressway 4 | // 5 | // Created by 朱德坤 on 2019/3/22. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import KafkaRefresh 13 | 14 | class TableViewController: ViewController, UIScrollViewDelegate { 15 | 16 | let headerRefreshTrigger = PublishSubject() 17 | let footerRefreshTrigger = PublishSubject() 18 | 19 | let isHeaderLoading = BehaviorRelay(value: false) 20 | let isFooterLoading = BehaviorRelay(value: false) 21 | 22 | lazy var tableView: UITableView = { 23 | let view = UITableView(frame: CGRect(), style: .plain) 24 | view.emptyDataSetSource = self 25 | view.emptyDataSetDelegate = self 26 | view.rowHeight = UITableView.automaticDimension 27 | view.estimatedRowHeight = 50 28 | view.tableFooterView = UIView() 29 | view.rx.setDelegate(self).disposed(by: rx.disposeBag) 30 | view.cellLayoutMarginsFollowReadableWidth = false 31 | return view 32 | }() 33 | 34 | var clearsSelectionOnViewWillAppear = true 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | } 39 | 40 | override func viewWillAppear(_ animated: Bool) { 41 | super.viewWillAppear(animated) 42 | 43 | if clearsSelectionOnViewWillAppear == true { 44 | deselectSelectedRow() 45 | } 46 | } 47 | 48 | override func makeUI() { 49 | super.makeUI() 50 | 51 | stackView.spacing = 0 52 | stackView.insertArrangedSubview(tableView, at: 0) 53 | // tableView.snp.makeConstraints{ $0.edges.equalToSuperview()} 54 | 55 | tableView.bindGlobalStyle(forHeadRefreshHandler: { [weak self] in 56 | self?.headerRefreshTrigger.onNext(()) 57 | self?.tableView.footRefreshControl.resumeRefreshAvailable() 58 | }) 59 | 60 | tableView.bindGlobalStyle(forFootRefreshHandler: { [weak self] in 61 | self?.footerRefreshTrigger.onNext(()) 62 | }) 63 | tableView.footRefreshControl.setAlertBackgroundColor( themeService.type.associatedObject.background) 64 | 65 | isHeaderLoading.bind(to: tableView.headRefreshControl.rx.isAnimating).disposed(by: rx.disposeBag) 66 | isFooterLoading.bind(to: tableView.footRefreshControl.rx.isAnimating).disposed(by: rx.disposeBag) 67 | 68 | tableView.footRefreshControl.autoRefreshOnFoot = true 69 | 70 | let updateEmptyDataSet = Observable.of(isLoading.mapToVoid().asObservable(), emptyDataSetImageTintColor.mapToVoid()).merge() 71 | updateEmptyDataSet.subscribe(onNext: { [weak self] () in 72 | self?.tableView.reloadEmptyDataSet() 73 | }).disposed(by: rx.disposeBag) 74 | themeService 75 | .rx 76 | .bind({ $0.background }, to: tableView.rx.backgroundColor) 77 | .disposed(by: rx.disposeBag) 78 | } 79 | 80 | override func updateUI() { 81 | super.updateUI() 82 | } 83 | } 84 | 85 | extension TableViewController { 86 | 87 | func deselectSelectedRow() { 88 | if let selectedIndexPaths = tableView.indexPathsForSelectedRows { 89 | selectedIndexPaths.forEach({ (indexPath) in 90 | tableView.deselectRow(at: indexPath, animated: false) 91 | }) 92 | } 93 | } 94 | } 95 | 96 | extension TableViewController: UITableViewDelegate { 97 | 98 | func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { 99 | if let view = view as? UITableViewHeaderFooterView { 100 | view.textLabel?.font = UIFont(name: ".SFUIText-Bold", size: 15.0)! 101 | themeService.rx 102 | .bind({ $0.text }, to: view.textLabel!.rx.textColor) 103 | .bind({ $0.primaryDark }, to: view.contentView.rx.backgroundColor) 104 | .disposed(by: rx.disposeBag) 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /DKVideo/Modules/Common/TestWebVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestWebVC.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/12/19. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import WebKit 11 | class TestWebVC: ViewController, UIWebViewDelegate { 12 | let webView = UIWebView() 13 | override func makeUI() { 14 | super.makeUI() 15 | view.addSubview(webView) 16 | webView.snp.makeConstraints { $0.edges.equalToSuperview() } 17 | webView.loadRequest(URLRequest(urlString: "https://jx.688ing.com/")!) 18 | webView.delegate = self 19 | webView.allowsInlineMediaPlayback = true 20 | webView.mediaPlaybackRequiresUserAction = true 21 | } 22 | 23 | 24 | 25 | func webView(_ webView: UIWebView, didFailLoadWithError error: Error) { 26 | print(error) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /DKVideo/Modules/Common/TextView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextView.swift 3 | // dataCenter 4 | // 5 | // Created by 朱德坤 on 2019/10/8. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @IBDesignable 12 | open class TextView: UITextView { 13 | 14 | private struct Constants { 15 | static let defaultiOSPlaceholderColor = UIColor(red: 0.0, green: 0.0, blue: 0.0980392, alpha: 0.22) 16 | } 17 | 18 | public let placeholderLabel: UILabel = UILabel() 19 | 20 | private var placeholderLabelConstraints = [NSLayoutConstraint]() 21 | 22 | @IBInspectable open var placeholder: String = "" { 23 | didSet { 24 | placeholderLabel.text = placeholder 25 | } 26 | } 27 | 28 | @IBInspectable open var placeholderColor: UIColor = TextView.Constants.defaultiOSPlaceholderColor { 29 | didSet { 30 | placeholderLabel.textColor = placeholderColor 31 | } 32 | } 33 | 34 | override open var font: UIFont! { 35 | didSet { 36 | if placeholderFont == nil { 37 | placeholderLabel.font = font 38 | } 39 | } 40 | } 41 | 42 | open var placeholderFont: UIFont? { 43 | didSet { 44 | let font = (placeholderFont != nil) ? placeholderFont : self.font 45 | placeholderLabel.font = font 46 | } 47 | } 48 | 49 | override open var textAlignment: NSTextAlignment { 50 | didSet { 51 | placeholderLabel.textAlignment = textAlignment 52 | } 53 | } 54 | 55 | override open var text: String! { 56 | didSet { 57 | textDidChange() 58 | } 59 | } 60 | 61 | override open var attributedText: NSAttributedString! { 62 | didSet { 63 | textDidChange() 64 | } 65 | } 66 | 67 | override open var textContainerInset: UIEdgeInsets { 68 | didSet { 69 | updateConstraintsForPlaceholderLabel() 70 | } 71 | } 72 | 73 | override public init(frame: CGRect, textContainer: NSTextContainer?) { 74 | super.init(frame: frame, textContainer: textContainer) 75 | commonInit() 76 | } 77 | 78 | required public init?(coder aDecoder: NSCoder) { 79 | super.init(coder: aDecoder) 80 | commonInit() 81 | } 82 | 83 | private func commonInit() { 84 | #if swift(>=4.2) 85 | let notificationName = UITextView.textDidChangeNotification 86 | #else 87 | let notificationName = NSNotification.Name.UITextView.textDidChangeNotification 88 | #endif 89 | 90 | NotificationCenter.default.addObserver(self, 91 | selector: #selector(textDidChange), 92 | name: notificationName, 93 | object: nil) 94 | 95 | placeholderLabel.font = font 96 | placeholderLabel.textColor = placeholderColor 97 | placeholderLabel.textAlignment = textAlignment 98 | placeholderLabel.text = placeholder 99 | placeholderLabel.numberOfLines = 0 100 | placeholderLabel.backgroundColor = UIColor.clear 101 | placeholderLabel.translatesAutoresizingMaskIntoConstraints = false 102 | addSubview(placeholderLabel) 103 | updateConstraintsForPlaceholderLabel() 104 | } 105 | 106 | private func updateConstraintsForPlaceholderLabel() { 107 | var newConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-(\(textContainerInset.left + textContainer.lineFragmentPadding))-[placeholder]", 108 | options: [], 109 | metrics: nil, 110 | views: ["placeholder": placeholderLabel]) 111 | newConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-(\(textContainerInset.top))-[placeholder]", 112 | options: [], 113 | metrics: nil, 114 | views: ["placeholder": placeholderLabel]) 115 | newConstraints.append(NSLayoutConstraint( 116 | item: placeholderLabel, 117 | attribute: .width, 118 | relatedBy: .equal, 119 | toItem: self, 120 | attribute: .width, 121 | multiplier: 1.0, 122 | constant: -(textContainerInset.left + textContainerInset.right + textContainer.lineFragmentPadding * 2.0) 123 | )) 124 | removeConstraints(placeholderLabelConstraints) 125 | addConstraints(newConstraints) 126 | placeholderLabelConstraints = newConstraints 127 | } 128 | 129 | @objc private func textDidChange() { 130 | placeholderLabel.isHidden = !text.isEmpty 131 | } 132 | 133 | open override func layoutSubviews() { 134 | super.layoutSubviews() 135 | placeholderLabel.preferredMaxLayoutWidth = textContainer.size.width - textContainer.lineFragmentPadding * 2.0 136 | } 137 | 138 | deinit { 139 | #if swift(>=4.2) 140 | let notificationName = UITextView.textDidChangeNotification 141 | #else 142 | let notificationName = NSNotification.Name.UITextView.textDidChangeNotification 143 | #endif 144 | 145 | NotificationCenter.default.removeObserver(self, 146 | name: notificationName, 147 | object: nil) 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /DKVideo/Modules/Common/VideoPlayerVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoPlayerVC.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/12/9. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import SuperPlayer 10 | import UIKit 11 | 12 | class VideoPlayerVC: ViewController { 13 | static let shared: VideoPlayerVC = { 14 | let vc = VideoPlayerVC() 15 | vc.modalPresentationStyle = .fullScreen 16 | return vc 17 | }() 18 | 19 | var urlStr = "" { 20 | didSet { 21 | isDownloaded = urlStr.contains(".mp4") 22 | } 23 | } 24 | 25 | let playerView = SuperPlayerView() 26 | let downloadBtn = UIButton() 27 | var isDownloaded = false { 28 | didSet { 29 | downloadBtn.isHidden = urlStr.starts(with: "http://127.0.0.1") || isDownloaded 30 | } 31 | } 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | 36 | // 设置代理,用于接受事件 37 | playerView.delegate = self 38 | // 设置父View,_playerView会被自动添加到下面 39 | playerView.fatherView = contentView 40 | view.backgroundColor = .black 41 | downloadBtn.setTitle("下载", for: []) 42 | downloadBtn.titleLabel?.font = .systemFont(ofSize: 14) 43 | downloadBtn.setTitleColor(.white, for: []) 44 | 45 | playerView.controlView.addSubview(downloadBtn) 46 | downloadBtn.snp.makeConstraints { make in 47 | make.right.top.equalTo(-safeAreaTopHeight) 48 | make.size.equalTo(CGSize(width: 80, height: 50)) 49 | } 50 | downloadBtn.rx.tap.bind { [unowned self] in 51 | let defaultName = Date().string(withFormat: "yyyyMMddHHmmss") 52 | let alert = UIAlertController(title: "下载", message: "请输入下载名称", defaultActionButtonTitle: "取消", tintColor: nil) 53 | alert.addTextField { textfiled in 54 | textfiled.placeholder = "请输入下载名称" 55 | textfiled.text = defaultName 56 | textfiled.clearButtonMode = .always 57 | } 58 | alert.addAction(title: "确定", style: .default, isEnabled: true) { [unowned self] _ in 59 | DownLoadManage.shared.addDownload(fileName: alert.textFields?.first?.text ?? defaultName, path: self.playerView.playerModel.playingDefinitionUrl, autoStart: true) 60 | self.isDownloaded = true 61 | } 62 | self.present(alert, animated: true, completion: nil) 63 | }.disposed(by: rx.disposeBag) 64 | } 65 | 66 | class func show() { 67 | // let vc = NavigationController(rootViewController: shared) 68 | // vc.navigationBar.isHidden = true 69 | // currentViewController()?.present(vc, animated: true, completion: nil) 70 | currentViewController()?.navigationController?.pushViewController(shared) 71 | } 72 | } 73 | 74 | extension VideoPlayerVC: SuperPlayerDelegate { 75 | override func viewWillAppear(_ animated: Bool) { 76 | super.viewWillAppear(animated) 77 | let playerModel = SuperPlayerModel() 78 | // 设置播放地址,直播、点播都可以 79 | playerModel.videoURL = urlStr 80 | let playurl = SuperPlayerUrl() 81 | playurl.title = "原始" 82 | playurl.url = urlStr 83 | playerModel.multiVideoURLs = [playurl] 84 | // 开始播放 85 | playerView.play(with: playerModel) 86 | playerView.playerConfig.hwAcceleration = false 87 | navigationController?.navigationBar.isHidden = true 88 | (keyWindow.rootViewController as? UISplitViewController)?.presentsWithGesture = false 89 | } 90 | 91 | override func viewWillDisappear(_ animated: Bool) { 92 | super.viewWillDisappear(animated) 93 | navigationController?.navigationBar.isHidden = false 94 | (keyWindow.rootViewController as? UISplitViewController)?.presentsWithGesture = true 95 | playerView.resetPlayer() 96 | } 97 | 98 | func superPlayerBackAction(_ player: SuperPlayerView!) { 99 | // player.resetPlayer() 100 | // dismiss(animated: true, completion: nil) 101 | navigationController?.popViewController() 102 | } 103 | 104 | func superPlayerFullScreenChanged(_ player: SuperPlayerView!) { 105 | (player.controlView as? SPDefaultControlView)?.danmakuBtn.isHidden = true 106 | 107 | downloadBtn.snp.remakeConstraints { make in 108 | make.top.equalTo(0) 109 | make.right.equalTo(player.isFullScreen ? -100 : -15) 110 | make.size.equalTo(CGSize(width: 80, height: 50)) 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /DKVideo/Modules/Common/View.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View.swift 3 | // 4 | // 5 | // Created by 朱德坤 on 2019/4/16. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class View: UIView { 12 | override init(frame: CGRect) { 13 | super.init(frame: frame) 14 | makeUI() 15 | } 16 | 17 | required init?(coder aDecoder: NSCoder) { 18 | fatalError("init(coder:) has not been implemented") 19 | } 20 | 21 | func makeUI(){ 22 | themeService.rx 23 | .bind({ $0.background }, to: rx.backgroundColor) 24 | .disposed(by: self.rx.disposeBag) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /DKVideo/Modules/Common/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // 4 | // 5 | // Created by 朱德坤 on 2019/3/6. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import DZNEmptyDataSet 10 | //import Hero 11 | import NVActivityIndicatorView 12 | import RxCocoa 13 | import RxSwift 14 | import SnapKit 15 | import UIKit 16 | import XLPagerTabStrip 17 | class ViewController: UIViewController, NVActivityIndicatorViewable { 18 | let isLoading = BehaviorRelay(value: false) 19 | 20 | var automaticallyAdjustsLeftBarButtonItem = true 21 | var canOpenFlex = true 22 | fileprivate var indicatorInfo = IndicatorInfo(title: "") 23 | var navigationTitle = "" { 24 | didSet { 25 | navigationItem.title = navigationTitle 26 | indicatorInfo.title = navigationTitle 27 | } 28 | } 29 | 30 | let spaceBarButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: nil, action: nil) 31 | 32 | let emptyDataSetButtonTap = PublishSubject() 33 | var emptyDataSetTitle = "" 34 | var emptyDataSetDescription = "暂无数据"//"出错啦!\n没有你要访问的页面~\n\n" 35 | var emptyDataSetImage = R.image.pic_common_404()! 36 | var emptyDataSetImageTintColor = BehaviorRelay(value: nil) 37 | 38 | let motionShakeEvent = PublishSubject() 39 | 40 | lazy var contentView: UIView = { 41 | let view = UIView() 42 | self.view.addSubview(view) 43 | view.snp.makeConstraints { make in 44 | make.edges.equalTo(self.view.safeAreaLayoutGuide) 45 | } 46 | return view 47 | }() 48 | 49 | lazy var stackView: UIStackView = { 50 | let subviews: [UIView] = [] 51 | let view = UIStackView(arrangedSubviews: subviews) 52 | view.axis = .vertical 53 | view.spacing = 0 54 | self.contentView.addSubview(view) 55 | view.snp.makeConstraints { make in 56 | make.edges.equalToSuperview() 57 | } 58 | return view 59 | }() 60 | 61 | lazy var backBarButton: UIBarButtonItem = { 62 | let view = UIBarButtonItem() 63 | view.title = "" 64 | return view 65 | }() 66 | 67 | lazy var closeBarButton: UIBarButtonItem = { 68 | let view = UIBarButtonItem(image: R.image.icon_navigation_back(), 69 | style: .plain, 70 | target: self, 71 | action: nil) 72 | return view 73 | }() 74 | 75 | public override func viewDidLoad() { 76 | super.viewDidLoad() 77 | 78 | // Do any additional setup after loading the view. 79 | makeUI() 80 | bindViewModel() 81 | 82 | closeBarButton.rx.tap.bind { [weak self] () in 83 | self?.dismiss(animated: true, completion: nil) 84 | }.disposed(by: rx.disposeBag) 85 | // Observe application did become active notification 86 | NotificationCenter.default 87 | .rx.notification(UIApplication.didBecomeActiveNotification) 88 | .subscribe { [weak self] _ in 89 | self?.didBecomeActive() 90 | }.disposed(by: rx.disposeBag) 91 | // Observe device orientation change 92 | NotificationCenter.default 93 | .rx.notification(UIDevice.orientationDidChangeNotification) 94 | .subscribe { [weak self] _ in 95 | self?.orientationChanged() 96 | }.disposed(by: rx.disposeBag) 97 | } 98 | 99 | public override func viewWillAppear(_ animated: Bool) { 100 | super.viewWillAppear(animated) 101 | 102 | if automaticallyAdjustsLeftBarButtonItem { 103 | adjustLeftBarButtonItem() 104 | } 105 | updateUI() 106 | } 107 | 108 | public override func viewDidAppear(_ animated: Bool) { 109 | super.viewDidAppear(animated) 110 | updateUI() 111 | logResourcesCount() 112 | } 113 | 114 | deinit { 115 | logDebug("\(type(of: self)): Deinited") 116 | logResourcesCount() 117 | } 118 | 119 | public override func didReceiveMemoryWarning() { 120 | super.didReceiveMemoryWarning() 121 | // Dispose of any resources that can be recreated. 122 | logDebug("\(type(of: self)): Received Memory Warning") 123 | } 124 | 125 | func makeUI() { 126 | // hero.isEnabled = true 127 | navigationItem.backBarButtonItem = backBarButton 128 | motionShakeEvent.subscribe(onNext: { () in 129 | //FIXME: - not complete 130 | // let theme = themeService.type.toggled() 131 | // themeService.switch(theme) 132 | }).disposed(by: rx.disposeBag) 133 | if #available(iOS 13.0, *) { 134 | if self.modalPresentationStyle == .pageSheet{ 135 | self.modalPresentationStyle = .fullScreen 136 | } 137 | overrideUserInterfaceStyle = .light 138 | } 139 | themeService.rx 140 | .bind({ $0.background }, to: view.rx.backgroundColor) 141 | .bind({ $0.secondaryDark }, to: [backBarButton.rx.tintColor, closeBarButton.rx.tintColor]) 142 | .disposed(by: rx.disposeBag) 143 | 144 | updateUI() 145 | } 146 | 147 | func bindViewModel() { 148 | emptyDataSetButtonTap.bind { [unowned self] in 149 | self.navigationController?.popToRootViewController(animated: true) 150 | self.dismiss(animated: true, completion: nil) 151 | 152 | }.disposed(by: rx.disposeBag) 153 | } 154 | 155 | func updateUI() { 156 | } 157 | 158 | override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { 159 | if motion == .motionShake { 160 | motionShakeEvent.onNext(()) 161 | } 162 | } 163 | 164 | func orientationChanged() { 165 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { 166 | self.updateUI() 167 | } 168 | } 169 | 170 | func didBecomeActive() { 171 | updateUI() 172 | } 173 | 174 | // MARK: Adjusting Navigation Item 175 | 176 | func adjustLeftBarButtonItem() { 177 | if navigationController?.viewControllers.count ?? 0 > 1 { // Pushed 178 | navigationItem.leftBarButtonItem = nil 179 | } else if presentingViewController != nil { // presented 180 | navigationItem.leftBarButtonItem = closeBarButton 181 | } 182 | } 183 | 184 | @objc func closeAction(sender: AnyObject) { 185 | dismiss(animated: true, completion: nil) 186 | } 187 | } 188 | 189 | extension ViewController { 190 | func emptyView(withHeight height: CGFloat) -> UIView { 191 | let view = UIView() 192 | view.snp.makeConstraints { make in 193 | make.height.equalTo(height) 194 | } 195 | return view 196 | } 197 | 198 | public var emptyTableFooterView: UIView { 199 | return UIView(frame: CGRect(x: 0, y: 0, width: screenWidth, height: 0.01)) 200 | } 201 | 202 | @objc func handleThreeFingerSwipe(swipeRecognizer: UISwipeGestureRecognizer) { 203 | if swipeRecognizer.state == .recognized { 204 | LibsManager.shared.showFlex() 205 | 206 | } 207 | } 208 | } 209 | 210 | extension Reactive where Base: ViewController { 211 | /// Bindable sink for `backgroundColor` property 212 | var emptyDataSetImageTintColorBinder: Binder { 213 | return Binder(base) { view, attr in 214 | view.emptyDataSetImageTintColor.accept(attr) 215 | } 216 | } 217 | } 218 | 219 | extension ViewController: DZNEmptyDataSetSource { 220 | func title(forEmptyDataSet scrollView: UIScrollView!) -> NSAttributedString! { 221 | return NSAttributedString(string: emptyDataSetTitle) 222 | } 223 | 224 | func description(forEmptyDataSet scrollView: UIScrollView!) -> NSAttributedString! { 225 | return NSAttributedString(string: emptyDataSetDescription) 226 | } 227 | 228 | func image(forEmptyDataSet scrollView: UIScrollView!) -> UIImage! { 229 | return emptyDataSetImage 230 | } 231 | 232 | func imageTintColor(forEmptyDataSet scrollView: UIScrollView!) -> UIColor! { 233 | return emptyDataSetImageTintColor.value 234 | } 235 | 236 | func backgroundColor(forEmptyDataSet scrollView: UIScrollView!) -> UIColor! { 237 | return .clear 238 | } 239 | 240 | func verticalOffset(forEmptyDataSet scrollView: UIScrollView!) -> CGFloat { 241 | return -60 242 | } 243 | } 244 | 245 | extension ViewController: DZNEmptyDataSetDelegate { 246 | func emptyDataSetShouldDisplay(_ scrollView: UIScrollView!) -> Bool { 247 | return !isLoading.value 248 | } 249 | 250 | func emptyDataSetShouldAllowScroll(_ scrollView: UIScrollView!) -> Bool { 251 | return true 252 | } 253 | 254 | func emptyDataSet(_ scrollView: UIScrollView!, didTap button: UIButton!) { 255 | emptyDataSetButtonTap.onNext(()) 256 | } 257 | } 258 | 259 | extension ViewController: IndicatorInfoProvider { 260 | 261 | func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo { 262 | return self.indicatorInfo 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /DKVideo/Modules/Common/ViewModelType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModelType.swift 3 | // szwhExpressway 4 | // 5 | // Created by 朱德坤 on 2019/3/20. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import RxCocoa 10 | import RxSwift 11 | 12 | protocol ViewModelType { 13 | associatedtype Input 14 | associatedtype Output 15 | 16 | func transform(input: Input) -> Output 17 | } 18 | 19 | class ViewModel: NSObject { 20 | var page = 1 21 | 22 | // let loading = ActivityIndicator() 23 | // let headerLoading = ActivityIndicator() 24 | // let footerLoading = ActivityIndicator() 25 | let loading = BehaviorRelay(value:false) 26 | let headerLoading = BehaviorRelay(value:false) 27 | let footerLoading = BehaviorRelay(value:false) 28 | 29 | /// 无更多数据 :false ,重置:true 30 | let noMoreDate = BehaviorRelay(value:false) 31 | override init() { 32 | super.init() 33 | } 34 | 35 | deinit { 36 | logDebug("\(type(of: self)): Deinited") 37 | logResourcesCount() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /DKVideo/Modules/Common/VipWebsites.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VipWebsites.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/12/9. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import HandyJSON 11 | struct VipAnalysis: HandyJSON, ListAble { 12 | var icon: UIImage? 13 | var text: String { 14 | return title 15 | } 16 | 17 | var id: String { 18 | return url 19 | } 20 | 21 | var selected = false 22 | var title = "" 23 | var url = "" 24 | 25 | static var vips: [VipAnalysis] = { 26 | let str = (try? String(contentsOf: R.file.vipwebsitesJson()!)) ?? "" 27 | return JSON(parseJSON: str).arrayValue.map(VipAnalysis.from(json:)) 28 | }() 29 | } 30 | -------------------------------------------------------------------------------- /DKVideo/Modules/Download/DKM3u8Helper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DKM3u8Helper.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/12/11. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | class DKM3u8Helper { 11 | fileprivate var url = "" 12 | fileprivate var success: ((URL) -> Void)? 13 | fileprivate var failed: ((M3u8ParaseError) -> Void)? 14 | var m3u8Data: String = "" 15 | var tsSegmentArray = [M3u8TsSegmentModel]() 16 | var tsPlaylist = M3u8Playlist() 17 | var identifier = "" 18 | func parser(url: String, name: String, success: ((URL) -> Void)?, failed: ((M3u8ParaseError) -> Void)?) { 19 | self.url = url 20 | self.success = success 21 | self.failed = failed 22 | self.identifier = name 23 | do { 24 | try self.parse(url: url) 25 | } catch { 26 | if let aError = error as? M3u8ParaseError { 27 | DispatchQueue.main.async { 28 | failed?(aError) 29 | } 30 | 31 | } else { 32 | DispatchQueue.main.async { 33 | failed?(.Other) 34 | } 35 | } 36 | } 37 | } 38 | 39 | fileprivate func parse(url: String) throws { 40 | self.url = url 41 | guard let m3u8Url = URL(string: url) else { throw M3u8ParaseError.URLInvalid } 42 | guard let m3u8Content = try? String(contentsOf: m3u8Url) else { 43 | throw M3u8ParaseError.EmptyM3u8Content 44 | } 45 | // if !(url.hasPrefix("http://") || url.hasPrefix("https://")) { 46 | // throw M3u8ParaseError.URLInvalid 47 | // } 48 | if m3u8Content == "" { 49 | throw M3u8ParaseError.EmptyM3u8Content 50 | } else if m3u8Content.contains("EXT-X-STREAM-INF") { 51 | let arr = m3u8Content.split(separator: "\n").map { String($0) } 52 | if let preIndex = arr.firstIndex(where: { $0.contains("#EXT-X-STREAM-INF") }) { 53 | if let newUrl = arr[safe: preIndex + 1] { 54 | if !newUrl.hasPrefix("http") { 55 | try self.parse(url: self.getFullPath(url: url, newUrl: newUrl)) 56 | } 57 | } 58 | } else { 59 | throw M3u8ParaseError.NoSteamInfo 60 | } 61 | } else { 62 | guard m3u8Content.range(of: "#EXTINF:") != nil else { 63 | throw M3u8ParaseError.NoEXTINFinfo 64 | } 65 | 66 | self.m3u8Data = m3u8Content 67 | if self.tsSegmentArray.count > 0 { self.tsSegmentArray.removeAll() } 68 | 69 | let segmentRange = m3u8Content.range(of: "#EXTINF:")! 70 | let segmentsString = String(m3u8Content.suffix(from: segmentRange.lowerBound)).components(separatedBy: "#EXT-X-ENDLIST") 71 | var segmentArray = segmentsString[0].components(separatedBy: "\n") 72 | segmentArray = segmentArray.filter { !$0.contains("#EXT-X-DISCONTINUITY") } 73 | 74 | while segmentArray.count > 2 { 75 | var segmentModel = M3u8TsSegmentModel() 76 | 77 | let segmentDurationPart = segmentArray[0].components(separatedBy: ":")[1] 78 | var segmentDuration: Float = 0.0 79 | 80 | if segmentDurationPart.contains(",") { 81 | segmentDuration = Float(segmentDurationPart.components(separatedBy: ",")[0])! 82 | } else { 83 | segmentDuration = Float(segmentDurationPart)! 84 | } 85 | 86 | var segmentURL = segmentArray[1] 87 | if !segmentURL.hasPrefix("http") { 88 | segmentURL = self.getFullPath(url: url, newUrl: segmentURL) 89 | } 90 | segmentModel.duration = segmentDuration 91 | segmentModel.locationURL = segmentURL 92 | 93 | self.tsSegmentArray.append(segmentModel) 94 | 95 | segmentArray.remove(at: 0) 96 | segmentArray.remove(at: 0) 97 | } 98 | 99 | self.tsPlaylist.tsSegmentArray = self.tsSegmentArray 100 | self.tsPlaylist.identifier = self.identifier 101 | let allts = self.tsSegmentArray.map { "#EXTINF:\($0.duration),\n\($0.locationURL)" }.joined(separator: "\n") 102 | self.writeToLocalM3U8file(allts: allts) 103 | } 104 | } 105 | 106 | func writeToLocalM3U8file(allts: String) { 107 | self.checkOrCreatedM3u8Directory() 108 | 109 | let filePath = getDocumentsDirectory().appendingPathComponent("m3u8Files").appendingPathComponent("\(self.tsPlaylist.identifier).m3u8") 110 | 111 | var header = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:15\n" 112 | header.append(allts) 113 | header.append("\n#EXT-X-ENDLIST\n") 114 | 115 | let writeData: Data = header.data(using: .utf8)! 116 | try! writeData.write(to: filePath) 117 | DispatchQueue.main.async { [weak self] in 118 | self?.success?(filePath) 119 | } 120 | } 121 | 122 | private func checkOrCreatedM3u8Directory() { 123 | let filePath = getDocumentsDirectory().appendingPathComponent("m3u8Files") 124 | 125 | if !FileManager.default.fileExists(atPath: filePath.path) { 126 | try! FileManager.default.createDirectory(at: filePath, withIntermediateDirectories: true, attributes: nil) 127 | } 128 | } 129 | 130 | internal func getFullPath(url: String, newUrl: String) -> String { 131 | var url = url 132 | url = url.replacingOccurrences(of: url.pathComponents.last!, with: "") 133 | return newUrl 134 | .split(separator: "/") 135 | .filter { !url.contains($0) } 136 | .reduce(url) { $0.appendingPathComponent(String($1)) } 137 | } 138 | } 139 | 140 | enum M3u8ParaseError: CustomStringConvertible, Error { 141 | /// 非法的url路径 142 | case URLInvalid 143 | /// m3u8文件内容获取失败 144 | case EmptyM3u8Content 145 | /// 没有获取到对应码率的视频 146 | case NoSteamInfo 147 | /// ts文件信息获取失败 148 | case NoEXTINFinfo 149 | case Other 150 | 151 | var description: String { 152 | switch self { 153 | case .URLInvalid: return "非法的url路径" 154 | case .EmptyM3u8Content: return "m3u8文件内容获取失败" 155 | case .NoSteamInfo: return "没有获取到对应码率的视频" 156 | case .NoEXTINFinfo: return "ts文件信息获取失败" 157 | case .Other: return "解析失败,未知错误" 158 | } 159 | } 160 | } 161 | 162 | struct M3u8TsSegmentModel { 163 | /// 起始时间 164 | var duration: Float = 0.0 165 | /// 原始地址 166 | var locationURL = "" 167 | /// 文件索引 168 | var index: Int = 0 169 | } 170 | 171 | class M3u8Playlist { 172 | var tsSegmentArray = [M3u8TsSegmentModel]() 173 | var length: Int { self.tsSegmentArray.count } 174 | var identifier = "" 175 | } 176 | 177 | public func getDocumentsDirectory() -> URL { 178 | let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) 179 | let documentsDirectory = paths[0] 180 | return documentsDirectory 181 | } 182 | -------------------------------------------------------------------------------- /DKVideo/Modules/Download/DownLoadManage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownLoadManage.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/12/16. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import RxRelay 10 | import UIKit 11 | class DownLoadManage { 12 | let downloads: BehaviorRelay<[M3U8Downloader]> = .init(value: []) 13 | static let shared = DownLoadManage() 14 | 15 | init() { 16 | let m3u8Dir = getDocumentsDirectory().appendingPathComponent("m3u8Files") 17 | if let enums = FileManager.default.enumerator(atPath: m3u8Dir.path)?.sorted(by: { 18 | ($0 as? String).unwrapped(or: "") > ($1 as? String).unwrapped(or: "") 19 | }) { 20 | enums.forEach { path in 21 | if let fileName = path as? String { 22 | if fileName.hasSuffix(".m3u8") { 23 | addDownload(fileName: fileName.replacingOccurrences(of: ".m3u8", with: ""), 24 | path: m3u8Dir.appendingPathComponent(fileName).absoluteString, 25 | autoStart: UserDefaults.autoStartDownload) 26 | } 27 | } 28 | } 29 | } 30 | } 31 | 32 | func addDownload(fileName: String, path: String, autoStart: Bool = false) { 33 | let newDownload = M3U8Downloader(fileName: fileName, m3u8URL: path) 34 | if (downloads.value.map { $0.directoryName }.contains(fileName)) { 35 | UIViewController.currentViewController()?.showAlert(title: "提示", message: "下载任务已存在", buttonTitles: ["继续下载", "重新下载"], highlightedButtonIndex: 0, completion: { [unowned self] index in 36 | if index == 1 { 37 | self.deleteDownload(fileName: fileName) 38 | newDownload.downloadStatus.filter { $0 == .started }.take(1).bind { [unowned self] _ in 39 | self.downloads.accept(self.downloads.value.filter { $0.directoryName != fileName } + [newDownload]) 40 | }.disposed(by: newDownload.rx.disposeBag) 41 | newDownload.parse(autoStart: autoStart) 42 | 43 | } else { 44 | self.downloads.value.first(where: { $0.directoryName == fileName })?.parse() 45 | } 46 | }) 47 | } else { 48 | downloads.accept(downloads.value.filter { $0.directoryName != fileName } + [newDownload]) 49 | newDownload.parse(autoStart: autoStart) 50 | } 51 | } 52 | 53 | func deleteDownload(fileName: String, success: (() -> Void)? = nil) { 54 | let filePath = getDocumentsDirectory() 55 | .appendingPathComponent("m3u8Files") 56 | .appendingPathComponent(fileName + ".m3u8") 57 | .path 58 | 59 | if FileManager.default.fileExists(atPath: filePath) { 60 | try? FileManager.default.removeItem(atPath: filePath) 61 | } 62 | 63 | deleteDownloadContent(fileName: fileName, success: success) 64 | downloads.accept(downloads.value.filter { $0.fileName != fileName }) 65 | } 66 | 67 | func deleteDownloadContent(fileName: String, success: (() -> Void)? = nil) { 68 | let filePath = getDocumentsDirectory() 69 | .appendingPathComponent("Downloads") 70 | .appendingPathComponent(fileName.replacingOccurrences(of: ".m3u8", with: "")) 71 | .path 72 | try? String(contentsOfFile: getDocumentsDirectory() 73 | .appendingPathComponent("m3u8Files") 74 | .appendingPathComponent(fileName + ".m3u8") 75 | .path) 76 | .split(separator: "\n") 77 | .filter { $0.contains("http") } 78 | .enumerated().reversed() 79 | .forEach { offset, element in 80 | appDelegate.sessionManagerBackground.remove(String(element), completely: true, onMainQueue: false) { _ in 81 | if offset == 0 {success?()} 82 | } 83 | } 84 | if FileManager.default.fileExists(atPath: filePath) { 85 | try? FileManager.default.removeItem(atPath: filePath) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /DKVideo/Modules/Download/DownloadViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadViewController.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/12/6. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import RxCocoa 10 | import RxSwift 11 | import SuperPlayer 12 | import UIKit 13 | class DownloadViewController: TableViewController { 14 | override func makeUI() { 15 | super.makeUI() 16 | navigationTitle = "下载" 17 | } 18 | 19 | override func bindViewModel() { 20 | tableView.footRefreshControl = nil 21 | tableView.headRefreshControl = nil 22 | tableView.register(cellWithClass: DownlaodCell.self) 23 | DownLoadManage.shared.downloads.asDriver().drive(tableView.rx.items(cellIdentifier: DownlaodCell.identifier, cellType: DownlaodCell.self)) { _, element, cell in 24 | cell.setup(info: element) 25 | }.disposed(by: rx.disposeBag) 26 | 27 | tableView.rx.modelSelected(M3U8Downloader.self).bind { [unowned self] info in 28 | self.loca(info: info) 29 | }.disposed(by: rx.disposeBag) 30 | } 31 | 32 | func loca(info: M3U8Downloader) { 33 | print(info.downloadStatus.value) 34 | } 35 | 36 | class DownlaodCell: TableViewCell { 37 | let stateBtn = UIButton(type: .custom) 38 | let playBtn = UIButton(type: .custom) 39 | let deleteBtn = UIButton(type: .custom) 40 | let shareBtn = UIButton(type: .custom) 41 | let progressLabel = UILabel(fontSize: 12, textColor: .textGray(), text: "已下载:1.00%") 42 | let nameLabel = UILabel(fontSize: 14, text: "文件1") 43 | override func makeUI() { 44 | super.makeUI() 45 | containerView.snp.makeConstraints { make in 46 | make.height.equalTo(65).priority(.high) 47 | } 48 | containerView.addSubviews([stateBtn, playBtn, deleteBtn, shareBtn, progressLabel, nameLabel]) 49 | stateBtn.snp.makeConstraints { make in 50 | make.left.top.equalTo(10) 51 | make.width.height.equalTo(45) 52 | } 53 | nameLabel.snp.makeConstraints { make in 54 | make.top.equalTo(10) 55 | make.left.equalTo(stateBtn.snp.right).offset(15) 56 | make.right.equalToSuperview() 57 | } 58 | progressLabel.snp.makeConstraints { make in 59 | make.left.equalTo(nameLabel) 60 | make.top.equalTo(nameLabel.snp.bottom).offset(10) 61 | } 62 | playBtn.snp.makeConstraints { make in 63 | make.right.equalTo(-150) 64 | make.top.equalTo(nameLabel.snp.bottom).offset(3) 65 | make.size.equalTo(CGSize(width: 50, height: 30)) 66 | } 67 | shareBtn.snp.makeConstraints { make in 68 | make.right.equalTo(-80) 69 | make.top.equalTo(nameLabel.snp.bottom).offset(3) 70 | make.size.equalTo(CGSize(width: 50, height: 30)) 71 | } 72 | shareBtn.setTitle("分享", for: []) 73 | playBtn.setTitle("播放", for: []) 74 | deleteBtn.setTitle("删除", for: []) 75 | deleteBtn.snp.makeConstraints { make in 76 | make.right.equalTo(-10) 77 | make.top.equalTo(nameLabel.snp.bottom).offset(3) 78 | make.size.equalTo(CGSize(width: 50, height: 30)) 79 | } 80 | playBtn.borderWidth = 0.5 81 | shareBtn.borderWidth = 0.5 82 | deleteBtn.borderWidth = 0.5 83 | deleteBtn.borderColor = .red 84 | deleteBtn.setTitleColor(.red, for: []) 85 | themeService.rx 86 | .bind({ $0.secondary }, to: [playBtn.rx.titleColor(for: []), playBtn.rx.borderColor, 87 | shareBtn.rx.titleColor(for: []), shareBtn.rx.borderColor]) 88 | .bind({ $0.secondaryDark }, to: stateBtn.rx.titleColor(for: [])) 89 | .bind({ $0.primary }, to: containerView.rx.backgroundColor) 90 | .disposed(by: rx.disposeBag) 91 | stateBtn.titleLabel?.font = .systemFont(ofSize: 9) 92 | } 93 | 94 | func setup(info: M3U8Downloader) { 95 | info.downloadStatus.map { $0.description }.bind(to: stateBtn.rx.title(for: [])).disposed(by: disposeBag) 96 | nameLabel.text = info.directoryName 97 | info.progress.map { "已下载\(Int($0 * 100))%" }.bind(to: progressLabel.rx.text).disposed(by: disposeBag) 98 | playBtn.rx.tap.bind { _ in 99 | if let playPath = info.getPlayPath() { 100 | VideoPlayerVC.shared.urlStr = playPath 101 | VideoPlayerVC.show() 102 | } 103 | }.disposed(by: disposeBag) 104 | stateBtn.rx.tap.bind { _ in 105 | if info.downloadStatus.value == .started { 106 | info.downloader.cancelDownloadSegment() 107 | } else { 108 | info.downloader.startDownload() 109 | } 110 | }.disposed(by: disposeBag) 111 | deleteBtn.rx.tap.bind { _ in 112 | UIViewController.currentViewController()?.showAlert(title: "确认删除", message: "将要删除下载内容", buttonTitles: ["重新下载", "删除内容", "取消"], highlightedButtonIndex: 0, completion: { index in 113 | if index == 0 { 114 | DownLoadManage.shared.deleteDownloadContent(fileName: info.fileName) { 115 | info.parse() 116 | } 117 | } else if index == 1 { 118 | DownLoadManage.shared.deleteDownload(fileName: info.fileName) 119 | } 120 | }) 121 | }.disposed(by: disposeBag) 122 | shareBtn.rx.tap.bind { _ in 123 | var playPath = info.getPlayPath() 124 | UIViewController.currentViewController()?.showAlert(title: "重要提示", message: "已复制播放地址,您可以发送给局域网(同一WIFI下)的好友,她可以直接播放无需下载,如果您正在使用移动热点,分享及播放不会消耗流量。同一时间您只能分享一集(好友收看期间您可以收看同一集,播放其他视频,好友可能无法继续观看)", buttonTitles: ["确定"], highlightedButtonIndex: 0, completion: { _ in 125 | if let ip = VideoPlayServer.currentServer?.serverURL?.absoluteString { 126 | playPath = playPath?.replacingOccurrences(of: "http://127.0.0.1:8080/", with: ip) 127 | } 128 | UIPasteboard.general.string = playPath 129 | let url = getDocumentsDirectory() 130 | .appendingPathComponent("Downloads") 131 | .appendingPathComponent(info.fileName) 132 | .appendingPathComponent(info.fileName + ".m3u8") 133 | self.shareM3u8(fileUrl: url) // URL(string: playPath!) 134 | 135 | }) 136 | 137 | }.disposed(by: disposeBag) 138 | } 139 | 140 | func shareM3u8(fileUrl: URL?) { 141 | guard let fileUrl = fileUrl else { return } 142 | let activityVC = UIActivityViewController(activityItems: [fileUrl], applicationActivities: nil) 143 | activityVC.popoverPresentationController?.sourceView = self.contentView 144 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 145 | UIViewController.currentViewController()!.present(activityVC, animated: true, completion: nil) 146 | } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /DKVideo/Modules/Download/M3U8Downloader.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import GCDWebServer 4 | import RxRelay 5 | open class M3U8Downloader: NSObject { 6 | public let downloader = VideoDownloader() 7 | public let progress: BehaviorRelay = .init(value: 0.0) 8 | public var fileName = "" 9 | var directoryName: String { 10 | self.fileName.replacingOccurrences(of: ".m3u8", with: "") 11 | } 12 | 13 | public var m3u8URL = "" 14 | private let m3u8Parser = DKM3u8Helper() 15 | public var downloadStatus: BehaviorRelay = .init(value: .paused) 16 | 17 | public init(fileName: String, m3u8URL: String) { 18 | self.fileName = fileName 19 | self.m3u8URL = m3u8URL 20 | super.init() 21 | self.downloader.downloadStatus.bind(to: self.downloadStatus).disposed(by: self.rx.disposeBag) 22 | self.downloader.progress.bind(to: self.progress).disposed(by: self.rx.disposeBag) 23 | } 24 | 25 | open func parse(autoStart: Bool = true) { 26 | DispatchQueue.global().async { [unowned self] in 27 | self.m3u8Parser.parser(url: self.m3u8URL, name: self.fileName, success: { [weak self] _ in 28 | guard let self = self else { return } 29 | self.downloader.tsPlaylist = self.m3u8Parser.tsPlaylist 30 | self.downloader.m3u8Data = self.m3u8Parser.m3u8Data 31 | if autoStart { self.downloader.startDownload() } 32 | }) { [weak self] error in 33 | print(error) 34 | showMessage(message: error.description) 35 | self?.downloadStatus.accept(.failed) 36 | } 37 | } 38 | } 39 | 40 | func getPlayPath() -> String? { 41 | return VideoPlayServer.getServer(name: self.directoryName) 42 | } 43 | } 44 | 45 | class VideoPlayServer { 46 | static var currentServer: GCDWebDAVServer? 47 | public static func getServer(name: String) -> String? { 48 | self.currentServer?.stop() 49 | let dirPath = getDocumentsDirectory().appendingPathComponent("Downloads").appendingPathComponent(name).path 50 | self.currentServer = GCDWebDAVServer(uploadDirectory: dirPath) 51 | // self.currentServer?.start(withPort: 8080, bonjourName: nil) 52 | try? self.currentServer?.start(options: ["Port": 8080, "AutomaticallySuspendInBackground": false]) 53 | let playPath = "http://127.0.0.1:8080/" + name + ".m3u8" 54 | return playPath 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /DKVideo/Modules/Download/SegmentDownloader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Tiercel 3 | protocol SegmentDownloaderDelegate { 4 | func segmentDownloadSucceeded(with downloader: SegmentDownloader) 5 | func segmentDownloadFailed(with downloader: SegmentDownloader) 6 | } 7 | 8 | class SegmentDownloader: NSObject { 9 | var fileName: String 10 | var filePath: String 11 | var downloadURL: String 12 | var duration: Float 13 | var index: Int 14 | 15 | lazy var downloadSession: SessionManager = { 16 | appDelegate.sessionManagerBackground 17 | }() 18 | 19 | var downloadTask: DownloadTask? 20 | var isDownloading = false 21 | var finishedDownload = false 22 | 23 | var delegate: SegmentDownloaderDelegate? 24 | 25 | init(with url: String, filePath: String, fileName: String, duration: Float, index: Int) { 26 | downloadURL = url 27 | self.filePath = filePath 28 | self.fileName = fileName 29 | self.duration = duration 30 | self.index = index 31 | } 32 | 33 | func startDownload() { 34 | if checkIfIsDownloaded() { 35 | finishedDownload = true 36 | delegate?.segmentDownloadSucceeded(with: self) 37 | } else { 38 | let url = downloadURL.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! 39 | guard let taskURL = URL(string: url) else { return } 40 | isDownloading = true 41 | downloadTask = downloadSession.download(taskURL, headers: nil, fileName: fileName) 42 | downloadTask?.success { [weak self] task in 43 | guard task.status == .succeeded else{return} 44 | guard let self = self else { return } 45 | let destinationURL = self.generateFilePath() 46 | 47 | self.finishedDownload = true 48 | self.isDownloading = false 49 | 50 | if FileManager.default.fileExists(atPath: destinationURL.path) { 51 | return 52 | } else { 53 | do { 54 | let furl = URL(fileURLWithPath: task.filePath) 55 | try FileManager.default.moveItem(at: furl, to: destinationURL) 56 | self.delegate?.segmentDownloadSucceeded(with: self) 57 | } catch let error as NSError { 58 | print(error.localizedDescription) 59 | } 60 | } 61 | }.failure { [weak self] _ in 62 | guard let self = self else { return } 63 | self.finishedDownload = false 64 | self.isDownloading = false 65 | self.delegate?.segmentDownloadFailed(with: self) 66 | } 67 | } 68 | } 69 | 70 | func cancelDownload() { 71 | downloadSession.cancel(downloadURL) 72 | isDownloading = false 73 | } 74 | 75 | func pauseDownload() { 76 | downloadSession.suspend(downloadURL) 77 | isDownloading = false 78 | } 79 | 80 | func resumeDownload() { 81 | downloadSession.start(downloadURL) 82 | isDownloading = true 83 | } 84 | 85 | func checkIfIsDownloaded() -> Bool { 86 | let filePath = generateFilePath().path 87 | 88 | if FileManager.default.fileExists(atPath: filePath) { 89 | return true 90 | } else { 91 | return false 92 | } 93 | } 94 | 95 | func generateFilePath() -> URL { 96 | return getDocumentsDirectory().appendingPathComponent("Downloads").appendingPathComponent(filePath).appendingPathComponent(fileName) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /DKVideo/Modules/Download/VideoDownloader.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | import Foundation 4 | import RxRelay 5 | public enum Status: CustomStringConvertible { 6 | case started 7 | case paused 8 | case canceled 9 | case finished 10 | case failed 11 | 12 | public var description: String { 13 | switch self { 14 | case .started: return "下载中" 15 | case .paused: return "已暂停" 16 | case .canceled: return "已取消" 17 | case .finished: return "下载完成" 18 | case .failed: return "已停止" 19 | } 20 | } 21 | } 22 | 23 | open class VideoDownloader { 24 | public var downloadStatus: BehaviorRelay = .init(value: .paused) 25 | public var progress: BehaviorRelay = .init(value: 0) 26 | 27 | var m3u8Data: String = "" 28 | var tsPlaylist = M3u8Playlist() 29 | var segmentDownloaders = [SegmentDownloader]() 30 | var tsFilesIndex = 0 31 | var neededDownloadTsFilesCount = 0 32 | /// 所有TS任务的下载地址【包括下载、未下载的】 33 | var downloadURLs = [String]() 34 | var downloadingProgress: Float { 35 | let finishedDownloadFilesCount = segmentDownloaders.filter { $0.finishedDownload == true }.count 36 | let fraction = Float(finishedDownloadFilesCount) / Float(neededDownloadTsFilesCount) 37 | let roundedValue = round(fraction * 100) / 100 38 | return roundedValue 39 | } 40 | 41 | fileprivate var startDownloadIndex = 0 42 | 43 | open func startDownload() { 44 | checkOrCreatedM3u8Directory() 45 | 46 | var newSegmentArray = [M3u8TsSegmentModel]() 47 | 48 | let notInDownloadList = tsPlaylist.tsSegmentArray.filter { !downloadURLs.contains($0.locationURL) } 49 | neededDownloadTsFilesCount = tsPlaylist.length 50 | // 将m3u8中的ts转化成下载任务 51 | for i in 0 ..< notInDownloadList.count { 52 | let fileName = "\(tsFilesIndex).ts" 53 | 54 | let segmentDownloader = SegmentDownloader(with: notInDownloadList[i].locationURL, 55 | filePath: tsPlaylist.identifier, 56 | fileName: fileName, 57 | duration: notInDownloadList[i].duration, 58 | index: tsFilesIndex) 59 | segmentDownloader.delegate = self 60 | 61 | segmentDownloaders.append(segmentDownloader) 62 | downloadURLs.append(notInDownloadList[i].locationURL) 63 | 64 | var segmentModel = M3u8TsSegmentModel() 65 | segmentModel.duration = segmentDownloaders[i].duration 66 | segmentModel.locationURL = segmentDownloaders[i].fileName 67 | segmentModel.index = segmentDownloaders[i].index 68 | newSegmentArray.append(segmentModel) 69 | 70 | tsPlaylist.tsSegmentArray = newSegmentArray 71 | 72 | tsFilesIndex += 1 73 | } 74 | // 开启下载任务 75 | segmentDownloaders[0 ..< UserDefaults.maxDownloadTS].forEach { $0.startDownload() } 76 | // 更新任务状态 77 | downloadStatus.accept(.started) 78 | } 79 | 80 | func checkDownloadQueue() {} 81 | 82 | func updateLocalM3U8file() { 83 | checkOrCreatedM3u8Directory() 84 | 85 | let filePath = getDocumentsDirectory().appendingPathComponent("Downloads").appendingPathComponent(tsPlaylist.identifier).appendingPathComponent("\(tsPlaylist.identifier).m3u8") 86 | 87 | var header = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:15\n" 88 | var content = "" 89 | 90 | for i in 0 ..< tsPlaylist.tsSegmentArray.count { 91 | let segmentModel = tsPlaylist.tsSegmentArray[i] 92 | 93 | let length = "#EXTINF:\(segmentModel.duration),\n" 94 | let fileName = "http://127.0.0.1:8080/\(segmentModel.index).ts\n" 95 | content += (length + fileName) 96 | } 97 | 98 | header.append(content) 99 | header.append("#EXT-X-ENDLIST\n") 100 | 101 | let writeData: Data = header.data(using: .utf8)! 102 | if !FileManager.default.fileExists(atPath: filePath.path) { 103 | try! writeData.write(to: filePath) 104 | } 105 | } 106 | 107 | private func checkOrCreatedM3u8Directory() { 108 | let filePath = getDocumentsDirectory() 109 | .appendingPathComponent("Downloads") 110 | .appendingPathComponent(tsPlaylist.identifier) 111 | 112 | if !FileManager.default.fileExists(atPath: filePath.path) { 113 | try! FileManager.default.createDirectory(at: filePath, withIntermediateDirectories: true, attributes: nil) 114 | } 115 | } 116 | 117 | open func pauseDownloadSegment() { 118 | segmentDownloaders.forEach { $0.pauseDownload() } 119 | downloadStatus.accept(.paused) 120 | } 121 | 122 | open func cancelDownloadSegment() { 123 | segmentDownloaders.forEach { $0.cancelDownload() } 124 | downloadStatus.accept(.canceled) 125 | } 126 | 127 | open func resumeDownloadSegment() { 128 | segmentDownloaders.forEach { $0.resumeDownload() } 129 | downloadStatus.accept(.started) 130 | } 131 | } 132 | 133 | extension VideoDownloader: SegmentDownloaderDelegate { 134 | func segmentDownloadSucceeded(with downloader: SegmentDownloader) { 135 | let finishedDownloadFilesCount = segmentDownloaders.filter { $0.finishedDownload == true }.count 136 | progress.accept(downloadingProgress) 137 | updateLocalM3U8file() 138 | 139 | let downloadingFilesCount = segmentDownloaders.filter { $0.isDownloading == true }.count 140 | 141 | if finishedDownloadFilesCount == neededDownloadTsFilesCount { 142 | //全部下载完成 143 | downloadStatus.accept(.finished) 144 | } else if startDownloadIndex == neededDownloadTsFilesCount - 1 { 145 | //所有任务,只剩下载完成的,和下载中的 146 | } else if downloadingFilesCount < UserDefaults.maxDownloadTS || finishedDownloadFilesCount != neededDownloadTsFilesCount { 147 | //还有任务未开始下载 148 | if startDownloadIndex < neededDownloadTsFilesCount - 1 { 149 | startDownloadIndex += 1 150 | } 151 | segmentDownloaders[safe: startDownloadIndex]?.startDownload() 152 | } 153 | } 154 | 155 | func segmentDownloadFailed(with downloader: SegmentDownloader) { 156 | downloadStatus.accept(.failed) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /DKVideo/Modules/Home/HomeTabbarVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeTabbarVC.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/12/6. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class HomeTabbarVC: UITabBarController { 12 | let homeVC = NavigationController(rootViewController: HomeViewController()) 13 | let downloadVC = NavigationController(rootViewController: DownloadViewController()) 14 | let settingVC = NavigationController(rootViewController: SettingViewController()) 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | viewControllers = [homeVC, downloadVC, settingVC] 18 | homeVC.tabBarItem.image = R.image.icon_navigation_web() 19 | homeVC.tabBarItem.title = "首页" 20 | downloadVC.tabBarItem.image = R.image.icon_cell_dir() 21 | downloadVC.tabBarItem.title = "下载" 22 | settingVC.tabBarItem.image = R.image.icon_tabbar_settings() 23 | settingVC.tabBarItem.title = "设置" 24 | 25 | // homeVC.tabBarItem 26 | 27 | themeService.rx 28 | .bind({ $0.textGray }, to: tabBar.rx.unselectedItemTintColor) 29 | .bind({ $0.secondary }, to: tabBar.rx.tintColor) 30 | .bind({ $0.primary }, to: tabBar.rx.barTintColor) 31 | .disposed(by: rx.disposeBag) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /DKVideo/Modules/Home/HomeViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeViewController.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/12/6. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | class HomeViewController: ViewController { 11 | lazy var searchBar: UIView = { 12 | let searchBar = UIView() 13 | let textView = UITextField(placeholder: "请输入地址", placeholderSize: 14) 14 | searchBar.backgroundColor = UIColor.white 15 | searchBar.addSubview(textView) 16 | textView.snp.makeConstraints { make in 17 | make.edges.equalTo(UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)) 18 | } 19 | let imgV = UIImageView(image: R.image.icon_tabbar_search()) 20 | textView.leftView = imgV 21 | textView.clearButtonMode = .whileEditing 22 | textView.leftViewMode = .always 23 | imgV.frame = CGRect(x: 10, y: 10, width: 34, height: 14) 24 | textView.font = UIFont.systemFont(ofSize: 14) 25 | imgV.contentMode = .center 26 | textView.rx.controlEvent(.editingDidEndOnExit).bind(onNext: { [unowned self] _ in 27 | if let url = URL(string: textView.text!) { 28 | currentWebVC.show(requestUrl: url) 29 | } else { 30 | showMessage(message: "请输入正确的地址") 31 | } 32 | }).disposed(by: rx.disposeBag) 33 | themeService.rx 34 | .bind({ $0.background }, to: textView.rx.backgroundColor) 35 | .bind({ $0.background }, to: searchBar.rx.backgroundColor) 36 | .bind({ $0.textGray }, to: textView.rx.placeholderColor) 37 | .disposed(by: self.rx.disposeBag) 38 | return searchBar 39 | }() 40 | 41 | lazy var collectionView: UICollectionView = { 42 | let layout = UICollectionViewFlowLayout() 43 | layout.scrollDirection = .vertical 44 | layout.sectionInset = .zero 45 | layout.minimumLineSpacing = 1 46 | layout.minimumInteritemSpacing = 1 47 | layout.itemSize = CGSize(width: screenWidth / 2 - 1, height: 120) 48 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) 49 | collectionView.backgroundColor = .white 50 | collectionView.showsHorizontalScrollIndicator = false 51 | collectionView.register(cellWithClass: MenuItem.self) 52 | return collectionView 53 | }() 54 | 55 | override func makeUI() { 56 | super.makeUI() 57 | navigationTitle = "首页" 58 | contentView.addSubview(searchBar) 59 | searchBar.snp.makeConstraints { make in 60 | make.top.equalToSuperview() 61 | make.height.equalTo(50) 62 | make.left.right.equalToSuperview() 63 | } 64 | contentView.addSubview(collectionView) 65 | collectionView.snp.makeConstraints { make in 66 | make.edges.equalTo(UIEdgeInsets(top: 55, left: 0, bottom: 0, right: 0)) 67 | } 68 | themeService 69 | .rx 70 | .bind({ $0.background }, to: collectionView.rx.backgroundColor) 71 | .disposed(by: rx.disposeBag) 72 | } 73 | 74 | override func bindViewModel() { 75 | let subject = PublishSubject<[SubMenu]>() 76 | let str = (try? String(contentsOf: R.file.platformJson()!)) ?? "" 77 | let arr = JSON(parseJSON: str).arrayValue 78 | let menus = arr.map(SubMenu.from(json:)) 79 | 80 | subject 81 | .bind(to: collectionView.rx.items(cellIdentifier: String(describing: MenuItem.self), cellType: MenuItem.self)) { _, model, cell in 82 | cell.setup(data: model) 83 | }.disposed(by: rx.disposeBag) 84 | subject.onNext(menus) 85 | collectionView.rx.modelSelected(SubMenu.self).bind { item in 86 | if let url = item.url { 87 | currentWebVC.show(requestUrl: url) 88 | } 89 | // self.navigationController?.pushViewController(TestWebVC()) 90 | }.disposed(by: rx.disposeBag) 91 | } 92 | 93 | override func viewWillAppear(_ animated: Bool) { 94 | super.viewWillAppear(animated) 95 | if waitToPresentVC != nil { 96 | present(waitToPresentVC!, animated: true, completion: nil) 97 | waitToPresentVC = nil 98 | } 99 | } 100 | } 101 | 102 | extension HomeViewController { 103 | struct SubMenu: HandyJSON { 104 | var url: URL? 105 | var image: URL? 106 | static func from(json: JSON) -> SubMenu { 107 | var menu = SubMenu() 108 | menu.url = json["url"].url 109 | menu.image = json["image"].url 110 | return menu 111 | } 112 | } 113 | 114 | /// 菜单cell 115 | class MenuItem: UICollectionViewCell { 116 | let titleLab = UILabel(fontSize: 16, textColor: .darkGray) 117 | let imageView = UIImageView(frame: .zero) 118 | 119 | override init(frame: CGRect) { 120 | super.init(frame: frame) 121 | contentView.addSubviews([titleLab, imageView]) 122 | themeService.rx 123 | .bind({ $0.primaryDark }, to: rx.backgroundColor) 124 | .bind({ $0.text }, to: titleLab.rx.textColor) 125 | .disposed(by: rx.disposeBag) 126 | imageView.snp.makeConstraints { make in 127 | make.edges.equalTo(UIEdgeInsets(top: 10, left: 15, bottom: 10, right: 15)) 128 | } 129 | imageView.contentMode = .scaleAspectFit 130 | 131 | titleLab.snp.makeConstraints { 132 | $0.center.equalToSuperview() 133 | } 134 | } 135 | 136 | func setup(data: SubMenu) { 137 | imageView.sd_setImage(with: data.image, completed: nil) 138 | //titleLab.text = String(data.url?.absoluteString.dropLast(5).dropFirst(11) ?? "") 139 | } 140 | 141 | required init?(coder aDecoder: NSCoder) { 142 | super.init(coder: aDecoder) 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /DKVideo/Modules/Settings/SettingViewCells.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingViewCells.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/12/6. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import RxRelay 10 | import UIKit 11 | extension SettingViewController { 12 | class SwitchCell: BaseTableViewCell { 13 | let aSwitch = UISwitch() 14 | override func makeUI() { 15 | super.makeUI() 16 | leftImageView.contentMode = .center 17 | leftImageView.snp.remakeConstraints { make in 18 | make.size.equalTo(35) 19 | } 20 | rightImageView.contentMode = .center 21 | rightImageView.snp.remakeConstraints { make in 22 | make.size.equalTo(25) 23 | } 24 | stackView.insertArrangedSubview(aSwitch, at: 2) 25 | 26 | themeService.rx 27 | .bind({ $0.secondary }, to: [aSwitch.rx.tintColor, aSwitch.rx.onTintColor]) 28 | .bind({ $0.secondary }, to: leftImageView.rx.tintColor) 29 | .bind({ $0.primary }, to: containerView.rx.backgroundColor) 30 | .disposed(by: rx.disposeBag) 31 | } 32 | 33 | override func bindViewModel(viewModel: TableCellViewModel) { 34 | super.bindViewModel(viewModel: viewModel) 35 | guard let viewModel = viewModel as? SettingSwitchCellViewModel else { return } 36 | viewModel.isEnabled.asDriver().drive(aSwitch.rx.isOn).disposed(by: disposeBag) 37 | aSwitch.rx.isOn.bind(to: viewModel.isEnabled).disposed(by: disposeBag) 38 | aSwitch.rx.isOn.bind(to: viewModel.switchChanged).disposed(by:disposeBag) 39 | } 40 | } 41 | 42 | class TextCell: BaseTableViewCell { 43 | override func makeUI() { 44 | super.makeUI() 45 | leftImageView.contentMode = .center 46 | leftImageView.snp.remakeConstraints { make in 47 | make.size.equalTo(35) 48 | } 49 | rightImageView.contentMode = .center 50 | rightImageView.snp.remakeConstraints { make in 51 | make.size.equalTo(25) 52 | } 53 | themeService.rx 54 | .bind({ $0.primary }, to: containerView.rx.backgroundColor) 55 | .disposed(by: rx.disposeBag) 56 | } 57 | } 58 | } 59 | 60 | class SettingCellViewModel: TableCellViewModel { 61 | init(with title: String, detail: String?, image: UIImage?, hidesDisclosure: Bool) { 62 | super.init() 63 | self.title.accept(title) 64 | self.detail.accept(detail) 65 | self.image.accept(image) 66 | self.hidesDisclosure.accept(hidesDisclosure) 67 | } 68 | } 69 | 70 | class SettingSwitchCellViewModel: SettingCellViewModel { 71 | let isEnabled = BehaviorRelay(value: false) 72 | 73 | let switchChanged: AnyObserver 74 | init(with title: String, detail: String?, image: UIImage?, isEnabled: Bool, valueChanged: @escaping (Bool) -> Void) { 75 | self.isEnabled.accept(isEnabled) 76 | self.switchChanged = .init(eventHandler: { event in 77 | switch event { 78 | case let .next(value): 79 | valueChanged(value) 80 | case .completed: break 81 | case .error: break 82 | } 83 | }) 84 | super.init(with: title, detail: detail, image: image, hidesDisclosure: true) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /DKVideo/Modules/Settings/SettingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingViewController.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/12/6. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import RxDataSources 10 | import UIKit 11 | class SettingViewController: TableViewController { 12 | var viewModle = SettingViewModel() 13 | 14 | override func makeUI() { 15 | super.makeUI() 16 | navigationTitle = "设置" 17 | tableView.headRefreshControl = nil 18 | tableView.footRefreshControl = nil 19 | } 20 | 21 | override func bindViewModel() { 22 | super.bindViewModel() 23 | tableView.register(cellWithClass: TextCell.self) 24 | tableView.register(cellWithClass: SwitchCell.self) 25 | let output = viewModle.transform(input: .init()) 26 | 27 | let datasource = RxTableViewSectionedReloadDataSource.init(configureCell: { (_, tableView, indexPath, item) -> UITableViewCell in 28 | switch item { 29 | case let .text(viewModel: cellModel): 30 | let cell = tableView.dequeueReusableCell(withClass: TextCell.self, for: indexPath) 31 | cell.bindViewModel(viewModel: cellModel) 32 | return cell 33 | case let .selects(viewModel: cellModel): 34 | let cell = tableView.dequeueReusableCell(withClass: SwitchCell.self, for: indexPath) 35 | cell.bindViewModel(viewModel: cellModel) 36 | return cell 37 | } 38 | 39 | }, titleForHeaderInSection: { (source, index) -> String? in 40 | source[index].title 41 | }) 42 | output.items.drive(tableView.rx.items(dataSource: datasource)).disposed(by: rx.disposeBag) 43 | 44 | tableView.rx.modelSelected(SettingItem.self).bind { [unowned self] item in 45 | if item.viewModel.title.value == "主题设置" { 46 | let themes = ColorTheme.allValues.map { CommonListData(id: $0.rawValue.string, text: $0.title, selected: UserDefaults.standard.themeColor == $0.rawValue, icon: UIImage(color: $0.color, size: CGSize(width: 30, height: 30))) } 47 | 48 | showSelectVC(inVC: self, height: CGFloat(themes.count * 44), listDataProvider: { list in 49 | list = themes 50 | }) { list in 51 | let theme = ColorTheme(rawValue: Int(list.first!.id)!)! 52 | themeService.switch(ThemeType.currentTheme().withColor(color: theme)) 53 | } 54 | } else if item.viewModel.title.value == "关于" { 55 | let webVC = WebViewController() 56 | webVC.requestURL = URL(string: "https://www.jianshu.com/p/f9d06ed27f24") 57 | self.navigationController?.pushViewController(webVC) 58 | } else if item.viewModel.title.value == "最大下载线程数" { 59 | self.changeMaxTs(vm: item.viewModel) 60 | } 61 | }.disposed(by: rx.disposeBag) 62 | tableView.rx.itemSelected.bind { [unowned self] indexPath in 63 | self.tableView.deselectRow(at: indexPath, animated: true) 64 | }.disposed(by: rx.disposeBag) 65 | } 66 | 67 | func changeMaxTs(vm: SettingCellViewModel) { 68 | let alert = UIAlertController(title: "最大下载线程数", message: nil, preferredStyle: .alert) 69 | alert.addTextField(text: UserDefaults.maxDownloadTS.string, placeholder: nil, editingChangedTarget: nil, editingChangedSelector: nil) 70 | alert.addAction(title: "确定", style: .default, isEnabled: true) { _ in 71 | UserDefaults.maxDownloadTS = alert.textFields?.first?.text?.int ?? 3 72 | vm.detail.accept(UserDefaults.maxDownloadTS.string) 73 | } 74 | alert.addAction(title: "取消", style: .cancel, isEnabled: true, handler: nil) 75 | alert.show() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /DKVideo/Modules/Settings/SettingViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingViewModel.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/12/18. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import RxCocoa 10 | import RxDataSources 11 | 12 | enum SettingSection: SectionModelType { 13 | case detail(title: String, items: [SettingItem]) 14 | 15 | typealias Item = SettingItem 16 | 17 | var title: String { 18 | switch self { 19 | case .detail(let title, _): return title 20 | } 21 | } 22 | 23 | var items: [SettingItem] { 24 | switch self { 25 | case .detail(_, let items): return items.map { $0 } 26 | } 27 | } 28 | 29 | init(original: SettingSection, items: [Item]) { 30 | switch original { 31 | case .detail(let title, let items): self = .detail(title: title, items: items) 32 | } 33 | } 34 | } 35 | 36 | enum SettingItem { 37 | case text(viewModel: SettingCellViewModel) 38 | case selects(viewModel: SettingSwitchCellViewModel) 39 | 40 | var viewModel: SettingCellViewModel { 41 | switch self { 42 | case .text(let viewModel): return viewModel 43 | case .selects(let viewModel): return viewModel 44 | } 45 | } 46 | } 47 | 48 | class SettingViewModel: ViewModel { 49 | struct Input {} 50 | 51 | struct Output { 52 | let items: Driver<[SettingSection]> 53 | // ["夜间模式","主题设置","开启时自动下载","使用WKWebview","浏览桌面版网页","显示解析视图","清除缓存","移动网络下载","关于"] 54 | } 55 | 56 | func transform(input: Input) -> Output { 57 | let items: [SettingSection] = [ 58 | .detail(title: "主题设置", items: [ 59 | .text(viewModel: .init(with: "主题设置", detail: nil, image: R.image.icon_cell_theme()?.template, hidesDisclosure: false)), 60 | .selects(viewModel: .init(with: "夜间模式", detail: nil, image: R.image.icon_cell_night_mode()?.template, isEnabled: ThemeType.currentTheme().isDark, valueChanged: { isDark in 61 | if ThemeType.currentTheme().isDark != isDark { 62 | themeService.switch(ThemeType.currentTheme().toggled()) 63 | } 64 | })) 65 | ]), 66 | .detail(title: "网页设置", items: [ 67 | .selects(viewModel: .init(with: "使用WKWebview", detail: "性能好,兼容差", image: R.image.icon_navigation_web()?.template, isEnabled: UserDefaults.useWKWebview, valueChanged: { UserDefaults.useWKWebview = $0 })), 68 | .selects(viewModel: .init(with: "浏览桌面版网页", detail: "", image: R.image.icon_navigation_web()?.template, isEnabled: UserDefaults.isPCAgent, valueChanged: { UserDefaults.isPCAgent = $0 })), 69 | .selects(viewModel: .init(with: "显示解析视图", detail: "", image: R.image.icon_navigation_web()?.template, isEnabled: UserDefaults.showVipWebView, valueChanged: { UserDefaults.showVipWebView = $0 })) 70 | ]), 71 | .detail(title: "其他", items: [ 72 | .text(viewModel: .init(with: 73 | "清除缓存", detail: "", image: R.image.icon_cell_dir()?.template, hidesDisclosure: false)), 74 | .text(viewModel: SettingCellViewModel(with: "最大下载线程数", detail: UserDefaults.maxDownloadTS.string, image: R.image.icon_navigation_refresh()?.template, hidesDisclosure: false)), 75 | .selects(viewModel: .init(with: "打开APP时自动下载", detail: nil, image: R.image.icon_navigation_refresh()?.template, isEnabled: UserDefaults.autoStartDownload, valueChanged: { UserDefaults.autoStartDownload = $0 })), 76 | .selects(viewModel: .init(with: "后台时下载", detail: "下次启动生效", image: R.image.icon_navigation_refresh()?.template, isEnabled: UserDefaults.backgroundDownload, valueChanged: { UserDefaults.backgroundDownload = $0 })), 77 | .selects(viewModel: .init(with: "使用流量下载", detail: "下次启动生效", image: R.image.icon_navigation_refresh()?.template, isEnabled: UserDefaults.downloadWithoutWifi, valueChanged: { UserDefaults.downloadWithoutWifi = $0 })), 78 | .text(viewModel: .init(with: 79 | "关于", detail: "", image: R.image.icon_cell_dir()?.template, hidesDisclosure: false)) 80 | ]) 81 | ] 82 | return Output(items: Driver.just(items)) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /DKVideo/Resources/AdStringFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdStringFile.swift 3 | // DKVideo 4 | // 5 | // Created by 朱德坤 on 2019/12/17. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | let adString = ["ajiajdiji"] 12 | var bccc = [ 13 | // 优酷 14 | "atm.youku.com", "Fvid.atm.youku.com", "html.atm.youku.com", "valb.atm.youku.com", "valf.atm.youku.com", "valo.atm.youku.com", "valp.atm.youku.com", "lstat.youku.com", "speed.lstat.youku.com", "urchin.lstat.youku.com", "stat.youku.com", "static.lstat.youku.com", "valc.atm.youku.com", "vid.atm.youku.com", "walp.atm.youku.com", 15 | // 百度: 16 | // "a.baidu.com", "baidutv.baidu.com", "bar.baidu.com", "c.baidu.com", "cjhq.baidu.com", "cpro.baidu.com", "drmcmm.baidu.com", "e.baidu.com", "eiv.baidu.com", "hc.baidu.com", "hm.baidu.com", "ma.baidu.com", "nsclick.baidu.com", "spcode.baidu.com", "tk.baidu.com", "union.baidu.com", "ucstat.baidu.com", "utility.baidu.com", "utk.baidu.com", "focusbaiduafp.allyes.com", 17 | 18 | // 奇艺", 19 | // "afp.qiyi.com", "focusbaiduafp.allyes.com", 20 | 21 | // CNTV" 22 | "a.cctv.com", "a.cntv.cn", "ad.cctv.com", "d.cntv.cn", "adguanggao.eee114.com", "cctv.adsunion.com", 23 | 24 | // 新浪视频", 25 | "dcads.sina.com.cn", 26 | 27 | // pptv" 28 | "pp2.pptv.com", 29 | 30 | // 乐视" 31 | "pro.hoye.letv.com", 32 | 33 | // 搜狐高清" 34 | "images.sohu.com", 35 | 36 | // 我乐网" 37 | // "acs.56.com", "acs.agent.56.com", "acs.agent.v-56.com", "bill.agent.56.com", "bill.agent.v-56.com", "stat.56.com", "stat2.corp.56.com", "union.56.com", "uvimage.56.com", "v16.56.com", 38 | 39 | // 6间房" 40 | // "pole.6rooms.com", "shrek.6.cn", "simba.6.cn", "union.6.cn", 41 | 42 | // 土豆网" 43 | // "adextensioncontrol.tudou.com", "iwstat.tudou.com", "nstat.tudou.com", "stats.tudou.com", "*.p2v.tudou.com*", "at-img1.tdimg.com", "at-img2.tdimg.com", "at-img3.tdimg.com", "adplay.tudou.com", "adcontrol.tudou.com", "stat.tudou.com", 44 | 45 | // 酷6网" 46 | // "1.allyes.com.cn", "analytics.ku6.com", "gug.ku6cdn.com", "ku6.allyes.com", "ku6afp.allyes.com", "pq.stat.ku6.com", "st.vq.ku6.cn", "stat0.888.ku6.com", "stat1.888.ku6.com", "stat2.888.ku6.com", "stat3.888.ku6.com", "static.ku6.com", "v0.stat.ku6.com", "v1.stat.ku6.com", "v2.stat.ku6.com", "v3.stat.ku6.com", 47 | 48 | // 迅雷看看屏蔽: 49 | // "mcfg.sandai.net", "server1.adpolestar.net", "mpv.sandai.net", "mtips.xunlei.com", "biz5.sandai.net", "kkpgv.xunlei.com", "statis.kankan.xunlei.com", "float.sandai.net", "cl.kankan.xunlei.com", "advstat.xunlei.com", "pubstat.sandai.net", "msg.client.xunlei.com", 50 | 51 | // 腾讯视频屏蔽" 52 | "adslvfile.qq.com", "adsfile.qq.com", "play.qq.com", "adslvfile.qq.com", "tj.video.qq.com", "vv.video.qq.com", "livep.l.qq.com", "rcgi.video.qq.com", "aid.video.qq.com", "pinghot.qq.com", "54kfqq.com", "qqbm.zhuiying.net", "qqbmad.mycode8.net", "vhotlxp.video.qq.com", "livem.l.qq.com", 53 | "img.09mk.cn", "img.xiaohui2.cn", ".xiaohui", ".apple.com", "img2.", "sysapr.cn", "vlive.qqvideo", "image.kdmmm","baidu.com" 54 | ] 55 | -------------------------------------------------------------------------------- /DKVideo/Resources/blank.caf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Resources/blank.caf -------------------------------------------------------------------------------- /DKVideo/Resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello World 5 | 6 | 34 | 35 | 36 | 37 | 38 | 39 | 246 | 247 | 248 | 249 | 250 | -------------------------------------------------------------------------------- /HowToInterceptRequests.md: -------------------------------------------------------------------------------- 1 | 如何拦截并转发APP中的网络请求 2 | === 3 | ####理论基础:URL Loading System 4 | ####主要实现类: [URLProtocol](https://developer.apple.com/documentation/foundation/nsurlprotocol) 5 | 6 | >iOS的Foundation框架提供了 URL Loading System 这个库(ULS),所有应用层的传输协议都可以通过ULS提供的基础类和协议来实现,你也可以你用它自定义自己通讯协议。 7 | >在每一个 HTTP 请求开始时,ULS创建一个合适的 `URLProtocol` 对象处理对应的 URL 请求,而我们需要做的就是写一个继承自 `URLProtocol` 的类,并通过` - registerClass: `方法注册我们的协议类,然后 URL 加载系统就会在请求发出时使用我们创建的协议对象对该请求进行处理。 8 | 9 | #`URLProtocol` 10 | 一个用于处理特定URL加载的抽象类,定义: 11 | ```swift 12 | class URLProtocol : NSObject 13 | ``` 14 | 15 | 一个父类是`NSObject`的抽象类,并不是swift中的`Protocol`, 16 | 实现此抽象类后理论上可以拦截APP中所有的Cocoa层网络请求, 17 | 18 | 由此我们定义一个URLProtocol的子类,来拦截处理我们的网络请求,例如截取网络请求中的视频播放地址,为每个请求添加统一的请求头,过滤掉部分网络请求等。 19 | 20 | ```swift 21 | /// 网络请求拦截器 22 | class URLIntercept: URLProtocol { } 23 | ``` 24 | 25 | 我们直接在`XCode`中点开`URLProtocol` 26 | 类的定义你主要有以下方法和属性 27 | ```swift 28 | /** 初始化方法 */ 29 | public init(request: URLRequest,...) 30 | public init(task: URLSessionTask,...) 31 | /** 用于获取加载结果 */ 32 | open var client: URLProtocolClient? { get } 33 | 34 | /** 当前加载的网络请求 */ 35 | open var request: URLRequest { get } 36 | 37 | /** 参数是当前的网络请求,返回是否需要监控此请求 */ 38 | open class func canInit(with request: URLRequest) -> Bool 39 | open class func canInit(with: URLSessionTask) -> Bool 40 | 41 | /** 根据当前的网络请求,返回一个我们自定义的网络请求 */ 42 | open class func canonicalRequest(for request: URLRequest) -> URLRequest 43 | 44 | /** 调用此方法,应该开始加载网络请求 */ 45 | open func startLoading() 46 | /** 实现方法以取消网络请求 */ 47 | open func stopLoading() 48 | 49 | /** 获取网络请求中的关联属性 */ 50 | open class func property(forKey key: String, in request: URLRequest) -> Any? 51 | /** 设置网络请求中的关联属性 */ 52 | open class func setProperty(_ value: Any, forKey key: String, in request: NSMutableURLRequest) 53 | /** 移除网络请求中的关联属性 */ 54 | open class func removeProperty(forKey key: String, in request: NSMutableURLRequest) 55 | 56 | /** 注册协议类*/ 57 | open class func registerClass(_ protocolClass: AnyClass) -> Bool 58 | /** 取消注册 */ 59 | open class func unregisterClass(_ protocolClass: AnyClass) 60 | } 61 | ``` 62 | 从上面也不难看出我们需要重点实现 `canInit`来确定是否监控此条网络请求 ,实现`canonicalRequest`实现拦截的具体处理逻辑。 63 | 64 | 首先我们需要确定哪些球球需要处理,注意:在我们发起一个网络请求的时候,首先会调用`canInitWithRequest:`方法,询问是否对该请求进行处理,接着会调用`canonicalRequestForRequest:`来自定义一个`request`,新的请求(request)又会去调用`canInitWithRequest:`询问自定义的`request`是否需要处理,如果我们又返回`true`,然后又去调用了`canonicalRequestForRequest:`这样,就形成了一个死循环了,为了打破这种循环,我们给处理过的网络请求设置一个标识,再次检测到此标识就不在处理这条请求。 65 | ```swift 66 | let URLInterceptKey = "Intercepted" 67 | /// 返回是否监控此条网络请求 68 | /// - Parameter request: 网络请求 69 | override class func canInit(with request: URLRequest) -> Bool { 70 | print(request.url?.absoluteString ?? "") 71 | // 如果是已经拦截过的就放行,避免出现死循环 72 | if URLProtocol.property(forKey: URLInterceptKey, in: request) as? Bool ?? false { 73 | return false 74 | } 75 | // 不是网络请求,不处理 76 | if let urlScheme = request.url?.scheme?.lowercased() { 77 | if ["http", "https", "ftp"].contains(urlScheme) { 78 | return true 79 | } 80 | } 81 | // 不拦截其他 82 | return false 83 | } 84 | 85 | /// 设置我们自己的自定义请求 86 | /// - Parameter request: 当前的网络请求 87 | override class func canonicalRequest(for request: URLRequest) -> URLRequest { 88 | var mutableReqeust: URLRequest = request 89 | guard let urlStr = request.url?.absoluteString else { return request } 90 | // 广告拦截标识字符 91 | let adStrings = ["img.09mk.cn", "img.xiaohui2.cn", ".xiaohui", ".apple.com", "img2.", "sysapr.cn"] 92 | adStrings.forEach { str in 93 | if urlStr.contains(str) { mutableReqeust.url = nil } 94 | } 95 | // 视频播放拦截 96 | if urlStr.pathExtension.hasPrefix("m3u8") { 97 | print("=====video获取到视频路径===\n\(urlStr)") 98 | DispatchQueue.main.async { 99 | //调用视频播放器播放拦截到的视频地址 100 | } 101 | } 102 | return mutableReqeust 103 | } 104 | 105 | ``` 106 | 然后我们需要实现发起和取消网络请求的方法,可以再此对拦截到的网络请求做统一的处理,如修改请求头信息,设置处理标识符等 107 | ```swift 108 | // 由于默认的task是只读属性,所以我们用newTask属性记录新发起的请求 109 | var newTask: URLSessionTask? 110 | override func startLoading() { 111 | // 给我们处理过的请求设置一个标识符, 防止无限循环, 112 | var request = self.request 113 | URLProtocol.setProperty(true, forKey: URLInterceptKey, in: request as! NSMutableURLRequest) 114 | let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil) 115 | //模拟PC版浏览器请求 116 | if UserDefaults.isPCAgent{ 117 | request.setValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36", forHTTPHeaderField:"User-Agent" ) 118 | }else{ 119 | request.setValue("Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", forHTTPHeaderField: "User-Agent") 120 | } 121 | self.newTask = session.dataTask(with: request) 122 | self.newTask?.resume() 123 | } 124 | 125 | override func stopLoading() { 126 | self.newTask?.cancel() 127 | } 128 | 129 | ``` 130 | 我们创建`URLSession`时将代理设为了`self`,所以还要实现代理方法 131 | ```swift 132 | extension URLIntercept: URLSessionDelegate, URLSessionDataDelegate { 133 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { 134 | //这里可以拦截返回的数据`data` 135 | client?.urlProtocol(self, didLoad: data) 136 | } 137 | 138 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { 139 | client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed) 140 | completionHandler(.allow) 141 | } 142 | 143 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 144 | client?.urlProtocolDidFinishLoading(self) 145 | } 146 | } 147 | ``` 148 | 149 | 到此我们已经完成了这个拦截类的实现,将其注册后就可以实现请求拦截了,我们直接在APPdelegate中进行注册 150 | ```swift 151 | class AppDelegate: UIResponder, UIApplicationDelegate { 152 | var window: UIWindow? 153 | 154 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 155 | 156 | URLProtocol.registerClass(URLIntercept.self) 157 | LibsManager.shared.configTheme() 158 | return true 159 | } 160 | } 161 | ``` 162 | 运行程序后就可以拦截到网络请求了,但是到此还没有结束,我们在拦截`WKWebView`的请求时发现,只能拦截到第一条,之后的都不在被拦截处理。 163 | 主要是由于WKWebView为内部调用请求对应的scheme注册了对应的URLProtocol,所以我们的`URLProtocol`实现无法拦截到这些请求,于是我们需要把这些注册了的scheme `unregister`掉,具体可以参考[webkit-TestProtocol.mm](https://github.com/WebKit/webkit/blob/master/Tools/TestWebKitAPI/cocoa/TestProtocol.mm)中的单元测试代码(60-73行)。 164 | 其中调用的 `unregisterSchemeForCustomProtocol`为私有API 165 | 所以我们需要借助运行时,去动态调用,在swift5中,我们已经不再能够使用`performSelector`方法,我们也不准备使用OC去调用这些私有api,关于swift调用运行时方法将在另一篇文章中介绍这里直接写结果代码 166 | ```swift 167 | class AppDelegate: UIResponder, UIApplicationDelegate { 168 | var window: UIWindow? 169 | 170 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 171 | let browCont = WKWebView().value(forKey: "browsingContextController")didFinishLaunchingWithOptions 172 | //获取类,你也可以直接使用NSClassFromString("WKBrowsingContextController")获取 173 | let classType = type(of: browCont!) as! AnyClass 174 | //获取注册scheme方法 175 | if let method = extractMethodFrom(owner: classType, selector: NSSelectorFromString("registerSchemeForCustomProtocol:")) { 176 | //反向注册http和https两个scheme 177 | _ = method("http") 178 | _ = method("https") 179 | } 180 | URLProtocol.registerClass(URLIntercept.self) 181 | LibsManager.shared.configTheme() 182 | 183 | return true 184 | } 185 | } 186 | 187 | ``` 188 | 反向注册scheme后,WKWebView中的http和https请求就会被我们实现的`URLIntercept`拦截到了 189 | 190 | 如果需要拦截其他网络框架的请求需要替换掉`URLSessionConfiguration`中的`protocolClasses`,在其中添加我们的拦截类,在APPdelegate中的`didFinishLaunchingWithOptions`中使用[Aspects](https://github.com/steipete/Aspects) 去Swizzle原获取方法(也可以使用MethodSwizzling) 191 | ```swift 192 | let rblock: @convention(block) (AspectInfo)-> Void = { info in 193 | let invocation = info.originalInvocation() 194 | var pros = [URLProtocol.Type]() 195 | invocation?.invoke() 196 | invocation?.getReturnValue(&pros) 197 | pros.append(URLIntercept.self) 198 | invocation?.setReturnValue(&pros) 199 | } 200 | try! type(of: URLSession.shared.configuration).aspect_hook(NSSelectorFromString("protocolClasses"), with: .positionInstead, usingBlock: rblock) 201 | ``` 202 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 DKJone 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | source 'https://github.com/CocoaPods/Specs.git' 3 | platform :ios, '11.0' 4 | 5 | target 'DKVideo' do 6 | # Comment the next line if you don't want to use dynamic frameworks 7 | use_frameworks! 8 | 9 | # Pods for DKVideo 10 | # UI 11 | pod 'NVActivityIndicatorView' 12 | pod 'SuperPlayer' 13 | pod 'SnapKit' 14 | pod 'IQKeyboardManagerSwift' 15 | pod 'ChameleonFramework/Swift', :git => 'https://github.com/luckychris/Chameleon' 16 | pod 'Toast-Swift' 17 | pod 'XLPagerTabStrip' 18 | pod 'KafkaRefresh' 19 | pod 'DZNEmptyDataSet' 20 | pod 'Aspects' 21 | pod 'FloatingPanel' 22 | # Debug 23 | pod 'FLEX', :configurations => ['Debug'] 24 | 25 | # Tools 26 | pod 'R.swift' 27 | pod 'SwifterSwift' 28 | pod "GCDWebServer/WebDAV" 29 | pod 'SwiftyJSON' 30 | pod 'AttributedLib' 31 | pod 'HandyJSON' 32 | pod 'Tiercel' 33 | # RX 34 | pod 'RxSwiftExt' 35 | pod 'NSObject+Rx' 36 | pod 'RxViewController' 37 | pod 'RxGesture' 38 | pod 'RxOptional' 39 | pod 'RxDataSources' 40 | pod 'RxTheme' 41 | pod 'RxSwift' , '~> 5.0.0' 42 | 43 | end 44 | 45 | # Cocoapods optimization, always clean project after pod updating 46 | post_install do |installer| 47 | Dir.glob(installer.sandbox.target_support_files_root + "Pods-*/*.sh").each do |script| 48 | flag_name = File.basename(script, ".sh") + "-Installation-Flag" 49 | folder = "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 50 | file = File.join(folder, flag_name) 51 | content = File.read(script) 52 | content.gsub!(/set -e/, "set -e\nKG_FILE=\"#{file}\"\nif [ -f \"$KG_FILE\" ]; then exit 0; fi\nmkdir -p \"#{folder}\"\ntouch \"$KG_FILE\"") 53 | File.write(script, content) 54 | end 55 | 56 | # enable tracing resources 57 | installer.pods_project.targets.each do |target| 58 | if target.name == 'RxSwift' 59 | target.build_configurations.each do |config| 60 | if config.name == 'Debug' 61 | config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['-D', 'TRACE_RESOURCES'] 62 | end 63 | end 64 | end 65 | 66 | if target.name == "CocoaHTTPServer" 67 | target.build_configurations.each do |config| 68 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)', 'DD_LEGACY_MACROS=1'] 69 | end 70 | end 71 | end 72 | end 73 | 74 | 75 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - AFNetworking (4.0.1): 3 | - AFNetworking/NSURLSession (= 4.0.1) 4 | - AFNetworking/Reachability (= 4.0.1) 5 | - AFNetworking/Security (= 4.0.1) 6 | - AFNetworking/Serialization (= 4.0.1) 7 | - AFNetworking/UIKit (= 4.0.1) 8 | - AFNetworking/NSURLSession (4.0.1): 9 | - AFNetworking/Reachability 10 | - AFNetworking/Security 11 | - AFNetworking/Serialization 12 | - AFNetworking/Reachability (4.0.1) 13 | - AFNetworking/Security (4.0.1) 14 | - AFNetworking/Serialization (4.0.1) 15 | - AFNetworking/UIKit (4.0.1): 16 | - AFNetworking/NSURLSession 17 | - Aspects (1.4.1) 18 | - AttributedLib (3.0.0) 19 | - ChameleonFramework/Default (2.1.0) 20 | - ChameleonFramework/Swift (2.1.0): 21 | - ChameleonFramework/Default 22 | - Differentiator (4.0.1) 23 | - DZNEmptyDataSet (1.8.1) 24 | - FLEX (4.1.1) 25 | - FloatingPanel (1.7.4) 26 | - GCDWebServer/Core (3.5.4) 27 | - GCDWebServer/WebDAV (3.5.4): 28 | - GCDWebServer/Core 29 | - HandyJSON (5.0.1) 30 | - IQKeyboardManagerSwift (6.5.5) 31 | - KafkaRefresh (1.4.8): 32 | - KafkaRefresh/Category (= 1.4.8) 33 | - KafkaRefresh/Configuration (= 1.4.8) 34 | - KafkaRefresh/Core (= 1.4.8) 35 | - KafkaRefresh/Default (= 1.4.8) 36 | - KafkaRefresh/Style (= 1.4.8) 37 | - KafkaRefresh/UIKit (= 1.4.8) 38 | - KafkaRefresh/Category (1.4.8) 39 | - KafkaRefresh/Configuration (1.4.8): 40 | - KafkaRefresh/UIKit/FootKit 41 | - KafkaRefresh/UIKit/HeadKit 42 | - KafkaRefresh/Core (1.4.8): 43 | - KafkaRefresh/Category 44 | - KafkaRefresh/Default (1.4.8): 45 | - KafkaRefresh/Style 46 | - KafkaRefresh/Style (1.4.8) 47 | - KafkaRefresh/UIKit (1.4.8): 48 | - KafkaRefresh/UIKit/FootKit (= 1.4.8) 49 | - KafkaRefresh/UIKit/HeadKit (= 1.4.8) 50 | - KafkaRefresh/UIKit/LayerKit (= 1.4.8) 51 | - KafkaRefresh/UIKit/FootKit (1.4.8): 52 | - KafkaRefresh/Category 53 | - KafkaRefresh/Core 54 | - KafkaRefresh/Default 55 | - KafkaRefresh/Style 56 | - KafkaRefresh/UIKit/LayerKit 57 | - KafkaRefresh/UIKit/HeadKit (1.4.8): 58 | - KafkaRefresh/Category 59 | - KafkaRefresh/Core 60 | - KafkaRefresh/Default 61 | - KafkaRefresh/Style 62 | - KafkaRefresh/UIKit/LayerKit 63 | - KafkaRefresh/UIKit/LayerKit (1.4.8): 64 | - KafkaRefresh/Category 65 | - KafkaRefresh/Default 66 | - Masonry (1.1.0) 67 | - MMLayout (0.3.0) 68 | - "NSObject+Rx (5.0.2)": 69 | - RxSwift (~> 5.0) 70 | - NVActivityIndicatorView (4.8.0): 71 | - NVActivityIndicatorView/Presenter (= 4.8.0) 72 | - NVActivityIndicatorView/Presenter (4.8.0) 73 | - R.swift (5.2.2): 74 | - R.swift.Library (~> 5.2.0) 75 | - R.swift.Library (5.2.0) 76 | - RxCocoa (5.1.1): 77 | - RxRelay (~> 5) 78 | - RxSwift (~> 5) 79 | - RxDataSources (4.0.1): 80 | - Differentiator (~> 4.0) 81 | - RxCocoa (~> 5.0) 82 | - RxSwift (~> 5.0) 83 | - RxGesture (3.0.1): 84 | - RxCocoa (~> 5.0) 85 | - RxSwift (~> 5.0) 86 | - RxOptional (4.1.0): 87 | - RxCocoa (~> 5) 88 | - RxSwift (~> 5) 89 | - RxRelay (5.1.1): 90 | - RxSwift (~> 5) 91 | - RxSwift (5.0.1) 92 | - RxSwiftExt (5.2.0): 93 | - RxSwiftExt/Core (= 5.2.0) 94 | - RxSwiftExt/RxCocoa (= 5.2.0) 95 | - RxSwiftExt/Core (5.2.0): 96 | - RxSwift (~> 5.0) 97 | - RxSwiftExt/RxCocoa (5.2.0): 98 | - RxCocoa (~> 5.0) 99 | - RxSwiftExt/Core 100 | - RxTheme (4.0.0): 101 | - RxCocoa (~> 5.0) 102 | - RxSwift (~> 5.0) 103 | - RxViewController (1.0.0): 104 | - RxCocoa (~> 5.0) 105 | - RxSwift (~> 5.0) 106 | - SDWebImage (5.7.3): 107 | - SDWebImage/Core (= 5.7.3) 108 | - SDWebImage/Core (5.7.3) 109 | - SnapKit (5.0.1) 110 | - SuperPlayer (3.2.5): 111 | - AFNetworking (~> 4.0) 112 | - Masonry (~> 1.1.0) 113 | - MMLayout (~> 0.3.0) 114 | - SDWebImage (~> 5.0) 115 | - SuperPlayer/Player (= 3.2.5) 116 | - SuperPlayer/Player (3.2.5): 117 | - AFNetworking (~> 4.0) 118 | - Masonry (~> 1.1.0) 119 | - MMLayout (~> 0.3.0) 120 | - SDWebImage (~> 5.0) 121 | - TXLiteAVSDK_Player (= 7.2.8932) 122 | - SwifterSwift (5.2.0): 123 | - SwifterSwift/AppKit (= 5.2.0) 124 | - SwifterSwift/CoreAnimation (= 5.2.0) 125 | - SwifterSwift/CoreGraphics (= 5.2.0) 126 | - SwifterSwift/CoreLocation (= 5.2.0) 127 | - SwifterSwift/Dispatch (= 5.2.0) 128 | - SwifterSwift/Foundation (= 5.2.0) 129 | - SwifterSwift/MapKit (= 5.2.0) 130 | - SwifterSwift/SceneKit (= 5.2.0) 131 | - SwifterSwift/SpriteKit (= 5.2.0) 132 | - SwifterSwift/StoreKit (= 5.2.0) 133 | - SwifterSwift/SwiftStdlib (= 5.2.0) 134 | - SwifterSwift/UIKit (= 5.2.0) 135 | - SwifterSwift/AppKit (5.2.0) 136 | - SwifterSwift/CoreAnimation (5.2.0) 137 | - SwifterSwift/CoreGraphics (5.2.0) 138 | - SwifterSwift/CoreLocation (5.2.0) 139 | - SwifterSwift/Dispatch (5.2.0) 140 | - SwifterSwift/Foundation (5.2.0) 141 | - SwifterSwift/MapKit (5.2.0) 142 | - SwifterSwift/SceneKit (5.2.0) 143 | - SwifterSwift/SpriteKit (5.2.0) 144 | - SwifterSwift/StoreKit (5.2.0) 145 | - SwifterSwift/SwiftStdlib (5.2.0) 146 | - SwifterSwift/UIKit (5.2.0) 147 | - SwiftyJSON (5.0.0) 148 | - Tiercel (3.2.0) 149 | - Toast-Swift (5.0.1) 150 | - TXLiteAVSDK_Player (7.2.8932) 151 | - XLPagerTabStrip (9.0.0) 152 | 153 | DEPENDENCIES: 154 | - Aspects 155 | - AttributedLib 156 | - ChameleonFramework/Swift (from `https://github.com/luckychris/Chameleon`) 157 | - DZNEmptyDataSet 158 | - FLEX 159 | - FloatingPanel 160 | - GCDWebServer/WebDAV 161 | - HandyJSON 162 | - IQKeyboardManagerSwift 163 | - KafkaRefresh 164 | - "NSObject+Rx" 165 | - NVActivityIndicatorView 166 | - R.swift 167 | - RxDataSources 168 | - RxGesture 169 | - RxOptional 170 | - RxSwift (~> 5.0.0) 171 | - RxSwiftExt 172 | - RxTheme 173 | - RxViewController 174 | - SnapKit 175 | - SuperPlayer 176 | - SwifterSwift 177 | - SwiftyJSON 178 | - Tiercel 179 | - Toast-Swift 180 | - XLPagerTabStrip 181 | 182 | SPEC REPOS: 183 | https://github.com/CocoaPods/Specs.git: 184 | - AFNetworking 185 | - Aspects 186 | - AttributedLib 187 | - Differentiator 188 | - DZNEmptyDataSet 189 | - FLEX 190 | - FloatingPanel 191 | - GCDWebServer 192 | - HandyJSON 193 | - IQKeyboardManagerSwift 194 | - KafkaRefresh 195 | - Masonry 196 | - MMLayout 197 | - "NSObject+Rx" 198 | - NVActivityIndicatorView 199 | - R.swift 200 | - R.swift.Library 201 | - RxCocoa 202 | - RxDataSources 203 | - RxGesture 204 | - RxOptional 205 | - RxRelay 206 | - RxSwift 207 | - RxSwiftExt 208 | - RxTheme 209 | - RxViewController 210 | - SDWebImage 211 | - SnapKit 212 | - SuperPlayer 213 | - SwifterSwift 214 | - SwiftyJSON 215 | - Tiercel 216 | - Toast-Swift 217 | - TXLiteAVSDK_Player 218 | - XLPagerTabStrip 219 | 220 | EXTERNAL SOURCES: 221 | ChameleonFramework: 222 | :git: https://github.com/luckychris/Chameleon 223 | 224 | CHECKOUT OPTIONS: 225 | ChameleonFramework: 226 | :commit: 347fcaa815f60281f9fbd32620c42d384dfc9b44 227 | :git: https://github.com/luckychris/Chameleon 228 | 229 | SPEC CHECKSUMS: 230 | AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce 231 | Aspects: 7595ba96a6727a58ebcbfc954497fc5d2fdde546 232 | AttributedLib: ad537d4f9285ed57fc4d9103dd83c2ee6be1582b 233 | ChameleonFramework: d21a3cc247abfe5e37609a283a8238b03575cf64 234 | Differentiator: 886080237d9f87f322641dedbc5be257061b0602 235 | DZNEmptyDataSet: 9525833b9e68ac21c30253e1d3d7076cc828eaa7 236 | FLEX: 81096107977a09836eb440173010ceeef01105d6 237 | FloatingPanel: 3c9d0e30fe350e1613157557769d2ec97f76b96b 238 | GCDWebServer: 2c156a56c8226e2d5c0c3f208a3621ccffbe3ce4 239 | HandyJSON: d45b8415c501e3affc59ca9762c1197ff0feb646 240 | IQKeyboardManagerSwift: 0fb93310284665245591f50f7a5e38de615960b7 241 | KafkaRefresh: aaae4ff23197f2ef38b6b22ca320740beac4bbb4 242 | Masonry: 678fab65091a9290e40e2832a55e7ab731aad201 243 | MMLayout: d559edecd69fbcb348368ba90917e047ab1c544e 244 | "NSObject+Rx": 2eb2cf51fd1c554118449a7fbc36e4cd620b9e94 245 | NVActivityIndicatorView: d24b7ebcf80af5dcd994adb650e2b6c93379270f 246 | R.swift: 7c52cdc57a66840ffe6cbd8a823d732059d42a32 247 | R.swift.Library: 5ba4f1631300caf9a4d890186930da85d540769d 248 | RxCocoa: 32065309a38d29b5b0db858819b5bf9ef038b601 249 | RxDataSources: efee07fa4de48477eca0a4611e6d11e2da9c1114 250 | RxGesture: a3f8dd6adf078110ed5e1c9c30d09d705a09852e 251 | RxOptional: b1fcd60856807a564c0215c2184b8d33e7826dc2 252 | RxRelay: d77f7d771495f43c556cbc43eebd1bb54d01e8e9 253 | RxSwift: e2dc62b366a3adf6a0be44ba9f405efd4c94e0c4 254 | RxSwiftExt: 4ca80336f43c28f11a2825cdd2fc61dd6c044697 255 | RxTheme: 0b642f47c7d197a803c6b9c083964188c34f7ddb 256 | RxViewController: 7330a46e5c31cd680db169da4c9fc8676e975a81 257 | SDWebImage: 97351f6582ceca541ea294ba66a1fcb342a331c2 258 | SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb 259 | SuperPlayer: 5fba5483acaf772d34a462fddfc92819a2d39410 260 | SwifterSwift: 334181863c416882d97b7a60c05054d9e4d799e2 261 | SwiftyJSON: 36413e04c44ee145039d332b4f4e2d3e8d6c4db7 262 | Tiercel: 37c00d3d309657951d00ae68dcb6ce8c7dccf584 263 | Toast-Swift: 9b6a70f28b3bf0b96c40d46c0c4b9d6639846711 264 | TXLiteAVSDK_Player: 7e2f5854f131f0de16ffdefa35081a36bb4f1e93 265 | XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5 266 | 267 | PODFILE CHECKSUM: 80a3aaf9544a1b06b4604f33c40323df34e08620 268 | 269 | COCOAPODS: 1.8.4 270 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | DKVideo - 一键VIP视频解析 2 | ===== 3 | 一键解析各网站VIP视频,去广告播放 4 | 解析接口来自网络,本项目只截取解析完成的内容来播放。 5 | 项目地址:[https://github.com/DKJone/DKVideo](https://github.com/DKJone/DKVideo) 6 | ####1.安装 7 | #####1.1 开发者: 8 | 下载[源码](https://github.com/DKJone/DKVideo) 9 | 后pod update 更换bundleId,运行到手机/IPad上 10 | 或者下载以下ipa后重签名应用:[未签名版](https://ali-fir-pro-binary.fir.im/7e9430a85b84f8a2a69653a3215a91c244eb1001?auth_key=1577169072-0-0-03f51e437338bbaca8aaae81389b53c0)  11 | 注:推荐使用[ios-app-signer](https://github.com/DanTheMan827/ios-app-signer)签名,使用方法自行百度 12 | #####1.2 普通用户: 13 | 前往以下地址下载:[未签名](https://ali-fir-pro-binary.fir.im/7e9430a85b84f8a2a69653a3215a91c244eb1001?auth_key=1577169072-0-0-03f51e437338bbaca8aaae81389b53c0) 下载后使用 [Cydia Impactor](http://www.cydiaimpactor.com/)安装 14 | 15 | ####2.使用 16 | #####2.1 播放vip或超前点映视频 17 | 打开APP找到对应网站的视频然后点击右上角VIP按钮即可实现一键解析如下图 18 | ![play.gif](https://upload-images.jianshu.io/upload_images/4066843-d68fc91d90c0f271.gif?imageMogr2/auto-orient/strip) 19 | #####2.2 下载视频 20 | 在播放页右上角点击下载按钮,输入名称后即可下载 21 | *注意:如果下载提示出错,可以在下载中心点击最前面的下载状态以继续下载* 22 | 暂时只支持m3u8点播视频下载,直播流下载可能出错 23 | ![download.gif](https://upload-images.jianshu.io/upload_images/4066843-433364520dbf50d7.gif?imageMogr2/auto-orient/strip) 24 | 25 | #####2.3 切换VIP接口 26 | 长按右上角vip按钮2秒后松开即可切换(第1~3较稳定) 27 | ![switchVip.gif](https://upload-images.jianshu.io/upload_images/4066843-d104cc128a9130a4.gif?imageMogr2/auto-orient/strip) 28 | 29 | #####2.4 播放下载 30 | 在下载中心点击播放 31 | #####2.5 打开浏览器中的网页 32 | 可以复制地址到首页的搜索框粘贴后点确定即可或者点击浏览器分享按钮然后选择DKVIdeo 33 | 34 | #####2.6 更多设置 35 | 在设置中心自行设置, 36 | *部分网页无法打开可切换电脑版和手机版* 37 | 38 | 39 | ###本项目如何实现的: 40 | >项目基于Swift5.0,项目源码不适合新手阅读,如想尝试实现本项目,需要具备基础的IOS开发技能,熟悉Swift 41 | >项目中度使用RXSwift, 42 | >使用RXTheme实现主题定制及暗色适配, 43 | >使用SwiftyJSON和HandyJSON解析json数据 44 | >使用SnapKit布局UI 45 | >使用RSwift管理图片和文件 46 | >播放器使用腾讯的SuperPlayer(Ijkplayer和TXLiteAVSDK的封装 ) 47 | >使用OBJCRuntime动态调用私有API(上架应用需要将方法名加密处理) 48 | 49 | 1.[如何拦截并转发APP中的网络请求](https://github.com/DKJone/DKVideo/blob/master/HowToInterceptRequests.md) 50 | 2.如何解析M3U8(hls)文件得到下载路径 51 | 3.将APP加入到系统分享 52 | 53 | 54 | 55 | 56 | ###TODO: 57 | * [x] 解析视频地址替换为腾讯的SuperPlayer播放 58 | * [x] 一键解析VIP视频 59 | * [x] 切换解析源 60 | * [x] 视频在线播放 61 | * [x] 获取PC版网页 62 | * [x] 从其他视频APP或网页直接跳转到DKVideo中播放和解析 63 | * [x] 视频下载 64 | * [x] 下载重命名 65 | * [x] 下载列表 66 | * [x] 删除下载 67 | * [x] 边下边播 68 | * [x] 暗色主题 69 | * [x] 自定义主题 70 | * [x] 局域网分享下载 71 | * [x] 后台下载 72 | * [ ] 缓存管理 73 | * [ ] 暴力解析获取所有可用的播放路径 74 | 75 | 更新内容: 76 | 2019-12-24 77 | 1.新增后台下载(替换原来下载为Tiercel) 78 | 2.新增局域网分享 79 | 3.新增使用流量时可选择关闭下载 80 | 4.修复综合解析重复弹出播放视图 81 | 5.修复iPAD播放页面快进手势与呼出侧边栏手势冲突 82 | 6.修复部分视频无法下载,修改错误提示 83 | -------------------------------------------------------------------------------- /VideoShare/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | VideoShare 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | 1 23 | NSExtension 24 | 25 | NSExtensionAttributes 26 | 27 | NSExtensionActivationRule 28 | TRUEPREDICATE 29 | 30 | NSExtensionPointIdentifier 31 | com.apple.share-services 32 | NSExtensionPrincipalClass 33 | VideoShare.ShareVC 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /VideoShare/ShareVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShareVC.swift 3 | // VideoShare 4 | // 5 | // Created by 朱德坤 on 2019/12/4. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | class ShareVC: UIViewController { 11 | let btn = UIButton(frame: CGRect(x: 0, y: 0, width: 300, height: 50)) 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | view.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.7) 15 | btn.center = view.center 16 | btn.setTitle("使用DKVideo打开", for: []) 17 | view.addSubview(btn) 18 | btn.addTarget(self, action: #selector(open), for: .touchUpInside) 19 | 20 | } 21 | 22 | @objc func open() { 23 | var urlStr = "" 24 | extensionContext?.inputItems.forEach { item in 25 | for provider in (item as? NSExtensionItem)?.attachments ?? [] { 26 | provider.loadItem(forTypeIdentifier: "public.url", options: nil, completionHandler: { [unowned self] url, _ in 27 | let str = (url as? URL)?.absoluteString ?? "" 28 | if !str.isEmpty { 29 | print(str) 30 | urlStr = str 31 | self.openMainAPP(url: str) 32 | self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) 33 | return 34 | } 35 | }) 36 | } 37 | } 38 | // if urlStr.isEmpty { 39 | // btn.setTitle("没有获取到视频链接", for: []) 40 | // } 41 | } 42 | 43 | func openMainAPP(url: String) { 44 | if let method = extractMethodFrom(owner: try! self.sharedApplication(), selector: NSSelectorFromString("openURL:")) { 45 | _ = method(URL(string: "DKVideo://\(url)")) 46 | } 47 | } 48 | 49 | func sharedApplication() throws -> UIApplication { 50 | var responder: UIResponder? = self 51 | while responder != nil { 52 | if let application = responder as? UIApplication { 53 | return application 54 | } 55 | responder = responder?.next 56 | } 57 | 58 | throw NSError(domain: "sharedApplication not found", code: 1, userInfo: nil) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /VideoShare/ShareViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShareViewController.swift 3 | // VideoShare 4 | // 5 | // Created by 朱德坤 on 2019/12/4. 6 | // Copyright © 2019 DKJone. All rights reserved. 7 | // 8 | 9 | import Social 10 | import UIKit 11 | 12 | class ShareViewController: SLComposeServiceViewController { 13 | override func isContentValid() -> Bool { 14 | // Do validation of contentText and/or NSExtensionContext attachments here 15 | return false 16 | } 17 | 18 | override func didSelectPost() { 19 | // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments. 20 | 21 | // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context. 22 | self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) 23 | } 24 | 25 | override func configurationItems() -> [Any]! { 26 | // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here. 27 | let item = SLComposeSheetConfigurationItem() 28 | item?.title = "打开" 29 | return [item] 30 | } 31 | 32 | override func viewDidLoad() { 33 | self.view.backgroundColor = .red 34 | // extensionContext?.inputItems.forEach { item in 35 | // print(item) 36 | // let provider = (extensionContext?.inputItems.first as? NSExtensionItem)?.attachments?.first 37 | // provider?.loadItem(forTypeIdentifier: "public.url", options: nil, completionHandler: { url, _ in 38 | // let str = (url as? URL)?.absoluteString ?? "" 39 | // print(str) 40 | // if let method = extractMethodFrom(owner: try! self.sharedApplication(), selector: NSSelectorFromString("openURL:")) { 41 | // _ = method(URL(string: "DKVideo://\(str)")) 42 | // } 43 | // }) 44 | // } 45 | } 46 | 47 | func sharedApplication() throws -> UIApplication { 48 | var responder: UIResponder? = self 49 | while responder != nil { 50 | if let application = responder as? UIApplication { 51 | return application 52 | } 53 | 54 | responder = responder?.next 55 | } 56 | 57 | throw NSError(domain: "UIInputViewController+sharedApplication.swift", code: 1, userInfo: nil) 58 | } 59 | } 60 | 61 | /// 动态获取方法 62 | /// - Parameters: 63 | /// - owner: 对象或者类对象 64 | /// - selector: 方法选择子 65 | func extractMethodFrom(owner: AnyObject, selector: Selector) -> ((Any?) -> Any)? { 66 | let method: Method? 67 | if owner is AnyClass { 68 | method = class_getClassMethod(owner as? AnyClass, selector) 69 | } else { 70 | print(type(of: owner)) 71 | method = class_getInstanceMethod(type(of: owner), selector) 72 | } 73 | 74 | if let one = method { 75 | let implementation = method_getImplementation(one) 76 | 77 | typealias Function = @convention(c) (AnyObject, Selector, Any?) -> Void 78 | 79 | let function = unsafeBitCast(implementation, to: Function.self) 80 | 81 | return { userinfo in function(owner, selector, userinfo) } 82 | 83 | } else { 84 | return nil 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /VideoShare/VideoShare.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | --------------------------------------------------------------------------------