AVAssetExportSession Crop Video Transformation problem

Viewed 17

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?

0 Answers
Related