Bladeren bron

版本 10

100Years 3 maanden geleden
bovenliggende
commit
3a6aef1d8d
21 gewijzigde bestanden met toevoegingen van 1299 en 141 verwijderingen
  1. 16 10
      TSLiveWallpaper.xcodeproj/project.pbxproj
  2. 140 22
      TSLiveWallpaper/Business/TSEditLiveVC/TSEditLiveVC.swift
  3. 9 17
      TSLiveWallpaper/Business/TSHomeVC/TSHomeVC.swift
  4. 2 2
      TSLiveWallpaper/Business/TSHomeVC/TSLiveWallpaperBrowseVC/EasyVC/TSLiveWallpaperCopyrightVC.swift
  5. 108 56
      TSLiveWallpaper/Business/TSHomeVC/TSLiveWallpaperBrowseVC/TSLiveWallpaperBrowseVC.swift
  6. 2 0
      TSLiveWallpaper/Business/TSHomeVC/View/TSHomeTopBannerCell.swift
  7. 5 3
      TSLiveWallpaper/Business/TSMineVC/TSMineVC.swift
  8. 2 2
      TSLiveWallpaper/Business/TSRandomWallpaperVC/EasyVC/TSRandomWallpaperCopyrightVC.swift
  9. 6 0
      TSLiveWallpaper/Business/TSRandomWallpaperVC/TSRandomWallpaperVC.swift
  10. 21 15
      TSLiveWallpaper/Business/TSViewTool/TSViewTool.swift
  11. 99 2
      TSLiveWallpaper/Common/ThirdParty/LivePhoto.swift
  12. 269 0
      TSLiveWallpaper/Common/ThirdParty/LivePhotoCreater.swift
  13. BIN
      TSLiveWallpaper/Common/ThirdParty/Util/1.mov
  14. 4 1
      TSLiveWallpaper/Common/ThirdParty/Util/Converter4Video.swift
  15. 123 2
      TSLiveWallpaper/Common/ThirdParty/Util/LivePhotoConverter.swift
  16. BIN
      TSLiveWallpaper/Common/ThirdParty/Util/origin.mp4
  17. 450 0
      TSLiveWallpaper/Common/ThirdParty/VideoRecorder.swift
  18. 31 0
      TSLiveWallpaper/Common/Tool/TSNetworkTool.swift
  19. 1 1
      TSLiveWallpaper/Common/Tool/TSToastTool.swift
  20. 11 4
      TSLiveWallpaper/DataManger/TSImageDataCenter.swift
  21. 0 4
      TSLiveWallpaper/Resource/Json/response.json

+ 16 - 10
TSLiveWallpaper.xcodeproj/project.pbxproj

@@ -82,14 +82,15 @@
 		A84C239F2D1E88CD00B61B55 /* TSFileManagerTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = A84C239E2D1E88C500B61B55 /* TSFileManagerTool.swift */; };
 		A8C4C01D2D2397B9003C46FC /* UIViewController+Ex.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C4C01C2D2397B4003C46FC /* UIViewController+Ex.swift */; };
 		A8C4C0982D242154003C46FC /* LivePhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = A858EE162D1CF49B004B680F /* LivePhoto.swift */; };
-		A8C4C0A22D24218A003C46FC /* origin.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = A8C4C09F2D24218A003C46FC /* origin.mp4 */; };
-		A8C4C0A32D24218A003C46FC /* 1.mov in Resources */ = {isa = PBXBuildFile; fileRef = A8C4C0A02D24218A003C46FC /* 1.mov */; };
 		A8C4C0A42D24218A003C46FC /* metadata.mov in Resources */ = {isa = PBXBuildFile; fileRef = A8C4C09E2D24218A003C46FC /* metadata.mov */; };
 		A8C4C0A52D24218A003C46FC /* Converter4Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C4C09B2D24218A003C46FC /* Converter4Video.swift */; };
 		A8C4C0A62D24218A003C46FC /* AVAssetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C4C0992D24218A003C46FC /* AVAssetExtension.swift */; };
 		A8C4C0A72D24218A003C46FC /* LivePhotoUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = A8C4C09D2D24218A003C46FC /* LivePhotoUtil.m */; };
 		A8C4C0A82D24218A003C46FC /* Converter4Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C4C09A2D24218A003C46FC /* Converter4Image.swift */; };
 		A8C4C0AB2D2427E7003C46FC /* LivePhotoConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C4C0AA2D2427D3003C46FC /* LivePhotoConverter.swift */; };
+		A8C4C0E62D268D02003C46FC /* LivePhotoCreater.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C4C0E42D268D02003C46FC /* LivePhotoCreater.swift */; };
+		A8C4C0E72D268D02003C46FC /* VideoRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C4C0E52D268D02003C46FC /* VideoRecorder.swift */; };
+		A8C4C0EF2D27BFF7003C46FC /* TSNetworkTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C4C0EE2D27BFEA003C46FC /* TSNetworkTool.swift */; };
 		A8E56BF62D1520EC003C54AF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8E56BEC2D1520EC003C54AF /* AppDelegate.swift */; };
 		A8E56BF92D1520EC003C54AF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A8E56BED2D1520EC003C54AF /* Assets.xcassets */; };
 		A8E56BFB2D1520EC003C54AF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A8E56BF02D1520EC003C54AF /* LaunchScreen.storyboard */; };
@@ -182,10 +183,11 @@
 		A8C4C09C2D24218A003C46FC /* LivePhotoUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LivePhotoUtil.h; sourceTree = "<group>"; };
 		A8C4C09D2D24218A003C46FC /* LivePhotoUtil.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LivePhotoUtil.m; sourceTree = "<group>"; };
 		A8C4C09E2D24218A003C46FC /* metadata.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; path = metadata.mov; sourceTree = "<group>"; };
-		A8C4C09F2D24218A003C46FC /* origin.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = origin.mp4; sourceTree = "<group>"; };
-		A8C4C0A02D24218A003C46FC /* 1.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; path = 1.mov; sourceTree = "<group>"; };
 		A8C4C0A92D242204003C46FC /* Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Bridging-Header.h"; sourceTree = "<group>"; };
 		A8C4C0AA2D2427D3003C46FC /* LivePhotoConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LivePhotoConverter.swift; sourceTree = "<group>"; };
+		A8C4C0E42D268D02003C46FC /* LivePhotoCreater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LivePhotoCreater.swift; sourceTree = "<group>"; };
+		A8C4C0E52D268D02003C46FC /* VideoRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRecorder.swift; sourceTree = "<group>"; };
+		A8C4C0EE2D27BFEA003C46FC /* TSNetworkTool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSNetworkTool.swift; sourceTree = "<group>"; };
 		A8E56BD42D1520DD003C54AF /* TSLiveWallpaper.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TSLiveWallpaper.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		A8E56BEC2D1520EC003C54AF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
 		A8E56BED2D1520EC003C54AF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@@ -314,6 +316,7 @@
 		A81CA4882D15840F00A3AAC8 /* Tool */ = {
 			isa = PBXGroup;
 			children = (
+				A8C4C0EE2D27BFEA003C46FC /* TSNetworkTool.swift */,
 				A84C239E2D1E88C500B61B55 /* TSFileManagerTool.swift */,
 				A81F5B4A2D19658300740085 /* PhotoTools.swift */,
 				A81CA4B52D169F1A00A3AAC8 /* WindowHelper.swift */,
@@ -545,6 +548,8 @@
 		A858EE182D1CF635004B680F /* ThirdParty */ = {
 			isa = PBXGroup;
 			children = (
+				A8C4C0E42D268D02003C46FC /* LivePhotoCreater.swift */,
+				A8C4C0E52D268D02003C46FC /* VideoRecorder.swift */,
 				A8C4C0A12D24218A003C46FC /* Util */,
 				A858EE162D1CF49B004B680F /* LivePhoto.swift */,
 			);
@@ -561,8 +566,6 @@
 				A8C4C09C2D24218A003C46FC /* LivePhotoUtil.h */,
 				A8C4C09D2D24218A003C46FC /* LivePhotoUtil.m */,
 				A8C4C09E2D24218A003C46FC /* metadata.mov */,
-				A8C4C09F2D24218A003C46FC /* origin.mp4 */,
-				A8C4C0A02D24218A003C46FC /* 1.mov */,
 			);
 			path = Util;
 			sourceTree = "<group>";
@@ -681,8 +684,6 @@
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				A8C4C0A22D24218A003C46FC /* origin.mp4 in Resources */,
-				A8C4C0A32D24218A003C46FC /* 1.mov in Resources */,
 				A8C4C0A42D24218A003C46FC /* metadata.mov in Resources */,
 				A8E56BF92D1520EC003C54AF /* Assets.xcassets in Resources */,
 				A81F5B522D19685900740085 /* response.json in Resources */,
@@ -743,12 +744,15 @@
 				A81CA4692D156AB600A3AAC8 /* TSBaseVC.swift in Sources */,
 				A81CA4992D1652C400A3AAC8 /* TSMineVC.swift in Sources */,
 				A8477C992D2291F800DF0B93 /* UserDefault+Ex.swift in Sources */,
+				A8C4C0EF2D27BFF7003C46FC /* TSNetworkTool.swift in Sources */,
 				A81CA46E2D156C7000A3AAC8 /* GlobalImports.swift in Sources */,
 				A81F5B4D2D1965F800740085 /* UIImage+Ex.swift in Sources */,
 				A81CA4832D157F5C00A3AAC8 /* UIImageView+Ex.swift in Sources */,
 				A81F5B322D18FA2E00740085 /* Component.swift in Sources */,
 				A81F5B332D18FA2E00740085 /* CommonSectionComponent.swift in Sources */,
 				A81F5B492D1956EA00740085 /* UIScreen.swift in Sources */,
+				A8C4C0E62D268D02003C46FC /* LivePhotoCreater.swift in Sources */,
+				A8C4C0E72D268D02003C46FC /* VideoRecorder.swift in Sources */,
 				A81F5B342D18FA2E00740085 /* CollectionViewComponent.swift in Sources */,
 				A81CA4722D1575B900A3AAC8 /* TSBaseNavigationBarView.swift in Sources */,
 				A81F5B5B2D1A5F2300740085 /* TSHomeTopBannerCell.swift in Sources */,
@@ -848,7 +852,7 @@
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 4;
+				CURRENT_PROJECT_VERSION = 10;
 				DEVELOPMENT_TEAM = 65UD255J84;
 				ENABLE_APP_SANDBOX = NO;
 				ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -859,6 +863,7 @@
 				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
 				INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
 				INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
@@ -883,7 +888,7 @@
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 4;
+				CURRENT_PROJECT_VERSION = 10;
 				DEVELOPMENT_TEAM = 65UD255J84;
 				ENABLE_APP_SANDBOX = NO;
 				ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -894,6 +899,7 @@
 				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
 				INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
 				INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",

+ 140 - 22
TSLiveWallpaper/Business/TSEditLiveVC/TSEditLiveVC.swift

@@ -110,7 +110,9 @@ extension TSEditLiveVC: UIImagePickerControllerDelegate {
         picker.allowsEditing = true // 启用编辑功能
         picker.delegate = self
         picker.videoMaximumDuration = 3.0
-        present(picker, animated: true, completion: nil)
+        present(picker, animated: true) {
+            TSToastShared.hideLoading()
+        }
     }
     // 用户完成选择
     func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
@@ -132,7 +134,7 @@ extension TSEditLiveVC: UIImagePickerControllerDelegate {
 //            LivePhotoConverter.convertVideo(targetURL) { success, image, video, msg in
 //                debugPrint(msg)
 //            }
-            
+  
             saveLive(videoPath: targetURL)
         }
         picker.dismiss(animated: true, completion: nil)
@@ -176,7 +178,7 @@ extension TSEditLiveVC{
                 
                 if let imageURL = imageURL,let videoURL = videoURL {
                     LivePhotoConverter.saveToLibrary(videoURL: videoURL, imageURL: imageURL) { success in
-                        kSavePhotoSuccesswShared.show(atView: self.view)
+                        kSavePhotoSuccesswShared.show(atView: self.view,text: "DIY Successfully".localized)
                     }
                     
                     
@@ -191,7 +193,7 @@ extension TSEditLiveVC{
                     let itemModel = TSImageDataItemModel()
                     itemModel.imageUrl = TSFileManagerTool.getCacheSubPath(at: saveImageURL)!
                     itemModel.videoUrl = TSFileManagerTool.getCacheSubPath(at: saveVideoURL)!
-                    self.editLiveHistorySectionModel.items.append(itemModel)
+                    self.editLiveHistorySectionModel.items.insert(itemModel, at: 0)
                     kImageDataCenterShared.editLiveHistoryListArray = [self.editLiveHistorySectionModel]
                     self.reloadView()
                 }
@@ -199,30 +201,146 @@ extension TSEditLiveVC{
                 debugPrint("Live Photo Not Saved,The live photo was not saved to Photos.")
             }
         }
+        
+        
+        
+        
+//        let ts = Date().timeIntervalSince1970
+//        let documentURL = TSFileManagerTool.documentsDirectory.appendingPathComponent("\(Int(ts)).mov")
+//        let documentURL = TSFileManagerTool.documentsDirectory    
+//        Converter4Video(path: videoPath.path).resizeVideo(at: videoPath.path, outputPath: documentURL.path, outputSize: CGSize(width: 1080, height: 1920)) { success, error in
+//            guard success else{
+//                debugPrint(error)
+//                return
+//            }
+//         
+//            LivePhoto.generate(from: nil, videoURL: documentURL) { progress in
+//    
+//            } completion: {[weak self] (livePhoto, resources) in
+//                guard let self = self else { return }
+//    
+//                if let resources = resources {
+//                    LivePhoto.saveToLibrary(resources, completion: { (success) in
+//                        kExecuteOnMainThread {
+//                            TSToastShared.hideLoading()
+//                            if success {
+//                                debugPrint("Live Photo Saved,The live photo was successfully saved to Photos.")
+//                                kSavePhotoSuccesswShared.show(atView: self.view)
+//                            }else {
+//                                debugPrint("Live Photo Not Saved,The live photo was not saved to Photos.")
+//                            }
+//    
+//                            TSFileManagerTool.removeItem(from: resources.pairedImage)
+//                            TSFileManagerTool.removeItem(from: resources.pairedVideo)
+//                        }
+//                    })
+//                }
+//            }
+//        }
+        
+//        LivePhoto.resizeVideoToFixedHeight(videoURL: videoPath, outputFolder: documentURL) { outputURL in
+//            if let outputURL = outputURL {
+//                print("Resized video saved to: \(outputURL)")
+//                
+//                LivePhoto.generate(from: nil, videoURL: outputURL) { progress in
+//        
+//                } completion: {[weak self] (livePhoto, resources) in
+//                    guard let self = self else { return }
+//        
+//                    if let resources = resources {
+//                        LivePhoto.saveToLibrary(resources, completion: { (success) in
+//                            kExecuteOnMainThread {
+//                                TSToastShared.hideLoading()
+//                                if success {
+//                                    debugPrint("Live Photo Saved,The live photo was successfully saved to Photos.")
+//                                    kSavePhotoSuccesswShared.show(atView: self.view)
+//                                }else {
+//                                    debugPrint("Live Photo Not Saved,The live photo was not saved to Photos.")
+//                                }
+//        
+//                                TSFileManagerTool.removeItem(from: resources.pairedImage)
+//                                TSFileManagerTool.removeItem(from: resources.pairedVideo)
+//                            }
+//                        })
+//                    }
+//                }
+//                
+//            } else {
+//                print("Failed to resize video.")
+//            }
+//        }
+        
+        
+        
+
 
-//        LivePhoto.generate(from: nil, videoURL: videoPath) { progress in
+        
+//        VideoRecorder.shared.saveLivePhoto(duration: 2.5, outputDirectory: TSFileManagerTool.saveLiveVideoPathURL) { [weak self] recordHandler in
 //
-//        } completion: {[weak self] (livePhoto, resources) in
+////                    recordHandler?()
+//        
+//        } completion: { [weak self] videoURL, imageURL, errorMsg in
 //            guard let self = self else { return }
-//
-//            if let resources = resources {
-//                LivePhoto.saveToLibrary(resources, completion: { (success) in
-//                    kExecuteOnMainThread {
-//                        TSToastShared.hideLoading()
-//                        if success {
-//                            debugPrint("Live Photo Saved,The live photo was successfully saved to Photos.")
-//                            kSavePhotoSuccesswShared.show(atView: self.view)
-//                        }else {
-//                            debugPrint("Live Photo Not Saved,The live photo was not saved to Photos.")
-//                        }
-//
-//                        TSFileManagerTool.removeItem(from: resources.pairedImage)
-//                        TSFileManagerTool.removeItem(from: resources.pairedVideo)
-//                    }
-//                })
+////        }
+////        LivePhotoCreater().saveLivePhoto(from: videoPath, outputDirectory: TSFileManagerTool.saveLiveVideoPathURL) { videoURL, imageURL, errorMsg in
+//            if let imageURL = imageURL,let videoURL = videoURL {
+//                LivePhotoConverter.saveToLibrary(videoURL: videoURL, imageURL: imageURL) { success in
+//                    kSavePhotoSuccesswShared.show(atView: self.view)
+//                }
+//                
+//                
+//                let saveURL = TSFileManagerTool.saveLiveVideoPathURL
+//                let timestampString = Date.timestampString
+//                let saveImageURL = saveURL.appendingPathComponent("image\(timestampString).heic")
+//                let saveVideoURL = saveURL.appendingPathComponent("video\(timestampString).mov")
+//                TSFileManagerTool.copyFileWithOverwrite(from: imageURL, to: saveImageURL)
+//                TSFileManagerTool.copyFileWithOverwrite(from: videoURL, to: saveVideoURL)
+//                
+//                
+//                let itemModel = TSImageDataItemModel()
+//                itemModel.imageUrl = TSFileManagerTool.getCacheSubPath(at: saveImageURL)!
+//                itemModel.videoUrl = TSFileManagerTool.getCacheSubPath(at: saveVideoURL)!
+//                self.editLiveHistorySectionModel.items.append(itemModel)
+//                kImageDataCenterShared.editLiveHistoryListArray = [self.editLiveHistorySectionModel]
+//                self.reloadView()
 //            }
 //        }
     }
+    
+//    func saveLivePhotoVideoRecorder(){
+//        
+//        
+//        VideoRecorder.shared.saveLivePhoto(duration: 3.0, outputDirectory: TSFileManagerTool.saveLiveVideoPathURL) { [weak self] recordHandler in
+//
+////                    recordHandler?()
+//        
+//        } completion: { [weak self] videoURL, imageURL, errorMsg in
+//            guard let self = self else { return }
+////        }
+////        LivePhotoCreater().saveLivePhoto(from: videoPath, outputDirectory: TSFileManagerTool.saveLiveVideoPathURL) { videoURL, imageURL, errorMsg in
+//            if let imageURL = imageURL,let videoURL = videoURL {
+//                LivePhotoConverter.saveToLibrary(videoURL: videoURL, imageURL: imageURL) { success in
+//                    kSavePhotoSuccesswShared.show(atView: self.view)
+//                }
+//                
+//                
+//                let saveURL = TSFileManagerTool.saveLiveVideoPathURL
+//                let timestampString = Date.timestampString
+//                let saveImageURL = saveURL.appendingPathComponent("image\(timestampString).heic")
+//                let saveVideoURL = saveURL.appendingPathComponent("video\(timestampString).mov")
+//                TSFileManagerTool.copyFileWithOverwrite(from: imageURL, to: saveImageURL)
+//                TSFileManagerTool.copyFileWithOverwrite(from: videoURL, to: saveVideoURL)
+//                
+//                
+//                let itemModel = TSImageDataItemModel()
+//                itemModel.imageUrl = TSFileManagerTool.getCacheSubPath(at: saveImageURL)!
+//                itemModel.videoUrl = TSFileManagerTool.getCacheSubPath(at: saveVideoURL)!
+//                self.editLiveHistorySectionModel.items.append(itemModel)
+//                kImageDataCenterShared.editLiveHistoryListArray = [self.editLiveHistorySectionModel]
+//                self.reloadView()
+//            }
+//        }
+//    }
 }
 
 

+ 9 - 17
TSLiveWallpaper/Business/TSHomeVC/TSHomeVC.swift

@@ -20,8 +20,6 @@ class TSHomeVC : TSBaseVC {
         titleImageView.snp.makeConstraints { make in
             make.centerY.equalToSuperview()
             make.left.equalTo(16)
-//            make.width.equalTo(214)
-//            make.height.equalTo(24)
         }
 
         return navBarView
@@ -31,14 +29,9 @@ class TSHomeVC : TSBaseVC {
         let layout = UICollectionViewFlowLayout()
         let cp = CollectionViewComponent(frame: CGRect.zero, layout: layout, attributes: [ :])
         cp.collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: k_Height_TabBar, right: 0)
-        cp.itemActionHandler = { [weak self] cellCp, indexPath in
-   
-        }
-         
         cp.itemDidSelectedHandler = { [weak self] (object, indexPath) in
             guard let self = self else { return }
-            
-            
+
             let obj = dataArray.safeObj(At: indexPath.section)
             if let bannerModel = obj as? TSHomeBannerDataSectionModel {
                 if let items = bannerModel.itemModels.first{
@@ -48,16 +41,7 @@ class TSHomeVC : TSBaseVC {
                 kPresentModalVC(target: self, modelVC: TSLiveWallpaperBrowseVC(itemModels: liveModel.items,currentIndex: indexPath.row))
             }
         }
-        cp.itemActionHandler = { obj, index in
 
-        }
-        cp.itemWillDisplayHandler = { cell, obj, IndexPath in
-
-        }
-        cp.itemDidEndDisplayingHandler = { cell, obj, indexPath in
-      
-        }
-        
         return cp
     }()
     
@@ -76,5 +60,13 @@ class TSHomeVC : TSBaseVC {
         }
    
         collectionComponent.reloadView(with:dataArray)
+        
+        TSNetworkShard.monitorNetworkPermission { success in
+            if success {
+                self.collectionComponent.reloadData()
+            }
+        }
+        
     }
+
 }

+ 2 - 2
TSLiveWallpaper/Business/TSHomeVC/TSLiveWallpaperBrowseVC/EasyVC/TSLiveWallpaperCopyrightVC.swift

@@ -15,12 +15,12 @@ class TSLiveWallpaperCopyrightVC: TSBaseVC {
     }()
     
     lazy var contentLabel: UILabel = {
-        let view = UILabel.createLabel(text: "100 Years Later Company Attaches Great Importance To The Copyright Of Images, But As A Content Provider, We Cannot Guarantee The Accuracy Of The Reviewed Content. If You Find Any Infringing Content, Please Inform Us Immediately And We Will Immediately Remove It. Contact",font: .font(size: 14,weight: .regular),textColor: UIColor.fromHex("FFFFFF",alpha: 0.6),numberOfLines: 0)
+        let view = UILabel.createLabel(text: "100 Years Later Company attaches great importance to the copyright of images, but as a content provider, we cannot guarantee the accuracy of the reviewed content. If you find any infringing content, please inform us immediately and we will immediately remove it. Contact",font: .font(size: 14,weight: .regular),textColor: UIColor.fromHex("FFFFFF",alpha: 0.6),numberOfLines: 0)
         return view
     }()
     
     lazy var emailLabel: UILabel = {
-        let view = UILabel.createLabel(text: "Email: Snapmusic6688@Gmail.Com",font: .font(size: 16,weight: .regular),textColor: UIColor.fromHex("FFFFFF"),numberOfLines: 0)
+        let view = UILabel.createLabel(text: "Email: snapmusic6688@gmail.com",font: .font(size: 16,weight: .regular),textColor: UIColor.fromHex("FFFFFF"),numberOfLines: 0)
         return view
     }()
     

+ 108 - 56
TSLiveWallpaper/Business/TSHomeVC/TSLiveWallpaperBrowseVC/TSLiveWallpaperBrowseVC.swift

@@ -18,6 +18,9 @@ class TSLiveWallpaperBrowseItemModel {
     
     var livePhoto:PHLivePhoto? = nil
     var livePhotoResources:(pairedImage: URL, pairedVideo: URL)?
+    
+    var imageCacheUrl:URL?
+    var videoCacheUrl:URL?
 }
 
 class TSLiveWallpaperBrowseVC: TSBaseVC {
@@ -68,7 +71,7 @@ class TSLiveWallpaperBrowseVC: TSBaseVC {
     //MARK: btnsAllView
     
     lazy var saveBtn: UIButton = {
-        let saveBtn = TSViewTool.createNormalSubmitBtn(title: "save".localized) { [weak self]  in
+        let saveBtn = TSViewTool.createNormalSubmitBtn(title: "Save".localized) { [weak self]  in
             guard let self = self else { return }
             if let cell = collectionView.cellForItem(at: IndexPath(item: currentIndex, section: 0)) as? TSLiveWallpaperBrowseCell {
                 cell.saveLivePhoto { [weak self]  success in
@@ -453,63 +456,69 @@ class TSLiveWallpaperBrowseCell : TSBaseCollectionCell,PHLivePhotoViewDelegate{
                         return
                     }
                     
-                    if videoCacheUrl.path.contains("/saveVideo/") {
-                        self.loading.stopAnimating()
-                        
-                        LivePhotoConverter.livePhotoRequest(videoURL: videoCacheUrl, imageURL: imageCacheUrl) { livePhoto in
-                            self.itemModel?.livePhoto = livePhoto
-                            self.itemModel?.livePhotoResources = (imageCacheUrl,videoCacheUrl)
-                            
-                            if let livePhoto = livePhoto {
-                                self.livePhotoView.livePhoto = livePhoto
-                                self.livePhotoView.isHidden = false
-                                self.livePhotoView.startPlayback(with: .full)
-                            }else{
-                                debugPrint("livePhoto.generate fail")
-                            }
-                        }
-                        return
-                    }
-//                    LivePhotoConverter.convertVideo(videoCacheUrl, imageURL: imageCacheUrl) { success, photoURL, videoURL, errorMsg in
-                    LivePhotoConverter.convertVideo(videoCacheUrl) { success, photoURL, videoURL, errorMsg in
-                        self.loading.stopAnimating()
-                        if success {
-                            LivePhotoConverter.livePhotoRequest(videoURL: videoURL!, imageURL: photoURL!) { livePhoto in
-                                self.itemModel?.livePhoto = livePhoto
-                                self.itemModel?.livePhotoResources = (photoURL!,videoURL!)
-                                
-                                if let livePhoto = livePhoto {
-                                    self.livePhotoView.livePhoto = livePhoto
-                                    self.livePhotoView.isHidden = false
-                                    self.livePhotoView.startPlayback(with: .full)
-                                }else{
-                                    debugPrint("livePhoto.generate fail")
-                                }
-                            }
-                        }else{
-                            debugPrint(errorMsg)
-                        }
-                    }
                     
+                    self.itemModel?.imageCacheUrl = imageCacheUrl
+                    self.itemModel?.videoCacheUrl = videoCacheUrl
                     
-//                    livePhotoTool.generate(from: imageCacheUrl, videoURL: videoCacheUrl, progress: { (percent) in
-//                        debugPrint(percent)
-//                    }) { [weak self] (livePhoto, resources) in
-//                        guard let self = self else { return }
-//                        
-//                        loading.stopAnimating()
-//                        itemModel?.livePhoto = livePhoto
-//                        itemModel?.livePhotoResources = resources
+//                    if videoCacheUrl.path.contains("/saveVideo/") {
+//                        self.loading.stopAnimating()
 //                        
-//                        if let livePhoto = livePhoto {
-//                            self.livePhotoView.livePhoto = livePhoto
-//                            self.livePhotoView.isHidden = false
-//                            self.livePhotoView.startPlayback(with: .full)
-//                        }else{
-//                            debugPrint("livePhoto.generate fail")
+//                        LivePhotoConverter.livePhotoRequest(videoURL: videoCacheUrl, imageURL: imageCacheUrl) { livePhoto in
+//                            self.itemModel?.livePhoto = livePhoto
+//                            self.itemModel?.livePhotoResources = (imageCacheUrl,videoCacheUrl)
+//                            
+//                            if let livePhoto = livePhoto {
+//                                self.livePhotoView.livePhoto = livePhoto
+//                                self.livePhotoView.isHidden = false
+//                                self.livePhotoView.startPlayback(with: .full)
+//                            }else{
+//                                debugPrint("livePhoto.generate fail")
+//                            }
+//                        }
+//                        return
+//                    }
+//                    
+//                    LivePhotoConverter.convertVideo(videoCacheUrl, imageURL: imageCacheUrl) { success, photoURL, videoURL, errorMsg in
+//                        DispatchQueue.main.async {
+//                            self.loading.stopAnimating()
+//                            if success {
+//                                LivePhotoConverter.livePhotoRequest(videoURL: videoURL!, imageURL: photoURL!) { livePhoto in
+//                                    self.itemModel?.livePhoto = livePhoto
+//                                    self.itemModel?.livePhotoResources = (photoURL!,videoURL!)
+//                                    
+//                                    if let livePhoto = livePhoto {
+//                                        self.livePhotoView.livePhoto = livePhoto
+//                                        self.livePhotoView.isHidden = false
+//                                        self.livePhotoView.startPlayback(with: .full)
+//                                    }else{
+//                                        debugPrint("livePhoto.generate fail")
+//                                    }
+//                                }
+//                            }else{
+//                                debugPrint(errorMsg)
+//                            }
 //                        }
 //                    }
                     
+                    
+                    livePhotoTool.generate(from: imageCacheUrl, videoURL: videoCacheUrl, progress: { (percent) in
+                        debugPrint(percent)
+                    }) { [weak self] (livePhoto, resources) in
+                        guard let self = self else { return }
+                        
+                        loading.stopAnimating()
+                        itemModel?.livePhoto = livePhoto
+                        itemModel?.livePhotoResources = resources
+                        
+                        if let livePhoto = livePhoto {
+                            self.livePhotoView.livePhoto = livePhoto
+                            self.livePhotoView.isHidden = false
+                            self.livePhotoView.startPlayback(with: .full)
+                        }else{
+                            debugPrint("livePhoto.generate fail")
+                        }
+                    }
+                    
                 }
             }
         }
@@ -533,7 +542,7 @@ class TSLiveWallpaperBrowseCell : TSBaseCollectionCell,PHLivePhotoViewDelegate{
     func livePhotoView(_ livePhotoView: PHLivePhotoView, didEndPlaybackWith playbackStyle: PHLivePhotoViewPlaybackStyle) {
         if !livePhotoView.isHidden {
             kDelayOnMainThread(1.0) {
-                livePhotoView.startPlayback(with: PHLivePhotoViewPlaybackStyle.full)
+                livePhotoView.startPlayback(with: .full)
             }
         }
     }
@@ -553,9 +562,30 @@ class TSLiveWallpaperBrowseCell : TSBaseCollectionCell,PHLivePhotoViewDelegate{
 //            })
 //        }
         
-        if let resources = itemModel?.livePhotoResources {
-            LivePhotoConverter.saveToLibrary(videoURL: resources.pairedVideo, imageURL: resources.pairedImage) { success in
+//        if let resources = itemModel?.livePhotoResources {
+//            LivePhotoConverter.saveToLibrary(videoURL: resources.pairedVideo, imageURL: resources.pairedImage) { success in
+//                kExecuteOnMainThread {
+//                    if success {
+//                        debugPrint("Live Photo Saved,The live photo was successfully saved to Photos.")
+//                        completion(true)
+//                    }else {
+//                        debugPrint("Live Photo Not Saved,The live photo was not saved to Photos.")
+//                        completion(false)
+//                    }
+//                }
+//            }
+//        }
+
+        guard let videoCacheUrl = itemModel?.videoCacheUrl, let imageCacheUrl = itemModel?.imageCacheUrl else{
+            TSToastShared.showToast(message: "save fail")
+            return
+        }
+        
+        TSToastShared.showLoading(in: self.contentView)
+        if videoCacheUrl.path.contains("/saveVideo/") {
+            LivePhotoConverter.saveToLibrary(videoURL: videoCacheUrl, imageURL: imageCacheUrl) { success in
                 kExecuteOnMainThread {
+                    TSToastShared.hideLoading()
                     if success {
                         debugPrint("Live Photo Saved,The live photo was successfully saved to Photos.")
                         completion(true)
@@ -565,8 +595,30 @@ class TSLiveWallpaperBrowseCell : TSBaseCollectionCell,PHLivePhotoViewDelegate{
                     }
                 }
             }
+            return
+        }
+        
+        
+        LivePhotoConverter.convertVideo(videoCacheUrl, imageURL: imageCacheUrl) { success, photoURL, videoURL, errorMsg in
+            DispatchQueue.main.async {
+                TSToastShared.hideLoading()
+                if success {
+                    LivePhotoConverter.saveToLibrary(videoURL: videoURL!, imageURL: photoURL!) { success in
+                        kExecuteOnMainThread {
+                            if success {
+                                debugPrint("Live Photo Saved,The live photo was successfully saved to Photos.")
+                                completion(true)
+                            }else {
+                                debugPrint("Live Photo Not Saved,The live photo was not saved to Photos.")
+                                completion(false)
+                            }
+                        }
+                    }
+                }else{
+                    debugPrint(errorMsg)
+                }
+            }
         }
-
     }
     
     func stopPlayLive() {

+ 2 - 0
TSLiveWallpaper/Business/TSHomeVC/View/TSHomeTopBannerCell.swift

@@ -87,6 +87,8 @@ class TSHomeTopBannerCell : TSBaseCollectionCell , TYCyclePagerViewDelegate, TYC
                 cyclePagerView.reloadData()
                 
                 itemDidSelectedHandler = component.itemDidSelectedHandler
+            }else{
+                cyclePagerView.reloadData()
             }
         }
     }

+ 5 - 3
TSLiveWallpaper/Business/TSMineVC/TSMineVC.swift

@@ -21,8 +21,6 @@ class TSMineVC: TSBaseVC, UITableViewDataSource, UITableViewDelegate {
         titleImageView.snp.makeConstraints { make in
             make.centerY.equalToSuperview()
             make.left.equalTo(16)
-//            make.width.equalTo(101)
-//            make.height.equalTo(24)
         }
 
         return navBarView
@@ -74,7 +72,11 @@ class TSMineVC: TSBaseVC, UITableViewDataSource, UITableViewDelegate {
                 }
             
                 if UIDevice.isPad {
-                    vc.modalPresentationStyle = .popover
+                    if let popover = vc.popoverPresentationController {
+                        popover.sourceView = self.view // 设置锚点视图
+                        popover.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0) // 设置弹窗位置为屏幕中心
+                        popover.permittedArrowDirections = [] // 禁止箭头指向
+                    }
                 }
                 
                 self.present(vc, animated: true)

+ 2 - 2
TSLiveWallpaper/Business/TSRandomWallpaperVC/EasyVC/TSRandomWallpaperCopyrightVC.swift

@@ -13,12 +13,12 @@ class TSRandomWallpaperCopyrightVC: TSBaseVC {
     }()
     
     lazy var contentLabel: UILabel = {
-        let view = UILabel.createLabel(text: "100 Years Later Company Attaches Great Importance To The Copyright Of Images, But As A Content Provider, We Cannot Guarantee The Accuracy Of The Reviewed Content. If You Find Any Infringing Content, Please Inform Us Immediately And We Will Immediately Remove It. Contact",font: .font(size: 14,weight: .regular),textColor: UIColor.fromHex("FFFFFF",alpha: 0.6),numberOfLines: 0)
+        let view = UILabel.createLabel(text: "100 Years Later Company attaches great importance to the copyright of images, but as a content provider, we cannot guarantee the accuracy of the reviewed content. If you find any infringing content, please inform us immediately and we will immediately remove it. Contact",font: .font(size: 14,weight: .regular),textColor: UIColor.fromHex("FFFFFF",alpha: 0.6),numberOfLines: 0)
         return view
     }()
     
     lazy var emailLabel: UILabel = {
-        let view = UILabel.createLabel(text: "Email: Snapmusic6688@Gmail.Com",font: .font(size: 16,weight: .regular),textColor: UIColor.fromHex("FFFFFF"),numberOfLines: 0)
+        let view = UILabel.createLabel(text: "Email: snapmusic6688@gmail.com",font: .font(size: 16,weight: .regular),textColor: UIColor.fromHex("FFFFFF"),numberOfLines: 0)
         return view
     }()
     

+ 6 - 0
TSLiveWallpaper/Business/TSRandomWallpaperVC/TSRandomWallpaperVC.swift

@@ -75,6 +75,12 @@ class TSRandomWallpaperVC: TSBaseVC {
         }
    
         reloadView()
+        
+        TSNetworkShard.monitorNetworkPermission { success in
+            if success {
+                self.collectionComponent.reloadData()
+            }
+        }
     }
     
     func reloadView() {

+ 21 - 15
TSLiveWallpaper/Business/TSViewTool/TSViewTool.swift

@@ -49,7 +49,14 @@ let kSavePhotoSuccesswShared = TSSavePhotoSuccessTool.shared
 class TSSavePhotoSuccessTool {
     
     static let shared = TSSavePhotoSuccessTool()
-    private let textLabel = UILabel()
+    
+    private lazy var textLabel:UILabel = {
+        let textLabel = UILabel()
+        textLabel.textColor = "#4A5178".color
+        textLabel.text = "Save Successfully".localized
+        textLabel.font = UIFont.font(size: 14)
+        return textLabel
+    }()
     
     private lazy var saveSuccessBg: UIView = {
         return creatSaveSuccessBg()
@@ -84,11 +91,8 @@ class TSSavePhotoSuccessTool {
             make.centerY.equalToSuperview()
             make.leading.equalTo(12)
         }
-        
+    
 
-        textLabel.textColor = "#4A5178".color
-        textLabel.text = "Set Successfully".localized
-        textLabel.font = UIFont.font(size: 14)
         view.addSubview(textLabel)
         
         let viewButton = UIButton.createButton(title: "View".localized ,backgroundColor: "4FEA9D".toColor()?.withAlphaComponent(0.2),font: UIFont.font(size: 14),titleColor: "4FEA9D".toColor(),corner: 14) {
@@ -119,17 +123,19 @@ class TSSavePhotoSuccessTool {
 
     
     
-    func show(atView:UIView,text:String = "Set Successfully".localized) {
-        textLabel.text = text
-        atView.addSubview(saveSuccessBg)
-        saveSuccessBg.snp.remakeConstraints { make in
-            make.width.equalTo(288)
-            make.height.equalTo(48)
-            make.centerX.equalToSuperview()
-//            make.bottom.equalTo(atView.safeAreaLayoutGuide.snp.bottom).offset(-92)
-            make.top.equalTo(k_Height_statusBar()+56.0)
-        }
+    func show(atView:UIView,text:String = "Save Successfully".localized) {
         
+        kExecuteOnMainThread {
+            self.textLabel.text = text
+            atView.addSubview(self.saveSuccessBg)
+            self.saveSuccessBg.snp.remakeConstraints { make in
+                make.width.equalTo(288)
+                make.height.equalTo(48)
+                make.centerX.equalToSuperview()
+                make.top.equalTo(k_Height_statusBar()+56.0)
+            }
+        }
+
         DispatchQueue.main.asyncAfter(deadline: .now()+5.0) {
             self.saveSuccessBg.removeFromSuperview()
         }

+ 99 - 2
TSLiveWallpaper/Common/ThirdParty/LivePhoto.swift

@@ -93,8 +93,11 @@ class LivePhoto {
         let assetIdentifier = UUID().uuidString
         
         let fileName = videoURL.lastPathComponent.replacingOccurrences(of: (".\(videoURL.pathExtension)"), with: "")
-        let saveToImagePath = cacheDirectory.appendingPathComponent(fileName).appendingPathExtension("jpg")
-
+        let saveToImagePath = cacheDirectory.appendingPathComponent(fileName).appendingPathExtension("heic")//.appendingPathExtension("jpg")
+        
+        let asset = AVURLAsset(url: videoURL)
+        let targetDuration = CMTimeGetSeconds(asset.duration)
+        let videoSize = asset.tracks(withMediaType: .video).first?.naturalSize
 
         let fileManager = FileManager.default
         if fileManager.fileExists(atPath: saveToImagePath.path) {
@@ -366,6 +369,100 @@ class LivePhoto {
         return item
     }
     
+
+    static func resizeVideoToFixedHeight(
+        videoURL: URL,
+        fixedHeight: CGFloat = 1920,
+        outputFolder: URL,
+        completion: @escaping (URL?) -> Void
+    ) {
+        let asset = AVURLAsset(url: videoURL)
+        guard let videoTrack = asset.tracks(withMediaType: .video).first else {
+            print("Invalid video track")
+            completion(nil)
+            return
+        }
+        
+        let videoSize = videoTrack.naturalSize
+        let videoWidth = videoSize.width
+        let videoHeight = videoSize.height
+        
+        // Calculate the new width based on the fixed height
+        let scale = fixedHeight / videoHeight
+        let newWidth = videoWidth * scale
+        
+        // Crop the video to the center
+        let xOffset = (newWidth - fixedHeight) / 2
+        let cropRect = CGRect(x: xOffset, y: 0, width: fixedHeight, height: fixedHeight)
+        
+        // Output URL
+        let outputURL = outputFolder.appendingPathComponent("resized_video.mov")
+        if FileManager.default.fileExists(atPath: outputURL.path) {
+            try? FileManager.default.removeItem(at: outputURL)
+        }
+        
+        // Configure the video composition
+        let composition = AVMutableComposition()
+        guard let videoCompositionTrack = composition.addMutableTrack(
+            withMediaType: .video,
+            preferredTrackID: kCMPersistentTrackID_Invalid
+        ) else {
+            print("Unable to create video composition track")
+            completion(nil)
+            return
+        }
+        
+        do {
+            try videoCompositionTrack.insertTimeRange(
+                CMTimeRange(start: .zero, duration: asset.duration),
+                of: videoTrack,
+                at: .zero
+            )
+        } catch {
+            print("Error inserting time range: \(error)")
+            completion(nil)
+            return
+        }
+        
+        // Configure the video transformer
+        let transformer = AVMutableVideoCompositionLayerInstruction(assetTrack: videoCompositionTrack)
+        let scaleTransform = CGAffineTransform(scaleX: scale, y: scale)
+        let translateTransform = CGAffineTransform(translationX: -xOffset, y: 0)
+        transformer.setTransform(scaleTransform.concatenating(translateTransform), at: .zero)
+        
+        let videoComposition = AVMutableVideoComposition()
+        videoComposition.renderSize = CGSize(width: fixedHeight, height: fixedHeight)
+        videoComposition.frameDuration = CMTime(value: 1, timescale: Int32(videoTrack.nominalFrameRate))
+        let instruction = AVMutableVideoCompositionInstruction()
+        instruction.timeRange = CMTimeRange(start: .zero, duration: asset.duration)
+        instruction.layerInstructions = [transformer]
+        videoComposition.instructions = [instruction]
+        
+        // Export the video
+        guard let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality) else {
+            print("Unable to create AVAssetExportSession")
+            completion(nil)
+            return
+        }
+        
+        exporter.outputURL = outputURL
+        exporter.outputFileType = .mov
+        exporter.videoComposition = videoComposition
+        
+        exporter.exportAsynchronously {
+            DispatchQueue.main.async {
+                if exporter.status == .completed {
+                    print("Export completed: \(outputURL)")
+                    completion(outputURL)
+                } else {
+                    print("Export failed: \(String(describing: exporter.error))")
+                    completion(nil)
+                }
+            }
+        }
+    }
+
+    
 }
 
 //fileprivate extension AVAsset {

+ 269 - 0
TSLiveWallpaper/Common/ThirdParty/LivePhotoCreater.swift

@@ -0,0 +1,269 @@
+//
+//  LivePhotoCreater.swift
+//  LivePhotoDemoSwift
+//
+//  Created by TSYH on 2024/5/16.
+//  Copyright © 2024 Genady Okrain. All rights reserved.
+//
+
+import UIKit
+import PhotosUI
+//import ExtensionsKit
+
+public class LivePhotoCreater: NSObject {
+    public static let shared = LivePhotoCreater()
+    
+    /// 创建并保存LivePhoto
+    /// - Parameters:
+    ///   - videoURL: 视频本地URL
+    ///   - outputDirectory: LivePhoto资源保存文件夹
+    ///   - completion: (LivePhoto视频URL, LivePhoto图片URL)
+    public func saveLivePhoto(from videoURL: URL, outputDirectory: URL,
+                       completion: ((URL?, URL?, String?) -> Void)?) {
+        createLivePhotoResource(with: videoURL, outputDirectory: outputDirectory) { videoURL, imageURL, erMsg in
+            guard let imageURL = imageURL, let videoURL = videoURL else {
+                DispatchQueue.main.async {
+                    completion?(nil, nil, erMsg ?? "Generate resource failure")
+                }
+                return
+            }
+            self.exportLivePhoto(videoURL: videoURL, imageURL: imageURL) { success, erMsg in
+                DispatchQueue.main.async {
+                    if success {
+                        completion?(videoURL, imageURL, nil)
+                    } else {
+                        completion?(nil, nil, erMsg ?? "Save LivePhoto Failure")
+                    }
+                }
+            }
+        }
+    }
+    
+    /// 根据视频创建LivePhoto, 显示
+    /// - Parameters:
+    ///   - videoURL: 视频本地URL
+    ///   - outputDirectory: LivePhoto资源保存文件夹
+    ///   - completion:
+    public func makeLivePhoto(with videoURL: URL,
+                       outputDirectory: URL,
+                       completion: ((PHLivePhoto?, String?) -> Void)?) {
+        createLivePhotoResource(with: videoURL, outputDirectory: outputDirectory) { videoURL, imageURL, erMsg in
+            DispatchQueue.main.sync {
+                guard let imageURL = imageURL, let videoURL = videoURL else {
+                    completion?(nil, erMsg)
+                    return
+                }
+                PHLivePhoto.request(withResourceFileURLs: [videoURL, imageURL],
+                                    placeholderImage: nil,
+                                    targetSize: .zero,
+                                    contentMode: PHImageContentMode.aspectFit,
+                                    resultHandler: { (livePhoto, info) -> Void in
+                    completion?(livePhoto, info.description)
+                })
+            }
+        }
+    }
+    
+    /// 根据视频创建LivePhoto资源
+    /// - Parameters:
+    ///   - videoURL: 视频本地URL
+    ///   - outputDirectory: LivePhoto资源保存文件夹
+    ///   - completion:
+//    public func makeLivePhotoResource(with videoURL: URL,
+//                                      outputDirectory: URL,
+//                                      completion: ((URL?, URL?, String?) -> Void)?) {
+//        let asset = AVURLAsset(url: videoURL)
+//        
+//        // 截取一帧图片
+//        let generator = AVAssetImageGenerator(asset: asset)
+//        generator.appliesPreferredTrackTransform = true
+//        let time = NSValue(time: CMTimeMakeWithSeconds(CMTimeGetSeconds(asset.duration)/2, preferredTimescale: asset.duration.timescale))
+//        generator.generateCGImagesAsynchronously(forTimes: [time]) { _, image, _, _, _ in
+//            guard let cgimage = image else {
+//                completion?(nil, nil, "Generate image failure~")
+//                return
+//            }
+//            let image = UIImage(cgImage: cgimage)
+//            var data: Data? = image.jpegData(compressionQuality: 1.0)
+//            var orgImageName = "-orgImage.jpg"
+//            var outputImageName = "-IMG.JPG"
+//            
+//            if #available(iOS 17, *) {
+//                data = image.heicData()
+//                orgImageName = "-orgImage.heif"
+//                outputImageName = "-IMG.HEIF"
+//            }
+//            guard let data = data else {
+//                completion?(nil, nil, "Generate image failure~")
+//                return
+//            }
+//                
+//            // 创建文件夹
+//            let _ = try? FileManager.default.createDirectory(atPath: outputDirectory.path, withIntermediateDirectories: true, attributes: nil)
+//            
+//            // 当前时间戳+随机字符串 作为文件名
+//            let outputName: String = "\(Int(Date().timeIntervalSince1970))" + String.randomString(count: 8)
+//            
+//            // 视频截取帧图片保存地址
+//            let originalImageURL = outputDirectory.appendingPathComponent(outputName + orgImageName)
+//            try? data.write(to: originalImageURL, options: [.atomic])
+//            
+//            let imageOutputURL = outputDirectory.appendingPathComponent(outputName + outputImageName)
+//            let videoOutputURL = outputDirectory.appendingPathComponent(outputName + "-IMG.MOV")
+//            
+//            /// 给视频、图片添加LivePhoto信息
+//            let assetIdentifier = UUID().uuidString
+//            LivePhotoJPEG(path: originalImageURL.path).write(imageOutputURL.path, assetIdentifier: assetIdentifier)
+//            LivePhotoMOV(path: videoURL.path).write(videoOutputURL.path, assetIdentifier: assetIdentifier)
+//            
+//            completion?(videoOutputURL, imageOutputURL, nil)
+//        }
+//    }
+    
+    /// 导出LivePhoto到相册
+    public func exportLivePhoto(videoURL: URL, imageURL: URL,
+                         completion: ((Bool, String?) -> Void)?) {
+        PHPhotoLibrary.shared().performChanges({ () -> Void in
+            let creationRequest = PHAssetCreationRequest.forAsset()
+            let options = PHAssetResourceCreationOptions()
+            creationRequest.addResource(with: PHAssetResourceType.pairedVideo, fileURL: videoURL, options: options)
+            creationRequest.addResource(with: PHAssetResourceType.photo, fileURL: imageURL, options: options)
+        }, completionHandler: { (success, error) -> Void in
+            completion?(success, error?.localizedDescription)
+        })
+    }
+}
+
+extension LivePhotoCreater {
+    /// 根据视频创建LivePhoto资源
+    /// - Parameters:
+    ///   - videoURL: 视频本地URL
+    ///   - outputDirectory: LivePhoto资源保存文件夹
+    ///   - completion: (Live视频URL, Live图片URL, 错误信息)
+    func createLivePhotoResource(with videoURL: URL, outputDirectory: URL, completion: ((URL?, URL?, String?) -> Void)?) {
+        
+        guard let metaURL = Bundle(for: self.classForCoder).url(forResource: "metadata", withExtension: "mov") else {
+            completion?(nil, nil, "Metadata file don't exist")
+            return
+        }
+        let videoConverter = Converter4Video(path: videoURL.path)
+        
+        // 创建文件夹
+        let _ = try? FileManager.default.createDirectory(atPath: outputDirectory.path, withIntermediateDirectories: true, attributes: nil)
+        
+        // 当前时间戳+随机字符串 作为文件名
+        let outputName: String = "\(Int(Date().timeIntervalSince1970))" + String.randomString(count: 8)
+        let imageOutputURL = outputDirectory.appendingPathComponent(outputName + "-Live.heic")
+        let videoOutputURL = outputDirectory.appendingPathComponent(outputName + "-Live.mov")
+        
+        let asset = AVURLAsset(url: videoURL, options: nil)
+        Log("===video duration:\(CMTimeGetSeconds(asset.duration))")
+//        guard CMTimeGetSeconds(asset.duration) <= 3 else {
+//            completion?(nil, nil, "The video is too long, please try again")
+//            return
+//        }
+        
+        let assetIdentifier = UUID().uuidString
+        let queue = DispatchQueue(label: "image")
+        queue.async {
+            // 视频写入LivePhoto信息
+            videoConverter.write(dest: videoOutputURL.path, assetIdentifier: assetIdentifier, metaURL: metaURL) { success, error in
+                guard success, FileManager.default.fileExists(atPath: videoOutputURL.path) else {
+                    completion?(nil, nil, "Convert Failure")
+                    return
+                }
+                guard let keyFrame = self.getKeyFrameImage(from: videoOutputURL) else {
+                    return
+                }
+                let imageConverter = Converter4Image(image: keyFrame)
+                // 图片写入LivePhoto信息
+                imageConverter.write(dest: imageOutputURL.path, assetIdentifier: assetIdentifier)
+                
+                guard FileManager.default.fileExists(atPath: imageOutputURL.path) else {
+                    completion?(nil, nil, "Convert Failure")
+                    return
+                }
+                
+                completion?(videoOutputURL, imageOutputURL, nil)
+            }
+        }
+        
+        
+        
+        
+//        let generator = AVAssetImageGenerator(asset: asset)
+//        generator.appliesPreferredTrackTransform = true
+////        generator.requestedTimeToleranceAfter = CMTimeMake(value: 1,timescale: 100)
+//        generator.requestedTimeToleranceAfter = .zero
+//        generator.requestedTimeToleranceBefore = .zero
+//        var times = [NSValue]()
+////        let time = CMTimeMakeWithSeconds(0.5*CMTimeGetSeconds(asset.duration), preferredTimescale: asset.duration.timescale)
+//        let time = CMTimeMakeWithSeconds(0.1, preferredTimescale: asset.duration.timescale)
+//        times.append(NSValue(time: time))
+//        
+//        generator.generateCGImagesAsynchronously(forTimes: times) { requestedTime, image, actualTime, result, error in
+//            guard let cgimage = image else {
+//                completion?(nil, nil, "Generate Frame Failure~")
+//                return
+//            }
+//            
+//            let imageConverter = Converter4Image(image: UIImage(cgImage: cgimage))
+//            queue.async {
+//                // 图片写入LivePhoto信息
+////                imageConverter.write(dest: imageOutputURL.path, assetIdentifier: assetIdentifier)
+//                
+//                // 视频写入LivePhoto信息
+//                videoConverter.write(dest: videoOutputURL.path, assetIdentifier: assetIdentifier, metaURL: metaURL) { success, error in
+//                    guard success else {
+//                        completion?(nil, nil, "Convert Failure")
+//                        return
+//                    }
+//                    guard FileManager.default.fileExists(atPath: videoOutputURL.path) else {
+//                        completion?(nil, nil, "Convert Failure")
+//                        return
+//                    }
+//                    
+//                    guard let keyFrame = self.getKeyFrameImage(from: videoOutputURL) else {
+//                        return
+//                    }
+//                    let imageConverter = Converter4Image(image: keyFrame)
+//                    // 图片写入LivePhoto信息
+//                    imageConverter.write(dest: imageOutputURL.path, assetIdentifier: assetIdentifier)
+//                    guard FileManager.default.fileExists(atPath: imageOutputURL.path) else {
+//                        completion?(nil, nil, "Convert Failure")
+//                        return
+//                    }
+//                    completion?(videoOutputURL, imageOutputURL, nil)
+//                }
+//            }
+//        }
+    }
+    
+    func getKeyFrameImage(from videoURL: URL) -> UIImage? {
+        var percent: Float = 0.5
+        let videoAsset = AVURLAsset(url: videoURL)
+        Log("live video duration: \(CMTimeGetSeconds(videoAsset.duration))")
+        if let stillImageTime = videoAsset.stillImageTime() {
+            percent = Float(stillImageTime.value) / Float(videoAsset.duration.value)
+        }
+        guard let imageFrame = videoAsset.getAssetFrame(percent: percent) else { return nil }
+        return imageFrame
+    }
+}
+
+extension String {
+    /// 获取指定长度随机字符串
+    static func randomString(count: Int) -> String {
+        let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+        let randomString = String((0..<count).map{ _ in letters.randomElement()! })
+        return randomString
+    }
+}
+
+
+public func Log<T>(_ messsage: T, file: String = #file, funcName: String = #function, lineNum: Int = #line) {
+    #if DEBUG
+    let fileName = (file as NSString).lastPathComponent
+    print(Date().description + " \(fileName) (\(funcName)): [\(lineNum)]- \(messsage)")
+    #endif
+}

BIN
TSLiveWallpaper/Common/ThirdParty/Util/1.mov


+ 4 - 1
TSLiveWallpaper/Common/ThirdParty/Util/Converter4Video.swift

@@ -355,7 +355,7 @@ import UIKit
             
             let widthRatio = outputSize.width / absoluteSize.width
             let heightRatio = outputSize.height / absoluteSize.height
-            let scaleFactor = min(widthRatio, heightRatio)
+            let scaleFactor = max(widthRatio, heightRatio)
 
             let newWidth = absoluteSize.width * scaleFactor
             let newHeight = absoluteSize.height * scaleFactor
@@ -497,15 +497,18 @@ import UIKit
                                                      at: .zero)
             } catch {
                 print("Failed to insert time range: \(error)")
+                completion(false, error)
                 return
             }
             
             guard let firstFrame = getFrame(from: asset, at: CMTime(value: 0, timescale: timeScale)) else {
                 print("Failed to insert getFrame firstFrame")
+                completion(false, NSError(domain: "Failed to insert getFrame firstFrame", code: 0))
                 return
             }
             guard let lastFrame = getFrame(from: asset, at: CMTimeSubtract(duration, CMTime(value: 1, timescale: timeScale))) else {
                 print("Failed to insert getFrame lastFrame")
+                completion(false, NSError(domain: "Failed to insert getFrame lastFrame", code: 0))
                 return
             }
             

+ 123 - 2
TSLiveWallpaper/Common/ThirdParty/Util/LivePhotoConverter.swift

@@ -11,6 +11,7 @@ import UIKit
 import Photos
 
 class LivePhotoConverter {
+    
     static func convertVideo(
         _ videoURL: URL,
         imageURL:URL? = nil,
@@ -19,6 +20,7 @@ class LivePhotoConverter {
         print("Start converting...")
         
         guard let metaURL = Bundle.main.url(forResource: "metadata", withExtension: "mov") else {
+//        guard let metaURL = Bundle.main.url(forResource: "output", withExtension: "mov") else {
             completion(false, nil, nil, "Metadata file not found")
             return
         }
@@ -52,7 +54,10 @@ class LivePhotoConverter {
         let asset = AVURLAsset(url: videoURL)
         let targetDuration = CMTimeGetSeconds(asset.duration)
 //        let videoSize = asset.tracks(withMediaType: .video).first?.naturalSize ?? CGSize(width: 1080, height: 1920)
+//        let videoSize = CGSize(width: 1080, height: 1920)
+        
         let videoSize = CGSize(width: 1080, height: 1920)
+//        let livePhotoDuration = CMTimeMake(value: 550, timescale: 600)
         let livePhotoDuration = CMTimeMake(value: 550, timescale: 600)
         let assetIdentifier = UUID().uuidString
         let finalPath = resizePath
@@ -69,7 +74,7 @@ class LivePhotoConverter {
                     completion(false, nil, nil, error?.localizedDescription)
                     return
                 }
-                
+
                 converter.resizeVideo(at: acceleratePath, outputPath: resizePath, outputSize: videoSize) { success, error in
                     guard success else {
                         completion(false, nil, nil, error?.localizedDescription)
@@ -107,6 +112,10 @@ class LivePhotoConverter {
                         
                         let photoURL = URL(fileURLWithPath: picturePath)
                         let pairedVideoURL = URL(fileURLWithPath: videoPath)
+                        
+                        debugPrint("picturePath=\(picturePath)")
+                        debugPrint("videoPath=\(videoPath)")
+                        
                         DispatchQueue.main.async {
                             completion(success, photoURL, pairedVideoURL, error?.localizedDescription)
                         }
@@ -116,6 +125,117 @@ class LivePhotoConverter {
         }
     }
     
+    
+    
+//    static func convertVideo(
+//        _ videoURL: URL,
+//        imageURL:URL? = nil,
+//        completion: @escaping (Bool, URL?, URL?, String?) -> Void
+//    ) {
+//        print("Start converting...")
+//        
+//        guard let metaURL = Bundle.main.url(forResource: "metadata", withExtension: "mov") else {
+//            completion(false, nil, nil, "Metadata file not found")
+//            return
+//        }
+//        
+//        
+//        let fileManager = FileManager.default
+//        let eidtVideoPathURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("livePhoto").appendingPathComponent("editVideo")
+//
+//        
+//        
+//        let documentPath = eidtVideoPathURL.path
+//        let durationPath = "\(documentPath)/duration.mp4"
+//        let acceleratePath = "\(documentPath)/accelerate.mp4"
+//        let resizePath = "\(documentPath)/resize.mp4"
+//        
+//        let destinationDirectory = eidtVideoPathURL
+//        // 如果目标文件夹不存在,创建文件夹
+//        if !fileManager.fileExists(atPath: destinationDirectory.path) {
+//            do {
+//                try fileManager.createDirectory(at: destinationDirectory, withIntermediateDirectories: true, attributes: nil)
+//                debugPrint("创建文件夹成功")
+//            } catch {
+//                debugPrint("尝试创建文件夹失败: \(error.localizedDescription)")
+//            }
+//        }
+//        
+//        try? FileManager.default.removeItem(atPath: durationPath)
+//        try? FileManager.default.removeItem(atPath: acceleratePath)
+//        try? FileManager.default.removeItem(atPath: resizePath)
+//        
+//        let asset = AVURLAsset(url: videoURL)
+//        let targetDuration = CMTimeGetSeconds(asset.duration)
+////        let videoSize = asset.tracks(withMediaType: .video).first?.naturalSize ?? CGSize(width: 1080, height: 1920)
+//        let videoSize = CGSize(width: 1080, height: 1920)
+//        let livePhotoDuration = CMTimeMake(value: 550, timescale: 600)
+//        let assetIdentifier = UUID().uuidString
+//        let finalPath = resizePath
+//        let converter = Converter4Video(path: finalPath)
+//        
+//        converter.durationVideo(at: videoURL.path, outputPath: durationPath, targetDuration: targetDuration) { success, error in
+//            guard success else {
+//                completion(false, nil, nil, error?.localizedDescription)
+//                return
+//            }
+//            
+//            converter.accelerateVideo(at: durationPath, to: livePhotoDuration, outputPath: acceleratePath) { success, error in
+//                guard success else {
+//                    completion(false, nil, nil, error?.localizedDescription)
+//                    return
+//                }
+//                
+//                converter.resizeVideo(at: acceleratePath, outputPath: resizePath, outputSize: videoSize) { success, error in
+//                    guard success else {
+//                        completion(false, nil, nil, error?.localizedDescription)
+//                        return
+//                    }
+//
+//                    let picturePath = "\(documentPath)/live.heic"
+//                    let videoPath = "\(documentPath)/live.mov"
+//                    
+//                    try? FileManager.default.removeItem(atPath: picturePath)
+//                    try? FileManager.default.removeItem(atPath: videoPath)
+//                    
+//                    let asset = AVURLAsset(url:URL(fileURLWithPath: finalPath))
+//                    var image:UIImage? = nil
+//                    if let imageURL = imageURL {
+//                        image = UIImage(contentsOfFile: imageURL.path)
+//                    }else{
+//                        if let imageData = self.generateKeyPhoto(from: asset) {
+//                            image = UIImage(data: imageData)
+//                        }
+//                    }
+//                    
+//                    guard let image = image else {
+//                        completion(false, nil, nil, "image nil")
+//                        return
+//                    }
+//                    
+//                    let imageConverter = Converter4Image(image:image)
+//                    imageConverter.write(dest: picturePath, assetIdentifier: assetIdentifier)
+//                    converter.write(dest: videoPath, assetIdentifier: assetIdentifier, metaURL: metaURL) { success, error in
+//                        guard success else {
+//                            completion(false, nil, nil, error?.localizedDescription)
+//                            return
+//                        }
+//                        
+//                        let photoURL = URL(fileURLWithPath: picturePath)
+//                        let pairedVideoURL = URL(fileURLWithPath: videoPath)
+//                        
+//                        debugPrint("picturePath=\(picturePath)")
+//                        debugPrint("videoPath=\(videoPath)")
+//                        
+//                        DispatchQueue.main.async {
+//                            completion(success, photoURL, pairedVideoURL, error?.localizedDescription)
+//                        }
+//                    }
+//                }
+//            }
+//        }
+//    }
+    
 
     private static func generateKeyPhoto(from videoAsset: AVAsset) -> Data? {
         var percent:Float = 0.5
@@ -128,7 +248,7 @@ class LivePhotoConverter {
     }
     
     static func livePhotoRequest(videoURL:URL,imageURL:URL,completion: @escaping (PHLivePhoto?) -> Void){
-        _ = PHLivePhoto.request(withResourceFileURLs: [videoURL, imageURL], placeholderImage: nil, targetSize: CGSize.zero, contentMode: PHImageContentMode.aspectFit, resultHandler: { (livePhoto: PHLivePhoto?, info: [AnyHashable : Any]) -> Void in
+        _ = PHLivePhoto.request(withResourceFileURLs: [videoURL, imageURL], placeholderImage: nil, targetSize: CGSize.zero, contentMode: PHImageContentMode.aspectFill, resultHandler: { (livePhoto: PHLivePhoto?, info: [AnyHashable : Any]) -> Void in
             if let isDegraded = info[PHLivePhotoInfoIsDegradedKey] as? Bool, isDegraded {
                 return
             }
@@ -154,5 +274,6 @@ class LivePhotoConverter {
             }
         })
     }
+    
 }
 

BIN
TSLiveWallpaper/Common/ThirdParty/Util/origin.mp4


+ 450 - 0
TSLiveWallpaper/Common/ThirdParty/VideoRecorder.swift

@@ -0,0 +1,450 @@
+//
+//  LivePhotoMaker.swift
+//  MediaManagerKit
+//
+//  Created by Max Mg on 2024/7/20.
+//
+
+import Foundation
+import AVFoundation
+//import ExtensionsKit
+
+public class VideoRecorder: NSObject {
+    public static let shared = VideoRecorder()
+    
+    public typealias LivePhotoCompletionHandler = (URL?, URL?, String?) -> Void
+    
+    class RecordConfig {
+        var duration: TimeInterval = 1
+        var frameRate: Int = 30
+        var captureView: UIView!
+        var completion: ((Error?) -> Void)?
+        var recordedDuration: TimeInterval = 0
+        var startTime: CFTimeInterval = CACurrentMediaTime()
+        var reqiureFrames: Int = 0
+        var recordFrames: Int = 0
+    }
+    
+    lazy var recordConfig = RecordConfig()
+    
+    var assetWriter: AVAssetWriter!
+    var assetWriterInput: AVAssetWriterInput!
+    var pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor!
+    
+    var timer: Timer?
+    lazy var isRecording = false
+    var displayLink: CADisplayLink?
+    let queue = DispatchQueue(label: "xxxxxx", attributes: .concurrent)
+    
+    var videoOutuptURL: URL {
+        let temp = FileManager.default.temporaryDirectory
+        return temp.appendingPathComponent("livePhotoOutput.mov")
+    }
+    
+    override init() {
+        super.init()
+    }
+    
+    public func capture(in view: UIView,
+                        duration: TimeInterval,
+                        outputFinder: URL,
+                        completion: LivePhotoCompletionHandler?) {
+        
+        captureVideo(in: view, duration: duration, frameRate: 30) { [weak self] videoURL, error in
+            guard let videoURL = videoURL else {
+                completion?(nil, nil, error?.localizedDescription)
+                return
+            }
+            LivePhotoCreater.shared.saveLivePhoto(from: videoURL, outputDirectory: outputFinder) { videoURL, imageURL, msg in
+                completion?(videoURL, imageURL, msg)
+            }
+        }
+    }
+    
+    /// 录制一个view生成视频
+    /// - Parameters:
+    ///   - view: 目标view
+    ///   - duration: 时长
+    ///   - frameRate: 帧率
+    ///   - completion: URL: 视频temp文件夹地址
+    public func captureVideo(in view: UIView,
+                             duration: TimeInterval,
+                             frameRate: Int,
+                             completion: ((URL?, Error?) -> Void)?) {
+        
+        guard !isRecording else {
+            recordConfig.completion?(NSError(domain: "Video is Recording, try again later", code: 401))
+            return
+        }
+        
+        recordConfig.duration = duration
+        recordConfig.frameRate = frameRate
+        recordConfig.reqiureFrames = Int(Double(frameRate) * duration)
+        recordConfig.recordFrames = 0
+        recordConfig.captureView = view
+        recordConfig.recordedDuration = 0
+        recordConfig.completion = { [weak self] error in
+            if let error = error {
+                completion?(nil, error)
+            } else {
+                completion?(self?.videoOutuptURL, nil)
+            }
+        }
+        
+        startRecordVideo()
+    }
+}
+
+// MARK: -- 视频录制
+extension VideoRecorder {
+    func startRecordVideo() {
+        guard !isRecording, let view = recordConfig.captureView else {
+            recordConfig.completion?(NSError(domain: "Video is Recording, try again later", code: 401))
+            return
+        }
+        
+        if FileManager.default.fileExists(atPath: videoOutuptURL.path) {
+            try? FileManager.default.removeItem(atPath: videoOutuptURL.path)
+        }
+        
+        do {
+            assetWriter = try AVAssetWriter(outputURL: videoOutuptURL, fileType: .mov)
+        } catch {
+            print("Error creating AVAssetWriter: \(error)")
+            recordConfig.completion?(error)
+        }
+        
+        let size = CGSize(width: view.bounds.width * UIScreen.main.scale, height: view.bounds.height * UIScreen.main.scale)
+//        let size = CGSize(width: view.bounds.width, height: view.bounds.height)
+        let outputSettings: [String: Any] = [
+            AVVideoCodecKey: AVVideoCodecType.h264,
+            AVVideoWidthKey: size.width,
+            AVVideoHeightKey: size.height
+        ]
+        assetWriterInput = AVAssetWriterInput(mediaType: .video, outputSettings: outputSettings)
+        assetWriterInput.expectsMediaDataInRealTime = true
+        assetWriter.add(assetWriterInput)
+        
+        let pixelBufferAttributes: [String: Any] = [
+            kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32ARGB,
+            kCVPixelBufferWidthKey as String: size.width,
+            kCVPixelBufferHeightKey as String: size.height
+        ]
+        pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: assetWriterInput, sourcePixelBufferAttributes: pixelBufferAttributes)
+        
+        assetWriter.startWriting()
+        assetWriter.startSession(atSourceTime: .zero)
+        isRecording = true
+        recordConfig.startTime = CACurrentMediaTime()
+        
+        // 第一帧
+        let layer = self.recordConfig.captureView.layer
+        let cgImage = layer.contents
+//        snapshotView(afterScreenUpdates: false)?.layer ?? self.recordConfig.captureView.layer
+        let videoSize = self.recordConfig.captureView.bounds.size
+//        queue.async {
+//            let image = self.drawViewHierarchyToImage(layer: layer)
+            if let pixelBuffer = self.createPixelBuffer(from: self.recordConfig.captureView, videoSize: videoSize) {
+                for _ in 0..<5 {
+                    while true {
+                        if self.pixelBufferAdaptor.assetWriterInput.isReadyForMoreMediaData {
+                            let presentationTime = CMTime(seconds: Double(recordConfig.recordedDuration), preferredTimescale: 600)
+                            self.pixelBufferAdaptor.append(pixelBuffer, withPresentationTime: presentationTime)
+                            
+                            recordConfig.recordFrames += 1
+                            recordConfig.recordedDuration += 1.0 / CGFloat(recordConfig.frameRate)
+                            Log("append===\(recordConfig.recordFrames)")
+                            break
+                        }
+                    }
+                }
+            }
+        
+//        appendPixelBuffer(at: CMTime.zero)
+        timer = Timer.scheduledTimer(withTimeInterval: 1.0 / CGFloat(recordConfig.frameRate), repeats: true) { [weak self] _ in
+            self?.timerHandler()
+        }
+//        displayLink = CADisplayLink(target: self, selector: #selector(timerHandler))
+//        displayLink?.preferredFramesPerSecond = 10
+//        displayLink?.add(to: .current, forMode: .common)
+    }
+    
+    func stopRecording() {
+        timer?.invalidate()
+        timer = nil
+        displayLink?.invalidate()
+        displayLink = nil
+        
+        assetWriterInput.markAsFinished()
+        assetWriter.finishWriting { [weak self] in
+            guard let self = self else { return }
+            self.isRecording = false
+            if self.assetWriter.status == .completed {
+                self.recordConfig.completion?(nil)
+            } else {
+                self.recordConfig.completion?(assetWriter.error)
+            }
+        }
+    }
+    
+    @objc func timerHandler() {
+        if recordConfig.recordFrames >= recordConfig.reqiureFrames {
+//        if recordConfig.recordedDuration >= recordConfig.duration {
+            stopRecording()
+            return
+        }
+        let presentationTime = CMTime(seconds: Double(recordConfig.recordedDuration), preferredTimescale: 600)
+        self.appendPixelBuffer(at: presentationTime)
+        recordConfig.recordFrames += 1
+        recordConfig.recordedDuration += 1.0 / CGFloat(recordConfig.frameRate)
+        Log("===\(recordConfig.recordedDuration)")
+    }
+}
+
+extension VideoRecorder {
+    func appendPixelBuffer(at time: CMTime) {
+        let layer = self.recordConfig.captureView.layer
+        let cgImage = layer.contents
+//        snapshotView(afterScreenUpdates: false)?.layer ?? self.recordConfig.captureView.layer
+        let size = self.recordConfig.captureView.bounds.size
+//        queue.async {
+//            let image = self.drawViewHierarchyToImage(layer: layer)
+            if let pixelBuffer = self.createPixelBuffer(from: self.recordConfig.captureView, videoSize: size),
+               self.pixelBufferAdaptor.assetWriterInput.isReadyForMoreMediaData {
+                self.pixelBufferAdaptor.append(pixelBuffer, withPresentationTime: time)
+            }
+//        }
+    }
+    
+    /// 获取Buffer
+    func createPixelBuffer(from view: UIView, videoSize: CGSize) -> CVPixelBuffer? {
+//        let videoSize = view.frame.size
+//        var contextSize = videoSize
+        var contextSize = CGSize(width: videoSize.width*UIScreen.main.scale, height: videoSize.height*UIScreen.main.scale)
+        
+        let pixelBufferAttributes: [String: Any] = [
+            kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32ARGB,
+            kCVPixelBufferWidthKey as String: contextSize.width,
+            kCVPixelBufferHeightKey as String: contextSize.height
+        ]
+        
+        var pixelBuffer: CVPixelBuffer?
+        let status = CVPixelBufferCreate(kCFAllocatorDefault, Int(contextSize.width), Int(contextSize.height), kCVPixelFormatType_32ARGB, pixelBufferAttributes as CFDictionary, &pixelBuffer)
+        
+        if status != kCVReturnSuccess || pixelBuffer == nil {
+            print("Error creating pixel buffer")
+            return nil
+        }
+        
+        CVPixelBufferLockBaseAddress(pixelBuffer!, .init(rawValue: 0))
+        guard let context = CGContext(data: CVPixelBufferGetBaseAddress(pixelBuffer!),
+                                width: Int(contextSize.width),
+                                height: Int(contextSize.height),
+                                bitsPerComponent: 8,
+                                bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer!),
+                                space: CGColorSpaceCreateDeviceRGB(),
+                                      bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue) else {
+            return nil
+        }
+        
+        context.translateBy(x: contextSize.width / 2, y: contextSize.height / 2)
+        context.scaleBy(x: 1, y: -1) // Flip vertically
+        context.translateBy(x: -contextSize.width / 2, y: -contextSize.height / 2)
+        
+        UIGraphicsPushContext(context)
+//        context.saveGState()
+//        CGContextSaveGState(context)
+//        layer.render(in: context)
+//        context.restoreGState()
+//        image.draw(in: CGRect(origin: .zero, size: contextSize))
+        
+        view.drawHierarchy(in: CGRect(origin: .zero, size: contextSize), afterScreenUpdates: false)
+        UIGraphicsPopContext()
+        
+        CVPixelBufferUnlockBaseAddress(pixelBuffer!, .init(rawValue: 0))
+        
+        return pixelBuffer
+    }
+    
+    func drawViewHierarchyToImage(layer: CALayer) -> UIImage? {
+        // 创建绘制上下文
+        UIGraphicsBeginImageContextWithOptions(UIScreen.main.bounds.size, false, UIScreen.main.scale)
+        defer { UIGraphicsEndImageContext() }
+
+        guard let context = UIGraphicsGetCurrentContext() else { return nil }
+
+        // 在绘制上下文中绘制视图内容
+        layer.render(in: context)
+
+        // 获取绘制完成的图片
+        let image = UIGraphicsGetImageFromCurrentImageContext()
+        return image
+    }
+
+}
+
+extension VideoRecorder {
+    func generateVideoFromAnimation(view: UIView, size: CGSize, duration: Double, completion: ((URL?) -> Void)?) {
+        // 设置视频的参数
+        let framePerSecond = 30
+        let totalFrames = Int(duration * Double(framePerSecond))
+        
+        let mainComposition = AVMutableComposition()
+        let compositionVideoTrack = mainComposition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)
+        try? compositionVideoTrack?.insertTimeRange(CMTimeRange(start: .zero, duration: CMTime(value: 1, timescale: CMTimeScale(framePerSecond))), of: mainComposition.tracks.first!, at: CMTime.zero)
+        
+        let videoComposition = AVMutableVideoComposition()
+        videoComposition.frameDuration = CMTime(value: 1, timescale: CMTimeScale(framePerSecond))
+        videoComposition.renderSize = size
+        let tool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: view.layer, in: view.layer)
+        videoComposition.animationTool = tool
+        
+        let videoInstruction = AVMutableVideoCompositionInstruction()
+        videoInstruction.timeRange = CMTimeRange(start: .zero, duration: CMTime(value: 1, timescale: CMTimeScale(framePerSecond)))
+        videoComposition.instructions = [videoInstruction]
+        
+        // 设置导出的路径和文件名
+        let outputPath = NSTemporaryDirectory() + "output.mp4"
+        let outputURL = URL(fileURLWithPath: outputPath)
+        
+        let fileManager = FileManager.default
+        if fileManager.fileExists(atPath: outputPath) {
+            try? fileManager.removeItem(atPath: outputPath)
+        }
+        
+        guard let exporter = AVAssetExportSession(asset: mainComposition, presetName: AVAssetExportPresetHighestQuality) else {
+            return
+        }
+        exporter.outputURL = outputURL
+        exporter.outputFileType = .mp4
+        exporter.shouldOptimizeForNetworkUse = true
+        exporter.videoComposition = videoComposition
+        exporter.audioMix = AVAudioMix()
+        
+        exporter.exportAsynchronously {
+            switch exporter.status {
+            case .completed:
+                print("视频生成成功: \(outputURL)")
+                completion?(outputURL)
+            case .failed:
+                print("视频生成失败: \(String(describing: exporter.error))")
+            case .cancelled:
+                print("视频生成取消")
+            default:
+                break
+            }
+        }
+    }
+}
+
+
+//import ImageIO
+//import MobileCoreServices
+import ReplayKit
+
+typealias StartRecordHandler = () -> Void
+extension VideoRecorder {
+    
+    /*
+     LivePhoto制作
+     1. 录屏生成视频
+     2. 从视频中获取最后一帧图片,并给图片添加livePhoto信息
+     3. 视频转为mov格式并添加livePhoto信息
+     4. 将拥有相同livePhoto信息的图片和视频合并生成livePhoto保存至相册
+     */
+    /// 录制生成LivePhoto并保存相册
+    func saveLivePhoto(duration: TimeInterval, outputDirectory: URL,
+                       prepearHandler: ((StartRecordHandler?) -> Void)?,
+                       completion: ((URL?, URL?, String?) -> Void)?) {
+        
+        startRecording(duration: duration, prepearHandler: prepearHandler) { videoURL, erMsg in
+            guard let videoURL = videoURL else {
+                completion?(nil, nil, erMsg ?? "Record Failure~")
+                return
+            }
+            LivePhotoCreater.shared.saveLivePhoto(from: videoURL, outputDirectory: outputDirectory, completion: completion)
+        }
+    }
+    
+    /// 开始录屏任务
+    /// - Parameters:
+    ///   - duration: 时长
+    ///   - prepearHandler: 录制前回调,用于业务层倒计时、清屏等操作
+    ///   - completion: (URL: 录制结果视频URL,String: 报错信息)
+    func startRecording(duration: TimeInterval,
+                        prepearHandler: (((StartRecordHandler)?) -> Void)?,
+                        completion: ((URL?, String?) -> Void)?) {
+        guard RPScreenRecorder.shared().isAvailable else {
+            Log("RPScreenRecorder isAvailable = false")
+            completion?(nil, "ScreenRecorder unAvailable".localized)
+            return
+        }
+        
+        let livePhotoQueue = DispatchQueue(label: "queue_recording_video")
+//        let semaphore = DispatchSemaphore(value: 0)
+        
+        livePhotoQueue.async {
+            Log("==点击开始")
+            
+            // 1. 录制视频
+            // 获取RPScreenRecorder
+            let recorder = RPScreenRecorder.shared()
+            recorder.isMicrophoneEnabled = false
+            recorder.isCameraEnabled = false
+//            recorder.delegate = self
+            
+            let duration = min(5, duration)
+            // 开始录制
+            recorder.startRecording { error in
+                if let error = error {
+                    Log(error.localizedDescription)
+                    if recorder.isRecording {
+                        recorder.stopRecording()
+                    }
+                    completion?(nil, error.localizedDescription)
+                } else {
+                    Log("已经获取权限")
+//                    recorder.stopRecording()
+                    
+                    // 开始录制操作
+                    let handler = {
+                        Log("==开始录制")
+                        recorder.startRecording { _ in
+//                            semaphore.signal()
+                        }
+                    }
+                    prepearHandler?(handler)
+                }
+            }
+            
+//            semaphore.wait()
+            
+            DispatchQueue.global().asyncAfter(deadline: .now()+duration) {
+                let ts = Date().timeIntervalSince1970
+                let tempDirectoryPath =  NSTemporaryDirectory()
+                let path = (tempDirectoryPath as NSString).appendingPathComponent("\(Int(ts)).mov")
+                
+                let documentURL = TSFileManagerTool.documentsDirectory.appendingPathComponent("\(Int(ts)).mov")
+                
+                let recorder = RPScreenRecorder.shared()
+                
+                // 停止录制
+                let pathURL = URL(fileURLWithPath: path)
+                recorder.stopRecording(withOutput: pathURL) { (error) in
+                    Log("==录制结束")
+                    
+                    TSFileManagerTool.copyFileWithOverwrite(from: pathURL, to: documentURL)
+                    
+                    if let error = error {
+                        Log(error)
+                        completion?(nil, error.localizedDescription)
+                    }
+                    else {
+                        completion?(pathURL, nil)
+                    }
+                }
+            }
+        }
+    }
+}
+

+ 31 - 0
TSLiveWallpaper/Common/Tool/TSNetworkTool.swift

@@ -0,0 +1,31 @@
+//
+//  TSNetworkTool.swift
+//  TSLiveWallpaper
+//
+//  Created by 100Years on 2025/1/2.
+//
+
+import Network
+
+let TSNetworkShard = TSNetworkTool.shared
+
+class TSNetworkTool {
+    static let shared = TSNetworkTool()
+    func monitorNetworkPermission(escapable result:@escaping (Bool)->Void) {
+        let monitor = NWPathMonitor()
+        let queue = DispatchQueue.global(qos: .background)
+        monitor.start(queue: queue)
+        monitor.pathUpdateHandler = { path in
+            DispatchQueue.main.async {
+                if path.status == .satisfied {
+                    debugPrint("网络可用,用户同意了权限")
+                    result(true)
+                } else {
+                    debugPrint("网络不可用,可能用户拒绝了权限")
+                    result(false)
+                }
+            }
+        }
+    }
+}
+

+ 1 - 1
TSLiveWallpaper/Common/Tool/TSToastTool.swift

@@ -33,7 +33,7 @@ class TSToastTool {
 
     /// 隐藏加载动画
     func hideLoading() {
-        kExecuteOnMainThread {
+        kDelayMainShort {
             WindowHelper.getCurrentWindow()?.hideToastActivity()
         }
     }

+ 11 - 4
TSLiveWallpaper/DataManger/TSImageDataCenter.swift

@@ -123,7 +123,7 @@ class TSImageDataCenter{
                 return totalArray
             }else{
                 let sectionModel = TSImageDataSectionModel()
-                sectionModel.type = "Historical".localized
+                sectionModel.type = "History".localized
                 return [sectionModel]
             }
         }
@@ -177,18 +177,25 @@ class TSImageDataCenter{
                 }
             }
 
-            
             //首页顶部
             liveBannerArray = {
                 let itemModel = TSHomeBannerDataItemModel()
+                let randomInts = [16, 50, 27, 1, 57, 26, 31, 32, 15, 44]
                 if liveBannerArray.count < 9 {
                     if let fistModel = liveListArray.first {
                         if let newModel = setSectionModelStype(sectionModel: fistModel, style: .homeLiveBanner) {
-                            itemModel.items = Array(newModel.items.prefix(9))
+                            
+                            var items = [TSImageDataItemModel]()
+                            for randomInt in randomInts {
+                                if let item = newModel.items.safeObj(At: randomInt) {
+                                    items.append(item)
+                                }
+                            }
+                            itemModel.items = items
                         }
                     }
                 }
-        
+
                 let sectionModel = TSHomeBannerDataSectionModel()
                 sectionModel.itemModels = [itemModel]
                 sectionModel.style = .homeLiveBanner

+ 0 - 4
TSLiveWallpaper/Resource/Json/response.json

@@ -30,10 +30,6 @@
         "imageUrl": "http://d3a93z8fj970a4.cloudfront.net/20241223/53c50d8f651029c4695f3cd59576fc51.jpg",
         "videoUrl": "http://d3a93z8fj970a4.cloudfront.net/20241223/0390d88ddcebc799b346145ac322b4a6.mp4"
       },
-      {
-        "imageUrl": "http://d3a93z8fj970a4.cloudfront.net/20241223/299eb8db0dadceaa8e123827d1de98db.jpg",
-        "videoUrl": "http://d3a93z8fj970a4.cloudfront.net/20241223/3490cffb1d2cbe5a98b1cf72ce5621d8.mp4"
-      },
       {
         "imageUrl": "http://d3a93z8fj970a4.cloudfront.net/20241223/556fba376e4ec189187a40b369ad1e7a.jpg",
         "videoUrl": "http://d3a93z8fj970a4.cloudfront.net/20241223/48a5fa35f6286f00193c29cb9b8469e7.mp4"