Last active
September 11, 2019 10:45
-
-
Save nazmulkp/f7b66e8c2516c9b0b443ff220679dfb0 to your computer and use it in GitHub Desktop.
marge video file
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// KVVideoManager.swift | |
// MergeVideos | |
// | |
// Created by Khoa Vo on 12/20/17. | |
// Copyright © 2017 Khoa Vo. All rights reserved. | |
// | |
import UIKit | |
import MediaPlayer | |
import MobileCoreServices | |
import AVKit | |
class KVVideoManager: NSObject { | |
static let shared = KVVideoManager() | |
let defaultSize = CGSize(width: 1920, height: 1080) // Default video size | |
var videoDuration = 30.0 // Duration of output video when merging videos & images | |
var imageDuration = 5.0 // Duration of each image | |
typealias Completion = (URL?, Error?) -> Void | |
// | |
// Merge array videos | |
// | |
func merge(arrayVideos:[AVAsset], completion:@escaping Completion) -> Void { | |
doMerge(arrayVideos: arrayVideos, animation: false, completion: completion) | |
} | |
private func doMerge(arrayVideos:[AVAsset], animation:Bool, completion:@escaping Completion) -> Void { | |
var insertTime = kCMTimeZero | |
var arrayLayerInstructions:[AVMutableVideoCompositionLayerInstruction] = [] | |
var outputSize = CGSize.init(width: 0, height: 0) | |
// Determine video output size | |
for videoAsset in arrayVideos { | |
let videoTrack = videoAsset.tracks(withMediaType: AVMediaType.video)[0] | |
let assetInfo = orientationFromTransform(transform: videoTrack.preferredTransform) | |
var videoSize = videoTrack.naturalSize | |
if assetInfo.isPortrait == true { | |
videoSize.width = videoTrack.naturalSize.height | |
videoSize.height = videoTrack.naturalSize.width | |
} | |
if videoSize.height > outputSize.height { | |
outputSize = videoSize | |
} | |
} | |
if outputSize.width == 0 || outputSize.height == 0 { | |
outputSize = defaultSize | |
} | |
// Init composition | |
let mixComposition = AVMutableComposition.init() | |
for videoAsset in arrayVideos { | |
// Get video track | |
guard let videoTrack = videoAsset.tracks(withMediaType: AVMediaType.video).first else { continue } | |
// Get audio track | |
var audioTrack:AVAssetTrack? | |
if videoAsset.tracks(withMediaType: AVMediaType.audio).count > 0 { | |
audioTrack = videoAsset.tracks(withMediaType: AVMediaType.audio).first | |
} | |
// Init video & audio composition track | |
let videoCompositionTrack = mixComposition.addMutableTrack(withMediaType: AVMediaType.video, | |
preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) | |
let audioCompositionTrack = mixComposition.addMutableTrack(withMediaType: AVMediaType.audio, | |
preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) | |
do { | |
let startTime = kCMTimeZero | |
let duration = videoAsset.duration | |
// Add video track to video composition at specific time | |
try videoCompositionTrack?.insertTimeRange(CMTimeRangeMake(startTime, duration), | |
of: videoTrack, | |
at: insertTime) | |
// Add audio track to audio composition at specific time | |
if let audioTrack = audioTrack { | |
try audioCompositionTrack?.insertTimeRange(CMTimeRangeMake(startTime, duration), | |
of: audioTrack, | |
at: insertTime) | |
} | |
// Add instruction for video track | |
let layerInstruction = videoCompositionInstructionForTrack(track: videoCompositionTrack!, | |
asset: videoAsset, | |
standardSize: outputSize, | |
atTime: insertTime) | |
arrayLayerInstructions.append(layerInstruction) | |
insertTime = CMTimeAdd(insertTime, duration) | |
} | |
catch { | |
print("Load track error") | |
} | |
} | |
// Main video composition instruction | |
let mainInstruction = AVMutableVideoCompositionInstruction() | |
mainInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, insertTime) | |
mainInstruction.layerInstructions = arrayLayerInstructions | |
// Main video composition | |
let mainComposition = AVMutableVideoComposition() | |
mainComposition.instructions = [mainInstruction] | |
mainComposition.frameDuration = CMTimeMake(1, 30) | |
mainComposition.renderSize = outputSize | |
//Roted video user desire | |
// let firstTrack = mixComposition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) | |
// | |
// | |
// var firstlayerInstruction: AVMutableVideoCompositionLayerInstruction? = nil | |
// if let firstTrack = firstTrack { | |
// firstlayerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: firstTrack) | |
// } | |
// | |
// firstlayerInstruction?.setTransform(CGAffineTransform(rotationAngle: .pi / 2), at: insertTime) | |
// | |
// firstlayerInstruction?.setOpacity(0.0, at: kCMTimeZero) | |
// | |
// mainInstruction.layerInstructions = [firstlayerInstruction].compactMap { $0 } | |
// | |
// let mainCompositionInst = AVMutableVideoComposition() | |
// | |
// mainCompositionInst.instructions = [mainInstruction] | |
// mainCompositionInst.frameDuration = CMTimeMake(1, 30) | |
// mainCompositionInst.renderSize = CGSize(width: 320.0, height: 480.0) | |
// Export to file | |
let path = NSTemporaryDirectory().appending("mergedVideo.mp4") | |
let exportURL = URL.init(fileURLWithPath: path) | |
// Remove file if existed | |
FileManager.default.removeItemIfExisted(exportURL) | |
// Init exporter | |
let exporter = AVAssetExportSession.init(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality) | |
exporter?.outputURL = exportURL | |
//exporter?.videoComposition = mainCompositionInst | |
exporter?.outputFileType = AVFileType.mp4 | |
exporter?.shouldOptimizeForNetworkUse = true | |
exporter?.videoComposition = mainComposition | |
// Do export | |
exporter?.exportAsynchronously(completionHandler: { | |
DispatchQueue.main.async { | |
self.exportDidFinish(exporter: exporter, videoURL: exportURL, completion: completion) | |
} | |
}) | |
} | |
} | |
// MARK:- Private methods | |
extension KVVideoManager { | |
fileprivate func orientationFromTransform(transform: CGAffineTransform) -> (orientation: UIImageOrientation, isPortrait: Bool) { | |
var assetOrientation = UIImageOrientation.up | |
var isPortrait = false | |
if transform.a == 0 && transform.b == 1.0 && transform.c == -1.0 && transform.d == 0 { | |
assetOrientation = .right | |
isPortrait = true | |
} else if transform.a == 0 && transform.b == -1.0 && transform.c == 1.0 && transform.d == 0 { | |
assetOrientation = .left | |
isPortrait = true | |
} else if transform.a == 1.0 && transform.b == 0 && transform.c == 0 && transform.d == 1.0 { | |
assetOrientation = .up | |
} else if transform.a == -1.0 && transform.b == 0 && transform.c == 0 && transform.d == -1.0 { | |
assetOrientation = .down | |
} | |
return (assetOrientation, isPortrait) | |
} | |
fileprivate func videoCompositionInstructionForTrack(track: AVCompositionTrack, asset: AVAsset, standardSize:CGSize, atTime: CMTime) -> AVMutableVideoCompositionLayerInstruction { | |
let instruction = AVMutableVideoCompositionLayerInstruction(assetTrack: track) | |
let assetTrack = asset.tracks(withMediaType: AVMediaType.video)[0] | |
let transform = assetTrack.preferredTransform | |
let assetInfo = orientationFromTransform(transform: transform) | |
var aspectFillRatio:CGFloat = 1 | |
if assetTrack.naturalSize.height < assetTrack.naturalSize.width { | |
aspectFillRatio = standardSize.height / assetTrack.naturalSize.height | |
} | |
else { | |
aspectFillRatio = standardSize.width / assetTrack.naturalSize.width | |
} | |
if assetInfo.isPortrait { | |
let scaleFactor = CGAffineTransform(scaleX: aspectFillRatio, y: aspectFillRatio) | |
let posX = standardSize.width/2 - (assetTrack.naturalSize.height * aspectFillRatio)/2 | |
let posY = standardSize.height/2 - (assetTrack.naturalSize.width * aspectFillRatio)/2 | |
let moveFactor = CGAffineTransform(translationX: posX, y: posY) | |
instruction.setTransform(assetTrack.preferredTransform.concatenating(scaleFactor).concatenating(moveFactor), at: atTime) | |
} else { | |
let scaleFactor = CGAffineTransform(scaleX: aspectFillRatio, y: aspectFillRatio) | |
let posX = standardSize.width/2 - (assetTrack.naturalSize.width * aspectFillRatio)/2 | |
let posY = standardSize.height/2 - (assetTrack.naturalSize.height * aspectFillRatio)/2 | |
let moveFactor = CGAffineTransform(translationX: posX, y: posY) | |
var concat = assetTrack.preferredTransform.concatenating(scaleFactor).concatenating(moveFactor) | |
if assetInfo.orientation == .down { | |
let fixUpsideDown = CGAffineTransform(rotationAngle: CGFloat(Double.pi)) | |
concat = fixUpsideDown.concatenating(scaleFactor).concatenating(moveFactor) | |
} | |
instruction.setTransform(concat, at: atTime) | |
} | |
return instruction | |
} | |
fileprivate func exportDidFinish(exporter:AVAssetExportSession?, videoURL:URL, completion:@escaping Completion) -> Void { | |
if exporter?.status == AVAssetExportSessionStatus.completed { | |
print("Exported file: \(videoURL.absoluteString)") | |
completion(videoURL,nil) | |
} | |
else if exporter?.status == AVAssetExportSessionStatus.failed { | |
completion(videoURL,exporter?.error) | |
} | |
} | |
} | |
extension FileManager { | |
func removeItemIfExisted(_ url:URL) -> Void { | |
if FileManager.default.fileExists(atPath: url.path) { | |
do { | |
try FileManager.default.removeItem(atPath: url.path) | |
} | |
catch { | |
print("Failed to delete file") | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment