Lance Samaria
Lance Samaria

Reputation: 19572

How to get two views to be the same width and height using CGAffineTransform

If I want to get 2 views the same width and height with both of their centers in the middle of the screen I use the below code which works fine. Both are side by side in the middle of the screen with the same exact width and height.

let width = view.frame.width
let insideRect = CGRect(x: 0, y: 0, width: width / 2, height: .infinity)
let rect = AVMakeRect(aspectRatio: CGSize(width: 9, height: 16), insideRect: insideRect)

// blue
leftView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
leftView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
leftView.widthAnchor.constraint(equalToConstant: rect.width).isActive = true
leftView.heightAnchor.constraint(equalToConstant: rect.height).isActive = true

// purple
rightView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
rightView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
rightView.widthAnchor.constraint(equalToConstant: leftView.widthAnchor).isActive = true
rightView.heightAnchor.constraint(equalToConstant: leftView.heightAnchor).isActive = true

enter image description here

How can I do the same thing using CGAffineTransform? I tried to find a way to make the rightView the same size as the left view but couldn't. The top of the leftView frame is in the middle of the screen instead its center and the rightView is completely off.

let width = view.frame.width
let insideRect = CGRect(x: 0, y: 0, width: width / 2, height: .infinity)
let rect = AVMakeRect(aspectRatio: CGSize(width: 9, height: 16), insideRect: insideRect)

leftView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
leftView.transform = CGAffineTransform(translationX: 0, y: view.frame.height / 2)

rightView.transform = leftView.transform
rightView.transform = CGAffineTransform(translationX: rect.width, y: view.frame.height / 2)

Upvotes: 1

Views: 603

Answers (1)

DonMag
DonMag

Reputation: 77462

You need to make your transforms based on the Composited Video's output size - its .renderSize.

Based on your other question...

So, if you have two 1280.0 x 720.0 videos, and you want them side-by-side in a 640 x 480 rendered frame, you need to:

  • get the size of the first video
  • scale it to 320 x 480
  • move it to 0, 0

then:

  • get the size of the second video
  • scale it to 320 x 480
  • move it to 320, 0

So your scale transform will be:

let targetWidth = renderSize.width / 2.0
let targetHeight = renderSize.height
let widthScale = targetWidth / sourceVideoSize.width
let heightScale = targetHeight / sourceVideoSize.height

let scale = CGAffineTransform(scaleX: widthScale, y: heightScale)

That should get your there --- except...

In my testing, I took 4 8-second videos in landscape orientation.

For reasons unbeknownst to me - the "native" preferredTransforms are:

Videos 1 & 3
[-1, 0, 0, -1, 1280, 720]

Videos 2 & 4
[1, 0, 0, 1, 0, 0]

So, the sizes returned by the recommended track.naturalSize.applying(track.preferredTransform) end up being:

Videos 1 & 3
-1280 x -720

Videos 2 & 4
1280 x 720

which messes with the transforms.

After a little experimentation, if the size is negative, we need to:

  • rotate the transform
  • scale the transform (making sure to use positive widths/heights)
  • translate the transform adjusted for the change in orientation

Here is a complete implementation (without the save-to-disk at the end):

import UIKit
import AVFoundation

class VideoViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .systemYellow
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        guard let originalVideoURL1 = Bundle.main.url(forResource: "video1", withExtension: "mov"),
              let originalVideoURL2 = Bundle.main.url(forResource: "video2", withExtension: "mov")
        else { return }

        let firstAsset = AVURLAsset(url: originalVideoURL1)
        let secondAsset = AVURLAsset(url: originalVideoURL2)

        let mixComposition = AVMutableComposition()
        
        guard let firstTrack = mixComposition.addMutableTrack(withMediaType: .video, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) else { return }
        let timeRange1 = CMTimeRangeMake(start: .zero, duration: firstAsset.duration)

        do {
            try firstTrack.insertTimeRange(timeRange1, of: firstAsset.tracks(withMediaType: .video)[0], at: .zero)
        } catch {
            return
        }

        guard let secondTrack = mixComposition.addMutableTrack(withMediaType: .video, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) else { return }
        let timeRange2 = CMTimeRangeMake(start: .zero, duration: secondAsset.duration)

        do {
            try secondTrack.insertTimeRange(timeRange2, of: secondAsset.tracks(withMediaType: .video)[0], at: .zero)
        } catch {
            return
        }
        
        let mainInstruction = AVMutableVideoCompositionInstruction()
        
        mainInstruction.timeRange = CMTimeRangeMake(start: .zero, duration: CMTimeMaximum(firstAsset.duration, secondAsset.duration))
        
        var track: AVAssetTrack!
        
        track = firstAsset.tracks(withMediaType: .video).first
        
        let firstSize = track.naturalSize.applying(track.preferredTransform)

        track = secondAsset.tracks(withMediaType: .video).first

        let secondSize = track.naturalSize.applying(track.preferredTransform)

        // debugging
        print("firstSize:", firstSize)
        print("secondSize:", secondSize)

        let renderSize = CGSize(width: 640, height: 480)
        
        var scale: CGAffineTransform!
        var move: CGAffineTransform!

        let firstLayerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: firstTrack)
        
        scale = .identity
        move = .identity
        
        if (firstSize.width < 0) {
            scale = CGAffineTransform(rotationAngle: .pi)
        }
        scale = scale.scaledBy(x: abs(renderSize.width / 2.0 / firstSize.width), y: abs(renderSize.height / firstSize.height))
        move = CGAffineTransform(translationX: 0, y: 0)
        if (firstSize.width < 0) {
            move = CGAffineTransform(translationX: renderSize.width / 2.0, y: renderSize.height)
        }

        firstLayerInstruction.setTransform(scale.concatenating(move), at: .zero)

        let secondLayerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: secondTrack)
        
        scale = .identity
        move = .identity
        
        if (secondSize.width < 0) {
            scale = CGAffineTransform(rotationAngle: .pi)
        }
        scale = scale.scaledBy(x: abs(renderSize.width / 2.0 / secondSize.width), y: abs(renderSize.height / secondSize.height))
        move = CGAffineTransform(translationX: renderSize.width / 2.0, y: 0)
        if (secondSize.width < 0) {
            move = CGAffineTransform(translationX: renderSize.width, y: renderSize.height)
        }
        
        secondLayerInstruction.setTransform(scale.concatenating(move), at: .zero)
        
        mainInstruction.layerInstructions = [firstLayerInstruction, secondLayerInstruction]
        
        let mainCompositionInst = AVMutableVideoComposition()
        mainCompositionInst.instructions = [mainInstruction]
        mainCompositionInst.frameDuration = CMTime(value: 1, timescale: 30)
        mainCompositionInst.renderSize = renderSize

        let newPlayerItem = AVPlayerItem(asset: mixComposition)
        newPlayerItem.videoComposition = mainCompositionInst
        
        let player = AVPlayer(playerItem: newPlayerItem)
        let playerLayer = AVPlayerLayer(player: player)

        playerLayer.frame = view.bounds
        view.layer.addSublayer(playerLayer)
        player.seek(to: .zero)
        player.play()
        
        // video export code goes here...

    }

}

It's possible that the preferredTransforms could also be different for front / back camera, mirrored, etc. But I'll leave that up to you to work out.

Edit

Sample project at: https://github.com/DonMag/VideoTest

Produces (using two 720 x 1280 video clips):

enter image description here

Upvotes: 1

Related Questions