Transform wrong when using both AVComposition and AVVideoComposition

1k views Asked by At

I am creating an AVMutableComposition. I need the asset to be flipped horizontally, so I am setting the transform of the composition track like this:

compositionTrack.preferredTransform = assetTrack.preferredTransform.scaledBy(x: -1, y: 1)
    

If I export this (I use AVAssetPreset960x640 as my preset), this works as expected.

However, I also need to add an AVMutableVideoComposition overlay to be rendered with this copmosition. This overlay shouldn't be flipped horizontally. I specify this like so:

// Create video composition
let videoComposition = AVMutableVideoComposition()
videoComposition.renderSize = videoSize
videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
videoComposition.animationTool = AVVideoCompositionCoreAnimationTool(
    postProcessingAsVideoLayer: videoLayer,
    in: outputLayer
)

let instruction = AVMutableVideoCompositionInstruction()
instruction.timeRange = CMTimeRange(
  start: .zero,
  duration: composition.duration
)
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: compositionTrack)
layerInstruction.setTransform(assetTrack.preferredTransform, at: .zero)
    
instruction.layerInstructions = [layerInstruction]
videoComposition.instructions = [instruction]

When I export the video with this, it isn't flipped horizontally. It stops the preferredTransform applied to the composition from being performed. Presumably it's this line that is causing this:

layerInstruction.setTransform(assetTrack.preferredTransform, at: .zero)

If I set the layer transform to instead be assetTrack.preferredTransform.scaledBy(x: -1, y: 1) or compositionTrack.preferredTransform, I'm presented with a black screen on export.

Apple have these docs explaining transforms on an AVVideoComposition. If I understand correctly, they say that I should just be setting the transform of the layer instruction. If I do that – applying a transform to the layer instruction and not the composition - I'm still presented with a black screen.

Why is this and how can I fix this?

1

There are 1 answers

3
aheze On BEST ANSWER

Edit: the transform that worked was

CGAffineTransform(a: -1.0, b: 0.0, c: 0.0, d: 1.0, tx: videoSize.width, ty: 0.0)

So I ran into this issue a while back, following this RayWenderlich tutorial (your code looks extremely familiar too!). The problem was that assetTrack.preferredTransform wasn't reliable, on some videos it would work, and on others it would be a black screen.

Instead of

let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: compositionTrack)
layerInstruction.setTransform(assetTrack.preferredTransform, at: .zero)

Try this:

func compositionLayerInstruction(for track: AVCompositionTrack, assetTrack: AVAssetTrack, orientation: UIImage.Orientation) -> AVMutableVideoCompositionLayerInstruction {

    let instruction = AVMutableVideoCompositionLayerInstruction(assetTrack: track)
    
    var transform = CGAffineTransform.identity
    let assetSize = assetTrack.naturalSize
    
    /// you should be able to play with these values to make a horizontal flip (try changing `a` and `d`)
    switch orientation {
    case .up:
        transform = CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0)
    case .down:
        transform = CGAffineTransform(a: -1, b: 0, c: 0, d: -1, tx: assetSize.width, ty: assetSize.height)
    case .left:
        transform = CGAffineTransform(a: 0, b: -1, c: 1, d: 0, tx: 0, ty: assetSize.width)
    case .right:
        transform = CGAffineTransform(a: 0, b: 1, c: -1, d: 0, tx: assetSize.height, ty: 0)
     default:
        print("Unsupported orientation")
    }
    
    instruction.setTransform(transform, at: .zero)
    
    return instruction
}

let layerInstruction = compositionLayerInstruction(
    for: compositionTrack, assetTrack: assetTrack, orientation: videoInfo.orientation)
/// assetTrack is the original video

I based this off another answer on SO, can't remember where though.

Here's a helper function, to get orientation

/// adjust the video orientation is the source has a different orientation
private func orientation(from 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 = .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)
}

Full usage:

/// get adjusted orientation of the video
let videoInfo = orientation(from: assetTrack.preferredTransform)
let videoSize: CGSize

if videoInfo.isPortrait {
    videoSize = CGSize(
    width: assetTrack.naturalSize.height,
    height: assetTrack.naturalSize.width)
} else {
    videoSize = assetTrack.naturalSize
}


 /// the video
let videoLayer = CALayer()
videoLayer.frame = CGRect(origin: .zero, size: videoSize)
        
/// overlay where you add graphics and other stuff
let overlayLayer = CALayer()
overlayLayer.frame = CGRect(origin: .zero, size: videoSize)

let outputLayer = CALayer()
outputLayer.frame = CGRect(origin: .zero, size: videoSize)
outputLayer.addSublayer(videoLayer)
outputLayer.addSublayer(overlayLayer)

let videoComposition = AVMutableVideoComposition()
videoComposition.renderSize = videoSize
videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
videoComposition.animationTool = AVVideoCompositionCoreAnimationTool(
postProcessingAsVideoLayer: videoLayer,
in: outputLayer)

// MARK: - previous `let layerInstruction` code goes here...

instruction.layerInstructions = [layerInstruction]

/// export session code here...