Chris
Chris

Reputation: 312

Transform Issue Merging Videos with AVMutableComposition

I have an AVURLAsset array. It contains videos that have been taken with the back camera, front camera (not mirrored), images turned into a video. All in portrait orientation.

I created a function to return a single merged video to play back.

My issue is the original presentation of each video is not supported in the final play back. A video is either rotated, stretched/squished, or mirrored.

I've tried to follow existing SO posts but nothing has worked.

The below function is where I have landed thus far. For added context, if I run the function for an individual AVURLAsset array item, the output video returns correctly. Any guidance would be extremely appreciated.

func merge(assets: [AVURLAsset], completion: @escaping (URL?, AVAssetExportSession?) -> Void) {

let mainComposition = AVMutableComposition()
var lastTime = CMTime.zero

guard let videoCompositionTrack = mainComposition.addMutableTrack(withMediaType: .video, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) else { return }
guard let audioCompositionTrack = mainComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) else { return }

let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoCompositionTrack)

for asset in assets {
                                    
    if let videoTrack = asset.tracks(withMediaType: .video)[safe: 0] {
        
        let t = videoTrack.preferredTransform
        layerInstruction.setTransform(t, at: lastTime)
        
        if let audioTrack = asset.tracks(withMediaType: .audio)[safe: 0] {
            
            do {
                
                try videoCompositionTrack.insertTimeRange(CMTimeRangeMake(start: .zero, duration: asset.duration), of: videoTrack, at: lastTime)
                try audioCompositionTrack.insertTimeRange(CMTimeRangeMake(start: .zero, duration: asset.duration), of: audioTrack, at: lastTime)
                
                print("Inserted audio + video track")
                
            } catch {
                print("Failed to insert audio or video track")
                return
            }
            
            lastTime = CMTimeAdd(lastTime, asset.duration)
            
        } else {
            
            do {
                
                try videoCompositionTrack.insertTimeRange(CMTimeRangeMake(start: .zero, duration: asset.duration), of: videoTrack, at: lastTime)
                
                print("Inserted just video track")
                
            } catch {
                
                print("Failed to insert just video track")
                return
            }
            
            lastTime = CMTimeAdd(lastTime, asset.duration)
        }
    }
}

let outputUrl = NSURL.fileURL(withPath: NSTemporaryDirectory() + "test" + ".mp4")

let videoComposition = AVMutableVideoComposition()
let instruction = AVMutableVideoCompositionInstruction()
instruction.layerInstructions = [layerInstruction]
instruction.timeRange = videoCompositionTrack.timeRange
videoComposition.instructions = [instruction]
videoComposition.frameDuration = videoCompositionTrack.minFrameDuration
videoComposition.renderSize = CGSize(width: 720, height: 1280) // Adjust as per your video dimensions

guard let exporter = AVAssetExportSession(asset: mainComposition, presetName: AVAssetExportPresetHighestQuality) else { return }

exporter.outputURL = outputUrl
exporter.outputFileType = .mp4
exporter.shouldOptimizeForNetworkUse = true
exporter.videoComposition = videoComposition

exporter.exportAsynchronously {
    
    if let outputUrl = exporter.outputURL {
        completion(outputUrl, exporter)
    }
}

play(video: exporter.asset)
}

Upvotes: 1

Views: 99

Answers (1)

Om Sanatan
Om Sanatan

Reputation: 130

let orientations: [UIInterfaceOrientation] = [.landscapeRight, .landscapeLeft, .portraitUpsideDown, .portrait]

func rotateVideo(to orientation: UIInterfaceOrientation) {
    guard let videoURL = "YOUR VIDEO URL" else { return }
    let asset = AVAsset(url: videoURL)
    let composition = AVMutableComposition()
    
    guard let videoTrack = asset.tracks(withMediaType: .video).first else {
        print("Error: No video track found")
        return
    }
    
    guard let videoCompositionTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) else {
        print("Error: Unable to add video track")
        return
    }
    
    do {
        try videoCompositionTrack.insertTimeRange(CMTimeRange(start: .zero, duration: asset.duration), of: videoTrack, at: .zero)
    } catch {
        print("Error: \(error.localizedDescription)")
        return
    }
    
    let videoComposition = AVMutableVideoComposition()
    videoComposition.renderSize = CGSize(width: videoTrack.naturalSize.width, height: videoTrack.naturalSize.height)
    videoComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
    
    let instruction = AVMutableVideoCompositionInstruction()
    instruction.timeRange = CMTimeRange(start: .zero, duration: asset.duration)
    
    let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoCompositionTrack)
    
    var transform = CGAffineTransform.identity
    switch orientation {
    case .portrait:
        print("portrait")
        transform = CGAffineTransform.identity
        videoComposition.renderSize = videoTrack.naturalSize
    case .portraitUpsideDown:
        print("portraitUpsideDown")
        transform = CGAffineTransform(rotationAngle: .pi)
        transform = transform.concatenating(CGAffineTransform(translationX: videoTrack.naturalSize.width, y: videoTrack.naturalSize.height))
        videoComposition.renderSize = videoTrack.naturalSize
    case .landscapeLeft:
        print("landscapeLeft")
        transform = CGAffineTransform(rotationAngle: .pi / 2)
        transform = transform.concatenating(CGAffineTransform(translationX: videoTrack.naturalSize.height, y: 0))
        videoComposition.renderSize = CGSize(width: videoTrack.naturalSize.height, height: videoTrack.naturalSize.width)
    case .landscapeRight:
        print("landscapeRight")
        transform = CGAffineTransform(rotationAngle: -.pi / 2)
        transform = transform.concatenating(CGAffineTransform(translationX: 0, y: videoTrack.naturalSize.width))
        videoComposition.renderSize = CGSize(width: videoTrack.naturalSize.height, height: videoTrack.naturalSize.width)
    default:
        break
    }
    
    layerInstruction.setTransform(transform, at: .zero)
    instruction.layerInstructions = [layerInstruction]
    videoComposition.instructions = [instruction]
    
    let filemanager = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
    let outputURL = filemanager.appendingPathComponent(UUID().uuidString + ".mp4")
    
    guard let exportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality) else {
        print("Error: Failed to create export session")
        return
    }
    
    exportSession.outputURL = outputURL
    exportSession.outputFileType = .mp4
    exportSession.videoComposition = videoComposition
    
    exportSession.exportAsynchronously {
        switch exportSession.status {
        case .completed:
            print(outputURL)
        case .failed, .cancelled:
            print("Error: \(exportSession.error?.localizedDescription ?? "Unknown error")")
        default:
            break
        }
    }
}

can you try this solution it worked for me earlier.

i don't think you want to separate audio and video tracks for transform.

Upvotes: 0

Related Questions