如何将(hstack)多个视频与 AVMutableVideoComposition 并排组合?

How to combine (hstack) multiple videos side by side with AVMutableVideoComposition?

我正在尝试将 3 个视频组合成单个视频,其中视频被裁剪并并排放置在 hstack 中。

这是我目前的解决方案,它只将第一个视频并排放置 3 次(重复)。我不知道我做错了什么。

import Foundation
import AVFoundation


func hstackVideos() {
    let videoPaths: [String] = [
        "path/to/video1.mp4",
        "path/to/video2.mp4",
        "path/to/video3.mp4",
    ]

    let composition = AVMutableComposition()

    let assetInfos: [(AVURLAsset, AVAssetTrack, AVMutableCompositionTrack)] = videoPaths.map {
        let asset = AVURLAsset(url: URL(fileURLWithPath: [=10=]))
        let track = composition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: kCMPersistentTrackID_Invalid)!
        let videoAssetTrack = asset.tracks(withMediaType: .video)[0]
        try! track.insertTimeRange(CMTimeRangeMake(start: videoAssetTrack.timeRange.start, duration: videoAssetTrack.timeRange.duration), of: videoAssetTrack, at: videoAssetTrack.timeRange.start)
        return (asset, videoAssetTrack, track)
    }

    let stackComposition = AVMutableVideoComposition()

    stackComposition.renderSize = CGSize(width: 512, height: 288)
    stackComposition.frameDuration = CMTime(seconds: 1/30, preferredTimescale: 600)

    var i = 0
    let instructions: [AVMutableVideoCompositionLayerInstruction] = assetInfos.map { (asset, assetTrack, compTrack) in
        let lInst = AVMutableVideoCompositionLayerInstruction(assetTrack: assetTrack)
        let w: CGFloat = 512/CGFloat(assetInfos.count)
        let inRatio = assetTrack.naturalSize.width / assetTrack.naturalSize.height
        let cropRatio = w / 288
        let scale: CGFloat
        if inRatio < cropRatio {
            scale = w / assetTrack.naturalSize.width
        } else {
            scale = 288 / assetTrack.naturalSize.height
        }
        lInst.setCropRectangle(CGRect(x: w/scale, y: 0, width: w/scale, height: 288/scale), at: CMTime.zero)
        let transform = CGAffineTransform(scaleX: scale, y: scale)
        let t2 = transform.concatenating(CGAffineTransform(translationX: -w + CGFloat(i)*w, y: 0))
        lInst.setTransform(t2, at: CMTime.zero)
        i += 1
        return lInst
    }

    let inst = AVMutableVideoCompositionInstruction()
    inst.timeRange = CMTimeRange(start: CMTime.zero, duration: assetInfos[0].0.duration)
    inst.layerInstructions = instructions

    stackComposition.instructions = [inst]

    let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality)!
    let outPath = "path/to/finalVideo.mp4"
    let outUrl = URL(fileURLWithPath: outPath)
    try? FileManager.default.removeItem(at: outUrl)
    exporter.outputURL = outUrl
    exporter.videoComposition = stackComposition
    exporter.outputFileType = .mp4
    exporter.shouldOptimizeForNetworkUse = true

    let group = DispatchGroup()
    group.enter()
    exporter.exportAsynchronously(completionHandler: {
        switch exporter.status {
            case .completed:
                print("SUCCESS!")
                if exporter.error != nil {
                    print("Error: \(String(describing: exporter.error))")
                    print("Description: \(exporter.description)")
                }
                group.leave()
            case .exporting:
                let progress = exporter.progress
                print("Progress: \(progress)")

            case .failed:
                print("Error: \(String(describing: exporter.error))")
                print("Description: \(exporter.description)")
                group.leave()
            default:
                break
        }
    })

    group.wait()
}

您正在将指令应用于源资产轨道,但您需要将它们应用于输出合成轨道,如下所示:

let instructions: [AVMutableVideoCompositionLayerInstruction] = assetInfos.map { (asset, assetTrack, compTrack) in
    let lInst = AVMutableVideoCompositionLayerInstruction(assetTrack: compTrack)
    let w: CGFloat = 512/CGFloat(assetInfos.count)
    let inRatio = compTrack.naturalSize.width / compTrack.naturalSize.height
    let cropRatio = w / 288
    let scale: CGFloat
    if inRatio < cropRatio {
        scale = w / compTrack.naturalSize.width
    } else {
        scale = 288 / compTrack.naturalSize.height
    }
    lInst.setCropRectangle(CGRect(x: w/scale, y: 0, width: w/scale, height: 288/scale), at: CMTime.zero)
    let transform = CGAffineTransform(scaleX: scale, y: scale)
    let t2 = transform.concatenating(CGAffineTransform(translationX: -w + CGFloat(i)*w, y: 0))
    lInst.setTransform(t2, at: CMTime.zero)
    i += 1
    return lInst
}