From 5254e777125f4ff3b8de4320cf98b702f2efbe0e Mon Sep 17 00:00:00 2001 From: du Date: Wed, 20 Nov 2024 18:28:17 +0800 Subject: [PATCH] add demo --- ...n_Normal@2x.png => ImageEditedIcon@2x.png} | Bin ...n_Normal@3x.png => ImageEditedIcon@3x.png} | Bin ...n_Normal@2x.png => VideoEditedIcon@2x.png} | Bin ...n_Normal@3x.png => VideoEditedIcon@3x.png} | Bin ...ker@2x.png => video_filled_sticker@2x.png} | Bin ...ker@3x.png => video_filled_sticker@3x.png} | Bin ...d_text@2x.png => video_filled_text@2x.png} | Bin ...d_text@3x.png => video_filled_text@3x.png} | Bin .../Base/ADPhotoKitConfiguration.swift | 9 +- .../Classes/Base/ADPhotoKitConstant.swift | 2 +- .../Core/Extension/ADImageDataProvider.swift | 2 + .../Core/Extension/AVAsset+ADExtension.swift | 3 +- .../Cell/Browser/ADBrowserToolBarCell.swift | 4 +- .../Cell/Thumbnail/ADThumbnailListCell.swift | 4 +- .../EditCore/View/ADEditControlsView.swift | 2 + .../VideoEdit/ADMusicSelectController.swift | 8 + .../VideoEdit/ADVideoEditConfigurable.swift | 1 + .../VideoEdit/ADVideoEditController.swift | 2 +- .../VideoEdit/ADVideoThumbnailOperation.swift | 1 + .../Classes/VideoEdit/Tools/ADVideoBGM.swift | 1 + .../Classes/VideoEdit/Tools/ADVideoClip.swift | 1 + .../VideoEdit/Tools/ADVideoSticker.swift | 5 +- .../VideoEdit/View/ADVideoPlayerView.swift | 2 - Example/ADPhotoKit.xcodeproj/project.pbxproj | 20 +- ...electView.swift => FilterSelectView.swift} | 4 +- Example/ADPhotoKit/ImageFilterTool.swift | 10 +- Example/ADPhotoKit/VideoExporter.swift | 202 +++++++++++++ Example/ADPhotoKit/VideoFilterTool.swift | 178 ++++++++++++ Example/ADPhotoKit/VideoPlayerView.swift | 268 ++++++++++++++++++ Example/ADPhotoKit/ViewController.swift | 111 +++++++- 30 files changed, 801 insertions(+), 39 deletions(-) rename ADPhotoKit/Assets/ImageEdit/{EditedIcon_Normal@2x.png => ImageEditedIcon@2x.png} (100%) rename ADPhotoKit/Assets/ImageEdit/{EditedIcon_Normal@3x.png => ImageEditedIcon@3x.png} (100%) rename ADPhotoKit/Assets/VideoEdit/{EditedIcon_Normal@2x.png => VideoEditedIcon@2x.png} (100%) rename ADPhotoKit/Assets/VideoEdit/{EditedIcon_Normal@3x.png => VideoEditedIcon@3x.png} (100%) rename ADPhotoKit/Assets/VideoEdit/{icons_filled_sticker@2x.png => video_filled_sticker@2x.png} (100%) rename ADPhotoKit/Assets/VideoEdit/{icons_filled_sticker@3x.png => video_filled_sticker@3x.png} (100%) rename ADPhotoKit/Assets/VideoEdit/{icons_filled_text@2x.png => video_filled_text@2x.png} (100%) rename ADPhotoKit/Assets/VideoEdit/{icons_filled_text@3x.png => video_filled_text@3x.png} (100%) rename Example/ADPhotoKit/{ImageFilterSelectView.swift => FilterSelectView.swift} (96%) create mode 100644 Example/ADPhotoKit/VideoExporter.swift create mode 100644 Example/ADPhotoKit/VideoFilterTool.swift create mode 100644 Example/ADPhotoKit/VideoPlayerView.swift diff --git a/ADPhotoKit/Assets/ImageEdit/EditedIcon_Normal@2x.png b/ADPhotoKit/Assets/ImageEdit/ImageEditedIcon@2x.png similarity index 100% rename from ADPhotoKit/Assets/ImageEdit/EditedIcon_Normal@2x.png rename to ADPhotoKit/Assets/ImageEdit/ImageEditedIcon@2x.png diff --git a/ADPhotoKit/Assets/ImageEdit/EditedIcon_Normal@3x.png b/ADPhotoKit/Assets/ImageEdit/ImageEditedIcon@3x.png similarity index 100% rename from ADPhotoKit/Assets/ImageEdit/EditedIcon_Normal@3x.png rename to ADPhotoKit/Assets/ImageEdit/ImageEditedIcon@3x.png diff --git a/ADPhotoKit/Assets/VideoEdit/EditedIcon_Normal@2x.png b/ADPhotoKit/Assets/VideoEdit/VideoEditedIcon@2x.png similarity index 100% rename from ADPhotoKit/Assets/VideoEdit/EditedIcon_Normal@2x.png rename to ADPhotoKit/Assets/VideoEdit/VideoEditedIcon@2x.png diff --git a/ADPhotoKit/Assets/VideoEdit/EditedIcon_Normal@3x.png b/ADPhotoKit/Assets/VideoEdit/VideoEditedIcon@3x.png similarity index 100% rename from ADPhotoKit/Assets/VideoEdit/EditedIcon_Normal@3x.png rename to ADPhotoKit/Assets/VideoEdit/VideoEditedIcon@3x.png diff --git a/ADPhotoKit/Assets/VideoEdit/icons_filled_sticker@2x.png b/ADPhotoKit/Assets/VideoEdit/video_filled_sticker@2x.png similarity index 100% rename from ADPhotoKit/Assets/VideoEdit/icons_filled_sticker@2x.png rename to ADPhotoKit/Assets/VideoEdit/video_filled_sticker@2x.png diff --git a/ADPhotoKit/Assets/VideoEdit/icons_filled_sticker@3x.png b/ADPhotoKit/Assets/VideoEdit/video_filled_sticker@3x.png similarity index 100% rename from ADPhotoKit/Assets/VideoEdit/icons_filled_sticker@3x.png rename to ADPhotoKit/Assets/VideoEdit/video_filled_sticker@3x.png diff --git a/ADPhotoKit/Assets/VideoEdit/icons_filled_text@2x.png b/ADPhotoKit/Assets/VideoEdit/video_filled_text@2x.png similarity index 100% rename from ADPhotoKit/Assets/VideoEdit/icons_filled_text@2x.png rename to ADPhotoKit/Assets/VideoEdit/video_filled_text@2x.png diff --git a/ADPhotoKit/Assets/VideoEdit/icons_filled_text@3x.png b/ADPhotoKit/Assets/VideoEdit/video_filled_text@3x.png similarity index 100% rename from ADPhotoKit/Assets/VideoEdit/icons_filled_text@3x.png rename to ADPhotoKit/Assets/VideoEdit/video_filled_text@3x.png diff --git a/ADPhotoKit/Classes/Base/ADPhotoKitConfiguration.swift b/ADPhotoKit/Classes/Base/ADPhotoKitConfiguration.swift index 62a5b951..7eaaad38 100644 --- a/ADPhotoKit/Classes/Base/ADPhotoKitConfiguration.swift +++ b/ADPhotoKit/Classes/Base/ADPhotoKitConfiguration.swift @@ -234,14 +234,15 @@ public class ADPhotoKitConfiguration { public var systemVideoEditTools: ADVideoEditTools = .all /// User custom video edit tools. Custom tools is default add after system tools. - public var customVideoEditToolsBlock: (() -> [ADVideoEditTool])? - - /// Custom video edit controller. - public var customVideoEditVCBlock: ((ADPhotoKitConfig, AVAsset, ADVideoEditInfo?) -> ADVideoEditConfigurable)? + /// - Note: Usually when you add a custom `ADVideoEditTool`, you need to set `customVideoPlayable` at the same time to respond to the modification of the corresponding tool. + public var customVideoEditToolsBlock: ((AVAsset) -> [ADVideoEditTool])? /// Custom video player. public var customVideoPlayable: ADVideoPlayable.Type? + /// Custom video edit controller. + public var customVideoEditVCBlock: ((ADPhotoKitConfig, AVAsset, ADVideoEditInfo?) -> ADVideoEditConfigurable)? + /* =============== bgm =============== */ /// System video bgm data source. diff --git a/ADPhotoKit/Classes/Base/ADPhotoKitConstant.swift b/ADPhotoKit/Classes/Base/ADPhotoKitConstant.swift index afbd56c3..5aed13a5 100644 --- a/ADPhotoKit/Classes/Base/ADPhotoKitConstant.swift +++ b/ADPhotoKit/Classes/Base/ADPhotoKitConstant.swift @@ -28,7 +28,7 @@ let screenBounds:CGRect = { return UIScreen.main.bounds }() let screenWidth:CGFloat = { return UIScreen.main.bounds.size.width }() let screenHeight:CGFloat = { return UIScreen.main.bounds.size.height }() -///包含 iPhone12 mini +/// 包含 iPhone12 mini let isPhoneXOrLater:Bool = { return isPhone && screenHeight >= 812.0 }() let statusBarHeight: CGFloat = UIApplication.shared.statusBarFrame.height diff --git a/ADPhotoKit/Classes/Core/Extension/ADImageDataProvider.swift b/ADPhotoKit/Classes/Core/Extension/ADImageDataProvider.swift index 646d97c7..a325523c 100644 --- a/ADPhotoKit/Classes/Core/Extension/ADImageDataProvider.swift +++ b/ADPhotoKit/Classes/Core/Extension/ADImageDataProvider.swift @@ -218,6 +218,8 @@ public class ADAssetImageDataProvider: ImageDataProvider { guard let strong = self else { return } if let av = asset as? AVAsset { let generator = AVAssetImageGenerator(asset: av) + generator.appliesPreferredTrackTransform = true + generator.apertureMode = .encodedPixels generator.generateCGImagesAsynchronously(forTimes: [NSValue(time: strong.time)]) { (requestedTime, image, imageTime, result, error) in if let error = error { diff --git a/ADPhotoKit/Classes/Core/Extension/AVAsset+ADExtension.swift b/ADPhotoKit/Classes/Core/Extension/AVAsset+ADExtension.swift index 0310b5c6..14ee451e 100644 --- a/ADPhotoKit/Classes/Core/Extension/AVAsset+ADExtension.swift +++ b/ADPhotoKit/Classes/Core/Extension/AVAsset+ADExtension.swift @@ -10,7 +10,8 @@ import AVFoundation extension AVAsset { - var naturalSize: CGSize { + /// Return asset naturalSize. + public var naturalSize: CGSize { if let videoTrack = tracks(withMediaType: .video).first { var size = videoTrack.naturalSize if AVAsset.isPortraitTrack(videoTrack) { diff --git a/ADPhotoKit/Classes/CoreUI/Cell/Browser/ADBrowserToolBarCell.swift b/ADPhotoKit/Classes/CoreUI/Cell/Browser/ADBrowserToolBarCell.swift index 4db3c774..94ea07a5 100644 --- a/ADPhotoKit/Classes/CoreUI/Cell/Browser/ADBrowserToolBarCell.swift +++ b/ADPhotoKit/Classes/CoreUI/Cell/Browser/ADBrowserToolBarCell.swift @@ -59,7 +59,7 @@ public class ADBrowserToolBarCell: UICollectionViewCell { if model.imageEditInfo != nil { imageView.image = model.imageEditInfo?.editImg tagImageView.isHidden = false - tagImageView.image = Bundle.image(name: "EditedIcon_Normal", module: .imageEdit) + tagImageView.image = Bundle.image(name: "ImageEditedIcon", module: .imageEdit) return } #endif @@ -67,7 +67,7 @@ public class ADBrowserToolBarCell: UICollectionViewCell { if model.videoEditInfo != nil { imageView.image = model.videoEditInfo?.editThumbnail tagImageView.isHidden = false - tagImageView.image = Bundle.image(name: "EditedIcon_Normal", module: .videoEdit) + tagImageView.image = Bundle.image(name: "VideoEditedIcon", module: .videoEdit) return } #endif diff --git a/ADPhotoKit/Classes/CoreUI/Cell/Thumbnail/ADThumbnailListCell.swift b/ADPhotoKit/Classes/CoreUI/Cell/Thumbnail/ADThumbnailListCell.swift index 2393ce9e..e34a8dcb 100644 --- a/ADPhotoKit/Classes/CoreUI/Cell/Thumbnail/ADThumbnailListCell.swift +++ b/ADPhotoKit/Classes/CoreUI/Cell/Thumbnail/ADThumbnailListCell.swift @@ -235,7 +235,7 @@ extension ADThumbnailListCell: ADThumbnailCellConfigurable { descLabel.text = "" imageView.image = imageEdit bottomMaskView.isHidden = false - tagImageView.image = Bundle.image(name: "EditedIcon_Normal", module: .imageEdit) + tagImageView.image = Bundle.image(name: "ImageEditedIcon", module: .imageEdit) return } #endif @@ -243,7 +243,7 @@ extension ADThumbnailListCell: ADThumbnailCellConfigurable { if let videoEdit = model.videoEditInfo?.editThumbnail { imageView.image = videoEdit bottomMaskView.isHidden = false - tagImageView.image = Bundle.image(name: "EditedIcon_Normal", module: .videoEdit) + tagImageView.image = Bundle.image(name: "VideoEditedIcon", module: .videoEdit) return } #endif diff --git a/ADPhotoKit/Classes/EditCore/View/ADEditControlsView.swift b/ADPhotoKit/Classes/EditCore/View/ADEditControlsView.swift index a9efb9ed..522e7524 100644 --- a/ADPhotoKit/Classes/EditCore/View/ADEditControlsView.swift +++ b/ADPhotoKit/Classes/EditCore/View/ADEditControlsView.swift @@ -207,6 +207,8 @@ extension ADEditControlsView: UICollectionViewDataSource, UICollectionViewDelega selectToolIndex = nil }else if tool.toolDidSelect(ctx: vc) { selectToolIndex = indexPath.row + }else{ + selectToolIndex = nil } } } diff --git a/ADPhotoKit/Classes/VideoEdit/ADMusicSelectController.swift b/ADPhotoKit/Classes/VideoEdit/ADMusicSelectController.swift index 7c7dfb48..7038addc 100644 --- a/ADPhotoKit/Classes/VideoEdit/ADMusicSelectController.swift +++ b/ADPhotoKit/Classes/VideoEdit/ADMusicSelectController.swift @@ -73,6 +73,14 @@ public class ADVideoSound { public var bgm: ADMusicItem? = nil /// Whether bgm loop play. public var bgmLoop: Bool = true + + /// Create video sound. + public init(lyricOn: Bool = false, ostOn: Bool = true, bgm: ADMusicItem? = nil, bgmLoop: Bool = true) { + self.lyricOn = lyricOn + self.ostOn = ostOn + self.bgm = bgm + self.bgmLoop = bgmLoop + } } /// Music select datasource. diff --git a/ADPhotoKit/Classes/VideoEdit/ADVideoEditConfigurable.swift b/ADPhotoKit/Classes/VideoEdit/ADVideoEditConfigurable.swift index 784f56f1..791d156e 100644 --- a/ADPhotoKit/Classes/VideoEdit/ADVideoEditConfigurable.swift +++ b/ADPhotoKit/Classes/VideoEdit/ADVideoEditConfigurable.swift @@ -16,6 +16,7 @@ public protocol ADVideoEditTool: ADEditTool { var playableRectUpdate: ((CGFloat, CGFloat, Bool) -> Void)! { set get } /// View to preview edit video. + /// - Note: This property is initialized and set by the system. Classes that implement this protocol should declare this property as weak. var videoPlayable: ADVideoPlayable? { set get } } diff --git a/ADPhotoKit/Classes/VideoEdit/ADVideoEditController.swift b/ADPhotoKit/Classes/VideoEdit/ADVideoEditController.swift index e908efc2..b3d0f1d6 100644 --- a/ADPhotoKit/Classes/VideoEdit/ADVideoEditController.swift +++ b/ADPhotoKit/Classes/VideoEdit/ADVideoEditController.swift @@ -171,7 +171,7 @@ extension ADVideoEditController { let clip = ADVideoClip(asset: asset, min: min, max: max) tools.append(clip) } - if let custom = ADPhotoKitConfiguration.default.customVideoEditToolsBlock?() { + if let custom = ADPhotoKitConfiguration.default.customVideoEditToolsBlock?(asset) { tools.append(contentsOf: custom) } diff --git a/ADPhotoKit/Classes/VideoEdit/ADVideoThumbnailOperation.swift b/ADPhotoKit/Classes/VideoEdit/ADVideoThumbnailOperation.swift index 3e3cc50e..b11e609d 100644 --- a/ADPhotoKit/Classes/VideoEdit/ADVideoThumbnailOperation.swift +++ b/ADPhotoKit/Classes/VideoEdit/ADVideoThumbnailOperation.swift @@ -7,6 +7,7 @@ import Foundation import AVFoundation +import UIKit class ADVideoThumbnailOperation: Operation { diff --git a/ADPhotoKit/Classes/VideoEdit/Tools/ADVideoBGM.swift b/ADPhotoKit/Classes/VideoEdit/Tools/ADVideoBGM.swift index 6d96fa4e..a67d58a0 100644 --- a/ADPhotoKit/Classes/VideoEdit/Tools/ADVideoBGM.swift +++ b/ADPhotoKit/Classes/VideoEdit/Tools/ADVideoBGM.swift @@ -6,6 +6,7 @@ // import Foundation +import UIKit class ADVideoBGM: ADVideoEditTool { diff --git a/ADPhotoKit/Classes/VideoEdit/Tools/ADVideoClip.swift b/ADPhotoKit/Classes/VideoEdit/Tools/ADVideoClip.swift index 614a7a5f..0b13847e 100644 --- a/ADPhotoKit/Classes/VideoEdit/Tools/ADVideoClip.swift +++ b/ADPhotoKit/Classes/VideoEdit/Tools/ADVideoClip.swift @@ -7,6 +7,7 @@ import Foundation import AVFoundation +import UIKit class ADVideoClip: ADVideoEditTool { diff --git a/ADPhotoKit/Classes/VideoEdit/Tools/ADVideoSticker.swift b/ADPhotoKit/Classes/VideoEdit/Tools/ADVideoSticker.swift index 6de347a4..940ee7d3 100644 --- a/ADPhotoKit/Classes/VideoEdit/Tools/ADVideoSticker.swift +++ b/ADPhotoKit/Classes/VideoEdit/Tools/ADVideoSticker.swift @@ -6,15 +6,16 @@ // import Foundation +import UIKit class ADVideoSticker: ADVideoEditTool { var image: UIImage { switch style { case .text: - return Bundle.image(name: "icons_filled_text", module: .videoEdit) ?? UIImage() + return Bundle.image(name: "video_filled_text", module: .videoEdit) ?? UIImage() case .image: - return Bundle.image(name: "icons_filled_sticker", module: .videoEdit) ?? UIImage() + return Bundle.image(name: "video_filled_sticker", module: .videoEdit) ?? UIImage() } } diff --git a/ADPhotoKit/Classes/VideoEdit/View/ADVideoPlayerView.swift b/ADPhotoKit/Classes/VideoEdit/View/ADVideoPlayerView.swift index 9657db62..9488c888 100644 --- a/ADPhotoKit/Classes/VideoEdit/View/ADVideoPlayerView.swift +++ b/ADPhotoKit/Classes/VideoEdit/View/ADVideoPlayerView.swift @@ -40,7 +40,6 @@ class ADVideoPlayerView: UIView, ADVideoPlayable { private var progressObservers: [Observer] = [] - private var videoSize: CGSize = .zero private var player: AVPlayer! private var videoPlayerLayer: AVPlayerLayer! @@ -48,7 +47,6 @@ class ADVideoPlayerView: UIView, ADVideoPlayable { self.asset = asset videoPlayerLayer = AVPlayerLayer() super.init(frame: .zero) - videoSize = asset.naturalSize player = AVPlayer() videoPlayerLayer.contentsGravity = .resizeAspect layer.insertSublayer(videoPlayerLayer, at: 0) diff --git a/Example/ADPhotoKit.xcodeproj/project.pbxproj b/Example/ADPhotoKit.xcodeproj/project.pbxproj index 77047d92..a6f8f739 100644 --- a/Example/ADPhotoKit.xcodeproj/project.pbxproj +++ b/Example/ADPhotoKit.xcodeproj/project.pbxproj @@ -14,6 +14,9 @@ 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; 810C016E2C522C6E0051AE90 /* CustomAlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 810C016D2C522C6E0051AE90 /* CustomAlertViewController.swift */; }; 816183C52CBD0A3D00FE3D2D /* bgms in Resources */ = {isa = PBXBuildFile; fileRef = 816183C42CBD0A3D00FE3D2D /* bgms */; }; + 819662512CEC813D00922582 /* VideoFilterTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 819662502CEC813D00922582 /* VideoFilterTool.swift */; }; + 819662532CEC940400922582 /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 819662522CEC940400922582 /* VideoPlayerView.swift */; }; + 819662552CED748700922582 /* VideoExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 819662542CED748700922582 /* VideoExporter.swift */; }; 81D9AA9A2B6A433000CE472F /* SwiftUIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81D9AA992B6A433000CE472F /* SwiftUIViewController.swift */; }; 81D9AA9D2B6A44C700CE472F /* MainSwiftUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81D9AA9C2B6A44C700CE472F /* MainSwiftUIView.swift */; }; 81D9AA9F2B6A45CC00CE472F /* DemosViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81D9AA9E2B6A45CC00CE472F /* DemosViewController.swift */; }; @@ -96,7 +99,7 @@ 95792A9026579AA300AE8ED5 /* VideoBrowserCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95792A8E26579AA300AE8ED5 /* VideoBrowserCell.swift */; }; 95792A9126579AA300AE8ED5 /* VideoBrowserCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 95792A8F26579AA300AE8ED5 /* VideoBrowserCell.xib */; }; 9582CACC26F338C100C250E7 /* ImageFilterTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9582CACB26F338C100C250E7 /* ImageFilterTool.swift */; }; - 9582CACE26F33B1700C250E7 /* ImageFilterSelectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9582CACD26F33B1700C250E7 /* ImageFilterSelectView.swift */; }; + 9582CACE26F33B1700C250E7 /* FilterSelectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9582CACD26F33B1700C250E7 /* FilterSelectView.swift */; }; BFEB069A60BB41EC33CB7F50 /* Pods_ADPhotoKit_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 24853DA6C7C8D2739F567EE4 /* Pods_ADPhotoKit_Example.framework */; }; /* End PBXBuildFile section */ @@ -115,6 +118,9 @@ 781C69769B128467B470C297 /* ADPhotoKit.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = ADPhotoKit.podspec; path = ../ADPhotoKit.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 810C016D2C522C6E0051AE90 /* CustomAlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlertViewController.swift; sourceTree = ""; }; 816183C42CBD0A3D00FE3D2D /* bgms */ = {isa = PBXFileReference; lastKnownFileType = folder; path = bgms; sourceTree = ""; }; + 819662502CEC813D00922582 /* VideoFilterTool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoFilterTool.swift; sourceTree = ""; }; + 819662522CEC940400922582 /* VideoPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = ""; }; + 819662542CED748700922582 /* VideoExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoExporter.swift; sourceTree = ""; }; 81D9AA992B6A433000CE472F /* SwiftUIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIViewController.swift; sourceTree = ""; }; 81D9AA9C2B6A44C700CE472F /* MainSwiftUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSwiftUIView.swift; sourceTree = ""; }; 81D9AA9E2B6A45CC00CE472F /* DemosViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemosViewController.swift; sourceTree = ""; }; @@ -197,7 +203,7 @@ 95792A8E26579AA300AE8ED5 /* VideoBrowserCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoBrowserCell.swift; sourceTree = ""; }; 95792A8F26579AA300AE8ED5 /* VideoBrowserCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = VideoBrowserCell.xib; sourceTree = ""; }; 9582CACB26F338C100C250E7 /* ImageFilterTool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFilterTool.swift; sourceTree = ""; }; - 9582CACD26F33B1700C250E7 /* ImageFilterSelectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFilterSelectView.swift; sourceTree = ""; }; + 9582CACD26F33B1700C250E7 /* FilterSelectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterSelectView.swift; sourceTree = ""; }; 95CADD19265CD0A100009D1B /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Package.swift; path = ../Package.swift; sourceTree = ""; }; A1D674AD92CA2F57D0C42C9D /* Pods-ADPhotoKit_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ADPhotoKit_Tests.release.xcconfig"; path = "Target Support Files/Pods-ADPhotoKit_Tests/Pods-ADPhotoKit_Tests.release.xcconfig"; sourceTree = ""; }; AD6AD32727AAE9474B8F3EE1 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; @@ -422,7 +428,10 @@ isa = PBXGroup; children = ( 9582CACB26F338C100C250E7 /* ImageFilterTool.swift */, - 9582CACD26F33B1700C250E7 /* ImageFilterSelectView.swift */, + 9582CACD26F33B1700C250E7 /* FilterSelectView.swift */, + 819662502CEC813D00922582 /* VideoFilterTool.swift */, + 819662522CEC940400922582 /* VideoPlayerView.swift */, + 819662542CED748700922582 /* VideoExporter.swift */, ); name = Edit; sourceTree = ""; @@ -638,16 +647,19 @@ 81D9AA9F2B6A45CC00CE472F /* DemosViewController.swift in Sources */, 95792A402657675A00AE8ED5 /* BrowserNavBar.swift in Sources */, 9522FB332655133D00A0D360 /* ProgressHUD.swift in Sources */, + 819662532CEC940400922582 /* VideoPlayerView.swift in Sources */, 9551F0B9263C0AF10089C8DE /* Stepper.swift in Sources */, 9551F0B2263BA9130089C8DE /* ConfigModel.swift in Sources */, - 9582CACE26F33B1700C250E7 /* ImageFilterSelectView.swift in Sources */, + 9582CACE26F33B1700C250E7 /* FilterSelectView.swift in Sources */, 9551F0B6263BACB70089C8DE /* ConfigCell.swift in Sources */, 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */, + 819662512CEC813D00922582 /* VideoFilterTool.swift in Sources */, 81D9AA9D2B6A44C700CE472F /* MainSwiftUIView.swift in Sources */, 81D9AA9A2B6A433000CE472F /* SwiftUIViewController.swift in Sources */, 95792A9026579AA300AE8ED5 /* VideoBrowserCell.swift in Sources */, 95792A2926563F2200AE8ED5 /* AlbumCell.swift in Sources */, 810C016E2C522C6E0051AE90 /* CustomAlertViewController.swift in Sources */, + 819662552CED748700922582 /* VideoExporter.swift in Sources */, 95792A8B26579A9200AE8ED5 /* ImageBrowserCell.swift in Sources */, 9582CACC26F338C100C250E7 /* ImageFilterTool.swift in Sources */, 9551F0A7263A98D80089C8DE /* LanguageViewController.swift in Sources */, diff --git a/Example/ADPhotoKit/ImageFilterSelectView.swift b/Example/ADPhotoKit/FilterSelectView.swift similarity index 96% rename from Example/ADPhotoKit/ImageFilterSelectView.swift rename to Example/ADPhotoKit/FilterSelectView.swift index 51363235..2154b6c5 100644 --- a/Example/ADPhotoKit/ImageFilterSelectView.swift +++ b/Example/ADPhotoKit/FilterSelectView.swift @@ -9,8 +9,8 @@ import UIKit import ADPhotoKit -#if Module_ImageEdit -class ImageFilterSelectView: UIView, ADToolConfigable { +#if Module_ImageEdit || Module_VideoEdit +class FilterSelectView: UIView, ADToolConfigable { weak var dataSource: (UICollectionViewDataSource & UICollectionViewDelegate)? diff --git a/Example/ADPhotoKit/ImageFilterTool.swift b/Example/ADPhotoKit/ImageFilterTool.swift index acf94f56..f26615f4 100644 --- a/Example/ADPhotoKit/ImageFilterTool.swift +++ b/Example/ADPhotoKit/ImageFilterTool.swift @@ -10,7 +10,7 @@ import UIKit import ADPhotoKit #if Module_ImageEdit -enum Filter: CaseIterable { +enum ImageFilter: CaseIterable { case none case chrome case fade @@ -133,13 +133,13 @@ class ImageFilterTool: NSObject, ADImageEditTool, ADSourceImageEditable { var filterImages: [UIImage] = [] var selectIndex: Int = -1 - init(image: UIImage, filters: [Filter] = Filter.allCases) { + init(image: UIImage, filters: [ImageFilter] = ImageFilter.allCases) { originImage = image super.init() let thumbnail = generateThumbnailImage(img: image) ?? image - let selectV = ImageFilterSelectView(dataSource: self) + let selectV = FilterSelectView(dataSource: self) toolConfigView = selectV DispatchQueue.global().async { @@ -172,14 +172,14 @@ class ImageFilterTool: NSObject, ADImageEditTool, ADSourceImageEditable { func indexDidChange() { guard selectIndex >= 0 else { - (toolConfigView as? ImageFilterSelectView)?.collectionView.reloadData() + (toolConfigView as? FilterSelectView)?.collectionView.reloadData() modifySourceImage?(originImage) return } guard selectIndex < filterImages.count else { return } - (toolConfigView as? ImageFilterSelectView)?.collectionView.reloadData() + (toolConfigView as? FilterSelectView)?.collectionView.reloadData() modifySourceImage?(filterImages[selectIndex]) } diff --git a/Example/ADPhotoKit/VideoExporter.swift b/Example/ADPhotoKit/VideoExporter.swift new file mode 100644 index 00000000..b2c6dea2 --- /dev/null +++ b/Example/ADPhotoKit/VideoExporter.swift @@ -0,0 +1,202 @@ +// +// VideoExporter.swift +// ADPhotoKit_Example +// +// Created by du on 2024/11/20. +// Copyright © 2024 CocoaPods. All rights reserved. +// + +import Foundation +import ADPhotoKit +import AVFoundation +import CoreImage + +#if Module_VideoEdit +class VideoExporter: ADVideoExporter { + + private var exportSession: AVAssetExportSession? + private var filterIndex: Int = -1 + + override init(asset: AVAsset, editInfo: ADVideoEditInfo) { + super.init(asset: asset, editInfo: editInfo) + setupSession() + } + + override func parseToolJosn() { + super.parseToolJosn() + guard let json = editInfo.toolsJson else { + return + } + for item in json { + if item.key == "com.adphoto.demo.videofilter" { + if let json = item.value as? Dictionary { + filterIndex = json["index"] as? Int ?? -1 + } + } + } + } + + override func export(to path: String, completionHandler handler: @escaping (URL?, Error?) -> Void) { + if exportSession == nil { + handler(nil, ADError.exportSessionCreateFailed) + return + } + startDisplayLink() + let exportURL = URL(fileURLWithPath: path) + if exportURL.pathExtension.lowercased() == "mp4" { + exportSession?.outputFileType = .mp4 + }else if exportURL.pathExtension.lowercased() == "mov" { + exportSession?.outputFileType = .mov + }else if exportURL.pathExtension.lowercased() == "m4v" { + exportSession?.outputFileType = .m4v + }else{ + exportSession?.outputFileType = .mp4 + } + exportSession?.outputURL = exportURL + exportSession?.exportAsynchronously { [weak self] in + guard let strong = self else { return } + if strong.exportSession?.status == .completed { + handler(exportURL, nil) + }else{ + handler(nil, strong.exportSession?.error) + } + } + } + + override func cancelExport() { + if exportSession?.status == .exporting { + exportSession?.cancelExport() + } + } + + private func setupSession() { + let composition = AVMutableComposition() + let videoComposition = AVMutableVideoComposition.init(propertiesOf: composition) + + let timeRange = clipRange ?? CMTimeRange(start: .zero, duration: asset.duration) + + let videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)! + if let video = asset.tracks(withMediaType: .video).first { + try? videoTrack.insertTimeRange(timeRange, of: video, at: .zero) + var instructions: [AVMutableVideoCompositionInstruction] = [] + VideoFilterCompositor.transform = video.preferredTransform + let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack) + layerInstruction.setTransform(video.preferredTransform, at: .zero) + let instruction = AVMutableVideoCompositionInstruction() + instruction.timeRange = CMTimeRangeMake(start: .zero, duration: timeRange.duration) + instruction.layerInstructions = [layerInstruction] + instructions.append(instruction) + videoComposition.instructions = instructions + } + + let audioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)! + if let audio = asset.tracks(withMediaType: .audio).first { + try? audioTrack.insertTimeRange(timeRange, of: audio, at: .zero) + } + + let bgmTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)! + if let bgmAsset = videoSound.bgm?.asset { + if let bgm = bgmAsset.tracks(withMediaType: .audio).first { + if !videoSound.bgmLoop { + let bgmRange = CMTimeRange(start: .zero, duration: min(timeRange.duration, bgmAsset.duration)) + try? bgmTrack.insertTimeRange(bgmRange, of: bgm, at: .zero) + }else{ + var duration = min(timeRange.duration, bgmAsset.duration) + var total = timeRange.duration + var start: CMTime = .zero + while total.seconds > 0 { + try? bgmTrack.insertTimeRange(CMTimeRange(start: .zero, duration: duration), of: bgm, at: start) + start = CMTimeAdd(start, duration) + total = timeRange.duration - start + duration = min(total, duration) + } + } + } + } + + let audioMix = AVMutableAudioMix() + let audioParameters = AVMutableAudioMixInputParameters(track: audioTrack) + audioParameters.setVolume(videoSound.ostOn ? 1 : 0, at: .zero) + let bgmParameters = AVMutableAudioMixInputParameters(track: bgmTrack) + bgmParameters.setVolume(1, at: .zero) + audioMix.inputParameters = [audioParameters, bgmParameters] + + let parentLayer = CALayer() + let videoLayer = CALayer() + parentLayer.frame = CGRect(origin: .zero, size: videoSize) + videoLayer.frame = CGRect(origin: .zero, size: videoSize) + videoLayer.transform = CATransform3DMakeScale(1, -1, 1) + parentLayer.sublayerTransform = CATransform3DMakeScale(1, -1, 1) + parentLayer.addSublayer(videoLayer) + for item in stkrs { + let layer = CALayer() + layer.frame = CGRect(origin: .zero, size: item.image.size) + layer.contents = item.image.cgImage + let scale = videoSize.width/UIScreen.main.bounds.width + layer.position = CGPoint(x: item.normalizeCenter.x*videoSize.width, y: item.normalizeCenter.y*videoSize.height) + layer.transform = CATransform3DMakeAffineTransform(item.transform.scaledBy(x: scale, y: scale).scaledBy(x: 1, y: -1)) + parentLayer.addSublayer(layer) + } + for changable in changables { + parentLayer.addSublayer(changable) + } + let tool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: videoLayer, in: parentLayer) + videoComposition.animationTool = tool + videoComposition.frameDuration = CMTime(value: 1, timescale: frameRate) + videoComposition.renderSize = videoSize + if filterIndex != -1 { + VideoFilterCompositor.filter = CIFilter(name: VideoFilter.allCases[filterIndex].filterName) + videoComposition.customVideoCompositorClass = VideoFilterCompositor.self + } + let exportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality) + exportSession?.audioMix = audioMix + exportSession?.videoComposition = videoComposition + self.exportSession = exportSession + if exportSession == nil { + print("AVAssetExportSession create failed!") + } + } +} + +class VideoFilterCompositor: NSObject, AVVideoCompositing { + + var sourcePixelBufferAttributes: [String : Any]? = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA] + var requiredPixelBufferAttributesForRenderContext: [String : Any] = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA] + + func renderContextChanged(_ newRenderContext: AVVideoCompositionRenderContext) { + renderContext = newRenderContext + } + + func cancelAllPendingVideoCompositionRequests() { + } + + static var filter: CIFilter? + static var transform: CGAffineTransform = .identity + + private var renderContext: AVVideoCompositionRenderContext? + private let context = CIContext() + + func startRequest(_ asyncVideoCompositionRequest: AVAsynchronousVideoCompositionRequest) { + guard let track = asyncVideoCompositionRequest.sourceTrackIDs.first?.int32Value, let frame = asyncVideoCompositionRequest.sourceFrame(byTrackID: track) else { + asyncVideoCompositionRequest.finish(with: NSError(domain: "VideoFilterCompositor", code: 0, userInfo: nil)) + return + } + let source = CIImage(cvPixelBuffer: frame) + if let filter = VideoFilterCompositor.filter { + filter.setValue(source, forKey: kCIInputImageKey) + if let outputImage = filter.outputImage?.transformed(by: VideoFilterCompositor.transform.inverted()), let outBuffer = renderContext?.newPixelBuffer() { + if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) { + let finalImage = CIImage(cgImage: cgImage) + context.render(finalImage, to: outBuffer) + } + asyncVideoCompositionRequest.finish(withComposedVideoFrame: outBuffer) + } else { + asyncVideoCompositionRequest.finish(with: NSError(domain: "VideoFilterCompositor", code: 0, userInfo: nil)) + } + }else{ + asyncVideoCompositionRequest.finish(with: NSError(domain: "VideoFilterCompositor", code: 0, userInfo: nil)) + } + } + +} +#endif diff --git a/Example/ADPhotoKit/VideoFilterTool.swift b/Example/ADPhotoKit/VideoFilterTool.swift new file mode 100644 index 00000000..d3e60b1d --- /dev/null +++ b/Example/ADPhotoKit/VideoFilterTool.swift @@ -0,0 +1,178 @@ +// +// VideoFilterTool.swift +// ADPhotoKit_Example +// +// Created by du on 2024/11/19. +// Copyright © 2024 CocoaPods. All rights reserved. +// + +import Foundation +import ADPhotoKit +import AVFoundation + +#if Module_VideoEdit +enum VideoFilter: CaseIterable { + case none + case chrome + case fade + case instant + case process + case transfer + + var name: String { + switch self { + case .none: + return "None" + case .chrome: + return "Chrome" + case .fade: + return "Fade" + case .instant: + return "Instant" + case .process: + return "Process" + case .transfer: + return "Transfer" + } + } + + var filterName: String { + switch self { + case .none: + return "" + case .chrome: + return "CIPhotoEffectChrome" + case .fade: + return "CIPhotoEffectFade" + case .instant: + return "CIPhotoEffectInstant" + case .process: + return "CIPhotoEffectProcess" + case .transfer: + return "CIPhotoEffectTransfer" + } + } + + func process(img: UIImage) -> UIImage { + if self == .none { + return img + } + if let cgImg = img.cgImage { + let ciImage = CIImage(cgImage: cgImg) + let filter = CIFilter(name: filterName) + filter?.setValue(ciImage, forKey: kCIInputImageKey) + if let output = filter?.outputImage { + let context = CIContext() + if let cgImage = context.createCGImage(output, from: output.extent) { + return UIImage(cgImage: cgImage) + } + } + } + return img + } +} + +class VideoFilterTool: NSObject, ADVideoEditTool { + var playableRectUpdate: ((CGFloat, CGFloat, Bool) -> Void)! + + weak var videoPlayable: ADVideoPlayable? + + var image: UIImage { + return UIImage(named: "filter")! + } + + var selectImage: UIImage? { + return UIImage(named: "filter_selected") + } + + var isSelected: Bool = false + + var isEdited: Bool { + return selectIndex != -1 + } + + var toolConfigView: ADToolConfigable? + + var toolInteractView: ADToolInteractable? + + private var filterInfos: [(String,UIImage)] = [] + private var filterImages: [UIImage] = [] + private var selectIndex: Int = -1 + + func toolDidSelect(ctx: UIViewController?) -> Bool { + return true + } + + var identifier: String { + return "com.adphoto.demo.videofilter" + } + + func encode() -> Any? { + return ["index":selectIndex] + } + + func decode(from: Any) { + if let json = from as? Dictionary { + selectIndex = json["index"] as? Int ?? 0 + } + indexDidChange() + } + + init(asset: AVAsset, filters: [VideoFilter] = VideoFilter.allCases) { + super.init() + let generator = AVAssetImageGenerator(asset: asset) + generator.appliesPreferredTrackTransform = true + generator.apertureMode = .encodedPixels + generator.generateCGImagesAsynchronously(forTimes: [NSValue(time: .zero)]) { [weak self] _, cgImage, _, result, error in + if result == .succeeded, let cg = cgImage { + let image = UIImage(cgImage: cg) + DispatchQueue.global().async { + for filter in filters { + let img = filter.process(img: image) + self?.filterInfos.append((filter.name,img)) + self?.filterImages.append(filter.process(img: image)) + } + DispatchQueue.main.async { + self?.indexDidChange() + } + } + } + } + let selectV = FilterSelectView(dataSource: self) + toolConfigView = selectV + } + + func indexDidChange() { + (videoPlayable as? VideoPlayerView)?.filterIndex = selectIndex + (toolConfigView as? FilterSelectView)?.collectionView.reloadData() + } +} + + +extension VideoFilterTool: UICollectionViewDataSource, UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return filterInfos.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FilterCell", for: indexPath) as! FilterCell + + let info = filterInfos[indexPath.row] + + cell.nameLabel.text = info.0 + cell.imageView.image = info.1 + + if selectIndex == indexPath.row { + cell.nameLabel.textColor = .red + } else { + cell.nameLabel.textColor = .white + } + + return cell + } + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + selectIndex = indexPath.row + indexDidChange() + } +} +#endif diff --git a/Example/ADPhotoKit/VideoPlayerView.swift b/Example/ADPhotoKit/VideoPlayerView.swift new file mode 100644 index 00000000..905228b8 --- /dev/null +++ b/Example/ADPhotoKit/VideoPlayerView.swift @@ -0,0 +1,268 @@ +// +// VideoPlayerView.swift +// ADPhotoKit_Example +// +// Created by du on 2024/11/19. +// Copyright © 2024 CocoaPods. All rights reserved. +// + +import UIKit +import ADPhotoKit +import AVFoundation + +#if Module_VideoEdit +class VideoPlayerView: UIView, ADVideoPlayable { + + class Observer { + var target: ((CGFloat, CMTime)->Void) + + init(target: @escaping (CGFloat, CMTime) -> Void) { + self.target = target + } + } + + class TargetProxy { + private weak var target: VideoPlayerView? + + init(target: VideoPlayerView) { + self.target = target + } + + @objc func onScreenUpdate() { + target?.onRenderFrame() + } + } + + let asset: AVAsset + var clipRange: CMTimeRange? { + didSet { + resetEdit() + } + } + var videoSound: ADVideoSound = ADVideoSound() { + didSet { + if videoSound.bgm?.id != lastBgm && videoSound.bgm != nil { + resetEdit() + }else{ + updateEdit() + } + } + } + + var filterIndex: Int = -1 { + didSet { + if filterIndex >= 0 { + filter = CIFilter(name: VideoFilter.allCases[filterIndex].filterName) + if displayLink == nil { + let _displayLink = CADisplayLink(target: TargetProxy(target: self), selector: #selector(TargetProxy.onScreenUpdate)) + _displayLink.add(to: .main, forMode: .default) + displayLink = _displayLink + } + }else{ + filter = nil + displayLink?.invalidate() + displayLink = nil + } + } + } + + static func exporter(from asset: AVAsset, editInfo: ADVideoEditInfo) -> ADVideoExporter { + return VideoExporter(asset: asset, editInfo: editInfo) + } + + private var composition: AVMutableComposition! + private var playerItem: AVPlayerItem! + private var lastBgm: String? + + private var progressObservers: [Observer] = [] + + private var player: AVPlayer! + private var videoPlayerLayer: AVPlayerLayer! + + private var videoOutput: AVPlayerItemVideoOutput! + private var displayLink: CADisplayLink? + private var ciContext: CIContext = CIContext() + private var filter: CIFilter? + private var renderImageView: UIImageView! + private var videoTransform: CGAffineTransform = .identity + + required init(asset: AVAsset) { + self.asset = asset + videoPlayerLayer = AVPlayerLayer() + super.init(frame: .zero) + player = AVPlayer() + videoPlayerLayer.contentsGravity = .resizeAspect + layer.insertSublayer(videoPlayerLayer, at: 0) + videoPlayerLayer.player = player + player.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 60), queue: DispatchQueue.main) { [weak self] time in + self?.playerTimeUpdate(time) + } + renderImageView = UIImageView() + renderImageView.contentMode = .scaleAspectFit + addSubview(renderImageView) + renderImageView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + resetEdit() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + NotificationCenter.default.removeObserver(self) + displayLink?.invalidate() + } + + override func layoutSubviews() { + super.layoutSubviews() + videoPlayerLayer.frame = bounds + } + + func pause(seekToZero: Bool = false) { + player?.pause() + if seekToZero { + player?.seek(to: .zero) + player?.play() + } + } + + func play() { + player.play() + } + + func seek(to: CMTime, pause: Bool) { + player?.pause() + player?.seek(to: to) + if !pause { + player.play() + } + } + + func addProgressObserver(_ observer: @escaping (_ progress: CGFloat, _ time: CMTime) -> Void) { + progressObservers.append(Observer(target: observer)) + } + +} + +extension VideoPlayerView { + + func resetEdit() { + composition = AVMutableComposition() + + let timeRange = clipRange ?? CMTimeRange(start: .zero, duration: asset.duration) + + let videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) + if let video = asset.tracks(withMediaType: .video).first { + videoTrack?.preferredTransform = video.preferredTransform + try? videoTrack?.insertTimeRange(timeRange, of: video, at: .zero) + videoTransform = video.preferredTransform + } + + let audioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) + if let audio = asset.tracks(withMediaType: .audio).first { + try? audioTrack?.insertTimeRange(timeRange, of: audio, at: .zero) + } + + let bgmTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) + if let bgmAsset = videoSound.bgm?.asset { + lastBgm = videoSound.bgm?.id + if let bgm = bgmAsset.tracks(withMediaType: .audio).first { + if !videoSound.bgmLoop { + let bgmRange = CMTimeRange(start: .zero, duration: min(timeRange.duration, bgmAsset.duration)) + try? bgmTrack?.insertTimeRange(bgmRange, of: bgm, at: .zero) + }else{ + var duration = min(timeRange.duration, bgmAsset.duration) + var total = timeRange.duration + var start: CMTime = .zero + while total.seconds > 0 { + try? bgmTrack?.insertTimeRange(CMTimeRange(start: .zero, duration: duration), of: bgm, at: start) + start = CMTimeAdd(start, duration) + total = timeRange.duration - start + duration = min(total, duration) + } + } + } + } + + playerItem = AVPlayerItem(asset: composition) + let outputSettings: [String: Any] = [ + (kCVPixelBufferPixelFormatTypeKey as String): kCVPixelFormatType_420YpCbCr8BiPlanarFullRange + ] + videoOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: outputSettings) + playerItem.add(videoOutput) + let audioMix = AVMutableAudioMix() + let audioParameters = AVMutableAudioMixInputParameters(track: audioTrack!) + audioParameters.setVolume(videoSound.ostOn ? 1 : 0, at: .zero) + let bgmParameters = AVMutableAudioMixInputParameters(track: bgmTrack!) + bgmParameters.setVolume(1, at: .zero) + audioMix.inputParameters = [audioParameters, bgmParameters] + playerItem.audioMix = audioMix + + NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(playDidFinish), name: .AVPlayerItemDidPlayToEndTime, object: playerItem) + + player.replaceCurrentItem(with: playerItem) + player.play() + } + + func updateEdit() { + let audioTracks = composition.tracks(withMediaType: .audio) + var audioMixInputParameters: [AVMutableAudioMixInputParameters] = [] + for track in audioTracks { + if track.trackID == 2 { + let inputParameters = AVMutableAudioMixInputParameters(track: track) + inputParameters.setVolume(videoSound.ostOn ? 1 : 0, at: .zero) + audioMixInputParameters.append(inputParameters) + } + if track.trackID == 3 { + let inputParameters = AVMutableAudioMixInputParameters(track: track) + inputParameters.setVolume(videoSound.bgm != nil ? 1 : 0, at: .zero) + audioMixInputParameters.append(inputParameters) + } + } + let audioMix = AVMutableAudioMix() + audioMix.inputParameters = audioMixInputParameters + playerItem.audioMix = audioMix + } +} + +extension VideoPlayerView { + @objc func playDidFinish() { + pause(seekToZero: true) + } + + func playerTimeUpdate(_ time: CMTime) { + let duration = clipRange?.duration.seconds ?? asset.duration.seconds + let progress = time.seconds / duration + for item in progressObservers { + item.target(progress, time) + } + } + + func onRenderFrame() { + guard let filter = filter else { + renderImageView.image = nil + return + } + + guard let time = player.currentItem?.currentTime() else { return } + + guard videoOutput.hasNewPixelBuffer(forItemTime: time), let pixelBuffer = videoOutput.copyPixelBuffer(forItemTime: time, itemTimeForDisplay: nil) else { + return + } + + let inputImage = CIImage(cvPixelBuffer: pixelBuffer).transformed(by: videoTransform.inverted()) + filter.setValue(inputImage, forKey: kCIInputImageKey) + guard let outputImage = filter.outputImage else { return } + + if let cgImage = ciContext.createCGImage(outputImage, from: inputImage.extent) { + DispatchQueue.main.async { + self.renderImageView.image = UIImage(cgImage: cgImage) + } + } + } +} +#endif diff --git a/Example/ADPhotoKit/ViewController.swift b/Example/ADPhotoKit/ViewController.swift index f09d64f0..ee6bea58 100644 --- a/Example/ADPhotoKit/ViewController.swift +++ b/Example/ADPhotoKit/ViewController.swift @@ -1013,10 +1013,65 @@ class ViewController: UIViewController { let customConfig = ConfigSection(title: "CustomUIConfig", models: customModels) dataSource.append(customConfig) - var imageEditModels: [ConfigModel] = [] - +#if Module_ImageEdit || Module_VideoEdit + var editModels: [ConfigModel] = [] + + let textColors = ConfigModel(title: "Text Colors", mode: .switch(true)) { (value) in + if let isOn = value as? Bool { + if isOn { + ADPhotoKitConfiguration.default.textStickerColors = [(.white,.black,.gray),(.black,.white,.gray)] + }else{ + ADPhotoKitConfiguration.default.textStickerColors = [(.systemBlue,.black,.gray),(.systemGray,.white,.gray)] + } + ProgressHUD.showSuccess("Update Success!") + } + } + editModels.append(textColors) + + let colorIndex = ConfigModel(title: "Default Color Index", mode: .switch(true)) { (value) in + if let isOn = value as? Bool { + if isOn { + ADPhotoKitConfiguration.default.lineDrawDefaultColorIndex = 1 + }else{ + ADPhotoKitConfiguration.default.lineDrawDefaultColorIndex = 0 + } + ProgressHUD.showSuccess("Update Success!") + } + } + editModels.append(colorIndex) + + let fontSize = ConfigModel(title: "Default Font Size", mode: .switch(true)) { (value) in + if let isOn = value as? Bool { + if isOn { + ADPhotoKitConfiguration.default.textStickerDefaultFontSize = 32 + }else{ + ADPhotoKitConfiguration.default.textStickerDefaultFontSize = 40 + } + ProgressHUD.showSuccess("Update Success!") + } + } + editModels.append(fontSize) + + let strokeWidth = ConfigModel(title: "Default Stroke Width", mode: .switch(true)) { (value) in + if let isOn = value as? Bool { + if isOn { + ADPhotoKitConfiguration.default.textStickerDefaultStrokeWidth = 6 + }else{ + ADPhotoKitConfiguration.default.textStickerDefaultStrokeWidth = 10 + } + ProgressHUD.showSuccess("Update Success!") + } + } + editModels.append(strokeWidth) + + let editConfig = ConfigSection(title: "EditConfig", models: editModels) + dataSource.append(editConfig) +#endif + #if Module_ImageEdit - let systenTools = ConfigModel(title: "System Tools", mode: .switch(true)) { (value) in + var imageEditModels: [ConfigModel] = [] + + let imageTools = ConfigModel(title: "System Tools", mode: .switch(true)) { (value) in if let isOn = value as? Bool { if isOn { ADPhotoKitConfiguration.default.systemImageEditTools = .all @@ -1026,9 +1081,9 @@ class ViewController: UIViewController { ProgressHUD.showSuccess("Update Success!") } } - imageEditModels.append(systenTools) + imageEditModels.append(imageTools) - let filter = ConfigModel(title: "Image Filter", mode: .switch(false)) { (value) in + let imagefilter = ConfigModel(title: "Image Filter", mode: .switch(false)) { (value) in if let isOn = value as? Bool { if isOn { ADPhotoKitConfiguration.default.customImageEditToolsBlock = { image in @@ -1040,7 +1095,7 @@ class ViewController: UIViewController { ProgressHUD.showSuccess("Update Success!") } } - imageEditModels.append(filter) + imageEditModels.append(imagefilter) let drawColors = ConfigModel(title: "Draw Colors", mode: .switch(true)) { (value) in if let isOn = value as? Bool { @@ -1069,24 +1124,54 @@ class ViewController: UIViewController { } } imageEditModels.append(mosaicWidth) + + let eraseOutlineWidth = ConfigModel(title: "Erase Outline Width", mode: .stepper(8)) { (value) in + if let count = value as? Int { + ADPhotoKitConfiguration.default.eraseOutlineWidth = CGFloat(count) + ProgressHUD.showSuccess("Update Success!") + } + } + imageEditModels.append(eraseOutlineWidth) + + let imageEditConfig = ConfigSection(title: "ImageEditConfig", models: imageEditModels) + dataSource.append(imageEditConfig) #endif -#if Module_ImageEdit || Module_VideoEdit - let textColors = ConfigModel(title: "Text Colors", mode: .switch(true)) { (value) in +#if Module_VideoEdit + var videoEditModels: [ConfigModel] = [] + + let videoTools = ConfigModel(title: "System Tools", mode: .switch(true)) { (value) in if let isOn = value as? Bool { if isOn { - ADPhotoKitConfiguration.default.textStickerColors = [(.white,.black,.gray),(.black,.white,.gray)] + ADPhotoKitConfiguration.default.systemVideoEditTools = .all }else{ - ADPhotoKitConfiguration.default.textStickerColors = [(.systemBlue,.black,.gray),(.systemGray,.white,.gray)] + ADPhotoKitConfiguration.default.systemVideoEditTools = [.clip, .textStkr] + } + ProgressHUD.showSuccess("Update Success!") + } + } + videoEditModels.append(videoTools) + + let videofilter = ConfigModel(title: "Video Filter", mode: .switch(false)) { (value) in + if let isOn = value as? Bool { + if isOn { + ADPhotoKitConfiguration.default.customVideoEditToolsBlock = { asset in + return [VideoFilterTool(asset: asset)] + } + ADPhotoKitConfiguration.default.customVideoPlayable = VideoPlayerView.self + }else{ + ADPhotoKitConfiguration.default.customVideoEditToolsBlock = nil + ADPhotoKitConfiguration.default.customVideoPlayable = nil } ProgressHUD.showSuccess("Update Success!") } } - imageEditModels.append(textColors) + videoEditModels.append(videofilter) + + let videoEditConfig = ConfigSection(title: "VideoEditConfig", models: videoEditModels) + dataSource.append(videoEditConfig) #endif - let imageEditConfig = ConfigSection(title: "ImageEditConfig", models: imageEditModels) - dataSource.append(imageEditConfig) } @IBAction func pushDemos() {