ZbadhabitZ
ZbadhabitZ

Reputation: 2913

Difficulty Merging Two Videos Using AVMutableComposition

I am working on a project wherein I am trying to merge two AVAssets (video files) together using an AVMutableComposition. My video files, when saved to my Camera Roll, are both exactly as expected. Their URL is valid, but yet my final exported product only shows the first video, and not the second merged video. This is the code I am working with:

    // Setup video asset
    let videoAsset: AVAsset = AVAsset( url: clip1 )

    // Setup composition
    let composition = AVMutableComposition()

    // Get video track
    let vtrack = videoAsset.tracks(withMediaType: AVMediaType.video)

    // Setup the first video track as asset track
    let videoTrack: AVAssetTrack = vtrack[0]

    // Setup the video timerange
    let vid_timerange = CMTimeRangeMake(kCMTimeZero, videoAsset.duration)

    // Setup the composition video track
    let compositionvideoTrack:AVMutableCompositionTrack = composition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: kCMPersistentTrackID_Invalid)!

    // Insert expected time range
    do {
        try compositionvideoTrack.insertTimeRange(vid_timerange, of: videoTrack, at: kCMTimeZero)
    } catch {
        print("An error occurred")
    }

    // Setup second video asset
    let reversedAsset: AVAsset = AVAsset( url: clip2 )

    // Setup the video track
    let vtrack1 = reversedAsset.tracks(withMediaType: AVMediaType.video)

    // Setup the video track
    let videoTrack1: AVAssetTrack = vtrack1[0]

    // Setup the video time range
    let vid1_timerange = CMTimeRangeMake(kCMTimeZero, reversedAsset.duration)

    // Setup the second composition video track
    let secondCompositionVideoTrack:AVMutableCompositionTrack = composition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: kCMPersistentTrackID_Invalid)!

    // Insert time range
    do {
        try secondCompositionVideoTrack.insertTimeRange(vid1_timerange, of: videoTrack1, at: videoAsset.duration)
    } catch {
        print("An error occurred")
    }

    // Setup the folder path
    let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)

    // Setup documents directory
    let documentsDirectory = paths[0] as String

    // Setup the last path component
    let lastPath = clip1.lastPathComponent

    // Setup the reverse string
    let reverseString = "loop-" + lastPath

    // Setup desired full path
    let fullPath: String = "\(documentsDirectory)/\(reverseString)"

    // Setup reverse destination URL
    let reverseURL = URL(fileURLWithPath: fullPath)

    // Export
    let exportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality)!

    // Setup the destination for output
    exportSession.outputURL = reverseURL

    // Setup the file type
    exportSession.outputFileType = AVFileType.mp4

    exportSession.exportAsynchronously(completionHandler: {
        if exportSession.status == .completed {

            // Send completion on main queue
            DispatchQueue.main.async(execute: {

                PHPhotoLibrary.shared().performChanges({PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: reverseURL)
                }) { saved, error in
                    if saved {
                        print("save of potentially looped video succesful")
                    }
                }

                // Send completion handler
                completionHandler(reverseURL)

            })
            return
        } else if exportSession.status == .failed {
            print("Loop Export failed - \(String(describing: exportSession.error))")
            completionHandler(nil)
        }

        completionHandler(nil)
        return
    })

What's being saved to my Camera Roll is only the first clip, with no evidence of the second clip in sight. Any help would be greatly appreciated. Thanks!

Upvotes: 3

Views: 4505

Answers (2)

Gordon Childs
Gordon Childs

Reputation: 36169

Your second clip starts at time zero when it should start at the first clip’s duration:

let vid1_timerange = CMTimeRangeMake(videoAsset.duration, reversedAsset.duration)

Upvotes: 4

Sagar Chauhan
Sagar Chauhan

Reputation: 5823

I have write a following code to merge video. This is working fine. I have also written comments above code line where is required.

let videoAssets1 = AVAsset(url: videoUrl1)
let videoAssets2 = AVAsset(url: videoUrl2)

let mixComposition = AVMutableComposition()

// Create composition track for first video
let firstCompositionTrack = mixComposition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)
do {
        try firstCompositionTrack?.insertTimeRange(CMTimeRange(start: kCMTimeZero, end: videoAssets1.duration), of: videoAssets1.tracks(withMediaType: .video)[0], at: kCMTimeZero)
    } catch {
        print("Error = \(error.localizedDescription)")
}

// Create composition track for second video
let secondCompositionTrack = mixComposition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)
do {
        try secondCompositionTrack?.insertTimeRange(CMTimeRange(start: kCMTimeZero, end: videoAssets2.duration), of: videoAssets2.tracks(withMediaType: .video)[0], at: kCMTimeZero)
} catch {
        print("Error = \(error.localizedDescription)")
}

//See how we are creating AVMutableVideoCompositionInstruction object.This object will contain the array of our AVMutableVideoCompositionLayerInstruction objects.You set the duration of the layer.You should add the lenght equal to the lingth of the longer asset in terms of duration.
let mainInstruction = AVMutableVideoCompositionInstruction()
mainInstruction.timeRange = CMTimeRange(start: kCMTimeZero, duration: videoAssets1.duration)

// We will be creating 2 AVMutableVideoCompositionLayerInstruction objects.
// Each for our 2 AVMutableCompositionTrack.
// Here we are creating AVMutableVideoCompositionLayerInstruction for out first track.
// See how we make use of CGAffineTransform to move and scale our First Track.
// So it is displayed at the bottom of the screen in smaller size.
// (First track in the one that remains on top).    
let firstLayerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: firstCompositionTrack!)
let firstScale : CGAffineTransform = CGAffineTransform(scaleX: 1, y: 1)
let firstMove: CGAffineTransform = CGAffineTransform(translationX: 0, y: 0)
firstLayerInstruction.setTransform(firstScale.concatenating(firstMove), at: kCMTimeZero)


// Here we are creating AVMutableVideoCompositionLayerInstruction for second track.
// See how we make use of CGAffineTransform to move and scale our second Track.
let secondLayerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: secondCompositionTrack!)
let secondScale : CGAffineTransform = CGAffineTransform(scaleX: 1, y: 1)
let secondMove : CGAffineTransform = CGAffineTransform(translationX: (firstCompositionTrack?.naturalSize.width)! + CGFloat(20), y: 0)
secondLayerInstruction.setTransform(secondScale.concatenating(secondMove), at: kCMTimeZero)

//Now we add our 2 created AVMutableVideoCompositionLayerInstruction objects to our AVMutableVideoCompositionInstruction in form of an array.
mainInstruction.layerInstructions = [firstLayerInstruction, secondLayerInstruction]

// Get the height and width of video.
let height = (Float((firstCompositionTrack?.naturalSize.height)!) > Float((secondCompositionTrack?.naturalSize.height)!)) ? firstCompositionTrack?.naturalSize.height : secondCompositionTrack?.naturalSize.height

//  height will be larger in both and width is total of both video.
let width = CGFloat((Float((firstCompositionTrack?.naturalSize.width)!) + Float((secondCompositionTrack?.naturalSize.width)!))) + CGFloat(20)

//Now we create AVMutableVideoComposition object.
//We can add mutiple AVMutableVideoCompositionInstruction to this object.
//We have only one AVMutableVideoCompositionInstruction object in our example.
//You can use multiple AVMutableVideoCompositionInstruction objects to add multiple layers of effects such as fade and transition but make sure that time ranges of the AVMutableVideoCompositionInstruction objects don't overlap.
let mainCompositionInst = AVMutableVideoComposition()
mainCompositionInst.instructions = [mainInstruction]
mainCompositionInst.frameDuration = CMTime(value: CMTimeValue(1), timescale: CMTimeScale(30))
mainCompositionInst.renderSize = CGSize(width: width, height: height!)


// Create the export session with the composition and set the preset to the highest quality.
let exporter = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality)

// Set the desired output URL for the file created by the export process.
exporter?.outputURL = URL(fileURLWithPath: self.getVideoPath(name: "videoCompose"))
exporter?.videoComposition = mainCompositionInst

// Set the output file type to be a mp4 movie.
exporter?.outputFileType = AVFileType.mp4
exporter?.shouldOptimizeForNetworkUse = true

exporter?.exportAsynchronously(completionHandler: {() -> Void in
    DispatchQueue.main.async(execute: {() -> Void in
        if exporter?.status == .completed {

            do {
                let videoData = try Data(contentsOf: exporter!.outputURL!)

                // Here video will save in document directory path, you can use your requirement.
                try videoData.write(to: URL(fileURLWithPath: self.getVideoPath(name: "videoCompose")), options: Data.WritingOptions.atomic)

            } catch {
                print("Failed to Save video ===>>> \(error.localizedDescription)")
            }
        }
    })
})

I hope this will helpful to you.

Upvotes: 7

Related Questions