2023-12-05 00:48:43 +04:00

766 lines
38 KiB
Swift

import Foundation
import UIKit
import Display
import ComponentFlow
import MetalEngine
import SwiftSignalKit
private let shadowImage: UIImage? = {
UIImage(named: "Call/VideoGradient")?.precomposed()
}()
private final class VideoContainerLayer: SimpleLayer {
let contentsLayer: SimpleLayer
override init() {
self.contentsLayer = SimpleLayer()
super.init()
self.addSublayer(self.contentsLayer)
}
override init(layer: Any) {
self.contentsLayer = SimpleLayer()
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(size: CGSize, transition: Transition) {
transition.setFrame(layer: self.contentsLayer, frame: CGRect(origin: CGPoint(), size: size))
}
}
final class VideoContainerView: HighlightTrackingButton {
enum Key {
case background
case foreground
}
private struct Params: Equatable {
var size: CGSize
var insets: UIEdgeInsets
var cornerRadius: CGFloat
var controlsHidden: Bool
var isMinimized: Bool
var isAnimatedOut: Bool
init(size: CGSize, insets: UIEdgeInsets, cornerRadius: CGFloat, controlsHidden: Bool, isMinimized: Bool, isAnimatedOut: Bool) {
self.size = size
self.insets = insets
self.cornerRadius = cornerRadius
self.controlsHidden = controlsHidden
self.isMinimized = isMinimized
self.isAnimatedOut = isAnimatedOut
}
}
private struct VideoMetrics: Equatable {
var resolution: CGSize
var rotationAngle: Float
var sourceId: Int
init(resolution: CGSize, rotationAngle: Float, sourceId: Int) {
self.resolution = resolution
self.rotationAngle = rotationAngle
self.sourceId = sourceId
}
}
private final class FlipAnimationInfo {
let isForward: Bool
let previousRotationAngle: Float
init(isForward: Bool, previousRotationAngle: Float) {
self.isForward = isForward
self.previousRotationAngle = previousRotationAngle
}
}
private final class DisappearingVideo {
let flipAnimationInfo: FlipAnimationInfo?
let videoLayer: PrivateCallVideoLayer
let videoMetrics: VideoMetrics
var isAlphaAnimationInitiated: Bool = false
init(flipAnimationInfo: FlipAnimationInfo?, videoLayer: PrivateCallVideoLayer, videoMetrics: VideoMetrics) {
self.flipAnimationInfo = flipAnimationInfo
self.videoLayer = videoLayer
self.videoMetrics = videoMetrics
}
}
private enum MinimizedPosition: CaseIterable {
case topLeft
case topRight
case bottomLeft
case bottomRight
}
let key: Key
private let videoContainerLayer: VideoContainerLayer
private var videoLayer: PrivateCallVideoLayer
private var disappearingVideoLayer: DisappearingVideo?
let blurredContainerLayer: SimpleLayer
private let shadowContainer: SimpleLayer
private let topShadowLayer: SimpleLayer
private let bottomShadowLayer: SimpleLayer
private var params: Params?
private var videoMetrics: VideoMetrics?
private var appliedVideoMetrics: VideoMetrics?
private var highlightedState: Bool = false
private(set) var isFillingBounds: Bool = false
private var minimizedPosition: MinimizedPosition = .bottomRight
private var initialDragPosition: CGPoint?
private var dragPosition: CGPoint?
private var dragVelocity: CGPoint = CGPoint()
private var dragPositionAnimatorLink: SharedDisplayLinkDriver.Link?
private var videoOnUpdatedListener: Disposable?
var video: VideoSource? {
didSet {
if self.video !== oldValue {
self.videoOnUpdatedListener?.dispose()
self.videoOnUpdatedListener = self.video?.addOnUpdated { [weak self] in
guard let self else {
return
}
var videoMetrics: VideoMetrics?
if let currentOutput = self.video?.currentOutput {
if let previousVideo = self.videoLayer.video, previousVideo.sourceId != currentOutput.sourceId {
self.initiateVideoSourceSwitch(flipAnimationInfo: FlipAnimationInfo(isForward: previousVideo.sourceId < currentOutput.sourceId, previousRotationAngle: previousVideo.rotationAngle))
}
self.videoLayer.video = currentOutput
videoMetrics = VideoMetrics(resolution: currentOutput.resolution, rotationAngle: currentOutput.rotationAngle, sourceId: currentOutput.sourceId)
} else {
self.videoLayer.video = nil
}
self.videoLayer.setNeedsUpdate()
if self.videoMetrics != videoMetrics {
self.videoMetrics = videoMetrics
self.update(transition: .easeInOut(duration: 0.2))
}
}
if oldValue != nil {
self.initiateVideoSourceSwitch(flipAnimationInfo: nil)
}
var videoMetrics: VideoMetrics?
if let currentOutput = self.video?.currentOutput {
self.videoLayer.video = currentOutput
videoMetrics = VideoMetrics(resolution: currentOutput.resolution, rotationAngle: currentOutput.rotationAngle, sourceId: currentOutput.sourceId)
} else {
self.videoLayer.video = nil
}
self.videoLayer.setNeedsUpdate()
if self.videoMetrics != videoMetrics || oldValue != nil {
self.videoMetrics = videoMetrics
self.update(transition: .easeInOut(duration: 0.2))
}
}
}
}
var pressAction: (() -> Void)?
init(key: Key) {
self.key = key
self.videoContainerLayer = VideoContainerLayer()
self.videoContainerLayer.backgroundColor = nil
self.videoContainerLayer.isOpaque = false
self.videoContainerLayer.contentsLayer.backgroundColor = nil
self.videoContainerLayer.contentsLayer.isOpaque = false
if #available(iOS 13.0, *) {
self.videoContainerLayer.contentsLayer.cornerCurve = .circular
}
self.videoLayer = PrivateCallVideoLayer()
self.videoLayer.masksToBounds = true
self.videoLayer.isDoubleSided = false
if #available(iOS 13.0, *) {
self.videoLayer.cornerCurve = .circular
}
self.blurredContainerLayer = SimpleLayer()
self.shadowContainer = SimpleLayer()
self.topShadowLayer = SimpleLayer()
self.topShadowLayer.transform = CATransform3DMakeScale(1.0, -1.0, 1.0)
self.bottomShadowLayer = SimpleLayer()
super.init(frame: CGRect())
self.videoContainerLayer.contentsLayer.addSublayer(self.videoLayer)
self.layer.addSublayer(self.videoContainerLayer)
self.blurredContainerLayer.addSublayer(self.videoLayer.blurredLayer)
self.topShadowLayer.contents = shadowImage?.cgImage
self.bottomShadowLayer.contents = shadowImage?.cgImage
self.shadowContainer.addSublayer(self.topShadowLayer)
self.shadowContainer.addSublayer(self.bottomShadowLayer)
self.layer.addSublayer(self.shadowContainer)
self.highligthedChanged = { [weak self] highlighted in
guard let self, let params = self.params, !self.videoContainerLayer.bounds.isEmpty else {
return
}
var highlightedState = false
if highlighted {
if params.isMinimized {
highlightedState = true
}
} else {
highlightedState = false
}
if self.highlightedState == highlightedState {
return
}
self.highlightedState = highlightedState
let measurementSide = min(self.videoContainerLayer.bounds.width, self.videoContainerLayer.bounds.height)
let topScale: CGFloat = (measurementSide - 8.0) / measurementSide
let maxScale: CGFloat = (measurementSide + 2.0) / measurementSide
if highlightedState {
self.videoContainerLayer.removeAnimation(forKey: "sublayerTransform")
let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut))
transition.setSublayerTransform(layer: self.videoContainerLayer, transform: CATransform3DMakeScale(topScale, topScale, 1.0))
} else {
let t = self.videoContainerLayer.presentation()?.sublayerTransform ?? self.videoContainerLayer.sublayerTransform
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
let transition = Transition(animation: .none)
transition.setSublayerTransform(layer: self.videoContainerLayer, transform: CATransform3DIdentity)
self.videoContainerLayer.animateSublayerScale(from: currentScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] completed in
guard let self, completed else {
return
}
self.videoContainerLayer.animateSublayerScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue)
})
}
}
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let params = self.params else {
return nil
}
if params.isMinimized {
let videoContainerPoint = self.layer.convert(point, to: self.videoContainerLayer)
if self.videoContainerLayer.bounds.contains(videoContainerPoint) {
return self
} else {
return nil
}
} else {
return nil
}
}
@objc private func pressed() {
self.pressAction?()
}
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began, .changed:
self.dragVelocity = CGPoint()
if let dragPositionAnimatorLink = self.dragPositionAnimatorLink {
self.dragPositionAnimatorLink = nil
dragPositionAnimatorLink.invalidate()
}
let translation = recognizer.translation(in: self)
let initialDragPosition: CGPoint
if let current = self.initialDragPosition {
initialDragPosition = current
} else {
initialDragPosition = self.videoContainerLayer.position
self.initialDragPosition = initialDragPosition
}
self.dragPosition = initialDragPosition.offsetBy(dx: translation.x, dy: translation.y)
self.update(transition: .immediate)
case .ended, .cancelled:
self.initialDragPosition = nil
self.dragVelocity = recognizer.velocity(in: self)
if let params = self.params, let dragPosition = self.dragPosition {
let endPosition = CGPoint(
x: dragPosition.x - self.dragVelocity.x / (1000.0 * log(0.99)),
y: dragPosition.y - self.dragVelocity.y / (1000.0 * log(0.99))
)
var minCornerDistance: (corner: MinimizedPosition, distance: CGFloat)?
for corner in MinimizedPosition.allCases {
let cornerPosition: CGPoint
switch corner {
case .topLeft:
cornerPosition = CGPoint(x: params.insets.left, y: params.insets.top)
case .topRight:
cornerPosition = CGPoint(x: params.size.width - params.insets.right, y: params.insets.top)
case .bottomLeft:
cornerPosition = CGPoint(x: params.insets.left, y: params.size.height - params.insets.bottom)
case .bottomRight:
cornerPosition = CGPoint(x: params.size.width - params.insets.right, y: params.size.height - params.insets.bottom)
}
let distance = CGPoint(x: endPosition.x - cornerPosition.x, y: endPosition.y - cornerPosition.y)
let scalarDistance = sqrt(distance.x * distance.x + distance.y * distance.y)
if let (_, minDistance) = minCornerDistance {
if scalarDistance < minDistance {
minCornerDistance = (corner, scalarDistance)
}
} else {
minCornerDistance = (corner, scalarDistance)
}
}
if let minCornerDistance {
self.minimizedPosition = minCornerDistance.corner
}
}
self.dragPositionAnimatorLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in
guard let self else {
return
}
self.updateDragPositionAnimation(deltaTime: deltaTime)
})
default:
break
}
}
private func updateVelocityUsingSpring(currentVelocity: CGPoint, currentPosition: CGPoint, attractor: CGPoint, springConstant: CGFloat, damping: CGFloat, deltaTime: CGFloat) -> CGPoint {
let displacement = CGPoint(x: attractor.x - currentPosition.x, y: attractor.y - currentPosition.y)
let springForce = CGPoint(x: -springConstant * displacement.x, y: -springConstant * displacement.y)
var newVelocity = CGPoint(x: currentVelocity.x + springForce.x * deltaTime, y: currentVelocity.y + springForce.y * deltaTime)
newVelocity = CGPoint(x: newVelocity.x * exp(-damping * deltaTime), y: newVelocity.y * exp(-damping * deltaTime))
return newVelocity
}
private func updateDragPositionAnimation(deltaTime: Double) {
guard let params = self.params, let videoMetrics = self.videoMetrics else {
self.dragPosition = nil
self.dragPositionAnimatorLink = nil
return
}
if !params.isMinimized {
self.dragPosition = nil
self.dragPositionAnimatorLink = nil
return
}
guard var dragPosition = self.dragPosition else {
self.dragPosition = nil
self.dragPositionAnimatorLink = nil
return
}
let videoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: videoMetrics, applyDragPosition: false)
let targetPosition = videoLayout.rotatedVideoFrame.center
self.dragVelocity = self.updateVelocityUsingSpring(
currentVelocity: self.dragVelocity,
currentPosition: dragPosition,
attractor: targetPosition,
springConstant: -130.0,
damping: 17.0,
deltaTime: CGFloat(deltaTime)
)
if sqrt(self.dragVelocity.x * self.dragVelocity.x + self.dragVelocity.y * self.dragVelocity.y) <= 0.1 {
self.dragVelocity = CGPoint()
self.dragPosition = nil
self.dragPositionAnimatorLink = nil
} else {
dragPosition.x += self.dragVelocity.x * CGFloat(deltaTime)
dragPosition.y += self.dragVelocity.y * CGFloat(deltaTime)
self.dragPosition = dragPosition
}
self.update(transition: .immediate)
}
private func initiateVideoSourceSwitch(flipAnimationInfo: FlipAnimationInfo?) {
guard let videoMetrics = self.videoMetrics else {
return
}
if let disappearingVideoLayer = self.disappearingVideoLayer {
disappearingVideoLayer.videoLayer.removeFromSuperlayer()
disappearingVideoLayer.videoLayer.blurredLayer.removeFromSuperlayer()
}
let previousVideoLayer = self.videoLayer
self.disappearingVideoLayer = DisappearingVideo(flipAnimationInfo: flipAnimationInfo, videoLayer: self.videoLayer, videoMetrics: videoMetrics)
self.videoLayer = PrivateCallVideoLayer()
self.videoLayer.opacity = previousVideoLayer.opacity
self.videoLayer.masksToBounds = true
self.videoLayer.isDoubleSided = false
if #available(iOS 13.0, *) {
self.videoLayer.cornerCurve = .circular
}
self.videoLayer.cornerRadius = previousVideoLayer.cornerRadius
self.videoLayer.blurredLayer.opacity = previousVideoLayer.blurredLayer.opacity
self.videoContainerLayer.contentsLayer.addSublayer(self.videoLayer)
self.blurredContainerLayer.addSublayer(self.videoLayer.blurredLayer)
self.dragPosition = nil
self.dragPositionAnimatorLink = nil
}
private func update(transition: Transition) {
guard let params = self.params else {
return
}
self.update(previousParams: params, params: params, transition: transition)
}
func update(size: CGSize, insets: UIEdgeInsets, cornerRadius: CGFloat, controlsHidden: Bool, isMinimized: Bool, isAnimatedOut: Bool, transition: Transition) {
let params = Params(size: size, insets: insets, cornerRadius: cornerRadius, controlsHidden: controlsHidden, isMinimized: isMinimized, isAnimatedOut: isAnimatedOut)
if self.params == params {
return
}
let previousParams = self.params
self.params = params
if let previousParams, previousParams.controlsHidden != params.controlsHidden {
self.dragPosition = nil
self.dragPositionAnimatorLink = nil
}
self.update(previousParams: previousParams, params: params, transition: transition)
}
private struct MinimizedLayout {
var videoIsRotated: Bool
var rotatedVideoSize: CGSize
var rotatedVideoResolution: CGSize
var rotatedVideoFrame: CGRect
var videoTransform: CATransform3D
var effectiveVideoFrame: CGRect
}
private func calculateMinimizedLayout(params: Params, videoMetrics: VideoMetrics, applyDragPosition: Bool) -> MinimizedLayout {
var rotatedResolution = videoMetrics.resolution
var videoIsRotated = false
if videoMetrics.rotationAngle == Float.pi * 0.5 || videoMetrics.rotationAngle == Float.pi * 3.0 / 2.0 {
rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width)
videoIsRotated = true
}
let minimizedBoundingSize: CGFloat = params.controlsHidden ? 140.0 : 240.0
let videoSize = rotatedResolution.aspectFitted(CGSize(width: minimizedBoundingSize, height: minimizedBoundingSize))
let videoResolution = rotatedResolution.aspectFittedOrSmaller(CGSize(width: 1280, height: 1280)).aspectFittedOrSmaller(CGSize(width: videoSize.width * 3.0, height: videoSize.height * 3.0))
let rotatedVideoResolution = videoIsRotated ? CGSize(width: videoResolution.height, height: videoResolution.width) : videoResolution
let rotatedVideoSize = videoIsRotated ? CGSize(width: videoSize.height, height: videoSize.width) : videoSize
let rotatedVideoFrame: CGRect
if applyDragPosition, let dragPosition = self.dragPosition {
rotatedVideoFrame = videoSize.centered(around: dragPosition)
} else {
switch self.minimizedPosition {
case .topLeft:
rotatedVideoFrame = CGRect(origin: CGPoint(x: params.insets.left, y: params.insets.top), size: videoSize)
case .topRight:
rotatedVideoFrame = CGRect(origin: CGPoint(x: params.size.width - params.insets.right - videoSize.width, y: params.insets.top), size: videoSize)
case .bottomLeft:
rotatedVideoFrame = CGRect(origin: CGPoint(x: params.insets.left, y: params.size.height - params.insets.bottom - videoSize.height), size: videoSize)
case .bottomRight:
rotatedVideoFrame = CGRect(origin: CGPoint(x: params.size.width - params.insets.right - videoSize.width, y: params.size.height - params.insets.bottom - videoSize.height), size: videoSize)
}
}
let effectiveVideoFrame = videoSize.centered(around: rotatedVideoFrame.center)
var videoTransform = CATransform3DIdentity
videoTransform.m34 = 1.0 / 600.0
videoTransform = CATransform3DRotate(videoTransform, CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0)
if params.isAnimatedOut {
videoTransform = CATransform3DScale(videoTransform, 0.6, 0.6, 1.0)
}
return MinimizedLayout(
videoIsRotated: videoIsRotated,
rotatedVideoSize: rotatedVideoSize,
rotatedVideoResolution: rotatedVideoResolution,
rotatedVideoFrame: rotatedVideoFrame,
videoTransform: videoTransform,
effectiveVideoFrame: effectiveVideoFrame
)
}
private func update(previousParams: Params?, params: Params, transition: Transition) {
guard let videoMetrics = self.videoMetrics else {
return
}
var transition = transition
if self.appliedVideoMetrics == nil {
transition = .immediate
}
self.appliedVideoMetrics = videoMetrics
if params.isMinimized {
self.isFillingBounds = false
let videoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: videoMetrics, applyDragPosition: true)
transition.setPosition(layer: self.videoContainerLayer, position: videoLayout.rotatedVideoFrame.center)
self.videoContainerLayer.contentsLayer.masksToBounds = true
if self.disappearingVideoLayer != nil {
self.videoContainerLayer.contentsLayer.backgroundColor = UIColor.black.cgColor
}
transition.setBounds(layer: self.videoContainerLayer, bounds: CGRect(origin: CGPoint(), size: videoLayout.rotatedVideoSize), completion: { [weak self] completed in
guard let self, completed else {
return
}
self.videoContainerLayer.contentsLayer.masksToBounds = false
self.videoContainerLayer.contentsLayer.backgroundColor = nil
})
self.videoContainerLayer.update(size: videoLayout.rotatedVideoSize, transition: transition)
var videoTransition = transition
if self.videoLayer.bounds.isEmpty {
videoTransition = .immediate
}
var animateFlipDisappearingVideo: DisappearingVideo?
if let disappearingVideoLayer = self.disappearingVideoLayer {
self.disappearingVideoLayer = nil
let disappearingVideoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: disappearingVideoLayer.videoMetrics, applyDragPosition: true)
let initialDisapparingVideoSize = disappearingVideoLayout.rotatedVideoSize
if !disappearingVideoLayer.isAlphaAnimationInitiated {
disappearingVideoLayer.isAlphaAnimationInitiated = true
if let flipAnimationInfo = disappearingVideoLayer.flipAnimationInfo {
var videoTransform = self.videoContainerLayer.transform
var axis: (x: CGFloat, y: CGFloat, z: CGFloat) = (0.0, 0.0, 0.0)
let previousVideoScale: CGPoint
if flipAnimationInfo.previousRotationAngle == Float.pi * 0.5 {
axis.x = -1.0
previousVideoScale = CGPoint(x: 1.0, y: -1.0)
} else if flipAnimationInfo.previousRotationAngle == Float.pi {
axis.y = -1.0
previousVideoScale = CGPoint(x: -1.0, y: -1.0)
} else if flipAnimationInfo.previousRotationAngle == Float.pi * 3.0 / 2.0 {
axis.x = 1.0
previousVideoScale = CGPoint(x: 1.0, y: 1.0)
} else {
axis.y = 1.0
previousVideoScale = CGPoint(x: -1.0, y: 1.0)
}
videoTransform = CATransform3DRotate(videoTransform, (flipAnimationInfo.isForward ? 1.0 : -1.0) * CGFloat.pi * 0.9999, axis.x, axis.y, axis.z)
self.videoContainerLayer.transform = videoTransform
disappearingVideoLayer.videoLayer.zPosition = 1.0
transition.setZPosition(layer: disappearingVideoLayer.videoLayer, zPosition: -1.0)
disappearingVideoLayer.videoLayer.transform = CATransform3DMakeScale(previousVideoScale.x, previousVideoScale.y, 1.0)
animateFlipDisappearingVideo = disappearingVideoLayer
disappearingVideoLayer.videoLayer.blurredLayer.removeFromSuperlayer()
} else {
let alphaTransition: Transition = .easeInOut(duration: 0.2)
let disappearingVideoLayerValue = disappearingVideoLayer.videoLayer
alphaTransition.setAlpha(layer: disappearingVideoLayerValue, alpha: 0.0, completion: { [weak self, weak disappearingVideoLayerValue] _ in
guard let self, let disappearingVideoLayerValue else {
return
}
disappearingVideoLayerValue.removeFromSuperlayer()
if self.disappearingVideoLayer?.videoLayer === disappearingVideoLayerValue {
self.disappearingVideoLayer = nil
self.update(transition: .immediate)
}
})
disappearingVideoLayer.videoLayer.blurredLayer.removeFromSuperlayer()
self.videoLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
self.videoLayer.position = disappearingVideoLayer.videoLayer.position
self.videoLayer.bounds = CGRect(origin: CGPoint(), size: videoLayout.rotatedVideoSize.aspectFilled(initialDisapparingVideoSize))
self.videoLayer.blurredLayer.position = disappearingVideoLayer.videoLayer.blurredLayer.position
self.videoLayer.blurredLayer.bounds = CGRect(origin: CGPoint(), size: videoLayout.rotatedVideoSize.aspectFilled(initialDisapparingVideoSize))
}
let disappearingVideoSize = initialDisapparingVideoSize.aspectFilled(videoLayout.rotatedVideoSize)
transition.setPosition(layer: disappearingVideoLayer.videoLayer, position: CGPoint(x: videoLayout.rotatedVideoSize.width * 0.5, y: videoLayout.rotatedVideoSize.height * 0.5))
transition.setBounds(layer: disappearingVideoLayer.videoLayer, bounds: CGRect(origin: CGPoint(), size: disappearingVideoSize))
transition.setPosition(layer: disappearingVideoLayer.videoLayer.blurredLayer, position: videoLayout.rotatedVideoFrame.center)
transition.setBounds(layer: disappearingVideoLayer.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: disappearingVideoSize))
}
let animateFlipDisappearingVideoLayer = animateFlipDisappearingVideo?.videoLayer
transition.setTransform(layer: self.videoContainerLayer, transform: videoLayout.videoTransform, completion: { [weak animateFlipDisappearingVideoLayer] _ in
animateFlipDisappearingVideoLayer?.removeFromSuperlayer()
})
transition.setPosition(layer: self.videoLayer, position: CGPoint(x: videoLayout.rotatedVideoSize.width * 0.5, y: videoLayout.rotatedVideoSize.height * 0.5))
transition.setBounds(layer: self.videoLayer, bounds: CGRect(origin: CGPoint(), size: videoLayout.rotatedVideoSize))
transition.setPosition(layer: self.videoLayer.blurredLayer, position: videoLayout.rotatedVideoFrame.center)
transition.setAlpha(layer: self.videoLayer.blurredLayer, alpha: 0.0)
transition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: videoLayout.rotatedVideoSize))
videoTransition.setTransform(layer: self.videoLayer.blurredLayer, transform: videoLayout.videoTransform)
if let previousParams, !previousParams.isMinimized {
self.videoContainerLayer.contentsLayer.cornerRadius = previousParams.cornerRadius
}
transition.setCornerRadius(layer: self.videoContainerLayer.contentsLayer, cornerRadius: 18.0, completion: { [weak self] completed in
guard let self, completed, let params = self.params else {
return
}
if params.isMinimized {
self.videoLayer.cornerRadius = 18.0
}
})
self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(videoLayout.rotatedVideoResolution.width), height: Int(videoLayout.rotatedVideoResolution.height)), edgeInset: 2)
} else {
var rotatedResolution = videoMetrics.resolution
var videoIsRotated = false
if videoMetrics.rotationAngle == Float.pi * 0.5 || videoMetrics.rotationAngle == Float.pi * 3.0 / 2.0 {
rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width)
videoIsRotated = true
}
var videoSize: CGSize
if params.isAnimatedOut {
self.isFillingBounds = true
videoSize = rotatedResolution.aspectFilled(params.size)
} else {
videoSize = rotatedResolution.aspectFitted(params.size)
let boundingAspectRatio = params.size.width / params.size.height
let videoAspectRatio = videoSize.width / videoSize.height
self.isFillingBounds = abs(boundingAspectRatio - videoAspectRatio) < 0.15
if self.isFillingBounds {
videoSize = rotatedResolution.aspectFilled(params.size)
}
}
let videoResolution = rotatedResolution.aspectFittedOrSmaller(CGSize(width: 1280, height: 1280)).aspectFittedOrSmaller(CGSize(width: videoSize.width * 3.0, height: videoSize.height * 3.0))
let rotatedVideoResolution = videoIsRotated ? CGSize(width: videoResolution.height, height: videoResolution.width) : videoResolution
let rotatedBoundingSize = videoIsRotated ? CGSize(width: params.size.height, height: params.size.width) : params.size
let rotatedVideoSize = videoIsRotated ? CGSize(width: videoSize.height, height: videoSize.width) : videoSize
let rotatedVideoBoundingSize = params.size
let rotatedVideoFrame = CGRect(origin: CGPoint(x: floor((rotatedVideoBoundingSize.width - rotatedVideoSize.width) * 0.5), y: floor((rotatedVideoBoundingSize.height - rotatedVideoSize.height) * 0.5)), size: rotatedVideoSize)
self.videoContainerLayer.contentsLayer.masksToBounds = true
if let previousParams, self.videoContainerLayer.contentsLayer.animation(forKey: "cornerRadius") == nil {
if previousParams.isMinimized {
self.videoContainerLayer.contentsLayer.cornerRadius = self.videoLayer.cornerRadius
} else {
self.videoContainerLayer.contentsLayer.cornerRadius = previousParams.cornerRadius
}
}
self.videoLayer.cornerRadius = 0.0
transition.setCornerRadius(layer: self.videoContainerLayer.contentsLayer, cornerRadius: params.cornerRadius, completion: { [weak self] completed in
guard let self, completed, let params = self.params else {
return
}
if !params.isMinimized && !params.isAnimatedOut {
self.videoContainerLayer.contentsLayer.cornerRadius = 0.0
}
})
transition.setPosition(layer: self.videoContainerLayer, position: CGPoint(x: params.size.width * 0.5, y: params.size.height * 0.5))
transition.setBounds(layer: self.videoContainerLayer, bounds: CGRect(origin: CGPoint(), size: rotatedBoundingSize))
self.videoContainerLayer.update(size: rotatedBoundingSize, transition: transition)
var videoTransition = transition
if self.videoLayer.bounds.isEmpty {
videoTransition = .immediate
if !transition.animation.isImmediate {
self.videoLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.videoLayer.blurredLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
if let disappearingVideoLayer = self.disappearingVideoLayer {
self.disappearingVideoLayer = nil
if !disappearingVideoLayer.isAlphaAnimationInitiated {
disappearingVideoLayer.isAlphaAnimationInitiated = true
let alphaTransition: Transition = .easeInOut(duration: 0.2)
let disappearingVideoLayerValue = disappearingVideoLayer.videoLayer
alphaTransition.setAlpha(layer: disappearingVideoLayerValue, alpha: 0.0, completion: { [weak disappearingVideoLayerValue] _ in
disappearingVideoLayerValue?.removeFromSuperlayer()
})
let disappearingVideoLayerBlurredLayerValue = disappearingVideoLayer.videoLayer.blurredLayer
alphaTransition.setAlpha(layer: disappearingVideoLayerBlurredLayerValue, alpha: 0.0, completion: { [weak disappearingVideoLayerBlurredLayerValue] _ in
disappearingVideoLayerBlurredLayerValue?.removeFromSuperlayer()
})
}
}
transition.setTransform(layer: self.videoContainerLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
videoTransition.setFrame(layer: self.videoLayer, frame: rotatedVideoSize.centered(around: CGPoint(x: rotatedBoundingSize.width * 0.5, y: rotatedBoundingSize.height * 0.5)))
videoTransition.setPosition(layer: self.videoLayer.blurredLayer, position: rotatedVideoFrame.center)
videoTransition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size))
videoTransition.setAlpha(layer: self.videoLayer.blurredLayer, alpha: 1.0)
videoTransition.setTransform(layer: self.videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
if !params.isAnimatedOut {
self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)), edgeInset: 2)
}
}
self.shadowContainer.masksToBounds = true
transition.setCornerRadius(layer: self.shadowContainer, cornerRadius: params.cornerRadius, completion: { [weak self] completed in
guard let self, completed else {
return
}
self.shadowContainer.masksToBounds = false
})
transition.setFrame(layer: self.shadowContainer, frame: CGRect(origin: CGPoint(), size: params.size))
let shadowAlpha: CGFloat = (params.controlsHidden || params.isMinimized || params.isAnimatedOut) ? 0.0 : 1.0
let topShadowHeight: CGFloat = 200.0
let topShadowFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.size.width, height: topShadowHeight))
transition.setPosition(layer: self.topShadowLayer, position: topShadowFrame.center)
transition.setBounds(layer: self.topShadowLayer, bounds: CGRect(origin: CGPoint(), size: topShadowFrame.size))
transition.setAlpha(layer: self.topShadowLayer, alpha: shadowAlpha)
let bottomShadowHeight: CGFloat = 200.0
transition.setFrame(layer: self.bottomShadowLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: params.size.height - bottomShadowHeight), size: CGSize(width: params.size.width, height: bottomShadowHeight)))
transition.setAlpha(layer: self.bottomShadowLayer, alpha: shadowAlpha)
}
}