我正在使用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}}是一个只能获取的财产...有人可以帮我吗?真的将不胜感激!
这里有一些可能有用的信息:
assetVideoTrack.preferredTransform
始终为(1280.0,720.0)(实际上让我有些惊讶,因为这意味着视频的宽度为1280,高度为720,即使视频看起来像是普通的纵向视频,还是我错误吗?这会导致问题吗?)assetVideoTrack.naturalSize
始终为assetVideoTrack.preferredTransform =
CGAffineTransform(a: 0.0, b: 1.0, c: 1.0, d: 0.0, tx: 0.0, ty: -560.0)
始终是assetVideoTrack.preferredTransform
答案 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...