更改assetVideoTrack的preferredTransform来解决合并视频时的镜像问题

时间:2019-07-17 19:47:51

标签: ios swift merge avfoundation transform

我正在使用2个窗格进行记录(SwiftyCam),然后合并记录的多个剪辑(Swift Video Generator)。

但是,我遇到的问题开始严重困扰我。我也为此打开了一个问题。如果您想阅读它,请访问以下链接:Last video in array of multiple videos dicatates whether previous videos are mirrored。请注意(在阅读问题的摘要之前),所有视频均以纵向录制,而使用前置摄像头录制的视频应镜像(作为单个剪辑,也包括在合并的视频中)。

总结一下:如果我仅用一个摄像机录制剪辑,合并的视频看起来就很好(例如,仅使用前置摄像机:每个剪辑都被镜像,并且合并时不会改变)。但是,如果我使用两个摄像机的多个剪辑,比如说我用前置摄像机录制一个剪辑,然后用后置摄像机录制另一个剪辑,则第一个视频(前置摄像机)将在合并的视频中“未镜像”。如果最后一个剪辑是使用前置摄像头录制的,则情况恰好相反:在这种情况下,后置摄像头的所有剪辑都会镜像到合并的视频中。

现在,我尝试查看视频生成器的代码,并在swift video generator, VideoGenerator.swift, l. 309找到了它:

var videoAssets: [AVURLAsset] = [] //at l. 274
//[...]
/// add audio and video tracks to the composition //at l. 309
if let videoTrack: AVMutableCompositionTrack = composition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: kCMPersistentTrackID_Invalid), let audioTrack: AVMutableCompositionTrack = composition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: kCMPersistentTrackID_Invalid) {

    var insertTime = CMTime(seconds: 0, preferredTimescale: 1)

    /// for each URL add the video and audio tracks and their duration to the composition
    for sourceAsset in videoAssets {
        do {
            if let assetVideoTrack = sourceAsset.tracks(withMediaType: .video).first, let assetAudioTrack = sourceAsset.tracks(withMediaType: .audio).first {
                let frameRange = CMTimeRange(start: CMTime(seconds: 0, preferredTimescale: 1), duration: sourceAsset.duration)
                try videoTrack.insertTimeRange(frameRange, of: assetVideoTrack, at: insertTime)
                try audioTrack.insertTimeRange(frameRange, of: assetAudioTrack, at: insertTime)

                videoTrack.preferredTransform = assetVideoTrack.preferredTransform //reference 1
            }

            insertTime = insertTime + sourceAsset.duration
        } catch {
            DispatchQueue.main.async {
                failure(error)
            }
        }
    }

就我而言,我要说的问题是,在遍历资产(assetVideoTrack.preferredTransform)阵列时,videoTrack.preferredTransform仅使用了最后一个视频的reference 1。这就是我遇到的问题。 我看不到解决此问题的方法。我考虑过根据数组中最后一个剪辑的preferredTransform更改每个剪辑(assetVideoTrack)的preferredTransform,这样就不会再出现此问题了,但总是说{{ 1}}是一个只能获取的财产...有人可以帮我吗?真的将不胜感激!

这里有一些可能有用的信息:

  1. 每个assetVideoTrack.preferredTransform始终为(1280.0,720.0)(实际上让我有些惊讶,因为这意味着视频的宽度为1280,高度为720,即使视频看起来像是普通的纵向视频,还是我错误吗?这会导致问题吗?)
  2. 前置摄像头剪辑assetVideoTrack.naturalSize始终为assetVideoTrack.preferredTransform =
  3. 后镜头剪辑CGAffineTransform(a: 0.0, b: 1.0, c: 1.0, d: 0.0, tx: 0.0, ty: -560.0)始终是assetVideoTrack.preferredTransform
  4. 请注意720-1280 = -560(我不知道此信息是否有用)

1 个答案:

答案 0 :(得分:1)

借助raywanderlich.com的一些研究和帮助,我设法找到了解决此问题的方法,甚至发现了由我提到的另一个Pod(SwiftyCam)引起的另一个更深层次的解决方案。由于SwiftyCam的这个问题,我不得不调整这里要介绍的解决方案,即我不得不更改通常不应该发生的CGAffineTransform的翻译。

解决方案:

首先,我们需要raywanderlich.com中的两个辅助函数:

这将为我们提供有关视频方向以及是否为人像的信息。实际上,原始功能中缺少[UIImage.Orientation]Mirrored个案例,但我也需要rightMirrored(第一个else if):

static func orientationFromTransform(_ transform: CGAffineTransform)
    -> (orientation: UIImage.Orientation, isPortrait: Bool) {
        var assetOrientation = UIImage.Orientation.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 = .rightMirrored
            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)
}

此功能基于上一个功能,将为我们提供单个剪辑的instruction,这对于将镜像和非镜像的视频合并为一个视频而不改变其他视频的“镜像”至关重要:

static func videoCompositionInstruction(_ track: AVCompositionTrack, asset: AVAsset) 
    -> AVMutableVideoCompositionLayerInstruction {
        let instruction = AVMutableVideoCompositionLayerInstruction(assetTrack: track)
        let assetTrack = asset.tracks(withMediaType: .video)[0]

        let transform = assetTrack.preferredTransform
        let assetInfo = orientationFromTransform(transform)

        var scaleToFitRatio = UIScreen.main.bounds.width / assetTrack.naturalSize.width
        if assetInfo.isPortrait {
            scaleToFitRatio = UIScreen.main.bounds.width / assetTrack.naturalSize.height
            let scaleFactor = CGAffineTransform(scaleX: scaleToFitRatio, y: scaleToFitRatio)
            instruction.setTransform(assetTrack.preferredTransform.concatenating(scaleFactor), at: kCMTimeZero)
        } else {
            let scaleFactor = CGAffineTransform(scaleX: scaleToFitRatio, y: scaleToFitRatio)
            var concat = assetTrack.preferredTransform.concatenating(scaleFactor)
                .concatenating(CGAffineTransform(translationX: 0, y: UIScreen.main.bounds.width / 2))
            if assetInfo.orientation == .down {
                let fixUpsideDown = CGAffineTransform(rotationAngle: CGFloat(Double.pi))
                let windowBounds = UIScreen.main.bounds
                let yFix = assetTrack.naturalSize.height + windowBounds.height
                let centerFix = CGAffineTransform(translationX: assetTrack.naturalSize.width, y: yFix)
                concat = fixUpsideDown.concatenating(centerFix).concatenating(scaleFactor)
            }
            instruction.setTransform(concat, at: kCMTimeZero)
        }

        return instruction
}

其余的基本上只是重写raywanderlich.com中的指令,以使该代码适用于URL数组而不是两个URL。请注意,本质区别是exportSession.videoComposition = mainComposition(在此代码框的末尾),当然还有mainComposition所需的所有内容:

let mixComposition = AVMutableComposition()

guard let completeMoviePath = completeMoviePathOp else {
    DispatchQueue.main.async {
        failure(VideoGeneratorError(error: .kFailedToFetchDirectory)) //NEW ERROR REQUIRED? @owner of swift-video-generator
    }
    return
}

var instructions: [AVMutableVideoCompositionLayerInstruction] = []
var insertTime = CMTime(seconds: 0, preferredTimescale: 1)

/// for each URL add the video and audio tracks and their duration to the composition
for sourceAsset in videoAssets {

    let frameRange = CMTimeRange(start: CMTime(seconds: 0, preferredTimescale: 1), duration: sourceAsset.duration)

    guard
        let nthVideoTrack = mixComposition.addMutableTrack(withMediaType: .video, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)),
        let nthAudioTrack = mixComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)), //0 used to be kCMPersistentTrackID_Invalid
        let assetVideoTrack = sourceAsset.tracks(withMediaType: .video).first,
        let assetAudioTrack = sourceAsset.tracks(withMediaType: .audio).first
    else {
        DispatchQueue.main.async {
            failure(VideoGeneratorError(error: .kMissingVideoURLs))
        }
        return
    }

    do {

        try nthVideoTrack.insertTimeRange(frameRange, of: assetVideoTrack, at: insertTime)
        try nthAudioTrack.insertTimeRange(frameRange, of: assetAudioTrack, at: insertTime)

        let nthInstruction = videoCompositionInstruction(nthVideoTrack, asset: sourceAsset)
        nthInstruction.setOpacity(0.0, at: CMTimeAdd(insertTime, sourceAsset.duration))

        instructions.append(nthInstruction)
        insertTime = insertTime + sourceAsset.duration

    } catch {
        DispatchQueue.main.async {
            failure(error)
        }
    }

}

let mainInstruction = AVMutableVideoCompositionInstruction()
mainInstruction.timeRange = CMTimeRange(start: CMTime(seconds: 0, preferredTimescale: 1), duration: insertTime)
mainInstruction.layerInstructions = instructions

let mainComposition = AVMutableVideoComposition()
mainComposition.instructions = [mainInstruction]
mainComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
mainComposition.renderSize = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)

/// try to start an export session and set the path and file type
if let exportSession = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality) { //DOES NOT WORK WITH AVAssetExportPresetPassthrough
    exportSession.outputFileType = .mov
    exportSession.outputURL = completeMoviePath
    exportSession.videoComposition = mainComposition
    exportSession.shouldOptimizeForNetworkUse = true

    /// try to export the file and handle the status cases
    exportSession.exportAsynchronously(completionHandler: { 

    /// same as before...