【问题标题】:Merging video with original orientation将视频与原始方向合并
【发布时间】:2022-12-21 02:29:21
【问题描述】:

我有一个链接,我可以通过 DM 获取最小复制示例!


录制视频

对于录制,AVCaptureConnection 为 AVCaptureSession,我在使用前置摄像头时将 isVideoMirrored 设置为 true,在使用后置摄像头时设置为 false。全部为纵向。

保存影片

当我保存视频时,我执行AVAssetExportSession。如果我使用前置摄像头,我想保持isVideoMirrored = true,所以我创建了一个AVMutableComposition来将AVAsset视频轨道的preferredTransform设置为CGAffineTransform(scaleX: -1.0, y: 1.0).rotated(by: CGFloat(Double.pi/2))。对于后置摄像头,我导出 AVAsset 作为输出。

我的部分保存代码:

        if didCaptureWithFrontCamera {
        
        let composition = AVMutableComposition()
        
        let assetVideoTrack = asset.tracks(withMediaType: .video).last!
        let assetAudioTrack = asset.tracks(withMediaType: .audio).last!
        
        let compositionVideoTrack = composition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: CMPersistentTrackID(kCMPersistentTrackID_Invalid))
        let compositionAudioTrack = composition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: CMPersistentTrackID(kCMPersistentTrackID_Invalid))
        
        try? compositionVideoTrack?.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: asset.duration), of: assetVideoTrack, at: CMTime.zero)
        try? compositionAudioTrack?.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: asset.duration), of: assetAudioTrack, at: CMTime.zero)
        
        compositionVideoTrack?.preferredTransform = CGAffineTransform(scaleX: -1.0, y: 1.0).rotated(by: CGFloat(Double.pi/2))
        
        guard let exportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPreset1280x720) else {
            handler(nil)
            return
        }
        
        exportSession.outputURL = outputURL
        exportSession.outputFileType = .mp4
        exportSession.shouldOptimizeForNetworkUse = true
        exportSession.exportAsynchronously { handler(exportSession) }
        
    } else {
        
        guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPreset1280x720) else {
            handler(nil)
            return
        }
        
        exportSession.outputURL = outputURL
        exportSession.outputFileType = .mp4
        exportSession.shouldOptimizeForNetworkUse = true
        exportSession.exportAsynchronously { handler(exportSession)  }
    }

合并视频

稍后,为了查看保存的视频,我想将它们合并为一个视频,并通过 AVMutableComposition 保持每个视频的原始方向。

部分起作用的是将 AVMutableComposition 的视频轨道设置为单个 AVAsset 视频的视频轨道的 preferredTransform 属性。唯一的问题是单一方向应用于所有视频(即镜像不应用于后置摄像头录制的视频,同样适用于前置摄像头视频)。

从我遇到的解决方案来看,我似乎需要申请AVMutableVideoCompositionInstruction,但在尝试这样做时,AVAssetExportSession 似乎根本没有考虑videoComposition 说明。

任何指导都是极其感谢,因为我一生都无法解决它......

我尝试的合并代码:

func merge(videos: [AVURLAsset], for date: Date, completion: @escaping (_ url: URL, _ asset: AVAssetExportSession)->()) {
let videoComposition = AVMutableComposition()
var lastTime: CMTime = .zero

var count = 0
var instructions = [AVMutableVideoCompositionInstruction]()
let renderSize = CGSize(width: 720, height: 1280)

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

for video in videos {
    
    if let videoTrack = video.tracks(withMediaType: .video)[safe: 0] {
            
        //this is the only thing that seems to work, but work not in the way i'd hope where each video keeps its original orientation
       //videoCompositionTrack.preferredTransform = videoTrack.preferredTransform
        
        if let audioTrack = video.tracks(withMediaType: .audio)[safe: 0] {
            
            do {
                
                try videoCompositionTrack.insertTimeRange(CMTimeRangeMake(start: .zero, duration: video.duration), of: videoTrack, at: lastTime)
                try audioCompositionTrack.insertTimeRange(CMTimeRangeMake(start: .zero, duration: video.duration), of: audioTrack, at: lastTime)
                
                let layerInstruction = videoCompositionInstruction(videoTrack, asset: video, count: count)

                let videoCompositionInstruction = AVMutableVideoCompositionInstruction()
                videoCompositionInstruction.timeRange = CMTimeRangeMake(start: lastTime, duration: video.duration)
                videoCompositionInstruction.layerInstructions = [layerInstruction]

                instructions.append(videoCompositionInstruction)

            } catch {
                return
            }
            lastTime = CMTimeAdd(lastTime, video.duration)
            count += 1
            
        } else {
            
            do {
                
                try videoCompositionTrack.insertTimeRange(CMTimeRangeMake(start: .zero, duration: video.duration), of: videoTrack, at: lastTime)
                
                let layerInstruction = videoCompositionInstruction(videoTrack, asset: video, count: count)

                let videoCompositionInstruction = AVMutableVideoCompositionInstruction()
                videoCompositionInstruction.timeRange = CMTimeRangeMake(start: lastTime, duration: video.duration)
                videoCompositionInstruction.layerInstructions = [layerInstruction]

                instructions.append(videoCompositionInstruction)

            } catch {
                return
            }
            
            lastTime = CMTimeAdd(lastTime, video.duration)
            count += 1
        }
    }
}

let mutableVideoComposition = AVMutableVideoComposition()
mutableVideoComposition.instructions = instructions
mutableVideoComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
mutableVideoComposition.renderSize = renderSize

dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .short
let date = dateFormatter.string(from: date)

let mergedURL = NSURL.fileURL(withPath: NSTemporaryDirectory() + "merged-\(date)" + ".mp4")

guard let exporter = AVAssetExportSession(asset: videoComposition, presetName: AVAssetExportPresetHighestQuality) else { return }
exporter.outputURL = mergedURL
exporter.outputFileType = .mp4
exporter.videoComposition = mutableVideoComposition
exporter.shouldOptimizeForNetworkUse = true
completion(mergedURL, exporter)
}

func videoCompositionInstruction(_ firstTrack: AVAssetTrack, asset: AVAsset, count: Int) -> AVMutableVideoCompositionLayerInstruction {
let renderSize = CGSize(width: 720, height: 1280)

let instruction = AVMutableVideoCompositionLayerInstruction(assetTrack: firstTrack)

let assetTrack = asset.tracks(withMediaType: .video)[0]
let t = assetTrack.fixedPreferredTransform // new transform fix
let assetInfo = orientationFromTransform(t)

if assetInfo.isPortrait {

    let scaleToFitRatio = renderSize.width / assetTrack.naturalSize.height
    let scaleFactor = CGAffineTransform(scaleX: scaleToFitRatio, y: scaleToFitRatio)
    var finalTransform = assetTrack.fixedPreferredTransform.concatenating(scaleFactor)
    
    if assetInfo.orientation == .rightMirrored || assetInfo.orientation == .leftMirrored {
        finalTransform = finalTransform.translatedBy(x: -t.ty, y: 0)
    }
    instruction.setTransform(t, at: CMTime.zero)

} else {

    let renderRect = CGRect(x: 0, y: 0, width: renderSize.width, height: renderSize.height)
    let videoRect = CGRect(origin: .zero, size: assetTrack.naturalSize).applying(assetTrack.fixedPreferredTransform)

    let scale = renderRect.width / videoRect.width
    let transform = CGAffineTransform(scaleX: renderRect.width / videoRect.width, y: (videoRect.height * scale) / assetTrack.naturalSize.height)
    let translate = CGAffineTransform(translationX: .zero, y: ((renderSize.height - (videoRect.height * scale))) / 2)

    instruction.setTransform(assetTrack.fixedPreferredTransform.concatenating(transform).concatenating(translate), at: .zero)
}

if count == 0 {
    instruction.setOpacity(0.0, at: asset.duration)
}

return instruction
}

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 == 0 && transform.b == -1.0 && transform.c == -1.0 && transform.d == 0 {
    assetOrientation = .leftMirrored
    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)
}

extension AVAssetTrack {

var fixedPreferredTransform: CGAffineTransform {
    var t = preferredTransform
    switch(t.a, t.b, t.c, t.d) {
    case (1, 0, 0, 1):
        t.tx = 0
        t.ty = 0
    case (1, 0, 0, -1):
        t.tx = 0
        t.ty = naturalSize.height
    case (-1, 0, 0, 1):
        t.tx = naturalSize.width
        t.ty = 0
    case (-1, 0, 0, -1):
        t.tx = naturalSize.width
        t.ty = naturalSize.height
    case (0, -1, 1, 0):
        t.tx = 0
        t.ty = naturalSize.width
    case (0, 1, -1, 0):
        t.tx = naturalSize.height
        t.ty = 0
    case (0, 1, 1, 0):
        t.tx = 0
        t.ty = 0
    case (0, -1, -1, 0):
        t.tx = naturalSize.height
        t.ty = naturalSize.width
    default:
        break
    }
    return t
}
}

【问题讨论】:

    标签: ios swift avcapturesession cgaffinetransform avmutablecomposition


    【解决方案1】:

    假设您的转换是正确的,我更新了您的 merge 函数。

    主要变化是使用单个AVMutableVideoCompositionInstruction和多个AVMutableVideoCompositionLayerInstruction,并将正确的CMTime值传递给要执行的层指令。

      func merge(videos: [AVURLAsset],
                 for date: Date,
                 completion: @escaping (_ url: URL, _ asset: AVAssetExportSession)->()) {
        let videoComposition = AVMutableComposition()
        
        guard let videoCompositionTrack = videoComposition.addMutableTrack(withMediaType: .video,
                                                                           preferredTrackID: Int32(kCMPersistentTrackID_Invalid)),
              let audioCompositionTrack = videoComposition.addMutableTrack(withMediaType: .audio,
                                                                           preferredTrackID: Int32(kCMPersistentTrackID_Invalid))
        else { return }
        
        
        var lastTime: CMTime = .zero
        var layerInstructions = [AVMutableVideoCompositionLayerInstruction]()
        
        for video in videos {
          guard let videoTrack = video.tracks(withMediaType: .video)[safe: 0] else { return }
          
          // add audio track if available
          if let audioTrack = video.tracks(withMediaType: .audio)[safe: 0] {
            do {
              try audioCompositionTrack.insertTimeRange(CMTimeRangeMake(start: .zero, duration: video.duration),
                                                        of: audioTrack,
                                                        at: lastTime)
            } catch {
              return
            }
          }
          
          // add video track
          do {
            try videoCompositionTrack.insertTimeRange(CMTimeRangeMake(start: .zero, duration: video.duration),
                                                      of: videoTrack,
                                                      at: lastTime)
            let layerInstruction = makeVideoCompositionInstruction(videoTrack,
                                                                   asset: video,
                                                                   atTime: lastTime)
            layerInstructions.append(layerInstruction)
          } catch {
            return
          }
          
          lastTime = CMTimeAdd(lastTime, video.duration)
        } // end for..in videos
        
        let renderSize = CGSize(width: 720, height: 1280)
        
        let videoInstruction = AVMutableVideoCompositionInstruction()
        videoInstruction.timeRange = CMTimeRangeMake(start: .zero, duration: lastTime)
        videoInstruction.layerInstructions = layerInstructions
        
        let mutableVideoComposition = AVMutableVideoComposition()
        mutableVideoComposition.instructions = [videoInstruction]
        mutableVideoComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
        mutableVideoComposition.renderSize = renderSize
        
        let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .long
        dateFormatter.timeStyle = .short
        
        let date = dateFormatter.string(from: date)
        
        let mergedURL = NSURL.fileURL(withPath: NSTemporaryDirectory() + "merged-(date)" + ".mp4")
        
        guard let exporter = AVAssetExportSession(asset: videoComposition,
                                                  presetName: AVAssetExportPresetHighestQuality) else { return }
        exporter.outputURL = mergedURL
        exporter.outputFileType = .mp4
        exporter.videoComposition = mutableVideoComposition
        exporter.shouldOptimizeForNetworkUse = true
        completion(mergedURL, exporter)
      }
      
      func makeVideoCompositionInstruction(_ videoTrack: AVAssetTrack,
                                           asset: AVAsset,
                                           atTime: CMTime) -> AVMutableVideoCompositionLayerInstruction {
        let renderSize = CGSize(width: 720, height: 1280)
        
        let instruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
        
        let assetTrack = asset.tracks(withMediaType: .video)[0]
        let t = assetTrack.fixedPreferredTransform // new transform fix
        let assetInfo = orientationFromTransform(t)
        
        if assetInfo.isPortrait {
          let scaleToFitRatio = renderSize.width / assetTrack.naturalSize.height
          let scaleFactor = CGAffineTransform(scaleX: scaleToFitRatio, y: scaleToFitRatio)
          var finalTransform = assetTrack.fixedPreferredTransform.concatenating(scaleFactor)
          
          if assetInfo.orientation == .rightMirrored || assetInfo.orientation == .leftMirrored {
            finalTransform = finalTransform.translatedBy(x: -t.ty, y: 0)
          }
          instruction.setTransform(t, at: atTime)
          
        } else {
          
          let renderRect = CGRect(x: 0, y: 0, width: renderSize.width, height: renderSize.height)
          let videoRect = CGRect(origin: .zero, size: assetTrack.naturalSize).applying(assetTrack.fixedPreferredTransform)
          
          let scale = renderRect.width / videoRect.width
          let transform = CGAffineTransform(scaleX: renderRect.width / videoRect.width,
                                            y: (videoRect.height * scale) / assetTrack.naturalSize.height)
          let translate = CGAffineTransform(translationX: .zero,
                                            y: ((renderSize.height - (videoRect.height * scale))) / 2)
          
          instruction.setTransform(assetTrack.fixedPreferredTransform.concatenating(transform).concatenating(translate),
                                   at: atTime)
        }
        
        // if atTime = 0, we can assume this is the first track being added
        if atTime == .zero {
          instruction.setOpacity(0.0,
                                 at: asset.duration)
        }
        
        return instruction
      }
    

    【讨论】:

    • 我尝试了代码,但方向仍然关闭......此外,视频图片还显示 -90 度的纵向视图:(。还有什么想法吗?
    • @Chris 你能在 github 上发布一个最小的工作项目吗?
    • 抱歉耽搁了!有什么办法可以直接把链接转发给你吗?
    猜你喜欢
    • 1970-01-01
    • 2012-02-15
    • 1970-01-01
    • 2016-06-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-04-05
    • 1970-01-01
    相关资源
    最近更新 更多