问题
I need to be able to merge videos taken with the time lapse function in the Camera app on iOS and export as a single video.
However, even if I try to export a single, unchanged time lapse video to the Photo Library, it saves as a completely black video (with the correct duration). Here is the sample code I wrote to just export a single, unchanged video (most of which is adapted from a Ray Wenderlich tutorial):
@IBAction func saveVideo(_ sender: UIBarButtonItem) {
// 1 - Early exit if there's no video file selected
guard let videoAsset = self.avAsset else {
let alert = UIAlertController(title: "Error", message: "Failed to load video asset.", preferredStyle: .alert)
let cancelAction = UIAlertAction(title: "OK", style: .cancel, handler: nil)
alert.addAction(cancelAction)
self.present(alert, animated: true, completion: nil)
return
}
// 2 - Create AVMutableComposition object. This object will hold your AVMutableCompositionTrack instances.
print("Preparing AVMutableComposition...")
let mixComposition = AVMutableComposition()
// 3 - Video track
let videoTrack = mixComposition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)
do {
if let videoAssetTrack = videoAsset.tracks(withMediaType: .video).first {
try videoTrack?.insertTimeRange(CMTimeRangeMake(kCMTimeZero, videoAsset.duration), of: videoAssetTrack, at: kCMTimeZero)
}
if let audioAssetTrack = videoAsset.tracks(withMediaType: .audio).first {
let audioTrack = mixComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)
try audioTrack?.insertTimeRange(CMTimeRangeMake(kCMTimeZero, videoAsset.duration), of: audioAssetTrack, at: kCMTimeZero)
}
} catch let error as NSError {
self.presentAlert(title: "Export Error", message: "Unable to complete export due to the following error: \(error). Please try again.", block: nil)
print("error: \(error)")
}
// 3.1 - Create AVMutableVideoCompositionInstruction
let mainInstruction = AVMutableVideoCompositionInstruction()
mainInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, videoAsset.duration)
// 3.2 - Create an AVMutableVideoCompositionLayerInstruction for the video track and fix the orientation.
let videoLayerInstruction: AVMutableVideoCompositionLayerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack!)
let videoAssetTrack = videoAsset.tracks(withMediaType: .video).first
var assetOrientation: UIImageOrientation = .up
var isPortrait = false
let t = videoAssetTrack!.preferredTransform
if t.a == 0 && t.b == 1.0 && t.c == -1.0 && t.d == 0 {
assetOrientation = .right
isPortrait = true
} else if t.a == 0 && t.b == -1.0 && t.c == 1.0 && t.d == 0 {
assetOrientation = .left
isPortrait = true
} else if t.a == 1.0 && t.b == 0 && t.c == 0 && t.d == 1.0 {
assetOrientation = .up
} else if t.a == -1.0 && t.b == 0 && t.c == 0 && t.d == -1.0 {
assetOrientation = .down
}
videoLayerInstruction.setTransform(videoAssetTrack!.preferredTransform, at: kCMTimeZero)
videoLayerInstruction.setOpacity(0.0, at: videoAsset.duration)
// 3.3 - Add instructions
mainInstruction.layerInstructions = [videoLayerInstruction]
let mainComposition = AVMutableVideoComposition()
mainComposition.instructions = [mainInstruction]
mainComposition.frameDuration = CMTimeMake(1, 30)
var naturalSize: CGSize
if isPortrait {
naturalSize = CGSize(width: videoAssetTrack!.naturalSize.height, height: videoAssetTrack!.naturalSize.width)
} else {
naturalSize = videoAssetTrack!.naturalSize
}
mainComposition.renderSize = CGSize(width: naturalSize.width, height: naturalSize.height)
// set up file destination
let tempName = "temp-thread.mov"
let tempURL = URL(fileURLWithPath: (NSTemporaryDirectory() as NSString).appendingPathComponent(tempName))
do {
if FileManager.default.fileExists(atPath: tempURL.path) {
try FileManager.default.removeItem(at: tempURL)
}
} catch {
print("Error removing temp file.")
}
// create final video using export session
guard let exportSession = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality) else { return }
exportSession.outputURL = tempURL
exportSession.outputFileType = AVFileType.mov
exportSession.shouldOptimizeForNetworkUse = true
exportSession.videoComposition = mainComposition
print("Exporting video...")
exportSession.exportAsynchronously {
DispatchQueue.main.async {
switch exportSession.status {
// Success
case .completed:
print("Saving to Photos Library...")
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: exportSession.outputURL!)
}) { success, error in
if success {
print("Added video to library - success: \(success), error: \(String(describing: error?.localizedDescription))")
} else {
print("Added video to library - success: \(success), error: \(String(describing: error!.localizedDescription))")
}
let _ = try? FileManager.default.removeItem(at: tempURL)
}
print("Export session completed")
// Status other than success
case .cancelled, .exporting, .failed, .unknown, .waiting:
print("Export status: \(exportSession.status.rawValue)")
print("Reason: \(String(describing: exportSession.error))")
}
}
}
}
Why would the resulting video show up completely black? I can't seem to find much documentation on Apple's time lapse videos, so I'm not sure why they might be different than a regular video file. They seem to have a frame rate of 30fps and if I inspect one on my Mac, it's just a regular QuickTime movie file without an audio channel. Any ideas? Exporting any other video with this code (even ones without audio) works flawlessly.
回答1:
The problem code is:
videoLayerInstruction.setTransform(videoAssetTrack!.preferredTransform, at: kCMTimeZero)
This transformation is eligible to "up" (default) orientation only and it makes a video completely black for the other orientations. You should make a correct transformation for each orientation e.g.:
var transform = videoAssetTrack.preferredTransform
// Right
if transform.a == 0 && transform.b == 1.0 && transform.c == -1.0 && transform.d == 0 {
isPortrait = true
let rotate = CGAffineTransform.identity.translatedBy(x: videoAssetTrack.naturalSize.height - videoAssetTrack.preferredTransform.tx, y: -videoAssetTrack.preferredTransform.ty)
transform = videoAssetTrack.preferredTransform.concatenating(rotate)
}
// Left
else if transform.a == 0 && transform.b == -1.0 && transform.c == 1.0 && transform.d == 0 {
isPortrait = true
let rotate = CGAffineTransform.identity.translatedBy(x: -videoAssetTrack.preferredTransform.tx, y: videoAssetTrack.naturalSize.width - videoAssetTrack.preferredTransform.ty)
transform = videoAssetTrack.preferredTransform.concatenating(rotate)
}
// Up
else if transform.a == 1.0 && transform.b == 0 && transform.c == 0 && transform.d == 1.0 {
transform = videoAssetTrack.preferredTransform
}
// Down
else if transform.a == -1.0 && transform.b == 0 && transform.c == 0 && transform.d == -1.0 {
let rotate = CGAffineTransform.identity.translatedBy(x: videoAssetTrack.naturalSize.width - videoAssetTrack.preferredTransform.tx, y: videoAssetTrack.naturalSize.height - videoAssetTrack.preferredTransform.ty)
transform = videoAssetTrack.preferredTransform.concatenating(rotate)
}
videoLayerInstruction.setTransform(transform, at: .zero)
来源:https://stackoverflow.com/questions/46243096/exporting-time-lapse-with-avassetexportsession-results-in-black-video