From 6c63b0eea082e923072ec4f602deba3eefb44fae Mon Sep 17 00:00:00 2001 From: bluesea <307723040@qq.com> Date: Tue, 5 Mar 2024 11:44:34 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E5=88=9D=E5=A7=8B=E5=8C=96gi?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tdvideo/tdvideo.xcodeproj/project.pbxproj | 592 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../UserInterfaceState.xcuserstate | Bin 0 -> 110025 bytes .../UserInterfaceState.xcuserstate | Bin 0 -> 70723 bytes .../UserInterfaceState.xcuserstate | Bin 0 -> 307853 bytes .../UserInterfaceState.xcuserstate | Bin 0 -> 13277 bytes .../xcdebugger/Breakpoints_v2.xcbkptlist | 6 + .../xcschemes/xcschememanagement.plist | 14 + .../xcdebugger/Breakpoints_v2.xcbkptlist | 227 +++++++ .../xcschemes/xcschememanagement.plist | 14 + .../xcdebugger/Breakpoints_v2.xcbkptlist | 6 + .../xcschemes/xcschememanagement.plist | 14 + .../xcschemes/xcschememanagement.plist | 14 + tdvideo/tdvideo/AppDelegate.swift | 53 ++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 13 + tdvideo/tdvideo/Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + tdvideo/tdvideo/Base.lproj/Main.storyboard | 24 + tdvideo/tdvideo/CCAddImage.swift | 263 ++++++++ tdvideo/tdvideo/CreateVideoByBuffer.swift | 140 +++++ tdvideo/tdvideo/FrameProcessor.swift | 233 +++++++ tdvideo/tdvideo/ImageProcessingShaders.metal | 26 + tdvideo/tdvideo/Info.plist | 31 + tdvideo/tdvideo/MetalPlayer.swift | 345 ++++++++++ tdvideo/tdvideo/PlayContoller10.swift | 92 +++ tdvideo/tdvideo/PlayContoller11.swift | 185 ++++++ tdvideo/tdvideo/PlayContoller4.swift | 285 +++++++++ tdvideo/tdvideo/PlayContoller5.swift | 401 ++++++++++++ tdvideo/tdvideo/PlayContoller6.swift | 160 +++++ tdvideo/tdvideo/PlayContoller7.swift | 313 +++++++++ tdvideo/tdvideo/PlayContoller8.swift | 292 +++++++++ tdvideo/tdvideo/PlayContoller9.swift | 235 +++++++ tdvideo/tdvideo/PlayController3.swift | 103 +++ tdvideo/tdvideo/PlayControllerVideo.swift | 277 ++++++++ tdvideo/tdvideo/SceneDelegate.swift | 52 ++ tdvideo/tdvideo/SpatialVideoConverter.swift | 421 +++++++++++++ tdvideo/tdvideo/SpatialVideoWriter.swift | 124 ++++ tdvideo/tdvideo/VideoConvertor.swift | 133 ++++ tdvideo/tdvideo/VideoConvertor2.swift | 302 +++++++++ tdvideo/tdvideo/VideoConvertor3.swift | 89 +++ tdvideo/tdvideo/VideoConvertor4.swift | 125 ++++ tdvideo/tdvideo/VideoFile.swift | 35 ++ tdvideo/tdvideo/VideoPlayer.swift | 7 + tdvideo/tdvideo/VideoPreview.swift | 40 ++ tdvideo/tdvideo/VideoWriter.swift | 161 +++++ tdvideo/tdvideo/ViewController.swift | 112 ++++ tdvideo/tdvideo/tdvideo.entitlements | 5 + tdvideo/tdvideo/转码/PlayController.swift | 505 +++++++++++++++ tdvideo/tdvideo/转码/PlayControllerImg.swift | 283 +++++++++ tdvideo/tdvideo/转码/ViewController2.swift | 299 +++++++++ 52 files changed, 7108 insertions(+) create mode 100644 tdvideo/tdvideo.xcodeproj/project.pbxproj create mode 100644 tdvideo/tdvideo.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 tdvideo/tdvideo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 tdvideo/tdvideo.xcodeproj/project.xcworkspace/xcuserdata/aaa.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 tdvideo/tdvideo.xcodeproj/project.xcworkspace/xcuserdata/i308051.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 tdvideo/tdvideo.xcodeproj/project.xcworkspace/xcuserdata/mac.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 tdvideo/tdvideo.xcodeproj/project.xcworkspace/xcuserdata/zhihaizhu.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 tdvideo/tdvideo.xcodeproj/xcuserdata/aaa.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist create mode 100644 tdvideo/tdvideo.xcodeproj/xcuserdata/aaa.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 tdvideo/tdvideo.xcodeproj/xcuserdata/i308051.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist create mode 100644 tdvideo/tdvideo.xcodeproj/xcuserdata/i308051.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 tdvideo/tdvideo.xcodeproj/xcuserdata/mac.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist create mode 100644 tdvideo/tdvideo.xcodeproj/xcuserdata/mac.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 tdvideo/tdvideo.xcodeproj/xcuserdata/zhihaizhu.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 tdvideo/tdvideo/AppDelegate.swift create mode 100644 tdvideo/tdvideo/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 tdvideo/tdvideo/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 tdvideo/tdvideo/Assets.xcassets/Contents.json create mode 100644 tdvideo/tdvideo/Base.lproj/LaunchScreen.storyboard create mode 100644 tdvideo/tdvideo/Base.lproj/Main.storyboard create mode 100644 tdvideo/tdvideo/CCAddImage.swift create mode 100644 tdvideo/tdvideo/CreateVideoByBuffer.swift create mode 100644 tdvideo/tdvideo/FrameProcessor.swift create mode 100644 tdvideo/tdvideo/ImageProcessingShaders.metal create mode 100644 tdvideo/tdvideo/Info.plist create mode 100644 tdvideo/tdvideo/MetalPlayer.swift create mode 100644 tdvideo/tdvideo/PlayContoller10.swift create mode 100644 tdvideo/tdvideo/PlayContoller11.swift create mode 100644 tdvideo/tdvideo/PlayContoller4.swift create mode 100644 tdvideo/tdvideo/PlayContoller5.swift create mode 100644 tdvideo/tdvideo/PlayContoller6.swift create mode 100644 tdvideo/tdvideo/PlayContoller7.swift create mode 100644 tdvideo/tdvideo/PlayContoller8.swift create mode 100644 tdvideo/tdvideo/PlayContoller9.swift create mode 100644 tdvideo/tdvideo/PlayController3.swift create mode 100644 tdvideo/tdvideo/PlayControllerVideo.swift create mode 100644 tdvideo/tdvideo/SceneDelegate.swift create mode 100644 tdvideo/tdvideo/SpatialVideoConverter.swift create mode 100644 tdvideo/tdvideo/SpatialVideoWriter.swift create mode 100644 tdvideo/tdvideo/VideoConvertor.swift create mode 100644 tdvideo/tdvideo/VideoConvertor2.swift create mode 100644 tdvideo/tdvideo/VideoConvertor3.swift create mode 100644 tdvideo/tdvideo/VideoConvertor4.swift create mode 100644 tdvideo/tdvideo/VideoFile.swift create mode 100644 tdvideo/tdvideo/VideoPlayer.swift create mode 100644 tdvideo/tdvideo/VideoPreview.swift create mode 100644 tdvideo/tdvideo/VideoWriter.swift create mode 100644 tdvideo/tdvideo/ViewController.swift create mode 100644 tdvideo/tdvideo/tdvideo.entitlements create mode 100644 tdvideo/tdvideo/转码/PlayController.swift create mode 100644 tdvideo/tdvideo/转码/PlayControllerImg.swift create mode 100644 tdvideo/tdvideo/转码/ViewController2.swift diff --git a/tdvideo/tdvideo.xcodeproj/project.pbxproj b/tdvideo/tdvideo.xcodeproj/project.pbxproj new file mode 100644 index 0000000..95e9c8d --- /dev/null +++ b/tdvideo/tdvideo.xcodeproj/project.pbxproj @@ -0,0 +1,592 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 007EFE022B60DA3600EFD078 /* IMG_0071.MOV in Resources */ = {isa = PBXBuildFile; fileRef = 007EFE012B60DA3600EFD078 /* IMG_0071.MOV */; }; + 007EFE042B60E00000EFD078 /* VideoConvertor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 007EFE032B60E00000EFD078 /* VideoConvertor.swift */; }; + 007EFE082B60EB5900EFD078 /* VideoConvertor2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 007EFE072B60EB5900EFD078 /* VideoConvertor2.swift */; }; + 009882722B6269B30076385E /* PlayController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 009882712B6269B30076385E /* PlayController.swift */; }; + 009B7E9B2B5BA788003BE217 /* VideoWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 009B7E9A2B5BA788003BE217 /* VideoWriter.swift */; }; + 009B7E9D2B5BB392003BE217 /* CoreVideo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 009B7E9C2B5BB392003BE217 /* CoreVideo.framework */; }; + 009B7E9F2B5BB39A003BE217 /* CoreImage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 009B7E9E2B5BB39A003BE217 /* CoreImage.framework */; }; + 00EED0532B5A4A2400637604 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00EED0522B5A4A2400637604 /* AppDelegate.swift */; }; + 00EED0552B5A4A2400637604 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00EED0542B5A4A2400637604 /* SceneDelegate.swift */; }; + 00EED0572B5A4A2400637604 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00EED0562B5A4A2400637604 /* ViewController.swift */; }; + 00EED05A2B5A4A2400637604 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 00EED0582B5A4A2400637604 /* Main.storyboard */; }; + 00EED05C2B5A4A2500637604 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 00EED05B2B5A4A2500637604 /* Assets.xcassets */; }; + 00EED05F2B5A4A2500637604 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 00EED05D2B5A4A2500637604 /* LaunchScreen.storyboard */; }; + 00EED0682B5A4B1400637604 /* ImageProcessingShaders.metal in Sources */ = {isa = PBXBuildFile; fileRef = 00EED0662B5A4B1400637604 /* ImageProcessingShaders.metal */; }; + 00EED0692B5A4B1400637604 /* VideoFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00EED0672B5A4B1400637604 /* VideoFile.swift */; }; + 00EED06B2B5A4B1B00637604 /* VideoPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00EED06A2B5A4B1B00637604 /* VideoPreview.swift */; }; + 00EED06D2B5A4B6400637604 /* MetalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00EED06C2B5A4B6400637604 /* MetalPlayer.swift */; }; + 00EED0722B5A4BD600637604 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 00EED0712B5A4BD600637604 /* AVFoundation.framework */; }; + 00EED0762B5A4CEB00637604 /* FrameProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00EED0752B5A4CEB00637604 /* FrameProcessor.swift */; }; + 00EED0792B5A686500637604 /* SpatialVideoConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00EED0782B5A686500637604 /* SpatialVideoConverter.swift */; }; + 043A6AD52B81AF04003776D2 /* PlayContoller8.swift in Sources */ = {isa = PBXBuildFile; fileRef = 043A6AD42B81AF04003776D2 /* PlayContoller8.swift */; }; + 043A6AD72B81B301003776D2 /* VideoConvertor3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 043A6AD62B81B301003776D2 /* VideoConvertor3.swift */; }; + 043C63122B6B90E80095F268 /* PlayContoller4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 043C63112B6B90E80095F268 /* PlayContoller4.swift */; }; + 043C63142B6B9CC90095F268 /* ExternalAccessory.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 043C63132B6B9CC90095F268 /* ExternalAccessory.framework */; }; + 043C63152B6BA6650095F268 /* ExternalAccessory.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 043C63132B6B9CC90095F268 /* ExternalAccessory.framework */; }; + 044447792B779B4200C7452B /* PlayContoller7.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044447782B779B4200C7452B /* PlayContoller7.swift */; }; + 0444477C2B779C1B00C7452B /* b.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 0444477A2B779C1B00C7452B /* b.HEIC */; }; + 0444477D2B779C1B00C7452B /* a.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 0444477B2B779C1B00C7452B /* a.HEIC */; }; + 04880AF22B6F702A00FF9E59 /* ViewController2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04880AF12B6F702A00FF9E59 /* ViewController2.swift */; }; + 04880AF42B6F7A3B00FF9E59 /* PlayControllerVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04880AF32B6F7A3B00FF9E59 /* PlayControllerVideo.swift */; }; + 049ABF9F2B861C450049A94B /* PlayContoller9.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049ABF9E2B861C450049A94B /* PlayContoller9.swift */; }; + 049EBD832B6C91C4005421C7 /* PlayContoller5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049EBD822B6C91C4005421C7 /* PlayContoller5.swift */; }; + 049EBD852B6CD4EA005421C7 /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 049EBD842B6CD4EA005421C7 /* ImageIO.framework */; }; + 04A042542B6F9F4A00EA3EF9 /* PlayContoller6.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04A042532B6F9F4A00EA3EF9 /* PlayContoller6.swift */; }; + 04A281A82B6A24840067FB28 /* img1.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 04A281A72B6A24840067FB28 /* img1.HEIC */; }; + 04A281AA2B6A2E730067FB28 /* img2.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 04A281A92B6A2E730067FB28 /* img2.HEIC */; }; + 04A281AC2B6A38DA0067FB28 /* img3.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 04A281AB2B6A38DA0067FB28 /* img3.HEIC */; }; + 04A4E0622B86FED3001894D2 /* PlayContoller10.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04A4E0612B86FED3001894D2 /* PlayContoller10.swift */; }; + 04A4E0642B870160001894D2 /* VideoConvertor4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04A4E0632B870160001894D2 /* VideoConvertor4.swift */; }; + 04A4E06B2B87465B001894D2 /* bb.MOV in Resources */ = {isa = PBXBuildFile; fileRef = 04A4E0692B87465A001894D2 /* bb.MOV */; }; + 04A4E06C2B87465B001894D2 /* aa.MOV in Resources */ = {isa = PBXBuildFile; fileRef = 04A4E06A2B87465B001894D2 /* aa.MOV */; }; + 04A4E06E2B87483C001894D2 /* SpatialVideoWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04A4E06D2B87483C001894D2 /* SpatialVideoWriter.swift */; }; + 04A4E0702B87697E001894D2 /* PlayContoller11.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04A4E06F2B87697E001894D2 /* PlayContoller11.swift */; }; + 04A4E0722B87789C001894D2 /* CreateVideoByBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04A4E0712B87789C001894D2 /* CreateVideoByBuffer.swift */; }; + 04A5AE312B6B3234000D26EA /* img4.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 04A5AE302B6B3234000D26EA /* img4.HEIC */; }; + 04A5AE332B6B45E8000D26EA /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 04A5AE322B6B45E8000D26EA /* Photos.framework */; }; + 04A5AE352B6B479E000D26EA /* CCAddImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04A5AE342B6B479E000D26EA /* CCAddImage.swift */; }; + 04A5AE372B6B47AB000D26EA /* PlayControllerImg.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04A5AE362B6B47AB000D26EA /* PlayControllerImg.swift */; }; + 04A5AE392B6B480C000D26EA /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 04A5AE382B6B480C000D26EA /* MobileCoreServices.framework */; }; + 04A5AE3B2B6B6AD6000D26EA /* PhotosUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 04A5AE3A2B6B6AD6000D26EA /* PhotosUI.framework */; }; + 04A5AE3D2B6B757F000D26EA /* PlayController3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04A5AE3C2B6B757F000D26EA /* PlayController3.swift */; }; + 04BD5B3E2B81E21100DBBE08 /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04BD5B3D2B81E21100DBBE08 /* VideoPlayer.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 007EFE012B60DA3600EFD078 /* IMG_0071.MOV */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; path = IMG_0071.MOV; sourceTree = ""; }; + 007EFE032B60E00000EFD078 /* VideoConvertor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoConvertor.swift; sourceTree = ""; }; + 007EFE072B60EB5900EFD078 /* VideoConvertor2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoConvertor2.swift; sourceTree = ""; }; + 009882712B6269B30076385E /* PlayController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayController.swift; sourceTree = ""; }; + 009B7E9A2B5BA788003BE217 /* VideoWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoWriter.swift; sourceTree = ""; }; + 009B7E9C2B5BB392003BE217 /* CoreVideo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreVideo.framework; path = System/Library/Frameworks/CoreVideo.framework; sourceTree = SDKROOT; }; + 009B7E9E2B5BB39A003BE217 /* CoreImage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreImage.framework; path = System/Library/Frameworks/CoreImage.framework; sourceTree = SDKROOT; }; + 00EED04F2B5A4A2400637604 /* tdvideo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = tdvideo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 00EED0522B5A4A2400637604 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 00EED0542B5A4A2400637604 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 00EED0562B5A4A2400637604 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 00EED0592B5A4A2400637604 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 00EED05B2B5A4A2500637604 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 00EED05E2B5A4A2500637604 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 00EED0602B5A4A2500637604 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 00EED0662B5A4B1400637604 /* ImageProcessingShaders.metal */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.metal; path = ImageProcessingShaders.metal; sourceTree = ""; }; + 00EED0672B5A4B1400637604 /* VideoFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoFile.swift; sourceTree = ""; }; + 00EED06A2B5A4B1B00637604 /* VideoPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPreview.swift; sourceTree = ""; }; + 00EED06C2B5A4B6400637604 /* MetalPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalPlayer.swift; sourceTree = ""; }; + 00EED0712B5A4BD600637604 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; + 00EED0752B5A4CEB00637604 /* FrameProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FrameProcessor.swift; sourceTree = ""; }; + 00EED0772B5A683200637604 /* tdvideo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = tdvideo.entitlements; sourceTree = ""; }; + 00EED0782B5A686500637604 /* SpatialVideoConverter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpatialVideoConverter.swift; sourceTree = ""; }; + 043A6AD42B81AF04003776D2 /* PlayContoller8.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayContoller8.swift; sourceTree = ""; }; + 043A6AD62B81B301003776D2 /* VideoConvertor3.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoConvertor3.swift; sourceTree = ""; }; + 043C63112B6B90E80095F268 /* PlayContoller4.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayContoller4.swift; sourceTree = ""; }; + 043C63132B6B9CC90095F268 /* ExternalAccessory.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ExternalAccessory.framework; path = System/Library/Frameworks/ExternalAccessory.framework; sourceTree = SDKROOT; }; + 044447782B779B4200C7452B /* PlayContoller7.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayContoller7.swift; sourceTree = ""; }; + 0444477A2B779C1B00C7452B /* b.HEIC */ = {isa = PBXFileReference; lastKnownFileType = file; path = b.HEIC; sourceTree = ""; }; + 0444477B2B779C1B00C7452B /* a.HEIC */ = {isa = PBXFileReference; lastKnownFileType = file; path = a.HEIC; sourceTree = ""; }; + 04880AF12B6F702A00FF9E59 /* ViewController2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController2.swift; sourceTree = ""; }; + 04880AF32B6F7A3B00FF9E59 /* PlayControllerVideo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayControllerVideo.swift; sourceTree = ""; }; + 049ABF9E2B861C450049A94B /* PlayContoller9.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayContoller9.swift; sourceTree = ""; }; + 049EBD822B6C91C4005421C7 /* PlayContoller5.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayContoller5.swift; sourceTree = ""; }; + 049EBD842B6CD4EA005421C7 /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = System/Library/Frameworks/ImageIO.framework; sourceTree = SDKROOT; }; + 04A042532B6F9F4A00EA3EF9 /* PlayContoller6.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayContoller6.swift; sourceTree = ""; }; + 04A281A72B6A24840067FB28 /* img1.HEIC */ = {isa = PBXFileReference; lastKnownFileType = file; path = img1.HEIC; sourceTree = ""; }; + 04A281A92B6A2E730067FB28 /* img2.HEIC */ = {isa = PBXFileReference; lastKnownFileType = file; path = img2.HEIC; sourceTree = ""; }; + 04A281AB2B6A38DA0067FB28 /* img3.HEIC */ = {isa = PBXFileReference; lastKnownFileType = file; path = img3.HEIC; sourceTree = ""; }; + 04A4E0612B86FED3001894D2 /* PlayContoller10.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayContoller10.swift; sourceTree = ""; }; + 04A4E0632B870160001894D2 /* VideoConvertor4.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoConvertor4.swift; sourceTree = ""; }; + 04A4E0692B87465A001894D2 /* bb.MOV */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; path = bb.MOV; sourceTree = ""; }; + 04A4E06A2B87465B001894D2 /* aa.MOV */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; path = aa.MOV; sourceTree = ""; }; + 04A4E06D2B87483C001894D2 /* SpatialVideoWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpatialVideoWriter.swift; sourceTree = ""; }; + 04A4E06F2B87697E001894D2 /* PlayContoller11.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayContoller11.swift; sourceTree = ""; }; + 04A4E0712B87789C001894D2 /* CreateVideoByBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateVideoByBuffer.swift; sourceTree = ""; }; + 04A5AE302B6B3234000D26EA /* img4.HEIC */ = {isa = PBXFileReference; lastKnownFileType = file; path = img4.HEIC; sourceTree = ""; }; + 04A5AE322B6B45E8000D26EA /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = System/Library/Frameworks/Photos.framework; sourceTree = SDKROOT; }; + 04A5AE342B6B479E000D26EA /* CCAddImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CCAddImage.swift; sourceTree = ""; }; + 04A5AE362B6B47AB000D26EA /* PlayControllerImg.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayControllerImg.swift; sourceTree = ""; }; + 04A5AE382B6B480C000D26EA /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; + 04A5AE3A2B6B6AD6000D26EA /* PhotosUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PhotosUI.framework; path = System/Library/Frameworks/PhotosUI.framework; sourceTree = SDKROOT; }; + 04A5AE3C2B6B757F000D26EA /* PlayController3.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayController3.swift; sourceTree = ""; }; + 04BD5B3D2B81E21100DBBE08 /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 00EED04C2B5A4A2400637604 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 00EED0722B5A4BD600637604 /* AVFoundation.framework in Frameworks */, + 04A5AE332B6B45E8000D26EA /* Photos.framework in Frameworks */, + 049EBD852B6CD4EA005421C7 /* ImageIO.framework in Frameworks */, + 009B7E9D2B5BB392003BE217 /* CoreVideo.framework in Frameworks */, + 04A5AE392B6B480C000D26EA /* MobileCoreServices.framework in Frameworks */, + 043C63142B6B9CC90095F268 /* ExternalAccessory.framework in Frameworks */, + 009B7E9F2B5BB39A003BE217 /* CoreImage.framework in Frameworks */, + 043C63152B6BA6650095F268 /* ExternalAccessory.framework in Frameworks */, + 04A5AE3B2B6B6AD6000D26EA /* PhotosUI.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 00EED0462B5A4A2400637604 = { + isa = PBXGroup; + children = ( + 00EED0512B5A4A2400637604 /* tdvideo */, + 00EED0502B5A4A2400637604 /* Products */, + 00EED0702B5A4BD600637604 /* Frameworks */, + ); + sourceTree = ""; + }; + 00EED0502B5A4A2400637604 /* Products */ = { + isa = PBXGroup; + children = ( + 00EED04F2B5A4A2400637604 /* tdvideo.app */, + ); + name = Products; + sourceTree = ""; + }; + 00EED0512B5A4A2400637604 /* tdvideo */ = { + isa = PBXGroup; + children = ( + 04A4E06A2B87465B001894D2 /* aa.MOV */, + 04A4E0692B87465A001894D2 /* bb.MOV */, + 0444477B2B779C1B00C7452B /* a.HEIC */, + 0444477A2B779C1B00C7452B /* b.HEIC */, + 04A5AE302B6B3234000D26EA /* img4.HEIC */, + 04A281AB2B6A38DA0067FB28 /* img3.HEIC */, + 04A281A92B6A2E730067FB28 /* img2.HEIC */, + 04A281A72B6A24840067FB28 /* img1.HEIC */, + 007EFE012B60DA3600EFD078 /* IMG_0071.MOV */, + 009B7E9A2B5BA788003BE217 /* VideoWriter.swift */, + 00EED0782B5A686500637604 /* SpatialVideoConverter.swift */, + 04A4E06D2B87483C001894D2 /* SpatialVideoWriter.swift */, + 04A4E0712B87789C001894D2 /* CreateVideoByBuffer.swift */, + 00EED0772B5A683200637604 /* tdvideo.entitlements */, + 00EED0752B5A4CEB00637604 /* FrameProcessor.swift */, + 00EED06A2B5A4B1B00637604 /* VideoPreview.swift */, + 00EED0662B5A4B1400637604 /* ImageProcessingShaders.metal */, + 00EED0672B5A4B1400637604 /* VideoFile.swift */, + 00EED0522B5A4A2400637604 /* AppDelegate.swift */, + 00EED0542B5A4A2400637604 /* SceneDelegate.swift */, + 00EED0562B5A4A2400637604 /* ViewController.swift */, + 1EFAF0BE2B8B72D4002A1773 /* 转码 */, + 00EED06C2B5A4B6400637604 /* MetalPlayer.swift */, + 007EFE032B60E00000EFD078 /* VideoConvertor.swift */, + 007EFE072B60EB5900EFD078 /* VideoConvertor2.swift */, + 043A6AD62B81B301003776D2 /* VideoConvertor3.swift */, + 04A4E0632B870160001894D2 /* VideoConvertor4.swift */, + 04880AF32B6F7A3B00FF9E59 /* PlayControllerVideo.swift */, + 04A5AE3C2B6B757F000D26EA /* PlayController3.swift */, + 043C63112B6B90E80095F268 /* PlayContoller4.swift */, + 049EBD822B6C91C4005421C7 /* PlayContoller5.swift */, + 04A042532B6F9F4A00EA3EF9 /* PlayContoller6.swift */, + 044447782B779B4200C7452B /* PlayContoller7.swift */, + 043A6AD42B81AF04003776D2 /* PlayContoller8.swift */, + 049ABF9E2B861C450049A94B /* PlayContoller9.swift */, + 04A4E0612B86FED3001894D2 /* PlayContoller10.swift */, + 04A4E06F2B87697E001894D2 /* PlayContoller11.swift */, + 04BD5B3D2B81E21100DBBE08 /* VideoPlayer.swift */, + 04A5AE342B6B479E000D26EA /* CCAddImage.swift */, + 00EED0582B5A4A2400637604 /* Main.storyboard */, + 00EED05B2B5A4A2500637604 /* Assets.xcassets */, + 00EED05D2B5A4A2500637604 /* LaunchScreen.storyboard */, + 00EED0602B5A4A2500637604 /* Info.plist */, + ); + path = tdvideo; + sourceTree = ""; + }; + 00EED0702B5A4BD600637604 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 049EBD842B6CD4EA005421C7 /* ImageIO.framework */, + 043C63132B6B9CC90095F268 /* ExternalAccessory.framework */, + 04A5AE3A2B6B6AD6000D26EA /* PhotosUI.framework */, + 04A5AE382B6B480C000D26EA /* MobileCoreServices.framework */, + 04A5AE322B6B45E8000D26EA /* Photos.framework */, + 009B7E9E2B5BB39A003BE217 /* CoreImage.framework */, + 009B7E9C2B5BB392003BE217 /* CoreVideo.framework */, + 00EED0712B5A4BD600637604 /* AVFoundation.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1EFAF0BE2B8B72D4002A1773 /* 转码 */ = { + isa = PBXGroup; + children = ( + 04880AF12B6F702A00FF9E59 /* ViewController2.swift */, + 009882712B6269B30076385E /* PlayController.swift */, + 04A5AE362B6B47AB000D26EA /* PlayControllerImg.swift */, + ); + path = "转码"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 00EED04E2B5A4A2400637604 /* tdvideo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 00EED0632B5A4A2500637604 /* Build configuration list for PBXNativeTarget "tdvideo" */; + buildPhases = ( + 00EED04B2B5A4A2400637604 /* Sources */, + 00EED04C2B5A4A2400637604 /* Frameworks */, + 00EED04D2B5A4A2400637604 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = tdvideo; + productName = tdvideo; + productReference = 00EED04F2B5A4A2400637604 /* tdvideo.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 00EED0472B5A4A2400637604 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1520; + LastUpgradeCheck = 1520; + TargetAttributes = { + 00EED04E2B5A4A2400637604 = { + CreatedOnToolsVersion = 15.2; + }; + }; + }; + buildConfigurationList = 00EED04A2B5A4A2400637604 /* Build configuration list for PBXProject "tdvideo" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 00EED0462B5A4A2400637604; + productRefGroup = 00EED0502B5A4A2400637604 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 00EED04E2B5A4A2400637604 /* tdvideo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 00EED04D2B5A4A2400637604 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0444477D2B779C1B00C7452B /* a.HEIC in Resources */, + 04A281AC2B6A38DA0067FB28 /* img3.HEIC in Resources */, + 007EFE022B60DA3600EFD078 /* IMG_0071.MOV in Resources */, + 04A281AA2B6A2E730067FB28 /* img2.HEIC in Resources */, + 04A4E06C2B87465B001894D2 /* aa.MOV in Resources */, + 04A281A82B6A24840067FB28 /* img1.HEIC in Resources */, + 00EED05F2B5A4A2500637604 /* LaunchScreen.storyboard in Resources */, + 00EED05C2B5A4A2500637604 /* Assets.xcassets in Resources */, + 00EED05A2B5A4A2400637604 /* Main.storyboard in Resources */, + 0444477C2B779C1B00C7452B /* b.HEIC in Resources */, + 04A5AE312B6B3234000D26EA /* img4.HEIC in Resources */, + 04A4E06B2B87465B001894D2 /* bb.MOV in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 00EED04B2B5A4A2400637604 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 04A042542B6F9F4A00EA3EF9 /* PlayContoller6.swift in Sources */, + 009B7E9B2B5BA788003BE217 /* VideoWriter.swift in Sources */, + 044447792B779B4200C7452B /* PlayContoller7.swift in Sources */, + 00EED06D2B5A4B6400637604 /* MetalPlayer.swift in Sources */, + 04A4E0722B87789C001894D2 /* CreateVideoByBuffer.swift in Sources */, + 00EED06B2B5A4B1B00637604 /* VideoPreview.swift in Sources */, + 043A6AD52B81AF04003776D2 /* PlayContoller8.swift in Sources */, + 04A5AE372B6B47AB000D26EA /* PlayControllerImg.swift in Sources */, + 049EBD832B6C91C4005421C7 /* PlayContoller5.swift in Sources */, + 04880AF42B6F7A3B00FF9E59 /* PlayControllerVideo.swift in Sources */, + 00EED0692B5A4B1400637604 /* VideoFile.swift in Sources */, + 00EED0572B5A4A2400637604 /* ViewController.swift in Sources */, + 04A5AE352B6B479E000D26EA /* CCAddImage.swift in Sources */, + 00EED0762B5A4CEB00637604 /* FrameProcessor.swift in Sources */, + 04A4E06E2B87483C001894D2 /* SpatialVideoWriter.swift in Sources */, + 00EED0532B5A4A2400637604 /* AppDelegate.swift in Sources */, + 049ABF9F2B861C450049A94B /* PlayContoller9.swift in Sources */, + 04880AF22B6F702A00FF9E59 /* ViewController2.swift in Sources */, + 00EED0792B5A686500637604 /* SpatialVideoConverter.swift in Sources */, + 04BD5B3E2B81E21100DBBE08 /* VideoPlayer.swift in Sources */, + 00EED0552B5A4A2400637604 /* SceneDelegate.swift in Sources */, + 009882722B6269B30076385E /* PlayController.swift in Sources */, + 007EFE042B60E00000EFD078 /* VideoConvertor.swift in Sources */, + 04A4E0642B870160001894D2 /* VideoConvertor4.swift in Sources */, + 04A5AE3D2B6B757F000D26EA /* PlayController3.swift in Sources */, + 043A6AD72B81B301003776D2 /* VideoConvertor3.swift in Sources */, + 00EED0682B5A4B1400637604 /* ImageProcessingShaders.metal in Sources */, + 04A4E0702B87697E001894D2 /* PlayContoller11.swift in Sources */, + 043C63122B6B90E80095F268 /* PlayContoller4.swift in Sources */, + 007EFE082B60EB5900EFD078 /* VideoConvertor2.swift in Sources */, + 04A4E0622B86FED3001894D2 /* PlayContoller10.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 00EED0582B5A4A2400637604 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 00EED0592B5A4A2400637604 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 00EED05D2B5A4A2500637604 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 00EED05E2B5A4A2500637604 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 00EED0612B5A4A2500637604 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 00EED0622B5A4A2500637604 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 00EED0642B5A4A2500637604 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = tdvideo/tdvideo.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 2RAN5PZH5L; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = tdvideo/Info.plist; + INFOPLIST_KEY_NSCameraUsageDescription = "我们需要访问您的摄像头以拍摄照片和录制视频"; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "我们需要访问您的麦克风以录制视频的音频"; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "我们需要保存视频到您的相册以便您后续查看"; + INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "我们需要访问您的照片库以保存照片和视频"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.nks.vptesst.sdk; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 00EED0652B5A4A2500637604 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = tdvideo/tdvideo.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 2RAN5PZH5L; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = tdvideo/Info.plist; + INFOPLIST_KEY_NSCameraUsageDescription = "我们需要访问您的摄像头以拍摄照片和录制视频"; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "我们需要访问您的麦克风以录制视频的音频"; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "我们需要保存视频到您的相册以便您后续查看"; + INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "我们需要访问您的照片库以保存照片和视频"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.nks.vptesst.sdk; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 00EED04A2B5A4A2400637604 /* Build configuration list for PBXProject "tdvideo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 00EED0612B5A4A2500637604 /* Debug */, + 00EED0622B5A4A2500637604 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 00EED0632B5A4A2500637604 /* Build configuration list for PBXNativeTarget "tdvideo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 00EED0642B5A4A2500637604 /* Debug */, + 00EED0652B5A4A2500637604 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 00EED0472B5A4A2400637604 /* Project object */; +} diff --git a/tdvideo/tdvideo.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/tdvideo/tdvideo.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/tdvideo/tdvideo.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/tdvideo/tdvideo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/tdvideo/tdvideo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/tdvideo/tdvideo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/tdvideo/tdvideo.xcodeproj/project.xcworkspace/xcuserdata/aaa.xcuserdatad/UserInterfaceState.xcuserstate b/tdvideo/tdvideo.xcodeproj/project.xcworkspace/xcuserdata/aaa.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..72019b0a6e09a74b8d09defc6486cdc2f7ba6111 GIT binary patch literal 110025 zcmeEv2YeL8`~J@C?%kH#-Mh=x2nd7@=^X_LO(Ru8Z}E~`NFd3DTmpomI~Hsx76cne z0tm!{Vnwl`*hK|b!jF@7eVNnjG0Bqo_jVN#hiCY{M( zGMOx9G&6=7%bdoHWAd2s%mij4Gl?l>W;1h`GNzoVU_wkKvzS@JoWY#Q=*&6Hh0I0F zWz6NwN@f*v9dkXimbr!5!raQ-#%yJ7XYOF`WbR^iFpn^gGCP^am}i)0nO)3t%uCG6 z%%zMoH%*V_p%-765<{RdB<^b~tVh}{b4k|>5S@XRp^MQKXa%|w zU4^bj*Pv@r9jZqg&<*G&bThgW-G%N(FQDCM4|);3gkDCkpjXjr=ymi4dJ}z$K0}|Q zFVJ4JAN`DeLBFEk&>t9KD;BVVz1WBSI2Ip=JK!F;C+>w$!~^g^d=egnbMRn11P{f- z@Nj%8o{Fd8>39a7iSzO4IDqHiGF*-;@Y(nZd_7)^Z@@R@Q&YsGSV8^gy+41aTb~-zQ4X_1lAzQ>QWEZiE*(K~5?3t|2 z680?iY<3xYIeP`WlD(R}hF#6BVe8m>b_06@yP0iZZ)CTyx3agfTiLtVyV-l#huJ6C zUF>dl5BnnfD*Gn;F8dz)5&JRw3Ht}faEQYk%W)jfSvV^va3ZI0F3!z)xDH%Lt~=L* z>&f-vdUMHK3YW^A$PM9gxl!C`ZX7p}o61e&^10Kw1zat+kXytp=9X}0aA$HlN4T@N zrQF5bCERlEN^T{$hP$3y%hhr9TmyF_cN2Ffx1HO;J;puGJ;6Q0Jrq)YRekS^_I1kI!nD}on@ot7RwgP zt(LnicUvB`JY;#;vcvL-!Lt)(+N=)=t*0*51}cYmzm~nr-cG9c&$99cj(8PO;9g&a@U-i>q_ft>l*9z)(zGhthZRVSZ}r7X5D7J z*Lt7zQR~yz7pyN?U$(w(ecSrJ^#kiC)=vdSKmrz6ffIPaB3K1M5Cut41-IZ4ItU$w z?m`ctr;sQl3E9FRVYqOrFhUq9j1|TUlZ7e53}L1)Pna((5Nd^m!Xja@utYdRI8)Gt zbA*e8Wy0mc6~eW`b;4?4jc~oNQP?EhCfp-z7akHG7Iq4c2~P=63(pA83a<&T3vUQ- z3U3K-3-1db3wwnxg|CDkg`b4|!q3715sR|u6n$a`_A;@f*hTCv_7?ky31XsnvUrM^ zE1oKj5J!rm#L?myajbZnI6<5)&JbscMPg7a7c0b&SSceOyj#3S+%7&SJ}f>eJ}bT`zAnBkz9W7hejASnNcu_IFa0e2BK<1;CjBn+vM5WkDr<6gxrf|S z?j`q@`^bIeesa8=AZN(ga({V%JW$S&2g}3c;c~8gsysp-Baf9&lgG&uzwr_0T+P<@WZ~MXaqwOc#e%sHs-xW@g6j_N;TuKL} zqmr&QJz zUQl)`dz2TISCw~^ca@KoPn55feabh=x5{_QugY(#OLeOr)vNkczZ$C^r*=>~s-4uX zYHziVnxrPH*=m3F1ocF9fI3VauFh2R)zj60TA&uHMQTtjR%fXtYMB~VYt@D7S?by9 zMd~tjy}CiYLEWfsQa7s&>W%76>dopc>Q;4|dart)x?SC&KB7LYKBGRX?oyvqUr}FG z_o?5g->ToK->W~UKdL{e`_-S-U(^E{uURykrf6=>qj@!-=GVGtUA6w&3EGL;0BxXl zk~T=o(FSWnw4vH5+8AwuHc^|Y?hg>*eBR0+9%m3+o#y4+Nas4 z+h^Ek+Vky&_SyDI`&@gieWCp<``Pwu?bq2?+t=8yx39I=+3W4=?Cb3t?3?Yk+HbRO zwclgkW`Eee!~TfKec~m|J?qCeXspX`&ag_?fdNC+kdtHX8+y6 zIyi^Tp*U2B=I}avjwDC2BgK*GNOPn+G8~zXEJwDZzhj_dm}7)vq+`5ef@6kbremIC zzGH!-*0IpB$g$Y5#Bqk>Oo#3`$8nM4GRNhPYaG`));ZQYwma^3Jm7fH@sQ(T#}3CM zjz=9k9gjJla=hT!?ReGkn&Umk`;HGBA3F9rzI5_Vi__{9oT5{5%1)b8ajH(u8RLv~ zc6N4g_ICDh_I0K@)0`unqnx9iW1M51r#Z(t^PJs;u( z+x{7V!Frli0K*AE2ei$ zpO}Q0^q7p8%$Tg0fiWk=oE&pXOm56+G2>!p$IOW-iz$z(hzZ41#>|bWiV4S5$1I3B zGv?fwr7@SpERR_gb5+dxm`yR8V{VSQC1z{P?J;-9+!OOq%)>D|Vjhdx74uxo^D!^R zyczRu%%?GXU3Qnt<#zd9v93<8<6UvCZmwRg-mZ98f-A+9>dJIwxlVKqaOJoLyN0_? zc8ze2bd7bL=9=J|=$h)9<_f#2T{W(GuKBJ7u3FbZ*CN+q*Af?To$tEDwcNGJb(L$a ztIoB}b+79_*LK(at_NHXx*l>p?Aqaa#Pzsqm+M8>ORl$EZ@WHred7AV&A5>pyID8q z=G_*z)h)P1x8ioWW8KHO*GQuk%<%iUMFSGccquXL|+U+b=SZ*p&T-{#)xzSDib z`vLdM?pNHex?gj@?ta7lru!}T+wOPV@47#9f8qYd{jK{K_pct-!+AP*I(j;Jj`wu- zbn$fc#Cf`Tx_f$f`goE(nVu}qAWx1b*K?|;z*Fcc@&rA_o>`s}PpN0NXO5@LQ|X!S zS>QRtbEfA!&-tDUJePSc_cVBJ^xWjR*>j6$i|1C)ZJw>3+dX%9?(sb6+39)Av&-|G z=M~SZo_(HgJl}f0^L+35!Skc%C(nM*&z@gA2fVyj_S(EIuiM+v+sT{Z&Gcq@v%USj zCwNcv4)6~2p5z_m9qJw7J09Yr<-5vvweK3=wZ7|ot9@&H^}fx%ExucQ z_xQH?9`^0h%C_pI+d-}}A~d>{Hg@_p?4#P_N1Gv8j{*S>weZ+t)ae)aw4`@@g? z*l+Qxey2ai@Adoq-TdACJ^VfWz5KoX@&06ghCkDv3@bC41>Ho_A zwSS-g$5A-YkIx)vHotZ97SDn`_x>Xl+QI~XCw{2p& zG2NLSOi%c?6a3pvS9Dd^bUXYjz`u%~m~VH}a!MWDWtd!2l=_x6lvoljWC&6EGb_%>DrKfimqoa|{3}O0h zX8JQHFefqtn1Rem%pfL*8LT^Wryirbbhqx&y}D2LZ-!AA#tdgpW=>&pnNwk0M#4D6 z>gVX^>gVeh=oi75TtY|14%Nyl3C$lJm{&S00Nrc;)c7^0DhQqIfK(HzOUp{DOM~G_ zrA5^xX0qIha3wgFLRC%48t*icO`ljAoIf;JP%~>542V5uRIof$ReN$(pt7Xt*JvFY zOPB%04J#^*F*m z7c7Iu1&bQ5f$Qw#s!&a(c>x@4kSeY*%h93g(!yYPL}_(``LWr3$2F!hE=M~%VK@jx zzP2^&ab)`OE^QmYJ{j)O+iF z;2nCxHN3Q}I#^{0(%{-r&@12)gf@>1gsU6tO@Gd}dnQyAx0%>y<`FEKklQk!XnhEC z7UNmFR`0c&IhR?=oX4C$)nu#+5cSFC$6$E+=)9q&AXlM^Kviu$q`F}8=)5t8$I=v= z7WK&s0SX~uw2WoUC5)$zxmfR8$1KXg#zV7bvy06*|DEE~Kn8 zHWUUK302GpN3fF%_k`D`=Ip%n(qSvEzpGx5>qRM!iToG$P&e~ay-kT5G}rtbH|WAj zo*|S$mdw86opuXiLlg&Nh4bD&}e+wP6b?%S^5uQ&U}5S`nmz zXgYjpvm+z6j=6@pmbswa>GSOc`Qxik9x^dGW9Y>3O=SS6Q!<*qHY;*`ZDlZ=87VG* zVqSh~a#K2^GxI0lpZWH>#$ZR=5cv>zLIPBkhyCC`#&> zjZD7{Og*!XSZqQToR6R{k*E2Q%V{B#`fL3k-w#d}8fKsxlgmi6N;tT>v$PI^K zTFSR~Ib0$`HuLS>50@sdx~itIx~2-`EWI(^A<4-soYDex*^p2ru<;?;!%S-u%wgH% zkU1nY;;1(ofO{>qGRD|1tV` zoOzN`!xQ?TI_4=o=djc;#(^simiWKEpdyIlt3sh_9c`9v-s8G->)E%TsnNZpsG8m3 zjB&X=5#?=qs4@tePN<4X3TR2yp*g{d;ibW{B9j6Q1qg__Dp&{-Vlu8lDxfw5E2wA` z4X&xK4#7Q?(3|8atP6Gqv_ej|?Di@C*k*d;TAkh9p<}1x+hSu-Gjpn{0=4y>IycfV z+}|~Bt{nCXigAj}suTM-$?);~z^6mbnY{t``Ys#VZHKSLJ++o4X!Nh`A)EkzyGC@Nc z2}@adpi&gV(p3%2TGznx)N{-W%pO>RdWCt7d7Jr+`JCAci%-8H9x2EU%T1lo@u&;x zfznYnEHVv3gJF@W7?zZlq07)#w96!C9A)d2mSl}SKg>K86{zQ#-4wtt=tJw6J^HZ2 z0+`NiufW{)+MmsB!~g!d?e>j7CGUNveRJDIV~vhD(zz|#C2zt|K$pCwpHj!Xqv!r< zmwW(S^3k7l$*D)DOTNr*XP5l7=|0*epE6%C{pu+|^^y9hdO+!y`e=OyrCI;nnqXD! z*Z?)HfE>b2#?g#ogD%Dfs^RyFXjb1cKT!t%j`^PXf%#D%qmR{3)5oo6_A@^-zc9b* zdHQ&Lf$H9GymMF@&0`(~JsH zD=JixZ+9Bs^1y_EnZM9bzU+)?DGm-)7nab;YkY7)wQ>LWV)*tVNS_5+84U?YL=uvx z7+H)DR+UesN@bBzm#M~&MuGYyozbW1)AztF3Q~~lN3;`|#o>;1@Uzd*Ud``j z(pYrdK}|B71Etra4pfn$mT_Zy8!sO8dt*PCxgL)wJGQ#9MPJsVE;Rc;Ni{Yy_^2D| z4TF!m1Euvuz4UzjbUmOKtVexNU(^rv(F^q=JqYjrkoADlP`YV7{5ews1D^o2fcopj zb?8KW)}PS=8pNzXga3>cNe46TUJJJL1mz#ai#>b;`WX2uAg`O9qO-^{0eBs<<6*R6Z|GYNca zv}142!ccv$zV*HPv|%3gi*IHgU1aiQT_O~ol(J5ez>5=;2kKF9C|F(K#Rw*Z=a&{& z+a+V3KMuCA3}8mUHkB#hGAMy9Dq)!87cv*aMwAVp`QHSaP#%Su{!Q3s@*(pjY%=*C zSwWPK2Z`tj+ep#?T_e#nR0LZ^Dp43MM(3jQVUE8BT?d;(HlX_sn(0#yn(6P43fM3- zlIeE?8jemzr=VPPDjK2B*5~MDdbwVqhi+iHq0zv!J9x?Jh5BOJB1(EjjY0bhfDjZ#l-fA>eMn7J6;yL% zpr)d*B+pb0#|4X_)fI)NpCTDjof4QAG-sHyvf5}aL&i=pa-Btcr=+y32<|bu!sJ2Y zT1&qUXwZ?Ose{i*Ft#cKBzNyQ)euyK$UeUq|etEMCH|F1mPA#Ru!uXc1b%To6^oqrH44I+yX# zUM5B_&wyTDV)Sy=2DB6$em&3ys8Bxx`dHHm^fkCbx<{LoQwEN_qFOrM6~jYSql5Fu z25P`}Vf2kzr%O=fIut^eqRXIhXX?7XV0wg*I(P5hIji}X6!VUiMk_!UShOgqU+4Jb zq$T}2FG|Tw>DM_cGi%8bJB}MvImnQPXcMkOYnXm@Xtlny4qdOGM}g1KViEPwl`r@2%AdSblS`qV{D59HD%QX zYE2((pTWW6P!)JK%PRv_rQuM8A?y>Nk0YI7I#x{z&?3xWm|nGI!KqCbXO0RiC@n8t z7=Z`~Y?DVe4_sr71QSf{W^WMzGgl80DXwYp^x?J^&5a}x9W+LM+S={RirG{SmVZCo@!Q)_TiLq zrL#(m#L?mZ5`9aH`3ilF_Mva|wR)XiudiDV2-}Z-0Ej9usOuv@od`ojO?X%)2+lXa zH7+pUU{+x6*p^o#4n;%3g;uw?XxtvE0vv!Fva}Ms32?usMG_--s_vT7^gBARR^MO} z&=SnlQ@)9`0AtL7NMTmrSciFi6UCYXjP=HTFzC|A28*b+9!pr(H|tyUMbQjY>|{La zu!il}p*QF^>Nl;!F-$k?)^CR9-=g=L0ogTqDyGwKO)DjK_(~fzDliM|hnk{b<9Ngk zs@m6_PH;!urBy*)nI5>Cek=6wZF;Y!;Q_CzX?aE~*&C<8(iZN6`{I5$9w*>LoP?A0 z+x0v2JN3KtyY+kYZTh`*X^Z~BqL%q*u!I7+Q?`gOMqi4Os3WfkxFH-W3sP|$Sz1s< zMZ@&dMCKv7!qsN#@ulT3tq000Tc+=GsPtoVhmMD9Ei&(SsKld!)qx^dF>9H;*P)VA zq_j+W$ZF@+6ohL*@XA}JPdrrCplnjV`DifLTHbNUx;IV=&6+xOOl7blw*pq+!k{nH znHisqYU}VR`U6cs9D&Dy#)U`XQFt^SgU902^au5a^oR8w`Xl#bos8^vV-E@t|z!-cpA2ldDGC-f)R;aRu@ zm*`Is{673o<C$7YEaTN~Z zYFvZo;rVz0uEh)SBD@$c!Drwzv5txUwEi1GeF-{^pc;a%A!s{6?-KMa!ES;x37$&u zLV{Nlyp7m1SZvV+ z^pPRcpE0wE=FI8J-KZ~&0}nAcK!fJ*aA$d-w4y0F^pP=$V~V4BHeJ(S(s${v>Tl@J z>UsJ0#Kwh#7M*Ek*yO}-M!fB=-^SRp@{0An3zcQpm1U!5J-fn zat=Hjk=T7$M)&L?-E(rf4@>W!o!ou!P~$^V_bm7TmomC%Wp*Ey)_w5c?t?Svr5yO2 zofy%K8~<&qQ_L>efgb@~4d%4yFh-$Kj~}Hy(bgq!N8!it=hbd@&|7QE?SA~O_z34>`{Z9I9AIIGY2CF7Ogd82=vk_}g@VM%-*JfA@q z#zU>XC&2P>A^c9SRzkiZX0}=Nr|~n>pyE-G*5hZH3!496rg{!PpKqrVNelEf0idNQ z*5emwtJ|G^4+hsAei4cIW&8@=15*$P2dBZmRlzdw7|siVfA4Mmt$%lnU&n8Nod-lx zk6$;&w6dn4thCUSjrg#!*8sM*G|DO61Yk}TQBWj|1;<6WCLCS*D+)&EgLm-fOuq*F zE`ATck3Ya4;*aph_!Imo{!D*g|3Lpx|49E>|3v>(|4jc}|DpkZVd^UQYrGGQ1EuRb z&^h-;^p$=3kD%fz`cJ0*8QWBvS(^lN+1hZ8%`d9kb#%lv?-klv8TUKT5;I?En{hK=0p?^y)_E_*?ftF_Melg9A#=eN& zP8w(c24|=a@w3!Ce2E#LpA`|S$V8yKJ5=WLVZct>iK{TH( zY&V)uS2j-Hum9XEp9#g#=7V#X*^=T6*=PH)@#YRPv%<~vk$N_PN&?+o*6La-{eVqk zli3tD6)%TR8Ehs&eFzc>lJ(#9UyUzTf}|GnE}PBvkIuUU@%n-2d}#LlAlmmi>|k~X zJ5>LJAn=*61aXa%9D6dITL@z4fCEE&`j^htiz6EEi^HP z5ro9rL5bU(n~bTnWlyIxN|2Cm z|2Jy{JG2TzF%fi z1lbVmr(=uRS!@Yg%Fbrzuw`sHTS1VGAcY{6AdMh9K@NhP1jP{K+RRq6bJ;333{qFa z&SU2@-3W3M6h{zz=&r9Ps0Tqk2`r!(&Lj;ysG#3d*{K8rJv?=4C{#W?K=;hS4o%Z3 z1!i1`?zwi0=U&5)%9UVT|tXwe{lKI%yQ^% zfC|u&n+lvZ1Zo1lwh$~gKq@Mh!J_e@%8|i&!Lmp`LxFJZL&5zC-5!X#j7*U_2OYS9 zJ(pd|p2wcgUcg>RkcS{YLB|o)k)Y!V>H^ZmUJS%{3A>!Vl)a4ZIDl;n)$;;n^dsFW zSQ;oJ$V-sV&gR=EE?NXW!?f&#jFhb8tfZvW?4-2xOmHM-W~C*hq$j0iWv8TOWMyZj zWg3pfw6xTOtn_4XFeW9Zr=@0P^y{3QmYtc9l9Zm7nVklkC)2Ysmn<=vX$5;FXbXlf zrJh{@&W%XTR?(Uf6bp4Yf*NFGA4v_OWnF8Q)uE}Z)THEu$(czh zfP&Pdbm+s3tgM8L%+%zp>?~+YW>$KtQC3oRYC>jKRtBIXDI+yCB@6m6B_%r{B`qx@ zIV&|gJuNFMeaVuzliuxBTQ+ppoG3Be#NN#GgN=(|X?t@AH`~tEWcS3K^t`#w1G?;@ zErQbrckgAQwIdO}88W@cJ;dU8@`MpklC79f+V1-DZ*;SPd&>l>yHo-#E) zH7mY42}Jr3b5QHaavNU8-p6i-$?^gALG~en67_co$|9&Aoh-)^lul4)%gJ&F`$*Gd znMzQ6%h~ctum#zt*r(ZN2udI*nV^)WnQ|NZJe~X!2}&~TKVzjY;=~D6MeINR$R@$J z%tUZ3RvO65wQlhDMA9P6dUP(>ObQ5j2dT;pV%(!TyfDcZ8TY zFXsn?f%6e`N*xDtn6Uxs&rJrd6952g+d443v+-HN~*57w%OF&qMT& z{0Mr?_!3w}E&(H>fA3cG#--t9Tn3kkmvh-%e@br?D7{T5uqqDpR!z{j2q4Ytj7~Hk)TP9^ajO&w~H2Hc()wXPhK__tk7VD+K!0WxJf3l zO)-hB>Zoh?ZB(~Wspn>ztTrv*{%=&oe0YfqaAiPITme_e6>&kXn486waHZUAZVo{+ zU?rQNe1c9VC_qpFL4^br5fmh-xPdD-DTjHc*lZA0~szMOA_M zz4F^)pPPYwws5xs`)uWIr|eTp*@qA`kFw9z1fAK6eeUA!ZepJ$1kG>BKKE1hd4PM6 zgUl8Xw1}X^P3&_s_b6qbT7niD%2i~$VFkF*z(kBFSIUub&5i+h`UhkKWMk9(i{fcud9h@i6wI-4Ls`?&-yCFneY z&L;>^4p#0(4csRYCi;Tg%Y6w<^fhIoWl<)&l%OjKT1lB`)iEYI3QPo>73#PH1YI0q zA|5=g>v_zx^hMAm1T8n;L0KAW_eJ=Cmv|cx5ib*TSsf39b9wuShoMPSz6JSvgFztHz+frES(T7wsqiCp9yTmXYtuQ*bb`+T1!w}BU?cs11VcUzSmQ>a<`PWln7m^M?_cr z2$QbXnRGSpsH;qER=OEq@#9Rs+K_Mmw<=>27C(W9Cte%)iTosfGCzf%%1`5`^E3FF zJZMZC3ED)^W`Y_Bf+4wypqmN0g`h12-P*thOv2)Wd@*b(?Z%f@y6-S| zw=&cCH9$RU`8uGUb^LmKIYAFm>Un~o`ziIn$HxxZDZy{#H#ITOqXa$Bl6h{S%(I2R zmA{Rk2MO9iV7p{=PXfP&zmqZ#FxilClp7G1>X6J^3K-;!CoH2 z)y#iO^`!5Bci=_POO$xv4S47LCuu>sozI9H6nQ4X9^E2pQ7o|X4QtO9jWW;Mz&sY7^49CEcK`gi&KA+R+gal;s4IpguDRs{E*HM$PNm zjF>EgO=9{Y-~P|m0hT$NmV@WklormRJG;I|{HL34-^QTDWDVCBF$^KNIwQOMnF_z=|!i zEG2}>)lUTNZvxl^b3q33H5O!?hC*vRvl5(d&V)w>vjry1elcMdKI$-QtGMVBtU zJcu}D~-H^I}+?|$vbaR-g(>dj^$l~ zeFPszaEB(|dDikFOl&mYSgM0}$SntYj4TAh4!OLN0BlVOC%~iTfik;rWE}L3!9lo_ z$w5nwItLxD?#-vZ{RSIhc-Z>itrP4zYnlp;9{!6<#+Ki$0%f)XmOreF6wYh-mjXDe6?f>Bore z?mA&m^Gd9SlXZSm~|*#W*u%l z885fyT2BS_37-J!6CO-(7NxN%1P?k`eX@?Sj&7o{fdpr_pfT%spfT$N>qP4$g8LJE zBEbV1Y0Ns*LKl4T2{02-8q*AQWH>w^@E3F^>nxMEPBM9`@~HDxtI9SiPSy&Os&ew} z|9mwfkeh2=4BTR^vWBhI)*9

wN11Ypr#mbrHcs2p&rCFoK5@d@{kO5S&Z!sRWN8 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 CWWCJ* literal 0 HcmV?d00001 diff --git a/tdvideo/tdvideo.xcodeproj/project.xcworkspace/xcuserdata/mac.xcuserdatad/UserInterfaceState.xcuserstate b/tdvideo/tdvideo.xcodeproj/project.xcworkspace/xcuserdata/mac.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..17863e047e9e27f16bd66e4f2c2228d6ab0b0841 GIT binary patch literal 307853 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

vY&y#hvSiQN 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 literal 0 HcmV?d00001 diff --git a/tdvideo/tdvideo.xcodeproj/project.xcworkspace/xcuserdata/zhihaizhu.xcuserdatad/UserInterfaceState.xcuserstate b/tdvideo/tdvideo.xcodeproj/project.xcworkspace/xcuserdata/zhihaizhu.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..458d63098c1c3284f04f0f6ee83dab7a0079efdb GIT binary patch literal 13277 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 + + diff --git a/tdvideo/tdvideo.xcodeproj/xcuserdata/aaa.xcuserdatad/xcschemes/xcschememanagement.plist b/tdvideo/tdvideo.xcodeproj/xcuserdata/aaa.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..dafc7f1 --- /dev/null +++ b/tdvideo/tdvideo.xcodeproj/xcuserdata/aaa.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + tdvideo.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/tdvideo/tdvideo.xcodeproj/xcuserdata/i308051.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/tdvideo/tdvideo.xcodeproj/xcuserdata/i308051.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..3a84f20 --- /dev/null +++ b/tdvideo/tdvideo.xcodeproj/xcuserdata/i308051.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,227 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tdvideo/tdvideo.xcodeproj/xcuserdata/i308051.xcuserdatad/xcschemes/xcschememanagement.plist b/tdvideo/tdvideo.xcodeproj/xcuserdata/i308051.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..dafc7f1 --- /dev/null +++ b/tdvideo/tdvideo.xcodeproj/xcuserdata/i308051.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + tdvideo.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/tdvideo/tdvideo.xcodeproj/xcuserdata/mac.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/tdvideo/tdvideo.xcodeproj/xcuserdata/mac.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..e237f4b --- /dev/null +++ b/tdvideo/tdvideo.xcodeproj/xcuserdata/mac.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/tdvideo/tdvideo.xcodeproj/xcuserdata/mac.xcuserdatad/xcschemes/xcschememanagement.plist b/tdvideo/tdvideo.xcodeproj/xcuserdata/mac.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..dafc7f1 --- /dev/null +++ b/tdvideo/tdvideo.xcodeproj/xcuserdata/mac.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + tdvideo.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/tdvideo/tdvideo.xcodeproj/xcuserdata/zhihaizhu.xcuserdatad/xcschemes/xcschememanagement.plist b/tdvideo/tdvideo.xcodeproj/xcuserdata/zhihaizhu.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..dafc7f1 --- /dev/null +++ b/tdvideo/tdvideo.xcodeproj/xcuserdata/zhihaizhu.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + tdvideo.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/tdvideo/tdvideo/AppDelegate.swift b/tdvideo/tdvideo/AppDelegate.swift new file mode 100644 index 0000000..576500e --- /dev/null +++ b/tdvideo/tdvideo/AppDelegate.swift @@ -0,0 +1,53 @@ +// +// AppDelegate.swift +// tdvideo +// +// Created by aaa on 2024/1/19. +// + +/* + https://www.figma.com/file/HtnafU9zeXR8qeW9CAIoIO/VR?type=design&node-id=379%3A963&mode=design&t=1Uxec3RbjuOvNuUK-1 + + 万川测试 com.peuid.snsdev + com.cjsztea.wanchuan + com.nks.vptest + */ + +import UIKit + +@main + +class AppDelegate: UIResponder, UIApplicationDelegate { + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + let window = UIWindow(frame: UIScreen.main.bounds) + window.backgroundColor = UIColor.white + + let vc = ViewController() + let navigationController = UINavigationController(rootViewController: vc) + let tab = UITabBarController.init() + tab.viewControllers = [vc] + window.rootViewController = tab + window.makeKeyAndVisible() + + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/tdvideo/tdvideo/Assets.xcassets/AccentColor.colorset/Contents.json b/tdvideo/tdvideo/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/tdvideo/tdvideo/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/tdvideo/tdvideo/Assets.xcassets/AppIcon.appiconset/Contents.json b/tdvideo/tdvideo/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13613e3 --- /dev/null +++ b/tdvideo/tdvideo/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/tdvideo/tdvideo/Assets.xcassets/Contents.json b/tdvideo/tdvideo/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/tdvideo/tdvideo/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/tdvideo/tdvideo/Base.lproj/LaunchScreen.storyboard b/tdvideo/tdvideo/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/tdvideo/tdvideo/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tdvideo/tdvideo/Base.lproj/Main.storyboard b/tdvideo/tdvideo/Base.lproj/Main.storyboard new file mode 100644 index 0000000..25a7638 --- /dev/null +++ b/tdvideo/tdvideo/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tdvideo/tdvideo/CCAddImage.swift b/tdvideo/tdvideo/CCAddImage.swift new file mode 100644 index 0000000..52d7a91 --- /dev/null +++ b/tdvideo/tdvideo/CCAddImage.swift @@ -0,0 +1,263 @@ +// +// CCAddImage.swift +// SwiftProject +// +// Created by soldoros on 2024/1/10. +// + +import UIKit +import Photos +import MobileCoreServices + + +/// 访问图片的方式 +/// +/// - formIpc: 访问相册 +/// - gallery: 访问图库 +/// - camer: 访问摄像头 +enum ImagePickerWayStyle { + case formIpc + case gallery + case camer +} + +/// 获取资源类型 +/// +/// - image: 获取图片 +/// - video: 获取视频 +/// - all: 全部获取 +enum ImagePickerModelType { + case image + case video + case all +} + +/// 获取资源后的回调代码块 +typealias AddImagePicekerBlock = (_ style:ImagePickerWayStyle,_ type:ImagePickerModelType,_ datas:Any) -> () + + +class CCAddImage: NSObject ,UIImagePickerControllerDelegate,UINavigationControllerDelegate{ + + //访问方式 + var _wayStyle:ImagePickerWayStyle? + //获取资源类型 + var _modelType:ImagePickerModelType? + //控制器 + var _controller:UIViewController? + //图片视频控制器核心对象 + var _imagePickerController:UIImagePickerController? + //回调block + var _pickerBlock:AddImagePicekerBlock? + + + + /// 直接加载系统的Alert弹窗来判断获取资源的方式 (相册 拍照) + /// + /// - Parameters: + /// - controller: 控制器对象 + /// - modelType: 获取的资源类型 + /// - pickerBlock: 代码块回调 + + public func getImagePicker(controller:UIViewController,modelType:ImagePickerModelType,pickerBlock:@escaping AddImagePicekerBlock){ + +// let alerts:NSArray = [[SSPickerWayGallery:"相册"],[SSPickerWayCamer:"拍摄"]] +// self .getImagePicker(controller: controller, alerts: alerts, modelType: modelType, pickerBlock: pickerBlock) + + + _controller = controller + _modelType = modelType + _pickerBlock = pickerBlock + + self._wayStyle = ImagePickerWayStyle.camer + self.addImagePickerFromCamer(modelType: modelType) + + } + +// /// 直接加载系统的Alert弹窗来判断获取资源的方式 (相册 拍照...) +// /// +// /// - Parameters: +// /// - controller: 控制器对象 +// /// - alerts: 系统Alert对象 +// /// - modelType: 获取资源类型 +// /// - pickerBlock: 代码块回调 +// public func getImagePicker(controller:UIViewController,alerts:NSArray,modelType:SSImagePickerModelType,pickerBlock:@escaping SSAddImagePicekerBlock){ +// +// _controller = controller +// _modelType = modelType +// _pickerBlock = pickerBlock +// +// let alertController = UIAlertController.init(title: nil, message: nil, preferredStyle: UIAlertController.Style.actionSheet) +// +// for i:NSInteger in 0...alerts.count-1{ +// +// let wayDic:NSDictionary = alerts[i] as! NSDictionary +// let wayKey:NSString = wayDic.allKeys[0] as! NSString +// let wayTitle:NSString = wayDic.value(forKey: wayKey as String) as! NSString +// +// let action = UIAlertAction.init(title: wayTitle as String, style: UIAlertAction.Style.default) { (UIAlertAction) in +// +// if(wayKey as String == SSPickerWayFormIpc){ +// self._wayStyle = SSImagePickerWayStyle.SSImagePickerWayGallery +// self.addImagePickerFromIpc(modelType: modelType) +// +// } +// else if(wayKey as String == SSPickerWayGallery){ +// self._wayStyle = SSImagePickerWayStyle.SSImagePickerWayGallery +// self.addImagePickerFromIpc(modelType: modelType) +// } +// else{ +// self._wayStyle = SSImagePickerWayStyle.SSImagePickerWayCamer +// self.addImagePickerFromCamer(modelType: modelType) +// } +// } +// +// alertController.addAction(action) +// } +// +// alertController.addAction(UIAlertAction.init(title: "取消", style: UIAlertAction.Style.cancel, handler: nil)) +// _controller?.present(alertController, animated: true, completion: nil) +// +// } + + + //通过摄像头获取资源 + func addImagePickerFromCamer(modelType:ImagePickerModelType){ + if(self.isCameraAvailable() == false){ + print("没有摄像头") + return + } + + _imagePickerController = UIImagePickerController() + _imagePickerController?.delegate = self + _imagePickerController!.modalTransitionStyle = UIModalTransitionStyle.flipHorizontal + + + //进入摄像头模式 + _imagePickerController!.sourceType = UIImagePickerController.SourceType.camera; + //视频上传质量 + _imagePickerController!.videoQuality = UIImagePickerController.QualityType.typeHigh; + + //可编辑 + _imagePickerController!.allowsEditing = true; + + //显示图片 + if(modelType == ImagePickerModelType.image){ + _imagePickerController?.mediaTypes = [kUTTypeImage as String] + } + //显示视频 + else if(modelType == ImagePickerModelType.video){ + _imagePickerController?.mediaTypes = [kUTTypeMovie as String] + } + //全部显示 + else{ + _imagePickerController?.mediaTypes = [kUTTypeImage as String,kUTTypeMovie as String] + } + + _imagePickerController?.modalPresentationStyle = UIModalPresentationStyle.overFullScreen + + _controller?.present(_imagePickerController!, animated: true, completion: nil) + + } + + + //通过相册获取资源 + func addImagePickerFromIpc(modelType:ImagePickerModelType){ + if(self.isPhotoLibraryAvailable() == false){ + print("相册不可用") + return + } + + _imagePickerController = UIImagePickerController() + _imagePickerController?.delegate = self + _imagePickerController!.modalTransitionStyle = UIModalTransitionStyle.flipHorizontal + _imagePickerController?.allowsEditing = true + + //访问相册 + if(_wayStyle == ImagePickerWayStyle.formIpc){ + _imagePickerController!.sourceType = UIImagePickerController.SourceType.photoLibrary; + } + //访问图库 + else{ + _imagePickerController!.sourceType = UIImagePickerController.SourceType.savedPhotosAlbum; + } + + //显示图片 + if(modelType == ImagePickerModelType.image){ + _imagePickerController?.mediaTypes = [kUTTypeImage as String] + } + //显示视频 + else if(modelType == ImagePickerModelType.video){ + _imagePickerController?.mediaTypes = [kUTTypeMovie as String] + } + //全部显示 + else{ + _imagePickerController?.mediaTypes = [kUTTypeImage as String,kUTTypeMovie as String] + } + + _imagePickerController?.modalPresentationStyle = UIModalPresentationStyle.overFullScreen + + _controller?.present(_imagePickerController!, animated: true, completion: nil) + + } + + //访问相册和摄像头回调 + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + + let mediaType:NSString = info[UIImagePickerController.InfoKey.mediaType] as! NSString + + //获取到图片 + if(mediaType as String == kUTTypeImage as String){ + _modelType = ImagePickerModelType.image + self.saveImageAndUpdataHeader(image: info[UIImagePickerController.InfoKey.editedImage] as! UIImage) + } + + //获取到视频 + else if(mediaType as String == kUTTypeMovie as String){ + _modelType = ImagePickerModelType.video + let url:NSURL = info[UIImagePickerController.InfoKey.mediaURL] as! NSURL + let urlStr:NSString = url.path! as NSString + + //保存视频到相簿 + if(UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(urlStr as String)){ + UISaveVideoAtPathToSavedPhotosAlbum(urlStr as String, self, #selector(didFinishSaveVideo(videoPath:)), nil) + } + } + + picker.dismiss(animated: false, completion: nil) + } + + //图片回调 + func saveImageAndUpdataHeader(image:UIImage){ + if(_pickerBlock != nil){ + _pickerBlock!(_wayStyle!,_modelType!,image) + } + else{ + _pickerBlock = nil + } + } + + + //视频保存 + @objc func didFinishSaveVideo(videoPath:NSString){ + print("视频保存成功") + if(_pickerBlock != nil){ + _pickerBlock!(_wayStyle!,_modelType!,videoPath) + } + else{ + _pickerBlock = nil + } + } + + + //判断是否有摄像头 + func isCameraAvailable()->Bool{ + return UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.camera) + } + + + //判断相册是否可用 + func isPhotoLibraryAvailable()->Bool{ + return UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.photoLibrary) + } +} + diff --git a/tdvideo/tdvideo/CreateVideoByBuffer.swift b/tdvideo/tdvideo/CreateVideoByBuffer.swift new file mode 100644 index 0000000..77b1182 --- /dev/null +++ b/tdvideo/tdvideo/CreateVideoByBuffer.swift @@ -0,0 +1,140 @@ +// +// CreateVideoByBuffer.swift +// tdvideo +// +// Created by mac on 2024/2/22. +// + + +import UIKit +import AVFoundation +import Photos + +class CreateVideoByBuffer: NSObject { + + private var videoWriter: AVAssetWriter? + private var videoWriterInput: AVAssetWriterInput? + private var adaptor: AVAssetWriterInputPixelBufferAdaptor? + private var outputURL: URL? + private var isWriting: Bool = false + + init(outputURL: URL) { + super.init() + self.outputURL = outputURL + setupWriter() + } + + private func setupWriter() { + guard let outputURL = outputURL else { + print("Output URL is nil") + return + } + + let width = 1920 // Example width + let height = 1080 // Example height + + // 设置视频的输出设置 + let outputSettings: [String: Any] = [ + AVVideoCodecKey: AVVideoCodecType.h264, + AVVideoWidthKey: width, + AVVideoHeightKey: height + ] + + // 创建视频写入器 + do { + videoWriter = try AVAssetWriter(outputURL: outputURL, fileType: .mov) + } catch { + print("Error creating AVAssetWriter: \(error)") + return + } + + // 创建视频写入器输入 + videoWriterInput = AVAssetWriterInput(mediaType: .video, outputSettings: outputSettings) + + // 检查创建视频写入器输入是否成功 + guard let videoWriterInput = videoWriterInput else { + print("Failed to create video writer input") + return + } + + // 将视频写入器输入添加到视频写入器 + guard let videoWriter = videoWriter, videoWriter.canAdd(videoWriterInput) else { + print("Cannot add video writer input to video writer") + return + } + + videoWriter.add(videoWriterInput) + + // 创建像素缓冲适配器 + adaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoWriterInput, sourcePixelBufferAttributes: nil) + + // 开始写入视频 + guard videoWriter.startWriting() else { + print("Failed to start writing") + return + } + videoWriter.startSession(atSourceTime: .zero) + } + + func createVideo(from imageBuffer: CVImageBuffer, presentationTime: CMTime) { + guard isWriting else { + print("Video writer is not initialized or already writing") + return + } + + // 将每个帧的图像缓冲区写入视频 + let pixelBuffer = imageBuffer + guard let adaptor = adaptor else { + print("Failed to get pixel buffer adaptor") + return + } + + guard let videoWriterInput = videoWriterInput else { + print("Failed to get video writer input") + return + } + + guard adaptor.append(pixelBuffer, withPresentationTime: presentationTime) else { + print("Failed to append pixel buffer") + return + } + } + + + func videoCreationFinished(error: Error?) { + + // 设置写入标志位 + isWriting = false + + // 完成视频写入 + guard let videoWriter = videoWriter, let videoWriterInput = videoWriterInput else { + print("Video writer or writer input is nil") + return + } + + videoWriterInput.markAsFinished() + videoWriter.finishWriting { [weak self] in + guard let self = self else { return } + if let error = videoWriter.error { + print("Error finishing AVAssetWriter: \(error)") + } else { + print("Video creation finished at \(self.outputURL!)") + // 保存到相册 + saveVideoToAlbum(url: outputURL!) + } + } + } + + + func saveVideoToAlbum(url: URL) { + PHPhotoLibrary.shared().performChanges { + PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url) + } completionHandler: { success, error in + if success { + print("Video saved to album") + } else { + print("Failed to save video to album: \(error?.localizedDescription ?? "Unknown error")") + } + } + } +} diff --git a/tdvideo/tdvideo/FrameProcessor.swift b/tdvideo/tdvideo/FrameProcessor.swift new file mode 100644 index 0000000..6897a97 --- /dev/null +++ b/tdvideo/tdvideo/FrameProcessor.swift @@ -0,0 +1,233 @@ +// +// FrameProcessor.swift +// SpatialVideoGist +// +// +// Created by Bryan on 12/15/23. +// + +import AVFoundation +import CoreImage + +///可用于裁剪和操作视频帧以及在底层图像类型之间进行转换的处理器。 +final class FrameProcessor { + // MARK: - Properties + + // MARK: Public + + ///一个决定处理器是否已经配置的标志。 + var isPrepared = false + + // MARK: Private + + /// CIContext可以用于图像处理。 + private var ciContext: CIContext? + + ///处理图像的输出颜色空间。 + private var outputColorSpace: CGColorSpace? + + /// ' CVPixelBufferPool '用于保存进程内像素缓冲区。 + private var outputPixelBufferPool: CVPixelBufferPool? + + ///输入' CMFormatDescription '用于配置处理器。 + private(set) var inputFormatDescription: CMFormatDescription? + + ///输出' CMFormatDescription '用于配置处理器。 + private(set) var outputFormatDescription: CMFormatDescription? + + ///系统的金属设备的GPU处理。 + private let metalDevice = MTLCreateSystemDefaultDevice()! + + ///保存' CVMetalTexture '项的缓存。 + private var textureCache: CVMetalTextureCache! + + // MARK: - Methods + + ///用提供的格式描述和要保留的缓冲区准备这个处理器。 + /// -参数: + /// - formatDescription:注入的' CMFormatDescription '来帮助处理器进行设置。 + /// - outputRetainedBufferCountHint:处理过程中保留的缓冲区数量。 + func prepare( + with formatDescription: CMFormatDescription, + outputRetainedBufferCountHint: Int + ) { + reset() + + (outputPixelBufferPool, + outputColorSpace, + outputFormatDescription) = allocateOutputBufferPool( + with: formatDescription, + outputRetainedBufferCountHint: outputRetainedBufferCountHint + ) + + if outputPixelBufferPool == nil { + return + } + inputFormatDescription = formatDescription + ciContext = CIContext() + + var metalTextureCache: CVMetalTextureCache? + if CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, metalDevice, nil, &metalTextureCache) != kCVReturnSuccess { + assertionFailure("Unable to allocate texture cache") + } else { + textureCache = metalTextureCache + } + + isPrepared = true + } + + ///将提供的' CIImage '裁剪为给定的' CGRect ',然后转换为' CVPixelBuffer ' + /// -参数: + /// - pixelBufferImage:源像素缓冲区,作为' CIImage '。 + /// - targetRect:像素缓冲图像应该裁剪到的目标“CGRect”。 + /// -返回:裁剪的图像作为' CVPixelBuffer ',如果它可以被处理。 + func cropPixelBuffer( + pixelBufferImage: CIImage, + targetRect: CGRect + ) -> CVPixelBuffer? { + guard let ciContext = ciContext, + isPrepared + else { + isPrepared = false + return nil + } + + var croppedImage = pixelBufferImage.cropped(to: targetRect) + + let originTransform = CGAffineTransform( + translationX: -croppedImage.extent.origin.x, + y: -croppedImage.extent.origin.y + ) + croppedImage = croppedImage.transformed(by: originTransform) + + var pbuf: CVPixelBuffer? + CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, outputPixelBufferPool!, &pbuf) + guard let outputPixelBuffer = pbuf else { + print("Allocation failure") + return nil + } + + //将过滤后的图像渲染到像素缓冲区(不需要锁定,因为CIContext的渲染方法会这样做) + ciContext.render( + croppedImage, + to: outputPixelBuffer, + bounds: croppedImage.extent, + colorSpace: outputColorSpace + ) + + return outputPixelBuffer + } + + // MARK: - Private + + ///复位处理器到未准备状态。 + private func reset() { + ciContext = nil + outputColorSpace = nil + outputPixelBufferPool = nil + outputFormatDescription = nil + inputFormatDescription = nil + textureCache = nil + isPrepared = false + } +} + +///设置处理器的Helper方法。 +private extension FrameProcessor { + + ///为输出像素缓冲池分配内存。 + /// -参数: + /// - inputFormatDescription:提供的用于配置处理器的' CMFormatDescription '。 + /// - outputRetainedBufferCountHint:保留提示的缓冲区数量。 + /// -返回:包含输出像素缓冲池,颜色空间和格式描述的元组。 + private func allocateOutputBufferPool( + with inputFormatDescription: CMFormatDescription, + outputRetainedBufferCountHint: Int + ) ->( + outputBufferPool: CVPixelBufferPool?, + outputColorSpace: CGColorSpace?, + outputFormatDescription: CMFormatDescription?) { + + let inputDimensions = CMVideoFormatDescriptionGetDimensions(inputFormatDescription) + var pixelBufferAttributes: [String: Any] = [ + kCVPixelBufferPixelFormatTypeKey as String: UInt(kCVPixelFormatType_32BGRA), + kCVPixelBufferWidthKey as String: Int(inputDimensions.width / 2), + kCVPixelBufferHeightKey as String: Int(inputDimensions.height), + kCVPixelBufferIOSurfacePropertiesKey as String: [:] + ] + + //从输入格式描述中获取像素缓冲区属性和颜色空间。 + var cgColorSpace = CGColorSpaceCreateDeviceRGB() + if let inputFormatDescriptionExtension = CMFormatDescriptionGetExtensions(inputFormatDescription) as Dictionary? { + let colorPrimaries = inputFormatDescriptionExtension[kCVImageBufferColorPrimariesKey] + + if let colorPrimaries = colorPrimaries { + var colorSpaceProperties: [String: AnyObject] = [kCVImageBufferColorPrimariesKey as String: colorPrimaries] + + if let yCbCrMatrix = inputFormatDescriptionExtension[kCVImageBufferYCbCrMatrixKey] { + colorSpaceProperties[kCVImageBufferYCbCrMatrixKey as String] = yCbCrMatrix + } + + if let transferFunction = inputFormatDescriptionExtension[kCVImageBufferTransferFunctionKey] { + colorSpaceProperties[kCVImageBufferTransferFunctionKey as String] = transferFunction + } + + pixelBufferAttributes[kCVBufferPropagatedAttachmentsKey as String] = colorSpaceProperties + } + + if let cvColorspace = inputFormatDescriptionExtension[kCVImageBufferCGColorSpaceKey] { + cgColorSpace = cvColorspace as! CGColorSpace + } else if (colorPrimaries as? String) == (kCVImageBufferColorPrimaries_P3_D65 as String) { + cgColorSpace = CGColorSpace(name: CGColorSpace.displayP3)! + } + } + + //创建与输入格式描述相同像素属性的像素缓冲池。 + let poolAttributes = [kCVPixelBufferPoolMinimumBufferCountKey as String: outputRetainedBufferCountHint] + var cvPixelBufferPool: CVPixelBufferPool? + CVPixelBufferPoolCreate(kCFAllocatorDefault, poolAttributes as NSDictionary?, pixelBufferAttributes as NSDictionary?, &cvPixelBufferPool) + guard let pixelBufferPool = cvPixelBufferPool else { + assertionFailure("Allocation failure: Could not allocate pixel buffer pool.") + return (nil, nil, nil) + } + + preallocateBuffers(pool: pixelBufferPool, allocationThreshold: outputRetainedBufferCountHint) + + //获取输出格式说明。 + var pixelBuffer: CVPixelBuffer? + var outputFormatDescription: CMFormatDescription? + let auxAttributes = [kCVPixelBufferPoolAllocationThresholdKey as String: outputRetainedBufferCountHint] as NSDictionary + CVPixelBufferPoolCreatePixelBufferWithAuxAttributes(kCFAllocatorDefault, pixelBufferPool, auxAttributes, &pixelBuffer) + if let pixelBuffer = pixelBuffer { + CMVideoFormatDescriptionCreateForImageBuffer(allocator: kCFAllocatorDefault, + imageBuffer: pixelBuffer, + formatDescriptionOut: &outputFormatDescription) + } + pixelBuffer = nil + + return (pixelBufferPool, cgColorSpace, outputFormatDescription) + } + + + ///从池中预分配像素缓冲区用于处理。 + /// -参数: + /// -池:' CVPixelBufferPool '预分配。 + /// - allocationThreshold:可以从池中分配多少缓冲区的阈值。 + private func preallocateBuffers( + pool: CVPixelBufferPool, + allocationThreshold: Int + ) { + var pixelBuffers = [CVPixelBuffer]() + var error: CVReturn = kCVReturnSuccess + let auxAttributes = [kCVPixelBufferPoolAllocationThresholdKey as String: allocationThreshold] as NSDictionary + var pixelBuffer: CVPixelBuffer? + while error == kCVReturnSuccess { + error = CVPixelBufferPoolCreatePixelBufferWithAuxAttributes(kCFAllocatorDefault, pool, auxAttributes, &pixelBuffer) + if let pixelBuffer = pixelBuffer { + pixelBuffers.append(pixelBuffer) + } + pixelBuffer = nil + } + pixelBuffers.removeAll() + } +} diff --git a/tdvideo/tdvideo/ImageProcessingShaders.metal b/tdvideo/tdvideo/ImageProcessingShaders.metal new file mode 100644 index 0000000..431beef --- /dev/null +++ b/tdvideo/tdvideo/ImageProcessingShaders.metal @@ -0,0 +1,26 @@ +// +// VideoHelpers.metal +// SpatialVideoGist +// +// Created by Bryan on 12/18/23. +// + +#include +using namespace metal; + +kernel void sideBySideEffect( + texture2d inputTextureA [[texture(0)]], + texture2d inputTextureB [[texture(1)]], + texture2d outputTexture [[texture(2)]], + uint2 gid [[thread_position_in_grid]] +) { + uint outputWidth = inputTextureA.get_width(); + + float4 inputColorLeft = inputTextureA.read(gid); + float4 inputColorRight = inputTextureB.read(gid); + + outputTexture.write(inputColorLeft, gid); + + uint2 gidB = uint2(gid.x + outputWidth, gid.y); + outputTexture.write(inputColorRight, gidB); +} diff --git a/tdvideo/tdvideo/Info.plist b/tdvideo/tdvideo/Info.plist new file mode 100644 index 0000000..740892b --- /dev/null +++ b/tdvideo/tdvideo/Info.plist @@ -0,0 +1,31 @@ + + + + + NSCameraUseContinuityCameraDeviceType + 连续拍摄 + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIBackgroundModes + + audio + + + diff --git a/tdvideo/tdvideo/MetalPlayer.swift b/tdvideo/tdvideo/MetalPlayer.swift new file mode 100644 index 0000000..e276471 --- /dev/null +++ b/tdvideo/tdvideo/MetalPlayer.swift @@ -0,0 +1,345 @@ +// +// MetalPlayer.swift +// tdvideo +// +// Created by aaa on 2024/1/19. +// + +import AVFoundation +import MetalKit +import SwiftUI + + +/// 呈现正在进行的视频转换的预览的Metal视图。 +class MetalPlayer: MTKView { + // MARK: - Properties + + // MARK: Private + + /// 用于显示视频帧的色彩空间。 + private let colorSpace = CGColorSpaceCreateDeviceRGB() + + /// 用于处理图形命令的命令队列。 + private lazy var commandQueue: MTLCommandQueue? = { + return self.device!.makeCommandQueue() + }() + + /// 用于将纹理写入视图以进行预览的CIContext。 + private lazy var context: CIContext = { + return CIContext( + mtlDevice: self.device!, + options: [CIContextOption.workingColorSpace : NSNull()] + ) + }() + + /// 加工管道时应使用的金属装置。 + private let metalDevice = MTLCreateSystemDefaultDevice()! + + /// 用于存储可写入的缓存纹理的纹理缓存。 + private var textureCache: CVMetalTextureCache? + + /// 一个可以提供' CVPixelBuffer '的输出池。 + private var outputPixelBufferPool: CVPixelBufferPool? + + /// 可用于将视频预览写入库的管道状态。 + private var computePipelineState: MTLComputePipelineState? + + /// 应该绘制到视图上的图像。 + private var image: CIImage? { + didSet { + draw() + } + } + + // MARK: - Methods + + // MARK: Public + + ///使用提供的坐标和大小初始化这个视图。 + /// -参数frameect:渲染视图的坐标和大小。 + init(frame frameRect: CGRect) { + super.init( + frame: frameRect, + device: metalDevice + ) + setup(frameSize: frameRect.size) + } + + ///初始化器需要的一致性。 + /// -参数aDecoder:用来创建这个视图的NSCoder。 + required init(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + device = MTLCreateSystemDefaultDevice() + setup(frameSize: .zero) + } + + // MARK: Private + + ///配置该视图及其属性,然后创建必要的缓存。 + /// -参数frameSize:该视图的期望大小。 + private func setup(frameSize: CGSize) { + framebufferOnly = false + enableSetNeedsDisplay = false + + guard let defaultLibrary = metalDevice.makeDefaultLibrary() else { + assertionFailure("Could not create default Metal device.") + return + } + + let kernelFunction = defaultLibrary.makeFunction(name: "sideBySideEffect") + do { + computePipelineState = try metalDevice.makeComputePipelineState(function: kernelFunction!) + } catch { + print("Could not create pipeline state: \(error)") + } + setupCache( + outputRetainedBufferCountHint: 5, + frameSize: frameSize + ) + } + + ///配置必要的缓存来保存正在写入的纹理。 + /// -参数: + /// - outputRetainedBufferCountHint:要保留的理想缓冲区数。 + /// - frameSize:这个视图和它的纹理的期望大小。 + private func setupCache( + outputRetainedBufferCountHint: Int, + frameSize: CGSize + ) { + reset() + + let outputSize = CGSize( + width: frameSize.width, + height: frameSize.height + ) + + guard let outputPixelBufferPool = createBufferPool(size: outputSize) else {return} + self.outputPixelBufferPool = outputPixelBufferPool + var metalTextureCache: CVMetalTextureCache? + if CVMetalTextureCacheCreate( + kCFAllocatorDefault, + nil, + metalDevice, + nil, + &metalTextureCache + ) != kCVReturnSuccess { + assertionFailure("Unable to allocate texture cache") + } else { + textureCache = metalTextureCache + } + } + + ///重置缓存以便重新配置。 + func reset() { + outputPixelBufferPool = nil + textureCache = nil + } + + ///将提供的' CVPixelBuffer '图像渲染到视图。 + /// -参数: + /// - leftPixelBuffer:渲染到视图的左眼预览图像。 + /// - rightPixelBuffer:渲染到视图的右眼预览图像。 + func render( + leftPixelBuffer: CVPixelBuffer, + rightPixelBuffer: CVPixelBuffer + ) { + var outputPixelBuffer: CVPixelBuffer? + CVPixelBufferPoolCreatePixelBuffer( + kCFAllocatorDefault, + outputPixelBufferPool!, + &outputPixelBuffer + ) + guard let outputBuffer = outputPixelBuffer else { + print("Allocation failure: Could not get pixel buffer from pool. (\(self.description))") + return + } + + guard + let leftInputTexture = makeTextureFromCVPixelBuffer( + pixelBuffer: leftPixelBuffer, + textureFormat: .bgra8Unorm + ), + let rightInputTexture = makeTextureFromCVPixelBuffer( + pixelBuffer: rightPixelBuffer, + textureFormat: .bgra8Unorm + ), + let outputTexture = makeTextureFromCVPixelBuffer( + pixelBuffer: outputBuffer, + textureFormat: .bgra8Unorm + ) + else { return } + + //设置命令队列、缓冲区和编码器。 + guard let commandQueue = commandQueue, + let commandBuffer = commandQueue.makeCommandBuffer(), + let commandEncoder = commandBuffer.makeComputeCommandEncoder() else { + print("Failed to create a Metal command queue.") + CVMetalTextureCacheFlush(textureCache!, 0) + return + } + + commandEncoder.label = "BlendGPU" + commandEncoder.setComputePipelineState(computePipelineState!) + commandEncoder.setTexture(leftInputTexture, index: 0) + commandEncoder.setTexture(rightInputTexture, index: 1) + commandEncoder.setTexture(outputTexture, index: 2) + + //设置线程组 + let width = computePipelineState!.threadExecutionWidth + let height = computePipelineState!.maxTotalThreadsPerThreadgroup / width + let threadsPerThreadgroup = MTLSizeMake(width, height, 1) + let threadgroupsPerGrid = MTLSize(width: (leftInputTexture.width + width - 1) / width, + height: (leftInputTexture.height + height - 1) / height, + depth: 1) + commandEncoder.dispatchThreadgroups(threadgroupsPerGrid, threadsPerThreadgroup: threadsPerThreadgroup) + commandEncoder.endEncoding() + commandBuffer.commit() + commandBuffer.waitUntilCompleted() + + guard let outputPixelBuffer else { return } + self.image = CIImage(cvPixelBuffer: outputPixelBuffer) + } + + ///将提供的' CVPixelBuffer '图像渲染到视图。 + /// -参数: + /// - leftPixelBuffer:渲染到视图的左眼预览图像。 + /// - rightPixelBuffer:渲染到视图的右眼预览图像。 + func render1( + pixelBuffer: CVPixelBuffer + ) { + var outputPixelBuffer: CVPixelBuffer? + CVPixelBufferPoolCreatePixelBuffer( + kCFAllocatorDefault, + outputPixelBufferPool!, + &outputPixelBuffer + ) + guard let outputBuffer = outputPixelBuffer else { + print("Allocation failure: Could not get pixel buffer from pool. (\(self.description))") + return + } + + guard + let inputTexture = makeTextureFromCVPixelBuffer( + pixelBuffer: pixelBuffer, + textureFormat: .bgra8Unorm + ), + let outputTexture = makeTextureFromCVPixelBuffer( + pixelBuffer: outputBuffer, + textureFormat: .bgra8Unorm + ) + else { return } + + //设置命令队列、缓冲区和编码器。 + guard let commandQueue = commandQueue, + let commandBuffer = commandQueue.makeCommandBuffer(), + let commandEncoder = commandBuffer.makeComputeCommandEncoder() else { + print("Failed to create a Metal command queue.") + CVMetalTextureCacheFlush(textureCache!, 0) + return + } + + commandEncoder.label = "BlendGPU" + commandEncoder.setComputePipelineState(computePipelineState!) + commandEncoder.setTexture(inputTexture, index: 0) + commandEncoder.setTexture(outputTexture, index: 2) + + //设置线程组 + let width = computePipelineState!.threadExecutionWidth + let height = computePipelineState!.maxTotalThreadsPerThreadgroup / width + let threadsPerThreadgroup = MTLSizeMake(width, height, 1) + let threadgroupsPerGrid = MTLSize(width: inputTexture.width, + height: inputTexture.height, + depth: 1) + commandEncoder.dispatchThreadgroups(threadgroupsPerGrid, threadsPerThreadgroup: threadsPerThreadgroup) + commandEncoder.endEncoding() + commandBuffer.commit() + commandBuffer.waitUntilCompleted() + + guard let outputPixelBuffer else { return } + self.image = CIImage(cvPixelBuffer: outputPixelBuffer) + } + + ///将' image '绘制到这个视图上。 + /// -参数rect:绘制图像的坐标和大小。 + override func draw(_ rect: CGRect) { + guard let image = image, + let currentDrawable = currentDrawable, + let commandBuffer = commandQueue?.makeCommandBuffer() + else { + return + } + let currentTexture = currentDrawable.texture + let drawingBounds = CGRect(origin: .zero, size: drawableSize) + + let scaleX = drawableSize.width / image.extent.width + let scaleY = drawableSize.height / image.extent.height + let scaledImage = image.transformed(by: CGAffineTransform(scaleX: scaleX, y: scaleY)) + + context.render(scaledImage, to: currentTexture, commandBuffer: commandBuffer, bounds: drawingBounds, colorSpace: colorSpace) + + commandBuffer.present(currentDrawable) + commandBuffer.commit() + } + + ///从提供的' CVPixelBuffer '中创建一个' MTLTexture '用于着色器。 + /// -参数: + /// - pixelBuffer:提供的像素缓冲区转换为纹理。 + /// - textureFormat:纹理的像素格式。 + /// -返回:转换后的' MTLTexture ',如果可以创建的话。 + private func makeTextureFromCVPixelBuffer( + pixelBuffer: CVPixelBuffer, + textureFormat: MTLPixelFormat + ) -> MTLTexture? { + let width = CVPixelBufferGetWidth(pixelBuffer) + let height = CVPixelBufferGetHeight(pixelBuffer) + guard let textureCache else { return nil } + //从图像缓冲区创建一个金属纹理。 + var cvTextureOut: CVMetalTexture? + CVMetalTextureCacheCreateTextureFromImage( + kCFAllocatorDefault, + textureCache, + pixelBuffer, + nil, + textureFormat, + width, + height, + 0, + &cvTextureOut + ) + + guard let cvTextureOut, + let texture = CVMetalTextureGetTexture(cvTextureOut) + else { + CVMetalTextureCacheFlush(textureCache, 0) + return nil + } + return texture + } + + ///创建' CVPixelBufferPool '来为正在进行的转换帧写入提供像素缓冲区。 + /// -参数大小:每个像素缓冲区的大小。 + /// -返回:' CVPixelBufferPool ',如果它可以被创建。 + private func createBufferPool(size: CGSize) -> CVPixelBufferPool? { + let allocationThreshold = 5 + let sourcePixelBufferAttributesDictionary = [ + kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: kCVPixelFormatType_32BGRA), + kCVPixelBufferWidthKey as String: NSNumber(value: Float(size.width)), + kCVPixelBufferHeightKey as String: NSNumber(value: Float(size.height)), + kCVPixelBufferMetalCompatibilityKey as String: kCFBooleanTrue!, + kCVPixelBufferIOSurfacePropertiesKey as String: [ + kCVPixelBufferIOSurfaceCoreAnimationCompatibilityKey:kCFBooleanTrue, + ] + ] as [String : Any] + let poolAttributes = [kCVPixelBufferPoolMinimumBufferCountKey as String: allocationThreshold] + + var cvPixelBufferPool: CVPixelBufferPool? + CVPixelBufferPoolCreate(kCFAllocatorDefault, poolAttributes as NSDictionary?, sourcePixelBufferAttributesDictionary as NSDictionary?, &cvPixelBufferPool) + + return cvPixelBufferPool + } +} + + + + + diff --git a/tdvideo/tdvideo/PlayContoller10.swift b/tdvideo/tdvideo/PlayContoller10.swift new file mode 100644 index 0000000..3d49960 --- /dev/null +++ b/tdvideo/tdvideo/PlayContoller10.swift @@ -0,0 +1,92 @@ +// +// PlayContoller10.swift +// tdvideo +// +// Created by mac on 2024/2/22. +// + +import UIKit +import AVFoundation +import AVKit +//两个普通视频合成空间视频 +class PlayContoller10: UIViewController { + + // 左右眼视频的 URL + var leftEyeVideoURL: URL? + var rightEyeVideoURL: URL? + var outputVideoURL: URL? + + // 播放器 + var player: AVPlayer? + var playerLayer: AVPlayerLayer? + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .brown + + let path1 = Bundle.main.path(forResource: "aa", ofType: "MOV") + let path2 = Bundle.main.path(forResource: "bb", ofType: "MOV") + leftEyeVideoURL = URL(fileURLWithPath: path1!) + rightEyeVideoURL = URL(fileURLWithPath: path2!) + outputVideoURL = URL.documentsDirectory.appending(path: "output1111.MOV") + + setupUI() + } + + private func setupUI() { + let generateButton = UIButton(type: .system) + generateButton.setTitleColor(UIColor.white, for: .normal) + generateButton.setTitle("生成空间视频并保存到相册", for: .normal) + generateButton.addTarget(self, action: #selector(generateSpatialVideo), for: .touchUpInside) + generateButton.frame = CGRect(x: 50, y: 200, width: view.frame.width - 100, height: 50) + view.addSubview(generateButton) + + // 添加播放器视图 + let playerView = UIView(frame: CGRect(x: 0, y: 300, width: view.frame.width, height: 300)) + view.addSubview(playerView) + playerView.backgroundColor = UIColor.black + + + // 初始化播放器和播放器图层 + player = AVPlayer() + playerLayer = AVPlayerLayer(player: player) + playerLayer?.frame = playerView.bounds + playerLayer?.backgroundColor = UIColor.black.cgColor + playerView.layer.addSublayer(playerLayer!) + } + + @objc private func generateSpatialVideo() { + guard let leftEyeVideoURL = leftEyeVideoURL, + let rightEyeVideoURL = rightEyeVideoURL, + let outputVideoURL = outputVideoURL else { + print("Invalid video URLs") + return + } + + let spatialVideoWriter = SpatialVideoWriter() + Task { + spatialVideoWriter.writeSpatialVideo(leftEyeVideoURL: leftEyeVideoURL, rightEyeVideoURL: rightEyeVideoURL, outputVideoURL: outputVideoURL) { success, error in + if success { + print("空间视频生成成功") + self.playSpatialVideo() + } else if let error = error { + print("生成空间视频失败:\(error.localizedDescription)") + } + } + } + } + + private func playSpatialVideo() { + guard let outputVideoURL = outputVideoURL else { + print("Output video URL is nil") + return + } + + // 创建 AVPlayerItem + let playerItem = AVPlayerItem(url: outputVideoURL) + player?.replaceCurrentItem(with: playerItem) + + // 播放视频 + player?.play() + } +} diff --git a/tdvideo/tdvideo/PlayContoller11.swift b/tdvideo/tdvideo/PlayContoller11.swift new file mode 100644 index 0000000..72d0b0f --- /dev/null +++ b/tdvideo/tdvideo/PlayContoller11.swift @@ -0,0 +1,185 @@ +// +// PlayContoller11.swift +// tdvideo +// +// Created by mac on 2024/2/22. +// + + +import UIKit +import AVFoundation + +class PlayContoller11: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate { + + var session = AVCaptureMultiCamSession() + var backCameraDeviceInput: AVCaptureDeviceInput? + var frontCameraDeviceInput: AVCaptureDeviceInput? + var backCameraVideoPreviewLayer: AVCaptureVideoPreviewLayer? + var frontCameraVideoPreviewLayer: AVCaptureVideoPreviewLayer? + var backCameraVideoDataOutput: AVCaptureVideoDataOutput? + var frontCameraVideoDataOutput: AVCaptureVideoDataOutput? + var movieOutput: AVCaptureMovieFileOutput? + var isRecording = false + var recordButton:UIButton? + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .white + + self.configureSession() + self.addRecordButton() + } + + private func configureSession() { + session.beginConfiguration() + defer { + session.commitConfiguration() + } + + // 配置后置摄像头 + guard let backCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else { + print("Could not find the back camera") + return + } + + do { + backCameraDeviceInput = try AVCaptureDeviceInput(device: backCamera) + + guard let backCameraDeviceInput = backCameraDeviceInput, + session.canAddInput(backCameraDeviceInput) else { + print("Could not add back camera input") + return + } + session.addInput(backCameraDeviceInput) + } catch { + print("Could not create back camera device input: \(error)") + return + } + + // 配置前置摄像头 + guard let frontCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) else { + print("Could not find the front camera") + return + } + + do { + frontCameraDeviceInput = try AVCaptureDeviceInput(device: frontCamera) + + guard let frontCameraDeviceInput = frontCameraDeviceInput, + session.canAddInput(frontCameraDeviceInput) else { + print("Could not add front camera input") + return + } + session.addInput(frontCameraDeviceInput) + } catch { + print("Could not create front camera device input: \(error)") + return + } + + // 配置预览图层 + backCameraVideoPreviewLayer = AVCaptureVideoPreviewLayer(session: session) + backCameraVideoPreviewLayer?.frame = CGRect(x: 0, y: 0, width: view.frame.size.width / 2, height: view.frame.size.height / 2) + if let backCameraVideoPreviewLayer = backCameraVideoPreviewLayer { + view.layer.addSublayer(backCameraVideoPreviewLayer) + } + + frontCameraVideoPreviewLayer = AVCaptureVideoPreviewLayer(session: session) + frontCameraVideoPreviewLayer?.frame = CGRect(x: view.frame.size.width / 2, y: 0, width: view.frame.size.width / 2, height: view.frame.size.height / 2) + if let frontCameraVideoPreviewLayer = frontCameraVideoPreviewLayer { + view.layer.addSublayer(frontCameraVideoPreviewLayer) + } + + // 配置后置摄像头视频数据输出 + backCameraVideoDataOutput = AVCaptureVideoDataOutput() + backCameraVideoDataOutput?.setSampleBufferDelegate(self, queue: DispatchQueue(label: "backCameraVideoDataOutputQueue")) + if let backCameraVideoDataOutput = backCameraVideoDataOutput, + session.canAddOutput(backCameraVideoDataOutput) { + session.addOutput(backCameraVideoDataOutput) + } + + // 配置前置摄像头视频数据输出 + frontCameraVideoDataOutput = AVCaptureVideoDataOutput() + frontCameraVideoDataOutput?.setSampleBufferDelegate(self, queue: DispatchQueue(label: "frontCameraVideoDataOutputQueue")) + if let frontCameraVideoDataOutput = frontCameraVideoDataOutput, + session.canAddOutput(frontCameraVideoDataOutput) { + session.addOutput(frontCameraVideoDataOutput) + } + + // 配置电影输出 + movieOutput = AVCaptureMovieFileOutput() + if let movieOutput = movieOutput, session.canAddOutput(movieOutput) { + session.addOutput(movieOutput) + } + + DispatchQueue.global().async { + self.session.startRunning() + } + } + + private func addRecordButton() { + recordButton = UIButton(type: .system) + recordButton!.setTitle("开始录制", for: .normal) + recordButton!.addTarget(self, action: #selector(toggleRecording), for: .touchUpInside) + recordButton!.frame = CGRect(x: 50, y: view.frame.height - 200, width: 200, height: 50) + view.addSubview(recordButton!) + } + + @objc private func toggleRecording() { + if isRecording { + stopRecording() + } else { + startRecording() + } + } + + private func startRecording() { + guard let movieOutput = movieOutput else { return } + + let outputURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("output.mov") + movieOutput.startRecording(to: outputURL, recordingDelegate: self) + isRecording = true + recordButton!.setTitle("停止录制", for: .normal) + } + + private func stopRecording() { + guard let movieOutput = movieOutput else { return } + + movieOutput.stopRecording() + isRecording = false + recordButton!.setTitle("开始录制", for: .normal) + } + + func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { + // 获取视频数据 + guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { + return + } + + // 判断是前置摄像头还是后置摄像头 + if isRecording { + if connection.inputPorts.contains(where: { $0.mediaType == .video && $0.sourceDeviceType == .builtInWideAngleCamera && $0.sourceDevicePosition == .back }) { + print("后置摄像头视频数据") + } else if connection.inputPorts.contains(where: { $0.mediaType == .video && $0.sourceDeviceType == .builtInWideAngleCamera && $0.sourceDevicePosition == .front }) { + print("前置摄像头视频数据") + } + } + + // 在这里进行视频数据的处理,例如打印像素数据等 + // 注意:在这里处理视频数据会影响性能,请谨慎使用 + } + +} + +extension PlayContoller11: AVCaptureFileOutputRecordingDelegate { + func fileOutput(_ output: AVCaptureFileOutput, didStartRecordingTo fileURL: URL, from connections: [AVCaptureConnection]) { + print("开始录制") + } + + func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) { + if let error = error { + print("录制出错:\(error.localizedDescription)") + } else { + print("录制完成:\(outputFileURL)") + } + } +} diff --git a/tdvideo/tdvideo/PlayContoller4.swift b/tdvideo/tdvideo/PlayContoller4.swift new file mode 100644 index 0000000..cf94c35 --- /dev/null +++ b/tdvideo/tdvideo/PlayContoller4.swift @@ -0,0 +1,285 @@ +// +// PlayContoller4.swift +// tdvideo +// +// Created by mac on 2024/2/1. +// + + +import AVKit +import UIKit + +//class PlayContoller4: UIViewController { +// +// private var playerViewController: AVPlayerViewController? +// private var player: AVPlayer? +// +// var link = false +// var play = false +// var btn:UIButton? +// +// override func viewDidLoad() { +// super.viewDidLoad() +// view.backgroundColor = UIColor.brown +// +// +// +// btn = UIButton(type: UIButton.ButtonType.custom) +// self.view.addSubview(btn!) +// btn!.frame = CGRect(x: 100, y: 100, width: 200, height: 50) +// btn!.setTitle("未连接", for: UIControl.State.normal) +// btn!.addTarget(self, action: #selector(buttonPressed(sender:)), for: UIControl.Event.touchUpInside) +// +// // 创建 AVPlayerViewController +// playerViewController = AVPlayerViewController() +// playerViewController?.view.frame = CGRect(x: 0, y: 200, width: self.view.frame.size.width, height: 350) +// addChild(playerViewController!) +// view.addSubview(playerViewController!.view) +// +// // 创建 AVPlayer +// guard let videoURL = Bundle.main.url(forResource: "IMG_0071", withExtension: "MOV") else { +// print("无法加载视频文件") +// return +// } +// player = AVPlayer(url: videoURL) +// playerViewController?.player = player +// player?.play() +// +// // 监听 UIScreenMirroringDidChange 通知 +// NotificationCenter.default.addObserver(self, selector: #selector(screenMirroringChanged(_:)), name: UIScreen.didConnectNotification, object: nil) +// NotificationCenter.default.addObserver(self, selector: #selector(screenMirroringChanged(_:)), name: UIScreen.didDisconnectNotification, object: nil) +// +// +// // 监听 AirPlay 设备的连接状态 +//// NotificationCenter.default.addObserver(self, selector: #selector(airPlayStatusDidChange(_:)), name: AVAudioSession.routeChangeNotification, object: nil) +// +// // 获取当前投屏状态 +// let isScreenMirroring = UIScreen.screens.count > 1 +// print("Screen Mirroring: \(isScreenMirroring)") +// setttinisScreenMirroring(isScreenMirroring: isScreenMirroring) +// } +// +// @objc private func screenMirroringChanged(_ notification: NSNotification) { +// // 投屏状态发生变化 +// let isScreenMirroring = UIScreen.screens.count > 1 +// print("Screen Mirroring: \(isScreenMirroring)") +// setttinisScreenMirroring(isScreenMirroring: isScreenMirroring) +// } +// +// func setttinisScreenMirroring(isScreenMirroring:Bool){ +// +// //已连接 +// if(isScreenMirroring){ +// link = true +//// let currentRoute = AVAudioSession.sharedInstance().currentRoute +//// let isAirPlayActive = currentRoute.outputs.contains { output in +//// return output.portType == AVAudioSession.Port.airPlay +//// } +// if(play == true){ +// player!.usesExternalPlaybackWhileExternalScreenIsActive = true +// player!.allowsExternalPlayback = true +// btn!.setTitle("串流中", for: UIControl.State.normal) +// }else{ +// player!.usesExternalPlaybackWhileExternalScreenIsActive = false +// player!.allowsExternalPlayback = false +// btn!.setTitle("已连接", for: UIControl.State.normal) +// } +// } +// +// //未连接 +// else{ +// link = false +// // 当前未连接到 AirPlay 设备 +// player!.usesExternalPlaybackWhileExternalScreenIsActive = false +// player!.allowsExternalPlayback = false +// btn!.setTitle("未连接", for: UIControl.State.normal) +// } +// } +// +// @objc private func airPlayStatusDidChange(_ notification: Notification) { +// print("设备连接发生变化") +// } +// +// deinit { +// NotificationCenter.default.removeObserver(self) +// } +// +// @objc func buttonPressed(sender:UIButton){ +// +// if(link == true){ +// play = !play +// if(play == true){ +// // 当前已连接到 AirPlay 设备 +// player!.usesExternalPlaybackWhileExternalScreenIsActive = true +// player!.allowsExternalPlayback = true +// btn!.setTitle("在串流", for: UIControl.State.normal) +// }else{ +// player!.usesExternalPlaybackWhileExternalScreenIsActive = false +// player!.allowsExternalPlayback = false +// btn!.setTitle("已连接", for: UIControl.State.normal) +// } +// } +// } +//} + + + +class PlayContoller4: UIViewController { + private var playerViewController: AVPlayerViewController? + private var player: AVPlayer? + + var link = false + var play = false + var btn:UIButton? + override func viewDidLoad() { + super.viewDidLoad() + self.view.backgroundColor = UIColor.brown + + btn = UIButton(type: UIButton.ButtonType.custom) + self.view.addSubview(btn!) + btn!.frame = CGRect(x: 100, y: 100, width: 200, height: 50) + btn!.setTitle("未连接设备", for: UIControl.State.normal) + btn!.addTarget(self, action: #selector(buttonPressed(sender:)), for: UIControl.Event.touchUpInside) + btn!.layer.borderWidth = 1 + btn!.layer.borderColor = UIColor.white.cgColor + + // 创建 AVPlayerViewController + playerViewController = AVPlayerViewController() + playerViewController?.view.frame = CGRect(x: 0, y: 200, width: self.view.frame.size.width, height: 350) + addChild(playerViewController!) + view.addSubview(playerViewController!.view) + + // 创建 AVPlayer + guard let videoURL = Bundle.main.url(forResource: "IMG_0071", withExtension: "MOV") else { + print("无法加载视频文件") + return + } + player = AVPlayer(url: videoURL) + playerViewController?.player = player + + // 监听 AirPlay 设备的连接状态 + NotificationCenter.default.addObserver(self, selector: #selector(airPlayStatusDidChange(_:)), name: AVAudioSession.routeChangeNotification, object: nil) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + // 检查当前是否已连接到 AirPlay 设备 + checkAirPlayStatus() + } + + @objc private func airPlayStatusDidChange(_ notification: Notification) { + checkAirPlayStatus() + } + + private func checkAirPlayStatus() { + print("设备连接变化") + let currentRoute = AVAudioSession.sharedInstance().currentRoute + let isAirPlayActive = currentRoute.outputs.contains { output in + return output.portType == AVAudioSession.Port.HDMI || + output.portType == AVAudioSession.Port.airPlay + } + + setttinisScreenMirroring(isScreenMirroring: isAirPlayActive) + } + + func setttinisScreenMirroring(isScreenMirroring:Bool){ + + //已连接 + if(isScreenMirroring){ + print("已连接") + link = true + play = true + player!.usesExternalPlaybackWhileExternalScreenIsActive = true + player!.allowsExternalPlayback = true + btn!.setTitle("串流播放中", for: UIControl.State.normal) + player!.play() + } + + //未连接 + else{ + print("未连接") + link = false + play = false + // 当前未连接到 AirPlay 设备 + player!.usesExternalPlaybackWhileExternalScreenIsActive = false + player!.allowsExternalPlayback = false + btn!.setTitle("未连接设备", for: UIControl.State.normal) + } + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + @objc func buttonPressed(sender:UIButton){ + + if(link == true){ + play = !play + if(play == true){ + // 当前已连接到 AirPlay 设备 + player!.usesExternalPlaybackWhileExternalScreenIsActive = true + player!.allowsExternalPlayback = true + btn!.setTitle("串流播放中", for: UIControl.State.normal) + }else{ + player!.usesExternalPlaybackWhileExternalScreenIsActive = false + player!.allowsExternalPlayback = false + btn!.setTitle("已连接设备", for: UIControl.State.normal) + } + } + } +} + + + + + +//import UIKit +//import ExternalAccessory +// +//class PlayContoller4: UIViewController { +// +// +// override func viewDidLoad() { +// super.viewDidLoad() +// self.view.backgroundColor = UIColor.brown +// +// +// NotificationCenter.default.addObserver(self, selector: #selector(didConnectAccessory(_:)), name: Notification.Name.EAAccessoryDidConnect, object: nil) +// NotificationCenter.default.addObserver(self, selector: #selector(didDisconnectAccessory(_:)), name: Notification.Name.EAAccessoryDidDisconnect, object: nil) +// EAAccessoryManager.shared().registerForLocalNotifications() +// print(EAAccessoryManager.shared().connectedAccessories) +// } +// +// +// @objc +// private func didConnectAccessory(_ notification: NSNotification) { +// let accessoryManager = EAAccessoryManager.shared() +// for accessory in accessoryManager.connectedAccessories { +// print(accessory.protocolStrings) +// if accessory.protocolStrings.contains("") { +// //We have found the accessory corresponding to our gadget +// let description = """ +// Accessory name: \(accessory.name) +// Manufacturer: \(accessory.manufacturer) +// Model number: \(accessory.modelNumber) +// Serial number: \(accessory.serialNumber) +// HW Revision: \(accessory.hardwareRevision) +// FW Revision: \(accessory.firmwareRevision) +// Connected: \(accessory.isConnected) +// Connection ID: \(accessory.connectionID) +// Protocol strings: \(accessory.protocolStrings.joined(separator: "; ")) +// """ +// print(description) +// +// } +// +//// self.accessory = accessory +// +// } +// } +// +// @objc +// private func didDisconnectAccessory(_ notification: NSNotification) { +// print("disconnect") +// } +//} diff --git a/tdvideo/tdvideo/PlayContoller5.swift b/tdvideo/tdvideo/PlayContoller5.swift new file mode 100644 index 0000000..4ada28b --- /dev/null +++ b/tdvideo/tdvideo/PlayContoller5.swift @@ -0,0 +1,401 @@ +// +// PlayContoller5.swift +// tdvideo +// +// Created by mac on 2024/2/2. +// + + +import UIKit +import AVFoundation +import Photos + +class PlayContoller5: UIViewController, AVCaptureFileOutputRecordingDelegate { + + var session = AVCaptureMultiCamSession() + var backCameraDeviceInput: AVCaptureDeviceInput? + var frontCameraDeviceInput: AVCaptureDeviceInput? + var backCameraVideoPreviewLayer: AVCaptureVideoPreviewLayer? + var frontCameraVideoPreviewLayer: AVCaptureVideoPreviewLayer? + var startRecordingButton: UIButton? + var backCameraMovieOutput: AVCaptureMovieFileOutput? + var frontCameraMovieOutput: AVCaptureMovieFileOutput? + var isRecording = false + + var imgs:NSMutableArray = NSMutableArray() + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .white + + imgs.removeAllObjects() + configureSession() + setupUI() + } + + private func configureSession() { + session.beginConfiguration() + defer { + session.commitConfiguration() + } + + //builtInWideAngleCamera + //builtInWideAngleCamera + //builtInTrueDepthCamera + // 配置后置摄像头 + guard let backCamera = AVCaptureDevice.default(.builtInUltraWideCamera, for: .video, position: .back) else { + print("Could not find the back camera") + return + } + + do { + backCameraDeviceInput = try AVCaptureDeviceInput(device: backCamera) + + guard let backCameraDeviceInput = backCameraDeviceInput, + session.canAddInput(backCameraDeviceInput) else { + print("Could not add back camera input") + return + } + session.addInput(backCameraDeviceInput) + } catch { + print("Could not create back camera device input: \(error)") + return + } + + // 配置前置摄像头 + guard let frontCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else { + print("Could not find the main camera") + return + } + + do { + frontCameraDeviceInput = try AVCaptureDeviceInput(device: frontCamera) + + guard let frontCameraDeviceInput = frontCameraDeviceInput, + session.canAddInput(frontCameraDeviceInput) else { + print("Could not add front camera input") + return + } + session.addInput(frontCameraDeviceInput) + } catch { + print("Could not create front camera device input: \(error)") + return + } + + // 配置音频输入设备 + guard let audioDevice = AVCaptureDevice.default(for: .audio) else { + print("Could not find audio device") + return + } + + do { + let audioDeviceInput = try AVCaptureDeviceInput(device: audioDevice) + + guard session.canAddInput(audioDeviceInput) else { + print("Could not add audio input") + return + } + + session.addInput(audioDeviceInput) + } catch { + print("Could not create audio device input: \(error)") + return + } + + // 配置后置摄像头输出 + backCameraMovieOutput = AVCaptureMovieFileOutput() + guard let backCameraMovieOutput = backCameraMovieOutput, + session.canAddOutput(backCameraMovieOutput) else { + print("Could not add the back camera movie output") + return + } + session.addOutput(backCameraMovieOutput) + + // 配置前置摄像头输出 + frontCameraMovieOutput = AVCaptureMovieFileOutput() + guard let frontCameraMovieOutput = frontCameraMovieOutput, + session.canAddOutput(frontCameraMovieOutput) else { + print("Could not add the front camera movie output") + return + } + session.addOutput(frontCameraMovieOutput) + + // 配置预览图层 + backCameraVideoPreviewLayer = AVCaptureVideoPreviewLayer(session: session) + backCameraVideoPreviewLayer?.frame = CGRect(x: 0, y: 0, width: view.frame.size.width / 2, height: view.frame.size.height / 2) + if let backCameraVideoPreviewLayer = backCameraVideoPreviewLayer { + view.layer.addSublayer(backCameraVideoPreviewLayer) + } + + frontCameraVideoPreviewLayer = AVCaptureVideoPreviewLayer(session: session) + frontCameraVideoPreviewLayer?.frame = CGRect(x: view.frame.size.width / 2, y: 0, width: view.frame.size.width / 2, height: view.frame.size.height / 2) + if let frontCameraVideoPreviewLayer = frontCameraVideoPreviewLayer { + view.layer.addSublayer(frontCameraVideoPreviewLayer) + } + + DispatchQueue.global().async { + self.session.startRunning() + } + } + + private func setupUI() { + startRecordingButton = UIButton(type: .system) + startRecordingButton?.setTitle("Start Recording", for: .normal) + startRecordingButton?.setTitleColor(UIColor.brown, for: UIControl.State.normal) + startRecordingButton?.addTarget(self, action: #selector(toggleRecording(_:)), for: .touchUpInside) + startRecordingButton?.frame = CGRect(x: 0, y: view.frame.size.height - 250, width: view.frame.size.width, height: 50) + view.addSubview(startRecordingButton!) + } + + @objc private func toggleRecording(_ sender: UIButton) { + startRecording() + } + + private func startRecording() { + + imgs.removeAllObjects() + guard let backCameraMovieOutput = backCameraMovieOutput, + let frontCameraMovieOutput = frontCameraMovieOutput else { + print("Movie output not configured") + return + } + + //快门声音 + let soundID: SystemSoundID = 1108 + AudioServicesPlaySystemSound(soundID) + + let time = Date().timeIntervalSince1970 + let name1 = "back" + String(time) + ".mov" + let name2 = "front" + String(time) + ".mov" + let backCameraOutputURL = URL.documentsDirectory.appending(path:name1) + let frontCameraOutputURL = URL.documentsDirectory.appending(path:name2) + + backCameraMovieOutput.startRecording(to: backCameraOutputURL, recordingDelegate: self) + frontCameraMovieOutput.startRecording(to: frontCameraOutputURL, recordingDelegate: self) + + //摄像头录像,取0.1秒长度,如果是拍摄照片去两张,拍摄成功会听到两次声音,所以这里用摄像的方式取图片 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.stopRecording() + } + } + + private func stopRecording() { + guard let backCameraMovieOutput = backCameraMovieOutput, + let frontCameraMovieOutput = frontCameraMovieOutput else { + print("Movie output not configured") + return + } + + backCameraMovieOutput.stopRecording() + frontCameraMovieOutput.stopRecording() + } + + func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) { + if let error = error { + print("Video recording finished with error: \(error.localizedDescription)") + } else { + if output == backCameraMovieOutput { + print("Back camera video recorded: \(outputFileURL)") + if let firstFrame = self.firstFrame(from: outputFileURL) { +// self.saveImageToLibrary(image: firstFrame) + imgs.add(firstFrame) + } + } else if output == frontCameraMovieOutput { + print("Front camera video recorded: \(outputFileURL)") + if let firstFrame = self.firstFrame(from: outputFileURL) { +// self.saveImageToLibrary(image: firstFrame) + imgs.add(firstFrame) + } + } + if(imgs.count == 2){ + bbb() + } + } + } + + private func firstFrame(from videoURL: URL) -> UIImage? { + let asset = AVURLAsset(url: videoURL) + let generator = AVAssetImageGenerator(asset: asset) + generator.appliesPreferredTrackTransform = true + let time = CMTime(seconds: 0.0, preferredTimescale: 1) + do { + let cgImage = try generator.copyCGImage(at: time, actualTime: nil) + return UIImage(cgImage: cgImage) + } catch { + print("Error generating first frame: \(error.localizedDescription)") + return nil + } + } + + private func saveImageToLibrary(image: UIImage) { + PHPhotoLibrary.shared().performChanges({ + PHAssetChangeRequest.creationRequestForAsset(from: image) + }) { success, error in + if success { + print("Image saved to library") + } else if let error = error { + print("Error saving image to library: \(error.localizedDescription)") + } + } + } + + + func bbb(){ + + let img1:UIImage = imgs[0] as! UIImage + let img2:UIImage = imgs[1] as! UIImage + + let imageSize1 = CGRect(x: 0, y: 0, width: img1.cgImage!.width, height: img1.cgImage!.height) + let imageSize2 = CGRect(x: 0, y: 0, width: img2.cgImage!.width, height: img2.cgImage!.height) + + let url = URL.documentsDirectory.appending(path:"aaa12.HEIC") + + let destination = CGImageDestinationCreateWithURL(url as CFURL, UTType.heic.identifier as CFString, 2, nil)! + + let properties1 = [ + kCGImagePropertyGroups: [ + kCGImagePropertyGroupIndex: 0, + kCGImagePropertyGroupType: kCGImagePropertyGroupTypeStereoPair, + kCGImagePropertyGroupImageIndexLeft: 0, + kCGImagePropertyGroupImageIndexRight: 1, + ], + kCGImagePropertyHEIFDictionary: [ + kIIOMetadata_CameraExtrinsicsKey: [ + kIIOCameraExtrinsics_CoordinateSystemID: 0, + kIIOCameraExtrinsics_Position: [ + 0, + 0, + 0 + ], + kIIOCameraExtrinsics_Rotation: [ + 1, 0, 0, + 0, 1, 0, + 0, 0, 1 + ] + ] + ] + ] + + let properties2 = [ + kCGImagePropertyGroups: [ + kCGImagePropertyGroupIndex: 0, + kCGImagePropertyGroupType: kCGImagePropertyGroupTypeStereoPair, + kCGImagePropertyGroupImageIndexLeft: 0, + kCGImagePropertyGroupImageIndexRight: 1, + ], + kCGImagePropertyHEIFDictionary: [ + kIIOMetadata_CameraExtrinsicsKey: [ + kIIOCameraExtrinsics_CoordinateSystemID: 0, + kIIOCameraExtrinsics_Position: [ + -0.019238, + 0, + 0 + ], + kIIOCameraExtrinsics_Rotation: [ + 1, 0, 0, + 0, 1, 0, + 0, 0, 1 + ] + ] + ] + ] + + let leftImg = fixOrientation(img1) + let rightImg = fixOrientation(img2) + + CGImageDestinationAddImage(destination, leftImg.cgImage!, properties1 as CFDictionary) + CGImageDestinationAddImage(destination, rightImg.cgImage!, properties2 as CFDictionary) + CGImageDestinationFinalize(destination) + + let image = UIImage(contentsOfFile: url.path()) + let source = CGImageSourceCreateWithURL(url as CFURL, nil)! + guard let properties22 = CGImageSourceCopyPropertiesAtIndex(source, 1, nil) as? [CFString: Any] else { + return + } + print(properties22) + + savePhoto(url) + + } + + func savePhoto(_ fileURL: URL) { + + // 创建 PHAssetCreationRequest + PHPhotoLibrary.shared().performChanges({ + let creationRequest = PHAssetCreationRequest.forAsset() + creationRequest.addResource(with: .photoProxy, fileURL: fileURL, options: nil) + + }) { success, error in + if let error = error { + print("Error saving photo to library: \(error.localizedDescription)") + } else { + print("Photo saved to library successfully.") + } + } + } + + //修正图片的方向 + func fixOrientation(_ image: UIImage) -> UIImage { + // No-op if the orientation is already correct + guard image.imageOrientation != .up else { return image } + + // We need to calculate the proper transformation to make the image upright. + // We do it in 2 steps: Rotate if Left/Right/Down, and then flip if Mirrored. + var transform = CGAffineTransform.identity + + switch image.imageOrientation { + case .down, .downMirrored: + transform = transform.translatedBy(x: image.size.width, y: image.size.height) + transform = transform.rotated(by: .pi) + case .left, .leftMirrored: + transform = transform.translatedBy(x: image.size.width, y: 0) + transform = transform.rotated(by: .pi / 2) + case .right, .rightMirrored: + transform = transform.translatedBy(x: 0, y: image.size.height) + transform = transform.rotated(by: -.pi / 2) + default: + break + } + + switch image.imageOrientation { + case .upMirrored, .downMirrored: + transform = transform.translatedBy(x: image.size.width, y: 0) + transform = transform.scaledBy(x: -1, y: 1) + case .leftMirrored, .rightMirrored: + transform = transform.translatedBy(x: image.size.height, y: 0) + transform = transform.scaledBy(x: -1, y: 1) + default: + break + } + + // Now we draw the underlying CGImage into a new context, applying the transform + // calculated above. + guard let cgImage = image.cgImage, + let colorSpace = cgImage.colorSpace, + let context = CGContext(data: nil, + width: Int(image.size.width), + height: Int(image.size.height), + bitsPerComponent: cgImage.bitsPerComponent, + bytesPerRow: 0, + space: colorSpace, + bitmapInfo: cgImage.bitmapInfo.rawValue) + else { + return image + } + + context.concatenate(transform) + + switch image.imageOrientation { + case .left, .leftMirrored, .right, .rightMirrored: + // Grr... + context.draw(cgImage, in: CGRect(x: 0, y: 0, width: image.size.height, height: image.size.width)) + default: + context.draw(cgImage, in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)) + } + + // And now we just create a new UIImage from the drawing context + guard let cgImageFixed = context.makeImage() else { return image } + let fixedImage = UIImage(cgImage: cgImageFixed) + return fixedImage + } +} diff --git a/tdvideo/tdvideo/PlayContoller6.swift b/tdvideo/tdvideo/PlayContoller6.swift new file mode 100644 index 0000000..e07167e --- /dev/null +++ b/tdvideo/tdvideo/PlayContoller6.swift @@ -0,0 +1,160 @@ +// +// PlayContoller6.swift +// tdvideo +// +// Created by mac on 2024/2/4. +// + +import UIKit +import AVFoundation +import CoreImage +import Foundation +import Observation +import VideoToolbox + + +class PlayContoller6: UIViewController { + private var player: AVPlayer! + private var playerLayer: AVPlayerLayer! + private var exportButton: UIButton! + + + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = UIColor.brown + setupPlayer() + setupExportButton() + } + + private func setupPlayer() { + + let path = Bundle.main.path(forResource: "IMG_0071", ofType: "MOV") + let videoURL = URL.init(filePath: path!) + let playerItem = AVPlayerItem(url: videoURL) + player = AVPlayer(playerItem: playerItem) + playerLayer = AVPlayerLayer(player: player) + playerLayer.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height - 100) + view.layer.addSublayer(playerLayer) + player.play() + } + + private func setupExportButton() { + exportButton = UIButton(type: .system) + exportButton.setTitleColor(UIColor.white, for: UIControl.State.normal) + exportButton.setTitle("导出", for: .normal) + exportButton.frame = CGRect(x: 0, y: view.bounds.height - 260, width: view.bounds.width, height: 100) + exportButton.addTarget(self, action: #selector(exportButtonTapped), for: .touchUpInside) + view.addSubview(exportButton) + } + + @objc private func exportButtonTapped() { + let path = Bundle.main.path(forResource: "IMG_0071", ofType: "MOV") + let videoURL = URL.init(filePath: path!) + let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + let outputURL = documentsDirectory.appendingPathComponent("output_path.mp4") + + do { + try FileManager.default.removeItem(atPath: outputURL.path) + print("视频文件删除成功") + } catch { + print("删除视频文件出错:\(error)") + } + + let width = 1280 + let height = 720 + let dataRate = 5000000 + let horizontalDisparity: Float = 0.0 + let horizontalFieldOfView: Float = 90.0 + + exportVideo(url: videoURL, outputURL: outputURL, width: width, height: height, dataRate: dataRate, horizontalDisparity: horizontalDisparity, horizontalFieldOfView: horizontalFieldOfView) { exportedAsset in + // 处理导出的新AVAsset对象 + // 在这里可以进行进一步的操作,比如播放导出的视频 + DispatchQueue.main.async { + let exportedPlayerItem = AVPlayerItem(asset: exportedAsset!) + self.player.replaceCurrentItem(with: exportedPlayerItem) + self.player.play() + } + } + } + + private func exportVideo(url: URL, outputURL: URL, width: Int, height: Int, dataRate: Int, horizontalDisparity: Float, horizontalFieldOfView: Float, completion: @escaping (AVAsset?) -> Void) { + let asset = AVAsset(url: url) + + guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetPassthrough) else { + print("Failed to create export session") + completion(nil) + return + } + + let composition = AVMutableComposition() + let videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) + + guard let assetTrack = asset.tracks(withMediaType: .video).first else { + print("Failed to get video track from asset") + completion(nil) + return + } + + do { + try videoTrack?.insertTimeRange(CMTimeRange(start: .zero, duration: asset.duration), of: assetTrack, at: .zero) + } catch { + print("Failed to insert video track into composition") + completion(nil) + return + } + + var videoSettings = AVOutputSettingsAssistant(preset: .mvhevc1440x1440)?.videoSettings + videoSettings?[AVVideoWidthKey] = width + videoSettings?[AVVideoHeightKey] = height + var compressionProperties = videoSettings?[AVVideoCompressionPropertiesKey] as! [String: Any] + compressionProperties[AVVideoAverageBitRateKey] = dataRate + compressionProperties[kVTCompressionPropertyKey_HorizontalDisparityAdjustment as String] = horizontalDisparity + compressionProperties[kCMFormatDescriptionExtension_HorizontalFieldOfView as String] = horizontalFieldOfView + compressionProperties[kCMFormatDescriptionExtension_HorizontalFieldOfView as String] = 180.0 + + videoSettings?[AVVideoCompressionPropertiesKey] = compressionProperties + + let writerInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings) + writerInput.expectsMediaDataInRealTime = false + + guard let writer = try? AVAssetWriter(outputURL: outputURL, fileType: AVFileType.mp4) else { + print("Failed to create AVAssetWriter") + completion(nil) + return + } + + writer.add(writerInput) + + guard let assetExportSession = exportSession as? AVAssetExportSession else { + print("Failed to cast export session to AVAssetExportSession") + completion(nil) + return + } + + assetExportSession.videoComposition = AVMutableVideoComposition(propertiesOf: composition) + assetExportSession.outputFileType = AVFileType.mp4 + assetExportSession.outputURL = outputURL + + assetExportSession.exportAsynchronously { + switch assetExportSession.status { + case .completed: + let exportedAsset = AVAsset(url: outputURL) + completion(exportedAsset) + case .failed: + if let error = assetExportSession.error { + print("Export failed with error: \(error.localizedDescription)") + } else { + print("Export failed") + } + completion(nil) + case .cancelled: + print("Export cancelled") + completion(nil) + default: + break + } + } + } + +} diff --git a/tdvideo/tdvideo/PlayContoller7.swift b/tdvideo/tdvideo/PlayContoller7.swift new file mode 100644 index 0000000..e7983f8 --- /dev/null +++ b/tdvideo/tdvideo/PlayContoller7.swift @@ -0,0 +1,313 @@ +// +// PlayContoller7.swift +// tdvideo +// +// Created by mac on 2024/2/10. +// + +import UIKit +import AVFoundation +import MobileCoreServices +import CoreImage +import ImageIO +import Photos + +class PlayContoller7: UIViewController { + + var startRecordingButton:UIButton? + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = UIColor.brown + + setupUI() +// bbb() +// aaa() + } + + private func setupUI() { + startRecordingButton = UIButton(type: .system) + startRecordingButton?.setTitle("开始合成空间图片", for: .normal) + startRecordingButton?.setTitleColor(UIColor.white, for: UIControl.State.normal) + startRecordingButton?.addTarget(self, action: #selector(toggleRecording(_:)), for: .touchUpInside) + startRecordingButton?.frame = CGRect(x: 0, y: view.frame.size.height - 250, width: view.frame.size.width, height: 50) + view.addSubview(startRecordingButton!) + } + + @objc private func toggleRecording(_ sender: UIButton) { + + bbb() + } + + func bbb(){ + let img1:UIImage = UIImage(named: "a.HEIC")! + let img2:UIImage = UIImage(named: "b.HEIC")! + + let imageSize1 = CGRect(x: 0, y: 0, width: img1.cgImage!.width, height: img1.cgImage!.height) + let imageSize2 = CGRect(x: 0, y: 0, width: img2.cgImage!.width, height: img2.cgImage!.height) + + let url = URL.documentsDirectory.appending(path:"aaa12.HEIC") + + let destination = CGImageDestinationCreateWithURL(url as CFURL, UTType.heic.identifier as CFString, 2, nil)! + + //左眼 --- 参数大于16.0 --- 配置普通图片转成左眼空间图片的参数 + /* + [ColorModel: RGB, ProfileName: sRGB IEC61966-2.1, Depth: 8, {TIFF}: { + Orientation = 1; + TileLength = 512; + TileWidth = 512; + }, {HEIF}: { + CameraExtrinsics = { + CoordinateSystemID = 0; + Position = ( + "-0.019238", + 0, + 0 + ); + Rotation = ( + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1 + ); + }; + }, PixelWidth: 4032, PixelHeight: 3024, Orientation: 1] + + */ + + let properties1 = [ + kCGImagePropertyGroups: [ + kCGImagePropertyGroupIndex: 0, + kCGImagePropertyGroupType: kCGImagePropertyGroupTypeStereoPair, + kCGImagePropertyGroupImageIndexLeft: 0, + kCGImagePropertyGroupImageIndexRight: 1, + + ], + kCGImagePropertyHEIFDictionary: [ + kIIOMetadata_CameraExtrinsicsKey: [ + kIIOCameraExtrinsics_CoordinateSystemID: 0, + kIIOCameraExtrinsics_Position: [ + 0, + 0, + 0 + ], + kIIOCameraExtrinsics_Rotation: [ + 1, 0, 0, + 0, 1, 0, + 0, 0, 1 + ] + ] + ] + ] + //右眼 + let properties2 = [ + kCGImagePropertyGroups: [ + kCGImagePropertyGroupIndex: 0, + kCGImagePropertyGroupType: kCGImagePropertyGroupTypeStereoPair, + kCGImagePropertyGroupImageIndexLeft: 0, + kCGImagePropertyGroupImageIndexRight: 1, + ], + kCGImagePropertyHEIFDictionary: [ + kIIOMetadata_CameraExtrinsicsKey: [ + kIIOCameraExtrinsics_CoordinateSystemID: 0, + kIIOCameraExtrinsics_Position: [ + -0.019238, + 0, + 0 + ], + kIIOCameraExtrinsics_Rotation: [ + 1, 0, 0, + 0, 1, 0, + 0, 0, 1 + ] + ] + ] + ] + + let leftImg = fixOrientation(img1) + let rightImg = fixOrientation(img2) + + CGImageDestinationAddImage(destination, leftImg.cgImage!, properties1 as CFDictionary) + CGImageDestinationAddImage(destination, rightImg.cgImage!, properties2 as CFDictionary) + CGImageDestinationFinalize(destination) + + let image = UIImage(contentsOfFile: url.path()) + let source = CGImageSourceCreateWithURL(url as CFURL, nil)! + guard let properties22 = CGImageSourceCopyPropertiesAtIndex(source, 1, nil) as? [CFString: Any] else { + return + } + print(properties22) + + savePhoto(url) + + } + + func aaa() { + let imageSize = CGRect(x: 0, y: 0, width: 3072, height: 3072) + let leftImage = CIContext().createCGImage(.red, from: imageSize)! + let rightImage = CIContext().createCGImage(.blue, from: imageSize)! + + let url = URL.documentsDirectory.appendingPathComponent("33122.HEIC") + print("URL: \(url)") + + let destination = CGImageDestinationCreateWithURL(url as CFURL, UTType.heic.identifier as CFString, 2, nil)! + + let properties1 = [ + kCGImagePropertyGroups: [ + kCGImagePropertyGroupIndex: 0, + kCGImagePropertyGroupType: kCGImagePropertyGroupTypeStereoPair, + kCGImagePropertyGroupImageIndexLeft: 0, + kCGImagePropertyGroupImageIndexRight: 1 + ], + kCGImagePropertyHEIFDictionary: [ + kIIOMetadata_CameraExtrinsicsKey: [ + kIIOCameraExtrinsics_CoordinateSystemID: 0, + kIIOCameraExtrinsics_Position: [ + 0, + 0, + 0 + ], + kIIOCameraExtrinsics_Rotation: [ + 1, 0, 0, + 0, 1, 0, + 0, 0, 1 + ] + ] + ] + ] + + let properties2 = [ + kCGImagePropertyGroups: [ + kCGImagePropertyGroupIndex: 0, + kCGImagePropertyGroupType: kCGImagePropertyGroupTypeStereoPair, + kCGImagePropertyGroupImageIndexLeft: 0, + kCGImagePropertyGroupImageIndexRight: 1 + + ], + kCGImagePropertyHEIFDictionary: [ + kIIOMetadata_CameraExtrinsicsKey: [ + kIIOCameraExtrinsics_CoordinateSystemID: 0, + kIIOCameraExtrinsics_Position: [ + -0.019238, + 0, + 0 + ], + kIIOCameraExtrinsics_Rotation: [ + 1, 0, 0, + 0, 1, 0, + 0, 0, 1 + ] + ] + ] + ] + + //图片合成的方向回正 + CGImageDestinationAddImage(destination, leftImage, properties1 as CFDictionary) + CGImageDestinationAddImage(destination, rightImage, properties2 as CFDictionary) + CGImageDestinationFinalize(destination) + + // 检查 imageSource 是否为 nil + guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil) else { + print("Failed to create CGImageSource") + return + } + + // 使用可选绑定来安全地解包 properties + if let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any] { + print(properties) + } else { + print("Failed to retrieve properties") + } + + let image = UIImage(contentsOfFile: url.path) + savePhoto(url) + } + + func savePhoto(_ fileURL: URL) { + + // 创建 PHAssetCreationRequest + PHPhotoLibrary.shared().performChanges({ + let creationRequest = PHAssetCreationRequest.forAsset() + creationRequest.addResource(with: .photoProxy, fileURL: fileURL, options: nil) + + }) { success, error in + if let error = error { + print("Error saving photo to library: \(error.localizedDescription)") + } else { + print("Photo saved to library successfully.") + } + } + } + + //修正图片的方向 + func fixOrientation(_ image: UIImage) -> UIImage { + // No-op if the orientation is already correct + guard image.imageOrientation != .up else { return image } + + // We need to calculate the proper transformation to make the image upright. + // We do it in 2 steps: Rotate if Left/Right/Down, and then flip if Mirrored. + var transform = CGAffineTransform.identity + + switch image.imageOrientation { + case .down, .downMirrored: + transform = transform.translatedBy(x: image.size.width, y: image.size.height) + transform = transform.rotated(by: .pi) + case .left, .leftMirrored: + transform = transform.translatedBy(x: image.size.width, y: 0) + transform = transform.rotated(by: .pi / 2) + case .right, .rightMirrored: + transform = transform.translatedBy(x: 0, y: image.size.height) + transform = transform.rotated(by: -.pi / 2) + default: + break + } + + switch image.imageOrientation { + case .upMirrored, .downMirrored: + transform = transform.translatedBy(x: image.size.width, y: 0) + transform = transform.scaledBy(x: -1, y: 1) + case .leftMirrored, .rightMirrored: + transform = transform.translatedBy(x: image.size.height, y: 0) + transform = transform.scaledBy(x: -1, y: 1) + default: + break + } + + // Now we draw the underlying CGImage into a new context, applying the transform + // calculated above. + guard let cgImage = image.cgImage, + let colorSpace = cgImage.colorSpace, + let context = CGContext(data: nil, + width: Int(image.size.width), + height: Int(image.size.height), + bitsPerComponent: cgImage.bitsPerComponent, + bytesPerRow: 0, + space: colorSpace, + bitmapInfo: cgImage.bitmapInfo.rawValue) + else { + return image + } + + context.concatenate(transform) + + switch image.imageOrientation { + case .left, .leftMirrored, .right, .rightMirrored: + // Grr... + context.draw(cgImage, in: CGRect(x: 0, y: 0, width: image.size.height, height: image.size.width)) + default: + context.draw(cgImage, in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)) + } + + // And now we just create a new UIImage from the drawing context + guard let cgImageFixed = context.makeImage() else { return image } + let fixedImage = UIImage(cgImage: cgImageFixed) + return fixedImage + } + + +} diff --git a/tdvideo/tdvideo/PlayContoller8.swift b/tdvideo/tdvideo/PlayContoller8.swift new file mode 100644 index 0000000..c799c94 --- /dev/null +++ b/tdvideo/tdvideo/PlayContoller8.swift @@ -0,0 +1,292 @@ +// +// PlayContoller8.swift +// tdvideo +// +// Created by mac on 2024/2/18. +// + +import Foundation +import AVKit +import VideoToolbox +import CoreImage +import ImageIO +import UIKit +import AVFoundation +import UIKit +import AVFoundation +import CoreMedia + +//边转边播 + +class PlayContoller8: UIViewController { + + var player: AVPlayer? + var playerLayer: AVPlayerLayer? + var isRedFilterEnabled = false + + var asset:AVAsset? + var assetReader:AVAssetReader? + var output:AVAssetReaderTrackOutput? + + var selectedIndex:NSInteger? + + var videoConver:VideoConvertor3 = VideoConvertor3() + + func loadVideo() async { + do { + if(assetReader != nil && assetReader!.status == .reading){ + assetReader?.cancelReading() + } + assetReader = try AVAssetReader(asset: asset!) + output = try await AVAssetReaderTrackOutput( + track: asset!.loadTracks(withMediaType: .video).first!, + outputSettings: [ + AVVideoDecompressionPropertiesKey: [ + kVTDecompressionPropertyKey_RequestedMVHEVCVideoLayerIDs: [0, 1] as CFArray, + ], + ] + ) + assetReader!.timeRange = CMTimeRange(start: .zero, duration: .positiveInfinity) + assetReader!.add(output!) + assetReader!.startReading() + } catch { + print("Error loading video: \(error)") + } + } + + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .black + selectedIndex = 0 + + let videoURL = Bundle.main.url(forResource: "IMG_0071", withExtension: "MOV")! + asset = AVAsset(url: videoURL) + Task { + await loadVideo() + } + + let playerItem = AVPlayerItem(asset: asset!) + //实时播放回调可以做滤镜操作 + playerItem.videoComposition = AVVideoComposition(asset: playerItem.asset) { [self] request in + let compositionTime = request.compositionTime + print(compositionTime.value) + if(selectedIndex == 0){ + //播放源视频 + request.finish(with: request.sourceImage, context: nil) + } + else if(selectedIndex == 1){ + //获取time时刻时 那一帧的左右眼图 + getImage(at: compositionTime) { [self] leftImage, rightImage in + if(leftImage != nil){ + //播放红蓝立体 + let lastImg = getHonalanImg(leftImage: leftImage!, rightImage: rightImage!) + request.finish(with: lastImg, context: nil) + } + } + } + } + + player = AVPlayer(playerItem: playerItem) + playerLayer = AVPlayerLayer(player: player!) + playerLayer?.frame = view.bounds + view.layer.addSublayer(playerLayer!) + player?.play() + // 循环播放设置 + player!.actionAtItemEnd = .none + // 监听播放结束的通知 + NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: playerItem, queue: nil) { [self] _ in + Task { + await loadVideo() + } + player?.seek(to: .zero) + player?.play() + } + + let segmentedControl = UISegmentedControl(items: ["空间视频", "红蓝立体"]) + segmentedControl.frame = CGRect(x: 20, y: 700, width: 360, height: 45) + segmentedControl.selectedSegmentIndex = 0 + self.view.addSubview(segmentedControl) + segmentedControl.layer.borderWidth = 1.0 + segmentedControl.layer.borderColor = UIColor.blue.cgColor + segmentedControl.tintColor = UIColor.blue + let normalTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white] + let selectedTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.blue] + segmentedControl.setTitleTextAttributes(normalTextAttributes, for: .normal) + segmentedControl.setTitleTextAttributes(selectedTextAttributes, for: .selected) + segmentedControl.addTarget(self, action: #selector(segmentedControlValueChanged(_:)), for: .valueChanged) + + } + + @objc func segmentedControlValueChanged(_ sender: UISegmentedControl) { + selectedIndex = sender.selectedSegmentIndex + print("选中了第 \(selectedIndex) 个选项") + } + + func getImage(at time: CMTime, completion: @escaping ((CIImage?, CIImage?) -> Void)) { + //传入time 同一个output去重新遍历了源文件 + while let nextSampleBuffer = output!.copyNextSampleBuffer() { + let presentationTime = CMSampleBufferGetPresentationTimeStamp(nextSampleBuffer) + if presentationTime == time { + guard let taggedBuffers = nextSampleBuffer.taggedBuffers else { return } + + let leftEyeBuffer = taggedBuffers.first(where: { + $0.tags.first(matchingCategory: .stereoView) == .stereoView(.leftEye) + })?.buffer + let rightEyeBuffer = taggedBuffers.first(where: { + $0.tags.first(matchingCategory: .stereoView) == .stereoView(.rightEye) + })?.buffer + + if let leftEyeBuffer, + let rightEyeBuffer, + case let .pixelBuffer(leftEyePixelBuffer) = leftEyeBuffer, + case let .pixelBuffer(rightEyePixelBuffer) = rightEyeBuffer { + + let lciImage = CIImage(cvPixelBuffer: leftEyePixelBuffer) + let rciImage = CIImage(cvPixelBuffer: rightEyePixelBuffer) + completion(lciImage,rciImage) + } + break + } + } + completion(nil,nil) + } + + func getHonalanImg(leftImage:CIImage,rightImage:CIImage)->CIImage{ + + // 创建红色和蓝色滤镜 + let redColorMatrix: [CGFloat] = [ + 0.0, 0.0, 0.0, 0.0, 0.0, // 红色通道 + 0.0, 0.0, 0.0, 0.0, 0.0, // 绿色通道 + 0.0, 0.0, 1.0, 0.0, 0.0, // 蓝色通道 + 0.0, 0.0, 0.0, 1.0, 0.0 // 透明通道 + ] + + let blueColorMatrix: [CGFloat] = [ + 1.0, 0.0, 0.0, 0.0, 0.0, // 红色通道 + 0.0, 0.0, 0.0, 0.0, 0.0, // 绿色通道 + 0.0, 0.0, 0.0, 0.0, 0.0, // 蓝色通道 + 0.0, 0.0, 0.0, 1.0, 0.0 // 透明通道 + ] + + let redFilter = CIFilter(name: "CIColorMatrix")! + redFilter.setValue(leftImage, forKey: kCIInputImageKey) + redFilter.setValue(CIVector(values: redColorMatrix, count: redColorMatrix.count), forKey: "inputRVector") + + let blueFilter = CIFilter(name: "CIColorMatrix")! + blueFilter.setValue(rightImage, forKey: kCIInputImageKey) + blueFilter.setValue(CIVector(values: blueColorMatrix, count: blueColorMatrix.count), forKey: "inputBVector") + + var lastImg:CIImage? + if let redOutputImage = redFilter.outputImage, + let blueOutputImage = blueFilter.outputImage { + + let compositeFilter = CIFilter(name: "CIScreenBlendMode")! + compositeFilter.setValue(redOutputImage, forKey: kCIInputImageKey) + compositeFilter.setValue(blueOutputImage, forKey: kCIInputBackgroundImageKey) + lastImg = compositeFilter.outputImage! + + } + return lastImg! + + } +} + + + + + +//import Foundation +//import AVKit +//import VideoToolbox +//import CoreImage +//import ImageIO +//import UIKit +//import AVFoundation +//import UIKit +//import AVFoundation +//import CoreMedia +// +// +//class PlayContoller8: UIViewController { +// +// var player: AVPlayer? +// var playerLayer: AVPlayerLayer? +// var isRedFilterEnabled = false +// +// var videoConver:VideoConvertor3 = VideoConvertor3() +// +// override func viewDidLoad() { +// super.viewDidLoad() +// view.backgroundColor = .black +// +// let videoURL = Bundle.main.url(forResource: "IMG_0071", withExtension: "MOV")! +// let asset = AVAsset(url: videoURL) +// let playerItem = AVPlayerItem(asset: asset) +// +// player = AVPlayer(playerItem: playerItem) +// +// playerLayer = AVPlayerLayer(player: player) +// playerLayer?.frame = view.bounds +// view.layer.addSublayer(playerLayer!) +// +// +// +// Task{ +// try await videoConver.convertVideo(asset: asset) { [self] pro in +// player?.play() +// } +// } +// +// +// +// +// let button = UIButton(type: .system) +// button.setTitle("交叉眼", for: .normal) +// button.addTarget(self, action: #selector(toggleRedFilter), for: .touchUpInside) +// button.translatesAutoresizingMaskIntoConstraints = false +// +// view.addSubview(button) +// +// NSLayoutConstraint.activate([ +// button.centerXAnchor.constraint(equalTo: view.centerXAnchor), +// button.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20) +// ]) +// } +// +// @objc func toggleRedFilter() { +// guard let playerItem = player?.currentItem else { return } +// +// if !isRedFilterEnabled { +// applyRedFilter(to: playerItem) +// } else { +// // 移除红色滤镜 +// playerItem.videoComposition = nil +// } +// +// isRedFilterEnabled.toggle() +// } +// +// func applyRedFilter(to playerItem: AVPlayerItem) { +// +// playerItem.videoComposition = AVVideoComposition(asset: playerItem.asset) { [self] request in +// let compositionTime = request.compositionTime +// let arr = videoConver.datas +// for i in 0...arr.count{ +// let dic = arr[i] as! NSDictionary +// let time = dic["time"] as! CMTime +// if(compositionTime == time){ +// let left = dic["left"] as! CIImage +// let right = dic["right"] as! CIImage +// let image = videoConver.joinImages(leftImage: left, rightImage: right) +// request.finish(with: image, context: nil) +// break +// } +// } +// } +// } +// +//} + + + diff --git a/tdvideo/tdvideo/PlayContoller9.swift b/tdvideo/tdvideo/PlayContoller9.swift new file mode 100644 index 0000000..4343e25 --- /dev/null +++ b/tdvideo/tdvideo/PlayContoller9.swift @@ -0,0 +1,235 @@ +// +// PlayContoller9.swift +// tdvideo +// +// Created by mac on 2024/2/21. +// + +/* + 拍摄空间视频 + */ +import UIKit +import AVFoundation +import Photos +import AVKit +import VideoToolbox + +class PlayContoller9: UIViewController, AVCaptureFileOutputRecordingDelegate { + + //AVCaptureSession --- 单摄像头 + var session = AVCaptureMultiCamSession()//多摄像头 + var wideAngleCameraDeviceInput: AVCaptureDeviceInput?//广角摄像头 .builtInWideAngleCamera + var ultraWideCameraDeviceInput: AVCaptureDeviceInput?//超广角 .builtInUltraWideCamera + + + var wideAngleCameraVideoPreviewLayer: AVCaptureVideoPreviewLayer?//广角摄像头 + var ultraWideCameraVideoPreviewLayer: AVCaptureVideoPreviewLayer?//超广角 + + var wideAngleCameraMovieOutput: AVCaptureMovieFileOutput? + var ultraWideCameraMovieOutput: AVCaptureMovieFileOutput? + + + + var isRecording = false + + + var startRecordingButton: UIButton? + + var leftEyeVideoURL:URL? + var rightEyeVideoURL:URL? + var outputVideoURL: URL? + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .white + + outputVideoURL = URL.documentsDirectory.appendingPathComponent("output.MOV") + + configureSession() + setupUI() + } + + private func configureSession() { + session.beginConfiguration() + defer { + session.commitConfiguration() + } + + // 配置后置摄像头 .builtInWideAngleCamera 广角摄像头(默认设备 --- 主摄,28mm左右焦段) .back 后置摄像头 + guard let backCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else { + print("Could not find the back camera") + return + } + + do { + wideAngleCameraDeviceInput = try AVCaptureDeviceInput(device: backCamera) + + guard let wideAngleCameraDeviceInput = wideAngleCameraDeviceInput, + session.canAddInput(wideAngleCameraDeviceInput) else { + print("Could not add back camera input") + return + } + session.addInput(wideAngleCameraDeviceInput) + } catch { + print("Could not create back camera device input: \(error)") + return + } + + // .builtInUltraWideCamera 超广角(默认设备的0.5x,只能使用AVCaptureDeviceDiscoverySession获取) .back 后置摄像头 + guard let frontCamera = AVCaptureDevice.default(.builtInUltraWideCamera, for: .video, position: .back) else { + print("Could not find the front camera") + return + } + + do { + ultraWideCameraDeviceInput = try AVCaptureDeviceInput(device: frontCamera) + + guard let ultraWideCameraDeviceInput = ultraWideCameraDeviceInput, + session.canAddInput(ultraWideCameraDeviceInput) else { + print("Could not add front camera input") + return + } + session.addInput(ultraWideCameraDeviceInput) + } catch { + print("Could not create front camera device input: \(error)") + return + } + + // 配置音频输入设备 + guard let audioDevice = AVCaptureDevice.default(for: .audio) else { + print("Could not find audio device") + return + } + + do { + let audioDeviceInput = try AVCaptureDeviceInput(device: audioDevice) + + guard session.canAddInput(audioDeviceInput) else { + print("Could not add audio input") + return + } + + session.addInput(audioDeviceInput) + } catch { + print("Could not create audio device input: \(error)") + return + } + + // 配置后置摄像头输出 + wideAngleCameraMovieOutput = AVCaptureMovieFileOutput() + guard let wideAngleCameraMovieOutput = wideAngleCameraMovieOutput, + session.canAddOutput(wideAngleCameraMovieOutput) else { + print("Could not add the back camera movie output") + return + } + session.addOutput(wideAngleCameraMovieOutput) + + // 配置前置摄像头输出 + ultraWideCameraMovieOutput = AVCaptureMovieFileOutput() + guard let ultraWideCameraMovieOutput = ultraWideCameraMovieOutput, + session.canAddOutput(ultraWideCameraMovieOutput) else { + print("Could not add the front camera movie output") + return + } + session.addOutput(ultraWideCameraMovieOutput) + + // 配置预览图层 + wideAngleCameraVideoPreviewLayer = AVCaptureVideoPreviewLayer(session: session) + wideAngleCameraVideoPreviewLayer?.frame = CGRect(x: 0, y: 0, width: view.frame.size.width / 2, height: view.frame.size.height / 2) + if let wideAngleCameraVideoPreviewLayer = wideAngleCameraVideoPreviewLayer { + view.layer.addSublayer(wideAngleCameraVideoPreviewLayer) + } + + ultraWideCameraVideoPreviewLayer = AVCaptureVideoPreviewLayer(session: session) + ultraWideCameraVideoPreviewLayer?.frame = CGRect(x: view.frame.size.width / 2, y: 0, width: view.frame.size.width / 2, height: view.frame.size.height / 2) + if let ultraWideCameraVideoPreviewLayer = ultraWideCameraVideoPreviewLayer { + view.layer.addSublayer(ultraWideCameraVideoPreviewLayer) + } + + DispatchQueue.global().async { + self.session.startRunning() + } + } + + private func setupUI() { + startRecordingButton = UIButton(type: .system) + startRecordingButton?.setTitle("Start Recording", for: .normal) + startRecordingButton?.setTitleColor(UIColor.brown, for: UIControl.State.normal) + startRecordingButton?.addTarget(self, action: #selector(toggleRecording(_:)), for: .touchUpInside) + startRecordingButton?.frame = CGRect(x: 0, y: view.frame.size.height - 250, width: view.frame.size.width, height: 50) + view.addSubview(startRecordingButton!) + } + + @objc private func toggleRecording(_ sender: UIButton) { + if isRecording { + stopRecording() + } else { + startRecording() + } + } + + private func startRecording() { + guard let wideAngleCameraMovieOutput = wideAngleCameraMovieOutput, + let ultraWideCameraMovieOutput = ultraWideCameraMovieOutput else { + print("Movie output not configured") + return + } + + let time = Date().timeIntervalSince1970 + let name1 = "back" + String(time) + ".mov" + let name2 = "front" + String(time) + ".mov" + let backCameraOutputURL = URL.documentsDirectory.appending(path:name1) + let frontCameraOutputURL = URL.documentsDirectory.appending(path:name2) + + wideAngleCameraMovieOutput.startRecording(to: backCameraOutputURL, recordingDelegate: self) + ultraWideCameraMovieOutput.startRecording(to: frontCameraOutputURL, recordingDelegate: self) + + isRecording = true + startRecordingButton?.setTitle("Stop Recording", for: .normal) + } + + private func stopRecording() { + guard let wideAngleCameraMovieOutput = wideAngleCameraMovieOutput, + let ultraWideCameraMovieOutput = ultraWideCameraMovieOutput else { + print("Movie output not configured") + return + } + + wideAngleCameraMovieOutput.stopRecording() + ultraWideCameraMovieOutput.stopRecording() + + isRecording = false + startRecordingButton?.setTitle("Start Recording", for: .normal) + } + + func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) { + if let error = error { + print("Video recording finished with error: \(error.localizedDescription)") + } else { + if output == wideAngleCameraMovieOutput { + print("Back camera video recorded: \(outputFileURL)") + leftEyeVideoURL = outputFileURL + } else if output == ultraWideCameraMovieOutput { + print("Front camera video recorded: \(outputFileURL)") + rightEyeVideoURL = outputFileURL + } + createSpVideo() + } + } + + func createSpVideo(){ + + if(rightEyeVideoURL != nil && leftEyeVideoURL != nil){ + let spatialVideoWriter = SpatialVideoWriter() + Task { + spatialVideoWriter.writeSpatialVideo(leftEyeVideoURL: leftEyeVideoURL!, rightEyeVideoURL: rightEyeVideoURL!, outputVideoURL: outputVideoURL!) { success, error in + if success { + print("空间视频生成成功") + } else if let error = error { + print("生成空间视频失败:\(error.localizedDescription)") + } + } + } + } + } +} diff --git a/tdvideo/tdvideo/PlayController3.swift b/tdvideo/tdvideo/PlayController3.swift new file mode 100644 index 0000000..94c572f --- /dev/null +++ b/tdvideo/tdvideo/PlayController3.swift @@ -0,0 +1,103 @@ +// +// PlayController3.swift +// tdvideo +// +// Created by mac on 2024/2/1. +// + + +import UIKit +import PhotosUI +import AVKit +import MobileCoreServices + +class PlayController3: UIViewController, PHPickerViewControllerDelegate { + var imageView: UIImageView! + var playerView: UIView! + + override func viewDidLoad() { + super.viewDidLoad() + self.view.backgroundColor = UIColor.white + setupUI() + } + + private func setupUI() { + imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 200, height: 200)) + imageView.contentMode = .scaleAspectFit + imageView.center = view.center + imageView.backgroundColor = UIColor.brown + view.addSubview(imageView) + + playerView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200)) + playerView.backgroundColor = .black + playerView.center = view.center + view.addSubview(playerView) + + let button = UIButton(type: .system) + button.setTitle("Open Photo Album", for: .normal) + button.addTarget(self, action: #selector(openPhotoAlbum), for: .touchUpInside) + button.frame = CGRect(x: 0, y: 0, width: 200, height: 50) + button.center = CGPoint(x: view.center.x, y: view.center.y + 250) + view.addSubview(button) + } + + @objc private func openPhotoAlbum() { + var configuration = PHPickerConfiguration() + configuration.selectionLimit = 1 + configuration.filter = .any(of: [.images, .videos]) + + let picker = PHPickerViewController(configuration: configuration) + picker.delegate = self + present(picker, animated: true, completion: nil) + } + + // MARK: - PHPickerViewControllerDelegate + + func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + picker.dismiss(animated: true, completion: nil) + + guard let result = results.first else { + return + } + + let itemProvider = result.itemProvider + + if itemProvider.canLoadObject(ofClass: UIImage.self) { + itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (image, error) in + if let image = image as? UIImage { + DispatchQueue.main.async { + self?.imageView.image = image + self?.playerView.isHidden = true + } + } + } + } + + let imageTypeIdentifier = kUTTypeImage as String + let videoTypeIdentifier = kUTTypeMovie as String + if itemProvider.hasItemConformingToTypeIdentifier(imageTypeIdentifier) { + print("Item provider contains an image") + } + + if itemProvider.hasItemConformingToTypeIdentifier(videoTypeIdentifier) { + + itemProvider.loadItem(forTypeIdentifier: videoTypeIdentifier, options: nil) { (item, error) in + if let url = item as? URL { + let asset = AVAsset(url: url) + DispatchQueue.main.async { + let playerItem = AVPlayerItem(asset: asset) + let player = AVPlayer(playerItem: playerItem) + let playerLayer = AVPlayerLayer(player: player) + playerLayer.frame = self.playerView.bounds ?? CGRect.zero + self.playerView.layer.addSublayer(playerLayer) + player.play() + self.playerView.isHidden = false + } + + } + } + + } + + } +} diff --git a/tdvideo/tdvideo/PlayControllerVideo.swift b/tdvideo/tdvideo/PlayControllerVideo.swift new file mode 100644 index 0000000..70eddb5 --- /dev/null +++ b/tdvideo/tdvideo/PlayControllerVideo.swift @@ -0,0 +1,277 @@ +// +// PlayControllerVideo.swift +// tdvideo +// +// Created by mac on 2024/2/4. +// + + + +import UIKit +import Photos +import ImageIO +import CoreFoundation +import UIKit +import Photos +import ImageIO +import CoreGraphics +import MobileCoreServices +import AVKit + + +class PhotoCell2: UICollectionViewCell { + + + let imageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + imageView.clipsToBounds = true + return imageView + }() + + let frameCountLabel: UILabel = { + let label = UILabel() + label.textColor = .white + label.backgroundColor = .red + label.textAlignment = .center + label.font = UIFont.boldSystemFont(ofSize: 12) + label.layer.cornerRadius = 8 + label.clipsToBounds = true + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setupViews() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupViews() + } + + private func setupViews() { + addSubview(imageView) + addSubview(frameCountLabel) + + imageView.translatesAutoresizingMaskIntoConstraints = false + frameCountLabel.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + imageView.topAnchor.constraint(equalTo: topAnchor), + imageView.leadingAnchor.constraint(equalTo: leadingAnchor), + imageView.trailingAnchor.constraint(equalTo: trailingAnchor), + imageView.bottomAnchor.constraint(equalTo: bottomAnchor), + + frameCountLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8), + frameCountLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8), + frameCountLabel.widthAnchor.constraint(equalToConstant: 40), + frameCountLabel.heightAnchor.constraint(equalToConstant: 20) + ]) + } +} + + +class PlayControllerVideo: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + var collectionView: UICollectionView! + var fetchResult: PHFetchResult! + var photos: [UIImage] = [] + + var mediaSelectedHandler: ((AVAsset) -> Void)? + + override func viewDidLoad() { + super.viewDidLoad() + setupCollectionView() + fetchPhotos() + collectionView.dataSource = self + collectionView.delegate = self + + } + + private func setupCollectionView() { + let layout = UICollectionViewFlowLayout() + layout.minimumLineSpacing = 10 + layout.minimumInteritemSpacing = 10 + collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) + collectionView.backgroundColor = .white + collectionView.register(PhotoCell2.self, forCellWithReuseIdentifier: "PhotoCell2") + view.addSubview(collectionView) + + collectionView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } + + // PHAsset.fetchAssets(with: .image, options: fetchOptions) + // PHAsset.fetchAssets(with: fetchOptions) + func fetchPhotos() { + let fetchOptions = PHFetchOptions() + fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] + fetchResult = PHAsset.fetchAssets(with: .video, options: fetchOptions) + + for index in 0.. Bool { + let metadata = asset.metadata(forFormat: AVMetadataFormat.quickTimeMetadata) + let isSpatialVideo = metadata.contains { item in + if let identifier = item.identifier?.rawValue { + return identifier == "mdta/com.apple.quicktime.spatial.format-version" + } + return false + } + return isSpatialVideo + } + + func isSSVideo(asset:AVAsset)async throws->Bool{ + + let userDataItems = try await asset.loadMetadata(for:.quickTimeMetadata) + let spacialCharacteristics = userDataItems.filter { $0.identifier?.rawValue == "mdta/com.apple.quicktime.spatial.format-version" } + if spacialCharacteristics.count == 0 { + return false + } + return true + } + + // MARK: - UICollectionViewDataSource + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return photos.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PhotoCell2", for: indexPath) as! PhotoCell2 + cell.imageView.image = photos[indexPath.item] + + let frameCount = getFrameCount(for: indexPath.item) + cell.frameCountLabel.isHidden = frameCount <= 1 + cell.frameCountLabel.text = "\(frameCount)" + + return cell + } + + // MARK: - UICollectionViewDelegateFlowLayout + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + let width = collectionView.bounds.width / 3 - 10 + return CGSize(width: width, height: width) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return 10 + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + return 10 + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + // 获取选中的视频资源 + let asset = fetchResult.object(at: indexPath.item) + let requestOptions = PHVideoRequestOptions() + requestOptions.isNetworkAccessAllowed = true + + PHImageManager.default().requestAVAsset(forVideo: asset, options: requestOptions) { [self] (avAsset, _, _) in + mediaSelectedHandler!(avAsset!) + DispatchQueue.main.async { + self.dismiss(animated: true, completion: nil) + } + } + + } + + + + func playVideo(asset: PHAsset) { + PHImageManager.default().requestPlayerItem(forVideo: asset, options: nil) { [weak self] (playerItem, _) in + guard let playerItem = playerItem else { return } + + DispatchQueue.main.async { + let playerViewController = AVPlayerViewController() + playerViewController.player = AVPlayer(playerItem: playerItem) + self?.present(playerViewController, animated: true) { + playerViewController.player?.play() + } + } + } + } + + + private func getFrameCount(for index: Int) -> Int { + let asset = fetchResult.object(at: index) + if let imageData = getImageData(for: asset) { + if let cgImageSource = CGImageSourceCreateWithData(imageData as CFData, nil) { + return CGImageSourceGetCount(cgImageSource) + } + } + return 0 + } + + private func getImageData(for asset: PHAsset) -> Data? { + var imageData: Data? + let requestOptions = PHImageRequestOptions() + requestOptions.isSynchronous = true + requestOptions.deliveryMode = .highQualityFormat + + PHImageManager.default().requestImageData(for: asset, options: requestOptions) { (data, _, _, _) in + imageData = data + } + + return imageData + } + + + func convertCGImageToCFData(cgImage: CGImage) -> CFData? { + let data = CFDataCreateMutable(kCFAllocatorDefault, 0) + + if let data = data { + if let destination = CGImageDestinationCreateWithData(data, kUTTypePNG, 1, nil) { + CGImageDestinationAddImage(destination, cgImage, nil) + CGImageDestinationFinalize(destination) + } + } + + return data + } +} diff --git a/tdvideo/tdvideo/SceneDelegate.swift b/tdvideo/tdvideo/SceneDelegate.swift new file mode 100644 index 0000000..64ddad0 --- /dev/null +++ b/tdvideo/tdvideo/SceneDelegate.swift @@ -0,0 +1,52 @@ +// +// SceneDelegate.swift +// tdvideo +// +// Created by aaa on 2024/1/19. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/tdvideo/tdvideo/SpatialVideoConverter.swift b/tdvideo/tdvideo/SpatialVideoConverter.swift new file mode 100644 index 0000000..9834a21 --- /dev/null +++ b/tdvideo/tdvideo/SpatialVideoConverter.swift @@ -0,0 +1,421 @@ +// +// SpatialVideoConverter.swift +// vp +// +// Created by soldoros on 2024/1/17. +// + +import AVFoundation +import CoreImage +import Foundation +import Observation +import VideoToolbox + +///将立体视频转换为空间视频的处理器。 +class SpatialVideoConverter:NSObject { + // MARK: - Properties + + // MARK: Public + + ///需要从源视频中处理的帧总数。 + var totalFrames: Double = .zero + + ///到目前为止已经处理的帧数。 + var framesProcessed: Double = 0.0 + + ///转换完成前的剩余时间。 + var timeRemaining: Double = .zero + + ///处理开始的时间戳。 + var startTime: Date = .now + + ///一个布尔标志,确定处理器是否已经在使用中。 + var isProcessing = false + + ///最后一个成功转换的文件。 + var lastConvertedFileURL: LastConvertedFile? + + ///在立体视频中,正在处理的当前帧的左眼视图。 + var leftEyeImage: CVPixelBuffer? + + ///在立体视频中,正在处理的当前帧的右眼视图。 + var rightEyeImage: CVPixelBuffer? + + // MARK: Private + + ///一个辅助处理器,可以用来裁剪和转换底层图像类型。 + private let processor = FrameProcessor() + + ///将视频帧写入文件的视频写入器。 + private var writer: AVAssetWriter? + + ///分配avassetwwriter的视频输入的队列。 + private let videoInputQueue = DispatchQueue(label: "com.test.spatialWriterVideo") + + ///分配avassetwwriter的音频输入的队列。 + private let audioInputQueue = DispatchQueue(label: "com.test.spatialWriterAudio") + + ///“avassetwwriter”的输入接收视频帧。 + private var writerVideoInput: AVAssetWriterInput? + + ///“avassetwwriter”的输入接收音频样本。 + private var writerAudioInput: AVAssetWriterInput? + + ///处理源视频的读取器。 + private var heroReader: AVAssetReader? + + /// AVAssetReader的视频轨迹输出 + private var readerVideoOutput: AVAssetReaderTrackOutput? + + /// AVAssetReader的音轨输出。 + private var readerAudioOutput: AVAssetReaderTrackOutput? + + ///一个内部标志,指示视频写入器是否完成所有帧的处理。 + private var videoWritingFinished = false + + ///一个内部标志,指示音频写入器是否完成所有帧的处理。 + private var audioWritingFinished = false + + ///一个格式化程序,可用于将时间戳(如处理时间)转换为字符串。 + private var dateFormatter: DateComponentsFormatter { + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.day, .hour, .minute, .second] + formatter.unitsStyle = .abbreviated + return formatter + } + + ///一个格式化程序,可用于将字节计数(如文件大小)转换为字符串。 + private var byteCountFormatter: ByteCountFormatter { + let formatter = ByteCountFormatter() + formatter.allowedUnits = [.useGB, .useMB, .useKB] + formatter.countStyle = .file + return formatter + } + + + // MARK: - Methods + + // MARK: Public + + ///将源立体视频转换为空间视频。 + /// -参数: + /// - sourceVideoURL:源立体视频的URL。必须是AVFoundation支持的格式,如H.264、H.265或ProRes文件。 + /// - outputVideoURL:如果转换成功完成,输出视频的URL。 + func convertStereoscopicVideoToSpatialVideo( + sourceVideoURL: URL, + outputVideoURL: URL, + progress: ((Float)->())? = nil + ) async throws { + + let heroAsset = AVAsset(url: sourceVideoURL) + + //删除临时文件,如果它存在。 + try removeExistingFile(at: outputVideoURL) + + writer = try AVAssetWriter(outputURL: outputVideoURL, fileType: .mov) + guard let videoTrack = try await heroAsset.loadTracks(withMediaType: .video).first else { + return + } + let audioTrack = try await heroAsset.loadTracks(withMediaType: .audio).first + + guard let videoFormatDescription = try await videoTrack.load(.formatDescriptions).first else { + return + } + + if !processor.isPrepared { + processor.prepare(with: videoFormatDescription, outputRetainedBufferCountHint: 1) + } + + //设置源视频大小并计算左眼和右眼图像的区域。 + let naturalSize = try await videoTrack.load(.naturalSize) + let leftEyeRegion = CGRect( + x: 0, + y: 0, + width: naturalSize.width / 2, + height: naturalSize.height + ) + let rightEyeRegion = CGRect( + x: naturalSize.width / 2, + y: 0, + width: naturalSize.width / 2, + height: naturalSize.height + ) + + //配置帧率和帧数,提供给视图计算剩余时间。 + let frameRate = try await videoTrack.load(.nominalFrameRate) + let dataRate = try await videoTrack.load(.estimatedDataRate) + let duration = try await heroAsset.load(.duration) + let frames = CMTimeGetSeconds(duration) * Double(frameRate) + totalFrames = frames + + // TODO:添加功能来指定所需的输出分辨率,比特率和 + // TODO:水平视差。 + + //设置视频输出设置 + var videoSettings = AVOutputSettingsAssistant(preset: .mvhevc1440x1440)?.videoSettings + videoSettings?[AVVideoWidthKey] = leftEyeRegion.width + videoSettings?[AVVideoHeightKey] = leftEyeRegion.height + var compressionProperties = videoSettings?[AVVideoCompressionPropertiesKey] as! [String: Any] + compressionProperties[AVVideoAverageBitRateKey] = dataRate + compressionProperties[kVTCompressionPropertyKey_HorizontalDisparityAdjustment as String] = 0 + compressionProperties[kCMFormatDescriptionExtension_HorizontalFieldOfView as String] = 90 + videoSettings?[AVVideoCompressionPropertiesKey] = compressionProperties + + //设置视频输入设置 + writerVideoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings) + guard let writerVideoInput else { return } + writerVideoInput.expectsMediaDataInRealTime = false + + //设置音频输入设置,如果有音频轨道 + if audioTrack != nil { + writerAudioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: nil) + } + + //设置像素缓冲组适配器,用于写左右眼框。 + let pixelBufferAdaptor = AVAssetWriterInputTaggedPixelBufferGroupAdaptor( + assetWriterInput: writerVideoInput, + sourcePixelBufferAttributes: .none) + + //设置阅读器的立体视频文件,或左眼(英雄眼)视图,如果视图提供给每一个 + //个人眼睛。 + let readerOutputSettings: [String:Any] = [ + kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: kCVPixelFormatType_32BGRA), + kCVPixelBufferWidthKey as String: naturalSize.width, + kCVPixelBufferHeightKey as String: naturalSize.height + ] + readerVideoOutput = AVAssetReaderTrackOutput( + track: videoTrack, + outputSettings: readerOutputSettings) + if let audioTrack { + readerAudioOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: nil) + } + heroReader = try AVAssetReader(asset: heroAsset) + + //向读取器添加必要的输出并开始读取。 + guard let heroReader, + let readerVideoOutput + else { return } + heroReader.add(readerVideoOutput) + if let readerAudioOutput { + heroReader.add(readerAudioOutput) + } + heroReader.startReading() + + //向写入器添加必要的输入并启动会话。 + guard let writer else { return } + writer.add(writerVideoInput) + if let writerAudioInput { + writer.add(writerAudioInput) + } + writer.startWriting() + writer.startSession(atSourceTime: .zero) + + //表示我们已经开始处理。 + isProcessing = true + + //设置处理的开始时间,允许下游估计时间 + //完成计算。 + startTime = Date.now + + writerVideoInput.requestMediaDataWhenReady(on: videoInputQueue) { [weak self] in + guard let self else { return } + while writerVideoInput.isReadyForMoreMediaData { + autoreleasepool { + guard self.processor.isPrepared else { + print("The processor is not prepared. Cannot write video") + return + } + + if let frame = readerVideoOutput.copyNextSampleBuffer(), + let frameBuffer = CMSampleBufferGetImageBuffer(frame) { + let sourceBuffer = CIImage(cvImageBuffer: frameBuffer) + + // Setup the left and right eye `CVPixelBuffer` references. + guard let leftEye = self.processor.cropPixelBuffer( + pixelBufferImage: sourceBuffer, + targetRect: leftEyeRegion + ), + let rightEye = self.processor.cropPixelBuffer( + pixelBufferImage: sourceBuffer, + targetRect: rightEyeRegion + ) + else { return } + + // Set a video preview + self.leftEyeImage = leftEye + self.rightEyeImage = rightEye + + // Create an array of `CMTaggedBuffers, one for each eye's view. + let taggedBuffers: [CMTaggedBuffer] = [ + .init(tags: [.videoLayerID(0), .stereoView(.leftEye)], pixelBuffer: leftEye), + .init(tags: [.videoLayerID(1), .stereoView(.rightEye)], pixelBuffer: rightEye) + ] + + let didAppend = pixelBufferAdaptor.appendTaggedBuffers( + taggedBuffers, + withPresentationTime: frame.presentationTimeStamp + ) + if !didAppend { + print("Failed to append frame.") + } + + // Increment the number of frames processed. + if self.framesProcessed < (self.totalFrames - 1) { + self.framesProcessed += 1 + } + + // Calculate the estimated time remaining based on how long this frame took to process. + self.calculateTimeRemaining() + progress?( Float(self.framesProcessed)/Float(self.totalFrames)) + + } else { + if !self.videoWritingFinished { + sourceVideoURL.stopAccessingSecurityScopedResource() + self.videoWritingFinished.toggle() + writerVideoInput.markAsFinished() + self.stop(with: outputVideoURL) + } + } + } + } + } + + //当它准备好时通知编写器的音频输入请求数据。 + if let writerAudioInput, + let readerAudioOutput { + writerAudioInput.requestMediaDataWhenReady(on: audioInputQueue) { [weak self] in + guard let self else { return } + while writerAudioInput.isReadyForMoreMediaData { + autoreleasepool { + if let sample = readerAudioOutput.copyNextSampleBuffer() { + writerAudioInput.append(sample) + } else { + if !self.audioWritingFinished { + self.audioWritingFinished.toggle() + writerAudioInput.markAsFinished() + self.stop(with: outputVideoURL) + } + } + } + } + } + } + } + + + ///取消当前正在转换的视频,并将写入器重置到准备转换的状态 + ///接受一个新文件进行转换。 + /// -参数expectedOutputURL:被取消的文件的预期输出' URL ',因此 + ///可以删除必要的临时文件并重置权限。 + func cancel(expectedOutputURL: URL) { + writerVideoInput?.markAsFinished() + writerAudioInput?.markAsFinished() + writer?.cancelWriting() + try? removeExistingFile(at: expectedOutputURL) + resetWriter() + } + + // MARK: - Private + + ///删除输出URL中已经存在的文件。 + /// -参数outputVideoURL:提供的视频输出URL。 + private func removeExistingFile(at outputVideoURL: URL) throws { + try FileManager.default.removeItem(atPath: outputVideoURL.path) + } + + ///计算估计的处理剩余时间。 + private func calculateTimeRemaining() { + let totalTimeElapsed = Date.now.timeIntervalSince1970 - startTime.timeIntervalSince1970 + let totalFramesCompleted = framesProcessed + let averageTimeBetweenFrames = totalTimeElapsed / totalFramesCompleted + let estimatedTimeRemaining = averageTimeBetweenFrames * (totalFrames - totalFramesCompleted) + guard self.timeRemaining != 0 else { + self.timeRemaining = estimatedTimeRemaining + return + } + if estimatedTimeRemaining < self.timeRemaining + 100 { + self.timeRemaining = estimatedTimeRemaining + } + } + + ///将输入标记为已完成,结束视频写入会话并完成写入。 + ///—参数outputURL:转换后文件的输出URL。 + private func stop(with outputURL: URL) { + guard isProcessing, + let writerVideoInput, + videoWritingFinished + else { return } + + if let writerAudioInput { + guard audioWritingFinished else { return } + } + + self.writer?.finishWriting { [weak self] in + guard let self else {return} + Task { + try? await self.saveLastConvertedFile(outputURL: outputURL) + outputURL.stopAccessingSecurityScopedResource() + self.resetWriter() + } + + print("finished writing") + } + } + + ///将读取、写入和时间计算重置到原始状态,为下一次计算做准备 + ///文件转换。 + private func resetWriter() { + isProcessing = false + self.totalFrames = 0 + self.framesProcessed = 0 + self.timeRemaining = 0 + self.startTime = .now + + self.writerVideoInput = nil + self.writerAudioInput = nil + self.writer = nil + self.readerVideoOutput = nil + self.readerAudioOutput = nil + self.heroReader = nil + + self.videoWritingFinished = false + self.audioWritingFinished = false + } + + ///创建一个引用到最后一个成功完成的文件进行处理,这个引用可以用于 + ///显示预览的用户访问最后转换的文件。 + /// -参数outputURL:最后转换文件的输出' URL '。 + private func saveLastConvertedFile(outputURL: URL) async throws { + do { + let attr = try FileManager.default.attributesOfItem(atPath: outputURL.path) + let fileSize = attr[FileAttributeKey.size] as? Int64 + + let asset = AVAsset(url: outputURL) + let duration = try await asset.load(.duration) + + self.lastConvertedFileURL = LastConvertedFile( + filePath: outputURL, + timeToProcess: dateFormatter.string(from: startTime, to: Date.now) ?? "Unknown", + fileSize: byteCountFormatter.string(fromByteCount: fileSize ?? 0), + duration: dateFormatter.string(from: duration.seconds) ?? "Unknown" + ) + } catch { + print("Error: \(error)") + } + } +} + +///提供上次转换的视频文件的基本元数据的结构体。 +struct LastConvertedFile: Codable, Equatable { + ///最后一次转换的视频文件路径。 + let filePath: URL + + ///将最后一个转换文件转换为字符串所花费的时间。 + let timeToProcess: String + + ///最后转换文件的文件大小,字符串形式。 + let fileSize: String + + ///最后转换文件的视频时长,字符串形式。 + let duration: String +} diff --git a/tdvideo/tdvideo/SpatialVideoWriter.swift b/tdvideo/tdvideo/SpatialVideoWriter.swift new file mode 100644 index 0000000..17a6abf --- /dev/null +++ b/tdvideo/tdvideo/SpatialVideoWriter.swift @@ -0,0 +1,124 @@ +// +// SpatialVideoWriter.swift +// tdvideo +// +// Created by mac on 2024/2/22. +//。两个普通视频合成空间视频 + +import UIKit +import AVFoundation +import VideoToolbox +import Photos + +class SpatialVideoWriter { + + private func removeExistingFile(at outputVideoURL: URL) throws { + do { + try FileManager.default.removeItem(atPath: outputVideoURL.path) + print("视频文件删除成功") + } catch { + print("删除视频文件出错:\(error)") + } + } + + func writeSpatialVideo(leftEyeVideoURL: URL, rightEyeVideoURL: URL, outputVideoURL: URL, completion: @escaping (Bool, Error?) -> Void) { + + do { + + try removeExistingFile(at: outputVideoURL) + + let leftEyeAsset = AVURLAsset(url: leftEyeVideoURL) + let rightEyeAsset = AVURLAsset(url: rightEyeVideoURL) + + let assetWriter = try AVAssetWriter(outputURL: outputVideoURL, fileType: .mov) + + let leftVideoTrack = leftEyeAsset.tracks(withMediaType: .video).first! + let videoSettings: [String: Any] = [ + AVVideoWidthKey: leftVideoTrack.naturalSize.width, + AVVideoHeightKey: leftVideoTrack.naturalSize.height, + AVVideoCodecKey: AVVideoCodecType.hevc, + AVVideoCompressionPropertiesKey: [ + kVTCompressionPropertyKey_MVHEVCVideoLayerIDs: [0, 1] as CFArray, + kCMFormatDescriptionExtension_HorizontalFieldOfView: 90_000, // asset-specific, in thousandths of a degree + kVTCompressionPropertyKey_HorizontalDisparityAdjustment: 200, // asset-specific + ] + ] + let input = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings) + assetWriter.add(input) + + let adaptor = AVAssetWriterInputTaggedPixelBufferGroupAdaptor(assetWriterInput: input) + assetWriter.startWriting() + assetWriter.startSession(atSourceTime: .zero) + + let leftEyeReader = try AVAssetReader(asset: leftEyeAsset) + let rightEyeReader = try AVAssetReader(asset: rightEyeAsset) + + let readerOutputSettings: [String:Any] = [ + kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: kCVPixelFormatType_32BGRA), + kCVPixelBufferWidthKey as String: leftVideoTrack.naturalSize.width, + kCVPixelBufferHeightKey as String: leftVideoTrack.naturalSize.height + ] + + let leftEyeOutput = AVAssetReaderTrackOutput(track: leftVideoTrack, outputSettings: readerOutputSettings) + let rightEyeOutput = AVAssetReaderTrackOutput(track: rightEyeAsset.tracks(withMediaType: .video).first!, outputSettings: readerOutputSettings) + + leftEyeReader.add(leftEyeOutput) + rightEyeReader.add(rightEyeOutput) + + leftEyeReader.startReading() + rightEyeReader.startReading() + + while let leftBuffer = leftEyeOutput.copyNextSampleBuffer(), + let rightBuffer = rightEyeOutput.copyNextSampleBuffer() { + + let time = Date().timeIntervalSince1970 + print("获取了一帧" + String(time)) + // 获取左右眼视频帧的像素缓冲区 + guard let leftFrameBuffer = CMSampleBufferGetImageBuffer(leftBuffer), + let rightFrameBuffer = CMSampleBufferGetImageBuffer(rightBuffer) else { + print("获取左右眼像素缓冲区失败") + return + } + + // 创建 CMTaggedBuffer 数组,包含左右眼视图的像素缓冲区 + let leftCVPixelBuffer = leftFrameBuffer as CVPixelBuffer + let rightCVPixelBuffer = rightFrameBuffer as CVPixelBuffer + + let left = CMTaggedBuffer(tags: [.stereoView(.leftEye), .videoLayerID(0)], pixelBuffer: leftCVPixelBuffer) + let right = CMTaggedBuffer(tags: [.stereoView(.rightEye), .videoLayerID(1)], pixelBuffer: rightCVPixelBuffer) + + while !adaptor.assetWriterInput.isReadyForMoreMediaData { + // 等待直到 writerInput 准备好接收下一个视频帧 + Thread.sleep(forTimeInterval: 0.1) // 暂停一段时间,避免过度占用资源 + } + adaptor.appendTaggedBuffers([left, right], withPresentationTime: leftBuffer.presentationTimeStamp) + } + + // 完成写入 + print("完成写入") + input.markAsFinished() + outputVideoURL.stopAccessingSecurityScopedResource() + assetWriter.finishWriting { [self] in + print("可以保存") + completion(true, nil) + + self.saveVideoToLibrary(videoURL: outputVideoURL, completion: completion) + } + } catch { + print("生成失败") + completion(false, error) + } + } + + private func saveVideoToLibrary(videoURL: URL, completion: @escaping (Bool, Error?) -> Void) { + PHPhotoLibrary.shared().performChanges({ + PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoURL) + }) { success, error in + if success { + print("保存成功") + } else if let error = error { + print("保存失败") + } + } + } +} diff --git a/tdvideo/tdvideo/VideoConvertor.swift b/tdvideo/tdvideo/VideoConvertor.swift new file mode 100644 index 0000000..b028e05 --- /dev/null +++ b/tdvideo/tdvideo/VideoConvertor.swift @@ -0,0 +1,133 @@ +// +// VideoConvertor.swift +// tdvideo +// +// Created by aaa on 2024/1/24. +// + + +import Foundation +import AVKit +import VideoToolbox + +enum VideoReaderError : Error { + case invalidVideo + case notSpacialVideo +} + +class VideoConvertor { + + + func convertVideo( inputFile : URL, outputFile: URL, progress: ((Float)->())? = nil ) async throws { + + do { + try FileManager.default.removeItem(atPath: outputFile.path) + print("视频文件删除成功") + } catch { + print("删除视频文件出错:\(error)") + } + + // Load the AVAsset + let asset = AVAsset(url: inputFile) + let assetReader = try AVAssetReader(asset: asset) + + //检查是否为空间视频 + let userDataItems = try await asset.loadMetadata(for:.quickTimeMetadata) + let spacialCharacteristics = userDataItems.filter { $0.identifier?.rawValue == "mdta/com.apple.quicktime.spatial.format-version" } + if spacialCharacteristics.count == 0 { + print("该视频不是空间视频") + } + + //获取输入视频的方向和大小(用于设置输出方向) + let (orientation, videoSize) = try await getOrientationAndResolutionSizeForVideo(asset: asset) + + //加载视频轨迹 + let output = try await AVAssetReaderTrackOutput( + track: asset.loadTracks(withMediaType: .video).first!, + outputSettings: [ + AVVideoDecompressionPropertiesKey: [ + kVTDecompressionPropertyKey_RequestedMVHEVCVideoLayerIDs: [0, 1] as CFArray, + ], + ] + ) + assetReader.add(output) + assetReader.startReading() + + + + //输出宽度为宽度的一半 + //我们有两个并排的视频,我们保持长宽比 + let vw = VideoWriter(url: outputFile, width: Int(videoSize.width), height: Int(videoSize.height/2), orientation: orientation, sessionStartTime: CMTime(value: 1, timescale: 30 ), isRealTime: false, queue: .main) + + let duration = try await asset.load(.duration) + + // Based on code from https://www.finnvoorhees.com/words/reading-and-writing-spatial-video-with-avfoundation + while let nextSampleBuffer = output.copyNextSampleBuffer() { + guard let taggedBuffers = nextSampleBuffer.taggedBuffers else { return } + + let leftEyeBuffer = taggedBuffers.first(where: { + $0.tags.first(matchingCategory: .stereoView) == .stereoView(.leftEye) + })?.buffer + let rightEyeBuffer = taggedBuffers.first(where: { + $0.tags.first(matchingCategory: .stereoView) == .stereoView(.rightEye) + })?.buffer + + if let leftEyeBuffer, + let rightEyeBuffer, + case let .pixelBuffer(leftEyePixelBuffer) = leftEyeBuffer, + case let .pixelBuffer(rightEyePixelBuffer) = rightEyeBuffer { + + let lciImage = CIImage(cvPixelBuffer: leftEyePixelBuffer) + let rciImage = CIImage(cvPixelBuffer: rightEyePixelBuffer) + + let newpb = joinImages( leftImage: lciImage, rightImage: rciImage ) + + let time = CMSampleBufferGetOutputPresentationTimeStamp(nextSampleBuffer) + + _ = vw!.add(image: newpb, presentationTime: time) + print( "Added frame at \(time)") + + // callback with progress + progress?( Float(time.value)/Float(duration.value)) + + // This sleep is needed to stop memory blooming - keeps around 280Mb rather than spiraling up to 8+Gig! + try await Task.sleep(nanoseconds: 3_000_000) + } + } + + _ = try await vw!.finish() + + print( "status - \(assetReader.status)") + print( "status - \(assetReader.error?.localizedDescription ?? "None")") + print( "Finished") + + } + + func getOrientationAndResolutionSizeForVideo(asset:AVAsset) async throws -> (CGAffineTransform, CGSize) { + guard let track = try await asset.loadTracks(withMediaType: AVMediaType.video).first + else{throw VideoReaderError.invalidVideo} + let naturalSize = try await track.load(.naturalSize) + let naturalTransform = try await track.load(.preferredTransform) + let size = naturalSize.applying(naturalTransform) + return (naturalTransform, CGSize(width: abs(size.width), height: abs(size.height)) ) + } + + //将两张图片合成一张图片 + func joinImages( leftImage:CIImage, rightImage:CIImage) -> CIImage { + let left = UIImage(ciImage: leftImage ) + let right = UIImage(ciImage: rightImage ) + + let imageWidth = left.size.width/2 + right.size.width/2 + let imageHeight = left.size.height/2 + + let newImageSize = CGSize(width:imageWidth, height: imageHeight); + UIGraphicsBeginImageContextWithOptions(newImageSize, false, 1); + left.draw(in: CGRect(x:0, y:0, width:imageWidth/2, height:imageHeight)) + right.draw(in: CGRect(x:imageWidth/2, y:0, width:imageWidth/2, height:imageHeight)) + let image = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext(); + + let ci = CIImage(cgImage: image.cgImage!) + return ci + } +} diff --git a/tdvideo/tdvideo/VideoConvertor2.swift b/tdvideo/tdvideo/VideoConvertor2.swift new file mode 100644 index 0000000..e18a1b4 --- /dev/null +++ b/tdvideo/tdvideo/VideoConvertor2.swift @@ -0,0 +1,302 @@ +// +// VideoConvertor2.swift +// tdvideo +// +// Created by aaa on 2024/1/24. +// 视频转码 + +//红蓝立体 + +//com.nsk.tdvideo + +/* + + let rr:AVAsynchronousCIImageFilteringRequest? + // 创建视频合成器,用于交叉眼视频 AVAsynchronousCIImageFilteringRequest + let videoComposition = AVMutableVideoComposition(asset: asset) { request in + // 获取当前帧的时间戳 + let time = request.compositionTime + // callback with progress + progress?(Float(time.value) / Float(duration.value)) + } + + + videoComposition.renderSize = CGSize(width: Int(videoSize.width), height: Int(videoSize.height/2)) + videoComposition.frameDuration = CMTime(value: 1, timescale: 30) + playerItem?.videoComposition = videoComposition + + + //开始播放 + let nc:NotificationCenter = NotificationCenter.default + let noti:NSNotification = NSNotification.init(name: NSNotification.Name(rawValue: "upvideo"), object: nil) + nc.post(noti as Notification) + + // Based on code from https://www.finnvoorhees.com/words/reading-and-writing-spatial-video-with-avfoundation + + +// CIScreenBlendMode: 通过将颜色通道值反转并相乘,然后将结果反转回来,将两个图像合成为屏幕混合效果。 +// CIHardLightBlendMode: 使用源图像的亮度来决定如何混合两个图像。较亮的像素将更多地影响结果。 +// CILightenBlendMode: 比较两个图像的像素,并选择较亮的像素作为最终结果。 +// CIColorDodgeBlendMode: 使用源图像的颜色信息来增加目标图像的颜色亮度。 +// CIColorBurnBlendMode: 使用源图像的颜色信息来降低目标图像的颜色亮度。 +// CIDarkenBlendMode: 比较两个图像的像素,并选择较暗的像素作为最终结果。 +// CILinearDodgeBlendMode: 使用线性增加的方式将两个图像相加,产生一种亮度叠加的效果。 +// CIMultiplyBlendMode: 将两个图像的像素值相乘,产生一种乘法混合效果。 +// CISourceOverCompositing: 将源图像放在目标图像上方,产生一种覆盖混合效果。 + */ + + +import Foundation +import AVKit +import VideoToolbox +import CoreImage +import ImageIO + +class VideoConvertor2 { + + + ///在立体视频中,正在处理的当前帧的左眼视图。 + var leftEyeImage: CVPixelBuffer? + + ///在立体视频中,正在处理的当前帧的右眼视图。 + var rightEyeImage: CVPixelBuffer? + + + //空间视频 交叉眼 红蓝立体 高斯模糊 + var type = 0 + + + + func convertVideo( asset : AVAsset, outputFile: URL, progress: ((Float)->())? = nil ) async throws { + do { + try FileManager.default.removeItem(atPath: outputFile.path) + print("视频文件删除成功") + } catch { + print("删除视频文件出错:\(error)") + } + + let assetReader = try AVAssetReader(asset: asset) + + + //检查是否为空间视频 print("该视频不是空间视频或图片") + let userDataItems = try await asset.loadMetadata(for:.quickTimeMetadata) + let spacialCharacteristics = userDataItems.filter { $0.identifier?.rawValue == "mdta/com.apple.quicktime.spatial.format-version" } + if spacialCharacteristics.count == 0 { + print("不是空间视频") + } + + //获取输入视频的方向和大小(用于设置输出方向) + let (orientation, videoSize) = try await getOrientationAndResolutionSizeForVideo(asset: asset) + + //输出宽度为宽度的一半 + //我们有两个并排的视频,我们保持长宽比 + let vw:VideoWriter? + if(type == 3){ + //空间视频+红蓝立体 + vw = VideoWriter(url: outputFile, width: Int(videoSize.width), height: Int(videoSize.height), orientation: orientation, sessionStartTime: CMTime(value: 1, timescale: 30 ), isRealTime: false, queue: .main) + } + else{ + //交叉眼 + 平行眼 + vw = VideoWriter(url: outputFile, width: Int(videoSize.width), height: Int(videoSize.height/2), orientation: orientation, sessionStartTime: CMTime(value: 1, timescale: 30 ), isRealTime: false, queue: .main) + } + + + + //加载视频轨道 + let output = try await AVAssetReaderTrackOutput( + track: asset.loadTracks(withMediaType: .video).first!, + outputSettings: [ + AVVideoDecompressionPropertiesKey: [ + kVTDecompressionPropertyKey_RequestedMVHEVCVideoLayerIDs: [0, 1] as CFArray, + ], + ] + ) + assetReader.add(output) + assetReader.startReading() + + let duration = try await asset.load(.duration) + + while let nextSampleBuffer = output.copyNextSampleBuffer() { + + guard let taggedBuffers = nextSampleBuffer.taggedBuffers else { return } + + let leftEyeBuffer = taggedBuffers.first(where: { + $0.tags.first(matchingCategory: .stereoView) == .stereoView(.leftEye) + })?.buffer + let rightEyeBuffer = taggedBuffers.first(where: { + $0.tags.first(matchingCategory: .stereoView) == .stereoView(.rightEye) + })?.buffer + + + if let leftEyeBuffer, + let rightEyeBuffer, + case let .pixelBuffer(leftEyePixelBuffer) = leftEyeBuffer, + case let .pixelBuffer(rightEyePixelBuffer) = rightEyeBuffer { + + leftEyeImage = leftEyePixelBuffer + rightEyeImage = rightEyePixelBuffer + + let lciImage = CIImage(cvPixelBuffer: leftEyePixelBuffer) + let rciImage = CIImage(cvPixelBuffer: rightEyePixelBuffer) + + + //交叉眼 + if(type == 2){ + + let newpb = joinImages( leftImage: lciImage, rightImage:rciImage ) + let time = CMSampleBufferGetOutputPresentationTimeStamp(nextSampleBuffer) + + _ = vw!.add(image: newpb, presentationTime: time) + print( "Added frame at \(time)") + + // callback with progress + progress?( Float(time.value)/Float(duration.value)) + + // This sleep is needed to stop memory blooming - keeps around 280Mb rather than spiraling up to 8+Gig! + try await Task.sleep(nanoseconds: 3_000_000) + + } + + //红蓝立体 + if(type == 3){ + // 创建红色和蓝色滤镜 + let redColorMatrix: [CGFloat] = [ + 0.0, 0.0, 0.0, 0.0, 0.0, // 红色通道 + 0.0, 0.0, 0.0, 0.0, 0.0, // 绿色通道 + 0.0, 0.0, 1.0, 0.0, 0.0, // 蓝色通道 + 0.0, 0.0, 0.0, 1.0, 0.0 // 透明通道 + ] + + let blueColorMatrix: [CGFloat] = [ + 1.0, 0.0, 0.0, 0.0, 0.0, // 红色通道 + 0.0, 0.0, 0.0, 0.0, 0.0, // 绿色通道 + 0.0, 0.0, 0.0, 0.0, 0.0, // 蓝色通道 + 0.0, 0.0, 0.0, 1.0, 0.0 // 透明通道 + ] + + + let redFilter = CIFilter(name: "CIColorMatrix")! + redFilter.setValue(lciImage, forKey: kCIInputImageKey) + redFilter.setValue(CIVector(values: redColorMatrix, count: redColorMatrix.count), forKey: "inputRVector") + + let blueFilter = CIFilter(name: "CIColorMatrix")! + blueFilter.setValue(rciImage, forKey: kCIInputImageKey) + blueFilter.setValue(CIVector(values: blueColorMatrix, count: blueColorMatrix.count), forKey: "inputBVector") + + // 获取处理后的图像 + if let redOutputImage = redFilter.outputImage, + let blueOutputImage = blueFilter.outputImage { + + let compositeFilter = CIFilter(name: "CIScreenBlendMode")! + compositeFilter.setValue(redOutputImage, forKey: kCIInputImageKey) + compositeFilter.setValue(blueOutputImage, forKey: kCIInputBackgroundImageKey) + + let sharpenedFilter = CIFilter(name: "CISharpenLuminance")! + sharpenedFilter.setValue(compositeFilter.outputImage, forKey: kCIInputImageKey) + sharpenedFilter.setValue(2, forKey: kCIInputSharpnessKey) + +// let colorControlsFilter = CIFilter(name: "CIColorControls")! +// colorControlsFilter.setValue(sharpenedFilter.outputImage, forKey: kCIInputImageKey) +// colorControlsFilter.setValue(0.7, forKey: kCIInputSaturationKey) + + let lastImg = sharpenedFilter.outputImage! + + let time = CMSampleBufferGetOutputPresentationTimeStamp(nextSampleBuffer) + + _ = vw!.add(image: lastImg, presentationTime: time) + print( "Added frame at \(time)") + + // callback with progress + progress?( Float(time.value)/Float(duration.value)) + + // This sleep is needed to stop memory blooming - keeps around 280Mb rather than spiraling up to 8+Gig! + try await Task.sleep(nanoseconds: 3_000_000) + + } + } + + //高斯模糊 + if(type == 4){ + let filter1 = CIFilter(name: "CIGaussianBlur")! + filter1.setValue(lciImage, forKey: kCIInputImageKey) + let filter2 = CIFilter(name: "CIGaussianBlur")! + filter2.setValue(rciImage, forKey: kCIInputImageKey) + + let newpb = joinImages( leftImage: filter1.outputImage!, rightImage:filter2.outputImage! ) + let time = CMSampleBufferGetOutputPresentationTimeStamp(nextSampleBuffer) + + _ = vw!.add(image: newpb, presentationTime: time) + print( "Added frame at \(time)") + + // callback with progress + progress?( Float(time.value)/Float(duration.value)) + + // This sleep is needed to stop memory blooming - keeps around 280Mb rather than spiraling up to 8+Gig! + try await Task.sleep(nanoseconds: 3_000_000) + + } + + + + } + } + + + print( "status - \(assetReader.status)") + print( "status - \(assetReader.error?.localizedDescription ?? "None")") + print( "Finished") + _ = try await vw!.finish() + + + } + + //获取ciimage的数据 + func isSpatialImage2(from ciImage: CIImage) { + let context = CIContext() + guard let cgImage = context.createCGImage(ciImage, from: ciImage.extent) else { + return + } + let dataProvider = CGDataProvider(data: cgImage.dataProvider!.data! as CFData) + let imageSource = CGImageSourceCreateWithDataProvider(dataProvider!, nil) + let frameCount = CGImageSourceGetCount(imageSource!) + print(frameCount) + for index in 0.. (CGAffineTransform, CGSize) { + guard let track = try await asset.loadTracks(withMediaType: AVMediaType.video).first + else{throw VideoReaderError.invalidVideo} + let naturalSize = try await track.load(.naturalSize) + let naturalTransform = try await track.load(.preferredTransform) + let size = naturalSize.applying(naturalTransform) + return (naturalTransform, CGSize(width: abs(size.width), height: abs(size.height)) ) + } + + //将两张图片合成一张图片 + func joinImages( leftImage:CIImage, rightImage:CIImage) -> CIImage { + let left = UIImage(ciImage: leftImage ) + let right = UIImage(ciImage: rightImage ) + + let imageWidth = left.size.width/2 + right.size.width/2 + let imageHeight = left.size.height/2 + + let newImageSize = CGSize(width:imageWidth, height: imageHeight); + UIGraphicsBeginImageContextWithOptions(newImageSize, false, 1); + left.draw(in: CGRect(x:0, y:0, width:imageWidth/2, height:imageHeight)) + right.draw(in: CGRect(x:imageWidth/2, y:0, width:imageWidth/2, height:imageHeight)) + let image = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext(); + + let ci = CIImage(cgImage: image.cgImage!) + return ci + } +} + diff --git a/tdvideo/tdvideo/VideoConvertor3.swift b/tdvideo/tdvideo/VideoConvertor3.swift new file mode 100644 index 0000000..08fde37 --- /dev/null +++ b/tdvideo/tdvideo/VideoConvertor3.swift @@ -0,0 +1,89 @@ +// +// VideoConvertor3.swift +// tdvideo +// +// Created by mac on 2024/2/18. +// + +import Foundation +import AVKit +import VideoToolbox +import CoreImage +import ImageIO + +class VideoConvertor3 { + + + ///在立体视频中,正在处理的当前帧的左眼视图。 + var datas:NSMutableArray = NSMutableArray() + + + func convertVideo( asset : AVAsset, progress: ((Float)->())? = nil ) async throws { + + let assetReader = try AVAssetReader(asset: asset) + + //加载音轨 + let output = try await AVAssetReaderTrackOutput( + track: asset.loadTracks(withMediaType: .video).first!, + outputSettings: [ + AVVideoDecompressionPropertiesKey: [ + kVTDecompressionPropertyKey_RequestedMVHEVCVideoLayerIDs: [0, 1] as CFArray, + ], + ] + ) + assetReader.add(output) + assetReader.startReading() + + + while let nextSampleBuffer = output.copyNextSampleBuffer() { + + guard let taggedBuffers = nextSampleBuffer.taggedBuffers else { return } + + let leftEyeBuffer = taggedBuffers.first(where: { + $0.tags.first(matchingCategory: .stereoView) == .stereoView(.leftEye) + })?.buffer + let rightEyeBuffer = taggedBuffers.first(where: { + $0.tags.first(matchingCategory: .stereoView) == .stereoView(.rightEye) + })?.buffer + + if let leftEyeBuffer, + let rightEyeBuffer, + case let .pixelBuffer(leftEyePixelBuffer) = leftEyeBuffer, + case let .pixelBuffer(rightEyePixelBuffer) = rightEyeBuffer { + + let lciImage = CIImage(cvPixelBuffer: leftEyePixelBuffer) + let rciImage = CIImage(cvPixelBuffer: rightEyePixelBuffer) + + + let presentationTime = CMSampleBufferGetPresentationTimeStamp(nextSampleBuffer) + let dataDic = ["time":presentationTime,"left":lciImage,"right":rciImage] as [String : Any] + print("解码") + datas.add(dataDic) + } + } + print( "status - \(assetReader.status)") + print( "status - \(assetReader.error?.localizedDescription ?? "None")") + print( "解码完成") + progress?(0.5) + } + + + //将两张图片合成一张图片 + func joinImages( leftImage:CIImage, rightImage:CIImage) -> CIImage { + let left = UIImage(ciImage: leftImage ) + let right = UIImage(ciImage: rightImage ) + + let imageWidth = left.size.width/2 + right.size.width/2 + let imageHeight = left.size.height/2 + + let newImageSize = CGSize(width:imageWidth, height: imageHeight); + UIGraphicsBeginImageContextWithOptions(newImageSize, false, 1); + left.draw(in: CGRect(x:0, y:0, width:imageWidth/2, height:imageHeight)) + right.draw(in: CGRect(x:imageWidth/2, y:0, width:imageWidth/2, height:imageHeight)) + let image = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext(); + + return CIImage(cgImage: image.cgImage!) + } +} + diff --git a/tdvideo/tdvideo/VideoConvertor4.swift b/tdvideo/tdvideo/VideoConvertor4.swift new file mode 100644 index 0000000..38ba1bb --- /dev/null +++ b/tdvideo/tdvideo/VideoConvertor4.swift @@ -0,0 +1,125 @@ +// +// VideoConvertor4.swift +// tdvideo +// +// Created by mac on 2024/2/22. +// + +import UIKit + + +import Foundation +import AVKit +import VideoToolbox +import CoreImage +import ImageIO + +class VideoConvertor4 { + + + ///在立体视频中,正在处理的当前帧的左眼视图。 + var leftEyeImage: CVPixelBuffer? + + ///在立体视频中,正在处理的当前帧的右眼视图。 + var rightEyeImage: CVPixelBuffer? + + + func convertVideo( asset : AVAsset, outputFile: URL, progress: ((Float)->())? = nil ) async throws { + do { + try FileManager.default.removeItem(atPath: outputFile.path) + print("视频文件删除成功") + } catch { + print("删除视频文件出错:\(error)") + } + + let assetReader = try AVAssetReader(asset: asset) + + + //检查是否为空间视频 print("该视频不是空间视频或图片") + let userDataItems = try await asset.loadMetadata(for:.quickTimeMetadata) + let spacialCharacteristics = userDataItems.filter { $0.identifier?.rawValue == "mdta/com.apple.quicktime.spatial.format-version" } + if spacialCharacteristics.count == 0 { + print("不是空间视频") + } + + //获取输入视频的方向和大小(用于设置输出方向) + let (orientation, videoSize) = try await getOrientationAndResolutionSizeForVideo(asset: asset) + + //输出宽度为宽度的一半 + //我们有两个并排的视频,我们保持长宽比 + let vw:VideoWriter? + vw = VideoWriter(url: outputFile, width: Int(videoSize.width), height: Int(videoSize.height), orientation: orientation, sessionStartTime: CMTime(value: 1, timescale: 30 ), isRealTime: false, queue: .main) + + + + //加载音轨 + let output = try await AVAssetReaderTrackOutput( + track: asset.loadTracks(withMediaType: .video).first!, + outputSettings: [ + AVVideoDecompressionPropertiesKey: [ + kVTDecompressionPropertyKey_RequestedMVHEVCVideoLayerIDs: [0, 1] as CFArray, + ], + ] + ) + assetReader.add(output) + assetReader.startReading() + + let duration = try await asset.load(.duration) + + while let nextSampleBuffer = output.copyNextSampleBuffer() { + + guard let taggedBuffers = nextSampleBuffer.taggedBuffers else { return } + + let leftEyeBuffer = taggedBuffers.first(where: { + $0.tags.first(matchingCategory: .stereoView) == .stereoView(.leftEye) + })?.buffer + let rightEyeBuffer = taggedBuffers.first(where: { + $0.tags.first(matchingCategory: .stereoView) == .stereoView(.rightEye) + })?.buffer + + + if let leftEyeBuffer, + let rightEyeBuffer, + case let .pixelBuffer(leftEyePixelBuffer) = leftEyeBuffer, + case let .pixelBuffer(rightEyePixelBuffer) = rightEyeBuffer { + + leftEyeImage = leftEyePixelBuffer + rightEyeImage = rightEyePixelBuffer + + let lciImage = CIImage(cvPixelBuffer: leftEyePixelBuffer) + let rciImage = CIImage(cvPixelBuffer: rightEyePixelBuffer) + + let compositeFilter = CIFilter(name: "CIScreenBlendMode")! + compositeFilter.setValue(lciImage, forKey: kCIInputImageKey) + compositeFilter.setValue(rciImage, forKey: kCIInputBackgroundImageKey) + let time = CMSampleBufferGetOutputPresentationTimeStamp(nextSampleBuffer) + _ = vw!.add(image: compositeFilter.outputImage!, presentationTime: time) + + print( "Added frame at \(time)") + progress?( Float(time.value)/Float(duration.value)) + try await Task.sleep(nanoseconds: 3_000_000) + + } + } + + print( "status - \(assetReader.status)") + print( "status - \(assetReader.error?.localizedDescription ?? "None")") + print( "Finished") + _ = try await vw!.finish() + + + } + + + func getOrientationAndResolutionSizeForVideo(asset:AVAsset) async throws -> (CGAffineTransform, CGSize) { + guard let track = try await asset.loadTracks(withMediaType: AVMediaType.video).first + else{throw VideoReaderError.invalidVideo} + let naturalSize = try await track.load(.naturalSize) + let naturalTransform = try await track.load(.preferredTransform) + let size = naturalSize.applying(naturalTransform) + return (naturalTransform, CGSize(width: abs(size.width), height: abs(size.height)) ) + } + + +} + diff --git a/tdvideo/tdvideo/VideoFile.swift b/tdvideo/tdvideo/VideoFile.swift new file mode 100644 index 0000000..ef4ee11 --- /dev/null +++ b/tdvideo/tdvideo/VideoFile.swift @@ -0,0 +1,35 @@ +// +// VideoFile.swift +// vp +// +// Created by soldoros on 2024/1/17. +// + + +import SwiftUI +import UniformTypeIdentifiers + +///一个' FileDocument '的空实现,它允许SwiftUI的' fileexporters '来验证我们是否可以写入输出目的地。 +struct VideoFile: FileDocument { + static var readableContentTypes: [UTType] = [.movie, .quickTimeMovie, .mpeg4Movie] + + // empty data + var data: Data + + init() { + self.data = Data() + } + + init(configuration: ReadConfiguration) throws { + if let readData = configuration.file.regularFileContents { + data = readData + } else { + data = Data() + } + } + + func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { + FileWrapper(regularFileWithContents: data) + } +} + diff --git a/tdvideo/tdvideo/VideoPlayer.swift b/tdvideo/tdvideo/VideoPlayer.swift new file mode 100644 index 0000000..83bec31 --- /dev/null +++ b/tdvideo/tdvideo/VideoPlayer.swift @@ -0,0 +1,7 @@ +// +// VideoPlayer.swift +// tdvideo +// +// Created by mac on 2024/2/18. +// + diff --git a/tdvideo/tdvideo/VideoPreview.swift b/tdvideo/tdvideo/VideoPreview.swift new file mode 100644 index 0000000..96bd612 --- /dev/null +++ b/tdvideo/tdvideo/VideoPreview.swift @@ -0,0 +1,40 @@ +// +// VideoPreview.swift +// vp +// +// Created by soldoros on 2024/1/17. +// + + +import AVFoundation +import MetalKit +import SwiftUI +// +//// 普通的 Swift 视图,用于渲染视频预览 +//struct VideoPreviewView: NSViewRepresentable { +// // 正在处理的当前帧的左眼视图。 +// var leftEyePreviewImage: CVPixelBuffer +// +// // 正在处理的当前帧的右眼视图。 +// var rightEyePreviewImage: CVPixelBuffer +// +// // 创建并返回底层 Metal 视图 +// func makeNSView(context: Context) -> MetalPlayer { +// let ciImage = CIImage(cvPixelBuffer: leftEyePreviewImage) +// let frame = CGRect( +// x: 0, +// y: 0, +// width: ciImage.extent.width * 2, +// height: ciImage.extent.height +// ) +// return MetalPlayer(frame: frame) +// } +// +// // 更新底层的 Metal 视图 +// func updateNSView(_ nsView: MetalPlayer, context: Context) { +// nsView.render( +// leftPixelBuffer: leftEyePreviewImage, +// rightPixelBuffer: rightEyePreviewImage +// ) +// } +//} diff --git a/tdvideo/tdvideo/VideoWriter.swift b/tdvideo/tdvideo/VideoWriter.swift new file mode 100644 index 0000000..b53c3fb --- /dev/null +++ b/tdvideo/tdvideo/VideoWriter.swift @@ -0,0 +1,161 @@ +// +// VideoWriter.swift +// SpacialVideoConvertor +// +// Created by Andy Qua on 04/01/2024. +// + +// Based on code from xaphod/VideoWriter.swift - https://gist.github.com/xaphod/de83379cc982108a5b38115957a247f9 +//图片转视频 + +import Foundation +import AVFoundation +import CoreImage + +class VideoWriter { + fileprivate var writer: AVAssetWriter + fileprivate var writerInput: AVAssetWriterInput + fileprivate var pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor + fileprivate let queue: DispatchQueue + static var ciContext = CIContext.init() // we reuse a single context for performance reasons + + let pixelSize: CGSize + var lastPresentationTime: CMTime? + + init?(url: URL, width: Int, height: Int, orientation: CGAffineTransform, sessionStartTime: CMTime, isRealTime: Bool, queue: DispatchQueue) { + print("VideoWriter init: width=\(width) height=\(height), url=\(url)") + self.queue = queue + let outputSettings: [String:Any] = [ + AVVideoCodecKey : AVVideoCodecType.h264, // or .hevc if you like + AVVideoWidthKey : width, + AVVideoHeightKey: height, + ] + self.pixelSize = CGSize.init(width: width, height: height) + let input = AVAssetWriterInput.init(mediaType: .video, outputSettings: outputSettings) + input.expectsMediaDataInRealTime = isRealTime + input.transform = orientation + + guard + let writer = try? AVAssetWriter.init(url: url, fileType: .mp4), + writer.canAdd(input), + sessionStartTime != .invalid + else { + return nil + } + + let sourceBufferAttributes: [String:Any] = [ + String(kCVPixelBufferPixelFormatTypeKey) : kCVPixelFormatType_32ARGB, // yes, ARGB is right here for images... + String(kCVPixelBufferWidthKey) : width, + String(kCVPixelBufferHeightKey) : height, + ] + let pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor.init(assetWriterInput: input, sourcePixelBufferAttributes: sourceBufferAttributes) + self.pixelBufferAdaptor = pixelBufferAdaptor + + writer.add(input) + writer.startWriting() + writer.startSession(atSourceTime: sessionStartTime) + + if let error = writer.error { + NSLog("VideoWriter init: ERROR - \(error)") + return nil + } + + self.writer = writer + self.writerInput = input + } + + func add(image: CIImage, presentationTime: CMTime) -> Bool { + if self.writerInput.isReadyForMoreMediaData == false { + return false + } + if self.pixelBufferAdaptor.appendPixelBufferForImage(image, presentationTime: presentationTime) { + self.lastPresentationTime = presentationTime + return true + } + return false + } + + func add(buffer: CVPixelBuffer, presentationTime: CMTime) -> Bool { + if self.writerInput.isReadyForMoreMediaData == false { + return false + } + if self.pixelBufferAdaptor.append(buffer, withPresentationTime: presentationTime) { + self.lastPresentationTime = presentationTime + return true + } + return false + } + + func add(sampleBuffer: CMSampleBuffer) -> Bool { + if self.writerInput.isReadyForMoreMediaData == false { + print("VideoWriter: not ready for more data") + return false + } + + if self.writerInput.append(sampleBuffer) { + self.lastPresentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + return true + } + return false + } + + func finish() async throws -> AVAsset? { + writerInput.markAsFinished() + print("VideoWriter: calling writer.finishWriting()") + await writer.finishWriting() + if self.writer.status != .completed { + print("VideoWriter finish: error in finishWriting - \(self.writer.error?.localizedDescription ?? "Unknown")") + return nil + } + + let asset = AVURLAsset.init(url: self.writer.outputURL, options: [AVURLAssetPreferPreciseDurationAndTimingKey : true]) + let duration = try await CMTimeGetSeconds( asset.load(.duration) ) + // can check for minimum duration here (ie. consider a failure if too short) + print("VideoWriter: finishWriting() complete, duration=\(duration)") + return asset + } +} + +extension AVAssetWriterInputPixelBufferAdaptor { + func appendPixelBufferForImage(_ image: CIImage, presentationTime: CMTime) -> Bool { + var appendSucceeded = false + + autoreleasepool { + guard let pixelBufferPool = self.pixelBufferPool else { + print("appendPixelBufferForImage: ERROR - missing pixelBufferPool") // writer can have error: writer.error=\(String(describing: self.writer.error)) + return + } + + let pixelBufferPointer = UnsafeMutablePointer.allocate(capacity: 1) + let status: CVReturn = CVPixelBufferPoolCreatePixelBuffer( + kCFAllocatorDefault, + pixelBufferPool, + pixelBufferPointer + ) + + if let pixelBuffer = pixelBufferPointer.pointee, status == 0 { + pixelBuffer.fillPixelBufferFromImage(image) + appendSucceeded = self.append(pixelBuffer, withPresentationTime: presentationTime) + if !appendSucceeded { + // If a result of NO is returned, clients can check the value of AVAssetWriter.status to determine whether the writing operation completed, failed, or was cancelled. If the status is AVAssetWriterStatusFailed, AVAsset.error will contain an instance of NSError that describes the failure. + print("VideoWriter appendPixelBufferForImage: ERROR appending") + } + pixelBufferPointer.deinitialize(count: 1) + } else { + print("VideoWriter appendPixelBufferForImage: ERROR - Failed to allocate pixel buffer from pool, status=\(status)") // -6680 = kCVReturnInvalidPixelFormat + } + pixelBufferPointer.deallocate() + } + return appendSucceeded + } +} + +extension CVPixelBuffer { + func fillPixelBufferFromImage(_ image: CIImage) { + CVPixelBufferLockBaseAddress(self, []) + + VideoWriter.ciContext.render(image, to: self) + + CVPixelBufferUnlockBaseAddress(self, []) + } +} diff --git a/tdvideo/tdvideo/ViewController.swift b/tdvideo/tdvideo/ViewController.swift new file mode 100644 index 0000000..ce097b3 --- /dev/null +++ b/tdvideo/tdvideo/ViewController.swift @@ -0,0 +1,112 @@ +// +// ViewController.swift +// tdvideo +// +// Created by aaa on 2024/1/19. +// https://www.finnvoorhees.com/words/reading-and-writing-spatial-video-with-avfoundation#reading-spatial-video-using-avassetreader + +//import UIKit +//import AVKit +//import AVFoundation +//import CoreImage +//import Foundation +//import Observation +//import VideoToolbox +//import CoreMedia + +import UIKit + +class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { + let options = ["视频转码", "图片转码", "设备投流","视频导出","普通图片合成空间图片","普通视频合成空间视频","拍摄空间图片","拍摄空间视频","边转边播"] + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = UIColor.brown + + let tableView = UITableView(frame: view.bounds, style: .plain) + tableView.delegate = self + tableView.dataSource = self + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") + view.addSubview(tableView) + } + + // MARK: - UITableViewDataSource + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return options.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) + cell.textLabel?.text = options[indexPath.row] + return cell + } + + // MARK: - UITableViewDelegate + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + //视频转码 + if(indexPath.row == 0){ + let vc:ViewController2 = ViewController2() + self.present(vc, animated: true, completion: nil) + + } + //图片转码 + if(indexPath.row == 1){ + let vc:PlayController = PlayController() + self.present(vc, animated: true, completion: nil) + + } + //设备连接 + if(indexPath.row == 2){ + let vc = PlayContoller4() + self.present(vc, animated: true, completion: nil) + + } + //视频导出 + if(indexPath.row == 3){ + let vc = PlayContoller6() + self.present(vc, animated: true, completion: nil) + + } + //两张普通图片合成空间图片 + if(indexPath.row == 4){ + let vc = PlayContoller7() + self.present(vc, animated: true, completion: nil) + + } + //两个普通视频合成空间视频 + if(indexPath.row == 5){ + let vc = PlayContoller10() + self.present(vc, animated: true, completion: nil) + + } + + //拍摄空间图片 + if(indexPath.row == 6){ + let vc = PlayContoller5() + self.present(vc, animated: true, completion: nil) + + } + //拍摄空间视频 + if(indexPath.row == 7){ + let vc = PlayContoller9() + self.present(vc, animated: true, completion: nil) + + } + + //边转边播 + if(indexPath.row == 8){ + let vc = PlayContoller8() + self.present(vc, animated: true, completion: nil) + + } + //边拍边转 + if(indexPath.row == 9){ + let vc = PlayContoller11() + self.present(vc, animated: true, completion: nil) + + } + } + +} diff --git a/tdvideo/tdvideo/tdvideo.entitlements b/tdvideo/tdvideo/tdvideo.entitlements new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/tdvideo/tdvideo/tdvideo.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/tdvideo/tdvideo/转码/PlayController.swift b/tdvideo/tdvideo/转码/PlayController.swift new file mode 100644 index 0000000..2687e09 --- /dev/null +++ b/tdvideo/tdvideo/转码/PlayController.swift @@ -0,0 +1,505 @@ +// +// PlayController.swift +// tdvideo +// +// Created by aaa on 2024/1/25. +// + +import Foundation +import AVKit +import VideoToolbox +import CoreVideo +import UIKit +import ImageIO +import CoreImage +import Photos + + +//图片转码 +class PlayController: UIViewController { + + var imgData:Data? + + ///在立体视频中,正在处理的当前帧的左眼视图。 + var leftEyeImage: CVPixelBuffer? + + ///在立体视频中,正在处理的当前帧的右眼视图。 + var rightEyeImage: CVPixelBuffer? + + //滤镜 + var lvjing = "CIGaussianBlur" + + //空间视频 交叉眼 红蓝立体 高斯模糊 + var type = 0 + +// var playerItem:AVPlayerItem? + + var playerLay:AVPlayerLayer? + var player:AVPlayer = AVPlayer() + var btn3:UIButton? + + var sourceVideoURL:URL? + var outputVideoURL:URL? + var playerLooper: AVPlayerLooper? + + var mImgView: UIImageView? + + + + //判断是不是空间照片 非空间照片只有一帧 + //空间照片包含 let makerAppleProperties = imageProperties["{HEIF}"] + func isSpatialImage(imageURL: URL) -> Bool { + + guard let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, nil) else { + return false + } + guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 1, nil) as? [CFString: Any] else { + return false + } + print(properties) + /* + [ProfileName: sRGB IEC61966-2.1, {TIFF}: { + Orientation = 1; + TileLength = 512; + TileWidth = 512; + }, PixelWidth: 4032, PixelHeight: 3024, {HEIF}: { + CameraExtrinsics = { + CoordinateSystemID = 0; + Position = ( + "-0.019238", + 0, + 0 + ); + Rotation = ( + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1 + ); + }; + }, Depth: 8, Orientation: 1, ColorModel: RGB] + + + 判断是否包含:{HEIF} 代表空间图片 + + */ + + //这里判断两张图片,gif也可能是两张 +// let frameCount = CGImageSourceGetCount(imageSource) +// if(frameCount == 1){ +// return false +// } + return true + } + + + override func viewDidLoad() { + super.viewDidLoad() + self.view.backgroundColor = UIColor.brown + + + let path = Bundle.main.path(forResource: "img3", ofType: "HEIC") + sourceVideoURL = URL.init(filePath: path!) + outputVideoURL = URL.documentsDirectory.appending(path:"output11114.jpg") + let nsdata = NSData(contentsOf: sourceVideoURL!) + imgData = nsdata as? Data + + let isSpatial = isSpatialImage(imageURL: sourceVideoURL!) + if !isSpatial { + print("这不是一张空间图片") + return + } + + + mImgView = UIImageView() + mImgView!.frame = CGRect.init(x: 0, y: 100, width: self.view.frame.size.width, height: 180) + self.view.addSubview(mImgView!) + let image = UIImage(contentsOfFile: sourceVideoURL!.path) + mImgView!.image = image + mImgView!.isUserInteractionEnabled = true + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(imageTapped(_:))) + mImgView!.addGestureRecognizer(tapGesture) + + // 创建分段选择器 + let segmentedControl = UISegmentedControl(items: ["空间照片", "平行眼", "交叉眼", "红蓝立体"]) + // 设置分段选择器的位置和大小 + segmentedControl.frame = CGRect(x: 20, y: 700, width: 360, height: 45) + // 设置默认选中的分段 + segmentedControl.selectedSegmentIndex = 0 + // 添加分段选择器到视图中 + self.view.addSubview(segmentedControl) +segmentedControl.layer.borderWidth = 1.0 // 边框宽度 + segmentedControl.layer.borderColor = UIColor.blue.cgColor // 边框颜色 + segmentedControl.tintColor = UIColor.blue // 选中状态颜色 +let normalTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white] + let selectedTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.blue] + + segmentedControl.setTitleTextAttributes(normalTextAttributes, for: .normal) + segmentedControl.setTitleTextAttributes(selectedTextAttributes, for: .selected) + // 添加分段选择器的事件处理 + segmentedControl.addTarget(self, action: #selector(segmentedControlValueChanged(_:)), for: .valueChanged) + } + + @objc func imageTapped(_ sender: UITapGestureRecognizer) { + + let vc:PlayControllerImg = PlayControllerImg() + self.present(vc, animated: true, completion: nil) + vc.mediaSelectedHandler = { [self]data in + print("回调") + print(data) + imgData = data + let image = UIImage(data: imgData!) + mImgView!.image = image + } + } + + //"空间照片", "平行眼", "交叉眼", "红蓝立体" + @objc func segmentedControlValueChanged(_ sender: UISegmentedControl) { + // 处理分段选择器值改变事件 + let selectedIndex = sender.selectedSegmentIndex + print("选中了第 \(selectedIndex) 个选项") + player.pause() + NotificationCenter.default.removeObserver(self) + mImgView!.frame = CGRect.init(x: 0, y: 100, width: self.view.frame.size.width, height: 180) + +// guard let imageSource = CGImageSourceCreateWithURL(sourceVideoURL! as CFURL, nil) else { +// return +// } + + guard let imageSource = CGImageSourceCreateWithData(imgData! as CFData, nil) else { + return + } + + print(imageSource) + + let frameCount = CGImageSourceGetCount(imageSource) + + var frames: [CGImage] = [] + + for index in 0.. CVPixelBuffer? { + let options: [String: Any] = [ + kCVPixelBufferCGImageCompatibilityKey as String: true, + kCVPixelBufferCGBitmapContextCompatibilityKey as String: true + ] + + var pixelBuffer: CVPixelBuffer? + let status = CVPixelBufferCreate(kCFAllocatorDefault, + Int(frame.width), + Int(frame.height), + kCVPixelFormatType_32BGRA, + options as CFDictionary, + &pixelBuffer) + + guard status == kCVReturnSuccess, let buffer = pixelBuffer else { + return nil + } + + CVPixelBufferLockBaseAddress(buffer, []) + let pixelData = CVPixelBufferGetBaseAddress(buffer) + let colorSpace = CGColorSpaceCreateDeviceRGB() + + guard let context = CGContext(data: pixelData, + width: Int(frame.width), + height: Int(frame.height), + bitsPerComponent: 8, + bytesPerRow: CVPixelBufferGetBytesPerRow(buffer), + space: colorSpace, + bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue) else { + return nil + } + + context.translateBy(x: -frame.origin.x, y: -frame.origin.y) + context.draw(image.cgImage!, in: CGRect(origin: .zero, size: image.size)) + + CVPixelBufferUnlockBaseAddress(buffer, []) + + return buffer + } + + + + //将两张图片合成一张图片 + func joinImages2( leftImage:CIImage, rightImage:CIImage) -> CIImage { + let left = UIImage(ciImage: leftImage ) + let right = UIImage(ciImage: rightImage ) + + let imageWidth = left.size.width/2 + right.size.width/2 + let imageHeight = left.size.height/2 + + let newImageSize = CGSize(width:imageWidth, height: imageHeight); + UIGraphicsBeginImageContextWithOptions(newImageSize, false, 1); + left.draw(in: CGRect(x:0, y:0, width:imageWidth/2, height:imageHeight)) + right.draw(in: CGRect(x:imageWidth/2, y:0, width:imageWidth/2, height:imageHeight)) + let image = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext(); + + let ci = CIImage(cgImage: image.cgImage!) + return ci + } + + @objc func buttonPressed(sender:UIButton){ + + if(sender.tag == 10){ + let vc:PlayContoller4 = PlayContoller4() + self.navigationController?.pushViewController(vc, animated: true) + } + } + + func convertVideo( inputFile : URL, outputFile: URL, progress: ((Float)->())? = nil ) async throws { + do { + try FileManager.default.removeItem(atPath: outputFile.path) + print("视频文件删除成功") + } catch { + print("删除视频文件出错:\(error)") + } + + // Load the AVAsset + let asset = AVAsset(url: inputFile) + let assetReader = try AVAssetReader(asset: asset) + + + //检查是否为空间视频 + let userDataItems = try await asset.loadMetadata(for:.quickTimeMetadata) + let spacialCharacteristics = userDataItems.filter { $0.identifier?.rawValue == "mdta/com.apple.quicktime.spatial.format-version" } + if spacialCharacteristics.count == 0 { + print("该视频不是空间视频") + } + + //获取输入视频的方向和大小(用于设置输出方向) + let (orientation, videoSize) = try await getOrientationAndResolutionSizeForVideo(asset: asset) + + //输出宽度为宽度的一半 + //我们有两个并排的视频,我们保持长宽比 + let vw:VideoWriter? + if(type == 3){ + vw = VideoWriter(url: outputFile, width: Int(videoSize.width), height: Int(videoSize.height), orientation: orientation, sessionStartTime: CMTime(value: 1, timescale: 30 ), isRealTime: false, queue: .main) + } + else{ + vw = VideoWriter(url: outputFile, width: Int(videoSize.width), height: Int(videoSize.height/2), orientation: orientation, sessionStartTime: CMTime(value: 1, timescale: 30 ), isRealTime: false, queue: .main) + } + + //加载音轨 + let output = try await AVAssetReaderTrackOutput( + track: asset.loadTracks(withMediaType: .video).first!, + outputSettings: [ + AVVideoDecompressionPropertiesKey: [ + kVTDecompressionPropertyKey_RequestedMVHEVCVideoLayerIDs: [0, 1] as CFArray, + ], + ] + ) + assetReader.add(output) + assetReader.startReading() + let duration = try await asset.load(.duration) + + if let playerItem = player.currentItem { + playerItem.videoComposition = AVVideoComposition(asset: playerItem.asset) { request in + + print(request.sourceImage) + } + } + + while let nextSampleBuffer = output.copyNextSampleBuffer() { + guard let taggedBuffers = nextSampleBuffer.taggedBuffers else { return } + + let leftEyeBuffer = taggedBuffers.first(where: { + $0.tags.first(matchingCategory: .stereoView) == .stereoView(.leftEye) + })?.buffer + let rightEyeBuffer = taggedBuffers.first(where: { + $0.tags.first(matchingCategory: .stereoView) == .stereoView(.rightEye) + })?.buffer + + if let leftEyeBuffer, + let rightEyeBuffer, + case let .pixelBuffer(leftEyePixelBuffer) = leftEyeBuffer, + case let .pixelBuffer(rightEyePixelBuffer) = rightEyeBuffer { + + let lciImage = CIImage(cvPixelBuffer: leftEyePixelBuffer) + let rciImage = CIImage(cvPixelBuffer: rightEyePixelBuffer) + //交叉眼 + let newpb = joinImages( leftImage: lciImage, rightImage:rciImage ) + let time = CMSampleBufferGetOutputPresentationTimeStamp(nextSampleBuffer) + _ = vw!.add(image: newpb, presentationTime: time) +// print( "Added frame at \(time)") + progress?( Float(time.value)/Float(duration.value)) + + +// try await Task.sleep(nanoseconds: 3_000_000) + } + } + + _ = try await vw!.finish() + print( "Finished") + + + } + + + func getOrientationAndResolutionSizeForVideo(asset:AVAsset) async throws -> (CGAffineTransform, CGSize) { + guard let track = try await asset.loadTracks(withMediaType: AVMediaType.video).first + else{throw VideoReaderError.invalidVideo} + let naturalSize = try await track.load(.naturalSize) + let naturalTransform = try await track.load(.preferredTransform) + let size = naturalSize.applying(naturalTransform) + return (naturalTransform, CGSize(width: abs(size.width), height: abs(size.height)) ) + } + + + func convertCIImageToUIImage(ciImage: CIImage) -> UIImage? { + let context = CIContext(options: nil) + if let cgImage = context.createCGImage(ciImage, from: ciImage.extent) { + let uiImage = UIImage(cgImage: cgImage) + return uiImage + } + return nil + } + + //将两张图片合成一张图片 + func joinImages( leftImage:CIImage, rightImage:CIImage) -> CIImage { + let left = UIImage(ciImage: leftImage ) + let right = UIImage(ciImage: rightImage ) + + let imageWidth = left.size.width/2 + right.size.width/2 + let imageHeight = left.size.height/2 + + let newImageSize = CGSize(width:imageWidth, height: imageHeight); + UIGraphicsBeginImageContextWithOptions(newImageSize, false, 1); + left.draw(in: CGRect(x:0, y:0, width:imageWidth/2, height:imageHeight)) + right.draw(in: CGRect(x:imageWidth/2, y:0, width:imageWidth/2, height:imageHeight)) + let image = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext(); + + let ci = CIImage(cgImage: image.cgImage!) + return ci + } + + func pixelBuffer(from ciImage: CIImage) -> CVPixelBuffer? { + var pixelBuffer: CVPixelBuffer? + let attributes: [String: Any] = [ + kCVPixelBufferCGImageCompatibilityKey as String: kCFBooleanTrue, + kCVPixelBufferCGBitmapContextCompatibilityKey as String: kCFBooleanTrue + ] + + let width = Int(ciImage.extent.width) + let height = Int(ciImage.extent.height) + + let status = CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_32ARGB, attributes as CFDictionary, &pixelBuffer) + + if status == kCVReturnSuccess, let pixelBuffer = pixelBuffer { + let context = CIContext() + context.render(ciImage, to: pixelBuffer) + return pixelBuffer + } + + return nil + } + +} + diff --git a/tdvideo/tdvideo/转码/PlayControllerImg.swift b/tdvideo/tdvideo/转码/PlayControllerImg.swift new file mode 100644 index 0000000..8c4f0a5 --- /dev/null +++ b/tdvideo/tdvideo/转码/PlayControllerImg.swift @@ -0,0 +1,283 @@ +// +// PlayController2.swift +// tdvideo +// +// Created by mac on 2024/2/1. +// +/* + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let image = photos[indexPath.item] + let h = isSpatialImage(from: image) + self.navigationController?.popViewController(animated: true) + } + + // 更新单元格的图像 + DispatchQueue.main.async { [self] in + + collectionView.reloadData() + } + + */ + + +import UIKit +import Photos +import ImageIO +import CoreFoundation +import UIKit +import Photos +import ImageIO +import CoreGraphics +import MobileCoreServices +import AVKit + +class PhotoCell: UICollectionViewCell { + + + let imageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + imageView.clipsToBounds = true + return imageView + }() + + let frameCountLabel: UILabel = { + let label = UILabel() + label.textColor = .white + label.backgroundColor = .red + label.textAlignment = .center + label.font = UIFont.boldSystemFont(ofSize: 12) + label.layer.cornerRadius = 8 + label.clipsToBounds = true + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setupViews() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupViews() + } + + private func setupViews() { + addSubview(imageView) + addSubview(frameCountLabel) + + imageView.translatesAutoresizingMaskIntoConstraints = false + frameCountLabel.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + imageView.topAnchor.constraint(equalTo: topAnchor), + imageView.leadingAnchor.constraint(equalTo: leadingAnchor), + imageView.trailingAnchor.constraint(equalTo: trailingAnchor), + imageView.bottomAnchor.constraint(equalTo: bottomAnchor), + + frameCountLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8), + frameCountLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8), + frameCountLabel.widthAnchor.constraint(equalToConstant: 40), + frameCountLabel.heightAnchor.constraint(equalToConstant: 20) + ]) + } +} + +class PlayControllerImg: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + var collectionView: UICollectionView! + var fetchResult: PHFetchResult! + var photos: [UIImage] = [] + + var mediaSelectedHandler: ((Data) -> Void)? + + override func viewDidLoad() { + super.viewDidLoad() + setupCollectionView() + fetchPhotos() + collectionView.dataSource = self + collectionView.delegate = self + + } + + private func setupCollectionView() { + let layout = UICollectionViewFlowLayout() + layout.minimumLineSpacing = 10 + layout.minimumInteritemSpacing = 10 + collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) + collectionView.backgroundColor = .white + collectionView.register(PhotoCell.self, forCellWithReuseIdentifier: "PhotoCell") + view.addSubview(collectionView) + + collectionView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } + + // PHAsset.fetchAssets(with: .image, options: fetchOptions) + // PHAsset.fetchAssets(with: fetchOptions) + func fetchPhotos() { + let fetchOptions = PHFetchOptions() + fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] + fetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions) + + for index in 0.. Bool { + let metadata = asset.metadata(forFormat: AVMetadataFormat.quickTimeMetadata) + let isSpatialVideo = metadata.contains { item in + if let identifier = item.identifier?.rawValue { + return identifier == "mdta/com.apple.quicktime.spatial.format-version" + } + return false + } + return isSpatialVideo + } + + func isSSVideo(asset:AVAsset)async throws->Bool{ + + let userDataItems = try await asset.loadMetadata(for:.quickTimeMetadata) + let spacialCharacteristics = userDataItems.filter { $0.identifier?.rawValue == "mdta/com.apple.quicktime.spatial.format-version" } + if spacialCharacteristics.count == 0 { + return false + } + return true + } + + // MARK: - UICollectionViewDataSource + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return photos.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PhotoCell", for: indexPath) as! PhotoCell + cell.imageView.image = photos[indexPath.item] + + let frameCount = getFrameCount(for: indexPath.item) + cell.frameCountLabel.isHidden = frameCount <= 1 + cell.frameCountLabel.text = "\(frameCount)" + + return cell + } + + // MARK: - UICollectionViewDelegateFlowLayout + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + let width = collectionView.bounds.width / 3 - 10 + return CGSize(width: width, height: width) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return 10 + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + return 10 + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + // 获取选中的视频资源 + let asset = fetchResult.object(at: indexPath.item) + + //返回图片 + if asset.mediaType == .image { + let requestOptions = PHImageRequestOptions() + requestOptions.isSynchronous = true + requestOptions.deliveryMode = .highQualityFormat + + PHImageManager.default().requestImageData(for: asset, options: requestOptions) { (imageData, _, _, _) in + + if let imageData = imageData { + self.mediaSelectedHandler?(imageData) + } + // 关闭控制器 + DispatchQueue.main.async { + self.dismiss(animated: true, completion: nil) + } + } + } + + + } + + private func getMediaURL(from asset: PHAsset, completion: @escaping (URL) -> Void) { + if asset.mediaType == .video { + let requestOptions = PHVideoRequestOptions() + requestOptions.isNetworkAccessAllowed = true + + PHImageManager.default().requestAVAsset(forVideo: asset, options: requestOptions) { (avAsset, _, _) in + if let avAsset = avAsset as? AVURLAsset { + let mediaURL = avAsset.url + completion(mediaURL) + } + } + } else { + + } + } + + + + private func getFrameCount(for index: Int) -> Int { + let asset = fetchResult.object(at: index) + if let imageData = getImageData(for: asset) { + if let cgImageSource = CGImageSourceCreateWithData(imageData as CFData, nil) { + return CGImageSourceGetCount(cgImageSource) + } + } + return 0 + } + + private func getImageData(for asset: PHAsset) -> Data? { + var imageData: Data? + let requestOptions = PHImageRequestOptions() + requestOptions.isSynchronous = true + requestOptions.deliveryMode = .highQualityFormat + + PHImageManager.default().requestImageData(for: asset, options: requestOptions) { (data, _, _, _) in + imageData = data + } + + return imageData + } + + + func convertCGImageToCFData(cgImage: CGImage) -> CFData? { + let data = CFDataCreateMutable(kCFAllocatorDefault, 0) + + if let data = data { + if let destination = CGImageDestinationCreateWithData(data, kUTTypePNG, 1, nil) { + CGImageDestinationAddImage(destination, cgImage, nil) + CGImageDestinationFinalize(destination) + } + } + + return data + } +} diff --git a/tdvideo/tdvideo/转码/ViewController2.swift b/tdvideo/tdvideo/转码/ViewController2.swift new file mode 100644 index 0000000..2afeaad --- /dev/null +++ b/tdvideo/tdvideo/转码/ViewController2.swift @@ -0,0 +1,299 @@ +// +// ViewController2.swift +// tdvideo +// +// Created by mac on 2024/2/4. +// 视频转码 + + +import UIKit +import AVKit +import AVFoundation +import CoreImage +import Foundation +import Observation +import VideoToolbox +import CoreMedia +import MobileCoreServices + +//视频转码 +class ViewController2: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { + +// let convertor = VideoConvertor() + let convertor2 = VideoConvertor2() + + let videoConverter = SpatialVideoConverter() + var metview:MetalPlayer? + + var videoOriginalAsset:AVAsset? + var videoTempAsset:AVAsset? + + var sourceVideoURL:URL? + var outputVideoURL:URL? + + + + var playerLay:AVPlayerLayer? + var player:AVPlayer = AVPlayer() + var btn3:UIButton? + +// var selectedIndex = 0 + + func startObserving() { + let nc = NotificationCenter.default + nc.addObserver(self, selector: #selector(handleNotification(_:)), name: NSNotification.Name(rawValue: "upvideo"), object: nil) + } + + @objc func handleNotification(_ notification: Notification) { + // 处理接收到的通知 + if notification.name.rawValue == "upvideo" { + + + } + } + + override func viewDidLoad() { + super.viewDidLoad() +// startObserving() + self.view.backgroundColor = UIColor.brown + //是否支持空间视频编/解码功能: +// print(VTIsStereoMVHEVCEncodeSupported()) + + let btn1 = UIButton.init(frame: CGRect.init(x: 20, y: 120, width: 180, height: 50)) + btn1.setTitle("从相册选择视频", for: UIControl.State.normal) + self.view.addSubview(btn1) + btn1.tag = 10 + btn1.addTarget(self, action: #selector(buttonPressed(sender:)), for: UIControl.Event.touchUpInside) +// +// let btn2 = UIButton.init(frame: CGRect.init(x: 250, y: 120, width: 100, height: 50)) +// btn2.setTitle("开始播放", for: UIControl.State.normal) +// self.view.addSubview(btn2) +// btn2.tag = 11 +// btn2.addTarget(self, action: #selector(buttonPressed(sender:)), for: UIControl.Event.touchUpInside) + + + btn3 = UIButton.init(frame: CGRect.init(x: 150, y: 60, width: 180, height: 50)) + btn3!.setTitle("进度=0.0", for: UIControl.State.normal) + self.view.addSubview(btn3!) + btn3!.tag = 12 + + + let path = Bundle.main.path(forResource: "IMG_0071", ofType: "MOV") + sourceVideoURL = URL.init(filePath: path!) + outputVideoURL = URL.documentsDirectory.appending(path:"output1111.mp4") + videoOriginalAsset = AVAsset(url: sourceVideoURL!) + videoTempAsset = videoOriginalAsset + + + + + // 创建分段选择器 + let segmentedControl = UISegmentedControl(items: ["立体视频","空间视频", "交叉眼", "红蓝立体","高斯模糊"]) + // 设置分段选择器的位置和大小 + segmentedControl.frame = CGRect(x: 20, y: 700, width: 360, height: 45) + // 设置默认选中的分段 + segmentedControl.selectedSegmentIndex = 0 + // 添加分段选择器到视图中 + self.view.addSubview(segmentedControl) + segmentedControl.layer.borderWidth = 1.0 // 边框宽度 + segmentedControl.layer.borderColor = UIColor.blue.cgColor // 边框颜色 + segmentedControl.tintColor = UIColor.blue // 选中状态颜色 + let normalTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white] + let selectedTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.blue] + + segmentedControl.setTitleTextAttributes(normalTextAttributes, for: .normal) + segmentedControl.setTitleTextAttributes(selectedTextAttributes, for: .selected) + // 添加分段选择器的事件处理 + segmentedControl.addTarget(self, action: #selector(segmentedControlValueChanged(_:)), for: .valueChanged) + + + + playerLay = AVPlayerLayer() + playerLay!.backgroundColor = UIColor.black.cgColor + playerLay!.frame = CGRect.init(x: 10, y: 180, width: 350, height: 380) + self.view.layer.addSublayer(playerLay!) + playerLay!.cornerRadius = 6 + + play() +// AVPlayerVideoOutput() + +// player.addPeriodicTimeObserver( +// forInterval: CMTime(value: 1, timescale: 30), +// queue: .main +// ) { _ in +// +// +// } + + + } + + + + //播放 + func play(){ + + let playerItem = AVPlayerItem(asset: videoTempAsset!) + playerLay!.player = AVPlayer(playerItem: playerItem) + playerLay!.player!.play() + } + + + @objc func segmentedControlValueChanged(_ sender: UISegmentedControl) { + // 处理分段选择器值改变事件 + let selectedIndex = sender.selectedSegmentIndex + print("选中了第 \(selectedIndex) 个选项") + player.pause() + NotificationCenter.default.removeObserver(self) + + //立体视频 + if(selectedIndex == 0){ + videoTempAsset = videoOriginalAsset + play() + } + else{ + outputVideoURL = URL.documentsDirectory.appending(path:"output11112.mp4") + } + + //空间视频 + if(selectedIndex == 1){ +// Task { +// try await videoConverter.convertStereoscopicVideoToSpatialVideo(sourceVideoURL: sourceVideoURL!,outputVideoURL: outputVideoURL!){[weak self] progress in +// print(progress) +// DispatchQueue.main.async { [weak self] in +// self!.btn3!.setTitle("进度=" + String(progress), for: UIControl.State.normal) +// if(progress > 0.99){ +// self!.videoTempAsset = AVAsset(url: self!.outputVideoURL!) +// self!.play() +// } +// } +// } +// } + } + //交叉眼 + if(selectedIndex == 2){ + Task { + convertor2.type = 2 + + try await convertor2.convertVideo(asset: videoTempAsset!, outputFile: outputVideoURL! ) { [self] progress in + print(progress) + DispatchQueue.main.async { [weak self] in + self!.btn3!.setTitle("进度=" + String(progress), for: UIControl.State.normal) + if(progress > 0.99){ + self!.videoTempAsset = AVAsset(url: self!.outputVideoURL!) + self!.play() + } + } + + } + } + } + + //红蓝立体 + if(selectedIndex == 3){ + Task { + convertor2.type = 3 + + try await convertor2.convertVideo(asset: videoTempAsset!, outputFile: outputVideoURL! ) { [self] progress in + print(progress) + DispatchQueue.main.async { [weak self] in + self!.btn3!.setTitle("进度=" + String(progress), for: UIControl.State.normal) + if(progress > 0.99){ + self!.videoTempAsset = AVAsset(url: self!.outputVideoURL!) + self!.play() + } + } + + } + } + } + //高斯模糊 + if(selectedIndex == 4){ + Task { + convertor2.type = 4 + + try await convertor2.convertVideo(asset: videoTempAsset!, outputFile: outputVideoURL! ) { [self] progress in + print(progress) + DispatchQueue.main.async { [weak self] in + self!.btn3!.setTitle("进度=" + String(progress), for: UIControl.State.normal) + if(progress > 0.99){ + self!.videoTempAsset = AVAsset(url: self!.outputVideoURL!) + self!.play() + } + } + + } + } + } + } + + @objc func buttonPressed(sender:UIButton){ + + if(sender.tag == 10){ +// let imagePickerController = UIImagePickerController() +// imagePickerController.delegate = self +// imagePickerController.sourceType = .photoLibrary +// imagePickerController.allowsEditing = false +// imagePickerController.mediaTypes = [kUTTypeMovie as String] +// present(imagePickerController, animated: true, completion: nil) + + let vc:PlayControllerVideo = PlayControllerVideo() + self.present(vc, animated: true, completion: nil) + vc.mediaSelectedHandler = { [self]ass in + print("回调") + print(ass) + videoTempAsset = ass + play() + } + + } else{ + + } + } + + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + + + if let mediaType = info[UIImagePickerController.InfoKey.mediaType] as? String, mediaType == "public.movie" { + let videoURL = info[.mediaURL] as? URL + print("Selected video URL: \(videoURL)") + sourceVideoURL = videoURL + videoOriginalAsset = AVAsset(url: sourceVideoURL!) + videoTempAsset = videoOriginalAsset + if(!isSpatialVideo(asset: videoTempAsset!)){ + showTextAlert(title: "提示", message: "当前视频不是空间视频") + } + play() + } + + dismiss(animated: true, completion: nil) + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + dismiss(animated: true, completion: nil) + } + + //检查是否为空间视频 + func isSpatialVideo(asset: AVAsset) -> Bool { + let metadata = asset.metadata(forFormat: AVMetadataFormat.quickTimeMetadata) + let isSpatialVideo = metadata.contains { item in + if let identifier = item.identifier?.rawValue { + return identifier == "mdta/com.apple.quicktime.spatial.format-version" + } + return false + } + return isSpatialVideo + } + + func showTextAlert(title: String, message: String) { + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) + alertController.addAction(okAction) + + // 在视图控制器中显示弹窗 + present(alertController, animated: true, completion: nil) + } + +} +