杨锴
2024-08-28 4d776ec3b272c40c5d9058af875211811835793f
fix UI
43个文件已添加
11个文件已修改
2557 ■■■■■ 已修改文件
XQMuse.xcodeproj/project.pbxproj 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/AppDelegate.swift 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Assets.xcassets/Btns/video_max.imageset/Contents.json 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Assets.xcassets/Btns/video_max.imageset/video_max@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Assets.xcassets/Btns/video_max.imageset/video_max@3x.png 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Assets.xcassets/Btns/video_min.imageset/Contents.json 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Assets.xcassets/Btns/video_min.imageset/video_min@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Assets.xcassets/Btns/video_min.imageset/video_min@3x.png 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Assets.xcassets/Btns/video_pause.imageset/Contents.json 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Assets.xcassets/Btns/video_pause.imageset/video_pause@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Assets.xcassets/Btns/video_pause.imageset/video_pause@3x.png 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Assets.xcassets/Btns/video_play.imageset/Contents.json 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Assets.xcassets/Btns/video_play.imageset/video_play@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Assets.xcassets/Btns/video_play.imageset/video_play@3x.png 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Base/BaseNav.swift 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Base/BaseTabBarVC.swift 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Base/BaseVC.swift 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Info.plist 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/.DS_Store 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLFullScreenController/CLAnimationTransitioning.swift 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLFullScreenController/CLFullScreenController.swift 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLFullScreenController/CLFullScreenLeftController.swift 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLFullScreenController/CLFullScreenRightController.swift 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLGCDTimer.swift 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLImageHelper.swift 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLBack@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLBack@3x.png 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLFullscreen@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLFullscreen@3x.png 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLMore@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLMore@3x.png 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLPause@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLPause@3x.png 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLPlay@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLPlay@3x.png 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLSlider@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLSlider@3x.png 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLSmallscreen@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayer.bundle/CLSmallscreen@3x.png 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayer.swift 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayerConfigure.swift 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayerContentView/CLPlayerContentPanelCell.swift 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayerContentView/CLPlayerContentPanelHeadView.swift 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayerContentView/CLPlayerContentView.swift 743 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayerContentView/CLPlayerContentViewDelegate.swift 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayerContentView/CLRotateAnimationView.swift 145 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayerContentView/CLSlider.swift 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayerDelegate.swift 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/CLPlayer/CLPlayerView.swift 516 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/Course/VC/CourseDetialVideoVC.swift 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/Course/VC/CourseDetialVideoVC.xib 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/Course/VC/CourseVCOfficalCommentVC.swift 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/Course/VC/CourseVCTeacherSpecialVC.swift 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
XQMuse/Root/Other/View/VideoView.swift 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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_Store
Binary 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,7 +9,10 @@
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)
@@ -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{
                
}