diff --git a/DPVideoMerger-Swift.podspec b/DPVideoMerger-Swift.podspec index 09d2fe5..eb39443 100644 --- a/DPVideoMerger-Swift.podspec +++ b/DPVideoMerger-Swift.podspec @@ -16,8 +16,8 @@ Pod::Spec.new do |s| # s.name = "DPVideoMerger-Swift" - s.version = "1.5.9" - s.summary = "Multiple videos merge in one video with manage scale & aspect ratio and also merge 4 videos to grid layout for Swift." + s.version = "1.5.10" + s.summary = "Multiple videos merge in one video with manage scale & aspect ratio and also merge videos to grid matrix layout for Swift." # This description is used to generate tags and improve search results. # * Think: What does it do? Why did you write it? What is the focus? diff --git a/DPVideoMerger/DPVideoMerger.xcodeproj/project.pbxproj b/DPVideoMerger/DPVideoMerger.xcodeproj/project.pbxproj index 5b6e2fa..8661bbb 100644 --- a/DPVideoMerger/DPVideoMerger.xcodeproj/project.pbxproj +++ b/DPVideoMerger/DPVideoMerger.xcodeproj/project.pbxproj @@ -477,7 +477,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.5.9; + MARKETING_VERSION = 1.5.10; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.datt.DPVideoMerger-Swift"; @@ -507,7 +507,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.5.9; + MARKETING_VERSION = 1.5.10; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.datt.DPVideoMerger-Swift"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; diff --git a/DPVideoMerger/DPVideoMerger.xcodeproj/project.xcworkspace/xcuserdata/datt.xcuserdatad/UserInterfaceState.xcuserstate b/DPVideoMerger/DPVideoMerger.xcodeproj/project.xcworkspace/xcuserdata/datt.xcuserdatad/UserInterfaceState.xcuserstate index bf5bde5..8aee8da 100644 Binary files a/DPVideoMerger/DPVideoMerger.xcodeproj/project.xcworkspace/xcuserdata/datt.xcuserdatad/UserInterfaceState.xcuserstate and b/DPVideoMerger/DPVideoMerger.xcodeproj/project.xcworkspace/xcuserdata/datt.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/DPVideoMerger/DPVideoMerger/DPVideoMerger.swift b/DPVideoMerger/DPVideoMerger/DPVideoMerger.swift index 536313b..50a3d9e 100644 --- a/DPVideoMerger/DPVideoMerger/DPVideoMerger.swift +++ b/DPVideoMerger/DPVideoMerger/DPVideoMerger.swift @@ -13,7 +13,7 @@ import AVKit @objc protocol VideoMerger { func mergeVideos(withFileURLs videoFileURLs: [URL], videoResolution:CGSize, videoQuality:String, completion: @escaping (_ mergedVideoURL: URL?, _ error: Error?) -> Void) - func gridMergeVideos(withFileURLs videoFileURLs: [URL], audioFileURL: URL?, videoResolution: CGSize, isRepeatVideo: Bool, isRepeatAudio: Bool, videoDuration: Int, videoQuality: String, completion: @escaping (_ mergedVideoURL: URL?, _ error: Error?) -> Void) + func gridMergeVideos(withFileURLs videoFileURLs: [URL], matrix: DPVideoMatrix, audioFileURL: URL?, videoResolution: CGSize, isRepeatVideo: Bool, isRepeatAudio: Bool, isAudio: Bool,videoDuration: Int, videoQuality: String, completion: @escaping (_ mergedVideoURL: URL?, _ error: Error?) -> Void) func parallelMergeVideos(withFileURLs videoFileURLs: [URL], audioFileURL: URL?, videoResolution: CGSize, isRepeatVideo: Bool, isRepeatAudio: Bool, videoDuration: Int, videoQuality: String, alignment: ParallelMergeAlignment, completion: @escaping (_ mergedVideoURL: URL?, _ error: Error?) -> Void) } @@ -22,6 +22,21 @@ import AVKit case horizontal } +@objc open class DPVideoMatrix: NSObject { + @objc fileprivate var rows: UInt + @objc fileprivate var columns: UInt + + public init(rows: UInt, columns: UInt) { + self.rows = rows + self.columns = columns + } + + @objc public func initWith(rows: UInt, columns: UInt) { + self.rows = rows + self.columns = columns + } +} + @objc open class DPVideoMerger : NSObject { } // MARK:- Public Functions @@ -39,7 +54,10 @@ extension DPVideoMerger : VideoMerger { videoResolution:CGSize = CGSize(width: -1, height: -1), videoQuality:String = AVAssetExportPresetMediumQuality, completion: @escaping (_ mergedVideoURL: URL?, _ error: Error?) -> Void) { - + if videoFileURLs.count <= 1 { + DispatchQueue.main.async { completion(nil, self.videoMoreThenOneError()) } + return + } let composition = AVMutableComposition() guard let videoTrack: AVMutableCompositionTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) else { DispatchQueue.main.async { completion(nil, self.videoTarckError()) } @@ -120,22 +138,8 @@ extension DPVideoMerger : VideoMerger { let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack) var isVideoAssetPortrait_ = false - let videoTransform: CGAffineTransform = videoAsset.preferredTransform - var videoAssetOrientation_: UIImage.Orientation = .up - if videoTransform.a == 0 && videoTransform.b == 1.0 && videoTransform.c == -1.0 && videoTransform.d == 0 { - videoAssetOrientation_ = .right - isVideoAssetPortrait_ = true - } - if videoTransform.a == 0 && videoTransform.b == -1.0 && videoTransform.c == 1.0 && videoTransform.d == 0 { - videoAssetOrientation_ = .left - isVideoAssetPortrait_ = true - } - if videoTransform.a == 1.0 && videoTransform.b == 0 && videoTransform.c == 0 && videoTransform.d == 1.0 { - videoAssetOrientation_ = .up - } - if videoTransform.a == -1.0 && videoTransform.b == 0 && videoTransform.c == 0 && videoTransform.d == -1.0 { - videoAssetOrientation_ = .down - } + let videoAssetOrientation_ = videoAssetOrientation(videoAsset) + if videoAssetOrientation_ == .right || videoAssetOrientation_ == .left { isVideoAssetPortrait_ = true } var videoAssetWidth: CGFloat = videoAsset.naturalSize.width var videoAssetHeight: CGFloat = videoAsset.naturalSize.height @@ -199,7 +203,7 @@ extension DPVideoMerger : VideoMerger { instructions.append(videoCompositionInstruction) currentTime = CMTimeAdd(currentTime, timeRange.duration) } catch { - print("Unable to load data: \(error)") + debugPrint("Unable to load data: \(error)") isError = true DispatchQueue.main.async { completion(nil, error) } } @@ -209,31 +213,45 @@ extension DPVideoMerger : VideoMerger { } } - /// Merge 4 videos to grid layout + /// Merge videos to grid matrix layout /// - Parameters: - /// - videoFileURLs: Video file path URLs, Array of 4 videos that going to grid merge + /// - videoFileURLs: Video file path URLs, Array of videos that going to grid merge + /// - matrix: Video matrix position (eg 3x3, 4x2, 1x3, ...) (default:- 2x2) /// - audioFileURL: Optional audio file for Merged Video /// - videoResolution: Output video resolution /// - isRepeatVideo: Repeat Video on grid if one or more video have shorter duartion time then output video duration /// - isRepeatAudio: Repeat Audio if Merged video have longer duartion time then provided Audio duration - /// - videoDuration: Output video duration (defult: -1, find max duration from provided 4 videos) + /// - isAudio: Allow Audio for grid video (default :- true) + /// - videoDuration: Output video duration (defult: -1, find max duration from provided videos) /// - videoQuality: AVAssetExportPresetMediumQuality(default) , AVAssetExportPresetLowQuality , AVAssetExportPresetHighestQuality /// - completion: completion give 2 optional values, 1)mergedVideoURL: URL path of successfully grid merged video 2)error: gives Error object if some error occur in videos merging process /// - mergedVideoURL: URL path of successfully grid merged video /// - error: gives Error object if some error occur in videos merging process open func gridMergeVideos(withFileURLs videoFileURLs: [URL], + matrix: DPVideoMatrix = DPVideoMatrix(rows: 2, columns: 2), audioFileURL: URL? = nil, videoResolution: CGSize, isRepeatVideo: Bool = false, isRepeatAudio: Bool = false, + isAudio: Bool = true, videoDuration: Int = -1, videoQuality: String = AVAssetExportPresetMediumQuality, completion: @escaping (_ mergedVideoURL: URL?, _ error: Error?) -> Void) { - if videoFileURLs.count != 4 { - DispatchQueue.main.async { completion(nil, self.videoCountError()) } + if videoFileURLs.count <= 1 { + DispatchQueue.main.async { completion(nil, self.videoMoreThenOneError()) } return } + let rows: CGFloat = CGFloat(matrix.rows) + let columns: CGFloat = CGFloat(matrix.columns) + if rows == 0 || columns == 0 { + DispatchQueue.main.async { completion(nil, self.gridMatrixError()) } + return + } + var videoFileURLs = videoFileURLs + if videoFileURLs.count > Int(rows * columns) { + videoFileURLs = videoFileURLs.dropLast(videoFileURLs.count - Int(rows * columns)) + } if (videoResolution.height < 100 || videoResolution.width < 100) { DispatchQueue.main.async { completion(nil, self.videoSizeError()) } return @@ -264,8 +282,12 @@ extension DPVideoMerger : VideoMerger { DispatchQueue.main.async { completion(nil,self.videoTarckError()) } return } + guard let videoAsset = asset.tracks(withMediaType: .video).first else { + DispatchQueue.main.async { completion(nil,self.videoTarckError()) } + return + } do { - try videoTrack.insertTimeRange(CMTimeRangeMake(start: .zero, duration: maxTime), of: asset.tracks(withMediaType: .video).first!, at: .zero) + try videoTrack.insertTimeRange(CMTimeRangeMake(start: .zero, duration: maxTime), of: videoAsset, at: .zero) } catch { DispatchQueue.main.async { completion(nil,error) } return @@ -273,54 +295,83 @@ extension DPVideoMerger : VideoMerger { let currentFrameRate = Int(roundf((videoTrack.nominalFrameRate))) highestFrameRate = (currentFrameRate > highestFrameRate) ? currentFrameRate : highestFrameRate + var isVideoAssetPortrait_ = false + let videoAssetOrientation_ = videoAssetOrientation(videoAsset) + if videoAssetOrientation_ == .right || videoAssetOrientation_ == .left { isVideoAssetPortrait_ = true } + + var videoAssetWidth: CGFloat = videoAsset.naturalSize.width + var videoAssetHeight: CGFloat = videoAsset.naturalSize.height + if isVideoAssetPortrait_ { + videoAssetWidth = videoAsset.naturalSize.height + videoAssetHeight = videoAsset.naturalSize.width + } + + let subInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack) var Scale = CGAffineTransform(scaleX: 1, y: 1) var Move = CGAffineTransform(translationX: 0, y: 0) + var factor:CGFloat = 1.0 var tx : CGFloat = 0 - if videoResolution.width / 2 - (videoTrack.naturalSize.width) != 0 { - tx = ((videoResolution.width / 2 - (videoTrack.naturalSize.width)) / 2) + if videoResolution.width / columns - (videoAssetWidth) != 0 { + tx = ((videoResolution.width / columns - videoAssetWidth) / 2) } var ty : CGFloat = 0 - if videoResolution.height / 2 - (videoTrack.naturalSize.height) != 0 { - ty = ((videoResolution.height / 2 - (videoTrack.naturalSize.height)) / 2) + if videoResolution.height / rows - (videoAssetHeight) != 0 { + ty = ((videoResolution.height / rows - videoAssetHeight) / 2) } if tx != 0 && ty != 0 { - if tx <= ty { - let factor = CGFloat(videoResolution.width / 2 / videoTrack.naturalSize.width) - Scale = CGAffineTransform(scaleX: CGFloat(factor), y: CGFloat(factor)) + let factorWidth = CGFloat(videoResolution.width / columns / videoAssetWidth) + let factorHeight = CGFloat(videoResolution.height / rows / videoAssetHeight) + if factorHeight > factorWidth { + factor = factorWidth + Scale = CGAffineTransform(scaleX: factor, y: factor) tx = 0 - ty = (videoResolution.height / 2 - videoTrack.naturalSize.height * factor) / 2 - } - if tx > ty { - let factor = CGFloat(videoResolution.height / 2 / videoTrack.naturalSize.height) + ty = (videoResolution.height / rows - videoAssetHeight * factor) / 2 + } else { + factor = factorHeight Scale = CGAffineTransform(scaleX: factor, y: factor) ty = 0 - tx = (videoResolution.width / 2 - videoTrack.naturalSize.width * factor) / 2 + tx = (videoResolution.width / columns - videoAssetWidth * factor) / 2 } } - switch i { - case 0: - Move = CGAffineTransform(translationX: CGFloat(0 + tx), y: 0 + ty) - case 1: - Move = CGAffineTransform(translationX: videoResolution.width / 2 + tx, y: 0 + ty) - case 2: - Move = CGAffineTransform(translationX: 0 + tx, y: videoResolution.height / 2 + ty) - case 3: - Move = CGAffineTransform(translationX: videoResolution.width / 2 + tx, y: videoResolution.height / 2 + ty) - default: - break + var orientation = CGAffineTransform.identity + switch videoAssetOrientation_ { + case .down: + orientation = CGAffineTransform(rotationAngle: degreeToRadian(180)) + tx = (videoAssetWidth*factor) + CGFloat(tx) + ty = (videoAssetHeight*factor) + CGFloat(ty) + case .left: + orientation = CGAffineTransform(rotationAngle: degreeToRadian(270)) + ty = (videoAssetHeight*factor) + CGFloat(ty) + case .right: + orientation = CGAffineTransform(rotationAngle: degreeToRadian(90)) + tx = (videoAssetWidth * factor) + CGFloat(tx) + default: + break; } - guard insertVideoWithTransform(isRepeatVideo, subInstruction, Scale, Move, &arrAVMutableVideoCompositionLayerInstruction, asset, composition, completion, maxTime) else { + let columnIndex = CGFloat(i % Int(columns)) + let rowIndex = CGFloat(i / Int(columns)) +// debugPrint("\(columnIndex) x \(rowIndex)") + Move = CGAffineTransform(translationX: CGFloat((videoResolution.width / columns)*columnIndex) + tx, y: CGFloat((videoResolution.width / rows)*rowIndex) + ty) + + + guard insertVideoWithTransform(isRepeatVideo, subInstruction, orientation.concatenating(Scale), Move, &arrAVMutableVideoCompositionLayerInstruction, asset, composition, completion, maxTime) else { return } } - - addAudioToMergedVideo(audioFileURL, composition, isRepeatAudio, maxTime, completion) - + if isAudio { + if let audioFileURL = audioFileURL { + addAudioToMergedVideo(audioFileURL, composition, isRepeatAudio, maxTime, completion) + } else { + videoFileURLs.forEach { (audioFileURL) in + addAudioToMergedVideo(audioFileURL, composition, isRepeatAudio, maxTime, completion) + } + } + } instruction.layerInstructions = arrAVMutableVideoCompositionLayerInstruction.reversed() exportMergedVideo([instruction], highestFrameRate, videoResolution, composition, videoQuality, completion) @@ -343,6 +394,7 @@ extension DPVideoMerger : VideoMerger { /// - completion: completion give 2 optional values, 1)mergedVideoURL: URL path of successfully parallel merged video 2)error: gives Error object if some error occur in videos merging process /// - mergedVideoURL: URL path of successfully parallel merged video /// - error: gives Error object if some error occur in videos merging process + @available(*, deprecated, message: "Use grid merge using matrix") open func parallelMergeVideos(withFileURLs videoFileURLs: [URL], audioFileURL: URL? = nil, @@ -353,99 +405,15 @@ extension DPVideoMerger : VideoMerger { videoQuality: String = AVAssetExportPresetMediumQuality, alignment: ParallelMergeAlignment = .vertical, completion: @escaping (_ mergedVideoURL: URL?, _ error: Error?) -> Void) { - if videoFileURLs.count <= 1 { - DispatchQueue.main.async { completion(nil, self.videoMoreThenOneError()) } - return - } - if (videoResolution.height < 100 || videoResolution.width < 100) { - DispatchQueue.main.async { completion(nil, self.videoSizeError()) } - return - } - - let composition = AVMutableComposition() - var maxTime = maxTimeFromVideos(videoFileURLs) - var highestFrameRate = 0 - - if (videoDuration != -1) { - let videoDurationTime = CMTimeMake(value: Int64(videoDuration), timescale: 1) - if CMTimeCompare(videoDurationTime, maxTime) == -1 { - DispatchQueue.main.async { completion(nil, self.videoDurationError()) } - return - } else { - maxTime = CMTimeMake(value: Int64(videoDuration), timescale: 1) - } - } - let instruction = AVMutableVideoCompositionInstruction() - instruction.timeRange = CMTimeRangeMake(start: .zero, duration: maxTime) - - var arrAVMutableVideoCompositionLayerInstruction: [AVMutableVideoCompositionLayerInstruction] = [] - for i in 0 ..< videoFileURLs.count { - let vCount : CGFloat = CGFloat(videoFileURLs.count) - let videoFileURL = videoFileURLs[i] - let asset = AVURLAsset(url: videoFileURL, options: nil) - guard let videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) else { - DispatchQueue.main.async { completion(nil,self.videoTarckError()) } - return - } - do { - try videoTrack.insertTimeRange(CMTimeRangeMake(start: .zero, duration: maxTime), of: asset.tracks(withMediaType: .video).first!, at: .zero) - } catch { - DispatchQueue.main.async { completion(nil,error) } - return - } - let currentFrameRate = Int(roundf((videoTrack.nominalFrameRate))) - highestFrameRate = (currentFrameRate > highestFrameRate) ? currentFrameRate : highestFrameRate - - let subInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack) - - var Scale = CGAffineTransform(scaleX: 1, y: 1) - var Move = CGAffineTransform(translationX: 0, y: 0) - var tx : CGFloat = 0 - if videoResolution.width / ((alignment == .vertical) ? vCount : 1) - (videoTrack.naturalSize.width) != 0 { - tx = ((videoResolution.width / ((alignment == .vertical) ? vCount : 1) - (videoTrack.naturalSize.width)) / 2) - } - var ty : CGFloat = 0 - if videoResolution.height / ((alignment == .vertical) ? 1 : vCount) - (videoTrack.naturalSize.height) != 0 { - ty = ((videoResolution.height / ((alignment == .vertical) ? 1 : vCount) - (videoTrack.naturalSize.height)) / 2) - } - if tx != 0 && ty != 0 { - if tx <= ty { - let factor = CGFloat(videoResolution.width / ((alignment == .vertical) ? vCount : 1) / videoTrack.naturalSize.width) - Scale = CGAffineTransform(scaleX: CGFloat(factor), y: CGFloat(factor)) - tx = 0 - ty = (videoResolution.height / ((alignment == .vertical) ? 1 : vCount) - videoTrack.naturalSize.height * factor) / 2 - } - if tx > ty { - let factor = CGFloat(videoResolution.height / ((alignment == .vertical) ? 1 : vCount) / videoTrack.naturalSize.height) - Scale = CGAffineTransform(scaleX: factor, y: factor) - ty = 0 - tx = (videoResolution.width / ((alignment == .vertical) ? vCount : 1) - videoTrack.naturalSize.width * factor) / 2 - } - } - - switch i { - case 0: - Move = CGAffineTransform(translationX: CGFloat(0 + tx), y: 0 + ty) - default: - Move = CGAffineTransform(translationX: ((alignment == .vertical) ? (videoResolution.width / (vCount/CGFloat(i))) : 0) + tx, y: ((alignment == .vertical) ? 0 : (videoResolution.height / (vCount/CGFloat(i)))) + ty) - break - } - - guard insertVideoWithTransform(isRepeatVideo, subInstruction, Scale, Move, &arrAVMutableVideoCompositionLayerInstruction, asset, composition, completion, maxTime) else { - return - } - + var matrix: DPVideoMatrix! + if alignment == .vertical { + matrix = DPVideoMatrix(rows: 1, columns: UInt(videoFileURLs.count)) + } else { + matrix = DPVideoMatrix(rows: UInt(videoFileURLs.count), columns: 1) } - - addAudioToMergedVideo(audioFileURL, composition, isRepeatAudio, maxTime, completion) - - instruction.layerInstructions = arrAVMutableVideoCompositionLayerInstruction.reversed() - - exportMergedVideo([instruction], highestFrameRate, videoResolution, composition, videoQuality, completion) - + gridMergeVideos(withFileURLs: videoFileURLs, matrix: matrix, audioFileURL: audioFileURL, videoResolution: videoResolution, isRepeatVideo: isRepeatVideo, isRepeatAudio: isRepeatAudio, isAudio: true, videoDuration: videoDuration, videoQuality: videoQuality, completion: completion) } - } @@ -473,6 +441,21 @@ fileprivate extension DPVideoMerger { return maxTime } + func videoAssetOrientation(_ videoAsset: AVAssetTrack) -> UIImage.Orientation { + let videoTransform: CGAffineTransform = videoAsset.preferredTransform + var videoAssetOrientation_: UIImage.Orientation = .up + if videoTransform.a == 0 && videoTransform.b == 1.0 && videoTransform.c == -1.0 && videoTransform.d == 0 { + videoAssetOrientation_ = .right + } else if videoTransform.a == 0 && videoTransform.b == -1.0 && videoTransform.c == 1.0 && videoTransform.d == 0 { + videoAssetOrientation_ = .left + } else if videoTransform.a == 1.0 && videoTransform.b == 0 && videoTransform.c == 0 && videoTransform.d == 1.0 { + videoAssetOrientation_ = .up + } else if videoTransform.a == -1.0 && videoTransform.b == 0 && videoTransform.c == 0 && videoTransform.d == -1.0 { + videoAssetOrientation_ = .down + } + return videoAssetOrientation_ + } + func insertVideoWithTransform(_ isRepeatVideo: Bool, _ subInstruction: AVMutableVideoCompositionLayerInstruction, _ Scale: CGAffineTransform, _ Move: CGAffineTransform, _ arrAVMutableVideoCompositionLayerInstruction: inout [AVMutableVideoCompositionLayerInstruction], _ asset: AVURLAsset, _ composition: AVMutableComposition, _ completion: @escaping (URL?, Error?) -> Void, _ maxTime: CMTime) -> Bool { if (isRepeatVideo) { subInstruction.setTransform(Scale.concatenating(Move), at: .zero) @@ -526,13 +509,13 @@ fileprivate extension DPVideoMerger { DispatchQueue.main.async {completion(nil, self.audioTarckError()) } return } - if (isRepeatAudio) { - do { - try audioTrack.insertTimeRange(CMTimeRangeMake(start: .zero, duration: asset.duration), of: audioAsset, at: .zero) - } catch { - DispatchQueue.main.async { completion(nil,error) } - return - } + do { + try audioTrack.insertTimeRange(CMTimeRangeMake(start: .zero, duration: maxTime.seconds < asset.duration.seconds ? maxTime : asset.duration), of: audioAsset, at: .zero) + } catch { + DispatchQueue.main.async { completion(nil,error) } + return + } + if (isRepeatAudio && maxTime.seconds > asset.duration.seconds) { var dur = asset.duration repeat { dur = CMTimeAdd(dur, asset.duration) @@ -555,13 +538,6 @@ fileprivate extension DPVideoMerger { } } while CMTimeCompare(maxTime, dur) != -1 - } else { - do { - try audioTrack.insertTimeRange(CMTimeRangeMake(start: .zero, duration: asset.duration), of: audioAsset, at: .zero) - } catch { - DispatchQueue.main.async { completion(nil,error) } - return - } } } } @@ -581,8 +557,8 @@ fileprivate extension DPVideoMerger { exporter?.outputFileType = .mp4 exporter?.shouldOptimizeForNetworkUse = true - print("Composition Duration: %ld s", lround(CMTimeGetSeconds(composition.duration))) - print("Composition Framerate: %d fps", highestFrameRate) + debugPrint("Composition Duration: %ld s", lround(CMTimeGetSeconds(composition.duration))) + debugPrint("Composition Framerate: %d fps", highestFrameRate) let exportCompletion: (() -> Void) = {() -> Void in DispatchQueue.main.async(execute: {() -> Void in @@ -593,22 +569,22 @@ fileprivate extension DPVideoMerger { exportSession.exportAsynchronously(completionHandler: {() -> Void in switch exportSession.status { case .completed: - print("Successfully merged: %@", exportSession.outputURL ?? "") + debugPrint("Successfully merged: %@", exportSession.outputURL ?? "") exportCompletion() case .failed: - print("Failed %@",exportSession.error ?? "") + debugPrint("Failed %@",exportSession.error ?? "") exportCompletion() case .cancelled: - print("Cancelled") + debugPrint("Cancelled") exportCompletion() case .unknown: - print("Unknown") + debugPrint("Unknown") case .exporting: - print("Exporting") + debugPrint("Exporting") case .waiting: - print("Wating") + debugPrint("Wating") @unknown default: - print("default") + debugPrint("default") } }) } @@ -657,7 +633,14 @@ fileprivate extension DPVideoMerger { func videoMoreThenOneError() -> Error { let userInfo: [AnyHashable : Any] = [ NSLocalizedDescriptionKey : NSLocalizedString("error", value: "Provide more then one Video", comment: "") , - NSLocalizedFailureReasonErrorKey : NSLocalizedString("error", value: "parallelMerge required more then one Video", comment: "")] + NSLocalizedFailureReasonErrorKey : NSLocalizedString("error", value: "Video merge required more then one Video", comment: "")] + return NSError(domain: String(describing:DPVideoMerger.self), code: 404, userInfo: (userInfo as! [String : Any])) + } + + func gridMatrixError() -> Error { + let userInfo: [AnyHashable : Any] = + [ NSLocalizedDescriptionKey : NSLocalizedString("error", value: "Grid matrix value error", comment: "") , + NSLocalizedFailureReasonErrorKey : NSLocalizedString("error", value: "Matrix rows or columns value should not be zero.", comment: "")] return NSError(domain: String(describing:DPVideoMerger.self), code: 404, userInfo: (userInfo as! [String : Any])) } diff --git a/DPVideoMerger/Example/ViewController.swift b/DPVideoMerger/Example/ViewController.swift index c8f6042..d8e0950 100644 --- a/DPVideoMerger/Example/ViewController.swift +++ b/DPVideoMerger/Example/ViewController.swift @@ -119,7 +119,7 @@ class ViewController: UIViewController { } if fileURLs.count == self.arrIndex.count { let audioFile = Bundle.main.url(forResource: "file_example_MP3_1MG", withExtension: "mp3") - DPVideoMerger().gridMergeVideos(withFileURLs: fileURLs, audioFileURL: audioFile, videoResolution: CGSize(width: 1000, height: 1000),isRepeatVideo: true, isRepeatAudio: true, videoQuality:AVAssetExportPresetHighestQuality ,completion: {(_ mergedVideoFile: URL?, _ error: Error?) -> Void in + DPVideoMerger().gridMergeVideos(withFileURLs: fileURLs, matrix: DPVideoMatrix(rows: 4, columns: 2), audioFileURL: audioFile, videoResolution: CGSize(width: 1000, height: 1000),isRepeatVideo: true, isRepeatAudio: true, isAudio: true,videoQuality:AVAssetExportPresetHighestQuality ,completion: {(_ mergedVideoFile: URL?, _ error: Error?) -> Void in self.activityIndicatorView.stopAnimating() self.view.isUserInteractionEnabled = true self.activityIndicatorView.isHidden = true @@ -173,6 +173,8 @@ class ViewController: UIViewController { fileURLs.append(url) } if fileURLs.count == self.arrIndex.count { + // .vertical = DPVideoMatrix(rows: 1, columns: fileURLs.count) + // .horizontal = DPVideoMatrix(rows: fileURLs.count, columns: 0) DPVideoMerger().parallelMergeVideos(withFileURLs: fileURLs, audioFileURL: fileURLs.first, videoResolution: CGSize(width: 1000, height: 900),isRepeatVideo: true, isRepeatAudio: true, videoQuality:AVAssetExportPresetHighestQuality , alignment: .vertical ,completion: {(_ mergedVideoFile: URL?, _ error: Error?) -> Void in self.activityIndicatorView.stopAnimating() self.view.isUserInteractionEnabled = true @@ -209,6 +211,10 @@ extension ViewController : UICollectionViewDelegate , UICollectionViewDataSource imageManager.requestImage(for: object, targetSize: CGSize(width: collectionView.frame.width/2 - 10, height: collectionView.frame.width/2 - 10), contentMode: .aspectFit, options: nil, resultHandler: { result, info in cell.img.image = result }) + cell.img.alpha = 1 + if arrIndex.contains(indexPath) { + cell.img.alpha = 0.5 + } return cell ; } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { diff --git a/DPVideoMerger_Swift.framework.zip b/DPVideoMerger_Swift.framework.zip index a1ffee8..a47f210 100644 Binary files a/DPVideoMerger_Swift.framework.zip and b/DPVideoMerger_Swift.framework.zip differ diff --git a/README.md b/README.md index 56bc261..4a69813 100644 --- a/README.md +++ b/README.md @@ -103,14 +103,16 @@ DPVideoMerger().mergeVideos(withFileURLs: fileURLs as! [URL], completion: {(_ me }) -/// Merge 4 videos to grid layout +/// Merge videos to grid matrix layout /// - Parameters: -/// - videoFileURLs: Video file path URLs, Array of 4 videos that going to grid merge +/// - videoFileURLs: Video file path URLs, Array of videos that going to grid merge +/// - matrix: Video matrix position (eg 3x3, 4x2, 1x3, ...) (default:- 2x2) /// - audioFileURL: Optional audio file for Merged Video /// - videoResolution: Output video resolution /// - isRepeatVideo: Repeat Video on grid if one or more video have shorter duartion time then output video duration /// - isRepeatAudio: Repeat Audio if Merged video have longer duartion time then provided Audio duration -/// - videoDuration: Output video duration (defult: -1, find max duration from provided 4 videos) +/// - isAudio: Allow Audio for grid video (default :- true) +/// - videoDuration: Output video duration (defult: -1, find max duration from provided videos) /// - videoQuality: AVAssetExportPresetMediumQuality(default) , AVAssetExportPresetLowQuality , AVAssetExportPresetHighestQuality /// - completion: completion give 2 optional values, 1)mergedVideoURL: URL path of successfully grid merged video 2)error: gives Error object if some error occur in videos merging process /// - mergedVideoURL: URL path of successfully grid merged video @@ -130,35 +132,4 @@ DPVideoMerger().gridMergeVideos(withFileURLs: fileURLs, videoResolution: CGSize( objAVPlayerVC.player?.play() }) }) - - -/// Merge side by side videos layout -/// - Parameters: -/// - videoFileURLs: Video file path URLs, Array videos that going to parallel merge -/// - audioFileURL: Optional audio file for Merged Video -/// - videoResolution: Output video resolution -/// - isRepeatVideo: Repeat Video if one or more video have shorter duartion time then output video duration -/// - isRepeatAudio: Repeat Audio if Merged video have longer duartion time then provided Audio duration -/// - videoDuration: Output video duration (defult: -1, find max duration from provided videos) -/// - videoQuality: AVAssetExportPresetMediumQuality(default) , AVAssetExportPresetLowQuality , AVAssetExportPresetHighestQuality -/// - alignment: Video merge alignment -1) vertical 2) horizontal (defult: vertical) -/// - completion: completion give 2 optional values, 1)mergedVideoURL: URL path of successfully parallel merged video 2)error: gives Error object if some error occur in videos merging process -/// - mergedVideoURL: URL path of successfully parallel merged video -/// - error: gives Error object if some error occur in videos merging process -DPVideoMerger().parallelMergeVideos(withFileURLs: fileURLs, videoResolution: CGSize(width: 1000, height: 600),isRepeatVideo: true, videoQuality:AVAssetExportPresetHighestQuality , alignment: .vertical ,completion: {(_ mergedVideoFile: URL?, _ error: Error?) -> Void in - if error != nil { - let errorMessage = "Could not merge videos: \(error?.localizedDescription ?? "error")" - let alert = UIAlertController(title: "Error", message: errorMessage, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (a) in - })) - self.present(alert, animated: true) {() -> Void in } - return - } - let objAVPlayerVC = AVPlayerViewController() - objAVPlayerVC.player = AVPlayer(url: mergedVideoFile!) - self.present(objAVPlayerVC, animated: true, completion: {() -> Void in - objAVPlayerVC.player?.play() - }) - -}) ```