I am trying to crop the video with AVMutableComposition. The exported video seem rotated to an unknown angle. It does not even choose the correct frame from the video. I have debugged the frame, and the provided crop frame is correct. Even if the video's preferredTransformation is Identity, then too it does not crop the correct video.
My code is like below:
public func exportVideo(asset: AVAsset, destinationDirectory: URL? = nil, editingOptions: MediaEditingOptions? = nil,
completion: @escaping VideoExportCompletion) -> ExportOperation? {
let composition = AVMutableComposition()
guard let videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid),
let audioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) else {
completion(.failure(Error.couldNotAddMutableTracks))
return nil
}
let timeRange = editingOptions?.videoEditingOptions.timeRange ?? CMTimeRange(start: .zero, duration: asset.duration)
if let videoAssetTrack = asset.tracks(withMediaType: .video).first {
do {
try videoTrack.insertTimeRange(timeRange, of: videoAssetTrack, at: .zero)
} catch {
completion(.failure(error))
return nil
}
} else {
completion(.failure(Error.couldNotFindVideoTrack))
return nil
}
if let audioAssetTrack = asset.tracks(withMediaType: .audio).first {
do {
try audioTrack.insertTimeRange(timeRange, of: audioAssetTrack, at: .zero)
} catch {
completion(.failure(error))
return nil
}
}
let preset = editingOptions?.videoEditingOptions.preset ?? VideoEditingOptions.defaultPreset
guard let exportSession = AVAssetExportSession(asset: composition, presetName: preset) else {
completion(.failure(Error.couldNotInitiateExportSession))
return nil
}
guard var exportURL = destinationDirectory ?? cacheDirectoryURL() else {
completion(.failure(Error.couldNotDetermineOutputDirectory))
return nil
}
/// Prepare Instructions
let compositionInstructions = AVMutableVideoCompositionInstruction()
compositionInstructions.timeRange = CMTimeRange(start: .zero, duration: asset.duration)
let layerInstructions = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
let cropFrame = editingOptions?.scrollBounds ?? CGRect(x: 0, y: 0, width: videoTrack.naturalSize.width, height: videoTrack.naturalSize.height)
let transform = getTransform(for: videoTrack, cropFrame: cropFrame)
layerInstructions.setTransform(transform, at: .zero)
layerInstructions.setOpacity(1.0, at: CMTime.zero)
compositionInstructions.layerInstructions = [layerInstructions]
/// Add video composition.
let videoComposition = AVMutableVideoComposition()
videoComposition.renderSize = cropFrame.size
videoComposition.instructions = [compositionInstructions]
videoComposition.frameDuration = videoTrack.minFrameDuration
exportURL.appendPathComponent(UUID().uuidString + ".mp4")
exportSession.videoComposition = videoComposition
exportSession.outputURL = exportURL
exportSession.outputFileType = AVFileType.mov
exportSession.exportAsynchronously {
if exportSession.status == .failed {
completion(.failure(exportSession.error ?? Error.unknown))
} else if exportSession.status == .completed {
completion(.success(exportURL))
} else {
completion(.failure(Error.unknown))
}
}
return exportSession
}
func getTransform(for videoTrack: AVAssetTrack, cropFrame: CGRect) -> CGAffineTransform {
let renderSize = cropFrame.size
let renderScale = renderSize.width / cropFrame.width
let offset = CGPoint(x: -cropFrame.origin.x, y: -cropFrame.origin.y)
let rotation = atan2(videoTrack.preferredTransform.b, videoTrack.preferredTransform.a)
var rotationOffset = CGPoint(x: 0, y: 0)
if videoTrack.preferredTransform.b == -1.0 {
rotationOffset.y = videoTrack.naturalSize.width
} else if videoTrack.preferredTransform.c == -1.0 {
rotationOffset.x = videoTrack.naturalSize.height
} else if videoTrack.preferredTransform.a == -1.0 {
rotationOffset.x = videoTrack.naturalSize.width
rotationOffset.y = videoTrack.naturalSize.height
}
var transform = CGAffineTransform.identity
transform = transform.scaledBy(x: renderScale, y: renderScale)
transform = transform.translatedBy(x: offset.x + rotationOffset.x, y: offset.y + rotationOffset.y)
transform = transform.rotated(by: rotation)
print("track size \(videoTrack.naturalSize)")
print("preferred Transform = \(videoTrack.preferredTransform)")
print("rotation angle \(rotation)")
print("rotation offset \(rotationOffset)")
print("actual Transform = \(transform)")
return transform
}
I don't find anything obvious here. Am I missing anything here?