XQMuse.xcodeproj/project.pbxproj
@@ -76,6 +76,23 @@ 134CC7E02C73283700EAEFB7 /* PavilionSearchVC.xib in Resources */ = {isa = PBXBuildFile; fileRef = 134CC7DF2C73283700EAEFB7 /* PavilionSearchVC.xib */; }; 134CC7E12C73283700EAEFB7 /* PavilionSearchVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 134CC7DE2C73283700EAEFB7 /* PavilionSearchVC.swift */; }; 135C2A502C7EC48D00CC2A67 /* apngb-animated_sun.png in Resources */ = {isa = PBXBuildFile; fileRef = 135C2A4F2C7EC48D00CC2A67 /* apngb-animated_sun.png */; }; 135C2A652C7F033300CC2A67 /* CLAnimationTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135C2A512C7F033300CC2A67 /* CLAnimationTransitioning.swift */; }; 135C2A662C7F033300CC2A67 /* CLFullScreenController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135C2A522C7F033300CC2A67 /* CLFullScreenController.swift */; }; 135C2A672C7F033300CC2A67 /* CLFullScreenLeftController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135C2A532C7F033300CC2A67 /* CLFullScreenLeftController.swift */; }; 135C2A682C7F033300CC2A67 /* CLFullScreenRightController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135C2A542C7F033300CC2A67 /* CLFullScreenRightController.swift */; }; 135C2A692C7F033300CC2A67 /* CLPlayerContentPanelCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135C2A562C7F033300CC2A67 /* CLPlayerContentPanelCell.swift */; }; 135C2A6A2C7F033300CC2A67 /* CLPlayerContentPanelHeadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135C2A572C7F033300CC2A67 /* CLPlayerContentPanelHeadView.swift */; }; 135C2A6B2C7F033300CC2A67 /* CLPlayerContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135C2A582C7F033300CC2A67 /* CLPlayerContentView.swift */; }; 135C2A6C2C7F033300CC2A67 /* CLPlayerContentViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135C2A592C7F033300CC2A67 /* CLPlayerContentViewDelegate.swift */; }; 135C2A6D2C7F033300CC2A67 /* CLRotateAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135C2A5A2C7F033300CC2A67 /* CLRotateAnimationView.swift */; }; 135C2A6E2C7F033300CC2A67 /* CLSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135C2A5B2C7F033300CC2A67 /* CLSlider.swift */; }; 135C2A6F2C7F033300CC2A67 /* CLGCDTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135C2A5D2C7F033300CC2A67 /* CLGCDTimer.swift */; }; 135C2A702C7F033300CC2A67 /* CLImageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135C2A5E2C7F033300CC2A67 /* CLImageHelper.swift */; }; 135C2A712C7F033300CC2A67 /* CLPlayer.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 135C2A5F2C7F033300CC2A67 /* CLPlayer.bundle */; }; 135C2A722C7F033300CC2A67 /* CLPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135C2A602C7F033300CC2A67 /* CLPlayer.swift */; }; 135C2A732C7F033300CC2A67 /* CLPlayerConfigure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135C2A612C7F033300CC2A67 /* CLPlayerConfigure.swift */; }; 135C2A742C7F033300CC2A67 /* CLPlayerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135C2A622C7F033300CC2A67 /* CLPlayerDelegate.swift */; }; 135C2A752C7F033300CC2A67 /* CLPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135C2A632C7F033300CC2A67 /* CLPlayerView.swift */; }; 13649F9A2C7709CD00F4E0EE /* ContactCustomerTCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13649F992C7709CD00F4E0EE /* ContactCustomerTCell.xib */; }; 13649F9B2C7709CD00F4E0EE /* ContactCustomerTCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13649F982C7709CD00F4E0EE /* ContactCustomerTCell.swift */; }; 13649F9E2C770C9C00F4E0EE /* ContactCustomerDetailVC.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13649F9D2C770C9C00F4E0EE /* ContactCustomerDetailVC.xib */; }; @@ -525,6 +542,23 @@ 134CC7DE2C73283700EAEFB7 /* PavilionSearchVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PavilionSearchVC.swift; sourceTree = "<group>"; }; 134CC7DF2C73283700EAEFB7 /* PavilionSearchVC.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PavilionSearchVC.xib; sourceTree = "<group>"; }; 135C2A4F2C7EC48D00CC2A67 /* apngb-animated_sun.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "apngb-animated_sun.png"; sourceTree = "<group>"; }; 135C2A512C7F033300CC2A67 /* CLAnimationTransitioning.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLAnimationTransitioning.swift; sourceTree = "<group>"; }; 135C2A522C7F033300CC2A67 /* CLFullScreenController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLFullScreenController.swift; sourceTree = "<group>"; }; 135C2A532C7F033300CC2A67 /* CLFullScreenLeftController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLFullScreenLeftController.swift; sourceTree = "<group>"; }; 135C2A542C7F033300CC2A67 /* CLFullScreenRightController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLFullScreenRightController.swift; sourceTree = "<group>"; }; 135C2A562C7F033300CC2A67 /* CLPlayerContentPanelCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLPlayerContentPanelCell.swift; sourceTree = "<group>"; }; 135C2A572C7F033300CC2A67 /* CLPlayerContentPanelHeadView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLPlayerContentPanelHeadView.swift; sourceTree = "<group>"; }; 135C2A582C7F033300CC2A67 /* CLPlayerContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLPlayerContentView.swift; sourceTree = "<group>"; }; 135C2A592C7F033300CC2A67 /* CLPlayerContentViewDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLPlayerContentViewDelegate.swift; sourceTree = "<group>"; }; 135C2A5A2C7F033300CC2A67 /* CLRotateAnimationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLRotateAnimationView.swift; sourceTree = "<group>"; }; 135C2A5B2C7F033300CC2A67 /* CLSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLSlider.swift; sourceTree = "<group>"; }; 135C2A5D2C7F033300CC2A67 /* CLGCDTimer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLGCDTimer.swift; sourceTree = "<group>"; }; 135C2A5E2C7F033300CC2A67 /* CLImageHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLImageHelper.swift; sourceTree = "<group>"; }; 135C2A5F2C7F033300CC2A67 /* CLPlayer.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = CLPlayer.bundle; sourceTree = "<group>"; }; 135C2A602C7F033300CC2A67 /* CLPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLPlayer.swift; sourceTree = "<group>"; }; 135C2A612C7F033300CC2A67 /* CLPlayerConfigure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLPlayerConfigure.swift; sourceTree = "<group>"; }; 135C2A622C7F033300CC2A67 /* CLPlayerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLPlayerDelegate.swift; sourceTree = "<group>"; }; 135C2A632C7F033300CC2A67 /* CLPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLPlayerView.swift; sourceTree = "<group>"; }; 13649F982C7709CD00F4E0EE /* ContactCustomerTCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactCustomerTCell.swift; sourceTree = "<group>"; }; 13649F992C7709CD00F4E0EE /* ContactCustomerTCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ContactCustomerTCell.xib; sourceTree = "<group>"; }; 13649F9C2C770C9C00F4E0EE /* ContactCustomerDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactCustomerDetailVC.swift; sourceTree = "<group>"; }; @@ -1041,6 +1075,46 @@ path = VC; sourceTree = "<group>"; }; 135C2A552C7F033300CC2A67 /* CLFullScreenController */ = { isa = PBXGroup; children = ( 135C2A512C7F033300CC2A67 /* CLAnimationTransitioning.swift */, 135C2A522C7F033300CC2A67 /* CLFullScreenController.swift */, 135C2A532C7F033300CC2A67 /* CLFullScreenLeftController.swift */, 135C2A542C7F033300CC2A67 /* CLFullScreenRightController.swift */, ); path = CLFullScreenController; sourceTree = "<group>"; }; 135C2A5C2C7F033300CC2A67 /* CLPlayerContentView */ = { isa = PBXGroup; children = ( 135C2A562C7F033300CC2A67 /* CLPlayerContentPanelCell.swift */, 135C2A572C7F033300CC2A67 /* CLPlayerContentPanelHeadView.swift */, 135C2A582C7F033300CC2A67 /* CLPlayerContentView.swift */, 135C2A592C7F033300CC2A67 /* CLPlayerContentViewDelegate.swift */, 135C2A5A2C7F033300CC2A67 /* CLRotateAnimationView.swift */, 135C2A5B2C7F033300CC2A67 /* CLSlider.swift */, ); path = CLPlayerContentView; sourceTree = "<group>"; }; 135C2A642C7F033300CC2A67 /* CLPlayer */ = { isa = PBXGroup; children = ( 135C2A552C7F033300CC2A67 /* CLFullScreenController */, 135C2A5C2C7F033300CC2A67 /* CLPlayerContentView */, 135C2A5D2C7F033300CC2A67 /* CLGCDTimer.swift */, 135C2A5E2C7F033300CC2A67 /* CLImageHelper.swift */, 135C2A5F2C7F033300CC2A67 /* CLPlayer.bundle */, 135C2A602C7F033300CC2A67 /* CLPlayer.swift */, 135C2A612C7F033300CC2A67 /* CLPlayerConfigure.swift */, 135C2A622C7F033300CC2A67 /* CLPlayerDelegate.swift */, 135C2A632C7F033300CC2A67 /* CLPlayerView.swift */, ); path = CLPlayer; sourceTree = "<group>"; }; 136C7C7E2C771CCB004540CD /* PayMusicView */ = { isa = PBXGroup; children = ( @@ -1287,6 +1361,7 @@ 13985DC92C69E9B60046B6DC /* Root */ = { isa = PBXGroup; children = ( 135C2A642C7F033300CC2A67 /* CLPlayer */, 137ECAD12C783C0700C338BE /* TreeGroup */, 136C7C7E2C771CCB004540CD /* PayMusicView */, 1385DFFF2C6C4F1200AADB1F /* Network */, @@ -1775,6 +1850,7 @@ 137ECAD32C783C2000C338BE /* bg.mov in Resources */, 1336EFA92C6DEC6B0075E070 /* PaymentOrderResultTopView.xib in Resources */, 1331391A2C742A0C009E179E /* UserProfileVC.xib in Resources */, 135C2A712C7F033300CC2A67 /* CLPlayer.bundle in Resources */, 13B0694C2C78593800477FA9 /* shu-1_053.png in Resources */, 13334FDC2C7321BE00914086 /* PavilionItemCell.xib in Resources */, 13B069A82C78593800477FA9 /* shu-idle_025.png in Resources */, @@ -2121,7 +2197,9 @@ buildActionMask = 2147483647; files = ( 13FB6D872C6EF9DE00A0685D /* CourseDetialVC.swift in Sources */, 135C2A652C7F033300CC2A67 /* CLAnimationTransitioning.swift in Sources */, 132DB8FE2C74826D00EF33A7 /* SettingVC.swift in Sources */, 135C2A662C7F033300CC2A67 /* CLFullScreenController.swift in Sources */, 138F0C352C7597CA0072A16C /* HelpCenterVC.swift in Sources */, 13D256B42C6C68E7006FC2D7 /* ShareView.swift in Sources */, 139466472C6B8E0200F6FB15 /* UpdatePhoneVC.swift in Sources */, @@ -2131,6 +2209,7 @@ 137175CB2C6C412A00B38EF1 /* BackgroundVoiceVC.swift in Sources */, 137776922C6AFE69004FF994 /* SearchVC.swift in Sources */, 13B021DC2C75DD0600414769 /* BankWithdrawVC.swift in Sources */, 135C2A702C7F033300CC2A67 /* CLImageHelper.swift in Sources */, 13897D892C7DB9D7006209E0 /* EqualCellSpaceFlowLayout.swift in Sources */, 13985D962C69B2410046B6DC /* AppDelegate.swift in Sources */, 13985DB02C69B7B00046B6DC /* BaseTabBarVC.swift in Sources */, @@ -2153,14 +2232,17 @@ 1385E0062C6C558200AADB1F /* HomeRelaxBanner_2_CCell.swift in Sources */, 13649F9B2C7709CD00F4E0EE /* ContactCustomerTCell.swift in Sources */, 130913EB2C6DE33200418201 /* PaymentOrderResultVC.swift in Sources */, 135C2A742C7F033300CC2A67 /* CLPlayerDelegate.swift in Sources */, 13A0A8AF2C74757200DF08B6 /* MessageTCell.swift in Sources */, 13FB6D7E2C6EE27100A0685D /* CourseOfficialItemCCell.swift in Sources */, 137ABE342C6B3F64003A91C5 /* ForgotPasswordVC.swift in Sources */, 13EFCDBE2C6DCF5800B51AE6 /* HomeTyroGuideVC.swift in Sources */, 134A45322C6E0D6400538D78 /* CourseVCOfficalCommentVC.swift in Sources */, 135C2A692C7F033300CC2A67 /* CLPlayerContentPanelCell.swift in Sources */, 139C16602C6A0FBB00A924D9 /* TestLeftRightCollectionViewFlowLayout.swift in Sources */, 13985DC72C69E9550046B6DC /* CourseVC.swift in Sources */, 13A379FD2C75B7280038D5C8 /* BindAccountVC.swift in Sources */, 135C2A6A2C7F033300CC2A67 /* CLPlayerContentPanelHeadView.swift in Sources */, 130C07132C76DA0500ADB098 /* SpendingDetailContentTCell.swift in Sources */, 1385E0022C6C4F1200AADB1F /* Services.swift in Sources */, 13FB6D8A2C6EFB4D00A0685D /* CourseDetailHeaderView.swift in Sources */, @@ -2169,11 +2251,13 @@ 130AA4A92C72F71700F20944 /* CourseDetialVideoVC.swift in Sources */, 1336EFA52C6DEB550075E070 /* HoverHeaderFlowLayout.swift in Sources */, 13985DB52C69B7DF0046B6DC /* Def.swift in Sources */, 135C2A732C7F033300CC2A67 /* CLPlayerConfigure.swift in Sources */, 136C7C812C771CF3004540CD /* PayMusicVC.swift in Sources */, 130F94662C7DAB27003A348B /* SearchHistoryCCell.swift in Sources */, 139228AF2C6B836B006F3CB6 /* Popup_1_View.swift in Sources */, 1336EFA72C6DEC640075E070 /* PaymentOrderResultTopView.swift in Sources */, 13CBCCE32C747C3D00C67701 /* NoticeCenterUserRepeaceDetailVC.swift in Sources */, 135C2A672C7F033300CC2A67 /* CLFullScreenLeftController.swift in Sources */, 138FE0DE2C757B2A00A964E8 /* BindPhone_1_VC.swift in Sources */, 13A0A8A62C746B5600DF08B6 /* CommonDatePickerView.swift in Sources */, 130C07052C76D1A000ADB098 /* SpendingDetailHeaderVC.swift in Sources */, @@ -2187,16 +2271,20 @@ 1333DC7C2C72E78F00D8ACAE /* CourseSendGiftView.swift in Sources */, 13391E032C73334000B9513F /* PavilionDetailVC.swift in Sources */, 134783CF2C6C86EC0096C736 /* PlaySettingView.swift in Sources */, 135C2A722C7F033300CC2A67 /* CLPlayer.swift in Sources */, 13F24E3E2C75866100D2BA90 /* BindPhone_3_VC.swift in Sources */, 135C2A6F2C7F033300CC2A67 /* CLGCDTimer.swift in Sources */, 139C16592C6A053000A924D9 /* Home_Style_2_TCell.swift in Sources */, 130B76592C6C4963006371AF /* HomeRelaxVoiceCCell.swift in Sources */, 137ABE382C6B6641003A91C5 /* WebVC.swift in Sources */, 135C2A6C2C7F033300CC2A67 /* CLPlayerContentViewDelegate.swift in Sources */, 13985DB12C69B7B00046B6DC /* BaseVC.swift in Sources */, 13985DD52C69FC1F0046B6DC /* Home_Style_1_TCell.swift in Sources */, 132EB01C2C6B32B200990429 /* RegisterVC.swift in Sources */, 134803DC2C7707BA00F4FDDA /* ContactCustomerVC.swift in Sources */, 1331391B2C742A0C009E179E /* UserProfileVC.swift in Sources */, 13E0FBF92C6C8BDE009997AE /* CountdownChooseListView.swift in Sources */, 135C2A6B2C7F033300CC2A67 /* CLPlayerContentView.swift in Sources */, 13A659472C6F4B9E00F731FA /* CourseDetail_1_TCell.swift in Sources */, 130ED7EE2C6AF05C00D0736E /* Home_Style_4_Inner_CCell.swift in Sources */, 139C165D2C6A0AC600A924D9 /* Home_Style_3_TCell.swift in Sources */, @@ -2207,6 +2295,7 @@ 1385DFFA2C6C4EBC00AADB1F /* RefreshModel.swift in Sources */, 134CC7E12C73283700EAEFB7 /* PavilionSearchVC.swift in Sources */, 13EA70012C75F880005DF280 /* IdCardView.swift in Sources */, 135C2A682C7F033300CC2A67 /* CLFullScreenRightController.swift in Sources */, 139C16632C6A108A00A924D9 /* HomeRelaxBannerCCell.swift in Sources */, 131E75C52C6B87C500E2C85D /* ForgotPasswordChangeVC.swift in Sources */, 13A6594F2C6F641100F731FA /* CourseDetail_2_Inner_TCell.swift in Sources */, @@ -2217,12 +2306,15 @@ 130C070B2C76D8F200ADB098 /* SpendingDetailContentVC.swift in Sources */, 13EC08912C74990B00E00128 /* EmptyCCell.swift in Sources */, 13F24E422C758DF100D2BA90 /* LogoutAccountVC.swift in Sources */, 135C2A6E2C7F033300CC2A67 /* CLSlider.swift in Sources */, 13649F9F2C770C9C00F4E0EE /* ContactCustomerDetailVC.swift in Sources */, 130913EF2C6DE67E00418201 /* HomeRelaxBanner_2_1_CCell.swift in Sources */, 1333DC7A2C72D8C400D8ACAE /* CourseDetail_3_TCell.swift in Sources */, 135C2A752C7F033300CC2A67 /* CLPlayerView.swift in Sources */, 1300BD3C2C6DFB1C000BCA5E /* VIPCenterVC.swift in Sources */, 134803D62C76E3E000F4FDDA /* WatchHistoryVC.swift in Sources */, 1377B4162C6DCC4300CF7CA5 /* Home_Style_4_Inner_1_CCell.swift in Sources */, 135C2A6D2C7F033300CC2A67 /* CLRotateAnimationView.swift in Sources */, 13E160212C6CB8930027F781 /* CommentListVC.swift in Sources */, 13A0A89E2C746A8700DF08B6 /* CommonAlertSheetView.swift in Sources */, 13271D862C75EF8200DE1328 /* AddBankInfoVC.swift in Sources */, XQMuse/AppDelegate.swift
@@ -19,6 +19,10 @@ return true } func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { return .allButUpsideDown } // MARK: UISceneSession Lifecycle func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { XQMuse/Assets.xcassets/Btns/video_max.imageset/Contents.json
New file @@ -0,0 +1,22 @@ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "filename" : "video_max@2x.png", "idiom" : "universal", "scale" : "2x" }, { "filename" : "video_max@3x.png", "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } XQMuse/Assets.xcassets/Btns/video_max.imageset/video_max@2x.png
XQMuse/Assets.xcassets/Btns/video_max.imageset/video_max@3x.png
XQMuse/Assets.xcassets/Btns/video_min.imageset/Contents.json
New file @@ -0,0 +1,22 @@ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "filename" : "video_min@2x.png", "idiom" : "universal", "scale" : "2x" }, { "filename" : "video_min@3x.png", "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } XQMuse/Assets.xcassets/Btns/video_min.imageset/video_min@2x.png
XQMuse/Assets.xcassets/Btns/video_min.imageset/video_min@3x.png
XQMuse/Assets.xcassets/Btns/video_pause.imageset/Contents.json
New file @@ -0,0 +1,22 @@ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "filename" : "video_pause@2x.png", "idiom" : "universal", "scale" : "2x" }, { "filename" : "video_pause@3x.png", "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } XQMuse/Assets.xcassets/Btns/video_pause.imageset/video_pause@2x.png
XQMuse/Assets.xcassets/Btns/video_pause.imageset/video_pause@3x.png
XQMuse/Assets.xcassets/Btns/video_play.imageset/Contents.json
New file @@ -0,0 +1,22 @@ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "filename" : "video_play@2x.png", "idiom" : "universal", "scale" : "2x" }, { "filename" : "video_play@3x.png", "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } XQMuse/Assets.xcassets/Btns/video_play.imageset/video_play@2x.png
XQMuse/Assets.xcassets/Btns/video_play.imageset/video_play@3x.png
XQMuse/Base/BaseNav.swift
@@ -97,6 +97,21 @@ } } // 是否支持自动转屏 override var shouldAutorotate: Bool { return topViewController?.shouldAutorotate ?? false } // 支持哪些屏幕方向 override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return topViewController?.supportedInterfaceOrientations ?? .portrait } // 默认的屏幕方向(当前ViewController必须是通过模态出来的UIViewController(模态带导航的无效)方式展现出来的,才会调用这个方法) override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { return topViewController?.preferredInterfaceOrientationForPresentation ?? .portrait } open override var childForStatusBarHidden: UIViewController? { return self.topViewController } XQMuse/Base/BaseTabBarVC.swift
@@ -46,6 +46,24 @@ } } // 是否支持自动转屏 override var shouldAutorotate: Bool { guard let navigationController = selectedViewController as? UINavigationController else { return selectedViewController?.shouldAutorotate ?? false } return navigationController.topViewController?.shouldAutorotate ?? false } // 支持哪些屏幕方向 override var supportedInterfaceOrientations: UIInterfaceOrientationMask { guard let navigationController = selectedViewController as? UINavigationController else { return selectedViewController?.supportedInterfaceOrientations ?? .portrait } return navigationController.topViewController?.supportedInterfaceOrientations ?? .portrait } // 默认的屏幕方向(当前ViewController必须是通过模态出来的UIViewController(模态带导航的无效)方式展现出来的,才会调用这个方法) override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { guard let navigationController = selectedViewController as? UINavigationController else { return selectedViewController?.preferredInterfaceOrientationForPresentation ?? .portrait } return navigationController.topViewController?.preferredInterfaceOrientationForPresentation ?? .portrait } } class CustomTabbar:UITabBar{ XQMuse/Base/BaseVC.swift
@@ -68,6 +68,21 @@ return .default } // 是否支持自动转屏 override var shouldAutorotate: Bool { return false } // 支持哪些屏幕方向 override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .portrait } // 默认的屏幕方向(当前ViewController必须是通过模态出来的UIViewController(模态带导航的无效)方式展现出来的,才会调用这个方法) override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { return .portrait } @objc fileprivate func backItemEvent() { // 拦截pop事件 if (yy_popBlock != nil) { XQMuse/Info.plist
@@ -6,8 +6,6 @@ <dict> <key>NSAllowsArbitraryLoads</key> <true/> <key>NSAllowsArbitraryLoadsInWebContent</key> <true/> </dict> <key>UIAppFonts</key> <array> XQMuse/Root/CLPlayer/.DS_StoreBinary files differ
XQMuse/Root/CLPlayer/CLFullScreenController/CLAnimationTransitioning.swift
New file @@ -0,0 +1,121 @@ // // CLAnimationTransitioning.swift // CLPlayer // // Created by Chen JmoVxia on 2021/10/27. // import SnapKit import UIKit extension CLAnimationTransitioning { enum AnimationType { case present case dismiss } enum AnimationOrientation { case left case right case fullRight } } class CLAnimationTransitioning: NSObject { private let keyWindow: UIWindow? = { if #available(iOS 13.0, *) { return UIApplication.shared.windows.filter { $0.isKeyWindow }.last } else { return UIApplication.shared.keyWindow } }() private weak var playerView: CLPlayerView? private weak var parentStackView: UIStackView? private var initialCenter: CGPoint = .zero private var finalCenter: CGPoint = .zero private var initialBounds: CGRect = .zero private var animationOrientation: AnimationOrientation = .left var animationType: AnimationType = .present init(playerView: CLPlayerView, animationOrientation: AnimationOrientation) { self.playerView = playerView self.animationOrientation = animationOrientation parentStackView = playerView.superview as? UIStackView initialBounds = playerView.bounds initialCenter = playerView.center finalCenter = playerView.convert(initialCenter, to: nil) } } extension CLAnimationTransitioning: UIViewControllerAnimatedTransitioning { func transitionDuration(using _: UIViewControllerContextTransitioning?) -> TimeInterval { return 0.35 } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let playerView = playerView else { return } if animationType == .present { guard let toView = transitionContext.view(forKey: .to) else { return } guard let toController = transitionContext.viewController(forKey: .to) as? CLFullScreenController else { return } let startCenter = transitionContext.containerView.convert(initialCenter, from: playerView) transitionContext.containerView.addSubview(toView) toController.mainStackView.addArrangedSubview(playerView) toView.bounds = initialBounds toView.center = startCenter toView.transform = .init(rotationAngle: toController.isKind(of: CLFullScreenLeftController.self) ? Double.pi * 0.5 : Double.pi * -0.5) if #available(iOS 11.0, *) { playerView.contentView.animationLayout(safeAreaInsets: keyWindow?.safeAreaInsets ?? .zero, to: .fullScreen) } else { playerView.contentView.animationLayout(safeAreaInsets: .zero, to: .fullScreen) } UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: .layoutSubviews, animations: { toView.transform = .identity toView.bounds = transitionContext.containerView.bounds toView.center = transitionContext.containerView.center playerView.contentView.setNeedsLayout() playerView.contentView.layoutIfNeeded() }) { _ in toView.transform = .identity toView.bounds = transitionContext.containerView.bounds toView.center = transitionContext.containerView.center transitionContext.completeTransition(true) UIViewController.attemptRotationToDeviceOrientation() } } else { guard let parentStackView = parentStackView else { return } guard let fromView = transitionContext.view(forKey: .from) else { return } guard let toView = transitionContext.view(forKey: .to) else { return } transitionContext.containerView.addSubview(toView) transitionContext.containerView.addSubview(fromView) toView.frame = transitionContext.containerView.bounds playerView.contentView.animationLayout(safeAreaInsets: .zero, to: .small) UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.0, options: .layoutSubviews, animations: { fromView.transform = .identity fromView.center = self.finalCenter fromView.bounds = self.initialBounds playerView.contentView.setNeedsLayout() playerView.contentView.layoutIfNeeded() }) { _ in fromView.transform = .identity fromView.center = self.finalCenter fromView.bounds = self.initialBounds parentStackView.addArrangedSubview(playerView) fromView.removeFromSuperview() transitionContext.completeTransition(true) UIViewController.attemptRotationToDeviceOrientation() } } } } XQMuse/Root/CLPlayer/CLFullScreenController/CLFullScreenController.swift
New file @@ -0,0 +1,101 @@ // // CLFullScreenController.swift // CLPlayer // // Created by Chen JmoVxia on 2021/10/27. // import UIKit // MARK: - JmoVxia---类-属性 class CLFullScreenController: UIViewController { override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) } @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit {} private(set) lazy var mainStackView: UIStackView = { let view = UIStackView() view.isUserInteractionEnabled = true view.axis = .horizontal view.distribution = .fill view.alignment = .fill view.insetsLayoutMarginsFromSafeArea = false view.isLayoutMarginsRelativeArrangement = true view.layoutMargins = .zero view.spacing = 0 return view }() } // MARK: - JmoVxia---生命周期 extension CLFullScreenController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) } override func viewDidLoad() { super.viewDidLoad() initUI() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() } } // MARK: - JmoVxia---布局 private extension CLFullScreenController { func initUI() { view.backgroundColor = .black view.addSubview(mainStackView) mainStackView.snp.makeConstraints { make in make.edges.equalToSuperview() } } } // MARK: - JmoVxia---override extension CLFullScreenController { override var shouldAutorotate: Bool { return true } override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .landscape } override var preferredStatusBarStyle: UIStatusBarStyle { return .default } override var prefersStatusBarHidden: Bool { return true } override var prefersHomeIndicatorAutoHidden: Bool { return true } } XQMuse/Root/CLPlayer/CLFullScreenController/CLFullScreenLeftController.swift
New file @@ -0,0 +1,20 @@ // // CLFullScreenLeftController.swift // CLPlayer // // Created by Chen JmoVxia on 2021/10/27. // import UIKit // MARK: - JmoVxia---类-属性 class CLFullScreenLeftController: CLFullScreenController { override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { return .landscapeLeft } deinit { print("CLFullScreenLeftController deinit") } } XQMuse/Root/CLPlayer/CLFullScreenController/CLFullScreenRightController.swift
New file @@ -0,0 +1,20 @@ // // CLFullScreenRightController.swift // CLPlayer // // Created by Chen JmoVxia on 2021/10/27. // import UIKit // MARK: - JmoVxia---类-属性 class CLFullScreenRightController: CLFullScreenController { override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { return .landscapeRight } deinit { print("CLFullScreenRightController deinit") } } XQMuse/Root/CLPlayer/CLGCDTimer.swift
New file @@ -0,0 +1,86 @@ // // CLGCDTimer.swift // CLPlayer // // Created by Chen JmoVxia on 2021/10/27. // import UIKit class CLGCDTimer: NSObject { public enum TimerState { case suspended case resumed } /// 执行时间 public private(set) var interval: TimeInterval! /// 第一次执行延迟时间延迟时间 public private(set) var initialDelay: TimeInterval! /// 队列 public private(set) var queue: DispatchQueue! /// 定时器 public private(set) var timer: DispatchSourceTimer! /// 运行状态 public private(set) var state: TimerState = .suspended /// 执行次数 public private(set) var numberOfActions = Int.zero /// 响应回调 public private(set) var eventHandler: ((Int) -> Void)? /// 创建定时器 /// /// - Parameters: /// - interval: 间隔时间 /// - delaySecs: 第一次执行延迟时间,默认为0 /// - queue: 定时器调用的队列,默认主队列 /// - repeats: 是否重复执行,默认true /// - action: 响应 public init(interval: TimeInterval, initialDelay: TimeInterval = 0, queue: DispatchQueue = .main) { super.init() self.interval = interval self.initialDelay = initialDelay self.queue = queue timer = DispatchSource.makeTimerSource(queue: queue) timer.schedule(deadline: .now() + initialDelay, repeating: interval) timer.setEventHandler { [weak self] in guard let self = self else { return } self.numberOfActions += 1 self.eventHandler?(self.numberOfActions) } } deinit { timer?.setEventHandler(handler: nil) timer?.cancel() eventHandler = nil resume() } } extension CLGCDTimer { /// 开始 public func run(_ handler: @escaping ((_ numberOfActions: Int) -> Void)) { eventHandler = handler resume() } /// 暂停 public func pause() { guard let timer = timer else { return } guard state != .suspended else { return } state = .suspended timer.suspend() } /// 恢复定时器 public func resume() { guard state != .resumed else { return } guard let timer = timer else { return } state = .resumed timer.resume() } } XQMuse/Root/CLPlayer/CLImageHelper.swift
New file @@ -0,0 +1,17 @@ // // CLImageHelper.swift // CLPlayer // // Created by Chen JmoVxia on 2021/10/27. // import UIKit public class CLImageHelper: NSObject { public static func imageWithName(_ name: String) -> UIImage? { let filePath = Bundle(for: classForCoder()).resourcePath! + "/CLPlayer.bundle" let bundle = Bundle(path: filePath) let scale = max(min(Int(UIScreen.main.scale), 2), 3) return .init(named: "\(name)@\(scale)x", in: bundle, compatibleWith: nil) } } XQMuse/Root/CLPlayer/CLPlayer.bundle/CLBack@2x.png
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLBack@3x.png
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLFullscreen@2x.png
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLFullscreen@3x.png
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLMore@2x.png
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLMore@3x.png
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLPause@2x.png
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLPause@3x.png
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLPlay@2x.png
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLPlay@3x.png
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLSlider@2x.png
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLSlider@3x.png
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLSmallscreen@2x.png
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLSmallscreen@3x.png
XQMuse/Root/CLPlayer/CLPlayer.swift
New file @@ -0,0 +1,154 @@ // // CLPlayer.swift // CLPlayer // // Created by Chen JmoVxia on 2023/11/10. // import UIKit // MARK: - JmoVxia---枚举 extension CLPlayer {} // MARK: - JmoVxia---类-属性 public class CLPlayer: UIStackView { public init(frame: CGRect = .zero, config: ((inout CLPlayerConfigure) -> Void)? = nil) { super.init(frame: frame) config?(&self.config) initSubViews() makeConstraints() } @available(*, unavailable) required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private lazy var playerView: CLPlayerView = { let view = CLPlayerView(config: config) view.backButtonTappedHandler = { [weak self] in guard let self else { return } self.delegate?.didClickBackButton(in: self) } view.playToEndHandler = { [weak self] in guard let self else { return } self.delegate?.didPlayToEnd(in: self) } view.playProgressChanged = { [weak self] value in guard let self else { return } self.delegate?.player(self, playProgressChanged: value) } view.playFailed = { [weak self] error in guard let self else { return } self.delegate?.player(self, playFailed: error) } return view }() private var config = CLPlayerConfigure() public var totalDuration: TimeInterval { playerView.totalDuration } public var currentDuration: TimeInterval { playerView.currentDuration } public var playbackProgress: CGFloat { playerView.playbackProgress } public var rate: Float { playerView.rate } public var isFullScreen: Bool { playerView.contentView.screenState == .fullScreen } public var isPlaying: Bool { playerView.contentView.playState == .playing } public var isBuffering: Bool { playerView.contentView.playState == .buffering } public var isFailed: Bool { playerView.contentView.playState == .failed } public var isPaused: Bool { playerView.contentView.playState == .pause } public var isEnded: Bool { playerView.contentView.playState == .ended } public var title: NSMutableAttributedString? { didSet { guard let title = title else { return } playerView.contentView.title = title } } public var url: URL? { didSet { guard let url = url else { return } playerView.url = url } } public weak var placeholder: UIView? { didSet { playerView.contentView.placeholderView = placeholder } } public weak var delegate: CLPlayerDelegate? } // MARK: - JmoVxia---布局 private extension CLPlayer { func initSubViews() { insetsLayoutMarginsFromSafeArea = false distribution = .fill alignment = .fill addArrangedSubview(playerView) } func makeConstraints() {} } // MARK: - JmoVxia---override extension CLPlayer {} // MARK: - JmoVxia---objc @objc private extension CLPlayer {} // MARK: - JmoVxia---私有方法 private extension CLPlayer {} // MARK: - JmoVxia---公共方法 public extension CLPlayer { func play() { playerView.play() } func pause() { playerView.pause() } func stop() { playerView.stop() } } XQMuse/Root/CLPlayer/CLPlayerConfigure.swift
New file @@ -0,0 +1,132 @@ // // CLPlayerConfigure.swift // CLPlayer // // Created by Chen JmoVxia on 2021/12/15. // import AVFoundation import UIKit public struct CLPlayerConfigure { public struct CLPlayerColor { /// 顶部工具条背景颜色 public var topToobar: UIColor /// 底部工具条背景颜色 public var bottomToolbar: UIColor /// 进度条背景颜色 public var progress: UIColor /// 缓冲条缓冲进度颜色 public var progressBuffer: UIColor /// 进度条播放完成颜色 public var progressFinished: UIColor /// 转子背景颜色 public var loading: UIColor public init(topToobar: UIColor = UIColor.black.withAlphaComponent(0.6), bottomToolbar: UIColor = UIColor.black.withAlphaComponent(0.6), progress: UIColor = UIColor.white.withAlphaComponent(0.35), progressBuffer: UIColor = UIColor.white.withAlphaComponent(0.5), progressFinished: UIColor = UIColor.white, loading: UIColor = UIColor.white) { self.topToobar = topToobar self.bottomToolbar = bottomToolbar self.progress = progress self.progressBuffer = progressBuffer self.progressFinished = progressFinished self.loading = loading } } public struct CLPlayerImage { /// 返回按钮图片 public var back: UIImage? /// 更多按钮图片 public var more: UIImage? /// 播放按钮图片 public var play: UIImage? /// 暂停按钮图片 public var pause: UIImage? /// 进度滑块图片 public var thumb: UIImage? /// 最大化按钮图片 public var max: UIImage? /// 最小化按钮图片 public var min: UIImage? public init(back: UIImage? = CLImageHelper.imageWithName("CLBack"), more: UIImage? = CLImageHelper.imageWithName("CLMore"), play: UIImage? = CLImageHelper.imageWithName("CLPlay"), pause: UIImage? = CLImageHelper.imageWithName("CLPause"), thumb: UIImage? = CLImageHelper.imageWithName("CLSlider"), max: UIImage? = CLImageHelper.imageWithName("CLFullscreen"), min: UIImage? = CLImageHelper.imageWithName("CLSmallscreen")) { self.back = back self.more = more self.play = play self.pause = pause self.thumb = thumb self.max = max self.min = min } } /// 顶部工具条隐藏风格 public enum CLPlayerTopBarHiddenStyle { /// 小屏和全屏都不隐藏 case never /// 小屏和全屏都隐藏 case always /// 小屏隐藏,全屏不隐藏 case onlySmall } /// 自动旋转类型 public enum CLPlayerAutoRotateStyle { /// 禁止 case none /// 只支持小屏 case small /// 只支持全屏 case fullScreen /// 全部 case all } /// 手势控制类型 public enum CLPlayerGestureInteraction { /// 禁止 case none /// 只支持小屏 case small /// 只支持全屏 case fullScreen /// 全部 case all } /// 是否隐藏更多面板 public var isHiddenMorePanel = false /// 初始界面是否显示工具条 public var isHiddenToolbarWhenStart = true /// 手势控制 public var gestureInteraction = CLPlayerGestureInteraction.fullScreen /// 自动旋转类型 public var rotateStyle = CLPlayerAutoRotateStyle.all /// 顶部工具条隐藏风格 public var topBarHiddenStyle = CLPlayerTopBarHiddenStyle.onlySmall /// 工具条自动消失时间 public var autoFadeOut = 8.0 /// 默认拉伸方式 public var videoGravity = AVLayerVideoGravity.resizeAspect /// 颜色 public var color = CLPlayerColor() /// 图片 public var image = CLPlayerImage() /// 滑块水平偏移量 public var thumbImageOffset = 0.0 /// 滑块点击范围偏移 public var thumbClickableOffset = CGPoint(x: 30, y: 40) } XQMuse/Root/CLPlayer/CLPlayerContentView/CLPlayerContentPanelCell.swift
New file @@ -0,0 +1,60 @@ // // CLPlayerContentPanelCell.swift // CLPlayer // // Created by Chen JmoVxia on 2021/12/13. // import SnapKit import UIKit class CLPlayerContentPanelCell: UICollectionViewCell { override init(frame: CGRect) { super.init(frame: frame) initSubViews() makeConstraints() } @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } private lazy var titleLabel: UILabel = { let view = UILabel() view.textAlignment = .left view.font = .systemFont(ofSize: 14) view.textColor = .white view.adjustsFontSizeToFitWidth = true return view }() var title: String? { didSet { guard title != oldValue else { return } titleLabel.text = title } } var isCurrent: Bool = false { didSet { guard isCurrent != oldValue else { return } titleLabel.textColor = isCurrent ? .orange : .white } } } private extension CLPlayerContentPanelCell { func initSubViews() { contentView.addSubview(titleLabel) } func makeConstraints() { titleLabel.snp.makeConstraints { make in make.top.equalTo(10) make.left.equalTo(15) make.right.equalTo(-15) make.bottom.equalTo(-10) } } } XQMuse/Root/CLPlayer/CLPlayerContentView/CLPlayerContentPanelHeadView.swift
New file @@ -0,0 +1,53 @@ // // CLPlayerContentPanelHeadView.swift // CLPlayer // // Created by Chen JmoVxia on 2021/12/13. // import SnapKit import UIKit class CLPlayerContentPanelHeadView: UICollectionReusableView { override init(frame: CGRect) { super.init(frame: frame) initSubViews() makeConstraints() } @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } private lazy var titleLabel: UILabel = { let view = UILabel() view.textAlignment = .left view.font = .systemFont(ofSize: 14) view.textColor = .white.withAlphaComponent(0.6) view.adjustsFontSizeToFitWidth = true return view }() var title: String? { didSet { guard title != oldValue else { return } titleLabel.text = title } } } private extension CLPlayerContentPanelHeadView { func initSubViews() { addSubview(titleLabel) } func makeConstraints() { titleLabel.snp.makeConstraints { make in make.top.equalTo(10) make.left.equalTo(15) make.right.equalTo(-15) make.bottom.equalTo(-10) } } } XQMuse/Root/CLPlayer/CLPlayerContentView/CLPlayerContentView.swift
New file @@ -0,0 +1,743 @@ // // CLPlayerContentView.swift // CLPlayer // // Created by Chen JmoVxia on 2021/10/26. // import AVFoundation import MediaPlayer import SnapKit import UIKit // MARK: - JmoVxia---枚举 extension CLPlayerContentView { enum CLPlayerScreenState { case small case animating case fullScreen } enum CLPlayerPlayState { case unknow case waiting case readyToPlay case playing case buffering case failed case pause case ended } enum CLPanDirection { case unknow case horizontal case leftVertical case rightVertical } } class CLPlayerContentView: UIView { init(config: CLPlayerConfigure) { self.config = config super.init(frame: .zero) initSubViews() makeConstraints() updateConfig() } @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } private lazy var placeholderStackView: UIStackView = { let view = UIStackView() view.isHidden = true view.axis = .horizontal view.distribution = .fill view.alignment = .fill view.insetsLayoutMarginsFromSafeArea = false view.isLayoutMarginsRelativeArrangement = true view.layoutMargins = .zero view.spacing = 0 return view }() private lazy var topToolView: UIView = { let view = UIView() view.backgroundColor = .black.withAlphaComponent(0.6) return view }() private lazy var bottomToolView: UIView = { let view = UIView() view.backgroundColor = .black.withAlphaComponent(0.6) return view }() private lazy var bottomContentView: UIView = { let view = UIView() return view }() private lazy var bottomSafeView: UIView = { let view = UIView() return view }() private lazy var loadingView: CLRotateAnimationView = { let view = CLRotateAnimationView(frame: .init(x: 0, y: 0, width: 40, height: 40)) view.startAnimation() return view }() private lazy var backButton: UIButton = { let view = UIButton() view.addTarget(self, action: #selector(backButtonAction), for: .touchUpInside) return view }() private lazy var titleLabel: UILabel = { let view = UILabel() return view }() private lazy var moreButton: UIButton = { let view = UIButton() view.addTarget(self, action: #selector(moreButtonAction), for: .touchUpInside) return view }() private lazy var playButton: UIButton = { let view = UIButton() view.addTarget(self, action: #selector(playButtonAction(_:)), for: .touchUpInside) return view }() private lazy var fullButton: UIButton = { let view = UIButton() view.addTarget(self, action: #selector(fullButtonAction(_:)), for: .touchUpInside) return view }() private lazy var currentDurationLabel: UILabel = { let view = UILabel() view.text = "00:00" view.font = .monospacedDigitSystemFont(ofSize: 14, weight: .regular) view.textColor = .white view.textAlignment = .center view.setContentCompressionResistancePriority(.required, for: .horizontal) view.setContentHuggingPriority(.required, for: .horizontal) return view }() private lazy var totalDurationLabel: UILabel = { let view = UILabel() view.text = "00:00" view.font = .monospacedDigitSystemFont(ofSize: 14, weight: .regular) view.textColor = .white view.textAlignment = .center view.setContentCompressionResistancePriority(.required, for: .horizontal) view.setContentHuggingPriority(.required, for: .horizontal) return view }() private lazy var progressView: UIProgressView = { let view = UIProgressView() view.clipsToBounds = true view.layer.cornerRadius = 1 view.trackTintColor = .white.withAlphaComponent(0.35) view.progressTintColor = .white.withAlphaComponent(0.5) return view }() private lazy var sliderView: CLSlider = { let view = CLSlider() view.isUserInteractionEnabled = false view.maximumValue = 1 view.minimumValue = 0 view.minimumTrackTintColor = .white view.addTarget(self, action: #selector(progressSliderTouchBegan(_:)), for: .touchDown) view.addTarget(self, action: #selector(progressSliderValueChanged(_:)), for: .valueChanged) view.addTarget(self, action: #selector(progressSliderTouchEnded(_:)), for: [.touchUpInside, .touchCancel, .touchUpOutside]) return view }() private lazy var failButton: UIButton = { let view = UIButton() view.isHidden = true view.titleLabel?.font = .systemFont(ofSize: 14) view.setTitle("加载失败,点击重试", for: .normal) view.setTitle("加载失败,点击重试", for: .selected) view.setTitle("加载失败,点击重试", for: .highlighted) view.setTitleColor(.white, for: .normal) view.setTitleColor(.white, for: .selected) view.setTitleColor(.white, for: .highlighted) view.addTarget(self, action: #selector(failButtonAction), for: .touchUpInside) return view }() private lazy var morePanelCollectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .vertical layout.minimumLineSpacing = .zero layout.minimumInteritemSpacing = .zero layout.sectionInset = .zero let view = UICollectionView(frame: .zero, collectionViewLayout: layout) view.register(CLPlayerContentPanelHeadView.classForCoder(), forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "CLPlayerContentPanelHeadView") view.register(CLPlayerContentPanelCell.classForCoder(), forCellWithReuseIdentifier: "CLPlayerContentPanelCell") view.delegate = self view.dataSource = self view.backgroundColor = .black.withAlphaComponent(0.8) view.alwaysBounceVertical = true view.isExclusiveTouch = true return view }() private lazy var tapGesture: UITapGestureRecognizer = { let gesture = UITapGestureRecognizer(target: self, action: #selector(tapAction)) gesture.delegate = self return gesture }() private lazy var panGesture: UIPanGestureRecognizer = { let gesture = UIPanGestureRecognizer(target: self, action: #selector(panDirection(_:))) gesture.maximumNumberOfTouches = 1 gesture.delaysTouchesBegan = true gesture.delaysTouchesEnded = true gesture.cancelsTouchesInView = true gesture.delegate = self return gesture }() private lazy var volumeSlider: UISlider? = { let view = MPVolumeView() return view.subviews.first(where: { $0 is UISlider }) as? UISlider }() private var config: CLPlayerConfigure! private var isShowMorePanel: Bool = false { didSet { guard isShowMorePanel != oldValue else { return } if isShowMorePanel { hiddenToolView() morePanelCollectionView.snp.updateConstraints { make in make.right.equalTo(0) } } else { if screenState == .fullScreen { showToolView() } morePanelCollectionView.snp.updateConstraints { make in make.right.equalTo(morePanelWidth) } } UIView.animate(withDuration: 0.25) { self.setNeedsLayout() self.layoutIfNeeded() } } } private var isHiddenToolView: Bool = true private var panDirection: CLPanDirection = .unknow private var autoFadeOutTimer: CLGCDTimer? private var rates: [Float] = [0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0] private var videoGravity: [(name: String, mode: AVLayerVideoGravity)] = [("适应", .resizeAspect), ("拉伸", .resizeAspectFill), ("填充", .resize)] private let morePanelWidth: CGFloat = max(UIScreen.main.bounds.width, UIScreen.main.bounds.height) * 0.382 weak var delegate: CLPlayerContentViewDelegate? weak var placeholderView: UIView? { didSet { guard placeholderView != oldValue else { return } placeholderStackView.isHidden = placeholderView == nil if let newView = placeholderView { placeholderStackView.addArrangedSubview(newView) } guard let oldView = oldValue else { return } placeholderStackView.removeArrangedSubview(oldView) } } var title: NSMutableAttributedString? { didSet { guard let title = title else { return } titleLabel.attributedText = title } } var currentRate: Float = 1.0 { didSet { guard currentRate != oldValue else { return } morePanelCollectionView.reloadData() delegate?.contentView(self, didChangeRate: currentRate) } } var currentVideoGravity: AVLayerVideoGravity = .resizeAspectFill { didSet { guard currentVideoGravity != oldValue else { return } morePanelCollectionView.reloadData() delegate?.contentView(self, didChangeVideoGravity: currentVideoGravity) } } var screenState: CLPlayerScreenState = .small { didSet { guard screenState != oldValue else { return } switch screenState { case .small: topToolView.isHidden = config.topBarHiddenStyle != .never hiddenMorePanel() case .animating: break case .fullScreen: topToolView.isHidden = config.topBarHiddenStyle == .always } } } var playState: CLPlayerPlayState = .unknow { didSet { guard playState != oldValue else { return } switch playState { case .unknow: sliderView.isUserInteractionEnabled = false failButton.isHidden = true playButton.isSelected = false placeholderStackView.isHidden = placeholderView == nil loadingView.startAnimation() case .waiting: sliderView.isUserInteractionEnabled = false failButton.isHidden = true placeholderStackView.isHidden = true loadingView.startAnimation() case .readyToPlay: sliderView.isUserInteractionEnabled = true case .playing: sliderView.isUserInteractionEnabled = true failButton.isHidden = true playButton.isSelected = true placeholderStackView.isHidden = true loadingView.stopAnimation() case .buffering: sliderView.isUserInteractionEnabled = true failButton.isHidden = true placeholderStackView.isHidden = true loadingView.startAnimation() case .failed: sliderView.isUserInteractionEnabled = false failButton.isHidden = false loadingView.stopAnimation() case .pause: sliderView.isUserInteractionEnabled = true playButton.isSelected = false case .ended: sliderView.isUserInteractionEnabled = true failButton.isHidden = true playButton.isSelected = false placeholderStackView.isHidden = placeholderView == nil loadingView.stopAnimation() } } } } // MARK: - JmoVxia---布局 private extension CLPlayerContentView { func initSubViews() { clipsToBounds = true autoresizesSubviews = true isUserInteractionEnabled = true addSubview(topToolView) addSubview(bottomToolView) addSubview(loadingView) topToolView.addSubview(backButton) topToolView.addSubview(titleLabel) topToolView.addSubview(moreButton) bottomToolView.addSubview(bottomContentView) bottomToolView.addSubview(bottomSafeView) bottomContentView.addSubview(playButton) bottomContentView.addSubview(fullButton) bottomContentView.addSubview(currentDurationLabel) bottomContentView.addSubview(totalDurationLabel) bottomContentView.addSubview(progressView) bottomContentView.addSubview(sliderView) addSubview(failButton) addSubview(morePanelCollectionView) addSubview(placeholderStackView) addGestureRecognizer(tapGesture) addGestureRecognizer(panGesture) guard !config.isHiddenToolbarWhenStart else { return } autoFadeOutTooView() } func makeConstraints() { topToolView.snp.makeConstraints { make in make.top.equalTo(config.isHiddenToolbarWhenStart ? -50 : 00) make.left.right.equalToSuperview() make.height.equalTo(50) } bottomToolView.snp.makeConstraints { make in make.left.right.equalToSuperview() if config.isHiddenToolbarWhenStart { make.top.equalTo(self.snp.bottom) } else { make.bottom.equalToSuperview() } } bottomSafeView.snp.makeConstraints { make in make.left.right.bottom.equalToSuperview() make.height.equalTo(0) } bottomContentView.snp.makeConstraints { make in make.left.right.top.equalToSuperview() make.height.equalTo(50) make.bottom.equalTo(bottomSafeView.snp.top) } loadingView.snp.makeConstraints { make in make.center.equalToSuperview() make.size.equalTo(40) } backButton.snp.makeConstraints { make in make.left.equalTo(-40) make.size.equalTo(40) make.centerY.equalToSuperview() } titleLabel.snp.makeConstraints { make in make.left.equalTo(backButton.snp.right).offset(15) make.right.equalTo(moreButton.snp.left).offset(-15) make.centerY.height.equalToSuperview() } moreButton.snp.makeConstraints { make in make.right.equalTo(40) make.size.equalTo(40) make.centerY.equalToSuperview() } playButton.snp.makeConstraints { make in make.left.equalTo(10) make.size.equalTo(40) make.centerY.equalToSuperview() } fullButton.snp.makeConstraints { make in make.right.equalTo(-10) make.size.equalTo(40) make.centerY.equalToSuperview() } currentDurationLabel.snp.makeConstraints { make in make.left.equalTo(playButton.snp.right).offset(10) make.centerY.equalToSuperview() } totalDurationLabel.snp.makeConstraints { make in make.right.equalTo(fullButton.snp.left).offset(-10) make.centerY.equalToSuperview() } progressView.snp.makeConstraints { make in make.left.equalTo(currentDurationLabel.snp.right).offset(15 + config.thumbImageOffset) make.centerY.equalToSuperview() make.height.equalTo(2) make.right.equalTo(totalDurationLabel.snp.left).offset(-15 - config.thumbImageOffset) } sliderView.snp.makeConstraints { make in make.left.equalTo(progressView).offset(-1) make.right.equalTo(progressView).offset(1) make.height.equalTo(30) make.centerY.equalTo(progressView) } failButton.snp.makeConstraints { make in make.center.equalToSuperview() } morePanelCollectionView.snp.makeConstraints { make in make.top.bottom.equalToSuperview() make.right.equalTo(morePanelWidth) make.width.equalTo(morePanelWidth) } placeholderStackView.snp.makeConstraints { make in make.edges.equalToSuperview() } } func updateConfig() { currentVideoGravity = config.videoGravity topToolView.isHidden = screenState == .small ? config.topBarHiddenStyle != .never : config.topBarHiddenStyle == .always moreButton.isHidden = config.isHiddenMorePanel topToolView.backgroundColor = config.color.topToobar bottomToolView.backgroundColor = config.color.bottomToolbar progressView.trackTintColor = config.color.progress progressView.progressTintColor = config.color.progressBuffer sliderView.minimumTrackTintColor = config.color.progressFinished loadingView.updateWithConfigure { $0.backgroundColor = self.config.color.loading } isHiddenToolView = config.isHiddenToolbarWhenStart backButton.setImage(config.image.back, for: .normal) moreButton.setImage(config.image.more, for: .normal) playButton.setImage(config.image.play, for: .normal) playButton.setImage(config.image.pause, for: .selected) fullButton.setImage(config.image.max, for: .normal) fullButton.setImage(config.image.min, for: .selected) sliderView.setThumbImage(config.image.thumb, for: .normal) sliderView.verticalSliderOffset = config.thumbImageOffset sliderView.thumbClickableOffset = config.thumbClickableOffset } } // MARK: - JmoVxia---objc @objc private extension CLPlayerContentView { func tapAction() { if isShowMorePanel { isShowMorePanel = false } else { isHiddenToolView ? showToolView() : hiddenToolView() } } func panDirection(_ pan: UIPanGestureRecognizer) { let locationPoint = pan.location(in: self) let veloctyPoint = pan.velocity(in: self) switch pan.state { case .began: if abs(veloctyPoint.x) > abs(veloctyPoint.y) { panDirection = .horizontal } else { panDirection = locationPoint.x < bounds.width * 0.5 ? .leftVertical : .rightVertical } case .changed: switch panDirection { case .horizontal: break case .leftVertical: UIScreen.main.brightness -= veloctyPoint.y / 10000 case .rightVertical: volumeSlider?.value -= Float(veloctyPoint.y / 10000) default: break } case .ended, .cancelled: panDirection = .unknow default: break } } func backButtonAction() { delegate?.didClickBackButton(in: self) } func moreButtonAction() { showMorePanel() } func playButtonAction(_ button: UIButton) { delegate?.contentView(self, didClickPlayButton: button.isSelected) } func fullButtonAction(_ button: UIButton) { delegate?.contentView(self, didClickFullButton: button.isSelected) } func failButtonAction() { delegate?.didClickFailButton(in: self) } func progressSliderTouchBegan(_ slider: CLSlider) { cancelAutoFadeOutTooView() delegate?.contentView(self, sliderTouchBegan: slider) } func progressSliderValueChanged(_ slider: CLSlider) { delegate?.contentView(self, sliderValueChanged: slider) } func progressSliderTouchEnded(_ slider: CLSlider) { autoFadeOutTooView() delegate?.contentView(self, sliderTouchEnded: slider) } } // MARK: - JmoVxia---私有方法 private extension CLPlayerContentView { func showMorePanel() { isShowMorePanel = true } func hiddenMorePanel() { isShowMorePanel = false } func showToolView() { isHiddenToolView = false topToolView.snp.updateConstraints { make in make.top.equalTo(0) } bottomToolView.snp.remakeConstraints { make in make.left.right.bottom.equalToSuperview() } UIView.animate(withDuration: 0.25, delay: 0) { self.setNeedsLayout() self.layoutIfNeeded() } completion: { _ in self.autoFadeOutTooView() } } func hiddenToolView() { isHiddenToolView = true topToolView.snp.updateConstraints { make in make.top.equalTo(-50) } bottomToolView.snp.remakeConstraints { make in make.left.right.equalToSuperview() make.top.equalTo(self.snp.bottom) } UIView.animate(withDuration: 0.25, delay: 0) { self.setNeedsLayout() self.layoutIfNeeded() } completion: { _ in self.cancelAutoFadeOutTooView() } } func autoFadeOutTooView() { guard config.autoFadeOut > .zero && config.autoFadeOut != .greatestFiniteMagnitude else { return } autoFadeOutTimer = CLGCDTimer(interval: 0.25 + config.autoFadeOut, initialDelay: 0.25 + config.autoFadeOut) autoFadeOutTimer?.run { [weak self] _ in self?.hiddenToolView() } } func cancelAutoFadeOutTooView() { autoFadeOutTimer = nil } } // MARK: - JmoVxia---公共方法 extension CLPlayerContentView { func animationLayout(safeAreaInsets: UIEdgeInsets, to screenState: CLPlayerScreenState) { bottomSafeView.snp.updateConstraints { make in make.height.equalTo(safeAreaInsets.bottom) } backButton.snp.updateConstraints { make in make.left.equalTo(screenState == .small ? -40 : safeAreaInsets.left + 10) } titleLabel.snp.updateConstraints { make in make.left.equalTo(backButton.snp.right).offset(screenState == .small ? 15 : 10) make.right.equalTo(moreButton.snp.left).offset(screenState == .small ? -15 : -10) } moreButton.snp.updateConstraints { make in make.right.equalTo(screenState == .small ? 40 : -safeAreaInsets.left - 10) } playButton.snp.updateConstraints { make in make.left.equalTo(safeAreaInsets.left + 10) } fullButton.snp.updateConstraints { make in make.right.equalTo(-safeAreaInsets.right - 10) } fullButton.isSelected = screenState == .fullScreen topToolView.isHidden = screenState == .small ? config.topBarHiddenStyle != .never : config.topBarHiddenStyle == .always } func setProgress(_ progress: Float, animated: Bool) { progressView.setProgress(min(max(0, progress), 1), animated: animated) } func setSliderProgress(_ progress: Float, animated: Bool) { sliderView.setValue(min(max(0, progress), 1), animated: animated) } func setTotalDuration(_ totalDuration: TimeInterval) { let time = Int(ceil(totalDuration)) let hours = time / 3600 let minutes = time / 60 let seconds = time % 60 totalDurationLabel.text = hours == .zero ? String(format: "%02ld:%02ld", minutes, seconds) : String(format: "%02ld:%02ld:%02ld", hours, minutes, seconds) } func setCurrentDuration(_ currentDuration: TimeInterval) { let time = Int(ceil(currentDuration)) let hours = time / 3600 let minutes = time / 60 let seconds = time % 60 currentDurationLabel.text = hours == .zero ? String(format: "%02ld:%02ld", minutes, seconds) : String(format: "%02ld:%02ld:%02ld", hours, minutes, seconds) } } extension CLPlayerContentView: UICollectionViewDelegate { func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) { indexPath.section == 0 ? (currentRate = rates[indexPath.row]) : (currentVideoGravity = videoGravity[indexPath.row].mode) } } extension CLPlayerContentView: UICollectionViewDelegateFlowLayout { func collectionView(_: UICollectionView, layout _: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: floor(morePanelWidth / (indexPath.section == 0 ? 5 : 3)), height: 40) } func collectionView(_: UICollectionView, layout _: UICollectionViewLayout, referenceSizeForHeaderInSection _: Int) -> CGSize { return CGSize(width: morePanelWidth, height: 40) } } extension CLPlayerContentView: UICollectionViewDataSource { func numberOfSections(in _: UICollectionView) -> Int { return 2 } func collectionView(_: UICollectionView, numberOfItemsInSection section: Int) -> Int { return section == 0 ? rates.count : videoGravity.count } func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { if kind == UICollectionView.elementKindSectionHeader { let headView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "CLPlayerContentPanelHeadView", for: indexPath) (headView as? CLPlayerContentPanelHeadView)?.title = indexPath.section == 0 ? "播放速度" : "填充模式" return headView } return UICollectionReusableView() } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CLPlayerContentPanelCell", for: indexPath) if let cell = cell as? CLPlayerContentPanelCell { cell.isCurrent = indexPath.section == 0 ? (rates[indexPath.row] == currentRate) : (videoGravity[indexPath.row].mode == currentVideoGravity) cell.title = indexPath.section == 0 ? "\(rates[indexPath.row])" : "\(videoGravity[indexPath.row].name)" } return cell } } extension CLPlayerContentView: UIGestureRecognizerDelegate { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { if !placeholderStackView.isHidden { return false } else if morePanelCollectionView.bounds.contains(touch.location(in: morePanelCollectionView)) { return false } else if topToolView.bounds.contains(touch.location(in: topToolView)) { return false } else if bottomToolView.bounds.contains(touch.location(in: bottomToolView)) { return false } else if gestureRecognizer == panGesture { guard screenState != .animating else { return false } if config.gestureInteraction == .none { return false } if config.gestureInteraction == .small, screenState == .fullScreen { return false } if config.gestureInteraction == .fullScreen, screenState == .small { return false } } return true } } XQMuse/Root/CLPlayer/CLPlayerContentView/CLPlayerContentViewDelegate.swift
New file @@ -0,0 +1,30 @@ // // CLPlayerContentViewDelegate.swift // CLPlayer // // Created by Chen JmoVxia on 2021/10/28. // import AVFoundation import Foundation import UIKit protocol CLPlayerContentViewDelegate: AnyObject { func didClickFailButton(in contentView: CLPlayerContentView) func didClickBackButton(in contentView: CLPlayerContentView) func contentView(_ contentView: CLPlayerContentView, didClickPlayButton isPlay: Bool) func contentView(_ contentView: CLPlayerContentView, didClickFullButton isFull: Bool) func contentView(_ contentView: CLPlayerContentView, didChangeRate rate: Float) func contentView(_ contentView: CLPlayerContentView, didChangeVideoGravity videoGravity: AVLayerVideoGravity) func contentView(_ contentView: CLPlayerContentView, sliderTouchBegan slider: CLSlider) func contentView(_ contentView: CLPlayerContentView, sliderValueChanged slider: CLSlider) func contentView(_ contentView: CLPlayerContentView, sliderTouchEnded slider: CLSlider) } XQMuse/Root/CLPlayer/CLPlayerContentView/CLRotateAnimationView.swift
New file @@ -0,0 +1,145 @@ // // CLRotateAnimationView.swift // CLPlayer // // Created by Chen JmoVxia on 2021/10/27. // import UIKit class CLRotateAnimationViewConfigure: NSObject { /// 开始起点 var startAngle: CGFloat = -(.pi * 0.5) /// 开始结束点 var endAngle: CGFloat = .pi * 1.5 /// 动画总时间 var duration: TimeInterval = 2 /// 动画间隔时间 var intervalDuration: TimeInterval = 0.12 /// 小球个数 var number: NSInteger = 5 /// 小球直径 var diameter: CGFloat = 8 /// 小球背景颜色 var backgroundColor: UIColor = .white fileprivate class func defaultConfigure() -> CLRotateAnimationViewConfigure { let configure = CLRotateAnimationViewConfigure() return configure } } class CLRotateAnimationView: UIView { /// 默认配置 private let configure = CLRotateAnimationViewConfigure.defaultConfigure() /// layer数组 private var layerArray: [CALayer] = Array() /// 是否开始动画 var isStart: Bool = false /// 是否暂停 private var isPause: Bool = false override init(frame: CGRect) { super.init(frame: frame) } @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } private func animation() { let origin_x: CGFloat = frame.size.width * 0.5 let origin_y: CGFloat = frame.size.height * 0.5 for item in 0 ..< configure.number { // 创建layer let scale = CGFloat(configure.number + 1 - item) / CGFloat(configure.number + 1) let layer = CALayer() layer.backgroundColor = configure.backgroundColor.cgColor layer.frame = CGRect(x: -5000, y: -5000, width: scale * configure.diameter, height: scale * configure.diameter) layer.cornerRadius = scale * configure.diameter * 0.5 // 运动路径 let pathAnimation = CAKeyframeAnimation(keyPath: "position") pathAnimation.calculationMode = .paced pathAnimation.fillMode = .forwards pathAnimation.isRemovedOnCompletion = false pathAnimation.duration = (configure.duration) - Double((configure.intervalDuration) * Double(configure.number)) pathAnimation.beginTime = Double(item) * configure.intervalDuration pathAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) pathAnimation.path = UIBezierPath(arcCenter: CGPoint(x: origin_x, y: origin_y), radius: (frame.size.width - configure.diameter) * 0.5, startAngle: configure.startAngle, endAngle: configure.endAngle, clockwise: true).cgPath // 动画组,动画组时间长于单个动画时间,会有停留效果 let group = CAAnimationGroup() group.animations = [pathAnimation] group.duration = configure.duration group.isRemovedOnCompletion = false group.fillMode = .forwards group.repeatCount = MAXFLOAT layer.add(group, forKey: "RotateAnimation") layerArray.append(layer) } } /// 更新配置 func updateWithConfigure(_ configureBlock: ((CLRotateAnimationViewConfigure) -> Void)?) { configureBlock?(configure) let intervalDuration = CGFloat(CGFloat(configure.duration) / 2.0 / CGFloat(configure.number)) configure.intervalDuration = min(configure.intervalDuration, TimeInterval(intervalDuration)) if isStart { stopAnimation() startAnimation() } } } extension CLRotateAnimationView { /// 开始动画 func startAnimation() { if layerArray.isEmpty { animation() for item in layerArray { layer.addSublayer(item) } isStart = true } } /// 停止动画 func stopAnimation() { for item in layerArray { item.removeFromSuperlayer() } layerArray.removeAll() isStart = false } /// 暂停动画 func pauseAnimation() { if isPause { return } isPause = true // 取出当前时间,转成动画暂停的时间 let pausedTime = layer.convertTime(CACurrentMediaTime(), from: nil) // 设置动画运行速度为0 layer.speed = 0.0 // 设置动画的时间偏移量,指定时间偏移量的目的是让动画定格在该时间点的位置 layer.timeOffset = pausedTime } /// 恢复动画 func resumeAnimation() { if !isPause { return } isPause = false // 获取暂停的时间差 let pausedTime = layer.timeOffset layer.speed = 1.0 layer.timeOffset = 0.0 layer.beginTime = 0.0 // 用现在的时间减去时间差,就是之前暂停的时间,从之前暂停的时间开始动画 let timeSincePause = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime layer.beginTime = timeSincePause } } XQMuse/Root/CLPlayer/CLPlayerContentView/CLSlider.swift
New file @@ -0,0 +1,38 @@ import UIKit class CLSlider: UISlider { private var lastThumbBounds = CGRect.zero var thumbClickableOffset = CGPoint(x: 30.0, y: 40.0) var verticalSliderOffset: CGFloat = 0.0 override func trackRect(forBounds bounds: CGRect) -> CGRect { let newTrackRect = super.trackRect(forBounds: bounds) return CGRect(origin: newTrackRect.origin, size: CGSize(width: newTrackRect.width, height: 2)) } override func thumbRect(forBounds bounds: CGRect, trackRect rect: CGRect, value: Float) -> CGRect { var thumbRect = rect thumbRect.origin.x = thumbRect.minX - verticalSliderOffset thumbRect.size.width = thumbRect.width + verticalSliderOffset * 2.0 lastThumbBounds = super.thumbRect(forBounds: bounds, trackRect: thumbRect, value: value) return lastThumbBounds } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let view = super.hitTest(point, with: event) guard view != self else { return view } guard point.x >= 0, point.x < bounds.width else { return view } guard point.y >= -thumbClickableOffset.x * 0.5, point.y < lastThumbBounds.height + thumbClickableOffset.y else { return view } return self } override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { let isInside = super.point(inside: point, with: event) guard !isInside else { return isInside } guard point.x >= lastThumbBounds.minX - thumbClickableOffset.x, point.x <= lastThumbBounds.maxX + thumbClickableOffset.x else { return isInside } guard point.y >= -thumbClickableOffset.y, point.y < lastThumbBounds.height + thumbClickableOffset.y else { return isInside } return true } } XQMuse/Root/CLPlayer/CLPlayerDelegate.swift
New file @@ -0,0 +1,26 @@ // // CLPlayerDelegate.swift // CLPlayer // // Created by Chen JmoVxia on 2021/12/15. // import UIKit public protocol CLPlayerDelegate: AnyObject { /// 点击顶部工具条返回按钮 func didClickBackButton(in player: CLPlayer) /// 视频播放结束 func didPlayToEnd(in player: CLPlayer) /// 播放器播放进度变化 func player(_ player: CLPlayer, playProgressChanged value: CGFloat) /// 播放器播放失败 func player(_ player: CLPlayer, playFailed error: Error?) } public extension CLPlayerDelegate { func didClickBackButton(in player: CLPlayer) {} func didPlayToEnd(in player: CLPlayer) {} func player(_ player: CLPlayer, playProgressChanged value: CGFloat) {} func player(_ player: CLPlayer, playFailed error: Error?) {} } XQMuse/Root/CLPlayer/CLPlayerView.swift
New file @@ -0,0 +1,516 @@ // // CLPlayerView.swift // CLPlayer // // Created by Chen JmoVxia on 2021/10/26. // import AVFoundation import SnapKit import UIKit extension CLPlayerView { enum CLWaitReadyToPlayState { case nomal case pause case play } } class CLPlayerView: UIView { init(config: CLPlayerConfigure) { super.init(frame: .zero) self.config = config initSubViews() makeConstraints() (layer as? AVPlayerLayer)?.videoGravity = self.config.videoGravity } @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil) NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil) } private(set) lazy var contentView: CLPlayerContentView = { let view = CLPlayerContentView(config: config) view.delegate = self return view }() private let keyWindow: UIWindow? = { if #available(iOS 13.0, *) { return UIApplication.shared.windows.filter { $0.isKeyWindow }.last } else { return UIApplication.shared.keyWindow } }() private var waitReadyToPlayState: CLWaitReadyToPlayState = .nomal private var sliderTimer: CLGCDTimer? private var bufferTimer: CLGCDTimer? private var config = CLPlayerConfigure() private var animationTransitioning: CLAnimationTransitioning? private var fullScreenController: CLFullScreenController? private var statusObserve: NSKeyValueObservation? private var loadedTimeRangesObserve: NSKeyValueObservation? private var playbackBufferEmptyObserve: NSKeyValueObservation? private var isUserPause: Bool = false private var isEnterBackground: Bool = false private var player: AVPlayer? private var playerItem: AVPlayerItem? { didSet { guard playerItem != oldValue else { return } if let oldPlayerItem = oldValue { NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: oldPlayerItem) } guard let playerItem = playerItem else { return } NotificationCenter.default.addObserver(self, selector: #selector(didPlaybackEnds), name: .AVPlayerItemDidPlayToEndTime, object: playerItem) statusObserve = playerItem.observe(\.status, options: [.new]) { [weak self] _, _ in self?.observeStatusAction() } } } private(set) var totalDuration: TimeInterval = .zero { didSet { guard totalDuration != oldValue else { return } contentView.setTotalDuration(totalDuration) } } private(set) var currentDuration: TimeInterval = .zero { didSet { guard currentDuration != oldValue else { return } contentView.setCurrentDuration(min(currentDuration, totalDuration)) } } private(set) var playbackProgress: CGFloat = .zero { didSet { guard playbackProgress != oldValue else { return } contentView.setSliderProgress(Float(playbackProgress), animated: false) let oldIntValue = Int(oldValue * 100) let intValue = Int(playbackProgress * 100) if intValue != oldIntValue { DispatchQueue.main.async { self.playProgressChanged?(CGFloat(intValue) / 100) } } } } private(set) var rate: Float = 1.0 { didSet { guard rate != oldValue else { return } play() } } var isFullScreen: Bool { return contentView.screenState == .fullScreen } var isPlaying: Bool { return contentView.playState == .playing } var isBuffering: Bool { return contentView.playState == .buffering } var isFailed: Bool { return contentView.playState == .failed } var isPaused: Bool { return contentView.playState == .pause } var isEnded: Bool { return contentView.playState == .ended } var title: NSMutableAttributedString? { didSet { guard let title = title else { return } contentView.title = title } } var url: URL? { didSet { guard let url = url else { return } stop() let session = AVAudioSession.sharedInstance() do { try session.setCategory(.playback) try session.setActive(true) } catch { print("set session error:\(error)") } playerItem = AVPlayerItem(asset: .init(url: url)) player = AVPlayer(playerItem: playerItem) (layer as? AVPlayerLayer)?.player = player } } weak var placeholder: UIView? { didSet { contentView.placeholderView = placeholder } } var backButtonTappedHandler: (() -> Void)? var playToEndHandler: (() -> Void)? var playProgressChanged: ((CGFloat) -> Void)? var playFailed: ((Error?) -> Void)? } // MARK: - JmoVxia---override extension CLPlayerView { override class var layerClass: AnyClass { return AVPlayerLayer.classForCoder() } } // MARK: - JmoVxia---布局 private extension CLPlayerView { func initSubViews() { backgroundColor = .black addSubview(contentView) NotificationCenter.default.addObserver(self, selector: #selector(appDidEnterBackground), name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(appDidEnterPlayground), name: UIApplication.didBecomeActiveNotification, object: nil) if !UIDevice.current.isGeneratingDeviceOrientationNotifications { UIDevice.current.beginGeneratingDeviceOrientationNotifications() } NotificationCenter.default.addObserver(self, selector: #selector(deviceOrientationDidChange), name: UIDevice.orientationDidChangeNotification, object: nil) } func makeConstraints() { contentView.snp.makeConstraints { make in make.edges.equalToSuperview() } } } // MARK: - JmoVxia---objc @objc private extension CLPlayerView { func didPlaybackEnds() { currentDuration = totalDuration playbackProgress = 1.0 contentView.playState = .ended sliderTimer?.pause() DispatchQueue.main.async { self.playToEndHandler?() } } func deviceOrientationDidChange() { guard config.rotateStyle != .none else { return } if config.rotateStyle == .small, isFullScreen { return } if config.rotateStyle == .fullScreen, !isFullScreen { return } switch UIDevice.current.orientation { case .portrait: dismiss() case .landscapeLeft: presentWithOrientation(.left) case .landscapeRight: presentWithOrientation(.right) default: break } } func appDidEnterBackground() { isEnterBackground = true pause() } func appDidEnterPlayground() { isEnterBackground = false guard contentView.playState != .ended else { return } play() } } // MARK: - JmoVxia---observe private extension CLPlayerView { func observeStatusAction() { guard let playerItem = playerItem else { return } if playerItem.status == .readyToPlay { contentView.playState = .readyToPlay totalDuration = TimeInterval(playerItem.duration.value) / TimeInterval(playerItem.duration.timescale) sliderTimer = CLGCDTimer(interval: 0.1) sliderTimer?.run { [weak self] _ in self?.sliderTimerAction() } loadedTimeRangesObserve = playerItem.observe(\.loadedTimeRanges, options: [.new]) { [weak self] _, _ in self?.observeLoadedTimeRangesAction() } playbackBufferEmptyObserve = playerItem.observe(\.isPlaybackBufferEmpty, options: [.new]) { [weak self] _, _ in self?.observePlaybackBufferEmptyAction() } switch waitReadyToPlayState { case .nomal: break case .pause: pause() case .play: play() } } else if playerItem.status == .failed { contentView.playState = .failed DispatchQueue.main.async { self.playFailed?(playerItem.error) } } } func observeLoadedTimeRangesAction() { guard let timeInterval = availableDuration() else { return } guard let duration = playerItem?.duration else { return } let totalDuration = TimeInterval(CMTimeGetSeconds(duration)) contentView.setProgress(Float(timeInterval / totalDuration), animated: false) } func observePlaybackBufferEmptyAction() { guard playerItem?.isPlaybackBufferEmpty ?? false else { return } bufferingSomeSecond() } } private extension CLPlayerView { func availableDuration() -> TimeInterval? { guard let timeRange = playerItem?.loadedTimeRanges.first?.timeRangeValue else { return nil } let startSeconds = CMTimeGetSeconds(timeRange.start) let durationSeconds = CMTimeGetSeconds(timeRange.duration) return .init(startSeconds + durationSeconds) } func bufferingSomeSecond() { guard playerItem?.status == .readyToPlay else { return } guard contentView.playState != .failed else { return } player?.pause() sliderTimer?.pause() contentView.playState = .buffering bufferTimer = CLGCDTimer(interval: 3.0, initialDelay: 3.0) bufferTimer?.run { [weak self] _ in guard let playerItem = self?.playerItem else { return } self?.bufferTimer = nil if playerItem.isPlaybackLikelyToKeepUp { self?.play() } else { self?.bufferingSomeSecond() } } } func sliderTimerAction() { guard let playerItem = playerItem else { return } guard playerItem.duration.timescale != .zero else { return } currentDuration = CMTimeGetSeconds(playerItem.currentTime()) playbackProgress = currentDuration / totalDuration } } // MARK: - JmoVxia---Screen private extension CLPlayerView { func dismiss() { guard Thread.isMainThread else { return DispatchQueue.main.async { self.dismiss() } } guard contentView.screenState == .fullScreen else { return } guard let controller = fullScreenController else { return } contentView.screenState = .animating controller.dismiss(animated: true, completion: { self.contentView.screenState = .small self.fullScreenController = nil UIViewController.attemptRotationToDeviceOrientation() }) } func presentWithOrientation(_ orientation: CLAnimationTransitioning.AnimationOrientation) { guard Thread.isMainThread else { return DispatchQueue.main.async { self.presentWithOrientation(orientation) } } guard superview != nil else { return } guard fullScreenController == nil else { return } guard contentView.screenState == .small else { return } guard let rootViewController = keyWindow?.rootViewController else { return } contentView.screenState = .animating animationTransitioning = CLAnimationTransitioning(playerView: self, animationOrientation: orientation) fullScreenController = orientation == .right ? CLFullScreenLeftController() : CLFullScreenRightController() fullScreenController?.transitioningDelegate = self fullScreenController?.modalPresentationStyle = .fullScreen rootViewController.present(fullScreenController!, animated: true, completion: { self.contentView.screenState = .fullScreen UIViewController.attemptRotationToDeviceOrientation() }) } } // MARK: - JmoVxia---公共方法 extension CLPlayerView { func play() { guard !isEnterBackground else { return } guard !isUserPause else { return } guard let playerItem = playerItem else { return } guard playerItem.status == .readyToPlay else { contentView.playState = .waiting waitReadyToPlayState = .play return } guard playerItem.isPlaybackLikelyToKeepUp else { bufferingSomeSecond() return } if contentView.playState == .ended { player?.seek(to: CMTimeMake(value: 0, timescale: 1), toleranceBefore: .zero, toleranceAfter: .zero) } contentView.playState = .playing player?.play() player?.rate = rate sliderTimer?.resume() waitReadyToPlayState = .nomal bufferTimer = nil } func pause() { guard playerItem?.status == .readyToPlay else { waitReadyToPlayState = .pause return } contentView.playState = .pause player?.pause() sliderTimer?.pause() bufferTimer = nil waitReadyToPlayState = .nomal } func stop() { statusObserve?.invalidate() loadedTimeRangesObserve?.invalidate() playbackBufferEmptyObserve?.invalidate() statusObserve = nil loadedTimeRangesObserve = nil playbackBufferEmptyObserve = nil playerItem = nil player = nil isUserPause = false waitReadyToPlayState = .nomal contentView.playState = .unknow contentView.setProgress(0, animated: false) playbackProgress = 0 totalDuration = 0 currentDuration = 0 sliderTimer = nil } } // MARK: - JmoVxia---UIViewControllerTransitioningDelegate extension CLPlayerView: UIViewControllerTransitioningDelegate { func animationController(forPresented _: UIViewController, presenting _: UIViewController, source _: UIViewController) -> UIViewControllerAnimatedTransitioning? { animationTransitioning?.animationType = .present return animationTransitioning } func animationController(forDismissed _: UIViewController) -> UIViewControllerAnimatedTransitioning? { animationTransitioning?.animationType = .dismiss return animationTransitioning } } // MARK: - JmoVxia---CLPlayerContentViewDelegate extension CLPlayerView: CLPlayerContentViewDelegate { func contentView(_ contentView: CLPlayerContentView, didClickPlayButton isPlay: Bool) { isUserPause = isPlay isPlay ? pause() : play() } func contentView(_ contentView: CLPlayerContentView, didClickFullButton isFull: Bool) { isFull ? dismiss() : presentWithOrientation(.fullRight) } func contentView(_ contentView: CLPlayerContentView, didChangeRate rate: Float) { self.rate = rate } func contentView(_ contentView: CLPlayerContentView, didChangeVideoGravity videoGravity: AVLayerVideoGravity) { (layer as? AVPlayerLayer)?.videoGravity = videoGravity } func contentView(_ contentView: CLPlayerContentView, sliderTouchBegan slider: CLSlider) { pause() } func contentView(_ contentView: CLPlayerContentView, sliderValueChanged slider: CLSlider) { currentDuration = totalDuration * TimeInterval(slider.value) let dragedCMTime = CMTimeMake(value: Int64(ceil(currentDuration)), timescale: 1) player?.seek(to: dragedCMTime, toleranceBefore: .zero, toleranceAfter: .zero) } func contentView(_ contentView: CLPlayerContentView, sliderTouchEnded slider: CLSlider) { guard let playerItem = playerItem else { return } if slider.value == 1 { didPlaybackEnds() } else if playerItem.isPlaybackLikelyToKeepUp { play() } else { bufferingSomeSecond() } } func didClickFailButton(in _: CLPlayerContentView) { guard let url = url else { return } self.url = url } func didClickBackButton(in contentView: CLPlayerContentView) { guard contentView.screenState == .fullScreen else { return } DispatchQueue.main.async { self.dismiss() self.backButtonTappedHandler?() } } } XQMuse/Root/Course/VC/CourseDetialVideoVC.swift
@@ -9,8 +9,11 @@ class CourseDetialVideoVC: BaseVC { @IBOutlet weak var view_bg_video: UIView! @IBOutlet weak var tableView: UITableView! private var videoView:VideoView? override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) (navigationItem.leftBarButtonItem?.customView as? UIButton)?.setImage(UIImage(named: "btn_back")?.withTintColor(.white), for: .normal) @@ -20,6 +23,12 @@ super.viewDidLoad() title = "课程详情" videoView = VideoView(url: "http://vfx.mtime.cn/Video/2021/07/10/mp4/210710094507540173.mp4") view_bg_video.addSubview(videoView!) videoView!.snp.makeConstraints { make in make.edges.equalToSuperview() } tableView.separatorStyle = .none tableView.delegate = self tableView.dataSource = self XQMuse/Root/Course/VC/CourseDetialVideoVC.xib
@@ -13,6 +13,7 @@ <connections> <outlet property="tableView" destination="9tI-Cr-xM8" id="y8l-vs-uxu"/> <outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/> <outlet property="view_bg_video" destination="a5v-5f-tro" id="wdA-dL-NSi"/> </connections> </placeholder> <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> XQMuse/Root/Course/VC/CourseVCOfficalCommentVC.swift
@@ -48,6 +48,10 @@ make.edges.equalToSuperview() } } override var shouldAutorotate: Bool{ return false } } extension CourseVCOfficalCommentVC:UICollectionViewDelegate & UICollectionViewDataSource{ XQMuse/Root/Course/VC/CourseVCTeacherSpecialVC.swift
@@ -39,11 +39,15 @@ } DispatchQueue.main.async { self.headerView = VideoView() self.headerView.frame = CGRect(x: 0, y: 0, width: JQ_ScreenW, height: JQ_ScreenW * 0.56) self.headerView = VideoView(url: "http://vfx.mtime.cn/Video/2021/07/10/mp4/210710094507540173.mp4") self.tableView!.tableHeaderView = self.headerView self.headerView.frame = CGRect(x: 0, y: 0, width: JQ_ScreenW, height: JQ_ScreenW * 0.56) } } override var shouldAutorotate: Bool{ return true } } extension CourseVCTeacherSpecialVC:UITableViewDataSource{ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { XQMuse/Root/Other/View/VideoView.swift
@@ -7,16 +7,49 @@ import UIKit import JQTools import AVKit import RxSwift class VideoView: UIView { override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .jq_randomColor private var url:String? private var disposeBag = DisposeBag() private lazy var player: CLPlayer = { let p = CLPlayer(frame: .zero) { config in config.topBarHiddenStyle = .always config.isHiddenMorePanel = true config.image.max = UIImage(named: "video_max") config.image.min = UIImage(named: "video_min") config.image.pause = UIImage(named: "video_pause") config.image.play = UIImage(named: "video_play") config.color.progressFinished = UIColor(hexStr: "#B7DC90") config.color.progress = .white config.color.progressBuffer = UIColor(hexStr: "#B7DC90").withAlphaComponent(0.75) config.image.thumb = UIImage.jq_image(color: UIColor(hexStr: "#B7DC90"), size: CGSize(width: 6.5, height: 6.5), corners: .allCorners, radius: 3.25) } return p }() required init(url:String) { super.init(frame: .zero) self.url = url if let Url = URL(string: url){ player.url = Url addSubview(player) player.delegate = self player.snp.makeConstraints { make in make.edges.equalToSuperview() } player.play() } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension VideoView:CLPlayerDelegate{ }